development
md 277 lines 11.9 KB
Rendered Raw
1 ---
2 name: ascapi-release
3 description: >
4 Cut a new release of the smbcloud-ascapi Rust workspace: merge development into
5 main, bump the single workspace version with cargo-workspaces, create the
6 Release commit and matching tags, keep server.json in sync, then push the tag
7 so release-crates.yml publishes the crates to crates.io and dispatches the
8 GitHub and MCP Registry releases. Use whenever the user asks to
9 release, cut a version, tag, bump the version, publish to crates.io, or publish
10 the MCP server for this repo. Covers the forge/remote quirks (GitHub, remote
11 named "setoelkahfi"), the signing identity, the tag-push trigger failure mode
12 that's been observed on this forge, and the registry workflow's hard invariants
13 that will fail a release if violated.
14 metadata:
15 author: smbcloudXYZ
16 version: "1.2"
17 ---
18
19 # smbcloud-ascapi release runbook
20
21 One Rust workspace, one version. The root `Cargo.toml` `[workspace.package]`
22 `version` (around line 15) is the single source of truth — every crate inherits
23 it with `version.workspace = true`. A release bumps that one number and tags
24 it. Pushing the `vX.Y.Z` tag starts `.github/workflows/release-crates.yml`,
25 which publishes the crates and then dispatches the GitHub release and the MCP
26 Registry release.
27
28 ## Facts that are easy to get wrong
29
30 - **Forge is GitHub** (`github.com/smbcloudXYZ`). The `mcp__sigit__*` tools do
31 NOT apply here. Use `git` / `gh` via the shell for any forge work.
32 - **The remote is named `setoelkahfi`, not `origin`.** Push with
33 `git push setoelkahfi main` and `git push setoelkahfi --tags`. Confirm with
34 `git remote -v` before pushing.
35 - **Signing identity** (confirmed, and already the repo's local git config):
36 - `user.email = 1797197+setoelkahfi@users.noreply.github.com`
37 - `user.signingkey = 2B76DF0F04014E36`
38 - A signed Release commit shows `sig=G key=2B76DF0F04014E36` under
39 `git log -1 --format='sig=%G? key=%GK'`. If it doesn't, stop and fix the
40 identity before tagging — do not release unsigned or under the wrong key.
41 (The `seto@smbcloud.xyz` / `B5690EEE…` key is a different, non-release
42 identity — don't use it here.)
43 - **Release tool is `cargo-workspaces`** (binary `cargo workspaces`). Install
44 with `cargo install cargo-workspaces` if missing.
45
46 ## Step 1 — merge `development` into `main`
47
48 ```sh
49 git checkout main
50 git pull setoelkahfi main
51 git merge --no-ff development
52 ```
53
54 Use `--no-ff` so the merge from development produces a merge commit — it
55 makes the release-boundary history legible. Conflicts are usually
56 `Cargo.lock` only; auto-merge almost always wins because the only real
57 changes are new crate entries or new dependency additions (the recent
58 development branches added `tokio` to `mcp`, for instance). Confirm with:
59
60 ```sh
61 cargo check --workspace --locked # MUST pass — this validates Cargo.lock
62 ```
63
64 Other files that have diverged between branches in the past:
65 `release-github.yml`, `release-crates.yml`, `release-mcp-registry.yml`,
66 `.agents/skills/ascapi-release/SKILL.md`. These are usually non-conflicting
67 (one side changed, the other didn't); the merge takes the change. If a real
68 conflict appears, take the version on the branch where the file is currently
69 in production use — for this repo, that's almost always `main` for docs and
70 release-mechanics, and `development` for code.
71
72 ## Step 2 — bump the version and tag
73
74 Pick `patch` / `minor` / `major`. To bump to an explicit version (e.g. when
75 `minor` would compute a version already published to crates.io), use the
76 `custom` form:
77
78 ```sh
79 cargo workspaces version minor \
80 --yes --allow-branch main --no-git-push --force '*'
81 # or:
82 cargo workspaces version custom 0.3.0 \
83 --yes --allow-branch main --no-git-push --force '*'
84 ```
85
86 - `--allow-branch main` because releases go out on `main`.
87 - `--no-git-push` so you control the push (to the `setoelkahfi` remote).
88 - `--force '*'` bumps every crate to the shared version even if cargo-workspaces
89 thinks some are unchanged (single-version workspace).
90
91 This creates the `Release X.Y.Z` commit, the `vX.Y.Z` tag, and **9 per-crate
92 tags** `smbcloud-ascapi-{aso,cli,core,frontend,mcp,play,pricing,reports,signing}@X.Y.Z`,
93 all pointing at the Release commit.
94
95 ## Step 3 — sync `server.json`
96
97 `server.json` is NOT touched by cargo-workspaces. The MCP Registry workflow
98 rewrites it from the tag at publish time, but keep the repo honest by bumping it
99 in the Release commit anyway. Two version fields:
100
101 - top-level `.version`
102 - `.packages[0].version`
103
104 Edit both to the new version, then fold them into the Release commit and move
105 every tag to the new commit:
106
107 ```sh
108 git add server.json
109 git commit --amend --no-edit
110 NEW=$(git rev-parse HEAD)
111 for t in \
112 smbcloud-ascapi-aso@X.Y.Z smbcloud-ascapi-cli@X.Y.Z \
113 smbcloud-ascapi-core@X.Y.Z smbcloud-ascapi-frontend@X.Y.Z \
114 smbcloud-ascapi-mcp@X.Y.Z smbcloud-ascapi-play@X.Y.Z \
115 smbcloud-ascapi-pricing@X.Y.Z \
116 smbcloud-ascapi-reports@X.Y.Z smbcloud-ascapi-signing@X.Y.Z \
117 vX.Y.Z; do
118 git tag -f "$t" "$NEW"
119 done
120 ```
121
122 Beware: any tooling that **amends a co-author trailer** after your commit
123 changes the hash again. After all amends settle, re-point the tags to the final
124 HEAD and verify they all land on one commit:
125
126 ```sh
127 git tag --points-at HEAD # must list all 10 tags
128 git log -1 --format='sig=%G? key=%GK' # must be sig=G key=2B76DF0F04014E36
129 ```
130
131 ## Step 4 — final consistency check, then push
132
133 All three must read the same version, and the locked build must pass:
134
135 ```sh
136 grep -n '"version"' server.json # both -> X.Y.Z
137 sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -1 # -> X.Y.Z
138 git status --short # clean
139 cargo check --workspace --locked
140 ```
141
142 Push the branch first, then the tags. The tag push is what starts the
143 publish, and the workflow checks out the tag, so `main` has to be on the
144 remote before it runs:
145
146 ```sh
147 git push setoelkahfi main
148 git push setoelkahfi --tags --force # --force only if you moved tags in step 3
149 ```
150
151 ## Step 5 — `release-crates.yml` publishes the crates
152
153 Pushing a `vX.Y.Z` tag starts `.github/workflows/release-crates.yml`. The
154 per-crate `smbcloud-ascapi-*@X.Y.Z` tags don't match its `v*.*.*` filter, so
155 they don't start extra runs. Three jobs run in order:
156
157 1. **`verify`** fails the run if the tag differs from the workspace version in
158 `Cargo.toml`, then runs `cargo fmt --check`, clippy with `-D warnings`,
159 `cargo test --workspace --locked`, and `cargo check` on `core`, `aso`,
160 `pricing`, `signing`, `reports` and `play` on their own.
161 2. **`publish`** (environment `crates-io`) publishes `core`, `aso`, `pricing`,
162 `signing`, `reports`, `play`, `frontend`, `mcp`, `cli`, in that order, with
163 `cargo publish -p <crate> --locked`. Authentication is crates.io trusted
164 publishing: the job exchanges its GitHub OIDC token through
165 `rust-lang/crates-io-auth-action`, so there is no registry token secret.
166 A crate version that is already on crates.io is skipped, so re-running the
167 workflow after a partial failure resumes where it stopped.
168 3. **`dispatch-distributions`** checks that the GitHub release for the tag
169 does not already exist, then dispatches `release-github.yml` (six binaries
170 attached to a GitHub release) and `release-mcp-registry.yml`, both with the
171 same tag. The existence check makes this job idempotent: a late or duplicate
172 tag-push trigger can't re-build the binaries or re-publish the MCP server
173 for a release that's already out.
174
175 Watch it with:
176
177 ```sh
178 gh run list --workflow release-crates.yml --limit 3
179 gh run watch <run-id>
180 ```
181
182 ### One-time setup it depends on
183
184 - Each of the nine crates has a trusted publisher on crates.io for repository
185 `smbcloudXYZ/smbcloud-ascapi` and workflow `release-crates.yml`. If a crate
186 is missing one, `cargo publish` fails for that crate and the later crates are
187 not published. Add the publisher and re-run.
188 - A brand-new crate can't get a trusted publisher until it exists on
189 crates.io, so its first version goes up by hand with a token
190 (`cargo publish -p <crate>`), then the publisher is added. `play` went
191 through this at 0.3.0. Add any new crate to the `crates=(...)` list in
192 `release-crates.yml` in dependency order before tagging.
193 - If a trusted publisher names an environment, it must be `crates-io`, which
194 is the environment the `publish` job runs in.
195
196 ### When the tag push does not fire the workflow
197
198 This has been observed on this forge at least once (v0.2.0 and v0.3.0 first
199 pushes both failed to trigger `release-crates.yml`, even though the workflow
200 file was on `main` at HEAD with a valid `push: tags: 'v*.*.*'` filter).
201 The trigger works on a re-push of the same tag, so the webhook delivery is
202 just occasionally missed. If a push doesn't fire the workflow within a couple
203 of minutes:
204
205 ```sh
206 gh run list --workflow release-crates.yml --limit 5
207 # if no recent run for the tag, dispatch by hand:
208 gh workflow run release-crates.yml --ref vX.Y.Z -f tag=vX.Y.Z
209 gh run watch $(gh run list --workflow release-crates.yml --limit 1 --json databaseId -q | jq -r '.[0].databaseId')
210 ```
211
212 The `dispatch-distributions` existence check makes the manual dispatch safe
213 even if the tag-push trigger fires later too: the second run will skip the
214 dispatch step and finish as a no-op.
215
216 ## Step 6 — MCP Registry release
217
218 `release-mcp-registry.yml` is dispatched by step 5, so there is normally
219 nothing to do here. It authenticates to the MCP Registry via GitHub OIDC (no
220 secret) and enforces these invariants. A release fails if any is violated:
221
222 1. **Version match:** root `Cargo.toml` workspace version == tag (minus the `v`).
223 Step 2 guarantees this.
224 2. **Ownership marker in source:** `mcp-name: io.github.smbcloudXYZ/ascapi` must
225 be present in BOTH `README.md` and `crates/cli/README.md`. (The server name
226 comes from `server.json` `.name`.)
227 3. **Ownership marker in the published crate:** the CLI crate downloaded from
228 crates.io for this exact version must contain the same marker in its README.
229 crates.io versions are immutable, so **the marker must be in the README
230 BEFORE you tag**. If a published version lacks it, you cannot fix that
231 version; you must cut a new release.
232
233 The workflow rewrites `server.json`'s version from the tag, publishes with
234 `mcp-publisher` (treating "duplicate version" as success), and verifies the
235 server is listed in the registry. It waits up to ten minutes for crates.io to
236 index the CLI crate, so it is safe that it starts while step 5 is still
237 publishing.
238
239 To publish the server again for a tag, dispatch it yourself:
240
241 ```sh
242 gh workflow run release-mcp-registry.yml --ref vX.Y.Z -f tag=vX.Y.Z
243 ```
244
245 ## Manual fallback
246
247 Only if the workflow can't run (Actions outage, trusted publishing not set up
248 yet, etc.). Publish in dependency order, because the CLI depends on the rest:
249
250 ```sh
251 cargo workspaces publish --from-git --allow-branch main --yes
252 ```
253
254 `--from-git` publishes exactly what the tags describe and skips versions
255 already on crates.io. Then dispatch `release-github.yml` and
256 `release-mcp-registry.yml` yourself with the tag. **After a manual publish,
257 still go back to step 5 the next time around** — the workflow is the source
258 of truth and the manual path exists for emergencies, not as a default.
259
260 ## Release-order summary
261
262 merge → bump+tag → sync server.json into Release commit → verify sig & versions
263 → push `main`, then tags → `release-crates.yml` verifies and publishes → it
264 dispatches the GitHub release and the MCP Registry release. Check the run in
265 Actions; nothing after the tag push is manual — unless the tag-push trigger
266 didn't fire, in which case dispatch `release-crates.yml` by hand and continue.
267
268 ## References
269
270 - `../../../.github/workflows/release-crates.yml` — verify, publish and
271 dispatch, in full.
272 - `../../../.github/workflows/release-github.yml` — the binary build and GitHub
273 release.
274 - `../../../.github/workflows/release-mcp-registry.yml` — the registry workflow
275 and its invariant checks, in full.
276 - Past release design notes live in `.agents/plans/` (upload / signing-identity
277 behaviour) — check there before reworking release mechanics.