-
Notifications
You must be signed in to change notification settings - Fork 1
369 lines (326 loc) · 16.4 KB
/
Copy pathrelease.yml
File metadata and controls
369 lines (326 loc) · 16.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
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 }}