Skip to content

Commit d3da963

Browse files
ci: restrict abi-drift + zig-test cartridge loops to PR-changed cartridges (#147)
## Summary Both `abi-drift` (verify) and `zig-test` (Zig FFI Tests) currently sweep every cartridge under `cartridges/*` and fail the PR check if any one of them fails. With ~66 cartridges in the abi-drift allowlist and ~73 in zig-test, a PR touching a single cartridge gets gated by unrelated breakage in others — e.g. browser-mcp and orchestrator-lsp-mcp Zig failures, container-mcp/git-mcp/queues-mcp/vordr-mcp ABI drift — that has been sitting on `main` for weeks. This PR constrains both per-cartridge loops on **pull_request events** to the intersection of: 1. the existing allowlist / discovery, and 2. cartridges with any file changed under `cartridges/<name>/**` in this PR's diff against `origin/<base_ref>`. **On `push: main` the full sweep is preserved**, so cross-cutting drift is still caught at trunk. This is purely a PR-time false-positive fix, not a relaxation of the gate. ## Why now Surfaced by the investigation in PR #146: that PR touches only `local-coord-mcp`, but was blocked by failures in 6+ cartridges it didn't change. The other gating workflows that scan all cartridges have the same pathology; this PR fixes the two that have clean per-cartridge loops. ## Changes - `.github/workflows/abi-drift.yml` — new "Restrict to cartridges changed in this PR" step; the verify loop reads the filtered set on PR, full allowlist on push. `fetch-depth: 0` added so the diff can resolve. - `.github/workflows/zig-test.yml` — new "Determine cartridges to test" step; both the FFI tests loop and the shared-library build loop read the in-scope set. Catalogue/readiness steps still run on every invocation — they test cross-cartridge plumbing. Empty changed-set on a PR (which the `paths:` filter shouldn't allow, but defensive) emits a `::notice::` and exits 0 instead of going red on nothing. ## Out of scope - `tests/aspect_tests.sh` (the "Aspect — Thread Safety + ABI Contract + SPDX" check) asserts **global** invariants across the whole tree, so a "changed-files" filter doesn't fit it cleanly. Best handled with a baseline-aware ratchet — separate PR. ## Test plan - [x] `python3 -c "import yaml; yaml.safe_load(...)"` both YAML files (clean) - [ ] This PR itself triggers `zig-test` (touches `.github/workflows/zig-test.yml` but no `cartridges/**`) — expect the "Determine cartridges to test" step to print an empty scope and the per-cartridge steps to no-op via the `::notice::` path - [ ] PR #146 (touches `cartridges/local-coord-mcp/**`) should, when rebased onto this once merged, run abi-drift and zig-test against only `local-coord-mcp` and skip the unrelated failing cartridges 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- _Generated by [Claude Code](https://claude.ai/code/session_018MBrAtPrwfgn2WG4BAerZW)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 2dff04d commit d3da963

2 files changed

Lines changed: 115 additions & 7 deletions

File tree

.github/workflows/abi-drift.yml

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ jobs:
4343
runs-on: ubuntu-latest
4444
steps:
4545
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
46+
with:
47+
# Need at least two commits so `git diff origin/<base>...HEAD`
48+
# can compute the changed-cartridge set on pull_request events.
49+
# `0` = full history (cheap on this repo).
50+
fetch-depth: 0
4651

4752
- name: Install Rust toolchain (stable)
4853
uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b # stable
@@ -165,11 +170,59 @@ jobs:
165170
echo 'EOF'
166171
} >> "$GITHUB_OUTPUT"
167172
168-
- name: Emit + verify each cartridge
173+
# On pull_request, restrict the verify loop to cartridges that
174+
# actually changed in this PR — unrelated cartridge drift is the
175+
# main branch's problem, not this PR's. On push to main, scan the
176+
# full allowlist so cross-cutting regressions are still caught at
177+
# trunk. Empty changed-set on a PR ⇒ skip the loop (handled in
178+
# the verify step's defensive empty check below).
179+
#
180+
# Single output, always-correct: scope.outputs.carts is the
181+
# PR-filtered allowlist on pull_request, the full allowlist on
182+
# push. Avoids the `${{ X && Y || Z }}` ternary footgun where Y
183+
# being an empty string short-circuits to Z (which would silently
184+
# re-expand to the full sweep — exactly the bug we're trying to
185+
# fix).
186+
- name: Scope cartridges to verify
187+
id: scope
169188
env:
189+
EVENT: ${{ github.event_name }}
190+
BASE_REF: ${{ github.base_ref }}
170191
CARTS: ${{ steps.discover.outputs.carts }}
171192
run: |
172193
set -euo pipefail
194+
if [ "$EVENT" = "pull_request" ]; then
195+
git fetch --no-tags --depth=50 origin "$BASE_REF" || true
196+
changed=$(git diff --name-only "origin/${BASE_REF}...HEAD" -- 'cartridges/**' \
197+
| awk -F/ '{print $2}' | sort -u)
198+
scope=""
199+
while IFS= read -r cart; do
200+
[ -z "$cart" ] && continue
201+
if printf '%s\n' "$changed" | grep -qx "$cart"; then
202+
scope="${scope}${cart}"$'\n'
203+
fi
204+
done <<< "$CARTS"
205+
else
206+
scope="$CARTS"
207+
fi
208+
{
209+
echo 'carts<<EOF'
210+
printf '%s' "$scope"
211+
echo
212+
echo 'EOF'
213+
} >> "$GITHUB_OUTPUT"
214+
echo "Cartridges in scope for this run:"
215+
printf ' • %s\n' $(printf '%s\n' "$scope" | grep -v '^$' || true)
216+
217+
- name: Emit + verify each cartridge
218+
env:
219+
CARTS: ${{ steps.scope.outputs.carts }}
220+
run: |
221+
set -euo pipefail
222+
if [ -z "$(printf '%s' "$CARTS" | tr -d '[:space:]')" ]; then
223+
echo "::notice::No allowlisted cartridges changed in this PR — skipping drift verify."
224+
exit 0
225+
fi
173226
failed=""
174227
while IFS= read -r cart; do
175228
[ -z "$cart" ] && continue

.github/workflows/zig-test.yml

Lines changed: 61 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@ on:
1111
paths:
1212
- 'ffi/**'
1313
- 'cartridges/**/ffi/**'
14+
- '.github/workflows/zig-test.yml'
1415
pull_request:
1516
branches: [main]
1617
paths:
1718
- 'ffi/**'
1819
- 'cartridges/**/ffi/**'
20+
- '.github/workflows/zig-test.yml'
1921

2022
permissions:
2123
contents: read
@@ -26,22 +28,68 @@ jobs:
2628
runs-on: ubuntu-latest
2729
steps:
2830
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
31+
with:
32+
# Needed so `git diff origin/<base>...HEAD` can identify the
33+
# cartridges actually changed in this PR. Full history is cheap
34+
# on this repo; cuts down on cross-cartridge false-positive
35+
# gating where a PR is blocked by pre-existing breakage in
36+
# unrelated cartridges (browser-mcp, orchestrator-lsp-mcp,
37+
# etc.). On push/main we keep the full per-cartridge sweep.
38+
fetch-depth: 0
2939

3040
- name: Install Zig
3141
uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2
3242
with:
3343
version: 0.15.2
3444

45+
- name: Determine cartridges to test
46+
id: scope
47+
env:
48+
EVENT: ${{ github.event_name }}
49+
BASE_REF: ${{ github.base_ref }}
50+
run: |
51+
set -euo pipefail
52+
all="$(ls -d cartridges/*/ffi/ 2>/dev/null | sed 's|cartridges/||;s|/ffi/||' | sort)"
53+
if [ "$EVENT" = "pull_request" ]; then
54+
git fetch --no-tags --depth=50 origin "$BASE_REF" || true
55+
changed="$(git diff --name-only "origin/${BASE_REF}...HEAD" -- 'cartridges/**' \
56+
| awk -F/ '{print $2}' | sort -u)"
57+
scope=""
58+
while IFS= read -r cart; do
59+
[ -z "$cart" ] && continue
60+
if printf '%s\n' "$changed" | grep -qx "$cart"; then
61+
scope="$scope$cart"$'\n'
62+
fi
63+
done <<< "$all"
64+
else
65+
scope="$all"
66+
fi
67+
{
68+
echo 'carts<<EOF'
69+
printf '%s' "$scope"
70+
echo
71+
echo 'EOF'
72+
} >> "$GITHUB_OUTPUT"
73+
echo "Cartridges in scope for this run:"
74+
printf ' • %s\n' $(printf '%s\n' "$scope" | grep -v '^$' || true)
75+
3576
- name: Run catalogue tests
3677
run: cd ffi/zig && zig build test --summary all
3778

3879
- name: Run readiness tests
3980
run: cd ffi/zig && zig build readiness --summary all
4081

41-
- name: Run cartridge FFI tests (all 73 cartridges)
82+
- name: Run cartridge FFI tests
83+
env:
84+
CARTS: ${{ steps.scope.outputs.carts }}
4285
run: |
86+
if [ -z "$(printf '%s' "$CARTS" | tr -d '[:space:]')" ]; then
87+
echo "::notice::No cartridges changed in this PR — skipping per-cartridge FFI tests."
88+
exit 0
89+
fi
4390
failed=""
44-
for cart in $(ls -d cartridges/*/ffi/ 2>/dev/null | sed 's|cartridges/||;s|/ffi/||' | sort); do
91+
while IFS= read -r cart; do
92+
[ -z "$cart" ] && continue
4593
echo "::group::Testing $cart..."
4694
if cd "cartridges/$cart/ffi" && zig build test --summary all 2>&1; then
4795
echo " ✓ $cart passed"
@@ -51,19 +99,26 @@ jobs:
5199
fi
52100
cd "$GITHUB_WORKSPACE"
53101
echo "::endgroup::"
54-
done
102+
done <<< "$CARTS"
55103
if [ -n "$failed" ]; then
56104
echo "::error::Failed cartridges:$failed"
57105
exit 1
58106
fi
59107
60-
- name: Build cartridge shared libraries (all 73 cartridges)
108+
- name: Build cartridge shared libraries
109+
env:
110+
CARTS: ${{ steps.scope.outputs.carts }}
61111
run: |
62-
for cart in $(ls -d cartridges/*/ffi/ 2>/dev/null | sed 's|cartridges/||;s|/ffi/||' | sort); do
112+
if [ -z "$(printf '%s' "$CARTS" | tr -d '[:space:]')" ]; then
113+
echo "::notice::No cartridges changed in this PR — skipping per-cartridge .so build."
114+
exit 0
115+
fi
116+
while IFS= read -r cart; do
117+
[ -z "$cart" ] && continue
63118
echo "Building $cart .so..."
64119
cd "cartridges/$cart/ffi" && zig build
65120
cd "$GITHUB_WORKSPACE"
66-
done
121+
done <<< "$CARTS"
67122
68123
- name: Build static library
69124
run: cd ffi/zig && zig build lib

0 commit comments

Comments
 (0)