dependabot-nuget 0.278.0 → 0.280.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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/helpers/build +1 -1
  3. data/helpers/lib/NuGetUpdater/.editorconfig +1 -0
  4. data/helpers/lib/NuGetUpdater/Directory.Build.props +1 -0
  5. data/helpers/lib/NuGetUpdater/Directory.Common.props +1 -1
  6. data/helpers/lib/NuGetUpdater/Directory.Packages.props +6 -1
  7. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Analyze.cs +7 -0
  8. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Discover.cs +6 -0
  9. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Update.cs +2 -3
  10. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalyzeWorker.cs +95 -84
  11. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/Requirement.cs +2 -2
  12. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs +53 -46
  13. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/ErrorType.cs +1 -0
  14. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/NativeResult.cs +1 -1
  15. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/DependencyFileNotFound.cs +6 -0
  16. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/JobErrorBase.cs +11 -0
  17. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/PrivateSourceAuthenticationFailure.cs +6 -0
  18. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/UnknownError.cs +6 -0
  19. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/ApiModel/UpdateNotPossible.cs +6 -0
  20. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/HttpApiHandler.cs +5 -0
  21. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/IApiHandler.cs +1 -0
  22. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/RunWorker.cs +67 -15
  23. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/UpdateNotPossibleException.cs +11 -0
  24. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/LockFileUpdater.cs +1 -1
  25. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/PackagesConfigUpdater.cs +2 -0
  26. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/SdkPackageUpdater.cs +2 -2
  27. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/UpdaterWorker.cs +58 -39
  28. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs +16 -5
  29. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/NuGetHelper.cs +1 -1
  30. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/ProcessExtensions.cs +2 -4
  31. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/AnalyzeWorkerTestBase.cs +5 -9
  32. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/RequirementTests.cs +4 -1
  33. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTestBase.cs +5 -8
  34. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/MockNuGetPackage.cs +10 -1
  35. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/RunWorkerTests.cs +92 -0
  36. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/TestApiHandler.cs +10 -4
  37. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTestBase.cs +10 -15
  38. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.PackagesConfig.cs +79 -1
  39. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.Sdk.cs +10 -1
  40. data/helpers/lib/NuGetUpdater/global.json +1 -1
  41. data/lib/dependabot/nuget/file_updater.rb +5 -1
  42. data/lib/dependabot/nuget/native_helpers.rb +9 -4
  43. data/lib/dependabot/nuget/requirement.rb +2 -0
  44. data/lib/dependabot/nuget/update_checker/repository_finder.rb +26 -2
  45. metadata +16 -10
@@ -1,3 +1,4 @@
1
+ using System.Net;
1
2
  using System.Text;
2
3
  using System.Text.Json;
3
4
  using System.Text.Json.Serialization;
@@ -35,25 +36,80 @@ public class RunWorker
35
36
  await File.WriteAllTextAsync(outputFilePath.FullName, resultJson);
36
37
  }
37
38
 
38
- public async Task<RunResult> RunAsync(Job job, DirectoryInfo repoContentsPath, string baseCommitSha)
39
+ public Task<RunResult> RunAsync(Job job, DirectoryInfo repoContentsPath, string baseCommitSha)
39
40
  {
40
- MSBuildHelper.RegisterMSBuild(repoContentsPath.FullName, repoContentsPath.FullName);
41
+ return RunWithErrorHandlingAsync(job, repoContentsPath, baseCommitSha);
42
+ }
43
+
44
+ private async Task<RunResult> RunWithErrorHandlingAsync(Job job, DirectoryInfo repoContentsPath, string baseCommitSha)
45
+ {
46
+ JobErrorBase? error = null;
47
+ string[] lastUsedPackageSourceUrls = []; // used for error reporting below
48
+ var runResult = new RunResult()
49
+ {
50
+ Base64DependencyFiles = [],
51
+ BaseCommitSha = baseCommitSha,
52
+ };
41
53
 
42
- var allDependencyFiles = new Dictionary<string, DependencyFile>();
43
- foreach (var directory in job.GetAllDirectories())
54
+ try
44
55
  {
45
- var result = await RunForDirectory(job, repoContentsPath, directory, baseCommitSha);
46
- foreach (var dependencyFile in result.Base64DependencyFiles)
56
+ MSBuildHelper.RegisterMSBuild(repoContentsPath.FullName, repoContentsPath.FullName);
57
+
58
+ var allDependencyFiles = new Dictionary<string, DependencyFile>();
59
+ foreach (var directory in job.GetAllDirectories())
47
60
  {
48
- allDependencyFiles[dependencyFile.Name] = dependencyFile;
61
+ var localPath = PathHelper.JoinPath(repoContentsPath.FullName, directory);
62
+ lastUsedPackageSourceUrls = NuGetContext.GetPackageSourceUrls(localPath);
63
+ var result = await RunForDirectory(job, repoContentsPath, directory, baseCommitSha);
64
+ foreach (var dependencyFile in result.Base64DependencyFiles)
65
+ {
66
+ allDependencyFiles[dependencyFile.Name] = dependencyFile;
67
+ }
49
68
  }
69
+
70
+ runResult = new RunResult()
71
+ {
72
+ Base64DependencyFiles = allDependencyFiles.Values.ToArray(),
73
+ BaseCommitSha = baseCommitSha,
74
+ };
75
+ }
76
+ catch (HttpRequestException ex)
77
+ when (ex.StatusCode == HttpStatusCode.Unauthorized || ex.StatusCode == HttpStatusCode.Forbidden)
78
+ {
79
+ error = new PrivateSourceAuthenticationFailure()
80
+ {
81
+ Details = $"({string.Join("|", lastUsedPackageSourceUrls)})",
82
+ };
83
+ }
84
+ catch (MissingFileException ex)
85
+ {
86
+ error = new DependencyFileNotFound()
87
+ {
88
+ Details = ex.FilePath,
89
+ };
90
+ }
91
+ catch (UpdateNotPossibleException ex)
92
+ {
93
+ error = new UpdateNotPossible()
94
+ {
95
+ Details = ex.Dependencies,
96
+ };
97
+ }
98
+ catch (Exception ex)
99
+ {
100
+ error = new UnknownError()
101
+ {
102
+ Details = ex.ToString(),
103
+ };
50
104
  }
51
105
 
52
- var runResult = new RunResult()
106
+ if (error is not null)
53
107
  {
54
- Base64DependencyFiles = allDependencyFiles.Values.ToArray(),
55
- BaseCommitSha = baseCommitSha,
56
- };
108
+ await _apiHandler.RecordUpdateJobError(error);
109
+ }
110
+
111
+ await _apiHandler.MarkAsProcessed(new() { BaseCommitSha = baseCommitSha });
112
+
57
113
  return runResult;
58
114
  }
59
115
 
@@ -61,7 +117,6 @@ public class RunWorker
61
117
  {
62
118
  var discoveryWorker = new DiscoveryWorker(_logger);
63
119
  var discoveryResult = await discoveryWorker.RunAsync(repoContentsPath.FullName, repoDirectory);
64
- // TODO: check discoveryResult.ErrorType
65
120
 
66
121
  _logger.Log("Discovery JSON content:");
67
122
  _logger.Log(JsonSerializer.Serialize(discoveryResult, DiscoveryWorker.SerializerOptions));
@@ -123,7 +178,6 @@ public class RunWorker
123
178
  };
124
179
  var analysisResult = await analyzeWorker.RunAsync(repoContentsPath.FullName, discoveryResult, dependencyInfo);
125
180
  // TODO: log analysisResult
126
- // TODO: check analysisResult.ErrorType
127
181
  if (analysisResult.CanUpdate)
128
182
  {
129
183
  // TODO: this is inefficient, but not likely causing a bottleneck
@@ -153,7 +207,6 @@ public class RunWorker
153
207
  var updateWorker = new UpdaterWorker(_logger);
154
208
  var dependencyFilePath = Path.Join(discoveryResult.Path, project.FilePath).NormalizePathToUnix();
155
209
  var updateResult = await updateWorker.RunAsync(repoContentsPath.FullName, dependencyFilePath, dependency.Name, dependency.Version!, analysisResult.UpdatedVersion, isTransitive: false);
156
- // TODO: check specific contents of result.ErrorType
157
210
  // TODO: need to report if anything was actually updated
158
211
  if (updateResult.ErrorType is null || updateResult.ErrorType == ErrorType.None)
159
212
  {
@@ -206,7 +259,6 @@ public class RunWorker
206
259
  // TODO: throw if no updates performed
207
260
  }
208
261
 
209
- await _apiHandler.MarkAsProcessed(new() { BaseCommitSha = baseCommitSha });
210
262
  var result = new RunResult()
211
263
  {
212
264
  Base64DependencyFiles = originalDependencyFileContents.Select(kvp => new DependencyFile()
@@ -0,0 +1,11 @@
1
+ namespace NuGetUpdater.Core;
2
+
3
+ internal class UpdateNotPossibleException : Exception
4
+ {
5
+ public string[] Dependencies { get; }
6
+
7
+ public UpdateNotPossibleException(string[] dependencies)
8
+ {
9
+ Dependencies = dependencies;
10
+ }
11
+ }
@@ -18,7 +18,7 @@ internal static class LockFileUpdater
18
18
 
19
19
  await MSBuildHelper.SidelineGlobalJsonAsync(projectDirectory, repoRootPath, async () =>
20
20
  {
21
- var (exitCode, stdout, stderr) = await ProcessEx.RunAsync("dotnet", $"restore --force-evaluate {projectPath}", workingDirectory: projectDirectory);
21
+ var (exitCode, stdout, stderr) = await ProcessEx.RunAsync("dotnet", ["restore", "--force-evaluate", projectPath], workingDirectory: projectDirectory);
22
22
  if (exitCode != 0)
23
23
  {
24
24
  logger.Log($" Lock file update failed.\nSTDOUT:\n{stdout}\nSTDERR:\n{stderr}");
@@ -139,6 +139,7 @@ internal static class PackagesConfigUpdater
139
139
 
140
140
  if (exitCodeAgain != 0)
141
141
  {
142
+ MSBuildHelper.ThrowOnMissingPackages(restoreOutput);
142
143
  throw new Exception($"Unable to restore.\nOutput:\n${restoreOutput}\n");
143
144
  }
144
145
 
@@ -147,6 +148,7 @@ internal static class PackagesConfigUpdater
147
148
 
148
149
  MSBuildHelper.ThrowOnUnauthenticatedFeed(fullOutput);
149
150
  MSBuildHelper.ThrowOnMissingFile(fullOutput);
151
+ MSBuildHelper.ThrowOnMissingPackages(fullOutput);
150
152
  throw new Exception(fullOutput);
151
153
  }
152
154
  }
@@ -231,7 +231,7 @@ internal static class SdkPackageUpdater
231
231
  logger.Log($" Adding [{dependencyName}/{newDependencyVersion}] as a top-level package reference.");
232
232
 
233
233
  // see https://learn.microsoft.com/nuget/consume-packages/install-use-packages-dotnet-cli
234
- var (exitCode, stdout, stderr) = await ProcessEx.RunAsync("dotnet", $"add {projectPath} package {dependencyName} --version {newDependencyVersion}", workingDirectory: projectDirectory);
234
+ var (exitCode, stdout, stderr) = await ProcessEx.RunAsync("dotnet", ["add", projectPath, "package", dependencyName, "--version", newDependencyVersion], workingDirectory: projectDirectory);
235
235
  MSBuildHelper.ThrowOnUnauthenticatedFeed(stdout);
236
236
  if (exitCode != 0)
237
237
  {
@@ -350,7 +350,7 @@ internal static class SdkPackageUpdater
350
350
  var specificResolvedDependency = resolvedDependencies.Where(d => d.Name.Equals(dependencyName, StringComparison.OrdinalIgnoreCase)).FirstOrDefault();
351
351
  if (specificResolvedDependency is null)
352
352
  {
353
- logger.Log($" Unable resolve requested dependency for {dependencyName} in {projectFile.Path}.");
353
+ logger.Log($" Unable to resolve requested dependency for {dependencyName} in {projectFile.Path}.");
354
354
  continue;
355
355
  }
356
356
 
@@ -25,57 +25,29 @@ public class UpdaterWorker
25
25
 
26
26
  public async Task RunAsync(string repoRootPath, string workspacePath, string dependencyName, string previousDependencyVersion, string newDependencyVersion, bool isTransitive, string? resultOutputPath = null)
27
27
  {
28
- var result = await RunAsync(repoRootPath, workspacePath, dependencyName, previousDependencyVersion, newDependencyVersion, isTransitive);
28
+ var result = await RunWithErrorHandlingAsync(repoRootPath, workspacePath, dependencyName, previousDependencyVersion, newDependencyVersion, isTransitive);
29
29
  if (resultOutputPath is { })
30
30
  {
31
31
  await WriteResultFile(result, resultOutputPath, _logger);
32
32
  }
33
33
  }
34
34
 
35
- public async Task<UpdateOperationResult> RunAsync(string repoRootPath, string workspacePath, string dependencyName, string previousDependencyVersion, string newDependencyVersion, bool isTransitive)
35
+ // this is a convenient method for tests
36
+ internal async Task<UpdateOperationResult> RunWithErrorHandlingAsync(string repoRootPath, string workspacePath, string dependencyName, string previousDependencyVersion, string newDependencyVersion, bool isTransitive)
36
37
  {
37
- MSBuildHelper.RegisterMSBuild(Environment.CurrentDirectory, repoRootPath);
38
- UpdateOperationResult result;
39
-
40
- if (!Path.IsPathRooted(workspacePath) || !File.Exists(workspacePath))
41
- {
42
- workspacePath = Path.GetFullPath(Path.Join(repoRootPath, workspacePath));
43
- }
44
-
38
+ UpdateOperationResult result = new(); // assumed to be ok until proven otherwise
45
39
  try
46
40
  {
47
- if (!isTransitive)
48
- {
49
- await DotNetToolsJsonUpdater.UpdateDependencyAsync(repoRootPath, workspacePath, dependencyName, previousDependencyVersion, newDependencyVersion, _logger);
50
- await GlobalJsonUpdater.UpdateDependencyAsync(repoRootPath, workspacePath, dependencyName, previousDependencyVersion, newDependencyVersion, _logger);
51
- }
52
-
53
- var extension = Path.GetExtension(workspacePath).ToLowerInvariant();
54
- switch (extension)
55
- {
56
- case ".sln":
57
- await RunForSolutionAsync(repoRootPath, workspacePath, dependencyName, previousDependencyVersion, newDependencyVersion, isTransitive);
58
- break;
59
- case ".proj":
60
- await RunForProjFileAsync(repoRootPath, workspacePath, dependencyName, previousDependencyVersion, newDependencyVersion, isTransitive);
61
- break;
62
- case ".csproj":
63
- case ".fsproj":
64
- case ".vbproj":
65
- await RunForProjectAsync(repoRootPath, workspacePath, dependencyName, previousDependencyVersion, newDependencyVersion, isTransitive);
66
- break;
67
- default:
68
- _logger.Log($"File extension [{extension}] is not supported.");
69
- break;
70
- }
71
-
72
- result = new(); // all ok
73
- _logger.Log("Update complete.");
41
+ result = await RunAsync(repoRootPath, workspacePath, dependencyName, previousDependencyVersion, newDependencyVersion, isTransitive);
74
42
  }
75
43
  catch (HttpRequestException ex)
76
44
  when (ex.StatusCode == HttpStatusCode.Unauthorized || ex.StatusCode == HttpStatusCode.Forbidden)
77
45
  {
78
- // TODO: consolidate this error handling between AnalyzeWorker, DiscoveryWorker, and UpdateWorker
46
+ if (!Path.IsPathRooted(workspacePath) || !File.Exists(workspacePath))
47
+ {
48
+ workspacePath = Path.GetFullPath(Path.Join(repoRootPath, workspacePath));
49
+ }
50
+
79
51
  result = new()
80
52
  {
81
53
  ErrorType = ErrorType.AuthenticationFailure,
@@ -90,11 +62,58 @@ public class UpdaterWorker
90
62
  ErrorDetails = ex.FilePath,
91
63
  };
92
64
  }
65
+ catch (UpdateNotPossibleException ex)
66
+ {
67
+ result = new()
68
+ {
69
+ ErrorType = ErrorType.UpdateNotPossible,
70
+ ErrorDetails = ex.Dependencies,
71
+ };
72
+ }
93
73
 
94
- _processedProjectPaths.Clear();
95
74
  return result;
96
75
  }
97
76
 
77
+ public async Task<UpdateOperationResult> RunAsync(string repoRootPath, string workspacePath, string dependencyName, string previousDependencyVersion, string newDependencyVersion, bool isTransitive)
78
+ {
79
+ MSBuildHelper.RegisterMSBuild(Environment.CurrentDirectory, repoRootPath);
80
+
81
+ if (!Path.IsPathRooted(workspacePath) || !File.Exists(workspacePath))
82
+ {
83
+ workspacePath = Path.GetFullPath(Path.Join(repoRootPath, workspacePath));
84
+ }
85
+
86
+ if (!isTransitive)
87
+ {
88
+ await DotNetToolsJsonUpdater.UpdateDependencyAsync(repoRootPath, workspacePath, dependencyName, previousDependencyVersion, newDependencyVersion, _logger);
89
+ await GlobalJsonUpdater.UpdateDependencyAsync(repoRootPath, workspacePath, dependencyName, previousDependencyVersion, newDependencyVersion, _logger);
90
+ }
91
+
92
+ var extension = Path.GetExtension(workspacePath).ToLowerInvariant();
93
+ switch (extension)
94
+ {
95
+ case ".sln":
96
+ await RunForSolutionAsync(repoRootPath, workspacePath, dependencyName, previousDependencyVersion, newDependencyVersion, isTransitive);
97
+ break;
98
+ case ".proj":
99
+ await RunForProjFileAsync(repoRootPath, workspacePath, dependencyName, previousDependencyVersion, newDependencyVersion, isTransitive);
100
+ break;
101
+ case ".csproj":
102
+ case ".fsproj":
103
+ case ".vbproj":
104
+ await RunForProjectAsync(repoRootPath, workspacePath, dependencyName, previousDependencyVersion, newDependencyVersion, isTransitive);
105
+ break;
106
+ default:
107
+ _logger.Log($"File extension [{extension}] is not supported.");
108
+ break;
109
+ }
110
+
111
+ _logger.Log("Update complete.");
112
+
113
+ _processedProjectPaths.Clear();
114
+ return new UpdateOperationResult();
115
+ }
116
+
98
117
  internal static async Task WriteResultFile(UpdateOperationResult result, string resultOutputPath, Logger logger)
99
118
  {
100
119
  logger.Log($" Writing update result to [{resultOutputPath}].");
@@ -322,7 +322,7 @@ internal static partial class MSBuildHelper
322
322
  try
323
323
  {
324
324
  var tempProjectPath = await CreateTempProjectAsync(tempDirectory, repoRoot, projectPath, targetFramework, packages);
325
- var (exitCode, stdOut, stdErr) = await ProcessEx.RunAsync("dotnet", $"restore \"{tempProjectPath}\"", workingDirectory: tempDirectory.FullName);
325
+ var (exitCode, stdOut, stdErr) = await ProcessEx.RunAsync("dotnet", ["restore", tempProjectPath], workingDirectory: tempDirectory.FullName);
326
326
 
327
327
  // NU1608: Detected package version outside of dependency constraint
328
328
 
@@ -359,7 +359,7 @@ internal static partial class MSBuildHelper
359
359
  try
360
360
  {
361
361
  string tempProjectPath = await CreateTempProjectAsync(tempDirectory, repoRoot, projectPath, targetFramework, packages);
362
- var (exitCode, stdOut, stdErr) = await ProcessEx.RunAsync("dotnet", $"restore \"{tempProjectPath}\"", workingDirectory: tempDirectory.FullName);
362
+ var (exitCode, stdOut, stdErr) = await ProcessEx.RunAsync("dotnet", ["restore", tempProjectPath], workingDirectory: tempDirectory.FullName);
363
363
 
364
364
  // Add Dependency[] packages to List<PackageToUpdate> existingPackages
365
365
  List<PackageToUpdate> existingPackages = packages
@@ -515,7 +515,7 @@ internal static partial class MSBuildHelper
515
515
  try
516
516
  {
517
517
  var tempProjectPath = await CreateTempProjectAsync(tempDirectory, repoRoot, projectPath, targetFramework, packages);
518
- var (exitCode, stdOut, stdErr) = await ProcessEx.RunAsync("dotnet", $"restore \"{tempProjectPath}\"", workingDirectory: tempDirectory.FullName);
518
+ var (exitCode, stdOut, stdErr) = await ProcessEx.RunAsync("dotnet", ["restore", tempProjectPath], workingDirectory: tempDirectory.FullName);
519
519
  ThrowOnUnauthenticatedFeed(stdOut);
520
520
 
521
521
  // simple cases first
@@ -540,7 +540,7 @@ internal static partial class MSBuildHelper
540
540
  foreach ((string PackageName, NuGetVersion packageVersion) in badPackagesAndVersions)
541
541
  {
542
542
  // this command dumps a JSON object with all versions of the specified package from all package sources
543
- (exitCode, stdOut, stdErr) = await ProcessEx.RunAsync("dotnet", $"package search {PackageName} --exact-match --format json", workingDirectory: tempDirectory.FullName);
543
+ (exitCode, stdOut, stdErr) = await ProcessEx.RunAsync("dotnet", ["package", "search", PackageName, "--exact-match", "--format", "json"], workingDirectory: tempDirectory.FullName);
544
544
  if (exitCode != 0)
545
545
  {
546
546
  continue;
@@ -770,7 +770,7 @@ internal static partial class MSBuildHelper
770
770
  var topLevelPackagesNames = packages.Select(p => p.Name).ToHashSet(StringComparer.OrdinalIgnoreCase);
771
771
  var tempProjectPath = await CreateTempProjectAsync(tempDirectory, repoRoot, projectPath, targetFramework, packages);
772
772
 
773
- var (exitCode, stdout, stderr) = await ProcessEx.RunAsync("dotnet", $"build \"{tempProjectPath}\" /t:_ReportDependencies", workingDirectory: tempDirectory.FullName);
773
+ var (exitCode, stdout, stderr) = await ProcessEx.RunAsync("dotnet", ["build", tempProjectPath, "/t:_ReportDependencies"], workingDirectory: tempDirectory.FullName);
774
774
  ThrowOnUnauthenticatedFeed(stdout);
775
775
 
776
776
  if (exitCode == 0)
@@ -833,6 +833,17 @@ internal static partial class MSBuildHelper
833
833
  }
834
834
  }
835
835
 
836
+ internal static void ThrowOnMissingPackages(string output)
837
+ {
838
+ var missingPackagesPattern = new Regex(@"Package '(?<PackageName>[^'].*)' is not found on source");
839
+ var matchCollection = missingPackagesPattern.Matches(output);
840
+ var missingPackages = matchCollection.Select(m => m.Groups["PackageName"].Value).Distinct().ToArray();
841
+ if (missingPackages.Length > 0)
842
+ {
843
+ throw new UpdateNotPossibleException(missingPackages);
844
+ }
845
+ }
846
+
836
847
  internal static bool TryGetGlobalJsonPath(string repoRootPath, string workspacePath, [NotNullWhen(returnValue: true)] out string? globalJsonPath)
837
848
  {
838
849
  globalJsonPath = PathHelper.GetFileInDirectoryOrParent(workspacePath, repoRootPath, "global.json", caseSensitive: false);
@@ -26,7 +26,7 @@ internal static class NuGetHelper
26
26
  try
27
27
  {
28
28
  var tempProjectPath = await MSBuildHelper.CreateTempProjectAsync(tempDirectory, repoRoot, projectPath, "netstandard2.0", packages, usePackageDownload: true);
29
- var (exitCode, stdOut, stdErr) = await ProcessEx.RunAsync("dotnet", $"restore \"{tempProjectPath}\"");
29
+ var (exitCode, stdOut, stdErr) = await ProcessEx.RunAsync("dotnet", ["restore", tempProjectPath]);
30
30
 
31
31
  return exitCode == 0;
32
32
  }
@@ -5,17 +5,15 @@ namespace NuGetUpdater.Core;
5
5
 
6
6
  public static class ProcessEx
7
7
  {
8
- public static Task<(int ExitCode, string Output, string Error)> RunAsync(string fileName, string arguments = "", string? workingDirectory = null)
8
+ public static Task<(int ExitCode, string Output, string Error)> RunAsync(string fileName, IEnumerable<string>? arguments = null, string? workingDirectory = null)
9
9
  {
10
10
  var tcs = new TaskCompletionSource<(int, string, string)>();
11
11
 
12
12
  var redirectInitiated = new ManualResetEventSlim();
13
13
  var process = new Process
14
14
  {
15
- StartInfo =
15
+ StartInfo = new ProcessStartInfo(fileName, arguments ?? [])
16
16
  {
17
- FileName = fileName,
18
- Arguments = arguments,
19
17
  UseShellExecute = false, // required to redirect output
20
18
  RedirectStandardOutput = true,
21
19
  RedirectStandardError = true,
@@ -35,10 +35,10 @@ public class AnalyzeWorkerTestBase
35
35
 
36
36
  var discoveryPath = Path.GetFullPath(DiscoveryWorker.DiscoveryResultFileName, directoryPath);
37
37
  var dependencyPath = Path.GetFullPath(relativeDependencyPath, directoryPath);
38
- var analysisPath = Path.GetFullPath(AnalyzeWorker.AnalysisDirectoryName, directoryPath);
39
38
 
40
39
  var worker = new AnalyzeWorker(new Logger(verbose: true));
41
- await worker.RunAsync(directoryPath, discoveryPath, dependencyPath, analysisPath);
40
+ var result = await worker.RunWithErrorHandlingAsync(directoryPath, discoveryPath, dependencyPath);
41
+ return result;
42
42
  });
43
43
 
44
44
  ValidateAnalysisResult(expectedResult, actualResult);
@@ -78,17 +78,13 @@ public class AnalyzeWorkerTestBase
78
78
  }
79
79
  }
80
80
 
81
- protected static async Task<AnalysisResult> RunAnalyzerAsync(string dependencyName, TestFile[] files, Func<string, Task> action)
81
+ protected static async Task<AnalysisResult> RunAnalyzerAsync(string dependencyName, TestFile[] files, Func<string, Task<AnalysisResult>> action)
82
82
  {
83
83
  // write initial files
84
84
  using var temporaryDirectory = await TemporaryDirectory.CreateWithContentsAsync(files);
85
85
 
86
86
  // run discovery
87
- await action(temporaryDirectory.DirectoryPath);
88
-
89
- // gather results
90
- var resultPath = Path.Join(temporaryDirectory.DirectoryPath, AnalyzeWorker.AnalysisDirectoryName, $"{dependencyName}.json");
91
- var resultJson = await File.ReadAllTextAsync(resultPath);
92
- return JsonSerializer.Deserialize<AnalysisResult>(resultJson, DiscoveryWorker.SerializerOptions)!;
87
+ var result = await action(temporaryDirectory.DirectoryPath);
88
+ return result;
93
89
  }
94
90
  }
@@ -46,11 +46,14 @@ public class RequirementTests
46
46
  }
47
47
 
48
48
  [Theory]
49
+ [InlineData("> *", "> 0")] // standard wildcard, no digits
49
50
  [InlineData("> 1.*", "> 1.0")] // standard wildcard, single digit
50
51
  [InlineData("> 1.2.*", "> 1.2.0")] // standard wildcard, multiple digit
52
+ [InlineData("> a", "> 0")] // alternate wildcard, no digits
51
53
  [InlineData("> 1.a", "> 1.0")] // alternate wildcard, single digit
52
54
  [InlineData("> 1.2.a", "> 1.2.0")] // alternate wildcard, multiple digit
53
- public void Parse_ConvertsWildcardInVersion(string givenRequirementString, string expectedRequirementString)
55
+ [InlineData(">= 1.40.0, ", ">= 1.40.0")] // empty string following comma
56
+ public void Parse_Requirement(string givenRequirementString, string expectedRequirementString)
54
57
  {
55
58
  var parsedRequirement = Requirement.Parse(givenRequirementString);
56
59
  var actualRequirementString = parsedRequirement.ToString();
@@ -25,7 +25,8 @@ public class DiscoveryWorkerTestBase
25
25
  await UpdateWorkerTestBase.MockNuGetPackagesInDirectory(packages, directoryPath);
26
26
 
27
27
  var worker = new DiscoveryWorker(new Logger(verbose: true));
28
- await worker.RunAsync(directoryPath, workspacePath, DiscoveryWorker.DiscoveryResultFileName);
28
+ var result = await worker.RunWithErrorHandlingAsync(directoryPath, workspacePath);
29
+ return result;
29
30
  });
30
31
 
31
32
  ValidateWorkspaceResult(expectedResult, actualResult);
@@ -108,18 +109,14 @@ public class DiscoveryWorkerTestBase
108
109
  }
109
110
  }
110
111
 
111
- protected static async Task<WorkspaceDiscoveryResult> RunDiscoveryAsync(TestFile[] files, Func<string, Task> action)
112
+ protected static async Task<WorkspaceDiscoveryResult> RunDiscoveryAsync(TestFile[] files, Func<string, Task<WorkspaceDiscoveryResult>> action)
112
113
  {
113
114
  // write initial files
114
115
  using var temporaryDirectory = await TemporaryDirectory.CreateWithContentsAsync(files);
115
116
 
116
117
  // run discovery
117
- await action(temporaryDirectory.DirectoryPath);
118
-
119
- // gather results
120
- var resultPath = Path.Join(temporaryDirectory.DirectoryPath, DiscoveryWorker.DiscoveryResultFileName);
121
- var resultJson = await File.ReadAllTextAsync(resultPath);
122
- return JsonSerializer.Deserialize<WorkspaceDiscoveryResult>(resultJson, DiscoveryWorker.SerializerOptions)!;
118
+ var result = await action(temporaryDirectory.DirectoryPath);
119
+ return result;
123
120
  }
124
121
 
125
122
  internal class PropertyComparer : IEqualityComparer<Property>
@@ -315,7 +315,7 @@ namespace NuGetUpdater.Core.Test
315
315
  </Project>
316
316
  """
317
317
  );
318
- var (exitCode, stdout, stderr) = ProcessEx.RunAsync("dotnet", $"msbuild {projectPath} /t:_ReportCurrentSdkVersion").Result;
318
+ var (exitCode, stdout, stderr) = ProcessEx.RunAsync("dotnet", ["msbuild", projectPath, "/t:_ReportCurrentSdkVersion"]).Result;
319
319
  if (exitCode != 0)
320
320
  {
321
321
  throw new Exception($"Failed to report the current SDK version:\n{stdout}\n{stderr}");
@@ -391,6 +391,7 @@ namespace NuGetUpdater.Core.Test
391
391
  WellKnownReferencePackage("Microsoft.AspNetCore.App", "net6.0"),
392
392
  WellKnownReferencePackage("Microsoft.AspNetCore.App", "net7.0"),
393
393
  WellKnownReferencePackage("Microsoft.AspNetCore.App", "net8.0"),
394
+ WellKnownReferencePackage("Microsoft.AspNetCore.App", "net9.0"),
394
395
  WellKnownReferencePackage("Microsoft.NETCore.App", "net6.0",
395
396
  [
396
397
  ("data/FrameworkList.xml", Encoding.UTF8.GetBytes("""
@@ -412,9 +413,17 @@ namespace NuGetUpdater.Core.Test
412
413
  </FileList>
413
414
  """))
414
415
  ]),
416
+ WellKnownReferencePackage("Microsoft.NETCore.App", "net9.0",
417
+ [
418
+ ("data/FrameworkList.xml", Encoding.UTF8.GetBytes("""
419
+ <FileList TargetFrameworkIdentifier=".NETCoreApp" TargetFrameworkVersion="9.0" FrameworkName="Microsoft.NETCore.App" Name=".NET Runtime">
420
+ </FileList>
421
+ """))
422
+ ]),
415
423
  WellKnownReferencePackage("Microsoft.WindowsDesktop.App", "net6.0"),
416
424
  WellKnownReferencePackage("Microsoft.WindowsDesktop.App", "net7.0"),
417
425
  WellKnownReferencePackage("Microsoft.WindowsDesktop.App", "net8.0"),
426
+ WellKnownReferencePackage("Microsoft.WindowsDesktop.App", "net9.0"),
418
427
  ];
419
428
  }
420
429
  }
@@ -169,6 +169,98 @@ public class RunWorkerTests
169
169
  );
170
170
  }
171
171
 
172
+ [Fact]
173
+ public async Task PrivateSourceAuthenticationFailureIsForwaredToApiHandler()
174
+ {
175
+ static (int, string) TestHttpHandler(string uriString)
176
+ {
177
+ var uri = new Uri(uriString, UriKind.Absolute);
178
+ var baseUrl = $"{uri.Scheme}://{uri.Host}:{uri.Port}";
179
+ return uri.PathAndQuery switch
180
+ {
181
+ // initial request is good
182
+ "/index.json" => (200, $$"""
183
+ {
184
+ "version": "3.0.0",
185
+ "resources": [
186
+ {
187
+ "@id": "{{baseUrl}}/download",
188
+ "@type": "PackageBaseAddress/3.0.0"
189
+ },
190
+ {
191
+ "@id": "{{baseUrl}}/query",
192
+ "@type": "SearchQueryService"
193
+ },
194
+ {
195
+ "@id": "{{baseUrl}}/registrations",
196
+ "@type": "RegistrationsBaseUrl"
197
+ }
198
+ ]
199
+ }
200
+ """),
201
+ // all other requests are unauthorized
202
+ _ => (401, "{}"),
203
+ };
204
+ }
205
+ using var http = TestHttpServer.CreateTestStringServer(TestHttpHandler);
206
+ await RunAsync(
207
+ packages:
208
+ [
209
+ ],
210
+ job: new Job()
211
+ {
212
+ PackageManager = "nuget",
213
+ Source = new()
214
+ {
215
+ Provider = "github",
216
+ Repo = "test/repo",
217
+ Directory = "/",
218
+ },
219
+ AllowedUpdates =
220
+ [
221
+ new() { UpdateType = "all" }
222
+ ]
223
+ },
224
+ files:
225
+ [
226
+ ("NuGet.Config", $"""
227
+ <configuration>
228
+ <packageSources>
229
+ <clear />
230
+ <add key="private_feed" value="{http.BaseUrl.TrimEnd('/')}/index.json" allowInsecureConnections="true" />
231
+ </packageSources>
232
+ </configuration>
233
+ """),
234
+ ("project.csproj", """
235
+ <Project Sdk="Microsoft.NET.Sdk">
236
+ <PropertyGroup>
237
+ <TargetFramework>net8.0</TargetFramework>
238
+ </PropertyGroup>
239
+ <ItemGroup>
240
+ <PackageReference Include="Some.Package" Version="1.0.0" />
241
+ </ItemGroup>
242
+ </Project>
243
+ """)
244
+ ],
245
+ expectedResult: new RunResult()
246
+ {
247
+ Base64DependencyFiles = [],
248
+ BaseCommitSha = "TEST-COMMIT-SHA",
249
+ },
250
+ expectedApiMessages:
251
+ [
252
+ new PrivateSourceAuthenticationFailure()
253
+ {
254
+ Details = $"({http.BaseUrl.TrimEnd('/')}/index.json)"
255
+ },
256
+ new MarkAsProcessed()
257
+ {
258
+ BaseCommitSha = "TEST-COMMIT-SHA",
259
+ }
260
+ ]
261
+ );
262
+ }
263
+
172
264
  private static async Task RunAsync(Job job, TestFile[] files, RunResult expectedResult, object[] expectedApiMessages, MockNuGetPackage[]? packages = null)
173
265
  {
174
266
  // arrange