feat(selector): route same-repository Dependabot runs to the fleet - #103
Conversation
Dependabot-triggered CI was a reviewed hosted exception; the org owner approved retiring it so dependency-bump lanes run in ephemeral one-job workers instead of drawing the hosted included-minute reserve. Fork and public guards are untouched, the observer key stays confined to the selector job, and Dependabot events source it from the Dependabot secrets store, which the org now mirrors. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JLfjHY8PY6ozhgZPiuHKik
|
Claude finished @kyle-sexton's task in 1m 55s —— View job PR Review
SummaryThis PR removes the A few items worth discussing: Defense-in-depth:
|
Step 2 of the selector revision lockstep requires a review note alongside each new policy.json allowlist entry; the initial commit added the 3931f91 allowlist entry and test pin but not its README note, flagged by chatgpt-codex-connector[bot] (P2). Source facts from merged melodic-software/ci-workflows#103. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…117) ## Why melodic-software/ci-workflows#103 (merged) retires the selector's Dependabot hosted-only guard — owner-approved — so same-repo bump PRs route to the ephemeral fleet instead of drawing the hosted included-minute reserve. Consumers can only adopt the new selector once its exact reference is reviewed in the owner-scoped allowlist. ## What - `policy.json`: add `select-runner.yml@3931f91` to the melodic-software owner-scoped selector allowlist (staged-set pattern; prior revisions remain until consumers roll forward) - `runner-policy.test.mjs`: pin the new reviewed reference; positive-audit loop covers it ## Rollout Dependabot's weekly `github-actions` group bumps move consumer pins to the new SHA; each repo starts routing dependabot CI to the fleet as its bump merges. Activation additionally requires the org Dependabot-store mirror of the observer key (fail-safe until then: dependabot runs keep falling back hosted via `missing-secret`). ## Verification `node --test components/runner-policy/runner-policy.test.mjs`: 112 pass / 0 fail 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01JLfjHY8PY6ozhgZPiuHKik --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…224) Closes #217 ## Related - #209 / #212 (github-script port pattern this issue's fix depends on for consumers) - medley#1628 report Theme 1 (the exit-127 defect class that motivates never routing a gh-CLI-dependent job self-hosted without porting it first) - medley#1632 (comment-review-gate.yml github-script port -- the reviewed no-checkout consumer this change enables) ## Summary - Adds `admits-comment-events` (workflow_call input, default `false`) to `select-runner.yml`. When a caller sets it `true`, `issue_comment`, `pull_request_review`, and `pull_request_review_comment` jobs become eligible for local (self-hosted) routing, subject to every existing policy/private-repo/fork gate. - Fixes a second, independent event enumeration in the "Mint read-only observer token" step (`prefer-self-hosted` policy path) that had its own hardcoded event list and was otherwise unreachable for comment/review events even with the flag set. ## Why (and the design this landed on) Org GitHub Actions spending is capped at $0. The governed policy is `self-hosted-only`, so private-repo required checks already run on the fleet -- but comment/review-event jobs were categorically excluded from local routing and fell back to GitHub-hosted runners, which now fail at startup on a billing-limit error whenever free minutes are exhausted (observed on medley's `comment-review-gate.yml`). **Original-rationale finding:** `LOCAL_EVENT_ALLOWLIST` (git blame/log through #92, #103, #123, #135) is a plain default-deny allowlist -- "only explicitly reviewed caller event classes may route locally" -- not a comment-specific security ban. Comment/review events were simply never reviewed, not deliberately blocked. **Checkout-bearing consumer found, scoped out:** Auditing fleet consumers surfaced medley's `claude-assistant.yml`, which triggers on `issue_comment`/`pull_request_review_comment`, checks out the repository, and runs Claude with `contents: write` and commit signing via the same selector. Under the current `self-hosted-only` policy that job is dormant (comment events route hosted, so its `route == 'self-hosted'` gate never passes). A blanket "admit these event classes" change -- the originally scoped approach -- would have made that dormant job self-hosted-eligible as a side effect: comment-triggered checkout + code execution + write perms on the fleet, without a security review of that specific exposure. Landed on a **per-caller opt-in** instead: `admits-comment-events` defaults `false`, so `claude-assistant.yml` is untouched by this PR and stays exactly as dormant as it is today. Its dormant assist job would only ever go live by **its own deliberate future opt-in** -- a separate decision requiring its own security review of checkout+write-on-fleet exposure, not a side effect of this change. Recording this explicitly here per the review discussion; will also leave a comment on #217 for anyone who revisits `claude-assistant.yml`'s routing later. `comment-review-gate.yml` (medley#1632) is the reviewed consumer this opt-in is for: verified pure `gh api`/now `github-script` calls, zero checkout anywhere in that workflow. ## Consumer follow-up (not in this PR) medley pins `select-runner.yml` by full commit SHA (currently `90f1c54935203fa31b5b3d1f41531228be2c2b7f # v0.6.1`). Once this merges and cuts a tag, medley's normal repin process picks up the new SHA; only *after* that repin can `comment-review-gate.yml` add `admits-comment-events: true` to its `select-runner` call (the pinned-SHA reusable doesn't know the input yet, so adding it any earlier would be rejected as an unexpected input). ## Verification - `node --test .github/scripts/select-runner.test.cjs` -- 112/112 passing, including new coverage: `admits-comment-events` true/false x the three comment/review events x `self-hosted-only`/`prefer-self-hosted`, plus a regression test proving the flag does not widen an unrelated blocked event class (`workflow_run`) - `node --test .github/scripts/*.test.cjs` -- 247/247 passing repo-wide - `actionlint .github/workflows/select-runner.yml` -- clean - `node .github/scripts/render-select-runner-workflow.cjs --check` -- generated block in sync with `select-runner.cjs` 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Why
Dependabot-triggered CI is the dominant remaining hosted spend (est. 1,500–3,000 min/mo) and draws down the 3,000 included-minute reserve the org keeps for must-be-hosted jobs and fleet-down fallback. The org owner explicitly approved retiring the selector's "Dependabot must never receive the observer credential" guard (named approval, this session) so same-repo bump PRs route to the ephemeral one-job fleet like any push.
What
select-runner.cjs: drop theisDependabothosted-only preflight term + the input; rationale comment updated (fork/public guards untouched)select-runner.yml: token-mint step no longer excludesdependabot[bot]/secret_source == 'Dependabot';IS_DEPENDABOTenv removed; embedded bundle regenerated (render-select-runner-workflow.cjs --checkpasses)select-runner.test.cjs: Dependabot hosted-route cases and static-guard terms removed (98/98 pass)Security boundary after this change
CI_RUNNER_OBSERVER_PRIVATE_KEYis mirrored there, Dependabot runs simply fall back hosted (missing-secret), so rollout is fail-safeActivation
Org owner mirrors the observer key:
gh secret set CI_RUNNER_OBSERVER_PRIVATE_KEY --org melodic-software --app dependabot --visibility selected --repos "claude-code-plugins,dotfiles,github-iac,medley,provisioning,standards" < <observer-key.pem>Follow-up
A dependabot-routing case in local-runner-canary would give this a proof-harness; none existed for the old behavior either.
🤖 Generated with Claude Code
https://claude.ai/code/session_01JLfjHY8PY6ozhgZPiuHKik