ci: fourteen jobs on one runner re-do what the gate script already does in parallel, and the two lists have drifted apart #362
Labels
No labels
breaking
bug
documentation
enhancement
epic
good first issue
help wanted
refactoring
resolution/duplicate
resolution/invalid
resolution/wontfix
security
severity/critical
severity/high
severity/low
severity/medium
status/abandoned
status/blocked
status/needs-decision
status/needs-info
status/needs-verification
testing
upstream
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
sixfold-space/madtea#362
Loading…
Add table
Add a link
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?
Noticed this while looking at why
gofixis a remote runner job. It is, and the answer generalises pastgofix.Most of CI cannot fail if the local gate was green
gofixruns in two places:scripts/gate.sh:142and.forgejo/workflows/ci.yml:175. It is a pure function of the source plus the toolchain, so a clean local gate on a commit mathematically guarantees the CI job on that same commit is green. That holds for 11 of the 14 jobs:vet,gofmt,gofix,build,tidy,staticcheck,modernize,gosec,docs,third-party-licenses,hook-tests.The three that genuinely add signal a local run cannot:
test(-raceis scheduler-dependent, so a second run on other hardware is new information),mcp-conformance(drives a real stdio server against the external SDK client), andgovulncheck- though see below, it is scheduled wrong.The cost is worse than the duplication, because there is one runner
docs/contributing/ci.md:21- CI is served by a single org-level runner. So the 14 jobs do not run in parallel, they queue, and each one pays a container start plus its owngit init --object-format=sha256and shallow fetch before doing any work.Meanwhile
gate.shdeliberately runs its 8 heavy analyzers concurrently, "roughly an order of magnitude less wall-clock". So the current shape discards the parallelism the gate already implements and pays 14 checkouts for the privilege. Fork PRs get no cache mount either, so those 14 starts are all cold.The mirror has drifted, in both directions
ci.yml's header says a green CI run "means the same thing as a clean local checkout". It does not, either way round:gate.shthird-party-licensesgate.sh, not in CIadr-check,cross-compile,check-go-versioncheck-go-versionis the one that stings.ci.yml:53setsGOTOOLCHAIN: localspecifically so that "scripts/check-go-version.sh (the gate's toolchain-honesty step) provably compares go.mod against the toolchain that actually ran" - the precondition is set up and documented, and then no job ever runs the check it was set up for. A drifted image digest is supposed to fail that check loudly; today nothing would catch it.This is the predictable end state of keeping two hand-maintained lists of the same checks.
govulncheckis scheduled wrong in the other directionIt is the only check whose answer changes without the code changing, since the vulnerability database moves under a static commit. Running it per-push means a stale answer between pushes and no answer at all during a quiet week. It belongs on the daily cron in
drift.yml, which already exists.What I want to do
Collapse
ci.ymlto two jobs and let the gate script be the single definition of the check set:gate- runs./scripts/gate.sh, which already covers everything above and parallelises the heavy half itselfmcp-conformance- stays its own job, because it needs node and the golang image has none (today it installs a pinned tarball for that job alone, and folding it in would make every run download node)govulncheckmoves to the daily cron indrift.ymlthird-party-licensesfolds intogate.shso the local run stops being the weaker oneThat makes "green CI == clean local gate" true by construction instead of by two lists someone has to keep in step, which is what failed here.
Needs a decision before I build it, because there are real trade-offs:
gate.shis fail-fast, so a formatting error would stop the run before the analyzers instead of reporting everything broken at once. The heavy half already runs all 8 and reports each.gate.shwould need a flag to skip conformance for the CI job.The alternatives, if the above is too much: just fix the drift (add the 3 missing checks to CI, add
third-party-licensesto the gate, keep 14 jobs), or drop the pure duplicates from CI outright and accept that an outside contributor who never ran the gate gets no formatting or vet enforcement on their PR.Acceptance criteria
adr-check,cross-compileandcheck-go-versionrun in CI.third-party-licensesruns locally.govulncheckruns on a schedule, not only per-push.Decision: collapsing to two jobs, per the plan above.
gate- runs./scripts/gate.sh, which needs a flag to skip conformance for this jobmcp-conformance- stays separate, since it needs node and the golang image has nonegovulncheckmoves to the daily cron indrift.ymlthird-party-licensesfolds intogate.shAccepting the two trade-offs knowingly: a failure reads
gaterather than the specific analyzer in the forge UI, and the cheap half stays fail-fast so a formatting error stops the run before the analyzers. The run output still names the failing step, and the heavy half still reports all 8 at once, so neither costs much in practice.The point is that the check set gets one definition instead of two.
check-go-versionis the proof it needs one: the workflow setsGOTOOLCHAIN: localspecifically so that check is meaningful, documents why, and then never runs it. That is not an oversight anyone would catch by reading either file on its own - it is only visible when you diff the two lists, which nothing does.Unblocked; picking this up after the branch currently in flight lands.