name: release-swift
description: Cut a release of OpenUsage (Swift menu-bar app): pick a version, generate a categorized changelog, tag from main, and publish the GitHub Release with notes. Use to ship an Early Access beta or a stable release.
Release Swift
Pushing a v* tag on main runs .github/workflows/release.yml, which builds, signs, notarizes, attaches OpenUsage-<version>.dmg to the GitHub Release, and updates the Sparkle appcast.xml on gh-pages. CI creates the release with an EMPTY body, so this skill generates the changelog, records it in CHANGELOG.md, and publishes the notes onto the release.
Channels
- Beta (Early Access): suffixed tag like
v0.7.1-beta.1. Marked a GitHub pre-release and added to Sparkle's beta channel. Only users with Early Access enabled get it; GitHub "Latest" is untouched.
- Stable: plain tag like
v0.7.1. Marked non-prerelease, becomes GitHub "Latest", and ships to everyone.
The tag IS the version: v0.7.1-beta.1 becomes CFBundleShortVersionString = 0.7.1-beta.1, and CFBundleVersion is the git commit count. There are no version files to bump.
Cutting a release
1. Choose the version
Next number in the current lane (default bump: patch). Beta builds add a -beta.N suffix. Confirm with the owner before proceeding.
2. Generate the changelog
Collect commits since the previous release in the same channel and categorize each:
- Stable cut: span from the last stable tag to this one (e.g.
v0.7.0...v0.7.1), so the notes roll up the entire beta series plus any post-beta commits. Never start a stable changelog at the last beta — that would omit every beta in the lane.
- Beta cut: span from the previous tag (the prior beta, or the last stable if it's the first beta in a lane) to this one.
| Commit prefix | Category |
|---|
feat, feature, or starts with "Add" | New Features |
fix or starts with "Fix" | Bug Fixes |
refactor, enhance | Refactor |
chore, style, docs, perf, test, ci, build | Chores |
| Uncategorized | Bug Fixes |
Author attribution (required on every entry):
- With a PR number
(#123): gh pr view 123 --json author -q '.author.login'.
- Without a PR number:
gh api /repos/robinebers/openusage/commits/{full_hash} -q '.author.login'.
- If the API returns null, fall back to the git author name.
Output the changelog in a code block (template below) for review.
3. Owner approval
Wait for explicit approval of the changelog before changing any files. Accept edits if offered.
4. Record it in CHANGELOG.md
Prepend the approved section right after the # Changelog header. Commit on main:
git switch main && git pull
git add CHANGELOG.md && git commit -m "docs: changelog for v{version}"
5. Tag and push
git tag -a v{version} -m "v{version}"
git push origin main
git push origin v{version}
6. Publish the notes
CI creates the release with an empty body, so attach the approved notes after it finishes:
gh run watch
gh release view v{version} >/dev/null 2>&1
gh release edit v{version} --notes-file /tmp/notes-v{version}.md
Never leave a release blank.
7. Verify (never leave a draft)
gh release view v{version} --json isDraft,isPrerelease,assets,body \
--jq '{isDraft, isPrerelease, assets:[.assets[].name], bodyLen:(.body|length)}'
git fetch origin gh-pages && git show origin/gh-pages:appcast.xml | grep -F "OpenUsage-{version}.dmg"
curl -s "https://robinebers.github.io/openusage/appcast.xml" | grep -F "OpenUsage-{version}.dmg"
The second check matters: publishing is two hops — Release (or pricing-supplement) pushes appcast.xml to the gh-pages branch, then .github/workflows/deploy-pages.yml on main deploys that branch to the live site (Pages source is "GitHub Actions", not legacy branch deploy). Auto deploy runs on workflow_run after Release completes; GitHub sometimes returns "Deployment failed, try again later" even though gh-pages is already correct. If the branch has the version but the live URL does not after ~10 minutes, check gh run list --workflow=deploy-pages.yml and re-run gh workflow run deploy-pages.yml --ref main (must use main — the workflow file is not on gh-pages). Sparkle clients only see the live URL.
Require isDraft=false, isPrerelease=true for beta or false for stable, an OpenUsage-<version>.dmg asset, bodyLen>0, and the version present in the appcast. If a draft was left behind, migrate its notes/assets onto the published release, then delete it — but only once a separate PUBLISHED release for the tag already exists:
tag="v{version}"
if [ "$(gh release view "$tag" --json isDraft --jq '.isDraft')" = "false" ]; then
gh api repos/robinebers/openusage/releases --paginate \
--jq '.[] | select(.draft and .tag_name=="'"$tag"'") | .id' \
| xargs -I{} gh api -X DELETE repos/robinebers/openusage/releases/{}
else
echo "No published release for $tag yet - publish it first; do NOT delete the draft."
fi
Changelog template
Only include category sections that have entries.
## v{version}
### New Features
- {message} ([#{pr}](https://github.com/robinebers/openusage/pull/{pr})) by @{author}
### Bug Fixes
- {message} ([#{pr}](https://github.com/robinebers/openusage/pull/{pr})) by @{author}
### Refactor
- {message} by @{author}
### Chores
- {message} by @{author}
---
### Changelog
**Full Changelog**: [{prev_tag}...v{version}](https://github.com/robinebers/openusage/compare/{prev_tag}...v{version})
- [{short_hash}](https://github.com/robinebers/openusage/commit/{full_hash}) {commit message} by @{author}
{prev_tag} is the previous release in the same channel: last stable for a stable cut, last beta (or last stable for the first beta in a lane) for a beta cut.
Rules
- 7-char short commit hashes; tags always prefixed with
v.
- Stable changelogs span last-stable → this-stable (roll up the whole beta series); beta changelogs span previous-tag → this-beta.
- Never push or tag automatically — ask the owner first.
- Always publish notes to the GitHub Release — never blank.
- The version is the tag; never edit version files.
- The appcast is append-only: older installs and the other channel's latest build must keep working, so the workflow aborts rather than shrink it.
Release secrets and one-time setup live in the README under Release setup.