release: publish the staged release by id, refuse duplicate drafts (v0.65.2 0-asset publish)
Incident
Release v0.65.2 (run 38621170537, attempt 5) was published with 0 assets. The 12 assets were uploaded to draft release 414419116, and a different release, 414419301, was published. (Both show the same created_at because GitHub reports the tag commit date there, not the object creation time.)
Root cause
From the attempt 5 job log:
Upload release asset:publish-release-assets.pyfound no release for the tag and staged draft 414419116, then uploaded all 12 assets to it.Clear prerelease on staged drafts:cleared prerelease on draft 414419116.Publish verified release(softprops/action-gh-release v3.0.0): its lookup isgetReleaseByTag, which returns 404 for drafts. It loggedCreating new GitHub release for tag v0.65.2, created 414419301, loggedRelease 414419301 is not yet discoverable by tag v0.65.2, retrying, and then published 414419301 with 0 assets.
So the publish step used a tag-name lookup that cannot see the staged draft. That lookup always creates a second release object, and which object gets published depends on list-endpoint consistency. Attempts 1 to 4 never reached the upload steps, so a stale draft from an earlier run did not cause this.
Fix
scripts/ci/publish-release-assets.py: when the tag lookup returns 404, the script lists drafts (now paginated). If more than one staged draft exists for the tag, it refuses before uploading anything and prints the ids. A new--github-outputflag reportsrelease_idandasset_count..github/workflows/release.yml: the softprops step and the separate clear-prerelease step are replaced by onePublish verified releasestep that:- refuses when any other release object exists for the tag;
- checks that the staged release id has the expected tag and has exactly
asset_countassets, alluploaded; - generates the release notes, clears
prerelease, then publishes by id (PATCH releases/$RELEASE_ID draft=false make_latest=...); - checks that
releases/tags/$TAGnow resolves to that same id, is not a draft, and has the expected asset count. If any check fails, the step fails loudly before the R2 appcast upload.
- The existing workflow-text tests were changed to match the new step shape. One existing case changed: a rerun with two drafts for one tag used to adopt the lowest id and now refuses. No new test files.
Not changed: nightly.yml still uses softprops, but only on already-published channel releases, which the tag lookup does find.
Verification: actionlint passes on release.yml. Per the no-tests-on-laptop rule, the Python tests (tests/test_github_release_publication.py, tests/test_ci_release_per_arch_stable.py) were not run locally. They run in CI (ci-guards, r2-upload-tests).
Do not merge: Lawrence approval required for main merges.
Need help on this PR? Tag @codesmith-bot with what you need.
Summary by cubic
Fixes the v0.65.2 0-asset publish incident: the publish step looked up releases by tag, which 404s for drafts, so it created and published a second, empty release while the 12 assets sat on the staged draft. Publishing now targets the staged release by id.
publish-release-assets.pyrefuses to upload when more than one release object exists for a tag, draft or published, and reports the stagedrelease_idandasset_countvia--github-output.release.ymlpublishes that exact release id, refuses any other release object for the tag, and verifies the published release by tag carries every expected asset, retrying the tag read a few times to ride out read-after-write lag.
Summary by CodeRabbit
- Chores
- Release publishing now finalizes the specific staged release after confirming its tag and expected assets are correct.
- Reruns can continue using an existing staged draft. Publishing stops if multiple releases match the tag, or if the staged release or its assets fail validation.
- After publishing, the release is verified against the staged release and expected assets. Tag lookup is retried if the published release is not immediately available.