From ea4f1ba9f35202699537f8e84d335ea942eda14f Mon Sep 17 00:00:00 2001 From: Doug <6060466+pixlwave@users.noreply.github.com> Date: Tue, 3 Jun 2025 17:52:16 +0100 Subject: [PATCH] Automatically open a PR to bump the calver (#4167) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update our tools package to Swift 6.1 Also improves the package layout with subdirectories 📁 * Update GenerateSDKMocks to be an Async command. * Add a tool to bump the project CalVer every month. * Add a workflow to automatically bump the calendar version. Note: This only does year & month, the patch is handled by the release script. --- .../workflows/automatic-calendar-version.yml | 42 ++++++++++++ .github/workflows/translations-pr.yml | 1 + Package.swift | 4 +- .../{ => Commands}/AppIconBanner.swift | 4 +- Tools/Sources/{ => Commands}/BuildSDK.swift | 2 +- .../Commands/BumpCalendarVersion.swift | 54 +++++++++++++++ .../{ => Commands}/DownloadStrings.swift | 2 +- .../Sources/{ => Commands}/GenerateSAS.swift | 2 +- Tools/Sources/Commands/GenerateSDKMocks.swift | 44 ++++++++++++ Tools/Sources/{ => Commands}/Locheck.swift | 2 +- .../{ => Commands}/OutdatedPackages.swift | 2 +- .../Sources/{ => Commands}/SetupProject.swift | 2 +- .../{ => Commands}/UnusedStrings.swift | 2 +- Tools/Sources/{ => Extensions}/URL.swift | 0 Tools/Sources/GenerateSDKMocks.swift | 68 ------------------- Tools/Sources/Tools.swift | 5 +- 16 files changed, 154 insertions(+), 82 deletions(-) create mode 100644 .github/workflows/automatic-calendar-version.yml rename Tools/Sources/{ => Commands}/AppIconBanner.swift (93%) rename Tools/Sources/{ => Commands}/BuildSDK.swift (98%) create mode 100644 Tools/Sources/Commands/BumpCalendarVersion.swift rename Tools/Sources/{ => Commands}/DownloadStrings.swift (91%) rename Tools/Sources/{ => Commands}/GenerateSAS.swift (98%) create mode 100644 Tools/Sources/Commands/GenerateSDKMocks.swift rename Tools/Sources/{ => Commands}/Locheck.swift (95%) rename Tools/Sources/{ => Commands}/OutdatedPackages.swift (93%) rename Tools/Sources/{ => Commands}/SetupProject.swift (93%) rename Tools/Sources/{ => Commands}/UnusedStrings.swift (94%) rename Tools/Sources/{ => Extensions}/URL.swift (100%) delete mode 100644 Tools/Sources/GenerateSDKMocks.swift diff --git a/.github/workflows/automatic-calendar-version.yml b/.github/workflows/automatic-calendar-version.yml new file mode 100644 index 000000000..cc8c4b115 --- /dev/null +++ b/.github/workflows/automatic-calendar-version.yml @@ -0,0 +1,42 @@ +name: Automatic Calendar Version +on: + schedule: + # At 03:00 UTC every Tuesday in preparation for an RC. + # The tool assumes the release is published in 6-days (the following Monday). + # Note: Most of these runs will be no-op until the release month changes. + - cron: '0 3 * * 2' + workflow_dispatch: + +# Bumps the year and month, resetting the patch. +# Patch bumps are handled by the release script. +jobs: + automatic-calendar-version: + runs-on: macos-15 + # Skip in forks + if: github.repository == 'element-hq/element-x-ios' + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup environment + run: + source ci_scripts/ci_common.sh && setup_github_actions_environment + + - name: Bump the CalVer if needed + run: swift run tools bump-calendar-version + + - name: Create a PR for the new version + uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e #v7.0.8 + with: + token: ${{ secrets.ELEMENT_BOT_TOKEN }} + author: ElementRobot + commit-message: Bump the calendar version ready for the next release + title: Bump the calendar version ready for the next release + body: | + - Version bump + labels: pr-build + branch: version/bump + base: develop + add-paths: | + *.yml + *.xcodeproj diff --git a/.github/workflows/translations-pr.yml b/.github/workflows/translations-pr.yml index 536c5ba73..c86286528 100644 --- a/.github/workflows/translations-pr.yml +++ b/.github/workflows/translations-pr.yml @@ -31,6 +31,7 @@ jobs: uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e #v7.0.8 with: token: ${{ secrets.ELEMENT_BOT_TOKEN }} + author: ElementRobot commit-message: Translations update title: Translations update body: | diff --git a/Package.swift b/Package.swift index 0bdec3032..ea3a28b80 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.7 +// swift-tools-version: 6.1 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -6,7 +6,7 @@ import PackageDescription let package = Package( name: "Element Swift", platforms: [ - .macOS(.v13) + .macOS(.v14) ], products: [ .executable(name: "tools", targets: ["Tools"]) diff --git a/Tools/Sources/AppIconBanner.swift b/Tools/Sources/Commands/AppIconBanner.swift similarity index 93% rename from Tools/Sources/AppIconBanner.swift rename to Tools/Sources/Commands/AppIconBanner.swift index 4d2369e4b..5dc875c31 100644 --- a/Tools/Sources/AppIconBanner.swift +++ b/Tools/Sources/Commands/AppIconBanner.swift @@ -2,9 +2,7 @@ import ArgumentParser import SwiftUI struct AppIconBanner: AsyncParsableCommand { - static var configuration = CommandConfiguration( - abstract: "A Swift command-line tool to add a banner to an app icons." - ) + static let configuration = CommandConfiguration(abstract: "A Swift command-line tool to add a banner to an app icons.") @Argument(help: "Path to the input image.") var path: String diff --git a/Tools/Sources/BuildSDK.swift b/Tools/Sources/Commands/BuildSDK.swift similarity index 98% rename from Tools/Sources/BuildSDK.swift rename to Tools/Sources/Commands/BuildSDK.swift index 052687e0b..8901a57b2 100644 --- a/Tools/Sources/BuildSDK.swift +++ b/Tools/Sources/Commands/BuildSDK.swift @@ -17,7 +17,7 @@ enum Target: String, ExpressibleByArgument, CaseIterable { } struct BuildSDK: ParsableCommand { - static var configuration = CommandConfiguration(abstract: "A tool to checkout and build MatrixRustSDK locally for development.") + static let configuration = CommandConfiguration(abstract: "A tool to checkout and build MatrixRustSDK locally for development.") @Argument(help: "An optional argument to specify a branch of the SDK.") var branch: String? diff --git a/Tools/Sources/Commands/BumpCalendarVersion.swift b/Tools/Sources/Commands/BumpCalendarVersion.swift new file mode 100644 index 000000000..13a422d41 --- /dev/null +++ b/Tools/Sources/Commands/BumpCalendarVersion.swift @@ -0,0 +1,54 @@ +import ArgumentParser +import CommandLineTools +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") + } + + /// Updates the project YAML with the new version. + private func updateProjectYAML() throws { + let yamlURL = URL.projectDirectory.appendingPathComponent("project.yml") + let yamlString = try String(contentsOf: yamlURL) + + // Use regex instead of Yams to preserve any whitespace, comments etc in the file. + let marketingVersionRegex = /MARKETING_VERSION:\s*([^\s]+)/ + var updatedYAMLString = "" + + yamlString.enumerateLines { line, _ in + let processedLine = if let match = line.firstMatch(of: marketingVersionRegex), + let newVersion = try? generateNewVersion(from: String(match.1)) { + line.replacingOccurrences(of: match.1, with: newVersion) + } else { + line + } + + updatedYAMLString.append(processedLine + "\n") + } + + try updatedYAMLString.write(to: yamlURL, atomically: true, encoding: .utf8) + } + + /// Returns the new version string if a change is necessary. + /// + /// **Note:** This tool does *not* handle patch bumps, those are done automatically in the release script. + private func generateNewVersion(from currentVersion: String) throws -> String? { + let releaseDate = Date.now.addingTimeInterval(6 * 24 * 60 * 60) // Always assume we're building the RC. + let releaseYear = Calendar.current.component(.year, from: releaseDate) % 1000 // We use the short year. + let releaseMonth = Calendar.current.component(.month, from: releaseDate) + let versionComponents = currentVersion.split(separator: ".").compactMap { Int($0) } + + guard versionComponents.count == 3 else { fatalError("Unexpected version format: \(currentVersion)") } + + if versionComponents[0] != releaseYear || versionComponents[1] != releaseMonth { + return "\(releaseYear).\(String(format: "%02d", releaseMonth)).0" + } else { + return nil + } + } +} diff --git a/Tools/Sources/DownloadStrings.swift b/Tools/Sources/Commands/DownloadStrings.swift similarity index 91% rename from Tools/Sources/DownloadStrings.swift rename to Tools/Sources/Commands/DownloadStrings.swift index c3862a817..772a51f86 100644 --- a/Tools/Sources/DownloadStrings.swift +++ b/Tools/Sources/Commands/DownloadStrings.swift @@ -3,7 +3,7 @@ import CommandLineTools import Foundation struct DownloadStrings: ParsableCommand { - static var configuration = CommandConfiguration(abstract: "A tool to download localizable strings from localazy") + 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 diff --git a/Tools/Sources/GenerateSAS.swift b/Tools/Sources/Commands/GenerateSAS.swift similarity index 98% rename from Tools/Sources/GenerateSAS.swift rename to Tools/Sources/Commands/GenerateSAS.swift index 9a91c9f26..c469b97da 100644 --- a/Tools/Sources/GenerateSAS.swift +++ b/Tools/Sources/Commands/GenerateSAS.swift @@ -2,7 +2,7 @@ import ArgumentParser import Foundation struct GenerateSAS: ParsableCommand { - static var configuration = CommandConfiguration(abstract: "A tool to download and generate SAS localization strings") + static let configuration = CommandConfiguration(abstract: "A tool to download and generate SAS localization strings") private static let defaultLanguage = "en" @Flag(name: .shortAndLong, help: "Increase output verbosity.") diff --git a/Tools/Sources/Commands/GenerateSDKMocks.swift b/Tools/Sources/Commands/GenerateSDKMocks.swift new file mode 100644 index 000000000..93c3c6a8d --- /dev/null +++ b/Tools/Sources/Commands/GenerateSDKMocks.swift @@ -0,0 +1,44 @@ +import ArgumentParser +import CommandLineTools +import Foundation + +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") + } else { + let path = try await downloadSDK(version: version) + try generateSDKMocks(ffiPath: path) + 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 ElementX/Sources/Mocks/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) + guard let fileURL = URL(string: fileURLString) else { + throw GenerateSDKMocksError.invalidFileUrl + } + + let (tempURL, _) = try await URLSession.shared.download(from: fileURL) + let sdkFilePath = NSTemporaryDirectory().appending("matrix_sdk_ffi.swift") + try FileManager.default.moveItem(at: tempURL, to: URL(fileURLWithPath: sdkFilePath)) + return sdkFilePath + } +} diff --git a/Tools/Sources/Locheck.swift b/Tools/Sources/Commands/Locheck.swift similarity index 95% rename from Tools/Sources/Locheck.swift rename to Tools/Sources/Commands/Locheck.swift index cf5d36022..f8cb30b90 100644 --- a/Tools/Sources/Locheck.swift +++ b/Tools/Sources/Commands/Locheck.swift @@ -17,7 +17,7 @@ struct Locheck: ParsableCommand { } } - static var configuration = CommandConfiguration(abstract: "A tool that verifies bad strings contained in localization files") + static let configuration = CommandConfiguration(abstract: "A tool that verifies bad strings contained in localization files") private var stringsDirectoryURL: URL { .projectDirectory.appendingPathComponent("ElementX/Resources/Localizations") diff --git a/Tools/Sources/OutdatedPackages.swift b/Tools/Sources/Commands/OutdatedPackages.swift similarity index 93% rename from Tools/Sources/OutdatedPackages.swift rename to Tools/Sources/Commands/OutdatedPackages.swift index 941eeece8..c3fb4aa07 100644 --- a/Tools/Sources/OutdatedPackages.swift +++ b/Tools/Sources/Commands/OutdatedPackages.swift @@ -3,7 +3,7 @@ import CommandLineTools import Foundation struct OutdatedPackages: ParsableCommand { - static var configuration = CommandConfiguration(abstract: "A tool to check outdated package dependencies. Please make sure you have already run setup-project before using this tool.") + 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") } diff --git a/Tools/Sources/SetupProject.swift b/Tools/Sources/Commands/SetupProject.swift similarity index 93% rename from Tools/Sources/SetupProject.swift rename to Tools/Sources/Commands/SetupProject.swift index 7e05510ed..4586dfc73 100644 --- a/Tools/Sources/SetupProject.swift +++ b/Tools/Sources/Commands/SetupProject.swift @@ -3,7 +3,7 @@ import CommandLineTools import Foundation struct SetupProject: ParsableCommand { - static var configuration = CommandConfiguration(abstract: "A tool to setup the required components to efficiently run and contribute to Element X iOS") + 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() diff --git a/Tools/Sources/UnusedStrings.swift b/Tools/Sources/Commands/UnusedStrings.swift similarity index 94% rename from Tools/Sources/UnusedStrings.swift rename to Tools/Sources/Commands/UnusedStrings.swift index aa8b71302..9418800f6 100644 --- a/Tools/Sources/UnusedStrings.swift +++ b/Tools/Sources/Commands/UnusedStrings.swift @@ -3,7 +3,7 @@ import CommandLineTools import Foundation struct UnusedStrings: ParsableCommand { - static var configuration = CommandConfiguration(abstract: "Generates a report showing which strings aren't used in the project.") + static let configuration = CommandConfiguration(abstract: "Generates a report showing which strings aren't used in the project.") @Flag(help: "Save the results to disk instead of printing them.") var saveToFile = false diff --git a/Tools/Sources/URL.swift b/Tools/Sources/Extensions/URL.swift similarity index 100% rename from Tools/Sources/URL.swift rename to Tools/Sources/Extensions/URL.swift diff --git a/Tools/Sources/GenerateSDKMocks.swift b/Tools/Sources/GenerateSDKMocks.swift deleted file mode 100644 index 6f8b9e847..000000000 --- a/Tools/Sources/GenerateSDKMocks.swift +++ /dev/null @@ -1,68 +0,0 @@ -import ArgumentParser -import CommandLineTools -import Foundation - -struct GenerateSDKMocks: ParsableCommand { - enum GenerateSDKMocksError: Error { - case invalidFileUrl - } - - static var 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() throws { - if version == "local" { - try generateSDKMocks(ffiPath: "\(URL.sdkDirectory.path)/bindings/apple/generated/swift") - } else { - try downloadSDK(version: version) { path in - try generateSDKMocks(ffiPath: path) - 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 ElementX/Sources/Mocks/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, completionHandler: @escaping (String) throws -> Void) throws { - var sdkFilePath = "" - let fileURLString = String(format: fileURLFormat, version) - guard let fileURL = URL(string: fileURLString) else { - throw GenerateSDKMocksError.invalidFileUrl - } - - let semaphore = DispatchSemaphore(value: 0) - - let task = URLSession.shared.downloadTask(with: fileURL) { tempURL, _, error in - guard let tempURL = tempURL else { - if let error = error { - print("Error downloading SDK file: \(error)") - } else { - print("Unknown error occurred while downloading SDK file.") - } - return - } - - do { - sdkFilePath = NSTemporaryDirectory().appending("matrix_sdk_ffi.swift") - try FileManager.default.moveItem(at: tempURL, to: URL(fileURLWithPath: sdkFilePath)) - try completionHandler(sdkFilePath) - semaphore.signal() - } catch { - print("Error setting up SDK: \(error)") - semaphore.signal() - } - } - - task.resume() - - _ = semaphore.wait(timeout: .distantFuture) - } -} diff --git a/Tools/Sources/Tools.swift b/Tools/Sources/Tools.swift index fd76870e5..761ff0dae 100644 --- a/Tools/Sources/Tools.swift +++ b/Tools/Sources/Tools.swift @@ -3,7 +3,7 @@ import Foundation @main struct Tools: AsyncParsableCommand { - static var configuration = CommandConfiguration(abstract: "A collection of command line tools for ElementX", + static let configuration = CommandConfiguration(abstract: "A collection of command line tools for ElementX", subcommands: [BuildSDK.self, SetupProject.self, OutdatedPackages.self, @@ -12,5 +12,6 @@ struct Tools: AsyncParsableCommand { GenerateSDKMocks.self, GenerateSAS.self, AppIconBanner.self, - UnusedStrings.self]) + UnusedStrings.self, + BumpCalendarVersion.self]) }