Skip to content

fix(resolver): stop confident fan-out to unrelated same-named local f… #18

fix(resolver): stop confident fan-out to unrelated same-named local f…

fix(resolver): stop confident fan-out to unrelated same-named local f… #18

Workflow file for this run

name: Release
on:
push:
tags: ['v*']
permissions:
contents: write
jobs:
# WS-13 (docs/plans/2026-08-01-calm-adopt-from-vheatm-plan.md P3-1 /
# docs/plans/2026-08-01-calm-master-upgrade-plan.md WS-13): a single
# quality gate every publish job below depends on, so a tag push can
# never reach a binary/container publish without the checks ci.yml
# already runs on main actually passing for the exact tagged commit --
# previously `build`/`docker` had no `needs:` at all and would publish
# unconditionally. Deliberately NOT a `workflow_call`/reusable-workflow
# refactor of ci.yml (bigger surface, more risk for a Tier-A/low-risk
# change) -- GitHub Actions `needs:` cannot cross workflow files, so this
# duplicates the checks ci.yml's `verify`/`stack-graphs-corpus`/
# `fitness-check`/`js-client-interop`/`status-drift` jobs already run,
# self-contained in this workflow. Keep the two in sync by hand if either
# changes -- the same trade VHEATM's own RG-00..RG-15 qualification job
# (evaluation.py) makes for its release gate.
qualify-release:
name: Qualify release
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
# Cargo.toml's own version field is cosmetic for the actual release
# artifacts -- the `build` job below stamps release binaries from the
# tag directly (sed, "Set crate version from tag"), and npm-publish's
# stage-release.sh takes the tag as its own argument -- so a stale
# Cargo.toml can't ship a wrong-versioned binary or npm package. What
# it DOES still break: `scripts/mcp-launcher.sh`'s verified-download
# fast path, which compares a checked-out `workspace_version` against
# the downloaded release binary's real `--version` and treats any
# mismatch as a corrupted download, falling back to a from-source
# rebuild on every single invocation for the whole release cycle --
# exactly what happened for real, stuck at 0.1.1 through
# v0.1.2/v0.1.3/v0.1.4 (see Cargo.toml's own version-field comment).
# Catch that here, before any publish job runs, not after.
- name: Verify Cargo.toml version matches the release tag
run: |
tag_version="${GITHUB_REF_NAME#v}"
cargo_version=$(grep -m1 '^version = ' Cargo.toml | sed -E 's/version = "(.*)"/\1/')
if [ "$cargo_version" != "$tag_version" ]; then
echo "::error::Cargo.toml's [workspace.package].version ('${cargo_version}') does not match release tag ${GITHUB_REF_NAME} (expected '${tag_version}'). Bump it in the same commit as cutting this tag -- see Cargo.toml's own comment for what breaks otherwise (scripts/mcp-launcher.sh's fast path, defeated for this whole release cycle)."
exit 1
fi
echo "Cargo.toml version ${cargo_version} matches release tag ${GITHUB_REF_NAME}"
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
with:
components: clippy, rustfmt
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
- name: Format check
run: cargo fmt --all -- --check
- name: Clippy
run: cargo clippy --workspace --all-targets -- -D warnings
- name: Test
run: cargo test --workspace
- name: Audit
run: cargo install cargo-audit --locked && cargo audit
- name: Stack Graphs regression corpus
run: cargo test --test parity_test test_formal_edges -- --nocapture
- name: Build calm binary
run: cargo build --bin calm
- name: Index this repo
run: ./target/debug/calm index --project-root .
- name: Fitness check
run: ./target/debug/calm fitness-check --project-root . --config thresholds.toml
- name: Check docs/status.generated.md is not stale
run: ./scripts/gen-status.sh --check
- name: Check hand-authored docs match the generated tool count
run: ./scripts/check-doc-truth.sh
- name: Check benchmark claims registry is structurally consistent
run: ./scripts/check-claims-registry.sh
- name: Install JS interop test dependencies
working-directory: tests/js_client_interop
run: npm ci
- name: Run cross-SDK MCP interop check
working-directory: tests/js_client_interop
run: node client.mjs "$GITHUB_WORKSPACE/target/debug/calm" "$GITHUB_WORKSPACE/crates/calm-core/tests/fixtures/rust_workspace"
build:
name: Build ${{ matrix.target }}
needs: qualify-release
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-unknown-linux-musl
os: ubuntu-latest
cross: true
- target: aarch64-unknown-linux-musl
os: ubuntu-latest
cross: true
- target: aarch64-apple-darwin
os: macos-14
cross: false
# Cross-compiled FROM the same arm64 macos-14 runner as the entry
# above, not a native macos-15-intel job — GitHub's only remaining
# x64 Mac label, itself scheduled for removal Fall 2027 (see
# .github/workflows/macos-x64-build-experiment.yml's header comment
# for the full reasoning + probe that validated this works).
- target: x86_64-apple-darwin
os: macos-14
cross: false
# Native build on a real Windows runner, not `cross` (which needs
# a Linux/Docker host) — validated by
# .github/workflows/windows-build-experiment.yml's 2026-07-15
# probe run: every C dependency (bundled SQLite, ~24 tree-sitter
# grammars, onig via the tokenizers crate) compiles clean under
# MSVC.
- target: x86_64-pc-windows-msvc
os: windows-latest
cross: false
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
with:
targets: ${{ matrix.target }}
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
key: ${{ matrix.target }}
# Workspace version defaults to whatever was last committed to
# Cargo.toml, which drifts from the release tag if nobody remembers to
# bump it by hand — stamp it from the tag so `calm --version` always
# matches the artifact it shipped in.
- name: Set crate version from tag
# `-i.bak` (suffix glued to the flag, no space) is the one `sed -i`
# spelling both GNU sed (Linux runners) and BSD sed (macOS runner)
# accept the same way — a bare `-i` is GNU-only and errors on BSD.
# `shell: bash` is required here: windows-latest's default `run:`
# shell is `pwsh`, which doesn't understand this sed/bash syntax —
# every other runner already defaults to bash, so this is a no-op
# there.
shell: bash
run: |
sed -i.bak "s/^version = \".*\"/version = \"${GITHUB_REF_NAME#v}\"/" Cargo.toml
rm -f Cargo.toml.bak
# `cross` runs the build in a container that already has the C
# cross-toolchain tree-sitter / stack-graphs / rusqlite need. Only the
# two Linux/musl entries opt into this (`cross: true`) — both macOS
# entries and the Windows entry build natively on their own runner via
# "Build (native)" below instead, since none of them are Linux hosts
# `cross` (which shells out to Docker) can run on.
- name: Install cross
if: matrix.cross
run: cargo install cross --locked
- name: Build (cross)
if: matrix.cross
run: cross build --release --bin calm --target ${{ matrix.target }}
- name: Build (native)
if: ${{ !matrix.cross }}
run: cargo build --release --bin calm --target ${{ matrix.target }}
# `shell: bash` for the same reason as "Set crate version from tag"
# above — this step's conditional + `tar`/`dirname` syntax needs a
# real POSIX shell, not windows-latest's default pwsh. Windows ships
# `tar` (bsdtar) and `bash` (Git Bash) out of the box on GitHub-hosted
# runners, so no extra tooling install is needed.
- name: Package
shell: bash
run: |
bin_name="calm"
case "${{ matrix.target }}" in
*-windows-*) bin_name="calm.exe" ;;
esac
bin="target/${{ matrix.target }}/release/${bin_name}"
tar -czf "calm-${{ matrix.target }}.tar.gz" -C "$(dirname "$bin")" "$bin_name"
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: calm-${{ matrix.target }}
path: calm-${{ matrix.target }}.tar.gz
release:
needs: build
runs-on: ubuntu-latest
permissions:
contents: write
# Needed by attest-build-provenance below: `id-token` to mint the
# Sigstore/Fulcio OIDC signing cert (same keyless mechanism the
# `docker` job's cosign step already uses), `attestations` to publish
# the resulting attestation to this repo's attestation store.
id-token: write
attestations: write
steps:
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
path: dist
merge-multiple: true
# Only the binary tarballs from the build matrix. Without this
# filter, "download all" also grabs the `*.dockerbuild` build-record
# artifact docker/build-push-action v6 uploads whenever the `docker`
# job happens to finish before this one — and downloading that
# artifact fails hard (5 retries, then kills the job), which is
# exactly how the first v0.3.2 release attempt died. `needs: build`
# only orders this job after the build matrix; `docker` runs in
# parallel, so whether it sneaks its artifact in first is a race.
pattern: "calm-*"
- name: Generate checksums
working-directory: dist
run: sha256sum calm-*.tar.gz > SHA256SUMS
# GitHub-native build provenance for the binary tarballs — previously
# only SHA256-checksummed (integrity against transfer corruption, not
# authenticity) while the `docker` job's container image alone got a
# real cosign signature (identity/authenticity). This closes that gap
# cheaply: no key management, verifiable by any user via `gh
# attestation verify calm-<target>.tar.gz --owner ${{ github.repository_owner }}`.
# Complements rather than replaces the `docker` job's cosign signing —
# provenance attestation proves "built by this exact workflow run from
# this commit", cosign proves "signed by this identity"; 2025 guidance
# (github.blog/changelog, sigstore.dev) treats these as layered, not
# redundant. Deliberately not extended to the container image too:
# cosign's signature already has the widest adoption in that ecosystem
# (Kubernetes admission controllers, etc.) and doubling up there isn't
# this pass's scope.
- name: Attest build provenance for release binaries
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
with:
subject-path: "dist/calm-*.tar.gz"
- name: Publish release
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2
with:
files: |
dist/*.tar.gz
dist/SHA256SUMS
generate_release_notes: true
docker:
name: Publish container image
needs: qualify-release
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
# Keyless cosign signing (below) provisions an ephemeral signing
# cert from Sigstore's Fulcio via this OIDC token — no key material
# to generate, store, or rotate. Ported from github/github-mcp-server's
# own .github/workflows/docker-publish.yml (verified against that
# real source, including its exact cosign-installer pin, before
# porting) — the same pattern GitHub's own MCP server release
# pipeline uses.
id-token: write
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Log in to GHCR
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Resolve image tag
id: version
run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
- name: Install cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
with:
cosign-release: "v2.2.4"
- name: Build and push container image
id: build-and-push
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
with:
context: .
file: Containerfile
push: true
tags: |
ghcr.io/eilodon/calm-mcp:${{ steps.version.outputs.version }}
ghcr.io/eilodon/calm-mcp:latest
# Signs the image DIGEST (not the mutable tags) so the signature
# stays valid regardless of tag reassignment, then attaches it to
# every tag pointing at that digest — `xargs` over the same
# newline-separated tags list `build-push-action` was given above.
# Writes to the public Rekor transparency log (this repo/image is
# public); see docker-publish.yml's own comment on that if this ever
# needs to be a private image instead.
- name: Sign the published container image
env:
TAGS: |
ghcr.io/eilodon/calm-mcp:${{ steps.version.outputs.version }}
ghcr.io/eilodon/calm-mcp:latest
DIGEST: ${{ steps.build-and-push.outputs.digest }}
run: echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST}
# Auto-publish the npm distribution once the GitHub Release for this tag
# exists (stage-release.sh downloads its assets — hence `needs: release`).
# This was deliberately manual until the first publish had been proven by
# hand (npm/README.md); v0.1.4 cleared that bar, so it now runs in CI,
# gated only on the NPM_TOKEN repo secret. Kept independent of build/docker
# so a missing/expired token can't block the GitHub Release itself — this
# job just fails visibly on 401 until the secret is (re)added.
npm-publish:
name: Publish npm packages
needs: release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
# GitHub masks secrets in logs; the token only lands in $HOME/.npmrc on
# the ephemeral runner. ubuntu-latest already ships node + npm.
- name: Authenticate to npm
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
run: echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > "$HOME/.npmrc"
# Downloads all 5 platform binaries from THIS tag's release + stamps
# every package.json under npm/ (wrapper's optionalDependencies pins
# included) to the tag version — fixing any wrapper/platform version
# skew automatically at publish time.
- name: Stage platform binaries + versions from this tag's release
run: npm/stage-release.sh "${GITHUB_REF_NAME}"
# Platform packages first so the wrapper's optionalDependencies are
# already resolvable the instant anyone installs it (npm/README.md).
- name: Publish platform packages, then the wrapper
run: |
for pkg in calm-mcp-linux-x64 calm-mcp-linux-arm64 calm-mcp-darwin-arm64 calm-mcp-darwin-x64 calm-mcp-win32-x64 calm-mcp; do
( cd "npm/$pkg" && npm publish --access public )
done
# Register the just-published version's metadata in the official MCP
# Registry so cold clients (VS Code / Cursor / Claude Code registry search)
# can discover CALM by name. Runs after npm-publish because the registry
# validates that the npm package at this version already exists. Reuses the
# standalone publish-mcp-registry.yml (still runnable by hand for re-pushes)
# via workflow_call so the mcp-publisher logic lives in exactly one place.
mcp-registry:
name: Publish to MCP Registry
needs: npm-publish
permissions:
id-token: write # OIDC auth to the registry — this repo's identity
contents: read
uses: ./.github/workflows/publish-mcp-registry.yml
with:
version: ${{ github.ref_name }}