Skip to content

Commit c99576c

Browse files
CopilotBunsDev
andauthored
Split iOS workflow from release train (#481)
Agent-Logs-Url: https://github.com/OpenKnots/okcode/sessions/8ea02c90-6101-4817-8bca-bcbc5647227e Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: BunsDev <68980965+BunsDev@users.noreply.github.com>
1 parent 2dfc9da commit c99576c

File tree

6 files changed

+307
-260
lines changed

6 files changed

+307
-260
lines changed

.github/workflows/release-ios.yml

Lines changed: 271 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1-
name: iOS Release Dry Run
1+
name: Release iOS
22

33
on:
4+
push:
5+
tags:
6+
- "v*.*.*"
47
workflow_dispatch:
58
inputs:
69
version:
@@ -12,11 +15,273 @@ permissions:
1215
contents: read
1316

1417
jobs:
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

Comments
 (0)