Skip to content

Instantly share code, notes, and snippets.

@nfarina
Created June 20, 2026 18:18
Show Gist options
  • Select an option

  • Save nfarina/78a6873ef66603abf8f80f2944db8239 to your computer and use it in GitHub Desktop.

Select an option

Save nfarina/78a6873ef66603abf8f80f2944db8239 to your computer and use it in GitHub Desktop.
Submit to App Store from macOS 27 Beta
#!/usr/bin/env bash
#
# GUI-free App Store release.
#
# The Xcode *app* won't launch on the macOS 27 beta, but the stable (GM) Xcode's
# command-line tools work fine — and App Store Connect only rejects builds made
# with the *beta* SDK. So we archive, export, and upload entirely from the CLI
# using the GM toolchain via DEVELOPER_DIR, no Xcode GUI required.
#
# Bump CURRENT_PROJECT_VERSION in the project first (App Store build numbers
# can't be reused), then just run from the repo root:
#
# ./ios/release.sh
#
# Credentials, in precedence order:
# 1. App Store Connect API key, if ASC_KEY_ID/ASC_ISSUER_ID are exported
# (put AuthKey_<KEY_ID>.p8 in ~/.appstoreconnect/private_keys/ first).
# 2. App-specific password pulled from the macOS login keychain item
# "AC_PASSWORD" — never stored on disk. Apple ID defaults to APPLE_ID
# below. See "Keychain setup" at the bottom to (re)store it on a machine.
set -euo pipefail
# Apple ID for the app-specific-password upload path; override by exporting
# APPLE_ID. The password is read from the login keychain (no plaintext on disk);
# override with APP_PW=… for a one-off.
APPLE_ID="${APPLE_ID:-nfarina@me.com}"
APP_PW="${APP_PW:-@keychain:AC_PASSWORD}"
# Which Xcode to build with. App Store Connect rejects both *beta* SDKs and
# anything older than the current Release Candidate (ITMS-90111), so this must
# point at the latest RC/GM Xcode — NOT whatever `xcode-select` happens to be.
# Override per-run, e.g.:
# DEVELOPER_DIR=/Applications/Xcode-26.6.app/Contents/Developer ./ios/release.sh
# Check the accepted versions at https://developer.apple.com/news/releases/.
export DEVELOPER_DIR="${DEVELOPER_DIR:-/Applications/Xcode.app/Contents/Developer}"
ARCHIVE=/tmp/Sage.xcarchive
EXPORT=/tmp/SageExport
IPA="$EXPORT/Sage.ipa"
echo "Using $(xcodebuild -version | tr '\n' ' ')"
rm -rf "$ARCHIVE" "$EXPORT"
xcodebuild archive \
-project ios/Sage.xcodeproj -scheme Sage \
-destination 'generic/platform=iOS' \
-archivePath "$ARCHIVE" \
-allowProvisioningUpdates
xcodebuild -exportArchive \
-archivePath "$ARCHIVE" \
-exportPath "$EXPORT" \
-exportOptionsPlist ios/ExportOptions.plist \
-allowProvisioningUpdates
echo "Built $IPA"
if [[ -n "${ASC_KEY_ID:-}" ]]; then
echo "Validating + uploading with App Store Connect API key…"
xcrun altool --validate-app -f "$IPA" -t ios --apiKey "$ASC_KEY_ID" --apiIssuer "$ASC_ISSUER_ID"
xcrun altool --upload-app -f "$IPA" -t ios --apiKey "$ASC_KEY_ID" --apiIssuer "$ASC_ISSUER_ID"
elif [[ -n "${APP_PW:-}" ]]; then
echo "Validating + uploading as $APPLE_ID with app-specific password…"
xcrun altool --validate-app -f "$IPA" -t ios -u "$APPLE_ID" -p "$APP_PW"
xcrun altool --upload-app -f "$IPA" -t ios -u "$APPLE_ID" -p "$APP_PW"
else
echo "No credentials found — IPA is ready at $IPA."
echo "Store the keychain item (see below), or re-run with APP_PW / ASC_KEY_ID+ASC_ISSUER_ID."
fi
# --- Keychain setup (one-time per machine) ----------------------------------
# Store the app-specific password (account.apple.com → Sign-In & Security) in
# the login keychain so this script reads it without a plaintext file. altool's
# own --store-password-in-keychain-item flag is inconsistent across versions, so
# create the item with `security` and grant altool access so it won't prompt:
#
# ALTOOL="$DEVELOPER_DIR/../SharedFrameworks/ContentDelivery.framework/Resources/altool"
# security add-generic-password -U -s AC_PASSWORD -a "$APPLE_ID" \
# -w "<app-specific-password>" -T "$ALTOOL" -T /usr/bin/xcrun
#
# Verify: xcrun altool --validate-app -f <ipa> -t ios -u "$APPLE_ID" -p @keychain:AC_PASSWORD
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment