Skip to content

ci: skip packages already on NuGet.org instead of re-pushing them (fixes the 403 Quota Exceeded runs) - #476

Merged
MPCoreDeveloper merged 1 commit into
masterfrom
chore/publish-skip-existing
Oct 25, 2026
Merged

MPCoreDeveloper merged 1 commit into
masterfrom
chore/publish-skip-existing

Conversation

@MPCoreDeveloper

@MPCoreDeveloper MPCoreDeveloper commented Oct 25, 2026 •

Copy link
Copy Markdown
Owner

Why

2.1.1 shipped fine: the run started by #475 (40185386962) published all 27 packages between 10:43 and 10:46 UTC. Four dependabot merges landed minutes later, each started its own master run, and each of those re-pushed the full set. NuGet.org answered 403 Quota Exceeded (retry after 822s) and four runs went red after the release was already out.

--skip-duplicate is not protection here: a duplicate is still a PUT and NuGet.org counts it. The old step also pushed everything twice by design — the layered passes, then push_layer "Remaining packages" "*.nupkg" over the same artifacts, which produced 50 already exists conflicts inside that single green run.

What

push_layer now asks the feed first:

  1. read <id> / <version> out of the nupkg nuspec (unzip -p … | grep -oP, so a pre-release 2.1.0-RC.3 comes from the package instead of being parsed out of the file name),
  2. GET https://api.nuget.org/v3-flatcontainer/{id}/index.json and match the version case-insensitively (the feed lowercases pre-release labels: 2.1.0-rc.3),
  3. skip when present, push only when missing,
  4. visit each artifact once (/tmp/seen.txt), so the catch-all layer costs nothing.

An unchanged tree therefore costs one GET per package and zero pushes, and the 30s indexing wait only runs for a layer that actually pushed something. The step summary now reports pushed vs skipped and says plainly when a run shipped nothing — ls *.nupkg | wc -l read "Published 28 package(s)" on runs that pushed none, which is the same false green that hid the original 2.1.0 no-op.

publish-manual.yml gets the same logic, so a re-dispatch after a partial publish costs one request per package instead of a full re-push.

Validation

Both files parse as YAML. Helpers run against the live feed:

case expected result
SharpCoreDB 2.1.1 skip skip
SharpCoreDB 9.9.9 push push
SharpCoreDB.Server 2.1.1 (no such package, index 404) push push
unknown package id (index 404) push push
nuspec 2.1.0-RC.3 vs feed 2.1.0-rc.3 skip skip
nuspec id/version extraction SharpCoreDB.VectorSearch / 2.1.1 as expected

Dedupe simulation over three fake artifacts and three layers (core, dependents, catch-all): 3 visits, 3 skips, 0 pushes.

End-to-end, on this branch (gh workflow run publish-manual.yml --ref chore/publish-skip-existing), where every package is already published at 2.1.1:

  • run 40187380441 — before the dedupe: 53 skips, 0 conflicts, 0 pushes, green.
  • run 40187647396 — with the dedupe: 27 packages, 27 skips, 0 PUTs, 0 conflicts, green. That is the whole 2.1.1 set, and it is the run that would have hit the quota before.

@MPCoreDeveloper
MPCoreDeveloper force-pushed the chore/publish-skip-existing branch from 1319abf to 859fd5e Compare October 25, 2026 11:04
@codecov

codecov Bot commented Oct 25, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@MPCoreDeveloper
MPCoreDeveloper force-pushed the chore/publish-skip-existing branch from 859fd5e to c797777 Compare October 25, 2026 11:08
… them

A master push packs every project and pushes every artifact. `--skip-duplicate` does not avoid the
PUT: a duplicate is still a request, and NuGet.org counts it, so a burst of merges exhausts the push
quota and the run dies with `403 Quota Exceeded` (retry after 822s) long after the release itself
shipped. That is what turned the four dependabot merges after #475 red: 2.1.1 was on the feed, the
publish job was not.

push_layer now reads id and version out of each nupkg nuspec, asks the flatcontainer index, and only
pushes what is missing. Anything already published is skipped without touching the push endpoint, so
an unchanged tree costs one GET per package and zero pushes, and the 30s indexing wait only happens
for a layer that actually pushed something.

The step summary now reports pushed vs skipped and says plainly when a run shipped nothing.
`ls *.nupkg | wc -l` never did: it read "Published 28 package(s)" on runs that pushed none.

The catch-all `push_layer "Remaining packages" "*.nupkg"` also re-pushed the whole set after the
layered passes - 50 already-exists conflicts in a single run - so every release paid for 28 extra
requests. It still catches packages the layers do not name; the feed check makes it free.

The same skip logic goes into publish-manual.yml, so a re-dispatch after a partial publish costs one
request per package instead of a full re-push.

Validated: both workflows parse as YAML; the two helpers run against the live feed (published version
skipped, unpublished pushed, unknown package id pushed, 404 index pushed, nuspec 2.1.0-RC.3 matching
the feed lowercase 2.1.0-rc.3); nuspec extraction returns id/version from a nuspec fragment.
@sonarqubecloud

Copy link
Copy Markdown

@MPCoreDeveloper
MPCoreDeveloper merged commit 5ba3ddf into master Oct 25, 2026
17 checks passed
@MPCoreDeveloper
MPCoreDeveloper deleted the chore/publish-skip-existing branch October 25, 2026 11:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant