From 4621267c29080b02e6f4f63b9b7bfa6135d39fdb Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Thu, 16 Jul 2026 23:17:46 -0400 Subject: [PATCH] ci: route control-plane lanes through the governed selector MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Route ccp's remaining GitHub-hosted control-plane lanes through the governed runner selector to the self-hosted fleet, ending hosted-minute spend on this private repo: - ci.yml ci-status and pr-title.yml pr-title: the fail-closed required gateways route via the selector instead of a fixed ubuntu-24.04 runner (fail-closed on selector/fleet outage is the accepted tradeoff). - pr-title validate-pr-title: bump semantic-pr to its runner-input variant, passing runner + prerequisite-result. - link-check: migrate to the runner-input link-check reusable behind a select-runner job; retire the strict-policy pause. - Add do-not-merge and pr-issue-linkage callers (ci-workflows#120/#121), both routed through the selector. - Drop all .github/runner-policy.json hosted exceptions (now zero). Refs melodic-software/github-iac#78. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01K3QehVwmWzkBLpKokNCkkt Co-authored-by: Claude Fable 5 --- .github/runner-policy.json | 15 +------ .github/workflows/ci.yml | 15 ++++--- .github/workflows/do-not-merge.yml | 54 ++++++++++++++++++++++++++ .github/workflows/link-check.yml | 25 ++++++++++-- .github/workflows/pr-issue-linkage.yml | 53 +++++++++++++++++++++++++ .github/workflows/pr-title.yml | 23 ++++++----- 6 files changed, 153 insertions(+), 32 deletions(-) create mode 100644 .github/workflows/do-not-merge.yml create mode 100644 .github/workflows/pr-issue-linkage.yml diff --git a/.github/runner-policy.json b/.github/runner-policy.json index 5582e7711..106f7e22c 100644 --- a/.github/runner-policy.json +++ b/.github/runner-policy.json @@ -3,18 +3,5 @@ "repositoryOwner": "melodic-software", "visibility": "private", "selfHostedCi": true, - "exceptions": { - ".github/workflows/ci.yml#ci-status": { - "reason": "hosted-control-plane", - "justification": "The required CI gateway remains independent of selector and fleet health so route failures cannot become successful skipped checks." - }, - ".github/workflows/link-check.yml#link-check": { - "reason": "privileged-control-plane", - "justification": "The scheduled link checker can create or update repository issues and therefore remains on GitHub-hosted compute." - }, - ".github/workflows/pr-title.yml#pr-title": { - "reason": "hosted-control-plane", - "justification": "The required PR-title gateway remains independent of selector and fleet health so route failures cannot become successful skipped checks." - } - } + "exceptions": {} } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index beba6a7d7..355f1a1b7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -124,7 +124,9 @@ jobs: files: >- .github/workflows/ci.yml .github/workflows/claude-review.yml + .github/workflows/do-not-merge.yml .github/workflows/link-check.yml + .github/workflows/pr-issue-linkage.yml .github/workflows/pr-title.yml - name: Verify shebang files are executable @@ -352,8 +354,8 @@ jobs: CI_REPOSITORY_VISIBILITY: ${{ github.event.repository.visibility }} ci-status: - if: always() needs: + - select-runner - hygiene - hook-utils-sync - cross-plugin-source-drift @@ -361,10 +363,13 @@ jobs: - miro-plugin - runner-policy - zizmor - # The required gateway remains independent of selector/fleet health so a - # failed route materializes as a failing required check, never a skipped - # check that GitHub could treat as successful. - runs-on: ubuntu-24.04 + # Fail-closed through execution: !cancelled() (never a success-guard) so a + # selector/fleet failure still runs this required aggregate on the hosted + # fallback and the result join below turns it red. A success-guard would skip + # the job, and a skipped required check reports success to branch protection. + # Happy path routes to the fleet (github-iac#78). + if: ${{ !cancelled() }} + runs-on: ${{ needs.select-runner.outputs.runner || 'ubuntu-24.04' }} timeout-minutes: 15 steps: - name: Aggregate lane results diff --git a/.github/workflows/do-not-merge.yml b/.github/workflows/do-not-merge.yml new file mode 100644 index 000000000..9eea655ab --- /dev/null +++ b/.github/workflows/do-not-merge.yml @@ -0,0 +1,54 @@ +name: do-not-merge + +# Blocks merging while the PR carries the `do-not-merge` label, via the shared +# do-not-merge-gate reusable from ci-workflows. Runs on pull_request_target so +# the base-branch definition evaluates (a head edit cannot neuter the gate); +# safe because the reusable reads PR label metadata via the API and runs no head +# code. `labeled`/`unlabeled` are required so applying or removing the label +# re-evaluates the gate — a status check binds to a SHA, so without them a check +# that passed before the label was applied stays green and the merge is never +# blocked. `merge_group` re-checks the label in the queue (inert without a +# queue). The emitted required-check context is `do-not-merge / do-not-merge`. +on: + pull_request_target: + types: [opened, reopened, synchronize, labeled, unlabeled] + merge_group: + +permissions: + pull-requests: read + +# pull_request_target runs the base-branch definition, so github.ref is the base +# branch: the PR number scopes cancellation and github.ref covers merge_group +# (which carries no pull_request object); the fallback is inert without a queue. +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + select-runner: + name: Select runner + permissions: {} + uses: melodic-software/ci-workflows/.github/workflows/select-runner.yml@cdc5917c15aade1995bd810b60d818cadc635b52 # cdc5917 2026-07-16 + secrets: + observer-private-key: ${{ secrets.CI_RUNNER_OBSERVER_PRIVATE_KEY }} + with: + policy: ${{ vars.CI_RUNNER_POLICY }} + self-hosted-label: ${{ vars.CI_SELF_HOSTED_LABEL }} + hosted-runner: ${{ vars.CI_HOSTED_RUNNER }} + scope: ${{ vars.CI_RUNNER_SCOPE }} + managed-runner-prefix: ${{ vars.CI_MANAGED_RUNNER_PREFIX }} + observer-client-id: ${{ vars.CI_RUNNER_OBSERVER_CLIENT_ID }} + + do-not-merge: + needs: select-runner + # Fail-closed selector-result reporter: always() so every routing outcome + # materializes the required check. The reusable receives the selector result + # and fail-closes on any non-success before evaluating the label, so a + # routing failure never dispatches this gate to paid hosted Linux. + if: ${{ always() }} + permissions: + pull-requests: read + uses: melodic-software/ci-workflows/.github/workflows/do-not-merge-gate.yml@885302176345486ca6c2c392d83131f9b5389251 # 8853021 2026-07-16 + with: + runner: ${{ needs.select-runner.outputs.runner || 'ubuntu-24.04' }} + prerequisite-result: ${{ needs.select-runner.result }} diff --git a/.github/workflows/link-check.yml b/.github/workflows/link-check.yml index 92df06934..a783717d1 100644 --- a/.github/workflows/link-check.yml +++ b/.github/workflows/link-check.yml @@ -13,17 +13,34 @@ permissions: contents: read jobs: + select-runner: + name: Select runner + permissions: {} + uses: melodic-software/ci-workflows/.github/workflows/select-runner.yml@cdc5917c15aade1995bd810b60d818cadc635b52 # cdc5917 2026-07-16 + secrets: + observer-private-key: ${{ secrets.CI_RUNNER_OBSERVER_PRIVATE_KEY }} + with: + policy: ${{ vars.CI_RUNNER_POLICY }} + self-hosted-label: ${{ vars.CI_SELF_HOSTED_LABEL }} + hosted-runner: ${{ vars.CI_HOSTED_RUNNER }} + scope: ${{ vars.CI_RUNNER_SCOPE }} + managed-runner-prefix: ${{ vars.CI_MANAGED_RUNNER_PREFIX }} + observer-client-id: ${{ vars.CI_RUNNER_OBSERVER_CLIENT_ID }} + link-check: - # This issue-writing advisory lane remains hosted by policy and is paused - # while strict local-only emergency routing is active. - if: ${{ vars.CI_RUNNER_POLICY != 'self-hosted-only' }} + # Routes through the governed selector to the fleet; the old strict-policy + # pause is retired now that this issue-writing lane no longer depends on + # hosted compute. + needs: select-runner + if: ${{ !cancelled() && needs.select-runner.result == 'success' }} # issues: write is scoped to this job (least privilege) — only the called # workflow files the rolling tracking issue. permissions: contents: read issues: write - uses: melodic-software/ci-workflows/.github/workflows/link-check.yml@99ac2f8c5b09dbb785d4eaf18465cbd96c30290c # 99ac2f8 2026-07-11 + uses: melodic-software/ci-workflows/.github/workflows/link-check.yml@3dfb18452a8c6059a22e62456390d84feb10b42f # 3dfb184 2026-07-16 with: + runner: ${{ needs.select-runner.outputs.runner || 'ubuntu-24.04' }} args: >- --cache --max-cache-age 7d diff --git a/.github/workflows/pr-issue-linkage.yml b/.github/workflows/pr-issue-linkage.yml new file mode 100644 index 000000000..3d9a08fcb --- /dev/null +++ b/.github/workflows/pr-issue-linkage.yml @@ -0,0 +1,53 @@ +name: pr-issue-linkage + +# Validates the PR body carries a native closing keyword (Closes/Fixes/Resolves +# #N, including owner/repo#N, or the literal "No linked issue" when the PR closes +# nothing) and a non-empty `## Related` section, via the shared +# pr-issue-linkage reusable from ci-workflows. `pull_request_target` runs the +# base-branch definition, so a head-branch edit cannot bypass the gate — safe +# here because the reusable reads PR body metadata from the event payload only +# and runs no head code. `edited` re-validates on a body edit. `merge_group` +# reports the check green in the queue (the body was validated at PR time; inert +# without a queue). The emitted required-check context is +# `pr-issue-linkage / pr-issue-linkage`. +on: + pull_request_target: + types: [opened, edited, reopened, synchronize] + merge_group: + +permissions: {} + +# pull_request_target runs the base-branch definition, so github.ref is the base +# branch: the PR number scopes cancellation and github.ref covers merge_group +# (which carries no pull_request object); the fallback is inert without a queue. +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + select-runner: + name: Select runner + permissions: {} + uses: melodic-software/ci-workflows/.github/workflows/select-runner.yml@cdc5917c15aade1995bd810b60d818cadc635b52 # cdc5917 2026-07-16 + secrets: + observer-private-key: ${{ secrets.CI_RUNNER_OBSERVER_PRIVATE_KEY }} + with: + policy: ${{ vars.CI_RUNNER_POLICY }} + self-hosted-label: ${{ vars.CI_SELF_HOSTED_LABEL }} + hosted-runner: ${{ vars.CI_HOSTED_RUNNER }} + scope: ${{ vars.CI_RUNNER_SCOPE }} + managed-runner-prefix: ${{ vars.CI_MANAGED_RUNNER_PREFIX }} + observer-client-id: ${{ vars.CI_RUNNER_OBSERVER_CLIENT_ID }} + + pr-issue-linkage: + needs: select-runner + # Fail-closed selector-result reporter: always() so every routing outcome + # materializes the required check. The reusable receives the selector result + # and fail-closes on any non-success before validating the body, so a + # routing failure never dispatches this gate to paid hosted Linux. + if: ${{ always() }} + permissions: {} + uses: melodic-software/ci-workflows/.github/workflows/pr-issue-linkage.yml@f7e94a80254fdca0aa85a600a07d784753b090e3 # f7e94a8 2026-07-16 + with: + runner: ${{ needs.select-runner.outputs.runner || 'ubuntu-24.04' }} + prerequisite-result: ${{ needs.select-runner.result }} diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml index 7ae7912f2..ba69bb733 100644 --- a/.github/workflows/pr-title.yml +++ b/.github/workflows/pr-title.yml @@ -34,24 +34,29 @@ jobs: observer-private-key: ${{ secrets.CI_RUNNER_OBSERVER_PRIVATE_KEY }} validate-pr-title: - # Run semantic validation only after a successful routing decision. A - # strict selector/configuration failure never dispatches this workload to - # paid hosted Linux. - if: ${{ !cancelled() && needs.select-runner.result == 'success' }} + # Fail-closed selector-result reporter: always() so every routing outcome + # materializes the required check. The reusable receives the selector result + # and fail-closes on any non-success, reporting the check as failed rather + # than dispatching validation to paid hosted Linux. + if: ${{ always() }} needs: select-runner permissions: pull-requests: read # Reads the pull-request title for validation. - uses: melodic-software/ci-workflows/.github/workflows/semantic-pr.yml@99ac2f8c5b09dbb785d4eaf18465cbd96c30290c # 99ac2f8 2026-07-11 + uses: melodic-software/ci-workflows/.github/workflows/semantic-pr.yml@51012e2c7b8bf74bc26e08c6446b488254a8770f # 51012e2 2026-07-12 with: runner: ${{ needs.select-runner.outputs.runner || 'ubuntu-24.04' }} + prerequisite-result: ${{ needs.select-runner.result }} pr-title: name: pr-title / pr-title needs: [select-runner, validate-pr-title] - if: ${{ always() && !cancelled() }} - # This fail-closed required gateway must report selector failure even when - # the local fleet cannot accept the validation workload. - runs-on: ubuntu-24.04 + # Fail-closed through execution: !cancelled() (never a success-guard) so a + # selector failure still runs this required wrapper on the hosted fallback + # and the step below turns it red. A success-guard would skip the job, and a + # skipped required check reports success to branch protection. Happy path + # routes to the fleet (github-iac#78). + if: ${{ !cancelled() }} + runs-on: ${{ needs.select-runner.outputs.runner || 'ubuntu-24.04' }} permissions: {} steps: - name: Enforce routing and semantic validation results