dependabot-nuget 0.294.0 → 0.295.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 97e76c69b2fed1672ebe1e9b3b890e8f445139f26729895c72a196d49cd43d63
4
- data.tar.gz: 10ef8cb4d30c84e9e8f51fdc5bf4f529b451936ad8a34e905e69fbaa0b813cac
3
+ metadata.gz: 8ac3a95f5211adff79f8e7954308a4d3656b63c8be20b70a7b6f129f04402881
4
+ data.tar.gz: a79771c0cc6c0a08bfaa2a7cef8253902c7e8b92b868568c231064098ba77a1c
5
5
  SHA512:
6
- metadata.gz: 73cdefd4ef762621a7142a040a747011ee4a390635379ee311deccaf6664062834a8e809b6203816e81f8ebb24ed10f68d04975e9bcde40e5ffd2d3720779c68
7
- data.tar.gz: a8cde74fdb1b400ced101cac7132bb32c8dc6b3d36ff6c249908e33e9e8c719e63999c02982ba2bf7704b18007be0daac14dcdd6c39ba74c4a8c38ccad2ae11e
6
+ metadata.gz: 800097bc4856927e983da508d16b4e6d0fd75cf40b5ca5ed25901fbb467f0f99e8f8dc724341c0da6cc6126303c6437a5bd8c7521132c3363ed752181402ee1b
7
+ data.tar.gz: c0a1675e7cdf06ec0314d58092de09d633fa46892ae8f01c20ec74553d756265fda431434babb6eb140184b3380b78576f6309c1ec010f41e0e09e6ad6b28adb
@@ -262,7 +262,8 @@ public partial class DiscoveryWorker : IDiscoveryWorker
262
262
  }
263
263
  }
264
264
 
265
- return expandedProjects.ToImmutableArray();
265
+ var result = expandedProjects.OrderBy(p => p).ToImmutableArray();
266
+ return result;
266
267
  }
267
268
 
268
269
  private static IEnumerable<string> ExpandItemGroupFilesFromProject(string projectPath, params string[] itemTypes)
@@ -114,7 +114,7 @@ internal static class SdkProjectDiscovery
114
114
  var (exitCode, stdOut, stdErr) = await ProcessEx.RunDotnetWithoutMSBuildEnvironmentVariablesAsync(args, startingProjectDirectory, experimentsManager);
115
115
  return (exitCode, stdOut, stdErr);
116
116
  }, logger, retainMSBuildSdks: true);
117
- MSBuildHelper.ThrowOnUnauthenticatedFeed(stdOut);
117
+ MSBuildHelper.ThrowOnError(stdOut);
118
118
  if (stdOut.Contains("""error MSB4057: The target "GenerateBuildDependencyFile" does not exist in the project."""))
119
119
  {
120
120
  // this can happen if it's a non-SDK-style project; totally normal, not worth examining the binlog
@@ -4,6 +4,8 @@ using System.Text;
4
4
  using System.Text.Json;
5
5
  using System.Text.Json.Serialization;
6
6
 
7
+ using Microsoft.Extensions.FileSystemGlobbing;
8
+
7
9
  using NuGet.Versioning;
8
10
 
9
11
  using NuGetUpdater.Core.Analyze;
@@ -115,161 +117,145 @@ public class RunWorker
115
117
  await _apiHandler.UpdateDependencyList(discoveredUpdatedDependencies);
116
118
 
117
119
  // TODO: pull out relevant dependencies, then check each for updates and track the changes
118
- // TODO: for each top-level dependency, _or_ specific dependency (if security, use transitive)
119
120
  var originalDependencyFileContents = new Dictionary<string, string>();
120
121
  var actualUpdatedDependencies = new List<ReportedDependency>();
121
- if (job.AllowedUpdates.Any(a => a.UpdateType == UpdateType.All))
122
+ await _apiHandler.IncrementMetric(new()
122
123
  {
123
- await _apiHandler.IncrementMetric(new()
124
- {
125
- Metric = "updater.started",
126
- Tags = { ["operation"] = "group_update_all_versions" },
127
- });
124
+ Metric = "updater.started",
125
+ Tags = { ["operation"] = "group_update_all_versions" },
126
+ });
127
+
128
+ // track original contents for later handling
129
+ async Task TrackOriginalContentsAsync(string directory, string fileName)
130
+ {
131
+ var repoFullPath = Path.Join(directory, fileName).FullyNormalizedRootedPath();
132
+ var localFullPath = Path.Join(repoContentsPath.FullName, repoFullPath);
133
+ var content = await File.ReadAllTextAsync(localFullPath);
134
+ originalDependencyFileContents[repoFullPath] = content;
135
+ }
128
136
 
129
- // track original contents for later handling
130
- async Task TrackOriginalContentsAsync(string directory, string fileName)
137
+ foreach (var project in discoveryResult.Projects)
138
+ {
139
+ var projectDirectory = Path.GetDirectoryName(project.FilePath);
140
+ await TrackOriginalContentsAsync(discoveryResult.Path, project.FilePath);
141
+ foreach (var extraFile in project.ImportedFiles.Concat(project.AdditionalFiles))
131
142
  {
132
- var repoFullPath = Path.Join(directory, fileName).FullyNormalizedRootedPath();
133
- var localFullPath = Path.Join(repoContentsPath.FullName, repoFullPath);
134
- var content = await File.ReadAllTextAsync(localFullPath);
135
- originalDependencyFileContents[repoFullPath] = content;
143
+ var extraFilePath = Path.Join(projectDirectory, extraFile);
144
+ await TrackOriginalContentsAsync(discoveryResult.Path, extraFilePath);
136
145
  }
146
+ // TODO: include global.json, etc.
147
+ }
137
148
 
138
- foreach (var project in discoveryResult.Projects)
149
+ // do update
150
+ _logger.Info($"Running update in directory {repoDirectory}");
151
+ foreach (var project in discoveryResult.Projects)
152
+ {
153
+ foreach (var dependency in project.Dependencies)
139
154
  {
140
- var projectDirectory = Path.GetDirectoryName(project.FilePath);
141
- await TrackOriginalContentsAsync(discoveryResult.Path, project.FilePath);
142
- foreach (var extraFile in project.ImportedFiles.Concat(project.AdditionalFiles))
155
+ if (!IsUpdateAllowed(job, dependency))
143
156
  {
144
- var extraFilePath = Path.Join(projectDirectory, extraFile);
145
- await TrackOriginalContentsAsync(discoveryResult.Path, extraFilePath);
157
+ continue;
146
158
  }
147
- // TODO: include global.json, etc.
148
- }
149
159
 
150
- // do update
151
- _logger.Info($"Running update in directory {repoDirectory}");
152
- foreach (var project in discoveryResult.Projects)
153
- {
154
- foreach (var dependency in project.Dependencies.Where(d => !d.IsTransitive))
160
+ var dependencyInfo = GetDependencyInfo(job, dependency);
161
+ var analysisResult = await _analyzeWorker.RunAsync(repoContentsPath.FullName, discoveryResult, dependencyInfo);
162
+ // TODO: log analysisResult
163
+ if (analysisResult.CanUpdate)
155
164
  {
156
- if (dependency.Name == "Microsoft.NET.Sdk")
157
- {
158
- // this can't be updated
159
- // TODO: pull this out of discovery?
160
- continue;
161
- }
162
-
163
- if (dependency.Version is null)
164
- {
165
- // if we don't know the version, there's nothing we can do
166
- continue;
167
- }
165
+ var dependencyLocation = Path.Join(discoveryResult.Path, project.FilePath).FullyNormalizedRootedPath();
168
166
 
169
- var dependencyInfo = GetDependencyInfo(job, dependency);
170
- var analysisResult = await _analyzeWorker.RunAsync(repoContentsPath.FullName, discoveryResult, dependencyInfo);
171
- // TODO: log analysisResult
172
- if (analysisResult.CanUpdate)
167
+ // TODO: this is inefficient, but not likely causing a bottleneck
168
+ var previousDependency = discoveredUpdatedDependencies.Dependencies
169
+ .Single(d => d.Name == dependency.Name && d.Requirements.Single().File == dependencyLocation);
170
+ var updatedDependency = new ReportedDependency()
173
171
  {
174
- var dependencyLocation = Path.Join(discoveryResult.Path, project.FilePath).FullyNormalizedRootedPath();
175
-
176
- // TODO: this is inefficient, but not likely causing a bottleneck
177
- var previousDependency = discoveredUpdatedDependencies.Dependencies
178
- .Single(d => d.Name == dependency.Name && d.Requirements.Single().File == dependencyLocation);
179
- var updatedDependency = new ReportedDependency()
180
- {
181
- Name = dependency.Name,
182
- Version = analysisResult.UpdatedVersion,
183
- Requirements =
184
- [
185
- new ReportedRequirement()
186
- {
187
- File = dependencyLocation,
188
- Requirement = analysisResult.UpdatedVersion,
189
- Groups = previousDependency.Requirements.Single().Groups,
190
- Source = new RequirementSource()
191
- {
192
- SourceUrl = analysisResult.UpdatedDependencies.FirstOrDefault(d => d.Name == dependency.Name)?.InfoUrl,
193
- },
194
- }
195
- ],
196
- PreviousVersion = dependency.Version,
197
- PreviousRequirements = previousDependency.Requirements,
198
- };
199
-
200
- var dependencyFilePath = Path.Join(discoveryResult.Path, project.FilePath).FullyNormalizedRootedPath();
201
- var updateResult = await _updaterWorker.RunAsync(repoContentsPath.FullName, dependencyFilePath, dependency.Name, dependency.Version!, analysisResult.UpdatedVersion, isTransitive: false);
202
- // TODO: need to report if anything was actually updated
203
- if (updateResult.Error is null)
204
- {
205
- if (dependencyLocation != dependencyFilePath)
172
+ Name = dependency.Name,
173
+ Version = analysisResult.UpdatedVersion,
174
+ Requirements =
175
+ [
176
+ new ReportedRequirement()
206
177
  {
207
- updatedDependency.Requirements.All(r => r.File == dependencyFilePath);
178
+ File = dependencyLocation,
179
+ Requirement = analysisResult.UpdatedVersion,
180
+ Groups = previousDependency.Requirements.Single().Groups,
181
+ Source = new RequirementSource()
182
+ {
183
+ SourceUrl = analysisResult.UpdatedDependencies.FirstOrDefault(d => d.Name == dependency.Name)?.InfoUrl,
184
+ },
208
185
  }
186
+ ],
187
+ PreviousVersion = dependency.Version,
188
+ PreviousRequirements = previousDependency.Requirements,
189
+ };
209
190
 
210
- actualUpdatedDependencies.Add(updatedDependency);
191
+ var dependencyFilePath = Path.Join(discoveryResult.Path, project.FilePath).FullyNormalizedRootedPath();
192
+ var updateResult = await _updaterWorker.RunAsync(repoContentsPath.FullName, dependencyFilePath, dependency.Name, dependency.Version!, analysisResult.UpdatedVersion, isTransitive: dependency.IsTransitive);
193
+ // TODO: need to report if anything was actually updated
194
+ if (updateResult.Error is null)
195
+ {
196
+ if (dependencyLocation != dependencyFilePath)
197
+ {
198
+ updatedDependency.Requirements.All(r => r.File == dependencyFilePath);
211
199
  }
200
+
201
+ actualUpdatedDependencies.Add(updatedDependency);
212
202
  }
213
203
  }
214
204
  }
205
+ }
215
206
 
216
- // create PR - we need to manually check file contents; we can't easily use `git status` in tests
217
- var updatedDependencyFiles = new Dictionary<string, DependencyFile>();
218
- async Task AddUpdatedFileIfDifferentAsync(string directory, string fileName)
207
+ // create PR - we need to manually check file contents; we can't easily use `git status` in tests
208
+ var updatedDependencyFiles = new Dictionary<string, DependencyFile>();
209
+ async Task AddUpdatedFileIfDifferentAsync(string directory, string fileName)
210
+ {
211
+ var repoFullPath = Path.Join(directory, fileName).FullyNormalizedRootedPath();
212
+ var localFullPath = Path.GetFullPath(Path.Join(repoContentsPath.FullName, repoFullPath));
213
+ var originalContent = originalDependencyFileContents[repoFullPath];
214
+ var updatedContent = await File.ReadAllTextAsync(localFullPath);
215
+ if (updatedContent != originalContent)
219
216
  {
220
- var repoFullPath = Path.Join(directory, fileName).FullyNormalizedRootedPath();
221
- var localFullPath = Path.GetFullPath(Path.Join(repoContentsPath.FullName, repoFullPath));
222
- var originalContent = originalDependencyFileContents[repoFullPath];
223
- var updatedContent = await File.ReadAllTextAsync(localFullPath);
224
- if (updatedContent != originalContent)
217
+ updatedDependencyFiles[localFullPath] = new DependencyFile()
225
218
  {
226
- updatedDependencyFiles[localFullPath] = new DependencyFile()
227
- {
228
- Name = Path.GetFileName(repoFullPath),
229
- Directory = Path.GetDirectoryName(repoFullPath)!.NormalizePathToUnix(),
230
- Content = updatedContent,
231
- };
232
- }
219
+ Name = Path.GetFileName(repoFullPath),
220
+ Directory = Path.GetDirectoryName(repoFullPath)!.NormalizePathToUnix(),
221
+ Content = updatedContent,
222
+ };
233
223
  }
224
+ }
234
225
 
235
- foreach (var project in discoveryResult.Projects)
226
+ foreach (var project in discoveryResult.Projects)
227
+ {
228
+ await AddUpdatedFileIfDifferentAsync(discoveryResult.Path, project.FilePath);
229
+ var projectDirectory = Path.GetDirectoryName(project.FilePath);
230
+ foreach (var extraFile in project.ImportedFiles.Concat(project.AdditionalFiles))
236
231
  {
237
- await AddUpdatedFileIfDifferentAsync(discoveryResult.Path, project.FilePath);
238
- var projectDirectory = Path.GetDirectoryName(project.FilePath);
239
- foreach (var extraFile in project.ImportedFiles.Concat(project.AdditionalFiles))
240
- {
241
- var extraFilePath = Path.Join(projectDirectory, extraFile);
242
- await AddUpdatedFileIfDifferentAsync(discoveryResult.Path, extraFilePath);
243
- }
244
- // TODO: handle global.json, etc.
232
+ var extraFilePath = Path.Join(projectDirectory, extraFile);
233
+ await AddUpdatedFileIfDifferentAsync(discoveryResult.Path, extraFilePath);
245
234
  }
235
+ // TODO: handle global.json, etc.
236
+ }
246
237
 
247
- if (updatedDependencyFiles.Count > 0)
248
- {
249
- var updatedDependencyFileList = updatedDependencyFiles
250
- .OrderBy(kvp => kvp.Key)
251
- .Select(kvp => kvp.Value)
252
- .ToArray();
253
- var createPullRequest = new CreatePullRequest()
254
- {
255
- Dependencies = actualUpdatedDependencies.ToArray(),
256
- UpdatedDependencyFiles = updatedDependencyFileList,
257
- BaseCommitSha = baseCommitSha,
258
- CommitMessage = "TODO: message",
259
- PrTitle = "TODO: title",
260
- PrBody = "TODO: body",
261
- };
262
- await _apiHandler.CreatePullRequest(createPullRequest);
263
- // TODO: log updated dependencies to console
264
- }
265
- else
238
+ if (updatedDependencyFiles.Count > 0)
239
+ {
240
+ var updatedDependencyFileList = updatedDependencyFiles
241
+ .OrderBy(kvp => kvp.Key)
242
+ .Select(kvp => kvp.Value)
243
+ .ToArray();
244
+ var createPullRequest = new CreatePullRequest()
266
245
  {
267
- // TODO: log or throw if nothing was updated, but was expected to be
268
- }
246
+ Dependencies = actualUpdatedDependencies.ToArray(),
247
+ UpdatedDependencyFiles = updatedDependencyFileList,
248
+ BaseCommitSha = baseCommitSha,
249
+ CommitMessage = "TODO: message",
250
+ PrTitle = "TODO: title",
251
+ PrBody = "TODO: body",
252
+ };
253
+ await _apiHandler.CreatePullRequest(createPullRequest);
254
+ // TODO: log updated dependencies to console
269
255
  }
270
256
  else
271
257
  {
272
- // TODO: throw if no updates performed
258
+ // TODO: log or throw if nothing was updated, but was expected to be
273
259
  }
274
260
 
275
261
  var result = new RunResult()
@@ -289,6 +275,62 @@ public class RunWorker
289
275
  return result;
290
276
  }
291
277
 
278
+ internal static bool IsUpdateAllowed(Job job, Dependency dependency)
279
+ {
280
+ if (dependency.Name.Equals("Microsoft.NET.Sdk", StringComparison.OrdinalIgnoreCase))
281
+ {
282
+ // this can't be updated
283
+ // TODO: pull this out of discovery?
284
+ return false;
285
+ }
286
+
287
+ if (dependency.Version is null)
288
+ {
289
+ // if we don't know the version, there's nothing we can do
290
+ // TODO: pull this out of discovery?
291
+ return false;
292
+ }
293
+
294
+ var version = NuGetVersion.Parse(dependency.Version);
295
+ var dependencyInfo = GetDependencyInfo(job, dependency);
296
+ var isVulnerable = dependencyInfo.Vulnerabilities.Any(v => v.IsVulnerable(version));
297
+ var allowed = job.AllowedUpdates.Any(allowedUpdate =>
298
+ {
299
+ // check name restriction, if any
300
+ if (allowedUpdate.DependencyName is not null)
301
+ {
302
+ var matcher = new Matcher(StringComparison.OrdinalIgnoreCase)
303
+ .AddInclude(allowedUpdate.DependencyName);
304
+ var result = matcher.Match(dependency.Name);
305
+ if (!result.HasMatches)
306
+ {
307
+ return false;
308
+ }
309
+ }
310
+
311
+ var isSecurityUpdate = allowedUpdate.UpdateType == UpdateType.Security || job.SecurityUpdatesOnly;
312
+ if (isSecurityUpdate)
313
+ {
314
+ // only update if it's vulnerable
315
+ return isVulnerable;
316
+ }
317
+ else
318
+ {
319
+ // not a security update, so only update if...
320
+ // ...we've been explicitly asked to update this
321
+ if ((job.Dependencies ?? []).Any(d => d.Equals(dependency.Name, StringComparison.OrdinalIgnoreCase)))
322
+ {
323
+ return true;
324
+ }
325
+
326
+ // ...no specific update being performed, do it if it's not transitive
327
+ return !dependency.IsTransitive;
328
+ }
329
+ });
330
+
331
+ return allowed;
332
+ }
333
+
292
334
  internal static ImmutableArray<Requirement> GetIgnoredRequirementsForDependency(Job job, string dependencyName)
293
335
  {
294
336
  var ignoreConditions = job.IgnoreConditions
@@ -375,7 +417,7 @@ public class RunWorker
375
417
  new ReportedDependency()
376
418
  {
377
419
  Name = d.Name,
378
- Requirements = d.IsTransitive ? [] : [new ReportedRequirement()
420
+ Requirements = [new ReportedRequirement()
379
421
  {
380
422
  File = GetFullRepoPath(p.FilePath),
381
423
  Requirement = d.Version!,
@@ -347,7 +347,7 @@ internal static class PackageReferenceUpdater
347
347
  projectDirectory,
348
348
  experimentsManager
349
349
  );
350
- MSBuildHelper.ThrowOnUnauthenticatedFeed(stdout);
350
+ MSBuildHelper.ThrowOnError(stdout);
351
351
  if (exitCode != 0)
352
352
  {
353
353
  logger.Warn($" Transitive dependency [{dependencyName}/{newDependencyVersion}] was not added.\nSTDOUT:\n{stdout}\nSTDERR:\n{stderr}");
@@ -148,18 +148,15 @@ internal static partial class PackagesConfigUpdater
148
148
 
149
149
  if (exitCodeAgain != 0)
150
150
  {
151
- MSBuildHelper.ThrowOnMissingFile(fullOutput);
152
- MSBuildHelper.ThrowOnMissingFile(restoreOutput);
153
- MSBuildHelper.ThrowOnMissingPackages(restoreOutput);
151
+ MSBuildHelper.ThrowOnError(fullOutput);
152
+ MSBuildHelper.ThrowOnError(restoreOutput);
154
153
  throw new Exception($"Unable to restore.\nOutput:\n${restoreOutput}\n");
155
154
  }
156
155
 
157
156
  goto doRestore;
158
157
  }
159
158
 
160
- MSBuildHelper.ThrowOnUnauthenticatedFeed(fullOutput);
161
- MSBuildHelper.ThrowOnMissingFile(fullOutput);
162
- MSBuildHelper.ThrowOnMissingPackages(fullOutput);
159
+ MSBuildHelper.ThrowOnError(fullOutput);
163
160
  throw new Exception(fullOutput);
164
161
  }
165
162
  }
@@ -903,21 +903,6 @@ internal static partial class MSBuildHelper
903
903
  }
904
904
  }
905
905
 
906
- internal static void ThrowOnUnauthenticatedFeed(string stdout)
907
- {
908
- var unauthorizedMessageSnippets = new string[]
909
- {
910
- "The plugin credential provider could not acquire credentials",
911
- "401 (Unauthorized)",
912
- "error NU1301: Unable to load the service index for source",
913
- "Response status code does not indicate success: 403",
914
- };
915
- if (unauthorizedMessageSnippets.Any(stdout.Contains))
916
- {
917
- throw new HttpRequestException(message: stdout, inner: null, statusCode: System.Net.HttpStatusCode.Unauthorized);
918
- }
919
- }
920
-
921
906
  internal static string? GetMissingFile(string output)
922
907
  {
923
908
  var missingFilePatterns = new[]
@@ -934,7 +919,30 @@ internal static partial class MSBuildHelper
934
919
  return null;
935
920
  }
936
921
 
937
- internal static void ThrowOnMissingFile(string output)
922
+ internal static void ThrowOnError(string output)
923
+ {
924
+ ThrowOnUnauthenticatedFeed(output);
925
+ ThrowOnMissingFile(output);
926
+ ThrowOnMissingPackages(output);
927
+ ThrowOnUnresolvableDependencies(output);
928
+ }
929
+
930
+ private static void ThrowOnUnauthenticatedFeed(string stdout)
931
+ {
932
+ var unauthorizedMessageSnippets = new string[]
933
+ {
934
+ "The plugin credential provider could not acquire credentials",
935
+ "401 (Unauthorized)",
936
+ "error NU1301: Unable to load the service index for source",
937
+ "Response status code does not indicate success: 403",
938
+ };
939
+ if (unauthorizedMessageSnippets.Any(stdout.Contains))
940
+ {
941
+ throw new HttpRequestException(message: stdout, inner: null, statusCode: System.Net.HttpStatusCode.Unauthorized);
942
+ }
943
+ }
944
+
945
+ private static void ThrowOnMissingFile(string output)
938
946
  {
939
947
  var missingFile = GetMissingFile(output);
940
948
  if (missingFile is not null)
@@ -943,9 +951,9 @@ internal static partial class MSBuildHelper
943
951
  }
944
952
  }
945
953
 
946
- internal static void ThrowOnMissingPackages(string output)
954
+ private static void ThrowOnMissingPackages(string output)
947
955
  {
948
- var missingPackagesPattern = new Regex(@"Package '(?<PackageName>[^'].*)' is not found on source");
956
+ var missingPackagesPattern = new Regex(@"Package '(?<PackageName>[^']*)' is not found on source");
949
957
  var matchCollection = missingPackagesPattern.Matches(output);
950
958
  var missingPackages = matchCollection.Select(m => m.Groups["PackageName"].Value).Distinct().ToArray();
951
959
  if (missingPackages.Length > 0)
@@ -954,6 +962,16 @@ internal static partial class MSBuildHelper
954
962
  }
955
963
  }
956
964
 
965
+ private static void ThrowOnUnresolvableDependencies(string output)
966
+ {
967
+ var unresolvablePackagePattern = new Regex(@"Unable to resolve dependencies\. '(?<PackageName>[^ ]+) (?<PackageVersion>[^']+)'");
968
+ var match = unresolvablePackagePattern.Match(output);
969
+ if (match.Success)
970
+ {
971
+ throw new UpdateNotPossibleException([$"{match.Groups["PackageName"].Value}.{match.Groups["PackageVersion"].Value}"]);
972
+ }
973
+ }
974
+
957
975
  internal static bool TryGetGlobalJsonPath(string repoRootPath, string workspacePath, [NotNullWhen(returnValue: true)] out string? globalJsonPath)
958
976
  {
959
977
  globalJsonPath = PathHelper.GetFileInDirectoryOrParent(workspacePath, repoRootPath, "global.json", caseSensitive: false);
@@ -1711,6 +1711,344 @@ public class RunWorkerTests
1711
1711
  );
1712
1712
  }
1713
1713
 
1714
+ [Fact]
1715
+ public async Task UpdatePackageWithDifferentVersionsInDifferentDirectories()
1716
+ {
1717
+ // this test passes `null` for discovery, analyze, and update workers to fully test the desired behavior
1718
+
1719
+ // the same dependency Some.Package is reported for 3 cases:
1720
+ // library1.csproj - top level dependency, already up to date
1721
+ // library2.csproj - top level dependency, needs direct update
1722
+ // library3.csproj - transitive dependency, needs pin
1723
+ await RunAsync(
1724
+ experimentsManager: new ExperimentsManager() { UseDirectDiscovery = true },
1725
+ packages: [
1726
+ MockNuGetPackage.CreateSimplePackage("Some.Package", "1.0.0", "net8.0"),
1727
+ MockNuGetPackage.CreateSimplePackage("Some.Package", "2.0.0", "net8.0"),
1728
+ MockNuGetPackage.CreateSimplePackage("Package.With.Transitive.Dependency", "0.1.0", "net8.0", [(null, [("Some.Package", "1.0.0")])]),
1729
+ ],
1730
+ job: new Job()
1731
+ {
1732
+ AllowedUpdates = [new() { UpdateType = UpdateType.Security }],
1733
+ SecurityAdvisories =
1734
+ [
1735
+ new()
1736
+ {
1737
+ DependencyName = "Some.Package",
1738
+ AffectedVersions = [Requirement.Parse("= 1.0.0")]
1739
+ }
1740
+ ],
1741
+ Source = new()
1742
+ {
1743
+ Provider = "github",
1744
+ Repo = "test/repo",
1745
+ Directory = "/"
1746
+ }
1747
+ },
1748
+ files: [
1749
+ ("dirs.proj", """
1750
+ <Project>
1751
+ <ItemGroup>
1752
+ <ProjectFile Include="library1\library1.csproj" />
1753
+ <ProjectFile Include="library2\library2.csproj" />
1754
+ <ProjectFile Include="library3\library3.csproj" />
1755
+ </ItemGroup>
1756
+ </Project>
1757
+ """),
1758
+ ("Directory.Build.props", "<Project />"),
1759
+ ("Directory.Build.targets", "<Project />"),
1760
+ ("Directory.Packages.props", """
1761
+ <Project>
1762
+ <PropertyGroup>
1763
+ <ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>
1764
+ </PropertyGroup>
1765
+ </Project>
1766
+ """),
1767
+ ("library1/library1.csproj", """
1768
+ <Project Sdk="Microsoft.NET.Sdk">
1769
+ <PropertyGroup>
1770
+ <TargetFramework>net8.0</TargetFramework>
1771
+ </PropertyGroup>
1772
+ <ItemGroup>
1773
+ <PackageReference Include="Some.Package" Version="2.0.0" />
1774
+ </ItemGroup>
1775
+ </Project>
1776
+ """),
1777
+ ("library2/library2.csproj", """
1778
+ <Project Sdk="Microsoft.NET.Sdk">
1779
+ <PropertyGroup>
1780
+ <TargetFramework>net8.0</TargetFramework>
1781
+ </PropertyGroup>
1782
+ <ItemGroup>
1783
+ <PackageReference Include="Some.Package" Version="1.0.0" />
1784
+ </ItemGroup>
1785
+ </Project>
1786
+ """),
1787
+ ("library3/library3.csproj", """
1788
+ <Project Sdk="Microsoft.NET.Sdk">
1789
+ <PropertyGroup>
1790
+ <TargetFramework>net8.0</TargetFramework>
1791
+ </PropertyGroup>
1792
+ <ItemGroup>
1793
+ <PackageReference Include="Package.With.Transitive.Dependency" Version="0.1.0" />
1794
+ </ItemGroup>
1795
+ </Project>
1796
+ """),
1797
+ ],
1798
+ discoveryWorker: null,
1799
+ analyzeWorker: null,
1800
+ updaterWorker: null,
1801
+ expectedResult: new RunResult()
1802
+ {
1803
+ Base64DependencyFiles =
1804
+ [
1805
+ new DependencyFile()
1806
+ {
1807
+ Directory = "/",
1808
+ Name = "Directory.Build.props",
1809
+ Content = Convert.ToBase64String(Encoding.UTF8.GetBytes("<Project />"))
1810
+ },
1811
+ new DependencyFile()
1812
+ {
1813
+ Directory = "/",
1814
+ Name = "Directory.Build.targets",
1815
+ Content = Convert.ToBase64String(Encoding.UTF8.GetBytes("<Project />"))
1816
+ },
1817
+ new DependencyFile()
1818
+ {
1819
+ Directory = "/",
1820
+ Name = "Directory.Packages.props",
1821
+ Content = Convert.ToBase64String(Encoding.UTF8.GetBytes("""
1822
+ <Project>
1823
+ <PropertyGroup>
1824
+ <ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>
1825
+ </PropertyGroup>
1826
+ </Project>
1827
+ """))
1828
+ },
1829
+ new DependencyFile()
1830
+ {
1831
+ Directory = "/library1",
1832
+ Name = "library1.csproj",
1833
+ Content = Convert.ToBase64String(Encoding.UTF8.GetBytes("""
1834
+ <Project Sdk="Microsoft.NET.Sdk">
1835
+ <PropertyGroup>
1836
+ <TargetFramework>net8.0</TargetFramework>
1837
+ </PropertyGroup>
1838
+ <ItemGroup>
1839
+ <PackageReference Include="Some.Package" Version="2.0.0" />
1840
+ </ItemGroup>
1841
+ </Project>
1842
+ """))
1843
+ },
1844
+ new DependencyFile()
1845
+ {
1846
+ Directory = "/library2",
1847
+ Name = "library2.csproj",
1848
+ Content = Convert.ToBase64String(Encoding.UTF8.GetBytes("""
1849
+ <Project Sdk="Microsoft.NET.Sdk">
1850
+ <PropertyGroup>
1851
+ <TargetFramework>net8.0</TargetFramework>
1852
+ </PropertyGroup>
1853
+ <ItemGroup>
1854
+ <PackageReference Include="Some.Package" Version="1.0.0" />
1855
+ </ItemGroup>
1856
+ </Project>
1857
+ """))
1858
+ },
1859
+ new DependencyFile()
1860
+ {
1861
+ Directory = "/library3",
1862
+ Name = "library3.csproj",
1863
+ Content = Convert.ToBase64String(Encoding.UTF8.GetBytes("""
1864
+ <Project Sdk="Microsoft.NET.Sdk">
1865
+ <PropertyGroup>
1866
+ <TargetFramework>net8.0</TargetFramework>
1867
+ </PropertyGroup>
1868
+ <ItemGroup>
1869
+ <PackageReference Include="Package.With.Transitive.Dependency" Version="0.1.0" />
1870
+ </ItemGroup>
1871
+ </Project>
1872
+ """))
1873
+ }
1874
+ ],
1875
+ BaseCommitSha = "TEST-COMMIT-SHA",
1876
+ },
1877
+ expectedApiMessages: [
1878
+ new UpdatedDependencyList()
1879
+ {
1880
+ Dependencies = [
1881
+ new()
1882
+ {
1883
+ Name = "Some.Package",
1884
+ Version = "2.0.0",
1885
+ Requirements = [
1886
+ new()
1887
+ {
1888
+ Requirement = "2.0.0",
1889
+ File = "/library1/library1.csproj",
1890
+ Groups = ["dependencies"],
1891
+ }
1892
+ ]
1893
+ },
1894
+ new()
1895
+ {
1896
+ Name = "Some.Package",
1897
+ Version = "1.0.0",
1898
+ Requirements = [
1899
+ new()
1900
+ {
1901
+ Requirement = "1.0.0",
1902
+ File = "/library2/library2.csproj",
1903
+ Groups = ["dependencies"],
1904
+ }
1905
+ ]
1906
+ },
1907
+ new()
1908
+ {
1909
+ Name = "Package.With.Transitive.Dependency",
1910
+ Version = "0.1.0",
1911
+ Requirements = [
1912
+ new()
1913
+ {
1914
+ Requirement = "0.1.0",
1915
+ File = "/library3/library3.csproj",
1916
+ Groups = ["dependencies"],
1917
+ }
1918
+ ]
1919
+ },
1920
+ new()
1921
+ {
1922
+ Name = "Some.Package",
1923
+ Version = "1.0.0",
1924
+ Requirements = [
1925
+ new()
1926
+ {
1927
+ Requirement = "1.0.0",
1928
+ File = "/library3/library3.csproj",
1929
+ Groups = ["dependencies"],
1930
+ }
1931
+ ]
1932
+ },
1933
+ ],
1934
+ DependencyFiles = [
1935
+ "/Directory.Build.props",
1936
+ "/Directory.Build.targets",
1937
+ "/Directory.Packages.props",
1938
+ "/library1/library1.csproj",
1939
+ "/library2/library2.csproj",
1940
+ "/library3/library3.csproj",
1941
+ ],
1942
+ },
1943
+ new IncrementMetric()
1944
+ {
1945
+ Metric = "updater.started",
1946
+ Tags = new()
1947
+ {
1948
+ ["operation"] = "group_update_all_versions"
1949
+ }
1950
+ },
1951
+ new CreatePullRequest()
1952
+ {
1953
+ Dependencies = [
1954
+ new()
1955
+ {
1956
+ Name = "Some.Package",
1957
+ Version = "2.0.0",
1958
+ Requirements = [
1959
+ new()
1960
+ {
1961
+ Requirement = "2.0.0",
1962
+ File = "/library2/library2.csproj",
1963
+ Groups = ["dependencies"],
1964
+ Source = new()
1965
+ {
1966
+ SourceUrl = null,
1967
+ Type = "nuget_repo",
1968
+ }
1969
+ }
1970
+ ],
1971
+ PreviousVersion = "1.0.0",
1972
+ PreviousRequirements = [
1973
+ new()
1974
+ {
1975
+ Requirement = "1.0.0",
1976
+ File = "/library2/library2.csproj",
1977
+ Groups = ["dependencies"],
1978
+ }
1979
+ ],
1980
+ },
1981
+ new()
1982
+ {
1983
+ Name = "Some.Package",
1984
+ Version = "2.0.0",
1985
+ Requirements = [
1986
+ new()
1987
+ {
1988
+ Requirement = "2.0.0",
1989
+ File = "/library3/library3.csproj",
1990
+ Groups = ["dependencies"],
1991
+ Source = new()
1992
+ {
1993
+ SourceUrl = null,
1994
+ Type = "nuget_repo",
1995
+ }
1996
+ }
1997
+ ],
1998
+ PreviousVersion = "1.0.0",
1999
+ PreviousRequirements = [
2000
+ new()
2001
+ {
2002
+ Requirement = "1.0.0",
2003
+ File = "/library3/library3.csproj",
2004
+ Groups = ["dependencies"],
2005
+ }
2006
+ ],
2007
+ },
2008
+ ],
2009
+ UpdatedDependencyFiles = [
2010
+ new()
2011
+ {
2012
+ Directory = "/library2",
2013
+ Name = "library2.csproj",
2014
+ Content = """
2015
+ <Project Sdk="Microsoft.NET.Sdk">
2016
+ <PropertyGroup>
2017
+ <TargetFramework>net8.0</TargetFramework>
2018
+ </PropertyGroup>
2019
+ <ItemGroup>
2020
+ <PackageReference Include="Some.Package" Version="2.0.0" />
2021
+ </ItemGroup>
2022
+ </Project>
2023
+ """
2024
+ },
2025
+ new()
2026
+ {
2027
+ Directory = "/library3",
2028
+ Name = "library3.csproj",
2029
+ Content = """
2030
+ <Project Sdk="Microsoft.NET.Sdk">
2031
+ <PropertyGroup>
2032
+ <TargetFramework>net8.0</TargetFramework>
2033
+ </PropertyGroup>
2034
+ <ItemGroup>
2035
+ <PackageReference Include="Package.With.Transitive.Dependency" Version="0.1.0" />
2036
+ <PackageReference Include="Some.Package" Version="2.0.0" />
2037
+ </ItemGroup>
2038
+ </Project>
2039
+ """
2040
+ }
2041
+ ],
2042
+ BaseCommitSha = "TEST-COMMIT-SHA",
2043
+ CommitMessage = "TODO: message",
2044
+ PrTitle = "TODO: title",
2045
+ PrBody = "TODO: body"
2046
+ },
2047
+ new MarkAsProcessed("TEST-COMMIT-SHA")
2048
+ ]
2049
+ );
2050
+ }
2051
+
1714
2052
  private static async Task RunAsync(Job job, TestFile[] files, IDiscoveryWorker? discoveryWorker, IAnalyzeWorker? analyzeWorker, IUpdaterWorker? updaterWorker, RunResult expectedResult, object[] expectedApiMessages, MockNuGetPackage[]? packages = null, ExperimentsManager? experimentsManager = null, string? repoContentsPath = null)
1715
2053
  {
1716
2054
  // arrange
@@ -0,0 +1,286 @@
1
+ using System.Collections.Immutable;
2
+
3
+ using NuGetUpdater.Core.Analyze;
4
+ using NuGetUpdater.Core.Run;
5
+ using NuGetUpdater.Core.Run.ApiModel;
6
+
7
+ using Xunit;
8
+
9
+ using DepType = NuGetUpdater.Core.Run.ApiModel.DependencyType;
10
+
11
+ namespace NuGetUpdater.Core.Test.Run;
12
+
13
+ public class UpdateAllowedTests
14
+ {
15
+ [Theory]
16
+ [MemberData(nameof(IsUpdateAllowedTestData))]
17
+ public void IsUpdateAllowed(Job job, Dependency dependency, bool expectedResult)
18
+ {
19
+ var actualResult = RunWorker.IsUpdateAllowed(job, dependency);
20
+ Assert.Equal(expectedResult, actualResult);
21
+ }
22
+
23
+ public static IEnumerable<object[]> IsUpdateAllowedTestData()
24
+ {
25
+ // with default allowed updates on a transitive dependency
26
+ yield return
27
+ [
28
+ CreateJob(
29
+ allowedUpdates: [
30
+ new AllowedUpdate() { DependencyType = DepType.Direct, UpdateType = UpdateType.All }
31
+ ],
32
+ securityAdvisories: [
33
+ new Advisory() { DependencyName = "Some.Package", AffectedVersions = [], PatchedVersions = [Requirement.Parse(">= 1.11.0")], UnaffectedVersions = [] }
34
+ ],
35
+ securityUpdatesOnly: false),
36
+ new Dependency("Some.Package", "1.8.0", DependencyType.PackageReference, IsTransitive: true),
37
+ // expectedResult
38
+ false,
39
+ ];
40
+
41
+ // when dealing with a security update
42
+ yield return
43
+ [
44
+ CreateJob(
45
+ allowedUpdates: [
46
+ new AllowedUpdate() { DependencyType = DepType.Direct, UpdateType = UpdateType.All }
47
+ ],
48
+ securityAdvisories: [
49
+ new Advisory() { DependencyName = "Some.Package", AffectedVersions = [], PatchedVersions = [Requirement.Parse(">= 1.11.0")], UnaffectedVersions = [] }
50
+ ],
51
+ securityUpdatesOnly: true),
52
+ new Dependency("Some.Package", "1.8.0", DependencyType.PackageReference, IsTransitive: true),
53
+ // expectedResult
54
+ true,
55
+ ];
56
+
57
+ // with a top-level dependency
58
+ yield return
59
+ [
60
+ CreateJob(
61
+ allowedUpdates: [
62
+ new AllowedUpdate() { DependencyType = DepType.Direct, UpdateType = UpdateType.All },
63
+ new AllowedUpdate() { DependencyType = DepType.Indirect, UpdateType = UpdateType.Security }
64
+ ],
65
+ securityAdvisories: [],
66
+ securityUpdatesOnly: false),
67
+ new Dependency("Some.Package", "1.8.0", DependencyType.PackageReference, IsTransitive: false),
68
+ // expectedResult
69
+ true,
70
+ ];
71
+
72
+ // with a sub-dependency
73
+ yield return
74
+ [
75
+ CreateJob(
76
+ allowedUpdates: [
77
+ new AllowedUpdate() { DependencyType = DepType.Direct, UpdateType = UpdateType.All },
78
+ new AllowedUpdate() { DependencyType = DepType.Indirect, UpdateType = UpdateType.Security }
79
+ ],
80
+ securityAdvisories: [],
81
+ securityUpdatesOnly: false),
82
+ new Dependency("Some.Package", "1.8.0", DependencyType.PackageReference, IsTransitive: true),
83
+ // expectedResult
84
+ false,
85
+ ];
86
+
87
+ // when insecure
88
+ yield return
89
+ [
90
+ CreateJob(
91
+ allowedUpdates: [
92
+ new AllowedUpdate() { DependencyType = DepType.Direct, UpdateType = UpdateType.All },
93
+ new AllowedUpdate() { DependencyType = DepType.Indirect, UpdateType = UpdateType.Security }
94
+ ],
95
+ securityAdvisories: [
96
+ new Advisory() { DependencyName = "Some.Package", AffectedVersions = [], PatchedVersions = [Requirement.Parse(">= 1.11.0")], UnaffectedVersions = [] }
97
+ ],
98
+ securityUpdatesOnly: false),
99
+ new Dependency("Some.Package", "1.8.0", DependencyType.PackageReference, IsTransitive: true),
100
+ // expectedResult
101
+ true,
102
+ ];
103
+
104
+ // when only security fixes are allowed
105
+ yield return
106
+ [
107
+ CreateJob(
108
+ allowedUpdates: [
109
+ new AllowedUpdate() { DependencyType = DepType.Direct, UpdateType = UpdateType.All },
110
+ new AllowedUpdate() { DependencyType = DepType.Indirect, UpdateType = UpdateType.Security }
111
+ ],
112
+ securityAdvisories: [],
113
+ securityUpdatesOnly: true),
114
+ new Dependency("Some.Package", "1.8.0", DependencyType.PackageReference, IsTransitive: false),
115
+ // expectedResult
116
+ false,
117
+ ];
118
+
119
+ // when dealing with a security fix
120
+ yield return
121
+ [
122
+ CreateJob(
123
+ allowedUpdates: [
124
+ new AllowedUpdate() { DependencyType = DepType.Direct, UpdateType = UpdateType.All },
125
+ new AllowedUpdate() { DependencyType = DepType.Indirect, UpdateType = UpdateType.Security }
126
+ ],
127
+ securityAdvisories: [
128
+ new Advisory() { DependencyName = "Some.Package", AffectedVersions = [], PatchedVersions = [Requirement.Parse(">= 1.11.0")], UnaffectedVersions = [] }
129
+ ],
130
+ securityUpdatesOnly: true),
131
+ new Dependency("Some.Package", "1.8.0", DependencyType.PackageReference, IsTransitive: false),
132
+ // expectedResult
133
+ true,
134
+ ];
135
+
136
+ // when dealing with a security fix that doesn't apply
137
+ yield return
138
+ [
139
+ CreateJob(
140
+ allowedUpdates: [
141
+ new AllowedUpdate() { DependencyType = DepType.Direct, UpdateType = UpdateType.All },
142
+ new AllowedUpdate() { DependencyType = DepType.Indirect, UpdateType = UpdateType.Security }
143
+ ],
144
+ securityAdvisories: [
145
+ new Advisory() { DependencyName = "Some.Package", AffectedVersions = [Requirement.Parse("> 1.8.0")], PatchedVersions = [], UnaffectedVersions = [] }
146
+ ],
147
+ securityUpdatesOnly: true),
148
+ new Dependency("Some.Package", "1.8.0", DependencyType.PackageReference, IsTransitive: false),
149
+ // expectedResult
150
+ false,
151
+ ];
152
+
153
+ // when dealing with a security fix that doesn't apply to some versions
154
+ yield return
155
+ [
156
+ CreateJob(
157
+ allowedUpdates: [
158
+ new AllowedUpdate() { DependencyType = DepType.Direct, UpdateType = UpdateType.All },
159
+ new AllowedUpdate() { DependencyType = DepType.Indirect, UpdateType = UpdateType.Security }
160
+ ],
161
+ securityAdvisories: [
162
+ new Advisory() { DependencyName = "Some.Package", AffectedVersions = [Requirement.Parse("< 1.8.0"), Requirement.Parse("> 1.8.0")], PatchedVersions = [], UnaffectedVersions = [] }
163
+ ],
164
+ securityUpdatesOnly: true),
165
+ new Dependency("Some.Package", "1.8.1", DependencyType.PackageReference, IsTransitive: false),
166
+ // expectedResult
167
+ true,
168
+ ];
169
+
170
+ // when a dependency allow list that includes the dependency
171
+ yield return
172
+ [
173
+ CreateJob(
174
+ allowedUpdates: [
175
+ new AllowedUpdate() { DependencyName = "Some.Package" }
176
+ ],
177
+ securityAdvisories: [],
178
+ securityUpdatesOnly: false),
179
+ new Dependency("Some.Package", "1.8.0", DependencyType.PackageReference, IsTransitive: false),
180
+ // expectedResult
181
+ true,
182
+ ];
183
+
184
+ // with a dependency allow list that uses a wildcard
185
+ yield return
186
+ [
187
+ CreateJob(
188
+ allowedUpdates: [
189
+ new AllowedUpdate() { DependencyName = "Some.*" }
190
+ ],
191
+ securityAdvisories: [],
192
+ securityUpdatesOnly: false),
193
+ new Dependency("Some.Package", "1.8.0", DependencyType.PackageReference, IsTransitive: false),
194
+ // expectedResult
195
+ true,
196
+ ];
197
+
198
+ // when dependency allow list that excludes the dependency
199
+ yield return
200
+ [
201
+ CreateJob(
202
+ allowedUpdates: [
203
+ new AllowedUpdate() { DependencyName = "Unrelated.Package" }
204
+ ],
205
+ securityAdvisories: [],
206
+ securityUpdatesOnly: false),
207
+ new Dependency("Some.Package", "1.8.0", DependencyType.PackageReference, IsTransitive: false),
208
+ // expectedResult
209
+ false,
210
+ ];
211
+
212
+ // when matching with an incomplete dependency name
213
+ yield return
214
+ [
215
+ CreateJob(
216
+ allowedUpdates: [
217
+ new AllowedUpdate() { DependencyName = "Some" }
218
+ ],
219
+ securityAdvisories: [],
220
+ securityUpdatesOnly: false),
221
+ new Dependency("Some.Package", "1.8.0", DependencyType.PackageReference, IsTransitive: false),
222
+ // expectedResult
223
+ false,
224
+ ];
225
+
226
+ // with a dependency allow list that uses a wildcard
227
+ yield return
228
+ [
229
+ CreateJob(
230
+ allowedUpdates: [
231
+ new AllowedUpdate() { DependencyName = "Unrelated.*" }
232
+ ],
233
+ securityAdvisories: [],
234
+ securityUpdatesOnly: false),
235
+ new Dependency("Some.Package", "1.8.0", DependencyType.PackageReference, IsTransitive: false),
236
+ // expectedResult
237
+ false,
238
+ ];
239
+
240
+ // when security fixes are also allowed
241
+ yield return
242
+ [
243
+ CreateJob(
244
+ allowedUpdates: [
245
+ new AllowedUpdate() { DependencyName = "Unrelated.Package" },
246
+ new AllowedUpdate() { UpdateType = UpdateType.Security }
247
+ ],
248
+ securityAdvisories: [],
249
+ securityUpdatesOnly: false),
250
+ new Dependency("Some.Package", "1.8.0", DependencyType.PackageReference, IsTransitive: false),
251
+ // expectedResult
252
+ false,
253
+ ];
254
+
255
+ // when dealing with a security fix
256
+ yield return
257
+ [
258
+ CreateJob(
259
+ allowedUpdates: [
260
+ new AllowedUpdate() { DependencyName = "Unrelated.Package"}, new AllowedUpdate(){ UpdateType = UpdateType.Security }
261
+ ],
262
+ securityAdvisories: [
263
+ new Advisory() { DependencyName = "Some.Package", AffectedVersions = [], PatchedVersions = [Requirement.Parse(">= 1.11.0")], UnaffectedVersions = [] }
264
+ ],
265
+ securityUpdatesOnly: false),
266
+ new Dependency("Some.Package", "1.8.0", DependencyType.PackageReference, IsTransitive: false),
267
+ // expectedResult
268
+ true,
269
+ ];
270
+ }
271
+
272
+ private static Job CreateJob(AllowedUpdate[] allowedUpdates, Advisory[] securityAdvisories, bool securityUpdatesOnly)
273
+ {
274
+ return new Job()
275
+ {
276
+ AllowedUpdates = allowedUpdates.ToImmutableArray(),
277
+ SecurityAdvisories = securityAdvisories.ToImmutableArray(),
278
+ SecurityUpdatesOnly = securityUpdatesOnly,
279
+ Source = new()
280
+ {
281
+ Provider = "nuget",
282
+ Repo = "test/repo",
283
+ }
284
+ };
285
+ }
286
+ }
@@ -94,7 +94,15 @@ public class UpdatedDependencyListTests
94
94
  {
95
95
  Name = "System.Text.Json",
96
96
  Version = "6.0.0",
97
- Requirements = [],
97
+ Requirements =
98
+ [
99
+ new ReportedRequirement()
100
+ {
101
+ Requirement = "6.0.0",
102
+ File = "/src/c/project.csproj",
103
+ Groups = ["dependencies"],
104
+ }
105
+ ],
98
106
  },
99
107
  new ReportedDependency()
100
108
  {
@@ -1,7 +1,8 @@
1
1
  using System.Collections.Immutable;
2
+ using System.Text.Json;
2
3
 
3
- using NuGet.Build.Tasks;
4
-
4
+ using NuGetUpdater.Core.Run;
5
+ using NuGetUpdater.Core.Run.ApiModel;
5
6
  using NuGetUpdater.Core.Test.Update;
6
7
 
7
8
  using Xunit;
@@ -1366,6 +1367,91 @@ public class MSBuildHelperTests : TestBase
1366
1367
  }
1367
1368
  #endregion
1368
1369
 
1370
+ [Theory]
1371
+ [MemberData(nameof(GenerateErrorFromToolOutputTestData))]
1372
+ public async Task GenerateErrorFromToolOutput(string output, JobErrorBase? expectedError)
1373
+ {
1374
+ Exception? exception = null;
1375
+ try
1376
+ {
1377
+ MSBuildHelper.ThrowOnError(output);
1378
+ }
1379
+ catch (Exception ex)
1380
+ {
1381
+ exception = ex;
1382
+ }
1383
+
1384
+ if (expectedError is null)
1385
+ {
1386
+ Assert.Null(exception);
1387
+ }
1388
+ else
1389
+ {
1390
+ Assert.NotNull(exception);
1391
+ using var tempDir = await TemporaryDirectory.CreateWithContentsAsync([("NuGet.Config", """
1392
+ <configuration>
1393
+ <packageSources>
1394
+ <clear />
1395
+ <add key="test-feed" value="http://localhost/test-feed" />
1396
+ </packageSources>
1397
+ </configuration>
1398
+ """)]);
1399
+ var actualError = JobErrorBase.ErrorFromException(exception, "TEST-JOB-ID", tempDir.DirectoryPath);
1400
+ if (actualError is DependencyFileNotFound notFound)
1401
+ {
1402
+ // normalize default message for the test
1403
+ actualError = new DependencyFileNotFound(notFound.Details["file-path"].ToString()!, "test message");
1404
+ }
1405
+
1406
+ var actualErrorJson = JsonSerializer.Serialize(actualError, RunWorker.SerializerOptions);
1407
+ var expectedErrorJson = JsonSerializer.Serialize(expectedError, RunWorker.SerializerOptions);
1408
+ Assert.Equal(expectedErrorJson, actualErrorJson);
1409
+ }
1410
+ }
1411
+
1412
+ public static IEnumerable<object?[]> GenerateErrorFromToolOutputTestData()
1413
+ {
1414
+ yield return
1415
+ [
1416
+ // output
1417
+ "Everything was good.",
1418
+ // expectedError
1419
+ null,
1420
+ ];
1421
+
1422
+ yield return
1423
+ [
1424
+ // output
1425
+ "Response status code does not indicate success: 403",
1426
+ // expectedError
1427
+ new PrivateSourceAuthenticationFailure(["http://localhost/test-feed"]),
1428
+ ];
1429
+
1430
+ yield return
1431
+ [
1432
+ // output
1433
+ "The imported file \"some.file\" does not exist",
1434
+ // expectedError
1435
+ new DependencyFileNotFound("some.file", "test message"),
1436
+ ];
1437
+
1438
+ yield return
1439
+ [
1440
+ // output
1441
+ "Package 'Some.Package' is not found on source",
1442
+ // expectedError
1443
+ new UpdateNotPossible(["Some.Package"]),
1444
+ ];
1445
+
1446
+ yield return
1447
+ [
1448
+ // output
1449
+ "Unable to resolve dependencies. 'Some.Package 1.2.3' is not compatible with",
1450
+ // expectedError
1451
+ new UpdateNotPossible(["Some.Package.1.2.3"]),
1452
+ ];
1453
+ }
1454
+
1369
1455
  public static IEnumerable<object[]> GetTopLevelPackageDependencyInfosTestData()
1370
1456
  {
1371
1457
  // simple case
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dependabot-nuget
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.294.0
4
+ version: 0.295.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dependabot
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-01-23 00:00:00.000000000 Z
11
+ date: 2025-01-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dependabot-common
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - '='
18
18
  - !ruby/object:Gem::Version
19
- version: 0.294.0
19
+ version: 0.295.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - '='
25
25
  - !ruby/object:Gem::Version
26
- version: 0.294.0
26
+ version: 0.295.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rubyzip
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -351,6 +351,7 @@ files:
351
351
  - helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/RunWorkerTests.cs
352
352
  - helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/SerializationTests.cs
353
353
  - helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/TestApiHandler.cs
354
+ - helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/UpdateAllowedTests.cs
354
355
  - helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/UpdatedDependencyListTests.cs
355
356
  - helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/TemporaryDirectory.cs
356
357
  - helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/TemporaryEnvironment.cs
@@ -528,7 +529,7 @@ licenses:
528
529
  - MIT
529
530
  metadata:
530
531
  bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
531
- changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.294.0
532
+ changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.295.0
532
533
  post_install_message:
533
534
  rdoc_options: []
534
535
  require_paths: