From 5cb33b2858f0175ccc687fc3fa45491a63b63200 Mon Sep 17 00:00:00 2001 From: Maurice Date: Thu, 5 Nov 2026 21:27:01 +0100 Subject: [PATCH] ci(ios): sign the release with fastlane so TestFlight uploads from a device-less runner --- .github/workflows/native-release.yml | 50 ++++--------- client/fastlane/Fastfile | 78 ++++++++++++++++++++ client/ios/App/App.xcodeproj/project.pbxproj | 2 +- 3 files changed, 94 insertions(+), 36 deletions(-) create mode 100644 client/fastlane/Fastfile diff --git a/.github/workflows/native-release.yml b/.github/workflows/native-release.yml index 7bd28395f..d32ce5967 100644 --- a/.github/workflows/native-release.yml +++ b/.github/workflows/native-release.yml @@ -151,43 +151,23 @@ jobs: ASC_KEY_P8_B64: ${{ secrets.ASC_KEY_P8_B64 }} run: printf '%s' "$ASC_KEY_P8_B64" | base64 -d > "$RUNNER_TEMP/AuthKey.p8" - - name: Archive - working-directory: client/ios/App + # Signs for distribution and uploads to TestFlight. Automatic signing cannot + # run on a device-less CI runner (it signs for development and then needs a + # development profile, which needs a registered device), so client/fastlane/Fastfile + # signs manually: it creates or reuses the distribution certificate and the App + # Store profile through the API key, so neither has to be stored as a secret. + - name: Build and upload to TestFlight + working-directory: client env: + ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }} + ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }} + ASC_KEY_PATH: ${{ runner.temp }}/AuthKey.p8 APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} - ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }} - ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }} - run: >- - xcodebuild archive - -project App.xcodeproj - -scheme App - -configuration Release - -destination 'generic/platform=iOS' - -archivePath "$RUNNER_TEMP/TREK.xcarchive" - MARKETING_VERSION=${{ steps.version.outputs.version }} - CURRENT_PROJECT_VERSION=${{ steps.version.outputs.build }} - DEVELOPMENT_TEAM="$APPLE_TEAM_ID" - -allowProvisioningUpdates - -authenticationKeyPath "$RUNNER_TEMP/AuthKey.p8" - -authenticationKeyID "$ASC_KEY_ID" - -authenticationKeyIssuerID "$ASC_ISSUER_ID" - - # ExportOptions.plist says destination=upload, so this step sends the build - # to App Store Connect, where it shows up in TestFlight after processing. - - name: Export and upload to App Store Connect - working-directory: client/ios/App - env: - ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }} - ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }} - run: >- - xcodebuild -exportArchive - -archivePath "$RUNNER_TEMP/TREK.xcarchive" - -exportOptionsPlist ../ExportOptions.plist - -exportPath "$RUNNER_TEMP/export" - -allowProvisioningUpdates - -authenticationKeyPath "$RUNNER_TEMP/AuthKey.p8" - -authenticationKeyID "$ASC_KEY_ID" - -authenticationKeyIssuerID "$ASC_ISSUER_ID" + MARKETING_VERSION: ${{ steps.version.outputs.version }} + CURRENT_PROJECT_VERSION: ${{ steps.version.outputs.build }} + FASTLANE_SKIP_UPDATE_CHECK: '1' + FASTLANE_HIDE_CHANGELOG: '1' + run: fastlane ios beta - name: Remove the API key if: always() diff --git a/client/fastlane/Fastfile b/client/fastlane/Fastfile new file mode 100644 index 000000000..142b453a7 --- /dev/null +++ b/client/fastlane/Fastfile @@ -0,0 +1,78 @@ +# iOS release signing and TestFlight upload, driven by .github/workflows/native-release.yml. +# +# Automatic signing cannot work on a CI runner with no registered devices: it signs +# the archive for development and then needs a development provisioning profile, which +# requires a device. So this signs manually for distribution instead. The distribution +# certificate and the App Store provisioning profile are created (or reused) through the +# App Store Connect API key, which means no certificate or profile has to be stored as a +# secret: the key the workflow already holds is enough. +# +# The lane expects these in the environment (the workflow sets them): +# ASC_KEY_ID, ASC_ISSUER_ID, ASC_KEY_PATH — the App Store Connect API key +# APPLE_TEAM_ID — the developer team +# MARKETING_VERSION, CURRENT_PROJECT_VERSION — the version and build number +# RUNNER_TEMP — a writable scratch directory + +default_platform(:ios) + +APP_IDENTIFIER = "com.liketrek.trek" +PROJECT = "ios/App/App.xcodeproj" + +platform :ios do + desc "Build the signed Release app and upload it to TestFlight (internal testing)." + lane :beta do + key = app_store_connect_api_key( + key_id: ENV.fetch("ASC_KEY_ID"), + issuer_id: ENV.fetch("ASC_ISSUER_ID"), + key_filepath: ENV.fetch("ASC_KEY_PATH"), + in_house: false, + ) + + # A throwaway keychain, so the certificate's private key lives only for this run. + setup_ci + + # Distribution certificate into the keychain, and an App Store profile that trusts it. + get_certificates(api_key: key, development: false) + get_provisioning_profile( + api_key: key, + app_identifier: APP_IDENTIFIER, + development: false, + ) + + # Pin the project to that profile for this build; automatic signing stays the default + # a developer sees when they open the project in Xcode. + update_code_signing_settings( + path: PROJECT, + use_automatic_signing: false, + team_id: ENV.fetch("APPLE_TEAM_ID"), + code_sign_identity: "Apple Distribution", + bundle_identifier: APP_IDENTIFIER, + profile_name: lane_context[SharedValues::SIGH_NAME], + targets: ["App"], + ) + + build_app( + project: PROJECT, + scheme: "App", + configuration: "Release", + export_method: "app-store", + output_directory: File.join(ENV.fetch("RUNNER_TEMP"), "export"), + xcargs: [ + "MARKETING_VERSION=#{ENV.fetch('MARKETING_VERSION')}", + "CURRENT_PROJECT_VERSION=#{ENV.fetch('CURRENT_PROJECT_VERSION')}", + ].join(" "), + export_options: { + signingStyle: "manual", + provisioningProfiles: { APP_IDENTIFIER => lane_context[SharedValues::SIGH_NAME] }, + }, + ) + + # Upload to App Store Connect; it shows up in TestFlight after Apple finishes processing. + # Internal testing needs no App Review, so nothing here submits the app for review. + upload_to_testflight( + api_key: key, + skip_waiting_for_build_processing: true, + skip_submission: true, + ) + end +end diff --git a/client/ios/App/App.xcodeproj/project.pbxproj b/client/ios/App/App.xcodeproj/project.pbxproj index 9041f2337..d2dc88309 100644 --- a/client/ios/App/App.xcodeproj/project.pbxproj +++ b/client/ios/App/App.xcodeproj/project.pbxproj @@ -291,7 +291,7 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - CODE_SIGN_IDENTITY = "Apple Distribution"; + CODE_SIGN_IDENTITY = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO;