From 00ee97078629ad4e36f89cb49fb391e0af8edd20 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 11 Jan 2026 16:58:53 +0100 Subject: [PATCH] Refactor release commands to support version specification --- app/Build/Commands/PrepareAction.cs | 10 +- app/Build/Commands/UpdateMetadataCommands.cs | 158 +++++++++++++------ 2 files changed, 115 insertions(+), 53 deletions(-) diff --git a/app/Build/Commands/PrepareAction.cs b/app/Build/Commands/PrepareAction.cs index 2f2ffcb2..5b383492 100644 --- a/app/Build/Commands/PrepareAction.cs +++ b/app/Build/Commands/PrepareAction.cs @@ -3,8 +3,10 @@ namespace Build.Commands; public enum PrepareAction { NONE, - - PATCH, - MINOR, - MAJOR, + + BUILD, + MONTH, + YEAR, + + SET, } \ No newline at end of file diff --git a/app/Build/Commands/UpdateMetadataCommands.cs b/app/Build/Commands/UpdateMetadataCommands.cs index 2ecf135f..5ad74591 100644 --- a/app/Build/Commands/UpdateMetadataCommands.cs +++ b/app/Build/Commands/UpdateMetadataCommands.cs @@ -13,13 +13,32 @@ namespace Build.Commands; public sealed partial class UpdateMetadataCommands { [Command("release", Description = "Prepare & build the next release")] - public async Task Release(PrepareAction action) + public async Task Release( + [Option("action", ['a'], Description = "The release action: patch, minor, or major")] PrepareAction action = PrepareAction.NONE, + [Option("version", ['v'], Description = "Set a specific version directly, e.g., 26.1.2")] string? version = null) { if(!Environment.IsWorkingDirectoryValid()) return; - + + // Validate parameters: either action or version must be specified, but not both: + if (action == PrepareAction.NONE && string.IsNullOrWhiteSpace(version)) + { + Console.WriteLine("- Error: You must specify either --action (-a) or --version (-v)."); + return; + } + + if (action != PrepareAction.NONE && !string.IsNullOrWhiteSpace(version)) + { + Console.WriteLine("- Error: You cannot specify both --action and --version. Please use only one."); + return; + } + + // If version is specified, use SET action: + if (!string.IsNullOrWhiteSpace(version)) + action = PrepareAction.SET; + // Prepare the metadata for the next release: - await this.PerformPrepare(action, true); + await this.PerformPrepare(action, true, version); // Build once to allow the Rust compiler to read the changed metadata // and to update all .NET artifacts: @@ -53,11 +72,30 @@ public sealed partial class UpdateMetadataCommands } [Command("prepare", Description = "Prepare the metadata for the next release")] - public async Task Prepare(PrepareAction action) + public async Task Prepare( + [Option("action", ['a'], Description = "The release action: patch, minor, or major")] PrepareAction action = PrepareAction.NONE, + [Option("version", ['v'], Description = "Set a specific version directly, e.g., 26.1.2")] string? version = null) { if(!Environment.IsWorkingDirectoryValid()) return; + // Validate parameters: either action or version must be specified, but not both: + if (action == PrepareAction.NONE && string.IsNullOrWhiteSpace(version)) + { + Console.WriteLine("- Error: You must specify either --action (-a) or --version (-v)."); + return; + } + + if (action != PrepareAction.NONE && !string.IsNullOrWhiteSpace(version)) + { + Console.WriteLine("- Error: You cannot specify both --action and --version. Please use only one."); + return; + } + + // If version is specified, use SET action: + if (!string.IsNullOrWhiteSpace(version)) + action = PrepareAction.SET; + Console.WriteLine("=============================="); Console.Write("- Are you trying to prepare a new release? (y/n) "); var userAnswer = Console.ReadLine(); @@ -66,18 +104,18 @@ public sealed partial class UpdateMetadataCommands Console.WriteLine("- Please use the 'release' command instead"); return; } - - await this.PerformPrepare(action, false); + + await this.PerformPrepare(action, false, version); } - private async Task PerformPrepare(PrepareAction action, bool internalCall) + private async Task PerformPrepare(PrepareAction action, bool internalCall, string? version = null) { if(internalCall) Console.WriteLine("=============================="); - + Console.WriteLine("- Prepare the metadata for the next release ..."); - - var appVersion = await this.UpdateAppVersion(action); + + var appVersion = await this.UpdateAppVersion(action, version); if (!string.IsNullOrWhiteSpace(appVersion.VersionText)) { var buildNumber = await this.IncreaseBuildNumber(); @@ -239,17 +277,6 @@ public sealed partial class UpdateMetadataCommands var pathChangelogs = Path.Combine(Environment.GetAIStudioDirectory(), "wwwroot", "changelog"); var nextBuildNumber = currentBuildNumber + 1; - // - // We assume that most of the time, there will be patch releases: - // - var nextMajor = currentAppVersion.Major; - var nextMinor = currentAppVersion.Minor; - var nextPatch = currentAppVersion.Patch + 1; - - var nextAppVersion = $"{nextMajor}.{nextMinor}.{nextPatch}"; - var nextChangelogFilename = $"v{nextAppVersion}.md"; - var nextChangelogFilePath = Path.Combine(pathChangelogs, nextChangelogFilename); - // // Regarding the next build time: We assume that the next release will take place in one week from now. // Thus, we check how many days this month has left. In the end, we want to predict the year and month @@ -259,6 +286,19 @@ public sealed partial class UpdateMetadataCommands var nextBuildYear = (DateTime.Today + TimeSpan.FromDays(7)).Year; var nextBuildTimeString = $"{nextBuildYear}-{nextBuildMonth:00}-xx xx:xx UTC"; + // + // We assume that most of the time, there will be patch releases: + // + // skipping the first 2 digits for major version + var nextBuildYearShort = nextBuildYear - 2000; + var nextMajor = nextBuildYearShort; + var nextMinor = nextBuildMonth; + var nextPatch = currentAppVersion.Major != nextBuildYearShort || currentAppVersion.Minor != nextBuildMonth ? 1 : currentAppVersion.Patch + 1; + + var nextAppVersion = $"{nextMajor}.{nextMinor}.{nextPatch}"; + var nextChangelogFilename = $"v{nextAppVersion}.md"; + var nextChangelogFilePath = Path.Combine(pathChangelogs, nextChangelogFilename); + var changelogHeader = $""" # v{nextAppVersion}, build {nextBuildNumber} ({nextBuildTimeString}) @@ -355,49 +395,69 @@ public sealed partial class UpdateMetadataCommands await File.WriteAllLinesAsync(pathMetadata, lines, Environment.UTF8_NO_BOM); } - private async Task UpdateAppVersion(PrepareAction action) + private async Task UpdateAppVersion(PrepareAction action, string? version = null) { const int APP_VERSION_INDEX = 0; - + if (action == PrepareAction.NONE) { Console.WriteLine("- No action specified. Skipping app version update."); return new(string.Empty, 0, 0, 0); } - + var pathMetadata = Environment.GetMetadataPath(); var lines = await File.ReadAllLinesAsync(pathMetadata, Encoding.UTF8); var currentAppVersionLine = lines[APP_VERSION_INDEX].Trim(); - var currentAppVersion = AppVersionRegex().Match(currentAppVersionLine); - var currentPatch = int.Parse(currentAppVersion.Groups["patch"].Value); - var currentMinor = int.Parse(currentAppVersion.Groups["minor"].Value); - var currentMajor = int.Parse(currentAppVersion.Groups["major"].Value); - - switch (action) + + int newMajor, newMinor, newPatch; + if (action == PrepareAction.SET && !string.IsNullOrWhiteSpace(version)) { - case PrepareAction.PATCH: - currentPatch++; - break; - - case PrepareAction.MINOR: - currentPatch = 0; - currentMinor++; - break; - - case PrepareAction.MAJOR: - currentPatch = 0; - currentMinor = 0; - currentMajor++; - break; + // Parse the provided version string: + var versionMatch = AppVersionRegex().Match(version); + if (!versionMatch.Success) + { + Console.WriteLine($"- Error: Invalid version format '{version}'. Expected format: major.minor.patch (e.g., 26.1.2)"); + return new(string.Empty, 0, 0, 0); + } + + newMajor = int.Parse(versionMatch.Groups["major"].Value); + newMinor = int.Parse(versionMatch.Groups["minor"].Value); + newPatch = int.Parse(versionMatch.Groups["patch"].Value); } - - var updatedAppVersion = $"{currentMajor}.{currentMinor}.{currentPatch}"; + else + { + // Parse current version and increment based on action: + var currentAppVersion = AppVersionRegex().Match(currentAppVersionLine); + newPatch = int.Parse(currentAppVersion.Groups["patch"].Value); + newMinor = int.Parse(currentAppVersion.Groups["minor"].Value); + newMajor = int.Parse(currentAppVersion.Groups["major"].Value); + + switch (action) + { + case PrepareAction.BUILD: + newPatch++; + break; + + case PrepareAction.MONTH: + newPatch = 1; + newMinor++; + break; + + case PrepareAction.YEAR: + newPatch = 1; + newMinor = 1; + newMajor++; + break; + } + } + + var updatedAppVersion = $"{newMajor}.{newMinor}.{newPatch}"; Console.WriteLine($"- Updating app version from '{currentAppVersionLine}' to '{updatedAppVersion}'."); - + lines[APP_VERSION_INDEX] = updatedAppVersion; await File.WriteAllLinesAsync(pathMetadata, lines, Environment.UTF8_NO_BOM); - - return new(updatedAppVersion, currentMajor, currentMinor, currentPatch); + + return new(updatedAppVersion, newMajor, newMinor, newPatch); } private async Task UpdateLicenceYear(string licenceFilePath)