chore(release): bump workspace version to 0.5.0 #16
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*'] | |
| 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 | |
| - 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: 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 }} |