Skip to content

Commit 64c4aca

Browse files
committed
Add iOS App Store build and publish workflow
Add GitHub Actions workflow for automated iOS builds and App Store Connect uploads: - Build iOS app for device (not simulator) using existing Xcode project - Configure certificates and provisioning profiles from secrets - Create archive and export IPA for App Store distribution - Optional upload to TestFlight via App Store Connect API - Upload IPA and dSYM as artifacts for backup Also add setup documentation explaining required secrets and configuration. https://claude.ai/code/session_01VD2HqWevobbytbN6GyK5gh
1 parent 72f5705 commit 64c4aca

2 files changed

Lines changed: 360 additions & 0 deletions

File tree

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
# iOS App Store Workflow Setup
2+
3+
This document explains how to configure the GitHub Actions workflow for building and publishing the iOS app to App Store Connect.
4+
5+
## Prerequisites
6+
7+
1. **Apple Developer Account** with App Store Connect access
8+
2. **Distribution Certificate** (.p12 file with private key)
9+
3. **Provisioning Profiles** for App Store distribution
10+
4. **App Store Connect API Key** for automated uploads
11+
12+
## Required GitHub Secrets
13+
14+
Go to your repository's **Settings → Secrets and variables → Actions** and add the following secrets:
15+
16+
### Certificate & Signing
17+
18+
| Secret Name | Description | How to Get It |
19+
|-------------|-------------|---------------|
20+
| `APPLE_CERTIFICATE_P12` | Base64 encoded distribution certificate | See "Exporting Certificate" below |
21+
| `APPLE_CERTIFICATE_PASSWORD` | Password for the .p12 file | Password you set when exporting |
22+
| `KEYCHAIN_PASSWORD` | Any secure password for temp keychain | Generate a random password |
23+
24+
### Provisioning Profiles
25+
26+
| Secret Name | Description | How to Get It |
27+
|-------------|-------------|---------------|
28+
| `APPLE_PROVISIONING_PROFILE` | Base64 encoded main app profile | Download from Apple Developer Portal |
29+
| `APPLE_PROVISIONING_PROFILE_WATCHKIT` | Base64 encoded WatchKit app profile | Download from Apple Developer Portal |
30+
| `APPLE_PROVISIONING_PROFILE_WATCHKIT_EXTENSION` | Base64 encoded WatchKit Extension profile | Download from Apple Developer Portal |
31+
32+
### App Store Connect API
33+
34+
| Secret Name | Description | How to Get It |
35+
|-------------|-------------|---------------|
36+
| `APP_STORE_CONNECT_API_KEY_ID` | API Key ID (e.g., "ABC123DEFG") | App Store Connect → Users and Access → Keys |
37+
| `APP_STORE_CONNECT_API_ISSUER_ID` | Issuer ID (UUID format) | App Store Connect → Users and Access → Keys |
38+
| `APP_STORE_CONNECT_API_KEY_P8` | Base64 encoded API Key (.p8 file) | See "Creating API Key" below |
39+
40+
## Step-by-Step Setup
41+
42+
### 1. Exporting Distribution Certificate
43+
44+
1. Open **Keychain Access** on your Mac
45+
2. Find your "Apple Distribution" or "iPhone Distribution" certificate
46+
3. Right-click → **Export** → Save as .p12 with a password
47+
4. Convert to Base64:
48+
```bash
49+
base64 -i certificate.p12 | pbcopy
50+
```
51+
5. Paste the result into `APPLE_CERTIFICATE_P12` secret
52+
53+
### 2. Getting Provisioning Profiles
54+
55+
1. Go to [Apple Developer Portal](https://developer.apple.com/account/resources/profiles/list)
56+
2. Download the **App Store** distribution profiles for:
57+
- Main app: `org.cagnulein.qdomyoszwift`
58+
- WatchKit app: `org.cagnulein.qdomyoszwift.watchkitapp`
59+
- WatchKit Extension: `org.cagnulein.qdomyoszwift.watchkitapp.watchkitextension`
60+
3. Convert each to Base64:
61+
```bash
62+
base64 -i profile.mobileprovision | pbcopy
63+
```
64+
4. Paste into the corresponding secrets
65+
66+
### 3. Creating App Store Connect API Key
67+
68+
1. Go to [App Store Connect → Users and Access → Integrations → App Store Connect API](https://appstoreconnect.apple.com/access/integrations/api)
69+
2. Click **+** to create a new key
70+
3. Give it a name (e.g., "GitHub Actions")
71+
4. Select **Admin** or **App Manager** access
72+
5. Download the .p8 file (you can only download it once!)
73+
6. Note the **Key ID** and **Issuer ID**
74+
7. Convert the .p8 to Base64:
75+
```bash
76+
base64 -i AuthKey_XXXXXXXX.p8 | pbcopy
77+
```
78+
8. Add all three values to secrets
79+
80+
### 4. Existing Secrets (Already Configured)
81+
82+
These secrets should already exist for the CI build:
83+
- `strava_secret_key`
84+
- `peloton_secret_key`
85+
- `smtp_username`
86+
- `smtp_password`
87+
- `smtp_server`
88+
- `intervalsicu_client_id`
89+
- `intervalsicu_client_secret`
90+
- `cesiumkey`
91+
92+
## Running the Workflow
93+
94+
1. Go to **Actions** tab in your repository
95+
2. Select **iOS App Store** workflow
96+
3. Click **Run workflow**
97+
4. Options:
98+
- **Upload to TestFlight**: Check to automatically upload after build
99+
- **Build number increment**: Enter a new build number (optional)
100+
101+
## Workflow Outputs
102+
103+
- **IPA file**: Downloaded as artifact, stored for 30 days
104+
- **dSYM files**: For crash reporting, stored for 30 days
105+
- **TestFlight upload**: Automatic if selected
106+
107+
## Troubleshooting
108+
109+
### "No signing certificate found"
110+
- Verify the certificate is not expired
111+
- Ensure it's a Distribution certificate (not Development)
112+
- Check the Base64 encoding is correct
113+
114+
### "Provisioning profile doesn't match"
115+
- Download fresh profiles from Apple Developer Portal
116+
- Ensure profiles are for App Store distribution
117+
- Verify bundle IDs match
118+
119+
### "Invalid API Key"
120+
- Regenerate the key in App Store Connect
121+
- Check Key ID and Issuer ID are correct
122+
- Ensure the key has sufficient permissions
123+
124+
## Manual Alternative
125+
126+
If you prefer the manual process:
127+
128+
1. Run the workflow with "Upload to TestFlight" unchecked
129+
2. Download the IPA artifact
130+
3. Use **Transporter** app on Mac to upload manually
131+
132+
## Build Number Management
133+
134+
The workflow can automatically update the build number. Enter a new build number in the workflow input, or leave empty to keep the current version from `project.pbxproj`.
135+
136+
Current version info in project:
137+
- Marketing Version: 2.20
138+
- Current Build Number: 1274
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
# iOS App Store Build and Publish Workflow
2+
# This workflow builds the iOS app and uploads it to App Store Connect
3+
#
4+
# Required secrets:
5+
# - APPLE_CERTIFICATE_P12: Base64 encoded distribution certificate (.p12)
6+
# - APPLE_CERTIFICATE_PASSWORD: Password for the .p12 certificate
7+
# - APPLE_PROVISIONING_PROFILE: Base64 encoded provisioning profile (.mobileprovision)
8+
# - APPLE_PROVISIONING_PROFILE_WATCHKIT: Base64 encoded WatchKit provisioning profile
9+
# - APPLE_PROVISIONING_PROFILE_WATCHKIT_EXTENSION: Base64 encoded WatchKit Extension provisioning profile
10+
# - APP_STORE_CONNECT_API_KEY_ID: App Store Connect API Key ID
11+
# - APP_STORE_CONNECT_API_ISSUER_ID: App Store Connect API Issuer ID
12+
# - APP_STORE_CONNECT_API_KEY_P8: Base64 encoded App Store Connect API Key (.p8)
13+
# - KEYCHAIN_PASSWORD: A password for the temporary keychain
14+
15+
name: iOS App Store
16+
17+
on:
18+
workflow_dispatch:
19+
inputs:
20+
upload_to_testflight:
21+
description: 'Upload to TestFlight after build'
22+
required: true
23+
default: 'true'
24+
type: boolean
25+
build_number_increment:
26+
description: 'Increment build number (leave empty to keep current)'
27+
required: false
28+
type: string
29+
30+
env:
31+
XCODE_PROJECT_PATH: 'build-qdomyos-zwift-Qt_5_15_2_for_iOS-Debug/qdomyoszwift.xcodeproj'
32+
SCHEME_NAME: 'qdomyoszwift'
33+
BUNDLE_ID: 'org.cagnulein.qdomyoszwift'
34+
TEAM_ID: '6335M7T29D'
35+
36+
jobs:
37+
build-and-upload:
38+
runs-on: macos-14
39+
40+
steps:
41+
- name: Checkout repository
42+
uses: actions/checkout@v4
43+
44+
- name: Checkout SmtpClient submodule
45+
uses: actions/checkout@v4
46+
with:
47+
repository: bluetiger9/SmtpClient-for-Qt
48+
path: "src/smtpclient/"
49+
ref: 3fa4a0fe5797070339422cf18b5e9ed8dcb91f9c
50+
51+
- name: Checkout googletest submodule
52+
uses: actions/checkout@v4
53+
with:
54+
repository: google/googletest
55+
path: "tst/googletest/"
56+
ref: "release-1.12.1"
57+
58+
- name: Select Xcode version
59+
run: sudo xcode-select -s /Applications/Xcode_15.2.app
60+
61+
- name: Show Xcode version
62+
run: xcodebuild -version
63+
64+
# Install Apple certificates and provisioning profiles
65+
- name: Install Apple Certificate
66+
env:
67+
APPLE_CERTIFICATE_P12: ${{ secrets.APPLE_CERTIFICATE_P12 }}
68+
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
69+
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
70+
run: |
71+
# Create variables
72+
CERTIFICATE_PATH=$RUNNER_TEMP/certificate.p12
73+
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
74+
75+
# Decode certificate from base64
76+
echo -n "$APPLE_CERTIFICATE_P12" | base64 --decode -o $CERTIFICATE_PATH
77+
78+
# Create temporary keychain
79+
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
80+
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
81+
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
82+
83+
# Import certificate to keychain
84+
security import $CERTIFICATE_PATH -P "$APPLE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
85+
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
86+
security list-keychain -d user -s $KEYCHAIN_PATH
87+
88+
- name: Install Provisioning Profiles
89+
env:
90+
APPLE_PROVISIONING_PROFILE: ${{ secrets.APPLE_PROVISIONING_PROFILE }}
91+
APPLE_PROVISIONING_PROFILE_WATCHKIT: ${{ secrets.APPLE_PROVISIONING_PROFILE_WATCHKIT }}
92+
APPLE_PROVISIONING_PROFILE_WATCHKIT_EXTENSION: ${{ secrets.APPLE_PROVISIONING_PROFILE_WATCHKIT_EXTENSION }}
93+
run: |
94+
# Create Provisioning Profiles directory
95+
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
96+
97+
# Decode and install main app provisioning profile
98+
echo -n "$APPLE_PROVISIONING_PROFILE" | base64 --decode -o ~/Library/MobileDevice/Provisioning\ Profiles/main.mobileprovision
99+
100+
# Decode and install WatchKit provisioning profile (if provided)
101+
if [ -n "$APPLE_PROVISIONING_PROFILE_WATCHKIT" ]; then
102+
echo -n "$APPLE_PROVISIONING_PROFILE_WATCHKIT" | base64 --decode -o ~/Library/MobileDevice/Provisioning\ Profiles/watchkit.mobileprovision
103+
fi
104+
105+
# Decode and install WatchKit Extension provisioning profile (if provided)
106+
if [ -n "$APPLE_PROVISIONING_PROFILE_WATCHKIT_EXTENSION" ]; then
107+
echo -n "$APPLE_PROVISIONING_PROFILE_WATCHKIT_EXTENSION" | base64 --decode -o ~/Library/MobileDevice/Provisioning\ Profiles/watchkit_extension.mobileprovision
108+
fi
109+
110+
# Increment build number if requested
111+
- name: Increment Build Number
112+
if: ${{ inputs.build_number_increment != '' }}
113+
run: |
114+
# Update CURRENT_PROJECT_VERSION in project.pbxproj
115+
NEW_BUILD_NUMBER="${{ inputs.build_number_increment }}"
116+
sed -i '' "s/CURRENT_PROJECT_VERSION = [0-9]*;/CURRENT_PROJECT_VERSION = $NEW_BUILD_NUMBER;/g" "$XCODE_PROJECT_PATH/project.pbxproj"
117+
echo "Updated build number to: $NEW_BUILD_NUMBER"
118+
119+
# Create secrets file
120+
- name: Create secrets file
121+
run: |
122+
cd src
123+
echo "#define STRAVA_SECRET_KEY ${{ secrets.strava_secret_key }}" > secret.h
124+
echo "#define PELOTON_SECRET_KEY ${{ secrets.peloton_secret_key }}" >> secret.h
125+
echo "#define SMTP_USERNAME ${{ secrets.smtp_username }}" >> secret.h
126+
echo "#define SMTP_PASSWORD ${{ secrets.smtp_password }}" >> secret.h
127+
echo "#define SMTP_SERVER ${{ secrets.smtp_server }}" >> secret.h
128+
echo "#define INTERVALSICU_CLIENT_ID ${{ secrets.intervalsicu_client_id }}" >> secret.h
129+
echo "#define INTERVALSICU_CLIENT_SECRET ${{ secrets.intervalsicu_client_secret }}" >> secret.h
130+
echo "${{ secrets.cesiumkey }}" >> inner_templates/googlemaps/cesium-key.js
131+
132+
# Build for Release (App Store)
133+
- name: Build Archive
134+
run: |
135+
xcodebuild archive \
136+
-project "$XCODE_PROJECT_PATH" \
137+
-scheme "$SCHEME_NAME" \
138+
-configuration Release \
139+
-destination "generic/platform=iOS" \
140+
-archivePath $RUNNER_TEMP/qdomyoszwift.xcarchive \
141+
CODE_SIGN_STYLE=Manual \
142+
DEVELOPMENT_TEAM=$TEAM_ID \
143+
CODE_SIGN_IDENTITY="iPhone Distribution" \
144+
-allowProvisioningUpdates
145+
146+
# Create ExportOptions.plist for App Store distribution
147+
- name: Create ExportOptions
148+
run: |
149+
cat > $RUNNER_TEMP/ExportOptions.plist << EOF
150+
<?xml version="1.0" encoding="UTF-8"?>
151+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
152+
<plist version="1.0">
153+
<dict>
154+
<key>method</key>
155+
<string>app-store-connect</string>
156+
<key>teamID</key>
157+
<string>${TEAM_ID}</string>
158+
<key>uploadSymbols</key>
159+
<true/>
160+
<key>destination</key>
161+
<string>upload</string>
162+
</dict>
163+
</plist>
164+
EOF
165+
166+
# Export IPA
167+
- name: Export IPA
168+
run: |
169+
xcodebuild -exportArchive \
170+
-archivePath $RUNNER_TEMP/qdomyoszwift.xcarchive \
171+
-exportOptionsPlist $RUNNER_TEMP/ExportOptions.plist \
172+
-exportPath $RUNNER_TEMP/export \
173+
-allowProvisioningUpdates
174+
175+
# Setup App Store Connect API Key
176+
- name: Setup App Store Connect API Key
177+
if: ${{ inputs.upload_to_testflight == true }}
178+
env:
179+
APP_STORE_CONNECT_API_KEY_P8: ${{ secrets.APP_STORE_CONNECT_API_KEY_P8 }}
180+
APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
181+
APP_STORE_CONNECT_API_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_ISSUER_ID }}
182+
run: |
183+
mkdir -p ~/.appstoreconnect/private_keys
184+
echo -n "$APP_STORE_CONNECT_API_KEY_P8" | base64 --decode -o ~/.appstoreconnect/private_keys/AuthKey_${APP_STORE_CONNECT_API_KEY_ID}.p8
185+
186+
# Upload to App Store Connect / TestFlight
187+
- name: Upload to TestFlight
188+
if: ${{ inputs.upload_to_testflight == true }}
189+
env:
190+
APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
191+
APP_STORE_CONNECT_API_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_ISSUER_ID }}
192+
run: |
193+
xcrun altool --upload-app \
194+
--type ios \
195+
--file "$RUNNER_TEMP/export/qdomyoszwift.ipa" \
196+
--apiKey "$APP_STORE_CONNECT_API_KEY_ID" \
197+
--apiIssuer "$APP_STORE_CONNECT_API_ISSUER_ID"
198+
199+
# Upload IPA as artifact (always, for backup)
200+
- name: Upload IPA Artifact
201+
uses: actions/upload-artifact@v4
202+
with:
203+
name: qdomyoszwift-ipa
204+
path: ${{ runner.temp }}/export/*.ipa
205+
retention-days: 30
206+
207+
# Upload dSYM for crash reporting
208+
- name: Upload dSYM Artifact
209+
uses: actions/upload-artifact@v4
210+
with:
211+
name: qdomyoszwift-dsym
212+
path: ${{ runner.temp }}/qdomyoszwift.xcarchive/dSYMs
213+
retention-days: 30
214+
215+
# Cleanup keychain
216+
- name: Cleanup
217+
if: always()
218+
run: |
219+
security delete-keychain $RUNNER_TEMP/app-signing.keychain-db || true
220+
rm -f $RUNNER_TEMP/certificate.p12 || true
221+
rm -rf ~/Library/MobileDevice/Provisioning\ Profiles/*.mobileprovision || true
222+
rm -rf ~/.appstoreconnect || true

0 commit comments

Comments
 (0)