26382a7ac6
CI / Test (macos-latest) (push) Waiting to run
CI / Test (windows-latest) (push) Waiting to run
CI / Build (no embeddings / no ORT) (push) Waiting to run
CI / Format (push) Waiting to run
CI / Cookbook (Node) (push) Waiting to run
CI / Pi Extension (Node) (push) Waiting to run
CI / Rust SDK (lean-ctx-client) (push) Waiting to run
CI / Embed SDK (lean-ctx-sdk) (push) Waiting to run
CI / Python SDK (leanctx) (push) Waiting to run
CI / Hermes Plugin (Python) (push) Waiting to run
CI / SDK Conformance Matrix (push) Waiting to run
CI / Coverage (push) Waiting to run
CI / cargo-deny (push) Waiting to run
CI / Adversarial Safety (push) Waiting to run
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
JetBrains Plugin / Actionlint (push) Waiting to run
CI / Benchmarks (push) Waiting to run
CI / Output-Quality Gate (eval A/B) (push) Waiting to run
CI / Documentation (push) Waiting to run
CI / CI Green (push) Blocked by required conditions
CodeQL / Analyze (actions) (push) Waiting to run
CodeQL / Analyze (rust) (push) Waiting to run
JetBrains Plugin / Validation (push) Waiting to run
JetBrains Plugin / Build (push) Waiting to run
JetBrains Plugin / Test (push) Blocked by required conditions
Security Check / Security Scan (push) Waiting to run
CI / Test (ubuntu-latest) (push) Has started running
CI / Clippy (push) Has started running
681 lines
27 KiB
YAML
681 lines
27 KiB
YAML
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
|
||
# 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:
|
||
test:
|
||
name: Test
|
||
strategy:
|
||
fail-fast: false
|
||
matrix:
|
||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||
runs-on: ${{ matrix.os }}
|
||
# --test-threads=1 (env-race legacy, v3.3.5) serializes ~70 binaries that
|
||
# now include heavy property/fuzz/benchmark suites (#291, #493, #551) —
|
||
# well beyond 90 min on hosted runners. Parallelization is tracked
|
||
# separately; until then the gate gets the time it actually needs.
|
||
#
|
||
# Tried switching to cargo-nextest (rust/.config/nextest.toml already
|
||
# exists and documents the same env-race for 3 known binaries via a
|
||
# `serial-state` test-group). Reverted: nextest's default parallelism
|
||
# (test-threads = -4) surfaced several more tests with hidden wall-clock
|
||
# timing assumptions (e.g. core::cache::tests::hebbian_eviction_bonus_is_wired
|
||
# depends on two calls landing in the same real-time 500ms window) that a
|
||
# `max-threads=1` test-group does NOT fix — that only serializes tests
|
||
# *within* the group against each other, it doesn't protect them from
|
||
# being scheduling-delayed by the many *other* tests still running in
|
||
# parallel elsewhere on the same runner. Revisit once those tests are
|
||
# made robust to scheduling jitter (or the burst window is made
|
||
# injectable for tests) rather than papering over it with test-groups.
|
||
timeout-minutes: 180
|
||
env:
|
||
# ~60 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:
|
||
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 /
|
||
- 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 -- --test-threads=1
|
||
else
|
||
"$CARGO" test --all-features -- --test-threads=1
|
||
fi
|
||
|
||
# #581: lock in the parallel BM25 (incremental) rebuild win against silent
|
||
# regression. Timing is only meaningful on a quiet, single-threaded runner,
|
||
# so the gate is env-opt-in (no-op in the matrix above) and runs once here.
|
||
- name: BM25 build-time regression gate (#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
|
||
|
||
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:
|
||
workspaces: rust -> target
|
||
- name: Run clippy
|
||
working-directory: rust
|
||
run: cargo clippy --all-features -- -D warnings
|
||
# 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:
|
||
workspaces: rust -> target
|
||
- name: Check build without ORT-backed features
|
||
working-directory: rust
|
||
run: cargo check --no-default-features --features "tree-sitter,http-server,team-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,team-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:
|
||
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
|
||
|
||
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:
|
||
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
|
||
- 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 (leanctx)
|
||
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: clients/python
|
||
run: pip install -e ".[dev]"
|
||
- name: Test
|
||
working-directory: clients/python
|
||
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
|
||
# Runtime dep (leanctx SDK) is wired onto sys.path by the plugin
|
||
# conftest from clients/python — no install needed. tiktoken makes the
|
||
# token count match the host model exactly; without it the plugin uses
|
||
# its char-based fallback (also tested).
|
||
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:
|
||
# GL #395: every first-party SDK must prove the full /v1 contract against
|
||
# a real engine build. Red when an SDK misses a route, mistypes a shape,
|
||
# or the engine drifts from the frozen surface.
|
||
name: SDK Conformance Matrix
|
||
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:
|
||
workspaces: |
|
||
rust -> target
|
||
clients/rust/lean-ctx-client -> target
|
||
- uses: actions/setup-node@v4
|
||
with:
|
||
node-version: "20"
|
||
cache: "npm"
|
||
cache-dependency-path: cookbook/package-lock.json
|
||
- uses: actions/setup-python@v5 # v5
|
||
with:
|
||
python-version: "3.11"
|
||
- name: Install Python SDK
|
||
working-directory: clients/python
|
||
# 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 ".[dev]" langchain-core
|
||
- name: Run conformance kit (3 SDKs vs real server)
|
||
run: ./scripts/sdk-conformance.sh
|
||
- name: Upload conformance matrix
|
||
if: always()
|
||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||
with:
|
||
name: sdk-conformance-matrix
|
||
path: docs/reference/sdk-conformance-matrix.md
|
||
if-no-files-found: ignore
|
||
|
||
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:
|
||
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:
|
||
workspaces: rust -> target
|
||
- name: Run adversarial compression tests
|
||
working-directory: rust
|
||
run: cargo test --test 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:
|
||
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:
|
||
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 after the diet: ~2147 tok (schemas 1685, instructions 462).
|
||
- name: Self-footprint gate (doctor overhead)
|
||
shell: bash
|
||
env:
|
||
LEAN_CTX_CONTEXT_BUDGET_TOKENS: "2600"
|
||
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:
|
||
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
|
||
|
||
# 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
|
||
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
|