CI has no concurrency group — superseded runs are never cancelled #78
Labels
No labels
area/ai
area/backend
area/frontend
area/infra
area/scheduler
area/wled
good-first-issue
priority/high
priority/low
priority/medium
type/bug
type/chore
type/ci-cd
type/docs
type/feature
type/qa
v1.0.0
v1.1.0
v1.2.0
v2.0.0
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
rbrooks/Iris-WLED#78
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
.forgejo/workflows/ci.ymlhas no top-levelconcurrency:key, so every push to a branch or PR starts a fresh run while the previous run for that same ref continues to completion.Context
The shared Forgejo Actions runners are saturated — see Rhoving/iac-repo#371. Measured 2026-08-29: all six fleet-wide job slots were occupied, with three repos pushing at once, and jobs turning over in roughly 1–2 minutes. Runner capacity is shared across every repo on this instance, so superseded runs here are directly other repos' queue wait.
Several repos on this instance already set a top-level concurrency group (
Quest-Board,Radome,TeaLeaves,ChoreMachine,HomeBooks,BatteryStorageCalculator). These workflows do not.Affected
.forgejo/workflows/ci.yml(jobs: python-lint, python-test)Proposed
Include
github.workflowin the group so separate workflows in the same repo don't cancel each other.⚠️ Before enabling
cancel-in-progress: true, check that any teardown this CI relies on runs under cancellation (if: always()). Cancelled jobs skip normal cleanup steps. Leakedpostgres:18service containers aged 7–11h were found inside the runners' DinD daemons on 2026-08-29 and had to be removed by hand (Rhoving/iac-repo#292) — that failure mode gets more frequent, not less, if cancellation is enabled without cleanup that survives it.Definition of done
concurrency:added to.forgejo/workflows/ci.ymlRefs Rhoving/iac-repo#371
Implemented — working tree, not yet committed
Teardown audit (first DoD item)
Walked every job in
ci.yml.docker-e2eis the only one that leaves state on the host —python-lint,python-test,migration-check,frontend, andsecurity-auditare allsetup-python/setup-node/uvand leak nothing.Its teardown is already
if: always(), which is what this issue asked me to verify. But that alone is not sufficient here, and the reason is specific to this repo:So if a cancelled job does not reach
always()(act's cancellation semantics differ from GitHub's, and I could not confirm them without actually cancelling a run), the leftovers are named after arun_idthat no future run will ever match. Nothing would ever reap them. That is precisely the Rhoving/iac-repo#292 failure mode this issue warns about, and turning oncancel-in-progresswithout addressing it would have made it more likely, not less.Rather than bet on
always()firing, I made the cleanup survive cancellation regardless.Changes to
.forgejo/workflows/ci.yml1. Top-level concurrency group — exactly as proposed, matching the six sibling repos:
2. A reaper step at the top of
docker-e2ethat sweeps anyiris-*/pw-*container oriris-net-*network older than 2h.The 2h cutoff is the important part: PR runs execute concurrently on this shared host, each with its own
run_id, so a naive "delete anything that isn't mine" sweep would kill a sibling PR's containers mid-test. A live run's resources are minutes old (longest observed run was 38 min, and that included the arm64 build now removed by #79); only genuinely abandoned ones reach 2h.3. A comment on the Cleanup step noting it is the fast path and the reaper is the backstop.
Verification
Exercised the reaper's shell against a stubbed
dockercovering four cases:docker ps.Createdtimestampdocker inspectfails (resource raced away)Every path exits 0 under
bash -e, so the reaper can never fail the job.ci.ymlstill parses as valid YAML.Remaining
The third DoD item — "Confirmed superseded runs actually cancel, by pushing twice in quick succession" — needs an actual push, so it stays unchecked until this is committed. Leaving open until then.
Refs Rhoving/iac-repo#371.
Done — all three DoD items satisfied
Shipped in
43d3a32.docker-e2eis the only job leaving host state; the other jobs are stateless. Its cleanup isif: always()and run-scoped (iris-<run_id>), so a cancelled job that never reaches it would strand resources under arun_idno future run matches. Rather than rely onalways()firing under act's cancellation, the job now reapsiris-*/pw-*containers andiris-net-*networks older than 2h — the age cutoff keeps a concurrent sibling PR's containers safe.concurrency:added toci.yml, keyed on${{ github.workflow }}-${{ github.ref }}withcancel-in-progress: true, matching the sibling repos.544ef55) wentcancelledafter 1s, superseded by #7637. It was cancelled beforedocker-e2estarted, so nothing leaked.Side benefit worth noting: combined with #79 removing the emulated arm64 build, full runs on
mainwent from 20m13s / 38m11s down to about 6m30s.Refs Rhoving/iac-repo#371.