require 'yaml' require 'semantic' require_relative 'changelog' before_all do xcversion(version: "~> 15.0") ENV["FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT"] = "180" ENV["FASTLANE_XCODE_LIST_TIMEOUT"] = "180" ENV["SENTRY_LOG_LEVEL"] = "DEBUG" end lane :alpha do app_store_connect_api_key( key_id: ENV["APPSTORECONNECT_KEY_ID"], issuer_id: ENV["APPSTORECONNECT_KEY_ISSUER_ID"], key_content: ENV["APPSTORECONNECT_KEY_CONTENT"] ) config_xcodegen_alpha() code_signing_identity = "Apple Distribution: Vector Creations Limited (7J4U792NQT)" app_provisioning_profile_name = "ElementX PR Ad Hoc" app_bundle_identifier = "io.element.elementx.pr" nse_provisioning_profile_name = "ElementX NSE PR Ad Hoc" nse_bundle_identifier = "io.element.elementx.pr.nse" update_code_signing_settings( targets: ["ElementX"], use_automatic_signing: false, bundle_identifier: app_bundle_identifier, profile_name: app_provisioning_profile_name, code_sign_identity: code_signing_identity ) get_provisioning_profile( app_identifier: app_bundle_identifier, provisioning_name: app_provisioning_profile_name, ignore_profiles_with_different_name: true, adhoc: true ) update_code_signing_settings( targets: ["NSE"], use_automatic_signing: false, bundle_identifier: nse_bundle_identifier, profile_name: nse_provisioning_profile_name, code_sign_identity: code_signing_identity ) get_provisioning_profile( app_identifier: nse_bundle_identifier, provisioning_name: nse_provisioning_profile_name, ignore_profiles_with_different_name: true, adhoc: true ) build_ios_app( scheme: "ElementX", clean: true, export_method: "ad-hoc", output_directory: "build", export_options: { provisioningProfiles: { app_bundle_identifier => app_provisioning_profile_name, nse_bundle_identifier => nse_provisioning_profile_name } } ) upload_to_diawi() upload_to_browserstack() end lane :unit_tests do create_simulator_if_necessary( name: "iPhone 14 (16.4)", type: "com.apple.CoreSimulator.SimDeviceType.iPhone-14", runtime: "com.apple.CoreSimulator.SimRuntime.iOS-16-4" ) run_tests( scheme: "UnitTests", device: 'iPhone 14 (16.4)', ensure_devices_found: true, result_bundle: true, number_of_retries: 3, ) run_tests( scheme: "PreviewTests", device: 'iPhone 14 (16.4)', result_bundle: true, number_of_retries: 3, xcargs: '-skipPackagePluginValidation', ) # We use xcresultparser in the workflow to collect coverage from both result bundles. end lane :ui_tests do |options| # Use a fresh simulator state to ensure hardware keyboard isn't attached. reset_simulator_contents() create_simulator_if_necessary( name: "iPhone 14 (16.4)", type: "com.apple.CoreSimulator.SimDeviceType.iPhone-14", runtime: "com.apple.CoreSimulator.SimRuntime.iOS-16-4" ) create_simulator_if_necessary( name: "iPad (9th generation)", type: "com.apple.CoreSimulator.SimDeviceType.iPad-9th-generation", runtime: "com.apple.CoreSimulator.SimRuntime.iOS-16-4" ) if options[:test_name] test_to_run = ["UITests/#{options[:test_name]}"] else test_to_run = nil end run_tests( scheme: "UITests", devices: ["iPhone 14", "iPad (9th generation)"], ensure_devices_found: true, prelaunch_simulator: true, result_bundle: true, only_testing: test_to_run, number_of_retries: 3, ) end lane :integration_tests do clear_derived_data() create_simulator_if_necessary( name: "iPhone 14 Pro", type: "com.apple.CoreSimulator.SimDeviceType.iPhone-14-Pro", runtime: "com.apple.CoreSimulator.SimRuntime.iOS-16-4" ) run_tests( scheme: "IntegrationTests", devices: ["iPhone 14 Pro"], ensure_devices_found: true, result_bundle: true, reset_simulator: true, include_simulator_logs: true ) end lane :config_nightly do |options| build_number = options[:build_number] UI.user_error!("Invalid build number.") unless !build_number.to_s.empty? target_file_path = "../project.yml" data = YAML.load_file target_file_path data["settings"]["BASE_APP_GROUP_IDENTIFIER"] = "io.element.nightly" data["settings"]["BASE_BUNDLE_IDENTIFIER"] = "io.element.elementx.nightly" config_secrets() File.open(target_file_path, 'w') { |f| YAML.dump(data, f) } xcodegen(spec: "project.yml") # Automatically done by Xcode Cloud. Cannot override # https://developer.apple.com/documentation/xcode/setting-the-next-build-number-for-xcode-cloud-builds # bump_build_number() release_version = get_version_number(target: "ElementX") update_app_icon(caption_text: "#{release_version}(#{build_number})", modulate: "100,20,100") end lane :config_production do config_secrets() xcodegen(spec: "project.yml") end $sentry_retry=0 lane :upload_dsyms_to_sentry do |options| auth_token = ENV["SENTRY_AUTH_TOKEN"] UI.user_error!("Invalid Sentry Auth token.") unless !auth_token.to_s.empty? dsym_path = options[:dsym_path] UI.user_error!("Invalid DSYM path.") unless !dsym_path.to_s.empty? begin sentry_upload_dif( auth_token: auth_token, org_slug: 'element', project_slug: 'element-x-ios', url: 'https://sentry.tools.element.io/', path: dsym_path, ) rescue => exception $sentry_retry += 1 if $sentry_retry <= 5 UI.message "Sentry failed, retrying." upload_dsyms_to_sentry options else raise exception end end end lane :release_to_github do api_token = ENV["GITHUB_TOKEN"] UI.user_error!("Invalid GitHub API token.") unless !api_token.to_s.empty? # Get the Diawi link from Diawi action shared value diawi_link = lane_context[SharedValues::UPLOADED_FILE_LINK_TO_DIAWI] release_version = get_version_number(target: "ElementX") changes = export_version_changes(version: release_version) description = "" if diawi_link.nil? description = "#{changes}" else # Generate the Diawi QR code file link diawi_app_id = URI(diawi_link).path.split('/').last diawi_qr_code_link = "https://www.diawi.com/qrcode/link/#{diawi_app_id}" "[iOS AdHoc Release - Diawi Link](#{diawi_link}) ![QR code](#{diawi_qr_code_link}) #{changes}" end github_release = set_github_release( repository_name: "element-hq/element-x-ios", api_token: api_token, name: release_version, tag_name: release_version, is_generate_release_notes: false, description: description ) end lane :prepare_next_release do target_file_path = "../project.yml" xcode_project_file_path = "../ElementX.xcodeproj" data = YAML.load_file target_file_path current_version = data["settings"]["MARKETING_VERSION"] version = Semantic::Version.new(current_version) new_version = version.increment!(:patch) # Bump the patch version. The empty string after -i is so that sed doesn't # create a backup file on macOS sh("sed -i '' 's/MARKETING_VERSION: #{current_version}/MARKETING_VERSION: #{new_version.to_string}/g' #{target_file_path}") xcodegen(spec: "project.yml") setup_git() sh("git add #{target_file_path} #{xcode_project_file_path}") sh("git commit -m 'Prepare next release'") git_push() end lane :tag_nightly do |options| build_number = options[:build_number] UI.user_error!("Invalid build number.") unless !build_number.to_s.empty? xcodegen_project_file_path = "../project.yml" data = YAML.load_file xcodegen_project_file_path current_version = data["settings"]["MARKETING_VERSION"] setup_git() tag_name = "nightly/#{current_version}.#{build_number}" sh("git tag #{tag_name}") git_push(tag_name: tag_name) end private_lane :setup_git do sh("git config --global user.name 'Element CI'") sh("git config --global user.email 'ci@element.io'") end private_lane :git_push do |options| # Use the Github API token for repo write access api_token = ENV["GITHUB_TOKEN"] UI.user_error!("Invalid GitHub API token.") unless !api_token.to_s.empty? # Get repo url path, without `http`, `https` or `git@` prefixes or `.git` suffix repo_url = sh("git ls-remote --get-url origin | sed 's#http://##g' | sed 's#https:\/\/##g' | sed 's#git@##g' | sed 's#.git##g'") # This sometimes has a trailing newline repo_url = repo_url.strip # Push the tag separately if available if options[:tag_name] sh("git push https://#{api_token}@#{repo_url} #{options[:tag_name]}") end sh("git push https://#{api_token}@#{repo_url}") end private_lane :config_xcodegen_alpha do target_file_path = "../project.yml" data = YAML.load_file target_file_path data["settings"]["BASE_APP_GROUP_IDENTIFIER"] = "io.element.pr" data["settings"]["BASE_BUNDLE_IDENTIFIER"] = "io.element.elementx.pr" File.open(target_file_path, 'w') { |f| YAML.dump(data, f) } xcodegen(spec: "project.yml") version = ENV["GITHUB_PR_NUMBER"] update_app_icon(caption_text: "PR #{version}", modulate: "100,100,200") bump_build_number() end private_lane :upload_to_diawi do api_token = ENV["DIAWI_API_TOKEN"] UI.user_error!("Invalid Diawi API token.") unless !api_token.to_s.empty? # Upload to Diawi diawi( token: api_token, wall_of_apps: false, file: lane_context[SharedValues::IPA_OUTPUT_PATH] ) # Get the Diawi link from Diawi action shared value diawi_link = lane_context[SharedValues::UPLOADED_FILE_LINK_TO_DIAWI] UI.command_output("Diawi link: " + diawi_link.to_s) # Generate the Diawi QR code file link diawi_app_id = URI(diawi_link).path.split('/').last diawi_qr_code_link = "https://www.diawi.com/qrcode/link/#{diawi_app_id}" # Set "DIAWI_FILE_LINK" to GitHub environment variables for Github actions sh("echo DIAWI_FILE_LINK=#{diawi_link} >> $GITHUB_ENV") sh("echo DIAWI_QR_CODE_LINK=#{diawi_qr_code_link} >> $GITHUB_ENV") end private_lane :upload_to_browserstack do browserstack_username = ENV["BROWSERSTACK_USERNAME"] UI.user_error!("Invalid BrowserStack username.") unless !browserstack_username.to_s.empty? browserstack_access_key = ENV["BROWSERSTACK_ACCESS_KEY"] UI.user_error!("Invalid BrowserStack access key.") unless !browserstack_access_key.to_s.empty? upload_to_browserstack_app_automate( browserstack_username: browserstack_username, browserstack_access_key: browserstack_access_key, file_path: lane_context[SharedValues::IPA_OUTPUT_PATH], custom_id: "element-x-ios-pr" ) end private_lane :bump_build_number do # Increment build number to current date build_number = Time.now.strftime("%Y%m%d%H%M") increment_build_number(build_number: build_number) end private_lane :export_version_changes do |options| Dir.chdir("..") do Changelog.update_topmost_section(version: options[:version], additional_entries: {}) Changelog.extract_first_section end end private_lane :update_app_icon do |options| caption_text = options[:caption_text] UI.user_error!("Invalid caption text.") unless !caption_text.to_s.empty? modulate = options[:modulate] UI.user_error!("Invalid icon modulate parameters.") unless !modulate.to_s.empty? Dir.glob("../ElementX/Resources/Assets.xcassets/AppIcon.appiconset/**/*.png") do |file_name| # Change the icons color sh("convert '#{file_name}' -modulate #{modulate} '#{file_name}'") image_width = sh("identify -format %w '#{file_name}'") image_height = sh("identify -format %h '#{file_name}'").to_i caption_height = image_height / 5 # Add a label on top sh("convert -background '#0008' -fill white -gravity center -size '#{image_width}'x'#{caption_height}' caption:'#{caption_text}' '#{file_name}' +swap -gravity south -composite '#{file_name}'") end end private_lane :create_simulator_if_necessary do |options| simulator_name = options[:name] UI.user_error!("Invalid simulator name") unless !simulator_name.to_s.empty? simulator_type = options[:type] UI.user_error!("Invalid simulator type") unless !simulator_type.to_s.empty? simulator_runtime = options[:runtime] UI.user_error!("Invalid simulator runtime") unless !simulator_runtime.to_s.empty? # Use a `(` here to avoid matching `iPhone 14 Pro` on `iPhone 14 Pro Max` for example begin sh("xcrun simctl list devices | grep '#{simulator_name} ('") UI.success "Simulator already exists" rescue sh("xcrun simctl create '#{simulator_name}' #{simulator_type} #{simulator_runtime}") end end private_lane :config_secrets do maplibre_api_key = ENV["MAPLIBRE_API_KEY"] UI.user_error!("Invalid Map Libre API key.") unless !maplibre_api_key.to_s.empty? otlp_tracing_url = ENV["OTLP_TRACING_URL"] UI.user_error!("Invalid OTLP tracing URL.") unless !otlp_tracing_url.to_s.empty? otlp_tracing_username = ENV["OTLP_TRACING_USERNAME"] UI.user_error!("Invalid OTLP tracing username.") unless !otlp_tracing_username.to_s.empty? otlp_tracing_password = ENV["OTLP_TRACING_PASSWORD"] UI.user_error!("Invalid OTLP tracing password.") unless !otlp_tracing_password.to_s.empty? set_xcconfig_value( path: './ElementX/SupportingFiles/secrets.xcconfig', name: 'MAPLIBRE_API_KEY', value: maplibre_api_key ) # URLs need special treatment to work properly in xcconfig files # https://stackoverflow.com/a/36297483/730924 # i.e. make sure to use https:/$()/ in the scheme in the stored secret set_xcconfig_value( path: './ElementX/SupportingFiles/secrets.xcconfig', name: 'OTLP_TRACING_URL', value: otlp_tracing_url ) set_xcconfig_value( path: './ElementX/SupportingFiles/secrets.xcconfig', name: 'OTLP_TRACING_USERNAME', value: otlp_tracing_username ) set_xcconfig_value( path: './ElementX/SupportingFiles/secrets.xcconfig', name: 'OTLP_TRACING_PASSWORD', value: otlp_tracing_password ) end