diff --git a/.github/workflows/abi-drift.yml b/.github/workflows/abi-drift.yml index db826d4f..3cf24685 100644 --- a/.github/workflows/abi-drift.yml +++ b/.github/workflows/abi-drift.yml @@ -43,6 +43,11 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + # Need at least two commits so `git diff origin/...HEAD` + # can compute the changed-cartridge set on pull_request events. + # `0` = full history (cheap on this repo). + fetch-depth: 0 - name: Install Rust toolchain (stable) uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b # stable @@ -165,11 +170,59 @@ jobs: echo 'EOF' } >> "$GITHUB_OUTPUT" - - name: Emit + verify each cartridge + # On pull_request, restrict the verify loop to cartridges that + # actually changed in this PR — unrelated cartridge drift is the + # main branch's problem, not this PR's. On push to main, scan the + # full allowlist so cross-cutting regressions are still caught at + # trunk. Empty changed-set on a PR ⇒ skip the loop (handled in + # the verify step's defensive empty check below). + # + # Single output, always-correct: scope.outputs.carts is the + # PR-filtered allowlist on pull_request, the full allowlist on + # push. Avoids the `${{ X && Y || Z }}` ternary footgun where Y + # being an empty string short-circuits to Z (which would silently + # re-expand to the full sweep — exactly the bug we're trying to + # fix). + - name: Scope cartridges to verify + id: scope env: + EVENT: ${{ github.event_name }} + BASE_REF: ${{ github.base_ref }} CARTS: ${{ steps.discover.outputs.carts }} run: | set -euo pipefail + if [ "$EVENT" = "pull_request" ]; then + git fetch --no-tags --depth=50 origin "$BASE_REF" || true + changed=$(git diff --name-only "origin/${BASE_REF}...HEAD" -- 'cartridges/**' \ + | awk -F/ '{print $2}' | sort -u) + scope="" + while IFS= read -r cart; do + [ -z "$cart" ] && continue + if printf '%s\n' "$changed" | grep -qx "$cart"; then + scope="${scope}${cart}"$'\n' + fi + done <<< "$CARTS" + else + scope="$CARTS" + fi + { + echo 'carts<> "$GITHUB_OUTPUT" + echo "Cartridges in scope for this run:" + printf ' • %s\n' $(printf '%s\n' "$scope" | grep -v '^$' || true) + + - name: Emit + verify each cartridge + env: + CARTS: ${{ steps.scope.outputs.carts }} + run: | + set -euo pipefail + if [ -z "$(printf '%s' "$CARTS" | tr -d '[:space:]')" ]; then + echo "::notice::No allowlisted cartridges changed in this PR — skipping drift verify." + exit 0 + fi failed="" while IFS= read -r cart; do [ -z "$cart" ] && continue diff --git a/.github/workflows/zig-test.yml b/.github/workflows/zig-test.yml index 8cbbf822..3809517e 100644 --- a/.github/workflows/zig-test.yml +++ b/.github/workflows/zig-test.yml @@ -11,11 +11,13 @@ on: paths: - 'ffi/**' - 'cartridges/**/ffi/**' + - '.github/workflows/zig-test.yml' pull_request: branches: [main] paths: - 'ffi/**' - 'cartridges/**/ffi/**' + - '.github/workflows/zig-test.yml' permissions: contents: read @@ -26,22 +28,68 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + # Needed so `git diff origin/...HEAD` can identify the + # cartridges actually changed in this PR. Full history is cheap + # on this repo; cuts down on cross-cartridge false-positive + # gating where a PR is blocked by pre-existing breakage in + # unrelated cartridges (browser-mcp, orchestrator-lsp-mcp, + # etc.). On push/main we keep the full per-cartridge sweep. + fetch-depth: 0 - name: Install Zig uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2 with: version: 0.15.2 + - name: Determine cartridges to test + id: scope + env: + EVENT: ${{ github.event_name }} + BASE_REF: ${{ github.base_ref }} + run: | + set -euo pipefail + all="$(ls -d cartridges/*/ffi/ 2>/dev/null | sed 's|cartridges/||;s|/ffi/||' | sort)" + if [ "$EVENT" = "pull_request" ]; then + git fetch --no-tags --depth=50 origin "$BASE_REF" || true + changed="$(git diff --name-only "origin/${BASE_REF}...HEAD" -- 'cartridges/**' \ + | awk -F/ '{print $2}' | sort -u)" + scope="" + while IFS= read -r cart; do + [ -z "$cart" ] && continue + if printf '%s\n' "$changed" | grep -qx "$cart"; then + scope="$scope$cart"$'\n' + fi + done <<< "$all" + else + scope="$all" + fi + { + echo 'carts<> "$GITHUB_OUTPUT" + echo "Cartridges in scope for this run:" + printf ' • %s\n' $(printf '%s\n' "$scope" | grep -v '^$' || true) + - name: Run catalogue tests run: cd ffi/zig && zig build test --summary all - name: Run readiness tests run: cd ffi/zig && zig build readiness --summary all - - name: Run cartridge FFI tests (all 73 cartridges) + - name: Run cartridge FFI tests + env: + CARTS: ${{ steps.scope.outputs.carts }} run: | + if [ -z "$(printf '%s' "$CARTS" | tr -d '[:space:]')" ]; then + echo "::notice::No cartridges changed in this PR — skipping per-cartridge FFI tests." + exit 0 + fi failed="" - for cart in $(ls -d cartridges/*/ffi/ 2>/dev/null | sed 's|cartridges/||;s|/ffi/||' | sort); do + while IFS= read -r cart; do + [ -z "$cart" ] && continue echo "::group::Testing $cart..." if cd "cartridges/$cart/ffi" && zig build test --summary all 2>&1; then echo " ✓ $cart passed" @@ -51,19 +99,26 @@ jobs: fi cd "$GITHUB_WORKSPACE" echo "::endgroup::" - done + done <<< "$CARTS" if [ -n "$failed" ]; then echo "::error::Failed cartridges:$failed" exit 1 fi - - name: Build cartridge shared libraries (all 73 cartridges) + - name: Build cartridge shared libraries + env: + CARTS: ${{ steps.scope.outputs.carts }} run: | - for cart in $(ls -d cartridges/*/ffi/ 2>/dev/null | sed 's|cartridges/||;s|/ffi/||' | sort); do + if [ -z "$(printf '%s' "$CARTS" | tr -d '[:space:]')" ]; then + echo "::notice::No cartridges changed in this PR — skipping per-cartridge .so build." + exit 0 + fi + while IFS= read -r cart; do + [ -z "$cart" ] && continue echo "Building $cart .so..." cd "cartridges/$cart/ffi" && zig build cd "$GITHUB_WORKSPACE" - done + done <<< "$CARTS" - name: Build static library run: cd ffi/zig && zig build lib