diff --git a/source/Calamari.Shared/Integration/Packages/NuGet/NuGetV3LibDownloader.cs b/source/Calamari.Shared/Integration/Packages/NuGet/NuGetV3LibDownloader.cs index a18e0fc838..4b28b2e22e 100644 --- a/source/Calamari.Shared/Integration/Packages/NuGet/NuGetV3LibDownloader.cs +++ b/source/Calamari.Shared/Integration/Packages/NuGet/NuGetV3LibDownloader.cs @@ -8,7 +8,6 @@ using System.IO; using System.Threading.Tasks; using Calamari.Common.Plumbing.Extensions; -using NuGet.Commands; using NuGet.Packaging.Core; using Octopus.Versioning; @@ -28,17 +27,38 @@ public static void DownloadPackage(string packageId, IVersion version, Uri feedU using (var sourceCacheContext = new SourceCacheContext() { NoCache = true }) { - var providers = new SourceRepositoryDependencyProvider(sourceRepository, logger, sourceCacheContext, sourceCacheContext.IgnoreFailedSources, false); var targetPath = Directory.GetParent(targetFilePath).FullName; if (!Directory.Exists(targetPath)) { Directory.CreateDirectory(targetPath); } + // Resolve the downloader from FindPackageByIdResource directly rather than going through + // SourceRepositoryDependencyProvider. The provider unconditionally dereferences the downloader returned + // here, which is null when the requested version isn't on the feed, producing a bare NullReferenceException + // (FD-440). Calling the resource directly lets us null-check it and surface an actionable error. The + // provider's throttle / ignore-failed-sources behaviour isn't relevant here: Calamari passes no throttle + // and downloads from a single source. + var findPackageByIdResource = sourceRepository.GetResourceAsync(CancellationToken.None) + .GetAwaiter() + .GetResult(); + + // GetResourceAsync returns null (rather than throwing) when no provider can supply the resource for this + // feed, so guard it explicitly — otherwise the dereference below would resurface the very + // NullReferenceException this change exists to eliminate (FD-440). + if (findPackageByIdResource == null) + throw new Exception($"The NuGet feed '{feedUri}' did not return a package lookup resource (FindPackageByIdResource). Make sure the feed URL is a valid NuGet V3 feed."); + + var packageIdentity = new PackageIdentity(packageId, version.ToNuGetVersion()); + string targetTempNupkg = Path.Combine(targetPath, Path.GetRandomFileName()); - var packageDownloader = providers.GetPackageDownloaderAsync(new PackageIdentity(packageId, version.ToNuGetVersion()), sourceCacheContext, logger, CancellationToken.None) - .GetAwaiter() - .GetResult(); + var packageDownloader = findPackageByIdResource.GetPackageDownloaderAsync(packageIdentity, sourceCacheContext, logger, CancellationToken.None) + .GetAwaiter() + .GetResult(); + + if (packageDownloader == null) + throw new Exception($"Package {packageId} version {version} was not found on the NuGet feed '{feedUri}'. Make sure the package version has been pushed to the feed."); + var fileCopied = packageDownloader.CopyNupkgFileToAsync(targetTempNupkg, CancellationToken.None).GetAwaiter().GetResult(); if (!fileCopied) //I would expect any actual standard exception to be thrown above and not returned as a bool diff --git a/source/Calamari.Tests/Fixtures/Integration/Packages/NuGetPackageDownloaderFixture.cs b/source/Calamari.Tests/Fixtures/Integration/Packages/NuGetPackageDownloaderFixture.cs index 3111f92cab..eff60b764c 100644 --- a/source/Calamari.Tests/Fixtures/Integration/Packages/NuGetPackageDownloaderFixture.cs +++ b/source/Calamari.Tests/Fixtures/Integration/Packages/NuGetPackageDownloaderFixture.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics; +using System.IO; using System.Net; using Calamari.Common.Plumbing.FileSystem; using Calamari.Common.Plumbing.Variables; @@ -64,5 +65,25 @@ public int AttemptsTheRightNumberOfTimesOnError(int maxDownloadAttempts) return calledCount; } + + [Test] + [RequiresNonFreeBSDPlatform] + public void GivesActionableErrorWhenV3FeedIsMissingTheRequestedVersion() + { + // FD-440: requesting a version that doesn't exist on a NuGet V3 feed used to throw a bare + // NullReferenceException from inside the NuGet client. It should give a clear "not found" message instead. + var targetFilePath = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():N}.nupkg"); + + var ex = Assert.Throws(() => + NuGetV3LibDownloader.DownloadPackage( + "Newtonsoft.Json", + VersionFactory.CreateSemanticVersion("999.999.999"), + new Uri("https://api.nuget.org/v3/index.json"), + null, + targetFilePath)); + + ex.Should().NotBeOfType("the missing version should surface an actionable message, not a bare NRE"); + ex!.Message.Should().Contain("was not found"); + } } }