mirror of
https://github.com/Comfy-Org/github-workflows.git
synced 2026-11-05 00:17:37 +00:00
[PR #275] [MERGED] feat(groom): optional environment input on the three token-minting jobs #276
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#276
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/275
Author: @mattmillerai
Created: 9/8/2026
Status: ✅ Merged
Merged: 9/9/2026
Merged by: @mattmillerai
Base:
main← Head:matt/be-12540-groom-environment-input📝 Commits (3)
df9650dfeat(groom): optionalenvironmentinput on the three token-minting jobsbef39edfix(groom): gate the environment binding on bot_app_id and harden its guard519036edocs(groom): stop asserting the untested missing-environment-secret case📊 Changes
4 files changed (+282 additions, -1 deletions)
View changed files
➕
.github/groom/tests/test_environment_binding.py(+156 -0)📝
.github/workflows/groom.yml(+76 -0)📝
README.md(+1 -1)📝
docs/callers/groom.md(+49 -0)📄 Description
ELI-5
groom.ymlmints a bot App token in three of its jobs, and today the private key that lets it do that has to be a repository secret — readable by a workflow on any branch. This adds an optionalenvironmentinput so a caller can say "put those three jobs inside mybot-mainenvironment", and then keep the key as an environment secret behind a deployment-branch policy instead. Leave it unset (the default) and absolutely nothing changes.What changed
.github/workflows/groom.yml— new optional inputenvironment(type: string,required: false,default: ''), declared next tobot_app_id, and bound asenvironment: ${{ inputs.environment }}on exactly three jobs:build_select,fileandbuild_pr. Those are the only three that readsecrets.BOT_APP_PRIVATE_KEY. The header's caller-pattern comment gains a commentedenvironment: bot-mainline..github/groom/tests/test_environment_binding.py(new) — a guard for the two properties that regress silently: the default really is'', and the binding set is exactly the token-minting set. See "Beyond the plan" below.docs/callers/groom.md— anenvironmentrow in the inputs table plus a "Scoping the bot key to an environment" section covering the same-name requirement and how the caller passes the value.README.md— one clause on thegroom.ymlcatalog row.Review round (second commit)
bot_app_id:${{ inputs.bot_app_id != '' && inputs.environment || '' }}on all three jobs.build_selectandfilemint the token underif: inputs.bot_app_id != '', so with no App configured there is no credential for an environment to guard, and binding one anyway parked those jobs behind protection rules for nothing — a denying rule would have dropped the run's findings after the audit was already billed.build_prmints unconditionally and keeps the identical expression for uniformity; it is reachable in builder mode only, wherebuild_selectalready rejects an emptybot_app_id, and the guard now asserts that dependency. This is a deliberate narrowing, not a pure no-op: settingenvironmentwith nobot_app_idpreviously bound anyway.${{ secrets.BOT_APP_PRIVATE_KEY }}and^ environment:at exactly four spaces, so a new credentialed job spelling the secret${{secrets.…}}or aliasing it through a job-levelenv:fell out of the minting set and the bound set at once while everyassertEqualstayed green. Nowsecrets\s*\.\s*BOT_APP_PRIVATE_KEYand^\s+environment\s*:, scanned over thejobs:section so the loosened indentation does not also match the input declaration.docs/callers/groom.md: dedicated environment only (binding injects all of its secrets into jobs including the patch-applyingbuild_pr); deployment-branch policies only (a required-reviewer or wait-timer rule pausesbuild_select, andcancel-in-progress: falsethen parks every later tick behind it —build_prraises one deployment per finding); create the environment and its policy first (a referenced-but-missing environment is auto-created with no rules, so a typo fails open); and cover every branch groom dispatches from, since a denied deployment loses the findings yet still advances the cadence clock.BOT_APP_PRIVATE_KEYresolving to''afterwards is the supported end state rather than a break.The security boundary this deliberately does not cross
audit_find,audit_verifyandbuildrun a model over untrusted repository content and get no binding. Putting one of them inside a credentialed environment would hand an environment's secrets to the job that reads attacker-influenceable code — the exact separation groom's split-job topology exists to hold.gategets none either (it mints no token). Nodeployment: false, keeping parity with the directly-gated jobs on the consumer side.Why an empty environment name is safe for existing callers
An empty
environment.namebinds no environment. I verified that claim against a live public precedent rather than taking it on faith:fluent/fluent-bit's.github/workflows/call-build-images.yamldeclaresenvironmentasrequired: falsewith no default and bindsenvironment: ${{ inputs.environment }}oncall-build-images-meta; its caller.github/workflows/pr-package-tests.yamlinvokes that reusable and never passes the input. Run29556928666(pr-package-tests.yaml, 2026-07-17) shows all four instances of that job concludingsuccess. Both files and the run were read back through the GitHub API for this PR, not quoted from the design note.On top of that, no existing caller can pass the input — it did not exist until this commit — so the pin-bump PRs that
bump-groom-callers.ymlopens after this merges need no other change.Beyond the plan
The plan asked only that the existing suites keep passing. I added
test_environment_binding.pybecause the two failure modes here are invisible in review of a 3000-line workflow: a hardcoded environment name would break every caller at startup with no local signal, and anenvironment:line added to an agent job is one green line in a diff. It pins the input'sdefault: '', that the minting set is exactly{build_select, file, build_pr}, that no agent job binds anything, that no other job does either, and that every binding is the input expression rather than a literal. Text-matched rather than parsed, matchingtest_interval.py's existing literal-pin idiom (this repo is stdlib-only — no PyYAML). I mutation-tested it: bindingaudit_verify, hardcodingbot-main, dropping the binding fromfile, and changing the default each turn it red.Verification
python3 -m unittest discover -s .github/groom/tests -p 'test_*.py'— 379 tests, OK (1 skipped); 374 before this change, +5 new.python3 -m unittest discover -s .github/workflow-pins/tests -p 'test_*.py'+python3 .github/workflow-pins/check_workflow_pins.py— 316 tests OK; lint OK, 11 workflows checked.actionlint .github/workflows/groom.yml— 16 findings, byte-identical in count and kind toorigin/main's copy of the same file (all pre-existingjob.workflow_shafalse positives actionlint does not know about). No new finding.zizmor --persona=regular .github/workflows/groom.yml— 8 findings (5 suppressed), same asorigin/main's copy. No new finding.python3 .github/agents-md-integrity/check_agents_md.py --root .— passed, 2 pre-existing warnings (AGENTS.md line count, no CODEOWNERS), untouched by this change.Residual
environment:from the caller — is a separate follow-up, and until it lands the repository-level bot key is still what every run reads. That follow-up is what would let the repository secret be deleted.workflow_dispatchadry_run: truegroom on a caller pinned at this branch.environment:on an environment that carries required reviewers, all three credentialed jobs will wait for manual approval — the issue-filing job included, on the default finds-only path. That is inherent to environment protection rules and not something this input can opt out of. It is now called out — in the input description, the README row and the caller docs — together with the consequence review surfaced that is worse than the wait: a pausedbuild_selectholds thegroom-<repo>concurrency group undercancel-in-progress: false, so every later daily tick queues behind it and is cancelled, stopping groom for that repo with no failure and no log. Documented rather than enforced: a reusable workflow cannot see the caller's environment protection rules.environmentin the reusable workflow at the job level, the environment secret will be used, and not the secret passed from the caller workflow." That quote is now carried in the input description and the caller docs. What remains unasserted is the name match: a differently-named environment secret fails silently, leaving the passed value in place. Nothing in this repo can detect that misconfiguration on the caller's behalf; the docs are the only guard.groom.yml(#261,fix(groom): bound finder exploration and spend) edits only theaudit_findjob — disjoint from every hunk here.Provenance
unittest: 380 passed, 0 failed (1 skipped); workflow-pinsunittest: 316 passed, 0 failed;check_workflow_pins.py: OK, 11 workflows, 0 violations;actionlinton groom.yml: 16 findings, count and kind identical toorigin/main's copy (all pre-existingjob.workflow_shafalse positives);zizmor --persona=regularon groom.yml: 8 findings (5 suppressed), identical to base;check_agents_md.py: passed with 2 pre-existing warnings. The hardened guard was differentially mutation-tested: a new credentialed job spelling the key${{secrets.BOT_APP_PRIVATE_KEY}}through a job-levelenv:and binding nothing passes the previous test and fails the current one; binding an agent job at four and six spaces, hardcoding an environment name, dropping a binding, changing the default, dropping thebot_app_idguard, ungating thefilemint step, and removingbuild_select's builder/bot_app_idvalidation each turn it red.test_environment_binding.py(described under "Beyond the plan") and the review round above, whose one behaviour change — dropping the binding whenbot_app_idis unset — narrows the input relative to the original plan and is flagged as such.🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.