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.
This commit is contained in:
@@ -1 +1,2 @@
|
||||
bin/
|
||||
bin/**
|
||||
obj/**
|
||||
|
||||
@@ -1,75 +1,196 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<root>
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string"/>
|
||||
<xsd:attribute name="type" type="xsd:string"/>
|
||||
<xsd:attribute name="mimetype" type="xsd:string"/>
|
||||
<xsd:attribute ref="xml:space"/>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string"/>
|
||||
<xsd:attribute name="name" type="xsd:string"/>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
|
||||
<xsd:attribute ref="xml:space"/>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required"/>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>1.3</value>
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="missing_mode" xml:space="preserve">
|
||||
<value>You must specify a mode; allowed modes are [overwrite|increment]</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="could_not_parse_mode" xml:space="preserve">
|
||||
<value>The mode '{0}' is invalid, the valid modes are [overwrite|increment]</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="could_not_parse_target" xml:space="preserve">
|
||||
<value>The increment target '{0}' is invalid, the valid increment targets are [patch|minor|patch]</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="missing_increment_target" xml:space="preserve">
|
||||
<value>You must specify an increment target if you specified an increment mode; allowed increment targets are [patch|minor|major]</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="missing_destinations" xml:space="preserve">
|
||||
<value>You must specify at least one destination.</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="no_project_in_directory" xml:space="preserve">
|
||||
<value>No project file was found within the current directory.</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="flag_parameter_length_incorrect" xml:space="preserve">
|
||||
<value>The flag '{0}' requires exactly '{1}' parameters. You have entered '{2}'.</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="flag_parameter_type_incorrect" xml:space="preserve">
|
||||
<value>The '{0}' flag requires that argument with index '{1}' be of type '{2}'. You have entered '{3}' which has failed to be converted.</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="version_string_not_formatted_correctly" xml:space="preserve">
|
||||
<value>The version string '{0}' is in an unidentifiable format.</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="tried_loading_non_csproj_file" xml:space="preserve">
|
||||
<value>Something went wrong; an attempt was made to load a non .csproj file as a project file.</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="generic_error" xml:space="preserve">
|
||||
<value>Something went wrong loading this file; {0}</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="found_multiple_csproj" xml:space="preserve">
|
||||
<value>The directory '{0}' contains multiple .csproj files; this tool can only process one at a time.</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="project_file_irreparable" xml:space="preserve">
|
||||
<value>The project file '{0}' is irreparable becuase it is missing a '{1}' property, and the value cannot be guessed.</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="dotnet_pack_failure" xml:space="preserve">
|
||||
<value>Failed to pack with exit code '{0}'; ensure that 'dotnet build' succeeds before running this program.</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="failed_to_clean_up" xml:space="preserve">
|
||||
<value>Could not delete temporary directory '{0}' due to error '{1}'</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="cloud_host_not_found" xml:space="preserve">
|
||||
<value>The cloud host '{0}' is not an entry on this user's config file.</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="failed_to_prepare_server_directory" xml:space="preserve">
|
||||
<value>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}'</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="dotnet_nuget_push_failure" xml:space="preserve">
|
||||
<value>The 'dotnet nuget push' command failed with error message '{0}'</value>
|
||||
<comment/>
|
||||
</data>
|
||||
<data name="destination_unrecognizable" xml:space="preserve">
|
||||
<value>The destination '{0}' is unrecognizable.</value>
|
||||
<comment/>
|
||||
</data>
|
||||
</root>
|
||||
+158
-418
@@ -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<string, string[]> Flags { get; set; }
|
||||
public static bool Verbose { get; set; } = false;
|
||||
const long BufferSize = 80 * 1024; // 80 KB
|
||||
|
||||
public static List<Action> RestoreActions { get; set; } = [];
|
||||
internal record RunContext(List<Action> 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()));
|
||||
}
|
||||
}
|
||||
|
||||
Mode = args[0] switch {
|
||||
RestoreActions.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
internal record ArgValues(Mode Mode, string[] Destinations, Dictionary<string, string[]> Flags, bool Verbose, IncrementTarget? Target);
|
||||
static Result<ArgValues, ReadableError> ReadArgs(string[] args) {
|
||||
if (args.Length < 1) // not enough args (min = 2)
|
||||
return new ReadableError(Exceptions.missing_mode.EscapeMarkup());
|
||||
|
||||
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<string, string[]> 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<ArgValues>(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<ArgValues>(new ArgValues(mode, destinations, flags, verbose, target)); // return with fully parsed flags.
|
||||
}
|
||||
|
||||
private static Dictionary<string, string[]> ReadFlags(string[] flags) {
|
||||
@@ -105,89 +112,66 @@ 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());
|
||||
};
|
||||
|
||||
string packageId = "";
|
||||
string version = "";
|
||||
int destinationsProcessed = 0;
|
||||
|
||||
try {
|
||||
var result = AnsiConsole.Status()
|
||||
.Spinner(Spinner.Known.Dots)
|
||||
.Start<bool>("Preparing project", ctx => {
|
||||
record ProjectResult(string PackageId, string Version);
|
||||
static async Task<Result<ProjectResult, ReadableError>> PrepareProject(RunContext rctx, StatusContext ctx) {
|
||||
ctx.Status = "Locating project file";
|
||||
if (!ProjectFile.TryLoad(Environment.CurrentDirectory, out var projectFile, out var error)) {
|
||||
ShowError(error.EscapeMarkup());
|
||||
return false;
|
||||
}
|
||||
if (!ProjectFile.TryLoad(Environment.CurrentDirectory, out var projectFile, out var error))
|
||||
return new ReadableError(error);
|
||||
|
||||
packageId = projectFile.GetPackageId();
|
||||
string packageId = projectFile.GetPackageId();
|
||||
string version;
|
||||
|
||||
try {
|
||||
projectFile.Backup();
|
||||
RestoreActions.Add(() => {
|
||||
rctx.RestoreActions.Add(() => {
|
||||
projectFile.Restore();
|
||||
AnsiConsole.MarkupLine("[yellow]Restored project file from backup.[/]");
|
||||
});
|
||||
|
||||
|
||||
if (Verbose)
|
||||
if (rctx.Args.Verbose)
|
||||
AnsiConsole.WriteLine(
|
||||
$"Created project file backup at {projectFile.GetDefaultBackupLocation()}");
|
||||
|
||||
ctx.Status = "Repairing project file";
|
||||
if (!Flags.ContainsKey("--skip-repair"))
|
||||
if (!rctx.Args.Flags.ContainsKey("--skip-repair"))
|
||||
if (!projectFile.TryRepair(out error)) {
|
||||
ShowError(error.EscapeMarkup());
|
||||
projectFile.Restore();
|
||||
return false;
|
||||
return new ReadableError(error.EscapeMarkup(), false);
|
||||
}
|
||||
|
||||
if (Mode == Mode.Increment && !Flags.ContainsKey("--simulate")) {
|
||||
if (rctx.Args.Mode == Mode.Increment && !rctx.Args.Flags.ContainsKey("--simulate")) {
|
||||
int delta = 1;
|
||||
if (Flags.TryGetValue("--delta", out var deltaStrings)) {
|
||||
if (rctx.Args.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;
|
||||
return new ReadableError(string.Format(Exceptions.flag_parameter_length_incorrect.EscapeMarkup(), "--delta", 1,
|
||||
deltaStrings.Length));
|
||||
}
|
||||
|
||||
if (!int.TryParse(deltaStrings[0], out delta)) {
|
||||
ShowError(Exceptions.flag_parameter_type_incorrect.EscapeMarkup(), "--delta", 0,
|
||||
return new ReadableError(string.Format(Exceptions.flag_parameter_type_incorrect.EscapeMarkup(), "--delta", 0,
|
||||
nameof(Int32),
|
||||
deltaStrings[0]);
|
||||
projectFile.Restore();
|
||||
ShowHelp();
|
||||
return false;
|
||||
deltaStrings[0]));
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Status = "Updating version";
|
||||
var version = projectFile.GetVersion();
|
||||
version = ChangeVersion(version, delta, Target ?? IncrementTarget.Patch);
|
||||
version = projectFile.GetVersion();
|
||||
version = ProjectFile.ChangeVersion(version, delta, rctx.Args.Target ?? IncrementTarget.Patch).Unwrap(rctx);
|
||||
|
||||
projectFile.SetVersion(version);
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
ShowError(Exceptions.generic_error.EscapeMarkup(), e.ToString().EscapeMarkup());
|
||||
RestoreActions.ForEach(x => x());
|
||||
return false;
|
||||
return new ReadableError(string.Format(Exceptions.generic_error.EscapeMarkup(), e.ToString().EscapeMarkup()), false);
|
||||
}
|
||||
|
||||
version = projectFile.GetVersion();
|
||||
|
||||
if (!Flags.ContainsKey("--simulate")) {
|
||||
if (!rctx.Args.Flags.ContainsKey("--simulate")) {
|
||||
try {
|
||||
var packageReferences = projectFile.GetPackageReferences();
|
||||
foreach (var reference in packageReferences.Where(x => !projectFile.IsTransitive(x)))
|
||||
@@ -201,22 +185,21 @@ public static class Program {
|
||||
var reference = projectReferences.Dequeue();
|
||||
visited.Add(reference.Include);
|
||||
|
||||
if (Verbose)
|
||||
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)) {
|
||||
ShowError(error.EscapeMarkup());
|
||||
RestoreActions.ForEach(x => x());
|
||||
return false;
|
||||
return new ReadableError(error.EscapeMarkup(), false);
|
||||
}
|
||||
|
||||
var referencedPackageReferences = referencedProjectFile.GetPackageReferences();
|
||||
foreach (var package in referencedPackageReferences) {
|
||||
if (Verbose)
|
||||
if (rctx.Args.Verbose)
|
||||
AnsiConsole.WriteLine(
|
||||
$"Hoisting package {package.Include} from {pathToReferencedProjectFile}");
|
||||
var hoisted = projectFile.AddPackage(package);
|
||||
@@ -233,373 +216,130 @@ public static class Program {
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
ShowError(Exceptions.generic_error.EscapeMarkup(), e.ToString().EscapeMarkup());
|
||||
RestoreActions.ForEach(x => x());
|
||||
return false;
|
||||
return new ReadableError(string.Format(Exceptions.generic_error.EscapeMarkup(), e.ToString().EscapeMarkup()), false);
|
||||
}
|
||||
}
|
||||
|
||||
projectFile.Save();
|
||||
return true;
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
return;
|
||||
return new Ok<ProjectResult>(new ProjectResult(packageId, version));
|
||||
}
|
||||
|
||||
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()));
|
||||
}
|
||||
});
|
||||
|
||||
string processError = "";
|
||||
var exitCode = await AnsiConsole.Status()
|
||||
.Spinner(Spinner.Known.Dots)
|
||||
.StartAsync<int>("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;
|
||||
static async Task<Result<Success, ReadableError>> 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);
|
||||
|
||||
p?.BeginOutputReadLine();
|
||||
p?.BeginErrorReadLine();
|
||||
if (result.ExitCode != 0)
|
||||
return new ReadableError(result.Error + "\n" + string.Format(Exceptions.dotnet_pack_failure.EscapeMarkup(), result.ExitCode), false);
|
||||
|
||||
try {
|
||||
await (p?.WaitForExitAsync(cts.Token) ?? Task.CompletedTask);
|
||||
}
|
||||
catch (TaskCanceledException) {
|
||||
p?.Kill();
|
||||
return Success.AsResult();
|
||||
}
|
||||
|
||||
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;
|
||||
enum DestinationType {
|
||||
Local,
|
||||
Github,
|
||||
Cloud
|
||||
}
|
||||
|
||||
if (Verbose)
|
||||
AnsiConsole.MarkupLine("Successfully created package with exit code [green]{0}[/]. Processing destinations.", exitCode);
|
||||
public static async Task Main(string[] args) {
|
||||
ArgValues? parsed = ReadArgs(args).Unwrap();
|
||||
if (parsed is null) return;
|
||||
|
||||
var package = Directory.GetFiles(outDir, "*.nupkg").FirstOrDefault();
|
||||
if (package == null) {
|
||||
ShowError(Exceptions.generic_error.EscapeMarkup());
|
||||
RestoreActions.ForEach(x => x());
|
||||
return;
|
||||
}
|
||||
RunContext rctx = new([], parsed, Path.GetRandomFileName());
|
||||
|
||||
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() {
|
||||
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(inMemory);
|
||||
using var reader = new MemoryStream(pkg.Read());
|
||||
var task = ctx.AddTask(dest, new ProgressTaskSettings() {
|
||||
MaxValue = size
|
||||
MaxValue = pkg.Size()
|
||||
});
|
||||
|
||||
DestinationType destType;
|
||||
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);
|
||||
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();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
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();
|
||||
});
|
||||
});
|
||||
}
|
||||
finally {
|
||||
try {
|
||||
Directory.Delete(outDir, true);
|
||||
}
|
||||
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());
|
||||
}
|
||||
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 <PackageReference Include=\"{0}\" Version=\"{1}\" />".EscapeMarkup(), packageId,
|
||||
version);
|
||||
"Example usage:\n\t <PackageReference Include=\"{0}\" Version=\"{1}\" />".EscapeMarkup(), project.PackageId,
|
||||
project.Version);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the version string by applying the specified operation to the major, minor, and patch components of the version.
|
||||
/// </summary>
|
||||
/// <param name="version">The current version string in the format "major.minor.patch[-tag]".</param>
|
||||
/// <param name="patch">The value to apply to the patch component.</param>
|
||||
/// <param name="minor">The value to apply to the minor component.</param>
|
||||
/// <param name="major">The value to apply to the major component.</param>
|
||||
/// <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 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());
|
||||
|
||||
@@ -218,4 +218,51 @@ internal class ProjectFile {
|
||||
MainPropertyGroup.SetProperty("Version", version);
|
||||
MainPropertyGroup.SetProperty("PackageVersion", version);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the version string by applying the specified operation to the major, minor, and patch components of the version.
|
||||
/// </summary>
|
||||
/// <param name="version">The current version string in the format "major.minor.patch[-tag]".</param>
|
||||
/// <param name="patch">The value to apply to the patch component.</param>
|
||||
/// <param name="minor">The value to apply to the minor component.</param>
|
||||
/// <param name="major">The value to apply to the major component.</param>
|
||||
/// <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>
|
||||
public static Result<string, ReadableError> 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -10,6 +10,10 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="aeqw89.xml.ProjectFile" Version="1.0.3" />
|
||||
<PackageReference Include="Aigamo.ResXGenerator" Version="4.3.0">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Spectre.Console" Version="0.51.2-preview.0.1" />
|
||||
<PackageReference Include="SSH.NET" Version="2025.0.0" />
|
||||
</ItemGroup>
|
||||
@@ -29,4 +33,8 @@
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<ResXGenerator_NullForgivingOperators>true</ResXGenerator_NullForgivingOperators>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user