TestFlight Upload Skill
Build, export, and upload visionOS/iOS/macOS apps to TestFlight using App Store Connect API.
Quick Start
# Full build + upload for internal testing (default)
~/.pi/agent/skills/testflight/upload.sh <project_name> <scheme_name>
# Full build + upload for App Store submission
~/.pi/agent/skills/testflight/upload.sh <project_name> <scheme_name> --appstore
# Upload existing IPA only (skip archive/export)
~/.pi/agent/skills/testflight/upload.sh --upload-only <scheme_name>
Examples
# Internal testing only (default) - works in TestFlight, NOT selectable for App Store
~/.pi/agent/skills/testflight/upload.sh PfizerOutdoCancer PfizerOutdoCancer
# App Store distribution - works in TestFlight AND selectable for App Store submission
~/.pi/agent/skills/testflight/upload.sh PfizerOutdoCancer PfizerOutdoCancer --appstore
# Upload only - if IPA/PKG already exists at ~/Desktop/<scheme>_Export/
~/.pi/agent/skills/testflight/upload.sh --upload-only PfizerOutdoCancer
# macOS app (Media Server) - run from repo root, not subdirectory!
cd /path/to/groovetech-media-server
~/.pi/agent/skills/testflight/upload.sh GrooveTechMediaServer "GrooveTech Media Server" --appstore macos
Credentials are auto-loaded from ~/.config/testflight/credentials.env.
Distribution Modes
| Mode | Flag | TestFlight | App Store Submission |
|---|---|---|---|
| Internal Only | (default) | ✅ Works | ❌ NOT selectable |
| App Store | --appstore |
✅ Works | ✅ Selectable |
Use Internal Only (default) for regular testing - faster iteration, no review needed.
Use --appstore when you're ready to submit to the App Store - build will be selectable in the "Build" section of your App Store version.
⚠️ Timeout Warning for Large Apps
Upload of large IPAs (1GB+) takes 15-30 minutes. This exceeds typical agent command timeouts.
Recommended workflow for large apps:
Run full script via agent (archive + export will complete):
~/.pi/agent/skills/testflight/upload.sh PfizerOutdoCancer PfizerOutdoCancer(Let it timeout on upload step - IPA is already created)
Complete upload manually in terminal:
~/.pi/agent/skills/testflight/upload.sh --upload-only PfizerOutdoCancer
The --upload-only flag skips rebuild and directly uploads the existing IPA from ~/Desktop/<scheme>_Export/.
Important: The Correct Upload Workflow
DO NOT upload archives directly. The workflow is:
- Archive → Build
.xcarchive - Export → Convert to
.ipawith App Store signing - Upload → Send
.ipato App Store Connect
The export step is critical because:
- Re-signs the app for App Store distribution
- Creates proper IPA structure with Payload directory
- Works around macOS rsync bug (set
uploadSymbols=false)
Credentials
Location: ~/.config/testflight/credentials.env
TESTFLIGHT_API_KEY_ID="KPQ86W6JWV"
TESTFLIGHT_ISSUER_ID="69a6de8c-f4be-47e3-e053-5b8c7c11a4d1"
TESTFLIGHT_TEAM_ID="UTK59YE75G"
API Key files: ~/.appstoreconnect/private_keys/AuthKey_<KEY_ID>.p8
The Three Steps (Manual)
Step 1: Build Archive
xcodebuild -project YourApp.xcodeproj \
-scheme YourApp \
archive \
-archivePath ~/Desktop/YourApp.xcarchive \
-destination 'generic/platform=visionOS'
Success: ** ARCHIVE SUCCEEDED **
Step 2: Export to IPA
Create ExportOptions.plist (uploadSymbols must be false for rsync bug):
<?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>compileBitcode</key>
<false/>
<key>destination</key>
<string>export</string>
<key>method</key>
<string>app-store</string>
<key>signingStyle</key>
<string>automatic</string>
<key>stripSwiftSymbols</key>
<true/>
<key>teamID</key>
<string>UTK59YE75G</string>
<key>testFlightInternalTestingOnly</key>
<true/>
<key>uploadSymbols</key>
<false/>
</dict>
</plist>
Note: Set
testFlightInternalTestingOnlytotruefor internal testing only (build won't be selectable for App Store). Remove this key or set tofalsefor App Store submission builds.
xcodebuild -exportArchive \
-archivePath ~/Desktop/YourApp.xcarchive \
-exportOptionsPlist ~/Desktop/ExportOptions.plist \
-exportPath ~/Desktop/YourApp_Export \
-allowProvisioningUpdates
Success: ** EXPORT SUCCEEDED **
Step 3: Upload IPA
xcrun altool --upload-package ~/Desktop/YourApp_Export/YourApp.ipa \
--type visionos \
--apiKey KPQ86W6JWV \
--apiIssuer 69a6de8c-f4be-47e3-e053-5b8c7c11a4d1
Success: No errors uploading
Common Errors
"IPA is invalid. It does not include a Payload directory"
Cause: Tried to upload .xcarchive instead of exported .ipa
Fix: Run the export step first. Never upload archives directly.
rsync Error During Export
error: exportArchive Copy failed
rsync: --extended-attributes: unknown option
Cause: macOS ships with ancient rsync (2006)
Fix: Set uploadSymbols to false in ExportOptions.plist
CFBundleVersion Mismatch
Cause: Extension build number doesn't match main app
Fix: Update extension build number in Xcode to match main app
API Key Not Found
Fix: Copy .p8 file to ~/.appstoreconnect/private_keys/AuthKey_<KEY_ID>.p8
RPBroadcastProcessMode Not Specified (Broadcast Extension)
Invalid Info.plist value. The value for the key 'RPBroadcastProcessMode'
in bundle YourApp.app/PlugIns/Extension.appex is invalid.
The key was not specified.
Cause: RPBroadcastProcessMode is in wrong location in extension Info.plist
Fix: The key MUST be a direct child of NSExtension dict (not at top level, not inside NSExtensionAttributes):
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.broadcast-services-upload</string>
<key>NSExtensionPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).SampleHandler</string>
<key>RPBroadcastProcessMode</key>
<string>RPBroadcastProcessModeSampleBuffer</string>
</dict>
Invalid Bundle Type (screen-capture entitlement)
Invalid bundle type. The com.apple.developer.screen-capture.include-passthrough
entitlement is not valid for bundles of type XPC!.
Cause: com.apple.developer.screen-capture.include-passthrough is in the main app
Fix: Remove from main app .entitlements. This entitlement is ONLY valid in broadcast extensions.
Missing NSEnterpriseMCAMUsageDescription
Cause: App uses enterprise camera APIs without privacy description
Fix: Add to main app Info.plist:
<key>NSEnterpriseMCAMUsageDescription</key>
<string>This app uses main camera access for enterprise features.</string>
Missing BGTaskSchedulerPermittedIdentifiers
Cause: App has processing background mode but no task identifiers
Fix: Add to main app Info.plist:
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>com.yourcompany.app.background-refresh</string>
</array>
Platform Types
| Platform | --type value |
Destination | Package |
|---|---|---|---|
| visionOS | visionos |
generic/platform=visionOS |
.ipa |
| iOS | ios |
generic/platform=iOS |
.ipa |
| macOS | macos |
platform=macOS |
.pkg |
macOS Apps (Media Server)
Media Server has a special setup - it uses a workspace at the repo root, not a project file in a subdirectory.
Why It's Different
- Uses
GrooveTechMediaServer.xcworkspace(not.xcodeproj) - Workspace lives at repo root (not in a subdirectory)
- Needs local
build/DerivedDatafor proper SPM package resolution - Exports
.pkginstead of.ipa
How to Upload Media Server
# 1. Navigate to repo ROOT (not the subdirectory!)
cd "/path/to/groovetech-media-server"
# 2. Run upload with workspace name, scheme, and macos platform
~/.pi/agent/skills/testflight/upload.sh GrooveTechMediaServer "GrooveTech Media Server" --appstore macos
Common mistake: Running from groovetech-media-server/GrooveTech Media Server/ subdirectory - this will fail because the workspace is at the parent level.
What the Script Does for macOS
- Detects workspace and uses
-workspaceflag - Uses
platform=macOSdestination (notgeneric/platform=macOS) - Sets
-derivedDataPath ./build/DerivedDatafor correct SPM resolution - Looks for
.pkgoutput instead of.ipa
Post-Upload
- Wait 10-15 minutes for Apple processing
- Check: https://appstoreconnect.apple.com/apps
- Go to your app → TestFlight tab
- Add testers (internal = immediate, external = beta review required)
Enterprise Delivery (for IT re-signing)
When a client needs to re-sign the app with their enterprise certificate (e.g., for internal app stores/MDM), provide the .xcarchive with a README.
Create Delivery Package
~/.pi/agent/skills/testflight/create-delivery.sh \
~/Desktop/MyApp.xcarchive \
"App Name" \
"1.0" \
"42" \
"com.company.bundleid" \
"visionOS (Apple Vision Pro)" \
"Description of the app"
This creates: ~/Desktop/App_Name_Delivery_v1.0_Build42/
MyApp.xcarchiveREADME.md(re-signing instructions)
GrooveTech Apps Reference
| App | Bundle ID | Platform | Project | Scheme |
|---|---|---|---|---|
| Outdo Cancer | com.groovejones.PfizerOutdoCancer |
visionOS | PfizerOutdoCancerV2.xcodeproj | PfizerOutdoCancer |
| GMP | com.groovetech.media-player |
visionOS | groovetech-media-player.xcodeproj | groovetech-media-player |
| Media Server | com.groovetech.media-server |
macOS | GrooveTechMediaServer.xcworkspace | GrooveTech Media Server |
| Orchestrator | com.groovejones.orchestrator |
iOS | orchestrator.xcodeproj | orchestrator |
Querying TestFlight Builds
Check current build numbers on App Store Connect:
source ~/.config/testflight/credentials.env
export TESTFLIGHT_API_KEY_ID TESTFLIGHT_ISSUER_ID
# Use node to query API (requires JWT signing)
# App IDs: GMP=6757438008, MS=6757471795, Orchestrator=6754814714, Pfizer=6737364780
See session history for full API query example.
Session History (for agents)
To find the original sessions where this workflow was developed and tested:
# Search for TestFlight upload sessions
cass search "upload-to-testflight-complete" --robot --limit 10 --days 365
# Key sessions:
# - Claude debug file with failed archive upload + successful IPA workflow:
# ~/.claude/debug/a8d1679e-6d2f-4e2c-b4a9-7379c4181945.txt
#
# - Git commit that added the scripts (Oct 22, 2025):
# git show e4cccf46 --stat
#
# - Git commit that updated guide after successful upload (Oct 31, 2025):
# git show d4f0674c
Key learnings from sessions:
- Direct archive upload (
--upload-app) FAILS with "IPA is invalid. It does not include a Payload directory" - Must export to IPA first with
uploadSymbols=false(rsync bug workaround) - Then upload IPA with
--upload-package - Successful upload: Version 1.2 (67) on Oct 31, 2025 at 4:45 PM