Skill: ivangdavila/testflight (version 1.0.0)

--- name: TestFlight slug: testflight version: 1.0.0 homepage: https://clawic.com/skills/testflight description: Distribute iOS and macOS beta builds with TestFlight, tester management, and CI/CD automation. metadata: {"clawdbot":{"emoji":"🛫","requires":{"bins":[]},"os":["darwin"]}} --- ## When to Use User needs to distribute beta builds via TestFlight. Agent handles App Store Connect setup, tester groups, build uploads, and CI/CD integration. ## Quick Reference | Topic | File | |-------|------| | CI/CD automation | `ci-cd.md` | ## Core Rules ### 1. App Store Connect Setup First Before uploading: - Bundle ID registered in Developer Portal - App created in App Store Connect - App-specific password or API key configured ### 2. Build Requirements Every TestFlight build needs: - Unique build number (CFBundleVersion) - increment ALWAYS - Valid provisioning profile (App Store distribution) - No missing compliance declarations ### 3. Tester Group Strategy | Group | Purpose | Limit | |-------|---------|-------| | Internal | Team members with App Store Connect access | 100 | | External | Beta testers, requires review | 10,000 | Internal builds available immediately. External requires Apple review (24-48h first time). ### 4. Upload Methods | Method | Best For | |--------|----------| | Xcode | Manual, one-off uploads | | `xcrun altool` | Scripts, CI without fastlane | | Fastlane | Full automation, multiple apps | | Xcode Cloud | Apple-native CI/CD | | Transporter | GUI for non-developers | ### 5. Build Expiration TestFlight builds expire after 90 days. Plan releases accordingly. ## TestFlight Traps - **Build number not incremented** - rejected immediately, must bump and rebuild - **Missing export compliance** - stuck in "Processing" until answered in App Store Connect - **External testers on first build** - requires full beta review, use internal first - **Expired provisioning profile** - upload fails silently, check before archiving - **App-specific password in CI** - use App Store Connect API key instead (more secure, no 2FA issues) ## CI/CD Quick Setup ### App Store Connect API Key (Recommended) 1. App Store Connect > Users > Keys > App Store Connect API 2. Generate key with "App Manager" role 3. Download `.p8` file (only shown ONCE) 4. Note: Issuer ID, Key ID ### Fastlane Upload ```bash # In Fastfile lane :beta do build_app(scheme: "MyApp") upload_to_testflight( api_key_path: "fastlane/api_key.json", skip_waiting_for_build_processing: true ) end ``` ### xcrun altool (No Fastlane) ```bash xcrun altool --upload-app \ --type ios \ --file "MyApp.ipa" \ --apiKey "KEY_ID" \ --apiIssuer "ISSUER_ID" ``` ## Security & Privacy **Data that leaves your machine:** - IPA/app binary uploaded to Apple servers - Build metadata (version, bundle ID, team) **Data that stays local:** - API keys and certificates (keep in Keychain) - Source code (not uploaded) **This skill does NOT:** - Store Apple credentials in plain text - Share builds outside Apple's infrastructure ## Related Skills Install with `clawhub install <slug>` if user confirms: - `ios` — iOS development patterns - `xcode` — Xcode workflows - `flutter` — cross-platform builds ## Feedback - If useful: `clawhub star testflight` - Stay updated: `clawhub sync`

path: ci-cd.md
content: # CI/CD Automation - TestFlight

## App Store Connect API Key Setup

### 1. Generate Key

1. [App Store Connect](https://appstoreconnect.apple.com) > Users and Access
2. Keys tab > App Store Connect API
3. Click + to generate new key
4. Role: "App Manager" (minimum for TestFlight)
5. Download the `.p8` file immediately (shown only once)

### 2. Store Securely

```bash
# Store in CI secrets, NOT in repo
APPSTORE_API_KEY_ID="XXXXXXXXXX"
APPSTORE_API_ISSUER_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
APPSTORE_API_KEY_CONTENT="[REDACTED_SECRET]"
```

### 3. Fastlane api_key.json

```json
{
  "key_id": "XXXXXXXXXX",
  "issuer_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "key": "[REDACTED_SECRET]",
  "in_house": false
}
```

## GitHub Actions Example

```yaml
name: TestFlight

on:
  push:
    tags: ['v*']

jobs:
  deploy:
    runs-on: macos-14
    steps:
      - uses: actions/checkout@v4
      
      - name: Install certificates
        env:
          P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
          P12_BASE64: ${{ secrets.P12_BASE64 }}
          PROVISION_BASE64: ${{ secrets.PROVISION_BASE64 }}
        run: |
          # Create keychain
          security create-keychain -p "" build.keychain
          security default-keychain -s build.keychain
          security unlock-keychain -p "" build.keychain
          
          # Import certificate
          echo "$P12_BASE64" | base64 -d > cert.p12
          security import cert.p12 -k build.keychain -P "$P12_PASSWORD" -T /usr/bin/codesign
          security set-key-partition-list -S apple-tool:,apple: -s -k "" build.keychain
          
          # Install provisioning profile
          mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
          echo "$PROVISION_BASE64" | base64 -d > ~/Library/MobileDevice/Provisioning\ Profiles/profile.mobileprovision

      - name: Build and upload
        env:
          APPSTORE_API_KEY: ${{ secrets.APPSTORE_API_KEY }}
        run: |
          echo "$APPSTORE_API_KEY" > api_key.json
          fastlane beta
```

## GitLab CI Example

```yaml
deploy_testflight:
  stage: deploy
  tags: [macos]
  script:
    - bundle exec fastlane beta
  only:
    - tags
  variables:
    FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: $APP_SPECIFIC_PASSWORD
```

## Fastlane Lanes

### Basic Upload

```ruby
lane :beta do
  increment_build_number(
    build_number: ENV["CI_PIPELINE_IID"] || Time.now.to_i
  )
  
  build_app(
    scheme: "MyApp",
    export_method: "app-store"
  )
  
  upload_to_testflight(
    api_key_path: "fastlane/api_key.json",
    skip_waiting_for_build_processing: true,
    distribute_external: false
  )
end
```

### With Changelog

```ruby
lane :beta do
  changelog = changelog_from_git_commits(
    commits_count: 10,
    pretty: "- %s"
  )
  
  build_app(scheme: "MyApp")
  
  upload_to_testflight(
    api_key_path: "fastlane/api_key.json",
    changelog: changelog,
    groups: ["Internal Testers"]
  )
end
```

### Distribute to External After Processing

```ruby
lane :distribute_external do
  upload_to_testflight(
    api_key_path: "fastlane/api_key.json",
    distribute_external: true,
    groups: ["Beta Testers"],
    submit_beta_review: true
  )
end
```

## Build Number Strategies

### Timestamp-based (Simple)

```ruby
increment_build_number(build_number: Time.now.strftime("%Y%m%d%H%M"))
# Result: 202602191530
```

### CI Pipeline ID (Recommended)

```ruby
# GitLab
increment_build_number(build_number: ENV["CI_PIPELINE_IID"])

# GitHub Actions
increment_build_number(build_number: ENV["GITHUB_RUN_NUMBER"])
```

### Version-based

```ruby
# 1.2.3 build 1 -> 10203001
version = get_version_number
parts = version.split('.').map(&:to_i)
build = (parts[0] * 10000) + (parts[1] * 100) + parts[2]
increment_build_number(build_number: "#{build}#{ENV['CI_PIPELINE_IID']}")
```

## Troubleshooting

### "No suitable application records found"

App not created in App Store Connect. Create it first with matching bundle ID.

### "The bundle version must be higher"

Build number already used. Always increment:

```ruby
latest = latest_testflight_build_number(api_key_path: "api_key.json")
increment_build_number(build_number: latest + 1)
```

### "Missing compliance information"

Add to Info.plist:

```xml
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
```

Or for apps with encryption:

```xml
<key>ITSAppUsesNonExemptEncryption</key>
<true/>
<key>ITSEncryptionExportComplianceCode</key>
<string>YOUR_CODE</string>
```

### Processing stuck

Usually export compliance. Check App Store Connect > TestFlight > build status for required actions.

### "Could not find App Store Connect API key"

Verify:
- Key ID matches (10 characters)
- Issuer ID is UUID format
- .p8 content includes BEGIN/END markers
- Key has correct permissions

## Xcode Cloud Alternative

For teams preferring Apple-native CI:

1. Xcode > Product > Xcode Cloud > Create Workflow
2. Configure triggers (branch, tag, PR)
3. Select "Archive" action
4. Add "TestFlight Internal Testing" post-action
5. Signing managed automatically via cloud signing


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: suspicious | SkillSpector: clean (LOW)