archive
Archive and distribute forager to TestFlight. Auto-increments build number, archives for Release, uploads to App Store Connect, waits for processing, sets export compliance, and adds to beta test grou
SKILL.md.claude/skills/archive/SKILL.md
GitHubArchive & Distribute to TestFlight
Full pipeline: version bump → archive → upload → distribute via script.
CRITICAL: Permission-safe execution. Run each operation as a SEPARATE Bash tool call. Never chain commands with &&, ||, or pipes. Never combine multiple operations in one Bash call. One command per call. This ensures all commands match pre-approved permission patterns.
Current State
- Marketing version: !
grep -m1 'MARKETING_VERSION' forager.xcodeproj/project.pbxproj - Build number: !
grep -m1 'CURRENT_PROJECT_VERSION' forager.xcodeproj/project.pbxproj - Branch: !
git branch --show-current - Uncommitted changes: !
git status --short
API key check — Run this as your first step (do NOT use ! backtick for this since it's outside the project dir):
test -f ~/.appstoreconnect/config && echo "API KEY CONFIGURED" || echo "NOT CONFIGURED"
Prerequisites: App Store Connect API Key
The TestFlight automation requires an App Store Connect API key. Check if configured (run as a Bash call):
test -f ~/.appstoreconnect/config && cat ~/.appstoreconnect/config || echo "NOT CONFIGURED"
If not configured, guide the user through setup:
- Go to App Store Connect → Users and Access → Integrations → App Store Connect API
- Click Generate API Key (or use existing)
- Role: Developer (minimum for TestFlight management)
- Download the
.p8file (can only download ONCE) - Note the Key ID and Issuer ID shown on the page
Then create the config:
mkdir -p ~/.appstoreconnect/private_keys
# User provides these values:
cat > ~/.appstoreconnect/config << 'EOF'
KEY_ID=XXXXXXXXXX
ISSUER_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
EOF
# User places their .p8 file:
# cp ~/Downloads/AuthKey_XXXXXXXXXX.p8 ~/.appstoreconnect/private_keys/
If API key is not set up and user wants to skip TestFlight automation, run steps 1-6 only and report that TestFlight distribution was skipped.
Step 1: Pre-Flight Checks
Verify before archiving:
- On
mainbranch (archives should be from main after PR merge) - Working tree is clean (no uncommitted changes)
- Build succeeds in Release configuration
If not on main or there are uncommitted changes, warn the user and ask whether to proceed.
Step 2: Auto-Increment Build Number
The build number (CURRENT_PROJECT_VERSION) auto-increments on every archive.
Read current value — run as separate calls:
grep -m1 'CURRENT_PROJECT_VERSION = ' forager.xcodeproj/project.pbxproj
Parse the number from output. Compute NEW_BUILD = CURRENT_BUILD + 1.
Update both Debug and Release — single script call:
Tools/bump-build.sh CURRENT_BUILD NEW_BUILD
Show result and confirm with user:
Version: 1.2 (build 24)
Step 3: Optional — Bump Marketing Version
Only if the user explicitly requests a version bump (e.g., /archive --version 1.3):
# $ARGUMENTS may contain: --version X.Y
# If present, update MARKETING_VERSION in the first two occurrences (app target only)
Marketing version format: MAJOR.MINOR (e.g., 1.2, 1.3, 2.0). Validate format before applying.
Step 4: Commit Version Bump
Use the current milestone from docs/current-story.md for the commit prefix. If no active milestone, use a generic prefix.
Run as three separate calls:
git add forager.xcodeproj/project.pbxproj
git commit -m "Bump build number to $NEW_BUILD for TestFlight"
git push
Step 5: Archive
xcodebuild archive \
-project forager.xcodeproj \
-scheme forager \
-archivePath ~/Desktop/forager-$MARKETING_VERSION-$NEW_BUILD.xcarchive \
-destination 'generic/platform=iOS' \
2>&1 | tail -5
This uses Release configuration by default (CloudKit enabled).
On success, report:
- Archive path
- Version and build number
- Archive size
On failure:
- Show full error output
- Check signing issues first (most common)
Step 6: Export & Upload to App Store Connect
Use the Write tool to create /tmp/ForagerExportOptions.plist with this content:
<?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>teamID</key>
<string>93FT2PT9NM</string>
<key>destination</key>
<string>upload</string>
<key>signingStyle</key>
<string>automatic</string>
</dict>
</plist>
Then run export as a separate call. CRITICAL: Always include API key auth flags — without them, xcodebuild cannot find App Store Connect credentials from CLI:
xcodebuild -exportArchive -archivePath ~/Desktop/forager-$MARKETING_VERSION-$NEW_BUILD.xcarchive -exportOptionsPlist /tmp/ForagerExportOptions.plist -exportPath ~/Desktop/forager-export -allowProvisioningUpdates -authenticationKeyPath /Users/rich/.appstoreconnect/private_keys/AuthKey_QBAG38ZPZR.p8 -authenticationKeyID QBAG38ZPZR -authenticationKeyIssuerID fbdd459e-6ae5-4dc5-82d0-c2bf095392e4
The destination: upload auto-uploads to App Store Connect.
Step 7: TestFlight Distribution (Automated Script)
After upload succeeds, run the distribution script. This single command handles:
- JWT generation
- Build processing poll (up to 20 minutes)
- Export compliance verification
- "What to Test" notes (from latest commit or custom text)
- Beta review submission + approval wait
- Adding build to Public Beta Testers group
Tools/appstore-connect/testflight-distribute.sh $NEW_BUILD
Or with custom "What to Test" notes:
Tools/appstore-connect/testflight-distribute.sh $NEW_BUILD "Custom release notes here"
The script outputs progress and a final report. If it fails at any step, it exits with a non-zero code and an error message.
If the script fails, check:
- JWT issues → verify
~/.appstoreconnect/configand.p8file - Build not found → upload may still be propagating, wait 2 min and re-run
- Review stuck → check App Store Connect web UI
Step 8: Final Report
After the script completes, report:
✅ Archive & TestFlight Distribution Complete
Version: $MARKETING_VERSION (build $NEW_BUILD)
Archive: ~/Desktop/forager-$MARKETING_VERSION-$NEW_BUILD.xcarchive
Status: Available to testers (see script output for details)
Arguments
- No arguments: full pipeline (archive → upload → distribute to Public Beta Testers)
--version X.Y: also bump marketing version (e.g.,/archive --version 1.3)--no-upload: archive and export only, skip TestFlight upload and distribution--no-wait: upload only, skip distribution script--dry-run: show what would happen without making changes--testflight-only: skip archive/upload, run only the distribution script (for builds already uploaded)
Configuration
- Team ID: 93FT2PT9NM
- Bundle ID: com.richhayn.forager
- Signing: Automatic (Apple Development → Distribution managed by Xcode)
- CloudKit: Enabled in Release, disabled in Debug
- API Key Config:
~/.appstoreconnect/config(KEY_ID, ISSUER_ID) - API Key File:
~/.appstoreconnect/private_keys/AuthKey_{KEY_ID}.p8 - JWT Generator:
Tools/appstore-connect/generate-jwt.swift - Distribution Script:
Tools/appstore-connect/testflight-distribute.sh
Troubleshooting
| Issue | Fix |
|---|---|
No signing certificate |
Open Xcode → Signing & Capabilities → resolve. Or: security find-identity -v -p codesigning |
Provisioning profile |
Xcode auto-manages. Try: Xcode → Preferences → Accounts → Download Manual Profiles |
Export failed |
Try xcodebuild -exportArchive with -allowProvisioningUpdates flag |
Upload rejected |
Check build number isn't reused. App Store Connect rejects duplicate build numbers per version |
JWT generation failed |
Verify .p8 file path and format. Run: swift Tools/appstore-connect/generate-jwt.swift <path> <key-id> <issuer-id> manually |
API returns 401 |
JWT may be expired (20 min). Script auto-refreshes after 15 poll cycles |
API returns 403 |
API key role too restrictive. Needs at least "Developer" role |
Build not found after upload |
Wait 2-5 minutes for propagation, then re-run the distribution script |
Build stuck in PROCESSING |
Normal for up to 15 min. If >30 min, check App Store Connect web UI |
Export compliance already set |
Info.plist has ITSAppUsesNonExemptEncryption. Script handles this automatically |