Skip to content

fix(devx): classify shard-attestation jobs by invocation, not by co-occurring substrings (#6589) - #6707

Merged
os-project-manager merged 1 commit into
mainfrom
claude/issue-6589-shard-attestation-classifier
Aug 8, 2026
Merged

fix(devx): classify shard-attestation jobs by invocation, not by co-occurring substrings (#6589)#6707
os-project-manager merged 1 commit into
mainfrom
claude/issue-6589-shard-attestation-classifier

Conversation

@os-project-manager

@os-project-manager os-project-manager commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

Fixes #6589

The defect

The static drift guard in scripts/check-shard-attestation.mjs decided whether a job was an aggregate gate from two substrings CO-OCCURRING in the job's JOINED run: text:

const text = runTextOf(job);                                  // every step's run:, concatenated
if (!text.includes(SCRIPT_BASENAME) || !text.includes('--verify')) continue;

The problem is that every attesting shard job already contains check-shard-attestation.mjs — its own --emit step at the bottom. So the sole discriminator was the bare substring --verify appearing anywhere in any run: step of that job, including inside a shell comment, since comments are part of run:.

The same function carried a second instance of the family: "publishes an attestation that no gate counts" tested runTextOf(job).includes('--emit'), which does not even require the script's name.

Reproduction (both directions, same tree, only the script swapped)

Adding the idiomatic strict ref-existence check git rev-parse --verify to the test shard job, then running the same input against each version of the script.

origin/main's script:

### baseline
gates=2 legs=3 attesters=3 problems=0

### git rev-parse --verify in a shard step
gates=3 legs=3 attesters=3 problems=2
  • job 'test' is an aggregate gate but its job-level if: is "${{ !cancelled() && needs.filter.outputs.core != 'false' }}", not always() — a gate that skips publishes no required context (#3622).
  • gate 'test' never downloads the shard attestations it claims to count.

This PR's script, same input:

### baseline
gates=2 legs=3 attesters=3 problems=0

### git rev-parse --verify in a shard step
gates=2 legs=3 attesters=3 problems=0

The misleading diagnostic is the heart of the issue: neither message names the classifier or the flag. They say the shard job lacks two properties that only an aggregate gate owes. Acting on them means editing the shard job's if: — which breaks #3622 / #4928.

The fix: adjacency, not co-occurrence

New invokesScript(runText, flag): the flag must be an ARGUMENT of a command that runs this script. Underneath it is a small quote-aware lexer, shellCommands(), which

  • joins backslash-newline continuations into one command (both real invocations in ci.yml are written that way — flag on the first line, arguments on continuations);
  • drops shell comments, which is the step that makes the second-order trap impossible by construction;
  • honours quoting, so a # or a | inside an argument stays an argument instead of truncating the command — a false negative in that direction would quietly disable the gate, which is worse than the false positive this issue is about;
  • splits commands at ; & && | || and newline.

The same predicate replaces the two other substring tests in that function (emitsHere, and the --emit site above), so all three now ask one question. The --emit side was already scoped per STEP, which is why it never had this issue's bug; it is now scoped per INVOCATION, one notch tighter.

scanWorkflow() additionally returns gateIds / attesterIds, so the classification itself can be asserted rather than inferred from the absence of a problem — #6589's misclassification produced problems that named neither the classifier nor the flag, which is exactly why inferring is unreliable here.

Self-test (--self-test)

21 new assertions (92, up from 71), all named, all with guarded bodies — nothing stack-traces under ablation.

The mandatory regression fixtures — a shard job that spells the flag for an UNRELATED reason must still be classified as an attester:

  • git rev-parse --verify "$GITHUB_SHA^{commit}" (the exact edit that was measured red)
  • git show-ref \ plus a continuation line --verify refs/heads/main (the flag on ANOTHER program's continuation line)
  • a run: comment spelling the whole invocation — the second-order trap goes straight into a fixture, proving it is safe by construction

The positive limb — a real aggregate gate must still read as a gate: changing test-gate's check-shard-attestation.mjs --verify to --verify-nothing stops that job being a gate, and the required-context check goes red.

The --emit twin — another program's --emit in a job no gate counts (esbuild src/index.ts --emit) leaves the guard green.

A no-op guard on the fixtures themselves — every fixture anchor is a literal lifted out of ci.yml, and a String.replace that matches nothing returns the input unchanged. The green-expecting assertions would then pass permanently and silently for having judged an unmodified workflow. A no-op mutation is now a named failure of its own, and the label says which anchor died.

Reverse verification

Putting the deleted co-occurrence test back (text.includes(SCRIPT_BASENAME) && text.includes(…)) turns the self-test red with 8 named failures, whose text reproduces the two misleading diagnostics verbatim:

✗ check-shard-attestation --self-test -- 8 failure(s)

  • #6589 'git rev-parse --verify in a shard step': job 'test' is NOT reclassified as an aggregate gate
  • #6589 'git rev-parse --verify in a shard step': the guard stays green — got ["job 'test' is an aggregate gate but its job-level if: is ... not always() ... (#3622).","gate 'test' never downloads the shard attestations it claims to count."]
  • #6589 'the flag on another program's continuation line': job 'test' is NOT reclassified as an aggregate gate
  • #6589 'a comment documenting the invocation': job 'test' is NOT reclassified as an aggregate gate
  • #6589 positive limb: a job that no longer INVOKES the script with the flag stops being a gate
  • #6589 positive limb: losing the real gate invocation ⇒ red, naming the branch-protection contract

The two positive-limb assertions go red under the same ablation, and the reason is worth stating on its own: the old substring test could not even stop --verify-nothing — that string CONTAINS the substring, so test-gate still read as a gate in the ablated version. The old test could not tell a flag from another flag's prefix.

Ablating only the --emit site reds exactly one fixture, the twin:

✗ check-shard-attestation --self-test -- 1 failure(s)
  • #6589 twin: the guard stays green — got ["job 'bystander' publishes an attestation that no gate counts — declared but not enforced."]

Scope

scripts/check-shard-attestation.mjs only (implementation plus self-test fixtures). .github/workflows/ci.yml is byte-for-byte untouched: #6195's git cat-file -e workaround, and the comment next to it explaining why, both stay exactly as they are. Whether that step should return to the idiomatic spelling now that the classifier is fixed is a separate question and is not part of this PR.

No changeset: this is a repo-internal CI guard script that releases nothing, hence skip-changeset.


Generated by Claude Code

…ccurring substrings (#6589)

`scanWorkflow` decided a job was an aggregate gate from
`text.includes(SCRIPT_BASENAME) && text.includes('--verify')` over the job's
JOINED `run:` text. Every attesting shard job already contains the basename
(its own `--emit` step), so the sole discriminator was a bare `--verify`
substring anywhere in any step — a `git rev-parse --verify` reclassified job
`test` as a gate and produced two complaints about gate properties a shard job
does not owe, pointing the reader at the #3622/#4928 contracts to break.
Documenting the collision in a `run:` comment re-armed it (#4890's shape).

The classification now asks whether the flag is an ARGUMENT of a command that
runs this script (`invokesScript`), over a small quote-aware lexer that joins
backslash-newline continuations, drops shell comments and splits at `; & | &&
||`. The same predicate replaces the two other substring tests in the same
function (`emitsHere`, "publishes an attestation that no gate counts").

Self-test: +3 workflow fixtures asserting a shard job that spells the flag for
an unrelated reason stays an attester (including one whose `run:` comment
spells the whole invocation), the `--emit` twin, and the positive limb — a
gate that no longer invokes the script stops being a gate. `scanWorkflow` now
reports `gateIds`/`attesterIds` so the classification is asserted directly, and
`fixture()` fails by name when its ci.yml anchor goes stale instead of silently
judging the pristine workflow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F8q5J1MQyocgtNspb15fSn
@vercel

vercel Bot commented Aug 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 8, 2026 1:13pm

Request Review

@os-project-manager
os-project-manager marked this pull request as ready for review August 8, 2026 13:13
@github-actions github-actions Bot added the size/m label Aug 8, 2026
@os-project-manager os-project-manager added skip-changeset PR has no user-facing published change; bypasses the changeset gate and removed size/m labels Aug 8, 2026 — with Claude
@os-project-manager
os-project-manager added this pull request to the merge queue Aug 8, 2026
Merged via the queue into main with commit 4e271b2 Aug 8, 2026
27 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-6589-shard-attestation-classifier branch August 8, 2026 13:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

check-shard-attestation.mjs misclassifies a shard job as an aggregate gate on a bare --verify substring anywhere in its run: text (false red, measured)

2 participants