enprot 0.5.61

Engyon Protected Text (EPT) — confidentiality processor and capability ledger
name: tests

on:
  pull_request:
  push:
    branches:
      - main
    tags:

concurrency:
  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: true

env:
  RUST_BACKTRACE: full
  BOTAN_VERSION: 3.7.0
  PREFIX: /usr

jobs:
  typos:
    name: Spell Check
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: crate-ci/typos@v1

  security:
    name: Security Audit
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - name: Generate Cargo.lock
        run: cargo generate-lockfile
      - name: Run cargo-audit
        uses: rustsec/audit-check@v2
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
      - name: Install cargo-deny
        uses: taiki-e/install-action@v2.86.3
        with:
          tool: cargo-deny
      - name: Check licenses and advisories
        run: cargo deny check licenses advisories

  test-unix-stable:
    # 45 (not 30): the ubuntu-stable leg additionally compiles the
    # telemetry feature's opentelemetry tree and the fuzz crate —
    # cold, since this job caches nothing. A 30-min cap canceled a
    # green run mid-compile (PR #401, 2026-08-23).
    timeout-minutes: 45
    strategy:
      fail-fast: false
      matrix:
        # ubuntu-24.04-arm: native Linux aarch64 (TODO.complete/46).
        # The deploy workflow cross-compiles aarch64-linux-musl but
        # never runs the tests there — this leg catches endianness /
        # alignment / glibc-ABI issues before release. Stable-only to
        # bound cost; continue-on-error until verified green (the
        # established OHOS pattern), then drop the flag.
        os: [ubuntu-latest, ubuntu-24.04-arm, macos-latest, windows-latest]
        rust: [stable, beta, "1.88"]
        exclude:
          # Beta on Windows is low-value; stable + 1.88 catch Windows
          # regressions without burning the extra minutes.
          - os: windows-latest
            rust: beta
          # One toolchain per architecture is enough for aarch64.
          - os: ubuntu-24.04-arm
            rust: beta
          - os: ubuntu-24.04-arm
            rust: "1.88"
    name: Test on ${{ matrix.rust }} (${{ matrix.os }})
    runs-on: ${{ matrix.os }}
    continue-on-error: ${{ matrix.os == 'ubuntu-24.04-arm' }}
    steps:
      - if: "startsWith(matrix.os, 'windows')"
        run: git config --global core.autocrlf false
      - uses: actions/checkout@v7
      - name: Install rust toolchain
        uses: dtolnay/rust-toolchain@master
        with:
          toolchain: ${{ matrix.rust }}
      - name: Install dependencies (linux)
        if: "startsWith(matrix.os, 'ubuntu')"
        run: ./ci/install.sh
      - name: Install Botan (macos)
        if: "startsWith(matrix.os, 'macos')"
        run: brew install botan
      # Unix: vendored-rnp builds librnp from source via rnp-src.
      # No system librnp/json-c/sexpp needed — fixes rnp-rs 0.1.10
      # linking issues where extra link directives can't find libs.
      - name: Build (unix)
        if: "!startsWith(matrix.os, 'windows')"
        run: cargo build --verbose --features vendored-rnp
      - name: Run tests (unix)
        if: "!startsWith(matrix.os, 'windows')"
        run: cargo test --verbose --features vendored-rnp -- --nocapture
      # The fuzz crate (fuzz/Cargo.toml) is standalone — excluded
      # from the workspace, so every other gate here skips it and
      # it bit-rotted once already (CasStore trait moved out of
      # scope; found 2026-08). One cheap check per PR keeps the
      # weekly cargo-fuzz runs buildable. Stable-only: libfuzzer-sys
      # builds fine without the nightly-only sanitizer flags that
      # `cargo fuzz` itself adds.
      - name: Check fuzz crate compiles (unix, stable)
        if: "matrix.rust == 'stable' && startsWith(matrix.os, 'ubuntu')"
        run: cargo check --manifest-path fuzz/Cargo.toml --features vendored-rnp
      # The OTLP export feature (TODO.complete/38) is opt-in and
      # therefore invisible to the default-build gates; keep its API
      # surface compiling (tests included) and its unit tests green
      # on one leg. The opentelemetry tree is heavy — check, don't
      # re-run the whole suite with the feature on.
      - name: Check + unit-test telemetry feature (ubuntu, stable)
        if: "matrix.rust == 'stable' && startsWith(matrix.os, 'ubuntu')"
        run: |
          cargo check --verbose --features vendored-rnp,telemetry --all-targets
          cargo test --verbose --features vendored-rnp,telemetry --lib telemetry
      # Windows: install.ps1 builds the full C dep stack from source.
      # vendored-rnp isn't ready on Windows yet (rnp-src 0.1.3 pending).
      - name: Install dependencies (windows)
        if: "startsWith(matrix.os, 'windows')"
        env:
          GIT_REDIRECT_STDERR: '2>&1'
        shell: pwsh
        run: |
          $Env:PREFIX = (Join-Path $PWD 'botan-install').Replace('\', '/')
          $Env:Path += ";$Env:PREFIX/lib"
          $Env:TARGET = "x86_64-pc-windows-msvc"
          ./ci/install.ps1
      - name: Build (windows)
        if: "startsWith(matrix.os, 'windows')"
        # Build the main crate and the FFI crate sequentially.
        # Both produce target/debug/deps/enprot.pdb (the main crate's
        # enprot.exe and enprot-ffi's enprot.dll share the same output
        # basename), and parallel link.exe invocations race on the
        # shared PDB file (LNK1201). Sequential linking eliminates the
        # race without changing crate names or disabling Defender.
        run: |
          cargo build --verbose --package enprot
          cargo build --verbose --package enprot-ffi
      - name: Run tests (windows)
        if: "startsWith(matrix.os, 'windows')"
        run: cargo test --verbose -- --nocapture

  cas-s3:
    # Live round-trip of the S3 CAS backend (TODO.complete/27)
    # against MinIO — SigV4 signing, PUT/GET/HEAD/LIST/DELETE, hash
    # verification, and NotFound mapping, exercised for real. The
    # backend itself compiles under the default gates via the
    # feature's cfg walls; this job is the only place the feature is
    # TESTED, so it doubles as the rot-guard for the cas-s3 surface.
    name: CAS S3 backend (MinIO)
    runs-on: ubuntu-latest
    timeout-minutes: 45
    env:
      AWS_ACCESS_KEY_ID: minioadmin
      AWS_SECRET_ACCESS_KEY: minioadmin
      AWS_DEFAULT_REGION: us-east-1
      AWS_ENDPOINT_URL: http://localhost:9000
      AWS_ALLOW_HTTP: "1"
      ENPROT_S3_TEST_SPEC: s3://enprot-cas-test/cas/
    steps:
      - uses: actions/checkout@v7
      # Not a `services:` container: GitHub's service support can't pass
      # the `server /data` command the official minio/minio image
      # requires, and the bitnami catalog was restructured in 2025
      # (bitnami/minio:latest no longer exists). A background docker
      # run with explicit args is the canonical-image equivalent.
      - name: Start MinIO (background)
        run: |
          docker run -d --name minio -p 9000:9000 \
            -e MINIO_ROOT_USER=minioadmin -e MINIO_ROOT_PASSWORD=minioadmin \
            minio/minio:latest server /data
      - name: Install rust toolchain
        uses: dtolnay/rust-toolchain@stable
      - name: Install dependencies
        run: ./ci/install.sh
      - name: Create test bucket (mc; waits for MinIO readiness)
        # The S3 API needs SigV4-signed requests — curl with basic
        # auth gets a 403 — so bucket creation goes through the
        # MinIO client, whose alias-set also doubles as the
        # readiness probe.
        run: |
          set -euxo pipefail
          curl -fsSL https://dl.min.io/client/mc/release/linux-amd64/mc -o /tmp/mc
          chmod +x /tmp/mc
          for i in $(seq 1 30); do
            if /tmp/mc alias set local http://localhost:9000 minioadmin minioadmin >/dev/null 2>&1; then
              echo "minio ready after ${i} probes"
              break
            fi
            sleep 2
          done
          /tmp/mc mb --ignore-existing local/enprot-cas-test
      - name: Test S3 backend
        run: "cargo test --verbose --features vendored-rnp,cas-s3 --lib cas::"

  coverage:
    name: Code coverage
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@v7
      - name: Install rust toolchain
        uses: dtolnay/rust-toolchain@stable
        with:
          components: llvm-tools-preview
      - name: Install dependencies
        run: ./ci/install.sh
      - name: Install cargo-llvm-cov
        uses: taiki-e/install-action@v2.86.3
        with:
          tool: cargo-llvm-cov
      - name: Generate coverage (gated)
        # TODO.complete/49: coverage gate. Floor is just under the
        # current baseline (83.32% as of 2026-08-15, after the
        # etree parse/transform/write spec modules landed). Raise the
        # floor as coverage grows; never lower it.
        run: cargo llvm-cov --lcov --output-path lcov.info --fail-under-lines 82
      - name: Upload to Codecov
        uses: codecov/codecov-action@v7
        with:
          token: ${{ secrets.CODECOV_TOKEN }}
          files: lcov.info
          fail_ci_if_error: false

  check-format:
    name: Check code format
    runs-on: ubuntu-latest
    timeout-minutes: 5
    steps:
      - uses: actions/checkout@v7
      - name: Install rust toolchain
        uses: dtolnay/rust-toolchain@stable
        with:
          components: rustfmt
      - name: Validate code format
        run: cargo fmt --all --check

  check-clippy:
    name: Clippy
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@v7
      - name: Install rust toolchain
        uses: dtolnay/rust-toolchain@stable
        with:
          components: clippy
      - name: Install dependencies
        run: ./ci/install.sh
      - name: Clippy
        run: cargo clippy --all-targets -- -D warnings

  bench:
    name: Benchmarks
    runs-on: ubuntu-latest
    timeout-minutes: 30
    steps:
      - uses: actions/checkout@v7
      - name: Install rust toolchain
        uses: dtolnay/rust-toolchain@stable
      - name: Install dependencies
        run: ./ci/install.sh
      - name: Compile benchmarks
        run: cargo bench --no-run
      - name: Smoke-run benchmarks (quick)
        run: |
          cargo bench --bench merkle           -- --quick --warm-up-time 1 --measurement-time 2
          cargo bench --bench crypto           -- --quick --warm-up-time 1 --measurement-time 2
          cargo bench --bench parser           -- --quick --warm-up-time 1 --measurement-time 2
          cargo bench --bench comparison       -- --quick --warm-up-time 1 --measurement-time 2
          cargo bench --bench cas              -- --quick --warm-up-time 1 --measurement-time 2
          cargo bench --bench parallel_scaling -- --quick --warm-up-time 1 --measurement-time 2
          cargo bench --bench sigstore         -- --quick --warm-up-time 1 --measurement-time 2
          # Peak-heap: one 10 MB run per invocation (dhat cannot reset).
          cargo bench --bench memory -- 10

  bench-compare:
    name: Benchmarks vs main
    runs-on: ubuntu-latest
    if: github.event_name == 'pull_request'
    # The full 7-bench suite compiles twice (PR head + merge-base);
    # at 30 min the job died mid-comparison (timeout), not in a bench.
    timeout-minutes: 60
    steps:
      - uses: actions/checkout@v7
        with:
          fetch-depth: 0
      - name: Install rust toolchain
        uses: dtolnay/rust-toolchain@stable
      - name: Install dependencies
        run: ./ci/install.sh
      - name: Build benchmarks on PR head
        run: cargo bench --no-run
      - name: Save PR baseline
        run: |
          cargo bench --bench merkle           -- --save-baseline pr --warm-up-time 1 --measurement-time 3
          cargo bench --bench crypto           -- --save-baseline pr --warm-up-time 1 --measurement-time 3
          cargo bench --bench parser           -- --save-baseline pr --warm-up-time 1 --measurement-time 3
          cargo bench --bench comparison       -- --save-baseline pr --warm-up-time 1 --measurement-time 3
          cargo bench --bench cas              -- --save-baseline pr --warm-up-time 1 --measurement-time 3
          cargo bench --bench parallel_scaling -- --save-baseline pr --warm-up-time 1 --measurement-time 3
          cargo bench --bench sigstore         -- --save-baseline pr --warm-up-time 1 --measurement-time 3
      - name: Switch to merge-base (origin/main)
        # -f: building benches on the PR head can touch Cargo.lock
        # (lock regeneration for optional deps); the comparison
        # doesn't need those local changes.
        run: git checkout -f origin/main
      - name: Build benchmarks on main
        run: cargo bench --no-run
      - name: Compare PR vs main (criterion auto-prints regression %)
        # Only the benches that exist on BOTH sides can be compared —
        # this step runs on main's checkout, where new-in-PR bench
        # targets don't exist ("no bench target named ..."). They get
        # their first baseline from the PR-head run above.
        run: |
          cargo bench --bench merkle     -- --baseline pr --warm-up-time 1 --measurement-time 3
          cargo bench --bench crypto     -- --baseline pr --warm-up-time 1 --measurement-time 3
          cargo bench --bench parser     -- --baseline pr --warm-up-time 1 --measurement-time 3
          cargo bench --bench comparison -- --baseline pr --warm-up-time 1 --measurement-time 3
          cargo bench --bench cas        -- --baseline pr --warm-up-time 1 --measurement-time 3