fix(ci): normalize .p8 via openssl + use step outputs to avoid key-ID… #6
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |