dependabot-nuget 0.278.0 → 0.280.0

Sign up to get free protection for your applications and to get access to all the features.
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