dependabot-nuget 0.291.0 → 0.293.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (94) hide show
  1. checksums.yaml +4 -4
  2. data/helpers/lib/NuGetUpdater/.editorconfig +1 -0
  3. data/helpers/lib/NuGetUpdater/.gitignore +1 -0
  4. data/helpers/lib/NuGetUpdater/Directory.Build.props +1 -0
  5. data/helpers/lib/NuGetUpdater/Directory.Packages.props +2 -1
  6. data/helpers/lib/NuGetUpdater/DotNetPackageCorrelation/Correlator.cs +197 -0
  7. data/helpers/lib/NuGetUpdater/DotNetPackageCorrelation/DotNetPackageCorrelation.csproj +12 -0
  8. data/helpers/lib/NuGetUpdater/DotNetPackageCorrelation/Model/PackageMapper.cs +68 -0
  9. data/helpers/lib/NuGetUpdater/DotNetPackageCorrelation/Model/PackageSet.cs +11 -0
  10. data/helpers/lib/NuGetUpdater/DotNetPackageCorrelation/Model/Release.cs +25 -0
  11. data/helpers/lib/NuGetUpdater/DotNetPackageCorrelation/Model/ReleasesFile.cs +9 -0
  12. data/helpers/lib/NuGetUpdater/DotNetPackageCorrelation/Model/RuntimePackages.cs +11 -0
  13. data/helpers/lib/NuGetUpdater/DotNetPackageCorrelation/Model/Sdk.cs +13 -0
  14. data/helpers/lib/NuGetUpdater/DotNetPackageCorrelation/Model/SemVerComparer.cs +16 -0
  15. data/helpers/lib/NuGetUpdater/DotNetPackageCorrelation/Model/SemVersionConverter.cs +42 -0
  16. data/helpers/lib/NuGetUpdater/DotNetPackageCorrelation.Cli/DotNetPackageCorrelation.Cli.csproj +16 -0
  17. data/helpers/lib/NuGetUpdater/DotNetPackageCorrelation.Cli/Program.cs +32 -0
  18. data/helpers/lib/NuGetUpdater/DotNetPackageCorrelation.Test/CorrelatorTests.cs +99 -0
  19. data/helpers/lib/NuGetUpdater/DotNetPackageCorrelation.Test/DotNetPackageCorrelation.Test.csproj +18 -0
  20. data/helpers/lib/NuGetUpdater/DotNetPackageCorrelation.Test/EndToEndTests.cs +30 -0
  21. data/helpers/lib/NuGetUpdater/DotNetPackageCorrelation.Test/RuntimePackagesTests.cs +206 -0
  22. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/AnalyzeCommand.cs +6 -4
  23. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/CloneCommand.cs +1 -1
  24. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/DiscoverCommand.cs +19 -4
  25. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/RunCommand.cs +5 -5
  26. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/UpdateCommand.cs +17 -5
  27. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Analyze.cs +8 -1
  28. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Discover.cs +128 -4
  29. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Update.cs +8 -0
  30. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalyzeWorker.cs +9 -7
  31. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/DependencyFinder.cs +4 -4
  32. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/Extensions.cs +1 -1
  33. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/RequirementConverter.cs +19 -1
  34. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/BadRequirementException.cs +9 -0
  35. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Clone/CloneWorker.cs +40 -8
  36. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/DependencyDiscovery.targets +2 -2
  37. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs +65 -23
  38. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/ProjectDiscoveryResult.cs +3 -3
  39. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/SdkProjectDiscovery.cs +99 -2
  40. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/EnsureDotNetPackageCorrelation.targets +25 -0
  41. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/ExperimentsManager.cs +15 -5
  42. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/PackagesConfigBuildFile.cs +5 -1
  43. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/MissingFileException.cs +2 -1
  44. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/NativeResult.cs +3 -3
  45. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/NuGetUpdater.Core.csproj +4 -0
  46. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/BadRequirement.cs +10 -0
  47. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/CommitOptions.cs +1 -1
  48. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/DependencyFileNotFound.cs +7 -2
  49. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/DependencyFileNotParseable.cs +15 -0
  50. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/JobErrorBase.cs +25 -2
  51. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/JobRepoNotFound.cs +1 -4
  52. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/PrivateSourceAuthenticationFailure.cs +1 -1
  53. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/UnknownError.cs +6 -2
  54. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/UpdateNotPossible.cs +1 -1
  55. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/RunWorker.cs +9 -18
  56. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/UnparseableFileException.cs +12 -0
  57. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/LockFileUpdater.cs +1 -1
  58. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/PackageReferenceUpdater.cs +21 -0
  59. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/UpdaterWorker.cs +6 -30
  60. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/WebApplicationTargetsConditionPatcher.cs +12 -1
  61. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/DependencyConflictResolver.cs +0 -7
  62. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/DotNetPackageCorrelationManager.cs +46 -0
  63. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs +59 -30
  64. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/NuGetHelper.cs +1 -1
  65. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/ProjectHelper.cs +4 -4
  66. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/AnalyzeWorkerTestBase.cs +15 -4
  67. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/AnalyzeWorkerTests.cs +15 -9
  68. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Clone/CloneWorkerTests.cs +60 -2
  69. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTestBase.cs +20 -3
  70. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.PackagesConfig.cs +56 -0
  71. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.Project.cs +108 -0
  72. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.cs +16 -12
  73. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/ExpectedDiscoveryResults.cs +1 -0
  74. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/MockNuGetPackage.cs +15 -28
  75. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/RunWorkerTests.cs +5 -4
  76. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/SerializationTests.cs +84 -40
  77. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/TestBase.cs +24 -0
  78. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/ExpectedUpdateOperationResult.cs +1 -1
  79. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTestBase.cs +25 -11
  80. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.DirsProj.cs +1 -1
  81. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.GlobalJson.cs +2 -2
  82. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.LockFile.cs +251 -0
  83. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.Mixed.cs +14 -8
  84. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.PackageReference.cs +154 -9
  85. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.PackagesConfig.cs +71 -15
  86. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/MSBuildHelperTests.cs +38 -20
  87. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/ProjectHelperTests.cs +65 -0
  88. data/helpers/lib/NuGetUpdater/NuGetUpdater.sln +18 -1
  89. data/helpers/lib/NuGetUpdater/global.json +1 -1
  90. data/lib/dependabot/nuget/language.rb +21 -5
  91. data/lib/dependabot/nuget/native_helpers.rb +41 -14
  92. data/lib/dependabot/nuget/package_manager.rb +4 -4
  93. metadata +30 -7
  94. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/ErrorType.cs +0 -11
@@ -7,6 +7,7 @@ using NuGet.Frameworks;
7
7
  using NuGet.Versioning;
8
8
 
9
9
  using NuGetUpdater.Core.Discover;
10
+ using NuGetUpdater.Core.Run.ApiModel;
10
11
 
11
12
  namespace NuGetUpdater.Core.Analyze;
12
13
 
@@ -16,6 +17,7 @@ public partial class AnalyzeWorker : IAnalyzeWorker
16
17
  {
17
18
  public const string AnalysisDirectoryName = "./.dependabot/analysis";
18
19
 
20
+ private readonly string _jobId;
19
21
  private readonly ExperimentsManager _experimentsManager;
20
22
  private readonly ILogger _logger;
21
23
 
@@ -25,8 +27,9 @@ public partial class AnalyzeWorker : IAnalyzeWorker
25
27
  Converters = { new JsonStringEnumConverter(), new RequirementArrayConverter() },
26
28
  };
27
29
 
28
- public AnalyzeWorker(ExperimentsManager experimentsManager, ILogger logger)
30
+ public AnalyzeWorker(string jobId, ExperimentsManager experimentsManager, ILogger logger)
29
31
  {
32
+ _jobId = jobId;
30
33
  _experimentsManager = experimentsManager;
31
34
  _logger = logger;
32
35
  }
@@ -48,15 +51,11 @@ public partial class AnalyzeWorker : IAnalyzeWorker
48
51
  {
49
52
  analysisResult = await RunAsync(repoRoot, discovery, dependencyInfo);
50
53
  }
51
- catch (HttpRequestException ex)
52
- when (ex.StatusCode == HttpStatusCode.Unauthorized || ex.StatusCode == HttpStatusCode.Forbidden)
54
+ catch (Exception ex)
53
55
  {
54
- var localPath = PathHelper.JoinPath(repoRoot, discovery.Path);
55
- using var nugetContext = new NuGetContext(localPath);
56
56
  analysisResult = new AnalysisResult
57
57
  {
58
- ErrorType = ErrorType.AuthenticationFailure,
59
- ErrorDetails = "(" + string.Join("|", nugetContext.PackageSources.Select(s => s.Source)) + ")",
58
+ Error = JobErrorBase.ErrorFromException(ex, _jobId, PathHelper.JoinPath(repoRoot, discovery.Path)),
60
59
  UpdatedVersion = string.Empty,
61
60
  CanUpdate = false,
62
61
  UpdatedDependencies = [],
@@ -68,6 +67,8 @@ public partial class AnalyzeWorker : IAnalyzeWorker
68
67
 
69
68
  public async Task<AnalysisResult> RunAsync(string repoRoot, WorkspaceDiscoveryResult discovery, DependencyInfo dependencyInfo)
70
69
  {
70
+ MSBuildHelper.RegisterMSBuild(repoRoot, repoRoot);
71
+
71
72
  var startingDirectory = PathHelper.JoinPath(repoRoot, discovery.Path);
72
73
 
73
74
  _logger.Info($"Starting analysis of {dependencyInfo.Name}...");
@@ -412,6 +413,7 @@ public partial class AnalyzeWorker : IAnalyzeWorker
412
413
  .SelectMany(p => p.TargetFrameworks)
413
414
  .Select(NuGetFramework.Parse)
414
415
  .Distinct()
416
+ .Select(f => f.GetShortFolderName())
415
417
  .ToImmutableArray();
416
418
 
417
419
  // When updating peer dependencies, we only need to consider top-level dependencies.
@@ -7,10 +7,10 @@ namespace NuGetUpdater.Core.Analyze;
7
7
 
8
8
  internal static class DependencyFinder
9
9
  {
10
- public static async Task<ImmutableDictionary<NuGetFramework, ImmutableArray<Dependency>>> GetDependenciesAsync(
10
+ public static async Task<ImmutableDictionary<string, ImmutableArray<Dependency>>> GetDependenciesAsync(
11
11
  string repoRoot,
12
12
  string projectPath,
13
- IEnumerable<NuGetFramework> frameworks,
13
+ IEnumerable<string> frameworks,
14
14
  ImmutableHashSet<string> packageIds,
15
15
  NuGetVersion version,
16
16
  NuGetContext nugetContext,
@@ -23,13 +23,13 @@ internal static class DependencyFinder
23
23
  .Select(id => new Dependency(id, versionString, DependencyType.Unknown))
24
24
  .ToImmutableArray();
25
25
 
26
- var result = ImmutableDictionary.CreateBuilder<NuGetFramework, ImmutableArray<Dependency>>();
26
+ var result = ImmutableDictionary.CreateBuilder<string, ImmutableArray<Dependency>>();
27
27
  foreach (var framework in frameworks)
28
28
  {
29
29
  var dependencies = await MSBuildHelper.GetAllPackageDependenciesAsync(
30
30
  repoRoot,
31
31
  projectPath,
32
- framework.ToString(),
32
+ framework,
33
33
  packages,
34
34
  experimentsManager,
35
35
  logger);
@@ -7,7 +7,7 @@ using NuGetUpdater.Core;
7
7
 
8
8
  internal static class Extensions
9
9
  {
10
- public static ImmutableArray<Dependency> GetDependencies(this ImmutableDictionary<NuGetFramework, ImmutableArray<Dependency>> dependenciesByTfm)
10
+ public static ImmutableArray<Dependency> GetDependencies(this ImmutableDictionary<string, ImmutableArray<Dependency>> dependenciesByTfm)
11
11
  {
12
12
  Dictionary<string, Dependency> dependencies = [];
13
13
  foreach (var (_framework, dependenciesForTfm) in dependenciesByTfm)
@@ -7,7 +7,25 @@ public class RequirementConverter : JsonConverter<Requirement>
7
7
  {
8
8
  public override Requirement? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
9
9
  {
10
- return Requirement.Parse(reader.GetString()!);
10
+ if (reader.TokenType != JsonTokenType.String)
11
+ {
12
+ throw new BadRequirementException($"Expected token type {nameof(JsonTokenType.String)}, but found {reader.TokenType}.");
13
+ }
14
+
15
+ var text = reader.GetString();
16
+ if (text is null)
17
+ {
18
+ throw new BadRequirementException("Unexpected null token.");
19
+ }
20
+
21
+ try
22
+ {
23
+ return Requirement.Parse(text);
24
+ }
25
+ catch
26
+ {
27
+ throw new BadRequirementException(text);
28
+ }
11
29
  }
12
30
 
13
31
  public override void Write(Utf8JsonWriter writer, Requirement value, JsonSerializerOptions options)
@@ -0,0 +1,9 @@
1
+ namespace NuGetUpdater.Core;
2
+
3
+ internal class BadRequirementException : Exception
4
+ {
5
+ public BadRequirementException(string details)
6
+ : base(details)
7
+ {
8
+ }
9
+ }
@@ -9,23 +9,49 @@ namespace NuGetUpdater.Core.Clone;
9
9
 
10
10
  public class CloneWorker
11
11
  {
12
+ private readonly string _jobId;
12
13
  private readonly IApiHandler _apiHandler;
13
14
  private readonly IGitCommandHandler _gitCommandHandler;
14
- private readonly ILogger _logger;
15
15
 
16
- public CloneWorker(IApiHandler apiHandler, IGitCommandHandler gitCommandHandler, ILogger logger)
16
+ public CloneWorker(string jobId, IApiHandler apiHandler, IGitCommandHandler gitCommandHandler)
17
17
  {
18
+ _jobId = jobId;
18
19
  _apiHandler = apiHandler;
19
20
  _gitCommandHandler = gitCommandHandler;
20
- _logger = logger;
21
21
  }
22
22
 
23
23
  // entrypoint for cli
24
24
  public async Task<int> RunAsync(FileInfo jobFilePath, DirectoryInfo repoContentsPath)
25
25
  {
26
26
  var jobFileContent = await File.ReadAllTextAsync(jobFilePath.FullName);
27
- var jobWrapper = RunWorker.Deserialize(jobFileContent);
28
- var result = await RunAsync(jobWrapper.Job, repoContentsPath.FullName);
27
+
28
+ // only a limited set of errors can occur here
29
+ JobFile? jobFile = null;
30
+ JobErrorBase? parseError = null;
31
+ try
32
+ {
33
+ jobFile = RunWorker.Deserialize(jobFileContent);
34
+ if (jobFile is null)
35
+ {
36
+ parseError = new UnknownError(new Exception("Job file could not be deserialized"), _jobId);
37
+ }
38
+ }
39
+ catch (BadRequirementException ex)
40
+ {
41
+ parseError = new BadRequirement(ex.Message);
42
+ }
43
+ catch (Exception ex)
44
+ {
45
+ parseError = new UnknownError(ex, _jobId);
46
+ }
47
+
48
+ if (parseError is not null)
49
+ {
50
+ await ReportError(parseError);
51
+ return 1;
52
+ }
53
+
54
+ var result = await RunAsync(jobFile!.Job, repoContentsPath.FullName);
29
55
  return result;
30
56
  }
31
57
 
@@ -44,23 +70,29 @@ public class CloneWorker
44
70
  catch (HttpRequestException ex)
45
71
  when (ex.StatusCode == HttpStatusCode.Unauthorized || ex.StatusCode == HttpStatusCode.Forbidden)
46
72
  {
73
+ // this is a _very_ specific case we want to handle before the common error handling kicks in
47
74
  error = new JobRepoNotFound(ex.Message);
48
75
  }
49
76
  catch (Exception ex)
50
77
  {
51
- error = new UnknownError(ex.ToString());
78
+ error = JobErrorBase.ErrorFromException(ex, _jobId, repoContentsPath);
52
79
  }
53
80
 
54
81
  if (error is not null)
55
82
  {
56
- await _apiHandler.RecordUpdateJobError(error);
57
- await _apiHandler.MarkAsProcessed(new("unknown"));
83
+ await ReportError(error);
58
84
  return 1;
59
85
  }
60
86
 
61
87
  return 0;
62
88
  }
63
89
 
90
+ private async Task ReportError(JobErrorBase error)
91
+ {
92
+ await _apiHandler.RecordUpdateJobError(error);
93
+ await _apiHandler.MarkAsProcessed(new("unknown"));
94
+ }
95
+
64
96
  internal static CommandArguments[] GetAllCommandArgs(Job job, string repoContentsPath)
65
97
  {
66
98
  var commandArgs = new List<CommandArguments>()
@@ -1,9 +1,9 @@
1
1
  <Project>
2
2
  <Import Project="DependencyDiscovery.props" />
3
3
 
4
- <Target Name="_DiscoverDependencies" DependsOnTargets="GenerateBuildDependencyFile;ResolvePackageAssets">
4
+ <Target Name="_DiscoverDependencies" DependsOnTargets="ResolveAssemblyReferences;GenerateBuildDependencyFile;ResolvePackageAssets">
5
5
  <!--
6
- The target GenerateBuildDependencyFile is sufficient for projects targeting .NET Standard or .NET Core.
6
+ The targets ResolveAssemblyReferences and GenerateBuildDependencyFile are sufficient for projects targeting .NET Standard or .NET Core.
7
7
  The target ResolvePackageAssets is necessary for projects targeting .NET Framework.
8
8
  -->
9
9
  </Target>
@@ -1,5 +1,4 @@
1
1
  using System.Collections.Immutable;
2
- using System.Net;
3
2
  using System.Text.Json;
4
3
  using System.Text.Json.Serialization;
5
4
 
@@ -8,7 +7,9 @@ using Microsoft.Build.Definition;
8
7
  using Microsoft.Build.Evaluation;
9
8
  using Microsoft.Build.Exceptions;
10
9
 
11
- using NuGetUpdater.Core.Analyze;
10
+ using NuGet.Frameworks;
11
+
12
+ using NuGetUpdater.Core.Run.ApiModel;
12
13
  using NuGetUpdater.Core.Utilities;
13
14
 
14
15
  namespace NuGetUpdater.Core.Discover;
@@ -17,6 +18,7 @@ public partial class DiscoveryWorker : IDiscoveryWorker
17
18
  {
18
19
  public const string DiscoveryResultFileName = "./.dependabot/discovery.json";
19
20
 
21
+ private readonly string _jobId;
20
22
  private readonly ExperimentsManager _experimentsManager;
21
23
  private readonly ILogger _logger;
22
24
  private readonly HashSet<string> _processedProjectPaths = new(StringComparer.Ordinal); private readonly HashSet<string> _restoredMSBuildSdks = new(StringComparer.OrdinalIgnoreCase);
@@ -27,8 +29,9 @@ public partial class DiscoveryWorker : IDiscoveryWorker
27
29
  Converters = { new JsonStringEnumConverter() },
28
30
  };
29
31
 
30
- public DiscoveryWorker(ExperimentsManager experimentsManager, ILogger logger)
32
+ public DiscoveryWorker(string jobId, ExperimentsManager experimentsManager, ILogger logger)
31
33
  {
34
+ _jobId = jobId;
32
35
  _experimentsManager = experimentsManager;
33
36
  _logger = logger;
34
37
  }
@@ -46,13 +49,11 @@ public partial class DiscoveryWorker : IDiscoveryWorker
46
49
  {
47
50
  result = await RunAsync(repoRootPath, workspacePath);
48
51
  }
49
- catch (HttpRequestException ex)
50
- when (ex.StatusCode == HttpStatusCode.Unauthorized || ex.StatusCode == HttpStatusCode.Forbidden)
52
+ catch (Exception ex)
51
53
  {
52
54
  result = new WorkspaceDiscoveryResult
53
55
  {
54
- ErrorType = ErrorType.AuthenticationFailure,
55
- ErrorDetails = "(" + string.Join("|", NuGetContext.GetPackageSourceUrls(PathHelper.JoinPath(repoRootPath, workspacePath))) + ")",
56
+ Error = JobErrorBase.ErrorFromException(ex, _jobId, PathHelper.JoinPath(repoRootPath, workspacePath)),
56
57
  Path = workspacePath,
57
58
  Projects = [],
58
59
  };
@@ -111,8 +112,7 @@ public partial class DiscoveryWorker : IDiscoveryWorker
111
112
  DotNetToolsJson = null,
112
113
  GlobalJson = null,
113
114
  Projects = projectResults.Where(p => p.IsSuccess).OrderBy(p => p.FilePath).ToImmutableArray(),
114
- ErrorType = failedProjectResult.ErrorType,
115
- ErrorDetails = failedProjectResult.FilePath,
115
+ Error = failedProjectResult.Error,
116
116
  IsSuccess = false,
117
117
  };
118
118
 
@@ -180,8 +180,7 @@ public partial class DiscoveryWorker : IDiscoveryWorker
180
180
  ImportedFiles = ImmutableArray<string>.Empty,
181
181
  AdditionalFiles = ImmutableArray<string>.Empty,
182
182
  IsSuccess = false,
183
- ErrorType = ErrorType.DependencyFileNotParseable,
184
- ErrorDetails = "Failed to parse project file found at " + invalidProjectFile,
183
+ Error = new DependencyFileNotParseable(invalidProjectFile),
185
184
  }];
186
185
  }
187
186
  if (projects.IsEmpty)
@@ -364,19 +363,62 @@ public partial class DiscoveryWorker : IDiscoveryWorker
364
363
  }
365
364
  }
366
365
 
367
- if (!results.ContainsKey(relativeProjectPath) &&
368
- packagesConfigResult is not null &&
369
- packagesConfigResult.Dependencies.Length > 0)
366
+ if (packagesConfigResult is not null)
370
367
  {
371
- // project contained only packages.config dependencies
372
- results[relativeProjectPath] = new ProjectDiscoveryResult()
368
+ // we might have to merge this dependency with some others
369
+ if (results.TryGetValue(relativeProjectPath, out var existingProjectDiscovery))
373
370
  {
374
- FilePath = relativeProjectPath,
375
- Dependencies = packagesConfigResult.Dependencies,
376
- TargetFrameworks = packagesConfigResult.TargetFrameworks,
377
- ImportedFiles = [], // no imported files resolved for packages.config scenarios
378
- AdditionalFiles = packagesConfigResult.AdditionalFiles,
379
- };
371
+ // merge SDK and packages.config results
372
+ var mergedDependencies = existingProjectDiscovery.Dependencies.Concat(packagesConfigResult.Dependencies)
373
+ .DistinctBy(d => d.Name, StringComparer.OrdinalIgnoreCase)
374
+ .OrderBy(d => d.Name)
375
+ .ToImmutableArray();
376
+ var mergedTargetFrameworks = existingProjectDiscovery.TargetFrameworks.Concat(packagesConfigResult.TargetFrameworks)
377
+ .Select(t =>
378
+ {
379
+ try
380
+ {
381
+ var tfm = NuGetFramework.Parse(t);
382
+ return tfm.GetShortFolderName();
383
+ }
384
+ catch
385
+ {
386
+ return string.Empty;
387
+ }
388
+ })
389
+ .Where(tfm => !string.IsNullOrEmpty(tfm))
390
+ .Distinct()
391
+ .OrderBy(tfm => tfm)
392
+ .ToImmutableArray();
393
+ var mergedProperties = existingProjectDiscovery.Properties; // packages.config discovery doesn't produce properties
394
+ var mergedImportedFiles = existingProjectDiscovery.ImportedFiles; // packages.config discovery doesn't produce imported files
395
+ var mergedAdditionalFiles = existingProjectDiscovery.AdditionalFiles.Concat(packagesConfigResult.AdditionalFiles)
396
+ .Distinct(StringComparer.OrdinalIgnoreCase)
397
+ .OrderBy(f => f)
398
+ .ToImmutableArray();
399
+ var mergedResult = new ProjectDiscoveryResult()
400
+ {
401
+ FilePath = existingProjectDiscovery.FilePath,
402
+ Dependencies = mergedDependencies,
403
+ TargetFrameworks = mergedTargetFrameworks,
404
+ Properties = mergedProperties,
405
+ ImportedFiles = mergedImportedFiles,
406
+ AdditionalFiles = mergedAdditionalFiles,
407
+ };
408
+ results[relativeProjectPath] = mergedResult;
409
+ }
410
+ else
411
+ {
412
+ // add packages.config results
413
+ results[relativeProjectPath] = new ProjectDiscoveryResult()
414
+ {
415
+ FilePath = relativeProjectPath,
416
+ Dependencies = packagesConfigResult.Dependencies,
417
+ TargetFrameworks = packagesConfigResult.TargetFrameworks,
418
+ ImportedFiles = [], // no imported files resolved for packages.config scenarios
419
+ AdditionalFiles = packagesConfigResult.AdditionalFiles,
420
+ };
421
+ }
380
422
  }
381
423
  }
382
424
  }
@@ -397,6 +439,6 @@ public partial class DiscoveryWorker : IDiscoveryWorker
397
439
  }
398
440
 
399
441
  var resultJson = JsonSerializer.Serialize(result, SerializerOptions);
400
- await File.WriteAllTextAsync(path: resultPath, resultJson);
442
+ await File.WriteAllTextAsync(resultPath, resultJson);
401
443
  }
402
444
  }
@@ -1,5 +1,6 @@
1
1
  using System.Collections.Immutable;
2
- using System.Text.Json.Serialization;
2
+
3
+ using NuGetUpdater.Core.Run.ApiModel;
3
4
 
4
5
  namespace NuGetUpdater.Core.Discover;
5
6
 
@@ -8,8 +9,7 @@ public record ProjectDiscoveryResult : IDiscoveryResultWithDependencies
8
9
  public required string FilePath { get; init; }
9
10
  public required ImmutableArray<Dependency> Dependencies { get; init; }
10
11
  public bool IsSuccess { get; init; } = true;
11
- public string? ErrorDetails { get; init; }
12
- public ErrorType? ErrorType { get; init; }
12
+ public JobErrorBase? Error { get; init; } = null;
13
13
  public ImmutableArray<Property> Properties { get; init; } = [];
14
14
  public ImmutableArray<string> TargetFrameworks { get; init; } = [];
15
15
  public ImmutableArray<string> ReferencedProjectPaths { get; init; } = [];
@@ -9,6 +9,8 @@ using NuGet.Versioning;
9
9
 
10
10
  using NuGetUpdater.Core.Utilities;
11
11
 
12
+ using Semver;
13
+
12
14
  using LoggerProperty = Microsoft.Build.Logging.StructuredLogger.Property;
13
15
 
14
16
  namespace NuGetUpdater.Core.Discover;
@@ -72,6 +74,9 @@ internal static class SdkProjectDiscovery
72
74
  Dictionary<string, HashSet<string>> topLevelPackagesPerProject = new(PathComparer.Instance);
73
75
  // projectPath, packageNames
74
76
 
77
+ Dictionary<string, Dictionary<string, Dictionary<string, string>>> packagesReplacedBySdkPerProject = new(PathComparer.Instance);
78
+ // projectPath tfm packageName, packageVersion
79
+
75
80
  Dictionary<string, Dictionary<string, string>> resolvedProperties = new(PathComparer.Instance);
76
81
  // projectPath propertyName, propertyValue
77
82
 
@@ -164,7 +169,7 @@ internal static class SdkProjectDiscovery
164
169
  }
165
170
  break;
166
171
  case NamedNode namedNode when namedNode is AddItem or RemoveItem:
167
- ProcessResolvedPackageReference(namedNode, packagesPerProject, topLevelPackagesPerProject);
172
+ ProcessResolvedPackageReference(namedNode, packagesPerProject, topLevelPackagesPerProject, experimentsManager);
168
173
 
169
174
  if (namedNode is AddItem addItem)
170
175
  {
@@ -203,6 +208,70 @@ internal static class SdkProjectDiscovery
203
208
  }
204
209
  }
205
210
  break;
211
+ case Target target when target.Name == "_HandlePackageFileConflicts":
212
+ // this only works if we've installed the exact SDK required
213
+ if (experimentsManager.InstallDotnetSdks)
214
+ {
215
+ var projectEvaluation = GetNearestProjectEvaluation(target);
216
+ if (projectEvaluation is null)
217
+ {
218
+ break;
219
+ }
220
+
221
+ var removedReferences = target.Children.OfType<RemoveItem>().FirstOrDefault(r => r.Name == "Reference");
222
+ var addedReferences = target.Children.OfType<AddItem>().FirstOrDefault(r => r.Name == "Reference");
223
+ if (removedReferences is null || addedReferences is null)
224
+ {
225
+ break;
226
+ }
227
+
228
+ foreach (var removedAssembly in removedReferences.Children.OfType<Item>())
229
+ {
230
+ var removedPackageName = GetChildMetadataValue(removedAssembly, "NuGetPackageId");
231
+ var removedFileName = Path.GetFileName(removedAssembly.Name);
232
+ if (removedPackageName is null || removedFileName is null)
233
+ {
234
+ continue;
235
+ }
236
+
237
+ var existingProjectPackagesByTfm = packagesPerProject.GetOrAdd(projectEvaluation.ProjectFile, () => new(PathComparer.Instance));
238
+ var existingProjectPackages = existingProjectPackagesByTfm.GetOrAdd(tfm, () => new(StringComparer.OrdinalIgnoreCase));
239
+ if (!existingProjectPackages.ContainsKey(removedPackageName))
240
+ {
241
+ continue;
242
+ }
243
+
244
+ var correspondingAddedFile = addedReferences.Children.OfType<Item>()
245
+ .FirstOrDefault(i => removedFileName.Equals(Path.GetFileName(i.Name), StringComparison.OrdinalIgnoreCase));
246
+ if (correspondingAddedFile is null)
247
+ {
248
+ continue;
249
+ }
250
+
251
+ var runtimePackageName = GetChildMetadataValue(correspondingAddedFile, "NuGetPackageId");
252
+ var runtimePackageVersion = GetChildMetadataValue(correspondingAddedFile, "NuGetPackageVersion");
253
+ if (runtimePackageName is null ||
254
+ runtimePackageVersion is null ||
255
+ !SemVersion.TryParse(runtimePackageVersion, out var parsedRuntimePackageVersion))
256
+ {
257
+ continue;
258
+ }
259
+
260
+ var packageMapper = DotNetPackageCorrelationManager.GetPackageMapper();
261
+ var replacementPackageVersion = packageMapper.GetPackageVersionThatShippedWithOtherPackage(runtimePackageName, parsedRuntimePackageVersion, removedPackageName);
262
+ if (replacementPackageVersion is null)
263
+ {
264
+ continue;
265
+ }
266
+
267
+ var packagesPerThisProject = packagesReplacedBySdkPerProject.GetOrAdd(projectEvaluation.ProjectFile, () => new(PathComparer.Instance));
268
+ var packagesPerTfm = packagesPerThisProject.GetOrAdd(tfm, () => new(StringComparer.OrdinalIgnoreCase));
269
+ packagesPerTfm[removedPackageName] = replacementPackageVersion.ToString();
270
+ var relativeProjectPath = Path.GetRelativePath(repoRootPath, projectEvaluation.ProjectFile).NormalizePathToUnix();
271
+ logger.Info($"Re-added SDK managed package [{removedPackageName}/{replacementPackageVersion}] to project [{relativeProjectPath}]");
272
+ }
273
+ }
274
+ break;
206
275
  }
207
276
  }, takeChildrenSnapshot: true);
208
277
  }
@@ -228,6 +297,33 @@ internal static class SdkProjectDiscovery
228
297
  {
229
298
  // gather some project-level information
230
299
  var packagesByTfm = packagesPerProject[projectPath];
300
+ if (packagesReplacedBySdkPerProject.TryGetValue(projectPath, out var packagesReplacedBySdk))
301
+ {
302
+ var consolidatedPackagesByTfm = new Dictionary<string, Dictionary<string, string>>(StringComparer.OrdinalIgnoreCase);
303
+
304
+ // copy the first dictionary
305
+ foreach (var kvp in packagesByTfm)
306
+ {
307
+ var tfm = kvp.Key;
308
+ var packages = kvp.Value;
309
+ consolidatedPackagesByTfm[tfm] = packages;
310
+ }
311
+
312
+ // merge in the second
313
+ foreach (var kvp in packagesReplacedBySdk)
314
+ {
315
+ var tfm = kvp.Key;
316
+ var packages = kvp.Value;
317
+ var replacedPackages = consolidatedPackagesByTfm.GetOrAdd(tfm, () => new(StringComparer.OrdinalIgnoreCase));
318
+ foreach (var packagePair in packages)
319
+ {
320
+ replacedPackages[packagePair.Key] = packagePair.Value;
321
+ }
322
+ }
323
+
324
+ packagesByTfm = consolidatedPackagesByTfm;
325
+ }
326
+
231
327
  var projectFullDirectory = Path.GetDirectoryName(projectPath)!;
232
328
  var doc = XDocument.Load(projectPath);
233
329
  var localPropertyDefinitionElements = doc.Root!.XPathSelectElements("/Project/PropertyGroup/*");
@@ -288,7 +384,8 @@ internal static class SdkProjectDiscovery
288
384
  private static void ProcessResolvedPackageReference(
289
385
  NamedNode node,
290
386
  Dictionary<string, Dictionary<string, Dictionary<string, string>>> packagesPerProject, // projectPath -> tfm -> (packageName, packageVersion)
291
- Dictionary<string, HashSet<string>> topLevelPackagesPerProject
387
+ Dictionary<string, HashSet<string>> topLevelPackagesPerProject,
388
+ ExperimentsManager experimentsManager
292
389
  )
293
390
  {
294
391
  var doRemoveOperation = node is RemoveItem;
@@ -0,0 +1,25 @@
1
+ <Project>
2
+
3
+ <PropertyGroup>
4
+ <PackageCorrelationCliDirectory>$(MSBuildThisFileDirectory)..\DotNetPackageCorrelation.Cli</PackageCorrelationCliDirectory>
5
+ <DotNetCoreDirectory>$(MSBuildThisFileDirectory)..\..\dotnet-core</DotNetCoreDirectory>
6
+ <PackageCorrelationFile>$(MSBuildThisFileDirectory)dotnet-package-correlation.json</PackageCorrelationFile>
7
+ </PropertyGroup>
8
+
9
+ <ItemGroup>
10
+ <None Include="$(PackageCorrelationFile)" CopyToOutputDirectory="PreserveNewest" />
11
+ <_DotNetCoreFiles Include="$(DotNetCoreDirectory)\**\*" />
12
+ </ItemGroup>
13
+
14
+ <Target Name="BuildDotNetPackageCorrelationFile" Inputs="@(_DotNetCoreFiles)" Outputs="$(PackageCorrelationFile)" BeforeTargets="GetCopyToOutputDirectoryItems">
15
+ <Exec Command="dotnet run --core-location &quot;$(DotNetCoreDirectory)&quot; --output &quot;$(PackageCorrelationFile)&quot;" WorkingDirectory="$(PackageCorrelationCliDirectory)" />
16
+ </Target>
17
+
18
+ <Target Name="CleanDotNetPackageCorrelationFile" BeforeTargets="Clean">
19
+ <Delete Files="$(PackageCorrelationFile)" Condition="Exists('$(PackageCorrelationFile)')" />
20
+ </Target>
21
+
22
+ <Target Name="RebuildDotNetPackageCorrelationFile" BeforeTargets="Rebuild" DependsOnTargets="CleanDotNetPackageCorrelationFile">
23
+ </Target>
24
+
25
+ </Project>
@@ -1,6 +1,7 @@
1
1
  using System.Text.Json;
2
2
 
3
3
  using NuGetUpdater.Core.Run;
4
+ using NuGetUpdater.Core.Run.ApiModel;
4
5
 
5
6
  namespace NuGetUpdater.Core;
6
7
 
@@ -30,19 +31,28 @@ public record ExperimentsManager
30
31
  };
31
32
  }
32
33
 
33
- public static async Task<ExperimentsManager> FromJobFileAsync(string jobFilePath, ILogger logger)
34
+ public static async Task<(ExperimentsManager ExperimentsManager, JobErrorBase? Error)> FromJobFileAsync(string jobId, string jobFilePath)
34
35
  {
35
- var jobFileContent = await File.ReadAllTextAsync(jobFilePath);
36
+ var experimentsManager = new ExperimentsManager();
37
+ JobErrorBase? error = null;
38
+ var jobFileContent = string.Empty;
36
39
  try
37
40
  {
41
+ jobFileContent = await File.ReadAllTextAsync(jobFilePath);
38
42
  var jobWrapper = RunWorker.Deserialize(jobFileContent);
39
- return GetExperimentsManager(jobWrapper.Job.Experiments);
43
+ experimentsManager = GetExperimentsManager(jobWrapper.Job.Experiments);
40
44
  }
41
45
  catch (JsonException ex)
42
46
  {
43
- logger.Info($"Error deserializing job file: {ex.ToString()}: {jobFileContent}");
44
- return new ExperimentsManager();
47
+ // this is a very specific case where we want to log the JSON contents for easier debugging
48
+ error = JobErrorBase.ErrorFromException(new NotSupportedException($"Error deserializing job file contents: {jobFileContent}", ex), jobId, Environment.CurrentDirectory); // TODO
45
49
  }
50
+ catch (Exception ex)
51
+ {
52
+ error = JobErrorBase.ErrorFromException(ex, jobId, Environment.CurrentDirectory); // TODO
53
+ }
54
+
55
+ return (experimentsManager, error);
46
56
  }
47
57
 
48
58
  private static bool IsEnabled(Dictionary<string, object>? experiments, string experimentName)
@@ -13,12 +13,16 @@ internal sealed class PackagesConfigBuildFile : XmlBuildFile
13
13
  public PackagesConfigBuildFile(string basePath, string path, XmlDocumentSyntax contents)
14
14
  : base(basePath, path, contents)
15
15
  {
16
+ var invalidPackageElements = Packages.Where(p => p.GetAttribute("id") is null || p.GetAttribute("version") is null).ToList();
17
+ if (invalidPackageElements.Any())
18
+ {
19
+ throw new UnparseableFileException("`package` element missing `id` or `version` attributes", path);
20
+ }
16
21
  }
17
22
 
18
23
  public IEnumerable<IXmlElementSyntax> Packages => Contents.RootSyntax.GetElements("package", StringComparison.OrdinalIgnoreCase);
19
24
 
20
25
  public IEnumerable<Dependency> GetDependencies() => Packages
21
- .Where(p => p.GetAttribute("id") is not null && p.GetAttribute("version") is not null)
22
26
  .Select(p => new Dependency(
23
27
  p.GetAttributeValue("id", StringComparison.OrdinalIgnoreCase),
24
28
  p.GetAttributeValue("version", StringComparison.OrdinalIgnoreCase),
@@ -4,7 +4,8 @@ internal class MissingFileException : Exception
4
4
  {
5
5
  public string FilePath { get; }
6
6
 
7
- public MissingFileException(string filePath)
7
+ public MissingFileException(string filePath, string? message = null)
8
+ : base(message)
8
9
  {
9
10
  FilePath = filePath;
10
11
  }