chore(release): v0.88.0 #272
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release | |
| on: | |
| push: | |
| tags: ['v*'] | |
| # Manual re-run of a failed release on an existing tag. Use when publish | |
| # broke (NPM_TOKEN expired, transient registry 5xx, etc.) and you don't | |
| # want to bump a new version just to re-trigger the pipeline. Dispatch from | |
| # the default branch; each job checks out `inputs.tag` instead of the | |
| # default-branch HEAD so the build/publish matches that tag's source. | |
| workflow_dispatch: | |
| inputs: | |
| tag: | |
| description: 'Existing git tag to (re-)release, e.g. v0.32.2. Must already exist on origin.' | |
| required: true | |
| type: string | |
| permissions: | |
| contents: write | |
| jobs: | |
| build: | |
| name: Build ${{ matrix.npm-pkg }} | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - target: x86_64-unknown-linux-gnu | |
| os: ubuntu-latest | |
| npm-pkg: linux-x64 | |
| binary: code-graph-mcp | |
| - target: aarch64-unknown-linux-gnu | |
| os: ubuntu-latest | |
| npm-pkg: linux-arm64 | |
| binary: code-graph-mcp | |
| - target: x86_64-apple-darwin | |
| os: macos-latest | |
| npm-pkg: darwin-x64 | |
| binary: code-graph-mcp | |
| - target: aarch64-apple-darwin | |
| os: macos-latest | |
| npm-pkg: darwin-arm64 | |
| binary: code-graph-mcp | |
| - target: x86_64-pc-windows-msvc | |
| os: windows-latest | |
| npm-pkg: win32-x64 | |
| binary: code-graph-mcp.exe | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| submodules: true | |
| # Push trigger: github.ref = refs/tags/vX.Y.Z, checkout is a no-op. | |
| # workflow_dispatch trigger: github.ref = main, must redirect to the | |
| # tag the operator specified, otherwise we'd build main's HEAD under | |
| # an old version number. | |
| ref: ${{ github.event.inputs.tag || github.ref }} | |
| # SHA-pinned for release-pipeline supply-chain integrity (cso #1, #3): | |
| # @stable was auto-resolving to whatever rustc was current at build time — | |
| # binary published to npm was built with a Rust version CI never tested, | |
| # and a moved tag would silently affect every release. Pinned to the same | |
| # 1.95.0 branch CI uses (ci.yml: dtolnay/rust-toolchain@1.95.0). | |
| - uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0 | |
| with: | |
| targets: ${{ matrix.target }} | |
| - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 | |
| with: | |
| key: ${{ matrix.target }} | |
| - name: Install cross-compilation tools | |
| if: matrix.target == 'aarch64-unknown-linux-gnu' | |
| 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 | |
| # `--features embed-model` keeps npm/npx/plugin users on the full hybrid | |
| # stack (FTS5 + vector). Cargo default flipped to [] in v0.18.4 to give | |
| # `cargo install` users a smaller binary; npm release explicitly opts | |
| # back in here so the user-visible npm contract is unchanged. | |
| - name: Build | |
| run: cargo build --release --features embed-model --target ${{ matrix.target }} | |
| - name: Upload binary | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: binary-${{ matrix.npm-pkg }} | |
| path: target/${{ matrix.target }}/release/${{ matrix.binary }} | |
| publish: | |
| name: Publish | |
| needs: build | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ github.event.inputs.tag || github.ref }} | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: 20 | |
| registry-url: https://registry.npmjs.org | |
| - name: Download all artifacts | |
| uses: actions/download-artifact@v8 | |
| with: | |
| path: artifacts/ | |
| - name: Set version from tag | |
| # On push trigger GITHUB_REF_NAME = "vX.Y.Z". On workflow_dispatch from | |
| # main, GITHUB_REF_NAME = "main" — fall back to inputs.tag instead. | |
| run: | | |
| REF_NAME="${{ github.event.inputs.tag || github.ref_name }}" | |
| VERSION=${REF_NAME#v} | |
| echo "VERSION=$VERSION" >> $GITHUB_ENV | |
| - name: Prepare platform packages | |
| run: | | |
| for pkg in linux-x64 linux-arm64 darwin-x64 darwin-arm64; do | |
| cp artifacts/binary-$pkg/code-graph-mcp npm/$pkg/ | |
| chmod +x npm/$pkg/code-graph-mcp | |
| done | |
| cp artifacts/binary-win32-x64/code-graph-mcp.exe npm/win32-x64/ | |
| - name: Update versions | |
| # SYNC_VERSIONS_SKIP_BUILD: the Publish job already has the 5 pre-built | |
| # platform binaries (matrix artifacts); sync-versions.js's local | |
| # "rebuild release binary" convenience is pointless here and made the | |
| # release fragile — a transient crates.io blip (v0.65.0: `download of | |
| # float8 failed`) failed the whole publish at the rebuild, after all | |
| # binaries were built. Downstream steps use artifacts/ + npm/, never | |
| # target/release/, so skipping the rebuild is safe. | |
| run: node scripts/sync-versions.js $VERSION | |
| env: | |
| SYNC_VERSIONS_SKIP_BUILD: '1' | |
| - name: Release smoke test | |
| run: node --test scripts/release-smoke.test.js | |
| - name: Prepare release assets | |
| run: | | |
| mkdir -p release-assets | |
| for pkg in linux-x64 linux-arm64 darwin-x64 darwin-arm64; do | |
| cp artifacts/binary-$pkg/code-graph-mcp release-assets/code-graph-mcp-$pkg | |
| done | |
| cp artifacts/binary-win32-x64/code-graph-mcp.exe release-assets/code-graph-mcp-win32-x64.exe | |
| # Per-binary sha256 sidecar (mirrors models.tar.gz.sha256 below). | |
| # auto-update.js fetches <asset>.sha256 and verifies the downloaded | |
| # binary BEFORE chmod/exec; same-origin, so it guards transit/CDN | |
| # corruption + truncation, not a full release-asset swap. | |
| for asset in code-graph-mcp-linux-x64 code-graph-mcp-linux-arm64 code-graph-mcp-darwin-x64 code-graph-mcp-darwin-arm64 code-graph-mcp-win32-x64.exe; do | |
| sha256sum "release-assets/$asset" | cut -d' ' -f1 > "release-assets/$asset.sha256" | |
| done | |
| - name: Package model files | |
| # Pinned to a specific HuggingFace commit (cso #2) instead of the | |
| # mutable `main` ref — without this, `main` moving (or HF account | |
| # compromise) silently changes the bundled model and the downstream | |
| # tarball sha256 would only validate the bundle against itself, not | |
| # against a known-good upstream. --fail makes curl exit non-zero on | |
| # HTTP 4xx/5xx so a 404 HTML page can't masquerade as model.safetensors. | |
| # Content pins below verify the downloaded bytes against known-good | |
| # hashes of the pinned revision, so even a compromised HF response (or | |
| # a silent revision bump in this file) fails the release instead of | |
| # shipping different weights. When HF_REVISION changes, recompute all | |
| # three sha256s AND bump MODEL_CONTENT_BLAKE3 in src/embedding/model.rs | |
| # (the client-side verifier) in the same commit — clients reject any | |
| # models.tar.gz whose model.safetensors doesn't match that constant. | |
| run: | | |
| mkdir -p models-pkg | |
| HF_REVISION=c9745ed1d9f207416be6d2e6f8de32d1f16199bf | |
| for f in model.safetensors tokenizer.json config.json; do | |
| # --retry: a transient HF 5xx must not fail the release. Mirrors the | |
| # smoke-verify model fetch. Combined with the publish-after-assets | |
| # ordering below, a HF blip now fails BEFORE npm publish (no half- | |
| # release shipping a version whose models.tar.gz 404s → FTS5-only). | |
| curl -L --fail --retry 3 --retry-delay 2 -o models-pkg/$f \ | |
| "https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2/resolve/$HF_REVISION/$f" | |
| done | |
| sha256sum -c - <<'PINS' | |
| 53aa51172d142c89d9012cce15ae4d6cc0ca6895895114379cacb4fab128d9db models-pkg/model.safetensors | |
| be50c3628f2bf5bb5e3a7f17b1f74611b2561a3a27eeab05e5aa30f411572037 models-pkg/tokenizer.json | |
| 953f9c0d463486b10a6871cc2fd59f223b2c70184f49815e7efbcab5d8908b41 models-pkg/config.json | |
| PINS | |
| tar czf release-assets/models.tar.gz -C models-pkg . | |
| sha256sum release-assets/models.tar.gz | cut -d' ' -f1 > release-assets/models.tar.gz.sha256 | |
| - name: Create GitHub Release | |
| uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3 | |
| with: | |
| generate_release_notes: true | |
| files: release-assets/* | |
| # On push.tags trigger github.ref_name = "vX.Y.Z" and softprops picks | |
| # it up implicitly. On workflow_dispatch from main, github.ref_name = | |
| # "main" — softprops then errors with "GitHub Releases requires a | |
| # tag". Explicit tag_name with the same inputs.tag fallback the rest | |
| # of the job uses keeps both triggers working. | |
| tag_name: ${{ github.event.inputs.tag || github.ref_name }} | |
| # npm publish runs LAST — only after the binaries, model tarball, and the | |
| # GitHub Release are all built and uploaded. Previously publish ran before | |
| # "Package model files" + "Create GitHub Release", so a transient HF failure | |
| # (or a forgotten sha bump) shipped npm packages for a version whose | |
| # models.tar.gz release asset never existed → every user on that version | |
| # silently degraded to FTS5-only until the workflow was re-run. The | |
| # EPUBLISHCONFLICT skip keeps re-runs idempotent. | |
| - name: Publish platform packages | |
| run: | | |
| for pkg in linux-x64 linux-arm64 darwin-x64 darwin-arm64 win32-x64; do | |
| cd npm/$pkg | |
| # Capture publish output so we can distinguish "already published" | |
| # (E403 / EPUBLISHCONFLICT — safe to skip on workflow re-run) from | |
| # auth / 404 / network failures (must fail the job). Old "|| echo | |
| # warning" swallowed everything and made smoke tests the only fail | |
| # signal — 15 min of red retries before anyone noticed in v0.30.0. | |
| if ! out=$(npm publish --access public 2>&1); then | |
| if echo "$out" | grep -qE "EPUBLISHCONFLICT|cannot publish over the previously published versions|You cannot publish over"; then | |
| echo "::warning::$pkg already published at $VERSION — skipping" | |
| else | |
| echo "$out" | |
| echo "::error::$pkg publish failed (not 'already exists' — check NPM_TOKEN / registry)" | |
| exit 1 | |
| fi | |
| else | |
| echo "$out" | |
| fi | |
| cd ../.. | |
| done | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| - name: Publish main package | |
| run: | | |
| if ! out=$(npm publish --access public 2>&1); then | |
| if echo "$out" | grep -qE "EPUBLISHCONFLICT|cannot publish over the previously published versions|You cannot publish over"; then | |
| echo "::warning::main package already published at $VERSION — skipping" | |
| else | |
| echo "$out" | |
| echo "::error::main package publish failed (not 'already exists' — check NPM_TOKEN / registry)" | |
| exit 1 | |
| fi | |
| else | |
| echo "$out" | |
| fi | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| # Post-publish verification: in a clean env on each OS, install the JUST- | |
| # PUBLISHED package from the npm registry and assert the binary runs and | |
| # reports the expected version. Catches: | |
| # - missing platform-binary optionalDependency | |
| # - find-binary.js OS/arch detection regressions | |
| # - sync-versions drift (npm pkg version != Cargo.toml version) | |
| # - npm registry propagation issues | |
| # Retries the install with backoff since npm's CDN can lag ~30-60s behind | |
| # the publish API. | |
| smoke-verify: | |
| name: Post-publish smoke (${{ matrix.os }}) | |
| needs: publish | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, macos-latest, windows-latest] | |
| steps: | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: 20 | |
| - name: Extract version from tag | |
| shell: bash | |
| run: | | |
| REF_NAME="${{ github.event.inputs.tag || github.ref_name }}" | |
| echo "VERSION=${REF_NAME#v}" >> $GITHUB_ENV | |
| - name: Install published package (with retry for registry propagation) | |
| shell: bash | |
| run: | | |
| set -eu | |
| # Post-publish smoke races npm registry propagation in TWO independent ways, | |
| # neither caught by npm's exit code, both of which the retry must tolerate: | |
| # | |
| # (1) MAIN-package metadata lag -> `npm install @sdsrs/code-graph@X` fails with | |
| # `ETARGET / No matching version` because the CDN edge this runner hit has | |
| # not listed version X in the packument yet (v0.82.1 ubuntu smoke). npm | |
| # then CACHES that stale packument locally (~/.npm, Cache-Control max-age | |
| # ~minutes), so a NAIVE retry re-reads the stale cache and keeps returning | |
| # ETARGET for the whole window even after the edge propagates. Fix: every | |
| # registry read below uses --prefer-online to force revalidation instead | |
| # of serving the stale cached packument. | |
| # (2) PLATFORM optionalDependency lag -> once the main package resolves, | |
| # `npm install -g` still EXITS 0 while silently skipping an unresolved | |
| # @sdsrs/code-graph-<os>-<arch> ("added 1 package" not 2), so the binary | |
| # is missing after a "successful" install (v0.66.0 ubuntu smoke). Fix: | |
| # gate success on the binary RESOLVING to the expected version, never on | |
| # npm's exit code. | |
| expected="code-graph-mcp ${VERSION}" | |
| for attempt in 1 2 3 4 5 6; do | |
| echo "Install attempt $attempt of 6..." | |
| if ! npm view --prefer-online "@sdsrs/code-graph@${VERSION}" version >/dev/null 2>&1; then | |
| echo "Main package @sdsrs/code-graph@${VERSION} not visible on registry yet (ETARGET) — metadata still propagating." | |
| else | |
| npm install -g --prefer-online "@sdsrs/code-graph@${VERSION}" || true | |
| if actual=$(code-graph-mcp --version 2>/dev/null) && [ "$actual" = "$expected" ]; then | |
| echo "Install + binary resolved on attempt $attempt: $actual" | |
| break | |
| fi | |
| echo "Main package resolved but binary not runnable (got '${actual:-<none>}', want '$expected') — platform optional-dep still propagating." | |
| fi | |
| if [ "$attempt" = "6" ]; then | |
| echo "::error::@sdsrs/code-graph@${VERSION} did not yield a working binary after 6 attempts (registry propagation exceeded retry window). Recover with: gh run rerun <run-id> --failed" | |
| exit 1 | |
| fi | |
| sleep $((attempt * 20)) | |
| done | |
| - name: Verify --version matches tag | |
| shell: bash | |
| run: | | |
| set -eu | |
| actual=$(code-graph-mcp --version) | |
| expected="code-graph-mcp ${VERSION}" | |
| if [ "$actual" != "$expected" ]; then | |
| echo "::error::--version mismatch: got '$actual', expected '$expected'" | |
| exit 1 | |
| fi | |
| echo "OK: $actual" | |
| - name: Verify --help runs | |
| run: code-graph-mcp --help | |
| - name: Verify a real subcommand runs on a minimal project | |
| shell: bash | |
| run: | | |
| set -eu | |
| tmpdir=$(mktemp -d) | |
| cd "$tmpdir" | |
| git init -q | |
| git config user.email ci@example.com | |
| git config user.name CI | |
| mkdir -p src | |
| cat > src/main.rs <<'EOF' | |
| fn hello() { println!("hi"); } | |
| fn main() { hello(); } | |
| EOF | |
| git add . && git commit -q -m init | |
| # map requires an index — build one first. | |
| code-graph-mcp incremental-index --quiet | |
| code-graph-mcp map --json > map.json | |
| test -s map.json | |
| # Use fs.readFileSync with a cwd-relative path so this works across | |
| # shells where `$tmpdir` from `mktemp -d` is a POSIX-style path | |
| # (`/tmp/tmp.XXXX` on Git Bash under Windows) that Node.js on | |
| # Windows can't resolve. `node -e require('/tmp/...')` fails there | |
| # because `/tmp/` doesn't exist in the Win32 filesystem view. | |
| node -e "const m = JSON.parse(require('fs').readFileSync('map.json','utf8')); if (!m || typeof m !== 'object') { console.error('map.json is not a valid JSON object'); process.exit(1); } console.log('map OK:', Object.keys(m).slice(0, 5));" | |
| - name: Verify embedding model loads & embeds (vector integrity gate) | |
| # The prior smoke proved the binary runs + indexes (FTS/AST) but NEVER that | |
| # the embedding model downloads/loads — so a release with a missing/corrupt/ | |
| # unloadable models.tar.gz shipped green while every user silently degraded to | |
| # FTS5-only (no vector). This pulls the PUBLISHED model, checksum-verifies it, | |
| # points the binary at it, and asserts it actually loads + embeds | |
| # (search_mode=hybrid). Linux-only: the model is platform-independent, so one | |
| # load+embed is enough to catch a broken model release. Boundary: validates the | |
| # published tarball loads & produces vectors; the blake3 content pin is enforced | |
| # at build (model packaging step) and at runtime in the normal cache path. | |
| if: runner.os == 'Linux' | |
| shell: bash | |
| run: | | |
| set -eu | |
| work=$(mktemp -d); cd "$work" | |
| base="https://github.com/sdsrss/code-graph-mcp/releases/download/v${VERSION}" | |
| curl -fL --retry 3 -o models.tar.gz "$base/models.tar.gz" | |
| curl -fL --retry 3 -o models.tar.gz.sha256 "$base/models.tar.gz.sha256" | |
| echo "$(cat models.tar.gz.sha256) models.tar.gz" | sha256sum -c - | |
| mkdir -p models && tar xzf models.tar.gz -C models | |
| test -s models/model.safetensors | |
| export CODE_GRAPH_MODEL_DIR="$work/models" | |
| proj=$(mktemp -d); cd "$proj" | |
| git init -q; git config user.email ci@example.com; git config user.name CI | |
| mkdir -p src | |
| printf 'fn alpha() {}\nfn beta() { alpha(); }\nfn main() { beta(); }\n' > src/main.rs | |
| git add . && git commit -q -m init | |
| code-graph-mcp incremental-index # loads model from CODE_GRAPH_MODEL_DIR + embeds | |
| code-graph-mcp health-check --json > hc.json | |
| node -e ' | |
| const h = JSON.parse(require("fs").readFileSync("hc.json","utf8")); | |
| console.log("health-check:", JSON.stringify({search_mode:h.search_mode, embedding_progress:h.embedding_progress, model_available:h.model_available})); | |
| const [done,total] = (h.embedding_progress||"0/0").split("/").map(Number); | |
| if (h.search_mode !== "hybrid" || !(done>0)) { | |
| console.error("::error::vector INACTIVE after installing the published model (search_mode="+h.search_mode+", embedded="+done+"/"+total+") — a broken model release; users would be silently FTS5-only"); | |
| process.exit(1); | |
| } | |
| console.log("OK: vector active — embedded "+done+"/"+total); | |
| ' |