diff --git a/package-cleanup/action.yaml b/package-cleanup/action.yaml index 68bdc74..a9cbe9f 100644 --- a/package-cleanup/action.yaml +++ b/package-cleanup/action.yaml @@ -18,6 +18,18 @@ inputs: api_token: description: "API token for authentication" required: true + dry_run: + description: "Execute without deleting packages" + required: false + default: "false" + +outputs: + deleted_packages: + description: "Number of packages deleted" + value: ${{ steps.cleanup.outputs.deleted_packages }} + processed_names: + description: "Number of package names processed" + value: ${{ steps.cleanup.outputs.processed_names }} runs: using: "composite" @@ -31,6 +43,7 @@ runs: chmod +x package-cleanup-linux-x64 - name: Run package-cleanup + id: cleanup shell: bash env: URL: ${{ inputs.url }} @@ -38,5 +51,7 @@ runs: TYPES: ${{ inputs.types }} NAMES: ${{ inputs.names }} API_TOKEN: ${{ inputs.api_token }} + DRY_RUN: ${{ inputs.dry_run }} + GITHUB_OUTPUT: ${{ env.GITHUB_OUTPUT }} run: | ./package-cleanup-linux-x64 diff --git a/sh.actions.package-cleanup/ConfigurationExtension.cs b/sh.actions.package-cleanup/ConfigurationExtension.cs index e248316..c650f7d 100644 --- a/sh.actions.package-cleanup/ConfigurationExtension.cs +++ b/sh.actions.package-cleanup/ConfigurationExtension.cs @@ -15,7 +15,7 @@ public static class ConfigurationExtension public static IConfigurationBuilder EnsureGiteaConfig(this IConfigurationBuilder builder) { var configuration = builder.Build(); - var requiredKeys = new[] { "URL", "OWNER", "TYPES", "NAME", "API_TOKEN" }; + var requiredKeys = new[] { "URL", "OWNER", "TYPES", "NAMES", "API_TOKEN" }; foreach (var key in requiredKeys) { diff --git a/sh.actions.package-cleanup/Service/GiteaPackageService.cs b/sh.actions.package-cleanup/Service/GiteaPackageService.cs index 3a95789..789c8b4 100644 --- a/sh.actions.package-cleanup/Service/GiteaPackageService.cs +++ b/sh.actions.package-cleanup/Service/GiteaPackageService.cs @@ -22,24 +22,75 @@ public class GiteaPackageService( } } - public async Task> GetPackagesByOwnerAsync(CancellationToken cancellationToken = default) + public async Task> GetPackagesByNameAsync(string name, CancellationToken cancellationToken = default) { try { var baseUrl = GetBaseUrl(); - var allPackages = new List(); + var packages = new List(); // Parse comma-separated types - var types = (configuration["TYPE"] ?? "") + var types = (configuration["TYPES"] ?? "") .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); if (types.Length == 0) { logger.LogWarning("No package types configured"); - return allPackages; + return packages; } - // Parse comma-separated names + foreach (var type in types) + { + var page = 1; + + while (true) + { + var url = $"{baseUrl}/{type}/{name}?page={page}"; + logger.LogInformation("Fetching packages from Gitea: {Url}", url); + + var request = new HttpRequestMessage(HttpMethod.Get, url); + AddAuthorizationHeader(request); + + var response = await httpClient.SendAsync(request, cancellationToken); + response.EnsureSuccessStatusCode(); + + var fetchedPackages = + await response.Content.ReadFromJsonAsync>(cancellationToken: cancellationToken) + ?? []; + + if (fetchedPackages.Count == 0) + { + break; + } + + packages.AddRange(fetchedPackages); + logger.LogInformation("Fetched {Count} packages from page {Page}", fetchedPackages.Count, page); + + page++; + } + } + + logger.LogInformation("Successfully fetched {Count} packages for name '{Name}'", packages.Count, name); + return packages; + } + catch (HttpRequestException ex) + { + logger.LogError(ex, "Error fetching packages from Gitea for name '{Name}'", name); + throw; + } + catch (Exception ex) + { + logger.LogError(ex, "Unexpected error fetching packages for name '{Name}'", name); + throw; + } + } + + public async Task> GetPackagesByOwnerAsync(CancellationToken cancellationToken = default) + { + try + { + var allPackages = new List(); + var names = (configuration["NAMES"] ?? "") .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); @@ -49,38 +100,10 @@ public class GiteaPackageService( return allPackages; } - foreach (var type in types) + foreach (var name in names) { - foreach (var name in names) - { - var page = 1; - - while (true) - { - var url = $"{baseUrl}/{type}/{name}?page={page}"; - logger.LogInformation("Fetching packages from Gitea: {Url}", url); - - var request = new HttpRequestMessage(HttpMethod.Get, url); - AddAuthorizationHeader(request); - - var response = await httpClient.SendAsync(request, cancellationToken); - response.EnsureSuccessStatusCode(); - - var packages = - await response.Content.ReadFromJsonAsync>(cancellationToken: cancellationToken) - ?? []; - - if (packages.Count == 0) - { - break; - } - - allPackages.AddRange(packages); - logger.LogInformation("Fetched {Count} packages from page {Page}", packages.Count, page); - - page++; - } - } + var packages = await GetPackagesByNameAsync(name, cancellationToken); + allPackages.AddRange(packages); } logger.LogInformation("Successfully fetched {Count} packages in total", allPackages.Count); diff --git a/sh.actions.package-cleanup/Service/IGiteaPackageService.cs b/sh.actions.package-cleanup/Service/IGiteaPackageService.cs index 7df007e..188ffde 100644 --- a/sh.actions.package-cleanup/Service/IGiteaPackageService.cs +++ b/sh.actions.package-cleanup/Service/IGiteaPackageService.cs @@ -4,6 +4,7 @@ using sh.actions.package_cleanup.Models; public interface IGiteaPackageService { + Task> GetPackagesByNameAsync(string name, CancellationToken cancellationToken = default); Task> GetPackagesByOwnerAsync(CancellationToken cancellationToken = default); Task DeletePackage(GiteaPackage package, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/sh.actions.package-cleanup/Service/PackageService.cs b/sh.actions.package-cleanup/Service/PackageService.cs index 3a03e9a..6fead71 100644 --- a/sh.actions.package-cleanup/Service/PackageService.cs +++ b/sh.actions.package-cleanup/Service/PackageService.cs @@ -6,8 +6,7 @@ public class PackageService(IGiteaPackageService packageService) : IPackageServi { public async Task> GetFilteredPackages() { - var packages = (await packageService.GetPackagesByOwnerAsync()).ToList(); - return packages; + return (await packageService.GetPackagesByOwnerAsync()).ToList(); } public List FilterPackagesToDelete(List packages) diff --git a/sh.actions.package-cleanup/Worker.cs b/sh.actions.package-cleanup/Worker.cs index 188db1c..659d965 100644 --- a/sh.actions.package-cleanup/Worker.cs +++ b/sh.actions.package-cleanup/Worker.cs @@ -11,20 +11,22 @@ public class Worker( IGiteaPackageService giteaPackageService ) : BackgroundService { - private async Task DeletePackages(List packages, CancellationToken cancellationToken = default) + private async Task DeletePackages(List packages, CancellationToken cancellationToken = default) { var dryRun = configuration["DRY_RUN"]?.ToLower() == "true"; if (dryRun) { logger.LogInformation("Dry run enabled, not deleting {Count} packages", packages.Count); - return; + return 0; } + var deletedCount = 0; foreach (var giteaPackage in packages) { try { await giteaPackageService.DeletePackage(giteaPackage, cancellationToken); + deletedCount++; } catch (Exception ex) { @@ -32,25 +34,77 @@ public class Worker( giteaPackage.Name, giteaPackage.Version); } } + return deletedCount; + } + + private void WriteGitHubOutput(string name, string value) + { + var githubOutput = configuration["GITHUB_OUTPUT"]; + if (string.IsNullOrEmpty(githubOutput)) + { + logger.LogDebug("GITHUB_OUTPUT not set, skipping output: {Name}={Value}", name, value); + return; + } + + try + { + File.AppendAllText(githubOutput, $"{name}={value}{Environment.NewLine}"); + logger.LogInformation("Wrote to GITHUB_OUTPUT: {Name}={Value}", name, value); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to write to GITHUB_OUTPUT file"); + } } protected override async Task ExecuteAsync(CancellationToken cancellationToken) { try { - var packages = await packageService.GetFilteredPackages(); - logger.LogInformation("Found {Count} packages", packages.Count); + // Parse comma-separated names + var names = (configuration["NAMES"] ?? "") + .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + + if (names.Length == 0) + { + logger.LogWarning("No package names configured"); + WriteGitHubOutput("deleted_packages", "0"); + WriteGitHubOutput("processed_names", "0"); + appLifetime.StopApplication(); + return; + } + + var totalDeleted = 0; + + // Process each name separately: collect -> filter -> delete + foreach (var name in names) + { + logger.LogInformation("Processing packages for name '{Name}'", name); + + var packages = (await giteaPackageService.GetPackagesByNameAsync(name, cancellationToken)).ToList(); + logger.LogInformation("Found {Count} packages for name '{Name}'", packages.Count, name); + + var packagesToDelete = packageService.FilterPackagesToDelete(packages); + logger.LogInformation("Found {Count} packages to delete for name '{Name}'", packagesToDelete.Count, name); + + var deletedCount = await DeletePackages(packagesToDelete, cancellationToken); + totalDeleted += deletedCount; + logger.LogInformation("Deleted {Count} packages for name '{Name}'", deletedCount, name); + + logger.LogInformation("Cleanup finished for name '{Name}'", name); + } - var packagesToDelete = packageService.FilterPackagesToDelete(packages); - logger.LogInformation("Found {Count} packages to delete", packagesToDelete.Count); + logger.LogInformation("All package names processed successfully"); - await DeletePackages(packagesToDelete, cancellationToken); - - logger.LogInformation("Cleanup finished successfully"); + // Write outputs to GITHUB_OUTPUT + WriteGitHubOutput("deleted_packages", totalDeleted.ToString()); + WriteGitHubOutput("processed_names", names.Length.ToString()); } catch (Exception ex) { logger.LogError(ex, "Cleanup failed with an error"); + WriteGitHubOutput("deleted_packages", "0"); + WriteGitHubOutput("processed_names", "0"); } finally {