diff --git a/sh.actions.package-cleanup.Tests/Service/PackageServiceDateVersionTests.cs b/sh.actions.package-cleanup.Tests/Service/PackageServiceDateVersionTests.cs deleted file mode 100644 index 8f45fcb..0000000 --- a/sh.actions.package-cleanup.Tests/Service/PackageServiceDateVersionTests.cs +++ /dev/null @@ -1,617 +0,0 @@ -using Microsoft.Extensions.Configuration; -using Moq; -using sh.actions.package_cleanup.Models; -using sh.actions.package_cleanup.Service; -using Xunit; - -namespace sh.actions.package_cleanup.Tests.Service; - -public class PackageServiceDateVersionTests -{ - private Mock _configurationMock; - private Mock _giteaPackageServiceMock; - private IPackageService _packageService; - - private void SetupMocks() - { - _configurationMock = new Mock(); - _giteaPackageServiceMock = new Mock(); - _packageService = new PackageService(_configurationMock.Object, _giteaPackageServiceMock.Object); - } - - // ...existing code... - - #region Version Detection Tests - - [Theory] - [InlineData("2024.12.25.1")] - [InlineData("2025.2.14.1")] - [InlineData("2026.1.1.100")] - [InlineData("2023.12.31.999")] - public void FilterPackagesToDelete_WithDateVersions_ShouldIdentifyCorrectly(string version) - { - // Arrange - SetupMocks(); - var packages = new List - { - new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Single prod date version should not be deleted - Assert.NotNull(result); - Assert.Empty(result); - } - - [Theory] - [InlineData("2024.12.25.1-dev")] - [InlineData("2025.2.14.1-alpha")] - [InlineData("2026.1.1.100-beta")] - [InlineData("2023.12.31.999-rc")] - public void FilterPackagesToDelete_WithDateVersionsAndSuffixes_ShouldIdentifyCorrectly(string version) - { - // Arrange - SetupMocks(); - var packages = new List - { - new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Single prerelease/dev date version should be kept - Assert.NotNull(result); - Assert.Empty(result); - } - - #endregion - - #region Prod Release Tests - Date Versioning - - [Fact] - public void FilterPackagesToDeleteByDateVersion_WithOnlyProdReleases_ShouldReturnEmpty() - { - // Arrange - CLEAN_PROD_VERSIONS not set, defaults to false - SetupMocks(); - var packages = new List - { - new() - { - Id = 1, Name = "test", Version = "2025.12.20.1", Type = "docker", - CreatedAt = DateTime.UtcNow.AddDays(-30) - }, - new() - { - Id = 2, Name = "test", Version = "2025.12.25.1", Type = "docker", - CreatedAt = DateTime.UtcNow.AddDays(-20) - }, - new() - { - Id = 3, Name = "test", Version = "2026.1.5.1", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-10) - } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Prod releases should be protected by default (CLEAN_PROD_VERSIONS=false) - Assert.Empty(result); - } - - [Fact] - public void FilterPackagesToDeleteByDateVersion_WithProdReleasesAndCleaningEnabled_ShouldDeleteOldest() - { - // Arrange - SetupMocks(); - _configurationMock.Setup(c => c["CLEAN_PROD_VERSIONS"]).Returns("true"); - _configurationMock.Setup(c => c["KEEP_PROD_VERSIONS"]).Returns("2"); - - var packages = new List - { - new() - { - Id = 1, Name = "test", Version = "2025.1.1.1", Type = "docker", - CreatedAt = DateTime.UtcNow.AddDays(-365) - }, - new() - { - Id = 2, Name = "test", Version = "2025.6.15.1", Type = "docker", - CreatedAt = DateTime.UtcNow.AddDays(-200) - }, - new() - { - Id = 3, Name = "test", Version = "2026.1.1.1", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-45) - }, - new() { Id = 4, Name = "test", Version = "2026.2.14.1", Type = "docker", CreatedAt = DateTime.UtcNow } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Should delete oldest 2 (2025.1.1 and 2025.6.15), keep newest 2 - Assert.Equal(2, result.Count); - Assert.Contains(result, p => p.Id == 1); - Assert.Contains(result, p => p.Id == 2); - Assert.DoesNotContain(result, p => p.Id == 3 || p.Id == 4); - } - - [Fact] - public void FilterPackagesToDeleteByDateVersion_WithMultipleProdVersions_ShouldKeepNewest() - { - // Arrange - SetupMocks(); - var packages = new List - { - new() - { - Id = 1, Name = "test", Version = "2025.1.1.1", Type = "docker", - CreatedAt = DateTime.UtcNow.AddDays(-365) - }, - new() - { - Id = 2, Name = "test", Version = "2025.6.15.1", Type = "docker", - CreatedAt = DateTime.UtcNow.AddDays(-200) - }, - new() - { - Id = 3, Name = "test", Version = "2026.1.1.1", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-45) - }, - new() { Id = 4, Name = "test", Version = "2026.2.14.1", Type = "docker", CreatedAt = DateTime.UtcNow } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Default is CLEAN_PROD_VERSIONS=false, so nothing should be deleted - Assert.Empty(result); - } - - #endregion - - #region Prerelease Tests - Date Versioning - - [Fact] - public void FilterPackagesToDeleteByDateVersion_WithPrereleaseVersions_ShouldKeepLatest() - { - // Arrange - Default KEEP_PRERELEASE_VERSIONS=3 - SetupMocks(); - var packages = new List - { - new() - { - Id = 1, Name = "test", Version = "2026.2.10.1-alpha", Type = "docker", - CreatedAt = DateTime.UtcNow.AddDays(-5) - }, - new() - { - Id = 2, Name = "test", Version = "2026.2.11.1-beta", Type = "docker", - CreatedAt = DateTime.UtcNow.AddDays(-4) - }, - new() - { - Id = 3, Name = "test", Version = "2026.2.12.1-rc", Type = "docker", - CreatedAt = DateTime.UtcNow.AddDays(-3) - }, - new() - { - Id = 4, Name = "test", Version = "2026.2.13.1-preview", Type = "docker", - CreatedAt = DateTime.UtcNow.AddDays(-2) - } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Should delete oldest 1 (alpha), keep newest 3 - Assert.Single(result); - Assert.Contains(result, p => p.Id == 1); // alpha is oldest - Assert.DoesNotContain(result, p => p.Id == 2 || p.Id == 3 || p.Id == 4); - } - - [Fact] - public void FilterPackagesToDeleteByDateVersion_WithManyPrereleases_ShouldEnforceLimit() - { - // Arrange - Custom KEEP_PRERELEASE_VERSIONS=2 - SetupMocks(); - _configurationMock.Setup(c => c["KEEP_PRERELEASE_VERSIONS"]).Returns("2"); - - var packages = new List(); - for (int i = 0; i < 10; i++) - { - var date = DateTime.UtcNow.AddDays(-i); - packages.Add(new GiteaPackage - { - Id = i, - Name = "test", - Version = $"{date.Year}.{date.Month}.{date.Day}.{i}-beta", - Type = "docker", - CreatedAt = date - }); - } - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Should delete 8 oldest, keep only 2 newest (Id=0,1) - Assert.Equal(8, result.Count); - Assert.DoesNotContain(result, p => p.Id < 2); - Assert.Contains(result, p => p.Id >= 2); - } - - [Fact] - public void FilterPackagesToDeleteByDateVersion_WithMixedPrereleases_ShouldOrderByDate() - { - // Arrange - SetupMocks(); - var packages = new List - { - new() - { - Id = 1, Name = "test", Version = "2025.12.1.1-beta", Type = "docker", - CreatedAt = DateTime.UtcNow.AddDays(-60) - }, - new() - { - Id = 2, Name = "test", Version = "2026.1.15.1-beta", Type = "docker", - CreatedAt = DateTime.UtcNow.AddDays(-30) - }, - new() - { - Id = 3, Name = "test", Version = "2026.2.1.1-alpha", Type = "docker", - CreatedAt = DateTime.UtcNow.AddDays(-14) - } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Default keep 3, so all should be kept - Assert.Empty(result); - } - - #endregion - - #region Dev Release Tests - Date Versioning - - [Fact] - public void FilterPackagesToDeleteByDateVersion_WithDevVersions_ShouldKeepLatest() - { - // Arrange - Default KEEP_DEV_VERSIONS=5 - SetupMocks(); - var packages = new List - { - new() - { - Id = 1, Name = "test", Version = "2026.2.10.1-dev", Type = "docker", - CreatedAt = DateTime.UtcNow.AddDays(-5) - }, - new() - { - Id = 2, Name = "test", Version = "2026.2.11.2-dev", Type = "docker", - CreatedAt = DateTime.UtcNow.AddDays(-4) - }, - new() - { - Id = 3, Name = "test", Version = "2026.2.12.3-dev", Type = "docker", - CreatedAt = DateTime.UtcNow.AddDays(-3) - } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Default keep 5, so all 3 should be kept - Assert.Empty(result); - } - - [Fact] - public void FilterPackagesToDeleteByDateVersion_WithManyDailyDevReleases_ShouldEnforceLimit() - { - // Arrange - Custom KEEP_DEV_VERSIONS=3 - SetupMocks(); - _configurationMock.Setup(c => c["KEEP_DEV_VERSIONS"]).Returns("3"); - - var packages = new List(); - for (int i = 0; i < 30; i++) - { - var date = DateTime.UtcNow.AddDays(-i); - packages.Add(new GiteaPackage - { - Id = i, - Name = "test", - Version = $"{date.Year}.{date.Month}.{date.Day}.{i}-dev", - Type = "docker", - CreatedAt = date - }); - } - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Should delete 27 oldest, keep only 3 newest (Id=0,1,2) - Assert.Equal(27, result.Count); - Assert.DoesNotContain(result, p => p.Id < 3); - Assert.Contains(result, p => p.Id >= 3); - } - - [Fact] - public void FilterPackagesToDeleteByDateVersion_WithManyDailyDevReleasesDefaultLimit_ShouldIdentifyAll() - { - // Arrange - SetupMocks(); - var packages = new List(); - for (int i = 0; i < 30; i++) - { - var date = DateTime.UtcNow.AddDays(-i); - packages.Add(new GiteaPackage - { - Id = i, - Name = "test", - Version = $"{date.Year}.{date.Month}.{date.Day}.{i}-dev", - Type = "docker", - CreatedAt = date - }); - } - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Default keep 5, so 25 should be deleted - Assert.Equal(25, result.Count); - Assert.DoesNotContain(result, p => p.Id < 5); // Keep newest 5 (Id 0-4) - Assert.Contains(result, p => p.Id >= 5); - } - - #endregion - - #region Mixed Release Types - Date Versioning - - [Fact] - public void FilterPackagesToDeleteByDateVersion_WithMixedReleaseTypes_ShouldSeparateAndCleanCorrectly() - { - // Arrange - SetupMocks(); - _configurationMock.Setup(c => c["KEEP_PRERELEASE_VERSIONS"]).Returns("2"); - _configurationMock.Setup(c => c["KEEP_DEV_VERSIONS"]).Returns("2"); - - var packages = new List - { - // Prod releases (should not be deleted - CLEAN_PROD_VERSIONS=false) - new() - { - Id = 1, Name = "test", Version = "2025.12.1.1", Type = "docker", - CreatedAt = DateTime.UtcNow.AddDays(-80) - }, - new() - { - Id = 2, Name = "test", Version = "2026.1.1.1", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-45) - }, - // Prerelease (keep 2 newest) - new() - { - Id = 3, Name = "test", Version = "2026.1.25.1-alpha", Type = "docker", - CreatedAt = DateTime.UtcNow.AddDays(-20) - }, - new() - { - Id = 4, Name = "test", Version = "2026.2.1.1-beta", Type = "docker", - CreatedAt = DateTime.UtcNow.AddDays(-14) - }, - new() - { - Id = 5, Name = "test", Version = "2026.2.5.1-rc", Type = "docker", - CreatedAt = DateTime.UtcNow.AddDays(-10) - }, - // Dev (keep 2 newest) - new() - { - Id = 6, Name = "test", Version = "2026.2.10.1-dev", Type = "docker", - CreatedAt = DateTime.UtcNow.AddDays(-5) - }, - new() { Id = 7, Name = "test", Version = "2026.2.14.1-dev", Type = "docker", CreatedAt = DateTime.UtcNow } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Assert.Equal(1, result.Count); // Should delete 1 prerelease (alpha), 0 dev (keep both) - Assert.DoesNotContain(result, p => p.Id <= 2); // Prod not deleted - Assert.DoesNotContain(result, p => p.Id >= 6); // Keep 2 latest dev - } - - [Fact] - public void FilterPackagesToDeleteByDateVersion_WithLargeSet_ShouldHandleCorrectly() - { - // Arrange - SetupMocks(); - _configurationMock.Setup(c => c["CLEAN_PROD_VERSIONS"]).Returns("false"); - _configurationMock.Setup(c => c["KEEP_PRERELEASE_VERSIONS"]).Returns("3"); - _configurationMock.Setup(c => c["KEEP_DEV_VERSIONS"]).Returns("5"); - - var packages = new List(); - - // Add prod releases - for (int i = 0; i < 5; i++) - { - var date = DateTime.UtcNow.AddMonths(-(5 - i)); - packages.Add(new GiteaPackage - { - Id = i, - Name = "test", - Version = $"{date.Year}.{date.Month}.1.1", - Type = "docker", - CreatedAt = date - }); - } - - // Add prerelease - for (int i = 5; i < 15; i++) - { - var date = DateTime.UtcNow.AddDays(-(15 - i) * 7); - packages.Add(new GiteaPackage - { - Id = i, - Name = "test", - Version = $"{date.Year}.{date.Month}.{date.Day}.1-beta", - Type = "docker", - CreatedAt = date - }); - } - - // Add dev - for (int i = 15; i < 30; i++) - { - var date = DateTime.UtcNow.AddDays(-(30 - i)); - packages.Add(new GiteaPackage - { - Id = i, - Name = "test", - Version = $"{date.Year}.{date.Month}.{date.Day}.{(30 - i)}-dev", - Type = "docker", - CreatedAt = date - }); - } - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Assert.NotNull(result); - // Prod releases should never be deleted - Assert.DoesNotContain(result, p => p.Id < 5); - // Should delete 7 oldest prerelease (keep 3 newest) - var prereleaseDeleted = result.Where(p => p.Id >= 5 && p.Id < 15).ToList(); - Assert.Equal(7, prereleaseDeleted.Count); - // Should delete 10 oldest dev (keep 5 newest) - var devDeleted = result.Where(p => p.Id >= 15).ToList(); - Assert.Equal(10, devDeleted.Count); - } - - #endregion - - #region Edge Cases - Date Versioning - - [Theory] - [InlineData("1000.1.1.1")] // Minimum valid year - [InlineData("2999.12.31.999")] // Maximum valid year - [InlineData("2025.2.14.0")] // Build number 0 - public void FilterPackagesToDeleteByDateVersion_WithBoundaryVersions_ShouldHandleCorrectly(string version) - { - // Arrange - SetupMocks(); - var packages = new List - { - new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Assert.NotNull(result); - } - - [Theory] - [InlineData("999.12.31.1")] // Year too small - [InlineData("3000.1.1.1")] // Year too large - [InlineData("2025.13.1.1")] // Month out of range - [InlineData("2025.0.1.1")] // Month zero - [InlineData("2025.2.32.1")] // Day out of range - [InlineData("2025.2.0.1")] // Day zero - public void FilterPackagesToDelete_WithInvalidDateVersions_ShouldNotProcess(string version) - { - // Arrange - SetupMocks(); - var packages = new List - { - new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow }, - new() { Id = 2, Name = "test", Version = "1.0.0.1", Type = "docker", CreatedAt = DateTime.UtcNow } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Invalid date version should be skipped, only semantic version processed - Assert.NotNull(result); - } - - [Fact] - public void FilterPackagesToDeleteByDateVersion_WithConsecutiveDates_ShouldOrderCorrectly() - { - // Arrange - SetupMocks(); - var packages = new List - { - new() - { - Id = 1, Name = "test", Version = "2025.2.10.1", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-5) - }, - new() - { - Id = 2, Name = "test", Version = "2025.2.11.1", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-4) - }, - new() - { - Id = 3, Name = "test", Version = "2025.2.12.1", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-3) - }, - new() - { - Id = 4, Name = "test", Version = "2025.2.13.1", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-2) - }, - new() { Id = 5, Name = "test", Version = "2025.2.14.1", Type = "docker", CreatedAt = DateTime.UtcNow } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Assert.NotNull(result); - } - - #endregion - - #region Mixed Versioning Variants - - [Fact] - public void FilterPackagesToDelete_WithMixedVersioningVariants_ShouldProcessBothTypes() - { - // Arrange - SetupMocks(); - var packages = new List - { - // Semantic versioning - new() - { - Id = 1, Name = "test", Version = "1.0.0", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-30) - }, - new() - { - Id = 2, Name = "test", Version = "1.1.0-dev", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-10) - }, - // Date versioning - new() - { - Id = 3, Name = "test", Version = "2025.1.1.1", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-45) - }, - new() - { - Id = 4, Name = "test", Version = "2025.2.10.1-dev", Type = "docker", - CreatedAt = DateTime.UtcNow.AddDays(-5) - } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Assert.NotNull(result); - // Prod releases should not be in delete list - Assert.DoesNotContain(result, p => p.Id == 1 || p.Id == 3); - } - - #endregion -} \ No newline at end of file diff --git a/sh.actions.package-cleanup.Tests/Service/PackageServiceSemanticVersionTests.cs b/sh.actions.package-cleanup.Tests/Service/PackageServiceSemanticVersionTests.cs deleted file mode 100644 index 0bb03da..0000000 --- a/sh.actions.package-cleanup.Tests/Service/PackageServiceSemanticVersionTests.cs +++ /dev/null @@ -1,498 +0,0 @@ -using Microsoft.Extensions.Configuration; -using Moq; -using sh.actions.package_cleanup.Models; -using sh.actions.package_cleanup.Service; -using Xunit; - -namespace sh.actions.package_cleanup.Tests.Service; - -public class PackageServiceSemanticVersionTests -{ - private Mock _configurationMock; - private Mock _giteaPackageServiceMock; - private IPackageService _packageService; - - private void SetupMocks() - { - _configurationMock = new Mock(); - _giteaPackageServiceMock = new Mock(); - _packageService = new PackageService(_configurationMock.Object, _giteaPackageServiceMock.Object); - } - - #region Version Detection Tests - - [Theory] - [InlineData("1.0.0.0")] - [InlineData("2.5.3.1")] - [InlineData("10.20.30.100")] - [InlineData("1.0.0")] - [InlineData("2.5")] - public void FilterPackagesToDelete_WithSemanticVersions_ShouldIdentifyCorrectly(string version) - { - // Arrange - SetupMocks(); - var packages = new List - { - new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Single prod version should not be deleted (CLEAN_PROD_VERSIONS=false by default) - Assert.NotNull(result); - Assert.Empty(result); - } - - [Theory] - [InlineData("1.0.0.0-dev")] - [InlineData("2.5.3.1-alpha")] - [InlineData("10.20.30.100-beta")] - [InlineData("1.0.0-rc1")] - [InlineData("2.5-preview")] - public void FilterPackagesToDelete_WithSemanticVersionsAndSuffixes_ShouldIdentifyCorrectly(string version) - { - // Arrange - SetupMocks(); - var packages = new List - { - new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Single prerelease/dev should be kept - Assert.NotNull(result); - Assert.Empty(result); - } - - #endregion - - #region Prod Release Tests - Semantic Versioning - - [Fact] - public void FilterPackagesToDeleteBySemanticVersion_WithOnlyProdReleases_ShouldReturnEmpty() - { - // Arrange - CLEAN_PROD_VERSIONS not set, defaults to false - SetupMocks(); - var packages = new List - { - new() { Id = 1, Name = "test", Version = "1.0.0.1", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-30) }, - new() { Id = 2, Name = "test", Version = "1.1.0.1", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-20) }, - new() { Id = 3, Name = "test", Version = "1.2.0.1", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-10) } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Prod releases should be protected by default (CLEAN_PROD_VERSIONS=false) - Assert.Empty(result); - Assert.DoesNotContain(result, p => p.Id == 1 || p.Id == 2 || p.Id == 3); - } - - [Fact] - public void FilterPackagesToDeleteBySemanticVersion_WithProdReleasesAndCleaningEnabled_ShouldDeleteOldest() - { - // Arrange - SetupMocks(); - _configurationMock.Setup(c => c["CLEAN_PROD_VERSIONS"]).Returns("true"); - _configurationMock.Setup(c => c["KEEP_PROD_VERSIONS"]).Returns("2"); - - var packages = new List - { - new() { Id = 1, Name = "test", Version = "1.0.0.0", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-30) }, - new() { Id = 2, Name = "test", Version = "1.1.0.0", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-20) }, - new() { Id = 3, Name = "test", Version = "2.0.0.0", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-10) }, - new() { Id = 4, Name = "test", Version = "2.1.0.0", Type = "docker", CreatedAt = DateTime.UtcNow } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Should delete oldest 2 (1.0.0.0 and 1.1.0.0), keep newest 2 (2.0.0.0 and 2.1.0.0) - Assert.Equal(2, result.Count); - Assert.Contains(result, p => p.Id == 1); - Assert.Contains(result, p => p.Id == 2); - Assert.DoesNotContain(result, p => p.Id == 3 || p.Id == 4); - } - - [Fact] - public void FilterPackagesToDeleteBySemanticVersion_WithMultipleProdVersions_ShouldKeepNewest() - { - // Arrange - SetupMocks(); - var packages = new List - { - new() { Id = 1, Name = "test", Version = "1.0.0.0", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-30) }, - new() { Id = 2, Name = "test", Version = "1.0.1.0", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-20) }, - new() { Id = 3, Name = "test", Version = "2.0.0.0", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-10) }, - new() { Id = 4, Name = "test", Version = "2.1.0.0", Type = "docker", CreatedAt = DateTime.UtcNow } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Default is CLEAN_PROD_VERSIONS=false, so nothing should be deleted - Assert.Empty(result); - } - - #endregion - - #region Prerelease Tests - Semantic Versioning - - [Fact] - public void FilterPackagesToDeleteBySemanticVersion_WithPrereleaseVersions_ShouldKeepLatest() - { - // Arrange - Default KEEP_PRERELEASE_VERSIONS=3 - SetupMocks(); - var packages = new List - { - new() { Id = 1, Name = "test", Version = "1.0.0-alpha", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-5) }, - new() { Id = 2, Name = "test", Version = "1.0.0-beta", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-4) }, - new() { Id = 3, Name = "test", Version = "1.0.0-rc1", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-3) }, - new() { Id = 4, Name = "test", Version = "1.0.0-preview", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-2) } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Should delete oldest 1 (alpha, id=1), keep newest 3 (beta, rc1, preview) - Assert.Single(result); - Assert.Contains(result, p => p.Id == 1); // alpha is oldest, should be deleted - Assert.DoesNotContain(result, p => p.Id == 2 || p.Id == 3 || p.Id == 4); // Keep newest 3 - } - - [Fact] - public void FilterPackagesToDeleteBySemanticVersion_WithManyPrereleases_ShouldEnforceLimit() - { - // Arrange - Custom KEEP_PRERELEASE_VERSIONS=2 - SetupMocks(); - _configurationMock.Setup(c => c["KEEP_PRERELEASE_VERSIONS"]).Returns("2"); - - var packages = new List(); - for (int i = 0; i < 10; i++) - { - packages.Add(new GiteaPackage - { - Id = i, - Name = "test", - Version = $"1.0.0-beta{i}", - Type = "docker", - CreatedAt = DateTime.UtcNow.AddDays(-(10 - i)) - }); - } - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Should delete 8 oldest, keep only 2 newest (Id=8,9) - Assert.Equal(8, result.Count); - Assert.DoesNotContain(result, p => p.Id >= 8); - Assert.Contains(result, p => p.Id < 8); - } - - [Fact] - public void FilterPackagesToDeleteBySemanticVersion_WithMixedPrereleases_ShouldOrderByVersion() - { - // Arrange - SetupMocks(); - var packages = new List - { - new() { Id = 1, Name = "test", Version = "1.0.0-beta", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-10) }, - new() { Id = 2, Name = "test", Version = "2.0.0-beta", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-5) }, - new() { Id = 3, Name = "test", Version = "1.5.0-alpha", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-3) } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Default keep 3, so all should be kept - Assert.Empty(result); - } - - #endregion - - #region Dev Release Tests - Semantic Versioning - - [Fact] - public void FilterPackagesToDeleteBySemanticVersion_WithDevVersions_ShouldKeepLatest() - { - // Arrange - Default KEEP_DEV_VERSIONS=5 - SetupMocks(); - var packages = new List - { - new() { Id = 1, Name = "test", Version = "1.0.0-dev", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-5) }, - new() { Id = 2, Name = "test", Version = "1.0.1-dev", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-4) }, - new() { Id = 3, Name = "test", Version = "2.0.0-dev", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-3) } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Default keep 5, so all 3 should be kept - Assert.Empty(result); - } - - [Fact] - public void FilterPackagesToDeleteBySemanticVersion_WithManyDevReleases_ShouldEnforceLimit() - { - // Arrange - Custom KEEP_DEV_VERSIONS=3 - SetupMocks(); - _configurationMock.Setup(c => c["KEEP_DEV_VERSIONS"]).Returns("3"); - - var packages = new List(); - for (int i = 0; i < 10; i++) - { - packages.Add(new GiteaPackage - { - Id = i, - Name = "test", - Version = $"1.0.{i}-dev", - Type = "docker", - CreatedAt = DateTime.UtcNow.AddDays(-i) - }); - } - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Should delete 7 oldest, keep only 3 newest (Id=7,8,9 are newest by CreatedAt) - Assert.Equal(7, result.Count); - Assert.DoesNotContain(result, p => p.Id >= 7); // Keep newest 3 - Assert.Contains(result, p => p.Id < 7); // Delete oldest 7 - } - - [Fact] - public void FilterPackagesToDeleteBySemanticVersion_WithManyDevReleases_ShouldIdentifyAll() - { - // Arrange - SetupMocks(); - var packages = new List(); - for (int i = 0; i < 20; i++) - { - packages.Add(new GiteaPackage - { - Id = i, - Name = "test", - Version = $"1.0.{i}-dev", - Type = "docker", - CreatedAt = DateTime.UtcNow.AddDays(-i) - }); - } - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Default keep 5, so 15 should be deleted - Assert.Equal(15, result.Count); - Assert.DoesNotContain(result, p => p.Id >= 15); // Keep newest 5 (Id 15-19) - Assert.Contains(result, p => p.Id < 15); - } - - #endregion - - #region Mixed Release Types - Semantic Versioning - - [Fact] - public void FilterPackagesToDeleteBySemanticVersion_WithMixedReleaseTypes_ShouldSeparateAndCleanCorrectly() - { - // Arrange - CLEAN_PROD_VERSIONS=false, KEEP_PRERELEASE_VERSIONS=2, KEEP_DEV_VERSIONS=2 - SetupMocks(); - _configurationMock.Setup(c => c["KEEP_PRERELEASE_VERSIONS"]).Returns("2"); - _configurationMock.Setup(c => c["KEEP_DEV_VERSIONS"]).Returns("2"); - - var packages = new List - { - // Prod releases (should not be deleted - CLEAN_PROD_VERSIONS=false) - new() { Id = 1, Name = "test", Version = "1.0.0", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-30) }, - new() { Id = 2, Name = "test", Version = "1.1.0", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-25) }, - // Prerelease (keep 2 newest) - new() { Id = 3, Name = "test", Version = "1.2.0-alpha", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-20) }, - new() { Id = 4, Name = "test", Version = "1.2.0-beta", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-15) }, - new() { Id = 5, Name = "test", Version = "1.2.1-rc", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-10) }, - // Dev (keep 2 newest) - new() { Id = 6, Name = "test", Version = "1.2.1-dev", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-5) }, - new() { Id = 7, Name = "test", Version = "1.2.2-dev", Type = "docker", CreatedAt = DateTime.UtcNow } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Assert.Equal(1, result.Count); // Should delete 1 prerelease (alpha), 0 dev (keep both) - Assert.DoesNotContain(result, p => p.Id <= 2); // Prod not deleted - Assert.DoesNotContain(result, p => p.Id >= 6); // Keep 2 latest dev - } - - [Fact] - public void FilterPackagesToDeleteBySemanticVersion_WithLargeSet_ShouldHandleCorrectly() - { - // Arrange - SetupMocks(); - _configurationMock.Setup(c => c["CLEAN_PROD_VERSIONS"]).Returns("false"); - _configurationMock.Setup(c => c["KEEP_PRERELEASE_VERSIONS"]).Returns("3"); - _configurationMock.Setup(c => c["KEEP_DEV_VERSIONS"]).Returns("5"); - - var packages = new List(); - - // Add prod releases - for (int i = 0; i < 5; i++) - { - packages.Add(new GiteaPackage - { - Id = i, - Name = "test", - Version = $"1.{i}.0", - Type = "docker", - CreatedAt = DateTime.UtcNow.AddDays(-(5 - i)) - }); - } - - // Add prerelease - for (int i = 5; i < 15; i++) - { - packages.Add(new GiteaPackage - { - Id = i, - Name = "test", - Version = $"2.0.{i - 5}-beta", - Type = "docker", - CreatedAt = DateTime.UtcNow.AddDays(-(15 - i)) - }); - } - - // Add dev - for (int i = 15; i < 30; i++) - { - packages.Add(new GiteaPackage - { - Id = i, - Name = "test", - Version = $"2.1.{i - 15}-dev", - Type = "docker", - CreatedAt = DateTime.UtcNow.AddDays(-(30 - i)) - }); - } - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Assert.NotNull(result); - // Prod releases should never be deleted - Assert.DoesNotContain(result, p => p.Id < 5); - // Should delete 7 oldest prerelease (keep 3 newest) - var prereleaseDeleted = result.Where(p => p.Id >= 5 && p.Id < 15).ToList(); - Assert.Equal(7, prereleaseDeleted.Count); - // Should delete 10 oldest dev (keep 5 newest) - var devDeleted = result.Where(p => p.Id >= 15).ToList(); - Assert.Equal(10, devDeleted.Count); - } - - #endregion - - #region Edge Cases - Semantic Versioning - - [Theory] - [InlineData("0.0.0.0")] - [InlineData("99.99.99.99")] - [InlineData("100.0.0.0")] // Boundary case - might be detected as date version - public void FilterPackagesToDeleteBySemanticVersion_WithBoundaryVersions_ShouldHandleCorrectly(string version) - { - // Arrange - SetupMocks(); - var packages = new List - { - new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Should not throw, and boundary versions should be handled properly - Assert.NotNull(result); - // Versions 0.0.0.0 and 99.99.99.99 are semantic, 100.0.0.0 is likely treated as date (will fail date validation) - } - - [Fact] - public void FilterPackagesToDeleteBySemanticVersion_WithVersionsHavingDifferentLengths_ShouldHandleCorrectly() - { - // Arrange - SetupMocks(); - var packages = new List - { - new() { Id = 1, Name = "test", Version = "1", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-5) }, - new() { Id = 2, Name = "test", Version = "1.0", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-4) }, - new() { Id = 3, Name = "test", Version = "1.0.0", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-3) }, - new() { Id = 4, Name = "test", Version = "1.0.0.0", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-2) } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Should not throw and should not delete any prod versions - Assert.NotNull(result); - // CLEAN_PROD_VERSIONS=false by default, so all versions should be kept - // Id=1 is invalid (only 1 component), ids 2-4 are valid but prod - var validVersions = new[] { 2, 3, 4 }; - foreach (var id in validVersions) - { - Assert.DoesNotContain(result, p => p.Id == id); - } - } - - [Fact] - public void FilterPackagesToDeleteBySemanticVersion_WithZeroKeepCount_ShouldKeepAll() - { - // Arrange - KEEP_DEV_VERSIONS=0 means keep all - SetupMocks(); - _configurationMock.Setup(c => c["KEEP_DEV_VERSIONS"]).Returns("0"); - - var packages = new List(); - for (int i = 0; i < 10; i++) - { - packages.Add(new GiteaPackage - { - Id = i, - Name = "test", - Version = $"1.0.{i}-dev", - Type = "docker", - CreatedAt = DateTime.UtcNow.AddDays(-i) - }); - } - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Should keep all (0 means infinite/keep all) - Assert.Empty(result); - Assert.DoesNotContain(result, p => p.Id >= 0); // No packages should be deleted - } - - [Fact] - public void FilterPackagesToDeleteBySemanticVersion_WithInvalidConfigValues_ShouldUseDefaults() - { - // Arrange - Set invalid config values - SetupMocks(); - _configurationMock.Setup(c => c["KEEP_DEV_VERSIONS"]).Returns("invalid"); - _configurationMock.Setup(c => c["CLEAN_PROD_VERSIONS"]).Returns("maybe"); - - var packages = new List - { - new() { Id = 1, Name = "test", Version = "1.0.0-dev", Type = "docker", CreatedAt = DateTime.UtcNow.AddDays(-10) }, - new() { Id = 2, Name = "test", Version = "1.0.1-dev", Type = "docker", CreatedAt = DateTime.UtcNow }, - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Should use defaults (KEEP_DEV_VERSIONS=5, CLEAN_PROD_VERSIONS=false) - // With 2 packages and keep 5, no deletion should happen - Assert.Empty(result); - Assert.DoesNotContain(result, p => p.Id == 1 || p.Id == 2); - } - - #endregion -} - diff --git a/sh.actions.package-cleanup.Tests/Service/PackageServiceTests.cs b/sh.actions.package-cleanup.Tests/Service/PackageServiceTests.cs new file mode 100644 index 0000000..c2bbec0 --- /dev/null +++ b/sh.actions.package-cleanup.Tests/Service/PackageServiceTests.cs @@ -0,0 +1,146 @@ +using Moq; +using sh.actions.package_cleanup.Models; +using sh.actions.package_cleanup.Service; +using Xunit; + +namespace sh.actions.package_cleanup.Tests.Service; + +public class PackageFilterTests +{ + private readonly IPackageService _packageService; + private readonly Mock _giteaPackageServiceMock = new Mock(); + private readonly List _packages; + + + public PackageFilterTests() + { + _packageService = new PackageService(_giteaPackageServiceMock.Object); + _packages = _versions.Select(v => new GiteaPackage + { + Version = v, + Name = $"test-{v}" + }).ToList(); + } + + [Fact] + public void TestFilterPackagesToDelete() + { + var toDelete = _packageService.FilterPackagesToDelete(_packages); + var toDeleteVersions = toDelete + .Select(x => x.Version) + .OrderByDescending(x => x) + .ToList(); + var expected = _expectDeleted.OrderByDescending(x => x).ToList(); + Assert.Equal(expected.Count, toDeleteVersions.Count); + Assert.Equal(expected, toDeleteVersions); + } + + private readonly List _versions = + [ + "0.1.0", + "0.1.1", + "0.1.2", + "1.0", + "1.1", + "1.2", + "2024.8.6.0", + "2024.8.7.0", + "2024.8.8.0", + "2024.8.9.0", + "2024.8.10.0", + "2024.8.11.0", + "latest", + // dev + "0-dev", + "1-dev", + "2-dev", + "1.0-dev", + "1.1-dev", + "1.2-dev", + "0.1.0-dev", + "0.1.1-dev", + "0.1.2-dev", + "2024.8.6.0-dev", + "2024.8.6.1-dev", + "2024.8.7.0-dev", + "2024.8.7.1-dev", + "2024.8.8.0-dev", + "2024.8.8.1-dev", + "2024.8.9.0-dev", + "2024.8.9.1-dev", + "2024.8.10.0-dev", + "2024.8.10.1-dev", + "2024.8.11.0-dev", + "2024.8.11.1-dev", + // test + "0.1.0-test", + "0.1.1-test", + "0.1.2-test", + "2024.8.6.0-test", + "2024.8.6.1-test", + "2024.8.7.0-test", + "2024.8.7.1-test", + "2024.8.8.0-test", + "2024.8.8.1-test", + "2024.8.9.0-test", + "2024.8.9.1-test", + "2024.8.10.0-test", + "2024.8.10.1-test", + "2024.8.11.0-test", + // exp + "0.1.0-exp", + "0.1.1-exp", + "0.1.2-exp", + "2024.8.6.0-exp", + "2024.8.6.1-exp", + "2024.8.7.0-exp", + "2024.8.7.1-exp", + "2024.8.8.0-exp", + "2024.8.8.1-exp", + "2024.8.9.0-exp", + "2024.8.9.1-exp", + "2024.8.10.0-exp", + "2024.8.10.1-exp", + "2024.8.11.0-exp", + "2024.8.11.2-exp" + ]; + + private readonly List _versionsToHold = + [ + // prod (no suffix) + "0.1.0", + "0.1.1", + "0.1.2", + "1.0", + "1.1", + "1.2", + "2024.8.6.0", + "2024.8.7.0", + "2024.8.8.0", + "2024.8.9.0", + "2024.8.10.0", + "2024.8.11.0", + "latest", + // dev + "1-dev", + "2-dev", + "1.1-dev", + "1.2-dev", + "0.1.1-dev", + "0.1.2-dev", + "2024.8.11.0-dev", + "2024.8.11.1-dev", + // test + "0.1.1-test", + "0.1.2-test", + "2024.8.10.1-test", + "2024.8.11.0-test", + // exp + "0.1.1-exp", + "0.1.2-exp", + "2024.8.11.0-exp", + "2024.8.11.2-exp" + ]; + + private List _expectDeleted => _versions.Except(_versionsToHold).ToList(); +} \ No newline at end of file diff --git a/sh.actions.package-cleanup.Tests/Service/PackageServiceVersionDetectionTests.cs b/sh.actions.package-cleanup.Tests/Service/PackageServiceVersionDetectionTests.cs deleted file mode 100644 index f8eb400..0000000 --- a/sh.actions.package-cleanup.Tests/Service/PackageServiceVersionDetectionTests.cs +++ /dev/null @@ -1,349 +0,0 @@ -using Microsoft.Extensions.Configuration; -using Moq; -using sh.actions.package_cleanup.Models; -using sh.actions.package_cleanup.Service; -using Xunit; - -namespace sh.actions.package_cleanup.Tests.Service; - -public class PackageServiceVersionDetectionTests -{ - private readonly IPackageService _packageService; - private readonly Mock _configurationMock; - private readonly Mock _giteaPackageServiceMock; - - public PackageServiceVersionDetectionTests() - { - _configurationMock = new Mock(); - _giteaPackageServiceMock = new Mock(); - _packageService = new PackageService(_configurationMock.Object, _giteaPackageServiceMock.Object); - } - - #region Release Type Detection Tests - - [Theory] - [InlineData("1.0.0")] - [InlineData("2.5.3")] - [InlineData("10.20.30")] - [InlineData("2025.2.14.1")] - public void FilterPackagesToDelete_WithProdReleases_ShouldIdentifyAsNonDeletable(string version) - { - // Arrange - var packages = new List - { - new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Prod releases should not be in delete list (CLEAN_PROD_VERSIONS=false by default) - Assert.DoesNotContain(result, p => p.Id == 1); - Assert.Empty(result); // No packages should be deleted without CLEAN_PROD_VERSIONS=true - } - - [Theory] - [InlineData("1.0.0-alpha")] - [InlineData("1.0.0-ALPHA")] - [InlineData("2.5.3-alpha123")] - [InlineData("1.0.0-beta")] - [InlineData("1.0.0-BETA")] - [InlineData("1.0.0-rc")] - [InlineData("1.0.0-RC1")] - [InlineData("1.0.0-preview")] - [InlineData("1.0.0-PREVIEW")] - public void FilterPackagesToDelete_WithPrereleaseVersions_ShouldIdentifyCorrectly(string version) - { - // Arrange - var packages = new List - { - new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Prerelease should be kept (default KEEP_PRERELEASE_VERSIONS=3, so 1 package is kept) - Assert.NotNull(result); - Assert.Empty(result); // Single prerelease should be kept as it's within the limit - } - - [Theory] - [InlineData("1.0.0-dev")] - [InlineData("1.0.0-DEV")] - [InlineData("2.5.3-dev123")] - [InlineData("1.0.0-development")] - [InlineData("1.0.0-DEV")] - public void FilterPackagesToDelete_WithDevVersions_ShouldIdentifyCorrectly(string version) - { - // Arrange - var packages = new List - { - new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Dev version should be kept (default KEEP_DEV_VERSIONS=5, so 1 package is kept) - Assert.NotNull(result); - Assert.Empty(result); // Single dev version should be kept as it's within the limit - } - - #endregion - - #region Semantic Version Detection Tests - - [Theory] - [InlineData("1.0")] - [InlineData("1.0.0")] - [InlineData("1.0.0.0")] - [InlineData("2.5")] - [InlineData("2.5.3")] - [InlineData("2.5.3.100")] - [InlineData("0.0.0.0")] - [InlineData("99.99.99.99")] - public void FilterPackagesToDelete_WithValidSemanticVersions_ShouldProcess(string version) - { - // Arrange - var packages = new List - { - new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Should process without throwing and not delete prod releases - Assert.NotNull(result); - Assert.DoesNotContain(result, p => p.Id == 1); // Prod version should not be deleted - } - - [Theory] - [InlineData("1")] // Only 1 component - [InlineData("1.0.0.0.0")] // 5 components - [InlineData("1.a.0.0")] // Non-numeric - [InlineData("a.b.c.d")] // All non-numeric - public void FilterPackagesToDelete_WithInvalidSemanticVersions_ShouldSkip(string version) - { - // Arrange - var packages = new List - { - new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow }, - new() { Id = 2, Name = "test", Version = "2.0.0.0", Type = "docker", CreatedAt = DateTime.UtcNow } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Invalid version should be skipped, only valid semantic version (id=2) is processed - Assert.NotNull(result); - // Should not crash and should only consider the valid version - Assert.DoesNotContain(result, p => p.Id == 2); // Valid prod should not be deleted - } - - [Theory] - [InlineData("101.0.0.0")] - [InlineData("200.5.3.100")] - [InlineData("1000.0.0.0")] - public void FilterPackagesToDelete_WithLargeMajorVersion_ShouldTreatAsDateVersion(string version) - { - // Arrange - var packages = new List - { - new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Large major versions (>100) are detected as date versions, not semantic - // These will fail date validation (invalid month/day) and be skipped - Assert.NotNull(result); - } - - #endregion - - #region Date Version Detection Tests - - [Theory] - [InlineData("2024.1.1.1")] - [InlineData("2025.2.14.1")] - [InlineData("2026.12.31.999")] - [InlineData("2000.6.15.100")] - [InlineData("1999.1.1.1")] - public void FilterPackagesToDelete_WithValidDateVersions_ShouldProcess(string version) - { - // Arrange - var packages = new List - { - new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Should process without throwing and not delete prod date versions - Assert.NotNull(result); - Assert.DoesNotContain(result, p => p.Id == 1); // Prod date version should not be deleted - } - - [Theory] - [InlineData("2025.1.1")] // 3 components (missing build) - [InlineData("2025.1.1.1.1")] // 5 components - [InlineData("2025.13.1.1")] // Month out of range - [InlineData("2025.0.1.1")] // Month zero - [InlineData("2025.2.32.1")] // Day out of range - [InlineData("2025.2.0.1")] // Day zero - [InlineData("999.2.14.1")] // Year too small - [InlineData("3000.2.14.1")] // Year too large - [InlineData("2025.a.14.1")] // Non-numeric month - public void FilterPackagesToDelete_WithInvalidDateVersions_ShouldSkip(string version) - { - // Arrange - var packages = new List - { - new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow }, - new() { Id = 2, Name = "test", Version = "1.0.0.0", Type = "docker", CreatedAt = DateTime.UtcNow } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Invalid date version should be skipped, valid semantic version (id=2) is processed as prod - Assert.NotNull(result); - Assert.DoesNotContain(result, p => p.Id == 2); // Valid semantic prod should not be deleted - } - - [Theory] - [InlineData("1000.1.1.1")] // Minimum valid year - [InlineData("2999.12.31.999")] // Maximum valid year - [InlineData("2025.1.1.1")] // January 1 - [InlineData("2025.12.31.1")] // December 31 - public void FilterPackagesToDelete_WithDateVersionBoundaries_ShouldProcess(string version) - { - // Arrange - var packages = new List - { - new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Boundary date versions should be recognized as valid - Assert.NotNull(result); - Assert.DoesNotContain(result, p => p.Id == 1); // Prod date version should not be deleted - } - - #endregion - - #region Case Sensitivity Tests - - [Theory] - [InlineData("1.0.0-alpha")] - [InlineData("1.0.0-ALPHA")] - [InlineData("1.0.0-Alpha")] - [InlineData("1.0.0-AlPhA")] - public void FilterPackagesToDelete_WithDifferentCasePrerelease_ShouldHandleCorrectly(string version) - { - // Arrange - var packages = new List - { - new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - All case variations should be recognized as prerelease - Assert.NotNull(result); - Assert.Empty(result); // Single prerelease should be kept - } - - [Theory] - [InlineData("1.0.0-dev")] - [InlineData("1.0.0-DEV")] - [InlineData("1.0.0-Dev")] - [InlineData("1.0.0-DeV")] - public void FilterPackagesToDelete_WithDifferentCaseDev_ShouldHandleCorrectly(string version) - { - // Arrange - var packages = new List - { - new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - All case variations should be recognized as dev version - Assert.NotNull(result); - Assert.Empty(result); // Single dev version should be kept - } - - #endregion - - #region Suffix/Identifier Tests - - [Theory] - [InlineData("1.0.0-alpha1")] - [InlineData("1.0.0-alpha.1")] - [InlineData("1.0.0-beta2")] - [InlineData("1.0.0-rc.1")] - [InlineData("2025.2.14.1-dev.1")] - [InlineData("2025.2.14.1-dev123")] - public void FilterPackagesToDelete_WithCompoundSuffixes_ShouldIdentifyCorrectly(string version) - { - // Arrange - var packages = new List - { - new() { Id = 1, Name = "test", Version = version, Type = "docker", CreatedAt = DateTime.UtcNow } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Compound suffixes should be recognized and not cause errors - Assert.NotNull(result); - Assert.Empty(result); // Single prerelease/dev should be kept - } - - #endregion - - #region Empty and Null Tests - - [Fact] - public void FilterPackagesToDelete_WithEmptyList_ShouldReturnEmpty() - { - // Arrange - var packages = new List(); - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Empty input should return empty result - Assert.NotNull(result); - Assert.Empty(result); - } - - [Fact] - public void FilterPackagesToDelete_WithNullableInputs_ShouldHandleGracefully() - { - // Arrange - var packages = new List - { - new() { Id = 1, Name = "test", Version = "1.0.0", Type = "docker", CreatedAt = DateTime.UtcNow } - }; - - // Act - var result = _packageService.FilterPackagesToDelete(packages); - - // Assert - Should handle without throwing and not delete prod - Assert.NotNull(result); - Assert.Empty(result); // Prod should not be deleted - } - - #endregion -} - diff --git a/sh.actions.package-cleanup/ConfigurationExtension.cs b/sh.actions.package-cleanup/ConfigurationExtension.cs index d6ca7ca..8bd891e 100644 --- a/sh.actions.package-cleanup/ConfigurationExtension.cs +++ b/sh.actions.package-cleanup/ConfigurationExtension.cs @@ -2,24 +2,25 @@ namespace sh.actions.package_cleanup; public static class ConfigurationExtension { - private static void EnsureVariable(IConfiguration configuration, string key) + private const string MissingConfigurationError = "Configuration variable '{0}' is required but was not found."; + + private static void ValidateConfigurationVariable(IConfiguration configuration, string key) { - var value = configuration[key]; - if (string.IsNullOrEmpty(value)) + if (string.IsNullOrWhiteSpace(configuration[key])) { - throw new InvalidOperationException($"Configuration variable '{key}' is required but was not found."); + throw new InvalidOperationException(string.Format(MissingConfigurationError, key)); } } public static IConfigurationBuilder EnsureGiteaConfig(this IConfigurationBuilder builder) { var configuration = builder.Build(); + var requiredKeys = new[] { "URL", "OWNER", "TYPE", "NAME", "API_TOKEN" }; - EnsureVariable(configuration, "URL"); - EnsureVariable(configuration, "OWNER"); - EnsureVariable(configuration, "TYPE"); - EnsureVariable(configuration, "NAME"); - EnsureVariable(configuration, "API_TOKEN"); + foreach (var key in requiredKeys) + { + ValidateConfigurationVariable(configuration, key); + } return builder; } diff --git a/sh.actions.package-cleanup/Models/SoftwareVersion.cs b/sh.actions.package-cleanup/Models/SoftwareVersion.cs new file mode 100644 index 0000000..6186eab --- /dev/null +++ b/sh.actions.package-cleanup/Models/SoftwareVersion.cs @@ -0,0 +1,97 @@ +using System.Text.RegularExpressions; + +namespace sh.actions.package_cleanup.Models; + +public class SoftwareVersion( + int major, + int minor, + int patch = 0, + int build = 0, + string? suffix = null, + string? input = null, + bool isLatest = false) +{ + public int Major { get; set; } = major; + public int Minor { get; set; } = minor; + public int Patch { get; set; } = patch; + public int Build { get; set; } = build; + public string? Suffix { get; set; } = suffix; + public string? Input { get; set; } = input; + public bool IsLatest { get; set; } = isLatest; + + public override string ToString() + { + var versionStr = $"{Major}.{Minor}.{Patch}.{Build}"; + if (!string.IsNullOrEmpty(Suffix)) + { + versionStr += $"-{Suffix}"; + } + + return versionStr; + } + + public override bool Equals(object? obj) + { + return obj is SoftwareVersion version && ToString() == version.ToString(); + } + + public override int GetHashCode() + { + return ToString().GetHashCode(); + } + + public static SoftwareVersion Parse(string versionString) + { + // Try matching format: X.X.X.X or X.X.X.X-suffix + var match = Regex.Match(versionString, @"(\d+)\.(\d+)\.(\d+)\.(\d+)(?:-(.*))?"); + if (match.Success) + { + var major = int.Parse(match.Groups[1].Value); + var minor = int.Parse(match.Groups[2].Value); + var patch = int.Parse(match.Groups[3].Value); + var build = int.Parse(match.Groups[4].Value); + var suffix = match.Groups[5].Value; + return new SoftwareVersion(major, minor, patch, build, string.IsNullOrEmpty(suffix) ? null : suffix, + versionString); + } + + // Try matching format: X.X.X or X.X.X-suffix + match = Regex.Match(versionString, @"(\d+)\.(\d+)\.(\d+)(?:-(.*))?"); + if (match.Success) + { + var major = int.Parse(match.Groups[1].Value); + var minor = int.Parse(match.Groups[2].Value); + var patch = int.Parse(match.Groups[3].Value); + var suffix = match.Groups[4].Value; + return new SoftwareVersion(major, minor, patch, 0, string.IsNullOrEmpty(suffix) ? null : suffix, + versionString); + } + + // Try matching format: X.X or X.X-suffix + match = Regex.Match(versionString, @"(\d+)\.(\d+)(?:-(.*))?"); + if (match.Success) + { + var major = int.Parse(match.Groups[1].Value); + var minor = int.Parse(match.Groups[2].Value); + var suffix = match.Groups[3].Value; + return new SoftwareVersion(major, minor, 0, 0, string.IsNullOrEmpty(suffix) ? null : suffix, versionString); + } + + // Try matching format: X or X-suffix + match = Regex.Match(versionString, @"(\d+)(?:-(.*))?"); + if (match.Success) + { + var major = int.Parse(match.Groups[1].Value); + var suffix = match.Groups[2].Value; + return new SoftwareVersion(major, 0, 0, 0, string.IsNullOrEmpty(suffix) ? null : suffix, versionString); + } + + // Special case for "latest" + if (versionString == "latest") + { + return new SoftwareVersion(1, 0, 0, 0, null, versionString, isLatest: true); + } + + throw new ArgumentException($"Invalid version string: {versionString}"); + } +} \ No newline at end of file diff --git a/sh.actions.package-cleanup/Program.cs b/sh.actions.package-cleanup/Program.cs index be417e2..33f69d3 100644 --- a/sh.actions.package-cleanup/Program.cs +++ b/sh.actions.package-cleanup/Program.cs @@ -15,7 +15,5 @@ builder.Services .AddHostedService() .AddHttpClient(); - var host = builder.Build(); - -host.Run(); \ No newline at end of file +await host.RunAsync(); diff --git a/sh.actions.package-cleanup/Service/GiteaPackageService.cs b/sh.actions.package-cleanup/Service/GiteaPackageService.cs index 1efcff4..4806331 100644 --- a/sh.actions.package-cleanup/Service/GiteaPackageService.cs +++ b/sh.actions.package-cleanup/Service/GiteaPackageService.cs @@ -10,22 +10,33 @@ public class GiteaPackageService( HttpClient httpClient) : IGiteaPackageService { + private string GetBaseUrl() => + $"{configuration["URL"]?.TrimEnd('/')}/packages/{configuration["OWNER"]}/{configuration["TYPE"]}/{configuration["NAME"]}"; + + private void AddAuthorizationHeader(HttpRequestMessage request) + { + var token = configuration["API_TOKEN"]; + if (!string.IsNullOrEmpty(token)) + { + request.Headers.Add("Authorization", $"token {token}"); + } + } + public async Task> GetPackagesByOwnerAsync(CancellationToken cancellationToken = default) { try { - var baseUrl = - $"{configuration["URL"]?.TrimEnd('/')}/packages/{configuration["OWNER"]}/{configuration["TYPE"]}/{configuration["NAME"]}"; + var baseUrl = GetBaseUrl(); var allPackages = new List(); var page = 1; while (true) { var url = $"{baseUrl}?page={page}"; - logger.LogInformation("Fetching packages from Gitea: {Url}", url); + var request = new HttpRequestMessage(HttpMethod.Get, url); - request.Headers.Add("Authorization", $"token {configuration["API_TOKEN"]}"); + AddAuthorizationHeader(request); var response = await httpClient.SendAsync(request, cancellationToken); response.EnsureSuccessStatusCode(); @@ -46,7 +57,6 @@ public class GiteaPackageService( } logger.LogInformation("Successfully fetched {Count} packages in total", allPackages.Count); - return allPackages; } catch (HttpRequestException ex) @@ -65,30 +75,30 @@ public class GiteaPackageService( { try { - var url = - $"{configuration["URL"]?.TrimEnd('/')}/packages/{configuration["OWNER"]}/{package.Type}/{package.Name}/{package.Version}"; - logger.LogInformation("Deleting package {PackageName} (ID: {PackageId}) from Gitea: {Url}", package.Name, - package.Id, url); + var baseUrl = GetBaseUrl(); + var url = $"{baseUrl}/{package.Version}"; + logger.LogInformation("Deleting package {PackageName} (ID: {PackageId}) from Gitea", + package.Name, package.Id); var request = new HttpRequestMessage(HttpMethod.Delete, url); - request.Headers.Add("Authorization", $"token {configuration["API_TOKEN"]}"); + AddAuthorizationHeader(request); var response = await httpClient.SendAsync(request, cancellationToken); response.EnsureSuccessStatusCode(); - logger.LogInformation("Successfully deleted package {PackageName} (ID: {PackageId})", package.Name, - package.Id); + logger.LogInformation("Successfully deleted package {PackageName} (ID: {PackageId})", + package.Name, package.Id); } catch (HttpRequestException ex) { - logger.LogError(ex, "Error deleting package {PackageName} (ID: {PackageId}) from Gitea", package.Name, - package.Id); + logger.LogError(ex, "Error deleting package {PackageName} (ID: {PackageId}) from Gitea", + package.Name, package.Id); throw; } catch (Exception ex) { - logger.LogError(ex, "Unexpected error deleting package {PackageName} (ID: {PackageId})", package.Name, - package.Id); + logger.LogError(ex, "Unexpected error deleting package {PackageName} (ID: {PackageId})", + package.Name, package.Id); throw; } } diff --git a/sh.actions.package-cleanup/Service/PackageService.cs b/sh.actions.package-cleanup/Service/PackageService.cs index 23a37eb..3a03e9a 100644 --- a/sh.actions.package-cleanup/Service/PackageService.cs +++ b/sh.actions.package-cleanup/Service/PackageService.cs @@ -2,7 +2,7 @@ using sh.actions.package_cleanup.Models; namespace sh.actions.package_cleanup.Service; -public class PackageService(IConfiguration configuration, IGiteaPackageService packageService) : IPackageService +public class PackageService(IGiteaPackageService packageService) : IPackageService { public async Task> GetFilteredPackages() { @@ -12,301 +12,40 @@ public class PackageService(IConfiguration configuration, IGiteaPackageService p public List FilterPackagesToDelete(List packages) { - var packagesToDelete = new List(); + var versionMap = packages.ToDictionary(p => p.Version, p => SoftwareVersion.Parse(p.Version)); + var versionsToKeep = new HashSet(); - // Group packages by versioning variant - var semanticVersionPackages = packages.Where(p => IsSemanticVersion(p.Version)).ToList(); - var dateVersionPackages = packages.Where(p => IsDateVersion(p.Version)).ToList(); + foreach (var package in packages + .Where(p => string.IsNullOrEmpty(versionMap[p.Version].Suffix)) + .GroupBy(p => versionMap[p.Version].ToString()) + .Select(g => g.OrderBy(p => p.Version.Length).First())) + { + versionsToKeep.Add(package.Version); + } - // Process each group with their respective variant logic - packagesToDelete.AddRange(FilterPackagesToDeleteBySemanticVersion(semanticVersionPackages)); - packagesToDelete.AddRange(FilterPackagesToDeleteByDateVersion(dateVersionPackages)); + var latestVersion = packages.FirstOrDefault(p => versionMap[p.Version].IsLatest); + if (latestVersion != null) + { + versionsToKeep.Add(latestVersion.Version); + } - return packagesToDelete; - } + var suffixedVersionsToKeep = packages + .Where(p => !string.IsNullOrEmpty(versionMap[p.Version].Suffix) && + !(versionMap[p.Version].Major == 0 && versionMap[p.Version].Minor == 0 && + versionMap[p.Version].Patch == 0)) + .GroupBy(p => (versionMap[p.Version].Suffix, versionMap[p.Version].Major, versionMap[p.Version].Minor)) + .SelectMany(g => g + .OrderByDescending(p => versionMap[p.Version].Patch) + .ThenByDescending(p => versionMap[p.Version].Build) + .Take(2) + .GroupBy(p => versionMap[p.Version].ToString()) + .Select(group => group.OrderBy(p => p.Version.Length).First())); - private List FilterPackagesToDeleteBySemanticVersion(List packages) - { - var packagesToDelete = new List(); + foreach (var package in suffixedVersionsToKeep) + { + versionsToKeep.Add(package.Version); + } - // Group packages by release type - // Note: Dev releases must be checked before prerelease since -dev is a suffix - var devPackages = packages.Where(p => IsDevRelease(p.Version)) - .OrderByDescending(p => ParseSemanticVersion(p.Version)) - .ThenByDescending(p => p.CreatedAt) - .ToList(); - var prereleasePackages = packages.Where(p => IsPrereleaseRelease(p.Version) && !IsDevRelease(p.Version)) - .OrderByDescending(p => ParseSemanticVersion(p.Version)) - .ThenByDescending(p => p.CreatedAt) - .ToList(); - var prodPackages = packages.Where(p => IsProdRelease(p.Version)) - .OrderByDescending(p => ParseSemanticVersion(p.Version)) - .ThenByDescending(p => p.CreatedAt) - .ToList(); - - // Apply cleanup rules for each type - packagesToDelete.AddRange(FilterProdReleases(prodPackages)); - packagesToDelete.AddRange(FilterPrereleaseReleases(prereleasePackages)); - packagesToDelete.AddRange(FilterDevReleases(devPackages)); - - return packagesToDelete; - } - - private List FilterPackagesToDeleteByDateVersion(List packages) - { - var packagesToDelete = new List(); - - // Group packages by release type - // Note: Dev releases must be checked before prerelease since -dev is a suffix - var devPackages = packages.Where(p => IsDevRelease(p.Version)) - .OrderByDescending(p => ParseDateVersion(p.Version)) - .ThenByDescending(p => p.CreatedAt) - .ToList(); - var prereleasePackages = packages.Where(p => IsPrereleaseRelease(p.Version) && !IsDevRelease(p.Version)) - .OrderByDescending(p => ParseDateVersion(p.Version)) - .ThenByDescending(p => p.CreatedAt) - .ToList(); - var prodPackages = packages.Where(p => IsProdRelease(p.Version)) - .OrderByDescending(p => ParseDateVersion(p.Version)) - .ThenByDescending(p => p.CreatedAt) - .ToList(); - - // Apply cleanup rules for each type - packagesToDelete.AddRange(FilterProdReleasesDateVersion(prodPackages)); - packagesToDelete.AddRange(FilterPrereleaseReleasesDateVersion(prereleasePackages)); - packagesToDelete.AddRange(FilterDevReleasesDateVersion(devPackages)); - - return packagesToDelete; - } - - // Prod release methods for semantic versioning - private List FilterProdReleases(List packages) - { - // Check if prod cleaning is enabled - if (!GetBoolConfigValue("CLEAN_PROD_VERSIONS", false)) - return new List(); - - // Get keep count - var keepCount = GetIntConfigValue("KEEP_PROD_VERSIONS", 5); - - // If keepCount <= 0, keep all - if (keepCount <= 0) - return new List(); - - // Packages are already sorted descending by version - // Keep top N, delete rest - return packages.Skip(keepCount).ToList(); - } - - // Prerelease methods for semantic versioning - private List FilterPrereleaseReleases(List packages) - { - // Get keep count - var keepCount = GetIntConfigValue("KEEP_PRERELEASE_VERSIONS", 3); - - // If keepCount <= 0, keep all - if (keepCount <= 0) - return new List(); - - // Packages are already sorted descending by version - // Keep top N, delete rest - return packages.Skip(keepCount).ToList(); - } - - // Dev release methods for semantic versioning - private List FilterDevReleases(List packages) - { - // Get keep count - var keepCount = GetIntConfigValue("KEEP_DEV_VERSIONS", 5); - - // If keepCount <= 0, keep all - if (keepCount <= 0) - return new List(); - - // Packages are already sorted descending by version - // Keep top N, delete rest - return packages.Skip(keepCount).ToList(); - } - - // Prod release methods for date-based versioning - private List FilterProdReleasesDateVersion(List packages) - { - // Check if prod cleaning is enabled - if (!GetBoolConfigValue("CLEAN_PROD_VERSIONS", false)) - return new List(); - - // Get keep count - var keepCount = GetIntConfigValue("KEEP_PROD_VERSIONS", 5); - - // If keepCount <= 0, keep all - if (keepCount <= 0) - return new List(); - - // Packages are already sorted descending by version - // Keep top N, delete rest - return packages.Skip(keepCount).ToList(); - } - - // Prerelease methods for date-based versioning - private List FilterPrereleaseReleasesDateVersion(List packages) - { - // Get keep count - var keepCount = GetIntConfigValue("KEEP_PRERELEASE_VERSIONS", 3); - - // If keepCount <= 0, keep all - if (keepCount <= 0) - return new List(); - - // Packages are already sorted descending by version - // Keep top N, delete rest - return packages.Skip(keepCount).ToList(); - } - - // Dev release methods for date-based versioning - private List FilterDevReleasesDateVersion(List packages) - { - // Get keep count - var keepCount = GetIntConfigValue("KEEP_DEV_VERSIONS", 5); - - // If keepCount <= 0, keep all - if (keepCount <= 0) - return new List(); - - // Packages are already sorted descending by version - // Keep top N, delete rest - return packages.Skip(keepCount).ToList(); - } - - // Helper methods to identify release types - private bool IsProdRelease(string version) - { - // Prod versions typically don't have pre-release identifiers (alpha, beta, rc, dev, etc.) - return !version.Contains("-", StringComparison.OrdinalIgnoreCase); - } - - private bool IsPrereleaseRelease(string version) - { - // Prerelease versions contain identifiers like -alpha, -beta, -rc, -preview - // But NOT -dev, which is a separate release type - var lowerVersion = version.ToLowerInvariant(); - return (lowerVersion.Contains("alpha") || lowerVersion.Contains("beta") || - lowerVersion.Contains("rc") || lowerVersion.Contains("preview")) || - lowerVersion.Contains("staging") && !lowerVersion.Contains("dev"); - } - - private bool IsDevRelease(string version) - { - // Dev versions contain identifiers like dev or similar - return version.ToLowerInvariant().Contains("dev"); - } - - // Helper methods to identify versioning variant - private bool IsSemanticVersion(string version) - { - // Semantic versioning: major.minor.micro.build with typically smaller numbers - // Examples: 1.2.3.4, 2.0.1.100 - var versionPart = version.Split('-')[0]; - var parts = versionPart.Split('.').Where(p => !string.IsNullOrEmpty(p)).ToArray(); - - if (parts.Length < 2 || parts.Length > 4) - return false; - - // Try to parse all parts as numbers - if (!parts.All(p => int.TryParse(p, out var num))) - return false; - - // Semantic versioning typically has smaller major version numbers (0-20 range) - if (int.TryParse(parts[0], out var major) && major > 100) - return false; // Likely a date version if first number > 100 - - return true; - } - - private bool IsDateVersion(string version) - { - // Date-based versioning: year.month.day.build - // Examples: 2024.12.25.1, 2025.2.14.100 - var versionPart = version.Split('-')[0]; - var parts = versionPart.Split('.').Where(p => !string.IsNullOrEmpty(p)).ToArray(); - - if (parts.Length != 4) - return false; - - // Try to parse all parts as numbers - if (!parts.All(p => int.TryParse(p, out var num))) - return false; - - // Date version should have year > 1000 and < 3000 - if (!int.TryParse(parts[0], out var year) || year < 1000 || year > 3000) - return false; - - // Month should be 1-12 - if (!int.TryParse(parts[1], out var month) || month < 1 || month > 12) - return false; - - // Day should be 1-31 - if (!int.TryParse(parts[2], out var day) || day < 1 || day > 31) - return false; - - return true; - } - - // Version parsing methods - private (int major, int minor, int micro, int build) ParseSemanticVersion(string version) - { - // Remove suffix if present (e.g., "-alpha", "-dev") - var versionPart = version.Split('-')[0]; - var parts = versionPart.Split('.').Select(p => int.TryParse(p, out var num) ? num : 0).ToArray(); - - return ( - parts.Length > 0 ? parts[0] : 0, - parts.Length > 1 ? parts[1] : 0, - parts.Length > 2 ? parts[2] : 0, - parts.Length > 3 ? parts[3] : 0 - ); - } - - private (int year, int month, int day, int build) ParseDateVersion(string version) - { - // Remove suffix if present (e.g., "-alpha", "-dev") - var versionPart = version.Split('-')[0]; - var parts = versionPart.Split('.').Select(p => int.TryParse(p, out var num) ? num : 0).ToArray(); - - return ( - parts.Length > 0 ? parts[0] : 0, - parts.Length > 1 ? parts[1] : 0, - parts.Length > 2 ? parts[2] : 0, - parts.Length > 3 ? parts[3] : 0 - ); - } - - // Configuration helper methods - private int GetIntConfigValue(string key, int defaultValue) - { - var value = configuration[key]; - if (string.IsNullOrEmpty(value)) - return defaultValue; - - if (int.TryParse(value, out var result)) - return result; - - return defaultValue; - } - - private bool GetBoolConfigValue(string key, bool defaultValue) - { - var value = configuration[key]; - if (string.IsNullOrEmpty(value)) - return defaultValue; - - if (bool.TryParse(value, out var result)) - return result; - - // Handle common string representations - if (value.Equals("true", StringComparison.OrdinalIgnoreCase) || - value.Equals("1", StringComparison.OrdinalIgnoreCase)) - return true; - - return defaultValue; + return packages.Where(p => !versionsToKeep.Contains(p.Version)).ToList(); } } \ No newline at end of file diff --git a/sh.actions.package-cleanup/Worker.cs b/sh.actions.package-cleanup/Worker.cs index d4d857f..188db1c 100644 --- a/sh.actions.package-cleanup/Worker.cs +++ b/sh.actions.package-cleanup/Worker.cs @@ -13,29 +13,48 @@ public class Worker( { private async Task DeletePackages(List packages, CancellationToken cancellationToken = default) { - if (configuration["DRY_RUN"]?.ToLower() == "true") + var dryRun = configuration["DRY_RUN"]?.ToLower() == "true"; + if (dryRun) { - logger.LogInformation("Dry run enabled, not deleting packages"); + logger.LogInformation("Dry run enabled, not deleting {Count} packages", packages.Count); return; } foreach (var giteaPackage in packages) { - await giteaPackageService.DeletePackage(giteaPackage, cancellationToken); + try + { + await giteaPackageService.DeletePackage(giteaPackage, cancellationToken); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to delete package {PackageName} version {Version}", + giteaPackage.Name, giteaPackage.Version); + } } } protected override async Task ExecuteAsync(CancellationToken cancellationToken) { - var packages = await packageService.GetFilteredPackages(); - Console.WriteLine($"Found {packages.Count()} packages"); - - var packagesToDelete = packageService.FilterPackagesToDelete(packages); - Console.WriteLine($"Found {packagesToDelete.Count()} packages to delete"); - - await DeletePackages(packagesToDelete, cancellationToken); - - logger.LogInformation("Cleanup finished, stopping application"); - appLifetime.StopApplication(); + try + { + var packages = await packageService.GetFilteredPackages(); + logger.LogInformation("Found {Count} packages", packages.Count); + + var packagesToDelete = packageService.FilterPackagesToDelete(packages); + logger.LogInformation("Found {Count} packages to delete", packagesToDelete.Count); + + await DeletePackages(packagesToDelete, cancellationToken); + + logger.LogInformation("Cleanup finished successfully"); + } + catch (Exception ex) + { + logger.LogError(ex, "Cleanup failed with an error"); + } + finally + { + appLifetime.StopApplication(); + } } } \ No newline at end of file