Migrate NuGet-backed managers to the V3 API - #5340
Conversation
There was a problem hiding this comment.
Pull request overview
Migrates supported NuGet-backed managers to V3 APIs while retaining V2 compatibility.
Changes:
- Adds V3 discovery, search, metadata, update, and package-content handling.
- Introduces ecosystem-specific SemVer and PEP 440 comparisons.
- Updates .NET Tool defaults, operation history, and regression coverage.
Reviewed changes
Copilot reviewed 30 out of 30 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
src/UniGetUI.PackageEngine.Tests/SemanticVersionTests.cs |
Tests semantic-version parsing and ordering. |
src/UniGetUI.PackageEngine.Tests/PythonVersionTests.cs |
Tests PEP 440 behavior. |
src/UniGetUI.PackageEngine.Tests/PackageTests.cs |
Tests manager-specific comparisons. |
src/UniGetUI.PackageEngine.Tests/PackageOperationsTests.cs |
Tests version-aware cleanup. |
src/UniGetUI.PackageEngine.Tests/OperationHistoryTests.cs |
Tests pinned-version history. |
src/UniGetUI.PackageEngine.Tests/NuGetV3ManagerTests.cs |
Tests V3 manager integration. |
src/UniGetUI.PackageEngine.Tests/NuGetV3ClientTests.cs |
Tests V3 protocol handling. |
src/UniGetUI.PackageEngine.Tests/Infrastructure/Helpers/TestHttpServer.cs |
Adds concurrent HTTP test support. |
src/UniGetUI.PackageEngine.Tests/DotNetManagerTests.cs |
Tests source and scope changes. |
src/UniGetUI.PackageEngine.PackageManagerClasses/Packages/Package.cs |
Uses manager-specific comparisons. |
src/UniGetUI.PackageEngine.PackageManagerClasses/Manager/PackageManager.cs |
Adds default version comparison. |
src/UniGetUI.PackageEngine.PackageManagerClasses/Manager/Classes/NullPackageManager.cs |
Implements the comparison contract. |
src/UniGetUI.PackageEngine.Operations/PackageOperations.cs |
Applies ecosystem ordering to cleanup. |
src/UniGetUI.PackageEngine.Operations/History/OperationHistoryRecord.cs |
Records pinned target versions. |
src/UniGetUI.PackageEngine.Managers.Pip/Pip.cs |
Adds PEP 440 comparison. |
src/UniGetUI.PackageEngine.Managers.Npm/Npm.cs |
Adds semantic comparison. |
src/UniGetUI.PackageEngine.Managers.Generic.NuGet/Internal/NuGetV3ServiceIndex.cs |
Resolves V3 resources. |
src/UniGetUI.PackageEngine.Managers.Generic.NuGet/Internal/NuGetV3Json.cs |
Defines source-generated JSON models. |
src/UniGetUI.PackageEngine.Managers.Generic.NuGet/Internal/NuGetV3Client.cs |
Implements V3 requests and caching. |
src/UniGetUI.PackageEngine.Managers.Generic.NuGet/Internal/NuGetManifestLoader.cs |
Generates V3 package URLs. |
src/UniGetUI.PackageEngine.Managers.Generic.NuGet/BaseNuGetDetailsHelper.cs |
Loads V3 details, versions, and icons. |
src/UniGetUI.PackageEngine.Managers.Generic.NuGet/BaseNuGet.cs |
Routes supported feeds through V3. |
src/UniGetUI.PackageEngine.Managers.Dotnet/Helpers/DotNetPkgOperationHelper.cs |
Defaults tool operations to global scope. |
src/UniGetUI.PackageEngine.Managers.Dotnet/DotNet.cs |
Switches NuGet.org to V3. |
src/UniGetUI.PackageEngine.Managers.Cargo/Cargo.cs |
Adds semantic comparison. |
src/UniGetUI.PackageEngine.Managers.Bun/Bun.cs |
Adds semantic comparison. |
src/UniGetUI.PackageEngine.Interfaces/IPackageManager.cs |
Exposes version comparison. |
src/UniGetUI.Core.Tools/SemanticVersion.cs |
Implements NuGet-compatible SemVer. |
src/UniGetUI.Core.Tools/PythonVersion.cs |
Implements PEP 440 ordering. |
AGENTS.md |
Documents V3 and comparison architecture. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 31 out of 31 changed files in this pull request and generated 2 comments.
Suppressed comments (4)
Previously missed (3) — in code that hasn't changed since the last review.
src/UniGetUI.PackageEngine.Managers.Generic.NuGet/Internal/NuGetV3Client.cs:511
- If the referenced catalog response is malformed, deserialization throws to the outer catch and returns
null, bypassing the nuspec fallback that is used for unavailable registration metadata. A single bad catalog document therefore leaves package details empty even when a valid flat-container nuspec exists. Catch this deserialization failure locally and fall back toGetNuspecMetadata.
V3CatalogEntry? entry = NuGetV3Json.DeserializeCatalogEntry(catalogContent);
src/UniGetUI.Core.Tools/SemanticVersion.cs:97
- SemVer numeric components are not limited to
Int32, but this rejects any component above 2,147,483,647. The manager overrides then fall back toVersionStringToStruct, which maps an overflowing segment to zero, so a valid version such as2147483648.0.0can compare below2.0.0and hide an update. Represent core and numeric prerelease components with arbitrary-precision integers (or compare digit strings by normalized length/value).
!int.TryParse(
parts[i],
NumberStyles.None,
CultureInfo.InvariantCulture,
out int number
src/UniGetUI.Core.Tools/PythonVersion.cs:95
- PEP 440 numeric components use arbitrary-precision integers, so rejecting release segments above
Int32.MaxValuedoes not match the claimed reference semantics. Pip then falls back to the shared parser, which maps an overflowing segment to zero; for example,9999999999.0can be ordered below2.0and suppress a real update. Use arbitrary-precision values for release, epoch, pre/post/dev, and numeric local segments.
!int.TryParse(
releaseParts[i],
NumberStyles.None,
CultureInfo.InvariantCulture,
out release[i]
src/UniGetUI.PackageEngine.Managers.Generic.NuGet/BaseNuGetDetailsHelper.cs:250
- For an update package,
VersionStringis the installed version andNewVersionStringis the target version. This path therefore loads and displays the old release's manifest, installer URL, dependencies, and date; the V2 path instead caches theGetUpdatesentry for the new version (BaseNuGet.cs:613-623). Derive one effective metadata version (NewVersionStringwhenIsUpgradable) and use it for the URLs, catalog fetch, icon lookup, and cache key.
details.ManifestUrl =
NuGetV3Client.GetRegistrationLeafUrl(index, package.Id, package.VersionString)
?? NuGetV3Client.GetNuspecUrl(index, package.Id, package.VersionString);
V3CatalogEntry? entry = GetOrFetchCatalogEntry(package, index);
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 31 out of 31 changed files in this pull request and generated 1 comment.
Suppressed comments (6)
Previously missed (5) — in code that hasn't changed since the last review.
src/UniGetUI.PackageEngine.Managers.Generic.NuGet/BaseNuGet.cs:527
- If the first copy of an ID has an unparseable version, every later comparison returns null, so that value remains the supposed highest version. The fallback below orders
CoreTools.Version.Nullbelow any candidate and can then offer an update already satisfied by a later valid installed copy. Since installed packages support multiple versions and their order is not a highest-first contract, prefer a parseable copy over an unparseable current value before comparing candidates.
string key = package.Id.ToLower();
if (
!highestInstalledById.TryGetValue(key, out string? highest)
|| CompareVersions(package.VersionString, highest) > 0
)
src/UniGetUI.Core.Tools/SemanticVersion.cs:164
- SemVer numeric identifiers have no 32-bit limit. Once both identifiers exceed
Int32.MaxValue, they are treated as strings, so a valid update such as1.0.0-9999999999→1.0.0-10000000000is ordered backwards and may be hidden for npm/Bun/Cargo. Compare digit strings by normalized length and ordinal value instead of parsing them asint.
bool leftNumeric = int.TryParse(
left,
NumberStyles.None,
CultureInfo.InvariantCulture,
out int leftValue
src/UniGetUI.PackageEngine.Operations/History/OperationHistoryRecord.cs:91
- This treats a whitespace-only option as a pinned version, while the successful install/update paths use
IsNullOrWhiteSpaceto decide whether an explicit version was requested. Such a record stores whitespace instead of the actual installed version, and retry/undo then reconstructs the package with that invalid version. Apply the same whitespace check here.
_ when pop.Options.Version is { Length: > 0 } pinnedVersion =>
pinnedVersion,
AGENTS.md:57
- The documented resource preference list does not match
NuGetV3ServiceIndex: search also prefers3.0.0-rc, and package base address also accepts the unversioned type. Keeping this table exact matters for future feed compatibility work.
| `SearchQueryService` (3.5.0 → 3.0.0-beta → unversioned) | package search |
| `PackageBaseAddress/3.0.0` | version enumeration, update detection, `.nupkg` and embedded-icon URLs |
src/UniGetUI.Core.Tools/PythonVersion.cs:153
- PEP 440 does not cap epoch, pre/post/dev, or local numeric components at
Int32.MaxValue. On overflow this helper silently returns the fallback (usually zero), so a valid version such as1.0.post2147483648is parsed as1.0.post0and update ordering becomes incorrect. Preserve arbitrary-size numeric values (or compare their digit strings) rather than coercing overflow to zero.
&& int.TryParse(
group.Value,
NumberStyles.None,
CultureInfo.InvariantCulture,
out int parsed
src/UniGetUI.PackageEngine.Managers.Generic.NuGet/Internal/NuGetV3Client.cs:254
- A service index with only
RegistrationsBaseUrlis accepted as usable, but this path treats the missingPackageBaseAddressas a successful empty version response.GetAvailableUpdatesV3consequently reports every package as current rather than reporting that versions could not be checked. Either enumerate registration data or at least propagate this as a failed check.
if (index.PackageBaseAddress is null)
return [];
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 31 out of 31 changed files in this pull request and generated 1 comment.
Suppressed comments (3)
Previously missed (3) — in code that hasn't changed since the last review.
src/UniGetUI.PackageEngine.Managers.Generic.NuGet/BaseNuGet.cs:527
- When the first installed copy has an unparseable version, comparing a later valid copy against it returns
null, so the valid copy never becomes the recorded maximum. The final fallback then treatsVersion.Nullas lower than a candidate and can offer a downgrade even though another installed copy already satisfies it. Apply the existing numeric fallback while reducing installed copies as well.
string key = package.Id.ToLower();
if (
!highestInstalledById.TryGetValue(key, out string? highest)
|| CompareVersions(package.VersionString, highest) > 0
)
src/UniGetUI.PackageEngine.Managers.Generic.NuGet/BaseNuGetDetailsHelper.cs:250
- For an upgradable
Package,VersionStringis the installed version andNewVersionStringis the update target. This V3 path builds the manifest URL and fetches/caches metadata using the installed version, so opening Details on an update shows the old installer URL, dependencies, and publication data. The V2 path preserves the target-version entry inBaseNuGet.cs:603-623; resolve one target metadata version here and use it consistently for details and icon lookup.
details.ManifestUrl =
NuGetV3Client.GetRegistrationLeafUrl(index, package.Id, package.VersionString)
?? NuGetV3Client.GetNuspecUrl(index, package.Id, package.VersionString);
V3CatalogEntry? entry = GetOrFetchCatalogEntry(package, index);
src/UniGetUI.PackageEngine.Managers.Generic.NuGet/Internal/NuGetV3Client.cs:583
- If a registration leaf is valid but its referenced catalog response is malformed, deserialization throws into this catch and returns
null, bypassing the nuspec fallback used for other metadata failures. A feed with a valid nuspec therefore produces empty package details solely because the catalog document is bad; fall back to nuspec metadata here too.
return null;
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 31 out of 31 changed files in this pull request and generated 1 comment.
Suppressed comments (3)
Previously missed (2) — in code that hasn't changed since the last review.
src/UniGetUI.Core.Tools/SemanticVersion.cs:97
- SemVer numeric identifiers are not limited to 32-bit values, but this rejects valid core versions above
Int32.MaxValue; the manager fallback then maps overflowing legacy segments to zero and can reverse update ordering. Numeric prerelease identifiers inCompareLabelhave the same overflow-to-lexical problem. Use arbitrary-precision numeric comparison for both core and prerelease components.
!int.TryParse(
parts[i],
NumberStyles.None,
CultureInfo.InvariantCulture,
out int number
src/UniGetUI.Core.Tools/PythonVersion.cs:95
- PEP 440 numeric components have arbitrary precision, but this rejects any valid release segment above
Int32.MaxValue. Pip then falls back to the legacy parser, which maps an overflowing segment to zero, so a valid version such as2147483648.0can be ordered below1.0and hide or invert an update. Represent and compare numeric components without anintlimit; the epoch, pre/post/dev, and numeric local fields need the same treatment.
!int.TryParse(
releaseParts[i],
NumberStyles.None,
CultureInfo.InvariantCulture,
out release[i]
src/UniGetUI.PackageEngine.Managers.Generic.NuGet/BaseNuGetDetailsHelper.cs:250
- For an update package,
VersionStringis the installed version whileNewVersionStringis the candidate created byGetAvailableUpdatesV3. This fetches the old version's registration metadata, so the update details dialog shows stale release notes, dependencies, and installer URL while offering the newer version. Use the effective candidate version for upgradable packages and key the metadata/icon caches by that same version.
details.ManifestUrl =
NuGetV3Client.GetRegistrationLeafUrl(index, package.Id, package.VersionString)
?? NuGetV3Client.GetNuspecUrl(index, package.Id, package.VersionString);
V3CatalogEntry? entry = GetOrFetchCatalogEntry(package, index);
There was a problem hiding this comment.
🤖 Pull request was approved automatically: the AI review is complete and all its review threads are resolved. 🎉
Integration Details
{
"deliveryId": "f1ba27c0-a57d-11f1-80a0-a46b344b985f",
"headSha": "8fb0ed9bcbdc468aa148e2df814444d2c9bee19d",
"reviewer": "copilot-pull-request-reviewer[bot]"
}
Summary
Engineering Manager for NuGet.org reported that UniGetUI's V2/OData traffic consumes a large share of nuget.org's database DTU. This moves the NuGet-backed managers onto the V3 API for feeds that support it, while leaving V2 fully intact for feeds that don't.
What is not migrated, and why
The PowerShell Gallery has no usable V3 service index. Every path under
powershellgallery.com/api/v3/returns403 — blocked by a Web Application Firewall rule(while/api/v2/returns 200, and a nonexistent/v3/returns 404, so the prefix exists and is deliberately blocked). Microsoft's own PSResourceGet docs still register PSGallery as.../api/v2. Step 7 of the issue isn't actionable for it, so both PowerShell managers stay on V2.Chocolatey stays on V2 too —
community.chocolatey.org/api/v3/index.jsonis a 404. Worth flagging one correction to the issue's framing: Chocolatey is not purely CLI-driven. It derives fromBaseNuGetandFindPackages_UnSafeissealed, so its search, details and icons run through the shared HTTP path; only updates and version listing arechoco. V2 is therefore a permanently maintained path, not a fallback to delete later.A V3-capable custom PowerShell repository (Azure Artifacts, GitHub Packages, JFrog, MyGet) is picked up automatically with no per-manager work.
Risk
Highest-risk area is
CompareVersions, because it touches every manager's update decision, not just NuGet. Mitigations: the default preserves current semantics exactly, and there are explicit tests asserting that revision ecosystems still read a trailing suffix as newer — so a future "simplification" that makes the shared parser SemVer-aware will fail CI.Second: users sitting on a pre-release across eight managers will now be offered the stable release they were previously denied. That's the fix working, but it will look like a surge of new updates.
Behaviour changes users will notice (intended)
.NET Toolsearch returns fewer results — libraries that can't be installed as tools are filtered outdotnet-efwent from 100 to all 266 versions)api.nuget.org/v3/registration5-…jsonURL.NET Toolinstalls now default to global