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:
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
12
Tools/Sources/Commands/CI/ConfigureProduction.swift
Normal file
12
Tools/Sources/Commands/CI/ConfigureProduction.swift
Normal 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"))
|
||||||
|
}
|
||||||
|
}
|
||||||
51
Tools/Sources/Commands/CI/TagNightly.swift
Normal file
51
Tools/Sources/Commands/CI/TagNightly.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
59
Tools/Sources/Commands/CI/UploadDSYMs.swift
Normal file
59
Tools/Sources/Commands/CI/UploadDSYMs.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user