commit cc89aadad02adbe78696fedc38362c7c21b1f1ce
parent a18a5e59728f1e9182b392786ae8ee9dd1bdff48
Author: Michael Camilleri <[email protected]>
Date: Mon, 2 Mar 2026 10:20:24 +0900
Add build and publishing scripts
Diffstat:
3 files changed, 235 insertions(+), 6 deletions(-)
diff --git a/AGENTS.md b/AGENTS.md
@@ -21,20 +21,23 @@
- `xcodebuild test -scheme "Listless iOS" -destination 'platform=iOS Simulator,name=iPhone 16,OS=18.6'` runs unit + UI tests.
- `swift format lint --recursive .` must be clean before opening a PR.
-## TestFlight Release
-- Publish scripts live in `.asc/` and handle archiving, signing, exporting, and uploading via `xcrun iTMSTransporter`.
+## App Store Connect Release
+- Publish scripts live in `Scripts/` and handle archiving, signing, exporting, and uploading via `xcrun iTMSTransporter`.
+- Secrets (team ID, API key ID, issuer ID, p12 passwords) are sourced from `.asc/secrets.sh`, which is gitignored.
- Signing uses a temporary keychain (created and cleaned up by the scripts) with distribution `.p12` files from `.asc/`. No login keychain unlock is required.
- App Store Connect API auth uses a `.p8` key file in `.asc/`, referenced by `$KEY_ID` from `secrets.sh`.
- Build numbers come from scheme pre-action (`YEAR.COMMIT_COUNT`), so archiving from latest `HEAD` produces the latest-commit build.
- Internal testers automatically receive all builds; no explicit group assignment is needed.
+- Both scripts archive with signing enabled to preserve entitlements (CloudKit, App Sandbox). Do not disable code signing during the archive step.
### iOS
-- Run `.asc/publish-ios.sh` from the repo root.
-- Archives without code signing, then exports an IPA with manual signing (`iPhone Distribution` cert, `Listless iOS Distribution` profile).
+- Run `Scripts/publish-ios.sh` from the repo root.
+- Pass `--check` to archive, export, and inspect entitlements without uploading.
+- Archives with signing, then exports an IPA with manual signing (`iPhone Distribution` cert, `Listless iOS Distribution` profile).
### macOS
-- Run `.asc/publish-macos.sh` from the repo root.
-- Archives with signing (required to preserve App Sandbox entitlements), then exports a `.pkg` with manual signing (`3rd Party Mac Developer Application` + `3rd Party Mac Developer Installer` certs, `Listless macOS Distribution` profile).
+- Run `Scripts/publish-macos.sh` from the repo root.
+- Archives with signing, then exports a `.pkg` with manual signing (`3rd Party Mac Developer Application` + `3rd Party Mac Developer Installer` certs, `Listless macOS Distribution` profile).
## Build Number
- `CFBundleVersion` in both Info.plist files uses `$(CURRENT_PROJECT_VERSION)`, sourced from `Generated/BuildNumber.xcconfig`.
diff --git a/Scripts/publish-ios.sh b/Scripts/publish-ios.sh
@@ -0,0 +1,118 @@
+#!/bin/bash
+set -euo pipefail
+
+REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
+SECRETS="$REPO_ROOT/.asc/secrets.sh"
+
+if [[ ! -f "$SECRETS" ]]; then
+ echo "Error: $SECRETS not found. See .asc/secrets.sh.example for the required format."
+ exit 1
+fi
+
+source "$SECRETS"
+
+SCHEME="Listless iOS"
+ARCHIVE_PATH="/tmp/Listless-latest.xcarchive"
+EXPORT_PATH="/tmp/Listless-export"
+EXPORT_PLIST="/tmp/Listless-ExportOptions.plist"
+IPA_PATH="$EXPORT_PATH/Listless iOS.ipa"
+DIST_P12="$REPO_ROOT/.asc/ios-signing/dist-headless.p12"
+TMP_KEYCHAIN="$REPO_ROOT/.asc/build.keychain-db"
+
+CHECK_ONLY=false
+if [[ "${1:-}" == "--check" ]]; then
+ CHECK_ONLY=true
+fi
+
+cd "$REPO_ROOT"
+
+echo "==> Setting up temporary keychain..."
+security delete-keychain "$TMP_KEYCHAIN" 2>/dev/null || true
+security create-keychain -p "$TMP_KEYCHAIN_PASS" "$TMP_KEYCHAIN"
+security unlock-keychain -p "$TMP_KEYCHAIN_PASS" "$TMP_KEYCHAIN"
+security set-keychain-settings -lut 21600 "$TMP_KEYCHAIN"
+security import "$DIST_P12" -k "$TMP_KEYCHAIN" -P "$DIST_P12_PASS" \
+ -T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild
+security set-key-partition-list -S apple-tool:,apple:,codesign:,productbuild: \
+ -s -k "$TMP_KEYCHAIN_PASS" "$TMP_KEYCHAIN"
+security list-keychains -d user -s "$TMP_KEYCHAIN" ~/Library/Keychains/login.keychain-db
+
+cleanup_keychain() {
+ echo "==> Restoring keychain search list..."
+ security list-keychains -d user -s ~/Library/Keychains/login.keychain-db
+ security default-keychain -d user -s ~/Library/Keychains/login.keychain-db
+ security delete-keychain "$TMP_KEYCHAIN" 2>/dev/null || true
+}
+trap cleanup_keychain EXIT
+
+echo "==> Archiving $SCHEME..."
+xcodebuild \
+ -scheme "$SCHEME" \
+ -project Listless.xcodeproj \
+ -configuration Release \
+ -destination 'generic/platform=iOS' \
+ -archivePath "$ARCHIVE_PATH" \
+ archive
+
+echo "==> Writing export options..."
+cat > "$EXPORT_PLIST" <<PLIST
+<?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>
+ <key>method</key>
+ <string>app-store-connect</string>
+ <key>signingStyle</key>
+ <string>manual</string>
+ <key>teamID</key>
+ <string>$TEAM_ID</string>
+ <key>signingCertificate</key>
+ <string>iPhone Distribution</string>
+ <key>provisioningProfiles</key>
+ <dict>
+ <key>net.inqk.listless</key>
+ <string>Listless iOS Distribution</string>
+ </dict>
+ <key>destination</key>
+ <string>export</string>
+ <key>stripSwiftSymbols</key>
+ <true/>
+ <key>manageAppVersionAndBuildNumber</key>
+ <false/>
+</dict>
+</plist>
+PLIST
+
+echo "==> Exporting IPA..."
+xcodebuild \
+ -exportArchive \
+ -archivePath "$ARCHIVE_PATH" \
+ -exportPath "$EXPORT_PATH" \
+ -exportOptionsPlist "$EXPORT_PLIST"
+
+echo "==> Checking entitlements in exported IPA..."
+CHECK_DIR="/tmp/Listless-ipa-check"
+rm -rf "$CHECK_DIR"
+unzip -q "$IPA_PATH" -d "$CHECK_DIR"
+codesign -d --entitlements - "$CHECK_DIR/Payload/Listless iOS.app"
+rm -rf "$CHECK_DIR"
+
+if $CHECK_ONLY; then
+ echo "==> Check complete. Skipping upload."
+ exit 0
+fi
+
+echo "==> Uploading to App Store Connect..."
+mkdir -p "$REPO_ROOT/private_keys"
+cp "$REPO_ROOT/.asc/AuthKey_${KEY_ID}.p8" "$REPO_ROOT/private_keys/"
+xcrun iTMSTransporter \
+ -m upload \
+ -assetFile "$IPA_PATH" \
+ -apiKey "$KEY_ID" \
+ -apiIssuer "$ISSUER_ID" \
+ -v informational
+rm -f "$REPO_ROOT/private_keys/AuthKey_${KEY_ID}.p8"
+rmdir "$REPO_ROOT/private_keys" 2>/dev/null || true
+
+echo "==> Done!"
diff --git a/Scripts/publish-macos.sh b/Scripts/publish-macos.sh
@@ -0,0 +1,108 @@
+#!/bin/bash
+set -euo pipefail
+
+REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
+SECRETS="$REPO_ROOT/.asc/secrets.sh"
+
+if [[ ! -f "$SECRETS" ]]; then
+ echo "Error: $SECRETS not found. See .asc/secrets.sh.example for the required format."
+ exit 1
+fi
+
+source "$SECRETS"
+
+APP_ID="6759801710"
+SCHEME="Listless macOS"
+ARCHIVE_PATH="/tmp/Listless-mac-latest.xcarchive"
+EXPORT_PATH="/tmp/Listless-mac-export"
+EXPORT_PLIST="/tmp/Listless-mac-ExportOptions.plist"
+PKG_PATH="$EXPORT_PATH/Listless.pkg"
+SIGNING_DIR="$REPO_ROOT/.asc/macos-signing"
+APP_P12="$SIGNING_DIR/app-headless.p12"
+INSTALLER_P12="$SIGNING_DIR/installer-headless.p12"
+TMP_KEYCHAIN="$REPO_ROOT/.asc/build.keychain-db"
+
+cd "$REPO_ROOT"
+
+echo "==> Setting up temporary keychain..."
+security delete-keychain "$TMP_KEYCHAIN" 2>/dev/null || true
+security create-keychain -p "$TMP_KEYCHAIN_PASS" "$TMP_KEYCHAIN"
+security unlock-keychain -p "$TMP_KEYCHAIN_PASS" "$TMP_KEYCHAIN"
+security set-keychain-settings -lut 21600 "$TMP_KEYCHAIN"
+security import "$APP_P12" -k "$TMP_KEYCHAIN" -P "$DIST_P12_PASS" \
+ -T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild
+security import "$INSTALLER_P12" -k "$TMP_KEYCHAIN" -P "$DIST_P12_PASS" \
+ -T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild
+security set-key-partition-list -S apple-tool:,apple:,codesign:,productbuild: \
+ -s -k "$TMP_KEYCHAIN_PASS" "$TMP_KEYCHAIN"
+security list-keychains -d user -s "$TMP_KEYCHAIN" ~/Library/Keychains/login.keychain-db
+
+cleanup_keychain() {
+ echo "==> Restoring keychain search list..."
+ security list-keychains -d user -s ~/Library/Keychains/login.keychain-db
+ security default-keychain -d user -s ~/Library/Keychains/login.keychain-db
+ security delete-keychain "$TMP_KEYCHAIN" 2>/dev/null || true
+}
+trap cleanup_keychain EXIT
+
+echo "==> Archiving $SCHEME..."
+xcodebuild \
+ -scheme "$SCHEME" \
+ -project Listless.xcodeproj \
+ -configuration Release \
+ -destination 'generic/platform=macOS' \
+ -archivePath "$ARCHIVE_PATH" \
+ archive
+
+echo "==> Writing export options..."
+cat > "$EXPORT_PLIST" <<PLIST
+<?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>
+ <key>method</key>
+ <string>app-store-connect</string>
+ <key>signingStyle</key>
+ <string>manual</string>
+ <key>teamID</key>
+ <string>$TEAM_ID</string>
+ <key>signingCertificate</key>
+ <string>3rd Party Mac Developer Application</string>
+ <key>installerSigningCertificate</key>
+ <string>3rd Party Mac Developer Installer</string>
+ <key>provisioningProfiles</key>
+ <dict>
+ <key>net.inqk.listless</key>
+ <string>Listless macOS Distribution</string>
+ </dict>
+ <key>destination</key>
+ <string>export</string>
+ <key>stripSwiftSymbols</key>
+ <true/>
+ <key>manageAppVersionAndBuildNumber</key>
+ <false/>
+</dict>
+</plist>
+PLIST
+
+echo "==> Exporting PKG..."
+xcodebuild \
+ -exportArchive \
+ -archivePath "$ARCHIVE_PATH" \
+ -exportPath "$EXPORT_PATH" \
+ -exportOptionsPlist "$EXPORT_PLIST"
+
+echo "==> Uploading to App Store Connect..."
+mkdir -p "$REPO_ROOT/private_keys"
+cp "$REPO_ROOT/.asc/AuthKey_${KEY_ID}.p8" "$REPO_ROOT/private_keys/"
+xcrun iTMSTransporter \
+ -m upload \
+ -assetFile "$PKG_PATH" \
+ -apiKey "$KEY_ID" \
+ -apiIssuer "$ISSUER_ID" \
+ -v informational
+rm -f "$REPO_ROOT/private_keys/AuthKey_${KEY_ID}.p8"
+rmdir "$REPO_ROOT/private_keys" 2>/dev/null || true
+
+echo "==> Done!"