From 1c1e7ddd6a1765143a7678ce6d2f9168c1191ef8 Mon Sep 17 00:00:00 2001 From: david morales <118213696+davidmoralesz@users.noreply.github.com> Date: Fri, 9 Oct 2026 18:37:51 -0600 Subject: [PATCH 1/2] fix(ci): attach pack zips to a draft release, then publish it Releases here are immutable, so a published release can no longer take its zip: v1.0.1 went out with none and the asset job has failed on every push to main since. release-please now cuts drafts and creates the tag itself, and the asset job uploads to the draft and publishes it. A release already published without a zip is reported and skipped. --- .github/workflows/release.yml | 46 ++++++++++++++++++++++++++--------- release-please-config.json | 2 ++ 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 76d2f25..67fe434 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,9 +3,15 @@ name: Release # The whole release path, in one run. # # push to main -> release-please grooms one release pull request per pack -# merge that pull request -> release-please tags it (-v) and cuts -# the GitHub Release -# -> assets builds the pack at that tag and attaches the zip +# merge that pull request -> release-please tags it (-v) and cuts a DRAFT +# GitHub Release +# -> assets builds the pack at that tag, attaches the zip to the +# draft, then publishes it +# +# The release is a draft because releases here are immutable: once one is published its +# assets can no longer change, so the zip has to be on it before it goes public. That is +# also why release-please creates the tag itself (force-tag-creation); a draft's tag would +# otherwise not exist until it is published. # # Both jobs have to share one run. A tag created with GITHUB_TOKEN does not trigger # another workflow -- GitHub suppresses that to stop runaway loops -- so a separate @@ -13,8 +19,10 @@ name: Release # # The assets job does not read release-please's outputs: with one package per pack # they are keyed by path, and the org wrapper only surfaces the root package. It -# asks the API instead -- any pack whose current release has no zip gets one -- which -# also means a failed upload heals itself on the next push to main. +# asks the API instead -- any pack whose current release is still a draft gets its zip +# and is published -- which also means a failed upload heals itself on the next push to +# main. A release that was published without a zip cannot be repaired; it is reported and +# skipped, and the next release carries the zip. on: push: @@ -47,7 +55,7 @@ jobs: fetch-depth: 0 persist-credentials: false - - name: Build and attach missing zips + - name: Build, attach and publish env: GH_TOKEN: ${{ github.token }} GH_REPO: ${{ github.repository }} @@ -57,19 +65,35 @@ jobs: version="$(jq -r --arg path "packs/${slug}" '.[$path] // empty' .release-please-manifest.json)" tag="${slug}-v${version}" - assets="$(gh release view "${tag}" --json assets --jq '.assets | length' 2>/dev/null || true)" - if [ -z "${assets}" ]; then + # The listing, not `gh release view`: a draft has no page under its tag yet. + release="$(gh api "repos/${GH_REPO}/releases" --paginate \ + --jq ".[] | select(.tag_name == \"${tag}\") | [.id, .draft, (.assets | length)] | @tsv" | head -n 1)" + if [ -z "${release}" ]; then echo "${tag}: not released yet, skipping" continue fi - if [ "${assets}" -gt 0 ]; then - echo "${tag}: already has its zip, skipping" + read -r id draft assets <<<"${release}" + + if [ "${draft}" != "true" ]; then + if [ "${assets}" -gt 0 ]; then + echo "${tag}: already published with its zip, skipping" + else + echo "::warning::${tag} was published without its zip and is immutable, so it cannot get one. The next release will." + fi continue fi # Build from the tag, never from main: main may already be ahead of it. git checkout --quiet "${tag}" python3 .devtools/scripts/build.py "${slug}" - gh release upload "${tag}" dist/"${slug}"-*.zip git checkout --quiet "${GITHUB_SHA}" + + for zip in dist/"${slug}"-*.zip; do + # Upload by release id; `gh release upload` looks the release up by tag. + gh api -X POST "https://uploads.github.com/repos/${GH_REPO}/releases/${id}/assets?name=$(basename "${zip}")" \ + -H "Content-Type: application/zip" --input "${zip}" >/dev/null + echo "✔ ${tag}: attached $(basename "${zip}")" + done + gh api -X PATCH "repos/${GH_REPO}/releases/${id}" -F draft=false >/dev/null + echo "✔ ${tag}: published" done diff --git a/release-please-config.json b/release-please-config.json index 1139440..cd497a4 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -4,6 +4,8 @@ "include-component-in-tag": true, "include-v-in-tag": true, "separate-pull-requests": true, + "draft": true, + "force-tag-creation": true, "changelog-sections": [ { "type": "feat", "section": "Features" }, { "type": "fix", "section": "Bug Fixes" }, From 8f372b37e845b17d776a2d7871ce6505dbbd4974 Mon Sep 17 00:00:00 2001 From: david morales <118213696+davidmoralesz@users.noreply.github.com> Date: Fri, 9 Oct 2026 18:50:08 -0600 Subject: [PATCH 2/2] fix(ci): replace a zip left on the draft by a failed run A run that attached the zip but died before publishing left it on the draft, and the next run's upload under the same name was rejected, so the job failed on every push until the draft was published by hand. gh release upload --clobber replaces it instead; gh resolves a draft by its tag, so the upload no longer needs the raw assets endpoint. --- .github/workflows/release.yml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 67fe434..8842056 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -88,12 +88,9 @@ jobs: python3 .devtools/scripts/build.py "${slug}" git checkout --quiet "${GITHUB_SHA}" - for zip in dist/"${slug}"-*.zip; do - # Upload by release id; `gh release upload` looks the release up by tag. - gh api -X POST "https://uploads.github.com/repos/${GH_REPO}/releases/${id}/assets?name=$(basename "${zip}")" \ - -H "Content-Type: application/zip" --input "${zip}" >/dev/null - echo "✔ ${tag}: attached $(basename "${zip}")" - done + # --clobber: a run that attached the zip but died before publishing leaves it on + # the draft, and a second upload under the same name would be rejected. + gh release upload "${tag}" dist/"${slug}"-*.zip --clobber gh api -X PATCH "repos/${GH_REPO}/releases/${id}" -F draft=false >/dev/null echo "✔ ${tag}: published" done