diff --git a/src/Maestro/FeedCleanerService/.config/settings.Development.json b/src/Maestro/FeedCleanerService/.config/settings.Development.json index d8f688287d..7f5f4c0dd5 100644 --- a/src/Maestro/FeedCleanerService/.config/settings.Development.json +++ b/src/Maestro/FeedCleanerService/.config/settings.Development.json @@ -8,7 +8,9 @@ "ConnectionString": "Data Source=localhost\\SQLEXPRESS;Initial Catalog=BuildAssetRegistry;Integrated Security=true" }, "AzureDevOps": { - "default": { + // The list of organizations needs to be explicit in here + // The feed cleaner uses the list to process those org's feeds + "dnceng": { "UseLocalCredentials": true } } diff --git a/src/Maestro/FeedCleanerService/.config/settings.Production.json b/src/Maestro/FeedCleanerService/.config/settings.Production.json index 8b1b69796e..3d95f6c7e5 100644 --- a/src/Maestro/FeedCleanerService/.config/settings.Production.json +++ b/src/Maestro/FeedCleanerService/.config/settings.Production.json @@ -11,8 +11,10 @@ "ConnectionString": "Data Source=tcp:maestro-prod.database.windows.net,1433; Initial Catalog=BuildAssetRegistry; Authentication=Active Directory Managed Identity; Persist Security Info=False; MultipleActiveResultSets=True; Connect Timeout=30; Encrypt=True; TrustServerCertificate=False;" }, "AzureDevOps": { - "default": { + // The list of organizations needs to be explicit in here + // The feed cleaner uses the list to process those org's feeds + "dnceng": { "ManagedIdentityId": "system" } } -} \ No newline at end of file +} diff --git a/src/Maestro/FeedCleanerService/FeedCleanerService.cs b/src/Maestro/FeedCleanerService/FeedCleanerService.cs index 0b51be8dd7..441026fd74 100644 --- a/src/Maestro/FeedCleanerService/FeedCleanerService.cs +++ b/src/Maestro/FeedCleanerService/FeedCleanerService.cs @@ -61,52 +61,64 @@ public Task RunAsync(CancellationToken cancellationToken) [CronSchedule("0 0 2 1/1 * ? *", TimeZones.PST)] public async Task CleanManagedFeedsAsync() { - if (Options.Enabled) + if (!Options.Enabled) { - Dictionary>> packagesInReleaseFeeds = - await GetPackagesForReleaseFeedsAsync(); + Logger.LogInformation("Feed cleaner service is disabled in this environment"); + return; + } + + Logger.LogInformation("Loading packages in release feeds..."); + + Dictionary>> packagesInReleaseFeeds = + await GetPackagesForReleaseFeedsAsync(); + + Logger.LogInformation("Loaded {versionCount} versions of {packageCount} packages from {feedCount} release feeds", + packagesInReleaseFeeds.Sum(feed => feed.Value.Sum(package => package.Value.Count)), + packagesInReleaseFeeds.Sum(feed => feed.Value.Keys.Count), + packagesInReleaseFeeds.Keys.Count); - foreach (var azdoAccount in Options.AzdoAccounts) + foreach (var azdoAccount in Options.AzdoAccounts) + { + Logger.LogInformation("Processing feeds for {account}...", azdoAccount); + + List allFeeds; + try + { + allFeeds = await _azureDevOpsClient.GetFeedsAsync(azdoAccount); + Logger.LogInformation("Found {count} feeds for {account}...", allFeeds.Count, azdoAccount); + } + catch (Exception ex) + { + Logger.LogError(ex, "Failed to get feeds for account {azdoAccount}", azdoAccount); + continue; + } + + IEnumerable managedFeeds = allFeeds.Where(f => Regex.IsMatch(f.Name, FeedConstants.MaestroManagedFeedNamePattern)); + + foreach (var feed in managedFeeds) { - List allFeeds; try { - allFeeds = await _azureDevOpsClient.GetFeedsAsync(azdoAccount); - } - catch (Exception ex) - { - Logger.LogError(ex, $"Failed to get feeds for account {azdoAccount}"); - continue; - } - IEnumerable managedFeeds = allFeeds.Where(f => Regex.IsMatch(f.Name, FeedConstants.MaestroManagedFeedNamePattern)); + var packages = await _azureDevOpsClient.GetPackagesForFeedAsync(feed.Account, feed.Project?.Name, feed.Name); - foreach (var feed in managedFeeds) - { - try + Logger.LogInformation("Cleaning feed {feed} with {count} packages...", feed.Name, packages.Count); + + foreach (var package in packages) { - await PopulatePackagesForFeedAsync(feed); - foreach (var package in feed.Packages) - { - HashSet updatedVersions = - await UpdateReleasedVersionsForPackageAsync(feed, package, packagesInReleaseFeeds); + HashSet updatedVersions = + await UpdateReleasedVersionsForPackageAsync(feed, package, packagesInReleaseFeeds); - await DeletePackageVersionsFromFeedAsync(feed, package.Name, updatedVersions); - } - // We may have deleted all packages in the previous operation, if so, we should delete the feed, - // refresh the packages in the feed to check this. - await PopulatePackagesForFeedAsync(feed); + await DeletePackageVersionsFromFeedAsync(feed, package.Name, updatedVersions); } - catch (Exception ex) - { - Logger.LogError(ex, $"Something failed while trying to update the released packages in feed {feed.Name}"); - } + + Logger.LogInformation("Feed {feed} cleaning finished", feed.Name); + } + catch (Exception ex) + { + Logger.LogError(ex, "Something failed while trying to update the released packages in feed {feed}", feed.Name); } } } - else - { - Logger.LogInformation("Feed cleaner service is disabled in this environment"); - } } /// @@ -321,14 +333,4 @@ private async Task IsPackageAvailableInNugetOrgAsync(string name, string v return false; } } - - /// - /// Populates the packages and versions for a given feed - /// - /// Feed to populate - /// - private async Task PopulatePackagesForFeedAsync(AzureDevOpsFeed feed) - { - feed.Packages = await _azureDevOpsClient.GetPackagesForFeedAsync(feed.Account, feed.Project?.Name, feed.Name); - } } diff --git a/src/ProductConstructionService/ProductConstructionService.Common/ProductConstructionServiceExtension.cs b/src/ProductConstructionService/ProductConstructionService.Common/ProductConstructionServiceExtension.cs index 04d2d1d4fa..a8c7c40bdc 100644 --- a/src/ProductConstructionService/ProductConstructionService.Common/ProductConstructionServiceExtension.cs +++ b/src/ProductConstructionService/ProductConstructionService.Common/ProductConstructionServiceExtension.cs @@ -32,8 +32,12 @@ public static void AddBuildAssetRegistry(this IHostApplicationBuilder builder) builder.Services.TryAddTransient(); builder.Services.AddDbContext(options => { - // Do not log DB context initialization - options.ConfigureWarnings(w => w.Ignore(CoreEventId.ContextInitialized)); + // Do not log DB context initialization and command executed events + options.ConfigureWarnings(w => + { + w.Ignore(CoreEventId.ContextInitialized); + w.Ignore(RelationalEventId.CommandExecuted); + }); options.UseSqlServer(databaseConnectionString, sqlOptions => { diff --git a/src/ProductConstructionService/ProductConstructionService.FeedCleaner/FakeAzureDevOpsClient.cs b/src/ProductConstructionService/ProductConstructionService.FeedCleaner/FakeAzureDevOpsClient.cs deleted file mode 100644 index 315ff7c34a..0000000000 --- a/src/ProductConstructionService/ProductConstructionService.FeedCleaner/FakeAzureDevOpsClient.cs +++ /dev/null @@ -1,42 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.DotNet.DarcLib; -using Newtonsoft.Json.Linq; - -namespace ProductConstructionService.FeedCleaner; - -// TODO (https://github.com/dotnet/arcade-services/issues/3808) delete this class and use the normal AzureDevOpsClient -internal class FakeAzureDevOpsClient : IAzureDevOpsClient -{ - public Task AdjustReleasePipelineArtifactSourceAsync(string accountName, string projectName, AzureDevOpsReleaseDefinition releaseDefinition, AzureDevOpsBuild build) - => Task.FromResult(releaseDefinition); - public Task DeleteFeedAsync(string accountName, string project, string feedIdentifier) => Task.CompletedTask; - public Task DeleteNuGetPackageVersionFromFeedAsync(string accountName, string project, string feedIdentifier, string packageName, string version) - => Task.CompletedTask; - public Task> GetBuildArtifactsAsync(string accountName, string projectName, int buildId, int maxRetries = 15) - => Task.FromResult>([]); - public Task GetBuildAsync(string accountName, string projectName, long buildId) - => Task.FromResult(new AzureDevOpsBuild()); - public Task GetBuildsAsync(string account, string project, int definitionId, string branch, int count, string status) - => Task.FromResult(new JObject()); - public Task GetFeedAndPackagesAsync(string accountName, string project, string feedIdentifier) - => Task.FromResult(new AzureDevOpsFeed("fake", "fake", "fake")); - public Task GetFeedAsync(string accountName, string project, string feedIdentifier) - => Task.FromResult(new AzureDevOpsFeed("fake", "fake", "fake")); - public Task> GetFeedsAndPackagesAsync(string accountName) - => Task.FromResult>([]); - public Task> GetFeedsAsync(string accountName) - => Task.FromResult>([]); - public Task> GetPackagesForFeedAsync(string accountName, string project, string feedIdentifier) - => Task.FromResult>([]); - public Task GetProjectIdAsync(string accountName, string projectName) => Task.FromResult(string.Empty); - public Task GetReleaseAsync(string accountName, string projectName, int releaseId) - => Task.FromResult(new AzureDevOpsRelease()); - public Task GetReleaseDefinitionAsync(string accountName, string projectName, long releaseDefinitionId) - => Task.FromResult(new AzureDevOpsReleaseDefinition()); - public Task StartNewBuildAsync(string accountName, string projectName, int buildDefinitionId, string sourceBranch, string sourceVersion, Dictionary queueTimeVariables, Dictionary templateParameters, Dictionary pipelineResources) - => Task.FromResult(0); - public Task StartNewReleaseAsync(string accountName, string projectName, AzureDevOpsReleaseDefinition releaseDefinition, int barBuildId) - => Task.FromResult(0); -} diff --git a/src/ProductConstructionService/ProductConstructionService.FeedCleaner/FeedCleaner.cs b/src/ProductConstructionService/ProductConstructionService.FeedCleaner/FeedCleaner.cs index 731b999ada..944ee035ad 100644 --- a/src/ProductConstructionService/ProductConstructionService.FeedCleaner/FeedCleaner.cs +++ b/src/ProductConstructionService/ProductConstructionService.FeedCleaner/FeedCleaner.cs @@ -4,21 +4,23 @@ using System.Net; using System.Text.RegularExpressions; using Maestro.Data; +using Maestro.Data.Models; using Microsoft.DotNet.DarcLib; using Microsoft.DotNet.DarcLib.Helpers; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using PackagesInReleaseFeeds = System.Collections.Generic.Dictionary>>; namespace ProductConstructionService.FeedCleaner; public class FeedCleaner { - private BuildAssetRegistryContext _context; + private readonly BuildAssetRegistryContext _context; private readonly HttpClient _httpClient; private readonly IAzureDevOpsClient _azureDevOpsClient; private readonly IOptions _options; - private ILogger _logger; + private readonly ILogger _logger; public FeedCleaner( BuildAssetRegistryContext context, @@ -43,55 +45,82 @@ public async Task CleanManagedFeedsAsync() return; } - Dictionary>> packagesInReleaseFeeds = + _logger.LogInformation("Loading packages in release feeds..."); + + PackagesInReleaseFeeds packagesInReleaseFeeds = await GetPackagesForReleaseFeedsAsync(); + _logger.LogInformation("Loaded {versionCount} versions of {packageCount} packages from {feedCount} feeds", + packagesInReleaseFeeds.Sum(feed => feed.Value.Sum(package => package.Value.Count)), + packagesInReleaseFeeds.Sum(feed => feed.Value.Keys.Count), + packagesInReleaseFeeds.Keys.Count); + foreach (var azdoAccount in Options.AzdoAccounts) { + _logger.LogInformation("Processing feeds for {account}...", azdoAccount); + List allFeeds; try { allFeeds = await _azureDevOpsClient.GetFeedsAsync(azdoAccount); + _logger.LogInformation("Found {count} feeds for {account}...", allFeeds.Count, azdoAccount); } catch (Exception ex) { - _logger.LogError(ex, $"Failed to get feeds for account {azdoAccount}"); + _logger.LogError(ex, "Failed to get feeds for account {azdoAccount}", azdoAccount); continue; } + IEnumerable managedFeeds = allFeeds.Where(f => Regex.IsMatch(f.Name, FeedConstants.MaestroManagedFeedNamePattern)); foreach (var feed in managedFeeds) { - try - { - await PopulatePackagesForFeedAsync(feed); - foreach (var package in feed.Packages) - { - HashSet updatedVersions = - await UpdateReleasedVersionsForPackageAsync(feed, package, packagesInReleaseFeeds); - - await DeletePackageVersionsFromFeedAsync(feed, package.Name, updatedVersions); - } - // We may have deleted all packages in the previous operation, if so, we should delete the feed, - // refresh the packages in the feed to check this. - await PopulatePackagesForFeedAsync(feed); - } - catch (Exception ex) - { - _logger.LogError(ex, $"Something failed while trying to update the released packages in feed {feed.Name}"); - } + await CleanFeedAsync(feed, packagesInReleaseFeeds); } } } + private async Task CleanFeedAsync(AzureDevOpsFeed feed, PackagesInReleaseFeeds packagesInReleaseFeeds) + { + try + { + var packages = await _azureDevOpsClient.GetPackagesForFeedAsync(feed.Account, feed.Project?.Name, feed.Name); + + _logger.LogInformation("Cleaning feed {feed} with {count} packages...", feed.Name, packages.Count); + + var updatedCount = 0; + + foreach (var package in packages) + { + HashSet updatedVersions = await UpdateReleasedVersionsForPackageAsync(feed, package, packagesInReleaseFeeds); + + await DeletePackageVersionsFromFeedAsync(feed, package.Name, updatedVersions); + updatedCount += updatedVersions.Count; + } + + _logger.LogInformation("Feed {feed} cleaning finished with {count} updated packages", feed.Name, updatedCount); + + packages = await _azureDevOpsClient.GetPackagesForFeedAsync(feed.Account, feed.Project?.Name, feed.Name); + if (!packages.Any(packages => packages.Versions.Any(v => !v.IsDeleted))) + { + _logger.LogInformation("Feed {feed} has no packages left, deleting the feed", feed.Name); + await _azureDevOpsClient.DeleteFeedAsync(feed.Account, feed.Project?.Name, feed.Name); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Something failed while trying to update the released packages in feed {feed}", feed.Name); + } + } + /// /// Get a mapping of feed -> (package, versions) for the release feeds so it /// can be easily queried whether a version of a package is in a feed. /// /// Mapping of packages to versions for the release feeds. - private async Task>>> GetPackagesForReleaseFeedsAsync() + private async Task GetPackagesForReleaseFeedsAsync() { - var packagesWithVersionsInReleaseFeeds = new Dictionary>>(); + var packagesWithVersionsInReleaseFeeds = new PackagesInReleaseFeeds(); IEnumerable dotnetManagedFeeds = Options.ReleasePackageFeeds; foreach ((string account, string project, string feedName) in dotnetManagedFeeds) { @@ -146,7 +175,7 @@ private async Task>> GetPackageVersionsForFee private async Task> UpdateReleasedVersionsForPackageAsync( AzureDevOpsFeed feed, AzureDevOpsPackage package, - Dictionary>> dotnetFeedsPackageMapping) + PackagesInReleaseFeeds dotnetFeedsPackageMapping) { var releasedVersions = new HashSet(StringComparer.OrdinalIgnoreCase); @@ -162,8 +191,11 @@ private async Task> UpdateReleasedVersionsForPackageAsync( if (matchingAsset == null) { - _logger.LogError($"Unable to find asset {package.Name}.{version.Version} in feed {feed.Name} in BAR. " + - $"Unable to determine if it was released or update its locations."); + _logger.LogError("Unable to find asset {package}.{version} in feed {feed} in BAR. " + + "Unable to determine if it was released or update its locations.", + package.Name, + version.Version, + feed.Name); continue; } else @@ -171,7 +203,9 @@ private async Task> UpdateReleasedVersionsForPackageAsync( if (matchingAsset.Locations.Any(l => l.Location == FeedConstants.NuGetOrgLocation || dotnetFeedsPackageMapping.Any(f => l.Location == f.Key))) { - _logger.LogInformation($"Package {package.Name}.{version.Version} is already present in a public location."); + _logger.LogInformation("Package {package}.{version} is already present in a public location.", + package.Name, + version.Version); releasedVersions.Add(version.Version); } else @@ -189,7 +223,9 @@ private async Task> UpdateReleasedVersionsForPackageAsync( } catch (HttpRequestException e) { - _logger.LogInformation(e, $"Failed to determine if package {package.Name}.{version.Version} is present in NuGet.org"); + _logger.LogInformation(e, "Failed to determine if package {package}.{version} is present in NuGet.org", + package.Name, + version.Version); } @@ -198,21 +234,22 @@ private async Task> UpdateReleasedVersionsForPackageAsync( releasedVersions.Add(version.Version); foreach (string feedToAdd in feedsWherePackageIsAvailable) { - _logger.LogInformation($"Found package {package.Name}.{version.Version} in " + - $"{feedToAdd}, adding location to asset."); + _logger.LogInformation("Found package {package}.{version} in {feed}, adding location to asset.", + package.Name, + version.Version, + feedToAdd); - // TODO (https://github.com/dotnet/arcade-services/issues/3808) Don't actually do anything in BAR before we migrate fully to PCS - /*matchingAsset.Locations.Add(new AssetLocation() + matchingAsset.Locations.Add(new AssetLocation() { Location = feedToAdd, Type = LocationType.NugetFeed }); - await _context.SaveChangesAsync();*/ + await _context.SaveChangesAsync(); } } else { - _logger.LogInformation($"Unable to find {package.Name}.{version} in any of the release feeds"); + _logger.LogInformation("Unable to find {package}.{version} in any of the release feeds", package.Name, version); } } } @@ -233,7 +270,8 @@ private async Task DeletePackageVersionsFromFeedAsync(AzureDevOpsFeed feed, stri { try { - _logger.LogInformation($"Deleting package {packageName}.{version} from feed {feed.Name}"); + _logger.LogInformation("Deleting package {package}.{version} from feed {feed}", + packageName, version, feed.Name); await _azureDevOpsClient.DeleteNuGetPackageVersionFromFeedAsync(feed.Account, feed.Project?.Name, @@ -243,7 +281,10 @@ await _azureDevOpsClient.DeleteNuGetPackageVersionFromFeedAsync(feed.Account, } catch (HttpRequestException e) { - _logger.LogError(e, $"There was an error attempting to delete package {packageName}.{version} from the {feed.Name} feed. Skipping..."); + _logger.LogError(e, "There was an error attempting to delete package {package}.{version} from the {feed} feed. Skipping...", + packageName, + version, + feed.Name); } } } @@ -255,10 +296,10 @@ await _azureDevOpsClient.DeleteNuGetPackageVersionFromFeedAsync(feed.Account, /// Version to search for /// Feeds to search /// List of feeds in the package mappings where the provided package and version are available - private List GetReleaseFeedsWherePackageIsAvailable( + private static List GetReleaseFeedsWherePackageIsAvailable( string name, string version, - Dictionary>> packageMappings) + PackagesInReleaseFeeds packageMappings) { List feeds = []; foreach ((string feedName, Dictionary> packages) in packageMappings) @@ -288,23 +329,13 @@ private async Task IsPackageAvailableInNugetOrgAsync(string name, string v using HttpResponseMessage response = await _httpClient.SendAsync(headRequest); response.EnsureSuccessStatusCode(); - _logger.LogInformation($"Found {name}.{version} in nuget.org URI: {packageContentsUri}"); + _logger.LogInformation("Found {package}.{version} in nuget.org URI: {uri}", name, version, packageContentsUri); return true; } catch (HttpRequestException e) when (e.Message.Contains(((int)HttpStatusCode.NotFound).ToString())) { - _logger.LogInformation($"Unable to find {name}.{version} in nuget.org URI: {packageContentsUri}"); + _logger.LogInformation("Unable to find {package}.{version} in nuget.org URI: {uri}", name, version, packageContentsUri); return false; } } - - /// - /// Populates the packages and versions for a given feed - /// - /// Feed to populate - /// - private async Task PopulatePackagesForFeedAsync(AzureDevOpsFeed feed) - { - feed.Packages = await _azureDevOpsClient.GetPackagesForFeedAsync(feed.Account, feed.Project?.Name, feed.Name); - } } diff --git a/src/ProductConstructionService/ProductConstructionService.FeedCleaner/FeedCleanerConfiguration.cs b/src/ProductConstructionService/ProductConstructionService.FeedCleaner/FeedCleanerConfiguration.cs index d94fd9e744..bee10096bc 100644 --- a/src/ProductConstructionService/ProductConstructionService.FeedCleaner/FeedCleanerConfiguration.cs +++ b/src/ProductConstructionService/ProductConstructionService.FeedCleaner/FeedCleanerConfiguration.cs @@ -26,14 +26,12 @@ public static void ConfigureFeedCleaner(this IHostApplicationBuilder builder, IT AzureDevOpsTokenProviderOptions azdoConfig = []; builder.Configuration.GetSection("AzureDevOps").Bind(azdoConfig); - options.AzdoAccounts = azdoConfig.Keys.ToList(); + options.AzdoAccounts = [.. azdoConfig.Keys]; }); builder.Services.AddTransient(); builder.Services.Configure("AzureDevOps", (o, s) => s.Bind(o)); - // TODO https://github.com/dotnet/arcade-services/issues/3808: - //builder.Services.AddTransient(); - builder.Services.AddTransient(); + builder.Services.AddTransient(); builder.Services.AddTransient(sp => sp.GetRequiredService>()); builder.Services.AddTransient(sp => ActivatorUtilities.CreateInstance(sp, "git")); diff --git a/src/ProductConstructionService/ProductConstructionService.FeedCleaner/appsettings.Production.json b/src/ProductConstructionService/ProductConstructionService.FeedCleaner/appsettings.Production.json index 9b74b68cbf..38a0cefabc 100644 --- a/src/ProductConstructionService/ProductConstructionService.FeedCleaner/appsettings.Production.json +++ b/src/ProductConstructionService/ProductConstructionService.FeedCleaner/appsettings.Production.json @@ -1,9 +1,14 @@ { "BuildAssetRegistrySqlConnectionString": "Data Source=tcp:maestro-prod.database.windows.net,1433; Initial Catalog=BuildAssetRegistry; Authentication=Active Directory Managed Identity; Persist Security Info=False; MultipleActiveResultSets=True; Connect Timeout=30; Encrypt=True; TrustServerCertificate=False; User Id=USER_ID_PLACEHOLDER", "AzureDevOps": { - "default": { + // The list of organizations needs to be explicit in here + // The feed cleaner uses the list to process those org's feeds + "dnceng": { "ManagedIdentityId": "520f92da-8df7-4bdf-afcd-400caf2c23b6" } }, + "FeedCleaner": { + "Enabled": true + }, "ManagedIdentityClientId": "520f92da-8df7-4bdf-afcd-400caf2c23b6" } diff --git a/src/ProductConstructionService/ProductConstructionService.FeedCleaner/appsettings.Staging.json b/src/ProductConstructionService/ProductConstructionService.FeedCleaner/appsettings.Staging.json index 538acac674..9f69c22035 100644 --- a/src/ProductConstructionService/ProductConstructionService.FeedCleaner/appsettings.Staging.json +++ b/src/ProductConstructionService/ProductConstructionService.FeedCleaner/appsettings.Staging.json @@ -1,7 +1,9 @@ { "BuildAssetRegistrySqlConnectionString": "Data Source=tcp:maestro-int-server.database.windows.net,1433; Initial Catalog=BuildAssetRegistry; Authentication=Active Directory Managed Identity; Persist Security Info=False; MultipleActiveResultSets=True; Connect Timeout=30; Encrypt=True; TrustServerCertificate=False; User Id=USER_ID_PLACEHOLDER", "AzureDevOps": { - "default": { + // The list of organizations needs to be explicit in here + // The feed cleaner uses the list to process those org's feeds + "dnceng": { "ManagedIdentityId": "52833a45-ec68-4d75-8c83-e7df24649158" } }, diff --git a/test/ProductConstructionService.FeedCleaner.Tests/FeedCleanerTests.cs b/test/ProductConstructionService.FeedCleaner.Tests/FeedCleanerTests.cs index 5fae4b4909..9a93f54526 100644 --- a/test/ProductConstructionService.FeedCleaner.Tests/FeedCleanerTests.cs +++ b/test/ProductConstructionService.FeedCleaner.Tests/FeedCleanerTests.cs @@ -96,7 +96,6 @@ public async Task OnlyDeletesReleasedPackagesFromManagedFeeds() } [Test] - [Ignore("(https://github.com/dotnet/arcade-services/issues/3808) ignore till this is resolved")] public async Task UpdatesAssetLocationsForReleasedPackages() { await _feedCleaner!.CleanManagedFeedsAsync();