diff --git a/.github/workflows/build-mobile.yml b/.github/workflows/build-mobile.yml index 1286b45409..5781276e4f 100644 --- a/.github/workflows/build-mobile.yml +++ b/.github/workflows/build-mobile.yml @@ -2,6 +2,15 @@ name: Build Mobile on: workflow_dispatch: + inputs: + environment: + description: 'Target environment' + required: true + default: 'development' + type: choice + options: + - production + - development workflow_call: inputs: ref: @@ -193,17 +202,22 @@ jobs: - name: Setup Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: '3.4.7' + ruby-version: '3.3' working-directory: ./mobile/ios - - name: Install Fastlane + - name: Install CocoaPods dependencies + working-directory: ./mobile/ios + run: | + pod install + + - name: Install Fastlane + working-directory: ./mobile/ios run: | - cd mobile/ios gem install bundler bundle config set --local path 'vendor/bundle' bundle install - - name: Create API Key JSON + - name: Create API Key env: API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }} API_KEY_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }} @@ -212,35 +226,55 @@ jobs: run: | mkdir -p ~/.appstoreconnect/private_keys echo "$API_KEY_CONTENT" | base64 --decode > ~/.appstoreconnect/private_keys/AuthKey_${API_KEY_ID}.p8 - cat > api_key.json << EOF - { - "key_id": "${API_KEY_ID}", - "issuer_id": "${API_KEY_ISSUER_ID}", - "key": "$(cat ~/.appstoreconnect/private_keys/AuthKey_${API_KEY_ID}.p8)", - "duration": 1200, - "in_house": false - } - EOF - - name: Import Certificate and Provisioning Profile + - name: Import Certificate and Provisioning Profiles 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 run: | + # Decode certificate echo "$IOS_CERTIFICATE_P12" | base64 --decode > certificate.p12 - echo "$IOS_PROVISIONING_PROFILE" | base64 --decode > profile.mobileprovision - - name: Create keychain + # 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 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 }} @@ -249,8 +283,14 @@ jobs: KEYCHAIN_PASSWORD: ${{ secrets.IOS_CERTIFICATE_PASSWORD }} 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' }} working-directory: ./mobile/ios - run: bundle exec fastlane release_ci + run: | + if [[ "$ENVIRONMENT" == "development" ]]; then + bundle exec fastlane release_dev + else + bundle exec fastlane release_ci + fi - name: Clean up keychain if: always() diff --git a/mobile/ios/Gemfile b/mobile/ios/Gemfile index bb94aef518..3b6771ad35 100644 --- a/mobile/ios/Gemfile +++ b/mobile/ios/Gemfile @@ -1,4 +1,5 @@ source "https://rubygems.org" gem "fastlane" -gem "cocoapods" \ No newline at end of file +gem "cocoapods" +gem "abbrev" # Required for Ruby 3.4+ \ No newline at end of file diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index 3f00b6c6aa..1b86a0b787 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 77; objects = { /* Begin PBXBuildFile section */ @@ -133,15 +133,11 @@ /* Begin PBXFileSystemSynchronizedRootGroup section */ B231F52D2E93A44A00BC45D1 /* Core */ = { isa = PBXFileSystemSynchronizedRootGroup; - exceptions = ( - ); path = Core; sourceTree = ""; }; B2CF7F8C2DDE4EBB00744BF6 /* Sync */ = { isa = PBXFileSystemSynchronizedRootGroup; - exceptions = ( - ); path = Sync; sourceTree = ""; }; @@ -530,10 +526,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; @@ -562,10 +562,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; diff --git a/mobile/ios/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile index 260b729579..5637745954 100644 --- a/mobile/ios/fastlane/Fastfile +++ b/mobile/ios/fastlane/Fastfile @@ -16,42 +16,81 @@ default_platform(:ios) platform :ios do - desc "iOS Release to TestFlight" - lane :release_ci do - # Setup CI environment - setup_ci - - # Load App Store Connect API Key - api_key = app_store_connect_api_key( + # Constants + TEAM_ID = "2F67MQ8R79" + CODE_SIGN_IDENTITY = "Apple Distribution: Hau Tran (#{TEAM_ID})" + BASE_BUNDLE_ID = "app.alextran.immich" + + # Helper method to get App Store Connect API key + def get_api_key + app_store_connect_api_key( key_id: ENV["APP_STORE_CONNECT_API_KEY_ID"], issuer_id: ENV["APP_STORE_CONNECT_API_KEY_ISSUER_ID"], - key_filepath: "api_key.json" + key_filepath: "#{Dir.home}/.appstoreconnect/private_keys/AuthKey_#{ENV['APP_STORE_CONNECT_API_KEY_ID']}.p8", + duration: 1200, + in_house: false ) + end + + # Helper method to configure code signing for all targets + def configure_code_signing(bundle_id_suffix: "") + bundle_suffix = bundle_id_suffix.empty? ? "" : ".#{bundle_id_suffix}" - # Import certificate and provisioning profile - import_certificate( - certificate_path: "certificate.p12", - certificate_password: ENV["IOS_CERTIFICATE_PASSWORD"], - keychain_name: ENV["KEYCHAIN_NAME"], - keychain_password: ENV["KEYCHAIN_PASSWORD"] - ) - - # Install provisioning profile - install_provisioning_profile(path: "profile.mobileprovision") - - # Configure code signing + # Runner (main app) update_code_signing_settings( use_automatic_signing: false, path: "./Runner.xcodeproj", - team_id: ENV["FASTLANE_TEAM_ID"], - profile_name: "app.alextran.immich AppStore" + 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", + targets: ["Runner"] ) + # ShareExtension + update_code_signing_settings( + use_automatic_signing: false, + path: "./Runner.xcodeproj", + 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", + targets: ["ShareExtension"] + ) + + # WidgetExtension + update_code_signing_settings( + use_automatic_signing: false, + path: "./Runner.xcodeproj", + 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", + targets: ["WidgetExtension"] + ) + end + + # Helper method to build and upload to TestFlight + def build_and_upload( + api_key:, + bundle_id_suffix: "", + configuration: "Release", + distribute_external: true, + version_number: nil + ) + bundle_suffix = bundle_id_suffix.empty? ? "" : ".#{bundle_id_suffix}" + app_identifier = "#{BASE_BUNDLE_ID}#{bundle_suffix}" + + # Set version number if provided + if version_number + increment_version_number(version_number: version_number) + end + # Increment build number increment_build_number( build_number: latest_testflight_build_number( api_key: api_key, - app_identifier: "app.alextran.immich" + app_identifier: app_identifier ) + 1, xcodeproj: "./Runner.xcodeproj" ) @@ -60,25 +99,74 @@ platform :ios do build_app( scheme: "Runner", workspace: "Runner.xcworkspace", + configuration: configuration, export_method: "app-store", + xcargs: "CODE_SIGN_IDENTITY='#{CODE_SIGN_IDENTITY}' CODE_SIGN_STYLE=Manual", export_options: { provisioningProfiles: { - "app.alextran.immich" => "app.alextran.immich AppStore" - } + "#{app_identifier}" => "#{app_identifier} AppStore", + "#{app_identifier}.ShareExtension" => "#{app_identifier}.ShareExtension AppStore", + "#{app_identifier}.Widget" => "#{app_identifier}.Widget AppStore" + }, + signingStyle: "manual", + signingCertificate: CODE_SIGN_IDENTITY } ) # Upload to TestFlight upload_to_testflight( api_key: api_key, - skip_waiting_for_build_processing: true + skip_waiting_for_build_processing: true, + distribute_external: distribute_external + ) + end + + desc "iOS Development Build to TestFlight (requires separate bundle ID)" + lane :release_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") + + # Configure code signing for dev bundle IDs + configure_code_signing(bundle_id_suffix: "development") + + # Build and upload + build_and_upload( + api_key: api_key, + bundle_id_suffix: "development", + configuration: "Profile", + distribute_external: false ) end - desc "iOS Release" - lane :release do + desc "iOS Release to TestFlight" + lane :release_ci 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") + + + # Configure code signing for production bundle IDs + configure_code_signing + + # Build and upload with version number + build_and_upload( + api_key: api_key, + version_number: "2.1.0" + ) + end + + desc "iOS Manual Release" + lane :release_manual do enable_automatic_code_signing( path: "./Runner.xcodeproj", + targets: ["Runner", "ShareExtension", "WidgetExtension"] ) increment_version_number( version_number: "2.2.0" @@ -86,9 +174,24 @@ platform :ios do increment_build_number( build_number: latest_testflight_build_number + 1, ) - build_app(scheme: "Runner", - workspace: "Runner.xcworkspace", - xcargs: "-allowProvisioningUpdates") + + # Build archive with automatic signing + gym( + scheme: "Runner", + workspace: "Runner.xcworkspace", + configuration: "Release", + export_method: "app-store", + skip_package_ipa: false, + xcargs: "-allowProvisioningUpdates", + export_options: { + method: "app-store", + signingStyle: "automatic", + uploadBitcode: false, + uploadSymbols: true, + compileBitcode: false + } + ) + upload_to_testflight( skip_waiting_for_build_processing: true ) diff --git a/mobile/ios/fastlane/README.md b/mobile/ios/fastlane/README.md index 2999821730..2bea622b18 100644 --- a/mobile/ios/fastlane/README.md +++ b/mobile/ios/fastlane/README.md @@ -15,13 +15,29 @@ For _fastlane_ installation instructions, see [Installing _fastlane_](https://do ## iOS -### ios release +### ios release_dev ```sh -[bundle exec] fastlane ios release +[bundle exec] fastlane ios release_dev ``` -iOS Release +iOS Development Build to TestFlight (requires separate bundle ID) + +### ios release_ci + +```sh +[bundle exec] fastlane ios release_ci +``` + +iOS Release to TestFlight + +### ios release_manual + +```sh +[bundle exec] fastlane ios release_manual +``` + +iOS Manual Release ----