Agent skill

testflight

Build and upload visionOS/iOS/macOS apps to TestFlight via App Store Connect API. Handles archive, export, and upload workflow with rsync bug workaround. Use when deploying to TestFlight or App Store Connect.

Stars 163
Forks 31

Install this agent skill to your Project

npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/data/testflight

SKILL.md

TestFlight Upload Skill

Build, export, and upload visionOS/iOS/macOS apps to TestFlight using App Store Connect API.

Quick Start

bash
# 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

bash
# 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:

  1. Run full script via agent (archive + export will complete):

    bash
    ~/.pi/agent/skills/testflight/upload.sh PfizerOutdoCancer PfizerOutdoCancer
    

    (Let it timeout on upload step - IPA is already created)

  2. Complete upload manually in terminal:

    bash
    ~/.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:

  1. Archive → Build .xcarchive
  2. Export → Convert to .ipa with App Store signing
  3. Upload → Send .ipa to 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

bash
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

bash
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
<?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 testFlightInternalTestingOnly to true for internal testing only (build won't be selectable for App Store). Remove this key or set to false for App Store submission builds.

bash
xcodebuild -exportArchive \
  -archivePath ~/Desktop/YourApp.xcarchive \
  -exportOptionsPlist ~/Desktop/ExportOptions.plist \
  -exportPath ~/Desktop/YourApp_Export \
  -allowProvisioningUpdates

Success: ** EXPORT SUCCEEDED **

Step 3: Upload IPA

bash
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):

xml
<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:

xml
<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:

xml
<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/DerivedData for proper SPM package resolution
  • Exports .pkg instead of .ipa

How to Upload Media Server

bash
# 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

  1. Detects workspace and uses -workspace flag
  2. Uses platform=macOS destination (not generic/platform=macOS)
  3. Sets -derivedDataPath ./build/DerivedData for correct SPM resolution
  4. Looks for .pkg output instead of .ipa

Post-Upload

  1. Wait 10-15 minutes for Apple processing
  2. Check: https://appstoreconnect.apple.com/apps
  3. Go to your app → TestFlight tab
  4. 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

bash
~/.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.xcarchive
  • README.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:

bash
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:

bash
# 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:

  1. Direct archive upload (--upload-app) FAILS with "IPA is invalid. It does not include a Payload directory"
  2. Must export to IPA first with uploadSymbols=false (rsync bug workaround)
  3. Then upload IPA with --upload-package
  4. Successful upload: Version 1.2 (67) on Oct 31, 2025 at 4:45 PM

Didn't find tool you were looking for?

Be as detailed as possible for better results