15 Commits
Author SHA1 Message Date
qwsdcvghyu89 c4d573a69e Added gitea packages recognition. 2026-06-05 00:57:38 +10:00
qwsdcvghyu89 3fb20e57d1 Switched to using setup-dotnet instead of containers 2026-06-05 00:33:58 +10:00
qwsdcvghyu89 59b623c170 Reverted to use node (actions runner now has node available) 2026-06-05 00:30:11 +10:00
qwsdcvghyu89 b52ae3ce03 Fixed clone command again 2026-06-05 00:22:31 +10:00
qwsdcvghyu89 c7ec07cef2 Fixed clone command to use gitea 2026-06-05 00:20:08 +10:00
qwsdcvghyu89 f41e83765a Added custom container to checkout step. 2026-06-05 00:16:55 +10:00
qwsdcvghyu89 2e943882ec Updated container use semantics order to allow checkout to work 2026-06-05 00:13:40 +10:00
qwsdcvghyu89 e11c689602 Updated runs-on to run only on 'Debian-x86' with a capital D
Nightly Build and Release / Build and Release Nightly (push) Failing after 33s
2026-06-05 00:11:11 +10:00
qwsdcvghyu89 320fee57b2 Updated runs-on to match upper and lowercase
Nightly Build and Release / Build and Release Nightly (push) Canceled after 0s
2026-06-05 00:08:42 +10:00
qwsdcvghyu89 2b848baa33 Fixed branch filtering
Nightly Build and Release / Build and Release Nightly (push) Canceled after 0s
2026-06-05 00:04:27 +10:00
qwsdcvghyu89 9065d614e1 Added manual run button for action 'Nightly Build and Release' 2026-06-05 00:03:29 +10:00
qwsdcvghyu89 7d330e9368 Merge branch 'master' of https://git.main.qwsdcvghyu.com/riley/aeqw89.tools.Publish 2026-06-04 23:58:22 +10:00
qwsdcvghyu89 61a4902ac4 Added workflow for nightly releases 2026-06-04 23:51:46 +10:00
qwsdcvghyu89 08a221202f Added new destination type : gitea, and revamped build workflow 2026-06-04 23:07:48 +10:00
qwsdcvghyu89 d2f2f671a4 Improved logic. 2026-06-04 16:45:35 +10:00
9 changed files with 184 additions and 244 deletions
+51
View File
@@ -0,0 +1,51 @@
name: Nightly Build and Release
on:
push:
branches:
- main
workflow_dispatch:
jobs:
build-releases:
name: 'Build and Release Nightly'
runs-on: Debian-x86
permissions:
contents: write
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Setup .NET 9.0
uses: actions/setup-dotnet@v4
with:
dotnet-version: '9.0.x'
# NEW: Authenticate with your private Gitea NuGet registry
- name: Add Gitea NuGet Source
run: >
dotnet nuget add source "https://git.main.qwsdcvghyu.com/api/packages/riley/nuget/index.json"
--name "GiteaRegistry"
--username "riley"
--password "${{ secrets.PACKAGES_KEY }}"
--store-password-in-clear-text
- name: 'Execute .NET Publish Target'
run: dotnet build aeqw89.tools.Publish/aeqw89.tools.Publish.csproj -t:PublishAll
- name: Update Nightly Release
uses: softprops/action-gh-release@v2
with:
tag_name: nightly
name: "Nightly Development Build"
body: |
Automated rolling nightly build from the latest commit on `main`.
**Commit:** ${{ github.sha }}
prerelease: true
files: |
aeqw89.tools.Publish/dist/*.zip
aeqw89.tools.Publish/dist/*.tar.gz
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+3 -2
View File
@@ -1,2 +1,3 @@
bin/**
obj/**
**/bin
**/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) {
var p = Process.Start(new ProcessStartInfo() {
using var p = Process.Start(new ProcessStartInfo() {
FileName = "dotnet",
Arguments = $"nuget push \"{Context.PackageFile.FullName}\" --source github",
Arguments = $"nuget push \"{Context.PackageFile.FullName}\" -s {Source} -k {ApiKey}",
WorkingDirectory = Environment.CurrentDirectory,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true
});
var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
using 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);
@@ -169,16 +189,16 @@ sealed record GithubDestination(DestinationContext Context, bool Verbose = false
try {
await (p?.WaitForExitAsync(cts.Token) ?? Task.CompletedTask);
}
catch (TaskCanceledException) {
catch (OperationCanceledException) {
p?.Kill();
await (p?.WaitForExitAsync(ct));
}
if (p?.ExitCode != 0) {
Context.Task.StopTask();
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();
}
}
-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 aeqw89.xml.ProjectFile;
namespace aeqw89.tools.Publish;
/*
@@ -116,7 +115,7 @@ public static class Program {
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) {
ctx.Status = "Locating project file";
if (!ProjectFile.TryLoad(Environment.CurrentDirectory, out var projectFile, out var error))
@@ -124,6 +123,7 @@ public static class Program {
string packageId = projectFile.GetPackageId();
string version;
string repoOwner;
try {
projectFile.Backup();
@@ -161,8 +161,8 @@ public static class Program {
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) {
@@ -170,6 +170,8 @@ public static class Program {
}
version = projectFile.GetVersion();
repoOwner = projectFile.GetRepositoryOwner();
if (!rctx.Args.Flags.ContainsKey("--simulate")) {
try {
@@ -221,7 +223,7 @@ public static class Program {
}
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) {
@@ -242,6 +244,7 @@ public static class Program {
enum DestinationType {
Local,
Github,
Main,
Cloud
}
@@ -286,7 +289,10 @@ public static class Program {
destType = DestinationType.Cloud;
} else if (dest == "github") {
destType = DestinationType.Github;
} else {
} else if (dest == "main") {
destType = DestinationType.Main;
}
else {
lock(rctx) {
ShowError(string.Format(Exceptions.destination_unrecognizable, dest));
ShowHelp();
@@ -298,16 +304,20 @@ public static class Program {
var dctx = new DestinationContext(task, ctx, reader, pkg.FileInfo, destType switch {
DestinationType.Cloud => dest["cloud-".Length..],
DestinationType.Github => dest["github".Length..],
DestinationType.Main => dest["main".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),
Result<IDestination, ReadableError> destinationResult = destType switch {
DestinationType.Local => new LocalDestination(dctx).Ok<IDestination>(),
DestinationType.Github => GitDestination.CreateForGithub(dctx, project.RepoOwner, rctx.Args.Verbose).UpcastSuccess<GitDestination, IDestination, ReadableError>(),
DestinationType.Main => GitDestination.CreateForGitea(dctx, project.RepoOwner, rctx.Args.Verbose).UpcastSuccess<GitDestination, IDestination, ReadableError>(),
DestinationType.Cloud => new CloudDestiantion(dctx).Ok<IDestination>(),
_ => throw new UnreachableException()
};
var destination = destinationResult.Unwrap(rctx);
var result = await destination.WaitForCompletion(ct);
lock(rctx) {
if (result.Unwrap(rctx) is not null) {
+5
View File
@@ -114,6 +114,7 @@ internal class ProjectFile {
set("PackageProjectUrl", "", required: false);
set("RepositoryUrl", "", required: true);
set("PackageId", "", required: true);
set("RepositoryOwner", "", required: true);
if (failed.Count > 0) {
error = string.Format(Exceptions.project_file_irreparable, Path, string.Join(", ", failed));
@@ -124,6 +125,10 @@ internal class ProjectFile {
return true;
}
public string GetRepositoryOwner() {
return MainPropertyGroup.GetProperty("RepositoryOwner");
}
public List<PackageReference> GetPackageReferences() {
return Project.ItemGroups
.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) {
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 {
public static Ok<T> Ok<T>(this T any) {
return new Ok<T>(any);
}
}
@@ -1,4 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="PublishAll.targets" />
<PropertyGroup>
<RuntimeIdentifiers>win-x64;linux-x64;osx-arm64;osx-x64</RuntimeIdentifiers>
</PropertyGroup>
<PropertyGroup>
<OutputType>Exe</OutputType>
@@ -9,7 +14,7 @@
</PropertyGroup>
<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">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>