Run SwiftFormat.

This commit is contained in:
Doug
2026-05-27 14:38:50 +01:00
committed by Doug
parent 5138e05e47
commit fa67713726
573 changed files with 2426 additions and 2426 deletions
@@ -23,7 +23,7 @@ final class TemplateScreenCoordinator: CoordinatorProtocol {
private let viewModel: TemplateScreenViewModelProtocol
private var cancellables = Set<AnyCancellable>()
private let actionsSubject: PassthroughSubject<TemplateScreenCoordinatorAction, Never> = .init()
var actionsPublisher: AnyPublisher<TemplateScreenCoordinatorAction, Never> {
actionsSubject.eraseToAnyPublisher()
@@ -47,7 +47,7 @@ final class TemplateScreenCoordinator: CoordinatorProtocol {
}
.store(in: &cancellables)
}
func toPresentable() -> AnyView {
AnyView(TemplateScreen(context: viewModel.context))
}
@@ -15,7 +15,7 @@ class TemplateScreenViewModel: TemplateScreenViewModelType, TemplateScreenViewMo
var actionsPublisher: AnyPublisher<TemplateScreenViewModelAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init() {
super.init(initialViewState: TemplateScreenViewState(title: "Template title",
placeholder: "Enter something here",
+2 -2
View File
@@ -6,10 +6,10 @@ struct AppIconBanner: AsyncParsableCommand {
@Argument(help: "Path to the input image.")
var path: String
@Option(help: "Text for the banner.")
var bannerText: String
@MainActor
func run() async throws {
let currentDirectoryURL = URL(filePath: FileManager.default.currentDirectoryPath)
+2 -2
View File
@@ -33,7 +33,7 @@ struct BuildSDK: AsyncParsableCommand {
@Option(help: "The profile to use when building the SDK. Omit this option to build in debug mode.")
var profile: Profile = .reldbg
enum Error: LocalizedError {
case rustupOutputFailure
case missingRustTargets([String])
@@ -133,7 +133,7 @@ struct BuildSDK: AsyncParsableCommand {
let updatedYAMLString = try Yams.serialize(node: projectConfig)
try updatedYAMLString.write(to: yamlURL, atomically: true, encoding: .utf8)
}
func generateSDKMocks() async throws {
var command = GenerateSDKMocks()
command.version = "local"
@@ -5,7 +5,7 @@ import Foundation
struct BumpCalendarVersion: ParsableCommand {
static let configuration = CommandConfiguration(abstract: "A tool that bumps the CalVer every month (if needed), setting the patch back to 0.",
discussion: "The tool assumes the release will be published in 6-days so bumps early.")
func run() throws {
try updateProjectYAML()
try Zsh.run(command: "xcodegen")
@@ -4,7 +4,7 @@ import Foundation
struct AccessibilityTests: AsyncParsableCommand {
static let configuration = CommandConfiguration(commandName: "accessibility-tests",
abstract: "Runs the accessibility test CI workflow.")
@Option(help: "Device name for tests.")
var device = "iPhone 17"
@@ -25,15 +25,15 @@ struct AccessibilityTests: AsyncParsableCommand {
testsFailed = true
logger.error("\n❌ Accessibility tests failed.\n")
}
// Zip results (best-effort, useful for CI artifact uploads)
await CI.zipResults(bundles: ["AccessibilityTests.xcresult"],
outputName: "AccessibilityTests.xcresult.zip")
if testsFailed {
throw ExitCode.failure
}
logger.info("\n✅ Accessibility tests passed.\n")
}
}
+1 -1
View File
@@ -152,7 +152,7 @@ struct CI: ParsableCommand {
else {
throw ValidationError("GITHUB_TOKEN environment variable is not set.")
}
let repoURL = try await CI.gitRepositoryURL()
if let tagName {
@@ -5,24 +5,24 @@ import Yams
struct ConfigureNightly: AsyncParsableCommand {
static let configuration = CommandConfiguration(abstract: "Configures the project for a Nightly build.",
discussion: "Adds the Nightly variant to project.yml, updates secrets, runs xcodegen, and generates the app icon banner.")
@Option(help: "The build number to display on the app icon banner.")
var buildNumber: String
func run() async throws {
guard !buildNumber.isEmpty else {
throw ValidationError("Invalid build number.")
}
try addNightlyVariant()
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)
}
/// Adds the Nightly variant include path to `project.yml` if it isn't already present.
private func addNightlyVariant() throws {
let projectURL = URL.projectDirectory.appending(component: "project.yml")
@@ -35,11 +35,11 @@ struct ConfigureNightly: AsyncParsableCommand {
if projectConfig["include"]?.sequence?.contains(where: { $0.mapping?["path"] == "Variants/Nightly/nightly.yml" }) == false {
projectConfig["include"]?.sequence?.append(["path": "Variants/Nightly/nightly.yml"])
}
let updatedYAMLString = try Yams.serialize(node: projectConfig)
try updatedYAMLString.write(to: projectURL, atomically: true, encoding: .utf8)
}
/// Generates the app icon banner with version and build number.
private func generateAppIconBanner(version: String, buildNumber: String) async throws {
let bannerText = "\(version) (\(buildNumber))"
@@ -8,7 +8,7 @@ struct IntegrationTests: AsyncParsableCommand {
Deletes old log files, runs integration tests, validates that logs are set
to the trace level and don't contain private messages, then collects results.
""")
@Option(help: "Device name for tests.")
var device = "iPhone 17"
+13 -13
View File
@@ -4,21 +4,21 @@ import Foundation
struct PreviewTests: AsyncParsableCommand {
static let configuration = CommandConfiguration(commandName: "preview-tests",
abstract: "Runs the preview test CI workflow, with optional snapshot recording.")
@Flag(help: "Re-record snapshots for tests that fail or are missing a reference image.")
var record = false
private static let scheme = "PreviewTests"
private static let device = "iPhone SE (3rd generation)"
private static let osVersion = "26.4.1"
private static let simulatorType = "com.apple.CoreSimulator.SimDeviceType.iPhone-SE-3rd-generation"
private static let testPlanPath = "PreviewTests/SupportingFiles/PreviewTests.xctestplan"
func run() async throws {
if record {
try setRecordFailures(enabled: true)
}
var testsFailed = false
do {
logger.info("\n🧪 Running preview tests…\n")
@@ -47,41 +47,41 @@ struct PreviewTests: AsyncParsableCommand {
testsFailed = true
}
}
// Collect coverage and test results regardless of test outcome (best-effort).
await CI.collectCoverage(resultBundle: "\(Self.scheme).xcresult", outputName: "preview-cobertura.xml")
await CI.collectTestResults(resultBundle: "\(Self.scheme).xcresult", outputName: "preview-junit.xml")
if testsFailed {
throw ExitCode.failure
}
if !record {
logger.info("\n✅ Preview tests passed.\n")
}
}
// MARK: - Test Plan
/// Enables or disables the `RECORD_FAILURES` environment variable entry in the test plan.
private func setRecordFailures(enabled: Bool) throws {
let url = URL.projectDirectory.appendingPathComponent(Self.testPlanPath)
let data = try Data(contentsOf: url)
guard var plan = try JSONSerialization.jsonObject(with: data) as? [String: Any],
var defaultOptions = plan["defaultOptions"] as? [String: Any],
var envVars = defaultOptions["environmentVariableEntries"] as? [[String: Any]] else {
throw ValidationError("Could not parse test plan at \(Self.testPlanPath).")
}
for index in envVars.indices where envVars[index]["key"] as? String == "RECORD_FAILURES" {
envVars[index]["enabled"] = enabled
break
}
defaultOptions["environmentVariableEntries"] = envVars
plan["defaultOptions"] = defaultOptions
let jsonData = try JSONSerialization.data(withJSONObject: plan, options: [.prettyPrinted, .sortedKeys])
try jsonData.write(to: url)
}
+29 -29
View File
@@ -5,14 +5,14 @@ import Yams
struct ReleaseToGitHub: AsyncParsableCommand {
static let configuration = CommandConfiguration(commandName: "release-to-github",
abstract: "Creates a GitHub release and updates CHANGES.md with generated release notes.")
enum ReleaseError: LocalizedError {
case missingGitHubToken
case failedToCreateRelease(String)
case failedToParseResponse
case missingReleaseNotes
case failedToReadVersion
var errorDescription: String? {
switch self {
case .missingGitHubToken:
@@ -28,45 +28,45 @@ struct ReleaseToGitHub: AsyncParsableCommand {
}
}
}
func run() async throws {
let currentVersion = try CI.readMarketingVersion()
logger.info("Creating GitHub release for version \(currentVersion)…")
let releaseBody = try await createGitHubRelease(version: currentVersion)
try updateChangelog(version: currentVersion, generatedNotes: releaseBody)
let changesFilePath = URL.projectDirectory.appendingPathComponent("CHANGES.md").path
try await CI.run(.name("git"), ["add", changesFilePath])
logger.info("Successfully created GitHub release \(currentVersion) and updated CHANGES.md.")
let targetFilePath = "project.yml"
let xcodeProjPath = "ElementX.xcodeproj"
guard let newVersion = bumpPatchVersion(currentVersion) else {
throw ValidationError("Invalid version format: \(currentVersion)")
}
// Bump the patch version using sed (preserves file formatting)
try await CI.run(.name("sed"), ["-i", "", "s/MARKETING_VERSION: \(currentVersion)/MARKETING_VERSION: \(newVersion)/g", targetFilePath])
logger.info("Version updated from \(currentVersion) to \(newVersion)")
try await CI.run(.name("xcodegen"))
try await CI.gitConfigureGlobals()
try await CI.run(.name("git"), ["add", targetFilePath, xcodeProjPath])
try await CI.run(.name("git"), ["commit", "-m", "Prepare next release"])
try await CI.gitPush()
try await rebaseMainOntoCurrentBranch()
}
// MARK: - Private
private func createGitHubRelease(version: String) async throws -> String {
guard let apiToken = ProcessInfo.processInfo.environment["GITHUB_TOKEN"], !apiToken.isEmpty
else {
@@ -79,23 +79,23 @@ struct ReleaseToGitHub: AsyncParsableCommand {
request.setValue("Bearer \(apiToken)", forHTTPHeaderField: "Authorization")
request.setValue("application/vnd.github+json", forHTTPHeaderField: "Accept")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let body: [String: Any] = ["tag_name": "release/\(version)",
"name": version,
"generate_release_notes": true]
request.httpBody = try JSONSerialization.data(withJSONObject: body)
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
throw ReleaseError.failedToParseResponse
}
guard (200...299).contains(httpResponse.statusCode) else {
let errorBody = String(data: data, encoding: .utf8) ?? "Unknown error"
throw ReleaseError.failedToCreateRelease("HTTP \(httpResponse.statusCode): \(errorBody)")
}
guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
let releaseBody = json["body"] as? String else {
throw ReleaseError.failedToParseResponse
@@ -103,25 +103,25 @@ struct ReleaseToGitHub: AsyncParsableCommand {
return releaseBody
}
private func updateChangelog(version: String, generatedNotes: String) throws {
let changesURL = URL.projectDirectory.appending(component: "CHANGES.md")
// Clean up the generated notes: remove HTML comments and adjust header levels
let cleanedNotes = generatedNotes
.replacingOccurrences(of: "<!-- .*? -->", with: "", options: .regularExpression)
.replacingOccurrences(of: "### ", with: "\n")
.replacingOccurrences(of: "## ", with: "### ")
guard !cleanedNotes.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
throw ReleaseError.missingReleaseNotes
}
let releaseDate = Date().formatted(.iso8601.year().month().day())
let existingContent = try String(contentsOf: changesURL)
let newContent = "## Changes in \(version) (\(releaseDate))\(cleanedNotes)\n\n\(existingContent)"
try newContent.write(to: changesURL, atomically: true, encoding: .utf8)
logger.info("Updated CHANGES.md with release notes.")
}
@@ -131,7 +131,7 @@ struct ReleaseToGitHub: AsyncParsableCommand {
guard let match = version.firstMatch(of: regex), var patch = Int(match.3) else {
return nil
}
let year = String(match.1)
let month = String(match.2)
patch = patch + 1
@@ -146,14 +146,14 @@ struct ReleaseToGitHub: AsyncParsableCommand {
}
logger.info("Current branch: \(currentBranch)")
try await CI.run(.name("git"), ["reset", "--hard"])
try await CI.run(.name("git"), ["checkout", "main"])
try await CI.run(.name("git"), ["pull", "origin", "main"])
try await CI.run(.name("git"), ["rebase", currentBranch])
try await CI.gitPush()
logger.info("Successfully rebased main onto \(currentBranch)")
}
}
+2 -2
View File
@@ -143,9 +143,9 @@ struct RunTests: AsyncParsableCommand {
if let testName {
command += " -only-testing:\(scheme)/\(testName)"
}
command += " | \(formatter)"
try await CI.run(.path("/bin/zsh"), ["-cu", command])
}
}
+4 -4
View File
@@ -4,22 +4,22 @@ 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.")
}
try await CI.gitConfigureGlobals()
let currentVersion = try CI.readMarketingVersion()
let tagName = "nightly/\(currentVersion).\(buildNumber)"
try await CI.gitPush(tagName: tagName)
logger.info("\n🚀 Successfully tagged nightly: \(tagName)\n")
}
}
+11 -11
View File
@@ -10,35 +10,35 @@ struct UITests: AsyncParsableCommand {
swift run tools ci ui-tests --device-type iPad
swift run tools ci ui-tests --device-type iPhone --test-name "ClassName/testName"
""")
enum DeviceType: String, CaseIterable, ExpressibleByArgument {
case iPhone
case iPad
}
@Option(help: "The device type to test (iPhone or iPad).")
var deviceType: DeviceType
@Option(help: "iOS version for the simulator.")
var osVersion = "26.4.1"
@Option(help: "Run only a specific test (format: 'ClassName/testName').")
var testName: String?
private var simulatorName: String {
switch deviceType {
case .iPhone: "iPhone-\(osVersion)"
case .iPad: "iPad-\(osVersion)"
}
}
private var simulatorType: String {
switch deviceType {
case .iPhone: "com.apple.CoreSimulator.SimDeviceType.iPhone-17"
case .iPad: "com.apple.CoreSimulator.SimDeviceType.iPad-A16"
}
}
/// 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.
func run() async throws {
@@ -49,11 +49,11 @@ struct UITests: AsyncParsableCommand {
"--create-simulator-name", simulatorName,
"--create-simulator-type", simulatorType
]
if let testName {
args += ["--test-name", testName]
}
var testsFailed = false
do {
print("\n🧪 Running UI tests (\(deviceType.rawValue))…\n")
@@ -68,11 +68,11 @@ struct UITests: AsyncParsableCommand {
await CI.collectCoverage(resultBundle: "UITests.xcresult", outputName: "ui-cobertura.xml")
await CI.collectTestResults(resultBundle: "UITests.xcresult", outputName: "ui-junit.xml")
if testsFailed {
throw ExitCode.failure
}
print("\n✅ UI tests (\(deviceType.rawValue)) passed.\n")
}
}
+1 -1
View File
@@ -4,7 +4,7 @@ import Foundation
struct UnitTests: AsyncParsableCommand {
static let configuration = CommandConfiguration(commandName: "unit-tests",
abstract: "Runs the unit test CI workflow: lint, unit tests, preview tests, and result collection.")
@Flag(help: "Skip preview tests")
var skipPreviews = false
+10 -10
View File
@@ -5,28 +5,28 @@ 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 --url "\(url)" dif upload \
--auth-token "\(authToken)" \
@@ -35,9 +35,9 @@ struct UploadDSYMs: AsyncParsableCommand {
--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")
@@ -49,7 +49,7 @@ struct UploadDSYMs: AsyncParsableCommand {
logger.error("\n❌ Sentry upload attempt \(attempt) failed: \(error.localizedDescription)\n")
}
}
if let lastError {
throw lastError
}
+4 -4
View File
@@ -4,16 +4,16 @@ import Foundation
struct DownloadStrings: ParsableCommand {
static let configuration = CommandConfiguration(abstract: "A tool to download localizable strings from localazy")
@Flag(help: "Use to download translation keys for all languages")
var allLanguages = false
func run() throws {
try localazyDownload()
try sortStringsFiles()
try swiftgen()
}
private func localazyDownload() throws {
let arguments = allLanguages ? " all" : ""
try Zsh.run(command: "localazy download\(arguments)")
@@ -75,7 +75,7 @@ struct DownloadStrings: ParsableCommand {
guard let closeQuote = line[afterOpen...].firstIndex(of: "\"") else { return nil }
return String(line[afterOpen..<closeQuote])
}
private func swiftgen() throws {
try Zsh.run(command: "swiftgen config run --config Tools/SwiftGen/swiftgen-config.yml")
}
@@ -6,14 +6,14 @@ struct GenerateSDKMocks: AsyncParsableCommand {
enum GenerateSDKMocksError: Error {
case invalidFileUrl
}
static let configuration = CommandConfiguration(abstract: "A tool to setup the mocks for the Matrix Rust SDK")
@Argument(help: "The argument to specify a branch of the SDK. Use `local` to use your local version")
var version: String
private var fileURLFormat = "https://raw.githubusercontent.com/element-hq/matrix-rust-components-swift/%@/Sources/MatrixRustSDK/matrix_sdk_ffi.swift"
func run() async throws {
if version == "local" {
try generateSDKMocks(ffiPath: "\(URL.sdkDirectory.path)/bindings/apple/generated/swift")
@@ -23,12 +23,12 @@ struct GenerateSDKMocks: AsyncParsableCommand {
try FileManager.default.removeItem(atPath: path)
}
}
/// Generates the SDK mocks using Sourcery.
func generateSDKMocks(ffiPath: String) throws {
try Zsh.run(command: "sourcery --sources \(ffiPath) --templates Tools/Sourcery/SDKAutoMockable.stencil --output SDKMocks/Sources/Generated/SDKGeneratedMocks.swift")
}
/// Downloads the specified version of the `matrix_sdk_ffi.swift` file and returns the path to the downloaded file.
func downloadSDK(version: String) async throws -> String {
let fileURLString = String(format: fileURLFormat, version)
+7 -7
View File
@@ -6,7 +6,7 @@ struct Locheck: ParsableCommand {
enum LocheckError: LocalizedError {
case missingMint
case outputError
var errorDescription: String? {
switch self {
case .missingMint:
@@ -16,28 +16,28 @@ struct Locheck: ParsableCommand {
}
}
}
static let configuration = CommandConfiguration(abstract: "A tool that verifies bad strings contained in localization files")
private var stringsDirectoryURL: URL {
.projectDirectory.appendingPathComponent("ElementX/Resources/Localizations")
}
func run() throws {
try checkMint()
try checkStrings()
}
func checkStrings() throws {
guard let output = try Zsh.run(command: "mint run locheck discoverlproj --ignore-missing --ignore lproj_file_missing_from_translation --treat-warnings-as-errors \(stringsDirectoryURL.path)") else {
throw LocheckError.missingMint
}
print(output)
}
private func checkMint() throws {
let result = try Zsh.run(command: "which mint")
if result?.contains("not found") == true {
throw LocheckError.missingMint
}
@@ -4,21 +4,21 @@ import Foundation
struct OutdatedPackages: ParsableCommand {
static let configuration = CommandConfiguration(abstract: "A tool to check outdated package dependencies. Please make sure you have already run setup-project before using this tool.")
private var projectSwiftPMDirectoryURL: URL {
.projectDirectory.appendingPathComponent("ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm")
}
func run() throws {
try checkToolsDependencies()
try checkProjectDependencies()
}
func checkToolsDependencies() throws {
guard let output = try Zsh.run(command: "swift outdated"), !output.isEmpty else { return }
print("outdated tools Swift packages:\n\(output)")
}
func checkProjectDependencies() throws {
guard let output = try Zsh.run(command: "swift outdated", directory: projectSwiftPMDirectoryURL), !output.isEmpty else { return }
print("outdated project Swift packages:\n\(output)")
+6 -6
View File
@@ -4,29 +4,29 @@ import Foundation
struct SetupProject: ParsableCommand {
static let configuration = CommandConfiguration(abstract: "A tool to setup the required components to efficiently run and contribute to Element X iOS")
func run() throws {
try setupGitHooks()
try brewInstall()
try mintPackagesInstall()
try xcodegen()
}
func setupGitHooks() throws {
try Zsh.run(command: "git config core.hooksPath .githooks")
}
func brewInstall() throws {
try Zsh.run(command: "brew install xcodegen swiftgen git-lfs sourcery mint pkl kiliankoe/formulae/swift-outdated localazy/tools/localazy peripheryapp/periphery/periphery")
// Install swiftformat from develop (for now), making sure to avoid conflicts with an existing versioned installation.
// Install swiftformat from develop (for now), making sure to avoid conflicts with an existing versioned installation.
try Zsh.run(command: "source ci_scripts/ci_common.sh && install_swiftformat_head")
}
func mintPackagesInstall() throws {
try Zsh.run(command: "mint install Asana/locheck")
}
func xcodegen() throws {
try Zsh.run(command: "xcodegen")
}
+2 -2
View File
@@ -7,11 +7,11 @@ struct UnusedStrings: ParsableCommand {
@Flag(help: "Save the results to disk instead of printing them.")
var saveToFile = false
func run() throws {
try peripheryScan()
}
func peripheryScan() throws {
print("Analysing project, this may take a while…")
+2 -2
View File
@@ -5,11 +5,11 @@ extension URL {
static var projectDirectory: URL {
URL(fileURLWithPath: FileManager.default.currentDirectoryPath)
}
static var parentDirectory: URL {
.projectDirectory.deletingLastPathComponent()
}
static var sdkDirectory: URL {
.parentDirectory.appendingPathComponent("matrix-rust-sdk")
}