Improve project file version handling and error recovery

Refactored the publish workflow to use a more robust error handling and cleanup mechanism with a RestoreActions list. The version increment logic now resets lower version components appropriately based on the increment target. ProjectFile now sets both Version and PackageVersion properties to ensure consistency. Improved process output handling and destination processing feedback.
This commit is contained in:
qwsdcvghyu89
2025-09-27 17:19:30 +10:00
parent 6904e0cfd8
commit 207805bad3
2 changed files with 431 additions and 331 deletions
+140 -44
View File
@@ -1,5 +1,6 @@
using System.Collections;
using System.Diagnostics;
using System.Text;
using Renci.SshNet;
using Spectre.Console;
using aeqw89.xml.ProjectFile;
@@ -26,6 +27,8 @@ public static class Program {
public static Dictionary<string, string[]> Flags { get; set; }
public static bool Verbose { get; set; } = false;
public static List<Action> RestoreActions { get; set; } = [];
public static void ReadArgs(string[] args) {
if (args.Length < 1) {
ShowError(Exceptions.missing_mode.EscapeMarkup());
@@ -108,9 +111,15 @@ public static class Program {
public static async Task Main(string[] args) {
ReadArgs(args);
Console.CancelKeyPress += (sender, eventArgs) => {
RestoreActions.ForEach(x => x());
};
string packageId = "";
string version = "";
int destinationsProcessed = 0;
try {
var result = AnsiConsole.Status()
.Spinner(Spinner.Known.Dots)
.Start<bool>("Preparing project", ctx => {
@@ -124,10 +133,11 @@ public static class Program {
try {
projectFile.Backup();
Console.CancelKeyPress += (sender, eventArgs) => {
RestoreActions.Add(() => {
projectFile.Restore();
AnsiConsole.MarkupLine("[yellow]Restored project file from backup.[/]");
};
});
if (Verbose)
AnsiConsole.WriteLine(
@@ -153,7 +163,8 @@ public static class Program {
}
if (!int.TryParse(deltaStrings[0], out delta)) {
ShowError(Exceptions.flag_parameter_type_incorrect.EscapeMarkup(), "--delta", 0, nameof(Int32),
ShowError(Exceptions.flag_parameter_type_incorrect.EscapeMarkup(), "--delta", 0,
nameof(Int32),
deltaStrings[0]);
projectFile.Restore();
ShowHelp();
@@ -163,18 +174,14 @@ public static class Program {
ctx.Status = "Updating version";
var version = projectFile.GetVersion();
version = ChangeVersion(version,
Target == IncrementTarget.Patch ? delta : -1,
Target == IncrementTarget.Minor ? delta : -1,
Target == IncrementTarget.Major ? delta : -1,
(x, y) => y < 0 ? x : x + y);
version = ChangeVersion(version, delta, Target ?? IncrementTarget.Patch);
projectFile.SetVersion(version);
}
}
catch (Exception e) {
ShowError(Exceptions.generic_error.EscapeMarkup(), e.ToString().EscapeMarkup());
projectFile.Restore();
RestoreActions.ForEach(x => x());
return false;
}
@@ -195,21 +202,23 @@ public static class Program {
visited.Add(reference.Include);
if (Verbose)
AnsiConsole.WriteLine($"Processing project reference {reference.Include} out of {visited.Count} so far");
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());
projectFile.Restore();
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}");
AnsiConsole.WriteLine(
$"Hoisting package {package.Include} from {pathToReferencedProjectFile}");
var hoisted = projectFile.AddPackage(package);
projectFile.SetTransitive(hoisted, true);
projectFile.SetPrivateAssets(hoisted, PrivateAssetsValue.None);
@@ -222,9 +231,10 @@ public static class Program {
projectReferences.Enqueue(project);
}
}
} catch (Exception e) {
}
catch (Exception e) {
ShowError(Exceptions.generic_error.EscapeMarkup(), e.ToString().EscapeMarkup());
projectFile.Restore();
RestoreActions.ForEach(x => x());
return false;
}
}
@@ -238,8 +248,9 @@ public static class Program {
}
var outDir = Path.GetRandomFileName();
Console.CancelKeyPress += (sender, eventArgs) => {
RestoreActions.Add(() => {
try {
if (!Directory.Exists(outDir)) return;
Directory.Delete(outDir, true);
AnsiConsole.MarkupLine("[yellow]Cleaned up temporary directory[/]");
}
@@ -247,7 +258,7 @@ public static class Program {
ShowError(string.Format(Exceptions.failed_to_clean_up.EscapeMarkup(), outDir.EscapeMarkup(),
e.ToString().EscapeMarkup()));
}
};
});
string processError = "";
var exitCode = await AnsiConsole.Status()
@@ -257,34 +268,60 @@ public static class Program {
FileName = "dotnet",
Arguments = $"pack -o {outDir}",
WorkingDirectory = Environment.CurrentDirectory,
UseShellExecute = Verbose,
RedirectStandardOutput = !Verbose,
RedirectStandardError = !Verbose
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();
try {
await (p?.WaitForExitAsync(cts.Token) ?? Task.CompletedTask);
processError = p?.StandardError?.ReadToEnd() ?? "";
return p?.ExitCode ?? -1;
}
catch (TaskCanceledException) {
p?.Kill();
}
processError = errorLines.ToString().EscapeMarkup();
return success == true ? 0 : p?.ExitCode ?? -1;
});
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;
}
@@ -316,7 +353,8 @@ public static class Program {
if (dest.StartsWith("local-")) {
var name = dest[("local-".Length)..];
var path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
var path = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
name, Path.GetFileName(package));
if (!Directory.Exists(Path.GetDirectoryName(path)))
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
@@ -332,7 +370,8 @@ public static class Program {
else if (dest.StartsWith("cloud-")) {
var name = dest[("cloud-".Length)..];
var connectionTask = ctx.AddTaskBefore($"Preparing cloud-{name}", new ProgressTaskSettings() {
var connectionTask = ctx.AddTaskBefore($"Preparing cloud-{name}",
new ProgressTaskSettings() {
MaxValue = 100
}, task);
@@ -361,41 +400,51 @@ public static class Program {
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);
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));
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}\"");
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);
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);
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));
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);
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");
ShowError(Exceptions.failed_to_prepare_server_directory, "n/a", name, os,
"Unsupported OS");
return;
}
connectionTask.Increment(33);
sshClient.Disconnect();
@@ -422,34 +471,52 @@ public static class Program {
Arguments = $"nuget push {package} --source github",
WorkingDirectory = Environment.CurrentDirectory,
UseShellExecute = false,
RedirectStandardOutput = !Verbose,
RedirectStandardError = !Verbose
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)
await p.WaitForExitAsync(cts.Token);
processError += p?.StandardError?.ReadToEnd() ?? "";
if (p?.ExitCode != 0) {
ShowError(processError.EscapeMarkup());
ShowError(Exceptions.dotnet_nuget_push_failure, p?.ExitCode ?? -1);
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();
});
});
@@ -459,11 +526,27 @@ public static class Program {
Directory.Delete(outDir, true);
}
catch (Exception e) {
ShowError(string.Format(Exceptions.failed_to_clean_up.EscapeMarkup(), outDir.EscapeMarkup(), e.ToString().EscapeMarkup()));
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());
}
if (destinationsProcessed == 0) {
AnsiConsole.MarkupLine("[bold red]No destinations were processed. Reverting changes to project file.[/]");
RestoreActions.ForEach(x => x());
}
else {
AnsiConsole.MarkupLine("Completed processing of all destinations.");
AnsiConsole.MarkupLine("Example usage:\n\t <PackageReference Include=\"{0}\" Version=\"{1}\" />".EscapeMarkup(), packageId, version);
AnsiConsole.MarkupLine(
"Example usage:\n\t <PackageReference Include=\"{0}\" Version=\"{1}\" />".EscapeMarkup(), packageId,
version);
}
}
/// <summary>
@@ -476,8 +559,7 @@ public static class Program {
/// <param name="operation">A function that defines the adjustment operation to be performed on each version component.</param>
/// <returns>A new version string with the updated major, minor, and patch components, preserving any existing tag.</returns>
/// <exception cref="Exception">Thrown if the version string is not in the correct format.</exception>
private static string ChangeVersion(string version, int patch, int minor, int major,
Func<int, int, int> operation) {
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));
@@ -494,9 +576,23 @@ public static class Program {
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
$"{operation(parsedVersion[0], major)}.{operation(parsedVersion[1], minor)}.{operation(parsedVersion[2], patch)}{tag}";
$"{parsedVersion[0]}.{parsedVersion[1]}.{parsedVersion[2]}{tag}";
}
private static void ShowError(string message, params object[] args) {
+5 -1
View File
@@ -106,6 +106,7 @@ internal class ProjectFile {
}
set("Version", "1.0.0");
set("PackageVersion", "1.0.0");
set("Title", System.IO.Path.GetFileNameWithoutExtension(Path));
set("Authors", "");
set("Company", "");
@@ -213,5 +214,8 @@ internal class ProjectFile {
public string GetVersion() => MainPropertyGroup.GetProperty("Version");
public void SetVersion(string version) => MainPropertyGroup.SetProperty("Version", version);
public void SetVersion(string version) {
MainPropertyGroup.SetProperty("Version", version);
MainPropertyGroup.SetProperty("PackageVersion", version);
}
}