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"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
<root>
|
<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
|
||||||
|
|
||||||
</xsd:element>
|
Version 2.0
|
||||||
</xsd:schema>
|
|
||||||
<resheader name="resmimetype">
|
The primary goals of this format is to allow a simple XML format
|
||||||
<value>text/microsoft-resx</value>
|
that is mostly human readable. The generation and parsing of the
|
||||||
</resheader>
|
various data types are done through the TypeConverter classes
|
||||||
<resheader name="version">
|
associated with the data types.
|
||||||
<value>1.3</value>
|
|
||||||
</resheader>
|
Example:
|
||||||
<resheader name="reader">
|
|
||||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
... ado.net/XML headers & schema ...
|
||||||
</resheader>
|
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||||
<resheader name="writer">
|
<resheader name="version">2.0</resheader>
|
||||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||||
</resheader>
|
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||||
<data name="missing_mode" xml:space="preserve">
|
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||||
<value>You must specify a mode; allowed modes are [overwrite|increment]</value>
|
<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>
|
||||||
<data name="could_not_parse_mode" xml:space="preserve">
|
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||||
<value>The mode '{0}' is invalid, the valid modes are [overwrite|increment]</value>
|
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||||
</data>
|
<comment>This is a comment</comment>
|
||||||
<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>
|
|
||||||
</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>
|
|
||||||
</data>
|
|
||||||
<data name="missing_destinations" xml:space="preserve">
|
|
||||||
<value>You must specify at least one destination.</value>
|
|
||||||
</data>
|
|
||||||
<data name="no_project_in_directory" xml:space="preserve">
|
|
||||||
<value>No project file was found within the current directory.</value>
|
|
||||||
</data>
|
|
||||||
<data name="flag_parameter_length_incorrect" xml:space="preserve">
|
|
||||||
<value>The flag '{0}' requires exactly '{1}' parameters. You have entered '{2}'.</value>
|
|
||||||
</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>
|
|
||||||
</data>
|
|
||||||
<data name="version_string_not_formatted_correctly" xml:space="preserve">
|
|
||||||
<value>The version string '{0}' is in an unidentifiable format.</value>
|
|
||||||
</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>
|
|
||||||
</data>
|
|
||||||
<data name="generic_error" xml:space="preserve">
|
|
||||||
<value>Something went wrong loading this file; {0}</value>
|
|
||||||
</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>
|
|
||||||
</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>
|
|
||||||
</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>
|
|
||||||
</data>
|
|
||||||
<data name="failed_to_clean_up" xml:space="preserve">
|
|
||||||
<value>Could not delete temporary directory '{0}' due to error '{1}'</value>
|
|
||||||
</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>
|
|
||||||
</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>
|
|
||||||
</data>
|
|
||||||
<data name="dotnet_nuget_push_failure" xml:space="preserve">
|
|
||||||
<value>The 'dotnet nuget push' command failed with error message '{0}'</value>
|
|
||||||
</data>
|
</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>2.0</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="reader">
|
||||||
|
<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=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>
|
</root>
|
||||||
+253
-513
@@ -5,6 +5,7 @@ using Renci.SshNet;
|
|||||||
using Spectre.Console;
|
using Spectre.Console;
|
||||||
using aeqw89.xml.ProjectFile;
|
using aeqw89.xml.ProjectFile;
|
||||||
|
|
||||||
|
|
||||||
namespace aeqw89.tools.Publish;
|
namespace aeqw89.tools.Publish;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -20,74 +21,80 @@ namespace aeqw89.tools.Publish;
|
|||||||
* e.g. publish overwrite|increment [patch|minor|major] destinations [flags]
|
* e.g. publish overwrite|increment [patch|minor|major] destinations [flags]
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public static class Program {
|
public static class Program {
|
||||||
public static Mode Mode { get; set; }
|
const long BufferSize = 80 * 1024; // 80 KB
|
||||||
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;
|
|
||||||
|
|
||||||
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 (deleteTempDir) {
|
||||||
if (args.Length < 1) {
|
try {
|
||||||
ShowError(Exceptions.missing_mode.EscapeMarkup());
|
if (!Directory.Exists(TempDir)) return;
|
||||||
ShowHelp();
|
Directory.Delete(TempDir, true);
|
||||||
return;
|
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<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());
|
||||||
|
|
||||||
Mode = args[0] switch {
|
var mode = args[0].ToLower() switch {
|
||||||
"overwrite" => Mode.Overwrite,
|
"overwrite" => Mode.Overwrite,
|
||||||
"increment" => Mode.Increment,
|
"increment" => Mode.Increment,
|
||||||
_ => (Mode)(-1)
|
_ => (Mode)(-1) // mode must be one of two values
|
||||||
};
|
};
|
||||||
|
|
||||||
if (Mode == (Mode)(-1)) {
|
if (mode == (Mode)(-1)) // invalid mode
|
||||||
ShowError(Exceptions.could_not_parse_mode.EscapeMarkup(), args[0].EscapeMarkup());
|
return new ReadableError(string.Format(Exceptions.could_not_parse_mode.EscapeMarkup(), args[0].EscapeMarkup()));
|
||||||
ShowHelp();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (args.Length < 2) {
|
if (args.Length < 2) // not enough args (min = 2)
|
||||||
if (Mode == Mode.Increment)
|
return new ReadableError(mode switch {
|
||||||
ShowError(Exceptions.missing_increment_target.EscapeMarkup());
|
Mode.Increment => Exceptions.missing_increment_target.EscapeMarkup(),
|
||||||
else if (Mode == Mode.Overwrite)
|
Mode.Overwrite => Exceptions.missing_destinations.EscapeMarkup(),
|
||||||
ShowError(Exceptions.missing_destinations.EscapeMarkup());
|
_ => throw new UnreachableException()
|
||||||
ShowHelp();
|
});
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Destinations = args[1..];
|
var destinations = args[1..]; // destinations is variarg.
|
||||||
Flags = [];
|
Dictionary<string, string[]> flags = []; // flags is parsed last
|
||||||
if (Mode == Mode.Increment) {
|
IncrementTarget? target = null;
|
||||||
if (args.Length < 3) {
|
if (mode == Mode.Increment) {
|
||||||
ShowError(Exceptions.missing_destinations.EscapeMarkup());
|
if (args.Length < 3) // increment mode requires target version in addition to destinations
|
||||||
ShowHelp();
|
return new ReadableError(Exceptions.missing_destinations.EscapeMarkup());
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
"patch" => IncrementTarget.Patch,
|
||||||
"minor" => IncrementTarget.Minor,
|
"minor" => IncrementTarget.Minor,
|
||||||
"major" => IncrementTarget.Major,
|
"major" => IncrementTarget.Major,
|
||||||
_ => (IncrementTarget)(-1)
|
_ => (IncrementTarget)(-1)
|
||||||
};
|
};
|
||||||
|
|
||||||
if (Target == (IncrementTarget)(-1)) {
|
if (target == (IncrementTarget)(-1)) // unrecognizable target entered
|
||||||
ShowError(Exceptions.could_not_parse_target.EscapeMarkup(), args[1].EscapeMarkup());
|
return new ReadableError(string.Format(Exceptions.could_not_parse_target.EscapeMarkup(), args[1].EscapeMarkup()));
|
||||||
ShowHelp();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
string? firstFlag = Destinations.FirstOrDefault(x => x.StartsWith('-'));
|
string? firstFlag = destinations.FirstOrDefault(x => x.StartsWith('-')); // find the first arg that starts with '-' signifying a flag.
|
||||||
if (firstFlag == null) return;
|
if (firstFlag == null) // no flags case - return early.
|
||||||
string[] flags = Destinations.SkipWhile(x => x != firstFlag).ToArray();
|
return new Ok<ArgValues>(new ArgValues(mode, destinations, flags, false, target));
|
||||||
Flags = ReadFlags(flags);
|
string[] flagsRaw = destinations.SkipWhile(x => x != firstFlag).ToArray(); // extract flags from destinations.
|
||||||
Destinations = Destinations.TakeWhile(x => x != firstFlag).ToArray();
|
flags = ReadFlags(flagsRaw); // get flags as dictionary to args (pattern is -flag0 [arg0] [arg1] -flag1 ...)
|
||||||
Verbose = Flags.ContainsKey("--verbose") || Flags.ContainsKey("-v");
|
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) {
|
private static Dictionary<string, string[]> ReadFlags(string[] flags) {
|
||||||
@@ -105,501 +112,234 @@ public static class Program {
|
|||||||
collected.Add(flag);
|
collected.Add(flag);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
result[lastKey] = collected.ToArray();
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task Main(string[] args) {
|
record ProjectResult(string PackageId, string Version);
|
||||||
ReadArgs(args);
|
static async Task<Result<ProjectResult, ReadableError>> PrepareProject(RunContext rctx, StatusContext ctx) {
|
||||||
|
ctx.Status = "Locating project file";
|
||||||
Console.CancelKeyPress += (sender, eventArgs) => {
|
if (!ProjectFile.TryLoad(Environment.CurrentDirectory, out var projectFile, out var error))
|
||||||
RestoreActions.ForEach(x => x());
|
return new ReadableError(error);
|
||||||
};
|
|
||||||
|
|
||||||
string packageId = "";
|
string packageId = projectFile.GetPackageId();
|
||||||
string version = "";
|
string version;
|
||||||
int destinationsProcessed = 0;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var result = AnsiConsole.Status()
|
projectFile.Backup();
|
||||||
.Spinner(Spinner.Known.Dots)
|
rctx.RestoreActions.Add(() => {
|
||||||
.Start<bool>("Preparing project", ctx => {
|
projectFile.Restore();
|
||||||
ctx.Status = "Locating project file";
|
AnsiConsole.MarkupLine("[yellow]Restored project file from backup.[/]");
|
||||||
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<string> visited = [];
|
|
||||||
var projectReferences = new Queue<Item>(projectFile.GetProjectReferences().Cast<Item>());
|
|
||||||
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()));
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
p?.BeginOutputReadLine();
|
if (rctx.Args.Verbose)
|
||||||
p?.BeginErrorReadLine();
|
AnsiConsole.WriteLine(
|
||||||
|
$"Created project file backup at {projectFile.GetDefaultBackupLocation()}");
|
||||||
|
|
||||||
try {
|
ctx.Status = "Repairing project file";
|
||||||
await (p?.WaitForExitAsync(cts.Token) ?? Task.CompletedTask);
|
if (!rctx.Args.Flags.ContainsKey("--skip-repair"))
|
||||||
}
|
if (!projectFile.TryRepair(out error)) {
|
||||||
catch (TaskCanceledException) {
|
return new ReadableError(error.EscapeMarkup(), false);
|
||||||
p?.Kill();
|
}
|
||||||
|
|
||||||
|
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();
|
if (!int.TryParse(deltaStrings[0], out delta)) {
|
||||||
return success == true ? 0 : p?.ExitCode ?? -1;
|
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<string> visited = [];
|
||||||
|
var projectReferences = new Queue<Item>(projectFile.GetProjectReferences().Cast<Item>());
|
||||||
|
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<ProjectResult>(new ProjectResult(packageId, version));
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
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) {
|
DestinationType destType;
|
||||||
ShowError(processError.EscapeMarkup());
|
if (dest.StartsWith("local-")) {
|
||||||
ShowError(Exceptions.dotnet_pack_failure.EscapeMarkup(), exitCode);
|
destType = DestinationType.Local;
|
||||||
RestoreActions.ForEach(x => x());
|
} else if (dest.StartsWith("cloud-")) {
|
||||||
return;
|
destType = DestinationType.Cloud;
|
||||||
}
|
} else if (dest == "github") {
|
||||||
|
destType = DestinationType.Github;
|
||||||
if (Verbose)
|
} else {
|
||||||
AnsiConsole.MarkupLine("Successfully created package with exit code [green]{0}[/]. Processing destinations.", exitCode);
|
lock(rctx) {
|
||||||
|
ShowError(string.Format(Exceptions.destination_unrecognizable, dest));
|
||||||
var package = Directory.GetFiles(outDir, "*.nupkg").FirstOrDefault();
|
ShowHelp();
|
||||||
if (package == null) {
|
}
|
||||||
ShowError(Exceptions.generic_error.EscapeMarkup());
|
task.StopTask();
|
||||||
RestoreActions.ForEach(x => x());
|
return;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
catch (Exception e) {
|
|
||||||
ShowError(string.Format(Exceptions.failed_to_clean_up.EscapeMarkup(), outDir.EscapeMarkup(),
|
|
||||||
e.ToString().EscapeMarkup()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
var dctx = new DestinationContext(task, ctx, reader, pkg.FileInfo, destType switch {
|
||||||
catch(Exception e) {
|
DestinationType.Cloud => dest["cloud-".Length..],
|
||||||
ShowError(Exceptions.generic_error.EscapeMarkup(), e.ToString().EscapeMarkup());;
|
DestinationType.Github => dest["github".Length..],
|
||||||
RestoreActions.ForEach(x => x());
|
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) {
|
if (destinationsProcessed == 0) {
|
||||||
AnsiConsole.MarkupLine("[bold red]No destinations were processed. Reverting changes to project file.[/]");
|
AnsiConsole.MarkupLine("[bold red]No destinations were processed. Reverting changes to project file.[/]");
|
||||||
RestoreActions.ForEach(x => x());
|
rctx.Restore();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
AnsiConsole.MarkupLine("Completed processing of all destinations.");
|
AnsiConsole.MarkupLine("Completed processing of all destinations.");
|
||||||
AnsiConsole.MarkupLine(
|
AnsiConsole.MarkupLine(
|
||||||
"Example usage:\n\t <PackageReference Include=\"{0}\" Version=\"{1}\" />".EscapeMarkup(), packageId,
|
"Example usage:\n\t <PackageReference Include=\"{0}\" Version=\"{1}\" />".EscapeMarkup(), project.PackageId,
|
||||||
version);
|
project.Version);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public static void ShowError(string message, params object[] args) {
|
||||||
/// 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) {
|
|
||||||
AnsiConsole.MarkupLine($"[bold red]{message}[/]", 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" +
|
AnsiConsole.Markup(("Usage: publish overwrite|increment [patch|minor|major] destinations [flags]\n" +
|
||||||
"\t if mode: overwrite destinations [flags]\n" +
|
"\t if mode: overwrite destinations [flags]\n" +
|
||||||
"\t if mode: increment patch|minor|major [flags]\n").EscapeMarkup());
|
"\t if mode: increment patch|minor|major [flags]\n").EscapeMarkup());
|
||||||
|
|||||||
@@ -218,4 +218,51 @@ internal class ProjectFile {
|
|||||||
MainPropertyGroup.SetProperty("Version", version);
|
MainPropertyGroup.SetProperty("Version", version);
|
||||||
MainPropertyGroup.SetProperty("PackageVersion", 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>
|
<ItemGroup>
|
||||||
<PackageReference Include="aeqw89.xml.ProjectFile" Version="1.0.3" />
|
<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="Spectre.Console" Version="0.51.2-preview.0.1" />
|
||||||
<PackageReference Include="SSH.NET" Version="2025.0.0" />
|
<PackageReference Include="SSH.NET" Version="2025.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
@@ -29,4 +33,8 @@
|
|||||||
</Compile>
|
</Compile>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<ResXGenerator_NullForgivingOperators>true</ResXGenerator_NullForgivingOperators>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
Reference in New Issue
Block a user