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
+25
View File
@@ -0,0 +1,25 @@
## What's New in 1.6.1
### Auto-updates
Scarf now ships with [Sparkle](https://sparkle-project.org). On launch (and daily thereafter) it checks an EdDSA-signed appcast at [awizemann.github.io/scarf/appcast.xml](https://awizemann.github.io/scarf/appcast.xml). When a new version is available you'll get an in-app update prompt — no more manually downloading zips and dragging into Applications.
You can disable automatic checks or trigger a manual one from **Settings → General → Updates**, the menu bar icon, or the **Scarf → Check for Updates…** menu item.
### Notarized & Developer ID signed
This is the first release that's properly Developer ID signed and notarized by Apple. Gatekeeper accepts it on first launch — no more right-click → Open dance, no more "Scarf cannot be opened because the developer cannot be verified" warnings.
### Fixes
- Chat works correctly when no terminal hermes session is running, and surfaces the real error when it can't reach the agent (b6df…)
### Under the hood
- Tracked `Info.plist` (replacing auto-generation) so signing-relevant keys live in version control
- New `UpdaterService` wraps Sparkle and is injected via SwiftUI `.environment()`
- One-command release pipeline at [scripts/release.sh](https://github.com/awizemann/scarf/blob/main/scripts/release.sh) handles archive → sign → notarize → staple → appcast → GitHub release → tag
---
**Migrating from 1.6.0:** unzip and replace your existing `Scarf.app` in `/Applications`. After this release, future updates install in-place via Sparkle.
+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