release, nightly: publish the staged release by id, refuse duplicate drafts, resume a complete staged release

Merged
LA
21d ago
+452-127

Summary

This PR ports the v0.65.2 duplicate-draft fix (manaflow-ai/cmux#21137, which is still open on main) to the feat-cmux-next universal-app release.yml. It also fixes the two follow-ups: beads cx-sqadk (the guard cannot see drafts) and cx-9ol90 (nightly uses a softprops tag lookup).

Incident: v0.65.2 was published with 0 assets. publish-release-assets.py staged draft 414419116 and uploaded 12 assets to it. Then softprops/action-gh-release looked the release up by tag. That lookup returns 404 for drafts, so the action created release 414419301 and published it with 0 assets (job log of run 38621170537, attempt 5).

Changes

  • scripts/ci/publish-release-assets.py: before any upload, the script refuses when the tag has more than one release object, draft or published. The new --github-output flag reports release_id, asset_count and asset_names.
  • New scripts/ci/finalize-release.py:
    • publish publishes the given release id. It refuses when any other release exists for the tag. It clears prerelease and sets the metadata first, then sets draft=false and make_latest. It then checks that the tag resolves to the same id, that the release is not a draft, and that the expected assets are there (exact count when one is given).
    • download takes assets from a staged draft and checks the digest.
  • release.yml:
    • The guard now lists drafts too, and it refuses duplicate release objects (cx-sqadk).
    • When the existing release (draft or published) already has every immutable asset, the run does not rebuild, because a rebuild makes new signed bytes. It resumes instead: it downloads the release's own appcast.xml, publishes the release by id and uploads the appcast to R2. Before, a failure between upload and publish could not be fixed by a rerun, and a missed R2 upload stayed missed.
    • Publish verified release now uses finalize-release.py with the upload step's id and asset count. softprops is removed.
  • nightly.yml: Publish verified nightly release metadata and Record verified nightly publication now publish the staged channel release by id with the same release body and token, in place of the softprops tag lookup (cx-9ol90).
  • tests/test_github_release_publication.py: the existing workflow-text assertions are changed to match the new steps. One existing case is changed to have a single draft, because two drafts for one tag are now refused. No new tests (behavior-test rule).

Changelog

none

Verification

  • actionlint passes on release.yml. On nightly.yml it reports only a pre-existing SC2129 at line 1597, which is not in this diff.
  • finalize-release.py publish/download --help load correctly, and py_compile passes.
  • Per the no-tests-on-laptop rule, the Python tests were not run locally. The CI ci-guards and r2-upload-tests jobs run them.
  • The real release and nightly paths run only on a tag push or the nightly schedule, so this PR does not exercise them.

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.


Summary by cubic

Fixes release and nightly publication so the staged release is published by id, preventing releases from being published with 0 assets (v0.65.2 shipped empty this way).

  • The upload script refuses when more than one release object exists for the tag and reports the staged release id, asset count, and asset names.
  • New finalize-release.py publishes a release by id, refuses other releases for the same tag, and verifies the published release holds the expected assets; its download command fetches staged assets by id without sending the token on GitHub's redirect.
  • release.yml: the guard lists drafts, refuses duplicate release objects, and skips the build when a complete staged draft exists; the rerun then resumes publication by id with the exact asset count the guard saw, without rebuilding, so signed bytes stay intact. A rerun of an already-published release is a no-op.
  • nightly.yml publishes the staged release by id in place of the softprops tag lookup.
  • CI now proves finalize-release.py loads.