|
| 1 | +# This file defines the build and signing lanes for iOS |
| 2 | + |
| 3 | +default_platform(:ios) |
| 4 | + |
| 5 | +TEAM_ID = "8B5X2M6H2Y" |
| 6 | +APP_ID_RELEASE = "app.status.mobile" |
| 7 | +APP_ID_PR = "app.status.mobile.pr" |
| 8 | +APP_NAME_RELEASE = "Status.app" |
| 9 | +APP_NAME_PR = "StatusPR.app" |
| 10 | +PROJECT_DIR = File.expand_path("../", __dir__) |
| 11 | +BUILD_DIR = File.join(PROJECT_DIR, "bin", "ios", "qt6") |
| 12 | + |
| 13 | +platform :ios do |
| 14 | + before_all do |
| 15 | + UI.message("Project directory: #{PROJECT_DIR}") |
| 16 | + UI.message("Build directory: #{BUILD_DIR}") |
| 17 | + end |
| 18 | + |
| 19 | + after_all do |
| 20 | + # Clean up CI keychain after build |
| 21 | + if is_ci |
| 22 | + delete_keychain(name: keychain_name) if File.exist?(File.expand_path("~/Library/Keychains/#{keychain_name}-db")) |
| 23 | + end |
| 24 | + end |
| 25 | + |
| 26 | + error do |
| 27 | + # Clean up CI keychain on failure too |
| 28 | + if is_ci |
| 29 | + delete_keychain(name: keychain_name) rescue nil |
| 30 | + end |
| 31 | + end |
| 32 | + |
| 33 | + # ============================================ |
| 34 | + # PR Builds |
| 35 | + # ============================================ |
| 36 | + desc "Build iOS app for PRs" |
| 37 | + lane :pr do |
| 38 | + setup_ci_keychain |
| 39 | + |
| 40 | + match( |
| 41 | + type: "development", |
| 42 | + app_identifier: APP_ID_PR, |
| 43 | + readonly: true, |
| 44 | + keychain_name: keychain_name, |
| 45 | + keychain_password: keychain_password |
| 46 | + ) |
| 47 | + |
| 48 | + sign_app( |
| 49 | + app_identifier: APP_ID_PR, |
| 50 | + app_name: APP_NAME_PR, |
| 51 | + profile_type: "development" |
| 52 | + ) |
| 53 | + |
| 54 | + create_ipa(app_name: APP_NAME_PR) |
| 55 | + end |
| 56 | + |
| 57 | + # ============================================ |
| 58 | + # Release Builds |
| 59 | + # ============================================ |
| 60 | + desc "Build iOS app for release" |
| 61 | + lane :release do |
| 62 | + setup_ci_keychain |
| 63 | + |
| 64 | + match( |
| 65 | + type: "appstore", |
| 66 | + app_identifier: APP_ID_RELEASE, |
| 67 | + readonly: true, |
| 68 | + keychain_name: keychain_name, |
| 69 | + keychain_password: keychain_password |
| 70 | + ) |
| 71 | + |
| 72 | + sign_app( |
| 73 | + app_identifier: APP_ID_RELEASE, |
| 74 | + app_name: APP_NAME_RELEASE, |
| 75 | + profile_type: "appstore" |
| 76 | + ) |
| 77 | + |
| 78 | + create_ipa(app_name: APP_NAME_RELEASE) |
| 79 | + |
| 80 | + if ENV["UPLOAD_TO_TESTFLIGHT"] == "true" |
| 81 | + upload_to_testflight_lane |
| 82 | + end |
| 83 | + end |
| 84 | + |
| 85 | + # ============================================ |
| 86 | + # TestFlight Upload |
| 87 | + # ============================================ |
| 88 | + desc "Upload IPA to TestFlight" |
| 89 | + lane :upload_to_testflight_lane do |
| 90 | + api_key = app_store_connect_api_key( |
| 91 | + key_id: ENV["ASC_KEY_ID"], |
| 92 | + issuer_id: ENV["ASC_ISSUER_ID"], |
| 93 | + key_filepath: ENV["ASC_KEY_FILE"], |
| 94 | + duration: 1200, |
| 95 | + in_house: false |
| 96 | + ) |
| 97 | + |
| 98 | + upload_to_testflight( |
| 99 | + api_key: api_key, |
| 100 | + ipa: File.join(BUILD_DIR, "Status.ipa"), |
| 101 | + skip_waiting_for_build_processing: true, |
| 102 | + changelog: ENV["CHANGELOG"] || "New build from CI" |
| 103 | + ) |
| 104 | + end |
| 105 | + |
| 106 | + # ============================================ |
| 107 | + # Helper Methods |
| 108 | + # ============================================ |
| 109 | + |
| 110 | + private_lane :setup_ci_keychain do |
| 111 | + if is_ci |
| 112 | + create_keychain( |
| 113 | + name: keychain_name, |
| 114 | + password: keychain_password, |
| 115 | + default_keychain: true, |
| 116 | + unlock: true, |
| 117 | + timeout: 3600, |
| 118 | + lock_when_sleeps: false |
| 119 | + ) |
| 120 | + end |
| 121 | + end |
| 122 | + |
| 123 | + private_lane :sign_app do |options| |
| 124 | + app_identifier = options[:app_identifier] |
| 125 | + app_name = options[:app_name] || "Status.app" |
| 126 | + profile_type = options[:profile_type] |
| 127 | + |
| 128 | + app_path = File.join(BUILD_DIR, app_name) |
| 129 | + |
| 130 | + unless File.exist?(app_path) |
| 131 | + UI.user_error!("#{app_name} not found at #{app_path}") |
| 132 | + end |
| 133 | + |
| 134 | + update_info_plist( |
| 135 | + plist_path: File.join(app_path, "Info.plist"), |
| 136 | + bundle_identifier: app_identifier |
| 137 | + ) |
| 138 | + |
| 139 | + UI.message("Signing app with identifier: #{app_identifier}") |
| 140 | + |
| 141 | + profile_name = "match #{profile_type.capitalize} #{app_identifier}" |
| 142 | + |
| 143 | + signing_identity = ENV["sigh_#{app_identifier}_#{profile_type}_certificate-name"] || |
| 144 | + lane_context[SharedValues::MATCH_PROVISIONING_PROFILE_MAPPING]&.dig(app_identifier) || |
| 145 | + get_signing_identity(profile_type) |
| 146 | + |
| 147 | + # Need to Sign all frameworks first, else App crashes at runtime |
| 148 | + frameworks_path = File.join(app_path, "Frameworks") |
| 149 | + if File.directory?(frameworks_path) |
| 150 | + Dir.glob("#{frameworks_path}/*.framework").each do |framework| |
| 151 | + UI.message("Signing framework: #{File.basename(framework)}") |
| 152 | + sh("codesign --force --sign '#{signing_identity}' --timestamp '#{framework}'") |
| 153 | + end |
| 154 | + end |
| 155 | + |
| 156 | + UI.message("Signing main app bundle...") |
| 157 | + |
| 158 | + profile_path = ENV["sigh_#{app_identifier}_#{profile_type}_profile-path"] |
| 159 | + |
| 160 | + if profile_path && File.exist?(profile_path) |
| 161 | + FileUtils.cp(profile_path, File.join(app_path, "embedded.mobileprovision")) |
| 162 | + end |
| 163 | + |
| 164 | + sh("codesign --force --sign '#{signing_identity}' --timestamp '#{app_path}'") |
| 165 | + |
| 166 | + UI.success("App signed successfully!") |
| 167 | + end |
| 168 | + |
| 169 | + private_lane :create_ipa do |options| |
| 170 | + app_name = options[:app_name] || "Status.app" |
| 171 | + ipa_name = app_name.sub(".app", ".ipa") |
| 172 | + |
| 173 | + app_path = File.join(BUILD_DIR, app_name) |
| 174 | + ipa_path = File.join(BUILD_DIR, ipa_name) |
| 175 | + |
| 176 | + UI.message("Creating IPA at #{ipa_path}...") |
| 177 | + |
| 178 | + FileUtils.rm_f(ipa_path) |
| 179 | + |
| 180 | + Dir.mktmpdir do |tmpdir| |
| 181 | + payload_dir = File.join(tmpdir, "Payload") |
| 182 | + FileUtils.mkdir_p(payload_dir) |
| 183 | + FileUtils.cp_r(app_path, payload_dir) |
| 184 | + |
| 185 | + Dir.chdir(tmpdir) do |
| 186 | + sh("zip -r '#{ipa_path}' Payload") |
| 187 | + end |
| 188 | + end |
| 189 | + |
| 190 | + UI.success("IPA created at #{ipa_path}") |
| 191 | + end |
| 192 | + |
| 193 | + def keychain_name |
| 194 | + "status_ci_#{ENV['BUILD_NUMBER'] || 'local'}.keychain" |
| 195 | + end |
| 196 | + |
| 197 | + def keychain_password |
| 198 | + # Reuse MATCH_PASSWORD for keychain to reduce credential count |
| 199 | + ENV["MATCH_PASSWORD"] || "fastlane_ci_password" |
| 200 | + end |
| 201 | + |
| 202 | + def get_signing_identity(profile_type) |
| 203 | + case profile_type |
| 204 | + when "development" |
| 205 | + "Apple Development" |
| 206 | + when "adhoc", "appstore" |
| 207 | + "Apple Distribution" |
| 208 | + else |
| 209 | + "Apple Distribution" |
| 210 | + end |
| 211 | + end |
| 212 | +end |
0 commit comments