fix(ci): the lockfile-dedupe gate reports on pull requests instead of blocking - #9707
Conversation
… blocking `check-lockfile-dedupe.mjs` takes a LIVE registry reading (`pnpm dedupe --check`), and that reading was measured disagreeing with itself: four green and one red on a byte-identical `pnpm-lock.yaml` inside ninety minutes, the red printing a confident `VERDICT not deduped` that no lockfile edit and no registry publish explained — and then instructing the reader to commit a dedupe to a file every open pull request shares. Per the maintainer's ruling (letter A): - the script gains one explicit flag, `--report-only`, which keeps every reading exactly as it is and changes only the consequence: annotations become `::warning::`, the same reading is written to `$GITHUB_STEP_SUMMARY`, and the process exits 0. `lockfile-dedupe.yml` passes it; - the bare script keeps its 0/1/2 exit codes for hand runs; - the finding text, in BOTH modes, now names the instrument — re-run before acting, dedupe only when the split reproduces — and the old unconditional instruction to commit a dedupe is gone from both. The check keeps its name, its path filter and its `OPTIONAL_CONTEXTS` classification, so nothing in branch protection or the merge queue moves. The relaxation is scoped to the VERDICT: a failed checkout, a broken pnpm setup or a `--self-test` that stops passing still reds this context. Report-only may never flatten the three answers into two. `clean` and `cannot-run` both exit 0 under the flag, so the exit code no longer separates them; the annotation does, and the tests read those two runs against each other rather than in isolation. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015h79niBMyoB1xcaQje3uiz
ACCEPT — in-seat review,
|
| check | result |
|---|---|
Clause-② |
✅ yes at line start, ruling id cited — ⛔ not softened to no because the diff looks small |
| Commit message | ✅ no model identifier; Co-authored-by: Claude <noreply@anthropic.com> + Claude-Session:; ⛔ no card trailer |
| Commit author | ✅ claude — ⛔ not a user account |
| Body line 0 | ✅ Fixes #9562 |
dependabot-merge-gate.mjs |
✅ untouched — the #9584 fence respected |
| New/renamed context | ✅ none; only functional line is run: node scripts/check-lockfile-dedupe.mjs --report-only |
| Changeset | ✅ none owed, by the gate's own verdict line |
| Ablation | ✅ four legs, each proven landed on disk and restored by blob-hash equality — ⛔ not by an exit code |
⭐ Lockfile Dedupe Check reports success on this very PR, because the diff touches its own runtime closure, which the path filter deliberately lists. The gate ran on its own change — that filter design earning its keep.
⚠️ Recorded, ⛔ not held against the PR
Your durability note on package.json's bare check:lockfile-dedupe: nothing pins that it must stay bare, so a future author could add the flag there and quietly remove the only place the 0/1/2 contract is still reachable. ⭐ Correctly kept as an Acceptance note rather than filed — it is a durability observation, not a defect. I am carrying it into the landing record so it is not lost with the PR.
Release
Flipping to ready, re-waiting for what the flip registers (measured on eleven occasions, two of them not this seat's), re-reading green, then arming. ⛔ No hand-merge — the queue rebases onto main itself.
Generated by Claude Code
Fixes #9562
Clause-②: yes
Authorised by the maintainer's ruling on the card (letter A, comment 5717182406, 2026-09-17T15:43Z, Director seat summon #24). This relaxes a blocking gate, which is normally a human floor; the authority exists, is named, and moved the card
needs-user-decision->pm:queue.What was wrong
scripts/check-lockfile-dedupe.mjstakes a LIVE registry reading (pnpm dedupe --check). objectui#9562 measured that reading disagreeing with itself: four green and one red on a byte-identicalpnpm-lock.yamlinside ninety minutes, same blob at every ref, no lockfile edit anywhere and no registry publish that day. The red printed a confidentVERDICT not dedupednaming anesbuildpeer split — and then told the reader to fix it here, by committing a dedupe, to a file every open pull request shares. The mechanism was never identified and this change does not adopt one.PR #9690 took the live reading out of the required test lane. This is the remainder the maintainer ruled on: the
Lockfile Dedupe Checkcontext itself.What this does
scripts/check-lockfile-dedupe.mjsgains one explicit flag,--report-only. It keeps every reading exactly as it is and changes only the consequence: annotations become::warning::, the same reading is appended to$GITHUB_STEP_SUMMARY, and the process exits 0..github/workflows/lockfile-dedupe.ymlpasses it. That one line is where the ruling lands.pnpm check:lockfile-dedupestill is. The mode is never inferred fromCIorGITHUB_ACTIONS— only from its own flag.pnpm dedupeand committing the lockfile" is gone from both modes — it is the sentence the card reported.What deliberately did NOT change
OPTIONAL_CONTEXTSclassification, so nothing in branch protection or the merge queue moves. No new workflow, no new or renamed context.ci-setup-pnpm.sh, or a--self-testthat stops passing still reds this context and still stops a Dependabot auto-merge. That is why the classification stays live rather than vestigial.cleanandcannot-runstay distinguishable. Both exit 0 under the flag, so the exit code no longer separates them; the annotation does, andcannot-runcarries one precisely because of that. A report-only mode that cannot say "I could not look" would be worse than the gate it replaces.pnpm-lock.yamlis untouched, and the live smoke run below confirms--checkstill does not write it.Verification
All runs under the shared verify lock, worktree
objectui-issue-9562-b, at4814064a1.Targeted tests —
npx vitest run --project unitover the six test files that read the workflow set (check-lockfile-dedupe,dependabot-merge-gate,ci-cd-pipeline-doc,check-lockfile-integrity,check-merge-queue-head,merge-queue-reporting): 212 passed (212),VERDICT command-exit 0. The dedupe file alone: 20 passed (16 before).node scripts/check-lockfile-dedupe.mjs --self-test: 32 cases pass (20 before).Reverse verification — four one-off legs, each mutated on disk, proven landed by a before/after occurrence count on the probed file itself, run, then restored and proven restored by
git hash-objectagainst the HEAD blob (never by an exit code):--report-only--report-only...cannot-runloses its annotation--self-test)Baseline before the legs: 20 passed. Final state after:
git status --porcelainempty on both files, both blob hashes equal to HEAD's.Live smoke, both modes (this tree is deduped today, so this exercises the green path end to end and not the red one): bare
node scripts/check-lockfile-dedupe.mjs->VERDICT deduped, exit 0;GITHUB_STEP_SUMMARY=... node scripts/check-lockfile-dedupe.mjs --report-only->VERDICT deduped, exit 0, 0 bytes written to the step summary (a clean tree writes nothing; the block itself is the signal).pnpm-lock.yamlsha256 identical before and after both runs.Repo gates, all exit 0:
check:control-bytes(7812 tracked text files),check:action-ref-convention(121 refs / 39 workflows, controlactions/checkoutpresent),check:required-check-set(32 cases),check:new-line-citations,check:shell-escape-residue,check:comment-mask-corpus(5046 files, residue within the held-open ceiling),check:test-path-roots,check:lockfile-integrity(VERDICT clean).Lint — targeted rather than repo-wide, and the narrowing is measured, not assumed:
npx eslintover the two changed JS/TS files,--format json, 2 files linted, 0 errors, 0 warnings; the flat config declares noproject/projectService, so type-aware linting is off and this diff cannot move the verdict on any file it does not touch. The.ymlfile is outside eslint's population. The repo-wide run is CI's.Changeset — the gate's own verdict line on this range: "No source or published contract of a released package changed in this range, so no changeset is owed." (3 files changed, 0 published source, 0 moved manifests, 0 changesets). So none is added, and no label is applied.
Acceptance notes
The fence held — measured, not assumed.
scripts/dependabot-merge-gate.mjswas not touched, and does not need to be:dependabot-merge-gate.test.tspasses unchanged. Its assertions over this name are bucket membership (OPTIONAL_CONTEXTSyes,NOT_A_GATEno,REQUIRED_CONTEXTSno), the path-filter shape, and a reason string longer than 40 characters. Nothing there asserts that a blocking-bucket name can actually fail, so the partition stays honest with the classification unchanged.The dispatching seat's least-certain claim is therefore confirmed, with one correction worth recording: its entry's words "Blocking when it runs" are now imprecise rather than false. The job still blocks when it fails — a broken checkout, pnpm setup or self-test — and only the dedupe verdict stopped blocking.
lockfile-dedupe.yml's header now says so and points at that entry, so the next reader of either file is not misled. Correcting the sentence in the held file is left for whoever lands PR #9584.Independent re-measurement of the hold, since it was offered for falsification: 11 open pull requests, fully paginated, 1795 filenames; 0 of them hold any of the three files changed here; positive control
scripts/dependabot-merge-gate.mjs -> #9584discriminates.noted, not filed:
package.json'scheck:lockfile-dedupescript stays the bare form on purpose — the ruling reserves the hard verdict for hand runs — but nothing pins that intent, so a future author could add the flag there and quietly remove the only place the 0/1/2 contract is still reachable. Prospective author: whoever next edits this gate; it is a durability observation, not a defect, so it is recorded here rather than filed.Generated by Claude Code