Release Process
When to read this: You are about to cut a Chatty release, or a release run did not produce the artifacts you expected.
Closed-system security contract
Only github.repository_owner and github-actions[bot] may arm releases.
Forks and outside collaborators cannot:
- Dispatch
release.yml/prepare-release.ymlmanually - Apply privileged labels (
ship:auto,release:patch|minor|major,cut-release) - Enable ship-auto auto-merge
- Trigger prepare-release from a fork PR head
Linear projects Chatty auto-ship and Chatty tech debt membership is Marcel (and agents he runs). Weekly dependency-check filings go to Linear only.
Emergency rebuild (owner only), when a published release event did not fire.
Dispatch against the tag ref (never from main with only an input):
gh workflow run release.yml --ref vX.Y.Z -f tag_name=vX.Y.Z
release.yml verifies the checked-out commit matches refs/tags/vX.Y.Z, asserts that
commit is an ancestor of origin/main, and matches Cargo.toml before building.
Authz gate regression check: bash scripts/check-release-authz.sh (also run in CI).
Preferred paths (use these)
A. PR merge with a release label (human path)
- Open a PR to
mainwith exactly one ofrelease:patch,release:minor,release:major(owner-applied; unauthorized labelers are stripped byprivileged-labels.yml). - Merge when CI is green.
ship-auto-guardenforces the path deny-list on any release-labeled PR. prepare-release.ymlbumpsCargo.tomlon arelease/vX.Y.ZPR (cut-release, notrelease:patch), merges that PR (main is protected — a direct push gets GH006), generates the changelog, tagsvX.Y.Z, creates the GitHub Release with that changelog as its body, and callsrelease.ymlviaworkflow_callto build artifacts. Runs are serialised by aprepare-releaseconcurrency group so two merges cannot race on the version bump or the tag.
On a PR branch, /create-release patch (or minor/major) only adds the label — it does
not create a tag by hand.
B. Auto-ship (zero-human patch releases)
For low-risk work filed in Linear project Chatty auto-ship or Chatty tech debt
(with ship:auto + owner:ai, not owner:human):
- Agent opens PR: branch
auto/*, titleauto: …, labelsship:auto+release:patch(owner/Actions only). ship-auto-guard.ymlenforces patch-only + path deny-list;ship-auto-merge.ymlenables squash auto-merge only for same-repo heads when the sender is the owner or the actor isgithub-actions[bot]. Latersynchronizeevents from other same-repo actors do not re-arm; if auto-merge is already on, the prepare-release waiter still starts.- When required checks are green, auto-merge squash-merges to
main. The ruleset requires the branch to be current withmain;update-next-queued-pr.ymlpresses “update branch” for the oldest auto-merge-armed PR whenevermainmoves (and every 30 min as a fallback), so PRs land one at a time without a human. It needs theAUTO_UPDATE_TOKENsecret: a fine-grained PAT scoped to this repo with contents + pull-requests write.GITHUB_TOKENcannot be used because its branch updates do not trigger CI. A PR in that queue is also in care ofpr-nurse.yml(hourly, after any failed CI run, or on dispatch):scripts/pr-nurse-triage.shpicks one PR that is conflicted withmainor has a red required check; a Sonnet session resolves the conflict or fixes the check without committing; the workflow validates the tree (no conflict markers, none of.github/workflows/,RESERVED.md,scripts/check-*.sh,.cursor/rules/touched) and pushes anurse:commit with the same token. After two nurse commits on one PR,scripts/pr-nurse-decide.sh(AGE-705) decides what happens next from CI on that second push: still pending, it waits; green, it clears the attempt budget and leaves auto-merge armed; red, it disarms auto-merge and labelsblocked:human. Removingblocked:humanby hand also resets the budget, so the next run does not immediately re-label it. Arming auto-merge is the only way into care; the nurse never merges here. - If Actions performed the squash (
GITHUB_TOKENdoes not emitpull_request.closed),ship-auto-mergedispatchesprepare-release(bump=patch). Owner merges still use thepull_requestclosed path. - Same
prepare-release→ bump PR → tag →release.ymlpipeline as (A).
Never hand-tag for auto-ship.
Deprecated for routine releases: GitHub UI release
release.yml has three triggers: the workflow_call from Prepare Release, an
owner-only workflow_dispatch (the emergency rebuild above), and the published
event of a GitHub Release. There is no tag-push trigger: pushing a vX.Y.Z tag by
hand builds nothing.
Publishing a release from the GitHub UI still works and is the way to ship custom
release notes, but hand-editing Cargo.toml is easy to desync from the tag. Prefer
(A) or (B).
-
Update version in Cargo.toml
vim Cargo.toml # Set version = "0.1.21" git add Cargo.toml git commit -m "Bump version to 0.1.21" git push origin main -
Create release via GitHub UI:
- Go to: https://github.com/boersmamarcel/chatty2/releases/new
- Choose tag:
v0.1.21(or create new tag) - Target:
mainbranch - Release title:
v0.1.21or custom title - Description: Write custom release notes
- Important: Click “Publish release” (NOT “Save as draft”) — only the
publishedevent triggers the workflow
-
Workflow runs automatically:
- Validates the tag against
Cargo.tomlandorigin/main - Builds for all platforms
- Uploads artifacts to your release, preserving your release notes
- Validates the tag against
What Gets Built
Each release includes:
- Linux:
chatty-linux-x86_64.AppImage - macOS:
chatty-macos-aarch64.dmg(ARM, code-signed if secrets configured) - Windows:
chatty-windows-x86_64.exe(Inno Setup installer) - Checksums:
checksums.txt(SHA-256 for all files) - Release signature:
checksums.txt.sig, an Ed25519 (minisign) signature overchecksums.txt, onceRELEASE_SIGNING_ENABLEDis on — seerelease-signing.md
The desktop app’s auto-updater (crates/chatty-gpui/src/auto_updater/) polls the
GitHub releases API for these assets, so a published release reaches existing
installs without any further step. It verifies checksums.txt.sig against the
release keys compiled into it before it trusts a hash, and on macOS checks the new
bundle’s code signature and Team ID before the swap.
Version Validation
The workflow validates that:
- Tag version (e.g.,
v0.1.21→0.1.21) matchesCargo.tomlversion - The tag commit is an ancestor of
origin/main - If either check fails, the workflow fails with an error
Fix version mismatches:
# If you accidentally created tag v0.1.21 but Cargo.toml has 0.1.20:
# Option 1: Update Cargo.toml and re-tag
vim Cargo.toml # Change to 0.1.21
git add Cargo.toml
git commit -m "Fix version mismatch"
git push origin main
git tag -d v0.1.21 # Delete local tag
git push origin :v0.1.21 # Delete remote tag
git tag v0.1.21 # Recreate tag
git push origin v0.1.21
# Option 2: Use correct version tag
git tag v0.1.20 # Tag matching Cargo.toml
git push origin v0.1.20
Code Signing (macOS)
See scripts/setup-codesigning.md for details.
Quick summary:
- Without secrets: Ad-hoc signed (works locally, Gatekeeper warnings)
- With secrets: Developer ID signed (no warnings, notarization optional)
Required secrets (optional):
MACOS_CERTIFICATE- Base64 P12 certificateMACOS_CERTIFICATE_PASSWORD- P12 passwordKEYCHAIN_PASSWORD- Random keychain passwordMACOS_SIGNING_IDENTITY- Certificate name
Troubleshooting
Workflow fails at validation
Error: “Version mismatch! Tag version (X.Y.Z) does not match Cargo.toml version (A.B.C)” or “Tag … is not an ancestor of origin/main”
Fix: See “Version Validation” section above
Assets not uploaded
Common causes:
- Build job failed (check Linux/macOS/Windows build logs)
- Artifact upload failed (check artifact names match)
fail_on_unmatched_files: true- files missing- Prepare Release created the GitHub Release but
release.ymldid not build. Reusable workflows inherit the caller’sgithub.event_name, so the reusable-call path is marked with an explicitfrom_prepare: trueinput rather than keyed off the event name.
Rebuild an existing tag (owner only; runs the workflow from that tag):
gh workflow run release.yml --ref vX.Y.Z -f tag_name=vX.Y.Z
Debug:
- Check the “Prepare release assets” step for warnings and the file list
- Ensure build scripts produced expected files
macOS code signing fails
Error: “No identity found” or certificate import fails
Check:
- Secrets are correctly set (base64 encoding, no extra newlines)
- Certificate hasn’t expired
- Keychain password is correct
Fallback: Remove/comment out the “Import code signing certificate” step to use ad-hoc signing
Monitoring Releases
- Actions: https://github.com/boersmamarcel/chatty2/actions/workflows/release.yml
- Releases: https://github.com/boersmamarcel/chatty2/releases
Best Practices
- Let Prepare Release bump the version — hand-edit
Cargo.tomlonly for a UI release - Use semantic versioning:
MAJOR.MINOR.PATCH(e.g.,0.1.21,1.0.0) - Tag format: Use
vprefix (v0.1.21) for consistency - Release notes: Use the GitHub UI path for releases that need hand-written notes
- Test locally first: Run
cargo build --releaseand./scripts/package-macos.shbefore releasing - Check Actions: Monitor the workflow to ensure all platforms build successfully
Not yet automated
- macOS Intel builds (only
aarch64is built) - Windows code signing (the installer is unsigned)