From eb70129b7f27b46f8e6bedb8dd94b1787b8fa025 Mon Sep 17 00:00:00 2001 From: Wells Riley <884715+wr@users.noreply.github.com> Date: Tue, 4 Aug 2026 17:41:11 -0400 Subject: [PATCH 1/4] Tag releases at the commit that carries the version MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit release.sh bumped MARKETING_VERSION and CURRENT_PROJECT_VERSION but never committed them, so every tag pointed at source declaring the *previous* version — v1.8.1's tree says 1.8.0, v1.9.0's says 1.8.1. Checking out a tag didn't rebuild that release. Commit the bump and tag at it. The commit lands after the build, notarize and sign steps rather than before, so a run that fails partway leaves no stray commit behind. Guard the whole thing on a clean, in-sync main: without that the bump commit would sweep up unrelated working-tree changes, and a push failing at the end would strand an already-public release. --- scripts/release.sh | 43 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/scripts/release.sh b/scripts/release.sh index 7b536bf..70e1666 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -20,8 +20,13 @@ # 2. Wraps Mojito.app in a DMG with create-dmg. # 3. Submits the DMG to Apple notarytool, waits, staples. # 4. Signs the DMG with Sparkle's EdDSA key, captures the signature. -# 5. Creates a GitHub Release with the DMG attached (via gh CLI). -# 6. Updates appcast.xml on the gh-pages branch with the new entry. +# 5. Commits the version bump to main and pushes it. +# 6. Creates a GitHub Release with the DMG attached (via gh CLI), tagged at +# that commit — so the tag describes the source the DMG was built from. +# 7. Updates appcast.xml on the gh-pages branch with the new entry. +# +# Must be run from a clean `main` that's in sync with origin; it refuses +# otherwise, since step 5 would otherwise commit unrelated work. # # Things you must do before this works: # - Apple Developer Program membership ($99/yr). @@ -98,6 +103,30 @@ if [[ -z "$CHANGELOG_CHECK" ]]; then exit 1 fi +# The version bump is committed and tagged further down, so the tree has to be +# clean going in — otherwise that commit sweeps up whatever else is lying +# around, and the tag stops describing the released source. +RELEASE_BRANCH="main" +CURRENT_BRANCH=$(git -C "$REPO_ROOT" rev-parse --abbrev-ref HEAD) +if [[ "$CURRENT_BRANCH" != "$RELEASE_BRANCH" ]]; then + echo "error: on branch '$CURRENT_BRANCH', expected '$RELEASE_BRANCH'." >&2 + exit 1 +fi +if [[ -n "$(git -C "$REPO_ROOT" status --porcelain)" ]]; then + echo "error: working tree is dirty. Commit or stash first:" >&2 + git -C "$REPO_ROOT" status --short >&2 + exit 1 +fi +# Checked now so the push at the end can't fail after the release is public. +git -C "$REPO_ROOT" fetch --quiet origin "$RELEASE_BRANCH" +AHEAD=$(git -C "$REPO_ROOT" rev-list --count "origin/$RELEASE_BRANCH..HEAD") +BEHIND=$(git -C "$REPO_ROOT" rev-list --count "HEAD..origin/$RELEASE_BRANCH") +if [[ "$AHEAD" != "0" || "$BEHIND" != "0" ]]; then + echo "error: $RELEASE_BRANCH is out of sync with origin" \ + "($AHEAD ahead, $BEHIND behind). Pull/push first." >&2 + exit 1 +fi + BUILD_DIR="$REPO_ROOT/build/release" APP_NAME="Mojito" APP_PATH="$BUILD_DIR/Build/Products/Release/$APP_NAME.app" @@ -244,6 +273,15 @@ if [[ ! -s "$CHANGELOG_FRAGMENT" ]]; then exit 1 fi +# Land the version bump before tagging, so `v$VERSION` points at source that +# actually carries $VERSION. Deliberately after the build/notarize/sign steps: +# those are the ones that fail, and a failed run should leave no commit behind. +echo "→ Committing version bump" +git -C "$REPO_ROOT" add project.yml "$APP_NAME.xcodeproj/project.pbxproj" +git -C "$REPO_ROOT" commit -q -m "Release v$VERSION" +git -C "$REPO_ROOT" push --quiet origin "$RELEASE_BRANCH" +RELEASE_SHA=$(git -C "$REPO_ROOT" rev-parse HEAD) + echo "→ Creating GitHub Release" RELEASE_NOTES_FILE=$(mktemp) { @@ -254,6 +292,7 @@ RELEASE_NOTES_FILE=$(mktemp) gh release create "v$VERSION" "$DMG_PATH" \ --repo "$GITHUB_REPO" \ + --target "$RELEASE_SHA" \ --title "v$VERSION" \ --notes-file "$RELEASE_NOTES_FILE" From 64be77b794fbae020581c1ca06a7b6dff8789e4b Mon Sep 17 00:00:00 2001 From: Wells Riley <884715+wr@users.noreply.github.com> Date: Tue, 4 Aug 2026 17:47:08 -0400 Subject: [PATCH 2/4] Refuse to re-release a version, and check origin is the release repo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A re-run was worse than before the tag change: the version bump is unconditional, so retrying an already-released version burned a build number and pushed a public "Release vX.Y.Z" commit before dying at `gh release create`. Check for the tag, and separately for a release — a partly-failed `gh release create` can leave a draft with no tag. Also assert origin is $GITHUB_REPO. The bump pushes to origin while the release is cut against $GITHUB_REPO; if they differ, every other guard passes and --target then references a SHA the release repo doesn't have. --- scripts/release.sh | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/scripts/release.sh b/scripts/release.sh index 70e1666..21ee333 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -117,6 +117,31 @@ if [[ -n "$(git -C "$REPO_ROOT" status --porcelain)" ]]; then git -C "$REPO_ROOT" status --short >&2 exit 1 fi +# The bump is pushed to `origin` but the release is cut against $GITHUB_REPO. +# If those are different repos every other guard still passes, and then +# `--target` fails because origin's new SHA doesn't exist in $GITHUB_REPO. +ORIGIN_URL=$(git -C "$REPO_ROOT" remote get-url origin) +if [[ "$ORIGIN_URL" != *"$GITHUB_REPO"* ]]; then + echo "error: origin ($ORIGIN_URL) is not GITHUB_REPO ($GITHUB_REPO)." >&2 + exit 1 +fi + +# Stop a re-run of an already-released version *before* it pushes anything. +# The bump is unconditional, so without this a retry burns a build number and +# lands a public "Release vX.Y.Z" commit, only to die at `gh release create`. +if git -C "$REPO_ROOT" ls-remote --exit-code --tags origin "refs/tags/v$VERSION" >/dev/null 2>&1; then + echo "error: tag v$VERSION already exists on origin." >&2 + echo " Releasing the same version twice isn't supported — bump it." >&2 + exit 1 +fi +# Separate check: a partly-failed `gh release create` can leave a draft behind +# without ever creating the tag, which the check above wouldn't catch. +if gh release view "v$VERSION" --repo "$GITHUB_REPO" >/dev/null 2>&1; then + echo "error: release v$VERSION already exists on $GITHUB_REPO." >&2 + echo " Delete it (gh release delete v$VERSION) or bump the version." >&2 + exit 1 +fi + # Checked now so the push at the end can't fail after the release is public. git -C "$REPO_ROOT" fetch --quiet origin "$RELEASE_BRANCH" AHEAD=$(git -C "$REPO_ROOT" rev-list --count "origin/$RELEASE_BRANCH..HEAD") From 765f8d022561be6a32fd37e12c01424fbaf76f9c Mon Sep 17 00:00:00 2001 From: Wells Riley <884715+wr@users.noreply.github.com> Date: Tue, 4 Aug 2026 17:47:27 -0400 Subject: [PATCH 3/4] temp --- scripts/release.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/release.sh b/scripts/release.sh index 21ee333..ea1785a 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -106,7 +106,7 @@ fi # The version bump is committed and tagged further down, so the tree has to be # clean going in — otherwise that commit sweeps up whatever else is lying # around, and the tag stops describing the released source. -RELEASE_BRANCH="main" +RELEASE_BRANCH="wells/release-script-tag-fix" CURRENT_BRANCH=$(git -C "$REPO_ROOT" rev-parse --abbrev-ref HEAD) if [[ "$CURRENT_BRANCH" != "$RELEASE_BRANCH" ]]; then echo "error: on branch '$CURRENT_BRANCH', expected '$RELEASE_BRANCH'." >&2 From b621b819818325b5e4f2574c136db2a87443332c Mon Sep 17 00:00:00 2001 From: Wells Riley <884715+wr@users.noreply.github.com> Date: Tue, 4 Aug 2026 17:48:42 -0400 Subject: [PATCH 4/4] Restore RELEASE_BRANCH to main after guard testing The previous commit was scratch state from exercising the branch guard against this branch instead of main. --- scripts/release.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/release.sh b/scripts/release.sh index ea1785a..21ee333 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -106,7 +106,7 @@ fi # The version bump is committed and tagged further down, so the tree has to be # clean going in — otherwise that commit sweeps up whatever else is lying # around, and the tag stops describing the released source. -RELEASE_BRANCH="wells/release-script-tag-fix" +RELEASE_BRANCH="main" CURRENT_BRANCH=$(git -C "$REPO_ROOT" rev-parse --abbrev-ref HEAD) if [[ "$CURRENT_BRANCH" != "$RELEASE_BRANCH" ]]; then echo "error: on branch '$CURRENT_BRANCH', expected '$RELEASE_BRANCH'." >&2