release
Releases stmo-cli. Most of the work is scripted via the xtask crate
(cargo xtask --help for the full command list); this skill is the order of
operations and the parts that stay manual by design.
Changelog
Only include user-facing changes. Internal changes (test fixes, code
formatting, dependency updates, CI improvements) should be omitted unless they
affect user-visible behavior. Group entries under ### Features and
### Fixes.
Curate the ## [Unreleased] section's content — this is a human judgment
call — before running prepare-release below. It only dates and renames the
heading; it does not compose the entries.
Remotes
origin is the fork (JohanLorenzo/stmo-cli-fork); upstream is the
canonical repo (mozilla/stmo-cli). A repo-safety hook blocks any push to
upstream — branch or tag — so the PR flow below goes through the fork, and
tagging (step 3) is a manual, human-only step for the same reason.
Steps
cargo xtask prepare-release X.Y.Z— bumpsCargo.toml, dates the CHANGELOGUnreleasedheading, runs the full gate (cargo test/clippy/fmt --check), and commits asRelease X.Y.Zon a newrelease-X.Y.Zbranch. Requires a clean tree onmainsynced withupstream/main.cargo xtask cut-release X.Y.Z— pushesrelease-X.Y.Ztooriginand opens a draft PR againstmozilla/stmo-climain. Review and merge it.- You run this step, not Claude/the skill. After the PR merges, sync
mainand cut the signed tag yourself:
Two separate hard blockers, not a style choice:git checkout main && git fetch upstream && git reset --hard upstream/main git tag -s X.Y.Z -m "X.Y.Z" git push upstream X.Y.Z- Signing needs your GPG key and an interactive passphrase/pinentry prompt — Claude has no way to answer that prompt.
- Pushing to
upstreamis refused outright by the localcheck_push_target.pysafety hook, which blocks any push to a non-fork remote — tag or branch — regardless of who's asking.
- CI (
release.yml) takes over from the tag: it validates the tag looks like a version, creates the GitHub Release with notes extracted straight fromCHANGELOG.md(cargo xtask extract-changelog X.Y.Z— no manualgh release edit --notes), and builds and attaches all 6 target binaries. It does not publish to crates.io — see the note below. - Publish to crates.io yourself:
cargo publish -p stmo-cli. - Verify:
cargo binstall --dry-run stmo-cli. - Only if this release changed a user-facing command, flag, or workflow:
sync the firefox skill — run
scripts/sync-firefox-skill.sh <firefox-checkout> [bug-number](or invoke theupdate-stmo-skillskill) to update bothmozilla-firefox/firefox/.claude/skills/stmo/SKILL.mdandmozilla-firefox/firefox/.agents/skills/stmo/SKILL.md(firefox mirrors the two and enforces they match via itsagent-skills-synclinter) and prepare a moz-phab submission. See.claude/skills/stmo/SKILL.md(vendored canonical copy) and.claude/skills/update-stmo-skill/SKILL.md.
Why crates.io publish isn't in CI
It was, briefly (softprops/action-gh-release + rust-lang/crates-io-auth-action
via OIDC Trusted Publishing), but Mozilla's GitHub org enforces an actions
allowlist (MoCo-GHE-Admin/Approved-GHE-add-ons) and neither
action is on it. A tag push with either in the workflow fails the whole run
at startup_failure before any job even starts — GitHub-first-party actions
(actions/*) and the pre-approved dtolnay/rust-toolchain /
Swatinem/rust-cache are fine, but third-party marketplace actions need a
Mozilla security review first. release.yml now uses the gh CLI
directly (preinstalled on all runner images, authenticated via the built-in
GITHUB_TOKEN) for release creation and asset upload instead — that's a
CLI invocation, not a uses: action, so it isn't subject to the allowlist.
crates.io has no such CLI-only path for Trusted Publishing, so it stays a
manual cargo publish step.
Re-running a tag
crates.io versions are immutable, so re-running cargo publish for an
already-published version fails as expected; the GitHub Release and binaries
still regenerate cleanly from a re-tagged commit.