Skip to content

fix(ci): normalize .p8 via openssl + use step outputs to avoid key-ID… #6

fix(ci): normalize .p8 via openssl + use step outputs to avoid key-ID…

fix(ci): normalize .p8 via openssl + use step outputs to avoid key-ID… #6

Workflow file for this run

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
env:
KEYCHAIN_PASSWORD: ${{ secrets.IOS_KEYCHAIN_PASSWORD }}
DIST_CERT_B64: ${{ secrets.IOS_DISTRIBUTION_CERTIFICATE }}
DIST_CERT_PASSWORD: ${{ secrets.IOS_DISTRIBUTION_CERT_PASSWORD }}
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 '"')
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 "$RUNNER_TEMP/dist.p12"
- 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 and trailing whitespace per line.
printf '%s' "$API_KEY" | tr -d '\r' | sed 's/[[:space:]]*$//' > "$RAW_FILE"
echo "Detected raw PEM format"
else
# Base64-encoded: decode then strip CR.
printf '%s' "$API_KEY" | tr -d '\r ' | base64 -D > "$RAW_FILE"
echo "Detected base64-encoded format"
fi
# Ensure file ends with exactly one newline.
if [[ "$(tail -c 1 "$RAW_FILE" | xxd -p)" != "0a" ]]; then
printf '\n' >> "$RAW_FILE"
fi
# Normalize via openssl so CryptoKit gets a canonical PEM structure
# (correct line width, no stray bytes, clean header/footer).
if openssl pkey -in "$RAW_FILE" -out "$KEY_FILE" 2>/dev/null; then
echo "Key normalized via openssl pkey"
else
cp "$RAW_FILE" "$KEY_FILE"
echo "Warning: openssl normalization skipped — using cleaned raw key"
fi
rm -f "$RAW_FILE"
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 }}
ASC_KEY_ID: ${{ steps.write-api-key.outputs.key_id }}
ASC_ISSUER_ID: ${{ steps.write-api-key.outputs.issuer_id }}
ASC_KEY_PATH: ${{ steps.write-api-key.outputs.key_path }}
run: |
# Strip the agent's stale GIT_CONFIG vars so SwiftPM can resolve bare repos.
unset GIT_CONFIG_COUNT GIT_CONFIG_KEY_0 GIT_CONFIG_VALUE_0
echo "Using key: $(basename "${ASC_KEY_PATH}")"
xcodebuild \
-project DoomCoderCompanion.xcodeproj \
-scheme DoomCoderCompanion \
-configuration Release \
-destination 'generic/platform=iOS' \
-archivePath "$RUNNER_TEMP/DoomCoderCompanion.xcarchive" \
-allowProvisioningUpdates \
-authenticationKeyPath "${ASC_KEY_PATH}" \
-authenticationKeyID "${ASC_KEY_ID}" \
-authenticationKeyIssuerID "${ASC_ISSUER_ID}" \
DEVELOPMENT_TEAM="${APPLE_TEAM_ID}" \
archive
- name: Write ExportOptions.plist
run: |
cat > "$RUNNER_TEMP/ExportOptions.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>destination</key><string>upload</string>
<key>teamID</key><string>${{ secrets.APPLE_TEAM_ID }}</string>
<key>signingStyle</key><string>automatic</string>
<key>uploadSymbols</key><true/>
<key>uploadBitcode</key><false/>
<key>manageAppVersionAndBuildNumber</key><false/>
</dict>
</plist>
EOF
- name: Export & upload to TestFlight
env:
ASC_KEY_ID: ${{ steps.write-api-key.outputs.key_id }}
ASC_ISSUER_ID: ${{ steps.write-api-key.outputs.issuer_id }}
ASC_KEY_PATH: ${{ steps.write-api-key.outputs.key_path }}
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" \
-allowProvisioningUpdates \
-authenticationKeyPath "${ASC_KEY_PATH}" \
-authenticationKeyID "${ASC_KEY_ID}" \
-authenticationKeyIssuerID "${ASC_ISSUER_ID}"
- name: Clean up API key
if: always()
run: rm -f ~/.appstoreconnect/private_keys/AuthKey_*.p8