Caddy's network had IPv4 alone, so Docker's userland proxy forwarded every IPv6 visitor to Caddy from the network's gateway, and the server counted all of them as one network (the second reading's M5, reasoned). The edge network gets a unique local /64 (fd4a:6c6b:7e80::/64), Caddy a fixed address in it beside its IPv4 one, both trusted as the proxy; the CI runner's unit denies fd00::/8 and its sandbox check tries Caddy's IPv6 address too. A commit of its own: it needs Docker 27 or later with ip6tables on the server and the network made anew (compose down, never -v, then up), checked by hand first (NEXT.md). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
168 lines
8.3 KiB
YAML
168 lines
8.3 KiB
YAML
# CI: every push of a branch runs the checks of the pre-push hook (tool/check.dart, every app's and
|
|
# shared package's); a push to main is then deployed to learn.halkyon.eu (deploy/redeploy.sh).
|
|
# 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 servers' Serverpod version
|
|
# (every package pins it); 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
|
|
# Caddy's own network by IPv6 too, where it has an address there.
|
|
if timeout 5 bash -c 'exec 3<>/dev/tcp/fd4a:6c6b:7e80::2/443' 2>/dev/null; then
|
|
fail "Caddy's IPv6 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 (each server's lib/src/generated, its 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. Every server package,
|
|
# the apps' and the shared module, through Melos' `generate` script.
|
|
- 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
|
|
PATH="$bin:$PATH" dart run melos run 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_totpKey \
|
|
SERVERPOD_PASSWORD_recoveryCodeKey \
|
|
SERVERPOD_PASSWORD_signInDecoyKey \
|
|
SERVERPOD_PASSWORD_serviceSecret; do
|
|
echo "$name=$(openssl rand -hex 32)" >> "$FORGEJO_ENV"
|
|
done
|
|
|
|
- name: Checks
|
|
if: env.CHECKED_BEFORE != 'true'
|
|
run: dart run tool/check.dart
|
|
|
|
# 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 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 }}"
|