Skip to content

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

  1. Actions tab → "Cut release" → Run workflow. Pick a bump level (patch / minor / major), or type an explicit version.
  2. Review and merge the release: vX.Y.Z PR it opens.
  3. 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:

WorkflowTriggerJob
cut-release.ymlmanual (workflow_dispatch)Bumps the workspace version + Cargo.lock, opens a release: vX.Y.Z PR.
tag-release.ymlpush to mainWhen the version actually changes, pushes the matching vX.Y.Z tag.
release.ymlpush of a v* tagdist 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 + publishes

The 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:

  • patchX.Y.(Z+1)
  • minorX.(Y+1).0
  • major(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.xz and dootdoot-x86_64-apple-darwin.tar.xz (each carrying the binary plus LICENSE and README.md),
  • a standalone dootdoot-update updater per arch,
  • dootdoot-installer.sh (shell installer) and dootdoot.rb (Homebrew formula),
  • source.tar.gz, a sha256.sum, and per-artifact .sha256 checksums,
  • 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 with contents: write on this repo. tag-release.yml pushes the tag with it. This is mandatory: the default GITHUB_TOKEN deliberately cannot trigger another workflow, so a tag pushed with it would never start release.yml. If this secret is missing, tag-release.yml fails loudly rather than tagging silently with no build.
  • HOMEBREW_TAP_TOKEN — a token with write access to skeswa/homebrew-tap, used by the Homebrew publish job in release.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-tap

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

Every 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.yml only acts when the version changed; confirm the merge actually changed [workspace.package]version. If it did, check that RELEASE_TOKEN is set and unexpired — a missing/expired PAT is the usual cause of "tag exists but release.yml never ran".
  • release.yml ran but Homebrew publishing failed. Check HOMEBREW_TAP_TOKEN and that skeswa/homebrew-tap exists and is writable.
  • dist plan says release.yml is out of date. Someone hand-edited the generated file; run dist generate to 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.

Independent open-source droid acoustics. Not affiliated with Lucasfilm or Disney.