require 'yaml' skip_docs enterprise = '../Enterprise/Pipeline/Scripts/iOS/Fastfile' if File.exist?(enterprise) import enterprise end before_all do xcversion(version: "16.3") ENV["FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT"] = "180" ENV["FASTLANE_XCODE_LIST_TIMEOUT"] = "180" ENV["SENTRY_LOG_LEVEL"] = "DEBUG" end lane :unit_tests do |options| reset_simulator = ENV.key?('CI') run_tests( scheme: "UnitTests", device: "iPhone 16 (18.4)", ensure_devices_found: true, result_bundle: true, number_of_retries: 3, reset_simulator: reset_simulator ) if !options[:skip_previews] run_tests( scheme: "PreviewTests", device: "iPhone SE (3rd generation) (18.4)", ensure_devices_found: true, result_bundle: true, number_of_retries: 3, reset_simulator: reset_simulator ) end # We use xcresultparser in the workflow to collect coverage from both result bundles. end lane :ui_tests do |options| # We used to run these simultaneously on iPhone and iPad but it is *really* slow on GitHub runners. # Presumably because launching 2 simulators uses more memory than the runner has available. if options[:device] == "iPhone" device = "iPhone-18.4" create_simulator_if_necessary( name: "iPhone-18.4", type: "com.apple.CoreSimulator.SimDeviceType.iPhone-16", runtime: "com.apple.CoreSimulator.SimRuntime.iOS-18-4" ) elsif options[:device] == "iPad" device = "iPad-18.4" create_simulator_if_necessary( name: "iPad-18.4", type: "com.apple.CoreSimulator.SimDeviceType.iPad-10th-generation", runtime: "com.apple.CoreSimulator.SimRuntime.iOS-18-4" ) else UI.user_error!("Please supply a device argument as device:iPhone or device:iPad") end if options[:test_name] test_to_run = ["UITests/#{options[:test_name]}"] else test_to_run = nil end reset_simulator = ENV.key?('CI') run_tests( scheme: "UITests", device: device, ensure_devices_found: true, prelaunch_simulator: false, result_bundle: true, only_testing: test_to_run, number_of_retries: 3, reset_simulator: reset_simulator ) end lane :integration_tests do clear_derived_data() create_simulator_if_necessary( name: "iPhone-18.4", type: "com.apple.CoreSimulator.SimDeviceType.iPhone-16-Pro", runtime: "com.apple.CoreSimulator.SimRuntime.iOS-18-4" ) reset_simulator = ENV.key?('CI') run_tests( scheme: "IntegrationTests", device: "iPhone-18.4", ensure_devices_found: true, result_bundle: true, reset_simulator: reset_simulator ) end lane :config_production do update_foss_secrets() xcodegen(spec: "project.yml") 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 # Check if the "path" already exists in the "include" array if !data["include"].any? { |item| item["path"] == "Variants/Nightly/nightly.yml" } data["include"].append({ "path" => "Variants/Nightly/nightly.yml" }) end update_foss_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") Dir.chdir ".." do sh("swift run tools app-icon-banner Variants/Nightly/Resources/Nightly.xcassets/NightlyAppIcon.appiconset/AppIcon.png --banner-text '#{release_version} (#{build_number})'") end 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? release_version = get_version_number(target: "ElementX") github_release = set_github_release( repository_name: "element-hq/element-x-ios", api_token: api_token, name: release_version, tag_name: "release/#{release_version}", is_generate_release_notes: true, ) release_date = Date.today.strftime("%Y-%m-%d") generated_notes = github_release["body"].gsub(//, '').gsub("### ", "\n").gsub("## ", "### ") UI.user_error!("The generated release notes are missing!") unless !generated_notes.to_s.empty? # Prepend the new release notes to the CHANGES.md file changes_file = "../CHANGES.md" File.open(changes_file, "r+") do |file| content = file.read file.rewind file.write("## Changes in #{release_version} (#{release_date})#{generated_notes}\n\n#{content}") end # The changelog will be committed when prepare_next_release is called. sh("git add #{changes_file}") 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"] matches = current_version.match(/^(\d{2})\.(\d{2})\.(\d+)$/) unless matches UI.user_error!("Invalid version format: #{current_version}") end year, month, build = matches.captures new_build = build.to_i + 1 new_version = "#{year}.#{month}.#{new_build}" # 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}/g' #{target_file_path}") UI.message("Version updated from #{current_version} to #{new_version}") 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() rebase_main_onto_current_branch() end def rebase_main_onto_current_branch # Capture the current branch name current_branch = sh("git rev-parse --abbrev-ref HEAD").strip UI.message("Current branch: #{current_branch}") # Switch to main and update it sh("git reset --hard") sh("git checkout main") sh("git pull origin main") sh("git rebase #{current_branch}") git_push() UI.success("Successfully rebased main onto #{current_branch}") 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 :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 :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