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:
42
.github/workflows/automatic-calendar-version.yml
vendored
Normal file
42
.github/workflows/automatic-calendar-version.yml
vendored
Normal 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
|
||||||
1
.github/workflows/translations-pr.yml
vendored
1
.github/workflows/translations-pr.yml
vendored
@@ -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: |
|
||||||
|
|||||||
@@ -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"])
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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?
|
||||||
54
Tools/Sources/Commands/BumpCalendarVersion.swift
Normal file
54
Tools/Sources/Commands/BumpCalendarVersion.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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.")
|
||||||
44
Tools/Sources/Commands/GenerateSDKMocks.swift
Normal file
44
Tools/Sources/Commands/GenerateSDKMocks.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
@@ -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") }
|
||||||
|
|
||||||
@@ -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()
|
||||||
@@ -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
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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])
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user