using sh.actions.package_cleanup.Models; namespace sh.actions.package_cleanup.Service; public class PackageService(IConfiguration configuration, IGiteaPackageService packageService) : IPackageService { public async Task> GetFilteredPackages() { var packages = (await packageService.GetPackagesByOwnerAsync()).ToList(); return packages; } public List FilterPackagesToDelete(List packages) { var packagesToDelete = new List(); // Group packages by versioning variant var semanticVersionPackages = packages.Where(p => IsSemanticVersion(p.Version)).ToList(); var dateVersionPackages = packages.Where(p => IsDateVersion(p.Version)).ToList(); // Process each group with their respective variant logic packagesToDelete.AddRange(FilterPackagesToDeleteBySemanticVersion(semanticVersionPackages)); packagesToDelete.AddRange(FilterPackagesToDeleteByDateVersion(dateVersionPackages)); return packagesToDelete; } private List FilterPackagesToDeleteBySemanticVersion(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 => 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; } }