Add IndieAbout + IndieBackup, TestFlight release pipeline
IndieAbout: About section in iOS Settings and macOS About window; LICENSE
switched to the portfolio-standard ISC text (reflowed for in-app rendering).
IndieBackup: files-only backup/restore of the iCloud container documents
('.notesbackup' document type on both platforms), SwiftData cache rebuilt
via SyncEngine.rebuildCache() after restore, monitors suspended during the
file swap, onOpenURL import with pre-launch queueing.
Release pipeline: Scripts/release.sh (ios|mac|all) archives and uploads via
xcodebuild + ASC API key; platform-partitioned build numbers (iOS even,
macOS odd) since both targets share one bundle ID and build sequence.
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<!-- TestFlight / App Store distribution for the iOS app (an embedded
|
||||
watchOS app, if any, is exported as part of the same bundle). -->
|
||||
<key>method</key>
|
||||
<string>app-store-connect</string>
|
||||
<key>teamID</key>
|
||||
<string>C32Z8JNLG6</string>
|
||||
<!-- destination=upload makes -exportArchive push the build straight to
|
||||
App Store Connect instead of writing a local .ipa. -->
|
||||
<key>destination</key>
|
||||
<string>upload</string>
|
||||
<key>signingStyle</key>
|
||||
<string>automatic</string>
|
||||
<key>uploadSymbols</key>
|
||||
<true/>
|
||||
<key>stripSwiftSymbols</key>
|
||||
<true/>
|
||||
<key>manageAppVersionAndBuildNumber</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<!-- TestFlight / App Store distribution for the macOS target. -->
|
||||
<key>method</key>
|
||||
<string>app-store-connect</string>
|
||||
<!-- Hardcoded because -exportOptionsPlist takes a static file path (no env-var
|
||||
substitution); release.sh does not template this plist at run time. Must
|
||||
match APPLE_TEAM_ID in .env.release — update both if the team ever changes. -->
|
||||
<key>teamID</key>
|
||||
<string>C32Z8JNLG6</string>
|
||||
<!-- destination=upload makes -exportArchive push the build straight to
|
||||
App Store Connect instead of writing a local .pkg. -->
|
||||
<key>destination</key>
|
||||
<string>upload</string>
|
||||
<key>signingStyle</key>
|
||||
<string>automatic</string>
|
||||
<key>uploadSymbols</key>
|
||||
<true/>
|
||||
<key>stripSwiftSymbols</key>
|
||||
<true/>
|
||||
<key>manageAppVersionAndBuildNumber</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
Executable
+114
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# release.sh — archive Notes and upload to TestFlight / App Store Connect.
|
||||
#
|
||||
# Usage: Scripts/release.sh [ios|mac|all] (default: all)
|
||||
#
|
||||
# No third-party tooling: pure xcodebuild + the App Store Connect API key.
|
||||
# Credentials live in .env.release (gitignored); see .env.release.example.
|
||||
#
|
||||
# What it does, per platform:
|
||||
# 1. Regenerates the Xcode project with XcodeGen.
|
||||
# 2. Stamps CFBundleVersion from the git commit count, platform-partitioned
|
||||
# (iOS even, macOS odd — Scripts/update_build_number.sh, a post-build
|
||||
# phase wired in project.yml).
|
||||
# 3. xcodebuild archive -> .xcarchive
|
||||
# 4. xcodebuild -exportArchive with destination=upload -> App Store Connect.
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
PROJECT="Notes.xcodeproj"
|
||||
BUILD_DIR="$ROOT/build"
|
||||
PLATFORM="${1:-all}"
|
||||
|
||||
# ---- credentials ------------------------------------------------------------
|
||||
ENV_FILE="$ROOT/.env.release"
|
||||
if [ -f "$ENV_FILE" ]; then
|
||||
set -a; . "$ENV_FILE"; set +a
|
||||
fi
|
||||
: "${APPLE_TEAM_ID:?Set APPLE_TEAM_ID (copy .env.release.example -> .env.release)}"
|
||||
: "${ASC_KEY_ID:?Set ASC_KEY_ID in .env.release}"
|
||||
: "${ASC_ISSUER_ID:?Set ASC_ISSUER_ID in .env.release}"
|
||||
: "${ASC_KEY_PATH:?Set ASC_KEY_PATH in .env.release}"
|
||||
[ -f "$ASC_KEY_PATH" ] || { echo "❌ API key not found at: $ASC_KEY_PATH"; exit 1; }
|
||||
|
||||
AUTH=(
|
||||
-authenticationKeyPath "$ASC_KEY_PATH"
|
||||
-authenticationKeyID "$ASC_KEY_ID"
|
||||
-authenticationKeyIssuerID "$ASC_ISSUER_ID"
|
||||
-allowProvisioningUpdates
|
||||
)
|
||||
|
||||
# ---- regenerate project -----------------------------------------------------
|
||||
if command -v xcodegen >/dev/null 2>&1; then
|
||||
echo "🧩 Generating $PROJECT ..."
|
||||
xcodegen generate
|
||||
elif [ ! -d "$PROJECT" ]; then
|
||||
echo "❌ $PROJECT missing and xcodegen not installed."; exit 1
|
||||
fi
|
||||
|
||||
# ---- versioning -------------------------------------------------------------
|
||||
# The Info.plists hold the literal "$(MARKETING_VERSION)" (resolved at build
|
||||
# time), so read the real value from project.yml. The build number is stamped
|
||||
# per-platform by Scripts/update_build_number.sh during the archive (even for
|
||||
# iOS, odd for macOS — see that script); shown here for reference.
|
||||
BUILD_NUMBER="$(git rev-list HEAD --count)"
|
||||
MARKETING_VERSION="$(grep -m1 'MARKETING_VERSION:' project.yml | sed -E 's/.*"([^"]+)".*/\1/')"
|
||||
echo "📦 Version $MARKETING_VERSION — iOS build $((BUILD_NUMBER * 2)), macOS build $((BUILD_NUMBER * 2 + 1))"
|
||||
|
||||
mkdir -p "$BUILD_DIR"
|
||||
|
||||
# ---- per-platform archive + upload ------------------------------------------
|
||||
# args: <scheme> <generic destination> <ExportOptions plist>
|
||||
archive_and_upload() {
|
||||
local scheme="$1" destination="$2" export_plist="$3"
|
||||
local archive_path="$BUILD_DIR/$scheme.xcarchive"
|
||||
local export_path="$BUILD_DIR/$scheme-export"
|
||||
|
||||
echo ""
|
||||
echo "──────── $scheme ────────"
|
||||
|
||||
rm -rf "$archive_path" "$export_path"
|
||||
|
||||
echo "🛠 Archiving $scheme ..."
|
||||
xcodebuild archive \
|
||||
-project "$PROJECT" \
|
||||
-scheme "$scheme" \
|
||||
-configuration Release \
|
||||
-destination "$destination" \
|
||||
-archivePath "$archive_path" \
|
||||
CODE_SIGN_STYLE=Automatic \
|
||||
DEVELOPMENT_TEAM="$APPLE_TEAM_ID" \
|
||||
"${AUTH[@]}"
|
||||
|
||||
echo "🚀 Exporting + uploading $scheme to App Store Connect ..."
|
||||
xcodebuild -exportArchive \
|
||||
-archivePath "$archive_path" \
|
||||
-exportPath "$export_path" \
|
||||
-exportOptionsPlist "$export_plist" \
|
||||
"${AUTH[@]}"
|
||||
|
||||
echo "✅ $scheme uploaded."
|
||||
}
|
||||
|
||||
do_ios() {
|
||||
archive_and_upload "Notes" "generic/platform=iOS" "$SCRIPT_DIR/ExportOptions-iOS.plist"
|
||||
}
|
||||
|
||||
do_mac() {
|
||||
archive_and_upload "NotesMac" "generic/platform=macOS" "$SCRIPT_DIR/ExportOptions-macOS.plist"
|
||||
}
|
||||
|
||||
case "$PLATFORM" in
|
||||
ios) do_ios ;;
|
||||
mac|macos) do_mac ;;
|
||||
all) do_ios; do_mac ;;
|
||||
*) echo "Usage: Scripts/release.sh [ios|mac|all]"; exit 1 ;;
|
||||
esac
|
||||
|
||||
echo ""
|
||||
echo "🎉 Done. Builds appear in App Store Connect > TestFlight after processing (~5–15 min)."
|
||||
Executable
+61
@@ -0,0 +1,61 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
## Notes: platform-aware build info (post-build phase).
|
||||
##
|
||||
## Sets, on every build:
|
||||
## - CFBundleVersion = git commit count, platform-partitioned (see below)
|
||||
## - BuildDate = today's date, e.g. 2026-07-14
|
||||
## - BuildHash = short git commit hash
|
||||
## in both the target Info.plist and its dSYM Info.plist, so IndieAbout's
|
||||
## version line has something to read.
|
||||
##
|
||||
## Platform partitioning: the iOS and macOS targets share one bundle ID and
|
||||
## therefore one App Store Connect build-number sequence, so stamping both
|
||||
## with the raw commit count makes the second upload collide ("bundle version
|
||||
## already used"). iOS gets even numbers, macOS odd — both strictly increase
|
||||
## with every commit and can never equal each other. (Same scheme as
|
||||
## QuickRabbit's update_build_number.sh.)
|
||||
|
||||
git=$(sh /etc/profile; which git || true)
|
||||
if [ -z "$git" ] || [ ! -x "$git" ]; then
|
||||
echo "error: update_build_number.sh: could not resolve a 'git' executable — refusing to stamp a build number." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
number_of_commits=$("$git" rev-list HEAD --count)
|
||||
case "$number_of_commits" in
|
||||
''|*[!0-9]*)
|
||||
echo "error: update_build_number.sh: 'git rev-list HEAD --count' returned '$number_of_commits', not a positive integer." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
case "${PLATFORM_NAME:-}" in
|
||||
macosx*) build_number=$((number_of_commits * 2 + 1)) ;;
|
||||
*) build_number=$((number_of_commits * 2)) ;;
|
||||
esac
|
||||
|
||||
git_commit=$("$git" rev-parse --short HEAD)
|
||||
build_date=$(date +%F)
|
||||
|
||||
target_plist="$TARGET_BUILD_DIR/$INFOPLIST_PATH"
|
||||
dsym_plist="$DWARF_DSYM_FOLDER_PATH/$DWARF_DSYM_FILE_NAME/Contents/Info.plist"
|
||||
|
||||
if [ ! -f "$target_plist" ] && [ ! -f "$dsym_plist" ]; then
|
||||
echo "warning: update_build_number.sh: neither $target_plist nor $dsym_plist exists — check the phase's Input Files"
|
||||
fi
|
||||
|
||||
echo "update_build_number: b$build_number ($PLATFORM_NAME) $git_commit $build_date"
|
||||
|
||||
for plist in "$target_plist" "$dsym_plist"; do
|
||||
if [ -f "$plist" ]; then
|
||||
/usr/libexec/PlistBuddy -c "Set :CFBundleVersion $build_number" "$plist"
|
||||
|
||||
/usr/libexec/PlistBuddy -c "Set :BuildDate $build_date" "$plist" 2>/dev/null || \
|
||||
/usr/libexec/PlistBuddy -c "Add :BuildDate string $build_date" "$plist"
|
||||
|
||||
/usr/libexec/PlistBuddy -c "Set :BuildHash $git_commit" "$plist" 2>/dev/null || \
|
||||
/usr/libexec/PlistBuddy -c "Add :BuildHash string $git_commit" "$plist"
|
||||
fi
|
||||
done
|
||||
Reference in New Issue
Block a user