fix(core): use component-aware comparison in macOS/Windows containmen… #825
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
| name: CI | |
| on: | |
| push: | |
| branches: [main, develop] | |
| pull_request: | |
| branches: [main, develop] | |
| schedule: | |
| # Weekly dependency security check | |
| - cron: '0 0 * * 0' | |
| env: | |
| CARGO_TERM_COLOR: always | |
| CARGO_INCREMENTAL: 0 | |
| CARGO_NET_RETRY: 10 | |
| RUST_BACKTRACE: short | |
| RUSTFLAGS: "-D warnings" | |
| RUSTUP_MAX_RETRIES: 10 | |
| # Default to minimal permissions (security best practice) | |
| permissions: | |
| contents: read | |
| # Cancel previous runs on new push | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| # Detect which files changed to optimize job execution | |
| changes: | |
| name: Detect Changes | |
| runs-on: ubuntu-latest | |
| permissions: | |
| pull-requests: read | |
| outputs: | |
| rust: ${{ steps.filter.outputs.rust }} | |
| python: ${{ steps.filter.outputs.python }} | |
| node: ${{ steps.filter.outputs.node }} | |
| ci: ${{ steps.filter.outputs.ci }} | |
| docs: ${{ steps.filter.outputs.docs }} | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: dorny/paths-filter@v4 | |
| id: filter | |
| with: | |
| filters: | | |
| rust: | |
| - '**/*.rs' | |
| - '**/Cargo.toml' | |
| - '**/Cargo.lock' | |
| - 'deny.toml' | |
| - 'clippy.toml' | |
| - 'rustfmt.toml' | |
| python: | |
| - 'crates/exarch-python/**' | |
| - 'crates/exarch-core/**' | |
| - 'Cargo.toml' | |
| - 'Cargo.lock' | |
| node: | |
| - 'crates/exarch-node/**' | |
| - 'crates/exarch-core/**' | |
| - 'Cargo.toml' | |
| - 'Cargo.lock' | |
| ci: | |
| - '.github/workflows/**' | |
| - '.github/labeler.yml' | |
| docs: | |
| - '**/*.md' | |
| - '**/README.md' | |
| # Quick checks first - fail fast | |
| format: | |
| name: Format Check | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| needs: changes | |
| if: | | |
| needs.changes.outputs.rust == 'true' || | |
| github.ref == 'refs/heads/main' || | |
| github.ref == 'refs/heads/develop' | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Install nightly Rust | |
| uses: dtolnay/rust-toolchain@nightly | |
| with: | |
| components: rustfmt | |
| - name: Check formatting | |
| run: cargo +nightly fmt --all -- --check | |
| clippy: | |
| name: Clippy | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| needs: changes | |
| if: | | |
| needs.changes.outputs.rust == 'true' || | |
| github.ref == 'refs/heads/main' || | |
| github.ref == 'refs/heads/develop' | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Install Rust stable | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| components: clippy | |
| - name: Cache Cargo dependencies | |
| uses: Swatinem/rust-cache@v2 | |
| with: | |
| shared-key: "clippy" | |
| save-if: ${{ github.ref == 'refs/heads/main' }} | |
| - name: Run Clippy | |
| run: cargo clippy --workspace --all-targets --all-features -- -D warnings | |
| documentation: | |
| name: Documentation | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| needs: changes | |
| if: | | |
| needs.changes.outputs.rust == 'true' || | |
| github.ref == 'refs/heads/main' || | |
| github.ref == 'refs/heads/develop' | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Install Rust stable | |
| uses: dtolnay/rust-toolchain@stable | |
| - name: Cache Cargo dependencies | |
| uses: Swatinem/rust-cache@v2 | |
| with: | |
| shared-key: "doc" | |
| save-if: ${{ github.ref == 'refs/heads/main' }} | |
| - name: Check documentation | |
| env: | |
| RUSTDOCFLAGS: "-D warnings" | |
| run: | | |
| cargo doc --no-deps --all-features --workspace 2>&1 | tee /tmp/cargo-doc.log | |
| # cargo emits target output-path collisions as a warning, not a rustdoc lint, so | |
| # RUSTDOCFLAGS="-D warnings" above cannot catch it (see #429) — assert it separately. | |
| if grep -q "output filename collision" /tmp/cargo-doc.log; then | |
| echo "::error::cargo doc reported an output filename collision (see #429)" | |
| exit 1 | |
| fi | |
| # Security audit | |
| security: | |
| name: Security Audit | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| needs: changes | |
| if: | | |
| needs.changes.outputs.rust == 'true' || | |
| github.ref == 'refs/heads/main' || | |
| github.ref == 'refs/heads/develop' || | |
| github.event_name == 'schedule' | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Run cargo-deny | |
| uses: EmbarkStudios/cargo-deny-action@v2 | |
| with: | |
| log-level: warn | |
| command: check | |
| arguments: --all-features | |
| # Cross-platform tests with matrix | |
| test: | |
| name: Test (${{ matrix.os }}) | |
| needs: [changes, format] | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 30 | |
| if: | | |
| needs.changes.outputs.rust == 'true' || | |
| github.ref == 'refs/heads/main' || | |
| github.ref == 'refs/heads/develop' | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, macos-latest, windows-latest] | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Install Rust stable | |
| uses: dtolnay/rust-toolchain@stable | |
| - name: Cache Cargo dependencies | |
| uses: Swatinem/rust-cache@v2 | |
| with: | |
| shared-key: "test-${{ matrix.os }}" | |
| save-if: ${{ github.ref == 'refs/heads/main' }} | |
| - name: Install nextest | |
| uses: taiki-e/install-action@v2.85.10 | |
| with: | |
| tool: nextest | |
| - name: Build all targets | |
| run: cargo build --workspace --all-targets --all-features --exclude exarch-python --exclude exarch-node | |
| - name: Run tests | |
| run: cargo nextest run --workspace --all-features --no-fail-fast --exclude exarch-python --exclude exarch-node | |
| - name: Run doctests | |
| run: cargo test --doc --workspace --all-features --exclude exarch-python --exclude exarch-node | |
| # Release-profile tests for exarch-core: exercises cfg(not(debug_assertions)) | |
| # tests (e.g. release-only redaction/security assertions) that the debug-mode | |
| # `test` job above never runs (see #476). | |
| test-release: | |
| name: Test (release profile) | |
| needs: [changes, format] | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| if: | | |
| needs.changes.outputs.rust == 'true' || | |
| github.ref == 'refs/heads/main' || | |
| github.ref == 'refs/heads/develop' | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Install Rust stable | |
| uses: dtolnay/rust-toolchain@stable | |
| - name: Cache Cargo dependencies | |
| uses: Swatinem/rust-cache@v2 | |
| with: | |
| shared-key: "test-release" | |
| save-if: ${{ github.ref == 'refs/heads/main' }} | |
| - name: Install nextest | |
| uses: taiki-e/install-action@v2.85.10 | |
| with: | |
| tool: nextest | |
| - name: Run release-profile tests (exarch-core) | |
| run: cargo nextest run -p exarch-core --release --all-features --no-fail-fast | |
| # Test Python bindings with coverage | |
| test-python: | |
| name: Test Python ${{ matrix.python-version }} | |
| needs: [changes, format] | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| # always() + explicit needs.format.result check is required here: a job | |
| # with `needs: [format]` is skipped by GitHub Actions whenever `format` | |
| # itself is skipped (e.g. on a Python-only PR, where rust == 'false'), | |
| # regardless of what this job's own `if` says, unless always() overrides | |
| # that default gating. | |
| if: | | |
| always() && | |
| needs.changes.result == 'success' && | |
| (needs.format.result == 'success' || needs.format.result == 'skipped') && | |
| (needs.changes.outputs.python == 'true' || | |
| github.ref == 'refs/heads/main' || | |
| github.ref == 'refs/heads/develop') | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| python-version: ['3.10', '3.11', '3.12', '3.13', '3.14'] | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Install Rust stable | |
| uses: dtolnay/rust-toolchain@stable | |
| - name: Cache Cargo dependencies | |
| uses: Swatinem/rust-cache@v2 | |
| with: | |
| shared-key: "python" | |
| save-if: ${{ github.ref == 'refs/heads/main' }} | |
| - name: Set up Python | |
| uses: actions/setup-python@v7 | |
| with: | |
| python-version: ${{ matrix.python-version }} | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v7 | |
| - name: Install dependencies | |
| run: | | |
| cd crates/exarch-python | |
| uv pip install --system maturin "ruff==0.16.0" mypy pytest pytest-cov | |
| - name: Run Rust unit tests (exarch-python) | |
| run: cargo test -p exarch-python --lib --no-default-features | |
| - name: Format check with ruff | |
| run: | | |
| cd crates/exarch-python | |
| ruff format --check . | |
| - name: Lint with ruff | |
| run: | | |
| cd crates/exarch-python | |
| ruff check . | |
| - name: Type check with mypy | |
| run: | | |
| cd crates/exarch-python | |
| mypy . --ignore-missing-imports || true | |
| - name: Build Python bindings | |
| run: | | |
| cd crates/exarch-python | |
| # panic-injection exposes a deliberate-panic test hook used by the | |
| # #395 regression test (test_panic_safety.py); never enabled for | |
| # published wheels (see release.yml). | |
| maturin build --release --features panic-injection,abi3 | |
| - name: Install Python bindings | |
| run: uv pip install --system target/wheels/*.whl | |
| - name: Test Python bindings | |
| if: matrix.python-version != '3.12' | |
| run: | | |
| cd crates/exarch-python | |
| pytest tests/ -v | |
| # Coverage is only measured once (Python version doesn't change which | |
| # lines of the Rust-backed bindings get exercised), so only one matrix | |
| # leg uploads to Codecov — uploading all 5 would just duplicate the | |
| # same flag's data and race codecov.yml's after_n_builds threshold. | |
| - name: Test Python bindings with coverage | |
| if: matrix.python-version == '3.12' | |
| run: | | |
| cd crates/exarch-python | |
| pytest tests/ -v --cov --cov-report=xml:coverage.xml | |
| - name: Upload Python coverage to Codecov | |
| if: matrix.python-version == '3.12' | |
| uses: codecov/codecov-action@v7 | |
| with: | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| files: crates/exarch-python/coverage.xml | |
| flags: exarch-python | |
| fail_ci_if_error: false | |
| # Test Node.js bindings with coverage | |
| test-node: | |
| name: Test Node.js Bindings | |
| needs: [changes, format] | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| if: | | |
| always() && | |
| needs.changes.result == 'success' && | |
| (needs.format.result == 'success' || needs.format.result == 'skipped') && | |
| (needs.changes.outputs.node == 'true' || | |
| github.ref == 'refs/heads/main' || | |
| github.ref == 'refs/heads/develop') | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Install Rust stable | |
| uses: dtolnay/rust-toolchain@stable | |
| - name: Cache Cargo dependencies | |
| uses: Swatinem/rust-cache@v2 | |
| with: | |
| shared-key: "node" | |
| save-if: ${{ github.ref == 'refs/heads/main' }} | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v7 | |
| with: | |
| node-version: '20' | |
| - name: Install dependencies | |
| run: | | |
| cd crates/exarch-node | |
| npm install | |
| - name: Run Rust unit tests (exarch-node) | |
| run: cargo test -p exarch-node --lib | |
| - name: Format check with Biome | |
| run: | | |
| cd crates/exarch-node | |
| npm run format:check | |
| - name: Lint with Biome | |
| run: | | |
| cd crates/exarch-node | |
| npm run lint | |
| - name: Build Node.js bindings | |
| run: | | |
| cd crates/exarch-node | |
| npm run build | |
| - name: Test Node.js bindings | |
| run: | | |
| cd crates/exarch-node | |
| npm test | |
| # Quick compile-only check for criterion benchmarks (does not run them) | |
| bench-build: | |
| name: Bench Build Check | |
| needs: [changes, format] | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| if: | | |
| needs.changes.outputs.rust == 'true' || | |
| github.ref == 'refs/heads/main' || | |
| github.ref == 'refs/heads/develop' | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Install Rust stable | |
| uses: dtolnay/rust-toolchain@stable | |
| - name: Cache Cargo dependencies | |
| uses: Swatinem/rust-cache@v2 | |
| with: | |
| shared-key: "bench-build" | |
| save-if: ${{ github.ref == 'refs/heads/main' }} | |
| - name: Build benchmarks (compile only, does not run them) | |
| run: cargo build --workspace --benches --all-features --exclude exarch-python --exclude exarch-node | |
| # MSRV check (exarch-core only) | |
| msrv: | |
| name: MSRV Check (exarch-core) | |
| needs: [changes, format] | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| if: | | |
| needs.changes.outputs.rust == 'true' || | |
| github.ref == 'refs/heads/main' || | |
| github.ref == 'refs/heads/develop' | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Install Rust 1.96.0 | |
| uses: dtolnay/rust-toolchain@master | |
| with: | |
| toolchain: "1.96.0" | |
| - name: Cache Cargo dependencies | |
| uses: Swatinem/rust-cache@v2 | |
| with: | |
| shared-key: "msrv" | |
| save-if: ${{ github.ref == 'refs/heads/main' }} | |
| - name: Check MSRV for exarch-core | |
| run: cargo check -p exarch-core --all-features | |
| # Code coverage (Linux only) - codecov auto-distributes by paths | |
| coverage: | |
| name: Code Coverage | |
| needs: [changes, format] | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| permissions: | |
| contents: read | |
| if: | | |
| needs.changes.outputs.rust == 'true' || | |
| github.ref == 'refs/heads/main' || | |
| github.ref == 'refs/heads/develop' | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Install Rust stable | |
| uses: dtolnay/rust-toolchain@stable | |
| - name: Cache Cargo dependencies | |
| uses: Swatinem/rust-cache@v2 | |
| with: | |
| shared-key: "coverage" | |
| save-if: ${{ github.ref == 'refs/heads/main' }} | |
| - name: Install cargo-llvm-cov | |
| uses: taiki-e/install-action@v2.85.10 | |
| with: | |
| tool: cargo-llvm-cov | |
| - name: Install nextest | |
| uses: taiki-e/install-action@v2.85.10 | |
| with: | |
| tool: nextest | |
| - name: Generate coverage report | |
| run: | | |
| cargo llvm-cov --all-features --workspace \ | |
| --exclude exarch-python --exclude exarch-node \ | |
| --lcov --output-path lcov.info nextest | |
| - name: Upload coverage to Codecov | |
| uses: codecov/codecov-action@v7 | |
| with: | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| files: lcov.info | |
| flags: exarch-core, exarch-cli | |
| fail_ci_if_error: false | |
| verbose: true | |
| # All checks passed | |
| ci-success: | |
| name: CI Success | |
| needs: [format, clippy, documentation, security, test, test-release, test-python, test-node, msrv, bench-build] | |
| runs-on: ubuntu-latest | |
| if: always() | |
| steps: | |
| - name: Check all jobs | |
| run: | | |
| # Required jobs that must pass | |
| REQUIRED_JOBS=( | |
| "${{ needs.format.result }}" | |
| "${{ needs.clippy.result }}" | |
| "${{ needs.documentation.result }}" | |
| "${{ needs.security.result }}" | |
| "${{ needs.test.result }}" | |
| "${{ needs.test-release.result }}" | |
| "${{ needs.test-python.result }}" | |
| "${{ needs.test-node.result }}" | |
| "${{ needs.msrv.result }}" | |
| "${{ needs.bench-build.result }}" | |
| ) | |
| for result in "${REQUIRED_JOBS[@]}"; do | |
| if [[ "$result" != "success" && "$result" != "skipped" ]]; then | |
| echo "Required job failed or was cancelled: $result" | |
| exit 1 | |
| fi | |
| done | |
| echo "All required jobs passed successfully!" |