From f02b4625a9b35f0bc320b171595889f4b59ad5f3 Mon Sep 17 00:00:00 2001 From: "denizirgin@gmail.com" Date: Tue, 8 Sep 2026 16:57:14 +0300 Subject: [PATCH] ci: verify that a NuGet.org push actually published --skip-duplicate reports every 409 as "already exists" and exits zero. Two dispatches went green under it while all four pushes were in fact refused, because the package ids fall under an ID prefix reservation this account does not hold; nothing reached the gallery and the lane still reported success. Removing the flag would trade that failure for another, since retrying a dispatch after a partial push must not abort on the package that already landed. It therefore stays, and stops being what decides success. A verification step now asks the flat container for the exact versions the pack step produced, which separates the two outcomes the flag conflates: a package that genuinely already existed answers yes, a package the gallery refused answers no. Validation is asynchronous, so the check waits rather than reading the registry once, and fails naming the versions that never appeared. --- .github/workflows/publish-nuget.yml | 46 +++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/.github/workflows/publish-nuget.yml b/.github/workflows/publish-nuget.yml index bb24b941..1df99eba 100644 --- a/.github/workflows/publish-nuget.yml +++ b/.github/workflows/publish-nuget.yml @@ -95,6 +95,12 @@ jobs: # dotnet nuget push implements its own glob expansion; the path is quoted so the shell does # not expand it first (two packable projects produce two .nupkg files here). + # + # --skip-duplicate reports any 409 as "already exists" and exits zero, which is how two + # dispatches went green while every push was in fact refused by an ID prefix reservation. The + # flag stays, because retrying a dispatch after a partial push must not abort on the package + # that already landed; what changes is that it no longer decides success on its own. The + # verification below does, and it separates the two outcomes the flag conflates. - name: Push to NuGet.org if: inputs.target == 'nuget' run: > @@ -103,6 +109,46 @@ jobs: --api-key ${{ steps.nuget-login.outputs.NUGET_API_KEY }} --skip-duplicate + # A push that exits zero is not a publication. This asks the flat container for the exact + # versions the pack step produced: a package that genuinely already existed answers yes, a + # package the gallery refused answers no. A newly accepted version appears only once + # validation completes, so the check waits rather than reading the registry once. + - name: Verify NuGet.org publication + if: inputs.target == 'nuget' + shell: pwsh + env: + PACKAGE_VERSION: ${{ inputs.version }} + run: | + $version = $env:PACKAGE_VERSION + $suffix = ".$version.nupkg" + $packages = Get-ChildItem packages | Where-Object { $_.Extension -eq '.nupkg' } + if ($packages.Count -eq 0) { throw "No .nupkg files were produced, so there is nothing to verify." } + $ids = $packages | ForEach-Object { + if (-not $_.Name.EndsWith($suffix)) { throw "Package file '$($_.Name)' does not carry the dispatched version '$version'." } + $_.Name.Substring(0, $_.Name.Length - $suffix.Length) + } + $wanted = $version.ToLowerInvariant() + $deadline = (Get-Date).AddMinutes(10) + while ($true) { + $missing = @() + foreach ($id in $ids) { + $url = "https://api.nuget.org/v3-flatcontainer/$($id.ToLowerInvariant())/index.json" + $response = Invoke-WebRequest -Uri $url -SkipHttpErrorCheck -Headers @{ 'Cache-Control' = 'no-cache' } + $published = $response.StatusCode -eq 200 -and + (($response.Content | ConvertFrom-Json).versions -contains $wanted) + if (-not $published) { $missing += $id } + } + if ($missing.Count -eq 0) { + Write-Host "Published on NuGet.org: $($ids -join ', ') $version" + break + } + if ((Get-Date) -ge $deadline) { + throw "Still absent from NuGet.org after 10 minutes: $($missing -join ', ') $version. The push reported success, so check the gallery for a refused or still-validating upload." + } + Write-Host "Awaiting NuGet.org validation: $($missing -join ', ')" + Start-Sleep -Seconds 20 + } + - name: Push to GitHub Packages if: inputs.target == 'github' run: >