From 3a2f5d68f6d4885ff5434609a45f8bac7666b6eb Mon Sep 17 00:00:00 2001 From: ekklesia Date: Mon, 5 Oct 2026 20:54:27 -0500 Subject: [PATCH] docs: ADR 0047, nothing the harness starts outlives its job 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 --- .../decisions/0047-module-ci-harness.md | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/docs/architecture/decisions/0047-module-ci-harness.md b/docs/architecture/decisions/0047-module-ci-harness.md index 98b1d53..8c1e333 100644 --- a/docs/architecture/decisions/0047-module-ci-harness.md +++ b/docs/architecture/decisions/0047-module-ci-harness.md @@ -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