Skip to content

chore(release): v0.88.0 #272

chore(release): v0.88.0

chore(release): v0.88.0 #272

Workflow file for this run

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);
'