dependabot-nuget 0.352.0 → 0.353.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.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs +15 -41
  3. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/SdkProjectDiscovery.cs +102 -46
  4. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/ExperimentsManager.cs +0 -3
  5. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/DependencyGroup.cs +19 -0
  6. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/Job.cs +23 -2
  7. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/JobErrorBase.cs +4 -2
  8. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/OutOfDisk.cs +9 -0
  9. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/IApiHandler.cs +11 -1
  10. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/RunWorker.cs +25 -4
  11. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/UpdateHandlers/CreateSecurityUpdatePullRequestHandler.cs +2 -2
  12. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/UpdateHandlers/GroupUpdateAllVersionsHandler.cs +4 -4
  13. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/UpdateHandlers/RefreshGroupUpdatePullRequestHandler.cs +3 -3
  14. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/UpdateHandlers/RefreshSecurityUpdatePullRequestHandler.cs +2 -2
  15. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/UpdateHandlers/RefreshVersionUpdatePullRequestHandler.cs +2 -2
  16. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/PackageReferenceUpdater.cs +20 -23
  17. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs +41 -1
  18. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/PathHelper.cs +93 -0
  19. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.PackagesConfig.cs +2 -5
  20. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.Project.cs +21 -9
  21. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.cs +23 -104
  22. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/SdkProjectDiscoveryTests.cs +1 -66
  23. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/ApiModel/JobTests.cs +39 -0
  24. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/EndToEndTests.cs +142 -0
  25. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/HttpApiHandlerTests.cs +1 -0
  26. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/JobErrorBaseTests.cs +7 -0
  27. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/MessageReportTests.cs +11 -0
  28. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/MiscellaneousTests.cs +76 -7
  29. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/SerializationTests.cs +8 -0
  30. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/PackageReferenceUpdaterTests.cs +30 -0
  31. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/MSBuildHelperTests.cs +25 -0
  32. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/PathHelperTests.cs +250 -0
  33. metadata +5 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 011bc96766fde7190676e83ac5f6fb5a227ad6a8efa644010309df485732231c
4
- data.tar.gz: 0fe5b3850138fcc5c1c2bef7bc180e17b4137851b34d7d096474561d211f15f4
3
+ metadata.gz: 7abf57af01e4a8a8aec9fb9d1979e6104a5059e057b4001bd53fa25e32c9a891
4
+ data.tar.gz: 6b983c64fbd42200d023004d03f039a68ca8a4b8dace9a63efaea0328f9dd572
5
5
  SHA512:
6
- metadata.gz: a549a494e8b71d2441455b71a7b48076d20ce0051fcbd02da7762b02e311b155a0affd5810b4af67279fb61d1db63a010d2e0f7798608b4a66e8ac5c02ae9611
7
- data.tar.gz: a62dbf38f60ecefbe4dac4ffc32564caecf35c4b80e70e5555d49ef0ac4677767613977f75ee5838f470fe2bf55aca0d25074919325f96b683dbdd975eac08d5
6
+ metadata.gz: bcc2466108e839493cb8146b2dcae4079c257956f8eaf0f2181f6d24ae9cdb2fe8b20bea620d5530a33f4a62b2ac3a31bc17d80def3049e51b4ae8969d8d626d
7
+ data.tar.gz: d4bbc768c4c02cc017a4e066d885fb66b5c7dd00fa5c60b22605a5bf078cfceac52bfa98cc798018aa5a5fe0e3a290b7c38f6fc72d9718527da8b9409967d6d4
@@ -252,52 +252,26 @@ public partial class DiscoveryWorker : IDiscoveryWorker
252
252
  filesToExpand.Push(projectPath);
253
253
  }
254
254
  }
255
-
256
- if (experimentsManager.UseSingleRestore)
255
+ else if (extension == ".proj")
257
256
  {
258
- // projects get shunted directly to the result because regular discovery handles it from there
259
- switch (extension)
257
+ var foundProjects = ExpandItemGroupFilesFromProject(candidateEntryPoint, "ProjectFile", "ProjectReference");
258
+ foreach (var foundProject in foundProjects)
260
259
  {
261
- case ".proj":
262
- case ".csproj":
263
- case ".fbproj":
264
- case ".fsproj":
265
- expandedProjects.Add(candidateEntryPoint);
266
- break;
267
- default:
268
- // unsupported project
269
- break;
260
+ filesToExpand.Push(foundProject);
270
261
  }
271
262
  }
272
- else
263
+
264
+ // projects get shunted directly to the result because regular discovery handles it from there
265
+ switch (extension)
273
266
  {
274
- if (extension == ".proj")
275
- {
276
- IEnumerable<string> foundProjects = ExpandItemGroupFilesFromProject(candidateEntryPoint, "ProjectFile", "ProjectReference");
277
- foreach (string foundProject in foundProjects)
278
- {
279
- filesToExpand.Push(foundProject);
280
- }
281
- }
282
- else
283
- {
284
- switch (extension)
285
- {
286
- case ".csproj":
287
- case ".fsproj":
288
- case ".vbproj":
289
- // keep this project and check for references
290
- expandedProjects.Add(candidateEntryPoint);
291
- IEnumerable<string> referencedProjects = ExpandItemGroupFilesFromProject(candidateEntryPoint, "ProjectReference");
292
- foreach (string referencedProject in referencedProjects)
293
- {
294
- filesToExpand.Push(referencedProject);
295
- }
296
- break;
297
- default:
298
- continue;
299
- }
300
- }
267
+ case ".csproj":
268
+ case ".vbproj":
269
+ case ".fsproj":
270
+ expandedProjects.Add(candidateEntryPoint);
271
+ break;
272
+ default:
273
+ // unsupported project
274
+ break;
301
275
  }
302
276
  }
303
277
  }
@@ -5,7 +5,9 @@ using System.Xml.XPath;
5
5
 
6
6
  using Microsoft.Build.Logging.StructuredLogger;
7
7
 
8
+ using NuGet;
8
9
  using NuGet.Frameworks;
10
+ using NuGet.Versioning;
9
11
 
10
12
  using NuGetUpdater.Core.Utilities;
11
13
 
@@ -58,7 +60,7 @@ internal static class SdkProjectDiscovery
58
60
  [
59
61
  "Restore",
60
62
  "ResolveProjectReferences",
61
- "ResolvePackageAssets"
63
+ "GenerateBuildDependencyFile"
62
64
  ];
63
65
 
64
66
  public static async Task<ImmutableArray<ProjectDiscoveryResult>> DiscoverAsync(string repoRootPath, string workspacePath, string startingProjectPath, ExperimentsManager experimentsManager, ILogger logger)
@@ -88,6 +90,9 @@ internal static class SdkProjectDiscovery
88
90
  Dictionary<string, Dictionary<string, Dictionary<string, string>>> explicitPackageVersionsPerProject = new(PathComparer.Instance);
89
91
  // projectPath, tfm, packageName, packageVersion
90
92
 
93
+ Dictionary<string, int> packageReferenceElementCounts = new(PathComparer.Instance);
94
+ // projectPath, count of `<PackageReference>` elements
95
+
91
96
  Dictionary<string, Dictionary<string, Dictionary<string, string>>> packagesReplacedBySdkPerProject = new(PathComparer.Instance);
92
97
  // projectPath tfm packageName packageVersion
93
98
 
@@ -106,47 +111,49 @@ internal static class SdkProjectDiscovery
106
111
  Dictionary<string, HashSet<string>> additionalFiles = new(PathComparer.Instance);
107
112
  // projectPath additionalFiles
108
113
 
109
- var requiresManualPackageResolution = false;
110
- var discoveredTfms = experimentsManager.UseSingleRestore
111
- ? ["unused-for-single-restore"] // we need to ensure we do the following just once because it'll restore all relevant projects
112
- : await MSBuildHelper.GetTargetFrameworkValuesFromProject(repoRootPath, startingProjectPath, logger);
113
- foreach (var discoveredTfm in discoveredTfms)
114
+ // due to how MSBuild handles multi-TFM projects with target platforms we may need to process each TFM separately
115
+ // we detect that by determining if there are multiple target frameworks specified and if any of them have a platform suffix (e.g., `-windows`, `-android`, etc)
116
+ var projectTfms = await MSBuildHelper.GetProjectTargetFrameworksAsync(startingProjectPath, logger);
117
+ var requiresIndividualRestores = projectTfms.Any(tfm => tfm.Contains('-'));
118
+ if (!requiresIndividualRestores)
119
+ {
120
+ projectTfms = [string.Empty]; // a single restore can handle everything, but we need to loop at least once and an empty TFM is our signal to not specify anything
121
+ }
122
+
123
+ foreach (var tfm in projectTfms)
114
124
  {
125
+ var isSingleTfmRestore = !string.IsNullOrEmpty(tfm);
126
+
115
127
  // create a binlog
116
128
  var binLogPath = Path.Combine(Path.GetTempPath(), $"msbuild_{Guid.NewGuid():d}.binlog");
117
129
  try
118
130
  {
119
- // the built-in target `GenerateBuildDependencyFile` forces resolution of all NuGet packages, but doesn't invoke a full build
131
+ // when using single restore, we can directly invoke the relevant targets
132
+ var args = new List<string>() { "msbuild", startingProjectPath };
133
+ var targets = await MSBuildHelper.GetProjectTargetsAsync(startingProjectPath, logger);
134
+ var useDirectRestore = SingleRestoreTargetNames.All(targets.Contains);
135
+ if (useDirectRestore || isSingleTfmRestore)
136
+ {
137
+ // directly call the required targets
138
+ args.Add($"/t:{string.Join(",", SingleRestoreTargetNames)}");
139
+ }
140
+ else
141
+ {
142
+ // delegate to the inner build and call those targets
143
+ args.Add("/t:Build");
144
+ args.Add($"/p:InnerTargets=\"{string.Join(";", SingleRestoreTargetNames)}\"");
145
+ }
146
+
147
+ // inject various props and targets to help with discovery
120
148
  var dependencyDiscoveryTargetingPacksPropsPath = MSBuildHelper.GetFileFromRuntimeDirectory("DependencyDiscoveryTargetingPacks.props");
121
149
  var dependencyDiscoveryTargetsPath = MSBuildHelper.GetFileFromRuntimeDirectory("DependencyDiscovery.targets");
122
- var args = new List<string>()
123
- {
124
- "build",
125
- startingProjectPath,
126
- "/t:_DiscoverDependencies",
127
- $"/p:TargetFramework={discoveredTfm}",
128
- $"/p:CustomBeforeMicrosoftCommonProps={dependencyDiscoveryTargetingPacksPropsPath}",
129
- $"/p:CustomAfterMicrosoftCommonCrossTargetingTargets={dependencyDiscoveryTargetsPath}",
130
- $"/p:CustomAfterMicrosoftCommonTargets={dependencyDiscoveryTargetsPath}",
131
- };
132
-
133
- if (experimentsManager.UseSingleRestore)
150
+ args.Add($"/p:CustomBeforeMicrosoftCommonProps={dependencyDiscoveryTargetingPacksPropsPath}");
151
+ args.Add($"/p:CustomAfterMicrosoftCommonCrossTargetingTargets={dependencyDiscoveryTargetsPath}");
152
+ args.Add($"/p:CustomAfterMicrosoftCommonTargets={dependencyDiscoveryTargetsPath}");
153
+
154
+ if (isSingleTfmRestore)
134
155
  {
135
- // when using single restore, we can directly invoke the relevant targets
136
- args = ["msbuild", startingProjectPath];
137
- var targets = await MSBuildHelper.GetProjectTargetsAsync(startingProjectPath, logger);
138
- var useDirectRestore = SingleRestoreTargetNames.All(targets.Contains);
139
- if (useDirectRestore)
140
- {
141
- // directly call the required targets
142
- args.Add($"/t:{string.Join(",", SingleRestoreTargetNames)}");
143
- }
144
- else
145
- {
146
- // delegate to the inner build and call those targets
147
- args.Add("/t:Build");
148
- args.Add($"/p:InnerTargets=\"{string.Join(";", SingleRestoreTargetNames)}\"");
149
- }
156
+ args.Add($"/p:TargetFramework={tfm}");
150
157
  }
151
158
 
152
159
  // if using CPM and a project also sets TreatWarningsAsErrors to true, this can cause discovery to fail; explicitly don't allow that
@@ -166,11 +173,6 @@ internal static class SdkProjectDiscovery
166
173
  }
167
174
 
168
175
  MSBuildHelper.ThrowOnError(stdOut);
169
- if (stdOut.Contains("_DependencyDiscovery_LegacyProjects::UseTemporaryProject"))
170
- {
171
- // special case - legacy project with <PackageReference> elements; this requires extra handling below
172
- requiresManualPackageResolution = true;
173
- }
174
176
  if (exitCode != 0)
175
177
  {
176
178
  // log error, but still try to resolve what we can
@@ -200,8 +202,8 @@ internal static class SdkProjectDiscovery
200
202
  // props and targets files might have been imported from these, but they're not to be considered as dependency files
201
203
  var forbiddenDirectories = new[]
202
204
  {
203
- GetPropertyValueFromProjectEvaluation(projectEvaluation, "BaseIntermediateOutputPath"), // e.g., "obj/"
204
- GetPropertyValueFromProjectEvaluation(projectEvaluation, "BaseOutputPath"), // e.g., "bin/"
205
+ GetPropertyValueFromProjectEvaluation(projectEvaluation, "BaseIntermediateOutputPath"), // e.g., "obj/"
206
+ GetPropertyValueFromProjectEvaluation(projectEvaluation, "BaseOutputPath"), // e.g., "bin/"
205
207
  }
206
208
  .Where(p => !string.IsNullOrEmpty(p))
207
209
  .Select(p => Path.Combine(Path.GetDirectoryName(projectEvaluation.ProjectFile)!, p!))
@@ -220,7 +222,7 @@ internal static class SdkProjectDiscovery
220
222
  }
221
223
  break;
222
224
  case NamedNode namedNode when namedNode is AddItem or RemoveItem:
223
- ProcessResolvedPackageReference(namedNode, packagesPerProject, topLevelPackagesPerProject, explicitPackageVersionsPerProject);
225
+ ProcessResolvedPackageReference(namedNode, packagesPerProject, topLevelPackagesPerProject, explicitPackageVersionsPerProject, packageReferenceElementCounts);
224
226
 
225
227
  if (namedNode is AddItem addItem)
226
228
  {
@@ -369,6 +371,23 @@ internal static class SdkProjectDiscovery
369
371
  }
370
372
  }
371
373
 
374
+ var requiresManualPackageResolution = false;
375
+ foreach (var projectPath in resolvedProperties.Keys)
376
+ {
377
+ var projectProperties = resolvedProperties[projectPath];
378
+ var isProjectLegacy = !projectProperties.ContainsKey("NETCoreSdkVersion"); // legacy projects don't contain this property
379
+ if (isProjectLegacy)
380
+ {
381
+ // if any TFM had any explicit packages defined, we need to do manual package resolution
382
+ if (explicitPackageVersionsPerProject.TryGetValue(projectPath, out var projectTfmRefs) &&
383
+ projectTfmRefs.Values.Any(v => v.Count > 0))
384
+ {
385
+ requiresManualPackageResolution = true;
386
+ break;
387
+ }
388
+ }
389
+ }
390
+
372
391
  if (requiresManualPackageResolution)
373
392
  {
374
393
  // we were able to collect all <PackageReference> elements, but no transitive dependencies were resolved
@@ -376,7 +395,6 @@ internal static class SdkProjectDiscovery
376
395
  packagesPerProject = await RebuildPackagesPerProject(
377
396
  repoRootPath,
378
397
  startingProjectPath,
379
- discoveredTfms,
380
398
  packagesPerProject,
381
399
  explicitPackageVersionsPerProject,
382
400
  experimentsManager,
@@ -601,13 +619,18 @@ internal static class SdkProjectDiscovery
601
619
  private static async Task<Dictionary<string, Dictionary<string, Dictionary<string, string>>>> RebuildPackagesPerProject(
602
620
  string repoRootPath,
603
621
  string projectPath,
604
- ImmutableArray<string> targetFrameworks,
605
622
  Dictionary<string, Dictionary<string, Dictionary<string, string>>> packagesPerProject,
606
623
  Dictionary<string, Dictionary<string, Dictionary<string, string>>> explicitPackageVersionsPerProject,
607
624
  ExperimentsManager experimentsManager,
608
625
  ILogger logger
609
626
  )
610
627
  {
628
+ // the secondary keys of these are TFMs
629
+ var targetFrameworks = packagesPerProject.Values.SelectMany(p => p.Keys)
630
+ .Concat(explicitPackageVersionsPerProject.Values.SelectMany(p => p.Keys))
631
+ .Distinct(StringComparer.OrdinalIgnoreCase)
632
+ .OrderBy(tfm => tfm)
633
+ .ToImmutableArray();
611
634
  var tempDirectory = Directory.CreateTempSubdirectory("legacy-package-reference-resolution_");
612
635
  try
613
636
  {
@@ -651,7 +674,8 @@ internal static class SdkProjectDiscovery
651
674
  NamedNode node,
652
675
  Dictionary<string, Dictionary<string, Dictionary<string, string>>> packagesPerProject, // projectPath -> tfm -> (packageName, packageVersion)
653
676
  Dictionary<string, Dictionary<string, HashSet<string>>> topLevelPackagesPerProject, // projectPath -> tfm -> packageName
654
- Dictionary<string, Dictionary<string, Dictionary<string, string>>> packageVersionsPerProject // projectPath -> tfm -> (packageName, packageVersion)
677
+ Dictionary<string, Dictionary<string, Dictionary<string, string>>> packageVersionsPerProject, // projectPath -> tfm -> (packageName, packageVersion)
678
+ Dictionary<string, int> packageReferenceElementCounts // projectPath -> count of `<PackageReference>` elements
655
679
  )
656
680
  {
657
681
  var doRemoveOperation = node is RemoveItem;
@@ -670,7 +694,11 @@ internal static class SdkProjectDiscovery
670
694
  continue;
671
695
  }
672
696
 
673
- var tfm = GetPropertyValueFromProjectEvaluation(projectEvaluation, "TargetFramework");
697
+ // count instances of `<PackageReference>`
698
+ //var packageReferenceElements = packageReferenceElementCounts.GetOrAdd(projectEvaluation.ProjectFile, () => 0);
699
+ //packageReferenceElementCounts[projectEvaluation.ProjectFile] = packageReferenceElements + 1;
700
+
701
+ var tfm = GetTargetFrameworkFromProjectEvaluation(projectEvaluation);
674
702
  if (tfm is not null)
675
703
  {
676
704
  var topLevelPackages = topLevelPackagesPerProject.GetOrAdd(projectEvaluation.ProjectFile, () => new(StringComparer.OrdinalIgnoreCase));
@@ -838,4 +866,32 @@ internal static class SdkProjectDiscovery
838
866
 
839
867
  return property.Value;
840
868
  }
869
+
870
+ private static string? GetTargetFrameworkFromProjectEvaluation(ProjectEvaluation projectEvaluation)
871
+ {
872
+ // try direct access of SDK-style property
873
+ var tfm = GetPropertyValueFromProjectEvaluation(projectEvaluation, "TargetFramework");
874
+ if (tfm is null)
875
+ {
876
+ // fall back to legacy properties
877
+ var frameworkMoniker = GetPropertyValueFromProjectEvaluation(projectEvaluation, "TargetFrameworkMoniker");
878
+ if (frameworkMoniker is not null)
879
+ {
880
+ var platformMoniker = GetPropertyValueFromProjectEvaluation(projectEvaluation, "TargetPlatformMoniker");
881
+ try
882
+ {
883
+ var framework = string.IsNullOrEmpty(platformMoniker)
884
+ ? NuGetFramework.Parse(frameworkMoniker)
885
+ : NuGetFramework.ParseComponents(frameworkMoniker, platformMoniker);
886
+ tfm = framework.GetShortFolderName();
887
+ }
888
+ catch
889
+ {
890
+ // if unable to parse, retain null
891
+ }
892
+ }
893
+ }
894
+
895
+ return tfm;
896
+ }
841
897
  }
@@ -8,14 +8,12 @@ namespace NuGetUpdater.Core;
8
8
  public record ExperimentsManager
9
9
  {
10
10
  public bool GenerateSimplePrBody { get; init; } = false;
11
- public bool UseSingleRestore { get; init; } = false;
12
11
 
13
12
  public Dictionary<string, object> ToDictionary()
14
13
  {
15
14
  return new()
16
15
  {
17
16
  ["nuget_generate_simple_pr_body"] = GenerateSimplePrBody,
18
- ["nuget_use_single_restore"] = UseSingleRestore,
19
17
  };
20
18
  }
21
19
 
@@ -24,7 +22,6 @@ public record ExperimentsManager
24
22
  return new ExperimentsManager()
25
23
  {
26
24
  GenerateSimplePrBody = IsEnabled(experiments, "nuget_generate_simple_pr_body"),
27
- UseSingleRestore = IsEnabled(experiments, "nuget_use_single_restore"),
28
25
  };
29
26
  }
30
27
 
@@ -13,15 +13,24 @@ public record DependencyGroup
13
13
  // "patterns" => string[] where each element is a wildcard name pattern
14
14
  // "exclude-patterns"=> string[] where each element is a wildcard name pattern
15
15
  // "dependency-type" => production|development // not used for nuget?
16
+ // "update-types" => string[] where each element is one of major|minor|patch
16
17
  public Dictionary<string, object> Rules { get; init; } = new();
17
18
 
18
19
  public GroupMatcher GetGroupMatcher() => GroupMatcher.FromRules(Rules);
19
20
  }
20
21
 
22
+ public enum GroupUpdateType
23
+ {
24
+ Major,
25
+ Minor,
26
+ Patch,
27
+ }
28
+
21
29
  public class GroupMatcher
22
30
  {
23
31
  public ImmutableArray<string> Patterns { get; init; } = ImmutableArray<string>.Empty;
24
32
  public ImmutableArray<string> ExcludePatterns { get; init; } = ImmutableArray<string>.Empty;
33
+ public ImmutableArray<GroupUpdateType> UpdateTypes { get; init; } = ImmutableArray<GroupUpdateType>.Empty;
25
34
 
26
35
  public bool IsMatch(string dependencyName)
27
36
  {
@@ -35,11 +44,21 @@ public class GroupMatcher
35
44
  {
36
45
  var patterns = GetStringArray(rules, "patterns", ["*"]); // default to matching everything unless explicitly excluded
37
46
  var excludePatterns = GetStringArray(rules, "exclude-patterns", []);
47
+ var updateTypes = GetStringArray(rules, "update-types", ["major", "minor", "patch"]) // default to everything unless explicitly specified
48
+ .Select(s => s.ToLowerInvariant() switch
49
+ {
50
+ "major" => GroupUpdateType.Major,
51
+ "minor" => GroupUpdateType.Minor,
52
+ "patch" => GroupUpdateType.Patch,
53
+ _ => throw new InvalidOperationException($"Unknown update type: {s}"),
54
+ })
55
+ .ToImmutableArray();
38
56
 
39
57
  return new GroupMatcher()
40
58
  {
41
59
  Patterns = patterns,
42
60
  ExcludePatterns = excludePatterns,
61
+ UpdateTypes = updateTypes,
43
62
  };
44
63
  }
45
64
 
@@ -42,7 +42,7 @@ public sealed record Job
42
42
  public int MaxUpdaterRunTime { get; init; } = 0;
43
43
  public Cooldown? Cooldown { get; init; } = null;
44
44
 
45
- public ImmutableArray<string> GetAllDirectories()
45
+ public ImmutableArray<string> GetRawDirectories()
46
46
  {
47
47
  var builder = ImmutableArray.CreateBuilder<string>();
48
48
  if (Source.Directory is not null)
@@ -59,6 +59,27 @@ public sealed record Job
59
59
  return builder.ToImmutable();
60
60
  }
61
61
 
62
+ public ImmutableArray<string> GetAllDirectories(string repoRoot)
63
+ {
64
+ // where possible we want to maintain the order of the specified directories, so we have to manually handle each one
65
+ var seenDirectories = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
66
+ var rawDirectories = GetRawDirectories();
67
+ var result = new List<string>();
68
+ foreach (var directory in rawDirectories)
69
+ {
70
+ var expandedDirectories = PathHelper.GetMatchingDirectoriesUnder(repoRoot, directory, caseSensitive: false);
71
+ foreach (var expanded in expandedDirectories)
72
+ {
73
+ if (seenDirectories.Add(expanded))
74
+ {
75
+ result.Add(expanded);
76
+ }
77
+ }
78
+ }
79
+
80
+ return [.. result];
81
+ }
82
+
62
83
  public ImmutableArray<DependencyGroup> GetRelevantDependencyGroups()
63
84
  {
64
85
  var appliesToKey = SecurityUpdatesOnly ? "security-updates" : "version-updates";
@@ -121,7 +142,7 @@ public sealed record Job
121
142
  }
122
143
 
123
144
  var version = NuGetVersion.Parse(dependency.Version);
124
- var dependencyInfo = RunWorker.GetDependencyInfo(this, dependency, allowCooldown: false);
145
+ var dependencyInfo = RunWorker.GetDependencyInfo(this, dependency, groupMatchers: [], allowCooldown: false);
125
146
  var isVulnerable = dependencyInfo.Vulnerabilities.Any(v => v.IsVulnerable(version));
126
147
 
127
148
  bool IsAllowed(AllowedUpdate allowedUpdate)
@@ -73,13 +73,15 @@ public abstract record JobErrorBase : MessageBase
73
73
  case InvalidDataException invalidData when invalidData.Message == "Central Directory corrupt.":
74
74
  return new PrivateSourceBadResponse(NuGetContext.GetPackageSourceUrls(currentDirectory), invalidData.Message);
75
75
  case InvalidProjectFileException invalidProjectFile:
76
- return new DependencyFileNotParseable(invalidProjectFile.ProjectFile);
76
+ return new DependencyFileNotParseable(Path.GetRelativePath(currentDirectory, invalidProjectFile.ProjectFile).NormalizePathToUnix());
77
+ case IOException ioException when ioException.Message.Contains("No space left on device", StringComparison.OrdinalIgnoreCase):
78
+ return new OutOfDisk();
77
79
  case MissingFileException missingFile:
78
80
  return new DependencyFileNotFound(missingFile.FilePath, missingFile.Message);
79
81
  case PrivateSourceTimedOutException timeout:
80
82
  return new PrivateSourceTimedOut(timeout.Url);
81
83
  case UnparseableFileException unparseableFile:
82
- return new DependencyFileNotParseable(unparseableFile.FilePath, unparseableFile.Message);
84
+ return new DependencyFileNotParseable(Path.GetRelativePath(currentDirectory, unparseableFile.FilePath).NormalizePathToUnix(), unparseableFile.Message);
83
85
  case UpdateNotPossibleException updateNotPossible:
84
86
  return new UpdateNotPossible(updateNotPossible.Dependencies);
85
87
  default:
@@ -0,0 +1,9 @@
1
+ namespace NuGetUpdater.Core.Run.ApiModel;
2
+
3
+ public record OutOfDisk : JobErrorBase
4
+ {
5
+ public OutOfDisk()
6
+ : base("out_of_disk")
7
+ {
8
+ }
9
+ }
@@ -30,7 +30,17 @@ public static class IApiHandlerExtensions
30
30
  }
31
31
  }
32
32
 
33
- public static Task UpdateDependencyList(this IApiHandler handler, UpdatedDependencyList updatedDependencyList) => handler.PostAsJson("update_dependency_list", updatedDependencyList);
33
+ public static async Task UpdateDependencyList(this IApiHandler handler, UpdatedDependencyList updatedDependencyList)
34
+ {
35
+ if (updatedDependencyList.Dependencies.Length == 0 && updatedDependencyList.DependencyFiles.Length == 0)
36
+ {
37
+ // directory wildcard expansion can lead to empty directories; don't report those
38
+ return;
39
+ }
40
+
41
+ await handler.PostAsJson("update_dependency_list", updatedDependencyList);
42
+ }
43
+
34
44
  public static Task IncrementMetric(this IApiHandler handler, IncrementMetric incrementMetric) => handler.PostAsJson("increment_metric", incrementMetric);
35
45
  public static Task CreatePullRequest(this IApiHandler handler, CreatePullRequest createPullRequest) => handler.PostAsJson("create_pull_request", createPullRequest);
36
46
  public static Task ClosePullRequest(this IApiHandler handler, ClosePullRequest closePullRequest) => handler.PostAsJson("close_pull_request", closePullRequest);
@@ -157,7 +157,7 @@ public class RunWorker
157
157
  }
158
158
  }
159
159
 
160
- internal static DependencyInfo GetDependencyInfo(Job job, Dependency dependency, bool allowCooldown)
160
+ internal static DependencyInfo GetDependencyInfo(Job job, Dependency dependency, IEnumerable<GroupMatcher> groupMatchers, bool allowCooldown)
161
161
  {
162
162
  var dependencyVersion = NuGetVersion.Parse(dependency.Version!);
163
163
  var securityAdvisories = job.SecurityAdvisories.Where(s => s.DependencyName.Equals(dependency.Name, StringComparison.OrdinalIgnoreCase)).ToArray();
@@ -178,8 +178,29 @@ public class RunWorker
178
178
  var ignoredUpdateTypes = job.IgnoreConditions
179
179
  .Where(c => FileSystemName.MatchesSimpleExpression(c.DependencyName, dependency.Name))
180
180
  .SelectMany(c => c.UpdateTypes ?? [])
181
- .Distinct()
182
- .ToImmutableArray();
181
+ .ToHashSet();
182
+
183
+ // if an update type isn't explicitly allowed by a group matcher, add it to the ignored set
184
+ foreach (var groupMatcher in groupMatchers)
185
+ {
186
+ if (groupMatcher.IsMatch(dependency.Name))
187
+ {
188
+ // group update types defaults to everything, so if it's not allowed then it's to be ignored
189
+ var allowedUpdateTypes = new HashSet<GroupUpdateType>(groupMatcher.UpdateTypes);
190
+ if (!allowedUpdateTypes.Contains(GroupUpdateType.Major))
191
+ {
192
+ ignoredUpdateTypes.Add(ConditionUpdateType.SemVerMajor);
193
+ }
194
+ if (!allowedUpdateTypes.Contains(GroupUpdateType.Minor))
195
+ {
196
+ ignoredUpdateTypes.Add(ConditionUpdateType.SemVerMinor);
197
+ }
198
+ if (!allowedUpdateTypes.Contains(GroupUpdateType.Patch))
199
+ {
200
+ ignoredUpdateTypes.Add(ConditionUpdateType.SemVerPatch);
201
+ }
202
+ }
203
+ }
183
204
 
184
205
  // while it would be nice to lift the cooldown options into the IgnoredUpdateTypes field, we don't know the
185
206
  // publish date of the packages, so we have to pass along the whole object for the version finder to sort out
@@ -193,7 +214,7 @@ public class RunWorker
193
214
  IsVulnerable = isVulnerable,
194
215
  IgnoredVersions = ignoredVersions,
195
216
  Vulnerabilities = vulnerabilities,
196
- IgnoredUpdateTypes = ignoredUpdateTypes,
217
+ IgnoredUpdateTypes = [.. ignoredUpdateTypes.OrderBy(t => t)],
197
218
  Cooldown = includeCooldown ? job.Cooldown : null,
198
219
  };
199
220
  return dependencyInfo;
@@ -31,7 +31,7 @@ internal class CreateSecurityUpdatePullRequestHandler : IUpdateHandler
31
31
  {
32
32
  var repoContentsPath = caseInsensitiveRepoContentsPath ?? originalRepoContentsPath;
33
33
  var jobDependencies = job.Dependencies.ToHashSet(StringComparer.OrdinalIgnoreCase);
34
- foreach (var directory in job.GetAllDirectories())
34
+ foreach (var directory in job.GetAllDirectories(repoContentsPath.FullName))
35
35
  {
36
36
  var discoveryResult = await discoveryWorker.RunAsync(repoContentsPath.FullName, directory);
37
37
  logger.ReportDiscovery(discoveryResult);
@@ -67,7 +67,7 @@ internal class CreateSecurityUpdatePullRequestHandler : IUpdateHandler
67
67
  {
68
68
  var dependencyName = dependencyGroupToUpdate.Key;
69
69
  var vulnerableCandidateDependenciesToUpdate = dependencyGroupToUpdate.Value
70
- .Select(o => (o.ProjectPath, o.Dependency, RunWorker.GetDependencyInfo(job, o.Dependency, allowCooldown: false)))
70
+ .Select(o => (o.ProjectPath, o.Dependency, RunWorker.GetDependencyInfo(job, o.Dependency, groupMatchers: [], allowCooldown: false)))
71
71
  .Where(set => set.Item3.IsVulnerable)
72
72
  .ToArray();
73
73
  var vulnerableDependenciesToUpdate = vulnerableCandidateDependenciesToUpdate
@@ -56,7 +56,7 @@ internal class GroupUpdateAllVersionsHandler : IUpdateHandler
56
56
  var updateOperationsPerformed = new List<UpdateOperationBase>();
57
57
  var updatedDependencies = new List<ReportedDependency>();
58
58
  var allUpdatedDependencyFiles = ImmutableArray.Create<DependencyFile>();
59
- foreach (var directory in job.GetAllDirectories())
59
+ foreach (var directory in job.GetAllDirectories(repoContentsPath.FullName))
60
60
  {
61
61
  var discoveryResult = await discoveryWorker.RunAsync(repoContentsPath.FullName, directory);
62
62
  logger.ReportDiscovery(discoveryResult);
@@ -91,7 +91,7 @@ internal class GroupUpdateAllVersionsHandler : IUpdateHandler
91
91
  continue;
92
92
  }
93
93
 
94
- var dependencyInfo = RunWorker.GetDependencyInfo(job, dependency, allowCooldown: true);
94
+ var dependencyInfo = RunWorker.GetDependencyInfo(job, dependency, groupMatchers: [groupMatcher], allowCooldown: true);
95
95
  var analysisResult = await analyzeWorker.RunAsync(repoContentsPath.FullName, discoveryResult, dependencyInfo);
96
96
  if (analysisResult.Error is not null)
97
97
  {
@@ -169,7 +169,7 @@ internal class GroupUpdateAllVersionsHandler : IUpdateHandler
169
169
  private async Task RunUngroupedDependencyUpdates(Job job, DirectoryInfo originalRepoContentsPath, DirectoryInfo? caseInsensitiveRepoContentsPath, string baseCommitSha, IDiscoveryWorker discoveryWorker, IAnalyzeWorker analyzeWorker, IUpdaterWorker updaterWorker, IApiHandler apiHandler, ExperimentsManager experimentsManager, ILogger logger)
170
170
  {
171
171
  var repoContentsPath = caseInsensitiveRepoContentsPath ?? originalRepoContentsPath;
172
- foreach (var directory in job.GetAllDirectories())
172
+ foreach (var directory in job.GetAllDirectories(repoContentsPath.FullName))
173
173
  {
174
174
  var discoveryResult = await discoveryWorker.RunAsync(repoContentsPath.FullName, directory);
175
175
  logger.ReportDiscovery(discoveryResult);
@@ -213,7 +213,7 @@ internal class GroupUpdateAllVersionsHandler : IUpdateHandler
213
213
  continue;
214
214
  }
215
215
 
216
- var dependencyInfo = RunWorker.GetDependencyInfo(job, dependency, allowCooldown: true);
216
+ var dependencyInfo = RunWorker.GetDependencyInfo(job, dependency, groupMatchers: [], allowCooldown: true);
217
217
  var analysisResult = await analyzeWorker.RunAsync(repoContentsPath.FullName, discoveryResult, dependencyInfo);
218
218
  if (analysisResult.Error is not null)
219
219
  {
@@ -21,7 +21,7 @@ internal class RefreshGroupUpdatePullRequestHandler : IUpdateHandler
21
21
  return false;
22
22
  }
23
23
 
24
- if (job.GetAllDirectories().Length > 1)
24
+ if (job.GetRawDirectories().Length > 1)
25
25
  {
26
26
  return true;
27
27
  }
@@ -62,7 +62,7 @@ internal class RefreshGroupUpdatePullRequestHandler : IUpdateHandler
62
62
 
63
63
  var groupMatcher = group.GetGroupMatcher();
64
64
  var jobDependencies = job.Dependencies.ToHashSet(StringComparer.OrdinalIgnoreCase);
65
- foreach (var directory in job.GetAllDirectories())
65
+ foreach (var directory in job.GetAllDirectories(repoContentsPath.FullName))
66
66
  {
67
67
  var discoveryResult = await discoveryWorker.RunAsync(repoContentsPath.FullName, directory);
68
68
  logger.ReportDiscovery(discoveryResult);
@@ -93,7 +93,7 @@ internal class RefreshGroupUpdatePullRequestHandler : IUpdateHandler
93
93
  var dependencyName = dependencyGroupToUpdate.Key;
94
94
  var relevantDependenciesToUpdate = dependencyGroupToUpdate.Value
95
95
  .Where(o => !job.IsDependencyIgnoredByNameOnly(o.Dependency.Name))
96
- .Select(o => (o.ProjectPath, o.Dependency, RunWorker.GetDependencyInfo(job, o.Dependency, allowCooldown: true)))
96
+ .Select(o => (o.ProjectPath, o.Dependency, RunWorker.GetDependencyInfo(job, o.Dependency, groupMatchers: [groupMatcher], allowCooldown: true)))
97
97
  .ToArray();
98
98
 
99
99
  foreach (var (projectPath, dependency, dependencyInfo) in relevantDependenciesToUpdate)
@@ -30,7 +30,7 @@ internal class RefreshSecurityUpdatePullRequestHandler : IUpdateHandler
30
30
  {
31
31
  var repoContentsPath = caseInsensitiveRepoContentsPath ?? originalRepoContentsPath;
32
32
  var jobDependencies = job.Dependencies.ToHashSet(StringComparer.OrdinalIgnoreCase);
33
- foreach (var directory in job.GetAllDirectories())
33
+ foreach (var directory in job.GetAllDirectories(repoContentsPath.FullName))
34
34
  {
35
35
  var discoveryResult = await discoveryWorker.RunAsync(repoContentsPath.FullName, directory);
36
36
  logger.ReportDiscovery(discoveryResult);
@@ -82,7 +82,7 @@ internal class RefreshSecurityUpdatePullRequestHandler : IUpdateHandler
82
82
  var dependencyName = dependencyGroupToUpdate.Key;
83
83
  var vulnerableDependenciesToUpdate = dependencyGroupToUpdate.Value
84
84
  .Where(o => !job.IsDependencyIgnoredByNameOnly(o.Dependency.Name))
85
- .Select(o => (o.ProjectPath, o.Dependency, RunWorker.GetDependencyInfo(job, o.Dependency, allowCooldown: false)))
85
+ .Select(o => (o.ProjectPath, o.Dependency, RunWorker.GetDependencyInfo(job, o.Dependency, groupMatchers: [], allowCooldown: false)))
86
86
  .Where(set => set.Item3.IsVulnerable)
87
87
  .ToArray();
88
88