build: Melos, a Dart check and every path for the single repository
- 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>
2026-10-19 20:39:09 +02:00
|
|
|
# CI: every push of a branch runs the checks of the pre-push hook (tool/check.dart, every app's and
|
build(ci,deploy)!: the runner at home in a nightly window, deploys by hand (ADR 0039, D6, D13, D16)
The CI runner moves to halkyon.li's host at home and runs from 02:00 to
05:00 alone: a timer starts it, its unit's RuntimeMaxSec ends it, and
night.sh rests the instance's heavy sidecars around it as deploy, while
Caddy, PostgreSQL and the apps' servers keep serving. Jobs pushed during the
day wait in Forgejo's queue.
Its sandbox fits the new host: its folder on the HDD, the rest of /data
hidden, containerd's store out of reach, no private IPv4 network and no
IPv6 at all (the home network's IPv6 addresses are public ones under a
changing prefix, on the internet's own interface), 5 GB of memory with swap
beyond. The Sandbox step checks the tunnel, the home router and IPv6.
BREAKING CHANGE: CI deploys nothing any more: the deploy job goes with
learn.halkyon.eu's server, and halkyon.li deploys by hand with redeploy.sh
once a commit's night's run passed. Jobs run on the runner labelled
balthasar; sahaquiel's runner picks up none.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-08 19:20:11 +01:00
|
|
|
# shared package's). Nothing is deployed from here: halkyon.li deploys by hand, a commit once
|
|
|
|
|
# its night's run passed (deploy/redeploy.sh; ADR 0039, D13).
|
|
|
|
|
# The job runs on the runner on halkyon.li's host at home, directly on the host as an
|
|
|
|
|
# unprivileged user, in the nightly window alone (02:00 to 05:00, deploy/runner/): what is
|
|
|
|
|
# pushed during the day waits in the queue. A commit that passed the checks before isn't
|
|
|
|
|
# checked again (see "Checked before?").
|
ci: check every push on sahaquiel and deploy a green main
Pushes to main were deployed by hand. Now Forgejo Actions runs tool/check.sh on every
push, and a green main deploys learn.halkyon.eu by itself.
The runner (forgejo-runner v13.1.0) runs on sahaquiel as the unprivileged user `runner`,
with jobs directly on the host: containers would take Docker access, which is root on the
production server, and Forgejo warns that host jobs have no isolation from the runner's
user. So its systemd unit (deploy/runner/forgejo-runner.service) provides it: /home and
the Docker socket invisible, Docker's container networks unreachable, no capabilities,
two cores and 3 GB at most, lowest priority. It is registered to this repository only,
with the UUID and token of Forgejo's "Create new runner" (the old `register` is
deprecated); the token reaches it as a systemd credential. The configuration
(deploy/runner/config.yml) was dry-run against v13.1.0, the unit rates 1.8 in
`systemd-analyze security`. CI runs on pushes only: a pull request from a fork would run
its code on the production server.
The deploy job calls deploy/redeploy.sh as `deploy`, through a key that authorized_keys
pins to that script and to connections from sahaquiel itself. The script deploys the
commit CI checked while it is still main's tip, as a fast-forward, only if every new
commit is signed by a key in deploy/allowed-signers (as the deployed checkout lists
them), after dumping the database (the last ten are kept), and fails unless the server
answers afterwards; a failure puts the checkout back. Tested against the local stack:
refusals of a malformed argument, a stale commit, a forged unsigned commit and a
non-fast-forward, a real deploy with its dump, a repeat, and a failed build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-09-30 21:49:04 +02:00
|
|
|
#
|
|
|
|
|
# Pushes only, no pull requests: a pull request from a fork would run its own code on
|
build(ci,deploy)!: the runner at home in a nightly window, deploys by hand (ADR 0039, D6, D13, D16)
The CI runner moves to halkyon.li's host at home and runs from 02:00 to
05:00 alone: a timer starts it, its unit's RuntimeMaxSec ends it, and
night.sh rests the instance's heavy sidecars around it as deploy, while
Caddy, PostgreSQL and the apps' servers keep serving. Jobs pushed during the
day wait in Forgejo's queue.
Its sandbox fits the new host: its folder on the HDD, the rest of /data
hidden, containerd's store out of reach, no private IPv4 network and no
IPv6 at all (the home network's IPv6 addresses are public ones under a
changing prefix, on the internet's own interface), 5 GB of memory with swap
beyond. The Sandbox step checks the tunnel, the home router and IPv6.
BREAKING CHANGE: CI deploys nothing any more: the deploy job goes with
learn.halkyon.eu's server, and halkyon.li deploys by hand with redeploy.sh
once a commit's night's run passed. Jobs run on the runner labelled
balthasar; sahaquiel's runner picks up none.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-08 19:20:11 +01:00
|
|
|
# the instance's host.
|
ci: check every push on sahaquiel and deploy a green main
Pushes to main were deployed by hand. Now Forgejo Actions runs tool/check.sh on every
push, and a green main deploys learn.halkyon.eu by itself.
The runner (forgejo-runner v13.1.0) runs on sahaquiel as the unprivileged user `runner`,
with jobs directly on the host: containers would take Docker access, which is root on the
production server, and Forgejo warns that host jobs have no isolation from the runner's
user. So its systemd unit (deploy/runner/forgejo-runner.service) provides it: /home and
the Docker socket invisible, Docker's container networks unreachable, no capabilities,
two cores and 3 GB at most, lowest priority. It is registered to this repository only,
with the UUID and token of Forgejo's "Create new runner" (the old `register` is
deprecated); the token reaches it as a systemd credential. The configuration
(deploy/runner/config.yml) was dry-run against v13.1.0, the unit rates 1.8 in
`systemd-analyze security`. CI runs on pushes only: a pull request from a fork would run
its code on the production server.
The deploy job calls deploy/redeploy.sh as `deploy`, through a key that authorized_keys
pins to that script and to connections from sahaquiel itself. The script deploys the
commit CI checked while it is still main's tip, as a fast-forward, only if every new
commit is signed by a key in deploy/allowed-signers (as the deployed checkout lists
them), after dumping the database (the last ten are kept), and fails unless the server
answers afterwards; a failure puts the checkout back. Tested against the local stack:
refusals of a malformed argument, a stale commit, a forged unsigned commit and a
non-fast-forward, a real deploy with its dump, a repeat, and a failed build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-09-30 21:49:04 +02:00
|
|
|
name: CI
|
|
|
|
|
|
|
|
|
|
on:
|
|
|
|
|
push:
|
2026-10-14 15:27:01 +02:00
|
|
|
# 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: ['**']
|
build(ci,deploy)!: the runner at home in a nightly window, deploys by hand (ADR 0039, D6, D13, D16)
The CI runner moves to halkyon.li's host at home and runs from 02:00 to
05:00 alone: a timer starts it, its unit's RuntimeMaxSec ends it, and
night.sh rests the instance's heavy sidecars around it as deploy, while
Caddy, PostgreSQL and the apps' servers keep serving. Jobs pushed during the
day wait in Forgejo's queue.
Its sandbox fits the new host: its folder on the HDD, the rest of /data
hidden, containerd's store out of reach, no private IPv4 network and no
IPv6 at all (the home network's IPv6 addresses are public ones under a
changing prefix, on the internet's own interface), 5 GB of memory with swap
beyond. The Sandbox step checks the tunnel, the home router and IPv6.
BREAKING CHANGE: CI deploys nothing any more: the deploy job goes with
learn.halkyon.eu's server, and halkyon.li deploys by hand with redeploy.sh
once a commit's night's run passed. Jobs run on the runner labelled
balthasar; sahaquiel's runner picks up none.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-08 19:20:11 +01:00
|
|
|
# Runs the workflow again from the web UI, e.g. to check a commit once more.
|
ci: check every push on sahaquiel and deploy a green main
Pushes to main were deployed by hand. Now Forgejo Actions runs tool/check.sh on every
push, and a green main deploys learn.halkyon.eu by itself.
The runner (forgejo-runner v13.1.0) runs on sahaquiel as the unprivileged user `runner`,
with jobs directly on the host: containers would take Docker access, which is root on the
production server, and Forgejo warns that host jobs have no isolation from the runner's
user. So its systemd unit (deploy/runner/forgejo-runner.service) provides it: /home and
the Docker socket invisible, Docker's container networks unreachable, no capabilities,
two cores and 3 GB at most, lowest priority. It is registered to this repository only,
with the UUID and token of Forgejo's "Create new runner" (the old `register` is
deprecated); the token reaches it as a systemd credential. The configuration
(deploy/runner/config.yml) was dry-run against v13.1.0, the unit rates 1.8 in
`systemd-analyze security`. CI runs on pushes only: a pull request from a fork would run
its code on the production server.
The deploy job calls deploy/redeploy.sh as `deploy`, through a key that authorized_keys
pins to that script and to connections from sahaquiel itself. The script deploys the
commit CI checked while it is still main's tip, as a fast-forward, only if every new
commit is signed by a key in deploy/allowed-signers (as the deployed checkout lists
them), after dumping the database (the last ten are kept), and fails unless the server
answers afterwards; a failure puts the checkout back. Tested against the local stack:
refusals of a malformed argument, a stale commit, a forged unsigned commit and a
non-fast-forward, a real deploy with its dump, a repeat, and a failed build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-09-30 21:49:04 +02:00
|
|
|
workflow_dispatch:
|
|
|
|
|
|
|
|
|
|
jobs:
|
|
|
|
|
check:
|
build(ci,deploy)!: the runner at home in a nightly window, deploys by hand (ADR 0039, D6, D13, D16)
The CI runner moves to halkyon.li's host at home and runs from 02:00 to
05:00 alone: a timer starts it, its unit's RuntimeMaxSec ends it, and
night.sh rests the instance's heavy sidecars around it as deploy, while
Caddy, PostgreSQL and the apps' servers keep serving. Jobs pushed during the
day wait in Forgejo's queue.
Its sandbox fits the new host: its folder on the HDD, the rest of /data
hidden, containerd's store out of reach, no private IPv4 network and no
IPv6 at all (the home network's IPv6 addresses are public ones under a
changing prefix, on the internet's own interface), 5 GB of memory with swap
beyond. The Sandbox step checks the tunnel, the home router and IPv6.
BREAKING CHANGE: CI deploys nothing any more: the deploy job goes with
learn.halkyon.eu's server, and halkyon.li deploys by hand with redeploy.sh
once a commit's night's run passed. Jobs run on the runner labelled
balthasar; sahaquiel's runner picks up none.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-08 19:20:11 +01:00
|
|
|
runs-on: balthasar
|
2026-10-30 18:56:44 +01:00
|
|
|
# A warm run took 42 minutes with Train's packages (30 Oct 2026), a new Flutter more:
|
|
|
|
|
# the limit only catches a hung job.
|
|
|
|
|
timeout-minutes: 60
|
ci: check every push on sahaquiel and deploy a green main
Pushes to main were deployed by hand. Now Forgejo Actions runs tool/check.sh on every
push, and a green main deploys learn.halkyon.eu by itself.
The runner (forgejo-runner v13.1.0) runs on sahaquiel as the unprivileged user `runner`,
with jobs directly on the host: containers would take Docker access, which is root on the
production server, and Forgejo warns that host jobs have no isolation from the runner's
user. So its systemd unit (deploy/runner/forgejo-runner.service) provides it: /home and
the Docker socket invisible, Docker's container networks unreachable, no capabilities,
two cores and 3 GB at most, lowest priority. It is registered to this repository only,
with the UUID and token of Forgejo's "Create new runner" (the old `register` is
deprecated); the token reaches it as a systemd credential. The configuration
(deploy/runner/config.yml) was dry-run against v13.1.0, the unit rates 1.8 in
`systemd-analyze security`. CI runs on pushes only: a pull request from a fork would run
its code on the production server.
The deploy job calls deploy/redeploy.sh as `deploy`, through a key that authorized_keys
pins to that script and to connections from sahaquiel itself. The script deploys the
commit CI checked while it is still main's tip, as a fast-forward, only if every new
commit is signed by a key in deploy/allowed-signers (as the deployed checkout lists
them), after dumping the database (the last ten are kept), and fails unless the server
answers afterwards; a failure puts the checkout back. Tested against the local stack:
refusals of a malformed argument, a stale commit, a forged unsigned commit and a
non-fast-forward, a real deploy with its dump, a repeat, and a failed build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-09-30 21:49:04 +02:00
|
|
|
env:
|
|
|
|
|
# The Flutter the app is developed with; move it together with the local one.
|
2026-10-30 17:43:24 +01:00
|
|
|
FLUTTER_VERSION: 3.47.8
|
build: Melos, a Dart check and every path for the single repository
- 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>
2026-10-19 20:39:09 +02:00
|
|
|
# The Serverpod CLI, for the generated-code check: the servers' Serverpod version
|
2026-10-23 15:14:23 +02:00
|
|
|
# (every package pins it; the root's tests hold it to the lock); move it together
|
|
|
|
|
# with them.
|
2026-10-09 11:09:52 +02:00
|
|
|
SERVERPOD_VERSION: 4.0.4
|
ci: check every push on sahaquiel and deploy a green main
Pushes to main were deployed by hand. Now Forgejo Actions runs tool/check.sh on every
push, and a green main deploys learn.halkyon.eu by itself.
The runner (forgejo-runner v13.1.0) runs on sahaquiel as the unprivileged user `runner`,
with jobs directly on the host: containers would take Docker access, which is root on the
production server, and Forgejo warns that host jobs have no isolation from the runner's
user. So its systemd unit (deploy/runner/forgejo-runner.service) provides it: /home and
the Docker socket invisible, Docker's container networks unreachable, no capabilities,
two cores and 3 GB at most, lowest priority. It is registered to this repository only,
with the UUID and token of Forgejo's "Create new runner" (the old `register` is
deprecated); the token reaches it as a systemd credential. The configuration
(deploy/runner/config.yml) was dry-run against v13.1.0, the unit rates 1.8 in
`systemd-analyze security`. CI runs on pushes only: a pull request from a fork would run
its code on the production server.
The deploy job calls deploy/redeploy.sh as `deploy`, through a key that authorized_keys
pins to that script and to connections from sahaquiel itself. The script deploys the
commit CI checked while it is still main's tip, as a fast-forward, only if every new
commit is signed by a key in deploy/allowed-signers (as the deployed checkout lists
them), after dumping the database (the last ten are kept), and fails unless the server
answers afterwards; a failure puts the checkout back. Tested against the local stack:
refusals of a malformed argument, a stale commit, a forged unsigned commit and a
non-fast-forward, a real deploy with its dump, a repeat, and a failed build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-09-30 21:49:04 +02:00
|
|
|
steps:
|
build(ci,deploy)!: the runner at home in a nightly window, deploys by hand (ADR 0039, D6, D13, D16)
The CI runner moves to halkyon.li's host at home and runs from 02:00 to
05:00 alone: a timer starts it, its unit's RuntimeMaxSec ends it, and
night.sh rests the instance's heavy sidecars around it as deploy, while
Caddy, PostgreSQL and the apps' servers keep serving. Jobs pushed during the
day wait in Forgejo's queue.
Its sandbox fits the new host: its folder on the HDD, the rest of /data
hidden, containerd's store out of reach, no private IPv4 network and no
IPv6 at all (the home network's IPv6 addresses are public ones under a
changing prefix, on the internet's own interface), 5 GB of memory with swap
beyond. The Sandbox step checks the tunnel, the home router and IPv6.
BREAKING CHANGE: CI deploys nothing any more: the deploy job goes with
learn.halkyon.eu's server, and halkyon.li deploys by hand with redeploy.sh
once a commit's night's run passed. Jobs run on the runner labelled
balthasar; sahaquiel's runner picks up none.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-08 19:20:11 +01:00
|
|
|
# Before anything else runs, the job proves it can neither see nor reach the instance
|
|
|
|
|
# or the home network (the runner's sandbox, deploy/runner/). If it can, nothing else
|
|
|
|
|
# should run.
|
ci: stop a job whose sandbox leaks, first thing
Installing the runner on sahaquiel, `useradd --system` gave `runner` user ID 999: the ID
under which PostgreSQL and the server run inside their containers, which Docker doesn't
remap. To the kernel, those processes were the runner's own, so a job could have read the
server's environment (database password, session pepper, GitHub secret) and, through
/proc, PostgreSQL's files and its local socket, which the official image trusts. The
sandbox's other walls don't help there: ProtectProc hides only other users' processes,
IPAddressDeny doesn't cover Unix sockets. Only our own check ran as 999; `runner` now has
user and group ID 61000.
The check job now starts with a Sandbox step that fails unless only the runner's own
processes are visible, all in one process namespace, /home/deploy is invisible, the
Docker socket is unusable and Docker's bridge network unreachable. It would have caught
this; outside a sandbox it fails as it should. The unit and AGENTS.md say how to create
the user.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-09-30 22:12:31 +02:00
|
|
|
- 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"
|
build(ci,deploy)!: the runner at home in a nightly window, deploys by hand (ADR 0039, D6, D13, D16)
The CI runner moves to halkyon.li's host at home and runs from 02:00 to
05:00 alone: a timer starts it, its unit's RuntimeMaxSec ends it, and
night.sh rests the instance's heavy sidecars around it as deploy, while
Caddy, PostgreSQL and the apps' servers keep serving. Jobs pushed during the
day wait in Forgejo's queue.
Its sandbox fits the new host: its folder on the HDD, the rest of /data
hidden, containerd's store out of reach, no private IPv4 network and no
IPv6 at all (the home network's IPv6 addresses are public ones under a
changing prefix, on the internet's own interface), 5 GB of memory with swap
beyond. The Sandbox step checks the tunnel, the home router and IPv6.
BREAKING CHANGE: CI deploys nothing any more: the deploy job goes with
learn.halkyon.eu's server, and halkyon.li deploys by hand with redeploy.sh
once a commit's night's run passed. Jobs run on the runner labelled
balthasar; sahaquiel's runner picks up none.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-08 19:20:11 +01:00
|
|
|
# Nor /data, where /home really lies, with Docker's data and the media: the
|
|
|
|
|
# runner sees its own folder there alone.
|
|
|
|
|
for hidden in /data/home /data/docker /data/containerd /data/media; do
|
|
|
|
|
[ ! -e "$hidden" ] || fail "$hidden is visible"
|
|
|
|
|
done
|
ci: stop a job whose sandbox leaks, first thing
Installing the runner on sahaquiel, `useradd --system` gave `runner` user ID 999: the ID
under which PostgreSQL and the server run inside their containers, which Docker doesn't
remap. To the kernel, those processes were the runner's own, so a job could have read the
server's environment (database password, session pepper, GitHub secret) and, through
/proc, PostgreSQL's files and its local socket, which the official image trusts. The
sandbox's other walls don't help there: ProtectProc hides only other users' processes,
IPAddressDeny doesn't cover Unix sockets. Only our own check ran as 999; `runner` now has
user and group ID 61000.
The check job now starts with a Sandbox step that fails unless only the runner's own
processes are visible, all in one process namespace, /home/deploy is invisible, the
Docker socket is unusable and Docker's bridge network unreachable. It would have caught
this; outside a sandbox it fails as it should. The unit and AGENTS.md say how to create
the user.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-09-30 22:12:31 +02:00
|
|
|
{ [ ! -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
|
2026-10-20 21:49:29 +02:00
|
|
|
# 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
|
build(ci,deploy)!: the runner at home in a nightly window, deploys by hand (ADR 0039, D6, D13, D16)
The CI runner moves to halkyon.li's host at home and runs from 02:00 to
05:00 alone: a timer starts it, its unit's RuntimeMaxSec ends it, and
night.sh rests the instance's heavy sidecars around it as deploy, while
Caddy, PostgreSQL and the apps' servers keep serving. Jobs pushed during the
day wait in Forgejo's queue.
Its sandbox fits the new host: its folder on the HDD, the rest of /data
hidden, containerd's store out of reach, no private IPv4 network and no
IPv6 at all (the home network's IPv6 addresses are public ones under a
changing prefix, on the internet's own interface), 5 GB of memory with swap
beyond. The Sandbox step checks the tunnel, the home router and IPv6.
BREAKING CHANGE: CI deploys nothing any more: the deploy job goes with
learn.halkyon.eu's server, and halkyon.li deploys by hand with redeploy.sh
once a commit's night's run passed. Jobs run on the runner labelled
balthasar; sahaquiel's runner picks up none.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-08 19:20:11 +01:00
|
|
|
# Nor Caddy at the tunnel's address, where ramiel reaches it (ADR 0039, D16).
|
|
|
|
|
if timeout 5 bash -c 'exec 3<>/dev/tcp/10.61.97.2/443' 2>/dev/null; then
|
|
|
|
|
fail "the tunnel's address is reachable"
|
|
|
|
|
fi
|
|
|
|
|
# Nor the home network: its router answers on 80.
|
|
|
|
|
if timeout 5 bash -c 'exec 3<>/dev/tcp/192.168.178.1/80' 2>/dev/null; then
|
|
|
|
|
fail "the home network is reachable"
|
|
|
|
|
fi
|
|
|
|
|
# Nor anything over IPv6, where the home network's addresses are public ones:
|
|
|
|
|
# Cloudflare's resolver answers on 443 over IPv6 wherever IPv6 works.
|
|
|
|
|
if timeout 5 bash -c 'exec 3<>/dev/tcp/2606:4700:4700::1111/443' 2>/dev/null; then
|
|
|
|
|
fail "IPv6 is reachable"
|
feat(deploy): Jellyfin at home, published through WireGuard (ADR 0032)
The instance serves jellyfin.halkyon.eu and its Caddy proxies through a
WireGuard tunnel to a Jellyfin on the operator's own machine, which dials
out, so nothing names the home network's address and its router opens no
port. compose.jellyfin-home.yaml takes the sidecar's place, with its
site's rules unchanged (jellyfin.caddy's upstream is now a setting), and
leaves JELLYFIN_URL unset, so the instance suggests it only while it
answers.
On the instance's server the tunnel reaches Jellyfin from Caddy alone and
nothing of the host (wg-jellyfin-rules.sh, set and removed with the
tunnel); the CI runner's unit denies it, and CI's Sandbox step fails if a
job reaches it. The home end keeps Jellyfin in WireGuard's network
namespace, its port on the loopback alone. The steps are in
deploy/sidecars/README.md, the rules held by test/deploy/jellyfin_home_test.dart.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-30 22:49:47 +01:00
|
|
|
fi
|
build(ci,deploy)!: the runner at home in a nightly window, deploys by hand (ADR 0039, D6, D13, D16)
The CI runner moves to halkyon.li's host at home and runs from 02:00 to
05:00 alone: a timer starts it, its unit's RuntimeMaxSec ends it, and
night.sh rests the instance's heavy sidecars around it as deploy, while
Caddy, PostgreSQL and the apps' servers keep serving. Jobs pushed during the
day wait in Forgejo's queue.
Its sandbox fits the new host: its folder on the HDD, the rest of /data
hidden, containerd's store out of reach, no private IPv4 network and no
IPv6 at all (the home network's IPv6 addresses are public ones under a
changing prefix, on the internet's own interface), 5 GB of memory with swap
beyond. The Sandbox step checks the tunnel, the home router and IPv6.
BREAKING CHANGE: CI deploys nothing any more: the deploy job goes with
learn.halkyon.eu's server, and halkyon.li deploys by hand with redeploy.sh
once a commit's night's run passed. Jobs run on the runner labelled
balthasar; sahaquiel's runner picks up none.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-08 19:20:11 +01:00
|
|
|
echo "This job can neither see nor reach the instance or the home network."
|
ci: stop a job whose sandbox leaks, first thing
Installing the runner on sahaquiel, `useradd --system` gave `runner` user ID 999: the ID
under which PostgreSQL and the server run inside their containers, which Docker doesn't
remap. To the kernel, those processes were the runner's own, so a job could have read the
server's environment (database password, session pepper, GitHub secret) and, through
/proc, PostgreSQL's files and its local socket, which the official image trusts. The
sandbox's other walls don't help there: ProtectProc hides only other users' processes,
IPAddressDeny doesn't cover Unix sockets. Only our own check ran as 999; `runner` now has
user and group ID 61000.
The check job now starts with a Sandbox step that fails unless only the runner's own
processes are visible, all in one process namespace, /home/deploy is invisible, the
Docker socket is unusable and Docker's bridge network unreachable. It would have caught
this; outside a sandbox it fails as it should. The unit and AGENTS.md say how to create
the user.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-09-30 22:12:31 +02:00
|
|
|
|
ci: a job first stops what earlier jobs left running
On 22 Oct a cancelled job left its analysis server running in the
runner, 4 GB, until someone noticed. forgejo-runner v13.1.0 starts every
host step as a process group and sends the group SIGKILL on a cancel or
at timeout-minutes (act/common/cmdgroup.go), and after that no later step
runs, `if: always()` ones neither (they get the cancelled context; only
actions' post steps get a fresh one), so a cleanup at the job's end
can't be relied on. The check job's second step, after the sandbox's
proof, stops every process of a job's working folder (`$HOME/work/`,
also one removed since) that is neither the runner nor the step itself,
TERM then KILL after 5 s, and names each in the log. The runner takes
one job at a time (capacity: 1), so nothing of a running job is touched.
No change on the server.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-23 15:22:17 +02:00
|
|
|
# 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
|
|
|
|
|
|
2026-10-05 22:17:12 +02:00
|
|
|
# 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
|
|
|
|
|
|
ci: check every push on sahaquiel and deploy a green main
Pushes to main were deployed by hand. Now Forgejo Actions runs tool/check.sh on every
push, and a green main deploys learn.halkyon.eu by itself.
The runner (forgejo-runner v13.1.0) runs on sahaquiel as the unprivileged user `runner`,
with jobs directly on the host: containers would take Docker access, which is root on the
production server, and Forgejo warns that host jobs have no isolation from the runner's
user. So its systemd unit (deploy/runner/forgejo-runner.service) provides it: /home and
the Docker socket invisible, Docker's container networks unreachable, no capabilities,
two cores and 3 GB at most, lowest priority. It is registered to this repository only,
with the UUID and token of Forgejo's "Create new runner" (the old `register` is
deprecated); the token reaches it as a systemd credential. The configuration
(deploy/runner/config.yml) was dry-run against v13.1.0, the unit rates 1.8 in
`systemd-analyze security`. CI runs on pushes only: a pull request from a fork would run
its code on the production server.
The deploy job calls deploy/redeploy.sh as `deploy`, through a key that authorized_keys
pins to that script and to connections from sahaquiel itself. The script deploys the
commit CI checked while it is still main's tip, as a fast-forward, only if every new
commit is signed by a key in deploy/allowed-signers (as the deployed checkout lists
them), after dumping the database (the last ten are kept), and fails unless the server
answers afterwards; a failure puts the checkout back. Tested against the local stack:
refusals of a malformed argument, a stale commit, a forged unsigned commit and a
non-fast-forward, a real deploy with its dump, a repeat, and a failed build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-09-30 21:49:04 +02:00
|
|
|
- name: Check out
|
2026-10-05 22:17:12 +02:00
|
|
|
if: env.CHECKED_BEFORE != 'true'
|
ci: check every push on sahaquiel and deploy a green main
Pushes to main were deployed by hand. Now Forgejo Actions runs tool/check.sh on every
push, and a green main deploys learn.halkyon.eu by itself.
The runner (forgejo-runner v13.1.0) runs on sahaquiel as the unprivileged user `runner`,
with jobs directly on the host: containers would take Docker access, which is root on the
production server, and Forgejo warns that host jobs have no isolation from the runner's
user. So its systemd unit (deploy/runner/forgejo-runner.service) provides it: /home and
the Docker socket invisible, Docker's container networks unreachable, no capabilities,
two cores and 3 GB at most, lowest priority. It is registered to this repository only,
with the UUID and token of Forgejo's "Create new runner" (the old `register` is
deprecated); the token reaches it as a systemd credential. The configuration
(deploy/runner/config.yml) was dry-run against v13.1.0, the unit rates 1.8 in
`systemd-analyze security`. CI runs on pushes only: a pull request from a fork would run
its code on the production server.
The deploy job calls deploy/redeploy.sh as `deploy`, through a key that authorized_keys
pins to that script and to connections from sahaquiel itself. The script deploys the
commit CI checked while it is still main's tip, as a fast-forward, only if every new
commit is signed by a key in deploy/allowed-signers (as the deployed checkout lists
them), after dumping the database (the last ten are kept), and fails unless the server
answers afterwards; a failure puts the checkout back. Tested against the local stack:
refusals of a malformed argument, a stale commit, a forged unsigned commit and a
non-fast-forward, a real deploy with its dump, a repeat, and a failed build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-09-30 21:49:04 +02:00
|
|
|
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
|
2026-10-05 22:17:12 +02:00
|
|
|
if: env.CHECKED_BEFORE != 'true'
|
ci: check every push on sahaquiel and deploy a green main
Pushes to main were deployed by hand. Now Forgejo Actions runs tool/check.sh on every
push, and a green main deploys learn.halkyon.eu by itself.
The runner (forgejo-runner v13.1.0) runs on sahaquiel as the unprivileged user `runner`,
with jobs directly on the host: containers would take Docker access, which is root on the
production server, and Forgejo warns that host jobs have no isolation from the runner's
user. So its systemd unit (deploy/runner/forgejo-runner.service) provides it: /home and
the Docker socket invisible, Docker's container networks unreachable, no capabilities,
two cores and 3 GB at most, lowest priority. It is registered to this repository only,
with the UUID and token of Forgejo's "Create new runner" (the old `register` is
deprecated); the token reaches it as a systemd credential. The configuration
(deploy/runner/config.yml) was dry-run against v13.1.0, the unit rates 1.8 in
`systemd-analyze security`. CI runs on pushes only: a pull request from a fork would run
its code on the production server.
The deploy job calls deploy/redeploy.sh as `deploy`, through a key that authorized_keys
pins to that script and to connections from sahaquiel itself. The script deploys the
commit CI checked while it is still main's tip, as a fast-forward, only if every new
commit is signed by a key in deploy/allowed-signers (as the deployed checkout lists
them), after dumping the database (the last ten are kept), and fails unless the server
answers afterwards; a failure puts the checkout back. Tested against the local stack:
refusals of a malformed argument, a stale commit, a forged unsigned commit and a
non-fast-forward, a real deploy with its dump, a repeat, and a failed build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-09-30 21:49:04 +02:00
|
|
|
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
|
2026-09-30 22:24:05 +02:00
|
|
|
# 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
|
ci: check every push on sahaquiel and deploy a green main
Pushes to main were deployed by hand. Now Forgejo Actions runs tool/check.sh on every
push, and a green main deploys learn.halkyon.eu by itself.
The runner (forgejo-runner v13.1.0) runs on sahaquiel as the unprivileged user `runner`,
with jobs directly on the host: containers would take Docker access, which is root on the
production server, and Forgejo warns that host jobs have no isolation from the runner's
user. So its systemd unit (deploy/runner/forgejo-runner.service) provides it: /home and
the Docker socket invisible, Docker's container networks unreachable, no capabilities,
two cores and 3 GB at most, lowest priority. It is registered to this repository only,
with the UUID and token of Forgejo's "Create new runner" (the old `register` is
deprecated); the token reaches it as a systemd credential. The configuration
(deploy/runner/config.yml) was dry-run against v13.1.0, the unit rates 1.8 in
`systemd-analyze security`. CI runs on pushes only: a pull request from a fork would run
its code on the production server.
The deploy job calls deploy/redeploy.sh as `deploy`, through a key that authorized_keys
pins to that script and to connections from sahaquiel itself. The script deploys the
commit CI checked while it is still main's tip, as a fast-forward, only if every new
commit is signed by a key in deploy/allowed-signers (as the deployed checkout lists
them), after dumping the database (the last ten are kept), and fails unless the server
answers afterwards; a failure puts the checkout back. Tested against the local stack:
refusals of a malformed argument, a stale commit, a forged unsigned commit and a
non-fast-forward, a real deploy with its dump, a repeat, and a failed build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-09-30 21:49:04 +02:00
|
|
|
echo "$sdk/bin" >> "$FORGEJO_PATH"
|
|
|
|
|
"$sdk/bin/flutter" --version
|
|
|
|
|
|
2026-10-02 20:43:07 +02:00
|
|
|
# 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).
|
ci: check every push on sahaquiel and deploy a green main
Pushes to main were deployed by hand. Now Forgejo Actions runs tool/check.sh on every
push, and a green main deploys learn.halkyon.eu by itself.
The runner (forgejo-runner v13.1.0) runs on sahaquiel as the unprivileged user `runner`,
with jobs directly on the host: containers would take Docker access, which is root on the
production server, and Forgejo warns that host jobs have no isolation from the runner's
user. So its systemd unit (deploy/runner/forgejo-runner.service) provides it: /home and
the Docker socket invisible, Docker's container networks unreachable, no capabilities,
two cores and 3 GB at most, lowest priority. It is registered to this repository only,
with the UUID and token of Forgejo's "Create new runner" (the old `register` is
deprecated); the token reaches it as a systemd credential. The configuration
(deploy/runner/config.yml) was dry-run against v13.1.0, the unit rates 1.8 in
`systemd-analyze security`. CI runs on pushes only: a pull request from a fork would run
its code on the production server.
The deploy job calls deploy/redeploy.sh as `deploy`, through a key that authorized_keys
pins to that script and to connections from sahaquiel itself. The script deploys the
commit CI checked while it is still main's tip, as a fast-forward, only if every new
commit is signed by a key in deploy/allowed-signers (as the deployed checkout lists
them), after dumping the database (the last ten are kept), and fails unless the server
answers afterwards; a failure puts the checkout back. Tested against the local stack:
refusals of a malformed argument, a stale commit, a forged unsigned commit and a
non-fast-forward, a real deploy with its dump, a repeat, and a failed build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-09-30 21:49:04 +02:00
|
|
|
- name: Dependencies
|
2026-10-05 22:17:12 +02:00
|
|
|
if: env.CHECKED_BEFORE != 'true'
|
2026-10-02 20:43:07 +02:00
|
|
|
run: flutter pub get --enforce-lockfile || flutter pub get --enforce-lockfile
|
ci: check every push on sahaquiel and deploy a green main
Pushes to main were deployed by hand. Now Forgejo Actions runs tool/check.sh on every
push, and a green main deploys learn.halkyon.eu by itself.
The runner (forgejo-runner v13.1.0) runs on sahaquiel as the unprivileged user `runner`,
with jobs directly on the host: containers would take Docker access, which is root on the
production server, and Forgejo warns that host jobs have no isolation from the runner's
user. So its systemd unit (deploy/runner/forgejo-runner.service) provides it: /home and
the Docker socket invisible, Docker's container networks unreachable, no capabilities,
two cores and 3 GB at most, lowest priority. It is registered to this repository only,
with the UUID and token of Forgejo's "Create new runner" (the old `register` is
deprecated); the token reaches it as a systemd credential. The configuration
(deploy/runner/config.yml) was dry-run against v13.1.0, the unit rates 1.8 in
`systemd-analyze security`. CI runs on pushes only: a pull request from a fork would run
its code on the production server.
The deploy job calls deploy/redeploy.sh as `deploy`, through a key that authorized_keys
pins to that script and to connections from sahaquiel itself. The script deploys the
commit CI checked while it is still main's tip, as a fast-forward, only if every new
commit is signed by a key in deploy/allowed-signers (as the deployed checkout lists
them), after dumping the database (the last ten are kept), and fails unless the server
answers afterwards; a failure puts the checkout back. Tested against the local stack:
refusals of a malformed argument, a stale commit, a forged unsigned commit and a
non-fast-forward, a real deploy with its dump, a repeat, and a failed build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-09-30 21:49:04 +02:00
|
|
|
|
2026-10-23 15:14:23 +02:00
|
|
|
# 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
|
2026-10-05 22:17:12 +02:00
|
|
|
if: env.CHECKED_BEFORE != 'true'
|
ci: check the generated code; deploy: migrate before the switch
Two of Serverpod's own conventions, as its v4 project template and roles have them.
CI now runs `serverpod generate` (CLI pinned to the server's Serverpod version, like
the template's workflow) and fails if the committed generated code differs from its
output, so a model or endpoint changed without regenerating can't reach main. Checked in
a clean clone: today's code passes, a committed stale generated file fails.
redeploy.sh now applies the migrations in a run of their own, with Serverpod's
maintenance role (--role=maintenance --apply-migrations), after the dump and before
switching to the new server. That run exits with 0 only if the migrations worked, and
each migration is one transaction, so a failing one stops the deploy while the old
server keeps serving; before, the new server applied them at start and would have
crash-looped. Tested on the local stack: a deploy through the new step, and a failing
run (wrong database password) that exits with 1 after six attempts. AGENTS.md says
migrations must keep the old server working until the switch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-09-30 22:55:24 +02:00
|
|
|
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
|
2026-10-23 15:14:23 +02:00
|
|
|
echo "$bin" >> "$FORGEJO_PATH"
|
ci: check the generated code; deploy: migrate before the switch
Two of Serverpod's own conventions, as its v4 project template and roles have them.
CI now runs `serverpod generate` (CLI pinned to the server's Serverpod version, like
the template's workflow) and fails if the committed generated code differs from its
output, so a model or endpoint changed without regenerating can't reach main. Checked in
a clean clone: today's code passes, a committed stale generated file fails.
redeploy.sh now applies the migrations in a run of their own, with Serverpod's
maintenance role (--role=maintenance --apply-migrations), after the dump and before
switching to the new server. That run exits with 0 only if the migrations worked, and
each migration is one transaction, so a failing one stops the deploy while the old
server keeps serving; before, the new server applied them at start and would have
crash-looped. Tested on the local stack: a deploy through the new step, and a failing
run (wrong database password) that exits with 1 after six attempts. AGENTS.md says
migrations must keep the old server working until the switch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-09-30 22:55:24 +02:00
|
|
|
|
2026-09-30 22:24:05 +02:00
|
|
|
# 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
|
2026-10-05 22:17:12 +02:00
|
|
|
if: env.CHECKED_BEFORE != 'true'
|
2026-09-30 22:24:05 +02:00
|
|
|
run: |
|
|
|
|
|
for name in SERVERPOD_DATABASE_PASSWORD \
|
|
|
|
|
SERVERPOD_PASSWORD_serverSideSessionKeyHashPepper \
|
feat(auth): sign in with an email address, keeping only a keyed hash of it
People without GitHub can now keep an anonymous account, and sign in on
another device, with an email address and a code mailed to it each time;
no password. The instance keeps only HMAC-SHA256 of the address, keyed
with its emailSignInPepper (ADR 0010): from what someone types it finds
the person and tells whether an address is free, it mails only an address
someone has just typed, and it can't write to anybody otherwise or show
anybody their address. The device remembers the address it signed in
with, in the person's folder, to show it.
It works as GitHub does. A code for a new address makes it the signed-in
person's, who becomes identified and keeps everything. A code for another
person's address gives a switch token, which signs in as them without a
session; a device holding studysets asks first. Asking for a code answers
alike for every address. Codes are eight Crockford characters, valid 15
minutes and three tries (Serverpod's code challenges); three codes an
hour per address, ten per network. In development the code goes to the
server's log; elsewhere there is no email sign-in until a mail service
is set up (instance.info().emailSignIn).
Someone identified may add the other way in: the Profile tab lists how
they sign in and adds GitHub or an address. They are never switched to
another account: an address or GitHub account that is another's is said
to be so. Switching now deletes the account left behind only when it was
anonymous; it deleted any, which only anonymous people could reach so far.
The migration adds email_identity and email_code_request with their
indexes and foreign keys to Serverpod's challenges; nothing else changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-15 19:42:21 +02:00
|
|
|
SERVERPOD_PASSWORD_emailSignInPepper \
|
2026-10-20 13:32:21 +02:00
|
|
|
SERVERPOD_PASSWORD_totpKey \
|
|
|
|
|
SERVERPOD_PASSWORD_recoveryCodeKey \
|
|
|
|
|
SERVERPOD_PASSWORD_signInDecoyKey \
|
2026-09-30 22:24:05 +02:00
|
|
|
SERVERPOD_PASSWORD_serviceSecret; do
|
|
|
|
|
echo "$name=$(openssl rand -hex 32)" >> "$FORGEJO_ENV"
|
|
|
|
|
done
|
|
|
|
|
|
ci: check every push on sahaquiel and deploy a green main
Pushes to main were deployed by hand. Now Forgejo Actions runs tool/check.sh on every
push, and a green main deploys learn.halkyon.eu by itself.
The runner (forgejo-runner v13.1.0) runs on sahaquiel as the unprivileged user `runner`,
with jobs directly on the host: containers would take Docker access, which is root on the
production server, and Forgejo warns that host jobs have no isolation from the runner's
user. So its systemd unit (deploy/runner/forgejo-runner.service) provides it: /home and
the Docker socket invisible, Docker's container networks unreachable, no capabilities,
two cores and 3 GB at most, lowest priority. It is registered to this repository only,
with the UUID and token of Forgejo's "Create new runner" (the old `register` is
deprecated); the token reaches it as a systemd credential. The configuration
(deploy/runner/config.yml) was dry-run against v13.1.0, the unit rates 1.8 in
`systemd-analyze security`. CI runs on pushes only: a pull request from a fork would run
its code on the production server.
The deploy job calls deploy/redeploy.sh as `deploy`, through a key that authorized_keys
pins to that script and to connections from sahaquiel itself. The script deploys the
commit CI checked while it is still main's tip, as a fast-forward, only if every new
commit is signed by a key in deploy/allowed-signers (as the deployed checkout lists
them), after dumping the database (the last ten are kept), and fails unless the server
answers afterwards; a failure puts the checkout back. Tested against the local stack:
refusals of a malformed argument, a stale commit, a forged unsigned commit and a
non-fast-forward, a real deploy with its dump, a repeat, and a failed build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-09-30 21:49:04 +02:00
|
|
|
- name: Checks
|
2026-10-05 22:17:12 +02:00
|
|
|
if: env.CHECKED_BEFORE != 'true'
|
build: Melos, a Dart check and every path for the single repository
- 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>
2026-10-19 20:39:09 +02:00
|
|
|
run: dart run tool/check.dart
|
ci: check every push on sahaquiel and deploy a green main
Pushes to main were deployed by hand. Now Forgejo Actions runs tool/check.sh on every
push, and a green main deploys learn.halkyon.eu by itself.
The runner (forgejo-runner v13.1.0) runs on sahaquiel as the unprivileged user `runner`,
with jobs directly on the host: containers would take Docker access, which is root on the
production server, and Forgejo warns that host jobs have no isolation from the runner's
user. So its systemd unit (deploy/runner/forgejo-runner.service) provides it: /home and
the Docker socket invisible, Docker's container networks unreachable, no capabilities,
two cores and 3 GB at most, lowest priority. It is registered to this repository only,
with the UUID and token of Forgejo's "Create new runner" (the old `register` is
deprecated); the token reaches it as a systemd credential. The configuration
(deploy/runner/config.yml) was dry-run against v13.1.0, the unit rates 1.8 in
`systemd-analyze security`. CI runs on pushes only: a pull request from a fork would run
its code on the production server.
The deploy job calls deploy/redeploy.sh as `deploy`, through a key that authorized_keys
pins to that script and to connections from sahaquiel itself. The script deploys the
commit CI checked while it is still main's tip, as a fast-forward, only if every new
commit is signed by a key in deploy/allowed-signers (as the deployed checkout lists
them), after dumping the database (the last ten are kept), and fails unless the server
answers afterwards; a failure puts the checkout back. Tested against the local stack:
refusals of a malformed argument, a stale commit, a forged unsigned commit and a
non-fast-forward, a real deploy with its dump, a repeat, and a failed build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-09-30 21:49:04 +02:00
|
|
|
|
2026-10-05 22:17:12 +02:00
|
|
|
# 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
|