ci: the nightly cron is a second buildcache writer, and the default auto-cancel does not cover it #18
Labels
No labels
deferred
waiting-on-julian
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
jlxq0/typst-mcp#18
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?
Reporting state from the concurrency probe in
jlxq0/mantis#32; the decision is yours..forgejo/workflows/ci.ymlguards the buildcache export onrefs/heads/main:That is one writer for
pushevents, which is what#4needed. It is not one writer overall, because two other triggers also produceGITHUB_REFofrefs/heads/main:Task
16519is that cron on 2026-08-25: adockerjob onmainthat exportedsha256:e269c2ea….workflow_dispatchhas the same shape and was not in the original analysis.What the probe changes
Measured on Forgejo
15.0.2+gitea-1.22.0, full evidence injlxq0/mantis#32:concurrency:block at all. A run was observedrunning, a push landed two seconds later, and it readcancelledsix seconds after that.on.pushandon.pull_request(synchronize) only. Ascheduleorworkflow_dispatchinvocation neither cancels nor is cancelled. So the cron overlapping a merge build is the one case in this repository that the default does not close, and it is a real second writer.concurrency:is honoured. Five runs across three refs serialised strictly one at a time for thirteen minutes on a runner withcapacity: 4that was demonstrably running other repositories' jobs at the same time.concurrency:is silently ignored. The identical block moved underjobs.had no effect:cancel-in-progress: falsedid not prevent the default cancellation, the workflow parsed, and nothing warned. This is the failure mode#4's pull requests refused to risk, and refusing was correct.The shape of the fix
Top-level, not under
jobs::Two things to weigh, which is why this is an issue rather than a pull request:
cargojob included, not justdocker. On a shared runner already queueing for minutes, that lengthens the critical path for every trigger on the same ref.group: ci-${{ github.ref }}puts the cron and amainpush in one group, which is the point, and leaves tag builds in their own group, which is fine now that a tag build only imports. Whetherworkflow_dispatchshould share the group is a judgement about what you use it for.The narrower alternative is to guard the export on the event as well as the ref, so only
pushexports and the cron imports only. That needs no concurrency support at all and costs the cron a cache refresh it may not need, since a cron run builds the same commitmainalready built.Fixed in #25, merged at
d94a68c. Taking the narrower option: guard the export on theevent as well as the ref, rather than a top-level
concurrency:block.Why not
concurrency:. Job-level is silently ignored on this Forgejo, so aworkflow-level block is one well-meant refactor away from being inert while still parsing.
It also serialises the whole run,
cargoincluded, on a shared-capacity runner to solvewhat one condition closes. And the file already makes this argument for tag builds: a cron
or dispatch run on
mainbuilds the commitmainjust built, so its export would bebyte-identical and buys nothing.
What passed
The condition, exercised over all five trigger cases rather than reasoned about:
The first row is the control. Without it the four
norows are equally satisfied by aguard that never exports at all.
The event vocabulary is this instance's own.
/actions/tasksover 100 tasks reportsexactly
push(117),pull_request(26),schedule(26) andworkflow_dispatch(4), anda
scheduletask carrieshead_branch=main, which is this issue's premise confirmed fromthe API rather than from the workflow file.
What failed, and it was my acceptance rather than the fix
I planned to prove it against the buildcache ref's own digest: record
D0before,merge (a push to
main, which must export and move it), then dispatch (which must not).D0andD1are identical,sha256:0592aacd0bbecb…both times, after a push tomainwhose
dockerjob reported success.That is not a failure of the export.
#25changed.forgejo/workflows/ci.ymlandAGENTS.md, and the Dockerfile COPYs onlyfonts,scripts/fetch-fonts.sh,Cargo.toml,Cargo.lock,srcandtemplates. Neither changed file enters any layer, so everylayer is byte-identical and a
mode=maxexport produces the same manifest and the samedigest. An unchanged digest is what a working export produces here.
So the instrument returns the same value whether the export ran or not, which is the
one thing an acceptance may not do. I designed it, ran it, and it could not have told me
the answer either way.
And it cannot be repaired, which is the part worth keeping. A cron or dispatch run
always builds the commit
mainhas just built, so its export is always byte-identical tothe one already there. The property that makes the fix correct is the same property that
makes it unobservable in the registry. The race this closes is a concurrent-write failure,
not a content difference.
What remains unmeasured
That the runner sets
GITHUB_EVENT_NAMEto those literals in the shell, as opposed togithub.event_namein YAML expressions, which this workflow already relies on and which isknown to work here. Job logs cannot be read on this instance (see #22), so it is not
directly observable.
Its failure mode is benign and eventually visible: if that variable were empty, the
condition would be false on every trigger, no run would export, and the cache would go stale
and builds slower. That is the safe direction, and it is the opposite of the concurrent-write
failure this issue is about.
Closing on the condition table and the event vocabulary, with the registry acceptance
recorded as attempted and inconclusive rather than quietly dropped.
Pitfall in
AGENTS.md:GITHUB_REFdoes not identify a trigger, so a ref-only guard readsas naming one writer while admitting three.