App Store Connect develop by @ethicnology #4
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: Upload App Store Connect | |
| run-name: App Store Connect ${{ github.ref_name }} by @${{ github.actor }} | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| build_number: | |
| description: Optional CFBundleVersion override (defaults to pubspec build + workflow run number) | |
| required: false | |
| type: string | |
| # Never cancel an upload already in progress, and serialize build-number use. | |
| concurrency: | |
| group: upload-app-store-connect | |
| cancel-in-progress: false | |
| jobs: | |
| release: | |
| # macos-15 is the stable arm64 image. Apple requires Xcode 26 / iOS 26 SDK | |
| # for uploads since 2026-04-28; the image's default Xcode is still 16.4. | |
| runs-on: macos-15 | |
| timeout-minutes: 120 | |
| environment: app-store-connect-upload | |
| permissions: | |
| contents: read | |
| env: | |
| RUST_VERSION: 1.95.0 | |
| BDK_RUST_VERSION: 1.85.1 | |
| RUSTUP_TOOLCHAIN: 1.95.0 | |
| RUSTUP_AUTO_INSTALL: 0 | |
| CARGO_NET_GIT_FETCH_WITH_CLI: true | |
| CARGO_INCREMENTAL: 0 | |
| steps: | |
| - name: Checkout selected branch | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - name: Validate App Store Connect configuration | |
| env: | |
| APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }} | |
| APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }} | |
| APP_STORE_CONNECT_API_PRIVATE_KEY: ${{ secrets.APP_STORE_CONNECT_API_PRIVATE_KEY }} | |
| IOS_DISTRIBUTION_CERTIFICATE_BASE64: ${{ secrets.IOS_DISTRIBUTION_CERTIFICATE_BASE64 }} | |
| IOS_DISTRIBUTION_CERTIFICATE_PASSWORD: ${{ secrets.IOS_DISTRIBUTION_CERTIFICATE_PASSWORD }} | |
| IOS_APP_STORE_PROVISIONING_PROFILE_BASE64: ${{ secrets.IOS_APP_STORE_PROVISIONING_PROFILE_BASE64 }} | |
| run: | | |
| missing=0 | |
| for name in \ | |
| APP_STORE_CONNECT_API_KEY_ID \ | |
| APP_STORE_CONNECT_ISSUER_ID \ | |
| APP_STORE_CONNECT_API_PRIVATE_KEY \ | |
| IOS_DISTRIBUTION_CERTIFICATE_BASE64 \ | |
| IOS_DISTRIBUTION_CERTIFICATE_PASSWORD \ | |
| IOS_APP_STORE_PROVISIONING_PROFILE_BASE64; do | |
| if [ -z "${!name}" ]; then | |
| echo "::error::$name is not configured in the app-store-connect-upload environment." | |
| missing=1 | |
| fi | |
| done | |
| exit "$missing" | |
| - name: Resolve release metadata | |
| id: metadata | |
| env: | |
| BUILD_NUMBER_INPUT: ${{ inputs.build_number }} | |
| run: | | |
| version_line="$(awk '/^version:/ { print $2; exit }' pubspec.yaml)" | |
| base_build="${version_line##*+}" | |
| if ! [[ "$base_build" =~ ^[1-9][0-9]*$ ]]; then | |
| echo "::error::pubspec.yaml must contain a positive integer build number." | |
| exit 1 | |
| fi | |
| if [ -n "$BUILD_NUMBER_INPUT" ]; then | |
| if ! [[ "$BUILD_NUMBER_INPUT" =~ ^[1-9][0-9]*$ ]]; then | |
| echo "::error::build_number must be a positive integer." | |
| exit 1 | |
| fi | |
| build_number="$BUILD_NUMBER_INPUT" | |
| else | |
| build_number=$((base_build + GITHUB_RUN_NUMBER)) | |
| fi | |
| echo "build_number=$build_number" >> "$GITHUB_OUTPUT" | |
| echo "sha_short=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT" | |
| echo "BUILD_NUMBER=$build_number" >> "$GITHUB_ENV" | |
| - name: Select Xcode 26.3 | |
| run: | | |
| sudo xcode-select --switch /Applications/Xcode_26.3.app/Contents/Developer | |
| xcodebuild -version | |
| test "$(xcodebuild -version | awk 'NR == 1 { print $2 }')" = "26.3" | |
| test "$(xcrun --sdk iphoneos --show-sdk-version | cut -d. -f1)" = "26" | |
| - name: Cache Flutter SDK | |
| uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 | |
| with: | |
| path: ~/fvm/versions | |
| key: fvm-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.fvmrc') }} | |
| - name: Cache pub dependencies | |
| uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 | |
| with: | |
| path: ~/.pub-cache | |
| key: pub-${{ runner.os }}-${{ hashFiles('pubspec.lock') }} | |
| - name: Cache Cargo sources | |
| uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 | |
| with: | |
| path: | | |
| ~/.cargo/registry | |
| ~/.cargo/git | |
| key: cargo-${{ runner.os }}-${{ hashFiles('pubspec.lock') }} | |
| - name: Cache CocoaPods downloads | |
| uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 | |
| with: | |
| path: ~/Library/Caches/CocoaPods | |
| key: cocoapods-${{ runner.os }}-${{ hashFiles('ios/Podfile.lock') }} | |
| # Install the pinned native FVM binary without executing the mutable | |
| # fvm.app installer script on a runner that later receives signing keys. | |
| - name: Install FVM 4.1.2 | |
| run: | | |
| archive="$RUNNER_TEMP/fvm-4.1.2-macos-arm64.tar.gz" | |
| curl -fsSL \ | |
| https://github.com/leoafarias/fvm/releases/download/4.1.2/fvm-4.1.2-macos-arm64.tar.gz \ | |
| -o "$archive" | |
| echo "0b2a146986c51f06331f135f0bdf2a202eb57f55d7edd420c9078e8520e4c033 $archive" \ | |
| | shasum -a 256 -c - | |
| mkdir -p "$HOME/fvm/bin" | |
| tar -xzf "$archive" -C "$HOME/fvm/bin" --strip-components=1 fvm/fvm | |
| chmod +x "$HOME/fvm/bin/fvm" | |
| test -x "$HOME/fvm/bin/fvm" | |
| echo "$HOME/fvm/bin" >> "$GITHUB_PATH" | |
| - name: Install pinned Rust toolchains | |
| run: | | |
| rustup toolchain install "$RUST_VERSION" --profile minimal | |
| rustup component add --toolchain "$RUST_VERSION" clippy rustfmt | |
| rustup target add --toolchain "$RUST_VERSION" aarch64-apple-ios | |
| rustup toolchain install "$BDK_RUST_VERSION" --profile minimal | |
| rustup component add --toolchain "$BDK_RUST_VERSION" clippy rustfmt | |
| rustup target add --toolchain "$BDK_RUST_VERSION" aarch64-apple-ios | |
| # Cargokit requests `rustup run stable` directly, which otherwise | |
| # bypasses RUSTUP_TOOLCHAIN and floats every six weeks. bdk_dart's | |
| # explicit 1.85.1 request passes through unchanged. The wrapper | |
| # rewrites any bare `stable` argument, wherever it sits in argv. | |
| rustup_path="$(command -v rustup)" | |
| mv "$rustup_path" "$rustup_path.real" | |
| cat > "$rustup_path" <<EOF | |
| #!/bin/bash | |
| args=() | |
| for arg in "\$@"; do | |
| [ "\$arg" = "stable" ] && arg="$RUST_VERSION" | |
| args+=("\$arg") | |
| done | |
| exec "\$(dirname "\$0")/rustup.real" "\${args[@]}" | |
| EOF | |
| chmod +x "$rustup_path" | |
| rustup run stable rustc --version | grep -F "rustc $RUST_VERSION" | |
| rustup run "$BDK_RUST_VERSION" rustc --version | grep -F "rustc $BDK_RUST_VERSION" | |
| - name: Install Flutter and generate sources | |
| run: | | |
| make fvm-check | |
| make deps | |
| fvm flutter precache --ios | |
| make build-runner | |
| make translations | |
| if [ -n "$(git status --porcelain)" ]; then | |
| git status --porcelain | |
| echo "::error::Code generation changed the checkout; commit the regenerated files." | |
| exit 1 | |
| fi | |
| - name: Install locked CocoaPods dependencies | |
| working-directory: ios | |
| run: | | |
| test -f Podfile.lock | |
| # The image's CocoaPods floats (1.16 -> 1.17 changed podspec checksums, | |
| # which `--deployment` rejects). Pin to whatever generated Podfile.lock | |
| # so the lockfile stays the single source of truth across image bumps. | |
| locked_version="$(awk '/^COCOAPODS:/ { print $2; exit }' Podfile.lock)" | |
| test -n "$locked_version" | |
| if [ "$(pod --version)" != "$locked_version" ]; then | |
| sudo gem install cocoapods -v "$locked_version" --no-document | |
| fi | |
| test "$(pod "_${locked_version}_" --version)" = "$locked_version" | |
| pod "_${locked_version}_" install --deployment | |
| - name: Import App Store distribution certificate | |
| uses: apple-actions/import-codesign-certs@5142e029c445c10ffc7149d172e540235a065466 # v7.0.0 | |
| with: | |
| p12-file-base64: ${{ secrets.IOS_DISTRIBUTION_CERTIFICATE_BASE64 }} | |
| p12-password: ${{ secrets.IOS_DISTRIBUTION_CERTIFICATE_PASSWORD }} | |
| - name: Install App Store provisioning profile | |
| env: | |
| PROFILE_BASE64: ${{ secrets.IOS_APP_STORE_PROVISIONING_PROFILE_BASE64 }} | |
| run: | | |
| encoded_profile="$RUNNER_TEMP/profile.b64" | |
| decoded_profile="$RUNNER_TEMP/profile.mobileprovision" | |
| profile_plist="$RUNNER_TEMP/profile.plist" | |
| printf '%s' "$PROFILE_BASE64" > "$encoded_profile" | |
| base64 -D -i "$encoded_profile" -o "$decoded_profile" | |
| security cms -D -i "$decoded_profile" > "$profile_plist" | |
| profile_uuid="$(/usr/libexec/PlistBuddy -c 'Print :UUID' "$profile_plist")" | |
| app_identifier="$(/usr/libexec/PlistBuddy -c 'Print :Entitlements:application-identifier' "$profile_plist")" | |
| get_task_allow="$(/usr/libexec/PlistBuddy -c 'Print :Entitlements:get-task-allow' "$profile_plist")" | |
| if [ "$app_identifier" != "BX99T32YGS.com.bullbitcoin.app" ] || [ "$get_task_allow" != "false" ]; then | |
| echo "::error::The provisioning profile is not an App Store profile for com.bullbitcoin.app." | |
| exit 1 | |
| fi | |
| if /usr/libexec/PlistBuddy -c 'Print :ProvisionedDevices' "$profile_plist" >/dev/null 2>&1; then | |
| echo "::error::Ad hoc and development provisioning profiles are not accepted." | |
| exit 1 | |
| fi | |
| if /usr/libexec/PlistBuddy -c 'Print :ProvisionsAllDevices' "$profile_plist" >/dev/null 2>&1; then | |
| echo "::error::Enterprise provisioning profiles are not accepted." | |
| exit 1 | |
| fi | |
| # Xcode 16 moved the provisioning profile directory; install to both. | |
| legacy_dir="$HOME/Library/MobileDevice/Provisioning Profiles" | |
| xcode_dir="$HOME/Library/Developer/Xcode/UserData/Provisioning Profiles" | |
| mkdir -p "$legacy_dir" "$xcode_dir" | |
| install -m 600 "$decoded_profile" "$legacy_dir/$profile_uuid.mobileprovision" | |
| install -m 600 "$decoded_profile" "$xcode_dir/$profile_uuid.mobileprovision" | |
| echo "IOS_PROVISIONING_PROFILE_UUID=$profile_uuid" >> "$GITHUB_ENV" | |
| - name: Configure manual signing | |
| run: | | |
| # The Runner target's Release configuration (97C14707...) has no | |
| # signing settings of its own: it inherits automatic signing and the | |
| # project-level "iPhone Developer" identity. Point it at the | |
| # installed distribution certificate and profile instead. | |
| awk -v uuid="$IOS_PROVISIONING_PROFILE_UUID" ' | |
| index($0, "97C147071CF9000F007C117D /* Release */ = {") { in_release = 1 } | |
| in_release && $0 == "\t\t\t};" && !inserted { | |
| print "\t\t\t\tCODE_SIGN_IDENTITY = \"Apple Distribution\";" | |
| print "\t\t\t\tCODE_SIGN_STYLE = Manual;" | |
| print "\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"" uuid "\";" | |
| inserted = 1 | |
| } | |
| { print } | |
| END { if (!inserted) exit 1 } | |
| ' ios/Runner.xcodeproj/project.pbxproj > "$RUNNER_TEMP/project.pbxproj" \ | |
| && mv "$RUNNER_TEMP/project.pbxproj" ios/Runner.xcodeproj/project.pbxproj | |
| grep -F "PROVISIONING_PROFILE_SPECIFIER = \"$IOS_PROVISIONING_PROFILE_UUID\";" \ | |
| ios/Runner.xcodeproj/project.pbxproj | |
| export_options="$RUNNER_TEMP/ExportOptions.plist" | |
| cat > "$export_options" <<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>signingStyle</key> | |
| <string>manual</string> | |
| <key>signingCertificate</key> | |
| <string>Apple Distribution</string> | |
| <key>teamID</key> | |
| <string>BX99T32YGS</string> | |
| <key>manageAppVersionAndBuildNumber</key> | |
| <false/> | |
| <key>provisioningProfiles</key> | |
| <dict> | |
| <key>com.bullbitcoin.app</key> | |
| <string>$IOS_PROVISIONING_PROFILE_UUID</string> | |
| </dict> | |
| </dict> | |
| </plist> | |
| EOF | |
| plutil -lint "$export_options" | |
| echo "EXPORT_OPTIONS_PLIST=$export_options" >> "$GITHUB_ENV" | |
| - name: Build signed IPA | |
| run: make ios-release BUILD_NUMBER="$BUILD_NUMBER" EXPORT_OPTIONS_PLIST="$EXPORT_OPTIONS_PLIST" | |
| - name: Verify signed IPA | |
| id: ipa | |
| run: | | |
| ipa_files=(build/ios/ipa/*.ipa) | |
| if [ "${#ipa_files[@]}" -ne 1 ] || [ ! -f "${ipa_files[0]}" ]; then | |
| echo "::error::Expected exactly one IPA in build/ios/ipa." | |
| exit 1 | |
| fi | |
| ipa_path="${ipa_files[0]}" | |
| verify_dir="$RUNNER_TEMP/verify-ipa" | |
| mkdir -p "$verify_dir" | |
| unzip -q "$ipa_path" -d "$verify_dir" | |
| apps=("$verify_dir"/Payload/*.app) | |
| if [ "${#apps[@]}" -ne 1 ] || [ ! -d "${apps[0]}" ]; then | |
| echo "::error::Expected exactly one application in the IPA payload." | |
| exit 1 | |
| fi | |
| codesign --verify --deep --strict --verbose=2 "${apps[0]}" | |
| actual_build="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleVersion' "${apps[0]}/Info.plist")" | |
| actual_bundle="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "${apps[0]}/Info.plist")" | |
| test "$actual_build" = "$BUILD_NUMBER" | |
| test "$actual_bundle" = "com.bullbitcoin.app" | |
| dsyms=(build/ios/archive/*.xcarchive/dSYMs) | |
| if [ "${#dsyms[@]}" -ne 1 ] || [ ! -d "${dsyms[0]}" ]; then | |
| echo "::error::Expected one dSYM directory in the Xcode archive." | |
| exit 1 | |
| fi | |
| echo "path=$ipa_path" >> "$GITHUB_OUTPUT" | |
| - name: Record artifact hash | |
| env: | |
| IPA_PATH: ${{ steps.ipa.outputs.path }} | |
| run: | | |
| echo "### App Store Connect build" >> "$GITHUB_STEP_SUMMARY" | |
| echo "- Ref: \`$GITHUB_REF_NAME\`" >> "$GITHUB_STEP_SUMMARY" | |
| echo "- Commit: \`$GITHUB_SHA\`" >> "$GITHUB_STEP_SUMMARY" | |
| echo "- Build number: \`$BUILD_NUMBER\`" >> "$GITHUB_STEP_SUMMARY" | |
| echo "- Xcode: \`$(xcodebuild -version | tr '\n' ' ')\`" >> "$GITHUB_STEP_SUMMARY" | |
| echo '```' >> "$GITHUB_STEP_SUMMARY" | |
| shasum -a 256 "$IPA_PATH" >> "$GITHUB_STEP_SUMMARY" | |
| echo '```' >> "$GITHUB_STEP_SUMMARY" | |
| - name: Preserve IPA and dSYMs | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: BULL-app-store-connect-${{ steps.metadata.outputs.sha_short }}-${{ steps.metadata.outputs.build_number }} | |
| path: | | |
| build/ios/ipa/*.ipa | |
| build/ios/archive/*.xcarchive/dSYMs | |
| if-no-files-found: error | |
| retention-days: 30 | |
| # This Developer API key only uploads the processed build. TestFlight | |
| # distribution and App Store submission remain manual Apple-side gates. | |
| - name: Upload build to App Store Connect | |
| uses: apple-actions/upload-testflight-build@5e75ff58276689011512ba87a381d93dc67dbcf8 # v5.3.0 | |
| with: | |
| app-path: ${{ steps.ipa.outputs.path }} | |
| issuer-id: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }} | |
| api-key-id: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }} | |
| api-private-key: ${{ secrets.APP_STORE_CONNECT_API_PRIVATE_KEY }} | |
| backend: appstore-api | |
| wait-for-processing: true | |
| - name: Remove provisioning profile | |
| if: always() | |
| run: | | |
| if [ -n "$IOS_PROVISIONING_PROFILE_UUID" ]; then | |
| rm -f "$HOME/Library/MobileDevice/Provisioning Profiles/$IOS_PROVISIONING_PROFILE_UUID.mobileprovision" | |
| rm -f "$HOME/Library/Developer/Xcode/UserData/Provisioning Profiles/$IOS_PROVISIONING_PROFILE_UUID.mobileprovision" | |
| fi |