Repository navigation
ci: declare the action reference convention and gate the odd one out - #8546
Conversation
objectui#8465. There were 13 distinct action references in .github/workflows/. Exactly one was spelled differently from the other twelve -- a commit SHA on actions/stale -- and it was the only reference in the repository that had never resolved: 236 scheduled runs of stale.yml since 2026-01-16, 0 successes, unnoticed for eight months because nothing downstream consumes that job. The finding is the sole instance, not the pinning. SHA pinning is normally the more secure spelling. What failed was one ref written in a form nothing else in the tree used, with no convention that would have made the odd one out visible and nothing verifying it. So this writes the convention down and makes it checkable: - The default spelling is a floating major tag, owner/action@vN -- what 12 of the 13 refs already were. No reference is changed by this commit, in either direction. - An exception is marked by an entry in DECLARED_EXCEPTIONS carrying the workflow, the action, the owning issue and a real reason. That is the only form an exception takes, and it is the part that was missing. - The gate goes red three ways: an undeclared off-convention ref, an exception matching nothing, and its own census collapsing. It checks spelling, not resolvability -- resolving a SHA needs the network. The offline half is what failed: no reason attached and no second instance to compare against. stale.yml's ref is declared as an exception owned by objectui#8126 rather than edited, so this gate is green on a tree it did not break without pre-empting what that card writes there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr
…merge `dependabot-merge-gate.test.ts` requires the three buckets to partition every check a pull request produces, so a new gate has to be classified by a human instead of quietly shrinking the set the gate waits for. Action Ref Convention is a blocking check whose workflow subscribes `pull_request` with no trigger-level path filter, which is exactly what REQUIRED_CONTEXTS holds. Listing it there also puts it inside the `merge_group` subscription floor that `merge-queue-reporting.test.ts` derives from the same list -- the workflow already subscribes. It earns the slot rather than inheriting it: this gate reads .github/workflows/**, which is what a Dependabot `github-actions` bump edits, and those are the pull requests least likely to have a human reading each edited line. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr
|
LANDED — Landing probe, on a re-fetched
|
| probe | expect | read |
|---|---|---|
the three new files present (git ls-tree) |
present | ✔ gate, workflow, test |
DECLARED_EXCEPTIONS in the gate |
> 0 | 7 |
actions/stale named in the exception |
> 0 | 3 |
8126 — the owning issue recorded in the entry |
> 0 | 5 |
Action Ref Convention in dependabot-merge-gate.mjs |
> 0 | 2 |
⭐ negative that must NOT be zero — stale.yml still carries the original SHA, i.e. ⛔ no reference was changed |
1 | 1 |
⭐ firing control — uses: in the new workflow |
> 0 | 3 |
⭐ nonsense control — zzq-not-a-real-token-8465 |
0 | 0 (exit 1, a real no-match) |
⭐ The stale.yml row is the probe that matters most on this card: triage forbade changing any reference in either direction, and the tree confirms the SHA pin is still exactly where it was. ⇒ The odd one out is now declared, ⛔ not silently normalised away.
What is now on main
The repository states, for the first time, which spelling it uses for action references and how an exception is marked — and the statement is checkable rather than only written, which is what this card's finding actually asked for: "no convention that would have made the odd one out visible". A sentence states a default; only something that reads the workflows makes the next odd one visible.
The gate reds three ways — an undeclared off-convention ref, a stale exception entry, and its own census collapsing (floors plus a positive control asserted in the same run, because an empty offender list renders identically to a clean repo) — and it checks spelling, ⛔ not resolvability, and says so.
⚠️ Standing consequence for objectui#8126 — repeated here because it is now live on main
The exception table is self-cleaning. When objectui#8126 changes stale.yml's reference to a tag, this gate goes RED on that PR until the now-unused entry is deleted — a one-line deletion, and the failure text says exactly that. ⇒ Deliberate: triage required that "无论 #8126 最终写成什么,理由要留在树上", and an entry nobody is forced to look at would not satisfy it. This will be carried into #8126's dispatch when its retriage is answered.
Generated by Claude Code
Fixes #8465
Writes down the convention this repository uses for action references, and makes it checkable. No action reference is changed by this PR, in either direction.
The re-derived census, with its control
Re-measured on a re-fetched
origin/mainat96cf3d2acdb77afd3667c23c63e3c7490b04529b(the card measured1706d8be4; the tree has moved since). Enumeration and reading both come from the same checkout of that commit..github/workflows/**grep -cP '@[0-9a-f]{40}$'over the list)actions/stale@e00e804f...actions/checkout@v7uses:occurrences / workflow filesThe card's and triage's numbers both reproduce exactly. Triage's noise warning also reproduces: the
changesets/action@v1with a trailing backtick sits inside a comment, and both the census and the gate strip comment lines before scanning, so it is not a 14th reference.What landed, and why there
.github/workflows/action-ref-convention.yml— the convention, stated in the workflow header. This is triage's "a README or comment under.github/workflows/", in the form this repository already uses for exactly this purpose (control-bytes.ymlandchangeset-guard.ymlboth carry their rationale as a header at length).scripts/check-action-ref-convention.mjs— the gate, so the convention is enforced rather than prose that drifts.content/docs/guide/ci-cd-pipeline.md— a section plus an inventory row. Not optional:scripts/__tests__/ci-cd-pipeline-doc.test.tsfails when a workflow has no heading naming its file.scripts/dependabot-merge-gate.mjs— the new check classified intoREQUIRED_CONTEXTS, becausedependabot-merge-gate.test.tsrequires the three buckets to partition every check a PR produces.A
.github/workflows/README.mdwas considered and rejected. objectui#3724 deleted.github/WORKFLOWS.mdfor being hand-maintained, linked from nowhere and pinned by nothing, andci-cd-pipeline-doc.test.tsstill asserts it stays deleted with the note that "a new inventory under a different name evades it". A fresh unlinked doc under.github/is that same shape.AGENTS.mdwas never a candidate: it is governed surface.The convention
Default spelling: a floating major tag,
owner/action@vN.An exception is marked by an entry in
DECLARED_EXCEPTIONS, carrying the workflow file, the action path, the issue that owns the decision, and a real reason. That is the only form an exception takes. A trailing# v9.0.0-style comment beside a ref is a version hint, not a reason, and the gate does not read it as one — which matters, because the ref at the centre of this card already has one.The exception mechanism is the point, not the default. The defect was an unmarked, unverified exception; a convention that names a default but gives an exception no visible form leaves the same hole.
Why the default is the tag, and what that does not mean
Because that is what the tree already was: 12 of 13, and the repository's one deliberately reasoned per-ref pin —
changesets/action@v1, held byscripts/__tests__/changeset-release-action-ref-pin.test.tsagainst two properties of v1's source — is itself a tag. Declaring it moves no reference and closes no door.This is not a ruling against SHA pinning. SHA pinning is normally the more secure spelling and supply-chain guidance recommends it. Per triage, moving this repository's pinning posture wholesale is a supply-chain decision that belongs on its own card, and the reverse — quietly turning the SHA into a tag — is equally out of bounds. The gate's failure text says both, so a future reader cannot mistake the default for a verdict.
What the gate deliberately does not do
It checks spelling, not resolvability. A SHA pointing at no commit and one pointing at a real commit are indistinguishable without the network. That is the honest boundary: what failed in #8465 was that the odd ref had no reason attached and no second instance to compare against, and both of those are checkable offline.
Interaction with #8126 — please read before that card lands
stale.ymlis not touched by this PR. Its ref is recorded as a declared exception owned by objectui#8126, so this gate is green on a tree it did not break without pre-empting what that card writes there.That entry carries a staleness check, like every other escape hatch in this repository (
KNOWN_OFFENDERS,DOCUMENTATION_EXEMPT,MUST_SUBSCRIBE_MERGE_GROUP). Consequence, stated plainly: if #8126 changes that ref to a tag, this gate goes red on that PR until the now-unused entry is deleted — a one-line deletion, with the failure text saying exactly that. That is deliberate. Triage's requirement was that whatever #8126 writes, the reason survives in the tree; an entry nobody is forced to look at would not satisfy it.Evidence
Ablation — the gate can fail. Mutation proved on disk before the run, restore proved by state, never by an exit code:
The mutation script carried a
trap ... EXIT INT TERMrestore with absolute paths throughout.Tests. All figures below are on the final commit.
lint:rootis the scan that coversscripts/; it ran over its full population rather than a narrowed subset, so no narrowing claim is being made. ESLint here is not type-aware (noparserOptions.project/projectServiceineslint.config.js).The new suite is 16 tests, including four that assert the gate's red branches fire, one that reproduces the census with a second independent parser and requires the two to agree, and one that asserts prose inside comments is never counted as a reference.
Note:
Bundle Analysisis red on every PR that triggers it right now — objectui#8541, not this PR, and not a required check.🤖 Generated with Claude Code
https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr
Generated by Claude Code