docs: ADR 0047, nothing the harness starts outlives its job
CI / verify (pull_request) Successful in 58s
CI / infrastructure-smoke (pull_request) Successful in 21s
CI / migration-smoke (pull_request) Successful in 1m51s
CI / image-build (pull_request) Successful in 19s
CI / deploy-smoke (same-origin) (pull_request) Successful in 43s
CI / deploy-smoke (split) (pull_request) Successful in 57s

A job the runner kills never runs its own cleanup trap, and the
platform's CI leaked a whole migration stack that way. The harness
follows the platform's three-layer rule:
- everything it starts is named and labeled as a CI stack;
- it cleans up in a trap;
- the template workflow sweeps stale CI stacks before running it, and
  removes its own in a final if: always() step.

The sweep ships in the harness image.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
ekklesia
2026-10-05 20:54:38 -05:00
co-authored by Claude Opus 5.5
parent 3827841205
commit 3a2f5d68f6
@@ -154,8 +154,9 @@ it.
- checks out the module with `persist-credentials: false` and pinned actions;
- runs the module's own unit tests, typecheck, and lint;
- runs the harness, once per pinned platform version, with one `docker run` of the harness image
**by digest**. The module directory is mounted read-only, and the runner's Docker is used exactly
- sweeps stale CI stacks from the runner, then runs the harness, once per pinned platform version,
with one `docker run` of the harness image **by digest**, and finally, always, removes this job's
own stacks. The module directory is mounted read-only, and the runner's Docker is used exactly
as the platform's smoke uses it.
It references no secret, needs no registry credential (the harness image is public within the
@@ -176,6 +177,22 @@ An image digest is the one reference that is equally immutable, equally reviewab
unchanged on Gitea 1.24, Gitea 1.26, and GitHub Actions. A composite action can be added later as a
convenience over the same image, without changing what a module pins.
### Nothing the harness starts outlives the job that started it
The harness starts Docker stacks on a shared runner, and a runner can kill a job outright: a
cancelled job, or the runner's own time limit, since Gitea 1.24 ignores `timeout-minutes`. A script's
own cleanup trap does not run then. The platform's CI leaked a whole migration stack that way, and it
ran until someone removed it by hand. So the harness follows the platform's rule
(`scripts/ci-sweep.sh`, `docs/development/security.md`):
- everything it starts is named and labeled as a CI stack of its run, attempt, and job key;
- it removes its own stacks in a trap;
- the template workflow runs the harness's **stale sweep** before the harness starts, and its
**job sweep** in a final `if: always()` step, so a killed run's stacks are removed by that run's
last step, or failing that by the next job on the runner.
The sweep ships inside the harness image, so a module never copies it.
### Publishing follows the instance's release pattern
The harness image is pushed only by a `publish` job that runs on `v*` tags, after the platform's CI