People without GitHub can now keep an anonymous account, and sign in on another device, with an email address and a code mailed to it each time; no password. The instance keeps only HMAC-SHA256 of the address, keyed with its emailSignInPepper (ADR 0010): from what someone types it finds the person and tells whether an address is free, it mails only an address someone has just typed, and it can't write to anybody otherwise or show anybody their address. The device remembers the address it signed in with, in the person's folder, to show it. It works as GitHub does. A code for a new address makes it the signed-in person's, who becomes identified and keeps everything. A code for another person's address gives a switch token, which signs in as them without a session; a device holding studysets asks first. Asking for a code answers alike for every address. Codes are eight Crockford characters, valid 15 minutes and three tries (Serverpod's code challenges); three codes an hour per address, ten per network. In development the code goes to the server's log; elsewhere there is no email sign-in until a mail service is set up (instance.info().emailSignIn). Someone identified may add the other way in: the Profile tab lists how they sign in and adds GitHub or an address. They are never switched to another account: an address or GitHub account that is another's is said to be so. Switching now deletes the account left behind only when it was anonymous; it deleted any, which only anonymous people could reach so far. The migration adds email_identity and email_code_request with their indexes and foreign keys to Serverpod's challenges; nothing else changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
163 lines
8 KiB
YAML
163 lines
8 KiB
YAML
# CI: every push of a branch runs the checks of the pre-push hook (tool/check.sh); a push to main is
|
|
# then deployed to learn.halkyon.eu (deploy/redeploy.sh; paused until 22 Oct 2026, see the deploy
|
|
# job). Both jobs run on the runner on learn.halkyon.eu's server, directly on the host as an
|
|
# unprivileged user (deploy/runner/). A commit that passed the checks before isn't checked again
|
|
# (see "Checked before?").
|
|
#
|
|
# Pushes only, no pull requests: a pull request from a fork would run its own code on
|
|
# the production server's runner.
|
|
name: CI
|
|
|
|
on:
|
|
push:
|
|
# Branches only, not tags: a release pushes main and then its tag, both on the same
|
|
# commit, and the tag's run would check that commit a second time while main's still
|
|
# runs ("Checked before?" knows only runs that finished). A tag deploys nothing.
|
|
branches: ['**']
|
|
# Runs the workflow again from the web UI, e.g. to retry a failed deploy.
|
|
workflow_dispatch:
|
|
|
|
jobs:
|
|
check:
|
|
runs-on: sahaquiel
|
|
timeout-minutes: 45
|
|
env:
|
|
# The Flutter the app is developed with; move it together with the local one.
|
|
FLUTTER_VERSION: 3.47.5
|
|
# The Serverpod CLI, for the generated-code check: the server's Serverpod version
|
|
# (the root pubspec's overrides); move it together with them.
|
|
SERVERPOD_VERSION: 4.0.4
|
|
steps:
|
|
# Before anything else runs, the job proves it can neither see nor reach production
|
|
# (the runner's sandbox, deploy/runner/). If it can, nothing else should run.
|
|
- name: Sandbox
|
|
run: |
|
|
fail() { echo "The runner's sandbox leaks: $*" >&2; exit 1; }
|
|
# Only the runner's own processes are visible. A container's process shows up
|
|
# (in a process namespace of its own) if it runs under the runner's user ID,
|
|
# and then its environment and files are readable too.
|
|
[ "$(ps -e -o uid= | sort -u | wc -l)" -eq 1 ] || fail "other users' processes are visible"
|
|
namespaces=$(for p in /proc/[0-9]*; do readlink "$p/ns/pid"; done 2>/dev/null | sort -u | wc -l)
|
|
[ "$namespaces" -le 1 ] || fail "processes of $namespaces process namespaces are visible"
|
|
# deploy's home (.env, the checkout, the dumps) and the Docker socket are out of
|
|
# reach.
|
|
[ ! -e /home/deploy ] || fail "/home/deploy is visible"
|
|
{ [ ! -r /run/docker.sock ] && [ ! -w /run/docker.sock ]; } || fail "the Docker socket is usable"
|
|
# Docker's networks, where the database listens, are unreachable. Caddy listens on
|
|
# every address of the host, the Docker bridge's included.
|
|
if timeout 5 bash -c 'exec 3<>/dev/tcp/172.17.0.1/443' 2>/dev/null; then
|
|
fail "Docker's network is reachable"
|
|
fi
|
|
echo "This job can neither see nor reach production."
|
|
|
|
# A commit that passed this job before isn't checked again: a fast-forward merge
|
|
# brings the commit a branch's push checked to main unchanged, and only its deploy
|
|
# is new. The marks live in the runner's home; only pushes run this workflow, so
|
|
# only those who may push can leave one. Every later step of this job asks.
|
|
- name: Checked before?
|
|
run: |
|
|
if [ -e "$HOME/checked/${{ github.sha }}" ]; then
|
|
echo "${{ github.sha }} passed these checks before; they aren't run again."
|
|
echo 'CHECKED_BEFORE=true' >> "$FORGEJO_ENV"
|
|
fi
|
|
|
|
- name: Check out
|
|
if: env.CHECKED_BEFORE != 'true'
|
|
env:
|
|
TOKEN: ${{ github.token }}
|
|
run: |
|
|
git init -q .
|
|
# The job's token as a header from the environment, so it never shows in a
|
|
# command line; plain git, so the host needs no Node.js for actions/checkout.
|
|
export GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=http.extraHeader \
|
|
GIT_CONFIG_VALUE_0="Authorization: Basic $(printf 'ci:%s' "$TOKEN" | base64 -w0)"
|
|
git fetch -q --depth 1 "${{ github.server_url }}/${{ github.repository }}.git" "${{ github.sha }}"
|
|
git checkout -q -f FETCH_HEAD
|
|
git clean -q -ffdx
|
|
|
|
- name: Flutter
|
|
if: env.CHECKED_BEFORE != 'true'
|
|
run: |
|
|
sdk="$HOME/sdk/flutter-$FLUTTER_VERSION"
|
|
if [ ! -x "$sdk/bin/flutter" ]; then
|
|
rm -rf "$sdk"
|
|
git clone -q --depth 1 --branch "$FLUTTER_VERSION" https://github.com/flutter/flutter.git "$sdk"
|
|
fi
|
|
# No usage reports to Google, every time: a cancelled job may have left an SDK
|
|
# behind before switching them off.
|
|
"$sdk/bin/flutter" config --no-analytics >/dev/null
|
|
"$sdk/bin/dart" --disable-analytics >/dev/null
|
|
echo "$sdk/bin" >> "$FORGEJO_PATH"
|
|
"$sdk/bin/flutter" --version
|
|
|
|
# Exactly the committed pubspec.lock, or a failure here with pub's own message:
|
|
# CI never resolves something else on its own.
|
|
#
|
|
# The second try works around pub's https://github.com/dart-lang/pub/issues/4912
|
|
# (Dart v3.13): when the runner's cached copy of a git dependency's repository (the
|
|
# editor fork, pure_zstd) lacks the locked commit, the first run resolves that
|
|
# package without its dependencies and fails, but fetches the commit, so the
|
|
# second goes through. Drop it once the Dart of FLUTTER_VERSION has pub's fix
|
|
# (ebba0dc, pull request 4914).
|
|
- name: Dependencies
|
|
if: env.CHECKED_BEFORE != 'true'
|
|
run: flutter pub get --enforce-lockfile || flutter pub get --enforce-lockfile
|
|
|
|
# The generated code (the server's lib/src/generated, the client package) is
|
|
# committed, so it has to be exactly what the generator makes of the models and
|
|
# endpoints: a model changed without regenerating fails here.
|
|
- name: Generated code
|
|
if: env.CHECKED_BEFORE != 'true'
|
|
run: |
|
|
bin="${XDG_STATE_HOME:-$HOME/.local/state}/Dart/install/bin"
|
|
if ! "$bin/serverpod" --no-analytics version 2>/dev/null | grep -qx "Serverpod version: $SERVERPOD_VERSION"; then
|
|
dart install "serverpod_cli@$SERVERPOD_VERSION"
|
|
fi
|
|
(cd halkyon_learn_server && "$bin/serverpod" --no-analytics generate)
|
|
if [ -n "$(git status --porcelain)" ]; then
|
|
git status --short
|
|
echo 'The generated code differs from what the generator makes now: run `serverpod generate` and commit.' >&2
|
|
exit 1
|
|
fi
|
|
|
|
# The test run mode's passwords, which passwords.yaml holds on a developer's machine
|
|
# and which is never committed. Without them the server tests stop without a word.
|
|
# Throwaway and new for every job: they only guard the test database the job creates
|
|
# in its own checkout.
|
|
- name: Test passwords
|
|
if: env.CHECKED_BEFORE != 'true'
|
|
run: |
|
|
for name in SERVERPOD_DATABASE_PASSWORD \
|
|
SERVERPOD_PASSWORD_serverSideSessionKeyHashPepper \
|
|
SERVERPOD_PASSWORD_emailSignInPepper \
|
|
SERVERPOD_PASSWORD_serviceSecret; do
|
|
echo "$name=$(openssl rand -hex 32)" >> "$FORGEJO_ENV"
|
|
done
|
|
|
|
- name: Checks
|
|
if: env.CHECKED_BEFORE != 'true'
|
|
run: tool/check.sh
|
|
|
|
# Reached only when every step above passed. Marks older than 90 days go.
|
|
- name: Remember the commit
|
|
if: env.CHECKED_BEFORE != 'true'
|
|
run: |
|
|
mkdir -p "$HOME/checked"
|
|
touch "$HOME/checked/${{ github.sha }}"
|
|
find "$HOME/checked" -type f -mtime +90 -delete
|
|
|
|
deploy:
|
|
needs: check
|
|
# Paused until the hackathon's judging ends (22 Oct 2026): the judges' v0.17.0 uses
|
|
# learn.halkyon.eu, which stays at v0.17.0 meanwhile, while main moves on and is
|
|
# tested locally. Afterwards, put back: if: github.ref == 'refs/heads/main'
|
|
if: false
|
|
runs-on: sahaquiel
|
|
timeout-minutes: 30
|
|
steps:
|
|
# The key can run nothing but deploy/redeploy.sh, and only from that server itself.
|
|
- name: Deploy to learn.halkyon.eu
|
|
run: >-
|
|
ssh -i "$HOME/.ssh/deploy_trigger" -o IdentitiesOnly=yes -o BatchMode=yes
|
|
-o StrictHostKeyChecking=yes -o UserKnownHostsFile="$HOME/.ssh/known_hosts"
|
|
deploy@127.0.0.1 "${{ github.sha }}"
|