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
This commit is contained in:
Stefan Ceriu
2026-03-05 16:14:09 +02:00
committed by GitHub
parent b1b0966cae
commit eddcc5c605
10 changed files with 133 additions and 85 deletions

View File

@@ -11,7 +11,9 @@ struct CI: ParsableCommand {
UITests.self, UITests.self,
IntegrationTests.self, IntegrationTests.self,
RunTests.self, RunTests.self,
ConfigureNightly.self ConfigureNightly.self,
ConfigureProduction.self,
TagNightly.self
]) ])
static let testOutputDirectory = "test_output" static let testOutputDirectory = "test_output"
@@ -106,6 +108,8 @@ struct CI: ParsableCommand {
environment: Environment = .inherit, environment: Environment = .inherit,
output: Output = .standardOutput, output: Output = .standardOutput,
error: Error = .standardError) async throws -> CollectedResult<Output, Error> { error: Error = .standardError) async throws -> CollectedResult<Output, Error> {
logger.info("Running \(executable), with arguments: \(arguments)")
let result = try await Subprocess.run(executable, let result = try await Subprocess.run(executable,
arguments: arguments, arguments: arguments,
environment: environment, environment: environment,

View File

@@ -17,8 +17,8 @@ struct ConfigureNightly: AsyncParsableCommand {
try addNightlyVariant() try addNightlyVariant()
try Zsh.run(command: "swift run pipeline update-foss-secrets") try await CI.run(.name("swift"), ["run", "pipeline", "update-foss-secrets"])
try Zsh.run(command: "xcodegen") try await CI.run(.name("xcodegen"))
let releaseVersion = try CI.readMarketingVersion() let releaseVersion = try CI.readMarketingVersion()
try await generateAppIconBanner(version: releaseVersion, buildNumber: buildNumber) try await generateAppIconBanner(version: releaseVersion, buildNumber: buildNumber)

View File

@@ -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"))
}
}

View File

@@ -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)
}
}

View File

@@ -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
}
}
}

View File

@@ -30,7 +30,7 @@ setup_xcode_cloud_environment () {
} }
install_xcode_cloud_brew_dependencies () { 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() { setup_github_actions_environment() {

View File

@@ -12,5 +12,5 @@ elif [ "$CI_WORKFLOW" = "Element Pro" ]; then
# Xcode Cloud automatically fetches the submodules. # Xcode Cloud automatically fetches the submodules.
swift run pipeline configure-element-pro swift run pipeline configure-element-pro
else else
bundle exec fastlane config_production swift run pipeline configure-production
fi fi

View File

@@ -9,7 +9,7 @@ fetch_unshallow_repository
# Upload dsyms no matter the workflow # Upload dsyms no matter the workflow
# Perform this step before releasing to github in case it fails. # 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 generate_what_to_test_notes
@@ -17,5 +17,5 @@ if [ "$CI_WORKFLOW" = "Release" ]; then
bundle exec fastlane release_to_github bundle exec fastlane release_to_github
bundle exec fastlane prepare_next_release bundle exec fastlane prepare_next_release
elif [ "$CI_WORKFLOW" = "Nightly" ]; then 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 fi

View File

@@ -9,40 +9,6 @@ before_all do
ENV["FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT"] = "180" ENV["FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT"] = "180"
ENV["FASTLANE_XCODE_LIST_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 end
lane :release_to_github do lane :release_to_github do
@@ -126,22 +92,6 @@ def rebase_main_onto_current_branch
UI.success("Successfully rebased main onto #{current_branch}") UI.success("Successfully rebased main onto #{current_branch}")
end 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 private_lane :setup_git do
sh("git config --global user.name 'Element CI'") sh("git config --global user.name 'Element CI'")
sh("git config --global user.email 'ci@element.io'") 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") build_number = Time.now.strftime("%Y%m%d%H%M")
increment_build_number(build_number: build_number) increment_build_number(build_number: build_number)
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 = "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

View File

@@ -3,7 +3,6 @@
# Ensure this file is checked in to source control! # Ensure this file is checked in to source control!
gem 'fastlane-plugin-xcodegen' gem 'fastlane-plugin-xcodegen'
gem 'fastlane-plugin-sentry'
gem 'fastlane-plugin-xcconfig' gem 'fastlane-plugin-xcconfig'
# Until Fastlane includes them directly. # Until Fastlane includes them directly.