fix(ci): make a push to main the only buildcache writer #25

Merged
jlxq0 merged 1 commit from fix/one-buildcache-writer into main 2026-09-01 23:33:05 +00:00
Owner

Closes #18, which had sat open for a week. I missed it, which is on me: it was filed on
2026-08-26 with the decision explicitly left to me and I did not see it until listing open
issues today.

The decision

#18 offered two shapes. Taking the narrower one: guard the export on the event as well as
the ref.

GITHUB_REF does not identify a trigger. refs/heads/main reads as "the main build", and
schedule and workflow_dispatch carry the same ref, so the ref-only guard admitted three
writers rather than one. Task 16519 is the cron doing exactly that on 2026-08-25.

Against the top-level concurrency: alternative:

  • It serialises the whole run, cargo included, on a runner that is already shared and
    queueing, to solve a problem one condition closes.
  • Job-level concurrency: is silently ignored on this Forgejo (jlxq0/mantis#32): the
    block parses, nothing warns, and it does nothing. A workflow-level block is therefore one
    well-intentioned refactor away from being inert while still looking present, which is the
    same shape as a required status that can only be green.
  • The file already makes this argument for tag builds: a tag build is the same commit
    main just built, so its cache would be byte-identical and it loses nothing by importing
    only. A cron and a dispatch run on main are the same case. Extending the existing
    argument beats adding a second mechanism beside it.

The cost #18 names is real and accepted: the cron no longer refreshes the cache. It was
refreshing it with a byte-identical export of a commit main had already built.

Verified by exercising the condition, not by reading it

ref                      event              old   new
refs/heads/main          push               yes   yes   <- control
refs/heads/main          schedule           yes   no    <- was a second writer
refs/heads/main          workflow_dispatch  yes   no    <- was a third writer
refs/tags/v0.2.2         push               no    no
refs/pull/25/head        pull_request       no    no

The first row is the control. Without it, "the export stopped happening" is
indistinguishable from "the export was disabled", and the four no rows alone are
satisfied by a guard that never exports at all.

Acceptance after merge

A condition table is a statement about the shell, not about the runner. The observable
consequence is the buildcache ref's own digest:

  1. D0 = sha256:0592aacd0bbecb…, recorded before this branch existed.
  2. Merging this PR is a push to main, so its docker job must export and the digest
    must move. That is the control, and it proves the export still works.
  3. Then one workflow_dispatch run, after which the digest must be unchanged. That is
    the treatment.

I will post both digests on #18 before closing it. A treatment with no control would be
satisfied by an export that had silently broken for an unrelated reason.

Also

Pitfall recorded in AGENTS.md, since the next person reading a ref-only guard will read
it as naming one writer, exactly as this one did.

Closes #18, which had sat open for a week. I missed it, which is on me: it was filed on 2026-08-26 with the decision explicitly left to me and I did not see it until listing open issues today. ## The decision #18 offered two shapes. **Taking the narrower one: guard the export on the event as well as the ref.** `GITHUB_REF` does not identify a trigger. `refs/heads/main` reads as "the main build", and `schedule` and `workflow_dispatch` carry the same ref, so the ref-only guard admitted three writers rather than one. Task `16519` is the cron doing exactly that on 2026-08-25. Against the top-level `concurrency:` alternative: - It **serialises the whole run**, `cargo` included, on a runner that is already shared and queueing, to solve a problem one condition closes. - **Job-level `concurrency:` is silently ignored on this Forgejo** (`jlxq0/mantis#32`): the block parses, nothing warns, and it does nothing. A workflow-level block is therefore one well-intentioned refactor away from being inert while still looking present, which is the same shape as a required status that can only be green. - The file **already makes this argument** for tag builds: a tag build is the same commit `main` just built, so its cache would be byte-identical and it loses nothing by importing only. A cron and a dispatch run on `main` are the same case. Extending the existing argument beats adding a second mechanism beside it. The cost #18 names is real and accepted: the cron no longer refreshes the cache. It was refreshing it with a byte-identical export of a commit `main` had already built. ## Verified by exercising the condition, not by reading it ref event old new refs/heads/main push yes yes <- control refs/heads/main schedule yes no <- was a second writer refs/heads/main workflow_dispatch yes no <- was a third writer refs/tags/v0.2.2 push no no refs/pull/25/head pull_request no no **The first row is the control.** Without it, "the export stopped happening" is indistinguishable from "the export was disabled", and the four `no` rows alone are satisfied by a guard that never exports at all. ## Acceptance after merge A condition table is a statement about the shell, not about the runner. The observable consequence is the buildcache ref's own digest: 1. `D0 = sha256:0592aacd0bbecb…`, recorded before this branch existed. 2. **Merging this PR is a push to `main`**, so its `docker` job must export and the digest must move. That is the control, and it proves the export still works. 3. Then one `workflow_dispatch` run, after which the digest must be **unchanged**. That is the treatment. I will post both digests on #18 before closing it. A treatment with no control would be satisfied by an export that had silently broken for an unrelated reason. ## Also Pitfall recorded in `AGENTS.md`, since the next person reading a ref-only guard will read it as naming one writer, exactly as this one did.
fix(ci): make a push to main the only buildcache writer
All checks were successful
CI / cargo (pull_request) Successful in 1m53s
CI / docker (pull_request) Successful in 23s
53564c7669
The export was guarded on GITHUB_REF == refs/heads/main, which reads as one
writer and is not. `schedule` and `workflow_dispatch` carry that same ref, so
three triggers could export to one unqualified :buildcache ref. Task 16519 is
the cron on 2026-08-25 doing exactly that.

Forgejo's default auto-cancel does not close it: measured in jlxq0/mantis#32,
it covers `push` and `pull_request` (synchronize) only, so a cron run
overlapping a merge build is neither cancelled nor cancelling. Two concurrent
exports lose a blob write and fail with `error writing layer blob: unknown`,
after the image has already been pushed.

Test the event as well as the ref. Both extra triggers build the same commit
`main` already built, so their exports would be byte-identical and buy nothing,
which is the argument the file already makes for tag builds.

Not fixed with a top-level `concurrency:` block. That serialises the whole run,
`cargo` included, on a shared-capacity runner to solve what one condition
closes, and job-level `concurrency:` is silently ignored on this Forgejo, so
the block is one refactor away from doing nothing while still parsing.

Verified by exercising the condition over all five trigger cases, with
main+push as the control so the change is distinguishable from having disabled
the export.

Closes #18.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018CXx4uyuDhqwungt4SMN4S
jlxq0 merged commit d94a68c1b7 into main 2026-09-01 23:33:05 +00:00
jlxq0 deleted branch fix/one-buildcache-writer 2026-09-01 23:33:06 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
jlxq0/typst-mcp!25
No description provided.