fix(ci): migrate npm publish to OIDC Trusted Publishing #3268
Workflow file for this run
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] | |
| pull_request: | |
| branches: [main] | |
| workflow_dispatch: | |
| # Cancel a still-running CI for the same ref when a new push supersedes it — | |
| # avoids burning runner-hours on stale commits during active PR iteration. | |
| # Never cancel on main: a run there should always finish (e.g. for the | |
| # scorecard/coverage artifacts it produces), even if another push lands. | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} | |
| permissions: | |
| contents: read | |
| env: | |
| CARGO_TERM_COLOR: always | |
| RUSTFLAGS: -Dwarnings -Aunreachable-pub | |
| # CI must be hermetic: never let tests download the embedding model | |
| # (~90 MB) in the background. Without this, the #551 auto-activation can | |
| # load the engine mid-suite and slow tarpaulin past its timeout. | |
| LEAN_CTX_EMBEDDINGS_AUTO_DOWNLOAD: "0" | |
| jobs: | |
| narrative-governance: | |
| name: Narrative governance | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| persist-credentials: false | |
| - name: Enforce internal product authority | |
| run: python3 scripts/check-narrative-governance.py | |
| package-assets: | |
| name: Published crate assets | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 # v4 | |
| with: | |
| persist-credentials: false | |
| - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable | |
| - name: Verify OCLA manifests are packaged | |
| working-directory: rust | |
| run: | | |
| set -euo pipefail | |
| cargo package --locked --no-verify --list -p lean-ctx > "$RUNNER_TEMP/package-files.txt" | |
| for asset in \ | |
| assets/ocla/capability-manifests/leanctx/context-optimization-v1.json \ | |
| assets/ocla/capability-manifests/leanctx/passthrough-v1.json \ | |
| assets/ocla/capability-manifests/example/word-count-optimizer-v1.json \ | |
| assets/ocla/capability-manifests/rtk/rtk-shell-v1.json \ | |
| assets/kits/code-review/kit.toml; do | |
| grep -Fx "$asset" "$RUNNER_TEMP/package-files.txt" | |
| done | |
| test: | |
| name: Test | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| # macOS runners are ~2x slower (Test step ~32 min vs ~24 win / ~16 ubuntu) | |
| # and bill ~10x the minutes, so keep them off the PR critical path: PRs | |
| # run ubuntu + windows only. macOS still runs on every push to main (i.e. | |
| # at merge), on workflow_dispatch, and in release.yml — so platform | |
| # regressions are still caught before release, just not on every PR push. | |
| os: ${{ github.event_name == 'pull_request' && fromJSON('["ubuntu-latest", "windows-latest"]') || fromJSON('["ubuntu-latest", "macos-latest", "windows-latest"]') }} | |
| runs-on: ${{ matrix.os }} | |
| # Tests run with libtest's default thread parallelism. The old | |
| # `--test-threads=1` (env-race legacy, v3.3.5) serialized ~70 binaries that | |
| # now include heavy property/fuzz/benchmark suites (#291, #493, #551) and | |
| # dominated the wall clock. What made the flag necessary is fixed at the | |
| # source instead: every env-mutating test serializes on the reentrant | |
| # `data_dir::test_env_lock()`, and the one test with a real-time window | |
| # (`hebbian_eviction_bonus_is_wired`) takes an injected window rather than | |
| # depending on scheduling. cargo-nextest was tried first (#1206) and | |
| # reverted — 33-80% slower here, since its process-per-test model re-pays | |
| # this crate's heavy static-link cost for every test. | |
| timeout-minutes: 180 | |
| env: | |
| # ~50 integration-test binaries each statically link the full lib | |
| # (tree-sitter ×18, rten, aws-lc, resvg, …). With default full debug | |
| # info that is 25-40 GB and the ubuntu runner dies with ENOSPC / the | |
| # linker with SIGBUS. Line tables keep file:line in panic backtraces | |
| # at a fraction of the size. | |
| CARGO_PROFILE_DEV_DEBUG: line-tables-only | |
| CARGO_PROFILE_TEST_DEBUG: line-tables-only | |
| # property_compression alone took 8012s serialized on ubuntu (87% of | |
| # the job) with proptest's default 256 cases — and pushed Windows past | |
| # the 180-min timeout. 64 cases keep the properties exercised on every | |
| # push at a quarter of the cost; local runs keep the full default. | |
| PROPTEST_CASES: 64 | |
| steps: | |
| - uses: actions/checkout@v4 # v4 | |
| with: | |
| persist-credentials: false | |
| - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable | |
| - name: Ensure toolchain is active | |
| shell: bash | |
| run: | | |
| CARGO_HOME="${CARGO_HOME:-$HOME/.cargo}" | |
| export PATH="$CARGO_HOME/bin:$PATH" | |
| rustup default stable | |
| cargo --version | |
| echo "$CARGO_HOME/bin" >> $GITHUB_PATH | |
| - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 | |
| with: | |
| save-if: ${{ github.ref == 'refs/heads/main' }} | |
| workspaces: rust -> target | |
| # ---- Windows: MSYS2 + GNU target + jemalloc ------------------------- | |
| # The default MSVC toolchain can't build jemalloc (autotools mismatch). | |
| # We switch to the GNU target with MSYS2/MinGW and pre-build jemalloc. | |
| # See .github/actions/windows-jemalloc for the gory details. | |
| - name: Setup MSYS2 and MinGW | |
| if: runner.os == 'Windows' | |
| uses: msys2/setup-msys2@e9898307ac31d1a803454791be09ab9973336e1c # v2 | |
| with: | |
| msystem: MINGW64 | |
| update: true | |
| install: base-devel mingw-w64-x86_64-toolchain | |
| path-type: inherit | |
| - name: Install Rust GNU target | |
| if: runner.os == 'Windows' | |
| uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable | |
| with: | |
| targets: x86_64-pc-windows-gnu | |
| - name: Build jemalloc + dummy liballoc.a (Windows only) | |
| if: runner.os == 'Windows' | |
| id: jemalloc | |
| uses: ./.github/actions/windows-jemalloc | |
| - name: Free disk space | |
| if: runner.os == 'Linux' | |
| shell: bash | |
| run: | | |
| sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc \ | |
| /opt/hostedtoolcache/CodeQL /usr/local/.ghcup /usr/share/swift \ | |
| /usr/local/share/powershell /usr/local/share/chromium \ | |
| /usr/local/share/boost /usr/lib/jvm /usr/local/julia* || true | |
| sudo docker system prune -af --volumes >/dev/null 2>&1 || true | |
| df -h / | |
| # mold is a much faster drop-in ELF linker. The ~50 integration-test | |
| # binaries each statically link the full lib (tree-sitter ×18, rten, | |
| # aws-lc, resvg, …), so linking — not compilation — dominates the Linux | |
| # job. (#1146 already merged 93 of the old ~145 test files into one | |
| # binary; the ~50 that remain still each pay the full-lib link.) mold is | |
| # Linux/ELF only, so macOS and the Windows-GNU target keep their default | |
| # linkers. | |
| - name: Install mold linker | |
| if: runner.os == 'Linux' | |
| shell: bash | |
| run: sudo apt-get update && sudo apt-get install -y mold | |
| - name: Run tests | |
| working-directory: rust | |
| shell: bash | |
| run: | | |
| if [[ "${{ runner.os }}" == "macOS" ]]; then | |
| CARGO="${CARGO_HOME}/bin/cargo" | |
| else | |
| CARGO="cargo" | |
| fi | |
| if [[ "${{ runner.os }}" == "Windows" ]]; then | |
| JEMALLOC_OVERRIDE="${{ steps.jemalloc.outputs.jemalloc-override }}" \ | |
| CARGO_TARGET_X86_64_PC_WINDOWS_GNU_RUSTFLAGS="-L ${{ steps.jemalloc.outputs.dummy-dir }}" \ | |
| "$CARGO" test --target x86_64-pc-windows-gnu --all-features | |
| elif [[ "${{ runner.os }}" == "Linux" ]]; then | |
| # `mold -run` redirects child linker invocations to mold without | |
| # touching RUSTFLAGS (which is pinned to `-Dwarnings` job-wide and | |
| # would override any .cargo/config.toml rustflags). | |
| mold -run "$CARGO" test --all-features | |
| else | |
| "$CARGO" test --all-features | |
| fi | |
| # Wall-clock gates: #581's parallel BM25 rebuild win, the context-kernel | |
| # micro-benchmarks and the performance-stress suite. Timing is only | |
| # meaningful on a quiet, single-threaded runner, so every one of them is | |
| # env-opt-in (no-op in the matrix above, which runs multi-threaded) and | |
| # they run serialized here instead. | |
| - name: Wall-clock regression gates (#581) | |
| if: runner.os == 'Linux' | |
| working-directory: rust | |
| shell: bash | |
| env: | |
| LEAN_CTX_PERF_GATE: "1" | |
| run: | | |
| cargo test --all-features --lib -- --test-threads=1 parallel_incremental_rebuild_perf_gate --nocapture | |
| cargo test --all-features --lib -- --test-threads=1 context_kernel::perf_benchmark | |
| cargo test --all-features --test main -- --test-threads=1 performance_stress | |
| clippy: | |
| name: Clippy | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 # v4 | |
| with: | |
| persist-credentials: false | |
| - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable | |
| with: | |
| components: clippy | |
| - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 | |
| with: | |
| save-if: ${{ github.ref == 'refs/heads/main' }} | |
| workspaces: rust -> target | |
| - name: Run clippy | |
| working-directory: rust | |
| run: cargo clippy --all-features -- -D warnings -A clippy::too_many_lines | |
| # Maintainability gate (#660): no Rust file grows past 1500 LOC | |
| # (legacy files are frozen at 2000 via the script's allowlist). | |
| - name: LOC gate (file size budget) | |
| run: ./scripts/loc-gate.sh | |
| # Guards the no-ORT build (#586). The `embeddings`/`neural` features pull the | |
| # `ort` crate, whose `load-dynamic` dylib resolver has no fallback arm for | |
| # non-tier-1 targets (FreeBSD, etc.), so community ports build with those | |
| # features off. This job catches any code that references `ort`/embeddings | |
| # without a feature gate. Dead code in the reduced build is expected, so | |
| # warnings are not denied here (override the workflow-wide -Dwarnings). | |
| build-minimal: | |
| name: Build (no embeddings / no ORT) | |
| runs-on: ubuntu-latest | |
| env: | |
| RUSTFLAGS: "" | |
| steps: | |
| - uses: actions/checkout@v4 # v4 | |
| with: | |
| persist-credentials: false | |
| - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable | |
| - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 | |
| with: | |
| save-if: ${{ github.ref == 'refs/heads/main' }} | |
| workspaces: rust -> target | |
| - name: Check build without ORT-backed features | |
| working-directory: rust | |
| run: cargo check --no-default-features --features "tree-sitter,http-server,secure-update,jemalloc" | |
| # The no-tree-sitter combo is the slim-binary candidate (#663) and had | |
| # rotted silently because only the no-ORT combo was checked. | |
| - name: Check build without tree-sitter | |
| working-directory: rust | |
| run: cargo check --no-default-features --features "embeddings,http-server,secure-update,jemalloc" | |
| fmt: | |
| name: Format | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@v4 # v4 | |
| with: | |
| persist-credentials: false | |
| - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable | |
| with: | |
| components: rustfmt | |
| - name: Check formatting | |
| working-directory: rust | |
| run: cargo fmt --check | |
| cookbook: | |
| name: Cookbook (Node) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 # v4 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: "20" | |
| cache: "npm" | |
| cache-dependency-path: cookbook/package-lock.json | |
| - name: Install | |
| working-directory: cookbook | |
| run: npm ci | |
| - name: Lint | |
| working-directory: cookbook | |
| run: npm run lint | |
| - name: Typecheck | |
| working-directory: cookbook | |
| run: npm run typecheck | |
| - name: Test | |
| working-directory: cookbook | |
| run: npm test | |
| - name: Build | |
| working-directory: cookbook | |
| run: npm run build | |
| pi-extension: | |
| name: Pi Extension (Node) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 # v4 | |
| with: | |
| persist-credentials: false | |
| # Fail fast if pi-lean-ctx / lean-ctx-bin drifted from the engine version, | |
| # so the Pi extension can never quietly fall behind (npm skips the publish | |
| # of a stale version). Runs on every push/PR — not just at release. | |
| - name: Check npm package ↔ engine version coupling | |
| run: python3 scripts/check-package-versions.py | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: "22" | |
| cache: "npm" | |
| cache-dependency-path: packages/pi-lean-ctx/package-lock.json | |
| - name: Install | |
| working-directory: packages/pi-lean-ctx | |
| run: npm ci | |
| - name: Typecheck | |
| working-directory: packages/pi-lean-ctx | |
| run: npm run typecheck | |
| - name: Test | |
| working-directory: packages/pi-lean-ctx | |
| run: npm test | |
| rust-sdk: | |
| name: Rust SDK (lean-ctx-client) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 # v4 | |
| with: | |
| persist-credentials: false | |
| - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable | |
| with: | |
| components: clippy, rustfmt | |
| - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 | |
| with: | |
| save-if: ${{ github.ref == 'refs/heads/main' }} | |
| workspaces: clients/rust/lean-ctx-client -> target | |
| - name: Format | |
| working-directory: clients/rust/lean-ctx-client | |
| run: cargo fmt --check | |
| - name: Clippy | |
| working-directory: clients/rust/lean-ctx-client | |
| run: cargo clippy --all-targets -- -D warnings | |
| - name: Test | |
| working-directory: clients/rust/lean-ctx-client | |
| run: cargo test | |
| - name: Docs | |
| working-directory: clients/rust/lean-ctx-client | |
| env: | |
| RUSTDOCFLAGS: -D warnings | |
| run: cargo doc --no-deps | |
| evidence-verifier: | |
| # This is deliberately separate from the engine workspace: it verifies the | |
| # published V1 archive and V2 customer-proof contracts without linking | |
| # LeanCTX, and must remain buildable as an auditor's offline binary. | |
| name: Evidence Verifier (standalone) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 # v4 | |
| with: | |
| persist-credentials: false | |
| - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable | |
| with: | |
| components: clippy, rustfmt | |
| - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 | |
| with: | |
| save-if: ${{ github.ref == 'refs/heads/main' }} | |
| workspaces: packages/leanctx-verify -> target | |
| - name: Format | |
| working-directory: packages/leanctx-verify | |
| run: cargo fmt --check | |
| - name: Test (V1 frozen boundary + V2 external trust) | |
| working-directory: packages/leanctx-verify | |
| run: cargo test | |
| - name: Clippy | |
| working-directory: packages/leanctx-verify | |
| run: cargo clippy --all-targets -- -D warnings | |
| - name: Release build | |
| working-directory: packages/leanctx-verify | |
| run: cargo build --release | |
| embed-sdk: | |
| # Track A — the in-process embedding SDK (rust/crates/lean-ctx-sdk). It is a | |
| # workspace member but excluded from `default-members`, so the engine's own | |
| # test/clippy/doc jobs never see it; this job is its dedicated gate | |
| # (pedantic clippy, doctests, the engine-integration tests, example build). | |
| name: Embed SDK (lean-ctx-sdk) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 # v4 | |
| with: | |
| persist-credentials: false | |
| - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable | |
| with: | |
| components: clippy, rustfmt | |
| - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 | |
| with: | |
| save-if: ${{ github.ref == 'refs/heads/main' }} | |
| workspaces: rust -> target | |
| - name: Format | |
| working-directory: rust | |
| run: cargo fmt -p lean-ctx-sdk --check | |
| - name: Clippy (pedantic, 0 warnings) | |
| working-directory: rust | |
| run: cargo clippy -p lean-ctx-sdk --all-targets -- -D warnings -A clippy::too_many_lines | |
| - name: Test (doctests + engine integration) | |
| working-directory: rust | |
| run: cargo test -p lean-ctx-sdk | |
| - name: Example builds | |
| working-directory: rust | |
| run: cargo build -p lean-ctx-sdk --example embed | |
| - name: Docs | |
| working-directory: rust | |
| env: | |
| RUSTDOCFLAGS: -D warnings | |
| run: cargo doc -p lean-ctx-sdk --no-deps | |
| python-sdk: | |
| name: Python SDK v1 (Preview) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 # v4 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-python@v5 # v5 | |
| with: | |
| python-version: "3.11" | |
| - name: Install | |
| working-directory: packages/python-lean-ctx | |
| run: pip install -e ".[test]" | |
| - name: Test | |
| working-directory: packages/python-lean-ctx | |
| run: python -m pytest -q | |
| hermes-plugin: | |
| # The Hermes context-engine plugin (integrations/hermes-lean-ctx) replaces | |
| # Hermes' built-in ContextCompressor with lean-ctx. Gate: pytest (ABC | |
| # conformance + tool_call/tool_result invariant + daemon-down graceful) and | |
| # a benchmark smoke that proves the harness runs offline (local fallback). | |
| name: Hermes Plugin (Python) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 # v4 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-python@v5 # v5 | |
| with: | |
| python-version: "3.11" | |
| - name: Install test deps | |
| # The plugin owns a historical compatibility shim; it is not a current | |
| # Python SDK integration. tiktoken makes the token count match the host | |
| # model exactly; without it the plugin uses its char-based fallback. | |
| run: pip install pytest tiktoken | |
| - name: Run plugin tests | |
| working-directory: integrations/hermes-lean-ctx | |
| run: python -m pytest tests/ -q | |
| - name: Benchmark smoke (offline, local fallback) | |
| working-directory: integrations/hermes-lean-ctx | |
| # Discard port → daemon unreachable → exercises deterministic local | |
| # compaction end-to-end. Competitors are auto-skipped when absent. | |
| run: python benchmarks/run.py --turns 40 --needles 6 --context-length 2000 --base-url http://127.0.0.1:9 --no-write | |
| sdk-conformance: | |
| # Canonical Python SDK v1 is the sole current SDK release surface. Archived | |
| # TypeScript/Go/Python prototypes are deliberately outside this gate. | |
| name: Python SDK v1 | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 45 | |
| steps: | |
| - uses: actions/checkout@v4 # v4 | |
| with: | |
| persist-credentials: false | |
| - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable | |
| - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 | |
| with: | |
| save-if: ${{ github.ref == 'refs/heads/main' }} | |
| workspaces: | | |
| rust -> target | |
| - uses: actions/setup-python@v5 # v5 | |
| with: | |
| python-version: "3.11" | |
| - name: Install Python SDK | |
| working-directory: packages/python-lean-ctx | |
| # langchain-core gives the adapter smoke a real framework round trip; | |
| # LlamaIndex/CrewAI skip cleanly (heavy dep trees, covered by the | |
| # framework-agnostic OpenAI adapter path). | |
| run: pip install -e ".[test]" langchain-core | |
| - name: Verify canonical Python SDK v1 | |
| run: ./scripts/sdk-conformance.sh | |
| coverage: | |
| name: Coverage | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 # v4 | |
| with: | |
| persist-credentials: false | |
| - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable | |
| with: | |
| # tarpaulin's LLVM engine (below) needs llvm-profdata/llvm-cov from | |
| # the rustup component — without it the engine silently falls back. | |
| components: llvm-tools-preview | |
| - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 | |
| with: | |
| save-if: ${{ github.ref == 'refs/heads/main' }} | |
| workspaces: rust -> target | |
| - name: Free disk space | |
| run: | | |
| sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL || true | |
| df -h / | |
| - uses: taiki-e/install-action@74e87cbfa15a59692b158178d8905a61bf6fca95 # v2 | |
| with: | |
| tool: cargo-tarpaulin | |
| - name: Generate coverage | |
| working-directory: rust | |
| env: | |
| CARGO_BUILD_JOBS: "2" | |
| # --engine llvm uses LLVM source-based coverage (instrument-coverage + | |
| # llvm-profdata) instead of tarpaulin's default ptrace engine. ptrace | |
| # is non-deterministically fragile on hosted runners: it corrupts the | |
| # tracee's registers under load, surfacing as a bogus | |
| # "memory allocation of 1407…bytes failed" abort followed by | |
| # "ECHILD: No child processes" — a flake unrelated to the code (the | |
| # Test matrix on the same SHA stays green). The LLVM engine never | |
| # ptraces, so the whole class of flake disappears. | |
| run: | | |
| cargo tarpaulin --engine llvm --out xml --skip-clean --all-features --lib --timeout 180 \ | |
| --exclude-files 'src/cli/*' --exclude-files 'src/main.rs' \ | |
| -- --test-threads=1 | |
| pkill -f 'target.*lean-ctx' 2>/dev/null || true | |
| - name: Upload to Codecov | |
| uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4 | |
| with: | |
| fail_ci_if_error: false | |
| deny: | |
| name: cargo-deny | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 # v4 | |
| with: | |
| persist-credentials: false | |
| - uses: EmbarkStudios/cargo-deny-action@5bb39ff5d5a0e94dc9e2dc94eced0c6129743a57 # v2 | |
| with: | |
| manifest-path: rust/Cargo.toml | |
| adversarial: | |
| name: Adversarial Safety | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 # v4 | |
| with: | |
| persist-credentials: false | |
| - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable | |
| - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 | |
| with: | |
| save-if: ${{ github.ref == 'refs/heads/main' }} | |
| workspaces: rust -> target | |
| - name: Run adversarial compression tests | |
| working-directory: rust | |
| run: cargo test --test main suite::adversarial_compression -- --nocapture | |
| bench: | |
| name: Benchmarks | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| persist-credentials: false | |
| - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable | |
| - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 | |
| with: | |
| save-if: ${{ github.ref == 'refs/heads/main' }} | |
| workspaces: rust -> target | |
| - name: Compile benchmarks | |
| working-directory: rust | |
| run: cargo bench --no-run | |
| - name: Generate reproducible scorecard | |
| working-directory: rust | |
| run: cargo run --quiet -- benchmark scorecard --json --output ../scorecard.json | |
| - name: Upload scorecard artifact | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| with: | |
| name: scorecard | |
| path: scorecard.json | |
| quality-gate: | |
| name: Output-Quality Gate (eval A/B) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| steps: | |
| - uses: actions/checkout@v4 # v4 | |
| with: | |
| persist-credentials: false | |
| - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable | |
| - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 | |
| with: | |
| save-if: ${{ github.ref == 'refs/heads/main' }} | |
| workspaces: rust -> target | |
| - name: Build lean-ctx | |
| working-directory: rust | |
| run: cargo build --quiet | |
| # Self-footprint gate (#578/#964): the fixed per-session cost lean-ctx | |
| # itself injects (advertised tool schemas + MCP instructions + wakeup) | |
| # must stay under budget. Runs in a scratch HOME + empty cwd so only OUR | |
| # footprint is measured — no repo rules files, no developer state. | |
| # Baseline: ~2790 tok (schemas ~2320, instructions ~470). | |
| # Raised 2700→2800 after #1017 added background-shell controls. | |
| # Raised 2800→3000 after #1020 encoded per-op required params in the | |
| # ctx_patch schema as if/then conditionals (~183 tok of discoverability on | |
| # the advertised surface); keep small headroom against further creep. | |
| # Raised 3000→3100 after wave of multi-agent merges pushed fixed cost to ~3008. | |
| - name: Self-footprint gate (doctor overhead) | |
| shell: bash | |
| env: | |
| LEAN_CTX_CONTEXT_BUDGET_TOKENS: "3130" | |
| run: | | |
| set -euo pipefail | |
| BIN="$GITHUB_WORKSPACE/rust/target/debug/lean-ctx" | |
| SCRATCH=$(mktemp -d) | |
| mkdir -p "$SCRATCH/home" "$SCRATCH/data" "$SCRATCH/work" | |
| cd "$SCRATCH/work" | |
| HOME="$SCRATCH/home" LEAN_CTX_DATA_DIR="$SCRATCH/data" "$BIN" doctor overhead --gate | |
| # Deterministic with/without output-quality proof. Three modes, in priority order: | |
| # 1. A committed recording (rust/eval/recording.json) → byte-stable replay, no secrets, | |
| # and the gate BLOCKS on a regression. | |
| # 2. Otherwise, if LEAN_CTX_EVAL_* secrets are set → capture from the live model + gate. | |
| # 3. Otherwise → skipped (the harness itself is still covered by the test job). | |
| # Capture a recording once with: lean-ctx eval ab --suite … --record rust/eval/recording.json | |
| - name: Run deterministic A/B output-quality gate | |
| working-directory: rust | |
| shell: bash | |
| env: | |
| LEAN_CTX_EVAL_MODEL_URL: ${{ secrets.LEAN_CTX_EVAL_MODEL_URL }} | |
| LEAN_CTX_EVAL_MODEL: ${{ secrets.LEAN_CTX_EVAL_MODEL }} | |
| LEAN_CTX_EVAL_MODEL_KEY: ${{ secrets.LEAN_CTX_EVAL_MODEL_KEY }} | |
| run: | | |
| set -euo pipefail | |
| BIN=./target/debug/lean-ctx | |
| REC=eval/recording.json | |
| "$BIN" eval init eval-suite | |
| if [ -f "$REC" ]; then | |
| echo "Replaying committed recording (deterministic, no secrets)…" | |
| "$BIN" eval ab --suite eval-suite/suite.ndjson --replay "$REC" --out ab-report.json --gate | |
| elif [ -n "${LEAN_CTX_EVAL_MODEL_URL:-}" ]; then | |
| echo "No committed recording; capturing from the configured live model…" | |
| mkdir -p eval | |
| "$BIN" eval ab --suite eval-suite/suite.ndjson --record "$REC" --out ab-report.json --gate | |
| else | |
| echo "::notice::Output-quality gate skipped — commit rust/eval/recording.json or set LEAN_CTX_EVAL_* secrets." | |
| fi | |
| - name: Verify artifact (signature + determinism digest) | |
| working-directory: rust | |
| shell: bash | |
| run: | | |
| if [ -f ab-report.json ]; then ./target/debug/lean-ctx eval verify ab-report.json; fi | |
| - name: Upload signed A/B report | |
| if: always() | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| with: | |
| name: eval-ab-report | |
| path: rust/ab-report.json | |
| if-no-files-found: ignore | |
| # Off-vs-on answer-quality testbench across pinned repos (#611). The committed | |
| # local-fixture subset replays with a recorded model → byte-stable, no secrets, | |
| # and BLOCKS on any per-repo regression. Refresh with: | |
| # lean-ctx eval testbench --lock eval/testbench/testbench.lock.json --record eval/testbench/recording.json | |
| - name: Run deterministic testbench gate (off vs on) | |
| working-directory: rust | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| BIN=./target/debug/lean-ctx | |
| LOCK=eval/testbench/testbench.lock.json | |
| REC=eval/testbench/recording.json | |
| if [ -f "$REC" ]; then | |
| echo "Replaying committed testbench subset (deterministic, no secrets)…" | |
| "$BIN" eval testbench --lock "$LOCK" --replay "$REC" --out testbench-out --gate | |
| else | |
| echo "::notice::Testbench gate skipped — commit rust/eval/testbench/recording.json." | |
| fi | |
| - name: Upload testbench FINDINGS | |
| if: always() | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| with: | |
| name: testbench-findings | |
| path: | | |
| rust/testbench-out/FINDINGS.md | |
| rust/testbench-out/regressions.json | |
| if-no-files-found: ignore | |
| doc: | |
| name: Documentation | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 # v4 | |
| with: | |
| persist-credentials: false | |
| - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable | |
| - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 | |
| with: | |
| save-if: ${{ github.ref == 'refs/heads/main' }} | |
| workspaces: rust -> target | |
| - name: Check docs compile | |
| working-directory: rust | |
| env: | |
| RUSTDOCFLAGS: -D warnings | |
| run: cargo doc --no-deps --all-features | |
| - name: Check generated reference docs are current | |
| working-directory: rust | |
| run: cargo run --example gen_docs --features dev-tools -- --check | |
| - name: Check bundled registry snapshots are canonical | |
| working-directory: rust | |
| run: cargo run --example gen_registry --features dev-tools -- --check | |
| - name: Check committed testbench recording is current | |
| working-directory: rust | |
| run: cargo run --example gen_testbench_recording --features dev-tools -- --check | |
| delivery-security-gate: | |
| # Keep promotion evidence and the audited public-tree delta on the regular | |
| # CI path, not only on release/security-specific workflows. | |
| name: Delivery + Security Verification | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@v4 # v4 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable | |
| - name: Install delivery test runner | |
| run: python3 -m pip install --disable-pip-version-check pytest | |
| - name: Verify OCLA contract-pack suite | |
| run: | | |
| cargo build --locked --manifest-path clients/rust/lean-ctx-client/Cargo.toml \ | |
| --bin lean-ctx-ocla-verify | |
| python3 scripts/verify-ocla-contract-suite.py \ | |
| --verifier clients/rust/lean-ctx-client/target/debug/lean-ctx-ocla-verify | |
| - name: Verify signed delivery manifest contract | |
| run: | | |
| python3 scripts/verify-delivery-manifest.py \ | |
| tests/delivery/valid/delivery-manifest.json \ | |
| --root . \ | |
| --trust-root tests/delivery/valid/release-trust-root.json | |
| - name: Verify delivery evidence contract | |
| run: | | |
| python3 scripts/verify-delivery-evidence.py \ | |
| docs/contracts/delivery-evidence-v1.json --root . | |
| - name: Rehearse hermetic rollback contract | |
| run: python3 tests/delivery/test_rehearse_delivery.py | |
| - name: Verify public protocol surface freeze | |
| run: python3 scripts/check-open-core-boundary.py | |
| - name: Run delivery, security, and OCLA contract tests | |
| run: python3 -m pytest -q tests/delivery tests/security tests/ocla_contract_suite | |
| - name: Verify audited public-tree delta | |
| run: >- | |
| python3 scripts/history-policy-gate.py gate --root . | |
| --policy security/history-policy-v1.json | |
| --output "$RUNNER_TEMP/history-delta-evidence.json" | |
| # Single aggregating gate. Point branch protection at THIS one job ("CI Green") | |
| # instead of every matrix leg — it stays correct when jobs are added/removed and | |
| # cannot be bypassed by a silently-missing required check. Runs always() so it can | |
| # inspect results even when a dependency failed; fails only on failure/cancelled | |
| # (a legitimately skipped job — e.g. the optional quality-gate — does not block). | |
| ci-green: | |
| name: CI Green | |
| if: always() | |
| needs: | |
| - test | |
| - clippy | |
| - build-minimal | |
| - fmt | |
| - cookbook | |
| - pi-extension | |
| - rust-sdk | |
| - embed-sdk | |
| - python-sdk | |
| - hermes-plugin | |
| - sdk-conformance | |
| - coverage | |
| - deny | |
| - adversarial | |
| - bench | |
| - quality-gate | |
| - doc | |
| - delivery-security-gate | |
| - narrative-governance | |
| - package-assets | |
| - evidence-verifier | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Verify no required job failed | |
| shell: bash | |
| env: | |
| NEEDS_JSON: ${{ toJSON(needs) }} | |
| run: | | |
| echo "$NEEDS_JSON" | |
| python3 - <<'PY' | |
| import json, os, sys | |
| needs = json.loads(os.environ["NEEDS_JSON"]) | |
| blocked = {k: v["result"] for k, v in needs.items() | |
| if v["result"] in ("failure", "cancelled")} | |
| if blocked: | |
| print("Required CI jobs did not pass:", blocked) | |
| sys.exit(1) | |
| print("All required CI jobs passed (success or skipped).") | |
| PY | |
| # Security audit runs in security-check.yml workflow |