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,
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<Output, Error> {
logger.info("Running \(executable), with arguments: \(arguments)")
let result = try await Subprocess.run(executable,
arguments: arguments,
environment: environment,

View File

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

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