Context
Two failures in one evening, neither of them about the content being written.
Views regenerated on branches. Every branch ran the generator and
committed the result, so two branches with disjoint sources conflicted on
nineteen shared pages. Resolving it was a ritual — checkout --theirs,
regenerate, re-stage — that produced no information four times running.
ADR-018 moved views to the default branch only.
Four merges in forty seconds. Each merge started a push run; each run
regenerated the views and pushed them to the same branch; three lost the race
and rebased their own regeneration commit onto the winner’s, conflicting on
the files both had just rewritten. One of them had already concretized five
temporary codes correctly and lost the renames with the failed rebase,
leaving main in the state the trunk guard exists to forbid.
Neither is a bug in luria, and neither is a bug in the record. Both are the same thing:
An assumption about pacing, never written down, true only because a person was doing the work.
At human tempo you merge one pull request, read the result, and merge the next; branches are worked one at a time because attention is serial. Every one of those is a serialization — supplied by the operator, invisible in the configuration, and removed the moment the operator is not a person.
Three details make this worth a decision rather than two fixes.
The class was not swept, so it bit twice. ADR-018 fixed the branch-level conflict and left the run-level one untouched, in the same workflow file. Making a file single-writer by location does nothing about concurrent writers at that location.
And the argument already existed, filed against a sibling. ADR-002 says a per-merge bot commit races in-flight rebases, which is why the changelog collector runs on a cadence and never per merge. The same file, a different job, the same hazard, reasoned about correctly two months earlier and never generalised. Three jobs in one workflow, one hazard, three separate discoveries.
A third instance exists and has not fired. Auditing every job in
.github/workflows/ for contents: write finds generate_summaries.yaml
with write permission, a tool whose default is to commit and push, and no
concurrency group. It is workflow_dispatch-only, so nothing has raced it
yet. It is the same shape.
Decision
1. A job that writes where other runs or branches also write declares its
serialization. A concurrency: group, or a comment saying why it needs
none. “It has never raced” is not a reason; it is the observation this
decision exists to distrust.
2. The first pacing failure in a repository is answered by an audit of
every writer to shared state, not by a patch to the one that broke. The
audit is small and mechanical — parse the workflows, list the jobs that
push, check each for a group. Listing the jobs that hold
contents: write is the version of this that was written first, and it is
the version that missed one: a job can push on the default token’s implicit
grant, so an audit reading declarations finds only the writers that declared
themselves. It found a third instance here in
one command, before anybody was hurt by it.
3. Shared state is inventoried explicitly, because the audit is only as good as the list of places two writers can meet. For this repository:
| shared location | writers | serialization |
|---|---|---|
main (ref) | docs-generate on every merge | workflow concurrency: per ref (ADR-018 + this) |
docs/ views | docs-generate only, on main | not written on branches at all (ADR-018) |
| temporary-code numbering | luria concretize, on main | inherits the ref’s group |
| changelog fragments | collect | a cadence, never per merge (ADR-002) |
| a pull request’s own branch | docs-check | one branch, one run at a time |
summaries | generate_summaries.yaml | a group, added by this decision’s own audit — the workflow was deleted in #91, and the row is kept because the audit is the point |
assets/logo.png | render_svg.yaml | a group, added when the audit was rerun by what it does rather than what it declares |
What this does not decide
It does not say to merge more slowly. Spacing merges by twenty seconds would have prevented the second failure and is worth doing, and it is not the decision, because it is a property of one operator’s habits rather than of the repository. It fails the moment there are two operators, or a scheduled job, or a queue that drains faster than usual. A mitigation that lives in somebody’s discipline is one the next person does not inherit.
Alternatives considered
Add the concurrency groups and stop. What was done for the two failures, and it is insufficient for the reason the context gives: the same hazard was correctly reasoned about for one job and left in place for two others in the same file. Fixing instances is how a class survives.
A lint that fails CI on an undeclared writer. The right end state, and
this decision is Proposed partly because the check does not exist yet. The
project’s bar for adding one is that the violation is always wrong and
mechanically fixable; “a writing job with no concurrency group” is
mechanically detectable, and whether it is always wrong is exactly what a
first pass over the six existing jobs would establish. Worth building after
the audit, not before it.
Push protection or a merge queue. Serializes merges properly and is the industrial answer. Rejected as disproportionate for a repository with one maintainer, and because it would not have prevented the first failure, which was two branches conflicting before either merged.
Consequences
- Six jobs checked, one fixed in the same contribution — and one missed,
for the reason above.
render_svg.yamlwas in the repository the whole time; the audit could not see it because the audit read declarations. The fix and the method correction are in the contribution that promoted this decision, which is later than it should have been and is the honest date — leaving a live hazard in place to preserve a worked example would be the wrong trade. The audit is a command, not a project. ADR-002’s reasoning becomes a general rule rather than a fact about the changelog collector, which is the outcome it should have had.- The honest cost is stated where it belongs: three of the nine contributions in this stretch of work were repairs to damage the tempo caused. Working faster finds the same defects sooner and finds defects a slower operator would never have reached. Both halves are real, and only the first is usually counted.