Skip to content

Commit 4968c91

Browse files
katipallyCopilot
andcommitted
fix(ci): normalize .p8 via openssl + use step outputs to avoid key-ID whitespace mismatch
Two root causes fixed: 1. APP_STORE_CONNECT_KEY_ID inline expressions (${{ secrets.X }}) in the run script vs env-var path in write step could diverge if the secret had trailing whitespace. Now: write step trims + emits step outputs; archive/export steps consume those outputs as env vars. 2. CryptoKit's PEM parser is stricter than any manual cleaning. Pipe the raw key through 'openssl pkey' to rewrite it in canonical PKCS8 PEM form (64-char lines, no stray bytes, clean header/footer) before xcodebuild reads it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent d8481ba commit 4968c91

1 file changed

Lines changed: 54 additions & 23 deletions

File tree

.github/workflows/ios-testflight.yml

Lines changed: 54 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -106,10 +106,17 @@ jobs:
106106
rm "$RUNNER_TEMP/dist.p12"
107107
108108
- name: Write App Store Connect API key
109+
id: write-api-key
109110
env:
110111
API_KEY: ${{ secrets.APP_STORE_CONNECT_PRIVATE_KEY }}
111-
API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }}
112+
API_KEY_ID_RAW: ${{ secrets.APP_STORE_CONNECT_KEY_ID }}
113+
API_ISSUER_RAW: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
112114
run: |
115+
set -euo pipefail
116+
# Strip any accidental whitespace/newlines from ID values pasted into secrets.
117+
API_KEY_ID=$(printf '%s' "$API_KEY_ID_RAW" | tr -d '[:space:]')
118+
API_ISSUER=$(printf '%s' "$API_ISSUER_RAW" | tr -d '[:space:]')
119+
113120
if [[ -z "$API_KEY" ]]; then
114121
echo "::error::APP_STORE_CONNECT_PRIVATE_KEY secret is empty or not set"
115122
exit 1
@@ -118,53 +125,73 @@ jobs:
118125
echo "::error::APP_STORE_CONNECT_KEY_ID secret is empty or not set"
119126
exit 1
120127
fi
121-
mkdir -p ~/.appstoreconnect/private_keys
122-
KEY_FILE=~/.appstoreconnect/private_keys/AuthKey_${API_KEY_ID}.p8
128+
129+
mkdir -p "$HOME/.appstoreconnect/private_keys"
130+
RAW_FILE="$RUNNER_TEMP/api_key_raw.p8"
131+
KEY_FILE="$HOME/.appstoreconnect/private_keys/AuthKey_${API_KEY_ID}.p8"
123132
124133
# Auto-detect: raw PEM (starts with -----BEGIN) or base64-encoded.
125134
if printf '%s' "$API_KEY" | head -c 20 | grep -q "^-----BEGIN"; then
126-
# Raw PEM — strip carriage returns and trailing whitespace per line
127-
# (CryptoKit rejects trailing spaces on -----END PRIVATE KEY-----).
128-
printf '%s' "$API_KEY" | tr -d '\r' | sed 's/[[:space:]]*$//' > "$KEY_FILE"
135+
# Raw PEM: strip CR and trailing whitespace per line.
136+
printf '%s' "$API_KEY" | tr -d '\r' | sed 's/[[:space:]]*$//' > "$RAW_FILE"
129137
echo "Detected raw PEM format"
130138
else
131-
# Base64-encoded .p8 — decode then strip carriage returns.
132-
printf '%s' "$API_KEY" | tr -d '\r ' | base64 -D > "$KEY_FILE"
133-
echo "Detected base64-encoded format, decoded successfully"
139+
# Base64-encoded: decode then strip CR.
140+
printf '%s' "$API_KEY" | tr -d '\r ' | base64 -D > "$RAW_FILE"
141+
echo "Detected base64-encoded format"
142+
fi
143+
144+
# Ensure file ends with exactly one newline.
145+
if [[ "$(tail -c 1 "$RAW_FILE" | xxd -p)" != "0a" ]]; then
146+
printf '\n' >> "$RAW_FILE"
134147
fi
135148
136-
# Ensure file ends with a single newline (CryptoKit requires it)
137-
if [[ "$(tail -c 1 "$KEY_FILE" | xxd -p)" != "0a" ]]; then
138-
printf '\n' >> "$KEY_FILE"
149+
# Normalize via openssl so CryptoKit gets a canonical PEM structure
150+
# (correct line width, no stray bytes, clean header/footer).
151+
if openssl pkey -in "$RAW_FILE" -out "$KEY_FILE" 2>/dev/null; then
152+
echo "Key normalized via openssl pkey"
153+
else
154+
cp "$RAW_FILE" "$KEY_FILE"
155+
echo "Warning: openssl normalization skipped — using cleaned raw key"
139156
fi
157+
rm -f "$RAW_FILE"
158+
chmod 600 "$KEY_FILE"
140159
160+
# Validate written file
141161
if ! head -c 20 "$KEY_FILE" | grep -q "^-----BEGIN"; then
142-
echo "::error::Written .p8 doesn't look like PEM."
162+
echo "::error::Written .p8 doesn't look like PEM"
143163
exit 1
144164
fi
145165
SIZE=$(wc -c < "$KEY_FILE" | tr -d ' ')
146-
FIRST=$(head -1 "$KEY_FILE")
147-
LAST=$(tail -1 "$KEY_FILE")
148-
echo "API key: ${SIZE} bytes | first: '${FIRST}' | last: '${LAST}'"
166+
echo "API key written: ${SIZE} bytes → $(basename "$KEY_FILE")"
167+
168+
# Emit trimmed values as step outputs so downstream steps use the exact same strings.
169+
echo "key_id=${API_KEY_ID}" >> "$GITHUB_OUTPUT"
170+
echo "issuer_id=${API_ISSUER}" >> "$GITHUB_OUTPUT"
171+
echo "key_path=${KEY_FILE}" >> "$GITHUB_OUTPUT"
149172
150173
- name: Archive (Release)
151174
working-directory: DoomCoderCompanion
152175
env:
153176
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
177+
ASC_KEY_ID: ${{ steps.write-api-key.outputs.key_id }}
178+
ASC_ISSUER_ID: ${{ steps.write-api-key.outputs.issuer_id }}
179+
ASC_KEY_PATH: ${{ steps.write-api-key.outputs.key_path }}
154180
run: |
155181
# Strip the agent's stale GIT_CONFIG vars so SwiftPM can resolve bare repos.
156182
unset GIT_CONFIG_COUNT GIT_CONFIG_KEY_0 GIT_CONFIG_VALUE_0
183+
echo "Using key: $(basename "${ASC_KEY_PATH}")"
157184
xcodebuild \
158185
-project DoomCoderCompanion.xcodeproj \
159186
-scheme DoomCoderCompanion \
160187
-configuration Release \
161188
-destination 'generic/platform=iOS' \
162189
-archivePath "$RUNNER_TEMP/DoomCoderCompanion.xcarchive" \
163190
-allowProvisioningUpdates \
164-
-authenticationKeyPath ~/.appstoreconnect/private_keys/AuthKey_${{ secrets.APP_STORE_CONNECT_KEY_ID }}.p8 \
165-
-authenticationKeyID ${{ secrets.APP_STORE_CONNECT_KEY_ID }} \
166-
-authenticationKeyIssuerID ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }} \
167-
DEVELOPMENT_TEAM=${APPLE_TEAM_ID} \
191+
-authenticationKeyPath "${ASC_KEY_PATH}" \
192+
-authenticationKeyID "${ASC_KEY_ID}" \
193+
-authenticationKeyIssuerID "${ASC_ISSUER_ID}" \
194+
DEVELOPMENT_TEAM="${APPLE_TEAM_ID}" \
168195
archive
169196
170197
- name: Write ExportOptions.plist
@@ -186,6 +213,10 @@ jobs:
186213
EOF
187214
188215
- name: Export & upload to TestFlight
216+
env:
217+
ASC_KEY_ID: ${{ steps.write-api-key.outputs.key_id }}
218+
ASC_ISSUER_ID: ${{ steps.write-api-key.outputs.issuer_id }}
219+
ASC_KEY_PATH: ${{ steps.write-api-key.outputs.key_path }}
189220
run: |
190221
unset GIT_CONFIG_COUNT GIT_CONFIG_KEY_0 GIT_CONFIG_VALUE_0
191222
xcodebuild \
@@ -194,9 +225,9 @@ jobs:
194225
-exportOptionsPlist "$RUNNER_TEMP/ExportOptions.plist" \
195226
-exportPath "$RUNNER_TEMP/export" \
196227
-allowProvisioningUpdates \
197-
-authenticationKeyPath ~/.appstoreconnect/private_keys/AuthKey_${{ secrets.APP_STORE_CONNECT_KEY_ID }}.p8 \
198-
-authenticationKeyID ${{ secrets.APP_STORE_CONNECT_KEY_ID }} \
199-
-authenticationKeyIssuerID ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
228+
-authenticationKeyPath "${ASC_KEY_PATH}" \
229+
-authenticationKeyID "${ASC_KEY_ID}" \
230+
-authenticationKeyIssuerID "${ASC_ISSUER_ID}"
200231
201232
- name: Clean up API key
202233
if: always()

0 commit comments

Comments
 (0)