From 457a37fc7389661d4475ed03261a43923f99926e Mon Sep 17 00:00:00 2001 From: "Brett V. Forsgren" Date: Tue, 28 Jan 2025 16:35:43 -0700 Subject: [PATCH] both job and dependency are considered for update eligibility --- .../Run/RunWorkerTests.cs | 9 + .../Run/UpdateAllowedTests.cs | 286 ++++++++++++++++++ .../NuGetUpdater.Core/Run/RunWorker.cs | 286 ++++++++++-------- 3 files changed, 459 insertions(+), 122 deletions(-) create mode 100644 nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/UpdateAllowedTests.cs diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/RunWorkerTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/RunWorkerTests.cs index 4419aca39e..c2704e39e6 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/RunWorkerTests.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/RunWorkerTests.cs @@ -1729,6 +1729,15 @@ await RunAsync( ], job: new Job() { + AllowedUpdates = [new() { UpdateType = UpdateType.Security }], + SecurityAdvisories = + [ + new() + { + DependencyName = "Some.Package", + AffectedVersions = [Requirement.Parse("= 1.0.0")] + } + ], Source = new() { Provider = "github", diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/UpdateAllowedTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/UpdateAllowedTests.cs new file mode 100644 index 0000000000..14a96e42cd --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/UpdateAllowedTests.cs @@ -0,0 +1,286 @@ +using System.Collections.Immutable; + +using NuGetUpdater.Core.Analyze; +using NuGetUpdater.Core.Run; +using NuGetUpdater.Core.Run.ApiModel; + +using Xunit; + +using DepType = NuGetUpdater.Core.Run.ApiModel.DependencyType; + +namespace NuGetUpdater.Core.Test.Run; + +public class UpdateAllowedTests +{ + [Theory] + [MemberData(nameof(IsUpdateAllowedTestData))] + public void IsUpdateAllowed(Job job, Dependency dependency, bool expectedResult) + { + var actualResult = RunWorker.IsUpdateAllowed(job, dependency); + Assert.Equal(expectedResult, actualResult); + } + + public static IEnumerable IsUpdateAllowedTestData() + { + // with default allowed updates on a transitive dependency + yield return + [ + CreateJob( + allowedUpdates: [ + new AllowedUpdate() { DependencyType = DepType.Direct, UpdateType = UpdateType.All } + ], + securityAdvisories: [ + new Advisory() { DependencyName = "Some.Package", AffectedVersions = [], PatchedVersions = [Requirement.Parse(">= 1.11.0")], UnaffectedVersions = [] } + ], + securityUpdatesOnly: false), + new Dependency("Some.Package", "1.8.0", DependencyType.PackageReference, IsTransitive: true), + // expectedResult + false, + ]; + + // when dealing with a security update + yield return + [ + CreateJob( + allowedUpdates: [ + new AllowedUpdate() { DependencyType = DepType.Direct, UpdateType = UpdateType.All } + ], + securityAdvisories: [ + new Advisory() { DependencyName = "Some.Package", AffectedVersions = [], PatchedVersions = [Requirement.Parse(">= 1.11.0")], UnaffectedVersions = [] } + ], + securityUpdatesOnly: true), + new Dependency("Some.Package", "1.8.0", DependencyType.PackageReference, IsTransitive: true), + // expectedResult + true, + ]; + + // with a top-level dependency + yield return + [ + CreateJob( + allowedUpdates: [ + new AllowedUpdate() { DependencyType = DepType.Direct, UpdateType = UpdateType.All }, + new AllowedUpdate() { DependencyType = DepType.Indirect, UpdateType = UpdateType.Security } + ], + securityAdvisories: [], + securityUpdatesOnly: false), + new Dependency("Some.Package", "1.8.0", DependencyType.PackageReference, IsTransitive: false), + // expectedResult + true, + ]; + + // with a sub-dependency + yield return + [ + CreateJob( + allowedUpdates: [ + new AllowedUpdate() { DependencyType = DepType.Direct, UpdateType = UpdateType.All }, + new AllowedUpdate() { DependencyType = DepType.Indirect, UpdateType = UpdateType.Security } + ], + securityAdvisories: [], + securityUpdatesOnly: false), + new Dependency("Some.Package", "1.8.0", DependencyType.PackageReference, IsTransitive: true), + // expectedResult + false, + ]; + + // when insecure + yield return + [ + CreateJob( + allowedUpdates: [ + new AllowedUpdate() { DependencyType = DepType.Direct, UpdateType = UpdateType.All }, + new AllowedUpdate() { DependencyType = DepType.Indirect, UpdateType = UpdateType.Security } + ], + securityAdvisories: [ + new Advisory() { DependencyName = "Some.Package", AffectedVersions = [], PatchedVersions = [Requirement.Parse(">= 1.11.0")], UnaffectedVersions = [] } + ], + securityUpdatesOnly: false), + new Dependency("Some.Package", "1.8.0", DependencyType.PackageReference, IsTransitive: true), + // expectedResult + true, + ]; + + // when only security fixes are allowed + yield return + [ + CreateJob( + allowedUpdates: [ + new AllowedUpdate() { DependencyType = DepType.Direct, UpdateType = UpdateType.All }, + new AllowedUpdate() { DependencyType = DepType.Indirect, UpdateType = UpdateType.Security } + ], + securityAdvisories: [], + securityUpdatesOnly: true), + new Dependency("Some.Package", "1.8.0", DependencyType.PackageReference, IsTransitive: false), + // expectedResult + false, + ]; + + // when dealing with a security fix + yield return + [ + CreateJob( + allowedUpdates: [ + new AllowedUpdate() { DependencyType = DepType.Direct, UpdateType = UpdateType.All }, + new AllowedUpdate() { DependencyType = DepType.Indirect, UpdateType = UpdateType.Security } + ], + securityAdvisories: [ + new Advisory() { DependencyName = "Some.Package", AffectedVersions = [], PatchedVersions = [Requirement.Parse(">= 1.11.0")], UnaffectedVersions = [] } + ], + securityUpdatesOnly: true), + new Dependency("Some.Package", "1.8.0", DependencyType.PackageReference, IsTransitive: false), + // expectedResult + true, + ]; + + // when dealing with a security fix that doesn't apply + yield return + [ + CreateJob( + allowedUpdates: [ + new AllowedUpdate() { DependencyType = DepType.Direct, UpdateType = UpdateType.All }, + new AllowedUpdate() { DependencyType = DepType.Indirect, UpdateType = UpdateType.Security } + ], + securityAdvisories: [ + new Advisory() { DependencyName = "Some.Package", AffectedVersions = [Requirement.Parse("> 1.8.0")], PatchedVersions = [], UnaffectedVersions = [] } + ], + securityUpdatesOnly: true), + new Dependency("Some.Package", "1.8.0", DependencyType.PackageReference, IsTransitive: false), + // expectedResult + false, + ]; + + // when dealing with a security fix that doesn't apply to some versions + yield return + [ + CreateJob( + allowedUpdates: [ + new AllowedUpdate() { DependencyType = DepType.Direct, UpdateType = UpdateType.All }, + new AllowedUpdate() { DependencyType = DepType.Indirect, UpdateType = UpdateType.Security } + ], + securityAdvisories: [ + new Advisory() { DependencyName = "Some.Package", AffectedVersions = [Requirement.Parse("< 1.8.0"), Requirement.Parse("> 1.8.0")], PatchedVersions = [], UnaffectedVersions = [] } + ], + securityUpdatesOnly: true), + new Dependency("Some.Package", "1.8.1", DependencyType.PackageReference, IsTransitive: false), + // expectedResult + true, + ]; + + // when a dependency allow list that includes the dependency + yield return + [ + CreateJob( + allowedUpdates: [ + new AllowedUpdate() { DependencyName = "Some.Package" } + ], + securityAdvisories: [], + securityUpdatesOnly: false), + new Dependency("Some.Package", "1.8.0", DependencyType.PackageReference, IsTransitive: false), + // expectedResult + true, + ]; + + // with a dependency allow list that uses a wildcard + yield return + [ + CreateJob( + allowedUpdates: [ + new AllowedUpdate() { DependencyName = "Some.*" } + ], + securityAdvisories: [], + securityUpdatesOnly: false), + new Dependency("Some.Package", "1.8.0", DependencyType.PackageReference, IsTransitive: false), + // expectedResult + true, + ]; + + // when dependency allow list that excludes the dependency + yield return + [ + CreateJob( + allowedUpdates: [ + new AllowedUpdate() { DependencyName = "Unrelated.Package" } + ], + securityAdvisories: [], + securityUpdatesOnly: false), + new Dependency("Some.Package", "1.8.0", DependencyType.PackageReference, IsTransitive: false), + // expectedResult + false, + ]; + + // when matching with an incomplete dependency name + yield return + [ + CreateJob( + allowedUpdates: [ + new AllowedUpdate() { DependencyName = "Some" } + ], + securityAdvisories: [], + securityUpdatesOnly: false), + new Dependency("Some.Package", "1.8.0", DependencyType.PackageReference, IsTransitive: false), + // expectedResult + false, + ]; + + // with a dependency allow list that uses a wildcard + yield return + [ + CreateJob( + allowedUpdates: [ + new AllowedUpdate() { DependencyName = "Unrelated.*" } + ], + securityAdvisories: [], + securityUpdatesOnly: false), + new Dependency("Some.Package", "1.8.0", DependencyType.PackageReference, IsTransitive: false), + // expectedResult + false, + ]; + + // when security fixes are also allowed + yield return + [ + CreateJob( + allowedUpdates: [ + new AllowedUpdate() { DependencyName = "Unrelated.Package" }, + new AllowedUpdate() { UpdateType = UpdateType.Security } + ], + securityAdvisories: [], + securityUpdatesOnly: false), + new Dependency("Some.Package", "1.8.0", DependencyType.PackageReference, IsTransitive: false), + // expectedResult + false, + ]; + + // when dealing with a security fix + yield return + [ + CreateJob( + allowedUpdates: [ + new AllowedUpdate() { DependencyName = "Unrelated.Package"}, new AllowedUpdate(){ UpdateType = UpdateType.Security } + ], + securityAdvisories: [ + new Advisory() { DependencyName = "Some.Package", AffectedVersions = [], PatchedVersions = [Requirement.Parse(">= 1.11.0")], UnaffectedVersions = [] } + ], + securityUpdatesOnly: false), + new Dependency("Some.Package", "1.8.0", DependencyType.PackageReference, IsTransitive: false), + // expectedResult + true, + ]; + } + + private static Job CreateJob(AllowedUpdate[] allowedUpdates, Advisory[] securityAdvisories, bool securityUpdatesOnly) + { + return new Job() + { + AllowedUpdates = allowedUpdates.ToImmutableArray(), + SecurityAdvisories = securityAdvisories.ToImmutableArray(), + SecurityUpdatesOnly = securityUpdatesOnly, + Source = new() + { + Provider = "nuget", + Repo = "test/repo", + } + }; + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/RunWorker.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/RunWorker.cs index e24912c30b..a6394858a5 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/RunWorker.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/RunWorker.cs @@ -4,6 +4,8 @@ using System.Text.Json; using System.Text.Json.Serialization; +using Microsoft.Extensions.FileSystemGlobbing; + using NuGet.Versioning; using NuGetUpdater.Core.Analyze; @@ -115,161 +117,145 @@ private async Task RunForDirectory(Job job, DirectoryInfo repoContent await _apiHandler.UpdateDependencyList(discoveredUpdatedDependencies); // TODO: pull out relevant dependencies, then check each for updates and track the changes - // TODO: for each top-level dependency, _or_ specific dependency (if security, use transitive) var originalDependencyFileContents = new Dictionary(); var actualUpdatedDependencies = new List(); - if (job.AllowedUpdates.Any(a => a.UpdateType == UpdateType.All)) + await _apiHandler.IncrementMetric(new() { - await _apiHandler.IncrementMetric(new() - { - Metric = "updater.started", - Tags = { ["operation"] = "group_update_all_versions" }, - }); + Metric = "updater.started", + Tags = { ["operation"] = "group_update_all_versions" }, + }); + + // track original contents for later handling + async Task TrackOriginalContentsAsync(string directory, string fileName) + { + var repoFullPath = Path.Join(directory, fileName).FullyNormalizedRootedPath(); + var localFullPath = Path.Join(repoContentsPath.FullName, repoFullPath); + var content = await File.ReadAllTextAsync(localFullPath); + originalDependencyFileContents[repoFullPath] = content; + } - // track original contents for later handling - async Task TrackOriginalContentsAsync(string directory, string fileName) + foreach (var project in discoveryResult.Projects) + { + var projectDirectory = Path.GetDirectoryName(project.FilePath); + await TrackOriginalContentsAsync(discoveryResult.Path, project.FilePath); + foreach (var extraFile in project.ImportedFiles.Concat(project.AdditionalFiles)) { - var repoFullPath = Path.Join(directory, fileName).FullyNormalizedRootedPath(); - var localFullPath = Path.Join(repoContentsPath.FullName, repoFullPath); - var content = await File.ReadAllTextAsync(localFullPath); - originalDependencyFileContents[repoFullPath] = content; + var extraFilePath = Path.Join(projectDirectory, extraFile); + await TrackOriginalContentsAsync(discoveryResult.Path, extraFilePath); } + // TODO: include global.json, etc. + } - foreach (var project in discoveryResult.Projects) + // do update + _logger.Info($"Running update in directory {repoDirectory}"); + foreach (var project in discoveryResult.Projects) + { + foreach (var dependency in project.Dependencies) { - var projectDirectory = Path.GetDirectoryName(project.FilePath); - await TrackOriginalContentsAsync(discoveryResult.Path, project.FilePath); - foreach (var extraFile in project.ImportedFiles.Concat(project.AdditionalFiles)) + if (!IsUpdateAllowed(job, dependency)) { - var extraFilePath = Path.Join(projectDirectory, extraFile); - await TrackOriginalContentsAsync(discoveryResult.Path, extraFilePath); + continue; } - // TODO: include global.json, etc. - } - // do update - _logger.Info($"Running update in directory {repoDirectory}"); - foreach (var project in discoveryResult.Projects) - { - foreach (var dependency in project.Dependencies) + var dependencyInfo = GetDependencyInfo(job, dependency); + var analysisResult = await _analyzeWorker.RunAsync(repoContentsPath.FullName, discoveryResult, dependencyInfo); + // TODO: log analysisResult + if (analysisResult.CanUpdate) { - if (dependency.Name == "Microsoft.NET.Sdk") - { - // this can't be updated - // TODO: pull this out of discovery? - continue; - } - - if (dependency.Version is null) - { - // if we don't know the version, there's nothing we can do - continue; - } + var dependencyLocation = Path.Join(discoveryResult.Path, project.FilePath).FullyNormalizedRootedPath(); - var dependencyInfo = GetDependencyInfo(job, dependency); - var analysisResult = await _analyzeWorker.RunAsync(repoContentsPath.FullName, discoveryResult, dependencyInfo); - // TODO: log analysisResult - if (analysisResult.CanUpdate) + // TODO: this is inefficient, but not likely causing a bottleneck + var previousDependency = discoveredUpdatedDependencies.Dependencies + .Single(d => d.Name == dependency.Name && d.Requirements.Single().File == dependencyLocation); + var updatedDependency = new ReportedDependency() { - var dependencyLocation = Path.Join(discoveryResult.Path, project.FilePath).FullyNormalizedRootedPath(); - - // TODO: this is inefficient, but not likely causing a bottleneck - var previousDependency = discoveredUpdatedDependencies.Dependencies - .Single(d => d.Name == dependency.Name && d.Requirements.Single().File == dependencyLocation); - var updatedDependency = new ReportedDependency() - { - Name = dependency.Name, - Version = analysisResult.UpdatedVersion, - Requirements = - [ - new ReportedRequirement() - { - File = dependencyLocation, - Requirement = analysisResult.UpdatedVersion, - Groups = previousDependency.Requirements.Single().Groups, - Source = new RequirementSource() - { - SourceUrl = analysisResult.UpdatedDependencies.FirstOrDefault(d => d.Name == dependency.Name)?.InfoUrl, - }, - } - ], - PreviousVersion = dependency.Version, - PreviousRequirements = previousDependency.Requirements, - }; - - var dependencyFilePath = Path.Join(discoveryResult.Path, project.FilePath).FullyNormalizedRootedPath(); - var updateResult = await _updaterWorker.RunAsync(repoContentsPath.FullName, dependencyFilePath, dependency.Name, dependency.Version!, analysisResult.UpdatedVersion, isTransitive: dependency.IsTransitive); - // TODO: need to report if anything was actually updated - if (updateResult.Error is null) - { - if (dependencyLocation != dependencyFilePath) + Name = dependency.Name, + Version = analysisResult.UpdatedVersion, + Requirements = + [ + new ReportedRequirement() { - updatedDependency.Requirements.All(r => r.File == dependencyFilePath); + File = dependencyLocation, + Requirement = analysisResult.UpdatedVersion, + Groups = previousDependency.Requirements.Single().Groups, + Source = new RequirementSource() + { + SourceUrl = analysisResult.UpdatedDependencies.FirstOrDefault(d => d.Name == dependency.Name)?.InfoUrl, + }, } + ], + PreviousVersion = dependency.Version, + PreviousRequirements = previousDependency.Requirements, + }; - actualUpdatedDependencies.Add(updatedDependency); + var dependencyFilePath = Path.Join(discoveryResult.Path, project.FilePath).FullyNormalizedRootedPath(); + var updateResult = await _updaterWorker.RunAsync(repoContentsPath.FullName, dependencyFilePath, dependency.Name, dependency.Version!, analysisResult.UpdatedVersion, isTransitive: dependency.IsTransitive); + // TODO: need to report if anything was actually updated + if (updateResult.Error is null) + { + if (dependencyLocation != dependencyFilePath) + { + updatedDependency.Requirements.All(r => r.File == dependencyFilePath); } + + actualUpdatedDependencies.Add(updatedDependency); } } } + } - // create PR - we need to manually check file contents; we can't easily use `git status` in tests - var updatedDependencyFiles = new Dictionary(); - async Task AddUpdatedFileIfDifferentAsync(string directory, string fileName) + // create PR - we need to manually check file contents; we can't easily use `git status` in tests + var updatedDependencyFiles = new Dictionary(); + async Task AddUpdatedFileIfDifferentAsync(string directory, string fileName) + { + var repoFullPath = Path.Join(directory, fileName).FullyNormalizedRootedPath(); + var localFullPath = Path.GetFullPath(Path.Join(repoContentsPath.FullName, repoFullPath)); + var originalContent = originalDependencyFileContents[repoFullPath]; + var updatedContent = await File.ReadAllTextAsync(localFullPath); + if (updatedContent != originalContent) { - var repoFullPath = Path.Join(directory, fileName).FullyNormalizedRootedPath(); - var localFullPath = Path.GetFullPath(Path.Join(repoContentsPath.FullName, repoFullPath)); - var originalContent = originalDependencyFileContents[repoFullPath]; - var updatedContent = await File.ReadAllTextAsync(localFullPath); - if (updatedContent != originalContent) + updatedDependencyFiles[localFullPath] = new DependencyFile() { - updatedDependencyFiles[localFullPath] = new DependencyFile() - { - Name = Path.GetFileName(repoFullPath), - Directory = Path.GetDirectoryName(repoFullPath)!.NormalizePathToUnix(), - Content = updatedContent, - }; - } + Name = Path.GetFileName(repoFullPath), + Directory = Path.GetDirectoryName(repoFullPath)!.NormalizePathToUnix(), + Content = updatedContent, + }; } + } - foreach (var project in discoveryResult.Projects) + foreach (var project in discoveryResult.Projects) + { + await AddUpdatedFileIfDifferentAsync(discoveryResult.Path, project.FilePath); + var projectDirectory = Path.GetDirectoryName(project.FilePath); + foreach (var extraFile in project.ImportedFiles.Concat(project.AdditionalFiles)) { - await AddUpdatedFileIfDifferentAsync(discoveryResult.Path, project.FilePath); - var projectDirectory = Path.GetDirectoryName(project.FilePath); - foreach (var extraFile in project.ImportedFiles.Concat(project.AdditionalFiles)) - { - var extraFilePath = Path.Join(projectDirectory, extraFile); - await AddUpdatedFileIfDifferentAsync(discoveryResult.Path, extraFilePath); - } - // TODO: handle global.json, etc. + var extraFilePath = Path.Join(projectDirectory, extraFile); + await AddUpdatedFileIfDifferentAsync(discoveryResult.Path, extraFilePath); } + // TODO: handle global.json, etc. + } - if (updatedDependencyFiles.Count > 0) - { - var updatedDependencyFileList = updatedDependencyFiles - .OrderBy(kvp => kvp.Key) - .Select(kvp => kvp.Value) - .ToArray(); - var createPullRequest = new CreatePullRequest() - { - Dependencies = actualUpdatedDependencies.ToArray(), - UpdatedDependencyFiles = updatedDependencyFileList, - BaseCommitSha = baseCommitSha, - CommitMessage = "TODO: message", - PrTitle = "TODO: title", - PrBody = "TODO: body", - }; - await _apiHandler.CreatePullRequest(createPullRequest); - // TODO: log updated dependencies to console - } - else + if (updatedDependencyFiles.Count > 0) + { + var updatedDependencyFileList = updatedDependencyFiles + .OrderBy(kvp => kvp.Key) + .Select(kvp => kvp.Value) + .ToArray(); + var createPullRequest = new CreatePullRequest() { - // TODO: log or throw if nothing was updated, but was expected to be - } + Dependencies = actualUpdatedDependencies.ToArray(), + UpdatedDependencyFiles = updatedDependencyFileList, + BaseCommitSha = baseCommitSha, + CommitMessage = "TODO: message", + PrTitle = "TODO: title", + PrBody = "TODO: body", + }; + await _apiHandler.CreatePullRequest(createPullRequest); + // TODO: log updated dependencies to console } else { - // TODO: throw if no updates performed + // TODO: log or throw if nothing was updated, but was expected to be } var result = new RunResult() @@ -289,6 +275,62 @@ async Task AddUpdatedFileIfDifferentAsync(string directory, string fileName) return result; } + internal static bool IsUpdateAllowed(Job job, Dependency dependency) + { + if (dependency.Name.Equals("Microsoft.NET.Sdk", StringComparison.OrdinalIgnoreCase)) + { + // this can't be updated + // TODO: pull this out of discovery? + return false; + } + + if (dependency.Version is null) + { + // if we don't know the version, there's nothing we can do + // TODO: pull this out of discovery? + return false; + } + + var version = NuGetVersion.Parse(dependency.Version); + var dependencyInfo = GetDependencyInfo(job, dependency); + var isVulnerable = dependencyInfo.Vulnerabilities.Any(v => v.IsVulnerable(version)); + var allowed = job.AllowedUpdates.Any(allowedUpdate => + { + // check name restriction, if any + if (allowedUpdate.DependencyName is not null) + { + var matcher = new Matcher(StringComparison.OrdinalIgnoreCase) + .AddInclude(allowedUpdate.DependencyName); + var result = matcher.Match(dependency.Name); + if (!result.HasMatches) + { + return false; + } + } + + var isSecurityUpdate = allowedUpdate.UpdateType == UpdateType.Security || job.SecurityUpdatesOnly; + if (isSecurityUpdate) + { + // only update if it's vulnerable + return isVulnerable; + } + else + { + // not a security update, so only update if... + // ...we've been explicitly asked to update this + if ((job.Dependencies ?? []).Any(d => d.Equals(dependency.Name, StringComparison.OrdinalIgnoreCase))) + { + return true; + } + + // ...no specific update being performed, do it if it's not transitive + return !dependency.IsTransitive; + } + }); + + return allowed; + } + internal static ImmutableArray GetIgnoredRequirementsForDependency(Job job, string dependencyName) { var ignoreConditions = job.IgnoreConditions