Skip to content

chore(release): bump workspace version to 0.3.6 (#41) #14

chore(release): bump workspace version to 0.3.6 (#41)

chore(release): bump workspace version to 0.3.6 (#41) #14

Workflow file for this run

name: Release
on:
push:
tags: ['v*']
permissions:
contents: write
jobs:
build:
name: Build ${{ matrix.target }}
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
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 }}