diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/MiscellaneousTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/MiscellaneousTests.cs new file mode 100644 index 0000000000..17db05d563 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/MiscellaneousTests.cs @@ -0,0 +1,85 @@ +using NuGet.Versioning; + +using NuGetUpdater.Core.Analyze; +using NuGetUpdater.Core.Run; +using NuGetUpdater.Core.Run.ApiModel; + +using Xunit; + +namespace NuGetUpdater.Core.Test.Run; + +public class MiscellaneousTests +{ + [Theory] + [MemberData(nameof(RequirementsFromIgnoredVersionsData))] + public void RequirementsFromIgnoredVersions(string dependencyName, Condition[] ignoreConditions, Requirement[] expectedRequirements) + { + var job = new Job() + { + Source = new() + { + Provider = "github", + Repo = "some/repo" + }, + IgnoreConditions = ignoreConditions + }; + var actualRequirements = RunWorker.GetIgnoredRequirementsForDependency(job, dependencyName); + var actualRequirementsStrings = string.Join("|", actualRequirements.Select(r => r.ToString())); + var expectedRequirementsStrings = string.Join("|", expectedRequirements.Select(r => r.ToString())); + Assert.Equal(expectedRequirementsStrings, actualRequirementsStrings); + } + + public static IEnumerable RequirementsFromIgnoredVersionsData() + { + yield return + [ + // dependencyName + "Some.Package", + // ignoredConditions + new Condition[] + { + new() + { + DependencyName = "SOME.PACKAGE", + VersionRequirement = Requirement.Parse("> 1.2.3") + }, + new() + { + DependencyName = "some.package", + VersionRequirement = Requirement.Parse("<= 2.0.0") + }, + new() + { + DependencyName = "Unrelated.Package", + VersionRequirement = Requirement.Parse("= 3.4.5") + } + }, + // expectedRequirements + new Requirement[] + { + new IndividualRequirement(">", NuGetVersion.Parse("1.2.3")), + new IndividualRequirement("<=", NuGetVersion.Parse("2.0.0")), + } + ]; + + // version requirement is null => ignore all + yield return + [ + // dependencyName + "Some.Package", + // ignoredConditions + new Condition[] + { + new() + { + DependencyName = "Some.Package" + } + }, + // expectedRequirements + new Requirement[] + { + new IndividualRequirement(">", NuGetVersion.Parse("0.0.0")) + } + ]; + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/SerializationTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/SerializationTests.cs index 2bbb9daced..ec6188f5c4 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/SerializationTests.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/SerializationTests.cs @@ -257,6 +257,48 @@ public void SerializeError() Assert.Equal(expected, actual); } + [Fact] + public void DeserializeJobIgnoreConditions() + { + var jobContent = """ + { + "job": { + "package-manager": "nuget", + "source": { + "provider": "github", + "repo": "some-org/some-repo", + "directory": "specific-sdk" + }, + "ignore-conditions": [ + { + "dependency-name": "Package.1", + "source": "some-file", + "version-requirement": "> 1.2.3" + }, + { + "dependency-name": "Package.2", + "updated-at": "2024-12-05T15:47:12Z" + } + ] + } + } + """; + var jobWrapper = RunWorker.Deserialize(jobContent)!; + Assert.Equal(2, jobWrapper.Job.IgnoreConditions.Length); + + Assert.Equal("Package.1", jobWrapper.Job.IgnoreConditions[0].DependencyName); + Assert.Equal("some-file", jobWrapper.Job.IgnoreConditions[0].Source); + Assert.Empty(jobWrapper.Job.IgnoreConditions[0].UpdateTypes); + Assert.Null(jobWrapper.Job.IgnoreConditions[0].UpdatedAt); + Assert.Equal("> 1.2.3", jobWrapper.Job.IgnoreConditions[0].VersionRequirement?.ToString()); + + Assert.Equal("Package.2", jobWrapper.Job.IgnoreConditions[1].DependencyName); + Assert.Null(jobWrapper.Job.IgnoreConditions[1].Source); + Assert.Empty(jobWrapper.Job.IgnoreConditions[1].UpdateTypes); + Assert.Equal(new DateTime(2024, 12, 5, 15, 47, 12), jobWrapper.Job.IgnoreConditions[1].UpdatedAt); + Assert.Null(jobWrapper.Job.IgnoreConditions[1].VersionRequirement); + } + private class CapturingTestLogger : ILogger { private readonly List _messages = new(); diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/RequirementConverter.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/RequirementConverter.cs new file mode 100644 index 0000000000..361643f67f --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/RequirementConverter.cs @@ -0,0 +1,17 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace NuGetUpdater.Core.Analyze; + +public class RequirementConverter : JsonConverter +{ + public override Requirement? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return Requirement.Parse(reader.GetString()!); + } + + public override void Write(Utf8JsonWriter writer, Requirement value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.ToString()); + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/Condition.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/Condition.cs new file mode 100644 index 0000000000..b043578b43 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/Condition.cs @@ -0,0 +1,19 @@ +using System.Text.Json.Serialization; + +using NuGetUpdater.Core.Analyze; + +namespace NuGetUpdater.Core.Run.ApiModel; + +public sealed record Condition +{ + [JsonPropertyName("dependency-name")] + public required string DependencyName { get; init; } + [JsonPropertyName("source")] + public string? Source { get; init; } = null; + [JsonPropertyName("update-types")] + public string[] UpdateTypes { get; init; } = []; + [JsonPropertyName("updated-at")] + public DateTime? UpdatedAt { get; init; } = null; + [JsonPropertyName("version-requirement")] + public Requirement? VersionRequirement { get; init; } = null; +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/Job.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/Job.cs index 09782192c1..7e688280d0 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/Job.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/Job.cs @@ -16,7 +16,7 @@ public sealed record Job public object[]? ExistingPullRequests { get; init; } = null; public object[]? ExistingGroupPullRequests { get; init; } = null; public Dictionary? Experiments { get; init; } = null; - public object[]? IgnoreConditions { get; init; } = null; + public Condition[] IgnoreConditions { get; init; } = []; public bool LockfileOnly { get; init; } = false; public string? RequirementsUpdateStrategy { get; init; } = null; public object[]? SecurityAdvisories { get; init; } = null; diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/RunWorker.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/RunWorker.cs index f0e7561581..588768bd92 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/RunWorker.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/RunWorker.cs @@ -1,3 +1,4 @@ +using System.Collections.Immutable; using System.Net; using System.Text; using System.Text.Json; @@ -21,7 +22,7 @@ public class RunWorker { PropertyNamingPolicy = JsonNamingPolicy.KebabCaseLower, WriteIndented = true, - Converters = { new JsonStringEnumConverter() }, + Converters = { new JsonStringEnumConverter(), new RequirementConverter() }, }; public RunWorker(IApiHandler apiHandler, IDiscoveryWorker discoverWorker, IAnalyzeWorker analyzeWorker, IUpdaterWorker updateWorker, ILogger logger) @@ -173,12 +174,13 @@ async Task TrackOriginalContentsAsync(string directory, string fileName) continue; } + var ignoredVersions = GetIgnoredRequirementsForDependency(job, dependency.Name); var dependencyInfo = new DependencyInfo() { Name = dependency.Name, Version = dependency.Version!, IsVulnerable = false, - IgnoredVersions = [], + IgnoredVersions = ignoredVersions, Vulnerabilities = [], }; var analysisResult = await _analyzeWorker.RunAsync(repoContentsPath.FullName, discoveryResult, dependencyInfo); @@ -303,6 +305,25 @@ async Task AddUpdatedFileIfDifferentAsync(string directory, string fileName) return result; } + internal static ImmutableArray GetIgnoredRequirementsForDependency(Job job, string dependencyName) + { + var ignoreConditions = job.IgnoreConditions + .Where(c => c.DependencyName.Equals(dependencyName, StringComparison.OrdinalIgnoreCase)) + .ToArray(); + if (ignoreConditions.Length == 1 && ignoreConditions[0].VersionRequirement is null) + { + // if only one match with no version requirement, ignore all versions + return [Requirement.Parse("> 0.0.0")]; + } + + var ignoredVersions = ignoreConditions + .Select(c => c.VersionRequirement) + .Where(r => r is not null) + .Cast() + .ToImmutableArray(); + return ignoredVersions; + } + internal static UpdatedDependencyList GetUpdatedDependencyListFromDiscovery(WorkspaceDiscoveryResult discoveryResult, string pathToContents) { string GetFullRepoPath(string path)