1- name : iOS Release Dry Run
1+ name : Release iOS
22
33on :
4+ push :
5+ tags :
6+ - " v*.*.*"
47 workflow_dispatch :
58 inputs :
69 version :
@@ -12,11 +15,273 @@ permissions:
1215 contents : read
1316
1417jobs :
15- guidance :
16- name : Coordinated train guidance
18+ preflight :
19+ name : Resolve iOS release metadata
1720 runs-on : ubuntu-24.04
21+ outputs :
22+ version : ${{ steps.release_meta.outputs.version }}
23+ tag : ${{ steps.release_meta.outputs.tag }}
24+ release_channel : ${{ steps.release_meta.outputs.release_channel }}
25+ build_timestamp : ${{ steps.release_meta.outputs.build_timestamp }}
26+ ref : ${{ github.sha }}
1827 steps :
19- - name : Explain release entrypoint
28+ - id : release_meta
29+ name : Resolve release version
30+ shell : bash
2031 run : |
21- echo "Use .github/workflows/release.yml for official tags and coordinated RC/stable releases."
22- echo "This workflow is reserved for manual iOS dry runs while stabilizing the TestFlight lane."
32+ set -euo pipefail
33+
34+ if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then
35+ raw="${{ github.event.inputs.version }}"
36+ else
37+ raw="${GITHUB_REF_NAME}"
38+ fi
39+
40+ version="${raw#v}"
41+ if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.-]+)?$ ]]; then
42+ echo "Invalid release version: $raw" >&2
43+ exit 1
44+ fi
45+
46+ build_timestamp="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
47+
48+ echo "version=$version" >> "$GITHUB_OUTPUT"
49+ echo "tag=v$version" >> "$GITHUB_OUTPUT"
50+ echo "build_timestamp=$build_timestamp" >> "$GITHUB_OUTPUT"
51+
52+ if [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
53+ echo "release_channel=stable" >> "$GITHUB_OUTPUT"
54+ else
55+ echo "release_channel=prerelease" >> "$GITHUB_OUTPUT"
56+ fi
57+
58+ ios_signing_preflight :
59+ name : iOS signing preflight
60+ needs : [preflight]
61+ runs-on : ubuntu-24.04
62+ steps :
63+ - name : Check iOS signing secrets
64+ shell : bash
65+ env :
66+ APPLE_API_KEY : ${{ secrets.APPLE_API_KEY }}
67+ APPLE_API_KEY_ID : ${{ secrets.APPLE_API_KEY_ID }}
68+ APPLE_API_ISSUER : ${{ secrets.APPLE_API_ISSUER }}
69+ APPLE_TEAM_ID : ${{ secrets.APPLE_TEAM_ID }}
70+ IOS_PROVISIONING_PROFILE : ${{ secrets.IOS_PROVISIONING_PROFILE }}
71+ IOS_PROVISIONING_PROFILE_NAME : ${{ secrets.IOS_PROVISIONING_PROFILE_NAME }}
72+ run : |
73+ set -euo pipefail
74+
75+ required_secrets=(
76+ APPLE_API_KEY
77+ APPLE_API_KEY_ID
78+ APPLE_API_ISSUER
79+ APPLE_TEAM_ID
80+ IOS_PROVISIONING_PROFILE
81+ IOS_PROVISIONING_PROFILE_NAME
82+ )
83+
84+ missing=()
85+ for secret_name in "${required_secrets[@]}"; do
86+ if [[ -z "${!secret_name}" ]]; then
87+ missing+=("$secret_name")
88+ fi
89+ done
90+
91+ if (( ${#missing[@]} > 0 )); then
92+ missing_csv="$(IFS=,; echo "${missing[*]}")"
93+ echo "Missing required iOS signing secrets: $missing_csv" >&2
94+ exit 1
95+ fi
96+
97+ echo "All required iOS signing secrets are configured."
98+
99+ ios_testflight :
100+ name : iOS TestFlight
101+ needs : [preflight, ios_signing_preflight]
102+ runs-on : macos-14
103+ env :
104+ RELEASE_VERSION : ${{ needs.preflight.outputs.version }}
105+ OKCODE_COMMIT_HASH : ${{ github.sha }}
106+ OKCODE_BUILD_TIMESTAMP : ${{ needs.preflight.outputs.build_timestamp }}
107+ OKCODE_RELEASE_CHANNEL : ${{ needs.preflight.outputs.release_channel }}
108+ steps :
109+ - name : Checkout
110+ uses : actions/checkout@v6
111+ with :
112+ ref : ${{ needs.preflight.outputs.ref }}
113+ fetch-depth : 0
114+
115+ - name : Setup Bun
116+ uses : oven-sh/setup-bun@v2
117+ with :
118+ bun-version-file : package.json
119+
120+ - name : Setup Node
121+ uses : actions/setup-node@v6
122+ with :
123+ node-version-file : package.json
124+
125+ - name : Install dependencies
126+ run : bun install --frozen-lockfile
127+
128+ - name : Patch Capacitor local-notifications for Xcode 15
129+ run : bun run patch:capacitor-local-notifications
130+
131+ - name : Align package versions to release version
132+ run : node scripts/update-release-package-versions.ts "$RELEASE_VERSION"
133+
134+ - name : Update iOS version in Xcode project
135+ run : node scripts/update-ios-version.ts "$RELEASE_VERSION" --build-number "$GITHUB_RUN_NUMBER"
136+
137+ - name : Build mobile web bundle
138+ run : bun run --cwd apps/mobile build
139+
140+ - name : Sync Capacitor iOS
141+ run : bunx cap sync ios --deployment
142+ working-directory : apps/mobile
143+
144+ - name : Log iOS build metadata
145+ run : |
146+ echo "version=$RELEASE_VERSION"
147+ echo "tag=${{ needs.preflight.outputs.tag }}"
148+ echo "commit=$OKCODE_COMMIT_HASH"
149+ echo "build_timestamp=$OKCODE_BUILD_TIMESTAMP"
150+ echo "channel=$OKCODE_RELEASE_CHANNEL"
151+
152+ - name : Install App Store Connect API key and provisioning profile
153+ env :
154+ APPLE_API_KEY : ${{ secrets.APPLE_API_KEY }}
155+ APPLE_API_KEY_ID : ${{ secrets.APPLE_API_KEY_ID }}
156+ APPLE_API_ISSUER : ${{ secrets.APPLE_API_ISSUER }}
157+ IOS_PROVISIONING_PROFILE : ${{ secrets.IOS_PROVISIONING_PROFILE }}
158+ IOS_PROVISIONING_PROFILE_NAME : ${{ secrets.IOS_PROVISIONING_PROFILE_NAME }}
159+ run : |
160+ set -euo pipefail
161+ for secret_name in APPLE_API_KEY APPLE_API_KEY_ID APPLE_API_ISSUER IOS_PROVISIONING_PROFILE IOS_PROVISIONING_PROFILE_NAME; do
162+ if [[ -z "${!secret_name}" ]]; then
163+ echo "Missing required secret: $secret_name" >&2
164+ exit 1
165+ fi
166+ done
167+
168+ KEY_PATH="$RUNNER_TEMP/AuthKey_${APPLE_API_KEY_ID}.p8"
169+ printf '%s' "$APPLE_API_KEY" > "$KEY_PATH"
170+ echo "APPLE_API_KEY_PATH=$KEY_PATH" >> "$GITHUB_ENV"
171+
172+ PROFILE_PATH="$RUNNER_TEMP/profile.mobileprovision"
173+ echo "$IOS_PROVISIONING_PROFILE" | base64 --decode > "$PROFILE_PATH"
174+ mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
175+ cp "$PROFILE_PATH" ~/Library/MobileDevice/Provisioning\ Profiles/
176+ echo "Installed provisioning profile: $IOS_PROVISIONING_PROFILE_NAME"
177+
178+ - name : Simulator smoke build
179+ run : |
180+ set -euo pipefail
181+ xcodebuild build \
182+ -project apps/mobile/ios/App/App.xcodeproj \
183+ -scheme App \
184+ -configuration Debug \
185+ -destination 'platform=iOS Simulator,name=iPhone 15' \
186+ COMPILER_INDEX_STORE_ENABLE=NO
187+
188+ - name : Build iOS archive
189+ env :
190+ APPLE_TEAM_ID : ${{ secrets.APPLE_TEAM_ID }}
191+ APPLE_API_KEY_ID : ${{ secrets.APPLE_API_KEY_ID }}
192+ APPLE_API_ISSUER : ${{ secrets.APPLE_API_ISSUER }}
193+ run : |
194+ set -euo pipefail
195+
196+ xcodebuild archive \
197+ -project apps/mobile/ios/App/App.xcodeproj \
198+ -scheme App \
199+ -configuration Release \
200+ -destination 'generic/platform=iOS' \
201+ -archivePath "$RUNNER_TEMP/App.xcarchive" \
202+ -allowProvisioningUpdates \
203+ -authenticationKeyPath "$APPLE_API_KEY_PATH" \
204+ -authenticationKeyID "$APPLE_API_KEY_ID" \
205+ -authenticationKeyIssuerID "$APPLE_API_ISSUER" \
206+ CODE_SIGN_STYLE=Manual \
207+ DEVELOPMENT_TEAM="$APPLE_TEAM_ID" \
208+ CODE_SIGN_IDENTITY="Apple Distribution" \
209+ PROVISIONING_PROFILE_SPECIFIER="${{ secrets.IOS_PROVISIONING_PROFILE_NAME }}" \
210+ COMPILER_INDEX_STORE_ENABLE=NO
211+
212+ - name : Generate ExportOptions.plist
213+ env :
214+ APPLE_TEAM_ID : ${{ secrets.APPLE_TEAM_ID }}
215+ run : |
216+ cat > "$RUNNER_TEMP/ExportOptions.plist" <<PLIST
217+ <?xml version="1.0" encoding="UTF-8"?>
218+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
219+ <plist version="1.0">
220+ <dict>
221+ <key>method</key>
222+ <string>app-store-connect</string>
223+ <key>destination</key>
224+ <string>upload</string>
225+ <key>teamID</key>
226+ <string>${APPLE_TEAM_ID}</string>
227+ <key>uploadSymbols</key>
228+ <true/>
229+ <key>signingStyle</key>
230+ <string>manual</string>
231+ <key>provisioningProfiles</key>
232+ <dict>
233+ <key>com.openknots.okcode.mobile</key>
234+ <string>${{ secrets.IOS_PROVISIONING_PROFILE_NAME }}</string>
235+ </dict>
236+ </dict>
237+ </plist>
238+ PLIST
239+
240+ - name : Export IPA
241+ env :
242+ APPLE_API_KEY_ID : ${{ secrets.APPLE_API_KEY_ID }}
243+ APPLE_API_ISSUER : ${{ secrets.APPLE_API_ISSUER }}
244+ run : |
245+ set -euo pipefail
246+ xcodebuild -exportArchive \
247+ -archivePath "$RUNNER_TEMP/App.xcarchive" \
248+ -exportPath "$RUNNER_TEMP/export" \
249+ -exportOptionsPlist "$RUNNER_TEMP/ExportOptions.plist" \
250+ -allowProvisioningUpdates \
251+ -authenticationKeyPath "$APPLE_API_KEY_PATH" \
252+ -authenticationKeyID "$APPLE_API_KEY_ID" \
253+ -authenticationKeyIssuerID "$APPLE_API_ISSUER"
254+
255+ - name : Stage App Store Connect API key for upload
256+ env :
257+ APPLE_API_KEY_ID : ${{ secrets.APPLE_API_KEY_ID }}
258+ run : |
259+ set -euo pipefail
260+ KEY_DIR="$HOME/private_keys"
261+ mkdir -p "$KEY_DIR"
262+ cp "$APPLE_API_KEY_PATH" "$KEY_DIR/AuthKey_${APPLE_API_KEY_ID}.p8"
263+
264+ - name : Upload to TestFlight
265+ env :
266+ APPLE_API_KEY_ID : ${{ secrets.APPLE_API_KEY_ID }}
267+ APPLE_API_ISSUER : ${{ secrets.APPLE_API_ISSUER }}
268+ run : |
269+ set -euo pipefail
270+
271+ IPA_FILE=$(find "$RUNNER_TEMP/export" -name "*.ipa" -print -quit)
272+ if [[ -z "$IPA_FILE" ]]; then
273+ echo "No IPA file found in export directory" >&2
274+ exit 1
275+ fi
276+
277+ xcrun altool --upload-app \
278+ -f "$IPA_FILE" \
279+ -t ios \
280+ --apiKey "$APPLE_API_KEY_ID" \
281+ --apiIssuer "$APPLE_API_ISSUER"
282+
283+ - name : Cleanup signing assets
284+ if : always()
285+ run : |
286+ rm -f "$APPLE_API_KEY_PATH" || true
287+ rm -f "$HOME/private_keys/AuthKey_${{ secrets.APPLE_API_KEY_ID }}.p8" || true
0 commit comments