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, publish the crates
7
- to crates.io, and run the MCP Registry workflow. Use whenever the user asks to
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
10
- named "setoelkahfi"), the signing identity, and the registry workflow's
11
- hard invariants that will fail a release if violated.
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
14
- version: "1.0"
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
21
-it with `version.workspace = true`. A release bumps that one number, tags it,
22
-publishes the crates, and republishes the MCP server.
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
48
```sh
49
git checkout main
50
git pull setoelkahfi main
47
-git merge development
51
+git merge --no-ff development
52
```
53
50
-The only conflict you're likely to hit is `Cargo.lock`, and it's almost always
51
-just package-entry **ordering** (new crates shift lines around), not a real
52
-dependency divergence. Resolve by taking development's lockfile, then prove it:
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
55
-git checkout development -- Cargo.lock # if the conflict is only ordering
56
-git add Cargo.lock
57
-git commit # finish the merge
61
cargo check --workspace --locked # MUST pass — this validates Cargo.lock
62
```
63
61
-If `--locked` fails, the lock really did diverge; regenerate with
62
-`cargo check --workspace` and commit the result instead.
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
66
-Pick `patch` / `minor` / `major`. This repo keeps the Release commit body as the
67
-plain cargo-workspaces default (`Release X.Y.Z` subject, per-crate lines, and a
68
-`Generated by cargo-workspaces` footer) — don't hand-edit it.
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`.
136
sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -1 # -> X.Y.Z
137
git status --short # clean
138
cargo check --workspace --locked
139
+```
140
+
141
+Push the branch first, then the tags. The tag push is what starts the
142
+publish, and the workflow checks out the tag, so `main` has to be on the
143
+remote before it runs:
144
+
145
+```sh
146
git push setoelkahfi main
147
git push setoelkahfi --tags --force # --force only if you moved tags in step 3
148
```
149
132
-## Step 5 — publish crates to crates.io
133
-
134
-Publish in dependency order (lower crates first), because the MCP Registry
135
-workflow verifies the **published** CLI crate, and the CLI depends on the rest:
150
+## Step 5 — `release-crates.yml` publishes the crates
151
+
152
+Pushing a `vX.Y.Z` tag starts `.github/workflows/release-crates.yml`. The
153
+per-crate `smbcloud-ascapi-*@X.Y.Z` tags don't match its `v*.*.*` filter, so
154
+they don't start extra runs. Three jobs run in order:
155
+
156
+1. **`verify`** fails the run if the tag differs from the workspace version in
157
+ `Cargo.toml`, then runs `cargo fmt --check`, clippy with `-D warnings`,
158
+ `cargo test --workspace --locked`, and `cargo check` on `core`, `aso`,
159
+ `pricing`, `signing` and `reports` on their own.
160
+2. **`publish`** (environment `crates-io`) publishes `core`, `aso`, `pricing`,
161
+ `signing`, `reports`, `frontend`, `mcp`, `cli`, in that order, with
162
+ `cargo publish -p <crate> --locked`. Authentication is crates.io trusted
163
+ publishing: the job exchanges its GitHub OIDC token through
164
+ `rust-lang/crates-io-auth-action`, so there is no registry token secret.
165
+ A crate version that is already on crates.io is skipped, so re-running the
166
+ workflow after a partial failure resumes where it stopped.
167
+3. **`dispatch-distributions`** checks that the GitHub release for the tag
168
+ does not already exist, then dispatches `release-github.yml` (six binaries
169
+ attached to a GitHub release) and `release-mcp-registry.yml`, both with the
170
+ same tag. The existence check makes this job idempotent: a late or duplicate
171
+ tag-push trigger can't re-build the binaries or re-publish the MCP server
172
+ for a release that's already out.
173
+
174
+Watch it with:
175
176
+```sh
177
+gh run list --workflow release-crates.yml --limit 3
178
+gh run watch <run-id>
179
```
138
-core → aso / pricing / signing / reports → frontend → mcp → cli
139
-```
180
+
181
+### One-time setup it depends on
182
+
183
+- Each of the eight crates has a trusted publisher on crates.io for repository
184
+ `smbcloudXYZ/smbcloud-ascapi` and workflow `release-crates.yml`. If a crate
185
+ is missing one, `cargo publish` fails for that crate and the later crates are
186
+ not published. Add the publisher and re-run.
187
+- If a trusted publisher names an environment, it must be `crates-io`, which
188
+ is the environment the `publish` job runs in.
189
+
190
+### When the tag push does not fire the workflow
191
+
192
+This has been observed on this forge at least once (v0.2.0 and v0.3.0 first
193
+pushes both failed to trigger `release-crates.yml`, even though the workflow
194
+file was on `main` at HEAD with a valid `push: tags: 'v*.*.*'` filter).
195
+The trigger works on a re-push of the same tag, so the webhook delivery is
196
+just occasionally missed. If a push doesn't fire the workflow within a couple
197
+of minutes:
198
199
```sh
142
-cargo workspaces publish --from-git --allow-branch main --yes
200
+gh run list --workflow release-crates.yml --limit 5
201
+# if no recent run for the tag, dispatch by hand:
202
+gh workflow run release-crates.yml --ref vX.Y.Z -f tag=vX.Y.Z
203
+gh run watch $(gh run list --workflow release-crates.yml --limit 1 --json databaseId -q | jq -r '.[0].databaseId')
204
```
205
145
-`--from-git` publishes exactly what the tags describe and skips versions already
146
-on crates.io (safe to re-run). If you publish manually with `cargo publish -p`,
147
-respect the order above and wait for each to index before the next.
206
+The `dispatch-distributions` existence check makes the manual dispatch safe
207
+even if the tag-push trigger fires later too: the second run will skip the
208
+dispatch step and finish as a no-op.
209
149
-## Step 6 — publish the MCP server
210
+## Step 6 — MCP Registry release
211
151
-Dispatch `.github/workflows/release-mcp-registry.yml` with the tag (e.g.
152
-`vX.Y.Z`). It authenticates to the MCP Registry via GitHub OIDC (no secret) and
153
-enforces these invariants — a release fails if any is violated:
212
+`release-mcp-registry.yml` is dispatched by step 5, so there is normally
213
+nothing to do here. It authenticates to the MCP Registry via GitHub OIDC (no
214
+secret) and enforces these invariants. A release fails if any is violated:
215
216
1. **Version match:** root `Cargo.toml` workspace version == tag (minus the `v`).
217
Step 2 guarantees this.
221
3. **Ownership marker in the published crate:** the CLI crate downloaded from
222
crates.io for this exact version must contain the same marker in its README.
223
crates.io versions are immutable, so **the marker must be in the README
163
- BEFORE you publish the crate in step 5** — if a published version lacks it,
164
- you cannot fix that version; you must cut a new release. This is why step 5
165
- precedes step 6.
224
+ BEFORE you tag**. If a published version lacks it, you cannot fix that
225
+ version; you must cut a new release.
226
227
The workflow rewrites `server.json`'s version from the tag, publishes with
228
`mcp-publisher` (treating "duplicate version" as success), and verifies the
169
-server is listed in the registry.
229
+server is listed in the registry. It waits up to ten minutes for crates.io to
230
+index the CLI crate, so it is safe that it starts while step 5 is still
231
+publishing.
232
+
233
+To publish the server again for a tag, dispatch it yourself:
234
+
235
+```sh
236
+gh workflow run release-mcp-registry.yml --ref vX.Y.Z -f tag=vX.Y.Z
237
+```
238
+
239
+## Manual fallback
240
+
241
+Only if the workflow can't run (Actions outage, trusted publishing not set up
242
+yet, etc.). Publish in dependency order, because the CLI depends on the rest:
243
+
244
+```sh
245
+cargo workspaces publish --from-git --allow-branch main --yes
246
+```
247
+
248
+`--from-git` publishes exactly what the tags describe and skips versions
249
+already on crates.io. Then dispatch `release-github.yml` and
250
+`release-mcp-registry.yml` yourself with the tag. **After a manual publish,
251
+still go back to step 5 the next time around** — the workflow is the source
252
+of truth and the manual path exists for emergencies, not as a default.
253
254
## Release-order summary
255
256
merge → bump+tag → sync server.json into Release commit → verify sig & versions
174
-→ push main+tags → **publish crates (step 5)** → **then** run MCP Registry
175
-workflow (step 6). Steps 5 and 6 are order-dependent; don't swap them.
257
+→ push `main`, then tags → `release-crates.yml` verifies and publishes → it
258
+dispatches the GitHub release and the MCP Registry release. Check the run in
259
+Actions; nothing after the tag push is manual — unless the tag-push trigger
260
+didn't fire, in which case dispatch `release-crates.yml` by hand and continue.
261
262
## References
263
264
+- `../../../.github/workflows/release-crates.yml` — verify, publish and
265
+ dispatch, in full.
266
+- `../../../.github/workflows/release-github.yml` — the binary build and GitHub
267
+ release.
268
- `../../../.github/workflows/release-mcp-registry.yml` — the registry workflow
269
and its invariant checks, in full.
270
- Past release design notes live in `.agents/plans/` (upload / signing-identity
182
- behaviour) — check there before reworking release mechanics.
271
+ behaviour) — check there before reworking release mechanics.
\ No newline at end of file