Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
244 changes: 244 additions & 0 deletions .agents/skills/ascapi-release/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
---
name: ascapi-release
description: >
Cut a new release of the smbcloud-ascapi Rust workspace: merge development into
main, bump the single workspace version with cargo-workspaces, create the
Release commit and matching tags, keep server.json in sync, then push the tag
so release-crates.yml publishes the crates to crates.io and dispatches the
GitHub and MCP Registry releases. Use whenever the user asks to
release, cut a version, tag, bump the version, publish to crates.io, or publish
the MCP server for this repo. Covers the forge/remote quirks (GitHub, remote
named "setoelkahfi"), the signing identity, and the registry workflow's
hard invariants that will fail a release if violated.
metadata:
author: smbcloudXYZ
version: "1.1"
---

# smbcloud-ascapi release runbook

One Rust workspace, one version. The root `Cargo.toml` `[workspace.package]`
`version` (around line 15) is the single source of truth β€” every crate inherits
it with `version.workspace = true`. A release bumps that one number and tags
it. Pushing the `vX.Y.Z` tag starts `.github/workflows/release-crates.yml`,
which publishes the crates and then dispatches the GitHub release and the MCP
Registry release.

## Facts that are easy to get wrong

- **Forge is GitHub** (`github.com/smbcloudXYZ`). The `mcp__sigit__*` tools do
NOT apply here. Use `git` / `gh` via the shell for any forge work.
- **The remote is named `setoelkahfi`, not `origin`.** Push with
`git push setoelkahfi main` and `git push setoelkahfi --tags`. Confirm with
`git remote -v` before pushing.
- **Signing identity** (confirmed, and already the repo's local git config):
- `user.email = 1797197+setoelkahfi@users.noreply.github.com`
- `user.signingkey = 2B76DF0F04014E36`
- A signed Release commit shows `sig=G key=2B76DF0F04014E36` under
`git log -1 --format='sig=%G? key=%GK'`. If it doesn't, stop and fix the
identity before tagging β€” do not release unsigned or under the wrong key.
(The `seto@smbcloud.xyz` / `B5690EEE…` key is a different, non-release
identity β€” don't use it here.)
- **Release tool is `cargo-workspaces`** (binary `cargo workspaces`). Install
with `cargo install cargo-workspaces` if missing.

## Step 1 β€” merge `development` into `main`

```sh
git checkout main
git pull setoelkahfi main
git merge development
```

The only conflict you're likely to hit is `Cargo.lock`, and it's almost always
just package-entry **ordering** (new crates shift lines around), not a real
dependency divergence. Resolve by taking development's lockfile, then prove it:

```sh
git checkout development -- Cargo.lock # if the conflict is only ordering
git add Cargo.lock
git commit # finish the merge
cargo check --workspace --locked # MUST pass β€” this validates Cargo.lock
```

If `--locked` fails, the lock really did diverge; regenerate with
`cargo check --workspace` and commit the result instead.

## Step 2 β€” bump the version and tag

Pick `patch` / `minor` / `major`. This repo keeps the Release commit body as the
plain cargo-workspaces default (`Release X.Y.Z` subject, per-crate lines, and a
`Generated by cargo-workspaces` footer) β€” don't hand-edit it.

```sh
cargo workspaces version minor \
--yes --allow-branch main --no-git-push --force '*'
```

- `--allow-branch main` because releases go out on `main`.
- `--no-git-push` so you control the push (to the `setoelkahfi` remote).
- `--force '*'` bumps every crate to the shared version even if cargo-workspaces
thinks some are unchanged (single-version workspace).

This creates the `Release X.Y.Z` commit, the `vX.Y.Z` tag, and **8 per-crate
tags** `smbcloud-ascapi-{aso,cli,core,frontend,mcp,pricing,reports,signing}@X.Y.Z`,
all pointing at the Release commit.

## Step 3 β€” sync `server.json`

`server.json` is NOT touched by cargo-workspaces. The MCP Registry workflow
rewrites it from the tag at publish time, but keep the repo honest by bumping it
in the Release commit anyway. Two version fields:

- top-level `.version`
- `.packages[0].version`

Edit both to the new version, then fold them into the Release commit and move
every tag to the new commit:

```sh
git add server.json
git commit --amend --no-edit
NEW=$(git rev-parse HEAD)
for t in \
smbcloud-ascapi-aso@X.Y.Z smbcloud-ascapi-cli@X.Y.Z \
smbcloud-ascapi-core@X.Y.Z smbcloud-ascapi-frontend@X.Y.Z \
smbcloud-ascapi-mcp@X.Y.Z smbcloud-ascapi-pricing@X.Y.Z \
smbcloud-ascapi-reports@X.Y.Z smbcloud-ascapi-signing@X.Y.Z \
vX.Y.Z; do
git tag -f "$t" "$NEW"
done
```

Beware: any tooling that **amends a co-author trailer** after your commit
changes the hash again. After all amends settle, re-point the tags to the final
HEAD and verify they all land on one commit:

```sh
git tag --points-at HEAD # must list all 9 tags
git log -1 --format='sig=%G? key=%GK' # must be sig=G key=2B76DF0F04014E36
```

## Step 4 β€” final consistency check, then push

All three must read the same version, and the locked build must pass:

```sh
grep -n '"version"' server.json # both -> X.Y.Z
sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -1 # -> X.Y.Z
git status --short # clean
cargo check --workspace --locked
```

Push the branch first, then the tags. The tag push is what starts the
publish, and the workflow checks out the tag, so `main` has to be on the
remote before it runs:

```sh
git push setoelkahfi main
git push setoelkahfi --tags --force # --force only if you moved tags in step 3
```

## Step 5 β€” `release-crates.yml` publishes the crates

Pushing a `vX.Y.Z` tag starts `.github/workflows/release-crates.yml`. The
per-crate `smbcloud-ascapi-*@X.Y.Z` tags don't match its `v*.*.*` filter, so
they don't start extra runs. Three jobs run in order:

1. **`verify`** fails the run if the tag differs from the workspace version in
`Cargo.toml`, then runs `cargo fmt --check`, clippy with `-D warnings`,
`cargo test --workspace --locked`, and `cargo check` on `core`, `aso`,
`pricing`, `signing` and `reports` on their own.
2. **`publish`** (environment `crates-io`) publishes `core`, `aso`, `pricing`,
`signing`, `reports`, `frontend`, `mcp`, `cli`, in that order, with
`cargo publish -p <crate> --locked`. Authentication is crates.io trusted
publishing: the job exchanges its GitHub OIDC token through
`rust-lang/crates-io-auth-action`, so there is no registry token secret.
A crate version that is already on crates.io is skipped, so re-running the
workflow after a partial failure resumes where it stopped.
3. **`dispatch-distributions`** dispatches `release-github.yml` (six binaries
attached to a GitHub release) and `release-mcp-registry.yml`, both with the
same tag. It only runs when `publish` succeeded.

Watch it with:

```sh
gh run list --workflow release-crates.yml --limit 3
gh run watch <run-id>
```

To run it by hand for an existing tag (for example after fixing a trusted
publisher config), dispatch it against that tag:

```sh
gh workflow run release-crates.yml --ref vX.Y.Z -f tag=vX.Y.Z
```

### One-time setup it depends on

- Each of the eight crates has a trusted publisher on crates.io for repository
`smbcloudXYZ/smbcloud-ascapi` and workflow `release-crates.yml`. If a crate
is missing one, `cargo publish` fails for that crate and the later crates are
not published. Add the publisher and re-run.
- If a trusted publisher names an environment, it must be `crates-io`, which
is the environment the `publish` job runs in.

## Step 6 β€” MCP Registry release

`release-mcp-registry.yml` is dispatched by step 5, so there is normally
nothing to do here. It authenticates to the MCP Registry via GitHub OIDC (no
secret) and enforces these invariants. A release fails if any is violated:

1. **Version match:** root `Cargo.toml` workspace version == tag (minus the `v`).
Step 2 guarantees this.
2. **Ownership marker in source:** `mcp-name: io.github.smbcloudXYZ/ascapi` must
be present in BOTH `README.md` and `crates/cli/README.md`. (The server name
comes from `server.json` `.name`.)
3. **Ownership marker in the published crate:** the CLI crate downloaded from
crates.io for this exact version must contain the same marker in its README.
crates.io versions are immutable, so **the marker must be in the README
BEFORE you tag**. If a published version lacks it, you cannot fix that
version; you must cut a new release.

The workflow rewrites `server.json`'s version from the tag, publishes with
`mcp-publisher` (treating "duplicate version" as success), and verifies the
server is listed in the registry. It waits up to ten minutes for crates.io to
index the CLI crate, so it is safe that it starts while step 5 is still
publishing.

To publish the server again for a tag, dispatch it yourself:

```sh
gh workflow run release-mcp-registry.yml --ref vX.Y.Z -f tag=vX.Y.Z
```

## Manual fallback

Only if the workflow can't run (Actions outage, trusted publishing not set up
yet). Publish in dependency order, because the CLI depends on the rest:

```sh
cargo workspaces publish --from-git --allow-branch main --yes
```

`--from-git` publishes exactly what the tags describe and skips versions
already on crates.io. Then dispatch `release-github.yml` and
`release-mcp-registry.yml` yourself with the tag.

## Release-order summary

merge β†’ bump+tag β†’ sync server.json into Release commit β†’ verify sig & versions
β†’ push `main`, then tags β†’ `release-crates.yml` verifies and publishes β†’ it
dispatches the GitHub release and the MCP Registry release. Check the run in
Actions; nothing after the tag push is manual.

## References

- `../../../.github/workflows/release-crates.yml` β€” verify, publish and
dispatch, in full.
- `../../../.github/workflows/release-github.yml` β€” the binary build and GitHub
release.
- `../../../.github/workflows/release-mcp-registry.yml` β€” the registry workflow
and its invariant checks, in full.
- Past release design notes live in `.agents/plans/` (upload / signing-identity
behaviour) β€” check there before reworking release mechanics.
1 change: 1 addition & 0 deletions .claude/skills
Loading