Skip to content

Commit ece30ad

Browse files
ci: make path-filtered required gates always report (unblock stuck PRs) (#216)
## What this fixes PRs **#213** (dependabot `flake.lock`) and **#215** (scripts-only) are stuck at `mergeable_state: blocked` despite every check green (and #213 approved). Root cause: seven gates are **workflow-level path-filtered** (`on.*.paths`). When a PR touches none of a gate's paths the workflow never runs, so its **required** status check stays *"Expected"* forever → permanent block. The asymmetry this exploits: **a path-filtered workflow that never runs blocks; a job skipped via `if:` reports SUCCESS to required checks.** This converts the former into the latter. ## Change (uniform across all 7) `abi-drift`, `backend-assurance`, `e2e`, `lsp-dap-bsp`, `proofs`, `truthfulness`, `zig-test`: 1. **Drop the `on.*.paths` filter** → the workflow always runs, so the required check is always created. 2. **Add a lightweight `changes` job** that recomputes the gate's *original* path set via `git diff origin/<base>...HEAD` (the same pattern `abi-drift`/`zig-test` already use internally). 3. **Gate every heavy job** with `needs: changes` + `if: needs.changes.outputs.run == 'true'`. Nothing relevant changed → heavy job **skipped → passes** → PR unblocked *and* no wasted CI. **No branch-protection change needed** — job/check names are unchanged. `workflow_dispatch` added to all 7 for manual full runs. ## Safety - **Fail-safe toward running:** the detector defaults `run=true` and only sets `false` when a *successful* diff shows no relevant path changed (any fetch/diff error or unknown base ⇒ run). The dangerous direction — skipping a gate that *should* run — can't happen on detection failure. - Each regex **mirrors the gate's original `paths:`**, so a relevant change runs it exactly as before (verified: 77/77 unit checks — `abi/`→proofs+abi-drift, `ffi/`→zig+truthfulness+abi-drift, `SafetyLemmas.idr`→backend-assurance+proofs, etc.; #213/#215 → all skip). ## Validation ``` actionlint -shellcheck= (all 7) → EXIT 0 (structure/expressions/needs-graph valid) actionlint (all 7) → only PRE-EXISTING shellcheck infos in original run-scripts (untouched) detection regexes (77 checks) → 77/77 correct gated heavy jobs → 15/15 (abi-drift 1, backend-assurance 1, e2e 5, lsp-dap-bsp 4, proofs 2, truthfulness 1, zig-test 1) leftover path filters → 0 ``` **Self-validating:** editing a workflow file no longer self-triggers its heavy gate (kept out of each regex), so **this PR's own checks exercise the skip path** — the gates should report *skipped/success* here, demonstrating the unblock live. ## Draft — why I can't runtime-test the `run=true` branch (it needs the GH runners), and this changes how **required** gates fire, so it wants your eyes before merge. Mark ready / squash-merge when you're satisfied. After it lands, re-check #213/#215 — they should flip from `blocked` to mergeable. > Out of scope (flagged, not fixed): the pre-existing shellcheck infos in `abi-drift`/`zig-test` original scripts, and the Hypatia baseline backlog (stale `GEMINI.md`, unpinned `governance.yml` action, `missing_timeout_minutes` on several workflows) — these are non-blocking and belong in a separate hygiene pass. https://claude.ai/code/session_019tMcRS1Dm1nWjjYP4WvbJa --- _Generated by [Claude Code](https://claude.ai/code/session_019tMcRS1Dm1nWjjYP4WvbJa)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent da74a5f commit ece30ad

7 files changed

Lines changed: 273 additions & 87 deletions

File tree

.github/workflows/abi-drift.yml

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,17 @@
1616

1717
name: ABI Drift Gate
1818

19+
# Was workflow-level path-filtered: a required check that never ran on PRs
20+
# touching none of its paths, leaving the check "Expected" and the PR blocked.
21+
# Now always runs; the `changes` job gates the heavy `verify` job, which when
22+
# skipped reports SUCCESS to required checks. (verify also does its own
23+
# per-cartridge scoping for the relevant case.)
1924
on:
2025
push:
2126
branches: [main]
22-
paths:
23-
- 'cartridges/**/abi/**'
24-
- 'cartridges/**/ffi/**'
25-
- '.github/workflows/abi-drift.yml'
2627
pull_request:
2728
branches: [main]
28-
paths:
29-
- 'cartridges/**/abi/**'
30-
- 'cartridges/**/ffi/**'
31-
- '.github/workflows/abi-drift.yml'
29+
workflow_dispatch:
3230

3331
concurrency:
3432
group: abi-drift-${{ github.workflow }}-${{ github.ref }}
@@ -38,8 +36,39 @@ permissions:
3836
contents: read
3937

4038
jobs:
39+
changes:
40+
name: Detect relevant changes
41+
runs-on: ubuntu-latest
42+
timeout-minutes: 5
43+
outputs:
44+
run: ${{ steps.detect.outputs.run }}
45+
steps:
46+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
47+
with:
48+
fetch-depth: 0
49+
- id: detect
50+
env:
51+
EVENT: ${{ github.event_name }}
52+
BASE: ${{ github.base_ref }}
53+
BEFORE: ${{ github.event.before }}
54+
run: |
55+
set -uo pipefail
56+
run=true
57+
if [ "$EVENT" = pull_request ]; then
58+
git fetch --no-tags --depth=200 origin "$BASE" 2>/dev/null \
59+
&& changed=$(git diff --name-only "origin/${BASE}...HEAD" 2>/dev/null) \
60+
&& { printf '%s\n' "$changed" | grep -qE '^cartridges/.*/abi/|^cartridges/.*/ffi/' && run=true || run=false; }
61+
elif [ "$EVENT" = push ] && [ -n "$BEFORE" ] && [ "$BEFORE" != 0000000000000000000000000000000000000000 ]; then
62+
changed=$(git diff --name-only "${BEFORE}...${GITHUB_SHA}" 2>/dev/null) \
63+
&& { printf '%s\n' "$changed" | grep -qE '^cartridges/.*/abi/|^cartridges/.*/ffi/' && run=true || run=false; }
64+
fi
65+
printf 'run=%s\n' "$run" >> "$GITHUB_OUTPUT"
66+
echo "relevant=$run; changed files:"; printf '%s\n' "${changed:-<none computed>}"
67+
4168
verify:
4269
name: Emit manifest + verify FFI
70+
needs: changes
71+
if: needs.changes.outputs.run == 'true'
4372
runs-on: ubuntu-latest
4473
steps:
4574
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

.github/workflows/backend-assurance.yml

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,25 +18,16 @@
1818

1919
name: Backend-Assurance Harness
2020

21+
# Was workflow-level path-filtered: a required check that never ran on PRs
22+
# touching none of its paths, leaving the check "Expected" and the PR blocked.
23+
# Now always runs; the `changes` job gates the heavy job, which when skipped
24+
# reports SUCCESS to required checks.
2125
on:
2226
push:
2327
branches: [main]
24-
paths:
25-
- 'src/abi/Boj/SafetyLemmas.idr'
26-
- 'elixir/test/backend_assurance/**'
27-
- 'elixir/mix.exs'
28-
- 'elixir/mix.lock'
29-
- 'docs/backend-assurance/**'
30-
- '.github/workflows/backend-assurance.yml'
3128
pull_request:
3229
branches: [main]
33-
paths:
34-
- 'src/abi/Boj/SafetyLemmas.idr'
35-
- 'elixir/test/backend_assurance/**'
36-
- 'elixir/mix.exs'
37-
- 'elixir/mix.lock'
38-
- 'docs/backend-assurance/**'
39-
- '.github/workflows/backend-assurance.yml'
30+
workflow_dispatch:
4031

4132
concurrency:
4233
group: backend-assurance-${{ github.workflow }}-${{ github.ref }}
@@ -46,8 +37,39 @@ permissions:
4637
contents: read
4738

4839
jobs:
40+
changes:
41+
name: Detect relevant changes
42+
runs-on: ubuntu-latest
43+
timeout-minutes: 5
44+
outputs:
45+
run: ${{ steps.detect.outputs.run }}
46+
steps:
47+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
48+
with:
49+
fetch-depth: 0
50+
- id: detect
51+
env:
52+
EVENT: ${{ github.event_name }}
53+
BASE: ${{ github.base_ref }}
54+
BEFORE: ${{ github.event.before }}
55+
run: |
56+
set -uo pipefail
57+
run=true
58+
if [ "$EVENT" = pull_request ]; then
59+
git fetch --no-tags --depth=200 origin "$BASE" 2>/dev/null \
60+
&& changed=$(git diff --name-only "origin/${BASE}...HEAD" 2>/dev/null) \
61+
&& { printf '%s\n' "$changed" | grep -qE '^src/abi/Boj/SafetyLemmas\.idr$|^elixir/test/backend_assurance/|^elixir/mix\.(exs|lock)$|^docs/backend-assurance/' && run=true || run=false; }
62+
elif [ "$EVENT" = push ] && [ -n "$BEFORE" ] && [ "$BEFORE" != 0000000000000000000000000000000000000000 ]; then
63+
changed=$(git diff --name-only "${BEFORE}...${GITHUB_SHA}" 2>/dev/null) \
64+
&& { printf '%s\n' "$changed" | grep -qE '^src/abi/Boj/SafetyLemmas\.idr$|^elixir/test/backend_assurance/|^elixir/mix\.(exs|lock)$|^docs/backend-assurance/' && run=true || run=false; }
65+
fi
66+
printf 'run=%s\n' "$run" >> "$GITHUB_OUTPUT"
67+
echo "relevant=$run; changed files:"; printf '%s\n' "${changed:-<none computed>}"
68+
4969
property-test:
5070
name: BEAM property tests
71+
needs: changes
72+
if: needs.changes.outputs.run == 'true'
5173
runs-on: ubuntu-latest
5274
timeout-minutes: 10
5375
defaults:

.github/workflows/e2e.yml

Lines changed: 43 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,27 +7,15 @@
77

88
name: E2E + Aspect + Bench
99

10+
# Was workflow-level path-filtered: required checks that never ran on PRs
11+
# touching none of its paths, leaving them "Expected" and the PR blocked.
12+
# Now always runs; the `changes` job gates the heavy jobs, which when skipped
13+
# report SUCCESS to required checks.
1014
on:
1115
push:
1216
branches: [main, master, develop]
13-
paths:
14-
- 'adapter/**'
15-
- 'cartridges/**'
16-
- 'ffi/**'
17-
- 'mcp-bridge/**'
18-
- 'tests/**'
19-
- 'src/**'
20-
- '.github/workflows/e2e.yml'
2117
pull_request:
2218
branches: [main, master]
23-
paths:
24-
- 'adapter/**'
25-
- 'cartridges/**'
26-
- 'ffi/**'
27-
- 'mcp-bridge/**'
28-
- 'tests/**'
29-
- 'src/**'
30-
- '.github/workflows/e2e.yml'
3119
workflow_dispatch:
3220

3321
permissions: read-all
@@ -37,9 +25,40 @@ concurrency:
3725
cancel-in-progress: true
3826

3927
jobs:
28+
changes:
29+
name: Detect relevant changes
30+
runs-on: ubuntu-latest
31+
timeout-minutes: 5
32+
outputs:
33+
run: ${{ steps.detect.outputs.run }}
34+
steps:
35+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
36+
with:
37+
fetch-depth: 0
38+
- id: detect
39+
env:
40+
EVENT: ${{ github.event_name }}
41+
BASE: ${{ github.base_ref }}
42+
BEFORE: ${{ github.event.before }}
43+
run: |
44+
set -uo pipefail
45+
run=true
46+
if [ "$EVENT" = pull_request ]; then
47+
git fetch --no-tags --depth=200 origin "$BASE" 2>/dev/null \
48+
&& changed=$(git diff --name-only "origin/${BASE}...HEAD" 2>/dev/null) \
49+
&& { printf '%s\n' "$changed" | grep -qE '^adapter/|^cartridges/|^ffi/|^mcp-bridge/|^tests/|^src/' && run=true || run=false; }
50+
elif [ "$EVENT" = push ] && [ -n "$BEFORE" ] && [ "$BEFORE" != 0000000000000000000000000000000000000000 ]; then
51+
changed=$(git diff --name-only "${BEFORE}...${GITHUB_SHA}" 2>/dev/null) \
52+
&& { printf '%s\n' "$changed" | grep -qE '^adapter/|^cartridges/|^ffi/|^mcp-bridge/|^tests/|^src/' && run=true || run=false; }
53+
fi
54+
printf 'run=%s\n' "$run" >> "$GITHUB_OUTPUT"
55+
echo "relevant=$run; changed files:"; printf '%s\n' "${changed:-<none computed>}"
56+
4057
# ─── End-to-End: Full REST + MCP Bridge ────────────────────────────
4158
e2e-full:
4259
name: E2E — Full REST + MCP Bridge
60+
needs: changes
61+
if: needs.changes.outputs.run == 'true'
4362
runs-on: ubuntu-latest
4463
timeout-minutes: 15
4564

@@ -104,6 +123,8 @@ jobs:
104123
# ─── End-to-End: Order Ticket (FFI layer) ──────────────────────────
105124
e2e-order-ticket:
106125
name: E2E — Order Ticket (FFI layer)
126+
needs: changes
127+
if: needs.changes.outputs.run == 'true'
107128
runs-on: ubuntu-latest
108129
timeout-minutes: 10
109130

@@ -125,6 +146,8 @@ jobs:
125146
# ─── Aspect Tests: Cross-Cutting Concerns ──────────────────────────
126147
aspect-tests:
127148
name: Aspect — Thread Safety + ABI Contract + SPDX
149+
needs: changes
150+
if: needs.changes.outputs.run == 'true'
128151
runs-on: ubuntu-latest
129152
timeout-minutes: 10
130153

@@ -138,6 +161,8 @@ jobs:
138161
# ─── Benchmarks: Performance Regression Detection ──────────────────
139162
benchmarks:
140163
name: Bench — FFI Catalogue + Mount/Unmount + Hash
164+
needs: changes
165+
if: needs.changes.outputs.run == 'true'
141166
runs-on: ubuntu-latest
142167
timeout-minutes: 15
143168

@@ -231,6 +256,8 @@ jobs:
231256
# deltas across pushes are visible inline.
232257
bench-bridge:
233258
name: Bench — mcp-bridge (path-claims)
259+
needs: changes
260+
if: needs.changes.outputs.run == 'true'
234261
runs-on: ubuntu-latest
235262
timeout-minutes: 5
236263
permissions:

.github/workflows/lsp-dap-bsp.yml

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,16 @@
55

66
name: LSP/DAP/BSP CI
77

8+
# Was workflow-level path-filtered: required checks that never ran on PRs
9+
# touching none of its paths, leaving them "Expected" and the PR blocked.
10+
# Now always runs; the `changes` job gates the heavy jobs, which when skipped
11+
# report SUCCESS to required checks.
812
on:
913
push:
10-
paths:
11-
- 'cartridges/lsp-mcp/**'
12-
- 'cartridges/dap-mcp/**'
13-
- 'cartridges/bsp-mcp/**'
14-
- 'src/abi/Boj/Protocol.idr'
15-
- '.github/workflows/lsp-dap-bsp.yml'
14+
branches: [main]
1615
pull_request:
17-
paths:
18-
- 'cartridges/lsp-mcp/**'
19-
- 'cartridges/dap-mcp/**'
20-
- 'cartridges/bsp-mcp/**'
16+
branches: [main]
17+
workflow_dispatch:
2118

2219
permissions: read-all
2320

@@ -26,8 +23,39 @@ concurrency:
2623
cancel-in-progress: true
2724

2825
jobs:
26+
changes:
27+
name: Detect relevant changes
28+
runs-on: ubuntu-latest
29+
timeout-minutes: 5
30+
outputs:
31+
run: ${{ steps.detect.outputs.run }}
32+
steps:
33+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
34+
with:
35+
fetch-depth: 0
36+
- id: detect
37+
env:
38+
EVENT: ${{ github.event_name }}
39+
BASE: ${{ github.base_ref }}
40+
BEFORE: ${{ github.event.before }}
41+
run: |
42+
set -uo pipefail
43+
run=true
44+
if [ "$EVENT" = pull_request ]; then
45+
git fetch --no-tags --depth=200 origin "$BASE" 2>/dev/null \
46+
&& changed=$(git diff --name-only "origin/${BASE}...HEAD" 2>/dev/null) \
47+
&& { printf '%s\n' "$changed" | grep -qE '^cartridges/(lsp|dap|bsp)-mcp/|^src/abi/Boj/Protocol\.idr$' && run=true || run=false; }
48+
elif [ "$EVENT" = push ] && [ -n "$BEFORE" ] && [ "$BEFORE" != 0000000000000000000000000000000000000000 ]; then
49+
changed=$(git diff --name-only "${BEFORE}...${GITHUB_SHA}" 2>/dev/null) \
50+
&& { printf '%s\n' "$changed" | grep -qE '^cartridges/(lsp|dap|bsp)-mcp/|^src/abi/Boj/Protocol\.idr$' && run=true || run=false; }
51+
fi
52+
printf 'run=%s\n' "$run" >> "$GITHUB_OUTPUT"
53+
echo "relevant=$run; changed files:"; printf '%s\n' "${changed:-<none computed>}"
54+
2955
abi-check:
3056
name: ABI Specification Check (Idris2)
57+
needs: changes
58+
if: needs.changes.outputs.run == 'true'
3159
runs-on: ubuntu-latest
3260
timeout-minutes: 10
3361
steps:
@@ -62,6 +90,8 @@ jobs:
6290
6391
ffi-build:
6492
name: FFI Build & Test (Zig)
93+
needs: changes
94+
if: needs.changes.outputs.run == 'true'
6595
runs-on: ubuntu-latest
6696
timeout-minutes: 15
6797
steps:
@@ -107,6 +137,8 @@ jobs:
107137
108138
panel-validation:
109139
name: Panel Manifest Validation
140+
needs: changes
141+
if: needs.changes.outputs.run == 'true'
110142
runs-on: ubuntu-latest
111143
timeout-minutes: 10
112144
steps:
@@ -141,7 +173,8 @@ jobs:
141173
name: Cartridge Completeness Check
142174
runs-on: ubuntu-latest
143175
timeout-minutes: 10
144-
needs: [abi-check, ffi-build, panel-validation]
176+
needs: [changes, abi-check, ffi-build, panel-validation]
177+
if: needs.changes.outputs.run == 'true'
145178
steps:
146179
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
147180
- name: Verify triadic structure

0 commit comments

Comments
 (0)