Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 24 additions & 17 deletions .claude/commands/release.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ git fetch origin main --quiet && git rev-list --left-right --count main...origin

# Required tools.
command -v xcodebuild xcodegen create-dmg gh python3
xcrun --find notarytool stapler
xcrun --find notarytool
xcrun --find stapler

# Required env vars (mirror scripts/release.sh).
printenv APPLE_TEAM_ID GITHUB_REPO
Expand All @@ -52,11 +53,16 @@ grep -E "MARKETING_VERSION|CURRENT_PROJECT_VERSION" project.yml
`xcrun notarytool store-credentials AC_PASSWORD --apple-id "$APPLE_ID" --team-id "$APPLE_TEAM_ID" --password "$APPLE_APP_SPECIFIC_PWD"`
- `SUPublicEDKey` is missing or null — instruct the user to run `./bin/generate_keys`, paste the public key into `project.yml`, then `xcodegen generate`.
- Requested version is not strictly greater than the current `MARKETING_VERSION` (semver compare). If it isn't, ask the user to confirm a downgrade or correction.
- No `## v<version>` section in `CHANGELOG.md`. The script itself guards on this before building (added after a release once burned ~4 min of notarization before failing on a missing entry), but check it here too so you catch it during pre-flight, not after. You'll write the entry in Step 2.
- Sanity-check that a Debug build compiles before kicking off the release: `xcodebuild -project Mojito.xcodeproj -scheme Mojito -configuration Debug -destination 'platform=macOS' build` (tail the output, only proceed on `BUILD SUCCEEDED`).

## Step 2 — Collect release notes
## Step 2 — Write the CHANGELOG.md entry

Ask the user for release notes in markdown. Suggest a starter template based on `git log --oneline` since the last tag:
**The release notes come from `CHANGELOG.md`, not a file you pass in.** `release.sh` extracts the `## v<version>` section (everything up to the next `## vX.Y.Z`) and uses it verbatim for *both* the GitHub release body and the Sparkle in-app notes — so the two can never drift. If that section is missing, the script aborts.

So the job here is to add a `## v<version>` section to the top of `CHANGELOG.md` (just above the previous version's heading). Match the existing house style: a `## New` and/or `## Fixed` subsection, each bullet ending with a `([#NN](https://github.com/wr/mojito/pull/NN))` PR link. Write for end users — describe the visible behavior change, not the internal ticket. Skip internal-only commits (test harnesses, CI, docs, cosmetic link tweaks).

Draft from the commits since the last tag:

```bash
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
Expand All @@ -65,7 +71,7 @@ if [[ -n "$LAST_TAG" ]]; then
fi
```

Show the user the commit list and ask them to either (a) write notes directly, or (b) tell you to draft from the commits and have them approve. Save the final notes to a temp file path you'll reuse in Step 4. Don't proceed until the user approves.
Show the user your drafted section and get approval before proceeding. (Easter-egg rule still applies — a bare "added easter eggs" and nothing more.) The entry gets committed to `main` in Step 5 alongside the version bump.

## Step 3 — Run release.sh

Expand All @@ -82,18 +88,16 @@ This script:
5. Wraps `Mojito.app` in a DMG via `create-dmg`.
6. Submits to `notarytool` (waits ~2–10 min), then staples.
7. Calls `./bin/sign_update` to produce an EdDSA signature for the DMG.
8. `gh release create v<version>` with the DMG attached. Release notes are placeholder "TODO"you will replace them in Step 4.
9. Clones the `gh-pages` branch to a tempdir, runs `scripts/update_appcast.py` to prepend the new release entry, commits, and pushes.
8. `gh release create v<version>` with the DMG attached, using the `## v<version>` section of `CHANGELOG.md` as the release body (the notes are already correctthere is no placeholder to replace).
9. Clones the `gh-pages` branch to a tempdir, renders the changelog fragment to HTML release notes, runs `scripts/update_appcast.py` to prepend the new entry (build number, length, EdDSA signature), commits, and pushes.

Do **not** suppress output — surface the script's progress so the user can see notarization timing. If the script exits non-zero at any step, stop the skill and report the failure with the last 30 lines of output.

## Step 4 — Replace the placeholder release notes
Because notarization can take minutes, run the script in the background and let the task-completion notification tell you when it's done, rather than polling.

```bash
gh release edit "v<version>" --repo "$GITHUB_REPO" --notes-file <path-from-step-2>
```
## Step 4 — Confirm the release notes rendered

Verify with:
The GitHub release body already carries the real notes (from `CHANGELOG.md`), so there's nothing to replace — just confirm:

```bash
gh release view "v<version>" --repo "$GITHUB_REPO"
Expand All @@ -119,17 +123,20 @@ Run in parallel and report results:
# Release exists with the DMG attached.
gh release view "v<version>" --repo "$GITHUB_REPO" --json assets,name,tagName

# Appcast contains the new entry.
curl -sf "https://mojito.wells.ee/appcast.xml" | head -40

# Sparkle EdDSA signature on the appcast matches what was uploaded.
curl -sf "https://mojito.wells.ee/appcast.xml" | grep -A1 "v<version>" | grep -o 'sparkle:edSignature="[^"]*"'
# Appcast contains the new entry, and its enclosure line carries the right
# build number + EdDSA signature. Grab the <enclosure> for THIS version's DMG —
# grepping near "v<version>" alone is unreliable, it can catch an adjacent
# item's signature (the appcast lists every past release).
curl -sf "https://mojito.wells.ee/appcast.xml" \
| grep -o "https://github.com/wr/mojito/releases/download/v<version>/Mojito.dmg[^/]*sparkle:edSignature=\"[^\"]*\""
```

The signature it prints must match the `sparkle:edSignature=...` line the script echoed under `→ Signing for Sparkle`. (CDN caching can briefly serve the old appcast; if it doesn't match, re-check against `raw.githubusercontent.com/wr/mojito/gh-pages/appcast.xml`.)

Confirm to the user:

- Release URL: `https://github.com/$GITHUB_REPO/releases/tag/v<version>`
- DMG path on disk: `dist/Mojito-<version>.dmg`
- DMG path on disk: `dist/Mojito.dmg` (single file, no version suffix — the version lives in the release-tag URL)
- Reminder: to test the update flow, launch a previously-installed older copy of Mojito and use **Mojito → Check for Updates** — Sparkle should offer the new version.

---
Expand Down
16 changes: 16 additions & 0 deletions scripts/release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,22 @@ if [[ -z "$SU_PUBKEY" || "$SU_PUBKEY" == "<null>" ]]; then
exit 1
fi

# Refuse to release without a changelog entry for this version. The
# `## v$VERSION` section in CHANGELOG.md is the single source of truth for the
# GitHub release body and the Sparkle notes (re-extracted below). This is
# checked here — before the ~4-minute build + notarize — so a missing entry
# fails in seconds instead of after all the expensive, irreversible work.
CHANGELOG_CHECK=$(awk -v header="## v$VERSION" '
$0 == header { capture = 1; next }
capture && /^## v[0-9]/ { exit }
capture { print }
' "$REPO_ROOT/CHANGELOG.md")
if [[ -z "$CHANGELOG_CHECK" ]]; then
echo "error: no '## v$VERSION' section found in CHANGELOG.md." >&2
echo " Add a '## v$VERSION' entry before releasing." >&2
exit 1
fi

BUILD_DIR="$REPO_ROOT/build/release"
APP_NAME="Mojito"
APP_PATH="$BUILD_DIR/Build/Products/Release/$APP_NAME.app"
Expand Down
Loading