feat: Add Fastlane TestFlight setup documentation for CI/CD pipeline

This commit is contained in:
Janez T
2025-10-22 12:20:56 +02:00
parent 01c7169a33
commit 5b6b606b0e
2 changed files with 716 additions and 1 deletions

411
.github/FASTLANE_TESTFLIGHT_SETUP.md vendored Normal file
View File

@@ -0,0 +1,411 @@
# Fastlane TestFlight Setup for CI/CD
This document describes how to configure Fastlane for automatic iOS beta deployments to TestFlight from GitHub Actions.
## Overview
The CI/CD pipeline automatically builds and uploads iOS beta builds to TestFlight for every push to `main`, `develop`, or release tags. This provides:
- **Automatic TestFlight distribution** for beta testers
- **Version management** with build number increments
- **Code signing** handled automatically in CI
- **Release notes** from git commits
## Prerequisites
Before setting up the CI/CD pipeline, ensure you have:
1. **Apple Developer Account** with:
- Paid Apple Developer Program membership ($99/year)
- App registered in App Store Connect
- TestFlight enabled for your app
2. **App Store Connect Access**:
- Admin or App Manager role
- App-specific password generated
3. **Code Signing Certificates**:
- Distribution certificate (`.p12` file)
- Ad Hoc or App Store provisioning profile
## Required GitHub Secrets
Configure these secrets in your GitHub repository settings (`Settings``Secrets and variables``Actions`):
### Certificate & Provisioning
| Secret Name | Description | How to Obtain |
|-------------|-------------|---------------|
| `IOS_P12_BASE64` | Base64-encoded distribution certificate | See [Exporting Certificate](#exporting-certificate) |
| `IOS_P12_PASSWORD` | Password for the .p12 certificate | Password you set when exporting |
| `IOS_PROVISION_PROFILE_BASE64` | Base64-encoded provisioning profile | See [Exporting Provisioning Profile](#exporting-provisioning-profile) |
### Apple Account Authentication
| Secret Name | Description | How to Obtain |
|-------------|-------------|---------------|
| `FASTLANE_USER` | Apple ID email address | Your Apple Developer account email (e.g., `hey@dz0ny.dev`) |
| `FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD` | App-specific password | See [Generating App-Specific Password](#generating-app-specific-password) |
| `FASTLANE_SESSION` | *(Optional)* Fastlane session for 2FA | See [Handling 2FA](#handling-2fa-optional) |
## Step-by-Step Setup
### 1. Exporting Certificate
#### Export Distribution Certificate from Keychain
1. Open **Keychain Access** on macOS
2. Select **login** keychain in the left sidebar
3. Select **Certificates** category
4. Find your **Apple Distribution** certificate
- Look for "Apple Distribution: Your Name (Team ID)"
- Ensure it has a valid private key (arrow to expand)
5. **Right-click****Export "Apple Distribution: ..."**
6. Save as: `distribution.p12`
7. **Set a strong password** (you'll need this for `IOS_P12_PASSWORD`)
8. Click **Save**
#### Convert to Base64
```bash
# Encode the .p12 file to base64
base64 -i distribution.p12 | pbcopy
```
The base64 string is now in your clipboard. Add it as `IOS_P12_BASE64` secret.
**Security Note**: Delete `distribution.p12` after encoding!
### 2. Exporting Provisioning Profile
#### Download from Apple Developer Portal
1. Log in to [Apple Developer Portal](https://developer.apple.com/account)
2. Navigate to **Certificates, Identifiers & Profiles**
3. Click **Profiles** in the left sidebar
4. Find your **App Store** or **Ad Hoc** provisioning profile
- Must match your app's bundle identifier: `com.meshcore.sar.meshcoreSarApp`
5. Click the profile → **Download**
6. Save as: `profile.mobileprovision`
#### Convert to Base64
```bash
# Encode the provisioning profile to base64
base64 -i profile.mobileprovision | pbcopy
```
The base64 string is now in your clipboard. Add it as `IOS_PROVISION_PROFILE_BASE64` secret.
**Security Note**: Delete `profile.mobileprovision` after encoding!
### 3. Generating App-Specific Password
App-specific passwords are required for App Store Connect API access when 2FA is enabled.
1. Log in to [Apple ID Account](https://appleid.apple.com/)
2. Navigate to **Sign-In and Security****App-Specific Passwords**
3. Click **Generate an app-specific password**
4. Enter a label: `GitHub Actions Fastlane`
5. Click **Create**
6. **Copy the generated password** (format: `xxxx-xxxx-xxxx-xxxx`)
- This will only be shown once!
7. Add it as `FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD` secret
### 4. Configure GitHub Secrets
1. Go to your GitHub repository
2. Navigate to **Settings****Secrets and variables****Actions**
3. Click **New repository secret** for each of the following:
**IOS_P12_BASE64**:
```
Paste the base64-encoded certificate from step 1
```
**IOS_P12_PASSWORD**:
```
The password you set when exporting the .p12 certificate
```
**IOS_PROVISION_PROFILE_BASE64**:
```
Paste the base64-encoded provisioning profile from step 2
```
**FASTLANE_USER**:
```
hey@dz0ny.dev
```
**FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD**:
```
xxxx-xxxx-xxxx-xxxx
```
### 5. Handling 2FA (Optional)
If your Apple account has two-factor authentication enabled, Fastlane may require a session token for long-running CI jobs.
#### Generate Fastlane Session
On your local machine:
```bash
# Install Fastlane if not already installed
gem install fastlane
# Generate session token
fastlane spaceauth -u hey@dz0ny.dev
```
You'll be prompted for:
1. Apple ID password
2. 2FA code (sent to your device)
Fastlane will output a session cookie. Copy the entire cookie string and add it as `FASTLANE_SESSION` secret.
**Note**: Session tokens expire after ~30 days. You'll need to regenerate periodically.
**Alternative**: If you don't set `FASTLANE_SESSION`, Fastlane will use the app-specific password, which works for most cases.
## Workflow Configuration
The workflow is already configured in `.github/workflows/build-multiplatform.yml`:
- **Triggers**: Pushes to `main`, `develop`, or version tags (`v*`)
- **Runs on**: macOS runner (required for iOS builds)
- **Fastlane Lane**: `beta` (builds IPA and uploads to TestFlight)
### What the Workflow Does
1. **Checks out code** and downloads versioned `pubspec.yaml` (for tags)
2. **Sets up Flutter** and installs dependencies
3. **Generates localizations** (`flutter gen-l10n`)
4. **Installs CocoaPods** dependencies
5. **Sets up Ruby** and installs Fastlane
6. **Imports code signing certificate** into temporary keychain
7. **Imports provisioning profile**
8. **Runs Fastlane beta lane**:
- Increments build number
- Builds signed IPA
- Uploads to TestFlight
9. **Cleans up keychain** (security)
10. **Uploads logs** if build fails (for debugging)
## Fastlane Configuration
The Fastlane configuration is in `ios/fastlane/`:
### Fastfile
```ruby
platform :ios do
desc "Push a new beta build to TestFlight"
lane :beta do
increment_build_number(xcodeproj: "Runner.xcodeproj")
build_app(workspace: "Runner.xcworkspace", scheme: "Runner")
upload_to_testflight
end
end
```
### Appfile
Contains your app configuration:
- Bundle identifier: `com.meshcore.sar.meshcoreSarApp`
- Apple ID: `hey@dz0ny.dev`
- Team IDs for App Store Connect and Developer Portal
## Testing the Setup
1. **Push a commit** to `main` or `develop` branch:
```bash
git checkout main
git commit --allow-empty -m "Test TestFlight deployment"
git push origin main
```
2. **Monitor the workflow**:
- Go to **Actions** tab in GitHub
- Click on the running workflow
- Watch the **Deploy iOS Beta to TestFlight** job
3. **Check TestFlight**:
- Log in to [App Store Connect](https://appstoreconnect.apple.com/)
- Navigate to **My Apps** → **MeshCore SAR** → **TestFlight**
- You should see a new build processing (takes 5-15 minutes)
4. **Verify build**:
- Once processing completes, the build is available for testing
- Add internal testers in TestFlight
- Testers will receive notification to download via TestFlight app
## Troubleshooting
### "Invalid certificate" error
**Cause**: Certificate doesn't match the provisioning profile or is expired.
**Solutions**:
- Verify certificate is **Distribution** type (not Development)
- Check certificate expiration date in Keychain Access
- Ensure provisioning profile includes the certificate
- Re-export and re-encode both certificate and profile
### "Invalid provisioning profile" error
**Cause**: Provisioning profile doesn't match app bundle ID or is expired.
**Solutions**:
- Verify bundle ID: `com.meshcore.sar.meshcoreSarApp`
- Check profile type: App Store or Ad Hoc (not Development)
- Regenerate profile in Apple Developer Portal
- Ensure profile includes all required devices (for Ad Hoc)
### "Authentication failed" error
**Cause**: Apple ID credentials are incorrect or expired.
**Solutions**:
- Verify `FASTLANE_USER` matches your Apple ID email
- Regenerate `FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD`
- If using 2FA, regenerate `FASTLANE_SESSION` token
- Check Apple Developer account is active (membership paid)
### "Build number conflict" error
**Cause**: Build number already exists in TestFlight.
**Solutions**:
- Fastlane auto-increments build number, but may fail if out of sync
- Manually increment version in `pubspec.yaml`
- Or modify Fastfile to use timestamp-based build numbers:
```ruby
increment_build_number(
build_number: Time.now.to_i.to_s,
xcodeproj: "Runner.xcodeproj"
)
```
### "Session expired" error
**Cause**: `FASTLANE_SESSION` token expired (lasts ~30 days).
**Solutions**:
- Regenerate session token: `fastlane spaceauth -u hey@dz0ny.dev`
- Update `FASTLANE_SESSION` secret in GitHub
- Or remove the secret and rely on app-specific password only
### Workflow doesn't run
**Cause**: Conditional check failed (wrong branch or secrets missing).
**Solutions**:
- Verify you pushed to `main` or `develop` branch
- Check all required secrets are configured in GitHub
- Review workflow logs for conditional evaluation
## Advanced Configuration
### Custom Release Notes
Add release notes from git commits:
Edit `ios/fastlane/Fastfile`:
```ruby
lane :beta do
increment_build_number(xcodeproj: "Runner.xcodeproj")
build_app(workspace: "Runner.xcworkspace", scheme: "Runner")
# Generate changelog from git
changelog = changelog_from_git_commits(
pretty: "- %s",
merge_commit_filtering: "exclude_merges"
)
upload_to_testflight(
changelog: changelog,
skip_waiting_for_build_processing: true
)
end
```
### Selective TestFlight Groups
Upload to specific tester groups:
```ruby
upload_to_testflight(
groups: ["Internal Testers", "Beta Team"],
distribute_external: false,
skip_waiting_for_build_processing: true
)
```
### Automatic Screenshot Upload
Generate and upload screenshots for App Store:
```ruby
lane :screenshots do
snapshot
end
lane :beta do
increment_build_number(xcodeproj: "Runner.xcodeproj")
build_app(workspace: "Runner.xcworkspace", scheme: "Runner")
upload_to_testflight
upload_to_app_store(
screenshots_path: "./fastlane/screenshots",
skip_binary_upload: true,
skip_metadata: true
)
end
```
## Security Best Practices
1. **Never commit secrets** to your repository
2. **Rotate certificates** before expiration (annually)
3. **Regenerate app-specific passwords** periodically
4. **Use temporary keychains** in CI (already implemented)
5. **Delete certificates** after encoding to base64
6. **Limit GitHub Actions secrets** to repository scope
7. **Review workflow logs** for sensitive data leaks
8. **Enable branch protection** for `main` and `develop`
## Cost Considerations
### Apple Developer Program
- **$99/year** for individual account
- **$299/year** for enterprise account
- Required for TestFlight distribution
### GitHub Actions
- **Free tier**: 2,000 minutes/month for private repos
- **macOS runners**: 10x multiplier (1 min = 10 mins)
- **Typical iOS build**: ~15-20 minutes (~150-200 minutes counted)
- **Monthly estimate**: ~10 builds = ~2,000 minutes (free tier limit)
**Tip**: Use caching and selective triggers to minimize build minutes.
## Additional Resources
- [Fastlane Documentation](https://docs.fastlane.tools/)
- [Fastlane TestFlight Guide](https://docs.fastlane.tools/actions/upload_to_testflight/)
- [Apple Developer Portal](https://developer.apple.com/account)
- [App Store Connect](https://appstoreconnect.apple.com/)
- [GitHub Actions for iOS](https://docs.github.com/en/actions/deployment/deploying-xcode-applications)
- [Fastlane Best Practices](https://docs.fastlane.tools/best-practices/)
## Support
For issues with:
- **Fastlane**: Check [Fastlane Docs](https://docs.fastlane.tools/) or [GitHub Issues](https://github.com/fastlane/fastlane/issues)
- **GitHub Actions**: Check [workflow logs](https://github.com/meshcore-dev/meshcore_sar_app/actions)
- **Code signing**: Check [Apple's Code Signing Guide](https://developer.apple.com/support/code-signing/)
- **TestFlight**: Check [App Store Connect Help](https://developer.apple.com/support/app-store-connect/)

View File

@@ -522,8 +522,312 @@ jobs:
} }
EOF EOF
- name: Generate index.html
env:
R2_PUBLIC_URL: ${{ secrets.R2_PUBLIC_URL }}
run: |
BUILD_DIR="r2-upload/unstable/${{ steps.metadata.outputs.build_prefix }}"
BASE_URL="${R2_PUBLIC_URL:-https://meshcore-sar.dz0ny.dev}/unstable/${{ steps.metadata.outputs.build_prefix }}"
# Get file sizes
get_size() {
if [ -f "$1" ]; then
ls -lh "$1" | awk '{print $5}'
else
echo "N/A"
fi
}
# Generate download links HTML
cat > "$BUILD_DIR/index.html" <<'HTMLEOF'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MeshCore SAR - Build ${{ steps.metadata.outputs.build_prefix }}</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.container {
background: white;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
max-width: 800px;
width: 100%;
padding: 40px;
}
.header {
text-align: center;
margin-bottom: 40px;
border-bottom: 2px solid #f0f0f0;
padding-bottom: 20px;
}
h1 {
color: #333;
font-size: 32px;
margin-bottom: 10px;
}
.build-info {
background: #f8f9fa;
border-radius: 8px;
padding: 20px;
margin-bottom: 30px;
}
.build-info h2 {
color: #555;
font-size: 18px;
margin-bottom: 15px;
}
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
}
.info-item {
display: flex;
flex-direction: column;
}
.info-label {
color: #888;
font-size: 12px;
text-transform: uppercase;
margin-bottom: 5px;
}
.info-value {
color: #333;
font-size: 14px;
font-family: 'Monaco', 'Courier New', monospace;
}
.downloads {
margin-top: 30px;
}
.downloads h2 {
color: #333;
font-size: 24px;
margin-bottom: 20px;
}
.download-card {
background: #fff;
border: 2px solid #e0e0e0;
border-radius: 12px;
padding: 20px;
margin-bottom: 15px;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: space-between;
}
.download-card:hover {
border-color: #667eea;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.15);
transform: translateY(-2px);
}
.download-info {
flex: 1;
}
.download-platform {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
}
.platform-icon {
font-size: 28px;
}
.platform-name {
font-size: 18px;
font-weight: 600;
color: #333;
}
.file-name {
color: #666;
font-size: 13px;
font-family: 'Monaco', 'Courier New', monospace;
margin-bottom: 5px;
}
.file-size {
color: #999;
font-size: 12px;
}
.download-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 8px;
padding: 12px 24px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
text-decoration: none;
display: inline-block;
transition: all 0.3s ease;
}
.download-btn:hover {
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.footer {
margin-top: 30px;
padding-top: 20px;
border-top: 2px solid #f0f0f0;
text-align: center;
color: #888;
font-size: 14px;
}
.footer a {
color: #667eea;
text-decoration: none;
}
.footer a:hover {
text-decoration: underline;
}
@media (max-width: 600px) {
.container { padding: 20px; }
h1 { font-size: 24px; }
.download-card { flex-direction: column; align-items: flex-start; }
.download-btn { margin-top: 15px; width: 100%; text-align: center; }
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🗺️ MeshCore SAR</h1>
<p style="color: #666; margin-top: 10px;">Unstable Build Downloads</p>
</div>
<div class="build-info">
<h2>📋 Build Information</h2>
<div class="info-grid">
<div class="info-item">
<span class="info-label">Build ID</span>
<span class="info-value">${{ steps.metadata.outputs.build_prefix }}</span>
</div>
<div class="info-item">
<span class="info-label">Timestamp</span>
<span class="info-value">${{ steps.metadata.outputs.timestamp }}</span>
</div>
<div class="info-item">
<span class="info-label">Commit</span>
<span class="info-value">${{ steps.metadata.outputs.commit_short }}</span>
</div>
<div class="info-item">
<span class="info-label">Workflow Run</span>
<span class="info-value">#${{ github.run_number }}</span>
</div>
</div>
</div>
<div class="downloads">
<h2>📦 Download Builds</h2>
HTMLEOF
# Add Android APK if exists
if [ -f "$BUILD_DIR"/*.apk ]; then
APK_FILE=$(basename "$BUILD_DIR"/*.apk)
APK_SIZE=$(get_size "$BUILD_DIR/$APK_FILE")
cat >> "$BUILD_DIR/index.html" <<HTMLEOF
<div class="download-card">
<div class="download-info">
<div class="download-platform">
<span class="platform-icon">🤖</span>
<span class="platform-name">Android APK</span>
</div>
<div class="file-name">$APK_FILE</div>
<div class="file-size">Size: $APK_SIZE</div>
</div>
<a href="$BASE_URL/$APK_FILE" class="download-btn" download>Download</a>
</div>
HTMLEOF
fi
# Add Android AAB if exists
if [ -f "$BUILD_DIR"/*.aab ]; then
AAB_FILE=$(basename "$BUILD_DIR"/*.aab)
AAB_SIZE=$(get_size "$BUILD_DIR/$AAB_FILE")
cat >> "$BUILD_DIR/index.html" <<HTMLEOF
<div class="download-card">
<div class="download-info">
<div class="download-platform">
<span class="platform-icon">📦</span>
<span class="platform-name">Android App Bundle</span>
</div>
<div class="file-name">$AAB_FILE</div>
<div class="file-size">Size: $AAB_SIZE</div>
</div>
<a href="$BASE_URL/$AAB_FILE" class="download-btn" download>Download</a>
</div>
HTMLEOF
fi
# Add macOS DMG if exists
if [ -f "$BUILD_DIR"/*.dmg ]; then
DMG_FILE=$(basename "$BUILD_DIR"/*.dmg)
DMG_SIZE=$(get_size "$BUILD_DIR/$DMG_FILE")
cat >> "$BUILD_DIR/index.html" <<HTMLEOF
<div class="download-card">
<div class="download-info">
<div class="download-platform">
<span class="platform-icon">🍎</span>
<span class="platform-name">macOS DMG</span>
</div>
<div class="file-name">$DMG_FILE</div>
<div class="file-size">Size: $DMG_SIZE</div>
</div>
<a href="$BASE_URL/$DMG_FILE" class="download-btn" download>Download</a>
</div>
HTMLEOF
fi
# Add Windows ZIP if exists
if [ -f "$BUILD_DIR"/*-windows.zip ]; then
WIN_FILE=$(basename "$BUILD_DIR"/*-windows.zip)
WIN_SIZE=$(get_size "$BUILD_DIR/$WIN_FILE")
cat >> "$BUILD_DIR/index.html" <<HTMLEOF
<div class="download-card">
<div class="download-info">
<div class="download-platform">
<span class="platform-icon">🪟</span>
<span class="platform-name">Windows Executable</span>
</div>
<div class="file-name">$WIN_FILE</div>
<div class="file-size">Size: $WIN_SIZE</div>
</div>
<a href="$BASE_URL/$WIN_FILE" class="download-btn" download>Download</a>
</div>
HTMLEOF
fi
# Close HTML
cat >> "$BUILD_DIR/index.html" <<'HTMLEOF'
</div>
<div class="footer">
<p>
<a href="manifest.json">📋 View Build Manifest</a> •
<a href="https://github.com/${{ github.repository }}">🔗 GitHub Repository</a>
</p>
<p style="margin-top: 10px; font-size: 12px;">
Built with ❤️ by GitHub Actions
</p>
</div>
</div>
</body>
</html>
HTMLEOF
echo "Generated index.html with download links"
echo "Prepared artifacts:" echo "Prepared artifacts:"
ls -lah r2-upload/unstable/${{ steps.metadata.outputs.build_prefix }}/ ls -lah "$BUILD_DIR/"
- name: Upload to Cloudflare R2 - name: Upload to Cloudflare R2
uses: ryand56/r2-upload-action@latest uses: ryand56/r2-upload-action@latest