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