ci: list every composite action in the paths filters that use it #62
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
| # CI for runner/ (mirroir-run Rust binary). Four per-push lanes: | |
| # - runner-fast : fmt + clippy + test + diff-text smoke on linux + macos. | |
| # - runner-smoke : process + http + web + cross_surface end-to-end (no Ollama). | |
| # - runner-full-loop : the thirteen-phase acceptance test for the whole loop. | |
| # - runner-e2e : full pipeline — Playwright (chromium) + real Ollama judge + mega-sample. | |
| # Plus runner-e2e-allbrowsers on a Sunday cron, and the deny / publish audits. | |
| name: runner | |
| on: | |
| push: | |
| paths: | |
| - 'runner/**' | |
| - '.github/workflows/runner.yml' | |
| - '.github/actions/setup-playwright/**' | |
| # Every branch, not just main: this repo squash-merges locally and opens no | |
| # pull requests, so a feature branch has no other way to reach these lanes | |
| # before it lands. The paths filter above keeps unrelated pushes out, and the | |
| # concurrency group below cancels superseded runs on the same branch. | |
| pull_request: | |
| paths: | |
| - 'runner/**' | |
| - '.github/workflows/runner.yml' | |
| - '.github/actions/setup-playwright/**' | |
| # Sunday 03:00 UTC — gates the all-browsers lane. | |
| schedule: | |
| - cron: '0 3 * * 0' | |
| workflow_dispatch: | |
| # Cancel superseded runs on the same branch / PR to save runner-minutes. | |
| concurrency: | |
| group: runner-${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| env: | |
| # Pinned to match runner/rust-toolchain.toml (channel = "1.96.0") so the | |
| # rustup-installed toolchain name lines up with what the toml selects — | |
| # clippy/rustfmt components land on the exact toolchain cargo uses, and CI | |
| # stops floating on `stable` (a float once broke clippy via collapsible_if). | |
| # Bump this and runner/rust-toolchain.toml together; keep Cargo.toml's | |
| # rust-version (MSRV floor) in step. | |
| RUST_TOOLCHAIN: 1.96.0 | |
| # Suppress Cargo's interactive progress; CI logs are nicer without it. | |
| CARGO_TERM_COLOR: always | |
| jobs: | |
| runner-fast: | |
| name: fmt + clippy + test (${{ matrix.os }}) | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, macos-latest] | |
| permissions: | |
| contents: read | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Install Rust toolchain (${{ env.RUST_TOOLCHAIN }}) | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| toolchain: ${{ env.RUST_TOOLCHAIN }} | |
| components: rustfmt, clippy | |
| - name: Cache cargo registry + git + runner target | |
| uses: Swatinem/rust-cache@v2 | |
| with: | |
| workspaces: runner -> target | |
| shared-key: runner-fast-${{ matrix.os }} | |
| - name: cargo fmt --check | |
| working-directory: runner | |
| run: cargo fmt --all -- --check | |
| - name: cargo clippy (deny pedantic+nursery) | |
| working-directory: runner | |
| run: cargo clippy --all-targets --all-features -- -D warnings | |
| - name: cargo test (all targets) | |
| working-directory: runner | |
| run: cargo test --all-targets --all-features --no-fail-fast | |
| - name: cargo build --release (smoke) | |
| working-directory: runner | |
| run: cargo build --release --bin mirroir-run | |
| - name: --validate sample scenarios (sanity) | |
| working-directory: runner | |
| run: | | |
| for f in samples/mega-sample/scenarios/*.yaml; do | |
| echo "::group::validate $f" | |
| ./target/release/mirroir-run --validate "$f" | |
| echo "::endgroup::" | |
| done | |
| - name: --diff-text MATCH path | |
| working-directory: runner | |
| run: | | |
| echo "Hello world from mirroir" > /tmp/diff-a.txt | |
| echo "Hello world from mirroir" > /tmp/diff-b.txt | |
| ./target/release/mirroir-run --diff-text /tmp/diff-a.txt /tmp/diff-b.txt | |
| # DRIFT has its own exit code (65) so a lane can tell "the semantics | |
| # moved" from "something broke" (1). Pinning the code here keeps that | |
| # distinction from regressing into a plain failure. | |
| - name: --diff-text DRIFT path (expected exit 65) | |
| working-directory: runner | |
| run: | | |
| echo "Completely unrelated text from a parallel universe" > /tmp/diff-c.txt | |
| set +e | |
| ./target/release/mirroir-run --diff-text /tmp/diff-a.txt /tmp/diff-c.txt | |
| status=$? | |
| set -e | |
| if [ "$status" != "65" ]; then | |
| echo "Expected exit 65 (DRIFT) from the drift path, got $status" | |
| exit 1 | |
| fi | |
| echo "DRIFT path correctly exited 65" | |
| runner-smoke: | |
| name: process + http + web + cross_surface smoke (${{ matrix.os }}) | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, macos-latest] | |
| needs: runner-fast | |
| permissions: | |
| contents: read | |
| timeout-minutes: 15 | |
| env: | |
| MIRROIR_PLAYWRIGHT_HOME: /tmp/mirroir-pw | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Install Rust toolchain | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| toolchain: ${{ env.RUST_TOOLCHAIN }} | |
| - name: Cache cargo registry + git + runner target | |
| uses: Swatinem/rust-cache@v2 | |
| with: | |
| workspaces: runner -> target | |
| shared-key: runner-smoke-${{ matrix.os }} | |
| - name: Setup Playwright + chromium | |
| uses: ./.github/actions/setup-playwright | |
| - name: Build release binary | |
| working-directory: runner | |
| run: cargo build --release --bin mirroir-run | |
| - name: Verify python3 available (used by smoke http server) | |
| run: python3 --version | |
| - name: --run-scenario with process + http (spawn python http.server) | |
| working-directory: runner | |
| run: | | |
| cat > /tmp/smoke-process-http.yaml <<'EOF' | |
| version: 1 | |
| name: ci smoke — process + http | |
| description: spawn python http.server, probe it, kill it | |
| steps: | |
| - spawn: { id: server, command: "python3 -m http.server 18950" } | |
| - wait_port: { port: 18950, timeout_s: 15 } | |
| - http: | |
| method: GET | |
| url: "http://127.0.0.1:18950/" | |
| expect_status: 200 | |
| expect_body_contains: ["Directory listing"] | |
| - kill: { id: server, grace_s: 2 } | |
| EOF | |
| ./target/release/mirroir-run --run-scenario /tmp/smoke-process-http.yaml | |
| - name: --run-scenario with cross_surface (pairwise equivalence) | |
| working-directory: runner | |
| run: | | |
| mkdir -p /tmp/cs | |
| printf 'Atmosphere supports WebSocket, SSE, and long-polling transports.\n' > /tmp/cs/web.txt | |
| printf 'Atmosphere transports include WebSocket SSE and long-polling support.\n' > /tmp/cs/ios.txt | |
| cat > /tmp/cs-pass.yaml <<'EOF' | |
| version: 1 | |
| name: ci smoke — cross_surface pass | |
| steps: | |
| - cross_surface: | |
| response_files: | |
| - "/tmp/cs/web.txt" | |
| - "/tmp/cs/ios.txt" | |
| min_similarity: 0.5 | |
| EOF | |
| ./target/release/mirroir-run --run-scenario /tmp/cs-pass.yaml | |
| - name: --run-scenario with cross_surface mismatch (expected exit 1) | |
| working-directory: runner | |
| run: | | |
| printf 'Completely unrelated text about quantum physics and gravity.\n' > /tmp/cs/bad.txt | |
| cat > /tmp/cs-fail.yaml <<'EOF' | |
| version: 1 | |
| name: ci smoke — cross_surface fail | |
| steps: | |
| - cross_surface: | |
| response_files: | |
| - "/tmp/cs/web.txt" | |
| - "/tmp/cs/bad.txt" | |
| min_similarity: 0.5 | |
| EOF | |
| set +e | |
| ./target/release/mirroir-run --run-scenario /tmp/cs-fail.yaml | |
| status=$? | |
| set -e | |
| if [ "$status" != "1" ]; then | |
| echo "Expected exit 1 from cross_surface mismatch, got $status" | |
| exit 1 | |
| fi | |
| echo "cross_surface mismatch path correctly exited 1" | |
| - name: --sample with a real web scenario (web-fixture through Playwright) | |
| working-directory: runner | |
| run: | | |
| # samples/web-fixture boots a python http.server over static pages and | |
| # signs in through Playwright. There is no way to ask the runner to | |
| # skip the web block, so a lane without Node fails here instead of | |
| # reporting a green run over assertions that never executed. | |
| ./target/release/mirroir-run --sample samples/web-fixture | |
| - name: --emit playwright writes the workspace a run executes | |
| working-directory: runner | |
| run: | | |
| ./target/release/mirroir-run --emit playwright samples/web-fixture --scenarios all | |
| for f in target/playwright/web-fixture/login/login.spec.ts \ | |
| target/playwright/web-fixture/login/playwright.config.ts \ | |
| target/playwright/web-fixture/order-summary/order-summary.spec.ts; do | |
| if [ ! -f "$f" ]; then | |
| echo "--emit did not write $f" | |
| exit 1 | |
| fi | |
| done | |
| grep -q "AUTO-GENERATED by mirroir-run" target/playwright/web-fixture/login/login.spec.ts | |
| grep -q "retain-on-failure" target/playwright/web-fixture/login/playwright.config.ts | |
| echo "--emit playwright wrote both scenario workspaces" | |
| - name: negative web scenarios (expected exit 1) | |
| working-directory: runner | |
| run: | | |
| # console-error.html: every locator resolves and both assertions hold, | |
| # and the page throws — the compiled spec's pageerror collector is the | |
| # only thing that catches it. wrong-selector: a locator naming nothing | |
| # must fail by name AND leave its trace on disk. | |
| set +e | |
| ./target/release/mirroir-run --run-scenario samples/web-fixture/scenarios/console-error.yaml | |
| console_status=$? | |
| ./target/release/mirroir-run --run-scenario samples/web-fixture/scenarios/wrong-selector.yaml | |
| selector_status=$? | |
| set -e | |
| if [ "$console_status" != "1" ]; then | |
| echo "Expected exit 1 from the throwing page, got $console_status" | |
| exit 1 | |
| fi | |
| if [ "$selector_status" != "1" ]; then | |
| echo "Expected exit 1 from the wrong selector, got $selector_status" | |
| exit 1 | |
| fi | |
| trace=$(find target/playwright/wrong-selector/test-results -name trace.zip | head -1) | |
| if [ -z "$trace" ]; then | |
| echo "The failing run left no trace.zip on disk" | |
| find target/playwright/wrong-selector -type f | |
| exit 1 | |
| fi | |
| echo "Both negative paths exited 1; trace kept at $trace" | |
| # The acceptance test for the whole runner loop, in one run and in order: | |
| # boot, one browser invocation, the capture channel, the oracle, the | |
| # runner-side hooks, PASS, an idempotent rerun, DRIFT, accept, green again, | |
| # a real break, the artifacts it leaves, and the lockfile gate. The suite | |
| # lives in runner/tests/e2e_full_loop.rs and needs a browser, which is why it | |
| # runs here rather than in runner-fast. | |
| runner-full-loop: | |
| name: e2e — the whole loop (${{ matrix.os }}) | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, macos-latest] | |
| needs: runner-fast | |
| permissions: | |
| contents: read | |
| timeout-minutes: 25 | |
| env: | |
| MIRROIR_PLAYWRIGHT_HOME: /tmp/mirroir-pw | |
| # This is the lane that owes the loop. If its browser setup ever breaks, | |
| # the test must go red rather than announce a skip and pass. | |
| MIRROIR_E2E_REQUIRED: "1" | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Install Rust toolchain | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| toolchain: ${{ env.RUST_TOOLCHAIN }} | |
| - name: Cache cargo registry + git + runner target | |
| uses: Swatinem/rust-cache@v2 | |
| with: | |
| workspaces: runner -> target | |
| shared-key: runner-full-loop-${{ matrix.os }} | |
| - name: Setup Playwright + chromium | |
| uses: ./.github/actions/setup-playwright | |
| - name: Verify python3 available (the fixture's static server) | |
| run: python3 --version | |
| - name: The thirteen-phase loop | |
| working-directory: runner | |
| run: | | |
| set -euo pipefail | |
| # On a host that provisioned no browser the suite reports "NOT RUN" | |
| # and stays green — that is what keeps runner-fast honest without an | |
| # #[ignore]. This lane provisioned one, so a run that did not observe | |
| # all thirteen phases is a failure here, not a skip. | |
| cargo test --test e2e_full_loop -- --nocapture 2>&1 | tee /tmp/full-loop.log | |
| if ! grep -q 'FULL LOOP: 13/13 phases observed' /tmp/full-loop.log; then | |
| echo "::error::the full-loop suite did not observe all thirteen phases" | |
| exit 1 | |
| fi | |
| runner-e2e: | |
| name: e2e — Playwright + Ollama + mega-sample (${{ matrix.os }}) | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, macos-latest] | |
| needs: runner-smoke | |
| permissions: | |
| contents: read | |
| timeout-minutes: 30 | |
| env: | |
| MIRROIR_PLAYWRIGHT_HOME: /tmp/mirroir-pw | |
| OLLAMA_JUDGE_MODEL: qwen2.5:0.5b | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Install Rust toolchain | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| toolchain: ${{ env.RUST_TOOLCHAIN }} | |
| - name: Cache cargo registry + git + runner target | |
| uses: Swatinem/rust-cache@v2 | |
| with: | |
| workspaces: runner -> target | |
| shared-key: runner-e2e-${{ matrix.os }} | |
| - name: Setup Playwright + chromium | |
| uses: ./.github/actions/setup-playwright | |
| - name: Cache Ollama models | |
| id: ollama-cache | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~/.ollama/models | |
| # Bump the suffix when the model digest changes. | |
| key: ollama-${{ runner.os }}-${{ env.OLLAMA_JUDGE_MODEL }}-v1 | |
| - name: Install Ollama (Linux) | |
| if: runner.os == 'Linux' | |
| run: | | |
| curl -fsSL https://ollama.com/install.sh | sh | |
| - name: Install Ollama (macOS) | |
| if: runner.os == 'macOS' | |
| run: | | |
| # The Homebrew `ollama` FORMULA bottle omits the inference runtime, so | |
| # the judge's chat-completion returns HTTP 500 ("llama-server binary not | |
| # found"). The official `ollama-app` cask (~574MB) bundles the runners | |
| # (libllama / libggml-cpu-*) and does inference in-process. Install it, | |
| # clear the Gatekeeper quarantine so the bundled binaries run headless, | |
| # and put its CLI on PATH so the daemon step's `ollama serve` uses it. | |
| brew install --cask ollama-app | |
| xattr -dr com.apple.quarantine /Applications/Ollama.app 2>/dev/null || true | |
| OLLAMA_BIN=/Applications/Ollama.app/Contents/Resources/ollama | |
| [ -x "$OLLAMA_BIN" ] || { echo "::error::ollama CLI not found at $OLLAMA_BIN"; exit 1; } | |
| echo "$(dirname "$OLLAMA_BIN")" >> "$GITHUB_PATH" | |
| - name: Start Ollama daemon | |
| run: | | |
| ollama serve > /tmp/ollama.log 2>&1 & | |
| echo $! > /tmp/ollama.pid | |
| # Wait for daemon ready (port 11434). | |
| for i in $(seq 1 60); do | |
| if curl -sf http://127.0.0.1:11434/api/version > /dev/null; then | |
| echo "ollama ready after ${i}s" | |
| break | |
| fi | |
| sleep 1 | |
| done | |
| curl -sf http://127.0.0.1:11434/api/version | tee /tmp/ollama-version.json | |
| - name: Pull judge model (${{ env.OLLAMA_JUDGE_MODEL }}) | |
| run: | | |
| ollama pull "$OLLAMA_JUDGE_MODEL" | |
| ollama list | |
| - name: Build release binary | |
| working-directory: runner | |
| run: cargo build --release --bin mirroir-run | |
| - name: Run mega-sample (Playwright + Ollama judge + drift + cross-surface) | |
| working-directory: runner | |
| run: | | |
| ./target/release/mirroir-run --sample samples/mega-sample | |
| - name: Tail Ollama log on failure | |
| if: failure() | |
| run: tail -200 /tmp/ollama.log || true | |
| - name: Stop Ollama daemon | |
| if: always() | |
| run: | | |
| if [ -f /tmp/ollama.pid ]; then | |
| kill "$(cat /tmp/ollama.pid)" 2>/dev/null || true | |
| fi | |
| # All three browsers + dual-OS. Heavy lane — Sunday-night cron + on-demand. | |
| # Reuses the same Ollama setup as runner-e2e; downloads firefox + webkit | |
| # (~600 MB total) which is why this isn't on every push. | |
| runner-e2e-allbrowsers: | |
| name: e2e — all browsers (${{ matrix.os }}) | |
| if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, macos-latest] | |
| permissions: | |
| contents: read | |
| timeout-minutes: 45 | |
| env: | |
| MIRROIR_PLAYWRIGHT_HOME: /tmp/mirroir-pw | |
| OLLAMA_JUDGE_MODEL: qwen2.5:0.5b | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Install Rust toolchain | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| toolchain: ${{ env.RUST_TOOLCHAIN }} | |
| - name: Cache cargo + target | |
| uses: Swatinem/rust-cache@v2 | |
| with: | |
| workspaces: runner -> target | |
| shared-key: runner-e2e-allbrowsers-${{ matrix.os }} | |
| - name: Setup Playwright + all browsers | |
| uses: ./.github/actions/setup-playwright | |
| with: | |
| browsers: chromium firefox webkit | |
| cache-key: allbrowsers-v1 | |
| with-deps: 'true' | |
| - name: Cache Ollama models | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/.ollama/models | |
| key: ollama-${{ runner.os }}-${{ env.OLLAMA_JUDGE_MODEL }}-v1 | |
| - name: Install Ollama (Linux) | |
| if: runner.os == 'Linux' | |
| run: curl -fsSL https://ollama.com/install.sh | sh | |
| - name: Install Ollama (macOS) | |
| if: runner.os == 'macOS' | |
| run: | | |
| # The Homebrew `ollama` FORMULA bottle omits the inference runtime, so | |
| # the judge's chat-completion returns HTTP 500 ("llama-server binary not | |
| # found"). The official `ollama-app` cask (~574MB) bundles the runners | |
| # (libllama / libggml-cpu-*) and does inference in-process. Install it, | |
| # clear the Gatekeeper quarantine so the bundled binaries run headless, | |
| # and put its CLI on PATH so the daemon step's `ollama serve` uses it. | |
| brew install --cask ollama-app | |
| xattr -dr com.apple.quarantine /Applications/Ollama.app 2>/dev/null || true | |
| OLLAMA_BIN=/Applications/Ollama.app/Contents/Resources/ollama | |
| [ -x "$OLLAMA_BIN" ] || { echo "::error::ollama CLI not found at $OLLAMA_BIN"; exit 1; } | |
| echo "$(dirname "$OLLAMA_BIN")" >> "$GITHUB_PATH" | |
| - name: Start Ollama daemon + pull model | |
| run: | | |
| ollama serve > /tmp/ollama.log 2>&1 & | |
| echo $! > /tmp/ollama.pid | |
| for i in $(seq 1 60); do | |
| curl -sf http://127.0.0.1:11434/api/version > /dev/null && break | |
| sleep 1 | |
| done | |
| ollama pull "$OLLAMA_JUDGE_MODEL" | |
| - name: Build release binary | |
| working-directory: runner | |
| run: cargo build --release --bin mirroir-run | |
| - name: Run mega-sample with three browsers | |
| working-directory: runner | |
| run: | | |
| # The per-push lane keeps web scenarios chromium-only; here we patch | |
| # the cross-browser scenario up to all three projects so firefox + | |
| # webkit actually execute in CI (this is the lane that installs them). | |
| sed -i.bak 's/browsers: \[chrome\]/browsers: [chrome, firefox, webkit]/' \ | |
| samples/mega-sample/scenarios/web-cross-browser.yaml | |
| rm -f samples/mega-sample/scenarios/web-cross-browser.yaml.bak | |
| ./target/release/mirroir-run --sample samples/mega-sample | |
| - name: Stop Ollama daemon | |
| if: always() | |
| run: | | |
| if [ -f /tmp/ollama.pid ]; then | |
| kill "$(cat /tmp/ollama.pid)" 2>/dev/null || true | |
| fi | |
| # Security / license / source audit over runner/ — runs the committed | |
| # deny.toml (advisories, license allowlist, bans, crates.io-only sources). | |
| runner-deny: | |
| name: cargo deny check | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: cargo-deny check | |
| uses: EmbarkStudios/cargo-deny-action@v2 | |
| with: | |
| manifest-path: runner/Cargo.toml | |
| arguments: --all-features | |
| # Rehearse `cargo publish` so crates.io packaging never silently drifts from | |
| # the source tree (catches bad metadata / excluded files / build breakage | |
| # before a release). Mirrors runner-release.yml's publish job — no token, no | |
| # upload. | |
| publish-rehearsal: | |
| name: cargo publish --dry-run | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: dtolnay/rust-toolchain@stable | |
| with: | |
| toolchain: ${{ env.RUST_TOOLCHAIN }} | |
| - uses: Swatinem/rust-cache@v2 | |
| with: | |
| workspaces: runner -> target | |
| shared-key: runner-publish-rehearsal | |
| - name: cargo publish --dry-run | |
| working-directory: runner | |
| run: cargo publish --dry-run --locked |