Added new destination type : gitea, and revamped build workflow

This commit is contained in:
qwsdcvghyu89
2026-06-04 23:07:48 +10:00
parent d2f2f671a4
commit 08a221202f
8 changed files with 133 additions and 244 deletions
+3 -2
View File
@@ -1,2 +1,3 @@
bin/** **/bin
obj/** **/obj
**/dist
+28 -8
View File
@@ -135,21 +135,41 @@ sealed record CloudDestiantion(DestinationContext Context) : IDestination {
} }
} }
sealed record GithubDestination(DestinationContext Context, bool Verbose = false) : IDestination {
sealed record GitDestination(DestinationContext Context, string Source, string ApiKey, bool Verbose = false) : IDestination {
private static Result<string, ReadableError> GetApiKey(string host) {
var key = Environment.GetEnvironmentVariable($"git-{host}-packages-key");
if (key is null) return new ReadableError(string.Format("No key stored in EnvironmentVariables with name {0}", $"git-{host}-packages-key"), false);
return key.Ok();
}
public static Result<GitDestination, ReadableError> CreateForGitea(DestinationContext context, string repoOwner, bool verbose = false) {
// gitea source structure = https://gitea.example.com/api/packages/{owner}/nuget/index.json
var keyResult = GetApiKey("gitea");
if (keyResult is ReadableError error) return error;
return new GitDestination(context, $"https://gitea.example.com/api/packages/{repoOwner}/nuget/index.json", keyResult.Unwrap(), verbose).Ok();
}
public static Result<GitDestination, ReadableError> CreateForGithub(DestinationContext context, string repoOwner, bool verbose = false) {
// github source structure = https://nuget.pkg.github.com/NAMESPACE/index.json
// namespace usually = repoOwner
var keyResult = GetApiKey("github");
if (keyResult is ReadableError error) return error;
return new GitDestination(context, $"https://nuget.pkg.github.com/{repoOwner}/index.json", keyResult.Unwrap(), verbose).Ok();
}
public async Task<Result<Success, ReadableError>> WaitForCompletion(CancellationToken ct = default) { public async Task<Result<Success, ReadableError>> WaitForCompletion(CancellationToken ct = default) {
var p = Process.Start(new ProcessStartInfo() { using var p = Process.Start(new ProcessStartInfo() {
FileName = "dotnet", FileName = "dotnet",
Arguments = $"nuget push \"{Context.PackageFile.FullName}\" --source github", Arguments = $"nuget push \"{Context.PackageFile.FullName}\" -s {Source} -k {ApiKey}",
WorkingDirectory = Environment.CurrentDirectory, WorkingDirectory = Environment.CurrentDirectory,
UseShellExecute = false, UseShellExecute = false,
RedirectStandardOutput = true, RedirectStandardOutput = true,
RedirectStandardError = true RedirectStandardError = true
}); });
var cts = CancellationTokenSource.CreateLinkedTokenSource(ct); using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
StringBuilder errorLines = new(); StringBuilder errorLines = new();
p?.ErrorDataReceived += (sender, eventArgs) => { p?.ErrorDataReceived += (sender, eventArgs) => {
cts.Cancel();
if (Verbose && eventArgs.Data != null) if (Verbose && eventArgs.Data != null)
AnsiConsole.WriteLine(eventArgs.Data); AnsiConsole.WriteLine(eventArgs.Data);
errorLines.Append(eventArgs.Data); errorLines.Append(eventArgs.Data);
@@ -169,16 +189,16 @@ sealed record GithubDestination(DestinationContext Context, bool Verbose = false
try { try {
await (p?.WaitForExitAsync(cts.Token) ?? Task.CompletedTask); await (p?.WaitForExitAsync(cts.Token) ?? Task.CompletedTask);
} }
catch (TaskCanceledException) { catch (OperationCanceledException) {
p?.Kill(); p?.Kill();
await (p?.WaitForExitAsync(ct));
} }
if (p?.ExitCode != 0) { if (p?.ExitCode != 0) {
Context.Task.StopTask(); Context.Task.StopTask();
return new ReadableError(errorLines.ToString().EscapeMarkup() + "\n" + string.Format(Exceptions.dotnet_nuget_push_failure, p?.ExitCode ?? -1), false); return new ReadableError(errorLines.ToString().EscapeMarkup() + "\n" + string.Format(Exceptions.dotnet_nuget_push_failure, p?.ExitCode ?? -1), false);
} }
Context.Task.Increment(Context.BufferSize / 2); Context.Task.Increment(Context.PackageSize / 2);
return Success.AsResult(); return Success.AsResult();
} }
} }
-224
View File
@@ -1,224 +0,0 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace aeqw89.tools.Publish {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Exceptions {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Exceptions() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("aeqw89.tools.Publish.Exceptions", typeof(Exceptions).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to The cloud host &apos;{0}&apos; is not an entry on this user&apos;s config file..
/// </summary>
internal static string cloud_host_not_found {
get {
return ResourceManager.GetString("cloud_host_not_found", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The mode &apos;{0}&apos; is invalid, the valid modes are [overwrite|increment].
/// </summary>
internal static string could_not_parse_mode {
get {
return ResourceManager.GetString("could_not_parse_mode", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The increment target &apos;{0}&apos; is invalid, the valid increment targets are [patch|minor|patch].
/// </summary>
internal static string could_not_parse_target {
get {
return ResourceManager.GetString("could_not_parse_target", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The &apos;dotnet nuget push&apos; command failed with error message &apos;{0}&apos;.
/// </summary>
internal static string dotnet_nuget_push_failure {
get {
return ResourceManager.GetString("dotnet_nuget_push_failure", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Failed to pack with exit code &apos;{0}&apos;; ensure that &apos;dotnet build&apos; succeeds before running this program..
/// </summary>
internal static string dotnet_pack_failure {
get {
return ResourceManager.GetString("dotnet_pack_failure", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Could not delete temporary directory &apos;{0}&apos; due to error &apos;{1}&apos;.
/// </summary>
internal static string failed_to_clean_up {
get {
return ResourceManager.GetString("failed_to_clean_up", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Failde to prepare an upload directory on the path {0} for the remote host &apos;{1}&apos;, after being detected as a {2} host. Server error is &apos;{3}&apos;.
/// </summary>
internal static string failed_to_prepare_server_directory {
get {
return ResourceManager.GetString("failed_to_prepare_server_directory", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The flag &apos;{0}&apos; requires exactly &apos;{1}&apos; parameters. You have entered &apos;{2}&apos;..
/// </summary>
internal static string flag_parameter_length_incorrect {
get {
return ResourceManager.GetString("flag_parameter_length_incorrect", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The &apos;{0}&apos; flag requires that argument with index &apos;{1}&apos; be of type &apos;{2}&apos;. You have entered &apos;{3}&apos; which has failed to be converted..
/// </summary>
internal static string flag_parameter_type_incorrect {
get {
return ResourceManager.GetString("flag_parameter_type_incorrect", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The directory &apos;{0}&apos; contains multiple .csproj files; this tool can only process one at a time..
/// </summary>
internal static string found_multiple_csproj {
get {
return ResourceManager.GetString("found_multiple_csproj", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Something went wrong loading this file; {0}.
/// </summary>
internal static string generic_error {
get {
return ResourceManager.GetString("generic_error", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to You must specify at least one destination..
/// </summary>
internal static string missing_destinations {
get {
return ResourceManager.GetString("missing_destinations", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to You must specify an increment target if you specified an increment mode; allowed increment targets are [patch|minor|major].
/// </summary>
internal static string missing_increment_target {
get {
return ResourceManager.GetString("missing_increment_target", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to You must specify a mode; allowed modes are [overwrite|increment].
/// </summary>
internal static string missing_mode {
get {
return ResourceManager.GetString("missing_mode", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to No project file was found within the current directory..
/// </summary>
internal static string no_project_in_directory {
get {
return ResourceManager.GetString("no_project_in_directory", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The project file &apos;{0}&apos; is irreparable becuase it is missing a &apos;{1}&apos; property, and the value cannot be guessed..
/// </summary>
internal static string project_file_irreparable {
get {
return ResourceManager.GetString("project_file_irreparable", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Something went wrong; an attempt was made to load a non .csproj file as a project file..
/// </summary>
internal static string tried_loading_non_csproj_file {
get {
return ResourceManager.GetString("tried_loading_non_csproj_file", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The version string &apos;{0}&apos; is in an unidentifiable format..
/// </summary>
internal static string version_string_not_formatted_correctly {
get {
return ResourceManager.GetString("version_string_not_formatted_correctly", resourceCulture);
}
}
}
}
+19 -9
View File
@@ -5,7 +5,6 @@ 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;
/* /*
@@ -116,7 +115,7 @@ public static class Program {
return result; return result;
} }
record ProjectResult(string PackageId, string Version); record ProjectResult(string PackageId, string Version, string RepoOwner);
static async Task<Result<ProjectResult, ReadableError>> PrepareProject(RunContext rctx, StatusContext ctx) { static async Task<Result<ProjectResult, ReadableError>> PrepareProject(RunContext rctx, StatusContext ctx) {
ctx.Status = "Locating project file"; ctx.Status = "Locating project file";
if (!ProjectFile.TryLoad(Environment.CurrentDirectory, out var projectFile, out var error)) if (!ProjectFile.TryLoad(Environment.CurrentDirectory, out var projectFile, out var error))
@@ -124,6 +123,7 @@ public static class Program {
string packageId = projectFile.GetPackageId(); string packageId = projectFile.GetPackageId();
string version; string version;
string repoOwner;
try { try {
projectFile.Backup(); projectFile.Backup();
@@ -161,8 +161,8 @@ public static class Program {
ctx.Status = "Updating version"; ctx.Status = "Updating version";
version = projectFile.GetVersion(); version = projectFile.GetVersion();
version = ProjectFile.ChangeVersion(version, delta, rctx.Args.Target ?? IncrementTarget.Patch).Unwrap(rctx); version = ProjectFile.ChangeVersion(version, delta, rctx.Args.Target ?? IncrementTarget.Patch).Unwrap(rctx);
projectFile.SetVersion(version); projectFile.SetVersion(version);
} }
} }
catch (Exception e) { catch (Exception e) {
@@ -170,6 +170,8 @@ public static class Program {
} }
version = projectFile.GetVersion(); version = projectFile.GetVersion();
repoOwner = projectFile.GetRepositoryOwner();
if (!rctx.Args.Flags.ContainsKey("--simulate")) { if (!rctx.Args.Flags.ContainsKey("--simulate")) {
try { try {
@@ -221,7 +223,7 @@ public static class Program {
} }
projectFile.Save(); projectFile.Save();
return new Ok<ProjectResult>(new ProjectResult(packageId, version)); return new Ok<ProjectResult>(new ProjectResult(packageId, version, repoOwner));
} }
static async Task<Result<Success, ReadableError>> PackProject(RunContext rctx, StatusContext ctx) { static async Task<Result<Success, ReadableError>> PackProject(RunContext rctx, StatusContext ctx) {
@@ -242,6 +244,7 @@ public static class Program {
enum DestinationType { enum DestinationType {
Local, Local,
Github, Github,
Main,
Cloud Cloud
} }
@@ -286,7 +289,10 @@ public static class Program {
destType = DestinationType.Cloud; destType = DestinationType.Cloud;
} else if (dest == "github") { } else if (dest == "github") {
destType = DestinationType.Github; destType = DestinationType.Github;
} else { } else if (dest == "main") {
destType = DestinationType.Main;
}
else {
lock(rctx) { lock(rctx) {
ShowError(string.Format(Exceptions.destination_unrecognizable, dest)); ShowError(string.Format(Exceptions.destination_unrecognizable, dest));
ShowHelp(); ShowHelp();
@@ -298,16 +304,20 @@ public static class Program {
var dctx = new DestinationContext(task, ctx, reader, pkg.FileInfo, destType switch { var dctx = new DestinationContext(task, ctx, reader, pkg.FileInfo, destType switch {
DestinationType.Cloud => dest["cloud-".Length..], DestinationType.Cloud => dest["cloud-".Length..],
DestinationType.Github => dest["github".Length..], DestinationType.Github => dest["github".Length..],
DestinationType.Main => dest["main".Length..],
DestinationType.Local => dest["local-".Length..] DestinationType.Local => dest["local-".Length..]
}, BufferSize, pkg.Size()); }, BufferSize, pkg.Size());
IDestination destination = destType switch { Result<IDestination, ReadableError> destinationResult = destType switch {
DestinationType.Local => new LocalDestination(dctx), DestinationType.Local => new LocalDestination(dctx).Ok<IDestination>(),
DestinationType.Github => new GithubDestination(dctx, rctx.Args.Verbose), DestinationType.Github => GitDestination.CreateForGithub(dctx, project.RepoOwner, rctx.Args.Verbose).UpcastSuccess<GitDestination, IDestination, ReadableError>(),
DestinationType.Cloud => new CloudDestiantion(dctx), DestinationType.Main => GitDestination.CreateForGitea(dctx, project.RepoOwner, rctx.Args.Verbose).UpcastSuccess<GitDestination, IDestination, ReadableError>(),
DestinationType.Cloud => new CloudDestiantion(dctx).Ok<IDestination>(),
_ => throw new UnreachableException() _ => throw new UnreachableException()
}; };
var destination = destinationResult.Unwrap(rctx);
var result = await destination.WaitForCompletion(ct); var result = await destination.WaitForCompletion(ct);
lock(rctx) { lock(rctx) {
if (result.Unwrap(rctx) is not null) { if (result.Unwrap(rctx) is not null) {
+5
View File
@@ -114,6 +114,7 @@ internal class ProjectFile {
set("PackageProjectUrl", "", required: false); set("PackageProjectUrl", "", required: false);
set("RepositoryUrl", "", required: true); set("RepositoryUrl", "", required: true);
set("PackageId", "", required: true); set("PackageId", "", required: true);
set("RepositoryOwner", "", required: true);
if (failed.Count > 0) { if (failed.Count > 0) {
error = string.Format(Exceptions.project_file_irreparable, Path, string.Join(", ", failed)); error = string.Format(Exceptions.project_file_irreparable, Path, string.Join(", ", failed));
@@ -124,6 +125,10 @@ internal class ProjectFile {
return true; return true;
} }
public string GetRepositoryOwner() {
return MainPropertyGroup.GetProperty("RepositoryOwner");
}
public List<PackageReference> GetPackageReferences() { public List<PackageReference> GetPackageReferences() {
return Project.ItemGroups return Project.ItemGroups
.SelectMany(g => g.Items) .SelectMany(g => g.Items)
+46
View File
@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by TARGET FORGE — MSBuild publish target -->
<!-- Save as PublishAll.targets and <Import Project="PublishAll.targets" /> in your .csproj, -->
<!-- or paste the <Target> below directly into the .csproj. -->
<!-- Run: dotnet msbuild -t:PublishAll -->
<Project>
<Target Name="PublishAll">
<ItemGroup>
<Rid Include="win-x64;linux-x64;osx-x64;osx-arm64" />
</ItemGroup>
<MSBuild Projects="$(MSBuildProjectFullPath)"
Targets="Publish"
BuildInParallel="true"
Properties="Configuration=Release;
Platform=Any CPU;
TargetFramework=net9.0;
RuntimeIdentifier=%(Rid.Identity);
SelfContained=true;
PublishSingleFile=true;
PublishDir=bin\Release\publish\%(Rid.Identity)\" />
</Target>
<PropertyGroup>
<!-- All four are overridable from the CLI: -p:ArchiveVersion=1.2.3 etc. -->
<ArchiveAppName Condition="'$(ArchiveAppName)' == ''">$(AssemblyName)</ArchiveAppName>
<ArchiveVersion Condition="'$(ArchiveVersion)' == ''">$(Version)</ArchiveVersion>
<ArchiveVersion Condition="'$(ArchiveVersion)' == ''">1.3.0</ArchiveVersion>
<ArchiveOutputDir Condition="'$(ArchiveOutputDir)' == ''">$(MSBuildProjectDirectory)\dist\</ArchiveOutputDir>
<PublishBaseDir Condition="'$(PublishBaseDir)' == ''">$(MSBuildProjectDirectory)\bin\Release\publish\</PublishBaseDir>
</PropertyGroup>
<Target Name="Archive" DependsOnTargets="PublishAll">
<MakeDir Directories="$(ArchiveOutputDir)" />
<ZipDirectory SourceDirectory="$(PublishBaseDir)%(Rid.Identity)\"
DestinationFile="$(ArchiveOutputDir)$(ArchiveAppName)-$(ArchiveVersion)-%(Rid.Identity).zip"
Overwrite="true" />
<Exec Command='tar -czf "$(ArchiveOutputDir)$(ArchiveAppName)-$(ArchiveVersion)-%(Rid.Identity).tar.gz" -C "$(PublishBaseDir)%(Rid.Identity)" .' />
<Message Importance="high" Text="Archived $(ArchiveAppName) $(ArchiveVersion) → $(ArchiveOutputDir)" />
</Target>
</Project>
+26
View File
@@ -28,10 +28,36 @@ internal static class ResultExtensions {
public static async Task<T> Unwrap<T>(this Task<Result<T, ReadableError>> result, Program.RunContext? rctx = null) { public static async Task<T> Unwrap<T>(this Task<Result<T, ReadableError>> result, Program.RunContext? rctx = null) {
return (await result).Unwrap(rctx); return (await result).Unwrap(rctx);
} }
public static Result<TTo, ETo> Cast<TFrom, EFrom, TTo, ETo>(this Result<TFrom, EFrom> result) where TTo : notnull, TFrom where ETo: notnull, EFrom {
return result switch {
TFrom tfrom => ((TTo)tfrom).Ok(),
EFrom efrom => ((ETo)efrom),
_ => throw new UnreachableException()
};
}
public static Result<TTo, ETo> Upcast<TFrom, EFrom, TTo, ETo>(this Result<TFrom, EFrom> result)
where TFrom : TTo
where EFrom : ETo
{
return result switch {
TFrom tfrom => ((TTo)tfrom).Ok(), // Foo -> IFoo, implicit upcast, can't throw
EFrom efrom => ((ETo)efrom),
_ => throw new UnreachableException()
};
}
public static Result<TTo, E> CastSuccess<TFrom, TTo, E>(this Result<TFrom, E> result) where TTo : notnull, TFrom where E: notnull
=> Cast<TFrom, E, TTo, E>(result);
public static Result<TTo, E> UpcastSuccess<TFrom, TTo, E>(this Result<TFrom, E> result) where TFrom : notnull, TTo where E: notnull
=> Upcast<TFrom, E, TTo, E>(result);
} }
internal static class ObjectExtensions { internal static class ObjectExtensions {
public static Ok<T> Ok<T>(this T any) { public static Ok<T> Ok<T>(this T any) {
return new Ok<T>(any); return new Ok<T>(any);
} }
} }
@@ -1,4 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<Import Project="PublishAll.targets" />
<PropertyGroup>
<RuntimeIdentifiers>win-x64;linux-x64;osx-arm64;osx-x64</RuntimeIdentifiers>
</PropertyGroup>
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
@@ -9,7 +14,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="aeqw89.xml.ProjectFile" Version="1.0.3" /> <PackageReference Include="aeqw89.xml.ProjectFile" Version="2.0.0" />
<PackageReference Include="Aigamo.ResXGenerator" Version="4.3.0"> <PackageReference Include="Aigamo.ResXGenerator" Version="4.3.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>