From 07a23cba3d76e789db449ba321f7238b1172096f Mon Sep 17 00:00:00 2001
From: Matthew Jackson <1085847+MattJackson@users.noreply.github.com>
Date: Tue, 13 Oct 2026 11:07:02 -0700
Subject: [PATCH] retire busbar-release from busbar: delete promote.yml;
ModernCI alone judges every move (row 163)
OWNER 2026-10-12 "ModernCI only"; 2026-10-13 "delete anything you don't need anymore, yes" and
"we keep the busbar-release repo, slimmed down. ModernCI replaces its CI half; the busbar-specific
tools stay." Spec Part 6 gains the ruling (keep/delete lists, the ladder on .ci/*.yaml, the oracle
home) and the passages it supersedes are struck, Part 5's DEV-GREEN item 6 included.
- .github/workflows/promote.yml deleted: the busbar-release hop ran on every PR into dev/qa/main
and ended INFRA every run (R2 keys unset).
- workflow-rules: R16 (engine pin), R18 (promote.yml release artifact), R19 (hop R2 keys) go with
the file they held; the R1/R11/R14-flow mutations move to plugin-ci.yml and planted files;
MIN_WORKFLOWS 5 -> 4 (the four plugin-*.yml workflows).
- no-self-filed-issues: discovery floor 5 -> 4, the same workflow count.
- infra: the "every gate the pipeline calls is registered" test reads .ci/*.yaml, the pipeline.
- .ci/predev.yaml: no longer a shadow; gates requires git-history as in .ci/default.yaml.
- RELEASE.md, required-status-checks, CONTRIBUTING, README badge, actions-pins: the ladder is
ModernCI's.
---
.ci/predev.yaml | 13 +-
.github/required-status-checks.md | 12 +-
.github/workflows/promote.yml | 474 -----------
CONTRIBUTING.md | 2 +-
README.md | 1 -
RELEASE.md | 56 +-
docs/ci/actions-pins.md | 4 +-
docs/design/BUSBAR-1.6.0.md | 150 ++--
xtask/src/gates/no_self_filed_issues.rs | 9 +-
xtask/src/gates/package_selectors.rs | 2 +-
xtask/src/gates/ship_ready.rs | 4 +-
xtask/src/gates/tests/workflow_rules_tests.rs | 276 -------
xtask/src/gates/workflow_rules.rs | 738 +-----------------
xtask/tests/infra.rs | 54 +-
14 files changed, 205 insertions(+), 1590 deletions(-)
delete mode 100644 .github/workflows/promote.yml
diff --git a/.ci/predev.yaml b/.ci/predev.yaml
index 966fa5be98..077c7b39c8 100644
--- a/.ci/predev.yaml
+++ b/.ci/predev.yaml
@@ -1,7 +1,9 @@
-# What predev must pass to move into dev: busbar-release's `dev` rung (crates/busbar-release-cli/
-# ladder.json: gates inherit, oracle, conformance; verdict absolute), the 1.6.0 done-oracle, and the
-# Q128 architecture audit (`cargo xtask q128 audit`). ModernCI names a branch file for the branch
-# being moved, so the move predev -> dev reads this file. Shadow while busbar-release still decides.
+# What predev must pass to move into dev: the `dev` rung (gates, oracle, conformance), the 1.6.0
+# done-oracle, and the Q128 architecture audit (`cargo xtask q128 audit`). ModernCI names a branch
+# file for the branch being moved, so the move predev -> dev reads this file, and ModernCI alone
+# judges it: promote.yml, the busbar-release hop that judged it before, is deleted (OWNER
+# 2026-10-12, "ModernCI only"; docs/design/BUSBAR-1.6.0.md Part 6, "BUSBAR-RELEASE RETIRED FROM
+# BUSBAR"). Its green landing is DEV-GREEN.
to: dev
on_green: merge
# The owner's approval on the pull request holds the merge too; q128-audit gates the move on the
@@ -24,6 +26,9 @@ checks:
cargo xtask gate --all --format=tsv
baseline: target
rules: ["qa/*.toml"]
+ # The ceiling ratchet reads the merge-base (XTASK_CEILING_BASE), as in .ci/default.yaml: only a
+ # backend with the repo's history may run this check.
+ requires: [git-history]
oracle:
# busbar's own check tool, the pinned busbar-oracle (the backend's busbar-oracle tool sets
# BUSBAR_ORACLE_RUST_BIN): the judge is the one dev's oracle-rust.pin names, run only when its
diff --git a/.github/required-status-checks.md b/.github/required-status-checks.md
index ffea47b689..530e300a00 100644
--- a/.github/required-status-checks.md
+++ b/.github/required-status-checks.md
@@ -5,22 +5,18 @@ Branch protection lives in repository rulesets. No script writes it.
| Branch | Ruleset | Required |
| --- | --- | --- |
| `predev` | 24945926 | ModernCI's `ci` check. ModernCI judges the PR (`.ci/default.yaml`) and lands it. |
-| `dev` | 24340358 | `preflight` and `hop`, both from `.github/workflows/promote.yml` (the predev to dev promotion). |
+| `dev` | 24340358 | ModernCI's `ci` check on the predev to dev move (`.ci/predev.yaml`). The ruleset still names `preflight` and `hop`, the jobs of the deleted `promote.yml`: the owner replaces them with `ci`. |
| `qa`, `main` | 24334334 | Owner-only; no status check stands in for the owner. |
-Use the exact **check-run name** (the job's `name:`), not the workflow name. In `promote.yml`, `hop`
-is the job id with no `name:`, so its context reads `hop`.
-
-* `preflight`: formatting, `Cargo.lock` current, the generated C header current, on a free runner.
-* `hop`: the pinned busbar-release engine plans the hop and admits the candidate (every registered
- `cargo xtask gate`, plus the oracle and conformance where the rung asks for them).
+Use the exact **check name** the judge posts. ModernCI posts one check, `ci`, per pull request head.
## Reusable workflows (plugin repos call these)
`plugin-ci.yml`, `plugin-consumer-verify.yml`, `plugin-repin.yml` and `plugin-release.yml` are
`workflow_call` only, called by the plugin repos' rendered callers (busbar-release `template/`,
`busbar-release plugin sync`). Their status bubbles up into the **caller's** job, so require the
-caller's job in the plugin repo, never these directly.
+caller's job in the plugin repo, never these directly. They stay until each plugin repo is
+registered with ModernCI and judged by its own branch files.
## Branch hygiene
diff --git a/.github/workflows/promote.yml b/.github/workflows/promote.yml
deleted file mode 100644
index aa1c9b0ae5..0000000000
--- a/.github/workflows/promote.yml
+++ /dev/null
@@ -1,474 +0,0 @@
-name: Promote
-# This workflow serves the predev->dev promotion pull request and the dev->qa->main ladder above it. The
-# hop is GetBusbar/busbar-release's `busbar-release ci hop|verdict|publish|fallback` at RELEASE_REF
-# (private, so no `uses:`); these jobs only check out both repos and run it. No job here runs BOARD
-# (`ci train promote` needs the train's write token, and the job that runs the candidate's code holds
-# none): a PR lands by merge.
-# Pull requests into predev are not judged here: ModernCI judges them (required check `ci`) and lands
-# them with its own merge train, so predev has no merge queue and this workflow runs for no pull request
-# into predev or into a lane branch (a stacked lane PR), nor for `merge_group`. The `merge_group` handling in the jobs below
-# is left in place, dormant: it is job logic, and the concurrency and env lines the preflight holds to busbar-release's compiled copy read it.
-on: { pull_request: { branches: [dev, qa, main] } }
-# R5 (busbar-release gate.rs, OWNER 2026-10-08): no run is ever cancelled in progress. A pull request
-# has at most one run running and one pending, and each newer push replaces the pending one, so the
-# pending run is always the newest head. A merge-queue run is its own group (a cancelled queue run
-# re-runs whole, owner money 2026-10-03). The preflight holds this line to the one compiled into
-# busbar-release: any drift is RED.
-concurrency: { group: "promote-${{ github.event.pull_request.number || github.event.merge_group.head_ref }}", cancel-in-progress: false }
-permissions: { contents: read }
-# THE ENGINE PIN IS NOT IN THIS FILE (OWNER 2026-10-10): the org variables ENGINE_REF (a full
-# busbar-release commit) and ENGINE_SHA256 (the sha256 of that commit's dev-bin ci asset), set by
-# hand with `busbar-release engine pin `, so an engine update never waits in predev's merge
-# queue. The preflight reads them ONCE and hands them to every later job as outputs: one run is one
-# engine even if the variables move mid-run. Unset or malformed is a hard FAIL (ENGINE-PIN), never
-# an older pin; the bytes run only when their sha256 is ENGINE_SHA256 (workflow-rules R16).
-env:
- # The engine contract (busbar-release contract.rs): 2 = the hop holds only the read-only R2 keys and `cache-publish` writes. An engine refuses a contract it does not support, and the auto-pin moves only to engines that support this number.
- BUSBAR_RELEASE_CONTRACT: "2"
- SHA: ${{ github.event.pull_request.head.sha || github.event.merge_group.head_sha }}
- TO: ${{ github.base_ref || github.event.merge_group.base_ref }}
- BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha }}
-jobs:
- preflight:
- if: github.event_name == 'merge_group' || github.event.pull_request.head.repo.full_name == github.repository
- runs-on: ubuntu-latest
- timeout-minutes: 15
- # THE RUN GATE reads GitHub (open pull requests, the merge queue, this head's promote runs) with
- # this job's token. This job runs the candidate's `cargo xtask`, so the token only READS
- # (busbar-release 941028a): the gate's writes (ORPHANS, R7 REPEAT) are recorded in the act
- # artifact before any candidate code runs, and `preflight-act` below makes them.
- # OLD-ENGINE SHIM (remove once ENGINE_REF is at or past busbar-release 941028a): an older engine's
- # R6 field `jobs.preflight.permissions (actions: read)` reads this line's text, comment included;
- # this job's token stays contents read only.
- permissions: { contents: read } # actions: read is public on this repo; the writes are preflight-act's
- # RUNNERS: the runner gateway's table (vars.RUNNERS), from which the preflight sizes `runner=`.
- env: { RELEASE_REF: "${{ vars.ENGINE_REF }}", ENGINE_SHA256: "${{ vars.ENGINE_SHA256 }}", RUNNERS: "${{ vars.RUNNERS }}" }
- outputs: { engine_ref: "${{ steps.engine.outputs.engine_ref }}", engine_sha256: "${{ steps.engine.outputs.engine_sha256 }}", runner: "${{ steps.pre.outputs.runner }}", soak: "${{ steps.pre.outputs.soak }}", one_runner: "${{ steps.pre.outputs.one_runner }}", needs_release_artifact: "${{ steps.pre.outputs.needs_release_artifact }}", run: "${{ steps.pre.outputs.run }}", reason: "${{ steps.pre.outputs.reason }}" }
- steps:
- # Full history: the pre-flight reads the pull request's diff against BASE_SHA to choose one runner or the matrix.
- - { uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1, with: { ref: "${{ env.SHA }}", path: busbar, fetch-depth: 0 } } # v7
- # THE ENGINE, downloaded, never compiled: CI's --no-default-features binary of RELEASE_REF from
- # busbar-release's dev-bin release (dev-bin.yml), run only when its sha256 is ENGINE_SHA256.
- # First the pin's shape: a full 40-hex commit and a 64-hex digest, or FAIL.
- - name: Engine (busbar-release at RELEASE_REF, sha256-pinned)
- id: engine
- env: { GH_TOKEN: "${{ secrets.TURNSTILE_DISPATCH_TOKEN }}" }
- run: |
- set -euo pipefail
- [[ "$RELEASE_REF" =~ ^[0-9a-f]{40}$ ]] || { echo "::error title=ENGINE-PIN FAIL::vars.ENGINE_REF is unset or not a full 40-hex busbar-release commit (busbar-release engine pin )"; exit 1; }
- [[ "$ENGINE_SHA256" =~ ^[0-9a-f]{64}$ ]] || { echo "::error title=ENGINE-PIN FAIL::vars.ENGINE_SHA256 is unset or not a 64-hex sha256 (busbar-release engine pin )"; exit 1; }
- api=https://api.github.com/repos/GetBusbar/busbar-release/releases
- name="busbar-release-ci-linux-x86_64-$RELEASE_REF"
- id=$(curl -fsSL -H "Authorization: Bearer $GH_TOKEN" "$api/tags/dev-bin" | jq -r --arg n "$name" '.assets[] | select(.name == $n) | .id')
- [ -n "$id" ] || { echo "::error::dev-bin has no $name"; exit 1; }
- curl -fsSL -H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/octet-stream" -o "$RUNNER_TEMP/busbar-release" "$api/assets/$id"
- echo "$ENGINE_SHA256 $RUNNER_TEMP/busbar-release" | sha256sum -c -
- chmod +x "$RUNNER_TEMP/busbar-release"
- { echo "engine_ref=$RELEASE_REF"; echo "engine_sha256=$ENGINE_SHA256"; } >> "$GITHUB_OUTPUT"
- - { uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1, with: { repository: GetBusbar/busbar-release, ref: "${{ env.RELEASE_REF }}", token: "${{ secrets.TURNSTILE_DISPATCH_TOKEN }}", path: busbar-release, persist-credentials: false } } # v7
- # The pre-flight is busbar-release's Rust CLI (preflight.json is the one list): it sizes the hop
- # (runner= to $GITHUB_OUTPUT), chooses how a pull request into predev runs (one_runner=: ONE runner
- # when the crates its diff affects are estimated within 20 min, else the shard matrix; OWNER-LOCKED
- # 2026-10-06), whether this run reads the release artifact (needs_release_artifact=: the rung runs
- # the oracle or conformance; predev runs neither), and runs every pre-flight check against the
- # candidate. Built with no default features, as the CLI documents for this job. FIRST it is THE
- # RUN GATE (OWNER 2026-10-08): run=1|0 and reason= from facts only (R1 STACK, R2 queued, R3
- # superseded, R4 duplicate head, R5/R6 this file's wiring); run=0 means nothing in this run
- # builds, and a RED rule fails here. The gate's writes are recorded (`--act-out act/act.json`,
- # before any candidate code runs) for preflight-act; this job makes none.
- # OLD-ENGINE SHIM (remove once ENGINE_REF is at or past 941028a): an engine without --act-out
- # runs without it.
- - id: pre
- working-directory: busbar-release
- env: { GITHUB_TOKEN: "${{ github.token }}", RUNNERS: "${{ vars.RUNNERS }}" }
- run: |
- act=()
- if "$RUNNER_TEMP/busbar-release" ci preflight --help | grep -q -- --act-out; then act=(--act-out "$GITHUB_WORKSPACE/act/act.json"); fi
- "$RUNNER_TEMP/busbar-release" ci preflight "${act[@]}"
- - { if: always(), uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a, with: { name: "preflight-act-${{ env.SHA }}", path: "act/", retention-days: 1, if-no-files-found: ignore, overwrite: true } } # v7.0.1
- # THE RUN GATE'S WRITES (busbar-release 941028a), in a job that runs NO candidate code: it checks
- # out nothing, downloads the engine (sha256-pinned) and the preflight's act artifact, and runs
- # `ci preflight-act`, which re-reads GitHub itself: ORPHANS cancels the merge-group runs a queue
- # rebuild left running (actions: write), and R7 REPEAT turns off the auto-merge of a PR whose head
- # already failed the queue as its own red (pull-requests: write), only the PR both the artifact
- # and the re-read name. Merge groups only: a pull request has nothing to act on. It runs whatever
- # the preflight concluded (R7 is a RED preflight), and never decides the verdict.
- preflight-act:
- needs: preflight
- if: ${{ always() && github.event_name == 'merge_group' && needs.preflight.outputs.engine_ref != '' }}
- runs-on: ubuntu-latest
- timeout-minutes: 10
- permissions: { contents: read, actions: write, pull-requests: write }
- env: { RELEASE_REF: "${{ needs.preflight.outputs.engine_ref }}", ENGINE_SHA256: "${{ needs.preflight.outputs.engine_sha256 }}" }
- steps:
- # THE ENGINE, downloaded, never compiled: CI's --no-default-features binary of RELEASE_REF from
- # busbar-release's dev-bin release (dev-bin.yml), run only when its sha256 is ENGINE_SHA256.
- # First the pin's shape: a full 40-hex commit and a 64-hex digest, or FAIL.
- - name: Engine (busbar-release at RELEASE_REF, sha256-pinned)
- env: { GH_TOKEN: "${{ secrets.TURNSTILE_DISPATCH_TOKEN }}" }
- run: |
- set -euo pipefail
- [[ "$RELEASE_REF" =~ ^[0-9a-f]{40}$ ]] || { echo "::error title=ENGINE-PIN FAIL::vars.ENGINE_REF is unset or not a full 40-hex busbar-release commit (busbar-release engine pin )"; exit 1; }
- [[ "$ENGINE_SHA256" =~ ^[0-9a-f]{64}$ ]] || { echo "::error title=ENGINE-PIN FAIL::vars.ENGINE_SHA256 is unset or not a 64-hex sha256 (busbar-release engine pin )"; exit 1; }
- api=https://api.github.com/repos/GetBusbar/busbar-release/releases
- name="busbar-release-ci-linux-x86_64-$RELEASE_REF"
- id=$(curl -fsSL -H "Authorization: Bearer $GH_TOKEN" "$api/tags/dev-bin" | jq -r --arg n "$name" '.assets[] | select(.name == $n) | .id')
- [ -n "$id" ] || { echo "::error::dev-bin has no $name"; exit 1; }
- curl -fsSL -H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/octet-stream" -o "$RUNNER_TEMP/busbar-release" "$api/assets/$id"
- echo "$ENGINE_SHA256 $RUNNER_TEMP/busbar-release" | sha256sum -c -
- chmod +x "$RUNNER_TEMP/busbar-release"
- - { continue-on-error: true, uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c, with: { name: "preflight-act-${{ env.SHA }}", path: act } } # v8.0.1
- # OLD-ENGINE SHIM (remove once ENGINE_REF is at or past 941028a): an engine without the verb
- # made these writes in the preflight itself.
- - name: The run gate's writes (ORPHANS, R7 REPEAT)
- env: { GITHUB_TOKEN: "${{ github.token }}" }
- run: |
- if ! "$RUNNER_TEMP/busbar-release" ci --help | grep -q preflight-act; then echo "engine $RELEASE_REF has no preflight-act: nothing to act on here"; exit 0; fi
- "$RUNNER_TEMP/busbar-release" ci preflight-act --act "$GITHUB_WORKSPACE/act/act.json"
- # The release artifact the oracle records and the slsa-verifier conformance rig judges. It runs in
- # every merge group (the fold requires it: a check on the release build), and in every run whose
- # rung reads the artifact (`needs_release_artifact=true`: the rung runs the oracle or conformance,
- # i.e. the predev->dev pull request and up). dev moves by pull request now, not by the ladder
- # fast-forwarding a merge group's sha (ARCHITECT 2026-10-02, superseded), so the run that judges a
- # promotion builds the artifact of exactly the commit it promotes: its shards download
- # `slsa-artifact-` from THIS run, which a merge-group-only build never produced (#740 failed
- # every run at that download). workflow-rules R18 holds this wiring. Its SLSA provenance is minted
- # from `base64-subjects` by the pinned slsa-github-generator once the owner rules on R14 (the
- # generator must be referenced by tag); until then the suite is not-run, a standing red here.
- slsa-build:
- if: needs.preflight.outputs.run == '1' && (github.event_name == 'merge_group' || needs.preflight.outputs.needs_release_artifact == 'true')
- needs: preflight
- permissions: { contents: read }
- runs-on: ${{ needs.preflight.outputs.runner }}
- timeout-minutes: 90
- outputs: { base64-subjects: "${{ steps.digest.outputs.base64-subjects }}" }
- steps:
- - { uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1, with: { ref: "${{ env.SHA }}", path: busbar, persist-credentials: false } } # v7
- - name: Build the release artifact
- working-directory: busbar
- run: |
- cargo build --locked --release -p busbar --bin busbar
- mkdir -p ../slsa && cp target/release/busbar ../slsa/busbar
- - id: digest
- name: Its sha256 (the provenance subject)
- working-directory: slsa
- run: |
- sha256sum busbar > busbar.sha256
- echo "base64-subjects=$(base64 -w0 busbar.sha256)" >> "$GITHUB_OUTPUT"
- - { uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a, with: { name: "slsa-artifact-${{ env.SHA }}", path: "slsa/", retention-days: 7, if-no-files-found: error } } # v7.0.1
- # THE QA SOAK (OWNER 2026-10-10; busbar #734): the soak stays, as a 1-hour stability gate on each
- # qa promotion. `busbar-release soak ` (profile.rs's full profile: 3600 s, 50 rps,
- # 16 concurrent) soaks THIS candidate's release binary on the runner the preflight chose, wherever
- # the ladder's entry asks for it (`soak=true`: qa). The hop folds this job's result like
- # slsa-build's (`ci turnstile merge --needs soak=...`): a failed soak DENIES the promotion. CI=false
- # selects the full hour (the CI context would pick the two-minute smoke). It runs the candidate, so
- # it holds no write token. The candidate binary is this run's slsa-build artifact (a rung with the
- # soak reads the artifact, so slsa-build runs); a run without it builds it here. The engine is the
- # downloaded ci binary (built with `--features soak`, dev-bin.yml), sha256-pinned like every other
- # job's.
- soak:
- needs: [preflight, slsa-build]
- if: ${{ !cancelled() && needs.preflight.result == 'success' && needs.preflight.outputs.run == '1' && needs.preflight.outputs.soak == 'true' }}
- permissions: { contents: read }
- runs-on: ${{ needs.preflight.outputs.runner }}
- timeout-minutes: 180
- env: { RELEASE_REF: "${{ needs.preflight.outputs.engine_ref }}", ENGINE_SHA256: "${{ needs.preflight.outputs.engine_sha256 }}" }
- steps:
- - { uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1, with: { ref: "${{ env.SHA }}", path: busbar, persist-credentials: false } } # v7
- # THE ENGINE, downloaded, never compiled: CI's --no-default-features binary of RELEASE_REF from
- # busbar-release's dev-bin release (dev-bin.yml), run only when its sha256 is ENGINE_SHA256.
- # First the pin's shape: a full 40-hex commit and a 64-hex digest, or FAIL.
- - name: Engine (busbar-release at RELEASE_REF, sha256-pinned)
- env: { GH_TOKEN: "${{ secrets.TURNSTILE_DISPATCH_TOKEN }}" }
- run: |
- set -euo pipefail
- [[ "$RELEASE_REF" =~ ^[0-9a-f]{40}$ ]] || { echo "::error title=ENGINE-PIN FAIL::vars.ENGINE_REF is unset or not a full 40-hex busbar-release commit (busbar-release engine pin )"; exit 1; }
- [[ "$ENGINE_SHA256" =~ ^[0-9a-f]{64}$ ]] || { echo "::error title=ENGINE-PIN FAIL::vars.ENGINE_SHA256 is unset or not a 64-hex sha256 (busbar-release engine pin )"; exit 1; }
- api=https://api.github.com/repos/GetBusbar/busbar-release/releases
- name="busbar-release-ci-linux-x86_64-$RELEASE_REF"
- id=$(curl -fsSL -H "Authorization: Bearer $GH_TOKEN" "$api/tags/dev-bin" | jq -r --arg n "$name" '.assets[] | select(.name == $n) | .id')
- [ -n "$id" ] || { echo "::error::dev-bin has no $name"; exit 1; }
- curl -fsSL -H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/octet-stream" -o "$RUNNER_TEMP/busbar-release" "$api/assets/$id"
- echo "$ENGINE_SHA256 $RUNNER_TEMP/busbar-release" | sha256sum -c -
- chmod +x "$RUNNER_TEMP/busbar-release"
- - { uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1, with: { repository: GetBusbar/busbar-release, ref: "${{ env.RELEASE_REF }}", token: "${{ secrets.TURNSTILE_DISPATCH_TOKEN }}", path: busbar-release, persist-credentials: false } } # v7
- - if: needs.slsa-build.result == 'success'
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
- with: { name: "slsa-artifact-${{ env.SHA }}", path: slsa }
- - if: needs.slsa-build.result != 'success'
- working-directory: busbar
- run: |
- cargo build --locked --release -p busbar --bin busbar
- mkdir -p ../slsa && cp target/release/busbar ../slsa/busbar
- - name: Soak the candidate (the full profile)
- working-directory: busbar-release
- env: { CI: "false", GH_TOKEN: "${{ github.token }}", TMPDIR: "${{ runner.temp }}" }
- run: |
- chmod +x "$GITHUB_WORKSPACE/slsa/busbar"
- "$RUNNER_TEMP/busbar-release" soak "$GITHUB_WORKSPACE/slsa/busbar"
- - { if: always(), uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a, with: { name: "soak-${{ env.SHA }}", path: "${{ runner.temp }}/busbar-soak-*/", retention-days: 30, if-no-files-found: warn } } # v7.0.1
- # The merge group's hop, and the predev->dev pull request's, is SHARDED (ARCHITECT 2026-10-02): four
- # runners each judge one part (`--shard K/4`: test:workspace's hash partition K, plus the unsplit steps
- # dealt to K; the gates in shard 1, the oracle and conformance in shard 4). Each builds and runs the
- # candidate's code, so it holds NO write token (contents and actions read only, no persisted
- # credentials); its report leaves as an artifact. It starts beside slsa-build, not after it (B3-P,
- # 2026-10-08), and no shard waits for it: the fold requires slsa-build's result (`needs:`,
- # `--needs`, `--must-run`), and nothing a shard runs reads the release artifact (the slsa-verifier
- # leg is not-run until `conformance record` is handed `--slsa-artifact`). The predev->dev shards
- # sat ~7 min each in `await-job` for a download nobody read (2026-10-11). Every shard runs to
- # the bottom (FULLY SEEN, busbar-release #152). A pull request into predev runs the shards only when the
- # pre-flight chose the matrix (one_runner=false: every crate affected, or over 20 min on one runner);
- # otherwise its hop is ONE runner, in the `hop` job below.
- hop-shard:
- name: "hop shard ${{ matrix.shard }}"
- strategy: { fail-fast: false, matrix: { shard: [1, 2, 3, 4] } }
- needs: [preflight]
- if: ${{ !cancelled() && needs.preflight.result == 'success' && needs.preflight.outputs.run == '1' && (github.event_name == 'merge_group' || github.base_ref == 'dev' || needs.preflight.outputs.one_runner != 'true') }}
- permissions: { contents: read, actions: read }
- runs-on: ${{ needs.preflight.outputs.runner }}
- timeout-minutes: 330
- env: { RELEASE_REF: "${{ needs.preflight.outputs.engine_ref }}", ENGINE_SHA256: "${{ needs.preflight.outputs.engine_sha256 }}", RUNNERS: "${{ vars.RUNNERS }}" }
- steps:
- - { uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1, with: { ref: "${{ env.SHA }}", path: busbar, fetch-depth: 0, persist-credentials: false } } # v7
- # THE ENGINE, downloaded, never compiled: CI's --no-default-features binary of RELEASE_REF from
- # busbar-release's dev-bin release (dev-bin.yml), run only when its sha256 is ENGINE_SHA256.
- # First the pin's shape: a full 40-hex commit and a 64-hex digest, or FAIL.
- - name: Engine (busbar-release at RELEASE_REF, sha256-pinned)
- env: { GH_TOKEN: "${{ secrets.TURNSTILE_DISPATCH_TOKEN }}" }
- run: |
- set -euo pipefail
- [[ "$RELEASE_REF" =~ ^[0-9a-f]{40}$ ]] || { echo "::error title=ENGINE-PIN FAIL::vars.ENGINE_REF is unset or not a full 40-hex busbar-release commit (busbar-release engine pin )"; exit 1; }
- [[ "$ENGINE_SHA256" =~ ^[0-9a-f]{64}$ ]] || { echo "::error title=ENGINE-PIN FAIL::vars.ENGINE_SHA256 is unset or not a 64-hex sha256 (busbar-release engine pin )"; exit 1; }
- api=https://api.github.com/repos/GetBusbar/busbar-release/releases
- name="busbar-release-ci-linux-x86_64-$RELEASE_REF"
- id=$(curl -fsSL -H "Authorization: Bearer $GH_TOKEN" "$api/tags/dev-bin" | jq -r --arg n "$name" '.assets[] | select(.name == $n) | .id')
- [ -n "$id" ] || { echo "::error::dev-bin has no $name"; exit 1; }
- curl -fsSL -H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/octet-stream" -o "$RUNNER_TEMP/busbar-release" "$api/assets/$id"
- echo "$ENGINE_SHA256 $RUNNER_TEMP/busbar-release" | sha256sum -c -
- chmod +x "$RUNNER_TEMP/busbar-release"
- # THE JUDGE, pinned in busbar (OWNER 2026-10-10): the busbar-oracle testing/shadow-oracle/oracle-rust.pin
- # names, read at the BASE (a candidate never picks its own judge), downloaded from busbar-release's
- # dev-bin release and run only when its sha256 is the pin's sha256_linux_x86_64. The engine pin
- # never moves it. Only the last shard runs the oracle; `ci hop` and bin/oracle both take the binary
- # from BUSBAR_ORACLE_RUST_BIN, so nothing builds a judge.
- - name: Oracle (pinned at the base, sha256-checked)
- if: matrix.shard == 4
- env: { GH_TOKEN: "${{ secrets.TURNSTILE_DISPATCH_TOKEN }}" }
- run: |
- set -euo pipefail
- pin="$(git -C busbar show "$BASE_SHA:testing/shadow-oracle/oracle-rust.pin" 2>/dev/null || true)"
- # A base from before the pinned binary (no sha256 line): the oracle is built as before.
- grep -q '^sha256_linux_x86_64=' <<<"$pin" || { echo "::notice::oracle-rust.pin at $BASE_SHA pins no binary: bin/oracle builds the judge"; exit 0; }
- ref="$(printf '%s\n' "$pin" | sed -n 's/^release_ref=//p')"
- ORACLE_SHA256="$(printf '%s\n' "$pin" | sed -n 's/^sha256_linux_x86_64=//p')"
- [[ "$ref" =~ ^[0-9a-f]{40}$ && "$ORACLE_SHA256" =~ ^[0-9a-f]{64}$ ]] || { echo "::error title=ORACLE-PIN FAIL::oracle-rust.pin at $BASE_SHA needs a 40-hex release_ref and a 64-hex sha256_linux_x86_64"; exit 1; }
- api=https://api.github.com/repos/GetBusbar/busbar-release/releases
- name="busbar-oracle-linux-x86_64-$ref"
- id=$(curl -fsSL -H "Authorization: Bearer $GH_TOKEN" "$api/tags/dev-bin" | jq -r --arg n "$name" '.assets[] | select(.name == $n) | .id')
- [ -n "$id" ] || { echo "::error title=ORACLE-PIN FAIL::dev-bin has no $name"; exit 1; }
- curl -fsSL -H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/octet-stream" -o "$RUNNER_TEMP/busbar-oracle" "$api/assets/$id"
- echo "$ORACLE_SHA256 $RUNNER_TEMP/busbar-oracle" | sha256sum -c -
- chmod +x "$RUNNER_TEMP/busbar-oracle"
- echo "BUSBAR_ORACLE_RUST_BIN=$RUNNER_TEMP/busbar-oracle" >> "$GITHUB_ENV"
- - { uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1, with: { repository: GetBusbar/busbar-release, ref: "${{ env.RELEASE_REF }}", token: "${{ secrets.TURNSTILE_DISPATCH_TOKEN }}", path: busbar-release, persist-credentials: false } } # v7
- # The conformance rigs `conformance record` runs in the full plan need node 22 and pnpm 10 (the mcp
- # suite's official runner, and rolldown's native binding built under it); the Latchkey large image
- # carries node 20, so mcp read not-run (CONFORMANCE-RIGS, 2026-10-02). Only the last shard of a
- # rung that runs conformance or the oracle (needs_release_artifact) records them; the others
- # skip the ~15 s install.
- - { if: "matrix.shard == 4 && needs.preflight.outputs.needs_release_artifact == 'true'", uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413, with: { version: 10 } } # v6.1.0
- - { if: "matrix.shard == 4 && needs.preflight.outputs.needs_release_artifact == 'true'", uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020, with: { node-version: 22 } } # v7.0.0
- # No debuginfo in the dev/test builds: build_test's all-features, feature-matrix and test builds plus
- # the oracle's release build filled the runner's ~41 GiB free disk with debuginfo (ENOSPC, no log).
- # Base and candidate build alike, so the no-worse-than-base verdict is unchanged.
- # GITHUB_TOKEN (contents read) is for `ci hop`'s R3 re-check alone (is this merge group still a
- # current queue entry's head?), which runs before any candidate code; the judge that runs the
- # candidate never holds it. A superseded group's shard builds nothing.
- # THE R2 KEYS HERE ONLY READ (busbar-release 069f032): this job runs candidate code, and the hop
- # refuses to start (INFRA) with the write keys in its environment. Unset read-only keys fail
- # open (no cache). The judge's cache puts land in r2-outbox/, uploaded below and published by
- # `cache-publish`, the one job that holds the write keys (workflow-rules R19).
- - working-directory: busbar-release
- run: $RUNNER_TEMP/busbar-release ci hop
- env: { GITHUB_TOKEN: "${{ github.token }}", R2_RO_KEY_ID: "${{ secrets.R2_CACHE_RO_ACCESS_KEY_ID }}", R2_RO_KEY_SECRET: "${{ secrets.R2_CACHE_RO_SECRET_ACCESS_KEY }}", CARGO_PROFILE_DEV_DEBUG: "0", SHARD: "${{ matrix.shard }}/4" }
- - { if: always(), uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a, with: { name: "turnstile-report-${{ env.SHA }}-shard${{ matrix.shard }}", path: "artifacts/", if-no-files-found: warn } } # v7.0.1
- - { if: always(), uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a, with: { name: "r2-outbox-${{ env.SHA }}-shard${{ matrix.shard }}", path: "r2-outbox/", retention-days: 1, if-no-files-found: ignore, overwrite: true } } # v7.0.1
- # `hop` (the required check) is the hop's ONE verdict. In a merge group and on the predev->dev pull
- # request it folds the shards: BOARD only when every shard reported and boarded and slsa-build is
- # green (or skipped: a pull request whose rung reads no release artifact): a red slsa-build denies
- # it; a shard that is missing, timed out or INFRA makes it INFRA, never green. A pull request
- # INTO PREDEV whose pre-flight
- # chose one runner (OWNER-LOCKED 2026-10-06, BUSBAR-1.6.0 Part 6 "Verification runs once per input")
- # runs it as ONE runner: no shard, no slsa-build; the ladder's predev `enter_pr`, the merge group's
- # gates and steps scoped to the crates the PR changes plus their reverse dependencies, on the PR merged
- # onto the base tip, judged no worse than the base the merge group landed in its 4 shards
- # (BASE_SHARDS). It runs the candidate's code, so it holds no write token either.
- hop:
- needs: [preflight, slsa-build, hop-shard, soak]
- if: ${{ !cancelled() && needs.preflight.result == 'success' }}
- permissions: { contents: read, actions: read }
- runs-on: ${{ (github.event_name == 'pull_request' && github.base_ref != 'dev' && needs.preflight.outputs.one_runner == 'true') && needs.preflight.outputs.runner || 'ubuntu-latest' }}
- timeout-minutes: ${{ (github.event_name == 'pull_request' && github.base_ref != 'dev' && needs.preflight.outputs.one_runner == 'true') && 330 || 30 }}
- env: { ONE_RUNNER: "${{ github.event_name == 'pull_request' && github.base_ref != 'dev' && needs.preflight.outputs.one_runner == 'true' }}", RELEASE_REF: "${{ needs.preflight.outputs.engine_ref }}", ENGINE_SHA256: "${{ needs.preflight.outputs.engine_sha256 }}", RUNNERS: "${{ vars.RUNNERS }}" }
- steps:
- # THE RUN GATE said run=0 (neutral: the merge queue, or another run of this head, judges it).
- # Nothing was built, so this run does NOT JUDGE the head: the required check fails here, never
- # passes by skipping (a skipped required check counts as success and would land an unjudged
- # merge group). The preflight writes one_runner=false on run=0, so nothing below builds either.
- - if: needs.preflight.outputs.run != '1'
- env: { REASON: "${{ needs.preflight.outputs.reason }}" }
- run: |
- echo "::error title=NOT JUDGED::not judged by this run (run=0): $REASON. Not a verdict on the code."
- exit 1
- - if: env.ONE_RUNNER == 'true'
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- with: { ref: "${{ env.SHA }}", path: busbar, fetch-depth: 0, persist-credentials: false }
- # THE ENGINE, downloaded, never compiled: CI's --no-default-features binary of RELEASE_REF from
- # busbar-release's dev-bin release (dev-bin.yml), run only when its sha256 is ENGINE_SHA256.
- # First the pin's shape: a full 40-hex commit and a 64-hex digest, or FAIL.
- - name: Engine (busbar-release at RELEASE_REF, sha256-pinned)
- env: { GH_TOKEN: "${{ secrets.TURNSTILE_DISPATCH_TOKEN }}" }
- run: |
- set -euo pipefail
- [[ "$RELEASE_REF" =~ ^[0-9a-f]{40}$ ]] || { echo "::error title=ENGINE-PIN FAIL::vars.ENGINE_REF is unset or not a full 40-hex busbar-release commit (busbar-release engine pin )"; exit 1; }
- [[ "$ENGINE_SHA256" =~ ^[0-9a-f]{64}$ ]] || { echo "::error title=ENGINE-PIN FAIL::vars.ENGINE_SHA256 is unset or not a 64-hex sha256 (busbar-release engine pin )"; exit 1; }
- api=https://api.github.com/repos/GetBusbar/busbar-release/releases
- name="busbar-release-ci-linux-x86_64-$RELEASE_REF"
- id=$(curl -fsSL -H "Authorization: Bearer $GH_TOKEN" "$api/tags/dev-bin" | jq -r --arg n "$name" '.assets[] | select(.name == $n) | .id')
- [ -n "$id" ] || { echo "::error::dev-bin has no $name"; exit 1; }
- curl -fsSL -H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/octet-stream" -o "$RUNNER_TEMP/busbar-release" "$api/assets/$id"
- echo "$ENGINE_SHA256 $RUNNER_TEMP/busbar-release" | sha256sum -c -
- chmod +x "$RUNNER_TEMP/busbar-release"
- - { uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1, with: { repository: GetBusbar/busbar-release, ref: "${{ env.RELEASE_REF }}", token: "${{ secrets.TURNSTILE_DISPATCH_TOKEN }}", path: busbar-release, persist-credentials: false } } # v7
- # No debuginfo in the dev/test builds (as in the shards: the runner's disk). Read-only R2 keys,
- # the puts to r2-outbox/ for `cache-publish` (as in the shards: the judge runs candidate code).
- - if: env.ONE_RUNNER == 'true'
- working-directory: busbar-release
- run: $RUNNER_TEMP/busbar-release ci hop
- env: { R2_RO_KEY_ID: "${{ secrets.R2_CACHE_RO_ACCESS_KEY_ID }}", R2_RO_KEY_SECRET: "${{ secrets.R2_CACHE_RO_SECRET_ACCESS_KEY }}", CARGO_PROFILE_DEV_DEBUG: "0", BASE_SHARDS: "4" }
- - if: always() && env.ONE_RUNNER == 'true'
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
- with: { name: "turnstile-report-${{ env.SHA }}", path: "artifacts/", if-no-files-found: warn }
- - if: always() && env.ONE_RUNNER == 'true'
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
- with: { name: "r2-outbox-${{ env.SHA }}-one", path: "r2-outbox/", retention-days: 1, if-no-files-found: ignore, overwrite: true }
- - if: env.ONE_RUNNER != 'true'
- continue-on-error: true
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
- with: { pattern: "turnstile-report-${{ env.SHA }}-shard*", path: shards }
- - name: Fold the shards
- if: env.ONE_RUNNER != 'true'
- env: { GH_TOKEN: "${{ github.token }}", SLSA_BUILD: "${{ needs.slsa-build.result }}", SOAK: "${{ needs.soak.result }}", SLSA_READ: "${{ github.event_name == 'merge_group' || needs.preflight.outputs.needs_release_artifact == 'true' }}", SOAK_READ: "${{ needs.preflight.outputs.soak }}" }
- run: |
- mkdir -p merge artifacts
- # A SKIPPED needed job DENIES (busbar-release 03cb349) unless the rung does not read it:
- # slsa-build is read in a merge group and wherever needs_release_artifact, the soak where
- # the ladder asks for it (soak=true). OLD-ENGINE SHIM (remove once ENGINE_REF is at or past
- # 03cb349): an engine without --must-run takes a skip as neutral itself.
- must=()
- if "$RUNNER_TEMP/busbar-release" ci turnstile merge --help | grep -q -- --must-run; then
- must=(--must-run "slsa-build=$SLSA_READ" --must-run "soak=$SOAK_READ")
- fi
- # The run's own job list: a shard with no report is INFRA:platform when its job was never
- # created (GitHub dropped it, as in the 2026-10-07 16:52-17:01Z Actions outage), else
- # INFRA:ours. A list that cannot be read leaves plain INFRA; the hop is never green on it.
- gh api "repos/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID/attempts/$GITHUB_RUN_ATTEMPT/jobs?per_page=100" \
- >"$GITHUB_WORKSPACE/shard-jobs.json" || true
- for k in 1 2 3 4; do
- d="shards/turnstile-report-$SHA-shard$k"
- mkdir -p "merge/shard-$k" "artifacts/shard-$k"
- [ -f "$d/turnstile-report.json" ] && cp "$d/turnstile-report.json" "merge/shard-$k/"
- [ -d "$d" ] && cp -r "$d/." "artifacts/shard-$k/"
- done
- cd busbar-release
- "$RUNNER_TEMP/busbar-release" \
- ci turnstile merge --sha "$SHA" --shards 4 --dir "$GITHUB_WORKSPACE/merge" --out "$GITHUB_WORKSPACE/artifacts/turnstile-report.json" \
- --shard-jobs "$GITHUB_WORKSPACE/shard-jobs.json" --needs "slsa-build=$SLSA_BUILD" --needs "soak=$SOAK" "${must[@]}"
- # The durable verdict (verdict/), from the folded report and every shard's step logs: the artifact publish reads.
- - if: always() && needs.preflight.outputs.run == '1'
- working-directory: busbar-release
- run: $RUNNER_TEMP/busbar-release ci verdict
- - { if: "always() && needs.preflight.outputs.run == '1'", uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a, with: { name: "verdict-${{ env.SHA }}", path: "verdict/", retention-days: 90, if-no-files-found: warn } } # v7.0.1
- # THE R2 CACHE'S ONE WRITER (busbar-release 069f032). The hop jobs judge with READ-ONLY keys and
- # leave their cache puts (base reports, step PASSes) in r2-outbox/ artifacts; this job uploads them
- # with the WRITE keys. It runs NO candidate code: it checks out nothing, downloads the engine
- # (sha256-pinned) and the outboxes, and runs `ci cache-publish`, which uploads only objects under
- # the cache's own prefixes. It is no part of the verdict: nothing needs it, it is no required
- # check, and it fails open (no keys, no outbox, a failed upload: a later cache miss).
- # workflow-rules R19: no job that runs candidate code names the write keys.
- cache-publish:
- needs: [preflight, hop-shard, hop]
- if: ${{ always() && needs.preflight.result == 'success' && needs.preflight.outputs.run == '1' }}
- runs-on: ubuntu-latest
- timeout-minutes: 15
- continue-on-error: true
- permissions: { contents: read }
- env: { RELEASE_REF: "${{ needs.preflight.outputs.engine_ref }}", ENGINE_SHA256: "${{ needs.preflight.outputs.engine_sha256 }}" }
- steps:
- # THE ENGINE, downloaded, never compiled: CI's --no-default-features binary of RELEASE_REF from
- # busbar-release's dev-bin release (dev-bin.yml), run only when its sha256 is ENGINE_SHA256.
- # First the pin's shape: a full 40-hex commit and a 64-hex digest, or FAIL.
- - name: Engine (busbar-release at RELEASE_REF, sha256-pinned)
- env: { GH_TOKEN: "${{ secrets.TURNSTILE_DISPATCH_TOKEN }}" }
- run: |
- set -euo pipefail
- [[ "$RELEASE_REF" =~ ^[0-9a-f]{40}$ ]] || { echo "::error title=ENGINE-PIN FAIL::vars.ENGINE_REF is unset or not a full 40-hex busbar-release commit (busbar-release engine pin )"; exit 1; }
- [[ "$ENGINE_SHA256" =~ ^[0-9a-f]{64}$ ]] || { echo "::error title=ENGINE-PIN FAIL::vars.ENGINE_SHA256 is unset or not a 64-hex sha256 (busbar-release engine pin )"; exit 1; }
- api=https://api.github.com/repos/GetBusbar/busbar-release/releases
- name="busbar-release-ci-linux-x86_64-$RELEASE_REF"
- id=$(curl -fsSL -H "Authorization: Bearer $GH_TOKEN" "$api/tags/dev-bin" | jq -r --arg n "$name" '.assets[] | select(.name == $n) | .id')
- [ -n "$id" ] || { echo "::error::dev-bin has no $name"; exit 1; }
- curl -fsSL -H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/octet-stream" -o "$RUNNER_TEMP/busbar-release" "$api/assets/$id"
- echo "$ENGINE_SHA256 $RUNNER_TEMP/busbar-release" | sha256sum -c -
- chmod +x "$RUNNER_TEMP/busbar-release"
- - { continue-on-error: true, uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c, with: { pattern: "r2-outbox-${{ env.SHA }}-*", path: r2-outbox, merge-multiple: true } } # v8.0.1
- # OLD-ENGINE SHIM (remove once ENGINE_REF is at or past 069f032): an engine without the verb
- # wrote the cache from the hop itself.
- - name: Publish the hop's cache puts (the R2 write keys, in this job only)
- env: { R2_KEY_ID: "${{ secrets.R2_CACHE_ACCESS_KEY_ID }}", R2_KEY_SECRET: "${{ secrets.R2_CACHE_SECRET_ACCESS_KEY }}" }
- run: |
- if ! "$RUNNER_TEMP/busbar-release" ci --help | grep -q cache-publish; then echo "engine $RELEASE_REF has no cache-publish: nothing published"; exit 0; fi
- "$RUNNER_TEMP/busbar-release" ci cache-publish --outbox "$GITHUB_WORKSPACE/r2-outbox"
- # A job with a write token (as is preflight-act). It never checks out the candidate: busbar-release at RELEASE_REF,
- # and the hop's verdict artifact. The sticky PR comment and the branch health issue (`ci publish`); a
- # hop whose runner was lost or timed out wrote no verdict, so `ci fallback` records the job and step
- # conclusions the API keeps after the logs are purged, and the run gate's reason when it refused the
- # run (STACK, wiring drift). A run the gate held neutral (run=0, preflight green) posts nothing: the
- # queue or another run judges that head.
- publish:
- needs: [preflight, hop]
- if: always() && needs.preflight.result != 'skipped' && (needs.preflight.result != 'success' || needs.preflight.outputs.run == '1')
- runs-on: ubuntu-latest
- timeout-minutes: 10
- # statuses: write: a merge group DENIED as its PR's own red (first in the group, or behind a green
- # group) marks that PR's head red
- # ("merge queue" status) beside its sticky verdict, so the watcher's fix agent starts at failure.
- permissions: { contents: read, actions: read, checks: read, pull-requests: write, statuses: write }
- env: { RELEASE_REF: "${{ needs.preflight.outputs.engine_ref }}", ENGINE_SHA256: "${{ needs.preflight.outputs.engine_sha256 }}" }
- steps:
- # THE ENGINE, downloaded, never compiled: CI's --no-default-features binary of RELEASE_REF from
- # busbar-release's dev-bin release (dev-bin.yml), run only when its sha256 is ENGINE_SHA256.
- # First the pin's shape: a full 40-hex commit and a 64-hex digest, or FAIL.
- - name: Engine (busbar-release at RELEASE_REF, sha256-pinned)
- env: { GH_TOKEN: "${{ secrets.TURNSTILE_DISPATCH_TOKEN }}" }
- run: |
- set -euo pipefail
- [[ "$RELEASE_REF" =~ ^[0-9a-f]{40}$ ]] || { echo "::error title=ENGINE-PIN FAIL::vars.ENGINE_REF is unset or not a full 40-hex busbar-release commit (busbar-release engine pin )"; exit 1; }
- [[ "$ENGINE_SHA256" =~ ^[0-9a-f]{64}$ ]] || { echo "::error title=ENGINE-PIN FAIL::vars.ENGINE_SHA256 is unset or not a 64-hex sha256 (busbar-release engine pin )"; exit 1; }
- api=https://api.github.com/repos/GetBusbar/busbar-release/releases
- name="busbar-release-ci-linux-x86_64-$RELEASE_REF"
- id=$(curl -fsSL -H "Authorization: Bearer $GH_TOKEN" "$api/tags/dev-bin" | jq -r --arg n "$name" '.assets[] | select(.name == $n) | .id')
- [ -n "$id" ] || { echo "::error::dev-bin has no $name"; exit 1; }
- curl -fsSL -H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/octet-stream" -o "$RUNNER_TEMP/busbar-release" "$api/assets/$id"
- echo "$ENGINE_SHA256 $RUNNER_TEMP/busbar-release" | sha256sum -c -
- chmod +x "$RUNNER_TEMP/busbar-release"
- - { uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1, with: { repository: GetBusbar/busbar-release, ref: "${{ env.RELEASE_REF }}", token: "${{ secrets.TURNSTILE_DISPATCH_TOKEN }}", path: busbar-release, persist-credentials: false } } # v7
- - { id: verdict, continue-on-error: true, uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c, with: { name: "verdict-${{ env.SHA }}", path: verdict } } # v8.0.1
- - if: steps.verdict.outcome == 'success'
- working-directory: busbar-release
- run: $RUNNER_TEMP/busbar-release ci publish
- env: { GH_TOKEN: "${{ github.token }}", HEALTH_BRANCHES: none }
- - if: needs.preflight.result != 'success' || needs.hop.result != 'success'
- working-directory: busbar-release
- run: $RUNNER_TEMP/busbar-release ci fallback
- env: { GH_TOKEN: "${{ github.token }}", PR_NUMBER: "${{ github.event.pull_request.number }}", PREFLIGHT_RESULT: "${{ needs.preflight.result }}", PREFLIGHT_REASON: "${{ needs.preflight.outputs.reason }}", HOP_RESULT: "${{ needs.hop.result }}", HEALTH_BRANCHES: none }
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 8aea311471..5e4db11e43 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -26,7 +26,7 @@ This page is the short version. Where the two differ, Part 6 wins.
both sides moved (theirs + ours − base; anything else stays a conflict):
`git config merge.ledger-count.driver "cargo xtask ledger-merge %O %A %B"`.
- Builds, tests, clippy, the gates and the oracle run in CI (ModernCI for `predev` PRs; `.github/workflows/promote.yml` for the promotion).
+ Builds, tests, clippy, the gates and the oracle run in CI: ModernCI, for `predev` PRs and for every promotion (`.ci/*.yaml`).
3. Push the lane and open a PR into `predev`. ModernCI (required check `ci`) judges and lands it.
Then stop; CI is the proof and a red comes back as a task.
4. The PR body says what changed, why, and the ruling or issue it implements. No AI attribution.
diff --git a/README.md b/README.md
index fb5f0810aa..aba9701729 100644
--- a/README.md
+++ b/README.md
@@ -13,7 +13,6 @@
-
diff --git a/RELEASE.md b/RELEASE.md
index d13efc6719..8680b85a0c 100644
--- a/RELEASE.md
+++ b/RELEASE.md
@@ -1,33 +1,21 @@
# Releasing Busbar
-The branch ladder is **predev → dev → qa → main**. Pull requests into `predev` are judged and landed by
-ModernCI (required check `ci`, configured by [`.ci/default.yaml`](.ci/default.yaml)). The promotion
-from `predev` to `dev` runs [`.github/workflows/promote.yml`](.github/workflows/promote.yml). The release engine lives in the private `GetBusbar/busbar-release` repository
-(pinned outside this repo by the org variables `ENGINE_REF` and `ENGINE_SHA256`, set with
-`busbar-release engine pin `; `workflow-rules` R16 holds promote.yml to a 40-hex check and a
-sha256 verify of the engine at run time); busbar-release's `crates/busbar-release-train/ladder.json`
-says what entering each rung needs.
-
-| Rung entered (the PR's base) | What the hop runs | Consent |
-|---|---|---|
-| **`predev`** (from a `lane-*` branch) | every registered gate (`cargo xtask gate --list`, each by name) | none |
-| **`dev`** | the gates, the oracle against the 1.5.5 golden, conformance | none |
-| **`qa`** | the same, plus build + test, then the release binaries and the container | the owner (the org ruleset keeps `qa` locked) |
-| **`main`** | the same, then publish | the owner (the org ruleset keeps `main` locked) |
-
-A hop has two jobs, both in `promote.yml`:
-
-* **`preflight`**, on a free runner: `cargo fmt --all -- --check`, `cargo metadata --locked`, and the
- generated C header is current (`cargo xtask gate abi-header --write` leaves no diff).
-* **`hop`**, on a Latchkey runner sized by the rung: the pinned engine plans the hop and admits the
- candidate sha, and the verdict is uploaded as an artifact. When
- the rung needs no consent and the repository variable `PROMOTE_BOARD` is `true`, the engine
- fast-forwards the base branch to the exact sha it tested. Both promotions are **fast-forward
- pushes**: linear history is what makes "main's HEAD *is* the qa commit" true.
-
-> The entry builds a plan names (`release-binaries`, `container`, `publish`) are not yet ported into
-> the engine: `promote.yml` fails a hop that plans one with that message rather than skipping it.
-> Nothing is built or published until they land in busbar-release.
+The branch ladder is **predev → dev → qa → main**, and ModernCI judges and moves every rung from
+this repository's branch files (OWNER 2026-10-12, "ModernCI only"; the ruling is
+[`docs/design/BUSBAR-1.6.0.md`](docs/design/BUSBAR-1.6.0.md) Part 6, "BUSBAR-RELEASE RETIRED FROM
+BUSBAR"). A branch file is named for the branch being moved and says what the move must pass.
+
+| Move | Branch file | What it runs | Consent |
+|---|---|---|---|
+| a `lane-*` branch into **`predev`** | [`.ci/default.yaml`](.ci/default.yaml) | preflight, every registered gate, clippy, compile, test, doctest, deletion matrix, `cargo deny`; no worse than predev (required check `ci`) | none |
+| **`predev`** into **`dev`** | [`.ci/predev.yaml`](.ci/predev.yaml) | the gates, the oracle against the 1.5.5 golden, conformance, `scripts/verify-1.6.0-done.sh`, the Q128 audit | the owner's approval |
+| **`dev`** into **`qa`** | `.ci/dev.yaml` | the same, plus build and test, the fuzz smoke and the signing gate, the soak, the release binaries and the container | the owner (the org ruleset keeps `qa` locked) |
+| **`qa`** into **`main`** | `.ci/qa.yaml` | the same; on landing, the release is signed and published from the build that was judged, never rebuilt | the owner (the org ruleset keeps `main` locked) |
+
+`.ci/dev.yaml` and `.ci/qa.yaml` are not written yet: until they land, nothing is built or published
+from `qa` or `main`. The busbar-release hop (`promote.yml`, the engine pin) is deleted. busbar-release
+remains the home of the oracle (`busbar-oracle`, pinned by `testing/shadow-oracle/oracle-rust.pin`),
+the soak and the plugin fleet generator.
**The tag is an OUTPUT of a green release, never an input.** Nobody tags by hand; no workflow is
triggered by a `v*` tag (`cargo xtask gate workflow-rules`, rule R1). Docker Hub tag immutability
@@ -49,19 +37,17 @@ python3 .github/scripts/roll_changelog.py 1.6.0 CHANGELOG.md
```
That bumps `crates/busbar/Cargo.toml` + `Cargo.lock`, regenerates the committed OpenAPI schema, and
-rolls `[Unreleased]` to `[1.6.0]` with today's date. It does **not** tag. Open the result as a pull
-request against `dev` and let the hop run.
+rolls `[Unreleased]` to `[1.6.0]` with today's date. It does **not** tag. Land it on `predev` like
+any other change; it reaches `dev` with the next promotion.
## Moving a version up the ladder
-Open a pull request whose base is the next rung (`dev` into `qa`, `qa` into `main`; a lane branch into `predev`). `promote.yml`
-runs the hop on the PR; the run's check names are `preflight` and `hop`. A red hop is fixed
-forward on the lower branch and the pull request re-run: every push re-runs the hop on the new sha.
+ModernCI opens and judges each promotion (`modernci call moves` shows how each stands). A red move
+is fixed forward on the lower branch: every push re-judges it on the new head.
## Branch protection this assumes
-`qa` and `main` require the two contexts `preflight` and `hop` (plus the org ruleset's owner
-consent).
+`predev` requires ModernCI's check `ci`; `qa` and `main` take the owner's consent (the org ruleset).
Branch protection lives in repository rulesets, not in a script (see `.github/required-status-checks.md`).
Keep `enforce_admins: true`, `required_linear_history: true`, `allow_force_pushes: false` and
diff --git a/docs/ci/actions-pins.md b/docs/ci/actions-pins.md
index 93e848d582..c3f9f461dc 100644
--- a/docs/ci/actions-pins.md
+++ b/docs/ci/actions-pins.md
@@ -63,8 +63,8 @@ PR that preserves the trailing tag comment.
## Every action currently in the tree
-Every `uses:` in `.github/workflows/` (`promote.yml` and the reusable `plugin-*.yml` workflows) is one
-of the nine distinct `(action, ref)` pairs above, pinned to a commit SHA with its tag kept as a
+Every `uses:` in `.github/workflows/` (the reusable `plugin-*.yml` workflows; `promote.yml` is deleted)
+is one of the distinct `(action, ref)` pairs above, pinned to a commit SHA with its tag kept as a
trailing comment. This table is the independent re-verification of that state. There is no
`.github/actions` composite action beyond `cargo-home`, and `R14` is proven able to catch a floating
tag inside one by `r14_reads_uses_out_of_a_composite_action_too_not_only_workflows` in
diff --git a/docs/design/BUSBAR-1.6.0.md b/docs/design/BUSBAR-1.6.0.md
index 5178d9ec6f..bf77b09c7a 100644
--- a/docs/design/BUSBAR-1.6.0.md
+++ b/docs/design/BUSBAR-1.6.0.md
@@ -2893,7 +2893,7 @@ shape guarantee each is a clean add, never a break. That is "fits-or-cleanly-ext
> definition and the owner rulings that shape execution.
## DONE = DEV-GREEN (the definition every wave drives to)
-DEV-GREEN is reached when ALL of the following hold and turnstile boards ADMIT:
+DEV-GREEN is reached when ALL of the following hold ~~and turnstile boards ADMIT~~ and ModernCI's `predev` → `dev` move (`.ci/predev.yaml`) is green and lands (Part 6, "BUSBAR-RELEASE RETIRED FROM BUSBAR", OWNER 2026-10-12/13):
1. **Oracle byte-identical vs the 1.5.5 golden on every family** (money sacred, #9/#10). The ONLY
accepted diffs are the entries of `testing/shadow-oracle/accepted-differences.json`, all 45
re-signed by the owner 2026-09-30. Everything else byte-green.
@@ -2911,7 +2911,7 @@ DEV-GREEN is reached when ALL of the following hold and turnstile boards ADMIT:
independent architecture audit on the DEV-GREEN candidate sha, graded against the Laws and THE DESIGN; it
passes with no open BLOCK findings. The "two audits ≥9/10" requirement is struck (#60; the §11a baseline
that score depended on never existed).
-6. **turnstile BOARD = ADMIT → train ff `predev`→`dev` = DEV-GREEN** (#32/#67).
+6. ~~**turnstile BOARD = ADMIT → train ff `predev`→`dev` = DEV-GREEN** (#32/#67).~~ SUPERSEDED 2026-10-13 by Part 6, "BUSBAR-RELEASE RETIRED FROM BUSBAR": **ModernCI's `predev` → `dev` move green on `.ci/predev.yaml` and landed = DEV-GREEN.**
## money-touching step shadow-oracle-gated; each wave loops `/codeaudit` to 2 clean passes #60)
@@ -2984,8 +2984,8 @@ Agents: (a) land `integration/conformance-b`; conformance MUST-set green for all
arm + green every gate in the DONE battery above (each RED-provable, red-before-green).
### W8 — BOARD → DEV-GREEN [#32/#67]
-turnstile runs gates + oracle + conformance → BOARD verdict. On ADMIT, the train fast-forwards
-`predev`→`dev`. **DEV-GREEN.**
+~~turnstile runs gates + oracle + conformance → BOARD verdict. On ADMIT, the train fast-forwards
+`predev`→`dev`.~~ ModernCI runs `.ci/predev.yaml` (gates, oracle, conformance, the done-oracle, the Q128 audit) on the `predev` → `dev` move; green, it lands. **DEV-GREEN.**
---
@@ -3110,14 +3110,60 @@ Everything in Parts 1–5 describes the product. This Part describes the machine
lives in **two repos**, and that split is the single most common source of "I can't find the
autoscaler" confusion.
+## BUSBAR-RELEASE RETIRED FROM BUSBAR; MODERNCI IS BUSBAR'S CI (OWNER 2026-10-12, 2026-10-13)
+
+> **OWNER 2026-10-12:** *"ModernCI only."* **OWNER 2026-10-13:** *"delete anything you don't need
+> anymore, yes"* and *"Yes, we keep the busbar-release repo, slimmed down. ModernCI replaces its CI
+> half; the busbar-specific tools stay."* This section governs Part 6. Every passage below that
+> names the hop, the turnstile, the train, the conductor, the runner gateway, `promote.yml` or the
+> engine pin is SUPERSEDED by it and is struck where it stands.
+
+**ModernCI judges and moves every rung, from busbar's own branch files** (ModernCI's answer,
+2026-10-13: a branch file per moving branch, `promote: auto` opens the promotion, `on.landed` steps
+run on each new target tip):
+
+| Move | Branch file | What it runs |
+|---|---|---|
+| `lane-*` → `predev` | `.ci/default.yaml` | preflight, gates, clippy, compile, test, doctest, deletion-matrix, deny; no worse than predev; ModernCI's merge train lands it (required check `ci`) |
+| `predev` → `dev` | `.ci/predev.yaml` | gates, the oracle against the 1.5.5 golden, conformance, `verify-1.6.0-done.sh`, the Q128 audit; the owner's approval. **Its green landing is DEV-GREEN** (Part 5) |
+| `dev` → `qa` | `.ci/dev.yaml` | the `dev` rung plus build and test, the fuzz smoke and the signing gate (today `qa/pipeline-checks.json`), the soak, the release binaries and the container; the owner's approval |
+| `qa` → `main` | `.ci/qa.yaml` | the same battery; on landing, the `release` step signs and publishes the build it judged (no rebuild) and a `run` step does cosign and the container push, with secrets scoped to `on_landed` |
+
+**busbar-release stays, as the tools repo** (judged by ModernCI, not by its own Actions, once
+ModernCI registers it). KEEP: `busbar-release-oracle` (the money judge `busbar-oracle`, pinned by
+`testing/shadow-oracle/oracle-rust.pin`: one commit, one sha256 per architecture), with
+`busbar-release-fetch` and `busbar-release-golden-scope`; the published `busbar-oracle-linux--[`
+assets every pin names, kept for as long as a busbar branch pins them; `oracle-rerecord` (the
+add-only golden recorder); `busbar-release-soak`; `busbar-release-plugin` (the fleet generator:
+`plugin new|sync|check`, `template/`) and its daily `plugin-drift` check, which stays on Actions
+until ModernCI's scheduled checks are live (ModernCI PR #91); `busbar-release-github` (the plugin
+generator and the soak call GitHub through it); and the `busbar-release` command line, cut to those
+verbs. DELETE: the conductor and its state repo, the turnstile, the hop, the train and the ladder
+(`ci` verbs, `ladder.json`, `preflight.json`), the runner gateway (`busbar-release-runners`,
+`vars.RUNNERS` once the last workflow that reads it is gone), the EC2 backend and autoscaler, the
+runner canary and probe, the `dev` dispatch workflow, the engine build on `dev-bin`, and the engine
+pin (the org variables `ENGINE_REF`, `ENGINE_SHA256`, `ENGINE_JUDGE`, and the R2 cache keys the
+hop alone read). In busbar: `.github/workflows/promote.yml` and the `workflow-rules` rows that
+held it (R16 the engine pin, R18 its release-artifact wiring, R19 its R2 keys).
+
+**The oracle and the golden do not move.** The 1.5.5 golden stays in
+`testing/shadow-oracle/golden/1.5.5`, read-only and add-only (above, "The 1.5.5 golden is
+read-only"). The judge is built from busbar-release at the pinned commit; ModernCI runs only that
+pinned install, checked against the pin's sha256 for its host, and a candidate never picks its own
+judge.
+
+**The plugin fleet.** Each plugin repo is judged by ModernCI once it is registered there with its
+own branch files; until then the reusable `plugin-ci.yml`, `plugin-consumer-verify.yml`,
+`plugin-release.yml` and `plugin-repin.yml` stay, because the fleet's rendered callers name them.
+
## The branch roster (#67) — four branches, and no others
| Branch | Rule | CI |
|---|---|---|
-| `predev` | **permanent WIP.** Every in-flight session lands here and forks from here. | Pull requests only, landed through GitHub's merge queue; `promote.yml` `preflight` + `hop` are the required checks on both the pull request (the fast pre-check) and the merge group (the full plan, the proof of record), and the verdict is "no worse than base" (see "How work lands" below). |
-| `dev` | **release-train-write-only.** "Next version's WIP, nowhere near done." | full `ci.yml` |
-| `qa` | promotion target. **The one release build happens here** — the PGO build that ships, tested for real. | full CI + the real-media matrix |
-| `main` | **a push here cuts a release** — tag, GitHub Release, container promotion, `latest` moved. Irreversible. **`main` never compiles:** it tags and publishes the artifacts, by digest, that `qa` built and verified. | release orchestration |
+| `predev` | **permanent WIP.** Every in-flight session lands here and forks from here. | ~~Pull requests only, landed through GitHub's merge queue; `promote.yml` `preflight` + `hop` are the required checks on both the pull request (the fast pre-check) and the merge group (the full plan, the proof of record), and the verdict is "no worse than base" (see "How work lands" below).~~ SUPERSEDED 2026-10-13 by "BUSBAR-RELEASE RETIRED FROM BUSBAR" above: pull requests only, judged by ModernCI on `.ci/default.yaml` (required check `ci`), no worse than predev, landed by ModernCI's merge train. |
+| `dev` | **release-train-write-only.** "Next version's WIP, nowhere near done." | ~~full `ci.yml`~~ SUPERSEDED 2026-10-13 by "BUSBAR-RELEASE RETIRED FROM BUSBAR" above: `.ci/predev.yaml` judges the move in |
+| `qa` | promotion target. **The one release build happens here** — the PGO build that ships, tested for real. | ~~full CI + the real-media matrix~~ SUPERSEDED 2026-10-13 by "BUSBAR-RELEASE RETIRED FROM BUSBAR" above: `.ci/dev.yaml` judges the move in |
+| `main` | **a push here cuts a release** — tag, GitHub Release, container promotion, `latest` moved. Irreversible. **`main` never compiles:** it tags and publishes the artifacts, by digest, that `qa` built and verified. | ~~release orchestration~~ SUPERSEDED 2026-10-13 by "BUSBAR-RELEASE RETIRED FROM BUSBAR" above: `.ci/qa.yaml` judges the move in; its `on.landed` steps publish |
**Never push `dev`, `qa` or `main` by hand.** `predev` and `dev` take changes by pull request only
(ruleset; OWNER R3 2026-10-01); `--force-with-lease` is permitted only on your own `lane-*` branch. **Branch protection is on `qa` only** —
@@ -3138,21 +3184,21 @@ Nothing is rebuilt between them.
**`GetBusbar/busbar`** — the product. `crates/`, `xtask/` (every gate), `scripts/` (including
`verify-1.6.0-done.sh`, the definition of done), `conformance/`, `testing/shadow-oracle/`.
-**`GetBusbar/busbar-release`** — the engine, nine crates. Being redesigned as one PR state machine (busbar-release docs/ARCHITECTURE.md).
+~~**`GetBusbar/busbar-release`** — the engine, nine crates. Being redesigned as one PR state machine (busbar-release docs/ARCHITECTURE.md).~~ SUPERSEDED 2026-10-13 by "BUSBAR-RELEASE RETIRED FROM BUSBAR" above: busbar-release is the tools repo; the rows below for the turnstile, the train, the autoscaler and the CI half of the command line are deleted.
| Crate | Job |
|---|---|
-| `busbar-release-turnstile` | The gate. Runs the batteries and returns a **BOARD** verdict — `ADMIT` or deny. Nothing reaches `dev` without it. |
-| `busbar-release-train` | The mover. Fast-forwards `predev` → `dev` → `qa` → `main` in lockstep, refusing unless every precondition holds. |
+| `busbar-release-turnstile` | ~~The gate. Runs the batteries and returns a **BOARD** verdict — `ADMIT` or deny. Nothing reaches `dev` without it.~~ DELETE |
+| `busbar-release-train` | ~~The mover. Fast-forwards `predev` → `dev` → `qa` → `main` in lockstep, refusing unless every precondition holds.~~ DELETE |
| `busbar-release-oracle` | The byte-identity oracle: this build vs the published 1.5.5 binary. |
-| `busbar-release-autoscaler` | Runner capacity. Ships the **`busbar-fleet`** binary (`src/bin/busbar-fleet.rs`) — this is the "fleet" tool; there is no separate fleet crate. |
+| `busbar-release-autoscaler` | ~~Runner capacity. Ships the **`busbar-fleet`** binary (`src/bin/busbar-fleet.rs`) — this is the "fleet" tool; there is no separate fleet crate.~~ DELETE |
| `busbar-release-corpus` | The recorded request corpus the oracle replays. |
-| `busbar-release-cli` | The one `busbar-release` command line: ship, dev, watch, status, plugin, and the CI internals under `ci`. |
+| `busbar-release-cli` | ~~The one `busbar-release` command line: ship, dev, watch, status, plugin, and the CI internals under `ci`.~~ KEEP only the oracle, soak and plugin verbs |
| `busbar-release-github` | The one door to GitHub: batched GraphQL reads, REST writes, shared rate-limit and retry policy. |
| `busbar-release-plugin` | The plugin fleet generator: renders `template/` into every `busbar--` repo, with drift check, sync and repin. |
| `busbar-release-soak` | The sustained mixed-traffic soak of a built binary against the oracle's mocks, judged row by row. |
-Its workflows: `turnstile.yml`, `fleet-control.yml`, `oracle-rerecord.yml`, `docker-autoscaler.yml`, `ci.yml`.
+~~Its workflows: `turnstile.yml`, `fleet-control.yml`, `oracle-rerecord.yml`, `docker-autoscaler.yml`, `ci.yml`.~~ SUPERSEDED 2026-10-13 by "BUSBAR-RELEASE RETIRED FROM BUSBAR" above.
**The marketing site is observed, never depended on** (owner, 2026-09-06). Nothing in the release
path triggers, `needs:` or waits on the site deploy; the site keeps its own trigger. Site checks
@@ -3165,7 +3211,7 @@ fully torn down — zero instances, zero volumes, zero NAT gateways, zero elasti
both cheaper and the money oracle's home (**#56 wins over #29** — the oracle rides Latchkey, it does
not need a dedicated fleet box; `CI_RUNNER_ONDEMAND_FLOOR` is 0, not 2).
-**Runner gateway (OWNER 2026-10-10, supersedes the 2026-09-28 capacity order):** every job asks for a logical size (small, medium, large, xlarge), never a vendor label. One table maps each size to a backend (latchkey, ec2 or github), so switching a backend is a one-line change and nothing else breaks. Today every size maps to Latchkey (account cap 80 concurrent jobs); EC2 burst is off. A daily canary job runs on every backend in the table, including the switched-off ones, so a dormant backend is proven before anyone switches to it. #78's spend breaker binds every paid backend.
+~~**Runner gateway (OWNER 2026-10-10, supersedes the 2026-09-28 capacity order):** every job asks for a logical size (small, medium, large, xlarge), never a vendor label. One table maps each size to a backend (latchkey, ec2 or github), so switching a backend is a one-line change and nothing else breaks. Today every size maps to Latchkey (account cap 80 concurrent jobs); EC2 burst is off. A daily canary job runs on every backend in the table, including the switched-off ones, so a dormant backend is proven before anyone switches to it. #78's spend breaker binds every paid backend.~~ SUPERSEDED 2026-10-13 by "BUSBAR-RELEASE RETIRED FROM BUSBAR" above: ModernCI places each check on its backends.
Agent compute is Latchkey only, through one
shared job budget (`1.6.0-TODO.md`, THE RULES 6.1-6.3); the owner has ruled no extra build capacity
(2026-09-29). EC2 is allowed for the performance phase (§11.9).
@@ -3270,7 +3316,7 @@ now fixed, one was a misdiagnosis, and one dissolves on the first promotion.** C
## Size is not a CI check (owner 2026-10-02)
-The hop (`promote` → `preflight` + `hop`) never denies a PR for size, at any rung. Line counts are
+ModernCI ~~The hop (`promote` → `preflight` + `hop`)~~ never denies a PR for size, at any rung. Line counts are
not measured in CI at all: the construction gate's `loc-ceilings:*` and `surface-ceiling:*` rows,
its `ceiling-rose` / `ceiling-slack` rows, `ship-ready`'s two ceiling rows and the per-cell counts
of `kind-isolation:matrix` are deleted, and with them every hand-kept size integer in
@@ -3297,33 +3343,36 @@ in the default `pull_request` types.
## THE CONDUCTOR (OWNER 2026-10-11) — one deterministic driver; authors write code, nothing else
-> **OWNER 2026-10-11.** *"We have the pieces, not a thing pushing things through the pieces, and
+> **SUPERSEDED 2026-10-13 by "BUSBAR-RELEASE RETIRED FROM BUSBAR" above.** The conductor is deleted: ModernCI is the one program that judges, trains and
+> moves busbar's branches. The text below is struck, kept for its history.
+
+> ~~**OWNER 2026-10-11.** *"We have the pieces, not a thing pushing things through the pieces, and
> that can't be an agent."* *"I wake up one day, never use agents again, replace them with human Bob,
> and the system still works. Bob codes, runs one command, and it does its thing and tells Bob (or
> the agent) if they need to do something else."* *"Every second of waste is $ burnt — money and
> time."* *"The workflow is the workflow"* (no run cap). Supersedes, where they disagree: "How work
> lands" item 3 (GitHub's merge queue, OWNER 2026-10-02), "Ship and stop", and every rule that has
-> an agent push, prove, retry, land, promote, or be spawned by the pipeline.
+> an agent push, prove, retry, land, promote, or be spawned by the pipeline.~~
-**Why (measured 2026-10-10/11, busbar-release's 2-hour audit ledger, 9 windows):** Latchkey $442,
+~~**Why (measured 2026-10-10/11, busbar-release's 2-hour audit ledger, 9 windows):** Latchkey $442,
of which $254 (57%) bought nothing: 166 of 264 merge-group runs were cascades thrown away behind a
red group, 211 of 419 PR runs were red (fixes nobody had run), and the proof that would have caught
-them cheaply was refused by a run cap.
+them cheaply was refused by a run cap.~~
-**The rule.** One program decides; authors only write code. An AUTHOR is whoever writes code — a
+~~**The rule.** One program decides; authors only write code. An AUTHOR is whoever writes code — a
person or an agent, interchangeably; the pipeline never knows or cares which, and never spawns one.
The CONDUCTOR is `busbar-release conductor`, one always-on service (today on the build server, the
M3 at `10.1.7.14`; it runs anywhere from one config directory). It is deterministic code: every
decision is a rule over measured facts. It updates itself from each green `main` release of
-busbar-release; nothing about running it is agent-driven.
+busbar-release; nothing about running it is agent-driven.~~
-**The author's whole interface is one command:** `busbar-release submit` from a branch (it replaces
+~~**The author's whole interface is one command:** `busbar-release submit` from a branch (it replaces
`ship`). The conductor takes it from there and tells the author only when they must act — what is
red, with the output, and the one thing to do next (fix and `submit` again; resolve a conflict;
owner sign-off needed) — on the terminal, as the PR's one verdict comment, and in `busbar-release
-status`.
+status`.~~
-**The stations after `submit`.** The conductor owns every move.
+~~**The stations after `submit`.** The conductor owns every move.~~
| Station | What happens | Junk it refuses |
|---|---|---|
@@ -3332,46 +3381,46 @@ status`.
| TRAIN | The conductor batches the PRs whose PR check is green onto the predev tip (`train/`, one merge per PR, a conflicting PR goes back to its author) and runs the full plan ONCE on the batch. Green: predev takes the batch and every PR in it lands. Red: the failing rows are attributed to the PRs whose changes reach them and those go back to their authors; the rest re-batch (unattributable: split in half). Only the conductor moves predev. | Speculative per-PR merge groups and their cascades (impossible by construction); a batch rebuilt for PRs that did not change. |
| PROMOTE | predev -> dev -> qa -> main on the same engine, each rung judged once, absolutely where the ladder says so. | A promotion run that cannot pass (a rung whose required suite is registered not-run). |
-**METER** runs under every station: each proof and CI run records backend, minutes, dollars and
-outcome in one ledger; the waste over any window is a `health` row that fails above 10%.
+~~**METER** runs under every station: each proof and CI run records backend, minutes, dollars and
+outcome in one ledger; the waste over any window is a `health` row that fails above 10%.~~
-**PROVERS are registered backends, like runners.** A prover is a drop-in driver beside the runner
+~~**PROVERS are registered backends, like runners.** A prover is a drop-in driver beside the runner
gateway's (`Driver`): its name, slots, price per minute, readiness, canary, its own idempotent
`up`/`down` (the M3 prover's `up` builds its Linux VM from nothing), and `prove(commit, commands) ->
verdict`. Which provers are live, in what order and with how many slots is one table (`PROVERS`),
switched with one command after a canary, never by code: today the M3 (an arm64 Linux VM, warm
cache), Latchkey as overflow; tomorrow any mix of M3, Latchkey, EC2. The conductor fills the first
prover with a free slot. A proof is a filter, never the verdict: the verdict stays CI's, on Linux
-x86_64, so a rare arm64/x86_64 difference costs at most one CI run.
+x86_64, so a rare arm64/x86_64 difference costs at most one CI run.~~
-**The conductor IS the workflow; GitHub is one forge driver (OWNER 2026-10-11).** *"Conductor is the
+~~**The conductor IS the workflow; GitHub is one forge driver (OWNER 2026-10-11).** *"Conductor is the
master workflow: what could be a GitHub Actions workflow is the Rust conductor with pluggable
infra."* *"We shouldn't be tightly coupled to GitHub; it is a code repo, and if I want to go to
-Forgejo I could, mid-work."* So:
-- The conductor executes every job of every station itself, on INFRA DRIVERS (the M3's per-job
+Forgejo I could, mid-work."* So:~~
+- ~~The conductor executes every job of every station itself, on INFRA DRIVERS (the M3's per-job
Linux VMs, the Latchkey CLI, EC2 boxes, GitHub-hosted runners), chosen by one owner command
(`busbar-release infra use `, canaried, effective on the next job). Every job asks for a
size, including the light orchestration jobs; nothing names a vendor. It stores logs and links
them in the PR's one verdict comment, and posts ONE verdict status per PR head that branch
- protection requires.
-- The code host is a FORGE DRIVER: git push/fetch, pull requests, comments, the verdict status, branch
+ protection requires.~~
+- ~~The code host is a FORGE DRIVER: git push/fetch, pull requests, comments, the verdict status, branch
protection. GitHub is the driver today; Forgejo is the same interface. Nothing in the pipeline
depends on a forge-only feature: Actions workflows (replaced by the conductor), the merge queue
- (replaced by the train), forge-only APIs. Switching forge is configuration and a mirror push.
-- Platform of record: busbar's VERDICT jobs run on x86_64 Linux (the 1.5.5 golden was recorded
+ (replaced by the train), forge-only APIs. Switching forge is configuration and a mirror push.~~
+- ~~Platform of record: busbar's VERDICT jobs run on x86_64 Linux (the 1.5.5 golden was recorded
there) unless a measured run proves arm64 byte-identical; the M3's arm64 VMs take proofs and
- busbar-release's own CI. The M3 runs untrusted-safe: one throwaway VM per job.
-- OPEN (owner, before step 7 of 1.6.0): release signing without a forge-specific identity (SLSA
+ busbar-release's own CI. The M3 runs untrusted-safe: one throwaway VM per job.~~
+- ~~OPEN (owner, before step 7 of 1.6.0): release signing without a forge-specific identity (SLSA
provenance today assumes GitHub's OIDC token, which exists only inside Actions): our own signing
- key, or the chosen forge's CI.
+ key, or the chosen forge's CI.~~
-**What the conductor replaces** (deleted as each station lands): GitHub's merge queue on predev,
+~~**What the conductor replaces** (deleted as each station lands): GitHub's merge queue on predev,
`jump`, the merge-group re-check and orphan machinery, the STALE/UPDATE rule, `ship` and `dev` as
author tools, and every agent the pipeline spawns (fix, stuck, rebase, shared). The turnstile, the
-hop, the ladder, the step cache, autopin and the EC2 runner backend stay as its internals.
+hop, the ladder, the step cache, autopin and the EC2 runner backend stay as its internals.~~
-**Interim, until the train is live:** predev's merge queue builds at most 2 groups at once (OWNER
-2026-10-11, was 10).
+~~**Interim, until the train is live:** predev's merge queue builds at most 2 groups at once (OWNER
+2026-10-11, was 10).~~
## How work lands: the PR flow and the rules every contributor follows
@@ -3385,10 +3434,11 @@ branches live on GitHub so CI runs the pipeline).
1. One branch per lane, `lane-`, cut from `origin/predev`. A branch that depends on another
unmerged lane says so in its PR body.
-2. Changes reach `predev` and `dev` **only by pull request**. The ruleset on both requires the two
- jobs of `.github/workflows/promote.yml`, `preflight` and `hop`; no direct push, no force-push, no
- deletion. `dev` moves only through the promote engine.
-3. predev lands through GitHub's merge queue (OWNER 2026-10-02). The MERGE-GROUP run is the proof of
+2. Changes reach `predev` and `dev` **only by pull request**. The ruleset on both requires ~~the two
+ jobs of `.github/workflows/promote.yml`, `preflight` and `hop`~~ ModernCI's check `ci`; no direct
+ push, no force-push, no deletion. `dev` moves only through ~~the promote engine~~ ModernCI's
+ `predev` → `dev` move (`.ci/predev.yaml`).
+3. ~~predev lands through GitHub's merge queue (OWNER 2026-10-02). The MERGE-GROUP run is the proof of
record: the full plan (every gate, every build/test step, the money oracle forced, conformance
produced at judge time and judged absolutely per #68) on the queue's merge commit, judged "no worse
than base" against the base the queue built it on; its report becomes the base of the commit it
@@ -3398,19 +3448,19 @@ branches live on GitHub so CI runs the pipeline).
PR merged onto the base tip, with no oracle and no conformance; on ONE runner when that is
estimated within 20 minutes, else as the merge group's shard matrix ("Verification runs once per
input", OWNER-LOCKED 2026-10-06). Neither may add a red. A local Latchkey
- run is for iteration only.
+ run is for iteration only.~~ SUPERSEDED 2026-10-13 by "BUSBAR-RELEASE RETIRED FROM BUSBAR" above: ModernCI judges every pull request on its branch file and lands it with its own merge train; `dev`, `qa` and `main` move by ModernCI's promotion.
4. A `$` (money-touching) change is its own PR, never bundled with anything else.
5. On merge the branch is deleted (OWNER 2026-10-01, BRANCH LIFETIME: "as soon as merged into
predev get rid of the branch").
-**Ship and stop** (ARCHITECT 2026-10-02, supersedes every polling instruction). When a slice is
+~~**Ship and stop** (ARCHITECT 2026-10-02, supersedes every polling instruction). When a slice is
ready, run one command from the lane's worktree:
`busbar-release ship "" [--body ]` (the release CLI, `bin/busbar-release` in a clone
of GetBusbar/busbar-release; OWNER 2026-10-02: every release tool lives there, and `cargo xtask
ship` is gone). It refuses any branch that is not `lane-*`, merges `origin/predev` in, runs the local pre-flight, commits what the pre-flight changed (tracked
files only), pushes, opens or reuses the PR into `predev` and turns on auto-merge. Then STOP: no
polling of CI, PRs or Latchkey, no sleep loops. A red or conflicted PR comes back to the owning
-lane as a new task; fix it, ship again, stop again.
+lane as a new task; fix it, ship again, stop again.~~ SUPERSEDED 2026-10-13 by "BUSBAR-RELEASE RETIRED FROM BUSBAR" above: the author pushes the branch and opens the pull request; ModernCI does the rest (`modernci done`).
**Never bless a golden or a ratchet.** A red is fixed forward with a root cause; nothing is waived
and nothing is blessed (OWNER 2026-10-01, PREDEV IS THE CONVERGENCE BRANCH). Customer-visible
diff --git a/xtask/src/gates/no_self_filed_issues.rs b/xtask/src/gates/no_self_filed_issues.rs
index 01cb82559b..85264976d2 100644
--- a/xtask/src/gates/no_self_filed_issues.rs
+++ b/xtask/src/gates/no_self_filed_issues.rs
@@ -42,9 +42,12 @@ pub const ROW_PERM: &str = "no-self-filed-issues:no-issues-permission";
const WF_DIR: &str = ".github/workflows";
-/// A discovery step that finds nothing passes everything. Twenty-odd workflows today; the floor is
-/// the shell's, a `const` with no environment override.
-const DISCOVERY_FLOOR: usize = 5;
+/// A discovery step that finds nothing passes everything. The floor is the workflow count, a `const`
+/// with no environment override: the four reusable `plugin-*.yml` workflows the plugin repos call.
+/// It was the shell's 5 while `promote.yml` (the busbar-release hop) stood; that file is deleted
+/// (OWNER 2026-10-12, "ModernCI only": busbar's moves are judged from `.ci/*.yaml`), so 5 would
+/// read the honest tree as broken discovery.
+const DISCOVERY_FLOOR: usize = 4;
const CLEAN: &str = "the scan cleared its floor and named nothing";
diff --git a/xtask/src/gates/package_selectors.rs b/xtask/src/gates/package_selectors.rs
index 0a92532e34..1e3973830b 100644
--- a/xtask/src/gates/package_selectors.rs
+++ b/xtask/src/gates/package_selectors.rs
@@ -1317,7 +1317,7 @@ mod tests {
#[test]
fn the_reader_is_picked_by_the_files_own_shape_and_only_markdown_is_unread() {
assert_eq!(reader_for("xtask/src/gates/x.rs"), Reader::Rust);
- assert_eq!(reader_for(".github/workflows/promote.yml"), Reader::Yaml);
+ assert_eq!(reader_for(".github/workflows/plugin-ci.yml"), Reader::Yaml);
assert_eq!(reader_for(".github/actions/a/action.yaml"), Reader::Yaml);
assert_eq!(reader_for("qa/pipeline-checks.json"), Reader::Json);
assert_eq!(reader_for("qa/DESIGN-BINDINGS.md"), Reader::Prose);
diff --git a/xtask/src/gates/ship_ready.rs b/xtask/src/gates/ship_ready.rs
index b4815f6579..6abb1a9aac 100644
--- a/xtask/src/gates/ship_ready.rs
+++ b/xtask/src/gates/ship_ready.rs
@@ -60,8 +60,8 @@ pub struct ShipReadyGate;
/// `HEAD` and the empty string are folded in here for the same reason: `git rev-parse --abbrev-ref
/// HEAD` answers `HEAD` on a detached checkout, which is git saying it cannot name a branch, not
/// git naming a branch called HEAD. Reading it as a dev-line name is the same fail-open one step
-/// removed. The pipeline sets `XTASK_SHIP_TARGET` explicitly (`promote.yml`, the PR's base branch), which
-/// is the supported way to say what a detached checkout is a checkout OF.
+/// removed. The pipeline sets `XTASK_SHIP_TARGET` explicitly (`.ci/*.yaml`, the move's target branch),
+/// which is the supported way to say what a detached checkout is a checkout OF.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Posture {
/// The target, named: `XTASK_SHIP_TARGET` when set and non-empty, else the branch git reports.
diff --git a/xtask/src/gates/tests/workflow_rules_tests.rs b/xtask/src/gates/tests/workflow_rules_tests.rs
index 7a4c011c5c..279daaa56d 100644
--- a/xtask/src/gates/tests/workflow_rules_tests.rs
+++ b/xtask/src/gates/tests/workflow_rules_tests.rs
@@ -175,210 +175,6 @@ fn every_rule_is_proven_able_to_go_red() {
crate::gates::verify_report(&gate, &report).expect("every rule proven RED");
}
-/// A two-job workflow: `slsa-build` (under `BUILDER_IF`) uploads the release artifact, and a shard
-/// downloads it from its own run under `STEP_IF`, in the block spelling promote.yml uses.
-const R18_FIXTURE: &str = "on: [pull_request, merge_group]
-jobs:
- slsa-build:
- if: BUILDER_IF
- steps:
- - run: cargo build --release
- - { uses: actions/upload-artifact@abc, with: { name: \"slsa-artifact-x\", path: slsa/ } } # v7
- hop-shard:
- steps:
- - if: STEP_IF
- uses: actions/download-artifact@abc # v8
- with: { name: \"slsa-artifact-x\", path: slsa }
- - run: ci hop
-";
-
-fn r18(builder_if: &str, step_if: &str) -> Vec {
- let text = R18_FIXTURE
- .replace("BUILDER_IF", builder_if)
- .replace("STEP_IF", step_if);
- release_artifact_findings("promote.yml", &text)
- .into_iter()
- .map(|f| {
- assert_eq!(f.rule, "R18");
- f.message
- })
- .collect()
-}
-
-const NEEDS_ARTIFACT: &str = "needs.preflight.outputs.needs_release_artifact == 'true'";
-
-#[test]
-fn r18_a_merge_group_only_build_under_a_pull_request_download_is_red() {
- // #740 exactly: the builder runs only in a merge group, the download whenever the rung reads it.
- let got = r18(
- "github.event_name == 'merge_group' && needs.preflight.outputs.run == '1'",
- NEEDS_ARTIFACT,
- );
- assert_eq!(got.len(), 1, "{got:?}");
- assert!(
- got[0].contains("`hop-shard`") && got[0].contains("`slsa-build`"),
- "{got:?}"
- );
- // The same narrowing written as a base-branch test is the same red.
- assert_eq!(r18("github.base_ref == 'qa'", NEEDS_ARTIFACT).len(), 1);
-}
-
-#[test]
-fn r18_a_builder_that_admits_the_download_condition_is_green() {
- let fixed = format!(
- "needs.preflight.outputs.run == '1' && (github.event_name == 'merge_group' || {NEEDS_ARTIFACT})"
- );
- assert!(r18(&fixed, NEEDS_ARTIFACT).is_empty());
- // Wrapped in `${{ }}` the same.
- assert!(r18(&format!("${{{{ {fixed} }}}}"), NEEDS_ARTIFACT).is_empty());
- // A builder narrowed only by the run gate, not by event, runs wherever the run runs.
- assert!(r18("needs.preflight.outputs.run == '1'", NEEDS_ARTIFACT).is_empty());
-}
-
-#[test]
-fn r18_a_download_that_waits_on_the_builders_success_is_green() {
- // The soak's spelling: it downloads only after slsa-build succeeded, else builds its own.
- assert!(r18(
- "github.event_name == 'merge_group'",
- "needs.slsa-build.result == 'success'"
- )
- .is_empty());
-}
-
-#[test]
-fn r18_an_unconditional_download_under_an_event_narrowed_builder_is_red() {
- let text = R18_FIXTURE
- .replace("BUILDER_IF", "github.event_name == 'merge_group'")
- .replace(" - if: STEP_IF\n uses:", " - uses:");
- assert_eq!(release_artifact_findings("promote.yml", &text).len(), 1);
-}
-
-#[test]
-fn r18_a_download_with_no_builder_in_the_run_is_red_and_a_run_id_download_is_not_judged() {
- let orphan = R18_FIXTURE
- .replace("BUILDER_IF", "github.event_name == 'merge_group'")
- .replace("actions/upload-artifact@abc", "actions/cache@abc")
- .replace("STEP_IF", NEEDS_ARTIFACT);
- let got = release_artifact_findings("promote.yml", &orphan);
- assert_eq!(got.len(), 1);
- assert!(got[0]
- .message
- .contains("no job of this workflow uploads it"));
- // Another run's artifact (by run-id) is not this run's to build.
- let other = orphan.replace(
- "with: { name: \"slsa-artifact-x\", path: slsa }",
- "with: { name: \"slsa-artifact-x\", path: slsa, run-id: \"${{ env.RUN }}\" }",
- );
- assert!(release_artifact_findings("promote.yml", &other).is_empty());
-}
-
-#[test]
-fn r18_a_flow_step_condition_is_read_up_to_the_next_key() {
- assert_eq!(
- step_condition(&[" - { if: always(), uses: actions/upload-artifact@abc }"]).as_deref(),
- Some("always()")
- );
- assert_eq!(
- step_condition(&[" - { uses: a@b, if: \"${{ x == 'y' }}\", with: { n: 1 } }"])
- .as_deref(),
- Some("x == 'y'")
- );
- assert_eq!(step_condition(&[" - run: true"]), None);
-}
-
-#[test]
-fn r18_the_mutation_anchor_rewrites_only_the_named_jobs_if() {
- let t = "jobs:\n a:\n if: one\n slsa-build:\n if: two\n needs: a\n";
- assert_eq!(
- reif_job(t, "slsa-build", "three"),
- "jobs:\n a:\n if: one\n slsa-build:\n if: three\n needs: a\n"
- );
- assert_eq!(reif_job(t, "absent", "three"), t);
-}
-
-/// R19: a shard that runs the hop on the candidate with the read-only keys, and the publisher that
-/// runs no candidate code with the write keys.
-const R19_FIXTURE: &str = "on: [pull_request, merge_group]
-jobs:
- hop-shard:
- steps:
- - { uses: actions/checkout@abc, with: { ref: x, path: busbar } } # v7
- - { uses: actions/checkout@abc, with: { repository: GetBusbar/busbar-release, ref: x, path: busbar-release } } # v7
- - run: ci hop
- env: { R2_RO_KEY_ID: \"${{ secrets.R2_CACHE_RO_ACCESS_KEY_ID }}\", R2_RO_KEY_SECRET: \"${{ secrets.R2_CACHE_RO_SECRET_ACCESS_KEY }}\" }
- cache-publish:
- needs: [hop-shard]
- steps:
- - run: echo the engine
- - run: ci cache-publish
- env: { R2_KEY_ID: \"${{ secrets.R2_CACHE_ACCESS_KEY_ID }}\", R2_KEY_SECRET: \"${{ secrets.R2_CACHE_SECRET_ACCESS_KEY }}\" }
-";
-
-fn r19(text: &str) -> Vec {
- r2_write_key_findings("promote.yml", text)
- .into_iter()
- .map(|f| {
- assert_eq!(f.rule, "R19");
- f.message
- })
- .collect()
-}
-
-#[test]
-fn r19_read_only_keys_on_the_candidate_and_write_keys_on_the_publisher_are_green() {
- let got = r19(R19_FIXTURE);
- assert!(got.is_empty(), "{got:?}");
-}
-
-#[test]
-fn r19_a_write_key_on_a_job_that_runs_candidate_code_is_red() {
- // The shard as it stood before busbar-release 069f032.
- let shard = R19_FIXTURE.replacen(
- "R2_RO_KEY_ID: \"${{ secrets.R2_CACHE_RO_ACCESS_KEY_ID }}\"",
- "R2_KEY_ID: \"${{ secrets.R2_CACHE_ACCESS_KEY_ID }}\"",
- 1,
- );
- let got = r19(&shard);
- assert_eq!(got.len(), 1, "{got:?}");
- assert!(
- got[0].contains("`hop-shard`") && got[0].contains("secrets.R2_CACHE_ACCESS_KEY_ID"),
- "{got:?}"
- );
- // A job that only builds (cargo, no checkout step of its own) runs candidate code too.
- let build = "jobs:\n b:\n steps:\n - run: cargo build\n env: { K: \"${{ secrets.R2_CACHE_SECRET_ACCESS_KEY }}\" }\n";
- assert_eq!(r19(build).len(), 1);
-}
-
-#[test]
-fn r19_a_workflow_level_write_key_reaches_every_candidate_job() {
- let text = String::from("env:\n K: \"${{ secrets.R2_CACHE_ACCESS_KEY_ID }}\"\n") + R19_FIXTURE;
- let got = r19(&text);
- assert_eq!(got.len(), 1, "{got:?}");
- assert!(got[0].contains("`hop-shard`"), "{got:?}");
-}
-
-#[test]
-fn r19_reads_a_secret_whole_and_another_repositorys_checkout_as_no_candidate() {
- assert!(r2_write_secrets_in("${{ secrets.R2_CACHE_ACCESS_KEY_ID2 }}").is_empty());
- assert!(r2_write_secrets_in("${{ secrets.R2_CACHE_RO_ACCESS_KEY_ID }}").is_empty());
- assert!(r2_write_secrets_in("${{ secrets.R2_CACHE_RO_SECRET_ACCESS_KEY }}").is_empty());
- assert_eq!(
- r2_write_secrets_in("${{ secrets.R2_CACHE_SECRET_ACCESS_KEY }}"),
- vec!["secrets.R2_CACHE_SECRET_ACCESS_KEY"]
- );
- let other = "jobs:\n p:\n steps:\n - { uses: actions/checkout@abc, with: { repository: GetBusbar/busbar-release, ref: x } } # v7\n - run: ci cache-publish\n env: { K: \"${{ secrets.R2_CACHE_ACCESS_KEY_ID }}\" }\n";
- assert!(r19(other).is_empty());
-}
-
-const ENGINE_VAR_JOB: &str = "env:\n SHA: x\njobs:\n pre:\n env: { RELEASE_REF: \"${{ vars.ENGINE_REF }}\", ENGINE_SHA256: \"${{ vars.ENGINE_SHA256 }}\" }\n steps:\n - name: Engine\n run: |\n [[ \"$RELEASE_REF\" =~ ^[0-9a-f]{40}$ ]] || { echo bad; exit 1; }\n name=\"busbar-release-ci-linux-x86_64-$RELEASE_REF\"\n echo \"$ENGINE_SHA256 bin\" | sha256sum -c -\n - { uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1, with: { repository: GetBusbar/busbar-release, ref: \"${{ env.RELEASE_REF }}\", path: busbar-release } } # v7\n";
-
-fn r16(text: &str) -> Vec {
- engine_pin_findings("w.yml", text)
- .into_iter()
- .map(|f| f.message)
- .collect()
-}
-
fn r17(text: &str) -> Vec {
vendor_runs_on(text)
.into_iter()
@@ -386,78 +182,6 @@ fn r17(text: &str) -> Vec {
.collect()
}
-#[test]
-fn r16_accepts_the_variable_pin_with_its_shape_check_and_its_sha256() {
- assert_eq!(r16(ENGINE_VAR_JOB), Vec::::new());
-}
-
-#[test]
-fn r16_accepts_an_in_file_sha_pin() {
- let t = "jobs:\n a:\n steps:\n - { uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1, with: { repository: GetBusbar/busbar-release, ref: 7ab21912d664d11c7df6cc98fe78eed6b0119177 } } # v7\n";
- assert_eq!(r16(t), Vec::::new());
- // and through a workflow-level env var holding the literal
- let t = "env:\n RELEASE_REF: 7ab21912d664d11c7df6cc98fe78eed6b0119177\njobs:\n a:\n steps:\n - { uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1, with: { repository: GetBusbar/busbar-release, ref: \"${{ env.RELEASE_REF }}\" } } # v7\n";
- assert_eq!(r16(t), Vec::::new());
-}
-
-#[test]
-fn r16_refuses_a_branch_or_a_tag_ref() {
- // RED: the checkout at a branch.
- let t = ENGINE_VAR_JOB.replace("ref: \"${{ env.RELEASE_REF }}\"", "ref: dev");
- assert!(r16(&t).iter().any(|m| m.contains("`dev`")), "{:?}", r16(&t));
- // RED: the variable is defined as a tag.
- let t = ENGINE_VAR_JOB.replace(
- "RELEASE_REF: \"${{ vars.ENGINE_REF }}\"",
- "RELEASE_REF: v1.6.0",
- );
- assert!(
- r16(&t).iter().any(|m| m.contains("v1.6.0")),
- "{:?}",
- r16(&t)
- );
- // RED: in-file, but a tag at the workflow level.
- let t = "env:\n RELEASE_REF: refs/tags/v1\njobs:\n a:\n steps:\n - { uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1, with: { repository: GetBusbar/busbar-release, ref: \"${{ env.RELEASE_REF }}\" } } # v7\n";
- assert!(!r16(t).is_empty());
- // RED: no ref at all (the default branch).
- let t = "jobs:\n a:\n steps:\n - { uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1, with: { repository: GetBusbar/busbar-release } } # v7\n";
- assert!(r16(t).iter().any(|m| m.contains("no `ref:`")));
-}
-
-#[test]
-fn r16_refuses_a_variable_pin_without_its_shape_check_or_its_sha256() {
- let t = ENGINE_VAR_JOB.replace("=~ ^[0-9a-f]{40}$", "!= \"\"");
- assert!(
- r16(&t).iter().any(|m| m.contains("default branch")),
- "{:?}",
- r16(&t)
- );
- let t = ENGINE_VAR_JOB.replace("sha256sum -c -", "cat");
- let got = r16(&t);
- assert_eq!(
- got.len(),
- 2,
- "the download and the pin both name the missing check: {got:?}"
- );
- // The shape check AFTER the checkout guards nothing.
- let t = "jobs:\n pre:\n env: { RELEASE_REF: \"${{ vars.ENGINE_REF }}\" }\n steps:\n - { uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1, with: { repository: GetBusbar/busbar-release, ref: \"${{ env.RELEASE_REF }}\" } } # v7\n - run: |\n [[ \"$RELEASE_REF\" =~ ^[0-9a-f]{40}$ ]] || exit 1\n echo \"$ENGINE_SHA256 b\" | sha256sum -c -\n";
- assert!(r16(t).iter().any(|m| m.contains("default branch")));
-}
-
-#[test]
-fn a_key_value_is_read_whole_from_a_flow_mapping() {
- assert_eq!(
- key_value("{ repository: o/r, ref: \"${{ env.X }}\", path: p }", "ref").as_deref(),
- Some("${{ env.X }}")
- );
- assert_eq!(
- key_value("{ ref: dev, path: p }", "ref").as_deref(),
- Some("dev")
- );
- assert_eq!(key_value("{ prefref: dev }", "ref"), None);
- assert_eq!(env_ref("${{ env.RELEASE_REF }}"), Some("RELEASE_REF"));
- assert_eq!(env_ref("${{ vars.ENGINE_REF }}"), None);
-}
-
#[test]
fn a_vendor_runs_on_label_is_refused_in_every_spelling() {
assert_eq!(
diff --git a/xtask/src/gates/workflow_rules.rs b/xtask/src/gates/workflow_rules.rs
index 261a98fd9e..30b80982fd 100644
--- a/xtask/src/gates/workflow_rules.rs
+++ b/xtask/src/gates/workflow_rules.rs
@@ -2,14 +2,16 @@
//!
//! This gate was `release-order`: "nothing may be tagged until it has been verified from the
//! consumer side", enforced over the SHAPE OF busbar's own release workflows (`release.yml`,
-//! `release-stage.yml`, `docker.yml`, `verify-deploy.yml`). Those workflows are deleted: busbar has
-//! one pipeline workflow, `promote.yml`, and the release engine lives in busbar-release. The rules
-//! that were ABOUT those files (the staged-verification graph, the draft flag, the red-branch gate,
-//! the first gate's run list) went with them, rows and code together.
+//! `release-stage.yml`, `docker.yml`, `verify-deploy.yml`). Those workflows are deleted, and so is
+//! `promote.yml`, the busbar-release hop (OWNER 2026-10-12, "ModernCI only"): busbar's moves are
+//! judged by ModernCI from `.ci/*.yaml`, which this gate does not read. The rules that were ABOUT
+//! those files (the staged-verification graph, the draft flag, the red-branch gate, the first gate's
+//! run list, and promote.yml's engine pin, release-artifact wiring and R2 keys, R16/R18/R19) went
+//! with them, rows and code together.
//!
//! Five rules were never about one file. They are bans over EVERY workflow and composite action
-//! that remains (`promote.yml` and the reusable `plugin-*.yml` workflows the plugin repos call),
-//! and each has a specific observed failure behind it:
+//! that remains (the reusable `plugin-*.yml` workflows the plugin repos call), and each has a
+//! specific observed failure behind it:
//!
//! * **R1 — no workflow is triggered by a version tag.** A tag is the OUTPUT of a verified release,
//! never its trigger.
@@ -18,22 +20,8 @@
//! * **R14 — every third-party action runs from a commit sha** with its tag as a trailing comment,
//! because an action that can be force-moved under a name we already trust runs with our tokens.
//! * **R15 — every attestation verify names the workflow that signed**, not just the repository.
-//! * **R16 — the engine runs from a commit, never a branch or a tag.** Every checkout of
-//! busbar-release names a full 40-hex sha, either in the file or through an env var the job
-//! shape-checks as 40 hex before the checkout (the org variable `vars.ENGINE_REF`, OWNER
-//! 2026-10-10: the engine pin lives outside this repo), and every job that downloads the engine
-//! binary runs it only after `sha256sum -c` against `ENGINE_SHA256`.
//! * **R17 — no job hard-codes a vendor runner label.** Every job asks the runner gateway for a
//! logical size and `vars.RUNNERS` names the label, so a workflow never spells a vendor.
-//! * **R18 — a step that downloads the release artifact from its own run runs only where the job
-//! that builds it runs.** `slsa-build` was merge-group-only while the predev->dev pull request's
-//! shards (its rung runs the oracle and conformance) downloaded `slsa-artifact-` from their
-//! own run: the artifact could never exist there, and #740 failed every run at that download.
-//! * **R19 — no job that runs candidate code holds the R2 cache write keys.** The hop judges the
-//! candidate's build scripts, tests and xtask in-process, and every process of a job can read
-//! another's environment: the shards held `secrets.R2_CACHE_ACCESS_KEY_ID` /
-//! `secrets.R2_CACHE_SECRET_ACCESS_KEY` until busbar-release 069f032, which refuses to start a
-//! hop that holds them. They belong to `cache-publish` alone, a job that runs no candidate code.
//!
//! **The parser stays deliberately small, and here that is a correctness argument rather than a
//! dependency one.** The rules are assertions about what a human WROTE (a trailing `# tag` comment
@@ -61,28 +49,12 @@ const WORKFLOWS: &str = ".github/workflows";
const ACTIONS_DIR: &str = ".github/actions";
/// The floor under the workflow discovery. A scan that finds nothing satisfies every ban, and a
-/// tree with fewer workflows than this has lost its subject, not been cleaned: `promote.yml` plus
-/// the four reusable `plugin-*.yml` workflows.
-const MIN_WORKFLOWS: usize = 5;
+/// tree with fewer workflows than this has lost its subject, not been cleaned: the four reusable
+/// `plugin-*.yml` workflows.
+const MIN_WORKFLOWS: usize = 4;
/// The rule ids, which are also the ledger row ids.
-const RULES: &[&str] = &["R1", "R11", "R14", "R15", "R16", "R17", "R18", "R19"];
-
-/// The release artifact's name prefix: `promote.yml`'s `slsa-build` uploads `slsa-artifact-`,
-/// and the steps that read the release binary download it by that name (R18).
-const RELEASE_ARTIFACT: &str = "slsa-artifact-";
-/// R16's subject: the release engine's repository, and the name prefix of its CI binary on the
-/// `dev-bin` release.
-const ENGINE_REPO: &str = "GetBusbar/busbar-release";
-const ENGINE_ASSET: &str = "busbar-release-ci-";
-/// The shape test R16 requires of a variable engine pin, and the bytes check of the binary.
-const SHA40_TEST: &str = "=~ ^[0-9a-f]{40}$";
-const SHA256_CHECK: &str = "sha256sum -c";
-const SHA256_VAR: &str = "ENGINE_SHA256";
-/// The pinned judge's binary on the `dev-bin` release, and the digest its job checks it against
-/// (`sha256_linux_x86_64=` of testing/shadow-oracle/oracle-rust.pin).
-const ORACLE_ASSET: &str = "busbar-oracle-linux-x86_64-";
-const ORACLE_SHA256_VAR: &str = "ORACLE_SHA256";
+const RULES: &[&str] = &["R1", "R11", "R14", "R15", "R17"];
// -------------------------------------------------------------------------------------------
// THE SMALL PARSER
@@ -379,257 +351,6 @@ fn pushes_bare_release_branch(cmd: &str) -> bool {
false
}
-/// A line that opens a YAML key at exactly `indent` spaces: not a comment, not a sequence item,
-/// not deeper.
-fn opens_key_at(line: &str, indent: usize) -> bool {
- let b = line.as_bytes();
- b.len() > indent
- && b[..indent].iter().all(|c| *c == b' ')
- && !matches!(b[indent], b' ' | b'\t' | b'#' | b'-')
-}
-
-/// An `if:` condition as written, without its `${{ }}` wrapper or the double quotes around the
-/// whole of it (a single quote that ends the condition, `== 'true'`, is the condition's own).
-fn bare_condition(raw: &str) -> String {
- let c = raw.trim();
- let c = c
- .strip_prefix('"')
- .and_then(|r| r.strip_suffix('"'))
- .unwrap_or(c)
- .trim();
- let c = c.strip_prefix("${{").unwrap_or(c);
- let c = c.strip_suffix("}}").unwrap_or(c);
- c.trim().to_string()
-}
-
-/// A job's own `if:` (the line opening `if:` at four spaces), bare. `None`: the job is
-/// unconditional.
-fn job_condition(lines: &[&str]) -> Option {
- lines
- .iter()
- .find(|l| opens_key_at(l, 4) && l.trim_start().starts_with("if:"))
- .map(|l| bare_condition(&l.trim_start()["if:".len()..]))
-}
-
-/// A job's steps: each step's lines, its item line first. The items sit at the indent of the first
-/// `- ` line under `steps:`; deeper lines (a `run: |` body, a block `with:`) belong to the step.
-fn steps_of<'a>(lines: &[&'a str]) -> Vec> {
- let mut out: Vec> = Vec::new();
- let Some(start) = lines
- .iter()
- .position(|l| opens_key_at(l, 4) && l.trim_end() == " steps:")
- else {
- return out;
- };
- let mut indent: Option = None;
- for line in &lines[start + 1..] {
- if opens_key_at(line, 4) {
- break;
- }
- let lead = line.len() - line.trim_start().len();
- let item = line.trim_start().starts_with("- ");
- if item && indent.is_none() {
- indent = Some(lead);
- }
- if item && Some(lead) == indent {
- out.push(vec![*line]);
- } else if let Some(step) = out.last_mut() {
- step.push(*line);
- }
- }
- out
-}
-
-/// A step's own `if:`, bare, in either spelling: `- if: X` or an `if:` line of the step (block),
-/// or `- { if: X, uses: ... }` (flow). `None`: the step is unconditional.
-fn step_condition(step: &[&str]) -> Option {
- let first = step.first()?.trim_start().strip_prefix("- ")?.trim_start();
- if let Some(flow) = first.strip_prefix('{') {
- let flow = flow.trim_start();
- let rest = match flow.strip_prefix("if:") {
- Some(r) => r,
- None => &flow[flow.find(", if:")? + ", if:".len()..],
- };
- let end = [
- ", uses:",
- ", run:",
- ", with:",
- ", id:",
- ", name:",
- ", env:",
- ", working-directory:",
- ", continue-on-error:",
- ]
- .iter()
- .filter_map(|k| rest.find(k))
- .min()
- .unwrap_or(rest.len());
- return Some(bare_condition(&rest[..end]));
- }
- if let Some(rest) = first.strip_prefix("if:") {
- return Some(bare_condition(rest));
- }
- step[1..]
- .iter()
- .map(|l| l.trim_start())
- .find_map(|l| l.strip_prefix("if:"))
- .map(bare_condition)
-}
-
-/// R18 over one workflow's comment-stripped text. Every step that downloads the release artifact
-/// from its OWN run (no `run-id:`) must run only where a job of this run uploads it. The step is
-/// safe when it waits on the builder's success (`needs..result == 'success'`), or when
-/// the builder's `if:` does not narrow it by event; a builder narrowed by event
-/// (`github.event_name`, `github.base_ref`) must also admit the step's own condition, written into
-/// its `if:` as one of the alternatives. No builder at all is the same red: the artifact can never
-/// exist in that run.
-fn release_artifact_findings(name: &str, text: &str) -> Vec {
- fn carries(step: &[&str], verb: &str) -> bool {
- let body = step.join("\n");
- body.contains(verb) && body.contains(RELEASE_ARTIFACT)
- }
- let jobs = jobs_of(text);
- let builders: Vec<(&str, Option)> = jobs
- .iter()
- .filter(|(_, lines)| {
- steps_of(lines)
- .iter()
- .any(|s| carries(s, "upload-artifact"))
- })
- .map(|(id, lines)| (id.as_str(), job_condition(lines)))
- .collect();
- let mut bad = Vec::new();
- for (job, lines) in &jobs {
- for step in steps_of(lines) {
- if !carries(&step, "download-artifact") || step.join("\n").contains("run-id:") {
- continue;
- }
- let gate = step_condition(&step).unwrap_or_default();
- let when = if gate.is_empty() {
- "always"
- } else {
- gate.as_str()
- };
- if builders.is_empty() {
- bad.push(Finding::new(
- "R18",
- format!(
- "{name}: job `{job}` downloads the release artifact \
- (`{RELEASE_ARTIFACT}*`) from its own run, and no job of this workflow \
- uploads it, so the download can never find it. Build it in this run, or \
- download it by `run-id:` from the run that built it."
- ),
- ));
- continue;
- }
- for (builder, cond) in &builders {
- if gate.contains(&format!("needs.{builder}.result == 'success'")) {
- continue;
- }
- let Some(cond) = cond else { continue };
- let by_event =
- cond.contains("github.event_name") || cond.contains("github.base_ref");
- if by_event && (gate.is_empty() || !cond.contains(gate.as_str())) {
- bad.push(Finding::new(
- "R18",
- format!(
- "{name}: job `{job}` downloads the release artifact \
- (`{RELEASE_ARTIFACT}*`) from its own run when `{when}`, but \
- `{builder}`, the job that uploads it, runs only when `{cond}`. In \
- every run where the first holds and the second does not, the artifact \
- does not exist and the download fails (#740: a merge-group-only \
- slsa-build under the predev->dev pull request's shards). Admit \
- `{when}` in `{builder}`'s `if:`."
- ),
- ));
- }
- }
- }
- }
- bad
-}
-
-/// R19's subject: the R2 cache's WRITE keys, as this repository's secrets name them. The read-only
-/// pair (`secrets.R2_CACHE_RO_*`) is another name, not a longer spelling of these.
-const R2_WRITE_SECRETS: [&str; 2] = [
- "secrets.R2_CACHE_ACCESS_KEY_ID",
- "secrets.R2_CACHE_SECRET_ACCESS_KEY",
-];
-
-/// The R2 write-key secrets `text` names, each as a whole name (`secrets.R2_CACHE_ACCESS_KEY_ID2`
-/// is not `secrets.R2_CACHE_ACCESS_KEY_ID`).
-fn r2_write_secrets_in(text: &str) -> Vec<&'static str> {
- R2_WRITE_SECRETS
- .into_iter()
- .filter(|s| {
- text.match_indices(*s).any(|(at, m)| {
- !text[at + m.len()..]
- .chars()
- .next()
- .is_some_and(|c| c.is_ascii_alphanumeric() || c == '_')
- })
- })
- .collect()
-}
-
-/// How job `lines` runs candidate code, when it does: a step checks out this repository (an
-/// `actions/checkout` with no `repository:` is the candidate), runs cargo, or runs the hop (`ci
-/// hop` judges the candidate in-process). A job that only downloads the engine, checks out another
-/// repository or downloads artifacts runs none.
-fn candidate_code(lines: &[&str]) -> Option<&'static str> {
- for step in steps_of(lines) {
- let body = step.join("\n");
- if body.contains("actions/checkout@") && key_value(&body, "repository").is_none() {
- return Some("checks out the candidate");
- }
- if body.contains("cargo ") {
- return Some("runs cargo");
- }
- if body.contains("ci hop") {
- return Some("runs `ci hop` on the candidate");
- }
- }
- None
-}
-
-/// R19 over one workflow's comment-stripped text. A job that runs candidate code names no R2 cache
-/// write key: not in its own lines (job env, step env, `with:`), and not through the workflow's
-/// top-level `env:`, which every job inherits.
-fn r2_write_key_findings(name: &str, text: &str) -> Vec {
- let top = r2_write_secrets_in(&top_level_block(text, "env"));
- let mut bad = Vec::new();
- for (job, lines) in jobs_of(text) {
- let Some(how) = candidate_code(&lines) else {
- continue;
- };
- let mut held = r2_write_secrets_in(&lines.join("\n"));
- for s in &top {
- if !held.contains(s) {
- held.push(*s);
- }
- }
- if held.is_empty() {
- continue;
- }
- let named = held
- .iter()
- .map(|s| format!("`{s}`"))
- .collect::>()
- .join(", ");
- bad.push(Finding::new(
- "R19",
- format!(
- "{name}: job `{job}` {how} and names the R2 cache write key(s) {named}: every \
- candidate process the job starts can read them from its environment, and `ci hop` \
- refuses to start with them (INFRA). Give it the read-only `secrets.R2_CACHE_RO_*` \
- keys; the write keys belong to `cache-publish` alone, a job that runs no candidate \
- code."
- ),
- ));
- }
- bad
-}
-
/// True when there is an actual composite-action manifest to scan, either on disk or planted by a
/// selftest overlay. Mirrors `Ctx::list`'s own `overlay_adds_it` test: `.github/actions` need not
/// exist in the real tree today (it does not) for a plant UNDER it to be a legitimate root, exactly
@@ -681,215 +402,6 @@ fn action_manifests(cx: &Ctx) -> Result, String> {
Ok(out)
}
-/// The jobs of a workflow (comments already stripped): each job's name and its lines, from its
-/// header (a key at exactly two spaces under the top-level `jobs:`) to the next job or top-level key.
-fn jobs_of(text: &str) -> Vec<(String, Vec<&str>)> {
- let mut out: Vec<(String, Vec<&str>)> = Vec::new();
- let mut in_jobs = false;
- for line in text.lines() {
- if !line.starts_with(' ') && !line.trim().is_empty() {
- in_jobs = line.trim_end() == "jobs:";
- continue;
- }
- if !in_jobs {
- continue;
- }
- let b = line.as_bytes();
- if b.len() > 2 && b[0] == b' ' && b[1] == b' ' && b[2] != b' ' {
- if let Some(name) = line.trim().strip_suffix(':') {
- out.push((name.to_string(), Vec::new()));
- continue;
- }
- }
- if let Some((_, lines)) = out.last_mut() {
- lines.push(line);
- }
- }
- out
-}
-
-/// The value of `key:` in `text` (a flow mapping or a block line), the first time `key` appears as
-/// a key (not as the tail of a longer one): a quoted string up to its closing quote, an `${{ }}`
-/// expression whole, or a bare scalar up to whitespace, `,` or `}`.
-fn key_value(text: &str, key: &str) -> Option {
- let needle = format!("{key}:");
- let mut from = 0usize;
- while let Some(rel) = text[from..].find(&needle) {
- let at = from + rel;
- from = at + needle.len();
- let before = text[..at].chars().next_back();
- if before.is_some_and(|c| c.is_alphanumeric() || c == '_' || c == '-' || c == '.') {
- continue;
- }
- let rest = text[from..].trim_start_matches([' ', '\t']);
- let v = if let Some(q) = rest.strip_prefix('"') {
- q.split('"').next().unwrap_or("")
- } else if let Some(q) = rest.strip_prefix('\'') {
- q.split('\'').next().unwrap_or("")
- } else if rest.starts_with("${{") {
- rest.find("}}").map(|i| &rest[..i + 2]).unwrap_or(rest)
- } else {
- let end = rest
- .find(|c: char| c.is_whitespace() || c == ',' || c == '}')
- .unwrap_or(rest.len());
- &rest[..end]
- };
- return Some(v.trim().to_string());
- }
- None
-}
-
-/// `${{ env.NAME }}` -> `NAME`.
-fn env_ref(v: &str) -> Option<&str> {
- let inner = v.strip_prefix("${{")?.strip_suffix("}}")?.trim();
- let name = inner.strip_prefix("env.")?;
- (!name.is_empty() && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_'))
- .then_some(name)
-}
-
-/// The step span holding line `at` of `lines`: from the nearest `- ` item at or above it to the
-/// next item at that indent (exclusive).
-fn step_span(lines: &[&str], at: usize) -> String {
- let indent = |l: &str| l.len() - l.trim_start().len();
- let start = (0..=at)
- .rev()
- .find(|&i| lines[i].trim_start().starts_with("- "))
- .unwrap_or(at);
- let ind = indent(lines[start]);
- let end = (start + 1..lines.len())
- .find(|&i| {
- let l = lines[i];
- !l.trim().is_empty() && indent(l) <= ind
- })
- .unwrap_or(lines.len());
- lines[start..end].join("\n")
-}
-
-/// R16 over one workflow's text (comments stripped).
-///
-/// THE ENGINE IS PINNED BY A COMMIT, AND ITS BYTES BY A DIGEST. promote.yml runs busbar-release's
-/// engine with the jobs' tokens; a checkout at a branch or a tag runs whatever that name points at
-/// today. The pin may be written in the file (a 40-hex sha) or held outside it (OWNER 2026-10-10:
-/// the org variable `vars.ENGINE_REF`, so an engine update never waits in predev's merge queue). The
-/// variable form is accepted only when the job proves at run time what the file can no longer show:
-/// a step BEFORE the checkout tests the variable against 40 hex and exits on anything else (an
-/// unset variable must FAIL, never fall back to the default branch an empty `ref:` checks out), and
-/// the downloaded binary runs only after `sha256sum -c` against `ENGINE_SHA256`.
-fn engine_pin_findings(name: &str, text: &str) -> Vec {
- let mut bad = Vec::new();
- let top_env = top_level_block(text, "env");
- for (job, lines) in jobs_of(text) {
- let job_text = lines.join("\n");
- let downloads = job_text.contains(ENGINE_ASSET);
- let verifies = lines
- .iter()
- .any(|l| l.contains(SHA256_CHECK) && l.contains(SHA256_VAR));
- // THE JUDGE'S BYTES TOO: a job that downloads the pinned oracle runs it only after
- // `sha256sum -c` against the pin's digest.
- if job_text.contains(ORACLE_ASSET)
- && !lines
- .iter()
- .any(|l| l.contains(SHA256_CHECK) && l.contains(ORACLE_SHA256_VAR))
- {
- bad.push(Finding::new(
- "R16",
- format!(
- "{name}: job `{job}` downloads the pinned oracle ({ORACLE_ASSET}*) without \
- `{SHA256_CHECK}` against ${ORACLE_SHA256_VAR} (oracle-rust.pin's \
- sha256_linux_x86_64): the judge would be whatever the release holds."
- ),
- ));
- }
- if downloads && !verifies {
- bad.push(Finding::new(
- "R16",
- format!(
- "{name}: job `{job}` downloads the engine binary ({ENGINE_ASSET}*) without \
- `{SHA256_CHECK}` against ${SHA256_VAR}: the bytes it runs with this job's \
- tokens are whatever the release holds under that name today."
- ),
- ));
- }
- for (i, line) in lines.iter().enumerate() {
- if !line.contains(&format!("repository: {ENGINE_REPO}")) {
- continue;
- }
- let span = step_span(&lines, i);
- let Some(r) = key_value(&span, "ref").filter(|r| !r.is_empty()) else {
- bad.push(Finding::new(
- "R16",
- format!(
- "{name}: job `{job}` checks out {ENGINE_REPO} with no `ref:`, so it runs \
- the engine's default branch, not a pinned commit."
- ),
- ));
- continue;
- };
- if is_sha40(&r) {
- continue;
- }
- let Some(var) = env_ref(&r) else {
- bad.push(Finding::new(
- "R16",
- format!(
- "{name}: job `{job}` checks out {ENGINE_REPO} at `{r}`, a branch or tag \
- the engine's owner can move, not a commit. Pin a 40-hex sha, or \
- `${{{{ env.RELEASE_REF }}}}` from vars.ENGINE_REF with its shape checked."
- ),
- ));
- continue;
- };
- // The variable's definition: the job's own env first, then the workflow's.
- let def = key_value(&job_text, var).or_else(|| key_value(&top_env, var));
- match def.as_deref() {
- Some(d) if is_sha40(d) => continue,
- Some(d) if d.starts_with("${{") => {}
- other => {
- bad.push(Finding::new(
- "R16",
- format!(
- "{name}: job `{job}` checks out {ENGINE_REPO} at ${var} = `{}`, which \
- is not a commit: a branch or tag the engine's owner can move. Pin a \
- 40-hex sha, or set {var} from vars.ENGINE_REF.",
- other.unwrap_or("(undefined)")
- ),
- ));
- continue;
- }
- }
- let guarded = lines[..i].iter().any(|l| {
- l.contains(SHA40_TEST)
- && (l.contains(&format!("${var}\""))
- || l.contains(&format!("${{{var}}}"))
- || l.contains(&format!("${var} ")))
- && l.contains("exit")
- });
- if !guarded {
- bad.push(Finding::new(
- "R16",
- format!(
- "{name}: job `{job}` checks out {ENGINE_REPO} at the variable ${var} with \
- no step before it that tests ${var} `{SHA40_TEST}` and exits: an unset or \
- malformed variable then checks out the engine's default branch instead \
- of failing."
- ),
- ));
- }
- if !verifies {
- bad.push(Finding::new(
- "R16",
- format!(
- "{name}: job `{job}` pins the engine through ${var} but never runs \
- `{SHA256_CHECK}` against ${SHA256_VAR}: a variable pin is only a pin \
- when the bytes are checked at run time."
- ),
- ));
- }
- }
- }
- bad
-}
-
/// One finding, carrying the rule id that owns it so the row set is derived from the findings
/// rather than restated beside them.
#[derive(Debug, Clone)]
@@ -1267,12 +779,6 @@ pub fn check(cx: &Ctx) -> Result, String> {
}
}
- // R16. THE ENGINE RUNS FROM A COMMIT, AND ITS BYTES FROM A DIGEST ([`engine_pin_findings`]).
- for name in &names {
- let text = strip_comments(&read(name).unwrap_or_default());
- bad.extend(engine_pin_findings(name, &text));
- }
-
// R17. NO JOB HARD-CODES A VENDOR RUNNER LABEL.
//
// Every job asks the runner gateway for a logical size (small, medium, large, xlarge) and the
@@ -1289,38 +795,12 @@ pub fn check(cx: &Ctx) -> Result, String> {
format!(
"{name}:{line}: job '{job}' runs on the vendor label '{label}'. A workflow \
never names a runner vendor: use `fromJSON(vars.RUNNERS)..label` \
- (size is small, medium, large or xlarge) or the preflight output \
- `needs.preflight.outputs.runner`."
+ (size is small, medium, large or xlarge)."
),
));
}
}
- // R18. A STEP THAT DOWNLOADS THE RELEASE ARTIFACT FROM ITS OWN RUN RUNS ONLY WHERE IT IS BUILT.
- //
- // The shards of a rung that runs the oracle or conformance (`needs_release_artifact=true`: the
- // predev->dev pull request and up) download `slsa-artifact-` from their own run, after
- // `ci turnstile await-job` reads `slsa-build` green, and a SKIPPED job reads green there. With
- // slsa-build gated to merge groups, every predev->dev run waited on nothing, failed that
- // download on all four shards, and the fold called it INFRA: #740, every run. The rule holds
- // the builder's `if:` to admit every condition a same-run download of its artifact runs under.
- for name in &names {
- let text = strip_comments(&read(name).unwrap_or_default());
- bad.extend(release_artifact_findings(name, &text));
- }
-
- // R19. NO JOB THAT RUNS CANDIDATE CODE HOLDS THE R2 CACHE WRITE KEYS ([`r2_write_key_findings`]).
- //
- // The shards and the one-runner hop passed `R2_KEY_ID` / `R2_KEY_SECRET` (the write keys) to
- // `ci hop`, whose judge runs the candidate's build scripts, tests and xtask: any of them could
- // read the keys and write the bucket every later hop reads its base reports and step PASSes
- // from. busbar-release 069f032 refuses (INFRA) a hop that holds them; this rule keeps the keys
- // from coming back to any job that runs candidate code, where the hop's refusal is not there.
- for name in &names {
- let text = strip_comments(&read(name).unwrap_or_default());
- bad.extend(r2_write_key_findings(name, &text));
- }
-
Ok(bad)
}
@@ -1335,10 +815,7 @@ fn rule_title(rule: &str) -> &'static str {
"R11" => "no workflow pushes a commit to a release branch",
"R14" => "every third-party action runs from a commit sha",
"R15" => "every attestation verify names the workflow that signed",
- "R16" => "the engine runs from a commit sha, its bytes from a sha256",
"R17" => "no job hard-codes a vendor runner label",
- "R18" => "a same-run download of the release artifact runs only where it is built",
- "R19" => "no job that runs candidate code holds the R2 cache write keys",
_ => "workflow rule",
}
}
@@ -1526,7 +1003,7 @@ fn mutations() -> Vec {
file: "zz-planted-verify.yml",
rule: "R15",
apply: |_| {
- "jobs:\n v:\n steps:\n - run: |\n gh attestation verify ./x --repo GetBusbar/busbar --signer-workflow GetBusbar/busbar/.github/workflows/promote.yml\n"
+ "jobs:\n v:\n steps:\n - run: |\n gh attestation verify ./x --repo GetBusbar/busbar --signer-workflow GetBusbar/busbar/.github/workflows/plugin-ci.yml\n"
.to_string()
},
creates: true,
@@ -1579,24 +1056,31 @@ fn mutations() -> Vec {
creates: false,
},
Mutation {
- // THE SAME TWO REGRESSIONS IN THE FLOW SPELLING the pipeline workflow itself is
- // written in: a step as one `{ uses: ..., with: ... }` mapping.
+ // THE SAME TWO REGRESSIONS IN THE FLOW SPELLING: a step written as one
+ // `{ uses: ..., with: ... }` mapping. No remaining workflow is written that way (the
+ // flow-spelled promote.yml is deleted), so the regression is a workflow that is.
label: "R14 a flow-style action reverts from a sha to a force-movable tag",
- file: "promote.yml",
+ file: "zz-planted-flow-pin.yml",
rule: "R14",
- apply: |t| retag_first_pin(t, false),
- creates: false,
+ apply: |_| {
+ "jobs:\n a:\n steps:\n - { uses: actions/checkout@v7, with: { fetch-depth: 0 } }\n"
+ .to_string()
+ },
+ creates: true,
},
Mutation {
label: "R14 a flow-style pin loses the trailing tag comment Dependabot reads",
- file: "promote.yml",
+ file: "zz-planted-flow-pin.yml",
rule: "R14",
- apply: |t| retag_first_pin(t, true),
- creates: false,
+ apply: |_| {
+ "jobs:\n a:\n steps:\n - { uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1, with: { fetch-depth: 0 } }\n"
+ .to_string()
+ },
+ creates: true,
},
Mutation {
label: "R1 a v* tag trigger comes back",
- file: "promote.yml",
+ file: "plugin-ci.yml",
rule: "R1",
apply: |t| replant_on(t, "on:\n push:\n tags:\n - \"v*\"\n"),
creates: false,
@@ -1605,75 +1089,17 @@ fn mutations() -> Vec {
// THE SAME RULE, IN THE OTHER YAML SPELLING. The block form above was caught; the flow
// form is ordinary YAML the runner treats identically.
label: "R1 a v* tag trigger comes back as a FLOW sequence",
- file: "promote.yml",
+ file: "plugin-ci.yml",
rule: "R1",
apply: |t| replant_on(t, "on:\n push:\n tags: [\"v*\"]\n"),
creates: false,
},
- Mutation {
- // THE REGRESSION AS IT WOULD ACTUALLY ARRIVE: "just follow dev" -- the engine checked
- // out at its branch, so every hop runs whatever dev holds at that minute.
- label: "R16 the engine is checked out at a branch",
- file: "promote.yml",
- rule: "R16",
- apply: |t| {
- replace_once(
- t,
- "repository: GetBusbar/busbar-release, ref: \"${{ env.RELEASE_REF }}\"",
- "repository: GetBusbar/busbar-release, ref: dev",
- )
- },
- creates: false,
- },
- Mutation {
- // The same through the variable: the pin becomes a release tag.
- label: "R16 the engine pin becomes a movable tag",
- file: "promote.yml",
- rule: "R16",
- apply: |t| {
- replace_once(
- t,
- "RELEASE_REF: \"${{ vars.ENGINE_REF }}\"",
- "RELEASE_REF: v1.6.0",
- )
- },
- creates: false,
- },
- Mutation {
- // The variable form loses its shape check: an unset variable is an empty `ref:`, and
- // an empty `ref:` is the default branch, a silent fallback instead of a FAIL.
- label: "R16 the variable pin's 40-hex shape check is dropped",
- file: "promote.yml",
- rule: "R16",
- apply: |t| drop_first_line_with(t, SHA40_TEST),
- creates: false,
- },
- Mutation {
- label: "R16 the engine binary runs without its sha256 check",
- file: "promote.yml",
- rule: "R16",
- apply: |t| drop_first_line_with(t, SHA256_CHECK),
- creates: false,
- },
- Mutation {
- label: "R16 the pinned oracle runs without its sha256 check",
- file: "promote.yml",
- rule: "R16",
- apply: |t| {
- replace_once(
- t,
- "echo \"$ORACLE_SHA256 $RUNNER_TEMP/busbar-oracle\" | sha256sum -c -\n",
- "",
- )
- },
- creates: false,
- },
Mutation {
// The refspec form a promote script documents, which the `HEAD:` test and the
// no-refspec test both walked past. THE ANCHOR IS `steps:`, NOT A `uses:` LINE: a
// structural feature of every job cannot be renamed by a pin bump or a formatting pass.
label: "R11 a workflow pushes a refspec straight to main",
- file: "promote.yml",
+ file: "plugin-ci.yml",
rule: "R11",
apply: |t| {
replace_once(
@@ -1684,109 +1110,9 @@ fn mutations() -> Vec {
},
creates: false,
},
- Mutation {
- // THE REGRESSION AS IT ARRIVED (#740): the release build gated to merge groups while
- // the predev->dev pull request's shards still download its artifact from their run.
- label: "R18 slsa-build goes back to merge-group-only under a pull request's download",
- file: "promote.yml",
- rule: "R18",
- apply: |t| {
- // The shards no longer download the artifact (nothing they run reads it), so the
- // pull request's same-run download is planted back into hop-shard, gated on the
- // rung as it was, with slsa-build narrowed to merge groups.
- let t = plant_step_in_job(
- t,
- "hop-shard",
- " - if: needs.preflight.outputs.needs_release_artifact == 'true'\n uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1\n with: { name: \"slsa-artifact-${{ env.SHA }}\", path: slsa }\n",
- );
- reif_job(
- &t,
- "slsa-build",
- "github.event_name == 'merge_group' && needs.preflight.outputs.run == '1'",
- )
- },
- creates: false,
- },
- Mutation {
- // THE REGRESSION AS IT STOOD BEFORE busbar-release 069f032: a hop shard's `ci hop` holds
- // the R2 write key again, where every candidate process it starts can read it.
- label: "R19 a hop shard's `ci hop` holds the R2 cache write key",
- file: "promote.yml",
- rule: "R19",
- apply: |t| {
- replace_once(
- t,
- r#"R2_RO_KEY_ID: "${{ secrets.R2_CACHE_RO_ACCESS_KEY_ID }}""#,
- r#"R2_KEY_ID: "${{ secrets.R2_CACHE_ACCESS_KEY_ID }}""#,
- )
- },
- creates: false,
- },
]
}
-/// Insert `step` (a whole ` - ...\n` step) as the first step of job `job`. A renamed job or one
-/// without `steps:` edits nothing, which the selftest reports as unproven.
-fn plant_step_in_job(t: &str, job: &str, step: &str) -> String {
- let head = format!(" {job}:");
- let mut in_job = false;
- let mut done = false;
- let mut out = String::with_capacity(t.len() + step.len());
- for line in t.split_inclusive('\n') {
- let bare = line.trim_end_matches('\n');
- if opens_key_at(bare, 2) {
- in_job = bare.trim_end() == head;
- }
- out.push_str(line);
- if !done && in_job && bare == " steps:" {
- out.push_str(step);
- done = true;
- }
- }
- out
-}
-
-/// Replace job `job`'s own `if:` line with `if: `. The anchor is the job id and its
-/// four-space `if:` key, not today's condition, so rewording the condition cannot unprove R18; a
-/// renamed or unconditional job edits nothing, which the selftest reports as unproven.
-fn reif_job(t: &str, job: &str, cond: &str) -> String {
- let head = format!(" {job}:");
- let mut in_job = false;
- let mut done = false;
- let mut out = String::with_capacity(t.len() + cond.len());
- for line in t.split_inclusive('\n') {
- let bare = line.trim_end_matches('\n');
- if opens_key_at(bare, 2) {
- in_job = bare.trim_end() == head;
- }
- if !done && in_job && opens_key_at(bare, 4) && bare.trim_start().starts_with("if:") {
- out.push_str(" if: ");
- out.push_str(cond);
- out.push('\n');
- done = true;
- } else {
- out.push_str(line);
- }
- }
- out
-}
-
-/// `t` without the first line containing `needle` (unchanged when none does: the case then
-/// reports its anchor moved).
-fn drop_first_line_with(t: &str, needle: &str) -> String {
- let mut done = false;
- t.split_inclusive('\n')
- .filter(|l| {
- if !done && l.contains(needle) {
- done = true;
- false
- } else {
- true
- }
- })
- .collect()
-}
-
/// The R14 mutations both target the FIRST fully-pinned third-party action in the file, found by
/// shape rather than by a literal sha. A literal would rot the day that action is bumped, and a
/// mutation that edits nothing proves nothing.
diff --git a/xtask/tests/infra.rs b/xtask/tests/infra.rs
index 61f9dee334..fbb2ea17da 100644
--- a/xtask/tests/infra.rs
+++ b/xtask/tests/infra.rs
@@ -722,35 +722,35 @@ jobs:
}
#[test]
-fn the_real_pipeline_workflow_parses_and_its_gate_call_sites_are_discoverable() {
- let text = std::fs::read_to_string(repo_root().join(".github/workflows/promote.yml")).unwrap();
- let wf = yaml_lite::parse_workflow(&text).expect("the real promote.yml parses");
+fn every_gate_the_modernci_branch_files_call_is_registered() {
+ // THE PIPELINE IS `.ci/*.yaml` (OWNER 2026-10-12, "ModernCI only"): ModernCI runs each branch
+ // file's checks on every move, and promote.yml, the busbar-release hop, is deleted. EVERY GATE A
+ // BRANCH FILE CALLS BY NAME IS ONE THE REGISTRY ANSWERS TO: a `cargo xtask gate ` is a
+ // check that exits 2 on every move, and the registry is what can say so here rather than in a
+ // red move. `.ci/default.yaml` calls `abi-header` by name, so the scan is never empty.
+ let dir = repo_root().join(".ci");
+ let mut files: Vec<_> = std::fs::read_dir(&dir)
+ .unwrap()
+ .filter_map(|e| e.ok().map(|e| e.path()))
+ .filter(|p| p.extension().is_some_and(|x| x == "yaml" || x == "yml"))
+ .collect();
+ files.sort();
+ let mut called_any = false;
+ for path in &files {
+ let text = std::fs::read_to_string(path).unwrap();
+ for name in yaml_lite::xtask_gate_invocations(&text) {
+ called_any = true;
+ assert!(
+ gates::find(&name).is_some(),
+ "{} calls `cargo xtask gate {name}`, which no registration answers to",
+ path.display()
+ );
+ }
+ }
assert!(
- wf.job_names().len() >= 2,
- "promote.yml carries the preflight and hop jobs: {:?}",
- wf.job_names()
+ called_any,
+ "no `.ci/*.yaml` calls a gate by name: the scan read nothing ({files:?})"
);
- assert!(
- wf.job_names()
- .iter()
- .any(|j| wf.job(j).unwrap().steps.iter().any(|s| s.run.is_some())),
- "every job's `run:` steps must be visible to the reader"
- );
-
- // EVERY GATE THE PIPELINE CALLS BY NAME IS ONE THE REGISTRY ANSWERS TO: a `cargo xtask gate
- // ` is a step that exits 2 on every hop, and the registry is what can say so here rather
- // than in a red run.
- // promote.yml is a thin bootstrap: it runs busbar-release's `busbar-release ci hop` at
- // RELEASE_REF, and the gate calls (the C header's `abi-header`, the registry's `--list`) live in
- // that engine, not in this file. So the file names no gate today; the loop below keeps holding
- // anything it ever does name to the registry.
- let called = yaml_lite::xtask_gate_invocations(&text);
- for name in &called {
- assert!(
- gates::find(name).is_some(),
- "promote.yml calls `cargo xtask gate {name}`, which no registration answers to"
- );
- }
assert_eq!(
yaml_lite::xtask_gate_invocations(" run: |\n cargo xtask gate plane-purity --selftest\n cargo xtask gate plane-purity\n"),
vec!["plane-purity".to_string()]
]