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; - checks out the module with `persist-credentials: false` and pinned actions;
- runs the module's own unit tests, typecheck, and lint; - 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 - sweeps stale CI stacks from the runner, then runs the harness, once per pinned platform version,
**by digest**. The module directory is mounted read-only, and the runner's Docker is used exactly 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. as the platform's smoke uses it.
It references no secret, needs no registry credential (the harness image is public within the 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 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. 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 ### 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 The harness image is pushed only by a `publish` job that runs on `v*` tags, after the platform's CI