zshrs 0.11.2

The first compiled Unix shell — bytecode VM, worker pool, AOP intercept, Rkyv caching
Documentation
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
  workflow_dispatch:

permissions:
  contents: read
  actions: write

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

env:
  CARGO_TERM_COLOR: always

jobs:
  check:
    name: Check
    runs-on: ubuntu-latest
    timeout-minutes: 30
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      - run: cargo check --all-targets --locked

  test:
    name: Test
    runs-on: ${{ matrix.os }}
    timeout-minutes: 45
    env:
      RUST_BACKTRACE: 1
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, macos-latest]
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      - run: cargo test --locked --no-fail-fast

  doc:
    name: Doc
    runs-on: ubuntu-latest
    timeout-minutes: 20
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      - env:
          RUSTDOCFLAGS: -D warnings
        run: cargo doc --no-deps --locked

  corpus:
    name: Corpus tests
    runs-on: ${{ matrix.os }}
    timeout-minutes: 15
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, macos-latest]
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      - run: cargo build --release --locked
      - name: Run zshrs corpus tests
        run: bash test_corpus/run_corpus.sh
        env:
          ZSHRS: ${{ github.workspace }}/target/release/zshrs
      - name: Upload corpus failure log
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: corpus-failures-${{ matrix.os }}
          path: test_corpus/corpus_failures.log
          if-no-files-found: ignore
          retention-days: 30

  # ztst job removed: the per-block fresh-spawn wrapper at tests/ztst_runner.rs
  # was a fake-pass (counted failures, never asserted). The corpus exercises
  # interactive ZLE/completion/job-control/signal-trap paths that need a real
  # pty + per-file persistent shell to drive correctly. Replacement is the
  # PTY harness designed in docs/ZTST_PTY_HARNESS.md; not CI-gated until that
  # lands. Local manual runs only via `cargo run --bin ztst-runner` once built.

  recorder-harness:
    # Plugin-Framework-Agnostic State-Modification Recorder (PFA-SMR) ——
    # see docs/RECORDER.md. The harness in tests/recorder_harness.rs runs
    # `zshrs-recorder --no-daemon --file PATH` against tests/recorder_corpus/
    # (synthetic per-kind scripts + verbatim zinit upstream `bin/*.zsh`) and
    # asserts the per-kind event counts the recorder must produce. A
    # regression that misses a dispatcher or double-fires one fails the
    # relevant test with a kind-by-kind diff.
    #
    # Hermetic: --no-daemon skips the end-of-run IPC bundle, so the
    # zshrs-daemon never starts and ~/.cache/zshrs/ is not written.
    name: Recorder harness
    runs-on: ${{ matrix.os }}
    timeout-minutes: 20
    env:
      RUST_BACKTRACE: 1
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, macos-latest]
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      - name: Build with --features recorder
        run: cargo build --locked --features recorder --bin zshrs-recorder
      - name: Verify Rule 1 — zero recorder code in default zshrs binary
        # docs/RECORDER.md §"Zero-overhead default binary": the daily-driver
        # `zshrs` must contain ZERO `zsh::recorder::*` module symbols.
        #
        # Match shape: Rust mangles module paths as <byte-length><name>, so
        # the recorder module appears in symbols as `8recorder` (the 8 = byte
        # length of "recorder"). This is precise — it matches the AOP
        # runtime module specifically, not arbitrary functions whose names
        # happen to contain the substring "recorder" (e.g. shell-side
        # `latest_recorder_shard()` which only WALKS THE FILESYSTEM for
        # `*-recorder.rkyv` shards the daemon's recorder produced — not
        # recorder runtime code).
        #
        # Whitelist: the daemon-side `op_recorder_ingest` ingest endpoint
        # always ships in the daemon (different concern; not the
        # shell-runtime AOP layer); `sqlite3RecordError*` from rusqlite is
        # not affected by the precise grep but kept in the whitelist for
        # belt-and-suspenders robustness.
        run: |
          cargo build --locked --bin zshrs
          set -e
          hits=$(nm target/debug/zshrs 2>/dev/null \
            | grep -E '\b8recorder[0-9a-zA-Z_]' \
            | grep -v sqlite3RecordError \
            | grep -v zshrs_daemon \
            | wc -l \
            | tr -d ' ')
          echo "shell-runtime zsh::recorder module symbols in default zshrs: $hits"
          if [ "$hits" -ne 0 ]; then
            echo "FAIL: Rule 1 broken — zsh::recorder code leaked into default zshrs"
            nm target/debug/zshrs 2>/dev/null | grep -E '\b8recorder[0-9a-zA-Z_]' \
              | grep -v sqlite3RecordError | grep -v zshrs_daemon
            exit 1
          fi
      - name: Run recorder harness
        run: cargo test --locked --features recorder --test recorder_harness -- --nocapture

  daemon-http:
    # zshrs-daemon HTTP listener — see daemon/http.rs and
    # docs/DAEMON_AS_SERVICE.md. Spawns the daemon with an isolated
    # XDG_CACHE_HOME + XDG_CONFIG_HOME (so each test owns its own
    # cache / socket / config), points it at a kernel-allocated free
    # port (127.0.0.1:0 → real port via TcpListener probe), and drives
    # `GET /health`, `GET /ops`, `POST /op/ping`, `POST /op/<unknown>`,
    # plus the bearer-token auth path. Hermetic: every test owns a
    # tempdir, no shared state across tests, no external network.
    name: Daemon HTTP
    runs-on: ${{ matrix.os }}
    timeout-minutes: 15
    env:
      RUST_BACKTRACE: 1
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, macos-latest]
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      # The integration test spawns `target/debug/zshrs-daemon` (per
      # tests/daemon_http.rs::zshrs_daemon_binary which falls back from
      # CARGO_BIN_EXE_zshrs-daemon to manifest/target/debug). The daemon
      # binary lives in the workspace's `daemon` crate (separate package
      # from `zsh`), so `cargo test -p zsh` does NOT auto-build it. Build
      # it explicitly here so each test's spawn isn't racing.
      - name: Build zshrs-daemon binary the test will spawn
        run: cargo build --locked -p zshrs-daemon --bin zshrs-daemon
      - name: Run daemon HTTP integration tests
        # --test-threads=1 because each test binds its own port + spawns
        # its own daemon; sequential gives clearer per-test diagnostics
        # and avoids the chance of two daemons racing the same XDG_CACHE
        # tempdir cleanup on slow CI runners.
        run: cargo test --locked --test daemon_http -- --test-threads=1 --nocapture

  release-build:
    name: Release Build
    needs: [check, test, doc, corpus, recorder-harness, daemon-http]
    runs-on: ${{ matrix.os }}
    timeout-minutes: 60
    strategy:
      fail-fast: false
      matrix:
        include:
          - os: ubuntu-latest
            target: x86_64-unknown-linux-gnu
          - os: macos-latest
            target: x86_64-apple-darwin
          - os: macos-latest
            target: aarch64-apple-darwin
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
        with:
          targets: ${{ matrix.target }}
      - uses: Swatinem/rust-cache@v2
      - run: cargo build --release --locked --target ${{ matrix.target }}
      - uses: actions/upload-artifact@v4
        with:
          name: zshrs-${{ matrix.target }}
          path: target/${{ matrix.target }}/release/zshrs