From 1549e35d0a1ca3e09859c6393fc1c148e167f032 Mon Sep 17 00:00:00 2001 From: hyperpolymath <6759885+hyperpolymath@users.noreply.github.com> Date: Sat, 30 May 2026 17:41:33 +0100 Subject: [PATCH] ci(reusables): gate Radicle mirror + Instant Sync dispatch on secret presence MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Two source-level fixes — zero fan-out needed ### mirror-reusable.yml: mirror-radicle Adds `if: secrets.RADICLE_KEY != ''` to Setup Rust / Install Radicle / Mirror to Radicle steps + `mkdir -p ~/.radicle/keys` before the key write (previously `echo > ~/.radicle/keys/radicle` errored with "No such file or directory" on a never-created dir). Adds a final advisory "Skipped" step on the inverse gate so the workflow prints a one-line :::notice instead of failing. Caught **26 estate repos** on the 2026-05-30 audit. Net effect: repos with RADICLE_MIRROR_ENABLED=true but no RADICLE_KEY secret get a green informational notice instead of a red "No such file" failure on every push. ### instant-sync.yml: dispatch Adds `if: secrets.FARM_DISPATCH_TOKEN != ''` to the Trigger Propagation step + an inverse :::notice step. Without the PAT, `peter-evans/repository-dispatch` cannot dispatch cross-repo and returns HTTP 401 — failing every push to every repo where the secret has not been propagated. Caught **39 estate repos** on the 2026-05-30 audit. Net effect: repos without the org-level PAT get a green notice instead of a red 401. ## Why source-level Both reusables are called via `secrets: inherit` from ~289 mirror.yml + instant-sync.yml caller wrappers across the estate. A source-level gate auto-resolves all callers — no per-repo fan-out PR needed. ## Owner action for full activation - mirror-radicle: configure `RADICLE_KEY` (per-repo or org-level secret); the gate then unblocks the existing mirror flow. - instant-sync: configure `FARM_DISPATCH_TOKEN` (org-level PAT with repo scope); the gate then unblocks the cross-repo dispatch. This PR makes those owner actions OPTIONAL — without the secret the workflow is no-op; with the secret it works. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/instant-sync.yml | 15 +++++++++++++++ .github/workflows/mirror-reusable.yml | 18 ++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/.github/workflows/instant-sync.yml b/.github/workflows/instant-sync.yml index d022c3e0..a18edfdc 100644 --- a/.github/workflows/instant-sync.yml +++ b/.github/workflows/instant-sync.yml @@ -15,7 +15,15 @@ jobs: dispatch: runs-on: ubuntu-latest steps: + # Gate the cross-repo repository_dispatch on FARM_DISPATCH_TOKEN + # being configured. Without the PAT, peter-evans/repository-dispatch + # falls back to GITHUB_TOKEN — which cannot dispatch cross-repo and + # returns HTTP 401 "Bad credentials", failing the job. Caught 39 + # estate repos on the 2026-05-30 audit. With this gate the workflow + # gracefully skips on repos where the secret has not been + # propagated, instead of red-ing main on every push. - name: Trigger Propagation + if: ${{ secrets.FARM_DISPATCH_TOKEN != '' }} uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v3 with: token: ${{ secrets.FARM_DISPATCH_TOKEN }} @@ -29,6 +37,13 @@ jobs: "forges": "" } + - name: Skipped (FARM_DISPATCH_TOKEN not configured) + if: ${{ secrets.FARM_DISPATCH_TOKEN == '' }} + env: + REPO_NAME: ${{ github.event.repository.name }} + run: | + echo "::notice::FARM_DISPATCH_TOKEN secret not configured on ${REPO_NAME}; skipping cross-repo dispatch. Configure the org-level FARM_DISPATCH_TOKEN PAT (repo scope) to enable instant forge propagation." + - name: Confirm env: REPO_NAME: ${{ github.event.repository.name }} diff --git a/.github/workflows/mirror-reusable.yml b/.github/workflows/mirror-reusable.yml index db35cd71..d500f00f 100644 --- a/.github/workflows/mirror-reusable.yml +++ b/.github/workflows/mirror-reusable.yml @@ -156,16 +156,34 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 + # All Radicle steps gate on secrets.RADICLE_KEY being set on the + # caller repo (resolved via `secrets: inherit`). Without this gate + # the workflow burned ~3 minutes of Rust+Radicle install on every + # push to every RADICLE_MIRROR_ENABLED repo only to fail at + # `~/.radicle/keys/radicle: No such file or directory` because the + # `echo "" > ...` write into a non-existent dir errors out — and + # even if the dir existed, the empty-key write would never sync. + # Caught 26 estate repos on the 2026-05-30 audit. The vars gate + # answers "is Radicle mirror desired here?"; the secret gate + # answers "are we configured to actually do it?". - name: Setup Rust + if: ${{ secrets.RADICLE_KEY != '' }} uses: dtolnay/rust-toolchain@efa25f7f19611383d5b0ccf2d1c8914531636bf9 # stable with: toolchain: stable - name: Install Radicle + if: ${{ secrets.RADICLE_KEY != '' }} run: | cargo install radicle-cli --locked echo "$HOME/.cargo/bin" >> $GITHUB_PATH - name: Mirror to Radicle + if: ${{ secrets.RADICLE_KEY != '' }} run: | + mkdir -p ~/.radicle/keys echo "${{ secrets.RADICLE_KEY }}" > ~/.radicle/keys/radicle chmod 600 ~/.radicle/keys/radicle rad sync --announce || echo "Radicle sync attempted" + - name: Skipped (RADICLE_KEY not configured) + if: ${{ secrets.RADICLE_KEY == '' }} + run: | + echo "::notice::RADICLE_MIRROR_ENABLED=true but secrets.RADICLE_KEY is empty. Skipping Radicle mirror. Configure the RADICLE_KEY org/repo secret to enable."