| 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. |