diff --git a/.github/workflows/ios-testflight.yml b/.github/workflows/ios-testflight.yml new file mode 100644 index 00000000..7e4a91a5 --- /dev/null +++ b/.github/workflows/ios-testflight.yml @@ -0,0 +1,99 @@ +name: Build & Upload to TestFlight + +on: + push: + tags: + - 'ios-v*' + +concurrency: + group: ios-testflight + cancel-in-progress: false + +jobs: + build-and-upload: + runs-on: macos-15 + timeout-minutes: 30 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install XcodeGen + run: brew install xcodegen + + - name: Generate Xcode project + working-directory: ios + run: xcodegen generate + + - name: Install Apple certificate and provisioning profile + env: + BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }} + P12_PASSWORD: ${{ secrets.P12_PASSWORD }} + BUILD_PROVISION_PROFILE_BASE64: ${{ secrets.BUILD_PROVISION_PROFILE_BASE64 }} + run: | + # Create variables + CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12 + PP_PATH=$RUNNER_TEMP/build_pp.mobileprovision + KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db + KEYCHAIN_PASSWORD=$(openssl rand -hex 16) + PP_INSTALL_PATH=~/Library/MobileDevice/Provisioning\ Profiles/build_pp.mobileprovision + + # Import certificate and provisioning profile from secrets + echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH + echo -n "$BUILD_PROVISION_PROFILE_BASE64" | base64 --decode -o $PP_PATH + + # Create temporary keychain + security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + security set-keychain-settings -lut 21600 $KEYCHAIN_PATH + security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + + # Import certificate to keychain + security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH + security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + security list-keychain -d user -s $KEYCHAIN_PATH $(security list-keychains -d user | tr -d '"') + + # Apply provisioning profile + mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles + cp "$PP_PATH" "$PP_INSTALL_PATH" + + - name: Build archive + working-directory: ios + run: | + xcodebuild archive \ + -project StillPoint.xcodeproj \ + -scheme StillPoint \ + -configuration Release \ + -archivePath $RUNNER_TEMP/StillPoint.xcarchive \ + -destination 'generic/platform=iOS' \ + -allowProvisioningUpdates + + - name: Upload to App Store Connect + env: + APPSTORE_API_KEY_ID: ${{ secrets.APPSTORE_API_KEY_ID }} + APPSTORE_API_ISSUER_ID: ${{ secrets.APPSTORE_API_ISSUER_ID }} + APPSTORE_API_PRIVATE_KEY: ${{ secrets.APPSTORE_API_PRIVATE_KEY }} + run: | + # Write API key to file for authentication + mkdir -p ~/.private_keys + printf '%s\n' "$APPSTORE_API_PRIVATE_KEY" > ~/.private_keys/AuthKey_${APPSTORE_API_KEY_ID}.p8 + + # Export and upload in one step using xcodebuild + # The ExportOptions.plist has destination=upload and method=app-store-connect + # which makes xcodebuild upload directly to App Store Connect / TestFlight + xcodebuild -exportArchive \ + -archivePath $RUNNER_TEMP/StillPoint.xcarchive \ + -exportOptionsPlist ios/ExportOptions.plist \ + -exportPath $RUNNER_TEMP/export \ + -allowProvisioningUpdates \ + -authenticationKeyPath ~/.private_keys/AuthKey_${APPSTORE_API_KEY_ID}.p8 \ + -authenticationKeyID "$APPSTORE_API_KEY_ID" \ + -authenticationKeyIssuerID "$APPSTORE_API_ISSUER_ID" + + - name: Clean up keychain, provisioning profile, and API key + if: always() + run: | + security delete-keychain $RUNNER_TEMP/app-signing.keychain-db 2>/dev/null || true + rm -f $RUNNER_TEMP/build_certificate.p12 2>/dev/null || true + rm -f $RUNNER_TEMP/build_pp.mobileprovision 2>/dev/null || true + rm -f ~/Library/MobileDevice/Provisioning\ Profiles/build_pp.mobileprovision 2>/dev/null || true + rm -rf ~/.private_keys 2>/dev/null || true diff --git a/ios/ExportOptions.plist b/ios/ExportOptions.plist new file mode 100644 index 00000000..d81d34a2 --- /dev/null +++ b/ios/ExportOptions.plist @@ -0,0 +1,16 @@ + + + + + method + app-store-connect + signingStyle + automatic + teamID + T5UU4BP6AV + uploadSymbols + + destination + upload + + diff --git a/ios/RELEASING.md b/ios/RELEASING.md new file mode 100644 index 00000000..c343b8c3 --- /dev/null +++ b/ios/RELEASING.md @@ -0,0 +1,61 @@ +# Releasing Still Point for iOS + +## Prerequisites + +The following GitHub repository secrets must be configured (Settings → Secrets and variables → Actions): + +| Secret | Description | How to get it | +|--------|-------------|---------------| +| `BUILD_CERTIFICATE_BASE64` | Base64-encoded `.p12` distribution certificate | Export from Keychain Access, then `base64 -i cert.p12 \| pbcopy` | +| `P12_PASSWORD` | Password used when exporting the `.p12` | The password you set during `.p12` export | +| `BUILD_PROVISION_PROFILE_BASE64` | Base64-encoded `.mobileprovision` file | Download from developer.apple.com, then `base64 -i profile.mobileprovision \| pbcopy` | +| `APPSTORE_API_KEY_ID` | App Store Connect API Key ID | From appstoreconnect.apple.com → Users and Access → Integrations | +| `APPSTORE_API_ISSUER_ID` | App Store Connect API Issuer ID | Same page as above | +| `APPSTORE_API_PRIVATE_KEY` | Contents of the `.p8` API key file | Paste the full file contents including BEGIN/END lines | + +## Releasing to TestFlight + +1. Update the version in `ios/project.yml`: + ```yaml + MARKETING_VERSION: "1.1.0" + CURRENT_PROJECT_VERSION: 2 + ``` + +2. Commit the version bump: + ```bash + git add ios/project.yml + git commit -m "Bump iOS version to 1.1.0 (build 2)" + ``` + +3. Tag and push: + ```bash + git tag ios-v1.1.0 + git push origin ios-v1.1.0 + ``` + +4. The GitHub Actions workflow builds and uploads to TestFlight automatically. + +5. After Apple processes the build (~15 minutes), it appears in the TestFlight app on your device. + +## Version numbering + +- `MARKETING_VERSION` — the user-facing version (e.g., `1.0.0`, `1.1.0`) +- `CURRENT_PROJECT_VERSION` — the build number, must increment with every upload (e.g., `1`, `2`, `3`) +- Tag format: `ios-v{MARKETING_VERSION}` (e.g., `ios-v1.0.0`) + +## Submitting to the App Store + +The same build uploaded to TestFlight can be submitted to the App Store: + +1. Go to [appstoreconnect.apple.com](https://appstoreconnect.apple.com) → your app +2. Fill in the required metadata (screenshots, description, privacy policy URL) +3. Select the TestFlight build under the version +4. Click "Add for Review" + +## Troubleshooting + +**Build fails with signing error:** Verify the certificate hasn't expired and the provisioning profile includes the correct bundle ID (`com.brettonauerbach.stillpoint`). + +**Upload fails with authentication error:** Regenerate the App Store Connect API key and update the GitHub secrets. + +**Build number conflict:** `CURRENT_PROJECT_VERSION` must be unique per upload. Increment it even for re-uploads of the same marketing version. diff --git a/ios/project.yml b/ios/project.yml index 1a8c0a06..01324386 100644 --- a/ios/project.yml +++ b/ios/project.yml @@ -1,6 +1,6 @@ name: StillPoint options: - bundleIdPrefix: com.stillpoint + bundleIdPrefix: com.brettonauerbach deploymentTarget: iOS: "17.0" xcodeVersion: "15.0" @@ -30,10 +30,10 @@ targets: - Fonts/JetBrainsMono-Variable.ttf settings: base: - PRODUCT_BUNDLE_IDENTIFIER: com.stillpoint.app + PRODUCT_BUNDLE_IDENTIFIER: com.brettonauerbach.stillpoint MARKETING_VERSION: "1.0.0" CURRENT_PROJECT_VERSION: 1 - DEVELOPMENT_TEAM: "" + DEVELOPMENT_TEAM: T5UU4BP6AV CODE_SIGN_STYLE: Automatic SWIFT_VERSION: "5.9" INFOPLIST_KEY_UIApplicationSceneManifest_Generation: "YES"