Automatically open a PR to bump the calver (#4167)

* 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.
This commit is contained in:
Doug
2025-06-03 17:52:16 +01:00
committed by GitHub
parent b18ec17f05
commit ea4f1ba9f3
16 changed files with 154 additions and 82 deletions

View File

@@ -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 <releases@riot.im>
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

View File

@@ -31,6 +31,7 @@ jobs:
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e #v7.0.8 uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e #v7.0.8
with: with:
token: ${{ secrets.ELEMENT_BOT_TOKEN }} token: ${{ secrets.ELEMENT_BOT_TOKEN }}
author: ElementRobot <releases@riot.im>
commit-message: Translations update commit-message: Translations update
title: Translations update title: Translations update
body: | body: |

View File

@@ -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. // The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription import PackageDescription
@@ -6,7 +6,7 @@ import PackageDescription
let package = Package( let package = Package(
name: "Element Swift", name: "Element Swift",
platforms: [ platforms: [
.macOS(.v13) .macOS(.v14)
], ],
products: [ products: [
.executable(name: "tools", targets: ["Tools"]) .executable(name: "tools", targets: ["Tools"])

View File

@@ -2,9 +2,7 @@ import ArgumentParser
import SwiftUI import SwiftUI
struct AppIconBanner: AsyncParsableCommand { struct AppIconBanner: AsyncParsableCommand {
static var configuration = CommandConfiguration( static let configuration = CommandConfiguration(abstract: "A Swift command-line tool to add a banner to an app icons.")
abstract: "A Swift command-line tool to add a banner to an app icons."
)
@Argument(help: "Path to the input image.") @Argument(help: "Path to the input image.")
var path: String var path: String

View File

@@ -17,7 +17,7 @@ enum Target: String, ExpressibleByArgument, CaseIterable {
} }
struct BuildSDK: ParsableCommand { 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.") @Argument(help: "An optional argument to specify a branch of the SDK.")
var branch: String? var branch: String?

View File

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

View File

@@ -3,7 +3,7 @@ import CommandLineTools
import Foundation import Foundation
struct DownloadStrings: ParsableCommand { 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") @Flag(help: "Use to download translation keys for all languages")
var allLanguages = false var allLanguages = false

View File

@@ -2,7 +2,7 @@ import ArgumentParser
import Foundation import Foundation
struct GenerateSAS: ParsableCommand { 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" private static let defaultLanguage = "en"
@Flag(name: .shortAndLong, help: "Increase output verbosity.") @Flag(name: .shortAndLong, help: "Increase output verbosity.")

View File

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

View File

@@ -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 { private var stringsDirectoryURL: URL {
.projectDirectory.appendingPathComponent("ElementX/Resources/Localizations") .projectDirectory.appendingPathComponent("ElementX/Resources/Localizations")

View File

@@ -3,7 +3,7 @@ import CommandLineTools
import Foundation import Foundation
struct OutdatedPackages: ParsableCommand { 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") } private var projectSwiftPMDirectoryURL: URL { .projectDirectory.appendingPathComponent("ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm") }

View File

@@ -3,7 +3,7 @@ import CommandLineTools
import Foundation import Foundation
struct SetupProject: ParsableCommand { 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 { func run() throws {
try setupGitHooks() try setupGitHooks()

View File

@@ -3,7 +3,7 @@ import CommandLineTools
import Foundation import Foundation
struct UnusedStrings: ParsableCommand { 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.") @Flag(help: "Save the results to disk instead of printing them.")
var saveToFile = false var saveToFile = false

View File

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

View File

@@ -3,7 +3,7 @@ import Foundation
@main @main
struct Tools: AsyncParsableCommand { 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, subcommands: [BuildSDK.self,
SetupProject.self, SetupProject.self,
OutdatedPackages.self, OutdatedPackages.self,
@@ -12,5 +12,6 @@ struct Tools: AsyncParsableCommand {
GenerateSDKMocks.self, GenerateSDKMocks.self,
GenerateSAS.self, GenerateSAS.self,
AppIconBanner.self, AppIconBanner.self,
UnusedStrings.self]) UnusedStrings.self,
BumpCalendarVersion.self])
} }