chore: import upstream snapshot with attribution
CI / Clippy (push) Failing after 15m13s
CI / Test (ubuntu-latest) (push) Failing after 16m1s
CI / Test (macos-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Build (no embeddings / no ORT) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Cookbook (Node) (push) Has been cancelled
CI / Pi Extension (Node) (push) Has been cancelled
CI / Rust SDK (lean-ctx-client) (push) Has been cancelled
CI / Embed SDK (lean-ctx-sdk) (push) Has been cancelled
CI / Python SDK (leanctx) (push) Has been cancelled
CI / Hermes Plugin (Python) (push) Has been cancelled
CI / SDK Conformance Matrix (push) Has been cancelled
CI / Coverage (push) Has been cancelled
CI / cargo-deny (push) Has been cancelled
CI / Adversarial Safety (push) Has been cancelled
CI / Benchmarks (push) Has been cancelled
CI / Output-Quality Gate (eval A/B) (push) Has been cancelled
CI / Documentation (push) Has been cancelled
CI / CI Green (push) Has been cancelled
JetBrains Plugin / Actionlint (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (rust) (push) Has been cancelled
JetBrains Plugin / Validation (push) Has been cancelled
JetBrains Plugin / Build (push) Has been cancelled
JetBrains Plugin / Test (push) Has been cancelled
Security Check / Security Scan (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:35:30 +08:00
commit 26382a7ac6
2353 changed files with 629146 additions and 0 deletions
+44
View File
@@ -0,0 +1,44 @@
name: Addon Registry Freshness
# The curated addon registry pins every upstream package to an exact version
# (supply-chain safety), but pins go stale silently. This lightweight,
# non-blocking check resolves each pin against its registry (PyPI/npm/NuGet/
# crates.io) and reports drift. It is intentionally NOT part of the `CI Green`
# gate — an upstream release must never break our own build.
on:
schedule:
- cron: "0 6 * * 1" # Mondays 06:00 UTC — weekly drift alert
workflow_dispatch:
pull_request:
branches: [main]
# Only when the registry or the checker itself changes, so unrelated PRs
# are never nagged — but a pin bump is validated the moment it is proposed.
paths:
- rust/data/addon_registry.json
- scripts/check-addon-versions.py
- .github/workflows/addon-versions.yml
permissions:
contents: read
jobs:
check:
name: Check addon pins vs upstream
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: Resolve pinned addon versions against upstream registries
shell: bash
# Warn-only on PRs / manual runs (annotations, exit 0); the weekly
# scheduled run is `--strict` so drift turns the run red as a real alert.
run: |
if [ "${{ github.event_name }}" = "schedule" ]; then
python3 scripts/check-addon-versions.py --strict
else
python3 scripts/check-addon-versions.py
fi
+680
View File
@@ -0,0 +1,680 @@
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
+53
View File
@@ -0,0 +1,53 @@
name: CLA Assistant
on:
issue_comment:
types: [created]
pull_request_target:
types: [opened, closed, synchronize]
permissions:
actions: write
contents: write
pull-requests: write
statuses: write
jobs:
cla:
runs-on: ubuntu-latest
steps:
- name: CLA Assistant
# Only act on the sign/recreate comment phrases or on PR lifecycle events.
if: >-
(github.event.comment.body == 'recreate-cla' ||
github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') ||
github.event_name == 'pull_request_target'
# Pinned to the v2.6.1 commit SHA (supply-chain hardening; a moving tag
# could be repointed to malicious code). Bump deliberately when upgrading.
uses: contributor-assistant/github-action@ca4a40a7d1004f18d9960b404b97e5f30a505a08 # v2.6.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Required: a PAT (classic "repo" scope, or fine-grained with Contents +
# Pull requests write on this repo) stored as the CLA_SIGNATURES_TOKEN
# secret. It lets the action commit signatures across forked PRs.
PERSONAL_ACCESS_TOKEN: ${{ secrets.CLA_SIGNATURES_TOKEN }}
with:
path-to-document: 'https://github.com/yvgude/lean-ctx/blob/main/CLA.md'
# Signatures are stored on a dedicated branch to keep main history clean.
path-to-signatures: 'signatures/v1/cla.json'
branch: 'cla-signatures'
# Maintainer + bots never need to sign.
allowlist: 'yvgude,dependabot[bot],*[bot]'
custom-pr-sign-comment: 'I have read the CLA Document and I hereby sign the CLA'
# Friendlier, self-contained first message: explains the one-time nature,
# the §8 Local-Free guarantee, and the exact phrase — so contributors are
# not surprised by a bare red check and can sign in one reply.
custom-notsigned-prcomment: |
👋 Thanks for the contribution — we would love to merge it!
One **one-time** step remains: please sign our [CLA](https://github.com/yvgude/lean-ctx/blob/main/CLA.md). It takes ~5 seconds, you sign **once**, and every future PR is then accepted automatically. The CLA keeps lean-ctx **Apache-2.0 and free for individual developers** (guaranteed in §8) while still allowing a hosted/commercial plane.
**To sign:** reply to this PR with a comment containing exactly:
> I have read the CLA Document and I hereby sign the CLA
custom-allsigned-prcomment: 'All contributors have signed the CLA. ✅'
+65
View File
@@ -0,0 +1,65 @@
name: "CodeQL"
on:
push:
branches: [main]
pull_request:
branches: [main]
schedule:
- cron: "30 5 * * 1"
permissions:
security-events: write
contents: read
actions: read
jobs:
analyze:
name: Analyze (${{ matrix.language }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
language: [javascript-typescript, actions]
steps:
- uses: actions/checkout@v4 # v4
with:
persist-credentials: false
- name: Initialize CodeQL
uses: github/codeql-action/init@ce64ddcb0d8d890d2df4a9d1c04ff297367dea2a # v3
with:
languages: ${{ matrix.language }}
queries: security-extended
- name: Autobuild
uses: github/codeql-action/autobuild@ce64ddcb0d8d890d2df4a9d1c04ff297367dea2a # v3
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@ce64ddcb0d8d890d2df4a9d1c04ff297367dea2a # v3
with:
category: "/language:${{ matrix.language }}"
analyze-rust:
name: Analyze (rust)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4 # v4
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
- name: Initialize CodeQL
uses: github/codeql-action/init@ce64ddcb0d8d890d2df4a9d1c04ff297367dea2a # v3
with:
languages: rust
- name: Build Rust
working-directory: rust
run: cargo build --release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@ce64ddcb0d8d890d2df4a9d1c04ff297367dea2a # v3
with:
category: "/language:rust"
+127
View File
@@ -0,0 +1,127 @@
# Dependency Auto-Update — ongoing patch/minor maintenance of the Cargo dependencies.
#
# Complements the one-off, manual major upgrades
# (docs/superpowers/specs/2026-06-17-dependency-upgrades-plan-a/b-design.md):
# those catch up accumulated major drift; THIS workflow then keeps
# patch + compatible minor current. It NEVER touches `--incompatible`/major —
# breaking-change bumps stay deliberately manual.
#
# Trigger: manual only (workflow_dispatch) — no cron. The maintainer
# triggers the run deliberately (e.g. after a security advisory).
#
# Token / CI gate: PR + push run via
# `${{ secrets.DEP_UPDATE_TOKEN || github.token }}`.
# IMPORTANT: a PR created with the default GITHUB_TOKEN does NOT trigger
# further workflows — the full CI suite (ci.yml: 3-OS matrix, clippy, fmt,
# deny, ...) then does NOT run automatically. Maintainer opt-in for
# automatic gating: create a fine-grained PAT as the repo secret DEP_UPDATE_TOKEN
# (contents:write + pull-requests:write), analogous to HOMEBREW_GITHUB_TOKEN.
# Without a PAT, the smoke step below guards against grossly broken PRs.
name: Dependency Auto-Update
on:
workflow_dispatch:
permissions:
contents: read
jobs:
update:
name: Compatible patch/minor update
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
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
- uses: taiki-e/install-action@74e87cbfa15a59692b158178d8905a61bf6fca95 # v2
with:
tool: cargo-edit
- name: Run compatible updates
id: update
working-directory: rust
shell: bash
run: |
set -euo pipefail
cargo upgrade --compatible
cargo update
if git diff --quiet -- Cargo.toml Cargo.lock; then
echo "changed=false" >> "$GITHUB_OUTPUT"
echo "::notice::No compatible updates available — nothing to do."
else
echo "changed=true" >> "$GITHUB_OUTPUT"
fi
- name: Smoke verify (default features)
if: steps.update.outputs.changed == 'true'
working-directory: rust
shell: bash
# relink other gh actions?
run: |
set -euo pipefail
cargo build
cargo test
cargo clippy -- -D warnings
# or set to workflow_run?
- name: Create or update pull request
if: steps.update.outputs.changed == 'true'
shell: bash
env:
GH_TOKEN: ${{ secrets.DEP_UPDATE_TOKEN || github.token }}
REPO: ${{ github.repository }}
run: |
set -euo pipefail
BRANCH="deps/auto-update"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
BODY_FILE="$(mktemp)"
{
echo "Automated **compatible** (patch/minor) dependency update."
echo
echo "Generated by \`.github/workflows/dep-update.yml\` via"
echo "\`cargo upgrade --compatible\` + \`cargo update\`."
echo "Contains **no** incompatible/major bumps — those stay manual"
echo "(see the dependency-upgrades-plan-a/b specs)."
echo
echo '<details><summary>Cargo.lock changes</summary>'
echo
echo '```diff'
git diff -- rust/Cargo.lock | head -n 300
echo '```'
echo '</details>'
} > "$BODY_FILE"
git switch -C "$BRANCH"
git add rust/Cargo.toml rust/Cargo.lock
git commit -m "chore(deps): compatible patch/minor update"
git push --force \
"https://x-access-token:${GH_TOKEN}@github.com/${REPO}.git" \
"HEAD:${BRANCH}"
if gh pr view "$BRANCH" --json number >/dev/null 2>&1; then
gh pr edit "$BRANCH" --body-file "$BODY_FILE"
echo "::notice::Updated existing PR for $BRANCH."
else
gh pr create \
--base main \
--head "$BRANCH" \
--title "chore(deps): compatible patch/minor update" \
--body-file "$BODY_FILE"
fi
gh pr edit "$BRANCH" --add-label dependencies \
|| echo "::notice::Label 'dependencies' missing — skipped. Create it once to enable."
+256
View File
@@ -0,0 +1,256 @@
# Grammar-addon CI matrix (#690, Phase 1c).
#
# Builds each `crates/grammar-addons/<name>` cdylib for every supported
# platform, uploads the dylibs to the rolling `grammar-addons` GitHub
# Release, and regenerates `rust/data/grammar_registry.json` with the
# resulting download URLs + SHA-256 pins via a PR (same bot-commit +
# `gh pr create` pattern as dep-update.yml).
#
# The bundled registry is trusted by construction (it is compiled into the
# lean-ctx binary itself, same as data/addon_registry.json — see
# core/addons/signing.rs's doc comment) — no separate Ed25519 signing step
# is needed here; the release binary's own provenance is the trust anchor.
#
# Trigger: manual only, like dep-update.yml — publishing a new grammar
# dylib is a deliberate, reviewed action, not a per-commit side effect.
#
# Adding a new grammar: add `crates/grammar-addons/<name>` (mirror `lua/`,
# package name `grammar-<name>`) and a workspace member entry in
# rust/Cargo.toml — this workflow discovers it automatically, no edits here.
name: Grammar Addons
on:
workflow_dispatch:
inputs:
grammar:
description: 'Grammar to build (slug under crates/grammar-addons/), or "all"'
default: 'all'
required: false
permissions:
contents: read
jobs:
discover:
name: Discover grammar-addon crates
runs-on: ubuntu-latest
outputs:
grammars: ${{ steps.list.outputs.grammars }}
steps:
- uses: actions/checkout@v4 # v4
with:
persist-credentials: false
- name: List crates/grammar-addons/*
id: list
shell: bash
run: |
set -euo pipefail
cd rust/crates/grammar-addons
if [[ "${{ inputs.grammar }}" == "all" || -z "${{ inputs.grammar }}" ]]; then
NAMES=$(find . -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | sort)
else
NAMES="${{ inputs.grammar }}"
fi
JSON=$(printf '%s\n' "$NAMES" | jq -R . | jq -sc .)
echo "grammars=${JSON}" >> "$GITHUB_OUTPUT"
build:
name: Build ${{ matrix.grammar }} (${{ matrix.target }})
needs: discover
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
grammar: ${{ fromJson(needs.discover.outputs.grammars) }}
include:
- target: x86_64-unknown-linux-gnu
os: ubuntu-22.04
ext: so
- target: aarch64-unknown-linux-gnu
os: ubuntu-22.04
ext: so
cross: true
- target: x86_64-apple-darwin
os: macos-latest
ext: dylib
- target: aarch64-apple-darwin
os: macos-latest
ext: dylib
- target: x86_64-pc-windows-msvc
os: windows-latest
ext: dll
steps:
- uses: actions/checkout@v4 # v4
with:
persist-credentials: false
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
with:
targets: ${{ matrix.target }}
- name: Install cross-compilation tools
if: matrix.cross == true
run: |
sudo apt-get update
sudo apt-get install -y gcc-aarch64-linux-gnu
echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc" >> $GITHUB_ENV
- name: Build
working-directory: rust
shell: bash
run: cargo build --release --locked -p "grammar-${{ matrix.grammar }}" --target ${{ matrix.target }}
- name: Rename + hash
id: asset
working-directory: rust
shell: bash
run: |
set -euo pipefail
crate_underscored="grammar_$(echo '${{ matrix.grammar }}' | tr '-' '_')"
src="target/${{ matrix.target }}/release/${crate_underscored}.${{ matrix.ext }}"
name="${{ matrix.grammar }}-${{ matrix.target }}.${{ matrix.ext }}"
cp "$src" "$name"
sha256sum "$name" > "${name}.sha256"
echo "name=${name}" >> "$GITHUB_OUTPUT"
# ABI version is a property of the grammar dylib itself (which
# tree-sitter core/tree-sitter-language version it was built against),
# not of the target platform — so it only needs deriving once per
# grammar. Native (non-cross) x86_64-linux is the one leg guaranteed
# to be able to run the dylib it just built, generically for any
# grammar via grammar-dlopen-host's --abi-only mode.
- name: Derive abi_version (native leg only)
if: matrix.target == 'x86_64-unknown-linux-gnu'
id: abi
working-directory: rust
shell: bash
run: |
set -euo pipefail
cargo build --release --locked -p grammar-dlopen-host --target ${{ matrix.target }}
out="$(./target/${{ matrix.target }}/release/grammar-dlopen-host \
"${{ steps.asset.outputs.name }}" --abi-only)"
echo "$out" > "${{ matrix.grammar }}.abi"
echo "${out#abi_version=}"
- name: Upload artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: ${{ steps.asset.outputs.name }}
path: |
rust/${{ steps.asset.outputs.name }}
rust/${{ steps.asset.outputs.name }}.sha256
rust/${{ matrix.grammar }}.abi
publish:
name: Publish release assets + registry PR
needs: build
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v4 # v4
with:
persist-credentials: false
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
path: artifacts
merge-multiple: true
- name: Upload dylibs to the rolling grammar-addons release
env:
GH_TOKEN: ${{ secrets.DEP_UPDATE_TOKEN || github.token }}
run: |
set -euo pipefail
if ! gh release view grammar-addons --repo "${{ github.repository }}" >/dev/null 2>&1; then
gh release create grammar-addons --repo "${{ github.repository }}" \
--title "Grammar addons" \
--notes "Rolling release: per-platform grammar-addon dylibs referenced by rust/data/grammar_registry.json. Not a lean-ctx version release." \
--prerelease
fi
shopt -s nullglob
for f in artifacts/*.dll artifacts/*.dylib artifacts/*.so; do
gh release upload grammar-addons "$f" --repo "${{ github.repository }}" --clobber
done
- name: Regenerate rust/data/grammar_registry.json
id: regen
shell: bash
run: |
set -euo pipefail
RELEASE_URL="https://github.com/${{ github.repository }}/releases/download/grammar-addons"
for dir in rust/crates/grammar-addons/*/; do
grammar="$(basename "$dir")"
crate="grammar-${grammar}"
version="$(grep -m1 '^version' "$dir/Cargo.toml" | sed -E 's/.*"([^"]+)".*/\1/')"
license="$(grep -m1 '^license' "$dir/Cargo.toml" | sed -E 's/.*"([^"]+)".*/\1/')"
# Derived on the native x86_64-linux build leg (see "Derive
# abi_version" step) — not a platform-specific value, so any one
# leg's reading is authoritative for every asset below.
abi_file="artifacts/${grammar}.abi"
if [ ! -f "$abi_file" ]; then
echo "::error::no ${abi_file} — did the x86_64-unknown-linux-gnu leg run for '${grammar}'?" >&2
exit 1
fi
abi_version="$(sed -E 's/^abi_version=//' "$abi_file")"
assets="{}"
for shafile in artifacts/"${grammar}"-*.sha256; do
[ -e "$shafile" ] || continue
filename="$(basename "${shafile%.sha256}")"
sha="$(awk '{print $1}' "$shafile")"
target="${filename#"${grammar}"-}"
target="${target%.*}"
assets=$(jq --arg t "$target" --arg f "$filename" --arg u "${RELEASE_URL}/${filename}" --arg s "$sha" \
'.[$t] = {filename: $f, url: $u, sha256: $s}' <<<"$assets")
done
jq -n --arg name "$grammar" --arg version "$version" --arg license "$license" \
--argjson abi_version "$abi_version" --argjson assets "$assets" \
'{name: $name, display_name: ($name | ascii_upcase[0:1] + $name[1:]), version: $version, license: $license, extensions: [$name], abi_version: $abi_version, assets: $assets}' \
> "/tmp/${grammar}.json"
done
jq -s '{"$schema": "https://leanctx.dev/schema/grammar-registry-v1.json", registry_version: 1, grammars: .}' \
/tmp/*.json > rust/data/grammar_registry.json.new
mv rust/data/grammar_registry.json.new rust/data/grammar_registry.json
if git diff --quiet -- rust/data/grammar_registry.json; then
echo "changed=false" >> "$GITHUB_OUTPUT"
else
echo "changed=true" >> "$GITHUB_OUTPUT"
fi
- name: Create or update pull request
if: steps.regen.outputs.changed == 'true'
shell: bash
env:
GH_TOKEN: ${{ secrets.DEP_UPDATE_TOKEN || github.token }}
REPO: ${{ github.repository }}
run: |
set -euo pipefail
BRANCH="grammar-addons/registry-update"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git switch -C "$BRANCH"
git add rust/data/grammar_registry.json
git commit -m "chore(grammar-addons): regenerate grammar_registry.json"
git push --force \
"https://x-access-token:${GH_TOKEN}@github.com/${REPO}.git" \
"HEAD:${BRANCH}"
BODY="Regenerated by .github/workflows/grammar-addons.yml after building/publishing the grammar-addon dylibs. Review the diff before merging — this changes what lean-ctx's next release will fetch and dlopen."
if gh pr view "$BRANCH" --json number >/dev/null 2>&1; then
gh pr edit "$BRANCH" --body "$BODY"
else
gh pr create --base main --head "$BRANCH" \
--title "chore(grammar-addons): regenerate grammar_registry.json" \
--body "$BODY"
fi
+47
View File
@@ -0,0 +1,47 @@
# `/reopen` command (GH #388): GitHub only lets issue authors reopen issues
# they closed themselves — once a maintainer closes, the author is locked out
# and ends up filing duplicates. This workflow restores that ability: the
# original author comments `/reopen` and the issue reopens automatically.
#
# Matching is deliberately relaxed (`contains`, not `startsWith`): people
# write "Please /reopen" or bury the command mid-comment (GH #388 feedback).
# False positives are bounded — only the original author on their own closed
# issue can trigger it, and the worst case is an issue reopening.
#
# Issues closed as "not planned" are excluded on purpose: that close state is
# a maintainer decision, so the command answers with a hint instead.
#
# Security: no checkout, comment body is only used in the `if` expression
# (never interpolated into shell), token scope is issues:write only.
name: Issue reopen command
on:
issue_comment:
types: [created]
permissions:
issues: write
jobs:
reopen:
if: >-
!github.event.issue.pull_request &&
github.event.issue.state == 'closed' &&
github.event.comment.user.login == github.event.issue.user.login &&
contains(github.event.comment.body, '/reopen')
runs-on: ubuntu-latest
steps:
- name: Reopen issue (or explain why not)
env:
GH_TOKEN: ${{ github.token }}
ISSUE: ${{ github.event.issue.number }}
run: |
set -euo pipefail
reason=$(gh api "repos/$GITHUB_REPOSITORY/issues/$ISSUE" --jq '.state_reason // "completed"')
if [ "$reason" = "not_planned" ]; then
gh issue comment "$ISSUE" --repo "$GITHUB_REPOSITORY" --body \
"This issue was closed as **not planned**, which is a maintainer decision, so \`/reopen\` does not apply here. A maintainer will review this request."
else
gh issue reopen "$ISSUE" --repo "$GITHUB_REPOSITORY" \
--comment "Reopened at the author's request via \`/reopen\`."
fi
+107
View File
@@ -0,0 +1,107 @@
name: JetBrains Plugin
on:
push:
branches: [ main ]
paths:
- 'packages/jetbrains-lean-ctx/**'
- '.github/workflows/jetbrains-plugin.yml'
pull_request:
branches: [ main ]
paths:
- 'packages/jetbrains-lean-ctx/**'
- '.github/workflows/jetbrains-plugin.yml'
# Release packaging lives in release.yml (the `jetbrains-plugin` job attaches the
# plugin .zip to each GitHub Release). This workflow is CI-only: validate + build +
# test on plugin source changes.
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
defaults:
run:
working-directory: packages/jetbrains-lean-ctx
jobs:
actionlint:
name: Actionlint
runs-on: ubuntu-latest
timeout-minutes: 5
defaults:
run:
working-directory: .
steps:
- name: Fetch Sources
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Run actionlint
run: |
bash <(curl -sSf https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash)
./actionlint -color .github/workflows/jetbrains-plugin.yml
validation:
name: Validation
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v6
- uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # v6
build:
name: Build
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Fetch Sources
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Setup Java
uses: actions/setup-java@v5
with:
distribution: zulu
java-version: 21
- name: Setup Gradle
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6
- name: Build plugin
run: ./gradlew buildPlugin --console=plain
test:
name: Test
needs: [ build ]
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Fetch Sources
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Setup Java
uses: actions/setup-java@v5
with:
distribution: zulu
java-version: 21
- name: Setup Gradle
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6
with:
gradle-home-cache-cleanup: true
- name: Run Tests
run: ./gradlew check --console=plain
- name: Collect Tests Result
if: ${{ failure() }}
uses: actions/upload-artifact@v7
with:
name: jetbrains-tests-result
path: packages/jetbrains-lean-ctx/build/reports/tests
+101
View File
@@ -0,0 +1,101 @@
name: Publish /v1 clients
# Publishes the thin /v1 contract clients under one consistent name `lean-ctx-client`:
# - npm -> cookbook/sdk (TypeScript)
# - PyPI -> clients/python (Python; import module stays `leanctx`)
# - crates -> clients/rust/... (Rust)
# Versioned independently of the engine, so NOT part of release.yml. Trigger manually
# or push a `client-v*` tag. Every step is idempotent: an already-published version is a no-op.
on:
workflow_dispatch:
inputs:
targets:
description: "Which registries to publish to"
type: choice
default: npm
options: [all, npm, pypi, crates]
push:
tags:
- 'client-v[0-9]*'
permissions:
contents: read
jobs:
publish-npm:
name: Publish lean-ctx-client to npm
if: ${{ github.event_name == 'push' || inputs.targets == 'all' || inputs.targets == 'npm' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4 # v4
with:
persist-credentials: false
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 22
registry-url: https://registry.npmjs.org
- name: Build, test, publish
working-directory: cookbook
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
set -euo pipefail
npm ci
npm --workspace lean-ctx-client run build
npm --workspace lean-ctx-client test
PKG_VERSION="$(node -p "require('./sdk/package.json').version")"
if npm view "lean-ctx-client@${PKG_VERSION}" version >/dev/null 2>&1; then
echo "npm already has lean-ctx-client@${PKG_VERSION} — skipping publish."
exit 0
fi
npm publish --workspace lean-ctx-client --access public
publish-pypi:
name: Publish lean-ctx-client to PyPI
if: ${{ github.event_name == 'push' || inputs.targets == 'all' || inputs.targets == 'pypi' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4 # v4
with:
persist-credentials: false
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Build and upload (skip if no token / already present)
working-directory: clients/python
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
run: |
set -euo pipefail
if [ -z "${TWINE_PASSWORD}" ]; then
echo "PYPI_TOKEN secret not set — skipping PyPI publish."
exit 0
fi
python -m pip install --quiet build twine
python -m build
python -m twine upload --skip-existing dist/*
publish-crates:
name: Publish lean-ctx-client to crates.io
if: ${{ github.event_name == 'push' || inputs.targets == 'all' || inputs.targets == 'crates' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4 # v4
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
- name: Publish (idempotent)
working-directory: clients/rust/lean-ctx-client
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: |
set -euo pipefail
CRATE_VERSION="$(grep -m1 '^version' Cargo.toml | sed -E 's/.*"([^"]+)".*/\1/')"
if curl -fsSL -H "User-Agent: lean-ctx-release" \
"https://crates.io/api/v1/crates/lean-ctx-client/${CRATE_VERSION}" >/dev/null 2>&1; then
echo "crates.io already has lean-ctx-client ${CRATE_VERSION} — skipping publish."
exit 0
fi
cargo publish --token "${CARGO_REGISTRY_TOKEN}"
+76
View File
@@ -0,0 +1,76 @@
name: Publish SDK
# Publishes the drop-in compress() SDKs (packages/node-lean-ctx -> npm `lean-ctx-sdk`,
# packages/python-lean-ctx -> PyPI `lean-ctx-sdk`). These versions independently of the
# engine, so they are NOT part of release.yml (engine `v*` tags). Trigger manually, or
# push an `sdk-v*` tag. Every step is idempotent: an already-published version is a no-op.
on:
workflow_dispatch:
inputs:
targets:
description: "Which registries to publish to"
type: choice
default: both
options: [both, npm, pypi]
push:
tags:
- 'sdk-v[0-9]*'
permissions:
contents: read
jobs:
publish-npm:
name: Publish lean-ctx-sdk to npm
if: ${{ github.event_name == 'push' || inputs.targets == 'both' || inputs.targets == 'npm' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4 # v4
with:
persist-credentials: false
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 22
registry-url: https://registry.npmjs.org
- name: Build, test, publish
working-directory: packages/node-lean-ctx
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
set -euo pipefail
npm ci
npm run build
npm test
PKG_VERSION="$(node -p "require('./package.json').version")"
if npm view "lean-ctx-sdk@${PKG_VERSION}" version >/dev/null 2>&1; then
echo "npm already has lean-ctx-sdk@${PKG_VERSION} — skipping publish."
exit 0
fi
npm publish --access public
publish-pypi:
name: Publish lean-ctx-sdk to PyPI
if: ${{ github.event_name == 'push' || inputs.targets == 'both' || inputs.targets == 'pypi' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4 # v4
with:
persist-credentials: false
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Build and upload (skip if no token / already present)
working-directory: packages/python-lean-ctx
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
run: |
set -euo pipefail
if [ -z "${TWINE_PASSWORD}" ]; then
echo "PYPI_TOKEN secret not set — skipping PyPI publish."
exit 0
fi
python -m pip install --quiet build twine
python -m build
python -m twine upload --skip-existing dist/*
+93
View File
@@ -0,0 +1,93 @@
name: Publish VS Code Extension
# Decoupled from the binary `Release` workflow so the editor extension ships on
# its own cadence. Trigger either by pushing a `vscode-v*` tag (e.g. vscode-v0.1.0)
# or manually via the Actions tab with an explicit version.
on:
push:
tags:
- 'vscode-v*'
workflow_dispatch:
inputs:
version:
description: 'Extension version to publish (e.g. 0.1.0). Defaults to package.json.'
required: false
type: string
permissions:
contents: write
jobs:
publish-vscode:
name: Package & publish (Marketplace + Open VSX)
runs-on: ubuntu-latest
defaults:
run:
working-directory: vscode-extension
env:
# Step-level `if` guards read these so the job still packages + uploads the
# .vsix even before the publishing tokens are configured as repo secrets.
VSCE_PAT: ${{ secrets.VSCE_PAT }}
OVSX_PAT: ${{ secrets.OVSX_PAT }}
steps:
- uses: actions/checkout@v4 # v4
with:
persist-credentials: false
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 22
- name: Install dependencies
run: npm install
- name: Resolve version
id: ver
run: |
if [ -n "${{ inputs.version }}" ]; then
VERSION="${{ inputs.version }}"
elif [[ "${GITHUB_REF_NAME}" == vscode-v* ]]; then
VERSION="${GITHUB_REF_NAME#vscode-v}"
else
VERSION="$(node -p "require('./package.json').version")"
fi
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
npm version "${VERSION}" --no-git-tag-version --allow-same-version
- name: Compile
run: npm run compile
- name: Package .vsix
run: npx @vscode/vsce package --no-dependencies -o "${GITHUB_WORKSPACE}/lean-ctx-${{ steps.ver.outputs.version }}.vsix"
- name: Publish to VS Code Marketplace
if: ${{ env.VSCE_PAT != '' }}
run: npx @vscode/vsce publish --packagePath "${GITHUB_WORKSPACE}/lean-ctx-${{ steps.ver.outputs.version }}.vsix" -p "${VSCE_PAT}"
- name: Publish to Open VSX (Cursor / VSCodium / Windsurf)
if: ${{ env.OVSX_PAT != '' }}
run: npx ovsx publish "${GITHUB_WORKSPACE}/lean-ctx-${{ steps.ver.outputs.version }}.vsix" -p "${OVSX_PAT}"
- name: Upload .vsix artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: lean-ctx-vsix
path: lean-ctx-*.vsix
- name: Attach .vsix to GitHub release
if: ${{ startsWith(github.ref, 'refs/tags/vscode-v') }}
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2
with:
tag_name: ${{ github.ref_name }}
# Never let the extension release become the repo's "latest" — install.sh
# and the npm postinstall resolve binaries via /releases/latest, so a
# vscode-v* release hijacking "latest" breaks CLI installs (see #345).
make_latest: false
name: lean-ctx VS Code extension ${{ steps.ver.outputs.version }}
files: lean-ctx-*.vsix
body: |
lean-ctx VS Code extension **${{ steps.ver.outputs.version }}**.
Install from the [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=LeanCTX.lean-ctx)
or [Open VSX](https://open-vsx.org/extension/LeanCTX/lean-ctx) (Cursor, VSCodium, Windsurf),
or download the `.vsix` below and run `code --install-extension lean-ctx-${{ steps.ver.outputs.version }}.vsix`.
+484
View File
@@ -0,0 +1,484 @@
name: Release
on:
push:
tags:
# Only true semver release tags (v1.2.3). The digit class deliberately
# excludes the editor-extension tags (`vscode-v*`), which have their own
# workflow (publish-vscode.yml) and must NOT trigger a binary release.
- 'v[0-9]*'
permissions:
contents: write
jobs:
sdk-gate:
# GL #395: an engine release must never ship while a first-party SDK
# cannot speak its http_mcp contract version. Minor-version drift across
# the SDK family surfaces as a warning annotation.
#
# Also gates the engine-coupled npm wrappers (pi-lean-ctx, lean-ctx-bin):
# a stale package.json would let the publish step silently skip, shipping a
# release whose Pi extension / npx wrapper lags the engine.
name: Release gates (SDK + package versions)
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4 # v4
with:
persist-credentials: false
- name: Check SDK ↔ engine coupling
run: python3 scripts/check-sdk-versions.py
- name: Check npm package ↔ engine version coupling
run: python3 scripts/check-package-versions.py
build:
name: Build ${{ matrix.artifact || matrix.target }}
needs: sdk-gate
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-unknown-linux-gnu
os: ubuntu-22.04
- target: x86_64-unknown-linux-gnu
artifact: x86_64-unknown-linux-gnu-cuda
os: ubuntu-22.04
cargo_features: ort-cuda
- target: aarch64-unknown-linux-gnu
os: ubuntu-22.04
cross: true
- target: x86_64-unknown-linux-musl
os: ubuntu-22.04
musl: true
- target: aarch64-unknown-linux-musl
os: ubuntu-22.04
musl: true
cross: true
zig: true
- target: x86_64-apple-darwin
os: macos-latest
- target: aarch64-apple-darwin
os: macos-latest
- target: x86_64-pc-windows-msvc
os: windows-latest
- target: x86_64-pc-windows-gnu
os: windows-latest
jemalloc: true
steps:
- uses: actions/checkout@v4 # v4
with:
persist-credentials: false
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
with:
targets: ${{ matrix.target }}
- name: Install musl tools
if: matrix.musl == true && matrix.zig != true
run: |
sudo apt-get update
sudo apt-get install -y musl-tools
if [[ "${{ matrix.target }}" == aarch64-* ]]; then
TOOLCHAIN_ROOT="${RUNNER_TEMP}/aarch64-linux-musl-cross"
curl -sL "https://musl.cc/aarch64-linux-musl-cross.tgz" | tar xz -C "${RUNNER_TEMP}"
echo "${TOOLCHAIN_ROOT}/bin" >> $GITHUB_PATH
echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER=aarch64-linux-musl-gcc" >> $GITHUB_ENV
echo "CC_aarch64_unknown_linux_musl=aarch64-linux-musl-gcc" >> $GITHUB_ENV
echo "AR_aarch64_unknown_linux_musl=aarch64-linux-musl-ar" >> $GITHUB_ENV
echo "RANLIB_aarch64_unknown_linux_musl=aarch64-linux-musl-ranlib" >> $GITHUB_ENV
fi
- name: Install cross-compilation tools
if: matrix.cross == true && matrix.musl != true
run: |
sudo apt-get update
sudo apt-get install -y gcc-aarch64-linux-gnu
echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc" >> $GITHUB_ENV
# ---- Windows jemalloc target: MSYS2 + pre-built jemalloc ------------
# See .github/actions/windows-jemalloc for the pitfall details.
- name: Setup MSYS2 and MinGW
if: matrix.jemalloc == true
uses: msys2/setup-msys2@e9898307ac31d1a803454791be09ab9973336e1c # v2
with:
msystem: MINGW64
update: true
install: base-devel mingw-w64-x86_64-toolchain
path-type: inherit
- name: Build jemalloc (Windows GNU target)
if: matrix.jemalloc == true
id: jemalloc
uses: ./.github/actions/windows-jemalloc
- name: Run tests
if: matrix.cross != true
working-directory: rust
shell: bash
run: |
if [[ "${{ matrix.jemalloc }}" == "true" ]]; then
JEMALLOC_OVERRIDE="${{ steps.jemalloc.outputs.jemalloc-override }}" \
CARGO_TARGET_X86_64_PC_WINDOWS_GNU_RUSTFLAGS="-L ${{ steps.jemalloc.outputs.dummy-dir }}" \
cargo test --lib --target x86_64-pc-windows-gnu --all-features --locked -- --test-threads=1
else
cargo test --lib --all-features --locked -- --test-threads=1
fi
- name: Setup Zig
if: matrix.zig == true
run: |
pip3 install -q ziglang==0.13.0
ZIG_BIN="$(python3 -c 'import ziglang, os; print(os.path.dirname(ziglang.__file__))')"
echo "${ZIG_BIN}" >> $GITHUB_PATH
"${ZIG_BIN}/zig" version
- name: Install cargo-zigbuild
if: matrix.zig == true
uses: taiki-e/install-action@74e87cbfa15a59692b158178d8905a61bf6fca95 # v2
with:
tool: cargo-zigbuild
# Ship the production feature set (Cargo default features incl. secure-update),
# NOT --all-features — the latter would pull dev-only tooling (dev-tools:
# gen_docs/gen_mcp_manifest) into the released binary. Default features are
# already exercised by the CI test job (which runs the --all-features superset).
# --locked pins Cargo.lock so releases are reproducible (no silent dep drift).
- name: Build
working-directory: rust
shell: bash
run: |
features="${{ matrix.cargo_features }}"
feature_args=()
if [[ -n "$features" ]]; then
feature_args+=(--features "$features")
fi
if [[ "${{ matrix.zig }}" == "true" ]]; then
cargo zigbuild --release --locked --target ${{ matrix.target }} "${feature_args[@]}"
elif [[ "${{ matrix.jemalloc }}" == "true" ]]; then
JEMALLOC_OVERRIDE="${{ steps.jemalloc.outputs.jemalloc-override }}" \
CARGO_TARGET_X86_64_PC_WINDOWS_GNU_RUSTFLAGS="-L ${{ steps.jemalloc.outputs.dummy-dir }}" \
cargo build --release --locked --target ${{ matrix.target }} "${feature_args[@]}"
else
cargo build --release --locked --target ${{ matrix.target }} "${feature_args[@]}"
fi
- name: Package (Unix)
if: runner.os != 'Windows'
shell: bash
run: |
artifact="${{ matrix.artifact || matrix.target }}"
cd rust/target/${{ matrix.target }}/release
tar czf ../../../../lean-ctx-${artifact}.tar.gz lean-ctx
cd ../../../..
- name: Package (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
Compress-Archive -Path "rust/target/${{ matrix.target }}/release/lean-ctx.exe" -DestinationPath "lean-ctx-${{ matrix.artifact || matrix.target }}.zip"
- name: Upload artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: lean-ctx-${{ matrix.artifact || matrix.target }}
path: |
lean-ctx-${{ matrix.artifact || matrix.target }}.tar.gz
lean-ctx-${{ matrix.artifact || matrix.target }}.zip
release:
name: Create Release
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4 # v4
with:
persist-credentials: false
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
merge-multiple: true
- name: Create stable source tarball
run: |
VERSION="${GITHUB_REF_NAME#v}"
git archive --format=tar.gz --prefix="lean-ctx-${VERSION}/" "${GITHUB_REF_NAME}" -o "lean-ctx-${VERSION}-source.tar.gz"
- name: Generate checksums
run: sha256sum lean-ctx-*.tar.gz lean-ctx-*.zip > SHA256SUMS
- name: Extract release notes from CHANGELOG
id: notes
run: |
VERSION="${GITHUB_REF_NAME#v}"
PREV_TAG=$(git tag --sort=-v:refname | grep -A1 "^v${VERSION}$" | tail -1)
NOTES=$(awk "/^## \\[${VERSION}\\]/{found=1; next} /^## \\[/{if(found) exit} found{print}" CHANGELOG.md)
{
echo "body<<BODY_EOF"
echo "$NOTES"
echo ""
echo "#### Upgrade"
echo '```bash'
echo "lean-ctx update # recommended (auto-downloads + refreshes shell hooks)"
echo "cargo install lean-ctx # or"
echo "npm update -g lean-ctx-bin # or"
echo "brew upgrade lean-ctx"
echo '```'
echo ""
echo "> **Note:** After upgrading via cargo/npm/brew, run \`lean-ctx setup\` to refresh shell aliases. \`lean-ctx update\` does this automatically."
echo ""
echo "**Full Changelog**: https://github.com/yvgude/lean-ctx/compare/${PREV_TAG}...v${VERSION}"
echo "BODY_EOF"
} >> "$GITHUB_OUTPUT"
- name: Create GitHub Release
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2
with:
body: ${{ steps.notes.outputs.body }}
files: |
lean-ctx-*.tar.gz
lean-ctx-*.zip
SHA256SUMS
jetbrains-plugin:
# Build the JetBrains/IntelliJ plugin and attach it to the GitHub Release as a
# downloadable .zip (#418). The plugin's own workflow only runs on plugin source
# changes; the GITHUB_TOKEN-created release here does not trigger other workflows,
# so the asset must be produced from inside this release pipeline. `-Pversion`
# mirrors the tag so the plugin version never drifts from the engine release.
name: Attach JetBrains Plugin
needs: release
runs-on: ubuntu-latest
if: ${{ !contains(github.ref_name, '-rc') }}
permissions:
contents: write
steps:
- uses: actions/checkout@v4 # v4
with:
persist-credentials: false
- name: Setup Java
uses: actions/setup-java@v5
with:
distribution: zulu
java-version: 21
- name: Setup Gradle
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6
- name: Build plugin distribution
working-directory: packages/jetbrains-lean-ctx
run: ./gradlew buildPlugin -Pversion="${GITHUB_REF_NAME#v}" --console=plain
- name: Attach plugin ZIP to release
env:
GH_TOKEN: ${{ github.token }}
# Gradle's rootProject.name is "lean-ctx", so buildPlugin emits
# build/distributions/lean-ctx-<version>.zip — indistinguishable from a
# source archive in the release asset list, which is why the plugin
# looked "missing" (#418). Re-name it to a discoverable, unambiguous
# asset before upload so users can actually find the JetBrains plugin.
run: |
VERSION="${GITHUB_REF_NAME#v}"
SRC="$(ls packages/jetbrains-lean-ctx/build/distributions/*.zip | head -1)"
if [ -z "$SRC" ]; then
echo "::error::No plugin distribution zip produced by buildPlugin" >&2
exit 1
fi
DEST="lean-ctx-jetbrains-plugin-${VERSION}.zip"
cp "$SRC" "$DEST"
gh release upload "${GITHUB_REF_NAME}" "$DEST" --clobber
publish-crates:
name: Publish to crates.io
needs: release
runs-on: ubuntu-latest
if: ${{ !contains(github.ref_name, '-rc') }}
steps:
- uses: actions/checkout@v4 # v4
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
# Fail the release if publishing fails — a silent skip used to ship a GitHub
# release whose crates.io artifact was missing. Re-runs are idempotent: an
# already-published version is treated as a no-op, not a failure.
- name: Publish
working-directory: rust
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: |
set -euo pipefail
CRATE_VERSION="$(grep -m1 '^version' Cargo.toml | sed -E 's/.*"([^"]+)".*/\1/')"
if curl -fsSL -H "User-Agent: lean-ctx-release" \
"https://crates.io/api/v1/crates/lean-ctx/${CRATE_VERSION}" >/dev/null 2>&1; then
echo "crates.io already has lean-ctx ${CRATE_VERSION} — skipping publish."
exit 0
fi
# --no-verify: rmcp 2.2.0 on crates.io has a compile bug (from_bytes_stream
# vs from_byte_stream); our [patch.crates-io] fixes it but is stripped by
# cargo package. Binary builds use --locked and are unaffected.
cargo publish --allow-dirty --no-verify --token "${CARGO_REGISTRY_TOKEN}"
publish-npm:
name: Publish to npm
needs: release
runs-on: ubuntu-latest
if: ${{ !contains(github.ref_name, '-rc') }}
steps:
- uses: actions/checkout@v4 # v4
with:
persist-credentials: false
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 22
registry-url: https://registry.npmjs.org
# Fail-hard on publish errors; idempotent on an already-published version
# (npm view exits non-zero when the exact version is absent). The version is
# read from package.json so the guard always matches what npm will publish.
- name: Publish lean-ctx-bin
working-directory: packages/lean-ctx-bin
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
set -euo pipefail
PKG_VERSION="$(node -p "require('./package.json').version")"
if npm view "lean-ctx-bin@${PKG_VERSION}" version >/dev/null 2>&1; then
echo "npm already has lean-ctx-bin@${PKG_VERSION} — skipping publish."
exit 0
fi
npm publish --access public
- name: Publish pi-lean-ctx
working-directory: packages/pi-lean-ctx
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
set -euo pipefail
PKG_VERSION="$(node -p "require('./package.json').version")"
if npm view "pi-lean-ctx@${PKG_VERSION}" version >/dev/null 2>&1; then
echo "npm already has pi-lean-ctx@${PKG_VERSION} — skipping publish."
exit 0
fi
# prepack builds the self-contained vendor bundle (esbuild devDep),
# so the published package ships with zero runtime dependencies (#670).
npm ci
npm publish --access public
update-homebrew:
name: Update Homebrew
needs: release
runs-on: ubuntu-latest
if: ${{ !contains(github.ref_name, '-rc') }}
steps:
- name: Download checksums and extract platform hashes
id: checksums
run: |
VERSION="${GITHUB_REF_NAME#v}"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
gh release download "${GITHUB_REF_NAME}" \
--repo yvgude/lean-ctx --pattern "SHA256SUMS" --dir .
AARCH64_DARWIN=$(grep "aarch64-apple-darwin\.tar\.gz" SHA256SUMS | awk '{print $1}')
X86_64_DARWIN=$(grep "x86_64-apple-darwin\.tar\.gz" SHA256SUMS | awk '{print $1}')
AARCH64_LINUX_GNU=$(grep "aarch64-unknown-linux-gnu\.tar\.gz" SHA256SUMS | awk '{print $1}')
X86_64_LINUX_GNU=$(grep "x86_64-unknown-linux-gnu\.tar\.gz" SHA256SUMS | awk '{print $1}')
echo "aarch64_darwin_sha=${AARCH64_DARWIN}" >> "$GITHUB_OUTPUT"
echo "x86_64_darwin_sha=${X86_64_DARWIN}" >> "$GITHUB_OUTPUT"
echo "aarch64_linux_sha=${AARCH64_LINUX_GNU}" >> "$GITHUB_OUTPUT"
echo "x86_64_linux_sha=${X86_64_LINUX_GNU}" >> "$GITHUB_OUTPUT"
env:
GH_TOKEN: ${{ github.token }}
- name: Clone and generate binary formula
run: |
VERSION="${{ steps.checksums.outputs.version }}"
AARCH64_DARWIN_SHA="${{ steps.checksums.outputs.aarch64_darwin_sha }}"
X86_64_DARWIN_SHA="${{ steps.checksums.outputs.x86_64_darwin_sha }}"
AARCH64_LINUX_SHA="${{ steps.checksums.outputs.aarch64_linux_sha }}"
X86_64_LINUX_SHA="${{ steps.checksums.outputs.x86_64_linux_sha }}"
git clone "https://x-access-token:${HOMEBREW_TOKEN}@github.com/yvgude/homebrew-lean-ctx.git"
cd homebrew-lean-ctx
cat > Formula/lean-ctx.rb <<EOF
class LeanCtx < Formula
desc "The Context Engineering Layer for AI Coding — 71 MCP tools, 10 read modes, 95+ shell patterns"
homepage "https://leanctx.com"
version "${VERSION}"
license "Apache-2.0"
# Semantic search (ctx_semantic_search / embeddings) loads
# libonnxruntime at runtime; the engine resolves it from the
# Homebrew prefix lib dir. Without this dependency the dylib is
# absent and ORT init fails. See issue #544.
depends_on "onnxruntime"
on_macos do
if Hardware::CPU.arm?
url "https://github.com/yvgude/lean-ctx/releases/download/v${VERSION}/lean-ctx-aarch64-apple-darwin.tar.gz"
sha256 "${AARCH64_DARWIN_SHA}"
else
url "https://github.com/yvgude/lean-ctx/releases/download/v${VERSION}/lean-ctx-x86_64-apple-darwin.tar.gz"
sha256 "${X86_64_DARWIN_SHA}"
end
end
on_linux do
if Hardware::CPU.arm?
url "https://github.com/yvgude/lean-ctx/releases/download/v${VERSION}/lean-ctx-aarch64-unknown-linux-gnu.tar.gz"
sha256 "${AARCH64_LINUX_SHA}"
else
url "https://github.com/yvgude/lean-ctx/releases/download/v${VERSION}/lean-ctx-x86_64-unknown-linux-gnu.tar.gz"
sha256 "${X86_64_LINUX_SHA}"
end
end
def install
bin.install "lean-ctx"
end
test do
assert_match "lean-ctx ${VERSION}", shell_output("#{bin}/lean-ctx --version")
end
end
EOF
sed -i 's/^ //' Formula/lean-ctx.rb
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add Formula/lean-ctx.rb
git diff --cached --quiet || git commit -m "lean-ctx ${VERSION}"
git push
env:
HOMEBREW_TOKEN: ${{ secrets.HOMEBREW_GITHUB_TOKEN }}
announce-twitter:
name: Announce on Twitter
needs: release
runs-on: ubuntu-latest
if: ${{ !contains(github.ref_name, '-rc') }}
steps:
- uses: actions/checkout@v4 # v4
with:
persist-credentials: false
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 22
- name: Post release tweet
env:
TWITTER_CONSUMER_KEY: ${{ secrets.TWITTER_CONSUMER_KEY }}
TWITTER_CONSUMER_SECRET: ${{ secrets.TWITTER_CONSUMER_SECRET }}
TWITTER_ACCESS_TOKEN: ${{ secrets.TWITTER_ACCESS_TOKEN }}
TWITTER_ACCESS_SECRET: ${{ secrets.TWITTER_ACCESS_SECRET }}
RELEASE_TAG: ${{ github.ref_name }}
REPO: ${{ github.repository }}
run: node .github/scripts/post-release-tweet.mjs
+171
View File
@@ -0,0 +1,171 @@
name: Security Check
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
jobs:
security:
name: Security Scan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check for dangerous patterns
run: |
echo "## Security Pattern Scan" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
FOUND=0
# Check for unauthorized network libraries
# Allowed: ureq (used for opt-in cloud sync, updates, error reports)
# Allowed: std::net::TcpListener (used for local dashboard server)
# Blocked: reqwest, hyper (heavy HTTP clients not needed)
if grep -rn 'reqwest::' rust/src/ 2>/dev/null; then
echo "::warning::Found reqwest usage — use ureq instead"
echo "- ⚠️ Found reqwest usage (use ureq)" >> $GITHUB_STEP_SUMMARY
FOUND=1
fi
if grep -rn 'hyper::' rust/src/ 2>/dev/null; then
echo "::warning::Found hyper usage — use ureq instead"
echo "- ⚠️ Found hyper usage (use ureq)" >> $GITHUB_STEP_SUMMARY
FOUND=1
fi
# Check for unsafe code
UNSAFE_COUNT=$(grep -rn 'unsafe {' rust/src/ 2>/dev/null | wc -l)
if [ "$UNSAFE_COUNT" -gt 0 ]; then
echo "::warning::Found $UNSAFE_COUNT unsafe blocks"
echo "- ⚠️ Found $UNSAFE_COUNT unsafe blocks" >> $GITHUB_STEP_SUMMARY
grep -rn 'unsafe {' rust/src/ >> $GITHUB_STEP_SUMMARY
FOUND=1
fi
# Check for environment manipulation
if grep -rn '\.env("LD_PRELOAD")' rust/src/ 2>/dev/null; then
echo "::error::Found LD_PRELOAD manipulation — potential library hijacking"
echo "- ❌ Found LD_PRELOAD manipulation" >> $GITHUB_STEP_SUMMARY
FOUND=1
fi
if grep -rn '\.env("DYLD_' rust/src/ 2>/dev/null; then
echo "::error::Found DYLD manipulation — potential library hijacking"
echo "- ❌ Found DYLD manipulation" >> $GITHUB_STEP_SUMMARY
FOUND=1
fi
# Check for hardcoded secrets patterns
if grep -rn 'sk_live_\|sk_test_\|AKIA[0-9A-Z]\|ghp_[a-zA-Z0-9]' rust/src/ 2>/dev/null; then
echo "::error::Found potential hardcoded secrets"
echo "- ❌ Found potential hardcoded secrets" >> $GITHUB_STEP_SUMMARY
FOUND=1
fi
# Check for shell injection vectors
SHELL_INJECT=$(grep -rn 'Command::new("sh")\.arg("-c")\.arg(format!' rust/src/ 2>/dev/null | wc -l)
if [ "$SHELL_INJECT" -gt 0 ]; then
echo "::warning::Found $SHELL_INJECT potential shell injection vectors"
echo "- ⚠️ Found $SHELL_INJECT shell injection patterns" >> $GITHUB_STEP_SUMMARY
FOUND=1
fi
# Check for unwrap() in production code (excluding tests)
UNWRAP_COUNT=$(grep -rn '\.unwrap()' rust/src/ 2>/dev/null | grep -v '#\[test\]' | grep -v 'mod tests' | wc -l)
echo "- ️ Found $UNWRAP_COUNT .unwrap() calls in src/" >> $GITHUB_STEP_SUMMARY
if [ "$FOUND" -eq 0 ]; then
echo "" >> $GITHUB_STEP_SUMMARY
echo "✅ No dangerous patterns detected" >> $GITHUB_STEP_SUMMARY
fi
- name: Proprietary code guardrail
run: |
echo "## Proprietary Code Guard" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
LEAK=0
# Keep in sync with .github-ignore. Business/monetization dirs are
# gitignored locally; listing them here is server-side defense-in-depth
# in case the local pre-push hook is not installed.
PRIVATE_PATHS="cloud/ docker-compose.yml .gitlab-ci.yml deploy.sh DEVELOPMENT.md Makefile.deploy docs/business/ memory-bank/ discord-bot/ n8n-workflows/ lab/ server.md"
for path in $PRIVATE_PATHS; do
if [ -e "$path" ]; then
echo "::error::PROPRIETARY CODE DETECTED: $path exists in the GitHub repository!"
echo "- **$path** — must not be on GitHub" >> $GITHUB_STEP_SUMMARY
LEAK=1
fi
done
if [ "$LEAK" -eq 1 ]; then
echo "" >> $GITHUB_STEP_SUMMARY
echo "These paths belong on GitLab only. See .github-ignore." >> $GITHUB_STEP_SUMMARY
exit 1
fi
echo "No proprietary code found." >> $GITHUB_STEP_SUMMARY
- name: Commercial-plane guard
run: |
echo "## Commercial-plane Guard" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
LEAK=0
# Billing/licensing logic lives in the private lean-ctx-cloud repo
# (oss-plane-separation-v1). core/billing/ is partially OPEN (metering,
# plans, mod), so we name the specific commercial files rather than the
# whole directory — the path guard above can't blanket-block them.
COMMERCIAL_PATHS="rust/src/core/license rust/src/core/licensing/keygen.rs rust/src/cli/license_cmd.rs rust/src/core/billing/success_fee.rs rust/src/core/billing/stripe_invoice.rs docs/contracts/license-v1.md docs/contracts/success-fee-invoice-v1.md"
for path in $COMMERCIAL_PATHS; do
if [ -e "$path" ]; then
echo "::error::COMMERCIAL CODE DETECTED: $path belongs in lean-ctx-cloud, not the open engine!"
echo "- **$path** — implement in lean-ctx-cloud" >> $GITHUB_STEP_SUMMARY
LEAK=1
fi
done
if [ "$LEAK" -eq 1 ]; then
echo "" >> $GITHUB_STEP_SUMMARY
echo "The open engine bills nothing and issues no licenses; it only emits the signed savings ledger. See .github-ignore." >> $GITHUB_STEP_SUMMARY
exit 1
fi
echo "No commercial-plane code found." >> $GITHUB_STEP_SUMMARY
- name: Critical files check
if: github.event_name == 'pull_request'
run: |
echo "## Critical Files Modified" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
CRITICAL_FILES="rust/src/shell.rs rust/src/server.rs rust/src/hooks.rs rust/src/core/cache.rs rust/Cargo.toml .github/workflows"
FOUND_CRITICAL=0
for file in $CRITICAL_FILES; do
if git diff --name-only origin/main...HEAD | grep -q "$file"; then
echo "- ⚠️ **$file** modified (requires security review)" >> $GITHUB_STEP_SUMMARY
FOUND_CRITICAL=1
fi
done
if [ "$FOUND_CRITICAL" -eq 0 ]; then
echo "✅ No critical files modified" >> $GITHUB_STEP_SUMMARY
fi
- name: Dependency audit
shell: bash
run: |
set -o pipefail
cargo install cargo-audit
cd rust && cargo audit 2>&1 | tee audit-output.txt
AUDIT_EXIT=${PIPESTATUS[0]}
echo "" >> $GITHUB_STEP_SUMMARY
echo "## Dependency Audit" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
cat audit-output.txt >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
exit $AUDIT_EXIT