diff --git a/.github/workflows/build-mobile.yml b/.github/workflows/build-mobile.yml index 10dc88088f..09ed129299 100644 --- a/.github/workflows/build-mobile.yml +++ b/.github/workflows/build-mobile.yml @@ -26,21 +26,9 @@ on: required: true APP_STORE_CONNECT_API_KEY: required: true - IOS_CERTIFICATE_P12: + MATCH_PASSWORD: required: true - IOS_CERTIFICATE_PASSWORD: - required: true - IOS_PROVISIONING_PROFILE: - required: true - IOS_PROVISIONING_PROFILE_SHARE_EXTENSION: - required: true - IOS_PROVISIONING_PROFILE_WIDGET_EXTENSION: - required: true - IOS_DEVELOPMENT_PROVISIONING_PROFILE: - required: true - IOS_DEVELOPMENT_PROVISIONING_PROFILE_SHARE_EXTENSION: - required: true - IOS_DEVELOPMENT_PROVISIONING_PROFILE_WIDGET_EXTENSION: + MATCH_GIT_BASIC_AUTHORIZATION: required: true FASTLANE_TEAM_ID: required: true @@ -193,6 +181,21 @@ jobs: runs-on: macos-latest steps: + - name: Generate token for ios-certs repo + id: token + uses: actions/create-github-app-token@v2 + with: + app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} + private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} + owner: immich-app + repositories: immich,ios-certs + + - name: Set up match authorization + id: match-auth + run: | + # Create base64-encoded authorization for match + echo "base64_token=$(echo -n 'x-access-token:${{ steps.token.outputs.token }}' | base64)" >> $GITHUB_OUTPUT + - name: Checkout code uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 with: @@ -240,64 +243,26 @@ jobs: mkdir -p ~/.appstoreconnect/private_keys echo "$API_KEY_CONTENT" | base64 --decode > ~/.appstoreconnect/private_keys/AuthKey_${API_KEY_ID}.p8 - - name: Import Certificate and Provisioning Profiles + - name: Create keychain for match env: - IOS_CERTIFICATE_P12: ${{ secrets.IOS_CERTIFICATE_P12 }} - IOS_CERTIFICATE_PASSWORD: ${{ secrets.IOS_CERTIFICATE_PASSWORD }} - IOS_PROVISIONING_PROFILE: ${{ secrets.IOS_PROVISIONING_PROFILE }} - IOS_PROVISIONING_PROFILE_SHARE_EXTENSION: ${{ secrets.IOS_PROVISIONING_PROFILE_SHARE_EXTENSION }} - IOS_PROVISIONING_PROFILE_WIDGET_EXTENSION: ${{ secrets.IOS_PROVISIONING_PROFILE_WIDGET_EXTENSION }} - IOS_DEVELOPMENT_PROVISIONING_PROFILE: ${{ secrets.IOS_DEVELOPMENT_PROVISIONING_PROFILE }} - IOS_DEVELOPMENT_PROVISIONING_PROFILE_SHARE_EXTENSION: ${{ secrets.IOS_DEVELOPMENT_PROVISIONING_PROFILE_SHARE_EXTENSION }} - IOS_DEVELOPMENT_PROVISIONING_PROFILE_WIDGET_EXTENSION: ${{ secrets.IOS_DEVELOPMENT_PROVISIONING_PROFILE_WIDGET_EXTENSION }} - ENVIRONMENT: ${{ inputs.environment || 'development' }} - working-directory: ./mobile/ios + KEYCHAIN_PASSWORD: ${{ github.run_id }} run: | - # Decode certificate - echo "$IOS_CERTIFICATE_P12" | base64 --decode > certificate.p12 - - # Decode provisioning profiles based on environment - if [[ "$ENVIRONMENT" == "development" ]]; then - echo "$IOS_DEVELOPMENT_PROVISIONING_PROFILE" | base64 --decode > profile_dev.mobileprovision - echo "$IOS_DEVELOPMENT_PROVISIONING_PROFILE_SHARE_EXTENSION" | base64 --decode > profile_dev_share.mobileprovision - echo "$IOS_DEVELOPMENT_PROVISIONING_PROFILE_WIDGET_EXTENSION" | base64 --decode > profile_dev_widget.mobileprovision - ls -lh profile_dev*.mobileprovision - else - echo "$IOS_PROVISIONING_PROFILE" | base64 --decode > profile.mobileprovision - echo "$IOS_PROVISIONING_PROFILE_SHARE_EXTENSION" | base64 --decode > profile_share.mobileprovision - echo "$IOS_PROVISIONING_PROFILE_WIDGET_EXTENSION" | base64 --decode > profile_widget.mobileprovision - ls -lh profile*.mobileprovision - fi - - - name: Create keychain and import certificate - env: - KEYCHAIN_PASSWORD: ${{ secrets.IOS_CERTIFICATE_PASSWORD }} - CERTIFICATE_PASSWORD: ${{ secrets.IOS_CERTIFICATE_PASSWORD }} - working-directory: ./mobile/ios - run: | - # Create keychain + # Create a temporary keychain for CI security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain security default-keychain -s build.keychain security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain security set-keychain-settings -t 3600 -u build.keychain - # Import certificate - security import certificate.p12 -k build.keychain -P "$CERTIFICATE_PASSWORD" -T /usr/bin/codesign -T /usr/bin/security - security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" build.keychain - - # Verify certificate was imported - security find-identity -v -p codesigning build.keychain - - name: Build and deploy to TestFlight env: FASTLANE_TEAM_ID: ${{ secrets.FASTLANE_TEAM_ID }} - IOS_CERTIFICATE_PASSWORD: ${{ secrets.IOS_CERTIFICATE_PASSWORD }} + MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} + MATCH_GIT_BASIC_AUTHORIZATION: ${{ steps.match-auth.outputs.base64_token }} KEYCHAIN_NAME: build.keychain - KEYCHAIN_PASSWORD: ${{ secrets.IOS_CERTIFICATE_PASSWORD }} + KEYCHAIN_PASSWORD: ${{ github.run_id }} APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }} APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }} ENVIRONMENT: ${{ inputs.environment || 'development' }} - BUNDLE_ID_SUFFIX: ${{ inputs.environment == 'production' && '' || 'development' }} GITHUB_REF: ${{ github.ref }} working-directory: ./mobile/ios run: | diff --git a/mobile/ios/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile index d167d5fb2d..2d5f6972ef 100644 --- a/mobile/ios/fastlane/Fastfile +++ b/mobile/ios/fastlane/Fastfile @@ -21,6 +21,20 @@ platform :ios do CODE_SIGN_IDENTITY = "Apple Distribution: Hau Tran (#{TEAM_ID})" BASE_BUNDLE_ID = "app.alextran.immich" + # App identifiers for production + PROD_APP_IDENTIFIERS = [ + "app.alextran.immich", + "app.alextran.immich.ShareExtension", + "app.alextran.immich.Widget" + ] + + # App identifiers for development + DEV_APP_IDENTIFIERS = [ + "app.alextran.immich.development", + "app.alextran.immich.development.ShareExtension", + "app.alextran.immich.development.Widget" + ] + # Helper method to get App Store Connect API key def get_api_key app_store_connect_api_key( @@ -32,6 +46,17 @@ platform :ios do ) end + # Helper method to sync certificates and profiles using match + def sync_code_signing(app_identifiers:, readonly: true) + match( + type: "appstore", + app_identifier: app_identifiers, + readonly: readonly, + keychain_name: ENV["KEYCHAIN_NAME"] || "login.keychain", + keychain_password: ENV["KEYCHAIN_PASSWORD"] || "" + ) + end + # Helper method to get version from pubspec.yaml def get_version_from_pubspec require 'yaml' @@ -54,7 +79,7 @@ end team_id: ENV["FASTLANE_TEAM_ID"] || TEAM_ID, code_sign_identity: CODE_SIGN_IDENTITY, bundle_identifier: "#{BASE_BUNDLE_ID}#{bundle_suffix}", - profile_name: "#{BASE_BUNDLE_ID}#{bundle_suffix} AppStore", + profile_name: "match AppStore #{BASE_BUNDLE_ID}#{bundle_suffix}", targets: ["Runner"] ) @@ -65,7 +90,7 @@ end team_id: ENV["FASTLANE_TEAM_ID"] || TEAM_ID, code_sign_identity: CODE_SIGN_IDENTITY, bundle_identifier: "#{BASE_BUNDLE_ID}#{bundle_suffix}.ShareExtension", - profile_name: "#{BASE_BUNDLE_ID}#{bundle_suffix}.ShareExtension AppStore", + profile_name: "match AppStore #{BASE_BUNDLE_ID}#{bundle_suffix}.ShareExtension", targets: ["ShareExtension"] ) @@ -76,7 +101,7 @@ end team_id: ENV["FASTLANE_TEAM_ID"] || TEAM_ID, code_sign_identity: CODE_SIGN_IDENTITY, bundle_identifier: "#{BASE_BUNDLE_ID}#{bundle_suffix}.Widget", - profile_name: "#{BASE_BUNDLE_ID}#{bundle_suffix}.Widget AppStore", + profile_name: "match AppStore #{BASE_BUNDLE_ID}#{bundle_suffix}.Widget", targets: ["WidgetExtension"] ) end @@ -115,9 +140,9 @@ end xcargs: "-skipMacroValidation CODE_SIGN_IDENTITY='#{CODE_SIGN_IDENTITY}' CODE_SIGN_STYLE=Manual", export_options: { provisioningProfiles: { - "#{app_identifier}" => "#{app_identifier} AppStore", - "#{app_identifier}.ShareExtension" => "#{app_identifier}.ShareExtension AppStore", - "#{app_identifier}.Widget" => "#{app_identifier}.Widget AppStore" + "#{app_identifier}" => "match AppStore #{app_identifier}", + "#{app_identifier}.ShareExtension" => "match AppStore #{app_identifier}.ShareExtension", + "#{app_identifier}.Widget" => "match AppStore #{app_identifier}.Widget" }, signingStyle: "manual", signingCertificate: CODE_SIGN_IDENTITY @@ -136,10 +161,8 @@ end lane :gha_testflight_dev do api_key = get_api_key - # Install development provisioning profiles - install_provisioning_profile(path: "profile_dev.mobileprovision") - install_provisioning_profile(path: "profile_dev_share.mobileprovision") - install_provisioning_profile(path: "profile_dev_widget.mobileprovision") + # Sync certificates and profiles using match + sync_code_signing(app_identifiers: DEV_APP_IDENTIFIERS) # Configure code signing for dev bundle IDs configure_code_signing(bundle_id_suffix: "development") @@ -157,11 +180,8 @@ end lane :gha_release_prod do api_key = get_api_key - # Install provisioning profiles - install_provisioning_profile(path: "profile.mobileprovision") - install_provisioning_profile(path: "profile_share.mobileprovision") - install_provisioning_profile(path: "profile_widget.mobileprovision") - + # Sync certificates and profiles using match + sync_code_signing(app_identifiers: PROD_APP_IDENTIFIERS) # Configure code signing for production bundle IDs configure_code_signing @@ -215,10 +235,8 @@ end # Use the same build process as production, just skip the upload # This ensures PR builds validate the same way as production builds - # Install provisioning profiles (use development profiles for PR builds) - install_provisioning_profile(path: "profile_dev.mobileprovision") - install_provisioning_profile(path: "profile_dev_share.mobileprovision") - install_provisioning_profile(path: "profile_dev_widget.mobileprovision") + # Sync certificates and profiles using match + sync_code_signing(app_identifiers: DEV_APP_IDENTIFIERS) # Configure code signing for dev bundle IDs configure_code_signing(bundle_id_suffix: "development") @@ -233,9 +251,9 @@ end xcargs: "-skipMacroValidation CODE_SIGN_IDENTITY='#{CODE_SIGN_IDENTITY}' CODE_SIGN_STYLE=Manual", export_options: { provisioningProfiles: { - "#{BASE_BUNDLE_ID}.development" => "#{BASE_BUNDLE_ID}.development AppStore", - "#{BASE_BUNDLE_ID}.development.ShareExtension" => "#{BASE_BUNDLE_ID}.development.ShareExtension AppStore", - "#{BASE_BUNDLE_ID}.development.Widget" => "#{BASE_BUNDLE_ID}.development.Widget AppStore" + "#{BASE_BUNDLE_ID}.development" => "match AppStore #{BASE_BUNDLE_ID}.development", + "#{BASE_BUNDLE_ID}.development.ShareExtension" => "match AppStore #{BASE_BUNDLE_ID}.development.ShareExtension", + "#{BASE_BUNDLE_ID}.development.Widget" => "match AppStore #{BASE_BUNDLE_ID}.development.Widget" }, signingStyle: "manual", signingCertificate: CODE_SIGN_IDENTITY @@ -243,4 +261,30 @@ end ) end + desc "Sync all certificates and profiles (run locally to update match repo)" + lane :sync_certificates do + # Sync production certificates and profiles + match( + type: "appstore", + app_identifier: PROD_APP_IDENTIFIERS, + readonly: false + ) + + # Sync development certificates and profiles + match( + type: "appstore", + app_identifier: DEV_APP_IDENTIFIERS, + readonly: false + ) + end + + desc "Regenerate all certificates and profiles (use when expired)" + lane :regenerate_certificates do + # Nuke existing certificates + match_nuke(type: "appstore") + + # Generate new ones + sync_certificates + end + end diff --git a/mobile/ios/fastlane/Matchfile b/mobile/ios/fastlane/Matchfile new file mode 100644 index 0000000000..7eba970fde --- /dev/null +++ b/mobile/ios/fastlane/Matchfile @@ -0,0 +1,19 @@ +git_url(ENV["MATCH_GIT_URL"] || "https://github.com/immich-app/ios-certs") + +storage_mode("git") + +type("appstore") + +team_id("2F67MQ8R79") + +app_identifier([ + "app.alextran.immich", + "app.alextran.immich.ShareExtension", + "app.alextran.immich.Widget", + "app.alextran.immich.development", + "app.alextran.immich.development.ShareExtension", + "app.alextran.immich.development.Widget" +]) + +# For all available options run `fastlane match --help` +# The docs are available on https://docs.fastlane.tools/actions/match diff --git a/mobile/ios/fastlane/README.md b/mobile/ios/fastlane/README.md index 5fc8101b3a..48d3edf380 100644 --- a/mobile/ios/fastlane/README.md +++ b/mobile/ios/fastlane/README.md @@ -39,6 +39,30 @@ iOS Release to TestFlight iOS Manual Release +### ios gha_build_only + +```sh +[bundle exec] fastlane ios gha_build_only +``` + +iOS Build Only (no TestFlight upload) + +### ios sync_certificates + +```sh +[bundle exec] fastlane ios sync_certificates +``` + +Sync all certificates and profiles (run locally to update match repo) + +### ios regenerate_certificates + +```sh +[bundle exec] fastlane ios regenerate_certificates +``` + +Regenerate all certificates and profiles (use when expired) + ---- This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run.