-
Notifications
You must be signed in to change notification settings - Fork 0
282 lines (256 loc) · 12.2 KB
/
ios-testflight.yml
File metadata and controls
282 lines (256 loc) · 12.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
name: iOS TestFlight
# Uploads the DoomCoder Companion iOS build to TestFlight whenever a
# tag prefixed with `ios-v` is pushed (e.g. `ios-v2.4.0`). Kept separate
# from the macOS release workflow because the cadences and signing
# pipelines are independent — the Mac app ships through Sparkle, the
# iOS app ships through the App Store / TestFlight.
#
# Required repository secrets (Settings → Secrets and variables → Actions):
# APP_STORE_CONNECT_KEY_ID — App Store Connect API key ID (10 chars)
# APP_STORE_CONNECT_ISSUER_ID — App Store Connect issuer UUID
# APP_STORE_CONNECT_PRIVATE_KEY — Raw text contents of AuthKey_xxx.p8 (paste the full file, including BEGIN/END lines)
# IOS_DISTRIBUTION_CERTIFICATE — Base64-encoded Apple Distribution .p12
# IOS_DISTRIBUTION_CERT_PASSWORD — Password for the .p12 above
# IOS_KEYCHAIN_PASSWORD — Throwaway password for the runner keychain
# APPLE_TEAM_ID — Apple Developer Team ID (e.g. A9P2388PHM)
#
on:
push:
tags:
- 'ios-v[0-9]*'
workflow_dispatch:
inputs:
version:
description: 'Marketing version (e.g. 2.4.0)'
required: true
default: '2.4.0'
permissions:
contents: read
jobs:
testflight:
name: Build & Upload to TestFlight
runs-on: macos-26
steps:
- name: Checkout
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Resolve version
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
VERSION="${{ github.event.inputs.version }}"
else
VERSION="${GITHUB_REF_NAME#ios-v}"
fi
BUILD="$(date -u +%Y%m%d%H%M)"
echo "VERSION=${VERSION}" >> $GITHUB_ENV
echo "BUILD=${BUILD}" >> $GITHUB_ENV
- name: Select Xcode
run: |
if [ -d /Applications/Xcode.app ]; then
sudo xcode-select -s /Applications/Xcode.app
else
XCODE_PATH=$(ls -d /Applications/Xcode*.app 2>/dev/null | sort -V | tail -1)
sudo xcode-select -s "${XCODE_PATH}"
fi
xcodebuild -version
- name: Install XcodeGen
run: brew install xcodegen
- name: Regenerate Xcode project from project.yml
working-directory: DoomCoderCompanion
run: xcodegen generate
- name: Stamp version + build number into project
working-directory: DoomCoderCompanion
run: |
/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString ${VERSION}" \
DoomCoderCompanion/Resources/Info.plist
/usr/libexec/PlistBuddy -c "Set :CFBundleVersion ${BUILD}" \
DoomCoderCompanion/Resources/Info.plist
/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString ${VERSION}" \
NotificationService/Info.plist
/usr/libexec/PlistBuddy -c "Set :CFBundleVersion ${BUILD}" \
NotificationService/Info.plist
- name: Create temporary keychain & install signing assets
env:
KEYCHAIN_PASSWORD: ${{ secrets.IOS_KEYCHAIN_PASSWORD }}
DIST_CERT_B64: ${{ secrets.IOS_DISTRIBUTION_CERTIFICATE }}
DIST_CERT_PASSWORD: ${{ secrets.IOS_DISTRIBUTION_CERT_PASSWORD }}
PROFILE_APP_B64: ${{ secrets.IOS_PROVISIONING_PROFILE_APP }}
PROFILE_NS_B64: ${{ secrets.IOS_PROVISIONING_PROFILE_NS }}
run: |
set -euo pipefail
KEYCHAIN_PATH="$RUNNER_TEMP/ios-build.keychain-db"
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 7200 "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security list-keychains -d user -s "$KEYCHAIN_PATH" $(security list-keychains -d user | tr -d '"')
# Import distribution certificate (must be valid — not revoked).
echo "$DIST_CERT_B64" | base64 -D > "$RUNNER_TEMP/dist.p12"
security import "$RUNNER_TEMP/dist.p12" \
-k "$KEYCHAIN_PATH" \
-P "$DIST_CERT_PASSWORD" \
-T /usr/bin/codesign \
-T /usr/bin/security
security set-key-partition-list \
-S apple-tool:,apple:,codesign: \
-s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
rm -f "$RUNNER_TEMP/dist.p12"
echo "Distribution certificate imported."
# Install provisioning profiles — Xcode scans all .mobileprovision files
# in this directory and matches by profile name, so fixed filenames work fine.
PP_DIR="$HOME/Library/MobileDevice/Provisioning Profiles"
mkdir -p "$PP_DIR"
echo "$PROFILE_APP_B64" | base64 -D > "$PP_DIR/companion_app.mobileprovision"
echo "$PROFILE_NS_B64" | base64 -D > "$PP_DIR/companion_ns.mobileprovision"
echo "Profiles installed:"
ls -lh "$PP_DIR/"
- name: Write App Store Connect API key
id: write-api-key
env:
API_KEY: ${{ secrets.APP_STORE_CONNECT_PRIVATE_KEY }}
API_KEY_ID_RAW: ${{ secrets.APP_STORE_CONNECT_KEY_ID }}
API_ISSUER_RAW: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
run: |
set -euo pipefail
# Strip any accidental whitespace/newlines from ID values pasted into secrets.
API_KEY_ID=$(printf '%s' "$API_KEY_ID_RAW" | tr -d '[:space:]')
API_ISSUER=$(printf '%s' "$API_ISSUER_RAW" | tr -d '[:space:]')
if [[ -z "$API_KEY" ]]; then
echo "::error::APP_STORE_CONNECT_PRIVATE_KEY secret is empty or not set"
exit 1
fi
if [[ -z "$API_KEY_ID" ]]; then
echo "::error::APP_STORE_CONNECT_KEY_ID secret is empty or not set"
exit 1
fi
mkdir -p "$HOME/.appstoreconnect/private_keys"
RAW_FILE="$RUNNER_TEMP/api_key_raw.p8"
KEY_FILE="$HOME/.appstoreconnect/private_keys/AuthKey_${API_KEY_ID}.p8"
# Auto-detect: raw PEM (starts with -----BEGIN) or base64-encoded.
if printf '%s' "$API_KEY" | head -c 20 | grep -q "^-----BEGIN"; then
# Raw PEM: strip CR, write as-is.
printf '%s' "$API_KEY" | tr -d '\r' > "$RAW_FILE"
echo "Detected raw PEM format"
else
# Base64-encoded outer wrapper: decode it first, strip CR.
printf '%s' "$API_KEY" | tr -d '\r ' | base64 -D > "$RAW_FILE"
echo "Detected base64-encoded format"
fi
# ── CANONICAL PEM RECONSTRUCTION ─────────────────────────────────────
# GitHub Secrets often strips newlines when pasting multiline values,
# resulting in: -----BEGIN PRIVATE KEY-----<base64>-----END PRIVATE KEY-----
# CryptoKit (used by xcodebuild) requires base64 wrapped at 64 chars.
# We always reconstruct the PEM regardless, so we handle both cases.
RAW_CONTENT=$(cat "$RAW_FILE" | tr -d '\r')
# Extract type from first "-----BEGIN <TYPE>-----" token
KEY_TYPE=$(printf '%s' "$RAW_CONTENT" | grep -o -- "-----BEGIN [^-]*-----" | head -1 | sed 's/-----BEGIN //;s/-----//')
if [[ -z "$KEY_TYPE" ]]; then
echo "::error::Could not detect PEM type from key content"
exit 1
fi
echo "PEM type detected: ${KEY_TYPE}"
# Extract raw base64 payload (strip all PEM headers/footers and whitespace)
B64=$(printf '%s' "$RAW_CONTENT" | grep -v "^-----" | tr -d '\n\r ')
if [[ -z "$B64" ]]; then
echo "::error::Empty base64 payload in key"
exit 1
fi
# Reconstruct with standard 64-char line wrapping
{
printf -- "-----BEGIN %s-----\n" "$KEY_TYPE"
printf '%s' "$B64" | fold -w 64
printf "\n-----END %s-----\n" "$KEY_TYPE"
} > "$KEY_FILE"
echo "PEM reconstructed: $(wc -l < "$KEY_FILE") lines, $(wc -c < "$KEY_FILE" | tr -d ' ') bytes"
rm -f "$RAW_FILE"
# Validate via openssl (non-fatal — CryptoKit may still accept it)
OPENSSL_RC=0
OPENSSL_OUT="$(openssl pkey -in "$KEY_FILE" -noout 2>&1)" || OPENSSL_RC=$?
if [ $OPENSSL_RC -eq 0 ]; then
echo "openssl validation: PASSED"
else
echo "::warning::openssl validation: ${OPENSSL_OUT}"
echo "::warning::Key may still work with xcodebuild — proceeding"
fi
chmod 600 "$KEY_FILE"
# Validate written file
if ! head -c 20 "$KEY_FILE" | grep -q "^-----BEGIN"; then
echo "::error::Written .p8 doesn't look like PEM"
exit 1
fi
SIZE=$(wc -c < "$KEY_FILE" | tr -d ' ')
echo "API key written: ${SIZE} bytes → $(basename "$KEY_FILE")"
# Emit trimmed values as step outputs so downstream steps use the exact same strings.
echo "key_id=${API_KEY_ID}" >> "$GITHUB_OUTPUT"
echo "issuer_id=${API_ISSUER}" >> "$GITHUB_OUTPUT"
echo "key_path=${KEY_FILE}" >> "$GITHUB_OUTPUT"
- name: Archive (Release)
working-directory: DoomCoderCompanion
env:
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: |
unset GIT_CONFIG_COUNT GIT_CONFIG_KEY_0 GIT_CONFIG_VALUE_0
# Manual signing: uses the distribution cert + profiles installed above.
# No allowProvisioningUpdates / cloud signing — avoids "Cloud signing
# permission error" which requires team-level cloud cert opt-in.
xcodebuild \
-project DoomCoderCompanion.xcodeproj \
-scheme DoomCoderCompanion \
-configuration Release \
-destination 'generic/platform=iOS' \
-archivePath "$RUNNER_TEMP/DoomCoderCompanion.xcarchive" \
CODE_SIGN_STYLE=Manual \
CODE_SIGN_IDENTITY="Apple Distribution" \
DEVELOPMENT_TEAM="${APPLE_TEAM_ID}" \
archive
- name: Write ExportOptions.plist
run: |
TEAM_ID="${{ secrets.APPLE_TEAM_ID }}"
cat > "$RUNNER_TEMP/ExportOptions.plist" << PLIST_EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key><string>app-store-connect</string>
<key>teamID</key><string>${TEAM_ID}</string>
<key>signingStyle</key><string>manual</string>
<key>provisioningProfiles</key>
<dict>
<key>com.doomcoder.app.companion</key><string>DoomCoder Companion AppStore</string>
<key>com.doomcoder.app.companion.NotificationService</key><string>DoomCoder Companion NS AppStore</string>
</dict>
<key>uploadSymbols</key><true/>
<key>manageAppVersionAndBuildNumber</key><false/>
</dict>
</plist>
PLIST_EOF
- name: Export archive to IPA
run: |
unset GIT_CONFIG_COUNT GIT_CONFIG_KEY_0 GIT_CONFIG_VALUE_0
xcodebuild \
-exportArchive \
-archivePath "$RUNNER_TEMP/DoomCoderCompanion.xcarchive" \
-exportOptionsPlist "$RUNNER_TEMP/ExportOptions.plist" \
-exportPath "$RUNNER_TEMP/export"
echo "IPA exported:"
ls -lh "$RUNNER_TEMP/export/"*.ipa 2>/dev/null || ls -lh "$RUNNER_TEMP/export/"
- name: Upload to TestFlight
env:
ASC_KEY_ID: ${{ steps.write-api-key.outputs.key_id }}
ASC_ISSUER_ID: ${{ steps.write-api-key.outputs.issuer_id }}
run: |
IPA_PATH=$(find "$RUNNER_TEMP/export" -name "*.ipa" | head -1)
if [[ -z "$IPA_PATH" ]]; then
echo "::error::No IPA found in export directory"
ls -la "$RUNNER_TEMP/export/"
exit 1
fi
echo "Uploading: $(basename "$IPA_PATH")"
xcrun altool --upload-app \
--type ios \
--file "$IPA_PATH" \
--apiKey "$ASC_KEY_ID" \
--apiIssuer "$ASC_ISSUER_ID"
- name: Clean up API key
if: always()
run: rm -f ~/.appstoreconnect/private_keys/AuthKey_*.p8