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
+
+