ci(release): verify the packages are on the feed, and republish as 1.0.0-beta.2 - #79
Merged
Conversation
`dotnet nuget push --skip-duplicate` exits 0 on any 409, and 409 is not only
"you already sent this". v1.0.0-beta.1 pushed both packages, was answered 409
Conflict on each, printed "already exists at feed", exited 0 — and nuget.org has
never held either package. Workflow green, GitHub release created, nothing
published. Every step said what it was supposed to say; nobody asked the feed.
The flag stays, because it is right: a rerun after a partial failure must be
able to send the package that did not go, and without it the first duplicate
stops the command before reaching the second. What was missing is the question
after it, so the release now asks the flat container for each package at the
version it just pushed, waits for indexing rather than for a status code, and
fails naming what is absent.
Asking for the .nupkg rather than for index.json is deliberate: the index lists
the versions a package has, so a stale one answers 200 while saying nothing
about this version. The test covers that case explicitly, along with the
half-published one a rerun exists for — an "any package answered" check calls
both a success.
The test runs offline against a file:// tree laid out the way the flat container
lays out its own, so "the package is there" and "it is not" are both answers
from the code under test rather than from a stub of it. curl's exit status
carries the verdict rather than %{http_code}, because file:// has no status line
and would leave the code at 000 for a file that is there.
A checker nobody checks is the failure it exists to catch, so it was mutated
before being trusted: forcing is_published to true fails three of the six
assertions.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GcQ3umdkRNsR7FRB9s2P2w
One tag push produced two runs on 2026-08-12. The concurrency group queued the second behind the first rather than cancelling it — correctly, since a release must never be killed midway — and the second then failed at the one step that could not be run twice: `gh release create` on a tag that already had a release. Nothing was damaged and the run was red, which is the worst of the two: a red release run reads as a release that did not happen, and the next person to look cannot tell that from one that truly failed. It also blocks the case a rerun exists for, where the push half-succeeded and everything after it must run again. So the step now looks first. An existing release keeps its notes — they are published, and a rerun overwriting them would rewrite what readers already have — and its assets are uploaded with --clobber, which is the part a genuine rerun needs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GcQ3umdkRNsR7FRB9s2P2w
The v1.0.0-beta.1 tag exists, its GitHub release exists, and nuget.org has neither package. The `v*` ruleset forbids deletion and update, so that version is spent whatever the cause, and the version it was meant to be is now 1.0.0-beta.2 — the section it wrote is renamed rather than rewritten, since nothing about the code changed between them. 1.0.0-beta.1 keeps a section of its own rather than disappearing, because a changelog that skips a version invites the reader to look for it. It says what happened: tagged, pushed, refused, reported green, never installable. Somebody holding the tag or the GitHub release needs to be able to find out that the packages it describes were never on the feed, and the changelog is where they will look. Both languages move together, which the structural comparison enforces: nine headings and sixty-five bullets on each side. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GcQ3umdkRNsR7FRB9s2P2w
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
v1.0.0-beta.1was tagged, the release workflow went green, a GitHub release was created — andnuget.org has neither package.
dotnet nuget push --skip-duplicateexits 0 on any 409, and the409 it received was not "you already sent this". This makes that failure impossible to miss the
next time, and prepares the republication as
1.0.0-beta.2.Type of change
Changes
tools/packaging/verify-published.sh— asks the flat container for each id at the version justpushed, waits for indexing rather than for a status code (
VERIFY_TIMEOUT, default 900s), andfails naming what is absent. It asks for the
.nupkgrather than forindex.jsonon purpose:the index lists the versions a package has, so a stale one answers 200 while saying nothing
about the version in question.
tools/packaging/verify-published.test.sh— runs offline against afile://tree laid out theway the flat container lays out its own, so both answers come from the code under test rather
than from a stub of it. Six cases: neither package, only the main one, both, a version never
pushed, no arguments, a version with no ids.
.github/workflows/release.yml— aVerify both packages are really on the feedstep after thepush, skipped on a dry run like the push itself.
.github/workflows/release.yml—Create the GitHub releasenow looks first. One tag pushproduced two runs on 2026-08-12; the concurrency group queued the second rather than cancelling
it (correctly — a release must not be killed midway), and it failed here, at the workflow's one
non-repeatable step. An existing release now keeps its published notes and gets its assets
uploaded with
--clobber, which is what a genuine rerun after a partial failure needs..github/workflows/ci.yml— the new test joins thescriptsjob.1.0.0-beta.2(nothing about the code changedbetween the two, so it is renamed rather than rewritten), and
1.0.0-beta.1keeps a shortsection of its own saying what it turned out to be. A changelog that skips a version invites the
reader to go looking for it, and somebody holding that tag or its GitHub release needs to be
able to find out that the packages it describes were never installable.
--skip-duplicateitself stays, and is right: a rerun after a partial failure must be able tosend the package that did not go, and without the flag the first duplicate stops the command
before it reaches the second. What was missing is the question after it.
Testing
dotnet build -c Release— cleandotnet test -c Release— 972 tests (109 + 613 + 58 + 192)tests/PackageSmokeTest/run.shtools/packaging/verify-published.test.sh,tools/release-notes/changelog-section.test.sh,tools/style/lint-layout.test.shandtools/style/lint-layout.shdotnet pack -p:Version=1.0.0-beta.2produces the four files under their beta names, andverify-packages.shaccepts both1.0.0-beta.2, 5 for1.0.0-beta.1, and exits 2 onUnreleasedThe new checker is the one this repository cannot afford to take on trust — it runs once, at the
moment nothing can be taken back, and the failure it exists to catch is silent. So it was mutated
before being trusted: forcing
is_publishedto return true fails three of the six assertions.shellcheckis not installed here; both new scripts passbash -n, and CI runs the real thing.Public API
Documentation
docs/updatedCHANGELOG.mdanddocs/for-users/CHANGELOG.fr.mdboth updatedNine headings and sixty-five bullets on each side, so the structural comparison holds.
What this does not answer
The cause of the 409 is still unknown. Ruled out so far: no colliding or similarly-named package
on nuget.org, nothing in the account's Manage Packages, a trusted-publishing policy Active for the
right repository and workflow, and no earlier run that pushed — all thirteen release runs were
enumerated, and the dry run's
Pushstep was skipped. The decisive datum would be the responsebody from a push without
--skip-duplicate, which this branch does not attempt: the flag is notthe bug, and removing it would trade a silent failure for a release that cannot be rerun.
What changes here is that the next attempt cannot report success without the packages being
retrievable. If
1.0.0-beta.2meets the same 409, the release run goes red and says so.Generated by Claude Code