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< **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-.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 <