- Melos 8.9 as the root's dev dependency, with scripts across the workspace (analyze, test:dart, test:flutter, generate, check) and its filters (--diff, --include-dependents); cli_util overridden to 0.5.2 for it, since Learn's flutter_launcher_icons asks for 0.4 and uses only its logger, unchanged in 0.5. - tool/check.dart in place of tool/check.sh: packages, formatting of our own files, analysis, the tools' tests, then every package's tests through Melos (--changed-since=<commit> for only what changed and what depends on it). tool/pre-push is the hook: the signature check, then the checks. CI generates every server's code and runs the same check. - The images build from the root with a workspace narrowed to what they need (tool/workspace_only.sh); compose, the deploy's migration test, the snap, the release script, the editor's launch files and Learn's asset tools (run from apps/learn) follow the new layout. - The project's source is named as project-halkyon/halkyon (source link, snap, F-Droid listing, release uploads); the AGPL packages carry the licence's own text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
153 lines
6.6 KiB
Shell
Executable file
153 lines
6.6 KiB
Shell
Executable file
#!/bin/sh
|
|
# Deploys learn.halkyon.eu from main. The only command CI's deploy key may run: deploy's
|
|
# ~/.ssh/authorized_keys on the server pins that key to this script, from the server itself:
|
|
#
|
|
# restrict,from="127.0.0.1,::1",command="/home/deploy/halkyon-learn/deploy/redeploy.sh" ssh-ed25519 … (CI deploy trigger)
|
|
#
|
|
# CI passes the commit it has checked. This deploys it only
|
|
# - while it is still the tip of main (otherwise the CI run of the newer tip deploys),
|
|
# - as a fast-forward of the deployed commit,
|
|
# - if every new commit is signed by a key in deploy/allowed-signers, as the deployed
|
|
# checkout lists them (so no commit can add its own key),
|
|
# dumps the database first (the last 3 dumps stay in ~/backups), applies the migrations
|
|
# before switching to the new server, starts it once more after a migration (see below),
|
|
# and fails unless that server answers afterwards. A failure leaves the checkout at the deployed commit, so
|
|
# running the CI job again retries. By hand, with your own key:
|
|
#
|
|
# ssh sahaquiel-deploy halkyon-learn/deploy/redeploy.sh <commit>
|
|
set -eu
|
|
|
|
main() {
|
|
commit=${SSH_ORIGINAL_COMMAND:-${1:-}}
|
|
case $commit in
|
|
'' | *[!0-9a-f]*) fail 'expects the full commit to deploy (40 hex digits)' ;;
|
|
esac
|
|
[ ${#commit} -eq 40 ] || fail 'expects the full commit to deploy (40 hex digits)'
|
|
|
|
cd "$HOME/halkyon-learn"
|
|
# One deploy at a time.
|
|
exec 9>"$HOME/.redeploy.lock"
|
|
flock 9
|
|
|
|
git fetch -q origin main
|
|
tip=$(git rev-parse origin/main)
|
|
if [ "$tip" != "$commit" ]; then
|
|
echo "main is at $tip now, not $commit: the CI run of $tip deploys that."
|
|
return 0
|
|
fi
|
|
deployed=$(git rev-parse HEAD)
|
|
if [ "$deployed" = "$commit" ]; then
|
|
echo "$commit is deployed already."
|
|
return 0
|
|
fi
|
|
git merge-base --is-ancestor "$deployed" "$commit" ||
|
|
fail "$commit is no fast-forward of the deployed $deployed"
|
|
|
|
unsigned=$(git log --format='%H %G? %GP' "$deployed..$commit" |
|
|
while read -r each status key; do
|
|
case $status in
|
|
# G: good signature; U: good, by a key deploy's keyring doesn't vouch for
|
|
# (the list below does the vouching).
|
|
G | U) [ -n "$key" ] && grep -qx "$key" deploy/allowed-signers && continue ;;
|
|
esac
|
|
echo "$each"
|
|
done)
|
|
[ -z "$unsigned" ] || fail "not signed by a key in deploy/allowed-signers: $unsigned"
|
|
|
|
echo "Deploying $commit (was $deployed)."
|
|
git merge -q --ff-only "$commit"
|
|
trap back_out EXIT
|
|
cd deploy
|
|
# Build while the running server keeps serving; then dump, migrate, switch.
|
|
docker compose build -q server dictionaries
|
|
dump
|
|
# Migrations as a run of their own, before the switch: Serverpod's maintenance role
|
|
# applies them and exits with 0 only if that worked. Each migration is one
|
|
# transaction, so a failing one stops the deploy here and the running server keeps
|
|
# serving the schema it knows. (Until the switch, the old server serves the new
|
|
# schema; migrations that only add, as they should, keep that harmless.)
|
|
docker compose run --rm --no-deps -e role=maintenance server --apply-migrations
|
|
switched=yes
|
|
docker compose up -d --remove-orphans
|
|
# Caddy reads its Caddyfile when it starts, and `up` doesn't restart it for a
|
|
# changed file it mounts. Nor would `caddy reload` see the change: Docker
|
|
# mounts a single file by its inode, and git writes a changed file anew, so
|
|
# the container keeps the old one (found 2026-10-10). A changed Caddyfile, or a
|
|
# sidecar's site, therefore recreates the container, which mounts the new file; the
|
|
# certificates are in a volume, and the instance is away for seconds.
|
|
if ! git diff --quiet "$deployed" "$commit" -- Caddyfile 'sidecars/*.caddy'; then
|
|
docker compose up -d --force-recreate --no-deps caddy
|
|
fi
|
|
wait_until_answering
|
|
# After a migration the new server is started once more. The sync engine
|
|
# (serverpod_offline_sync v0.0.10) reconciles its registry of synced tables as a
|
|
# server first uses the database; when a migration changed a synced table, that
|
|
# changes the registry, and the server then rebuilds every space's projections in
|
|
# every session until it stops (issue #150): after v0.13.0's deploy every request
|
|
# took a minute or two, and syncs failed, until the server was restarted
|
|
# (2026-10-13). The maintenance run doesn't spare it. Started again, the server
|
|
# finds the registry as the first start left it.
|
|
if ! git diff --quiet "$deployed" "$commit" -- ../apps/learn/halkyon_learn_server/migrations; then
|
|
docker compose restart server
|
|
wait_until_answering
|
|
fi
|
|
trap - EXIT
|
|
docker image prune -f >/dev/null
|
|
echo "Deployed $commit."
|
|
}
|
|
|
|
# Dumps the database, as it is before the new server applies its migrations.
|
|
dump() {
|
|
mkdir -p "$HOME/backups"
|
|
file="$HOME/backups/halkyon_learn-$(git rev-parse --short "$deployed")-$(date -u +%Y%m%dT%H%M%SZ).dump"
|
|
(
|
|
umask 077
|
|
docker compose exec -T postgres pg_dump -U halkyon_learn -Fc halkyon_learn >"$file"
|
|
)
|
|
echo "Database dumped to $file ($(du -h "$file" | cut -f1))."
|
|
# Only the newest 3 (decided 2026-10-02): they are the deploys' way back, not backups;
|
|
# those go off-site. Every file people keep is in each of them, so fewer leave room.
|
|
# (ls is safe here: the names are ours, letters, digits and dashes.)
|
|
# shellcheck disable=SC2012
|
|
ls -1t "$HOME"/backups/halkyon_learn-*.dump | tail -n +4 | xargs -r rm --
|
|
}
|
|
|
|
# Waits up to a minute for the new server to answer inside the stack's network.
|
|
wait_until_answering() {
|
|
tries=0
|
|
while [ $tries -lt 30 ]; do
|
|
if docker compose exec -T caddy wget -q -O /dev/null http://server:8080/ 2>/dev/null; then
|
|
echo 'The server answers.'
|
|
return 0
|
|
fi
|
|
tries=$((tries + 1))
|
|
sleep 2
|
|
done
|
|
docker compose logs --tail 40 server >&2
|
|
fail "the server doesn't answer a minute after the switch"
|
|
}
|
|
|
|
# A failed deploy puts the checkout back at the deployed commit, so running the CI job
|
|
# again retries. Failing after the switch, it also brings the containers back as that
|
|
# commit has them (found 2026-10-14: Caddy couldn't start on the new network, and the
|
|
# instance stayed down until they were brought back by hand), on the new images, which
|
|
# fit the database the migrations have changed. Before the switch, the running
|
|
# containers were never touched.
|
|
back_out() {
|
|
cd "$HOME/halkyon-learn"
|
|
git reset -q --hard "$deployed"
|
|
echo "The checkout is back at $deployed." >&2
|
|
if [ "${switched:-}" = yes ]; then
|
|
(cd deploy && docker compose up -d --remove-orphans) >&2 &&
|
|
echo "The containers are back as $deployed has them." >&2
|
|
fi
|
|
}
|
|
|
|
fail() {
|
|
echo "Error: $*" >&2
|
|
exit 1
|
|
}
|
|
|
|
# All of the above is read before anything runs, so the script can update its own file.
|
|
main "$@"
|
|
exit
|