mirror of
https://github.com/Comfy-Org/github-workflows.git
synced 2026-11-05 00:17:37 +00:00
[PR #101] [MERGED] feat(groom): track the pinned Claude Code CLI version in a Dependabot-visible manifest (BE-5373) #111
Labels
No labels
cursor-review-catalog-drift
groom
groom
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Comfy-Org/github-workflows#111
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
📋 Pull Request Information
Original PR: https://github.com/Comfy-Org/github-workflows/pull/101
Author: @mattmillerai
Created: 7/31/2026
Status: ✅ Merged
Merged: 8/4/2026
Merged by: @mattmillerai
Base:
main← Head:matt/be-5373-claude-code-version-pin📝 Commits (6)
8550dccfeat(groom): track the pinned Claude Code CLI version in a Dependabot-visible manifest (BE-5373)c714a84fix(groom): harden the CLI-pin gate and its guards (BE-5373)36289dffix(groom): read the CLI pin from the workflow's own commit, tighten the guards (BE-5373)78331badocs(groom): scope the install-step pin comment to the top-level version (BE-5373)b50da95docs(groom): document the CLI pin's accepted boundary and guard its dependency shape (BE-5582) (#104)efe5212Merge remote-tracking branch 'origin/main' into matt/be-5373-claude-code-version-pin📊 Changes
7 files changed (+1281 additions, -16 deletions)
View changed files
📝
.github/dependabot.yml(+35 -0)📝
.github/groom/README.md(+180 -0)➕
.github/groom/package.json(+8 -0)➕
.github/groom/tests/test_claude_code_pin.py(+835 -0)📝
.github/workflows/groom.yml(+177 -5)📝
.github/workflows/test-groom-scripts.yml(+41 -10)📝
AGENTS.md(+5 -1)📄 Description
ELI-5
The groom workflow installs the Claude Code CLI at a specific version, and that version number was typed into the workflow file three separate times. Nothing anywhere checked whether it was out of date, so it just quietly aged — and if someone bumped it by hand they could easily update two of the three and leave the workflow in a split state. This moves the version into a tiny
package.json, which is a file format Dependabot actually knows how to read, so it will now open a PR when a newer CLI ships. The workflow reads the version out of that one file at run time and hands it to all three install steps, so there is only one number to change and it can never disagree with itself.What changed
.github/groom/package.json(new) — the single source of truth for the@anthropic-ai/claude-codepin, currently2.1.217(unchanged from what the workflow installed before). It is a pin carrier, not a project: no lockfile, nothing is evernpm installed from that directory, and no CI job runs there. It exists purely so Dependabot's npm ecosystem can see the version..github/workflows/groom.yml— thegatejob (which already checks out this repo's groom assets) reads the pin once, validates it, and exports it as aclaude_code_versionjob output. All threeInstall Claude Codesteps (audit_find,audit_verify,build) consume that output vianeeds.gate.outputs— every one of those jobs alreadyneeds: gate, so no new job edges. No version literal remains in the workflow..github/dependabot.yml— annpmentry fordirectory: "/.github/groom"matching the existing monthly schedule,labels: ["dependencies"], and cooldown settings, plus an explicitversioning-strategy: "increase"..github/groom/tests/test_claude_code_pin.py(new) — guards the arrangement: exact pin, no hardcoded version literal anywhere ingroom.yml, every install step wired to the gate output, every CLI-installing jobneeds: gate, and the Dependabot entry still present..github/workflows/test-groom-scripts.yml— the path filter now also covers.github/workflows/groom.ymland.github/dependabot.yml. Without this the new guard would never run on the very changes it guards (a re-added literal ingroom.yml, or the npm entry being dropped).AGENTS.md/.github/groom/README.md— document the manifest and the two rules around it.Why a manifest instead of a workflow
env:constantThe ticket allowed either. A manifest wins because the
env:-constant variant needs a second file for Dependabot to bump plus a CI assertion that the two agree — i.e. it keeps two copies of the version and adds machinery to keep them equal. Reading the manifest at run time was straightforward here because all three install jobs already do anactions/checkoutof this repo atinputs.workflows_refinto$GROOM_ASSETS(that is how the briefs andledger.pyare loaded), so the manifest is already on disk before the install step runs. One file, one number.The exact-pin guard, and why it fails loudly
The gate rejects anything that is not an exact
X.Y.Z(optionally with a prerelease/build suffix). The agent CLI is executable supply chain for steps that read untrusted repo content, so a^/~/wildcard/dist-tag requirement would silently un-pin the thing the pin exists to protect.This is the riskiest line in the diff, because a false rejection would break groom for every live caller at once. Three things bound that:
versioning-strategy: "increase"is set explicitly, so Dependabot rewrites an exact requirement to a new exact requirement rather than widening it into a range.@anthropic-ai/claude-codeon the registry match it, zero rejected. Spot cases:2.1.217,2.1.218-beta.1,1.0.0+b5,1.0.0-rc.1+b5(accepted);^2.1.217,~2.1.0,*,latest,2.1.x,>=2.0.0,"",1.2.3-a..b(rejected).package.jsonmatches the.github/groom/**path filter, sotest_claude_code_pin.pyruns on that PR and fails it before merge. I confirmed the guard fails as intended by temporarily setting the pin to^2.1.217and re-adding a hardcoded literal togroom.yml— 3 tests failed — then restoring.The guard is the last step in
gateand runs only whenshould_run == 'true'— i.e. only on ticks that would actually install the CLI. It first ran eagerly, ahead of the interval/volume gates; the review panel pushed back and was right. It is the one fail-closed step in a job whose every other step fails open, and a scheduled caller skips ~6 of 7 daily ticks, so running it eagerly let a broken manifest (say, at a staleworkflows_ref) red out ticks that were never going to install anything.Nothing is lost by deferring it: all six consumer jobs are themselves
if: needs.gate.outputs.should_run == 'true', so the empty output on a skipped tick is unobservable, and a bad pin cannot reachmainin the first place —test_claude_code_pin.pyruns on any PR touching.github/groom/**, which includespackage.jsonitself. This step is the backstop for a stale ref, not the primary guard.Verification
python3 -m unittest discover -s .github/groom/tests -p 'test_*.py'— 178 passed (8 new).python3 -m unittest discover -s .github/cursor-review/tests …and… agents-md-integrity/tests …— 40 and 18 passed.python3 .github/agents-md-integrity/check_agents_md.py --root .— passed (1 pre-existing CODEOWNERS warning). AGENTS.md is 146 lines.actionlintclean on every workflow. This is the substantive check on the part I could not run for real: actionlint statically resolvesneeds.<job>.outputs.<name>against declared outputs. I proved it is a real check by injecting a typo'd output reference and a bad step id — it flagged both at the exact lines — then restoring.jqread → validation →$GITHUB_OUTPUT);shellcheckclean; confirmed the${VAR:?…}guard exits non-zero with a legible message when the variable is unset or empty.groom.yml,dependabot.yml,test-groom-scripts.yml; JSON parse ofpackage.json.Unmet acceptance criterion — please read
The ticket's step 4 ("verify a groom run still installs and executes the CLI via
workflow_dispatchonci-groom.yml") is NOT done, and is not currently doable by anyone.ci-groom.ymlonmainstill carries the literal placeholderREPLACE_AT_MERGE_WITH_THIS_PRS_SQUASH_SHAin both itsuses:ref and itsworkflows_ref:input — the BE-4004 bump-at-merge step was never performed. Every scheduled run has failed since the workflow was added: the oldest run in history (2026-07-21) is astartup_failureand the five since are allfailure, including today's. So this repo's own groom caller has never had a green run, before or after this change.I did not fix that here. Bumping those two refs means choosing a SHA and thereby switching on a live daily agent sweep that files issues on a public repo — a deliberate operator action with real spend, not a side effect of a pin refactor. It is also a bump-at-merge by nature: the correct value is this PR's squash SHA, which does not exist yet.
Consequence for review: the end-to-end "the CLI actually installs at the resolved version" leg is unproven. What is proven is the wiring (actionlint), the resolution logic (simulated against the real manifest), and the guards (unit tests, demonstrated failing on regression). The other four callers on the
GROOM_CALLERSroster pick this change up through the normalbump-groom-callersflow after merge, and the first of those runs is the real end-to-end confirmation.Judgment calls
package-lock.json. Dependabot's npm ecosystem resolves a manifest-only directory fine, and nothing installs from it, so a lockfile would be a second artifact to keep in sync for no benefit. The ticket allowed one "if the npm ecosystem needs one" — it does not.groom.yml's "Verified empirically against the pinned 2.1.217" comment alone. That is a historical record of which CLI version the agent sandbox rules were validated against (the rule surface is version-specific), not a pin. It is useful to whoever bumps next, so removing it would delete information.2.1.217value. This PR changes where the version lives, not what it is.Review round 1 — resolved (
c714a84)All ten panel findings addressed; every thread replied to and resolved. Seven produced code changes, three were answered without one.
Fixed in
groom.yml's gate step: the eager-fail ordering above;?on the jq index so adependenciesthat is present but not an object reaches the legible error instead of dying on a raw jq "cannot index" underset -e; newlines stripped from the version before it is echoed into a pre-validation::error::(it is unvalidated JSON at that point, and an embedded newline could emit further workflow commands into the log); and the guard tightened to strict SemVer, since the old suffix match accepted1.2.3-a..b, which npm resolves as a mutable dist-tag.Fixed in
test_claude_code_pin.py:fullmatchinstead ofassertRegex; job-boundary regex widened to allow-and uppercase (both legal job IDs);needs:parsed rather than substring-matched, so the block-sequence form andneeds: [audit_find, gate]stop false-failing; and the Dependabot keys checked against one parsed entry instead of three independent substring matches over the whole file.Found while verifying that last set, not raised by the panel:
test_every_install_job_depends_on_the_gatewas passing vacuously on thegatejob itself.gate's prose contains both the stringnpm install -g <pkg>@<ver>(explaining why the pin is not inline) and the wordsneeds: gate(explaining why it resolves there), so the substring scan saw a phantom install step and then "proved" the job depends on itself. Comment lines are now stripped before scanning, and the set of installing jobs is asserted explicitly so a silent drop to zero matches cannot pass.Answered without a code change: the
$GROOM_ASSETS-unbound finding does not reproduce (it is defined at workflow level,groom.yml:371, and points one level deeper than the checkoutpath:, so the suggested literal would be the wrong path); the "nodependencieskey" jq repro does not reproduce either (jq permits string-indexingnull) though the adjacent non-object case was real and is fixed; and theworkflows_refsplit-pin concern is the repo's existing, documented trust model rather than something this PR introduces —config.py,interval.py,ledger.pyand the three agent briefs already travel at that same ref, so a version number read from it is strictly less powerful than the prompts and Python already loaded from it. No follow-up tickets filed.Re-verified after the changes: 178 groom / 40 cursor-review / 18 agents-md tests pass,
actionlintclean across every workflow (and confirmed still load-bearing — a typo'd step id in the newif:is flagged at the exact line), AGENTS.md integrity passes, and the gate shell was re-simulated against the real manifest plus the hostile ones from the findings.Unchanged: the "Unmet acceptance criterion" section above still stands.
ci-groom.ymlonmainstill carries theREPLACE_AT_MERGE_WITH_THIS_PRS_SQUASH_SHAplaceholder, so the end-to-end "the CLI actually installs at the resolved version" leg remains unproven by anyone.🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.
external(BE-6715) #150