halkyon/.forgejo/workflows/ci.yaml
Tizian 「ティツィアーン」 72c15300ed
fix: Serverpod v4.0.4 and serverpod_offline_sync v0.0.10
serverpod_offline_sync v0.0.10 collects outbound changes in one
consistent snapshot, so a sync no longer fails on writes made while it
runs (upstream #84, #163), keeps checkpoints per author, so a device
clock moving backwards no longer stalls sync or resends data, and keeps
unique conflicts and foreign key defaults alike on every device. Its
tables and its messages are unchanged: no migration, and apps on v0.0.8
keep syncing. It needs Serverpod >=4.0.4 <4.1.0 (v4.0.4 is fixes only),
so the root overrides that held the workspace on one Serverpod go: each
package pins v4.0.4 itself. The generated code is identical under the
v4.0.4 CLI, which CI now uses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-09 18:09:27 +02:00

154 lines
7.4 KiB
YAML

# CI: every push 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). Both jobs run on the runner on
# sahaquiel, 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:
# 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
if: github.ref == 'refs/heads/main'
runs-on: sahaquiel
timeout-minutes: 30
steps:
# The key can run nothing but deploy/redeploy.sh, and only from sahaquiel 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 }}"