dependabot-nuget 0.292.0 → 0.293.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 (70) hide show
  1. checksums.yaml +4 -4
  2. data/helpers/lib/NuGetUpdater/.gitignore +1 -0
  3. data/helpers/lib/NuGetUpdater/Directory.Packages.props +1 -0
  4. data/helpers/lib/NuGetUpdater/DotNetPackageCorrelation/Correlator.cs +197 -0
  5. data/helpers/lib/NuGetUpdater/DotNetPackageCorrelation/DotNetPackageCorrelation.csproj +12 -0
  6. data/helpers/lib/NuGetUpdater/DotNetPackageCorrelation/Model/PackageMapper.cs +68 -0
  7. data/helpers/lib/NuGetUpdater/DotNetPackageCorrelation/Model/PackageSet.cs +11 -0
  8. data/helpers/lib/NuGetUpdater/DotNetPackageCorrelation/Model/Release.cs +25 -0
  9. data/helpers/lib/NuGetUpdater/DotNetPackageCorrelation/Model/ReleasesFile.cs +9 -0
  10. data/helpers/lib/NuGetUpdater/DotNetPackageCorrelation/Model/RuntimePackages.cs +11 -0
  11. data/helpers/lib/NuGetUpdater/DotNetPackageCorrelation/Model/Sdk.cs +13 -0
  12. data/helpers/lib/NuGetUpdater/DotNetPackageCorrelation/Model/SemVerComparer.cs +16 -0
  13. data/helpers/lib/NuGetUpdater/DotNetPackageCorrelation/Model/SemVersionConverter.cs +42 -0
  14. data/helpers/lib/NuGetUpdater/DotNetPackageCorrelation.Cli/DotNetPackageCorrelation.Cli.csproj +16 -0
  15. data/helpers/lib/NuGetUpdater/DotNetPackageCorrelation.Cli/Program.cs +32 -0
  16. data/helpers/lib/NuGetUpdater/DotNetPackageCorrelation.Test/CorrelatorTests.cs +99 -0
  17. data/helpers/lib/NuGetUpdater/DotNetPackageCorrelation.Test/DotNetPackageCorrelation.Test.csproj +18 -0
  18. data/helpers/lib/NuGetUpdater/DotNetPackageCorrelation.Test/EndToEndTests.cs +30 -0
  19. data/helpers/lib/NuGetUpdater/DotNetPackageCorrelation.Test/RuntimePackagesTests.cs +206 -0
  20. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/AnalyzeCommand.cs +6 -4
  21. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/DiscoverCommand.cs +8 -7
  22. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/RunCommand.cs +4 -4
  23. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/UpdateCommand.cs +17 -5
  24. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Analyze.cs +7 -1
  25. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Discover.cs +46 -6
  26. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Update.cs +8 -0
  27. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalyzeWorker.cs +8 -17
  28. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/DependencyFinder.cs +4 -4
  29. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/Extensions.cs +1 -1
  30. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Clone/CloneWorker.cs +2 -1
  31. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/DependencyDiscovery.targets +2 -2
  32. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs +7 -20
  33. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/ProjectDiscoveryResult.cs +3 -3
  34. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/SdkProjectDiscovery.cs +99 -2
  35. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/EnsureDotNetPackageCorrelation.targets +25 -0
  36. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/ExperimentsManager.cs +9 -22
  37. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/PackagesConfigBuildFile.cs +5 -1
  38. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/MissingFileException.cs +2 -1
  39. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/NativeResult.cs +3 -3
  40. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/NuGetUpdater.Core.csproj +3 -0
  41. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/DependencyFileNotFound.cs +7 -3
  42. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/DependencyFileNotParseable.cs +15 -0
  43. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/JobErrorBase.cs +24 -0
  44. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/RunWorker.cs +6 -21
  45. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/UnparseableFileException.cs +12 -0
  46. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/PackageReferenceUpdater.cs +21 -0
  47. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/UpdaterWorker.cs +6 -30
  48. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/DotNetPackageCorrelationManager.cs +46 -0
  49. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs +51 -27
  50. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/NuGetHelper.cs +1 -1
  51. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/AnalyzeWorkerTestBase.cs +15 -4
  52. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/AnalyzeWorkerTests.cs +15 -9
  53. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTestBase.cs +20 -12
  54. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.Project.cs +108 -0
  55. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.cs +16 -12
  56. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/ExpectedDiscoveryResults.cs +1 -1
  57. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/MockNuGetPackage.cs +15 -28
  58. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/RunWorkerTests.cs +5 -4
  59. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/SerializationTests.cs +9 -1
  60. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/TestBase.cs +24 -0
  61. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/ExpectedUpdateOperationResult.cs +1 -1
  62. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTestBase.cs +11 -15
  63. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.DirsProj.cs +1 -1
  64. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.Mixed.cs +14 -8
  65. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.PackageReference.cs +148 -3
  66. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.PackagesConfig.cs +12 -14
  67. data/helpers/lib/NuGetUpdater/NuGetUpdater.sln +18 -1
  68. data/lib/dependabot/nuget/native_helpers.rb +41 -16
  69. metadata +25 -6
  70. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/ErrorType.cs +0 -12
@@ -3,6 +3,7 @@ using System.Text.Json;
3
3
  using System.Text.Json.Serialization;
4
4
 
5
5
  using NuGetUpdater.Core.Analyze;
6
+ using NuGetUpdater.Core.Run.ApiModel;
6
7
  using NuGetUpdater.Core.Updater;
7
8
  using NuGetUpdater.Core.Utilities;
8
9
 
@@ -10,6 +11,7 @@ namespace NuGetUpdater.Core;
10
11
 
11
12
  public class UpdaterWorker : IUpdaterWorker
12
13
  {
14
+ private readonly string _jobId;
13
15
  private readonly ExperimentsManager _experimentsManager;
14
16
  private readonly ILogger _logger;
15
17
  private readonly HashSet<string> _processedProjectPaths = new(StringComparer.OrdinalIgnoreCase);
@@ -20,8 +22,9 @@ public class UpdaterWorker : IUpdaterWorker
20
22
  Converters = { new JsonStringEnumConverter() },
21
23
  };
22
24
 
23
- public UpdaterWorker(ExperimentsManager experimentsManager, ILogger logger)
25
+ public UpdaterWorker(string jobId, ExperimentsManager experimentsManager, ILogger logger)
24
26
  {
27
+ _jobId = jobId;
25
28
  _experimentsManager = experimentsManager;
26
29
  _logger = logger;
27
30
  }
@@ -43,42 +46,15 @@ public class UpdaterWorker : IUpdaterWorker
43
46
  {
44
47
  result = await RunAsync(repoRootPath, workspacePath, dependencyName, previousDependencyVersion, newDependencyVersion, isTransitive);
45
48
  }
46
- catch (HttpRequestException ex)
47
- when (ex.StatusCode == HttpStatusCode.Unauthorized || ex.StatusCode == HttpStatusCode.Forbidden)
49
+ catch (Exception ex)
48
50
  {
49
51
  if (!Path.IsPathRooted(workspacePath) || !File.Exists(workspacePath))
50
52
  {
51
53
  workspacePath = Path.GetFullPath(Path.Join(repoRootPath, workspacePath));
52
54
  }
53
-
54
- result = new()
55
- {
56
- ErrorType = ErrorType.AuthenticationFailure,
57
- ErrorDetails = "(" + string.Join("|", NuGetContext.GetPackageSourceUrls(workspacePath)) + ")",
58
- };
59
- }
60
- catch (MissingFileException ex)
61
- {
62
- result = new()
63
- {
64
- ErrorType = ErrorType.MissingFile,
65
- ErrorDetails = ex.FilePath,
66
- };
67
- }
68
- catch (UpdateNotPossibleException ex)
69
- {
70
- result = new()
71
- {
72
- ErrorType = ErrorType.UpdateNotPossible,
73
- ErrorDetails = ex.Dependencies,
74
- };
75
- }
76
- catch (Exception ex)
77
- {
78
55
  result = new()
79
56
  {
80
- ErrorType = ErrorType.Unknown,
81
- ErrorDetails = ex.ToString(),
57
+ Error = JobErrorBase.ErrorFromException(ex, _jobId, workspacePath),
82
58
  };
83
59
  }
84
60
 
@@ -0,0 +1,46 @@
1
+ using System.Text.Json;
2
+
3
+ using DotNetPackageCorrelation;
4
+
5
+ using NuGetUpdater.Core.Discover;
6
+
7
+ namespace NuGetUpdater.Core.Utilities;
8
+
9
+ internal static class DotNetPackageCorrelationManager
10
+ {
11
+ private static readonly PackageMapper _packageMapper;
12
+ private static readonly Dictionary<string, PackageMapper> _packageMapperByOverrideFile = new();
13
+
14
+ static DotNetPackageCorrelationManager()
15
+ {
16
+ var packageCorrelationPath = Path.Combine(Path.GetDirectoryName(typeof(SdkProjectDiscovery).Assembly.Location)!, "dotnet-package-correlation.json");
17
+ var runtimePackages = LoadRuntimePackagesFromFile(packageCorrelationPath);
18
+ _packageMapper = PackageMapper.Load(runtimePackages);
19
+ }
20
+
21
+ public static PackageMapper GetPackageMapper()
22
+ {
23
+ var packageCorrelationFileOverride = Environment.GetEnvironmentVariable("DOTNET_PACKAGE_CORRELATION_FILE_PATH");
24
+ if (packageCorrelationFileOverride is not null)
25
+ {
26
+ // this is used as a test hook to allow unit tests to be SDK agnostic
27
+ if (_packageMapperByOverrideFile.TryGetValue(packageCorrelationFileOverride, out var packageMapper))
28
+ {
29
+ return packageMapper;
30
+ }
31
+
32
+ var runtimePackages = LoadRuntimePackagesFromFile(packageCorrelationFileOverride);
33
+ packageMapper = PackageMapper.Load(runtimePackages);
34
+ _packageMapperByOverrideFile[packageCorrelationFileOverride] = packageMapper;
35
+ return packageMapper;
36
+ }
37
+
38
+ return _packageMapper;
39
+ }
40
+
41
+ private static RuntimePackages LoadRuntimePackagesFromFile(string filePath)
42
+ {
43
+ var packageCorrelationJson = File.ReadAllText(filePath);
44
+ return JsonSerializer.Deserialize<RuntimePackages>(packageCorrelationJson, Correlator.SerializerOptions)!;
45
+ }
46
+ }
@@ -19,6 +19,7 @@ using NuGet.Frameworks;
19
19
  using NuGet.Versioning;
20
20
 
21
21
  using NuGetUpdater.Core.Analyze;
22
+ using NuGetUpdater.Core.Discover;
22
23
  using NuGetUpdater.Core.Utilities;
23
24
 
24
25
  namespace NuGetUpdater.Core;
@@ -342,7 +343,7 @@ internal static partial class MSBuildHelper
342
343
  var tempDirectory = Directory.CreateTempSubdirectory("package-dependency-coherence_");
343
344
  try
344
345
  {
345
- var tempProjectPath = await CreateTempProjectAsync(tempDirectory, repoRoot, projectPath, targetFramework, packages, logger);
346
+ var tempProjectPath = await CreateTempProjectAsync(tempDirectory, repoRoot, projectPath, targetFramework, packages, experimentsManager, logger);
346
347
  var (exitCode, stdOut, stdErr) = await ProcessEx.RunDotnetWithoutMSBuildEnvironmentVariablesAsync(["restore", tempProjectPath], tempDirectory.FullName, experimentsManager);
347
348
 
348
349
  // NU1608: Detected package version outside of dependency constraint
@@ -362,7 +363,7 @@ internal static partial class MSBuildHelper
362
363
 
363
364
  try
364
365
  {
365
- string tempProjectPath = await CreateTempProjectAsync(tempDirectory, repoRoot, projectPath, targetFramework, packages, logger);
366
+ string tempProjectPath = await CreateTempProjectAsync(tempDirectory, repoRoot, projectPath, targetFramework, packages, experimentsManager, logger);
366
367
  var (exitCode, stdOut, stdErr) = await ProcessEx.RunDotnetWithoutMSBuildEnvironmentVariablesAsync(["restore", tempProjectPath], tempDirectory.FullName, experimentsManager);
367
368
 
368
369
  // Add Dependency[] packages to List<PackageToUpdate> existingPackages
@@ -518,7 +519,7 @@ internal static partial class MSBuildHelper
518
519
  var tempDirectory = Directory.CreateTempSubdirectory("package-dependency-coherence_");
519
520
  try
520
521
  {
521
- var tempProjectPath = await CreateTempProjectAsync(tempDirectory, repoRoot, projectPath, targetFramework, packages, logger);
522
+ var tempProjectPath = await CreateTempProjectAsync(tempDirectory, repoRoot, projectPath, targetFramework, packages, experimentsManager, logger);
522
523
  var (exitCode, stdOut, stdErr) = await ProcessEx.RunDotnetWithoutMSBuildEnvironmentVariablesAsync(["restore", tempProjectPath], tempDirectory.FullName, experimentsManager);
523
524
  ThrowOnUnauthenticatedFeed(stdOut);
524
525
 
@@ -668,12 +669,22 @@ internal static partial class MSBuildHelper
668
669
  string projectPath,
669
670
  string targetFramework,
670
671
  IReadOnlyCollection<Dependency> packages,
672
+ ExperimentsManager experimentsManager,
671
673
  ILogger logger,
672
674
  bool usePackageDownload = false)
673
675
  {
674
676
  var projectDirectory = Path.GetDirectoryName(projectPath);
675
677
  projectDirectory ??= repoRoot;
676
- var topLevelFiles = Directory.GetFiles(repoRoot);
678
+
679
+ if (experimentsManager.InstallDotnetSdks)
680
+ {
681
+ var globalJsonPath = PathHelper.GetFileInDirectoryOrParent(projectPath, repoRoot, "global.json", caseSensitive: true);
682
+ if (globalJsonPath is not null)
683
+ {
684
+ File.Copy(globalJsonPath, Path.Combine(tempDir.FullName, "global.json"));
685
+ }
686
+ }
687
+
677
688
  var nugetConfigPath = PathHelper.GetFileInDirectoryOrParent(projectPath, repoRoot, "NuGet.Config", caseSensitive: false);
678
689
  if (nugetConfigPath is not null)
679
690
  {
@@ -832,40 +843,53 @@ internal static partial class MSBuildHelper
832
843
  string targetFramework,
833
844
  IReadOnlyCollection<Dependency> packages,
834
845
  ExperimentsManager experimentsManager,
835
- ILogger logger)
846
+ ILogger logger
847
+ )
836
848
  {
837
849
  var tempDirectory = Directory.CreateTempSubdirectory("package-dependency-resolution_");
838
850
  try
839
851
  {
840
852
  var topLevelPackagesNames = packages.Select(p => p.Name).ToHashSet(StringComparer.OrdinalIgnoreCase);
841
- var tempProjectPath = await CreateTempProjectAsync(tempDirectory, repoRoot, projectPath, targetFramework, packages, logger);
853
+ var tempProjectPath = await CreateTempProjectAsync(tempDirectory, repoRoot, projectPath, targetFramework, packages, experimentsManager, logger);
842
854
 
843
- var (exitCode, stdout, stderr) = await ProcessEx.RunDotnetWithoutMSBuildEnvironmentVariablesAsync(["build", tempProjectPath, "/t:_ReportDependencies"], tempDirectory.FullName, experimentsManager);
844
- ThrowOnUnauthenticatedFeed(stdout);
845
-
846
- if (exitCode == 0)
855
+ Dependency[] allDependencies;
856
+ if (experimentsManager.UseDirectDiscovery)
847
857
  {
848
- ImmutableArray<string> tfms = [targetFramework];
849
- var lines = stdout.Split('\n').Select(line => line.Trim());
850
- var pattern = PackagePattern();
851
- var allDependencies = lines
852
- .Select(line => pattern.Match(line))
853
- .Where(match => match.Success)
854
- .Select(match =>
855
- {
856
- var PackageName = match.Groups["PackageName"].Value;
857
- var isTransitive = !topLevelPackagesNames.Contains(PackageName);
858
- return new Dependency(PackageName, match.Groups["PackageVersion"].Value, DependencyType.Unknown, TargetFrameworks: tfms, IsTransitive: isTransitive);
859
- })
860
- .ToArray();
861
-
862
- return allDependencies;
858
+ var projectDiscovery = await SdkProjectDiscovery.DiscoverAsync(repoRoot, tempDirectory.FullName, tempProjectPath, experimentsManager, logger);
859
+ allDependencies = projectDiscovery
860
+ .Where(p => p.FilePath == Path.GetFileName(tempProjectPath))
861
+ .FirstOrDefault()
862
+ ?.Dependencies.ToArray() ?? [];
863
863
  }
864
864
  else
865
865
  {
866
- logger?.Warn($"dotnet build in {nameof(GetAllPackageDependenciesAsync)} failed. STDOUT: {stdout} STDERR: {stderr}");
867
- return [];
866
+ var (exitCode, stdout, stderr) = await ProcessEx.RunDotnetWithoutMSBuildEnvironmentVariablesAsync(["build", tempProjectPath, "/t:_ReportDependencies"], tempDirectory.FullName, experimentsManager);
867
+ ThrowOnUnauthenticatedFeed(stdout);
868
+
869
+ if (exitCode == 0)
870
+ {
871
+ ImmutableArray<string> tfms = [targetFramework];
872
+ var lines = stdout.Split('\n').Select(line => line.Trim());
873
+ var pattern = PackagePattern();
874
+ allDependencies = lines
875
+ .Select(line => pattern.Match(line))
876
+ .Where(match => match.Success)
877
+ .Select(match =>
878
+ {
879
+ var PackageName = match.Groups["PackageName"].Value;
880
+ var isTransitive = !topLevelPackagesNames.Contains(PackageName);
881
+ return new Dependency(PackageName, match.Groups["PackageVersion"].Value, DependencyType.Unknown, TargetFrameworks: tfms, IsTransitive: isTransitive);
882
+ })
883
+ .ToArray();
884
+ }
885
+ else
886
+ {
887
+ logger?.Warn($"dotnet build in {nameof(GetAllPackageDependenciesAsync)} failed. STDOUT: {stdout} STDERR: {stderr}");
888
+ allDependencies = [];
889
+ }
868
890
  }
891
+
892
+ return allDependencies;
869
893
  }
870
894
  finally
871
895
  {
@@ -9,7 +9,7 @@ internal static class NuGetHelper
9
9
  var tempDirectory = Directory.CreateTempSubdirectory("msbuild_sdk_restore_");
10
10
  try
11
11
  {
12
- var tempProjectPath = await MSBuildHelper.CreateTempProjectAsync(tempDirectory, repoRoot, projectPath, "netstandard2.0", packages, logger, usePackageDownload: true);
12
+ var tempProjectPath = await MSBuildHelper.CreateTempProjectAsync(tempDirectory, repoRoot, projectPath, "netstandard2.0", packages, experimentsManager, logger, usePackageDownload: true);
13
13
  var (exitCode, stdOut, stdErr) = await ProcessEx.RunDotnetWithoutMSBuildEnvironmentVariablesAsync(["restore", tempProjectPath], tempDirectory.FullName, experimentsManager);
14
14
 
15
15
  return exitCode == 0;
@@ -12,7 +12,7 @@ namespace NuGetUpdater.Core.Test.Analyze;
12
12
 
13
13
  using TestFile = (string Path, string Content);
14
14
 
15
- public class AnalyzeWorkerTestBase
15
+ public class AnalyzeWorkerTestBase : TestBase
16
16
  {
17
17
  protected static async Task TestAnalyzeAsync(
18
18
  WorkspaceDiscoveryResult discovery,
@@ -39,7 +39,7 @@ public class AnalyzeWorkerTestBase
39
39
  var discoveryPath = Path.GetFullPath(DiscoveryWorker.DiscoveryResultFileName, directoryPath);
40
40
  var dependencyPath = Path.GetFullPath(relativeDependencyPath, directoryPath);
41
41
 
42
- var worker = new AnalyzeWorker(experimentsManager, new TestLogger());
42
+ var worker = new AnalyzeWorker("TEST-JOB-ID", experimentsManager, new TestLogger());
43
43
  var result = await worker.RunWithErrorHandlingAsync(directoryPath, discoveryPath, dependencyPath);
44
44
  return result;
45
45
  });
@@ -55,8 +55,7 @@ public class AnalyzeWorkerTestBase
55
55
  Assert.Equal(expectedResult.VersionComesFromMultiDependencyProperty, actualResult.VersionComesFromMultiDependencyProperty);
56
56
  ValidateDependencies(expectedResult.UpdatedDependencies, actualResult.UpdatedDependencies);
57
57
  Assert.Equal(expectedResult.ExpectedUpdatedDependenciesCount ?? expectedResult.UpdatedDependencies.Length, actualResult.UpdatedDependencies.Length);
58
- Assert.Equal(expectedResult.ErrorType, actualResult.ErrorType);
59
- Assert.Equal(expectedResult.ErrorDetails, actualResult.ErrorDetails);
58
+ ValidateResult(expectedResult, actualResult);
60
59
 
61
60
  return;
62
61
 
@@ -81,6 +80,18 @@ public class AnalyzeWorkerTestBase
81
80
  }
82
81
  }
83
82
 
83
+ protected static void ValidateResult(ExpectedAnalysisResult? expectedResult, AnalysisResult actualResult)
84
+ {
85
+ if (expectedResult?.Error is not null)
86
+ {
87
+ ValidateError(expectedResult.Error, actualResult.Error);
88
+ }
89
+ else
90
+ {
91
+ Assert.Null(actualResult.Error);
92
+ }
93
+ }
94
+
84
95
  protected static async Task<AnalysisResult> RunAnalyzerAsync(string dependencyName, TestFile[] files, Func<string, Task<AnalysisResult>> action)
85
96
  {
86
97
  // write initial files
@@ -4,6 +4,7 @@ using System.Text.Json;
4
4
  using NuGet;
5
5
 
6
6
  using NuGetUpdater.Core.Analyze;
7
+ using NuGetUpdater.Core.Run.ApiModel;
7
8
 
8
9
  using Xunit;
9
10
 
@@ -1015,8 +1016,7 @@ public partial class AnalyzeWorkerTests : AnalyzeWorkerTestBase
1015
1016
  using var temporaryDirectory = await TemporaryDirectory.CreateWithContentsAsync([]);
1016
1017
  await AnalyzeWorker.WriteResultsAsync(temporaryDirectory.DirectoryPath, "Some.Dependency", new()
1017
1018
  {
1018
- ErrorType = ErrorType.AuthenticationFailure,
1019
- ErrorDetails = "<some package feed>",
1019
+ Error = new PrivateSourceAuthenticationFailure(["<some package feed>"]),
1020
1020
  UpdatedVersion = "",
1021
1021
  UpdatedDependencies = [],
1022
1022
  }, new TestLogger());
@@ -1025,16 +1025,23 @@ public partial class AnalyzeWorkerTests : AnalyzeWorkerTestBase
1025
1025
  // raw result file should look like this:
1026
1026
  // {
1027
1027
  // ...
1028
- // "ErrorType": "AuthenticationFailure",
1028
+ // "Error": {
1029
+ // "error-type": "private_source_authentication_failure",
1030
+ // "error-details": {
1031
+ // "source": "(<some package feed>)"
1032
+ // }
1033
+ // }
1029
1034
  // "ErrorDetails": "<some package feed>",
1030
1035
  // ...
1031
1036
  // }
1032
1037
  var jsonDocument = JsonDocument.Parse(discoveryContents);
1033
- var errorType = jsonDocument.RootElement.GetProperty("ErrorType");
1034
- var errorDetails = jsonDocument.RootElement.GetProperty("ErrorDetails");
1038
+ var error = jsonDocument.RootElement.GetProperty("Error");
1039
+ var errorType = error.GetProperty("error-type");
1040
+ var errorDetails = error.GetProperty("error-details");
1041
+ var errorSource = errorDetails.GetProperty("source");
1035
1042
 
1036
- Assert.Equal("AuthenticationFailure", errorType.GetString());
1037
- Assert.Equal("<some package feed>", errorDetails.GetString());
1043
+ Assert.Equal("private_source_authentication_failure", errorType.GetString());
1044
+ Assert.Equal("(<some package feed>)", errorSource.GetString());
1038
1045
  }
1039
1046
 
1040
1047
  [Fact]
@@ -1110,8 +1117,7 @@ public partial class AnalyzeWorkerTests : AnalyzeWorkerTestBase
1110
1117
  },
1111
1118
  expectedResult: new()
1112
1119
  {
1113
- ErrorType = ErrorType.AuthenticationFailure,
1114
- ErrorDetails = $"({http.BaseUrl.TrimEnd('/')}/index.json)",
1120
+ Error = new PrivateSourceAuthenticationFailure([$"{http.BaseUrl.TrimEnd('/')}/index.json"]),
1115
1121
  UpdatedVersion = string.Empty,
1116
1122
  CanUpdate = false,
1117
1123
  UpdatedDependencies = [],
@@ -5,7 +5,9 @@ using System.Text.Json;
5
5
 
6
6
  using NuGetUpdater.Core.Discover;
7
7
  using NuGetUpdater.Core.Test.Update;
8
+ using NuGetUpdater.Core.Test.Updater;
8
9
  using NuGetUpdater.Core.Test.Utilities;
10
+ using NuGetUpdater.Core.Updater;
9
11
  using NuGetUpdater.Core.Utilities;
10
12
 
11
13
  using Xunit;
@@ -28,7 +30,7 @@ public class DiscoveryWorkerTestBase : TestBase
28
30
  {
29
31
  await UpdateWorkerTestBase.MockNuGetPackagesInDirectory(packages, directoryPath);
30
32
 
31
- var worker = new DiscoveryWorker(experimentsManager, new TestLogger());
33
+ var worker = new DiscoveryWorker("TEST-JOB-ID", experimentsManager, new TestLogger());
32
34
  var result = await worker.RunWithErrorHandlingAsync(directoryPath, workspacePath);
33
35
  return result;
34
36
  });
@@ -44,17 +46,7 @@ public class DiscoveryWorkerTestBase : TestBase
44
46
  ValidateResultWithDependencies(expectedResult.DotNetToolsJson, actualResult.DotNetToolsJson);
45
47
  ValidateProjectResults(expectedResult.Projects, actualResult.Projects, experimentsManager);
46
48
  Assert.Equal(expectedResult.ExpectedProjectCount ?? expectedResult.Projects.Length, actualResult.Projects.Length);
47
- Assert.Equal(expectedResult.ErrorType, actualResult.ErrorType);
48
- if (expectedResult.ErrorDetailsPattern is not null)
49
- {
50
- var errorDetails = actualResult.ErrorDetails?.ToString();
51
- Assert.NotNull(errorDetails);
52
- Assert.Matches(expectedResult.ErrorDetailsPattern, errorDetails);
53
- }
54
- else
55
- {
56
- Assert.Equal(expectedResult.ErrorDetails, actualResult.ErrorDetails);
57
- }
49
+ ValidateDiscoveryOperationResult(expectedResult, actualResult);
58
50
 
59
51
  return;
60
52
 
@@ -76,6 +68,22 @@ public class DiscoveryWorkerTestBase : TestBase
76
68
  }
77
69
  }
78
70
 
71
+ protected static void ValidateDiscoveryOperationResult(ExpectedWorkspaceDiscoveryResult? expectedResult, WorkspaceDiscoveryResult actualResult)
72
+ {
73
+ if (expectedResult?.Error is not null)
74
+ {
75
+ ValidateError(expectedResult.Error, actualResult.Error);
76
+ }
77
+ else if (expectedResult?.ErrorRegex is not null)
78
+ {
79
+ ValidateErrorRegex(expectedResult.ErrorRegex, actualResult.Error);
80
+ }
81
+ else
82
+ {
83
+ Assert.Null(actualResult.Error);
84
+ }
85
+ }
86
+
79
87
  internal static void ValidateProjectResults(ImmutableArray<ExpectedSdkProjectDiscoveryResult> expectedProjects, ImmutableArray<ProjectDiscoveryResult> actualProjects, ExperimentsManager experimentsManager)
80
88
  {
81
89
  if (expectedProjects.IsDefaultOrEmpty)
@@ -1263,5 +1263,113 @@ public partial class DiscoveryWorkerTests
1263
1263
  }
1264
1264
  );
1265
1265
  }
1266
+
1267
+ [Fact]
1268
+ public async Task PackagesManagedAndRemovedByTheSdkAreReported()
1269
+ {
1270
+ // To avoid a unit test that's tightly coupled to the installed SDK, some files are faked.
1271
+ // First up, the `dotnet-package-correlation.json` is faked to have the appropriate shape to report a
1272
+ // package replacement. Doing this requires a temporary file and environment variable override.
1273
+ using var tempDirectory = new TemporaryDirectory();
1274
+ var packageCorrelationFile = Path.Combine(tempDirectory.DirectoryPath, "dotnet-package-correlation.json");
1275
+ await File.WriteAllTextAsync(packageCorrelationFile, """
1276
+ {
1277
+ "Runtimes": {
1278
+ "1.0.0": {
1279
+ "Packages": {
1280
+ "Dependabot.App.Core.Ref": "1.0.0",
1281
+ "Test.Only.Package": "1.0.0"
1282
+ }
1283
+ },
1284
+ "1.0.1": {
1285
+ "Packages": {
1286
+ "Dependabot.App.Core.Ref": "1.0.1",
1287
+ "Test.Only.Package": "1.0.99"
1288
+ }
1289
+ }
1290
+ }
1291
+ }
1292
+ """);
1293
+ using var tempEnvironment = new TemporaryEnvironment([("DOTNET_PACKAGE_CORRELATION_FILE_PATH", packageCorrelationFile)]);
1294
+
1295
+ // The SDK package handling is detected in a very specific circumstance; an assembly being removed from the
1296
+ // `@(References)` item group in the `_HandlePackageFileConflicts` target. Since we don't want to involve
1297
+ // the real SDK, we fake some required targets.
1298
+ await TestDiscoveryAsync(
1299
+ experimentsManager: new ExperimentsManager() { InstallDotnetSdks = true, UseDirectDiscovery = true },
1300
+ packages: [],
1301
+ workspacePath: "",
1302
+ files:
1303
+ [
1304
+ ("project.csproj", """
1305
+ <Project>
1306
+ <!-- note that the attribute `Sdk="Microsoft.NET.Sdk"` is missing because we don't want the real SDK interfering -->
1307
+
1308
+ <!-- this allows custom targets to be injected for dependency detection -->
1309
+ <Import Project="$(CustomAfterMicrosoftCommonTargets)" Condition="Exists('$(CustomAfterMicrosoftCommonTargets)')" />
1310
+
1311
+ <PropertyGroup>
1312
+ <TargetFramework>net8.0</TargetFramework>
1313
+ </PropertyGroup>
1314
+
1315
+ <ItemGroup>
1316
+ <!-- we need a value in this item group with the appropriate metadata to simulate it having been added by NuGet -->
1317
+ <RuntimeCopyLocalItems Include="TestOnlyAssembly.dll" NuGetPackageId="Test.Only.Package" NuGetPackageVersion="1.0.0" />
1318
+
1319
+ <!-- this represents the assemblies being extracted from the package -->
1320
+ <Reference Include="@(RuntimeCopyLocalItems)" />
1321
+ </ItemGroup>
1322
+
1323
+ <Target Name="_HandlePackageFileConflicts">
1324
+ <!-- this target needs to exist for discovery to work -->
1325
+ <ItemGroup>
1326
+ <!-- this removal is what triggers the package lookup in the correlation file -->
1327
+ <Reference Remove="TestOnlyAssembly.dll" />
1328
+
1329
+ <!-- this addition is what's used for the lookup -->
1330
+ <Reference Include="TestOnlyAssembly.dll" NuGetPackageId="Dependabot.App.Core.Ref" NuGetPackageVersion="1.0.1" />
1331
+ </ItemGroup>
1332
+ </Target>
1333
+
1334
+ <Target Name="ResolveAssemblyReferences" DependsOnTargets="_HandlePackageFileConflicts">
1335
+ <!-- this target needs to exist for discovery to work -->
1336
+ </Target>
1337
+
1338
+ <Target Name="GenerateBuildDependencyFile">
1339
+ <!-- this target needs to exist for discovery to work -->
1340
+ <ItemGroup>
1341
+ <!-- this removal is what removes the regular package reference from the project -->
1342
+ <RuntimeCopyLocalItems Remove="TestOnlyAssembly.dll" />
1343
+ </ItemGroup>
1344
+ </Target>
1345
+
1346
+ <Target Name="ResolvePackageAssets">
1347
+ <!-- this target needs to exist for discovery to work -->
1348
+ </Target>
1349
+ </Project>
1350
+ """)
1351
+ ],
1352
+ expectedResult: new()
1353
+ {
1354
+ Path = "",
1355
+ Projects = [
1356
+ new()
1357
+ {
1358
+ FilePath = "project.csproj",
1359
+ Dependencies = [
1360
+ new("Test.Only.Package", "1.0.99", DependencyType.Unknown, TargetFrameworks: ["net8.0"], IsTransitive: true)
1361
+ ],
1362
+ Properties = [
1363
+ new("TargetFramework", "net8.0", "project.csproj")
1364
+ ],
1365
+ TargetFrameworks = ["net8.0"],
1366
+ ReferencedProjectPaths = [],
1367
+ ImportedFiles = [],
1368
+ AdditionalFiles = [],
1369
+ }
1370
+ ]
1371
+ }
1372
+ );
1373
+ }
1266
1374
  }
1267
1375
  }
@@ -3,6 +3,7 @@ using System.Runtime.InteropServices;
3
3
  using System.Text.Json;
4
4
 
5
5
  using NuGetUpdater.Core.Discover;
6
+ using NuGetUpdater.Core.Run.ApiModel;
6
7
 
7
8
  using Xunit;
8
9
 
@@ -1130,8 +1131,7 @@ public partial class DiscoveryWorkerTests : DiscoveryWorkerTestBase
1130
1131
  {
1131
1132
  Path = "",
1132
1133
  Projects = [],
1133
- ErrorType = ErrorType.DependencyFileNotParseable,
1134
- ErrorDetails = "project2.csproj",
1134
+ Error = new DependencyFileNotParseable("project2.csproj"),
1135
1135
  });
1136
1136
  }
1137
1137
 
@@ -1142,8 +1142,7 @@ public partial class DiscoveryWorkerTests : DiscoveryWorkerTestBase
1142
1142
  var discoveryResultPath = Path.Combine(temporaryDirectory.DirectoryPath, DiscoveryWorker.DiscoveryResultFileName);
1143
1143
  await DiscoveryWorker.WriteResultsAsync(temporaryDirectory.DirectoryPath, discoveryResultPath, new()
1144
1144
  {
1145
- ErrorType = ErrorType.AuthenticationFailure,
1146
- ErrorDetails = "<some package feed>",
1145
+ Error = new PrivateSourceAuthenticationFailure(["<some package feed>"]),
1147
1146
  Path = "/",
1148
1147
  Projects = [],
1149
1148
  });
@@ -1152,16 +1151,22 @@ public partial class DiscoveryWorkerTests : DiscoveryWorkerTestBase
1152
1151
  // raw result file should look like this:
1153
1152
  // {
1154
1153
  // ...
1155
- // "ErrorType": "AuthenticationFailure",
1156
- // "ErrorDetails": "<some package feed>",
1154
+ // "Error": {
1155
+ // "error-type": "private_source_authentication_failure",
1156
+ // "error-detail": {
1157
+ // "source": "(<some package feed>)"
1158
+ // }
1159
+ // }
1157
1160
  // ...
1158
1161
  // }
1159
1162
  var jsonDocument = JsonDocument.Parse(discoveryContents);
1160
- var errorType = jsonDocument.RootElement.GetProperty("ErrorType");
1161
- var errorDetails = jsonDocument.RootElement.GetProperty("ErrorDetails");
1163
+ var error = jsonDocument.RootElement.GetProperty("Error");
1164
+ var errorType = error.GetProperty("error-type");
1165
+ var errorDetail = error.GetProperty("error-details");
1166
+ var errorSource = errorDetail.GetProperty("source");
1162
1167
 
1163
- Assert.Equal("AuthenticationFailure", errorType.GetString());
1164
- Assert.Equal("<some package feed>", errorDetails.GetString());
1168
+ Assert.Equal("private_source_authentication_failure", errorType.GetString());
1169
+ Assert.Equal("(<some package feed>)", errorSource.GetString());
1165
1170
  }
1166
1171
 
1167
1172
  [Theory]
@@ -1236,8 +1241,7 @@ public partial class DiscoveryWorkerTests : DiscoveryWorkerTestBase
1236
1241
  ],
1237
1242
  expectedResult: new()
1238
1243
  {
1239
- ErrorType = ErrorType.AuthenticationFailure,
1240
- ErrorDetails = $"({http.BaseUrl.TrimEnd('/')}/index.json)",
1244
+ Error = new PrivateSourceAuthenticationFailure([$"{http.BaseUrl.TrimEnd('/')}/index.json"]),
1241
1245
  Path = "",
1242
1246
  Projects = [],
1243
1247
  }
@@ -12,7 +12,7 @@ public record ExpectedWorkspaceDiscoveryResult : NativeResult
12
12
  public int? ExpectedProjectCount { get; init; }
13
13
  public ExpectedDependencyDiscoveryResult? GlobalJson { get; init; }
14
14
  public ExpectedDependencyDiscoveryResult? DotNetToolsJson { get; init; }
15
- public string? ErrorDetailsPattern { get; init; } = null;
15
+ public string? ErrorRegex { get; init; } = null;
16
16
  }
17
17
 
18
18
  public record ExpectedSdkProjectDiscoveryResult : ExpectedDependencyDiscoveryResult