#!/usr/bin/env bash # ------------------------------------------------------------ # release.sh – semi-automatic release helper # ------------------------------------------------------------ set -euo pipefail set -o errtrace trap 'echo "❌ Error – aborting"; exit 1' ERR # -------- configurable ----------------- APP_NAME="${1:-LoopFollow}" SECOND_DIR="${APP_NAME}_Second" THIRD_DIR="${APP_NAME}_Third" VERSION_FILE="Config.xcconfig" MARKETING_KEY="LOOP_FOLLOW_MARKETING_VERSION" DEV_BRANCH="dev" MAIN_BRANCH="main" # --------------------------------------- # --- functions here --- pause() { read -rp "▶▶ Press Enter to continue (Ctrl-C to abort)…"; } echo_run() { echo "+ $*"; "$@"; } push_cmds=() queue_push() { push_cmds+=("git -C \"$(pwd)\" $*"); echo "+ [queued] (in $(pwd)) git $*"; } update_follower () { local DIR="$1" local build_minute="$2" # staggered Sunday-build minute (see calls below) local suffix=".${DIR#${APP_NAME}_}" # LoopFollow_Second -> .Second local display="$DIR" # LoopFollow_Second local upstream="loopandlearn/${DIR}" # loopandlearn/LoopFollow_Second echo; echo "🔄 Updating $DIR …" cd "$DIR" # 1 · Make sure we’re on a clean, up-to-date main echo_run git switch "$MAIN_BRANCH" echo_run git fetch echo_run git pull # 2 · Full mirror of the release tree from the primary repo. # Every tracked file (including the overlay files) is synced; only git # metadata and local/build dirs are protected. --delete makes the tree an # exact mirror, auto-correcting any drift. echo_run rsync -a --delete \ --exclude='.git/' \ --exclude='.claude/' \ --exclude='build/' \ "$PRIMARY_ABS_PATH"/ ./ # 3 · Re-apply this instance's overlay on top of the mirror perl -i -pe "s|^app_suffix\s*=.*|app_suffix = ${suffix}|" LoopFollowDisplayNameConfig.xcconfig perl -i -pe "s|^display_name\s*=.*|display_name = ${display}|" LoopFollowDisplayNameConfig.xcconfig perl -i -pe "s|^(\s*)UPSTREAM_REPO:.*|\${1}UPSTREAM_REPO: ${upstream}|" .github/workflows/build_LoopFollow.yml perl -i -pe "s|^(\s*)- cron:.*|\${1}- cron: \"${build_minute} 10 * * 0\" # Sunday at UTC 10:${build_minute}|" .github/workflows/build_LoopFollow.yml # 4 · Rename the synced workspace to this instance's name rm -rf "${DIR}.xcworkspace" if [ -d "${APP_NAME}.xcworkspace" ]; then mv "${APP_NAME}.xcworkspace" "${DIR}.xcworkspace" fi # 5 · Single commit capturing the mirror + overlay git add -A if git diff --cached --quiet; then echo "✓ $DIR already up to date — nothing to commit." else echo_run git commit -m "transfer v${new_ver} updates from LF to ${DIR}" fi echo_run git status echo "💻 Build & test $DIR now."; pause # build & test checkpoint queue_push push origin "$MAIN_BRANCH" cd .. } # ---------- PRIMARY REPO ---------- PRIMARY_ABS_PATH="$(pwd -P)" echo "🏁 Working in $PRIMARY_ABS_PATH …" # --- guard: make sure we're running the latest release.sh from origin/dev ---- # The release flow assumes the newest script. If this copy differs from the one # on origin/dev, we're likely running a stale version — abort and tell the user. SCRIPT_PATH="${BASH_SOURCE[0]}" echo_run git fetch origin "$DEV_BRANCH" if ! diff -q <(git show "origin/${DEV_BRANCH}:release.sh") "$SCRIPT_PATH" >/dev/null 2>&1; then echo "❌ This release.sh does not match origin/${DEV_BRANCH}:release.sh." echo " You're likely running an outdated copy of the release script." echo " Switch to the latest '${DEV_BRANCH}' (git switch ${DEV_BRANCH} && git pull) and re-run. Aborting." exit 1 fi # --- start out in main to capture old_ver ---- echo_run git switch "$MAIN_BRANCH" echo_run git fetch echo_run git pull # -------- version bump logic (unchanged) ----------- old_ver=$(grep -E "^${MARKETING_KEY}[[:space:]]*=" "$VERSION_FILE" | awk '{print $3}') major_candidate="$(awk -F. '{printf "%d.0.0", $1 + 1}' <<<"$old_ver")" minor_candidate="$(awk -F. '{printf "%d.%d.0", $1, $2 + 1}' <<<"$old_ver")" echo echo "Which version bump do you want?" echo " 1) Major → $major_candidate" echo " 2) Minor → $minor_candidate" read -rp "Enter 1 or 2 (default = 2): " choice echo case "$choice" in 1) new_ver="$major_candidate" ;; ""|2) new_ver="$minor_candidate" ;; *) echo "❌ Invalid choice – aborting."; exit 1 ;; esac echo "🔢 Bumping version: $old_ver → $new_ver" # --- switch to dev so the release branch is cut from latest dev ---- echo_run git switch "$DEV_BRANCH" echo_run git fetch echo_run git pull # --- create release branch from dev's tip ---- RELEASE_BRANCH="release/v${new_ver}" echo_run git switch -c "$RELEASE_BRANCH" # --- bump version on the release branch ---- sed -i '' "s/${MARKETING_KEY}[[:space:]]*=.*/${MARKETING_KEY} = ${new_ver}/" "$VERSION_FILE" echo_run git diff "$VERSION_FILE"; pause # No [skip ci] here on purpose: it would skip the required SwiftFormat lint check # (leaving the release PRs blocked) and the tag_on_main workflow (no auto-tag). # auto_version_dev already avoids re-bumping via its "Config.xcconfig changed" guard. echo_run git commit -m "update version to ${new_ver}" "$VERSION_FILE" echo "💻 Build & test release branch now."; pause queue_push push origin "$RELEASE_BRANCH" # --- mirror the release tree into the sister repos ---- # Second arg = each app's scheduled browser-build minute (Sundays at 10:xx UTC), # staggered from the main app (:17) so a user who forked all three apps doesn't # trigger three simultaneous builds. cd .. update_follower "$SECOND_DIR" "27" update_follower "$THIRD_DIR" "40" # ---------- GitHub Actions Test --------- echo; echo "💻 Test GitHub Build Actions for all three repositories and then continue."; pause # --- return to primary path cd ${PRIMARY_ABS_PATH} # ---------- push queue ---------- echo; echo "🚀 Ready to push changes upstream and open the release PR." echo_run git log --oneline -2 read -rp "▶▶ Push everything now? (y/n): " confirm if [[ $confirm =~ ^[Yy]$ ]]; then for cmd in "${push_cmds[@]}"; do echo "+ $cmd"; bash -c "$cmd"; done echo "🎉 All pushes completed." # --- resolve the GitHub repo explicitly so gh doesn't depend on a default ---- # Clones with multiple remotes have no inferred default repo, which makes # `gh pr create` fail. Derive owner/repo from the origin remote URL. # Handles both https://github.com/owner/repo(.git) and git@github.com:owner/repo(.git) REPO_SLUG="$(git remote get-url origin | sed -E -e 's#\.git$##' -e 's#^.*github\.com[:/]##')" echo; echo "📝 Opening sync PR ${RELEASE_BRANCH} → ${DEV_BRANCH} …" gh pr create \ --repo "$REPO_SLUG" \ --base "$DEV_BRANCH" \ --head "$RELEASE_BRANCH" \ --title "Sync v${new_ver} version bump to dev" \ --body "Syncs the v${new_ver} version bump from the release branch back to \`dev\` so subsequent auto-bumps on \`dev\` continue from the released minor. \`auto_version_dev\` detects that \`Config.xcconfig\` was changed in this push and skips re-bumping. ⚠️ **Use \"Create a merge commit\"** (not squash or rebase) so \`dev\` and \`main\` share the same release lineage." echo; echo "📝 Opening release PR ${RELEASE_BRANCH} → ${MAIN_BRANCH} …" gh pr create \ --repo "$REPO_SLUG" \ --base "$MAIN_BRANCH" \ --head "$RELEASE_BRANCH" \ --title "Release v${new_ver}" \ --body "Release v${new_ver}. Merging this PR triggers the tagging workflow, which creates tag \`v${new_ver}\` from \`LOOP_FOLLOW_MARKETING_VERSION\` in \`Config.xcconfig\`. ⚠️ **Use \"Create a merge commit\"** (not squash or rebase) so \`main\` keeps the original feature-PR commits." echo; echo "🎉 All repos updated to v${new_ver} (local). Release PRs opened (sync → dev, release → main)." echo "👉 Review and merge both PRs — the tag will be created automatically by .github/workflows/tag_on_main.yml." echo "👉 Remember to create a GitHub release for tag v${new_ver} after the tag exists." else echo "🚫 Pushes skipped. Run manually if needed:"; printf ' %s\n' "${push_cmds[@]}" echo "🚫 Release not completed, pushes to GitHub were skipped" fi