Watch: TestFlight archive, iOS stub container, testflight.sh
The App Store only accepts a watch-only app inside an empty iOS stub (watchapp2-container), the pair Xcode's Watch-only App template makes. The stub takes org.gbrown.wristcode; the watch app, and so the APNs topic, becomes org.gbrown.wristcode.watchkitapp. Adds ITSAppUsesNonExemptEncryption = NO and scripts/testflight.sh, which archives with API key automatic signing and a timestamp build number, then exports and uploads (or exports only with --no-upload). Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
16bae4410d
commit
f772f4e4c4
4 files changed
+141
-4
No files matched your search
+24
-3
@@ -13,6 +13,27 @@ xcodebuild -scheme WristCode -destination 'platform=watchOS Simulator,name=Apple
|
||||
xcodebuild -scheme WristCode -destination 'platform=watchOS Simulator,name=Apple Watch Series 12 (46mm)' test
|
||||
```
|
||||
|
||||
## TestFlight
|
||||
|
||||
The App Store only takes a watch-only app inside an empty iOS stub, so `project.yml` has two app
|
||||
targets, the same pair Xcode's "Watch-only App" template makes:
|
||||
|
||||
| Target | Bundle ID | Job |
|
||||
| --- | --- | --- |
|
||||
| `WristCodeContainer` | `org.gbrown.wristcode` | iOS stub that owns the App Store Connect record and embeds the watch app |
|
||||
| `WristCode` | `org.gbrown.wristcode.watchkitapp` | The watch app, and the APNs topic |
|
||||
|
||||
```sh
|
||||
scripts/testflight.sh # archive, export and upload to App Store Connect
|
||||
scripts/testflight.sh --no-upload # archive and export an .ipa to build/export-<n> only
|
||||
```
|
||||
|
||||
The build number defaults to a `YYYYMMDDHHMM` timestamp (override it with `BUILD_NUMBER`). Signing is
|
||||
automatic through the App Store Connect API key in `ASC_KEY_PATH`, `ASC_KEY_ID` and `ASC_ISSUER_ID`,
|
||||
which default to the key in `~/.appstoreconnect/private_keys`. The entitlements file says
|
||||
`aps-environment: development`; the App Store export re-signs it as `production`. Release builds
|
||||
register their token with the gateway as `production`, Debug builds as `sandbox`.
|
||||
|
||||
## Demo mode
|
||||
|
||||
`DemoGateway` serves fixture data shaped like the docs/api.md examples, and it reacts to writes:
|
||||
@@ -20,9 +41,9 @@ Allow moves a thread to working, a reply restarts a finished thread. The SwiftUI
|
||||
the pairing screen has a "Try the demo" button, and launch arguments open a screen directly:
|
||||
|
||||
```sh
|
||||
xcrun simctl launch <udid> org.gbrown.wristcode -demo # feed
|
||||
xcrun simctl launch <udid> org.gbrown.wristcode -demo -thread littleman/t-next # thread
|
||||
xcrun simctl launch <udid> org.gbrown.wristcode -demo -screen new # new thread
|
||||
xcrun simctl launch <udid> org.gbrown.wristcode.watchkitapp -demo # feed
|
||||
xcrun simctl launch <udid> org.gbrown.wristcode.watchkitapp -demo -thread littleman/t-next # thread
|
||||
xcrun simctl launch <udid> org.gbrown.wristcode.watchkitapp -demo -screen new # new thread
|
||||
```
|
||||
|
||||
`screenshots/` holds the screens captured this way.
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>WKApplication</key>
|
||||
<true/>
|
||||
<key>WKWatchOnly</key>
|
||||
|
||||
+26
-1
@@ -11,6 +11,7 @@ settings:
|
||||
SWIFT_VERSION: "6.0"
|
||||
SWIFT_STRICT_CONCURRENCY: complete
|
||||
MARKETING_VERSION: "0.1.0"
|
||||
# Default build number; scripts/testflight.sh overrides it with CURRENT_PROJECT_VERSION=<n>.
|
||||
CURRENT_PROJECT_VERSION: "1"
|
||||
targets:
|
||||
WristCode:
|
||||
@@ -29,14 +30,37 @@ targets:
|
||||
# this and WKRunsIndependentlyOfCompanionApp, and requires WKCompanionAppBundleIdentifier
|
||||
# when WKRunsIndependentlyOfCompanionApp is set without WKWatchOnly.
|
||||
WKWatchOnly: true
|
||||
# HTTPS only, so no export compliance documentation is needed.
|
||||
ITSAppUsesNonExemptEncryption: false
|
||||
entitlements:
|
||||
path: WristCode/WristCode.entitlements
|
||||
properties:
|
||||
# Archives are signed for development; exporting for App Store Connect re-signs with the
|
||||
# distribution profile, which turns this into production.
|
||||
aps-environment: development
|
||||
settings:
|
||||
base:
|
||||
PRODUCT_BUNDLE_IDENTIFIER: org.gbrown.wristcode
|
||||
# The container's bundle ID plus .watchkitapp, as App Store validation requires. This is also
|
||||
# the APNs topic.
|
||||
PRODUCT_BUNDLE_IDENTIFIER: org.gbrown.wristcode.watchkitapp
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
|
||||
# The App Store only takes a watch-only app inside an empty iOS stub, the same pair Xcode's
|
||||
# "Watch-only App" template creates. The stub owns the App Store Connect record
|
||||
# (org.gbrown.wristcode) and embeds the watch app; it never installs on an iPhone.
|
||||
WristCodeContainer:
|
||||
type: application.watchapp2-container
|
||||
platform: iOS
|
||||
deploymentTarget: "27.0"
|
||||
dependencies:
|
||||
- target: WristCode
|
||||
settings:
|
||||
base:
|
||||
PRODUCT_NAME: WristCode
|
||||
PRODUCT_BUNDLE_IDENTIFIER: org.gbrown.wristcode
|
||||
GENERATE_INFOPLIST_FILE: YES
|
||||
INFOPLIST_KEY_CFBundleDisplayName: WristCode
|
||||
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption: NO
|
||||
TARGETED_DEVICE_FAMILY: "1"
|
||||
WristCodeTests:
|
||||
type: bundle.unit-test
|
||||
platform: watchOS
|
||||
@@ -55,6 +79,7 @@ schemes:
|
||||
build:
|
||||
targets:
|
||||
WristCode: all
|
||||
WristCodeContainer: [archive]
|
||||
WristCodeTests: [test]
|
||||
run:
|
||||
config: Debug
|
||||
|
||||
Executable
+89
@@ -0,0 +1,89 @@
|
||||
#!/bin/bash
|
||||
# Archive WristCode and upload it to App Store Connect for TestFlight.
|
||||
#
|
||||
# scripts/testflight.sh archive, then export and upload
|
||||
# scripts/testflight.sh --no-upload archive, then export an .ipa to build/export without uploading
|
||||
#
|
||||
# BUILD_NUMBER defaults to a timestamp so every upload is unique. The App Store Connect API key comes
|
||||
# from ASC_KEY_PATH, ASC_KEY_ID and ASC_ISSUER_ID.
|
||||
set -euo pipefail
|
||||
|
||||
upload=1
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--no-upload) upload=0 ;;
|
||||
*) echo "usage: $0 [--no-upload]" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
TEAM_ID="${TEAM_ID:-GQJNM5YK2M}"
|
||||
ASC_KEY_ID="${ASC_KEY_ID:-864GCL5SDC}"
|
||||
ASC_ISSUER_ID="${ASC_ISSUER_ID:-af37ae36-1f23-4245-b220-a7dc56d1a724}"
|
||||
ASC_KEY_PATH="${ASC_KEY_PATH:-$HOME/.appstoreconnect/private_keys/AuthKey_${ASC_KEY_ID}.p8}"
|
||||
BUILD_NUMBER="${BUILD_NUMBER:-$(date +%Y%m%d%H%M)}"
|
||||
|
||||
build_dir="build"
|
||||
archive_path="$build_dir/WristCode-$BUILD_NUMBER.xcarchive"
|
||||
export_path="$build_dir/export-$BUILD_NUMBER"
|
||||
export_options="$build_dir/ExportOptions.plist"
|
||||
|
||||
auth=(
|
||||
-allowProvisioningUpdates
|
||||
-authenticationKeyPath "$ASC_KEY_PATH"
|
||||
-authenticationKeyID "$ASC_KEY_ID"
|
||||
-authenticationKeyIssuerID "$ASC_ISSUER_ID"
|
||||
)
|
||||
|
||||
if [[ ! -f "$ASC_KEY_PATH" ]]; then
|
||||
echo "App Store Connect API key not found at $ASC_KEY_PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
xcodegen generate
|
||||
mkdir -p "$build_dir"
|
||||
|
||||
echo "Archiving build $BUILD_NUMBER to $archive_path"
|
||||
xcodebuild archive \
|
||||
-project WristCode.xcodeproj \
|
||||
-scheme WristCode \
|
||||
-destination 'generic/platform=watchOS' \
|
||||
-archivePath "$archive_path" \
|
||||
"${auth[@]}" \
|
||||
CURRENT_PROJECT_VERSION="$BUILD_NUMBER"
|
||||
|
||||
if (( upload )); then destination=upload; else destination=export; fi
|
||||
cat > "$export_options" <<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>destination</key>
|
||||
<string>$destination</string>
|
||||
<key>teamID</key>
|
||||
<string>$TEAM_ID</string>
|
||||
<key>signingStyle</key>
|
||||
<string>automatic</string>
|
||||
<key>uploadSymbols</key>
|
||||
<true/>
|
||||
<key>manageAppVersionAndBuildNumber</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
PLIST
|
||||
|
||||
echo "Exporting ($destination) to $export_path"
|
||||
xcodebuild -exportArchive \
|
||||
-archivePath "$archive_path" \
|
||||
-exportPath "$export_path" \
|
||||
-exportOptionsPlist "$export_options" \
|
||||
"${auth[@]}"
|
||||
|
||||
if (( upload )); then
|
||||
echo "Uploaded build $BUILD_NUMBER. It appears in TestFlight once App Store Connect finishes processing."
|
||||
else
|
||||
echo "Exported build $BUILD_NUMBER to $export_path"
|
||||
fi
|
||||
Reference in new issue
Block a user