name: CI

on:
  push:
    branches: [main]
  pull_request:
    paths-ignore:
      - 'docs/**'
      - '**/*.md'
      - 'CHANGELOG.md'

# v0.21.0 cost optimisation (W15): cancel superseded runs on the same ref.
# A rapid push train (common during batch work) used to launch one full
# 12-job matrix per commit, even though only the last matters. This
# top-level concurrency group cancels the older run as soon as a newer
# one is queued. Effect: 60-80% saving on "rapid pushes" work days.
concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: true

# v0.19.1 supply-chain hardening (H5): every third-party action is pinned
# by full commit SHA. A floating tag like @v4 can be re-pointed by a
# compromised maintainer (CVE-2025-30066 class). The `# vN.m.k` comment
# next to each SHA is a human-readable hint only — the SHA is the load-
# bearing identifier. When Dependabot proposes a bump, review the new SHA
# against the release tag before merging.

jobs:
  rust-assembler:
    runs-on: ${{ matrix.os }}
    strategy:
      # v0.21.0: macOS only on push-to-main (10x billing multiplier). PRs get ubuntu-only.
      matrix:
        os: ${{ (github.event_name == 'push' && github.ref == 'refs/heads/main') && fromJSON('["ubuntu-latest","macos-latest"]') || fromJSON('["ubuntu-latest"]') }}
    steps:
      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5  # v4.3.1
      - uses: dtolnay/rust-toolchain@stable  # exception to SHA-pin rule: this action uses named-branch convention (stable/nightly/beta/1.NN.0) — pinning a SHA locks to a specific Rust version (validator V-2026-04-22 confirmed 3c5f7ea was rust 1.94.1 branch tip, not generic "install stable"). dtolnay is a trusted maintainer (author of serde/anyhow/cxx). Supply-chain risk of @stable re-point is LOW and accepted here.
      - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4  # v2.9.1
        with:
          workspaces: _assembler
      - run: cd _assembler && cargo test --release

  rust-primitives:
    runs-on: ${{ matrix.os }}
    strategy:
      # v0.21.0: macOS only on push-to-main. --release dropped — debug mode
      # runs 2-3× faster and still catches architectural breakage. Release-
      # build regressions (debug_assertions!) caught by rust-assembler above.
      matrix:
        os: ${{ (github.event_name == 'push' && github.ref == 'refs/heads/main') && fromJSON('["ubuntu-latest","macos-latest"]') || fromJSON('["ubuntu-latest"]') }}
    steps:
      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5  # v4.3.1
      - uses: dtolnay/rust-toolchain@stable  # exception to SHA-pin rule: this action uses named-branch convention (stable/nightly/beta/1.NN.0) — pinning a SHA locks to a specific Rust version (validator V-2026-04-22 confirmed 3c5f7ea was rust 1.94.1 branch tip, not generic "install stable"). dtolnay is a trusted maintainer (author of serde/anyhow/cxx). Supply-chain risk of @stable re-point is LOW and accepted here.
      - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4  # v2.9.1
        with:
          workspaces: _primitives/_rust
      # v0.30+ CI fix (2026-04-29): 98-crate workspace OOMs the default
      # GHA runner's 7 GB at link time. Iteration 2 (after lld+jobs=2 still
      # OOM'd at signal 7): add 12 GB swap on Linux + use `mold` (smaller
      # peak RSS than lld for large workspaces) + drop to JOBS=1 for the
      # link phase. macOS runner has 14 GB RAM and was already passing.
      - name: Free disk + resize swap (Linux runners only)
        if: runner.os == 'Linux'
        run: |
          # Iter 4 fix: iter 3's 12 GB swap on /mnt filled the disk
          # (`No space left on device` on syn rlib). GHA /mnt has only
          # ~14 GB; cargo target/ + 12 GB swap = bust. Strategy now:
          # (a) free ~30 GB on / by removing pre-installed bloat
          #     (Android SDK, .NET, GHC, CodeQL — none used by Rust+Node);
          # (b) resize the existing /swapfile (4 GB → 8 GB) in place.
          df -h
          sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL
          echo "--- after rm ---"
          df -h
          sudo swapoff /swapfile
          sudo rm /swapfile
          sudo fallocate -l 8G /swapfile
          sudo chmod 600 /swapfile
          sudo mkswap /swapfile
          sudo swapon /swapfile
          echo "--- after swap ---"
          free -h
          df -h
      - name: Install mold (Linux runners only)
        if: runner.os == 'Linux'
        run: sudo apt-get update && sudo apt-get install -y mold
      - name: cargo test --workspace (Linux)
        if: runner.os == 'Linux'
        run: cd _primitives/_rust && cargo test --workspace
        env:
          CARGO_BUILD_JOBS: '1'
          RUSTFLAGS: '-C link-arg=-fuse-ld=mold'
      - name: cargo test --workspace (macOS)
        if: runner.os == 'macOS'
        run: cd _primitives/_rust && cargo test --workspace
        env:
          CARGO_BUILD_JOBS: '2'

  ts-packages:
    # v0.21.0: ubuntu-only. Node is x-plat; no macOS-specific behaviour to
    # test. Matrix: 2 jobs (ubuntu × 2 nodes) instead of 4. Saves 2 macOS jobs.
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node: ['20', '22']
    steps:
      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5  # v4.3.1
      - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020  # v4.4.0
        with:
          node-version: ${{ matrix.node }}
      - run: cd _ts_packages && npm ci
      - run: cd _ts_packages && npm run build --workspaces
      - run: cd _ts_packages && npm test --workspaces --if-present

  install-dry-run:
    # v0.21.0: ubuntu-only. All 3 profiles on main push; PRs get minimal-only
    # (full profile pulls everything, rarely signals PR-specific regressions).
    runs-on: ubuntu-latest
    strategy:
      matrix:
        profile: ${{ (github.event_name == 'push' && github.ref == 'refs/heads/main') && fromJSON('["minimal","dev","full"]') || fromJSON('["minimal"]') }}
    steps:
      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5  # v4.3.1
      - uses: dtolnay/rust-toolchain@stable  # exception to SHA-pin rule: this action uses named-branch convention (stable/nightly/beta/1.NN.0) — pinning a SHA locks to a specific Rust version (validator V-2026-04-22 confirmed 3c5f7ea was rust 1.94.1 branch tip, not generic "install stable"). dtolnay is a trusted maintainer (author of serde/anyhow/cxx). Supply-chain risk of @stable re-point is LOW and accepted here.
      - name: Install hard deps
        run: sudo apt-get update && sudo apt-get install -y jq pandoc
      - run: bash -n install.sh
      - run: ./install.sh --no-execute --profile=${{ matrix.profile }}

  shell-lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5  # v4.3.1
      - run: sudo apt-get update && sudo apt-get install -y shellcheck
      - name: shellcheck (advisory)
        # v0.15.1: kept advisory because local shellcheck sweep not yet clean
        # (quoted-var nits in hooks). Flip to fatal once the sweep is committed;
        # planned for v0.16.
        # v0.20.1: explicit `|| true` in addition to continue-on-error — the
        # latter doesn't always suppress the step-level exit-1 in the GH
        # Actions annotation stream.
        run: |
          find hooks _primitives -name '*.sh' -exec shellcheck -S warning {} + || \
            echo "shellcheck emitted warnings (advisory-only, not blocking)"
        continue-on-error: true

  workflow-lint:
    # v0.20.1: guards against the dtolnay-SHA-class incident.
    # actionlint catches workflow syntax; validate-workflow-shas.sh catches
    # fabricated / force-pushed SHA pins. Runs fast (<30s).
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5  # v4.3.1
      - name: Install actionlint
        run: bash scripts/install-actionlint.sh
      - name: Lint workflows (actionlint)
        run: PATH="${HOME}/.local/bin:${PATH}" bash scripts/lint-workflows.sh
      - name: Validate pinned SHAs
        run: bash scripts/validate-workflow-shas.sh
