From eddcc5c6058ab6bcf27a77c69cc99e3d59ba504e Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Thu, 5 Mar 2026 16:14:09 +0200 Subject: [PATCH] Convert more fastlane tools to swift commands (#5163) * Upload dSYMs to sentry using swift and the sentry-cli instead of fastlane. * Replace the fastlane `config_production` lane with a swift tools `ConfigureProduction` command. * Replace Zsh with CI.run in the new ConfigureNightly command * Replace the fastlane `tag_nightly` lane with a swift tools `TagNightly` command. * Print out all the commands going through CI.Run and their arguments * Address PR comments --- Tools/Sources/Commands/CI/CI.swift | 6 +- .../Commands/CI/ConfigureNightly.swift | 4 +- .../Commands/CI/ConfigureProduction.swift | 12 +++ Tools/Sources/Commands/CI/TagNightly.swift | 51 ++++++++++++ Tools/Sources/Commands/CI/UploadDSYMs.swift | 59 ++++++++++++++ ci_scripts/ci_common.sh | 2 +- ci_scripts/ci_post_clone.sh | 2 +- ci_scripts/ci_post_xcodebuild.sh | 4 +- fastlane/Fastfile | 77 ------------------- fastlane/Pluginfile | 1 - 10 files changed, 133 insertions(+), 85 deletions(-) create mode 100644 Tools/Sources/Commands/CI/ConfigureProduction.swift create mode 100644 Tools/Sources/Commands/CI/TagNightly.swift create mode 100644 Tools/Sources/Commands/CI/UploadDSYMs.swift diff --git a/Tools/Sources/Commands/CI/CI.swift b/Tools/Sources/Commands/CI/CI.swift index 9906fa7f4..86f39624d 100644 --- a/Tools/Sources/Commands/CI/CI.swift +++ b/Tools/Sources/Commands/CI/CI.swift @@ -11,7 +11,9 @@ struct CI: ParsableCommand { UITests.self, IntegrationTests.self, RunTests.self, - ConfigureNightly.self + ConfigureNightly.self, + ConfigureProduction.self, + TagNightly.self ]) static let testOutputDirectory = "test_output" @@ -106,6 +108,8 @@ struct CI: ParsableCommand { environment: Environment = .inherit, output: Output = .standardOutput, error: Error = .standardError) async throws -> CollectedResult { + logger.info("Running \(executable), with arguments: \(arguments)") + let result = try await Subprocess.run(executable, arguments: arguments, environment: environment, diff --git a/Tools/Sources/Commands/CI/ConfigureNightly.swift b/Tools/Sources/Commands/CI/ConfigureNightly.swift index f6ae94446..17acfd8e1 100644 --- a/Tools/Sources/Commands/CI/ConfigureNightly.swift +++ b/Tools/Sources/Commands/CI/ConfigureNightly.swift @@ -17,8 +17,8 @@ struct ConfigureNightly: AsyncParsableCommand { try addNightlyVariant() - try Zsh.run(command: "swift run pipeline update-foss-secrets") - try Zsh.run(command: "xcodegen") + try await CI.run(.name("swift"), ["run", "pipeline", "update-foss-secrets"]) + try await CI.run(.name("xcodegen")) let releaseVersion = try CI.readMarketingVersion() try await generateAppIconBanner(version: releaseVersion, buildNumber: buildNumber) diff --git a/Tools/Sources/Commands/CI/ConfigureProduction.swift b/Tools/Sources/Commands/CI/ConfigureProduction.swift new file mode 100644 index 000000000..c2639ae64 --- /dev/null +++ b/Tools/Sources/Commands/CI/ConfigureProduction.swift @@ -0,0 +1,12 @@ +import ArgumentParser +import CommandLineTools +import Foundation + +struct ConfigureProduction: AsyncParsableCommand { + static let configuration = CommandConfiguration(abstract: "Configures the project for a production build.") + + func run() async throws { + try await CI.run(.name("swift"), ["run", "pipeline", "update-foss-secrets"]) + try await CI.run(.name("xcodegen")) + } +} diff --git a/Tools/Sources/Commands/CI/TagNightly.swift b/Tools/Sources/Commands/CI/TagNightly.swift new file mode 100644 index 000000000..22841ec3c --- /dev/null +++ b/Tools/Sources/Commands/CI/TagNightly.swift @@ -0,0 +1,51 @@ +import ArgumentParser +import CommandLineTools +import Foundation +import Yams + +struct TagNightly: AsyncParsableCommand { + static let configuration = CommandConfiguration(abstract: "Tags the current commit as a nightly build and pushes the tag.") + + @Option(help: "The build number to include in the tag.") + var buildNumber: String + + func run() async throws { + guard !buildNumber.isEmpty else { + throw ValidationError("Invalid build number.") + } + + guard let apiToken = ProcessInfo.processInfo.environment["GITHUB_TOKEN"], + !apiToken.isEmpty else { + throw ValidationError("Invalid GitHub API token. Please set the GITHUB_TOKEN environment variable.") + } + + let repoURL = try getRepoURL() + + try await CI.run(.name("git"), ["config", "--global", "user.name", "Element CI"]) + try await CI.run(.name("git"), ["config", "--global", "user.email", "ci@element.io"]) + + let currentVersion = try CI.readMarketingVersion() + let tagName = "nightly/\(currentVersion).\(buildNumber)" + try await CI.run(.name("git"), ["tag", tagName]) + + try await CI.run(.name("git"), ["push", "https://\(apiToken)@\(repoURL)", tagName]) + + logger.info("\n🚀 Successfully tagged nightly: \(tagName)\n") + } + + // MARK: - Private + + private func getRepoURL() throws -> String { + guard let rawURL = try Zsh.run(command: "git ls-remote --get-url origin") else { + throw ValidationError("Could not determine the git remote URL.") + } + + return + rawURL + .replacingOccurrences(of: "http://", with: "") + .replacingOccurrences(of: "https://", with: "") + .replacingOccurrences(of: "git@", with: "") + .replacingOccurrences(of: ".git", with: "") + .trimmingCharacters(in: .whitespacesAndNewlines) + } +} diff --git a/Tools/Sources/Commands/CI/UploadDSYMs.swift b/Tools/Sources/Commands/CI/UploadDSYMs.swift new file mode 100644 index 000000000..b054f2685 --- /dev/null +++ b/Tools/Sources/Commands/CI/UploadDSYMs.swift @@ -0,0 +1,59 @@ +import ArgumentParser +import CommandLineTools +import Foundation + +struct UploadDSYMs: AsyncParsableCommand { + static let configuration = CommandConfiguration(commandName: "upload-dsyms", + abstract: "Uploads dSYMs to Sentry using sentry-cli.", + discussion: "Requires the SENTRY_AUTH_TOKEN environment variable to be set.") + + @Option(help: "The path to the dSYMs directory or file to upload.") + var dsymPath: String + + @Option(help: "The Sentry organization slug.") + var orgSlug = "element" + + @Option(help: "The Sentry project slug.") + var projectSlug = "element-x-ios" + + @Option(help: "The Sentry server URL.") + var url = "https://sentry.tools.element.io/" + + @Option(help: "The maximum number of upload attempts.") + var maxRetries = 5 + + func run() async throws { + guard let authToken = ProcessInfo.processInfo.environment["SENTRY_AUTH_TOKEN"], + !authToken.isEmpty else { + throw ValidationError("SENTRY_AUTH_TOKEN environment variable is not set.") + } + + let command = """ + sentry-cli dif upload \ + --auth-token "\(authToken)" \ + --org "\(orgSlug)" \ + --project "\(projectSlug)" \ + --url "\(url)" \ + --log-level DEBUG \ + "\(dsymPath)" + """ + + var lastError: Swift.Error? + + for attempt in 1...maxRetries { + do { + logger.info("\n📡 Uploading dSYMs to Sentry (attempt \(attempt)/\(maxRetries))…\n") + try await CI.run(.path("/bin/zsh"), ["-cu", command]) + logger.info("\n✅ Successfully uploaded dSYMs to Sentry.\n") + return + } catch { + lastError = error + logger.error("\n❌ Sentry upload attempt \(attempt) failed: \(error.localizedDescription)\n") + } + } + + if let lastError { + throw lastError + } + } +} diff --git a/ci_scripts/ci_common.sh b/ci_scripts/ci_common.sh index 72520d033..df41ca282 100755 --- a/ci_scripts/ci_common.sh +++ b/ci_scripts/ci_common.sh @@ -30,7 +30,7 @@ setup_xcode_cloud_environment () { } install_xcode_cloud_brew_dependencies () { - brew update && brew install xcodegen pkl + brew update && brew install xcodegen pkl getsentry/tools/sentry-cli } setup_github_actions_environment() { diff --git a/ci_scripts/ci_post_clone.sh b/ci_scripts/ci_post_clone.sh index 5a5316ac1..d71682ae1 100755 --- a/ci_scripts/ci_post_clone.sh +++ b/ci_scripts/ci_post_clone.sh @@ -12,5 +12,5 @@ elif [ "$CI_WORKFLOW" = "Element Pro" ]; then # Xcode Cloud automatically fetches the submodules. swift run pipeline configure-element-pro else - bundle exec fastlane config_production + swift run pipeline configure-production fi diff --git a/ci_scripts/ci_post_xcodebuild.sh b/ci_scripts/ci_post_xcodebuild.sh index c8863af3c..14dad89d5 100755 --- a/ci_scripts/ci_post_xcodebuild.sh +++ b/ci_scripts/ci_post_xcodebuild.sh @@ -9,7 +9,7 @@ fetch_unshallow_repository # Upload dsyms no matter the workflow # Perform this step before releasing to github in case it fails. -bundle exec fastlane upload_dsyms_to_sentry dsym_path:"$CI_ARCHIVE_PATH/dSYMs" +swift run -q tools upload-dsyms --dsym-path "$CI_ARCHIVE_PATH/dSYMs" generate_what_to_test_notes @@ -17,5 +17,5 @@ if [ "$CI_WORKFLOW" = "Release" ]; then bundle exec fastlane release_to_github bundle exec fastlane prepare_next_release elif [ "$CI_WORKFLOW" = "Nightly" ]; then - bundle exec fastlane tag_nightly build_number:"$CI_BUILD_NUMBER" + swift run -q tools ci tag-nightly --build-number "$CI_BUILD_NUMBER" fi diff --git a/fastlane/Fastfile b/fastlane/Fastfile index f3c14969e..ec10c59fe 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -9,40 +9,6 @@ before_all do ENV["FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT"] = "180" ENV["FASTLANE_XCODE_LIST_TIMEOUT"] = "180" - ENV["SENTRY_LOG_LEVEL"] = "DEBUG" -end - -lane :config_production do - sh("(cd .. && swift run pipeline update-foss-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_debug_files_upload( - 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 @@ -126,22 +92,6 @@ def rebase_main_onto_current_branch 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'") @@ -171,30 +121,3 @@ private_lane :bump_build_number do 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 = "com.apple.CoreSimulator.SimRuntime.iOS-#{simulator_version.gsub('.', '-')}" - - simulators = sh("xcrun simctl list devices \"iOS #{simulator_version}\" available") - # Use a `(` here to avoid matching `iPhone 14 Pro` on `iPhone 14 Pro Max` for example - existing_simulator = simulators.lines.find { |line| line.include?("#{simulator_name} (") } - - if existing_simulator - UI.message("Found simulator: #{existing_simulator.inspect}") - device_id = existing_simulator.match(/\(([A-F0-9-]+)\)/)[1] # Extract the device ID for the existing simulator - else - UI.message "Simulator #{simulator_name} not found. Creating new simulator…" - create_command = "xcrun simctl create '#{simulator_name}\' #{simulator_type} #{simulator_runtime}" - device_id = sh(create_command).strip # Create a new simulator and get its device ID. - - UI.message "Created new simulator: #{simulator_name} (#{device_id})" - end - - # device_id is unused right now but is useful to check e.g. the boot status of a simulator. -end diff --git a/fastlane/Pluginfile b/fastlane/Pluginfile index 0718ea19d..012aa20c2 100644 --- a/fastlane/Pluginfile +++ b/fastlane/Pluginfile @@ -3,7 +3,6 @@ # Ensure this file is checked in to source control! gem 'fastlane-plugin-xcodegen' -gem 'fastlane-plugin-sentry' gem 'fastlane-plugin-xcconfig' # Until Fastlane includes them directly.