245 lines
10 KiB
YAML
245 lines
10 KiB
YAML
name: 'Flutter Build iOS'
|
|
description: 'Installs Apple signing certificates and provisioning profiles in an isolated keychain, builds an iOS release IPA, and optionally uploads to TestFlight and Shorebird.'
|
|
inputs:
|
|
working_directory:
|
|
description: 'Path to the Flutter app package directory (e.g., packages/hfw_flutter or .)'
|
|
required: false
|
|
default: '.'
|
|
export_options_plist:
|
|
description: 'Relative path inside working_directory to ExportOptions.plist'
|
|
required: false
|
|
default: 'ios/ExportOptions.plist'
|
|
build_args:
|
|
description: 'Extra arguments passed to flutter build ipa / ios (e.g., --dart-define=...)'
|
|
required: false
|
|
default: ''
|
|
disable_spm:
|
|
description: 'Disable Flutter Swift Package Manager before building'
|
|
required: false
|
|
default: 'true'
|
|
artifact_prefix:
|
|
description: 'Optional prefix for copied versioned IPA in output_dir (e.g., hfw-ios -> hfw-ios-v1.0.0.ipa)'
|
|
required: false
|
|
default: ''
|
|
output_dir:
|
|
description: 'Directory to copy versioned IPA into when artifact_prefix is set'
|
|
required: false
|
|
default: 'artifacts'
|
|
certificate_base64:
|
|
description: 'Base64-encoded Apple iOS distribution .p12 certificate'
|
|
required: false
|
|
default: ''
|
|
certificate_password:
|
|
description: 'Password for the Apple .p12 certificate'
|
|
required: false
|
|
default: ''
|
|
provisioning_profile_base64:
|
|
description: 'Base64-encoded .mobileprovision profile'
|
|
required: false
|
|
default: ''
|
|
asc_key_id:
|
|
description: 'App Store Connect API Key ID'
|
|
required: false
|
|
default: ''
|
|
asc_issuer_id:
|
|
description: 'App Store Connect API Issuer ID'
|
|
required: false
|
|
default: ''
|
|
asc_private_key:
|
|
description: 'App Store Connect API Private Key (.p8 content)'
|
|
required: false
|
|
default: ''
|
|
apple_id_username:
|
|
description: 'Fallback Apple ID email for altool upload'
|
|
required: false
|
|
default: ''
|
|
apple_id_password:
|
|
description: 'Fallback Apple ID app-specific password for altool upload'
|
|
required: false
|
|
default: ''
|
|
upload_to_testflight:
|
|
description: 'Whether to upload signed IPA to TestFlight when credentials are provided'
|
|
required: false
|
|
default: 'true'
|
|
shorebird_token:
|
|
description: 'Shorebird token for optional Shorebird iOS release'
|
|
required: false
|
|
default: ''
|
|
|
|
outputs:
|
|
version:
|
|
description: 'Resolved package version from pubspec.yaml'
|
|
value: ${{ steps.build.outputs.version }}
|
|
ipa_path:
|
|
description: 'Path to the built or packaged IPA file'
|
|
value: ${{ steps.build.outputs.ipa_path }}
|
|
signed:
|
|
description: 'Whether a signed IPA was exported by Xcode'
|
|
value: ${{ steps.build.outputs.signed }}
|
|
|
|
runs:
|
|
using: 'composite'
|
|
steps:
|
|
- name: Install Apple Certificate and Provisioning Profile
|
|
shell: bash
|
|
env:
|
|
IOS_CERT_BASE64: ${{ inputs.certificate_base64 }}
|
|
IOS_CERT_PWD: ${{ inputs.certificate_password }}
|
|
IOS_PROFILE_BASE64: ${{ inputs.provisioning_profile_base64 }}
|
|
run: |
|
|
if [ -n "$IOS_CERT_BASE64" ]; then
|
|
echo "Setting up temporary keychain for iOS signing..."
|
|
security delete-keychain signing_temp.keychain 2>/dev/null || true
|
|
rm -f "$HOME/Library/Keychains/signing_temp.keychain-db" || true
|
|
|
|
KEYCHAIN="$HOME/Library/Keychains/signing_temp.keychain-db"
|
|
KEYCHAIN_PWD="temp_password_123"
|
|
security create-keychain -p "$KEYCHAIN_PWD" "$KEYCHAIN" || true
|
|
security unlock-keychain -p "$KEYCHAIN_PWD" "$KEYCHAIN" || true
|
|
security set-keychain-settings -lut 21600 "$KEYCHAIN" || true
|
|
security list-keychains -d user -s "$KEYCHAIN" "$HOME/Library/Keychains/login.keychain-db" || true
|
|
security default-keychain -s "$HOME/Library/Keychains/login.keychain-db" || true
|
|
|
|
echo "$IOS_CERT_BASE64" | base64 --decode > /tmp/certificate.p12
|
|
security import "/tmp/certificate.p12" -k "$KEYCHAIN" -P "$IOS_CERT_PWD" -T /usr/bin/codesign -T /usr/bin/security -A || true
|
|
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PWD" "$KEYCHAIN" || true
|
|
|
|
if [ -n "$IOS_PROFILE_BASE64" ]; then
|
|
mkdir -p "$HOME/Library/MobileDevice/Provisioning Profiles" "$HOME/Library/Developer/Xcode/UserData/Provisioning Profiles"
|
|
echo "$IOS_PROFILE_BASE64" | base64 --decode > /tmp/profile.mobileprovision
|
|
PROFILE_UUID=$(security cms -D -i /tmp/profile.mobileprovision 2>/dev/null | plutil -extract UUID raw - 2>/dev/null || echo "profile")
|
|
cp /tmp/profile.mobileprovision "$HOME/Library/MobileDevice/Provisioning Profiles/${PROFILE_UUID}.mobileprovision"
|
|
cp /tmp/profile.mobileprovision "$HOME/Library/Developer/Xcode/UserData/Provisioning Profiles/${PROFILE_UUID}.mobileprovision"
|
|
fi
|
|
echo "iOS signing credentials installed successfully."
|
|
else
|
|
echo "Notice: certificate_base64 not configured."
|
|
fi
|
|
|
|
- name: Build iOS IPA
|
|
id: build
|
|
shell: bash
|
|
env:
|
|
WORK_DIR: ${{ inputs.working_directory }}
|
|
EXPORT_PLIST: ${{ inputs.export_options_plist }}
|
|
BUILD_ARGS: ${{ inputs.build_args }}
|
|
DISABLE_SPM: ${{ inputs.disable_spm }}
|
|
ARTIFACT_PREFIX: ${{ inputs.artifact_prefix }}
|
|
OUTPUT_DIR: ${{ inputs.output_dir }}
|
|
IOS_CERT_BASE64: ${{ inputs.certificate_base64 }}
|
|
ASC_KEY_ID: ${{ inputs.asc_key_id }}
|
|
ASC_ISSUER_ID: ${{ inputs.asc_issuer_id }}
|
|
ASC_KEY_CONTENT: ${{ inputs.asc_private_key }}
|
|
run: |
|
|
export PATH="/usr/bin:/bin:/usr/sbin:/sbin:$PATH"
|
|
ROOT_DIR="$(pwd)"
|
|
VERSION=$(grep '^version: ' "$WORK_DIR/pubspec.yaml" | head -n 1 | cut -d ' ' -f 2 | tr -d '\r')
|
|
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
|
|
|
if [ -n "$ASC_KEY_ID" ] && [ -n "$ASC_KEY_CONTENT" ]; then
|
|
mkdir -p "$HOME/.appstoreconnect/private_keys"
|
|
echo "$ASC_KEY_CONTENT" > "$HOME/.appstoreconnect/private_keys/AuthKey_${ASC_KEY_ID}.p8"
|
|
fi
|
|
|
|
cd "$WORK_DIR"
|
|
if [ "$DISABLE_SPM" = "true" ]; then
|
|
flutter config --no-enable-swift-package-manager
|
|
fi
|
|
|
|
if [ -f "$EXPORT_PLIST" ] && [ -n "$IOS_CERT_BASE64" ]; then
|
|
# shellcheck disable=SC2086
|
|
flutter build ipa --release --export-options-plist="$EXPORT_PLIST" $BUILD_ARGS || flutter build ios --release --no-codesign $BUILD_ARGS
|
|
else
|
|
# shellcheck disable=SC2086
|
|
flutter build ios --release --no-codesign $BUILD_ARGS
|
|
fi
|
|
cd "$ROOT_DIR"
|
|
|
|
SIGNED_IPA=$(ls "$WORK_DIR"/build/ios/ipa/*.ipa 2>/dev/null | head -n 1 || true)
|
|
if [ -n "$SIGNED_IPA" ]; then
|
|
echo "signed=true" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "signed=false" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
|
|
IPA_OUT="${SIGNED_IPA#./}"
|
|
if [ -n "$ARTIFACT_PREFIX" ]; then
|
|
mkdir -p "$OUTPUT_DIR"
|
|
TARGET_IPA="$OUTPUT_DIR/${ARTIFACT_PREFIX}-v${VERSION}.ipa"
|
|
if [ -n "$SIGNED_IPA" ]; then
|
|
cp "$SIGNED_IPA" "$TARGET_IPA"
|
|
elif [ -d "$WORK_DIR/build/ios/iphoneos/Runner.app" ]; then
|
|
rm -rf /tmp/Payload
|
|
mkdir -p /tmp/Payload
|
|
cp -r "$WORK_DIR/build/ios/iphoneos/Runner.app" /tmp/Payload/
|
|
(cd /tmp && zip -qr "$ROOT_DIR/$TARGET_IPA" Payload)
|
|
rm -rf /tmp/Payload
|
|
else
|
|
touch "$TARGET_IPA"
|
|
fi
|
|
IPA_OUT="$TARGET_IPA"
|
|
echo "Collected iOS artifact at $IPA_OUT"
|
|
fi
|
|
|
|
echo "ipa_path=$IPA_OUT" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Push to TestFlight via App Store Connect API
|
|
if: inputs.upload_to_testflight == 'true'
|
|
shell: bash
|
|
env:
|
|
WORK_DIR: ${{ inputs.working_directory }}
|
|
ASC_KEY_ID: ${{ inputs.asc_key_id }}
|
|
ASC_ISSUER_ID: ${{ inputs.asc_issuer_id }}
|
|
ASC_KEY_CONTENT: ${{ inputs.asc_private_key }}
|
|
AC_USERNAME: ${{ inputs.apple_id_username }}
|
|
AC_PASSWORD: ${{ inputs.apple_id_password }}
|
|
run: |
|
|
IPA_FILE=$(ls "$WORK_DIR"/build/ios/ipa/*.ipa 2>/dev/null | head -n 1 || true)
|
|
if [ -z "$IPA_FILE" ]; then
|
|
echo "Skipping TestFlight upload: no signed App Store IPA was exported in $WORK_DIR/build/ios/ipa/."
|
|
exit 0
|
|
fi
|
|
if [ -n "$ASC_KEY_ID" ] && [ -n "$ASC_ISSUER_ID" ] && [ -n "$ASC_KEY_CONTENT" ]; then
|
|
echo "Uploading signed IPA ($IPA_FILE) to TestFlight via App Store Connect API Key..."
|
|
mkdir -p "$HOME/.appstoreconnect/private_keys"
|
|
echo "$ASC_KEY_CONTENT" > "$HOME/.appstoreconnect/private_keys/AuthKey_${ASC_KEY_ID}.p8"
|
|
xcrun altool --upload-app \
|
|
-f "$IPA_FILE" \
|
|
--type ios \
|
|
--apiKey "$ASC_KEY_ID" \
|
|
--apiIssuer "$ASC_ISSUER_ID"
|
|
elif [ -n "$AC_USERNAME" ] && [ -n "$AC_PASSWORD" ]; then
|
|
echo "Uploading signed IPA ($IPA_FILE) to TestFlight via Apple ID App-Specific Password..."
|
|
xcrun altool --upload-app \
|
|
-f "$IPA_FILE" \
|
|
--type ios \
|
|
--u "$AC_USERNAME" \
|
|
--p "$AC_PASSWORD"
|
|
else
|
|
echo "Skipping TestFlight upload (App Store Connect credentials not configured)."
|
|
fi
|
|
|
|
- name: Release via Shorebird (Optional)
|
|
if: inputs.shorebird_token != ''
|
|
shell: bash
|
|
env:
|
|
WORK_DIR: ${{ inputs.working_directory }}
|
|
SHOREBIRD_TOKEN: ${{ inputs.shorebird_token }}
|
|
run: |
|
|
if [ -n "$SHOREBIRD_TOKEN" ]; then
|
|
ROOT_DIR="$(pwd)"
|
|
cd "$WORK_DIR"
|
|
flutter pub global activate shorebird_cli
|
|
shorebird release ios --artifact=ipa
|
|
cd "$ROOT_DIR"
|
|
fi
|
|
|
|
- name: Clean up keychain
|
|
if: always()
|
|
shell: bash
|
|
run: |
|
|
security list-keychains -d user -s "$HOME/Library/Keychains/login.keychain-db" || true
|
|
security default-keychain -s "$HOME/Library/Keychains/login.keychain-db" || true
|
|
security delete-keychain "$HOME/Library/Keychains/signing_temp.keychain-db" 2>/dev/null || true
|
|
rm -f "$HOME/Library/Keychains/signing_temp.keychain-db" /tmp/certificate.p12 /tmp/profile.mobileprovision || true
|