chore: release script supports --draft + RELEASE_NOTES.md

Drafts skip the appcast push and main tag, so a draft release won't
show up in users' Sparkle update feed and v1.6.0 stays "latest" until
explicitly promoted. The signed appcast entry is saved to the release
dir for later manual promotion.

Also adds release notes file convention: releases/v<VERSION>/RELEASE_NOTES.md
is auto-included in the version-bump commit and used as the GitHub
release body.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Alan Wizemann
2026-04-16 19:04:27 -07:00
parent 9bdd928469
commit 868e61979e
2 changed files with 107 additions and 26 deletions
+82 -26
View File
@@ -2,7 +2,18 @@
#
# Scarf release pipeline — local, manual, repeatable.
#
# Usage: ./scripts/release.sh 1.7.0
# Usage:
# ./scripts/release.sh 1.7.0 # full release: build, sign, notarize,
# # appcast push, GitHub release, tag
# ./scripts/release.sh 1.7.0 --draft # everything builds + notarizes, but the
# # GitHub release is created as draft, the
# # appcast is NOT updated, and main is NOT
# # tagged. Promote later with --promote.
#
# Release notes:
# If `releases/v<VERSION>/RELEASE_NOTES.md` exists, it is committed alongside the
# version bump and used as the GitHub release body. Otherwise a minimal autogenerated
# note is used.
#
# Prerequisites (one-time setup):
# 1. Developer ID Application cert installed in login Keychain.
@@ -19,8 +30,20 @@
#
set -euo pipefail
# ---------- arg parsing ----------
VERSION=""
DRAFT=0
for arg in "$@"; do
case "$arg" in
--draft) DRAFT=1 ;;
-h|--help) sed -n '2,30p' "$0"; exit 0 ;;
-*) printf '[ERR] unknown flag: %s\n' "$arg" >&2; exit 1 ;;
*) [[ -z "$VERSION" ]] && VERSION="$arg" || { printf '[ERR] unexpected arg: %s\n' "$arg" >&2; exit 1; } ;;
esac
done
[[ -n "$VERSION" ]] || { printf 'usage: ./scripts/release.sh <marketing-version> [--draft]\n' >&2; exit 1; }
# ---------- config ----------
VERSION="${1:?usage: ./scripts/release.sh <marketing-version> e.g. 1.7.0}"
TEAM_ID="3Q6X2L86C4"
BUNDLE_ID="com.scarf.app"
SCHEME="scarf"
@@ -81,6 +104,11 @@ NEW_BUILD=$((CUR_BUILD + 1))
sed -i '' -E "s/MARKETING_VERSION = [0-9]+\.[0-9]+\.[0-9]+;/MARKETING_VERSION = ${VERSION};/g" "$PBXPROJ"
sed -i '' -E "s/CURRENT_PROJECT_VERSION = [0-9]+;/CURRENT_PROJECT_VERSION = ${NEW_BUILD};/g" "$PBXPROJ"
git add "$PBXPROJ"
# Include release notes in the bump commit if user prepared them ahead of time.
NOTES_FILE="$RELEASE_DIR/RELEASE_NOTES.md"
if [[ -f "$NOTES_FILE" ]]; then
git add "$NOTES_FILE"
fi
git commit -m "chore: Bump version to ${VERSION}"
# ---------- build ----------
@@ -154,17 +182,9 @@ ED_SIGNATURE="$(echo "$SIG_OUTPUT" | sed -nE 's/.*sparkle:edSignature="([^"]+)".
FILE_LENGTH="$(echo "$SIG_OUTPUT" | sed -nE 's/.*length="([^"]+)".*/\1/p')"
[[ -n "$ED_SIGNATURE" && -n "$FILE_LENGTH" ]] || die "sign_update did not produce signature: $SIG_OUTPUT"
# ---------- update appcast on gh-pages ----------
log "Update appcast.xml on gh-pages worktree"
if [[ ! -d "$GH_PAGES_WORKTREE" ]]; then
git worktree add "$GH_PAGES_WORKTREE" gh-pages
fi
(
cd "$GH_PAGES_WORKTREE"
git pull --ff-only origin gh-pages
PUB_DATE="$(LC_TIME=en_US.UTF-8 date -u +"%a, %d %b %Y %H:%M:%S +0000")"
DOWNLOAD_URL="$DOWNLOAD_URL_BASE/v${VERSION}/Scarf-v${VERSION}-Universal.zip"
NEW_ITEM=$(cat <<EOF
DOWNLOAD_URL="$DOWNLOAD_URL_BASE/v${VERSION}/Scarf-v${VERSION}-Universal.zip"
PUB_DATE="$(LC_TIME=en_US.UTF-8 date -u +"%a, %d %b %Y %H:%M:%S +0000")"
APPCAST_ITEM=$(cat <<EOF
<item>
<title>Version ${VERSION}</title>
<sparkle:version>${NEW_BUILD}</sparkle:version>
@@ -178,8 +198,18 @@ fi
</item>
EOF
)
# Insert new item after <language>en</language> line
python3 - "$NEW_ITEM" <<'PY'
# ---------- update appcast on gh-pages (skipped for drafts) ----------
if [[ $DRAFT -eq 0 ]]; then
log "Update appcast.xml on gh-pages worktree"
if [[ ! -d "$GH_PAGES_WORKTREE" ]]; then
git worktree add "$GH_PAGES_WORKTREE" gh-pages
fi
(
cd "$GH_PAGES_WORKTREE"
git pull --ff-only origin gh-pages
# Insert new item after <language>en</language> line
python3 - "$APPCAST_ITEM" <<'PY'
import sys, pathlib
new_item = sys.argv[1]
p = pathlib.Path("appcast.xml")
@@ -190,22 +220,48 @@ if marker not in xml:
xml = xml.replace(marker, marker + "\n" + new_item, 1)
p.write_text(xml)
PY
git add appcast.xml
git commit -m "release: v${VERSION}"
git push origin gh-pages
)
git add appcast.xml
git commit -m "release: v${VERSION}"
git push origin gh-pages
)
else
log "Draft mode — skipping appcast push. Saving entry to $RELEASE_DIR/appcast-entry.xml for later promotion."
printf '%s\n' "$APPCAST_ITEM" > "$RELEASE_DIR/appcast-entry.xml"
fi
# ---------- github release ----------
log "Create GitHub release and upload artifacts"
GH_FLAGS=()
[[ $DRAFT -eq 1 ]] && GH_FLAGS+=(--draft)
if [[ -f "$NOTES_FILE" ]]; then
GH_FLAGS+=(--notes-file "$NOTES_FILE")
else
GH_FLAGS+=(--notes "Release v${VERSION}. See commit history for details.")
fi
gh release create "v${VERSION}" \
--title "Scarf v${VERSION}" \
--notes "Release notes: https://github.com/awizemann/scarf/releases/tag/v${VERSION}" \
"${GH_FLAGS[@]}" \
"$UNIVERSAL_ZIP"
log "Tag main and push"
git tag "v${VERSION}"
git push origin main --tags
# ---------- tag main (skipped for drafts) ----------
if [[ $DRAFT -eq 0 ]]; then
log "Tag main and push"
git tag "v${VERSION}"
git push origin main --tags
else
log "Draft mode — skipping tag. Bump commit is local only; push manually with: git push origin main"
fi
log "Release v${VERSION} complete"
log " Download: $DOWNLOAD_URL_BASE/v${VERSION}/Scarf-v${VERSION}-Universal.zip"
log " Appcast: $APPCAST_URL"
if [[ $DRAFT -eq 1 ]]; then
log "Draft release v${VERSION} ready"
log " Review: https://github.com/awizemann/scarf/releases"
log " Promote: in GitHub UI, edit the draft and uncheck 'Set as a pre-release / draft' → Publish."
log " Then commit + push appcast-entry.xml to gh-pages, and tag main:"
log " git push origin main"
log " git tag v${VERSION} && git push origin v${VERSION}"
log " (manually merge $RELEASE_DIR/appcast-entry.xml into gh-pages branch's appcast.xml after <language>en</language>)"
else
log "Release v${VERSION} complete"
log " Download: $DOWNLOAD_URL"
log " Appcast: $APPCAST_URL"
fi