docs(agents): document the release-crates.yml trigger and the missed-trigger workaround

Bumps the skill to v1.2. Three changes: - Restores the release-crates.yml-driven flow as the default. The previous version's "Step 5: cargo workspaces publish --from-git" was a manual fallback that v0.2.0 used only because the workflow file didn't exist on main yet at tag-push time. The workflow now exists on main and is the documented happy path. - Calls out the tag-push trigger failure mode observed on this forge (v0.2.0 and v0.3.0 first pushes both missed, despite a valid workflow file and trigger filter). Adds a "When the tag push does not fire the workflow" subsection with the gh workflow run command to recover. - Notes that dispatch-distributions now skips when the GitHub release for the tag already exists, so a manual dispatch is safe even if the missed trigger later wakes up and fires the workflow too. Co-Authored-By: OndeCode v1.0.3-acp <noreply@ondeinference.com>

Seto Elkahfi committed Oct 13, 2026 at 20:25 UTC cf73d5125def31ff6bd30d59ef318634af0d8252
1 file changed +129 -40
.agents/skills/ascapi-release/SKILL.md
+129 -40
@@ -3,23 +3,27 @@ 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, 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
@@ -44,32 +48,39 @@ publishes the crates, and republishes the MCP server.
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`.
@@ -125,32 +136,82 @@ grep -n '"version"' server.json # both -> X.Y.Z
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.
@@ -160,23 +221,51 @@ enforces these invariants — a release fails if any is violated:
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