Skip to content

ci: declare the action reference convention and gate the odd one out - #8546

Merged
baozhoutao merged 2 commits into
mainfrom
claude/issue-8465-action-ref-convention
Sep 8, 2026
Merged

baozhoutao merged 2 commits into
mainfrom
claude/issue-8465-action-ref-convention

Conversation

@claude

@claude claude Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

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/main at 96cf3d2acdb77afd3667c23c63e3c7490b04529b (the card measured 1706d8be4; the tree has moved since). Enumeration and reading both come from the same checkout of that commit.

reading value
distinct action references in .github/workflows/** 13
of those, SHA-pinned (grep -cP '@[0-9a-f]{40}$' over the list) 1 — actions/stale@e00e804f...
positive control in the same run, actions/checkout@v7 found (count 1 in the distinct list)
total uses: occurrences / workflow files 107 / 34 before this PR, 109 / 35 after

The card's and triage's numbers both reproduce exactly. Triage's noise warning also reproduces: the changesets/action@v1 with 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.yml and changeset-guard.yml both 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.ts fails when a workflow has no heading naming its file.
  • scripts/dependabot-merge-gate.mjs — the new check classified into REQUIRED_CONTEXTS, because dependabot-merge-gate.test.ts requires the three buckets to partition every check a PR produces.

A .github/workflows/README.md was considered and rejected. objectui#3724 deleted .github/WORKFLOWS.md for being hand-maintained, linked from nowhere and pinned by nothing, and ci-cd-pipeline-doc.test.ts still 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.md was 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 by scripts/__tests__/changeset-release-action-ref-pin.test.ts against 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.yml is 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:

HEAD blob: 43f7cb1c39fe4e008be75601a7bcc941d8baf266
pre-mutation  'uses: actions/checkout@v7' occurrences: 1
post-mutation injected SHA occurrences : 1
post-mutation removed  tag occurrences : 0
blob now: 594d9e8d27e8c638a440cda4603d9825afe0de24   (differs from HEAD blob)
MUTATED_GATE_EXIT=1
  control-bytes.yml:69  actions/checkout@0123456789abcdef0123456789abcdef01234567   [sha]
RESTORE: blob after restore 43f7cb1c... matches HEAD blob YES
         git diff HEAD -- control-bytes.yml  =>  empty
RESTORED_GATE_EXIT=0

The mutation script carried a trap ... EXIT INT TERM restore with absolute paths throughout.

Tests. All figures below are on the final commit.

pnpm exec vitest run scripts/            122 files, 3651 tests passed
pnpm type-check:scripts                  exit 0
pnpm lint:root --format json             279 files, 0 errors, 32 warnings, exit 0
node scripts/check-action-ref-convention.mjs
  OK  109 action reference(s) in 35 workflow file(s), 13 distinct;
      0 off-convention and undeclared, 0 stale exception(s);
      control `actions/checkout` present: true
node scripts/check-control-bytes.mjs     exit 0 (6747 tracked text files)
node scripts/check-changeset-presence.mjs
  exit 0 — no source or published contract of a released package changed, so no changeset is owed
node scripts/check-governed-queue-guard.mjs --test (all changed paths)
  NOT GOVERNED — 5 paths checked against 5 governed surfaces; none matched

lint:root is the scan that covers scripts/; it ran over its full population rather than a narrowed subset, so no narrowing claim is being made. ESLint here is not type-aware (no parserOptions.project / projectService in eslint.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 Analysis is 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

claude Bot and others added 2 commits September 8, 2026 08:47
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
@baozhoutao
baozhoutao marked this pull request as ready for review September 8, 2026 09:43
@baozhoutao
baozhoutao added this pull request to the merge queue Sep 8, 2026
Merged via the queue into main with commit 3f775ee Sep 8, 2026
32 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-8465-action-ref-convention branch September 8, 2026 09:59
@claude

claude Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

LANDED — 3f775eeb8 on origin/main. domain:devx @ objectui execution seat, PM session session_01FhBNJcLRZLe8M87VcUgpKr, R47, 2026-09-08T10:18Z. PR #8546 (card objectui#8465), merged 09:59:51Z, first pass.

Landing probe, on a re-fetched origin/main (3f775eeb8)

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Only one action ref in this repository is SHA-pinned, and it is the only one that has never resolved

1 participant