ci: make path-filtered required gates always report (unblock stuck PR… #285
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # SPDX-License-Identifier: MPL-2.0 | |
| # Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk> | |
| # | |
| # Full test suite for BoJ Server: E2E, aspect tests, and benchmarks. | |
| # Covers all merge requirement categories: P2P (zig-test.yml), E2E (here), | |
| # aspect, execution, lifecycle, and benchmarks. | |
| name: E2E + Aspect + Bench | |
| # Was workflow-level path-filtered: required checks that never ran on PRs | |
| # touching none of its paths, leaving them "Expected" and the PR blocked. | |
| # Now always runs; the `changes` job gates the heavy jobs, which when skipped | |
| # report SUCCESS to required checks. | |
| on: | |
| push: | |
| branches: [main, master, develop] | |
| pull_request: | |
| branches: [main, master] | |
| workflow_dispatch: | |
| permissions: read-all | |
| concurrency: | |
| group: e2e-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| changes: | |
| name: Detect relevant changes | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| outputs: | |
| run: ${{ steps.detect.outputs.run }} | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| fetch-depth: 0 | |
| - id: detect | |
| env: | |
| EVENT: ${{ github.event_name }} | |
| BASE: ${{ github.base_ref }} | |
| BEFORE: ${{ github.event.before }} | |
| run: | | |
| set -uo pipefail | |
| run=true | |
| if [ "$EVENT" = pull_request ]; then | |
| git fetch --no-tags --depth=200 origin "$BASE" 2>/dev/null \ | |
| && changed=$(git diff --name-only "origin/${BASE}...HEAD" 2>/dev/null) \ | |
| && { printf '%s\n' "$changed" | grep -qE '^adapter/|^cartridges/|^ffi/|^mcp-bridge/|^tests/|^src/' && run=true || run=false; } | |
| elif [ "$EVENT" = push ] && [ -n "$BEFORE" ] && [ "$BEFORE" != 0000000000000000000000000000000000000000 ]; then | |
| changed=$(git diff --name-only "${BEFORE}...${GITHUB_SHA}" 2>/dev/null) \ | |
| && { printf '%s\n' "$changed" | grep -qE '^adapter/|^cartridges/|^ffi/|^mcp-bridge/|^tests/|^src/' && run=true || run=false; } | |
| fi | |
| printf 'run=%s\n' "$run" >> "$GITHUB_OUTPUT" | |
| echo "relevant=$run; changed files:"; printf '%s\n' "${changed:-<none computed>}" | |
| # ─── End-to-End: Full REST + MCP Bridge ──────────────────────────── | |
| e2e-full: | |
| name: E2E — Full REST + MCP Bridge | |
| needs: changes | |
| if: needs.changes.outputs.run == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Install Zig | |
| uses: goto-bus-stop/setup-zig@9566bb3e8749893055694249726756f25e099b30 # v2 | |
| with: | |
| version: 0.15.1 | |
| - name: Install Deno | |
| uses: denoland/setup-deno@667a34cdef165d8d2b2e98dde39547c9daac7282 # v2.0.4 | |
| with: | |
| deno-version: v2.x | |
| - name: Install Elixir + OTP | |
| # tests/e2e_full.sh requires `mix` on PATH to start the Elixir | |
| # backend (elixir/ — `mix run --no-halt`). Pinned to match the | |
| # estate convention (see hypatia-scan.yml across the org). | |
| uses: erlef/setup-beam@fc68ffb90438ef2936bbb3251622353b3dcb2f93 # v1.18.2 | |
| with: | |
| elixir-version: '1.18' | |
| otp-version: '27' | |
| - name: Install system dependencies | |
| run: sudo apt-get update && sudo apt-get install -y curl jq | |
| - name: Fetch Elixir deps | |
| working-directory: elixir | |
| run: mix deps.get | |
| - name: Compile Elixir deps | |
| # Pre-compile so the in-test `mix run --no-halt` doesn't have | |
| # to do it on the critical path of the 60s server-start window. | |
| working-directory: elixir | |
| run: mix compile | |
| - name: Build FFI libraries | |
| run: | | |
| # Subshell keeps the cd contained so the cartridge loop below runs | |
| # from the repo root (otherwise its glob never matches). `zig build | |
| # invoke` builds the boj-invoke CLI the Elixir Invoker shells out to | |
| # for tool dispatch — without it every FFI invoke returns {} (cli_missing). | |
| (cd ffi/zig && zig build && zig build invoke) | |
| for cart in cartridges/*/ffi; do | |
| if [ -f "$cart/build.zig" ]; then (cd "$cart" && zig build) || true; fi | |
| done | |
| - name: Run E2E full test suite | |
| run: bash tests/e2e_full.sh | |
| - name: Upload test logs | |
| if: always() | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| with: | |
| name: e2e-full-logs | |
| path: /tmp/boj-e2e-test.* | |
| retention-days: 7 | |
| # ─── End-to-End: Order Ticket (FFI layer) ────────────────────────── | |
| e2e-order-ticket: | |
| name: E2E — Order Ticket (FFI layer) | |
| needs: changes | |
| if: needs.changes.outputs.run == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Install Zig | |
| uses: goto-bus-stop/setup-zig@9566bb3e8749893055694249726756f25e099b30 # v2 | |
| with: | |
| version: 0.15.1 | |
| - name: Build FFI libraries | |
| run: cd ffi/zig && zig build | |
| - name: Run order ticket E2E | |
| run: bash tests/order_ticket_e2e.sh | |
| # ─── Aspect Tests: Cross-Cutting Concerns ────────────────────────── | |
| aspect-tests: | |
| name: Aspect — Thread Safety + ABI Contract + SPDX | |
| needs: changes | |
| if: needs.changes.outputs.run == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Run aspect tests | |
| run: bash tests/aspect_tests.sh | |
| # ─── Benchmarks: Performance Regression Detection ────────────────── | |
| benchmarks: | |
| name: Bench — FFI Catalogue + Mount/Unmount + Hash | |
| needs: changes | |
| if: needs.changes.outputs.run == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Install Zig | |
| uses: goto-bus-stop/setup-zig@9566bb3e8749893055694249726756f25e099b30 # v2 | |
| with: | |
| version: 0.15.1 | |
| - name: Build FFI libraries | |
| run: cd ffi/zig && zig build | |
| - name: Run benchmarks | |
| run: cd ffi/zig && zig build bench | |
| - name: Upload benchmark results | |
| if: always() | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| with: | |
| name: benchmark-results | |
| path: ffi/zig/zig-out/bench* | |
| retention-days: 30 | |
| # ─── Bridge Tests: unit suite + boot smoke × {Node, Deno, Bun} ───── | |
| # The README blesses three runtimes for the zero-dependency bridge: | |
| # Deno (documented runtime), Node (the npx install path every client | |
| # quickstart uses), and Bun (zero-install). Only Deno is exercised by | |
| # development, so a runtime leak in the other two ships silently — a | |
| # bare `Deno.env` reference once broke the entire npx path at import | |
| # time (fixed in PR #211). This job runs the full unit suite AND an | |
| # MCP initialize → tools/list boot smoke of main.js under each | |
| # runtime. The smoke matters separately: the unit suite imports lib/ | |
| # but never boots main.js, whose stdio/exit paths are runtime-gated. | |
| bridge-tests: | |
| name: Bridge — ${{ matrix.runtime }} (unit + boot smoke) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - runtime: node | |
| unit: node --test mcp-bridge/tests/dispatch_test.js mcp-bridge/tests/http_transport_test.js mcp-bridge/tests/path_claims_test.js | |
| boot: node mcp-bridge/main.js | |
| - runtime: deno | |
| unit: deno test --allow-read --allow-env --allow-run --allow-net mcp-bridge/tests/dispatch_test.js mcp-bridge/tests/http_transport_test.js mcp-bridge/tests/path_claims_test.js | |
| # Scoped perms matching main.js's shebang, NOT -A: the boot smoke | |
| # must exercise the exact permission set a real install uses, or it | |
| # would mask a missing-grant bug that scoped users would hit. | |
| boot: deno run --allow-net --allow-env --allow-read mcp-bridge/main.js | |
| - runtime: bun | |
| unit: bun test mcp-bridge/tests/dispatch_test.js mcp-bridge/tests/http_transport_test.js mcp-bridge/tests/path_claims_test.js | |
| boot: bun mcp-bridge/main.js | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Setup Node | |
| # Node is needed on every leg: it is the subject on the node | |
| # leg and the boot-smoke orchestrator on all three. | |
| uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 | |
| with: | |
| node-version: '22' | |
| - name: Install Deno | |
| if: matrix.runtime == 'deno' | |
| uses: denoland/setup-deno@667a34cdef165d8d2b2e98dde39547c9daac7282 # v2.0.4 | |
| with: | |
| deno-version: v2.x | |
| - name: Install Bun | |
| if: matrix.runtime == 'bun' | |
| uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 | |
| with: | |
| bun-version: '1.x' | |
| - name: Unit suite (${{ matrix.runtime }}) | |
| run: ${{ matrix.unit }} | |
| - name: Boot smoke (${{ matrix.runtime }}) | |
| run: node mcp-bridge/tests/boot_smoke.js ${{ matrix.boot }} | |
| # ─── Bench Bridge: mcp-bridge JS perf (path-claims) ──────────────── | |
| # Separate job from `benchmarks` above because the bridge has no Zig | |
| # toolchain dependency — keeps logs untangled and lets the JS bench | |
| # run in parallel on its own runner. Posts a sticky PR comment so | |
| # deltas across pushes are visible inline. | |
| bench-bridge: | |
| name: Bench — mcp-bridge (path-claims) | |
| needs: changes | |
| if: needs.changes.outputs.run == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| permissions: | |
| contents: read | |
| # Per-job override: only this step needs to write PR comments. | |
| # Workflow-level permissions stay read-all. | |
| pull-requests: write | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Setup Node | |
| uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 | |
| with: | |
| node-version: '22' | |
| - name: Run bridge bench | |
| run: node mcp-bridge/tests/path_claims_bench.js | tee bench-bridge.txt | |
| - name: Upload bridge bench artifact | |
| if: always() | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| with: | |
| name: bench-bridge-results | |
| path: bench-bridge.txt | |
| retention-days: 30 | |
| - name: Sticky PR comment with bench numbers | |
| if: github.event_name == 'pull_request' | |
| # Advisory — a comment failure must never gate the bench job. | |
| # Same reasoning as the hypatia-scan PR-comment step. | |
| continue-on-error: true | |
| uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v7 | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| const MARKER = '<!-- bench-bridge:path-claims -->'; | |
| const output = fs.readFileSync('bench-bridge.txt', 'utf8'); | |
| const body = `${MARKER}\n## 🏁 path-claims bench\n\nCommit \`${context.sha.slice(0, 7)}\`\n\n<details open><summary>Numbers</summary>\n\n\`\`\`\n${output}\n\`\`\`\n</details>\n\n*Host-dependent — compare deltas across commits, not absolute values.*`; | |
| const { owner, repo } = context.repo; | |
| const issue_number = context.issue.number; | |
| const existing = await github.paginate( | |
| github.rest.issues.listComments, | |
| { owner, repo, issue_number, per_page: 100 }, | |
| ); | |
| const prior = existing.find((c) => c.body && c.body.includes(MARKER)); | |
| if (prior) { | |
| await github.rest.issues.updateComment({ | |
| owner, repo, comment_id: prior.id, body, | |
| }); | |
| } else { | |
| await github.rest.issues.createComment({ | |
| owner, repo, issue_number, body, | |
| }); | |
| } |