198 lines
10 KiB
Bash
Executable File
198 lines
10 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# Archive + upload a NucleicRemote iPhone build to App Store Connect (→ TestFlight).
|
|
#
|
|
# scripts/ios-release.sh {beta|canary} [--archive-only]
|
|
#
|
|
# iOS ships two coexisting TestFlight apps — the same channel model as the Mac app, minus the rc
|
|
# channel. Each channel is a distinct app record (distinct bundle id) so all can be installed
|
|
# side-by-side, exactly like the Mac's separate "Nucleic Beta", "Nucleic Canary", and "Nucleic":
|
|
#
|
|
# channel bundle id display name icon default bump
|
|
# ------- ------------------------------------- -------------- ---------------- ------------
|
|
# beta xyz.blakeslee.nucleic-remote.beta Nucleic Beta app-logo-beta minor
|
|
# canary xyz.blakeslee.nucleic-remote.canary Nucleic Canary app-logo-canary build
|
|
#
|
|
# The bare base id (xyz.blakeslee.nucleic-remote / "Nucleic" / app-logo-file — the Xcode project
|
|
# defaults) is reserved for the stable App Store release, kept separate from the beta app so a beta
|
|
# TestFlight build never shares stable's record.
|
|
#
|
|
# The channel is stamped into Info.plist (NucleicChannel → BuildBanner.swift) and drives the
|
|
# per-channel bundle id / display name / app icon via build settings the archive step overrides
|
|
# (NUCLEIC_BUNDLE_SUFFIX / NUCLEIC_NAME_SUFFIX / NUCLEIC_APPICON, defaulted in the Xcode project to
|
|
# the bare base/stable identity so a plain `xcodebuild`/Xcode build still produces a runnable app).
|
|
# The widget extension's bundle id tracks the app's via the same suffix (…$(NUCLEIC_BUNDLE_SUFFIX).widgets).
|
|
#
|
|
# Prerequisites (one-time — see signing/README.md):
|
|
# • An "Apple Distribution" certificate for the paid team (L7UDTQ6F5W) in the keychain.
|
|
# • BOTH App IDs registered + an app record created in App Store Connect, one per channel:
|
|
# xyz.blakeslee.nucleic-remote.beta (+ .beta.widgets) and xyz.blakeslee.nucleic-remote.canary (+ .canary.widgets)
|
|
# (The bare xyz.blakeslee.nucleic-remote (+ .widgets) is the separate stable App Store record.)
|
|
# • An App Store Connect API key (the same one notarization uses), passed by env:
|
|
# NUCLEIC_ASC_KEY_PATH=/path/AuthKey_XXXX.p8
|
|
# NUCLEIC_ASC_KEY_ID=<KeyID>
|
|
# NUCLEIC_ASC_ISSUER_ID=<IssuerID>
|
|
# With the key + -allowProvisioningUpdates, Xcode creates/downloads the distribution profile.
|
|
#
|
|
# Versioning: iOS keeps its own version state in ios/VERSION (seeded at 0.1.0), shared across both
|
|
# channels (as the Mac's ./VERSION is shared across its channels) and bumped by the same
|
|
# scripts/bump-version.sh. The default bump is channel-aware, mirroring the Mac: the fast-moving
|
|
# canary channel defaults to build-only (build number +1, marketing version unchanged), while beta
|
|
# defaults to a minor (we're pre-1.0). NUCLEIC_BUMP overrides either — e.g. BUMP=major|patch. A
|
|
# failed run restores ios/VERSION so the build number isn't spent; a successful upload commits it
|
|
# so numbers stay monotonic (per app, since the shared counter only ever increases).
|
|
# NUCLEIC_BUMP semver bump: build | patch | minor | major (default: minor; canary: build)
|
|
# NUCLEIC_VERSION pin the marketing version (skips the ios/VERSION bump)
|
|
# NUCLEIC_IOS_BUILD pin the build number (skips the ios/VERSION bump)
|
|
# NUCLEIC_VERSION_COMMIT commit the ios/VERSION bump on success (default 1; 0 leaves it unstaged)
|
|
|
|
set -euo pipefail
|
|
|
|
# Parse args: a channel keyword (beta|canary) and/or the --archive-only flag, in any order.
|
|
CHANNEL=""
|
|
ARCHIVE_ONLY=0
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
--archive-only) ARCHIVE_ONLY=1 ;;
|
|
beta|canary) CHANNEL="$arg" ;;
|
|
*) echo "usage: $0 {beta|canary} [--archive-only]" >&2; exit 2 ;;
|
|
esac
|
|
done
|
|
CHANNEL="${CHANNEL:-beta}" # default to the public beta
|
|
|
|
# Per-channel identity. The suffixes are appended to the base bundle id / app name; the icon is the
|
|
# Icon Composer package selected by ASSETCATALOG_COMPILER_APPICON_NAME. Both distributed channels are
|
|
# distinct apps layered on the base identity via their own suffix + icon (repo-root .icon packages
|
|
# wired into the Xcode project) — mirroring the Mac's separate "Nucleic Beta" and "Nucleic Canary".
|
|
# The bare base identity (empty suffixes / app-logo-file, the Xcode project defaults) is reserved for
|
|
# the stable App Store release, so a beta build no longer collides with stable.
|
|
case "$CHANNEL" in
|
|
beta) BUNDLE_SUFFIX=".beta"; NAME_SUFFIX=" Beta"; APPICON="app-logo-beta"; DEFAULT_BUMP="minor" ;;
|
|
canary) BUNDLE_SUFFIX=".canary"; NAME_SUFFIX=" Canary"; APPICON="app-logo-canary"; DEFAULT_BUMP="build" ;;
|
|
*) echo "usage: $0 {beta|canary} [--archive-only]" >&2; exit 2 ;;
|
|
esac
|
|
|
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
cd "$ROOT"
|
|
|
|
PROJ="ios/NucleicRemote/NucleicRemote.xcodeproj"
|
|
SCHEME="NucleicRemote"
|
|
ARCHIVE="$ROOT/dist/ios/NucleicRemote-$CHANNEL.xcarchive"
|
|
EXPORT_DIR="$ROOT/dist/ios/export-$CHANNEL"
|
|
|
|
IOS_VERSION_FILE="$ROOT/ios/VERSION"
|
|
|
|
# Resolve the version. Default path: bump ios/VERSION (same mechanism as the Mac release) and,
|
|
# unless it succeeds, restore it on exit so a failed upload doesn't spend the build number. If
|
|
# NUCLEIC_VERSION or NUCLEIC_IOS_BUILD is set, treat it as an explicit pin and leave the file alone.
|
|
# The default bump is channel-aware (canary: build-only; beta: minor), overridable via NUCLEIC_BUMP.
|
|
BUMP="${NUCLEIC_BUMP:-$DEFAULT_BUMP}"
|
|
BUMPED=0
|
|
SUCCEEDED=0
|
|
RSYNC_SHIM=""
|
|
# Single EXIT handler: restore an unspent version bump on failure (so a failed upload doesn't
|
|
# skip a build number), and remove the rsync shim dir (set up before the export, below).
|
|
cleanup() {
|
|
[ "$SUCCEEDED" = 1 ] || [ "$BUMPED" = 0 ] || git checkout -- ios/VERSION 2>/dev/null || true
|
|
[ -n "$RSYNC_SHIM" ] && rm -rf "$RSYNC_SHIM" 2>/dev/null || true
|
|
}
|
|
trap cleanup EXIT
|
|
if [ -n "${NUCLEIC_VERSION:-}" ] || [ -n "${NUCLEIC_IOS_BUILD:-}" ]; then
|
|
NUCLEIC_MARKETING_VERSION=0.1.0; NUCLEIC_BUILD_NUMBER=0
|
|
# shellcheck source=/dev/null
|
|
[ -f "$IOS_VERSION_FILE" ] && . "$IOS_VERSION_FILE"
|
|
MARKETING_VERSION="${NUCLEIC_VERSION:-$NUCLEIC_MARKETING_VERSION}"
|
|
BUILD_NUMBER="${NUCLEIC_IOS_BUILD:-$NUCLEIC_BUILD_NUMBER}"
|
|
echo "▸ Version (pinned): $MARKETING_VERSION (build $BUILD_NUMBER)"
|
|
else
|
|
read -r MARKETING_VERSION BUILD_NUMBER <<EOF
|
|
$(NUCLEIC_VERSION_FILE="$IOS_VERSION_FILE" scripts/bump-version.sh "$BUMP")
|
|
EOF
|
|
BUMPED=1
|
|
echo "▸ Version: $MARKETING_VERSION (build $BUILD_NUMBER) [channel=$CHANNEL bump=$BUMP]"
|
|
fi
|
|
|
|
# rsync pin (Xcode exportArchive workaround). Xcode's IPA-packaging step runs the system openrsync
|
|
# (/usr/bin/rsync) with Apple's --extended-attributes flag, but openrsync spawns its local copy
|
|
# "server" by resolving `rsync` on PATH. If a Homebrew/samba rsync (which doesn't support
|
|
# --extended-attributes) is ahead on PATH, the copy dies with
|
|
# rsync: on remote machine: --extended-attributes: unknown option … Copy failed
|
|
# and the export fails. Pin `rsync` to the system binary for our xcodebuild subprocesses so both
|
|
# ends of the copy are openrsync. Only kicks in when a different rsync is currently shadowing it.
|
|
if [ -x /usr/bin/rsync ] && [ "$(command -v rsync 2>/dev/null)" != /usr/bin/rsync ]; then
|
|
RSYNC_SHIM="$(mktemp -d)"
|
|
ln -sf /usr/bin/rsync "$RSYNC_SHIM/rsync"
|
|
echo "▸ Pinning rsync → /usr/bin/rsync (shadowing $(command -v rsync)) for the Xcode export"
|
|
PATH="$RSYNC_SHIM:$PATH"; export PATH
|
|
fi
|
|
|
|
# App Store Connect API-key flags for xcodebuild (provisioning updates + upload).
|
|
AUTH=()
|
|
if [ -n "${NUCLEIC_ASC_KEY_PATH:-}" ] && [ -n "${NUCLEIC_ASC_KEY_ID:-}" ] && [ -n "${NUCLEIC_ASC_ISSUER_ID:-}" ]; then
|
|
AUTH=(-authenticationKeyPath "$NUCLEIC_ASC_KEY_PATH"
|
|
-authenticationKeyID "$NUCLEIC_ASC_KEY_ID"
|
|
-authenticationKeyIssuerID "$NUCLEIC_ASC_ISSUER_ID")
|
|
elif [ "$ARCHIVE_ONLY" -eq 0 ]; then
|
|
cat >&2 <<EOF
|
|
ios-release: upload requested but no App Store Connect API key in the environment.
|
|
|
|
export NUCLEIC_ASC_KEY_PATH=/path/AuthKey_XXXX.p8
|
|
export NUCLEIC_ASC_KEY_ID=<KeyID>
|
|
export NUCLEIC_ASC_ISSUER_ID=<IssuerID>
|
|
|
|
Or run with --archive-only to just build the archive (then upload via Xcode Organizer).
|
|
EOF
|
|
exit 1
|
|
fi
|
|
|
|
echo "▸ Archiving $SCHEME ($CHANNEL) (version $MARKETING_VERSION, build $BUILD_NUMBER)"
|
|
rm -rf "$ARCHIVE"
|
|
xcodebuild \
|
|
-project "$PROJ" \
|
|
-scheme "$SCHEME" \
|
|
-configuration Release \
|
|
-destination 'generic/platform=iOS' \
|
|
-archivePath "$ARCHIVE" \
|
|
-allowProvisioningUpdates \
|
|
"${AUTH[@]}" \
|
|
MARKETING_VERSION="$MARKETING_VERSION" \
|
|
CURRENT_PROJECT_VERSION="$BUILD_NUMBER" \
|
|
NUCLEIC_CHANNEL="$CHANNEL" \
|
|
NUCLEIC_BUNDLE_SUFFIX="$BUNDLE_SUFFIX" \
|
|
NUCLEIC_NAME_SUFFIX="$NAME_SUFFIX" \
|
|
NUCLEIC_APPICON="$APPICON" \
|
|
clean archive
|
|
|
|
if [ "$ARCHIVE_ONLY" -eq 1 ]; then
|
|
# A real archive with this version now exists; keep the bump (don't let the trap revert it) so a
|
|
# later automated upload won't reuse the same build number, but leave it uncommitted since we
|
|
# didn't upload.
|
|
SUCCEEDED=1
|
|
echo "✓ Archive built: $ARCHIVE"
|
|
echo " Upload via Xcode → Window → Organizer → Distribute App → App Store Connect."
|
|
[ "$BUMPED" -eq 1 ] && echo " (ios/VERSION bumped to $MARKETING_VERSION build $BUILD_NUMBER, left uncommitted.)"
|
|
exit 0
|
|
fi
|
|
|
|
echo "▸ Exporting + uploading to App Store Connect (TestFlight)"
|
|
rm -rf "$EXPORT_DIR"
|
|
xcodebuild -exportArchive \
|
|
-archivePath "$ARCHIVE" \
|
|
-exportOptionsPlist ios/ExportOptions.plist \
|
|
-exportPath "$EXPORT_DIR" \
|
|
-allowProvisioningUpdates \
|
|
"${AUTH[@]}"
|
|
|
|
# The build is uploaded — the version is "spent". Keep the bump (the EXIT trap no longer reverts
|
|
# ios/VERSION) and commit it so build numbers stay monotonic across uploads.
|
|
SUCCEEDED=1
|
|
if [ "$BUMPED" -eq 1 ] && [ "${NUCLEIC_VERSION_COMMIT:-1}" = "1" ] && ! git diff --quiet -- ios/VERSION 2>/dev/null; then
|
|
git add ios/VERSION
|
|
git commit -q -m "release(ios/$CHANNEL): v$MARKETING_VERSION (build $BUILD_NUMBER)" \
|
|
&& echo "▸ Committed iOS version bump → v$MARKETING_VERSION (build $BUILD_NUMBER)"
|
|
fi
|
|
|
|
echo "✓ Uploaded $CHANNEL build $BUILD_NUMBER ($MARKETING_VERSION) to App Store Connect."
|
|
echo " It will appear in TestFlight after processing. Assign it to a tester group there,"
|
|
echo " or submit it for App Store review to ship a release."
|