From ef25216552e765f3e2885bf0bdd6332e8c4a2a6a Mon Sep 17 00:00:00 2001 From: stillkill Date: Sat, 14 Feb 2026 16:19:46 -0600 Subject: [PATCH] Workflow test --- .gitea/workflows/workflows.yaml | 72 ++++++++ GiteaPublish/ApplicationVersion.cs | 17 ++ GiteaPublish/Commands/AuthSettings.cs | 43 +++++ .../Package/PackageDeleteArchCommand.cs | 65 +++++++ .../Package/PackagePublishArchCommand.cs | 70 ++++++++ .../Commands/Package/PackageSettings.cs | 12 ++ .../Commands/Release/Json/AddAssetOptions.cs | 31 ++++ .../Release/Json/CreateReleaseOption.cs | 72 ++++++++ .../Release/Json/CreateReleaseResponse.cs | 166 ++++++++++++++++++ .../Commands/Release/PublishReleaseCommand.cs | 115 ++++++++++++ .../Commands/Release/ReleaseService.cs | 55 ++++++ GiteaPublish/Gitea/ArchPackageRepo.cs | 50 ++++-- GiteaPublish/Gitea/BasicAuthModel.cs | 8 +- GiteaPublish/Gitea/IPackageRepo.cs | 10 +- GiteaPublish/GiteaPublish.csproj | 6 +- GiteaPublish/PKGBUILD | 29 +++ GiteaPublish/Program.cs | 80 ++++++++- 17 files changed, 876 insertions(+), 25 deletions(-) create mode 100644 .gitea/workflows/workflows.yaml create mode 100644 GiteaPublish/ApplicationVersion.cs create mode 100644 GiteaPublish/Commands/AuthSettings.cs create mode 100644 GiteaPublish/Commands/Package/PackageDeleteArchCommand.cs create mode 100644 GiteaPublish/Commands/Package/PackagePublishArchCommand.cs create mode 100644 GiteaPublish/Commands/Package/PackageSettings.cs create mode 100644 GiteaPublish/Commands/Release/Json/AddAssetOptions.cs create mode 100644 GiteaPublish/Commands/Release/Json/CreateReleaseOption.cs create mode 100644 GiteaPublish/Commands/Release/Json/CreateReleaseResponse.cs create mode 100644 GiteaPublish/Commands/Release/PublishReleaseCommand.cs create mode 100644 GiteaPublish/Commands/Release/ReleaseService.cs create mode 100644 GiteaPublish/PKGBUILD diff --git a/.gitea/workflows/workflows.yaml b/.gitea/workflows/workflows.yaml new file mode 100644 index 0000000..62d2fd8 --- /dev/null +++ b/.gitea/workflows/workflows.yaml @@ -0,0 +1,72 @@ +name: Build Arch Package + +on: + push: + branches: + - master + +jobs: + build: + runs-on: ubuntu-latest + container: + image: archlinux:latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Arch build environment + run: | + pacman -Syu --noconfirm + pacman -S --noconfirm base-devel dotnet-sdk git + #will need this later for uploading the package + dotnet publish -c Release -r linux-x64 --self-contained \ + -p:PublishSingleFile=true \ + -p:PublishReadyToRun=true \ + -p:UseAppHost=true \ + -p:DebugType=embedded \ + -o "/usr/local/bin" GiteaPublish + + - name: Get application version + id: get_version + run: | + VERSION=$(dotnet run -v q --property WarningLevel=0 --project GiteaPublish -- --version) + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "Application version: $VERSION" + + - name: Prepare PKGBUILD + run: | + cd GiteaPublish + sed -i "s//${{ steps.get_version.outputs.version }}/g" PKGBUILD + cat PKGBUILD + + - name: Build Arch package + run: | + cd GiteaPublish + pwd + useradd -m builder + chown -R builder:builder . + echo "builder ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers + su builder -c "makepkg -s --noconfirm" + + - name: List built packages + run: | + cd GiteaPublish + pwd + ls -lah *.pkg.tar.zst + echo "package_file=$(ls *.pkg.tar.zst | head -n 1)" >> $GITHUB_OUTPUT + id: package_info + + - name: Upload Arch package artifact + run: | + #I used the package to upload the package + GiteaPublish \ + package arch publish --username ${{ secrets.USERNAME }} --password ${{ secrets.PASSWORD }} \ + -b https://git.stillkill.net -o Stillkill -r GiteaPublish -f "GiteaPublish/${{ steps.package_info.outputs.package_file }}" + + + - name: Display package info + run: | + echo "Built package version: ${{ steps.get_version.outputs.version }}" + echo "Package file: ${{ steps.package_info.outputs.package_file }}" + diff --git a/GiteaPublish/ApplicationVersion.cs b/GiteaPublish/ApplicationVersion.cs new file mode 100644 index 0000000..0320274 --- /dev/null +++ b/GiteaPublish/ApplicationVersion.cs @@ -0,0 +1,17 @@ +using System.Reflection; + +namespace GiteaPublish; + +public static class ApplicationVersion +{ + public static string Version + { + get + { + Assembly? assembly = Assembly.GetEntryAssembly(); + AssemblyName? assemblyName = assembly?.GetName(); + Version? version = assemblyName?.Version; + return version?.ToString() ?? "Unknown"; + } + } +} \ No newline at end of file diff --git a/GiteaPublish/Commands/AuthSettings.cs b/GiteaPublish/Commands/AuthSettings.cs new file mode 100644 index 0000000..31223bd --- /dev/null +++ b/GiteaPublish/Commands/AuthSettings.cs @@ -0,0 +1,43 @@ +using System.ComponentModel; +using GiteaPublish.Gitea; +using Spectre.Console; +using Spectre.Console.Cli; + +namespace GiteaPublish.Commands; + +public class AuthSettings : CommandSettings +{ + [CommandOption("--username ")] + [Description("Username for authentication. Required if --auth-file is not used.")] + public string? Username { get; set; } + + [CommandOption("--password ")] + [Description("Password or token for authentication. Required if --auth-file is not used.")] + public string? Password { get; set; } + + [CommandOption("--auth-file ")] + [Description("Path to a file containing username and password/token on separate lines. Overrides --username and --password if provided.")] + public string? AuthFile { get; set; } + + public BasicAuthModel GetAuth() + { + if (!string.IsNullOrWhiteSpace(AuthFile)) + { + string[] lines = File.ReadAllLines(AuthFile); + if (lines.Length < 2) { + AnsiConsole.MarkupLine("[red]Auth file must contain at least two lines: username on the first line and password on the second line.[/]"); + throw new InvalidOperationException("Invalid auth file format."); + } + return new BasicAuthModel(lines[0].Trim(), lines[1].Trim()); + } + else + { + if(string.IsNullOrWhiteSpace(Username) || string.IsNullOrWhiteSpace(Password)) + { + AnsiConsole.MarkupLine("[red]Username and password must be provided if auth file is not used.[/]"); + throw new InvalidOperationException("Username and password are required."); + } + return new BasicAuthModel(Username!, Password!); + } + } +} \ No newline at end of file diff --git a/GiteaPublish/Commands/Package/PackageDeleteArchCommand.cs b/GiteaPublish/Commands/Package/PackageDeleteArchCommand.cs new file mode 100644 index 0000000..4e56edd --- /dev/null +++ b/GiteaPublish/Commands/Package/PackageDeleteArchCommand.cs @@ -0,0 +1,65 @@ +using GiteaPublish.Gitea; +using Spectre.Console; +using Spectre.Console.Cli; + +namespace GiteaPublish.Commands.Package; + +public class PackageDeleteArchCommand : AsyncCommand, ICommandLimiter +{ + private readonly IPackageRepo _packageRepo; + private readonly IAnsiConsole _console; + + public PackageDeleteArchCommand(IPackageRepo packageRepo, IAnsiConsole console) + { + _packageRepo = packageRepo; + _console = console; + } + + public override async Task ExecuteAsync(CommandContext context, DeleteArchSettings settings, CancellationToken cancellationToken) + { + _console.MarkupLine($"[blue]Deleting package {settings.Package} version {settings.Version} for architecture {settings.Architecture}...[/]"); + bool success = await _packageRepo.DeletePackageAsync(settings.Owner, settings.Architecture, settings.Name, settings.Version, settings.BaseUrl); + if (success) + { + _console.MarkupLine("[green]Package deleted successfully![/]"); + return 0; + } + _console.MarkupLine("[red]Failed to delete package.[/]"); + return 1; + } + + public override ValidationResult Validate(CommandContext context, DeleteArchSettings settings) + { + if (string.IsNullOrWhiteSpace(settings.Package)) + return ValidationResult.Error("Package is required."); + + if (string.IsNullOrWhiteSpace(settings.Architecture)) + return ValidationResult.Error("Architecture is required."); + + if (string.IsNullOrWhiteSpace(settings.Name)) + return ValidationResult.Error("Name is required."); + + if (string.IsNullOrWhiteSpace(settings.Version)) + return ValidationResult.Error("Version is required."); + + if (!Uri.TryCreate(settings.BaseUrl, UriKind.Absolute, out Uri? uriResult) || (uriResult.Scheme != Uri.UriSchemeHttp && uriResult.Scheme != Uri.UriSchemeHttps)) + return ValidationResult.Error("Base URL must be a valid HTTP or HTTPS URL."); + + return ValidationResult.Success(); + } + + public class DeleteArchSettings : PackageSettings + { + [CommandOption("-p|--package ", isRequired: true)] + public required string Package { get; set; } + + [CommandOption("-a|--architecture ", isRequired: true)] + public required string Architecture { get; set; } + + [CommandOption("-n|--name ", isRequired: true)] + public required string Name { get; set; } + + [CommandOption("-v|--version ", isRequired: true)] + public required string Version { get; set; } + } +} \ No newline at end of file diff --git a/GiteaPublish/Commands/Package/PackagePublishArchCommand.cs b/GiteaPublish/Commands/Package/PackagePublishArchCommand.cs new file mode 100644 index 0000000..eb8aaf7 --- /dev/null +++ b/GiteaPublish/Commands/Package/PackagePublishArchCommand.cs @@ -0,0 +1,70 @@ +using GiteaPublish.Gitea; +using Spectre.Console; +using Spectre.Console.Cli; +using ValidationResult = Spectre.Console.ValidationResult; + +namespace GiteaPublish.Commands.Package; + +public class PackagePublishArchCommand : AsyncCommand +{ + private readonly ArchPackageRepo _packageRepo; + private readonly IAnsiConsole _console; + + public PackagePublishArchCommand(IPackageRepo packageRepo, IAnsiConsole console) + { + _packageRepo = packageRepo as ArchPackageRepo + ?? throw new ArgumentException("Package repository must be of type ArchPackageRepo.", nameof(packageRepo)); + _console = console; + } + + + public override async Task ExecuteAsync(CommandContext context, PublishArchSettings settings, CancellationToken cancellationToken) + { + _packageRepo.SetAuthentication(settings.GetAuth()); + bool success = await _packageRepo.PublishPackageAsync(settings.Owner, settings.Repository, settings.File, settings.BaseUrl); + if (success) + { + _console.MarkupLine("[green]Package published successfully![/]"); + return 0; + } + + _console.MarkupLine("[red]Failed to publish package.[/]"); + return 1; + } + + public override ValidationResult Validate(CommandContext context, PublishArchSettings settings) + { + if (!File.Exists(settings.File)) + return ValidationResult.Error($"File '{settings.File}' does not exist."); + + if (string.IsNullOrWhiteSpace(settings.Owner)) + return ValidationResult.Error("Owner is required."); + + if(string.IsNullOrWhiteSpace(settings.Repository)) + return ValidationResult.Error("Repository is required."); + + if(string.IsNullOrWhiteSpace(settings.Username)) + return ValidationResult.Error("Username is required."); + + if(string.IsNullOrWhiteSpace(settings.Password)) + return ValidationResult.Error("Password is required."); + + if (!Uri.TryCreate(settings.BaseUrl, UriKind.Absolute, out Uri? uriResult) || (uriResult.Scheme != Uri.UriSchemeHttp && uriResult.Scheme != Uri.UriSchemeHttps)) + return ValidationResult.Error("Base URL must be a valid HTTP or HTTPS URL."); + + return ValidationResult.Success(); + } + + + public sealed class PublishArchSettings : PackageSettings + { + + [CommandOption("-f|--file ", isRequired: true)] + public required string File { get; init; } + + [CommandOption("-r|--repository ", isRequired: true)] + public string Repository { get; set; } = string.Empty; + + } +} + diff --git a/GiteaPublish/Commands/Package/PackageSettings.cs b/GiteaPublish/Commands/Package/PackageSettings.cs new file mode 100644 index 0000000..cc79f5a --- /dev/null +++ b/GiteaPublish/Commands/Package/PackageSettings.cs @@ -0,0 +1,12 @@ +using Spectre.Console.Cli; + +namespace GiteaPublish.Commands.Package; + +public class PackageSettings : AuthSettings +{ + [CommandOption("-o|--owner ", isRequired: true)] + public string Owner { get; set; } = string.Empty; + + [CommandOption("-b|--base-url ", isRequired: true)] + public string BaseUrl { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/GiteaPublish/Commands/Release/Json/AddAssetOptions.cs b/GiteaPublish/Commands/Release/Json/AddAssetOptions.cs new file mode 100644 index 0000000..043c47b --- /dev/null +++ b/GiteaPublish/Commands/Release/Json/AddAssetOptions.cs @@ -0,0 +1,31 @@ +namespace GiteaPublish.Commands.Release; + +public class AddAssetOptions +{ + public string Owner { get; set; } + public string Repository { get; set; } + public int ReleaseId { get; set; } + public string AssetName { get; set; } + public byte[] AssetContent { get; set; } + + public AddAssetOptions(string owner, string repository, int releaseId, string assetPath) + { + Owner = owner; + Repository = repository; + ReleaseId = releaseId; + AssetName = Path.GetFileName(assetPath); + AssetContent = File.ReadAllBytes(assetPath); + } + + public MultipartFormDataContent ToFormContent() + { + var form = new MultipartFormDataContent(); + + form.Add(new StringContent(Owner), "owner"); + form.Add(new StringContent(Repository), "repo"); + form.Add(new StringContent(ReleaseId.ToString()), "id"); + form.Add(new StringContent(AssetName), "name"); + form.Add(new ByteArrayContent(AssetContent), "attachment"); + return form; + } +} \ No newline at end of file diff --git a/GiteaPublish/Commands/Release/Json/CreateReleaseOption.cs b/GiteaPublish/Commands/Release/Json/CreateReleaseOption.cs new file mode 100644 index 0000000..c0f3399 --- /dev/null +++ b/GiteaPublish/Commands/Release/Json/CreateReleaseOption.cs @@ -0,0 +1,72 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace GiteaPublish.Commands.Release; + +/// +/// CreateReleaseOption options when creating a release. +/// +public class CreateReleaseOption +{ + /// + /// The release notes or description. + /// + [JsonPropertyName("body")] + public string? Body { get; set; } + + /// + /// Whether to create the release as a draft. + /// + [JsonPropertyName("draft")] + public bool Draft { get; set; } + + /// + /// The display title of the release. + /// + [JsonPropertyName("name")] + public string? Name { get; set; } + + /// + /// Whether to mark the release as a prerelease. + /// + [JsonPropertyName("prerelease")] + public bool Prerelease { get; set; } + + /// + /// The message for the git tag. + /// + [JsonPropertyName("tag_message")] + public string? TagMessage { get; set; } + + /// + /// The tag name for the release (required). + /// + [JsonPropertyName("tag_name")] + public string TagName { get; set; } = string.Empty; + + /// + /// The target commitish for the release. + /// + [JsonPropertyName("target_commitish")] + public string? TargetCommitish { get; set; } + + public CreateReleaseOption() + { + } + + public CreateReleaseOption(PublishReleaseCommandSettings settings) + { + Body = settings.Body; + Draft = settings.Draft; + Name = settings.Name; + Prerelease = settings.Prerelease; + TagMessage = settings.TagMessage; + TagName = settings.TagName; + TargetCommitish = settings.TargetCommitish; + } + + + + public string ToJson() + => JsonSerializer.Serialize(this); +} \ No newline at end of file diff --git a/GiteaPublish/Commands/Release/Json/CreateReleaseResponse.cs b/GiteaPublish/Commands/Release/Json/CreateReleaseResponse.cs new file mode 100644 index 0000000..157844c --- /dev/null +++ b/GiteaPublish/Commands/Release/Json/CreateReleaseResponse.cs @@ -0,0 +1,166 @@ +using System.Text.Json.Serialization; + +namespace GiteaPublish.Commands.Release; + +public class CreateReleaseResponse +{ + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("id")] + public int? Id { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("tag_name")] + public string TagName { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("target_commitish")] + public string TargetCommitish { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("body")] + public string Body { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("url")] + public Uri Url { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("html_url")] + public Uri HtmlUrl { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("tarball_url")] + public Uri TarballUrl { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("zipball_url")] + public Uri ZipballUrl { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("upload_url")] + public Uri UploadUrl { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("draft")] + public bool? Draft { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("prerelease")] + public bool? Prerelease { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("created_at")] + public DateTimeOffset? CreatedAt { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("published_at")] + public DateTimeOffset? PublishedAt { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("author")] + public AuthorModel Author { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("assets")] + public object[] Assets { get; set; } + + public class AuthorModel + { + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("id")] + public long? Id { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("login")] + public string Login { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("login_name")] + public string LoginName { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("source_id")] + public long? SourceId { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("full_name")] + public string FullName { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("email")] + public string Email { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("avatar_url")] + public Uri AvatarUrl { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("html_url")] + public Uri HtmlUrl { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("language")] + public string Language { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("is_admin")] + public bool? IsAdmin { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("last_login")] + public DateTimeOffset? LastLogin { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("created")] + public DateTimeOffset? Created { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("restricted")] + public bool? Restricted { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("active")] + public bool? Active { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("prohibit_login")] + public bool? ProhibitLogin { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("location")] + public string Location { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("website")] + public Uri Website { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("description")] + public string Description { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("visibility")] + public string Visibility { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("followers_count")] + public long? FollowersCount { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("following_count")] + public long? FollowingCount { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("starred_repos_count")] + public long? StarredReposCount { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("username")] + public string Username { get; set; } + } +} + diff --git a/GiteaPublish/Commands/Release/PublishReleaseCommand.cs b/GiteaPublish/Commands/Release/PublishReleaseCommand.cs new file mode 100644 index 0000000..11edeca --- /dev/null +++ b/GiteaPublish/Commands/Release/PublishReleaseCommand.cs @@ -0,0 +1,115 @@ +using GiteaPublish.Gitea; +using Spectre.Console; +using Spectre.Console.Cli; + +namespace GiteaPublish.Commands.Release; + +public class PublishReleaseCommand : AsyncCommand +{ + private readonly ReleaseService _releaseService; + private readonly IAnsiConsole _console; + + public PublishReleaseCommand(ReleaseService releaseService, IAnsiConsole console) + { + _releaseService = releaseService; + _console = console; + } + + public override async Task ExecuteAsync(CommandContext context, PublishReleaseCommandSettings settings, + CancellationToken cancellationToken) + { + _releaseService.SetAuthorization(settings.GetAuth()); + int releaseId = await _releaseService.CreateReleaseAsync(new CreateReleaseOption(settings), settings.Owner, settings.Repository, settings.BaseUrl); + if (releaseId != int.MinValue) + { + _console.MarkupLine("[green]Release created successfully![/]"); + } + else + { + _console.MarkupLine("[red]Operation Failed.[/]"); + return 1; + } + + if (settings.Files.Length == 0) + { + _console.MarkupLine("[yellow]No assets to upload.[/]"); + return 0; + } + + _console.MarkupLine($"[blue]Uploading {settings.Files.Length} asset(s)...[/]"); + foreach (var file in settings.Files) + { + if (!File.Exists(file)) + { + _console.MarkupLine($"[red]File not found: {file}[/]"); + continue; + } + + AddAssetOptions options = new AddAssetOptions(settings.Owner, settings.Repository, releaseId, file); + + bool assetSuccess = + await _releaseService.AddAssetAsync(options, settings.BaseUrl); + if (assetSuccess) + _console.MarkupLine($"[green]Uploaded asset: {file}[/]"); + } + + return 0; + } + + public override ValidationResult Validate(CommandContext context, PublishReleaseCommandSettings settings) + { + if (string.IsNullOrWhiteSpace(settings.Owner)) + return ValidationResult.Error("Owner is required."); + if (string.IsNullOrWhiteSpace(settings.Repository)) + return ValidationResult.Error("Repository is required."); + if (string.IsNullOrWhiteSpace(settings.BaseUrl)) + return ValidationResult.Error("Base URL is required."); + if (string.IsNullOrWhiteSpace(settings.TagName)) + return ValidationResult.Error("Tag name is required."); + if (string.IsNullOrWhiteSpace(settings.AuthFile) && (string.IsNullOrWhiteSpace(settings.Username) || string.IsNullOrWhiteSpace(settings.Password))) + return ValidationResult.Error("Either auth file or username/password must be provided."); + + return ValidationResult.Success(); + } +} +//release publish --auth-file "/home/gitea-package-token.txt" -o Stillkill -r GiteaPublish -b https://git.stillkill.net +//-e "Release body" -d -n "Release name" -p --tag-message "Tag message" --tag-name "v1.0.0" -tc "master" -f "/home/Downloads/ACTv3.zip" --file "/home/stillkill/Downloads/test.gif" + +public class PublishReleaseCommandSettings : AuthSettings +{ + [CommandOption("-o|--owner ", isRequired: true)] + public string Owner { get; set; } = string.Empty; + + [CommandOption("-r|--repository ", isRequired: true)] + public string Repository { get; set; } = string.Empty; + + [CommandOption("-b|--base-url ", isRequired: true)] + public string BaseUrl { get; set; } = string.Empty; + + [CommandOption("-e|--body ")] + public string? Body { get; set; } = null; + + [CommandOption("-d|--draft")] + public bool Draft { get; set; } = false; + + [CommandOption("-n|--name ")] + public string? Name { get; set; } = null; + + [CommandOption("-p|--prerelease")] + public bool Prerelease { get; set; } = false; + + [CommandOption("--tag-message ")] + public string? TagMessage { get; set; } = null; + + [CommandOption("--tag-name ", isRequired: true)] + public string TagName { get; set; } = string.Empty; + + [CommandOption("-c|--target-commitish ")] + public string? TargetCommitish { get; set; } = null; + + [CommandOption("-f|--file ")] + public string[] Files { get; set; } = Array.Empty(); + + + +} \ No newline at end of file diff --git a/GiteaPublish/Commands/Release/ReleaseService.cs b/GiteaPublish/Commands/Release/ReleaseService.cs new file mode 100644 index 0000000..7510dac --- /dev/null +++ b/GiteaPublish/Commands/Release/ReleaseService.cs @@ -0,0 +1,55 @@ +using System.Net.Http.Json; +using GiteaPublish.Gitea; +using Spectre.Console; +using Spectre.Console.Cli; + +namespace GiteaPublish.Commands.Release; + +public class ReleaseService +{ + private readonly IAnsiConsole _console; + private readonly HttpClient _client; + + public ReleaseService(IAnsiConsole console, HttpClient client) + { + _console = console; + _client = client; + } + + public async Task CreateReleaseAsync(CreateReleaseOption option, string owner, string repository, string baseUrl) + { + _console.MarkupLine($"[blue]Creating release '{option.TagName}' for {owner}/{repository}...[/]"); + _client.BaseAddress ??= new Uri(baseUrl.TrimEnd('/')); + string endpoint = $"/api/v1/repos/{owner}/{repository}/releases"; + var response = await _client.PostAsJsonAsync(endpoint, option); + if (response.IsSuccessStatusCode) + { + var release = await response.Content.ReadFromJsonAsync(); + if (release != null && release.Id.HasValue) + { + _console.MarkupLine($"[green]Release created with ID: {release.Id.Value}[/]"); + return release.Id.Value; + } + } + _console.MarkupLine($"[red]Failed to create release: {response.StatusCode} - {response.ReasonPhrase}[/]"); + return int.MinValue; + } + + public async Task AddAssetAsync(AddAssetOptions options, string? baseUrl = null) + { + _console.MarkupLine($"[blue]Adding asset '{options.AssetName}' to release {options.ReleaseId}...[/]"); + _client.BaseAddress ??= new Uri(baseUrl!.TrimEnd('/')); + + MultipartFormDataContent form = options.ToFormContent(); + var response = await _client.PostAsync($"/api/v1/repos/{options.Owner}/{options.Repository}/releases/{options.ReleaseId}/assets", form); + if (response.IsSuccessStatusCode) return true; + _console.MarkupLine($"[red]Failed to add asset: {response.StatusCode} - {response.ReasonPhrase}[/]"); + return false; + } + + public void SetAuthorization(BasicAuthModel auth) + { + _console.MarkupLine("[blue]Setting up authentication...[/]"); + _client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", auth.AuthHeader); + } +} \ No newline at end of file diff --git a/GiteaPublish/Gitea/ArchPackageRepo.cs b/GiteaPublish/Gitea/ArchPackageRepo.cs index 614f6e3..49331e1 100644 --- a/GiteaPublish/Gitea/ArchPackageRepo.cs +++ b/GiteaPublish/Gitea/ArchPackageRepo.cs @@ -3,32 +3,50 @@ using Spectre.Console; namespace GiteaPublish.Gitea; -public class ArchPackageRepo(HttpClient httpClient, IAnsiConsole ansiConsole, BasicAuthModel basicAuth) - : IPackageRepo +public class ArchPackageRepo : IPackageRepo { - public async Task PublishPackageAsync(string owner, string repository, string filePath) + private BasicAuthModel BasicAuth { get; set; } = new BasicAuthModel(); + + private readonly IAnsiConsole _console; + private readonly HttpClient _httpClient; + + public ArchPackageRepo(HttpClient httpClient, IAnsiConsole ansiConsole) { - ansiConsole.MarkupLine($"[blue]Publishing package {filePath} to {owner}/{repository}...[/]"); - HttpRequestMessage request = new(HttpMethod.Put, $"api/packages/{owner}/arch/{repository}"); + _console = ansiConsole; + _httpClient = httpClient; + } + + public async Task PublishPackageAsync(string owner, string repository, string filePath, string baseUrl) + { + _httpClient.BaseAddress = new Uri(baseUrl.TrimEnd('/')); + _console.MarkupLine($"[blue]Publishing package {filePath} to {owner}/{repository}...[/]"); + HttpRequestMessage request = new(HttpMethod.Put, $"/api/packages/{owner}/arch/{repository}"); request.Content = new ByteArrayContent(await File.ReadAllBytesAsync(filePath)); - request.Headers.Authorization = new AuthenticationHeaderValue("Basic", basicAuth.AuthHeader); - HttpResponseMessage response = await httpClient.SendAsync(request); + request.Headers.Authorization = new AuthenticationHeaderValue("Basic", BasicAuth.AuthHeader); + HttpResponseMessage response = await _httpClient.SendAsync(request); if (response.IsSuccessStatusCode) return true; - ansiConsole.MarkupLine($"[red]Failed to publish package: {response.StatusCode}[/]"); - ansiConsole.MarkupLine($"[red]{(await response.Content.ReadAsStringAsync()).EscapeMarkup()}[/]"); + _console.MarkupLine($"[red]Failed to publish package: {response.StatusCode}[/]"); + _console.MarkupLine($"[red]{(await response.Content.ReadAsStringAsync()).EscapeMarkup()}[/]"); return false; } - public async Task DeletePackageAsync(string owner, string repository, string architecture, string packageName, - string packageVersion) + public async Task DeletePackageAsync(string owner, string repository, string packageName, + string packageVersion, string baseUrl) { + _httpClient.BaseAddress = new Uri(baseUrl.TrimEnd('/')); HttpRequestMessage request = new(HttpMethod.Delete, - $"api/packages/{owner}/arch/{repository}/{architecture}/{packageName}/{packageVersion}"); - request.Headers.Authorization = new AuthenticationHeaderValue("Basic", basicAuth.AuthHeader); - HttpResponseMessage response = await httpClient.SendAsync(request); + $"/api/packages/{owner}/arch/{repository}/{packageName}/{packageVersion}"); + request.Headers.Authorization = new AuthenticationHeaderValue("Basic", BasicAuth.AuthHeader); + HttpResponseMessage response = await _httpClient.SendAsync(request); if (response.IsSuccessStatusCode) return true; - ansiConsole.MarkupLine($"[red]Failed to delete package: {response.StatusCode}[/]"); - ansiConsole.MarkupLine($"[red]{(await response.Content.ReadAsStringAsync()).EscapeMarkup()}[/]"); + _console.MarkupLine($"[red]Failed to delete package: {response.StatusCode}[/]"); + _console.MarkupLine($"[red]{(await response.Content.ReadAsStringAsync()).EscapeMarkup()}[/]"); return false; } + + public void SetAuthentication(BasicAuthModel basicAuth) + { + BasicAuth = basicAuth; + _console.MarkupLine("[green]Authentication set.[/]"); + } } \ No newline at end of file diff --git a/GiteaPublish/Gitea/BasicAuthModel.cs b/GiteaPublish/Gitea/BasicAuthModel.cs index a25e11a..8b0dc6a 100644 --- a/GiteaPublish/Gitea/BasicAuthModel.cs +++ b/GiteaPublish/Gitea/BasicAuthModel.cs @@ -4,12 +4,16 @@ public class BasicAuthModel { public string AuthHeader => Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes($"{_username}:{_password}")); - private readonly string _username; - private readonly string _password; + private readonly string _username = string.Empty; + private readonly string _password = string.Empty; public BasicAuthModel(string username, string password) { _username = username; _password = password; } + + public BasicAuthModel() + { + } } \ No newline at end of file diff --git a/GiteaPublish/Gitea/IPackageRepo.cs b/GiteaPublish/Gitea/IPackageRepo.cs index 5069494..e31df57 100644 --- a/GiteaPublish/Gitea/IPackageRepo.cs +++ b/GiteaPublish/Gitea/IPackageRepo.cs @@ -9,7 +9,7 @@ public interface IPackageRepo /// The name of the repository /// The path to the package file /// A bool representing whether the task was successful - public Task PublishPackageAsync(string owner, string repository, string filePath); + public Task PublishPackageAsync(string owner, string repository, string filePath, string baseUrl); /// @@ -21,5 +21,11 @@ public interface IPackageRepo /// The name of the package /// The version of the package /// A bool representing whether the task was successful - public Task DeletePackageAsync(string owner, string repository, string architecture, string packageName, string packageVersion); + public Task DeletePackageAsync(string owner, string architecture, string packageName, string packageVersion, string baseUrl); + + /// + /// Sets the authentication for the package repository. This method should be called before any operations that require authentication (e.g., publishing or deleting packages). The BasicAuthModel contains the necessary information for basic authentication, such as the username and password. + /// + /// + public void SetAuthentication(BasicAuthModel basicAuth); } \ No newline at end of file diff --git a/GiteaPublish/GiteaPublish.csproj b/GiteaPublish/GiteaPublish.csproj index eb906af..a75aa05 100644 --- a/GiteaPublish/GiteaPublish.csproj +++ b/GiteaPublish/GiteaPublish.csproj @@ -5,14 +5,18 @@ net10.0 enable enable + 1.0.0 + - + + PreserveNewest + diff --git a/GiteaPublish/PKGBUILD b/GiteaPublish/PKGBUILD new file mode 100644 index 0000000..5ee5a80 --- /dev/null +++ b/GiteaPublish/PKGBUILD @@ -0,0 +1,29 @@ +pkgname=gitea-publish +pkgver= +pkgrel=1 +pkgdesc="" +arch=('x86_64') +url="https://git.stillkill.net/Stillkill/GiteaPublish" +license=('BEERWARE') +depends=() +makedepends=('dotnet-sdk' 'git') +source=("git+https://git.stillkill.net/Stillkill/GiteaPublish.git#branch=master") +sha256sums=('SKIP') + +build() { +cd "$srcdir/GiteaPublish" +git checkout "v${pkgver}" || true +dotnet publish -c Release -r linux-x64 --self-contained \ +-p:PublishSingleFile=true \ +-p:PublishReadyToRun=true \ +-p:UseAppHost=true \ +-p:DebugType=embedded \ +-o "$srcdir/output" +echo "$srcdir/output" +} + +package() { +cd "$srcdir/output" +pwd +install -Dm755 "GiteaPublish" "$pkgdir/usr/bin/gitea-publish" +} diff --git a/GiteaPublish/Program.cs b/GiteaPublish/Program.cs index 545ab96..af8c3fd 100644 --- a/GiteaPublish/Program.cs +++ b/GiteaPublish/Program.cs @@ -1,5 +1,11 @@ -using GiteaPublish.Gitea; +using System.Globalization; +using GiteaPublish.Commands; +using GiteaPublish.Commands.Package; +using GiteaPublish.Commands.Release; +using GiteaPublish.Gitea; +using Microsoft.Extensions.DependencyInjection; using Spectre.Console; +using Spectre.Console.Cli; namespace GiteaPublish; @@ -7,8 +13,74 @@ class Program { static async Task Main(string[] args) { - var basicAuthModel = new BasicAuthModel("", ""); - ArchPackageRepo repo = new(new HttpClient { BaseAddress = new Uri("https://git.stillkill.net") }, AnsiConsole.Console, basicAuthModel); - await repo.PublishPackageAsync("Stillkill", "core", "/home/stillkill/CollectHorse.tar.gz"); + ServiceCollection services = new(); + services.AddSingleton(); + services.AddSingleton(CreateHttpClient()); + services.AddSingleton(); + TypeRegistrar registrar = new TypeRegistrar(services); + CommandApp app = new CommandApp(registrar); + app.Configure(config => + { + config.SetApplicationName("GiteaPublish"); + config.SetApplicationVersion(ApplicationVersion.Version); + config.SetApplicationCulture(CultureInfo.GetCultureInfo("en-us")); + + config.AddBranch("package", packageConfig => + { + packageConfig.AddBranch("arch", archConfig => + { + archConfig.AddCommand("publish") + .WithDescription("Publish an Arch package to a Gitea repository") + .WithExample("package arch publish --username --password " + + "-o -r -f -b "); + + // archConfig.AddCommand("delete") + // .WithDescription("Deletes an Arch package from a Gitea repository"); + }); + }); + + config.AddBranch("release", releaseConfig => + { + releaseConfig.AddCommand("publish") + .WithDescription("Publish a release to a Gitea repository") + .WithExample("release publish --username --password " + + "-o -r -f -b " + + "--tag-name [--name ] [--body ] " + + "[--draft] [--prerelease] [--tag-message ] [--target-commitish ]"); + }); + }); + + try + { + await app.RunAsync(args); + } + catch (Exception e) + { + AnsiConsole.WriteException(e); + Environment.Exit(1); + } } + + private static HttpClient CreateHttpClient() + { + HttpClient httpClient = new(); + httpClient.DefaultRequestHeaders.UserAgent.ParseAdd($"GiteaPublish/{ApplicationVersion.Version}"); + return httpClient; + } +} + +public sealed class TypeRegistrar(IServiceCollection services) : ITypeRegistrar +{ + public ITypeResolver Build() => new TypeResolver(services.BuildServiceProvider()); + + public void Register(Type service, Type implementation) => services.AddSingleton(service, implementation); + + public void RegisterInstance(Type service, object implementation) => services.AddSingleton(service, implementation); + + public void RegisterLazy(Type service, Func factory) => services.AddSingleton(service, _ => factory()); +} + +public sealed class TypeResolver(IServiceProvider provider) : ITypeResolver +{ + public object? Resolve(Type? type) => type == null ? null : provider.GetService(type); } \ No newline at end of file