From 5467de0d1ae363cd718c668eabcef2ad99daed69 Mon Sep 17 00:00:00 2001 From: qwsdcvghyu89 <61093706+qwsdcvghyu89@users.noreply.github.com> Date: Thu, 4 Jun 2026 16:15:02 +1000 Subject: [PATCH] Add version update logic and improve tool configuration - Introduced `ChangeVersion` method for version manipulation based on major, minor, and patch increments. - Updated .gitignore to include `obj/` directory. - Added `Aigamo.ResXGenerator` dependency and configuration for ResX generation. - Refactored argument parsing in `Program.cs` for improved readability and error handling. - Cleaned up `Exceptions.resx` file format and extended comments for clarity. --- aeqw89.tools.Publish/.gitignore | 3 +- aeqw89.tools.Publish/Exceptions.resx | 261 ++++-- aeqw89.tools.Publish/Program.cs | 766 ++++++------------ aeqw89.tools.Publish/ProjectFile.cs | 47 ++ .../aeqw89.tools.Publish.csproj | 8 + 5 files changed, 501 insertions(+), 584 deletions(-) diff --git a/aeqw89.tools.Publish/.gitignore b/aeqw89.tools.Publish/.gitignore index 6dd29b7..7a0795f 100644 --- a/aeqw89.tools.Publish/.gitignore +++ b/aeqw89.tools.Publish/.gitignore @@ -1 +1,2 @@ -bin/ \ No newline at end of file +bin/** +obj/** diff --git a/aeqw89.tools.Publish/Exceptions.resx b/aeqw89.tools.Publish/Exceptions.resx index 32544d0..daa28ec 100644 --- a/aeqw89.tools.Publish/Exceptions.resx +++ b/aeqw89.tools.Publish/Exceptions.resx @@ -1,75 +1,196 @@  - - - - - - - - text/microsoft-resx - - - 1.3 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - You must specify a mode; allowed modes are [overwrite|increment] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + You must specify a mode; allowed modes are [overwrite|increment] + + + + The mode '{0}' is invalid, the valid modes are [overwrite|increment] + + + + The increment target '{0}' is invalid, the valid increment targets are [patch|minor|patch] + + + + You must specify an increment target if you specified an increment mode; allowed increment targets are [patch|minor|major] + + + + You must specify at least one destination. + + + + No project file was found within the current directory. + + + + The flag '{0}' requires exactly '{1}' parameters. You have entered '{2}'. + + + + The '{0}' flag requires that argument with index '{1}' be of type '{2}'. You have entered '{3}' which has failed to be converted. + + + + The version string '{0}' is in an unidentifiable format. + + + + Something went wrong; an attempt was made to load a non .csproj file as a project file. + + + + Something went wrong loading this file; {0} + + + + The directory '{0}' contains multiple .csproj files; this tool can only process one at a time. + + + + The project file '{0}' is irreparable becuase it is missing a '{1}' property, and the value cannot be guessed. + + + + Failed to pack with exit code '{0}'; ensure that 'dotnet build' succeeds before running this program. + + + + Could not delete temporary directory '{0}' due to error '{1}' + + + + The cloud host '{0}' is not an entry on this user's config file. + + + + Failde to prepare an upload directory on the path {0} for the remote host '{1}', after being detected as a {2} host. Server error is '{3}' + + + + The 'dotnet nuget push' command failed with error message '{0}' + + + + The destination '{0}' is unrecognizable. + + \ No newline at end of file diff --git a/aeqw89.tools.Publish/Program.cs b/aeqw89.tools.Publish/Program.cs index 046afa2..7421d93 100644 --- a/aeqw89.tools.Publish/Program.cs +++ b/aeqw89.tools.Publish/Program.cs @@ -5,6 +5,7 @@ using Renci.SshNet; using Spectre.Console; using aeqw89.xml.ProjectFile; + namespace aeqw89.tools.Publish; /* @@ -20,74 +21,80 @@ namespace aeqw89.tools.Publish; * e.g. publish overwrite|increment [patch|minor|major] destinations [flags] */ + + public static class Program { - public static Mode Mode { get; set; } - public static IncrementTarget? Target { get; set; } - public static string[] Destinations { get; set; } - public static Dictionary Flags { get; set; } - public static bool Verbose { get; set; } = false; + const long BufferSize = 80 * 1024; // 80 KB - public static List RestoreActions { get; set; } = []; + internal record RunContext(List RestoreActions, ArgValues Args, string TempDir) { + public void Restore(bool deleteTempDir = true) { + RestoreActions.ForEach(x => x()); - public static void ReadArgs(string[] args) { - if (args.Length < 1) { - ShowError(Exceptions.missing_mode.EscapeMarkup()); - ShowHelp(); - return; + if (deleteTempDir) { + try { + if (!Directory.Exists(TempDir)) return; + Directory.Delete(TempDir, true); + AnsiConsole.MarkupLine("[yellow]Cleaned up temporary directory[/]"); + } + catch (Exception e) { + ShowError(string.Format(Exceptions.failed_to_clean_up.EscapeMarkup(), TempDir.EscapeMarkup(), + e.ToString().EscapeMarkup())); + } + } + + RestoreActions.Clear(); } + } + + internal record ArgValues(Mode Mode, string[] Destinations, Dictionary Flags, bool Verbose, IncrementTarget? Target); + static Result ReadArgs(string[] args) { + if (args.Length < 1) // not enough args (min = 2) + return new ReadableError(Exceptions.missing_mode.EscapeMarkup()); - Mode = args[0] switch { + var mode = args[0].ToLower() switch { "overwrite" => Mode.Overwrite, "increment" => Mode.Increment, - _ => (Mode)(-1) + _ => (Mode)(-1) // mode must be one of two values }; - if (Mode == (Mode)(-1)) { - ShowError(Exceptions.could_not_parse_mode.EscapeMarkup(), args[0].EscapeMarkup()); - ShowHelp(); - return; - } + if (mode == (Mode)(-1)) // invalid mode + return new ReadableError(string.Format(Exceptions.could_not_parse_mode.EscapeMarkup(), args[0].EscapeMarkup())); - if (args.Length < 2) { - if (Mode == Mode.Increment) - ShowError(Exceptions.missing_increment_target.EscapeMarkup()); - else if (Mode == Mode.Overwrite) - ShowError(Exceptions.missing_destinations.EscapeMarkup()); - ShowHelp(); - return; - } + if (args.Length < 2) // not enough args (min = 2) + return new ReadableError(mode switch { + Mode.Increment => Exceptions.missing_increment_target.EscapeMarkup(), + Mode.Overwrite => Exceptions.missing_destinations.EscapeMarkup(), + _ => throw new UnreachableException() + }); - Destinations = args[1..]; - Flags = []; - if (Mode == Mode.Increment) { - if (args.Length < 3) { - ShowError(Exceptions.missing_destinations.EscapeMarkup()); - ShowHelp(); - return; - } + var destinations = args[1..]; // destinations is variarg. + Dictionary flags = []; // flags is parsed last + IncrementTarget? target = null; + if (mode == Mode.Increment) { + if (args.Length < 3) // increment mode requires target version in addition to destinations + return new ReadableError(Exceptions.missing_destinations.EscapeMarkup()); - Destinations = args[2..]; + destinations = args[2..]; // target is args[1] (args[0] is mode) - Target = args[1] switch { + target = args[1].ToLower() switch { "patch" => IncrementTarget.Patch, "minor" => IncrementTarget.Minor, "major" => IncrementTarget.Major, _ => (IncrementTarget)(-1) }; - if (Target == (IncrementTarget)(-1)) { - ShowError(Exceptions.could_not_parse_target.EscapeMarkup(), args[1].EscapeMarkup()); - ShowHelp(); - return; - } + if (target == (IncrementTarget)(-1)) // unrecognizable target entered + return new ReadableError(string.Format(Exceptions.could_not_parse_target.EscapeMarkup(), args[1].EscapeMarkup())); } - string? firstFlag = Destinations.FirstOrDefault(x => x.StartsWith('-')); - if (firstFlag == null) return; - string[] flags = Destinations.SkipWhile(x => x != firstFlag).ToArray(); - Flags = ReadFlags(flags); - Destinations = Destinations.TakeWhile(x => x != firstFlag).ToArray(); - Verbose = Flags.ContainsKey("--verbose") || Flags.ContainsKey("-v"); + string? firstFlag = destinations.FirstOrDefault(x => x.StartsWith('-')); // find the first arg that starts with '-' signifying a flag. + if (firstFlag == null) // no flags case - return early. + return new Ok(new ArgValues(mode, destinations, flags, false, target)); + string[] flagsRaw = destinations.SkipWhile(x => x != firstFlag).ToArray(); // extract flags from destinations. + flags = ReadFlags(flagsRaw); // get flags as dictionary to args (pattern is -flag0 [arg0] [arg1] -flag1 ...) + destinations = destinations.TakeWhile(x => x != firstFlag).ToArray(); // remove flags from destinations. + bool verbose = flags.ContainsKey("--verbose") || flags.ContainsKey("-v"); // verbosity switch is extracted early. + return new Ok(new ArgValues(mode, destinations, flags, verbose, target)); // return with fully parsed flags. } private static Dictionary ReadFlags(string[] flags) { @@ -105,501 +112,234 @@ public static class Program { collected.Add(flag); } + result[lastKey] = collected.ToArray(); return result; } - public static async Task Main(string[] args) { - ReadArgs(args); - - Console.CancelKeyPress += (sender, eventArgs) => { - RestoreActions.ForEach(x => x()); - }; + record ProjectResult(string PackageId, string Version); + static async Task> PrepareProject(RunContext rctx, StatusContext ctx) { + ctx.Status = "Locating project file"; + if (!ProjectFile.TryLoad(Environment.CurrentDirectory, out var projectFile, out var error)) + return new ReadableError(error); - string packageId = ""; - string version = ""; - int destinationsProcessed = 0; + string packageId = projectFile.GetPackageId(); + string version; try { - var result = AnsiConsole.Status() - .Spinner(Spinner.Known.Dots) - .Start("Preparing project", ctx => { - ctx.Status = "Locating project file"; - if (!ProjectFile.TryLoad(Environment.CurrentDirectory, out var projectFile, out var error)) { - ShowError(error.EscapeMarkup()); - return false; - } - - packageId = projectFile.GetPackageId(); - - try { - projectFile.Backup(); - RestoreActions.Add(() => { - projectFile.Restore(); - AnsiConsole.MarkupLine("[yellow]Restored project file from backup.[/]"); - }); - - - if (Verbose) - AnsiConsole.WriteLine( - $"Created project file backup at {projectFile.GetDefaultBackupLocation()}"); - - ctx.Status = "Repairing project file"; - if (!Flags.ContainsKey("--skip-repair")) - if (!projectFile.TryRepair(out error)) { - ShowError(error.EscapeMarkup()); - projectFile.Restore(); - return false; - } - - if (Mode == Mode.Increment && !Flags.ContainsKey("--simulate")) { - int delta = 1; - if (Flags.TryGetValue("--delta", out var deltaStrings)) { - if (deltaStrings.Length != 1) { - ShowError(Exceptions.flag_parameter_length_incorrect.EscapeMarkup(), "--delta", 1, - deltaStrings.Length); - projectFile.Restore(); - ShowHelp(); - return false; - } - - if (!int.TryParse(deltaStrings[0], out delta)) { - ShowError(Exceptions.flag_parameter_type_incorrect.EscapeMarkup(), "--delta", 0, - nameof(Int32), - deltaStrings[0]); - projectFile.Restore(); - ShowHelp(); - return false; - } - } - - ctx.Status = "Updating version"; - var version = projectFile.GetVersion(); - version = ChangeVersion(version, delta, Target ?? IncrementTarget.Patch); - - projectFile.SetVersion(version); - } - } - catch (Exception e) { - ShowError(Exceptions.generic_error.EscapeMarkup(), e.ToString().EscapeMarkup()); - RestoreActions.ForEach(x => x()); - return false; - } - - version = projectFile.GetVersion(); - - if (!Flags.ContainsKey("--simulate")) { - try { - var packageReferences = projectFile.GetPackageReferences(); - foreach (var reference in packageReferences.Where(x => !projectFile.IsTransitive(x))) - projectFile.SetPrivateAssets(reference, PrivateAssetsValue.All); - foreach (var reference in packageReferences.Where(x => projectFile.IsTransitive(x))) - projectFile.RemovePackage(reference); - - HashSet visited = []; - var projectReferences = new Queue(projectFile.GetProjectReferences().Cast()); - while (projectReferences.Count != 0) { - var reference = projectReferences.Dequeue(); - visited.Add(reference.Include); - - if (Verbose) - AnsiConsole.WriteLine( - $"Processing project reference {reference.Include} out of {visited.Count} so far"); - - projectFile.SetPrivateAssets(reference, PrivateAssetsValue.All); - string pathToReferencedProjectFile = projectFile.GetAbsoluteIncludePath(reference); - if (!ProjectFile.TryLoad(pathToReferencedProjectFile, out var referencedProjectFile, - out error)) { - ShowError(error.EscapeMarkup()); - RestoreActions.ForEach(x => x()); - return false; - } - - var referencedPackageReferences = referencedProjectFile.GetPackageReferences(); - foreach (var package in referencedPackageReferences) { - if (Verbose) - AnsiConsole.WriteLine( - $"Hoisting package {package.Include} from {pathToReferencedProjectFile}"); - var hoisted = projectFile.AddPackage(package); - projectFile.SetTransitive(hoisted, true); - projectFile.SetPrivateAssets(hoisted, PrivateAssetsValue.None); - referencedProjectFile.SetPrivateAssets(package, PrivateAssetsValue.All); - } - - var referencedProjectReferences = referencedProjectFile.GetProjectReferences(); - foreach (var project in referencedProjectReferences) { - if (!visited.Contains(project.Include)) - projectReferences.Enqueue(project); - } - } - } - catch (Exception e) { - ShowError(Exceptions.generic_error.EscapeMarkup(), e.ToString().EscapeMarkup()); - RestoreActions.ForEach(x => x()); - return false; - } - } - - projectFile.Save(); - return true; - }); - - if (!result) { - return; - } - - var outDir = Path.GetRandomFileName(); - RestoreActions.Add(() => { - try { - if (!Directory.Exists(outDir)) return; - Directory.Delete(outDir, true); - AnsiConsole.MarkupLine("[yellow]Cleaned up temporary directory[/]"); - } - catch (Exception e) { - ShowError(string.Format(Exceptions.failed_to_clean_up.EscapeMarkup(), outDir.EscapeMarkup(), - e.ToString().EscapeMarkup())); - } + projectFile.Backup(); + rctx.RestoreActions.Add(() => { + projectFile.Restore(); + AnsiConsole.MarkupLine("[yellow]Restored project file from backup.[/]"); }); - string processError = ""; - var exitCode = await AnsiConsole.Status() - .Spinner(Spinner.Known.Dots) - .StartAsync("Creating package with 'dotnet pack' ", async ctx => { - var p = Process.Start(new ProcessStartInfo() { - FileName = "dotnet", - Arguments = $"pack -o {outDir}", - WorkingDirectory = Environment.CurrentDirectory, - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true - }); - - CancellationTokenSource cts = new CancellationTokenSource(); - StringBuilder errorLines = new(); - p?.ErrorDataReceived += (sender, eventArgs) => { - cts.Cancel(); - if (Verbose && eventArgs.Data != null) - AnsiConsole.WriteLine(eventArgs.Data); - }; - bool success = false; - p?.OutputDataReceived += (sender, eventArgs) => { - if (eventArgs.Data?.ToLower().Contains("press any key") == true) - cts.Cancel(); - if (Verbose && eventArgs.Data != null) - AnsiConsole.WriteLine(eventArgs.Data); - // Successfully created package 'C:\Users\qwsdc\source\repos\Beam\aeqw89.Beam\tozsxqaj.alp\Beam.1.0.0.nupkg'. - if (eventArgs.Data?.ToLower() - .Contains($"successfully created package '{Path.GetFullPath(outDir)}") == true) { - AnsiConsole.MarkupLine($"[bold]{eventArgs.Data}[/]"); - success = true; - } - }; - p?.BeginOutputReadLine(); - p?.BeginErrorReadLine(); + if (rctx.Args.Verbose) + AnsiConsole.WriteLine( + $"Created project file backup at {projectFile.GetDefaultBackupLocation()}"); - try { - await (p?.WaitForExitAsync(cts.Token) ?? Task.CompletedTask); - } - catch (TaskCanceledException) { - p?.Kill(); + ctx.Status = "Repairing project file"; + if (!rctx.Args.Flags.ContainsKey("--skip-repair")) + if (!projectFile.TryRepair(out error)) { + return new ReadableError(error.EscapeMarkup(), false); + } + + if (rctx.Args.Mode == Mode.Increment && !rctx.Args.Flags.ContainsKey("--simulate")) { + int delta = 1; + if (rctx.Args.Flags.TryGetValue("--delta", out var deltaStrings)) { + if (deltaStrings.Length != 1) { + return new ReadableError(string.Format(Exceptions.flag_parameter_length_incorrect.EscapeMarkup(), "--delta", 1, + deltaStrings.Length)); } - processError = errorLines.ToString().EscapeMarkup(); - return success == true ? 0 : p?.ExitCode ?? -1; + if (!int.TryParse(deltaStrings[0], out delta)) { + return new ReadableError(string.Format(Exceptions.flag_parameter_type_incorrect.EscapeMarkup(), "--delta", 0, + nameof(Int32), + deltaStrings[0])); + } + } + + ctx.Status = "Updating version"; + version = projectFile.GetVersion(); + version = ProjectFile.ChangeVersion(version, delta, rctx.Args.Target ?? IncrementTarget.Patch).Unwrap(rctx); + + projectFile.SetVersion(version); + } + } + catch (Exception e) { + return new ReadableError(string.Format(Exceptions.generic_error.EscapeMarkup(), e.ToString().EscapeMarkup()), false); + } + + version = projectFile.GetVersion(); + + if (!rctx.Args.Flags.ContainsKey("--simulate")) { + try { + var packageReferences = projectFile.GetPackageReferences(); + foreach (var reference in packageReferences.Where(x => !projectFile.IsTransitive(x))) + projectFile.SetPrivateAssets(reference, PrivateAssetsValue.All); + foreach (var reference in packageReferences.Where(x => projectFile.IsTransitive(x))) + projectFile.RemovePackage(reference); + + HashSet visited = []; + var projectReferences = new Queue(projectFile.GetProjectReferences().Cast()); + while (projectReferences.Count != 0) { + var reference = projectReferences.Dequeue(); + visited.Add(reference.Include); + + if (rctx.Args.Verbose) + AnsiConsole.WriteLine( + $"Processing project reference {reference.Include} out of {visited.Count} so far"); + + // if (Flags.ContainsKey("--force-private")) + projectFile.SetPrivateAssets(reference, PrivateAssetsValue.All); + string pathToReferencedProjectFile = projectFile.GetAbsoluteIncludePath(reference); + if (!ProjectFile.TryLoad(pathToReferencedProjectFile, out var referencedProjectFile, + out error)) { + return new ReadableError(error.EscapeMarkup(), false); + } + + var referencedPackageReferences = referencedProjectFile.GetPackageReferences(); + foreach (var package in referencedPackageReferences) { + if (rctx.Args.Verbose) + AnsiConsole.WriteLine( + $"Hoisting package {package.Include} from {pathToReferencedProjectFile}"); + var hoisted = projectFile.AddPackage(package); + projectFile.SetTransitive(hoisted, true); + projectFile.SetPrivateAssets(hoisted, PrivateAssetsValue.None); + referencedProjectFile.SetPrivateAssets(package, PrivateAssetsValue.All); + } + + var referencedProjectReferences = referencedProjectFile.GetProjectReferences(); + foreach (var project in referencedProjectReferences) { + if (!visited.Contains(project.Include)) + projectReferences.Enqueue(project); + } + } + } + catch (Exception e) { + return new ReadableError(string.Format(Exceptions.generic_error.EscapeMarkup(), e.ToString().EscapeMarkup()), false); + } + } + + projectFile.Save(); + return new Ok(new ProjectResult(packageId, version)); + } + + static async Task> PackProject(RunContext rctx, StatusContext ctx) { + DataReceivedEventHandler dataHandler = rctx.Args.Verbose switch { + false => (_, _) => {}, + true => (_ , e) => { + AnsiConsole.MarkupLine("[Packing]: " + e.Data?.EscapeMarkup() ?? string.Empty); + } + }; + var result = (await Shell.Dotnet.Pack(rctx.TempDir, dataHandler, dataHandler)).Unwrap(rctx); + + if (result.ExitCode != 0) + return new ReadableError(result.Error + "\n" + string.Format(Exceptions.dotnet_pack_failure.EscapeMarkup(), result.ExitCode), false); + + return Success.AsResult(); + } + + enum DestinationType { + Local, + Github, + Cloud + } + + public static async Task Main(string[] args) { + ArgValues? parsed = ReadArgs(args).Unwrap(); + if (parsed is null) return; + + RunContext rctx = new([], parsed, Path.GetRandomFileName()); + + Console.CancelKeyPress += (sender, eventArgs) => { + rctx.Restore(); + }; + + var stager = new Stager(rctx); + + ProjectResult? project = await stager.Spinner("Preparing project", PrepareProject); + if (project is null) return; + + int destinationsProcessed = 0; + + var dotnetPackResult = await stager.Spinner("Creating package with 'dotnet pack'", PackProject); + if (dotnetPackResult is null) return; + + if (rctx.Args.Verbose) + AnsiConsole.MarkupLine("Successfully created package with exit code [green]{0}[/]. Processing destinations.", 0); + + var progress = stager.Progress(); + var task = progress.Run("push to destinations", async (rctx, ctx) => { + var pkg = await Shell.Dotnet.FindPackage(rctx.TempDir).Unwrap(rctx); + await Parallel.ForEachAsync(rctx.Args.Destinations, new ParallelOptions() { + MaxDegreeOfParallelism = Environment.ProcessorCount, + }, async (dest, ct) => { + using var reader = new MemoryStream(pkg.Read()); + var task = ctx.AddTask(dest, new ProgressTaskSettings() { + MaxValue = pkg.Size() }); - if (exitCode != 0) { - ShowError(processError.EscapeMarkup()); - ShowError(Exceptions.dotnet_pack_failure.EscapeMarkup(), exitCode); - RestoreActions.ForEach(x => x()); - return; - } - - if (Verbose) - AnsiConsole.MarkupLine("Successfully created package with exit code [green]{0}[/]. Processing destinations.", exitCode); - - var package = Directory.GetFiles(outDir, "*.nupkg").FirstOrDefault(); - if (package == null) { - ShowError(Exceptions.generic_error.EscapeMarkup()); - RestoreActions.ForEach(x => x()); - return; - } - - var inMemory = await File.ReadAllBytesAsync(package); - var size = new FileInfo(package).Length; - const long bufferSize = 80 * 1024; // 80 KB - try { - await AnsiConsole.Progress() - .AutoClear(true) - .HideCompleted(false) - .Columns(new ProgressColumn[] { - new TaskDescriptionColumn(), - new ProgressBarColumn() - .RemainingStyle(Style.Parse("dim gray slowblink")) - .CompletedStyle(Style.Parse("green strikethrough")) - .FinishedStyle("green strikethrough"), - new DownloadedColumn(), - new RemainingTimeColumn(), - new TransferSpeedColumn(), - }) - .StartAsync(async ctx => { - await Parallel.ForEachAsync(Destinations, new ParallelOptions() { - MaxDegreeOfParallelism = Environment.ProcessorCount, - }, async (dest, ct) => { - using var reader = new MemoryStream(inMemory); - var task = ctx.AddTask(dest, new ProgressTaskSettings() { - MaxValue = size - }); - - if (dest.StartsWith("local-")) { - var name = dest[("local-".Length)..]; - var path = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), - name, Path.GetFileName(package)); - if (!Directory.Exists(Path.GetDirectoryName(path))) - Directory.CreateDirectory(Path.GetDirectoryName(path)!); - await using var writer = File.OpenWrite(path); - var buffer = new byte[bufferSize]; - int read; - do { - read = await reader.ReadAsync(buffer, ct); - writer.Write(buffer, 0, read); - task.Increment(read); - } while (read > 0); - } - - else if (dest.StartsWith("cloud-")) { - var name = dest[("cloud-".Length)..]; - var connectionTask = ctx.AddTaskBefore($"Preparing cloud-{name}", - new ProgressTaskSettings() { - MaxValue = 100 - }, task); - - if (!SshHosts.TryGetHost(name, out var host)) { - ShowError(Exceptions.cloud_host_not_found.EscapeMarkup(), name); - return; - } - - var connectionInfo = SshHosts.GetConnection(name); - using var sshClient = new SshClient(connectionInfo); - if (!sshClient.IsConnected) - await sshClient.ConnectAsync(ct); - connectionTask.Increment(33); - - var winC = sshClient.RunCommand("cmd /c ver"); - var othC = sshClient.RunCommand("uname -s"); - - var os = (winC.ExitStatus, othC.ExitStatus) switch { - (0, _) => "windows", - (_, 0) => "linux", - _ => "unknown" - }; - - string remoteDirectory; - string packageFileDirectory; - if (os == "windows") { - var userDirC = sshClient.RunCommand("cmd /c echo %USERPROFILE%"); - if (userDirC.ExitStatus != 0) { - ShowError(Exceptions.failed_to_prepare_server_directory, "n/a", name, os, - userDirC.Result); - return; - } - - var userDir = userDirC.Result.Trim(); - remoteDirectory = RemotePath.Combine(RemoteOs.Windows, userDir, "dotnet-packages"); - packageFileDirectory = RemotePath.Combine(RemoteOs.Windows, remoteDirectory, - Path.GetFileName(package)); - - var mkdirC = sshClient.RunCommand( - $"cmd /c if not exist \"{remoteDirectory}\" mkdir \"{remoteDirectory}\""); - if (mkdirC.ExitStatus != 0) { - ShowError(Exceptions.failed_to_prepare_server_directory, remoteDirectory, name, - os, mkdirC.Result); - return; - } - } - else if (os == "linux") { - var homeDirC = sshClient.RunCommand("printf %s \"$HOME\""); - if (homeDirC.ExitStatus != 0) { - ShowError(Exceptions.failed_to_prepare_server_directory, "n/a", name, os, - homeDirC.Result); - return; - } - - var homeDir = homeDirC.Result.Trim(); // no CRLF on unix, but Trim() is safest - remoteDirectory = RemotePath.Combine(RemoteOs.Unix, homeDir, ".dotnet-packages"); - packageFileDirectory = RemotePath.Combine(RemoteOs.Unix, remoteDirectory, - Path.GetFileName(package)); - - // Use -p and single quotes to handle spaces safely - var mkdirC = sshClient.RunCommand($"mkdir -p '{remoteDirectory}'"); - if (mkdirC.ExitStatus != 0) { - ShowError(Exceptions.failed_to_prepare_server_directory, remoteDirectory, name, - os, mkdirC.Result); - return; - } - } - else { - ShowError(Exceptions.failed_to_prepare_server_directory, "n/a", name, os, - "Unsupported OS"); - return; - } - - connectionTask.Increment(33); - - sshClient.Disconnect(); - - using var client = new SftpClient(connectionInfo); - if (!client.IsConnected) - await client.ConnectAsync(ct); - connectionTask.Increment(33); - connectionTask.StopTask(); - - await using var writer = client.OpenWrite(packageFileDirectory); - byte[] buffer = new byte[bufferSize]; - int read; - do { - read = await reader.ReadAsync(buffer, ct); - writer.Write(buffer, 0, read); - task.Increment(read); - } while (read > 0); - } - - else if (dest == "github") { - var p = Process.Start(new ProcessStartInfo() { - FileName = "dotnet", - Arguments = $"nuget push {package} --source github", - WorkingDirectory = Environment.CurrentDirectory, - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true - }); - - var cts = CancellationTokenSource.CreateLinkedTokenSource(ct); - StringBuilder errorLines = new(); - p?.ErrorDataReceived += (sender, eventArgs) => { - cts.Cancel(); - if (Verbose && eventArgs.Data != null) - AnsiConsole.WriteLine(eventArgs.Data); - errorLines.Append(eventArgs.Data); - }; - p?.OutputDataReceived += (sender, eventArgs) => { - if (eventArgs.Data?.ToLower().Contains("press any key") == true) - cts.Cancel(); - if (Verbose && eventArgs.Data != null) - AnsiConsole.WriteLine(eventArgs.Data); - }; - - p?.BeginOutputReadLine(); - p?.BeginErrorReadLine(); - - if (p == null) { - ShowError(Exceptions.generic_error.EscapeMarkup()); - } - - task.Increment(size / 2); - if (p != null) - try { - await (p?.WaitForExitAsync(cts.Token) ?? Task.CompletedTask); - } - catch (TaskCanceledException) { - p?.Kill(); - } - - if (p?.ExitCode != 0) { - ShowError(errorLines.ToString().EscapeMarkup()); - ShowError(Exceptions.dotnet_nuget_push_failure, p?.ExitCode ?? -1); - task.StopTask(); - return; - } - - task.Increment(size / 2); - } - - Interlocked.Increment(ref destinationsProcessed); - task.StopTask(); - }); - }); - } - finally { - try { - Directory.Delete(outDir, true); + DestinationType destType; + if (dest.StartsWith("local-")) { + destType = DestinationType.Local; + } else if (dest.StartsWith("cloud-")) { + destType = DestinationType.Cloud; + } else if (dest == "github") { + destType = DestinationType.Github; + } else { + lock(rctx) { + ShowError(string.Format(Exceptions.destination_unrecognizable, dest)); + ShowHelp(); + } + task.StopTask(); + return; } - catch (Exception e) { - ShowError(string.Format(Exceptions.failed_to_clean_up.EscapeMarkup(), outDir.EscapeMarkup(), - e.ToString().EscapeMarkup())); - } - } - } - catch(Exception e) { - ShowError(Exceptions.generic_error.EscapeMarkup(), e.ToString().EscapeMarkup());; - RestoreActions.ForEach(x => x()); - } + var dctx = new DestinationContext(task, ctx, reader, pkg.FileInfo, destType switch { + DestinationType.Cloud => dest["cloud-".Length..], + DestinationType.Github => dest["github".Length..], + DestinationType.Local => dest["local-".Length..] + }, BufferSize, pkg.Size()); + + IDestination destination = destType switch { + DestinationType.Local => new LocalDestination(dctx), + DestinationType.Github => new GithubDestination(dctx, rctx.Args.Verbose), + DestinationType.Cloud => new CloudDestiantion(dctx), + _ => throw new UnreachableException() + }; + + var result = await destination.WaitForCompletion(ct); + lock(rctx) { + if (result.Unwrap(rctx) is not null) { + Interlocked.Increment(ref destinationsProcessed); + } + } + + task.StopTask(); + }); + + return Success.AsResult(); + }); + + await task.Value; if (destinationsProcessed == 0) { AnsiConsole.MarkupLine("[bold red]No destinations were processed. Reverting changes to project file.[/]"); - RestoreActions.ForEach(x => x()); + rctx.Restore(); } else { AnsiConsole.MarkupLine("Completed processing of all destinations."); AnsiConsole.MarkupLine( - "Example usage:\n\t ".EscapeMarkup(), packageId, - version); + "Example usage:\n\t ".EscapeMarkup(), project.PackageId, + project.Version); } } - /// - /// Updates the version string by applying the specified operation to the major, minor, and patch components of the version. - /// - /// The current version string in the format "major.minor.patch[-tag]". - /// The value to apply to the patch component. - /// The value to apply to the minor component. - /// The value to apply to the major component. - /// A function that defines the adjustment operation to be performed on each version component. - /// A new version string with the updated major, minor, and patch components, preserving any existing tag. - /// Thrown if the version string is not in the correct format. - private static string ChangeVersion(string version, int delta, IncrementTarget target) { - string[] split = version.Split('.'); - if (split.Length != 3) { - throw new Exception(string.Format(Exceptions.version_string_not_formatted_correctly, version)); - } - - string tag = ""; - if (split[2].Contains('-')) { - var split2 = split[2].Split('-'); - split[2] = split2[0]; - tag = "-" + split2[1]; - } - - if (split.Any(x => !int.TryParse(x, out _))) - throw new Exception(string.Format(Exceptions.version_string_not_formatted_correctly, version)); - - int[] parsedVersion = split.Select(int.Parse).ToArray(); - switch (target) { - case IncrementTarget.Major: - parsedVersion[0] += delta; - parsedVersion[1] = 0; - parsedVersion[2] = 0; - break; - case IncrementTarget.Minor: - parsedVersion[1] += delta; - parsedVersion[2] = 0; - break; - case IncrementTarget.Patch: - parsedVersion[2] += delta; - break; - } - - return - $"{parsedVersion[0]}.{parsedVersion[1]}.{parsedVersion[2]}{tag}"; - } - - private static void ShowError(string message, params object[] args) { + public static void ShowError(string message, params object[] args) { AnsiConsole.MarkupLine($"[bold red]{message}[/]", args); } - private static void ShowHelp() { + public static void ShowHelp() { AnsiConsole.Markup(("Usage: publish overwrite|increment [patch|minor|major] destinations [flags]\n" + "\t if mode: overwrite destinations [flags]\n" + "\t if mode: increment patch|minor|major [flags]\n").EscapeMarkup()); diff --git a/aeqw89.tools.Publish/ProjectFile.cs b/aeqw89.tools.Publish/ProjectFile.cs index 46a4ec0..8819c18 100644 --- a/aeqw89.tools.Publish/ProjectFile.cs +++ b/aeqw89.tools.Publish/ProjectFile.cs @@ -218,4 +218,51 @@ internal class ProjectFile { MainPropertyGroup.SetProperty("Version", version); MainPropertyGroup.SetProperty("PackageVersion", version); } + + /// + /// Updates the version string by applying the specified operation to the major, minor, and patch components of the version. + /// + /// The current version string in the format "major.minor.patch[-tag]". + /// The value to apply to the patch component. + /// The value to apply to the minor component. + /// The value to apply to the major component. + /// A function that defines the adjustment operation to be performed on each version component. + /// A new version string with the updated major, minor, and patch components, preserving any existing tag. + /// Thrown if the version string is not in the correct format. + public static Result ChangeVersion(string version, int delta, IncrementTarget target) { + string[] split = version.Split('.'); + if (split.Length != 3) { + return new ReadableError(string.Format(Exceptions.version_string_not_formatted_correctly, version)); + } + + string tag = ""; + if (split[2].Contains('-')) { + var split2 = split[2].Split('-'); + split[2] = split2[0]; + tag = "-" + split2[1]; + } + + if (split.Any(x => !int.TryParse(x, out _))) + return new ReadableError(string.Format(Exceptions.version_string_not_formatted_correctly, version)); + + int[] parsedVersion = split.Select(int.Parse).ToArray(); + switch (target) { + case IncrementTarget.Major: + parsedVersion[0] += delta; + parsedVersion[1] = 0; + parsedVersion[2] = 0; + break; + case IncrementTarget.Minor: + parsedVersion[1] += delta; + parsedVersion[2] = 0; + break; + case IncrementTarget.Patch: + parsedVersion[2] += delta; + break; + } + + return + $"{parsedVersion[0]}.{parsedVersion[1]}.{parsedVersion[2]}{tag}".Ok(); + } + } diff --git a/aeqw89.tools.Publish/aeqw89.tools.Publish.csproj b/aeqw89.tools.Publish/aeqw89.tools.Publish.csproj index d31429e..5ea3a04 100644 --- a/aeqw89.tools.Publish/aeqw89.tools.Publish.csproj +++ b/aeqw89.tools.Publish/aeqw89.tools.Publish.csproj @@ -10,6 +10,10 @@ + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + @@ -29,4 +33,8 @@ + + true + +