mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-02 00:06:23 +00:00
fix: sign iOS archives manually with an App Store provisioning profile
This commit is contained in:
@@ -65,8 +65,9 @@ jobs:
|
||||
CERT: ${{ secrets.IOS_DIST_CERT_BASE64 }}
|
||||
ASC_KEY: ${{ secrets.APP_STORE_CONNECT_PRIVATE_KEY }}
|
||||
TEAM: ${{ secrets.APPLE_TEAM_ID }}
|
||||
PROFILE: ${{ secrets.IOS_PROVISIONING_PROFILE_BASE64 }}
|
||||
run: |
|
||||
if [ -n "$CERT" ] && [ -n "$ASC_KEY" ] && [ -n "$TEAM" ]; then
|
||||
if [ -n "$CERT" ] && [ -n "$ASC_KEY" ] && [ -n "$TEAM" ] && [ -n "$PROFILE" ]; then
|
||||
echo "signed=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "signed=false" >> "$GITHUB_OUTPUT"
|
||||
@@ -132,43 +133,64 @@ jobs:
|
||||
ASC_KEY_BASE64: ${{ secrets.APP_STORE_CONNECT_PRIVATE_KEY }}
|
||||
ASC_KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }}
|
||||
run: |
|
||||
# Both xcodebuild -allowProvisioningUpdates and altool look the key up
|
||||
# by ID in this directory.
|
||||
# altool looks the key up by ID in this directory.
|
||||
mkdir -p ~/private_keys
|
||||
echo "$ASC_KEY_BASE64" | base64 -d > ~/private_keys/AuthKey_${ASC_KEY_ID}.p8
|
||||
chmod 600 ~/private_keys/AuthKey_${ASC_KEY_ID}.p8
|
||||
|
||||
- name: Install the provisioning profile
|
||||
if: steps.creds.outputs.signed == 'true'
|
||||
id: profile
|
||||
env:
|
||||
PROFILE_BASE64: ${{ secrets.IOS_PROVISIONING_PROFILE_BASE64 }}
|
||||
run: |
|
||||
PROFILE="$RUNNER_TEMP/profile.mobileprovision"
|
||||
echo "$PROFILE_BASE64" | base64 -d > "$PROFILE"
|
||||
|
||||
# A .mobileprovision is a CMS-signed plist, so it has to be decoded
|
||||
# before anything inside it can be read.
|
||||
PLIST=$(security cms -D -i "$PROFILE")
|
||||
UUID=$(echo "$PLIST" | plutil -extract UUID raw -)
|
||||
NAME=$(echo "$PLIST" | plutil -extract Name raw -)
|
||||
# application-identifier is "TEAMID.bundle.id" - deriving the bundle ID
|
||||
# from the profile keeps ExportOptions honest if app.config.js changes.
|
||||
APP_ID=$(echo "$PLIST" | plutil -extract Entitlements.application-identifier raw -)
|
||||
BUNDLE_ID="${APP_ID#*.}"
|
||||
EXPIRY=$(echo "$PLIST" | plutil -extract ExpirationDate raw -)
|
||||
|
||||
# Xcode moved the profile directory in 16; write both so this does not
|
||||
# silently stop working on a runner image bump.
|
||||
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
|
||||
mkdir -p ~/Library/Developer/Xcode/UserData/Provisioning\ Profiles
|
||||
cp "$PROFILE" ~/Library/MobileDevice/Provisioning\ Profiles/"$UUID".mobileprovision
|
||||
cp "$PROFILE" ~/Library/Developer/Xcode/UserData/Provisioning\ Profiles/"$UUID".mobileprovision
|
||||
|
||||
echo "uuid=$UUID" >> "$GITHUB_OUTPUT"
|
||||
echo "name=$NAME" >> "$GITHUB_OUTPUT"
|
||||
echo "bundle_id=$BUNDLE_ID" >> "$GITHUB_OUTPUT"
|
||||
echo "Profile '$NAME' ($UUID) for $BUNDLE_ID, expires $EXPIRY"
|
||||
|
||||
- name: Archive
|
||||
if: steps.creds.outputs.signed == 'true'
|
||||
env:
|
||||
ASC_KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }}
|
||||
ASC_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
|
||||
TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
run: |
|
||||
# -allowProvisioningUpdates lets Xcode create/refresh the App Store
|
||||
# provisioning profile through the API key, so no .mobileprovision has
|
||||
# to be stored as a secret.
|
||||
#
|
||||
# CODE_SIGN_IDENTITY has to be forced: the Expo iOS template hardcodes
|
||||
# "Apple Development", and automatic signing honours it even when
|
||||
# archiving Release. Xcode then asks Apple for a *development* profile,
|
||||
# which needs a registered device, and fails with "Your team has no
|
||||
# devices from which to generate a provisioning profile" - misleading,
|
||||
# because a TestFlight build never needs one.
|
||||
# Manual signing, deliberately. Automatic signing always signs the
|
||||
# archive with a *development* identity and relies on -exportArchive to
|
||||
# re-sign it for distribution afterwards - which needs a development
|
||||
# certificate and a registered device that a CI-only TestFlight
|
||||
# pipeline has no reason to own. Naming the App Store profile up front
|
||||
# skips that dance entirely.
|
||||
xcodebuild archive \
|
||||
-workspace "${{ steps.xcode.outputs.workspace }}" \
|
||||
-scheme "${{ steps.xcode.outputs.scheme }}" \
|
||||
-configuration Release \
|
||||
-destination 'generic/platform=iOS' \
|
||||
-archivePath "$RUNNER_TEMP/Bulwark.xcarchive" \
|
||||
-allowProvisioningUpdates \
|
||||
-authenticationKeyPath ~/private_keys/AuthKey_${ASC_KEY_ID}.p8 \
|
||||
-authenticationKeyID "$ASC_KEY_ID" \
|
||||
-authenticationKeyIssuerID "$ASC_ISSUER_ID" \
|
||||
CODE_SIGN_STYLE=Automatic \
|
||||
CODE_SIGN_STYLE=Manual \
|
||||
DEVELOPMENT_TEAM="$TEAM_ID" \
|
||||
CODE_SIGN_IDENTITY="Apple Distribution" \
|
||||
PROVISIONING_PROFILE_SPECIFIER=""
|
||||
PROVISIONING_PROFILE_SPECIFIER="${{ steps.profile.outputs.uuid }}"
|
||||
|
||||
# Signing failures report a symptom ("no profiles found") rather than the
|
||||
# setting that caused it, and a re-run costs ~30 minutes. Dump the
|
||||
@@ -189,9 +211,9 @@ jobs:
|
||||
if: steps.creds.outputs.signed == 'true'
|
||||
id: export
|
||||
env:
|
||||
ASC_KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }}
|
||||
ASC_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
|
||||
TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
PROFILE_NAME: ${{ steps.profile.outputs.name }}
|
||||
BUNDLE_ID: ${{ steps.profile.outputs.bundle_id }}
|
||||
run: |
|
||||
cat > "$RUNNER_TEMP/ExportOptions.plist" <<PLIST
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
@@ -200,7 +222,12 @@ jobs:
|
||||
<dict>
|
||||
<key>method</key><string>app-store</string>
|
||||
<key>teamID</key><string>${TEAM_ID}</string>
|
||||
<key>signingStyle</key><string>automatic</string>
|
||||
<key>signingStyle</key><string>manual</string>
|
||||
<key>signingCertificate</key><string>Apple Distribution</string>
|
||||
<key>provisioningProfiles</key>
|
||||
<dict>
|
||||
<key>${BUNDLE_ID}</key><string>${PROFILE_NAME}</string>
|
||||
</dict>
|
||||
<key>uploadSymbols</key><true/>
|
||||
<key>destination</key><string>export</string>
|
||||
</dict>
|
||||
@@ -210,11 +237,7 @@ jobs:
|
||||
xcodebuild -exportArchive \
|
||||
-archivePath "$RUNNER_TEMP/Bulwark.xcarchive" \
|
||||
-exportPath "$RUNNER_TEMP/export" \
|
||||
-exportOptionsPlist "$RUNNER_TEMP/ExportOptions.plist" \
|
||||
-allowProvisioningUpdates \
|
||||
-authenticationKeyPath ~/private_keys/AuthKey_${ASC_KEY_ID}.p8 \
|
||||
-authenticationKeyID "$ASC_KEY_ID" \
|
||||
-authenticationKeyIssuerID "$ASC_ISSUER_ID"
|
||||
-exportOptionsPlist "$RUNNER_TEMP/ExportOptions.plist"
|
||||
|
||||
VERSION=$(cat VERSION | tr -d '[:space:]')
|
||||
SRC=$(ls "$RUNNER_TEMP"/export/*.ipa | head -1)
|
||||
|
||||
+19
-3
@@ -58,9 +58,7 @@ App Store Connect → Users and Access → Integrations → App Store Connect AP
|
||||
**+**, with the **App Manager** role. Download the `.p8` (**one download only**)
|
||||
and note the **Key ID** and the **Issuer ID** shown above the key list.
|
||||
|
||||
This one key does two jobs: it lets `xcodebuild -allowProvisioningUpdates` create
|
||||
and refresh the provisioning profile, which is why no `.mobileprovision` has to
|
||||
be stored anywhere, and it authenticates the TestFlight upload.
|
||||
The key authenticates the TestFlight upload.
|
||||
|
||||
### 4. App ID, then app record
|
||||
|
||||
@@ -80,6 +78,23 @@ the entire App Store.
|
||||
The TestFlight upload is rejected if the app record does not exist - and that
|
||||
failure lands at the very end of a ~30 minute build.
|
||||
|
||||
### 5. App Store provisioning profile
|
||||
|
||||
developer.apple.com → Profiles → **+** → Distribution → **App Store Connect**.
|
||||
Pick the App ID from step 4 and the distribution certificate from step 2, name it
|
||||
`Bulwark Mobile App Store`, and download the `.mobileprovision`.
|
||||
|
||||
The build signs **manually** and this profile is not optional. Automatic signing
|
||||
would be less to maintain, but `xcodebuild archive` under automatic signing always
|
||||
signs with a *development* identity and leans on `-exportArchive` to re-sign for
|
||||
distribution afterwards. That needs a development certificate and at least one
|
||||
registered device - neither of which a CI-only TestFlight pipeline has any reason
|
||||
to own, and Apple will not issue a development profile without a device. Naming
|
||||
the App Store profile up front avoids the whole problem.
|
||||
|
||||
The profile expires when the certificate does (2027-08-01). Regenerate it here and
|
||||
update the secret when that happens.
|
||||
|
||||
## Repository secrets
|
||||
|
||||
Settings → Secrets and variables → Actions:
|
||||
@@ -92,6 +107,7 @@ Settings → Secrets and variables → Actions:
|
||||
| `APP_STORE_CONNECT_KEY_ID` | Key ID from step 3 |
|
||||
| `APP_STORE_CONNECT_ISSUER_ID` | Issuer ID from step 3 |
|
||||
| `APP_STORE_CONNECT_PRIVATE_KEY` | The `.p8` from step 3, base64-encoded |
|
||||
| `IOS_PROVISIONING_PROFILE_BASE64` | The `.mobileprovision` from step 5, base64-encoded |
|
||||
|
||||
To base64-encode a file:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user