Skip to content

Commit 52db79b

Browse files
fix(ci): clear residual workflow startup_failures (secrets-in-if, hashFiles-in-job-if) (#363)
## What Fixes the workflow **`startup_failure`** classes that fail *before any job runs* and were **left on `main` after #360/#361**. Rebased onto current `main` (`bec6161`); now scoped to exactly the two remaining root causes. ## Why this is still needed after #360 / #361 #360 added `timeout-minutes` + fixed changelog YAML; #361 added more `timeout-minutes` + fixed two malformed workflows. Neither addressed the two `startup_failure` causes below — I re-verified both are still present on `bec6161`. (Where they overlapped, the upstream fixes are kept: `changelog-reusable.yml` and `boj-build.yml`'s malformed YAML are already fixed upstream, so they drop out of this PR.) ## Fixes ### 1. `secrets` context in `if:` → *"Unrecognized named-value: 'secrets'"* (parse-time startup failure) | File | Before | Fix | |---|---|---| | `boj-build.yml` | job-level `if: …secrets.BOJ_SERVER_URL…` | **Drop the job `if`** — the Trigger step already self-gates (exits 0 on empty `BOJ_URL`), so the job is a cheap no-op when unconfigured. | | `instant-sync.yml` | step `if: secrets.FARM_DISPATCH_TOKEN …` (×2) | Map secret → job-level `env`, gate steps on `env.FARM_DISPATCH_TOKEN`. | | `mirror-reusable.yml` (`mirror-radicle`) | step `if: secrets.RADICLE_KEY …` (×4) | Map secret → job-level `env`, gate steps on `env.RADICLE_KEY`. | `env` **is** available in step `if:` and secrets **are** valid in job-level `env` — this is the canonical presence-gate pattern and is correct whether or not `secrets`-in-`if` parses, so it can't regress a working file. ### 2. `hashFiles()` in a **job-level** `if:` (always `''` → job silently skipped on every repo) Job `if:` is evaluated server-side **before checkout**, so `hashFiles(...)` sees an empty workspace and returns `''` — meaning `rust-ci`/`elixir-ci` skipped on **every** consumer, Rust/Elixir or not. - `rust-ci-reusable.yml` (×4) and `elixir-ci-reusable.yml` (×1): add a small **`detect`** job that checks out and exports a real boolean (`has_cargo` / `has_mix`); the real jobs gate on `needs.detect.outputs.*`. Behaviour preserved; the guard actually works now. (Step-level `with: hashFiles(mix.lock)` for caching is untouched — that runs on the runner and is fine.) ## Verification - All 5 files parse as YAML. - `git grep 'if:.*secrets\.'` over all workflows → **none**. - `git grep 'if:.*hashFiles'` over all workflows → **none** (step-level `with:` usages remain, correctly). ## Known Hypatia scan findings (no action — see below) The non-blocking Hypatia scan reports `secret_action_without_presence_gate` on: - **`instant-sync.yml` · `peter-evans/repository-dispatch` (high)** — **false-positive introduced by this PR's correct fix.** The rule pattern-matches a literal `secrets.X != ''` gate on the step; I replaced it with `env.FARM_DISPATCH_TOKEN != ''` precisely because `secrets`-in-`if` is the `startup_failure` being fixed. The presence gate genuinely still exists (env mapped from the secret) — Hypatia just doesn't recognise the env-indirection. Satisfying the rule would mean reintroducing the real startup failure, so **GitHub correctness wins**. The proper root fix is to teach Hypatia's `secret_action_without_presence_gate` rule to accept an `env`-mapped-from-`secret` gate (separate change in the `hypatia` repo). - **`mirror-reusable.yml` · `webfactory/ssh-agent` (high, ×2)** — pre-existing (GITEA key), unrelated to this PR; a hygiene flag, not a `startup_failure`. ## Registry-verify on this PR The `Registry + topology in sync` check is red here, but **not because of this PR** — these five edits are all top-level `.github/workflows/*` files (no spec home), adding zero registry drift. It's the **pre-existing `main` drift that #366 fixes** (inherited because this branched off `main`). Once #366 lands, a rebase clears it. Deliberately not regenerating the registry here, to avoid colliding with #366. Consumers pin the reusables by SHA, so callers are unaffected until they bump their pin. https://claude.ai/code/session_0178nMYCNXgotTeekePkoUjd Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6022e49 commit 52db79b

5 files changed

Lines changed: 92 additions & 17 deletions

File tree

.github/workflows/boj-build.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,11 @@ jobs:
1919
trigger-boj:
2020
runs-on: ubuntu-latest
2121
timeout-minutes: 10
22-
if: ${{ vars.BOJ_SERVER_URL != '' || secrets.BOJ_SERVER_URL != '' }}
22+
# No job-level gate: the `secrets` context is not available in `if:`
23+
# (referencing it is an "Unrecognized named-value: 'secrets'" startup
24+
# failure). The Trigger step below already self-gates — it exits 0 when
25+
# BOJ_URL (secret-or-var) is empty — so the whole job is a cheap no-op
26+
# when unconfigured.
2327
steps:
2428
- name: Checkout
2529
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

.github/workflows/elixir-ci-reusable.yml

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,12 +98,41 @@ permissions:
9898
contents: read
9999

100100
jobs:
101+
# Guard on mix.exs so the wrapper is safe to add unconditionally. This must
102+
# be a checked-out step, NOT a job-level `if: hashFiles(...)`: job `if:` is
103+
# evaluated server-side before any checkout, so hashFiles() sees an empty
104+
# workspace and always returns '' — which silently skipped the test job on
105+
# EVERY repo, Elixir or not. The detect job checks out and exposes a real
106+
# boolean output the test job gates on.
107+
detect:
108+
timeout-minutes: 5
109+
name: Detect mix.exs
110+
runs-on: ${{ inputs.runs-on }}
111+
permissions:
112+
contents: read
113+
outputs:
114+
has_mix: ${{ steps.detect.outputs.has_mix }}
115+
steps:
116+
- name: Checkout repository
117+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
118+
with:
119+
repository: ${{ github.repository }}
120+
ref: ${{ github.ref }}
121+
- name: Detect mix.exs
122+
id: detect
123+
run: |
124+
if [ -f "${{ inputs.working_directory }}/mix.exs" ]; then
125+
echo "has_mix=true" >> "$GITHUB_OUTPUT"
126+
else
127+
echo "has_mix=false" >> "$GITHUB_OUTPUT"
128+
fi
129+
101130
test:
102131
timeout-minutes: 20
103132
name: Compile + test
104133
runs-on: ${{ inputs.runs-on }}
105-
# Guard on mix.exs so the wrapper is safe to add unconditionally.
106-
if: ${{ hashFiles(format('{0}/mix.exs', inputs.working_directory)) != '' }}
134+
needs: detect
135+
if: ${{ needs.detect.outputs.has_mix == 'true' }}
107136
permissions:
108137
contents: read
109138
env:

.github/workflows/instant-sync.yml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ jobs:
1515
dispatch:
1616
timeout-minutes: 10
1717
runs-on: ubuntu-latest
18+
# Map the secret to env so step `if:`s can gate on its presence: the
19+
# `secrets` context is NOT available in `if:` (using it is an
20+
# "Unrecognized named-value: 'secrets'" startup failure). `env` IS
21+
# available in step `if:`, and secrets are valid in job-level `env`.
22+
env:
23+
FARM_DISPATCH_TOKEN: ${{ secrets.FARM_DISPATCH_TOKEN }}
1824
steps:
1925
# Gate the cross-repo repository_dispatch on FARM_DISPATCH_TOKEN
2026
# being configured. Without the PAT, peter-evans/repository-dispatch
@@ -24,7 +30,7 @@ jobs:
2430
# gracefully skips on repos where the secret has not been
2531
# propagated, instead of red-ing main on every push.
2632
- name: Trigger Propagation
27-
if: ${{ secrets.FARM_DISPATCH_TOKEN != '' }}
33+
if: ${{ env.FARM_DISPATCH_TOKEN != '' }}
2834
uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v3
2935
with:
3036
token: ${{ secrets.FARM_DISPATCH_TOKEN }}
@@ -39,7 +45,7 @@ jobs:
3945
}
4046
4147
- name: Skipped (FARM_DISPATCH_TOKEN not configured)
42-
if: ${{ secrets.FARM_DISPATCH_TOKEN == '' }}
48+
if: ${{ env.FARM_DISPATCH_TOKEN == '' }}
4349
env:
4450
REPO_NAME: ${{ github.event.repository.name }}
4551
run: |

.github/workflows/mirror-reusable.yml

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,12 @@ jobs:
159159
timeout-minutes: 20
160160
runs-on: ${{ inputs.runs-on }}
161161
if: vars.RADICLE_MIRROR_ENABLED == 'true'
162+
# Map the secret to env so step `if:`s can gate on its presence: the
163+
# `secrets` context is NOT available in `if:` (using it is an
164+
# "Unrecognized named-value: 'secrets'" startup failure). `env` IS
165+
# available in step `if:`, and secrets are valid in job-level `env`.
166+
env:
167+
RADICLE_KEY: ${{ secrets.RADICLE_KEY }}
162168
steps:
163169
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
164170
with:
@@ -174,23 +180,23 @@ jobs:
174180
# answers "is Radicle mirror desired here?"; the secret gate
175181
# answers "are we configured to actually do it?".
176182
- name: Setup Rust
177-
if: ${{ secrets.RADICLE_KEY != '' }}
183+
if: ${{ env.RADICLE_KEY != '' }}
178184
uses: dtolnay/rust-toolchain@efa25f7f19611383d5b0ccf2d1c8914531636bf9 # stable
179185
with:
180186
toolchain: stable
181187
- name: Install Radicle
182-
if: ${{ secrets.RADICLE_KEY != '' }}
188+
if: ${{ env.RADICLE_KEY != '' }}
183189
run: |
184190
cargo install radicle-cli --locked
185191
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
186192
- name: Mirror to Radicle
187-
if: ${{ secrets.RADICLE_KEY != '' }}
193+
if: ${{ env.RADICLE_KEY != '' }}
188194
run: |
189195
mkdir -p ~/.radicle/keys
190196
echo "${{ secrets.RADICLE_KEY }}" > ~/.radicle/keys/radicle
191197
chmod 600 ~/.radicle/keys/radicle
192198
rad sync --announce || echo "Radicle sync attempted"
193199
- name: Skipped (RADICLE_KEY not configured)
194-
if: ${{ secrets.RADICLE_KEY == '' }}
200+
if: ${{ env.RADICLE_KEY == '' }}
195201
run: |
196202
echo "::notice::RADICLE_MIRROR_ENABLED=true but secrets.RADICLE_KEY is empty. Skipping Radicle mirror. Configure the RADICLE_KEY org/repo secret to enable."

.github/workflows/rust-ci-reusable.yml

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -100,14 +100,42 @@ permissions:
100100
contents: read
101101

102102
jobs:
103-
# Skip the whole reusable when the repo has no Cargo.toml — lets
104-
# consumers add the wrapper unconditionally without worrying about
105-
# repos that don't ship Rust code at the moment.
103+
# Skip the whole reusable when the repo has no Cargo.toml — lets consumers
104+
# add the wrapper unconditionally without worrying about repos that don't
105+
# ship Rust code yet. This MUST be a checked-out step, NOT a job-level
106+
# `if: hashFiles(...)`: job `if:` is evaluated server-side before any
107+
# checkout, so hashFiles() sees an empty workspace and always returns ''
108+
# — which silently skipped every job on EVERY repo, Rust or not. The
109+
# detect job checks out and exposes a real boolean the others gate on.
110+
detect:
111+
timeout-minutes: 5
112+
name: Detect Cargo.toml
113+
runs-on: ${{ inputs.runs-on }}
114+
permissions:
115+
contents: read
116+
outputs:
117+
has_cargo: ${{ steps.detect.outputs.has_cargo }}
118+
steps:
119+
- name: Checkout repository
120+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
121+
with:
122+
repository: ${{ github.repository }}
123+
ref: ${{ github.ref }}
124+
- name: Detect Cargo.toml
125+
id: detect
126+
run: |
127+
if [ -f "${{ inputs.working_directory }}/Cargo.toml" ]; then
128+
echo "has_cargo=true" >> "$GITHUB_OUTPUT"
129+
else
130+
echo "has_cargo=false" >> "$GITHUB_OUTPUT"
131+
fi
132+
106133
check:
107134
timeout-minutes: 20
108135
name: Cargo check + clippy + fmt
109136
runs-on: ${{ inputs.runs-on }}
110-
if: ${{ hashFiles(format('{0}/Cargo.toml', inputs.working_directory)) != '' }}
137+
needs: detect
138+
if: ${{ needs.detect.outputs.has_cargo == 'true' }}
111139
permissions:
112140
contents: read
113141
defaults:
@@ -147,8 +175,8 @@ jobs:
147175
timeout-minutes: 20
148176
name: Cargo test
149177
runs-on: ${{ inputs.runs-on }}
150-
needs: check
151-
if: ${{ hashFiles(format('{0}/Cargo.toml', inputs.working_directory)) != '' }}
178+
needs: [detect, check]
179+
if: ${{ needs.detect.outputs.has_cargo == 'true' }}
152180
permissions:
153181
contents: read
154182
defaults:
@@ -187,7 +215,8 @@ jobs:
187215
timeout-minutes: 20
188216
name: Cargo audit (security)
189217
runs-on: ${{ inputs.runs-on }}
190-
if: ${{ inputs.enable_audit && hashFiles(format('{0}/Cargo.toml', inputs.working_directory)) != '' }}
218+
needs: detect
219+
if: ${{ inputs.enable_audit && needs.detect.outputs.has_cargo == 'true' }}
191220
permissions:
192221
contents: read
193222
defaults:
@@ -216,7 +245,8 @@ jobs:
216245
timeout-minutes: 20
217246
name: Coverage (tarpaulin + codecov)
218247
runs-on: ${{ inputs.runs-on }}
219-
if: ${{ inputs.enable_coverage && hashFiles(format('{0}/Cargo.toml', inputs.working_directory)) != '' }}
248+
needs: detect
249+
if: ${{ inputs.enable_coverage && needs.detect.outputs.has_cargo == 'true' }}
220250
permissions:
221251
contents: read
222252
defaults:

0 commit comments

Comments
 (0)