halkyon/.forgejo/workflows/ci.yaml
Tizian 「ティツィアーン」 39926ddc5b
ci: deploys wait until the hackathon's judging ends on 22 Oct
The judges' v0.17.0 uses learn.halkyon.eu, which stays at v0.17.0
meanwhile, while main moves on and is tested locally. The deploy job is
switched off until then; afterwards it runs for main again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-17 19:46:25 +02:00

162 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_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 }}"