Releasing
How a dootdoot release is cut, built, and published. Scope is macOS only (Apple Silicon + Intel) by design for now; see packaging.md for the install side and the dist configuration.
The short version
- Actions tab → "Cut release" → Run workflow. Pick a bump level (
patch/minor/major), or type an explicit version. - Review and merge the
release: vX.Y.ZPR it opens. - That's it. The tag is pushed automatically and the artifacts build and publish on their own.
You take two actions (dispatch, merge); everything else is automated.
The pipeline
Three workflows chain together. Each does one job:
| Workflow | Trigger | Job |
|---|---|---|
cut-release.yml | manual (workflow_dispatch) | Bumps the workspace version + Cargo.lock, opens a release: vX.Y.Z PR. |
tag-release.yml | push to main | When the version actually changes, pushes the matching vX.Y.Z tag. |
release.yml | push of a v* tag | dist builds the macOS artifacts and publishes the GitHub Release + Homebrew formula. |
text
Cut release (dispatch) ──> release: vX.Y.Z PR ──(you merge)──> main
│
tag-release.yml detects the bump
│
pushes tag vX.Y.Z
│
release.yml (dist) builds + publishesThe version bump travels through a PR because main is branch-protected (direct pushes are blocked). tag-release.yml keys on an actual Cargo.toml version change (it diffs HEAD~1), not the commit message, so it works regardless of whether the PR is squashed, rebased, or merged with a merge commit.
Versioning
The single source of truth is version under [workspace.package] in the root Cargo.toml; all crates inherit it. cut-release.yml computes the next value:
patch→X.Y.(Z+1)minor→X.(Y+1).0major→(X+1).0.0- or the exact version you type in the version input (overrides the level).
The tag is always v + that version (e.g. v0.2.0), which matches both the release.yml tag filter and the Homebrew formula URLs.
What gets published
dist (cargo-dist) produces, for each tagged release:
dootdoot-aarch64-apple-darwin.tar.xzanddootdoot-x86_64-apple-darwin.tar.xz(each carrying the binary plusLICENSEandREADME.md),- a standalone
dootdoot-updateupdater per arch, dootdoot-installer.sh(shell installer) anddootdoot.rb(Homebrew formula),source.tar.gz, asha256.sum, and per-artifact.sha256checksums,- GitHub build attestations (
github-attestations = true).
Only the dootdoot binary ships — packages = ["dootdoot"] keeps the build-time xtask crate (and its ML stack) out of every release.
Both macOS target builds are routed to the dootdoot-macos-arm64 self-hosted Apple Silicon runner. The Intel archive is cross-built from that Apple Silicon host, so the runner must stay online, have that GitHub Actions label, and have working Rust plus Xcode command-line tools.
Required repository secrets
Both must exist in Settings → Secrets and variables → Actions:
RELEASE_TOKEN— a fine-grained PAT withcontents: writeon this repo.tag-release.ymlpushes the tag with it. This is mandatory: the defaultGITHUB_TOKENdeliberately cannot trigger another workflow, so a tag pushed with it would never startrelease.yml. If this secret is missing,tag-release.ymlfails loudly rather than tagging silently with no build.HOMEBREW_TAP_TOKEN— a token with write access toskeswa/homebrew-tap, used by the Homebrew publish job inrelease.yml.
sh
gh secret set RELEASE_TOKEN # PAT with contents: write on skeswa/dootdoot
gh secret set HOMEBREW_TAP_TOKEN # token with write on skeswa/homebrew-tapFirst release
The automation triggers on a version change. To ship the current version without bumping it, push the tag once by hand (using a PAT so the build fires):
sh
git tag v0.1.0
git push origin v0.1.0Every release after that should go through Cut release.
dist is the source of truth
release.yml is generated by dist; do not hand-edit it. Its configuration lives in [workspace.metadata.dist] (and the SHA pins in [workspace.metadata.dist.github-action-commits]) in the root Cargo.toml. After changing that config or upgrading dist, regenerate and commit:
sh
dist generate # rewrites .github/workflows/release.yml from config
dist plan # shows the planned release; must report no "out of date"release.yml is intentionally excluded from oxfmt (.oxfmtrc.jsonignorePatterns) so the formatter and dist never fight over it.
Verifying locally before a release
sh
scripts/release-smoke # packaging smoke checks
dist plan # what CI will build, no building
dist build # build + package for the host arch into target/distrib/dist build is the strongest local check: it produces the real artifacts, and you can extract the tarball and run the binary (dootdoot --version) to confirm the packaged build is sound.
Troubleshooting
- The bump PR merged but no release built.
tag-release.ymlonly acts when the version changed; confirm the merge actually changed[workspace.package]version. If it did, check thatRELEASE_TOKENis set and unexpired — a missing/expired PAT is the usual cause of "tag exists butrelease.ymlnever ran". release.ymlran but Homebrew publishing failed. CheckHOMEBREW_TAP_TOKENand thatskeswa/homebrew-tapexists and is writable.dist plansaysrelease.ymlis out of date. Someone hand-edited the generated file; rundist generateto restore it from config.- Need to re-run a release for an existing tag. Delete the tag and Release, then push the tag again (with a PAT), or cut a new patch version.