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
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:
@@ -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
|
||||
@@ -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
|
||||
@@ -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. ✅'
|
||||
@@ -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"
|
||||
@@ -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."
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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}"
|
||||
@@ -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/*
|
||||
@@ -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`.
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user