A patch release; the release tool refuses to build with another Flutter than CI tests with. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
202 lines
9.8 KiB
YAML
202 lines
9.8 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.8
|
|
# The Serverpod CLI, for the generated-code check: the servers' Serverpod version
|
|
# (every package pins it; the root's tests hold it to the lock); 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 job cancelled, or stopped at timeout-minutes, is killed with its step's process
|
|
# group (forgejo-runner v13.1.0 starts every host step as a group and sends it
|
|
# SIGKILL), and no later step runs, `if: always()` ones neither. Yet on 22 Oct a
|
|
# cancelled job left its analysis server running, 4 GB, until someone noticed. The
|
|
# runner takes one job at a time (capacity: 1), so a process here in a job's working
|
|
# folder that is neither the runner nor this step is such a leftover: it is stopped
|
|
# before anything else starts, and the log names it.
|
|
- name: Leftovers
|
|
run: |
|
|
own_group=$(ps -o pgid= -p $$ | tr -d ' ')
|
|
# This step and the runner above it.
|
|
keep=' '
|
|
pid=$$
|
|
while [ -n "$pid" ] && [ "$pid" -gt 1 ]; do
|
|
keep="$keep$pid "
|
|
pid=$(ps -o ppid= -p "$pid" | tr -d ' ')
|
|
done
|
|
left=$(ps -e -o pid=,pgid= | while read -r pid pgid; do
|
|
case "$keep" in *" $pid "*) continue ;; esac
|
|
[ "$pgid" != "$own_group" ] || continue
|
|
# Only what a job started: its working folder, or one since removed.
|
|
case "$(readlink "/proc/$pid/cwd" 2>/dev/null)" in
|
|
"$HOME/work/"*) echo "$pid" ;;
|
|
esac
|
|
done)
|
|
if [ -z "$left" ]; then
|
|
echo 'Nothing left behind by earlier jobs.'
|
|
exit 0
|
|
fi
|
|
echo 'Left behind by earlier jobs, stopped now:'
|
|
# shellcheck disable=SC2086 # one process ID a word
|
|
ps -o pid=,etime=,rss=,args= -p "$(echo $left | tr ' ' ,)" | cut -c1-200
|
|
# shellcheck disable=SC2086
|
|
kill -TERM $left 2>/dev/null || true
|
|
sleep 5
|
|
# shellcheck disable=SC2086
|
|
kill -KILL $left 2>/dev/null || true
|
|
|
|
# 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 Serverpod CLI at SERVERPOD_VERSION, for the checks' generated-code step
|
|
# (tool/check.dart): the generated code is committed, so it has to be exactly what
|
|
# the generator makes of the models and endpoints, and the step refuses a CLI at
|
|
# another version than the lock holds Serverpod at.
|
|
- name: Serverpod CLI
|
|
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
|
|
echo "$bin" >> "$FORGEJO_PATH"
|
|
|
|
# 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 }}"
|