fix(ci): make release re-cut reliable after stale-bytes runs#5209
Merged
Conversation
The post-merge run for #5207 (workflow run 25501868685) hit the failure mode @avallete flagged on the PR: every platform package was already live on npm at 2.99.0-beta.1 (published earlier the same day from an older commit), so the new idempotent skip happily exited 0 even though the bytes from today's commits never shipped. semantic-release in `dry_run: true` mode keeps recomputing the same prerelease version off `v2.98.2` because the `v2.99.0-beta.1` tag isn't registered with it as a released version, so push events alone can't escape this state. Changes: - `release.yml` — flip `workflow_dispatch.dry_run` default to `false` and document workflow_dispatch as the manual re-cut path. Operators recovering from a stale-bytes run can now dispatch with `version=2.99.0-beta.2` (or whichever next-unused version) without having to remember to also untick dry-run. - `release-shared.yml` — push the `v<version>` git tag explicitly in the publish job, right after npm publish and before the GitHub-release step, idempotent on origin. Previously the tag only landed via softprops as part of GH-release creation, so any failure between npm and softprops left origin without the tag for a version that was already live on npm. - `publish.ts` — track per-package publish/skip outcomes and print a summary. When every package was skipped, log a loud warning advising the operator to re-cut as a fresh version via workflow_dispatch. We can't auto-detect "stale bytes vs downstream-failure recovery" from inside the script, but a visible signal is enough for a human reviewing the workflow run to make the call.
avallete
reviewed
May 7, 2026
avallete
approved these changes
May 7, 2026
avallete's review point: a stray Run-workflow click shouldn't publish. Cost of the safer default is one extra click when recovering; cost of the unsafer default is a half-broken accidental release. Reverts the default-flip from the previous commit and adds an inline comment so the intent is durable. The rest of the recovery-path improvements (version description, top-of-trigger comment, tag-push step, all-skipped warning) are unchanged.
avallete
added a commit
that referenced
this pull request
May 20, 2026
Fixes the backfill-release-notes workflow so it actually produces the right notes for an arbitrary historical tag, and moves the engine out of inline bash into a reusable bun script. ## Why the original workflow couldn't produce correct notes Backfilling an old tag isn't symmetric with running semantic-release on a normal push — it trips on five separate things at once, each of which silently sends it down a wrong path: - **Branch detection.** `cycjimmy/semantic-release-action` reads the branch via `env-ci` (`$GITHUB_REF` / `$GITHUB_REF_NAME`), not `git rev-parse --abbrev-ref HEAD`. Dispatching the workflow from any branch other than `develop`/`main` left semantic-release looking at an unconfigured branch and silently emitting no `new_release_*` outputs. - **"Behind remote" check.** semantic-release runs `git ls-remote <repositoryUrl> <branch>` and exits silently if the remote tip differs from local HEAD — which is always true when re-staging at an old tag. - **Channel notes for historical tags.** semantic-release reads `git log --notes=refs/notes/semantic-release*`. `actions/checkout` doesn't fetch `refs/notes/*` by default, and some historical tags (e.g. `v2.99.0-beta.1`) have no channel annotation at all — leaving them as `channels=[null]`, which the prerelease filter drops. semantic-release then walks past them and `lastRelease` drifts back far enough to drag unrelated commits into the changelog. - **Drift in `release.branches` config.** Before commit `2515885` (May 11) the `develop` branch had no explicit `"channel": "beta"`, so semantic-release defaulted the channel to the branch name; before #5316 the plugin chain didn't include `release-notes-generator`. A historical checkout therefore produces empty or misclassified notes. - **Output format.** Parsing `cycjimmy/semantic-release-action`'s stdout returns marked-terminal rendered ANSI/whitespace, not raw markdown — so even when the right notes were computed, the GH release body would render wrong. ## What this PR does Moves the workflow's logic into `apps/cli/scripts/backfill-release-notes.ts` and calls `semantic-release` *programmatically* so it can return `nextRelease.notes` as raw markdown directly. The script's setup works around each of the issues above in a temp clone (so the original workspace stays clean): 1. Clones the repo to a temp directory and fetches `refs/notes/*` from both the source repo and origin. 2. Synthesises a `develop`/`main` branch at the tag's commit and seeds the other configured branch from `refs/remotes/origin/<other>`. 3. Backfills missing channel notes on every reachable tag (`v*-beta.*` → `beta`, `v*-alpha.*` → `alpha`, else `latest`). 4. Patches the temp clone's `apps/cli/package.json` with the *current* `release` config so historical checkouts use today's `channel: "beta"` and plugin chain. 5. Uses `git config --local url.<local>.insteadOf <github>` so semantic-release's `ls-remote` silently targets the local clone (satisfying the "behind remote" check) while `repositoryUrl` stays the real GitHub URL — keeping commit/PR links correct in the rendered notes. 6. Calls `semanticRelease({ dryRun: true, noCi: true, repositoryUrl }, { cwd: ... })` and prints `nextRelease.notes` to stdout, or with `--apply` calls `gh release edit --notes-file`. The workflow itself reduces to: checkout, setup, `bun apps/cli/scripts/backfill-release-notes.ts --tag $TAG`, mirror to the job summary, then conditionally re-run with `--apply`. ## Sample output (`v2.99.0-beta.2`) ```sh $ bun apps/cli/scripts/backfill-release-notes.ts --tag v2.99.0-beta.2 ``` ```markdown # [2.99.0-beta.2](v2.99.0-beta.1...v2.99.0-beta.2) (2026-05-20) ### Bug Fixes * **ci:** make release re-cut reliable after stale-bytes runs ([#5209](#5209)) ([ef1b13a](ef1b13a)), closes [#5205](#5205) [#5207](#5207) [#5205](#5205) [#5207](#5207) [#App](https://github.com/supabase/cli/issues/App) * **cli:** make npm publish idempotent for partial-failure re-runs ([#5207](#5207)) ([bbeaec7](bbeaec7)), closes [#release-creation](https://github.com/supabase/cli/issues/release-creation) * **cli:** mark platform binaries executable in pnpm publish tarball ([#5201](#5201)) ([b2b397a](b2b397a)), closes [#5199](#5199) * **cli:** smoke test now actually verifies our local build ([#5205](#5205)) ([9109056](9109056)), closes [#5199](#5199) [#5200](#5200) ``` Exactly the 5 commits between `v2.99.0-beta.1` and `v2.99.0-beta.2`, with GitHub links rendered against `https://github.com/supabase/cli` regardless of the local-clone redirection. ## Related The same `release.repositoryUrl` / `ls-remote` interaction and channel-notes drift will need to be addressed on the production publish path (`.github/workflows/release-shared.yml`) once Stage B of the changelog plumbing (`claude/wire-release-notes-publish`) lands. --------- Co-authored-by: Claude <noreply@anthropic.com>
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.
Current Behavior
Follow-up to #5207. The post-merge run on develop (workflow run 25501868685) reproduced the failure mode @avallete flagged in the original review:
supabase@2.99.0-beta.1was published earlier the same day from62509de5(the musl-glibc fix).semantic-release --dry-runagain. With only@semantic-release/commit-analyzerconfigured and dry-run mode never registering the prior release, semantic-release foundv2.98.2as the last release on develop and recomputed the same2.99.0-beta.1from the 110 commits since.200 OKon npm and the new idempotent script logged[skip] @supabase/...@2.99.0-beta.1 already published.× 9 → exit 0.The original PR's idempotent-skip semantics is correct for partial-failure recovery (5/8 published mid-run, retry publishes the remaining 3) — but it is also correct in the stale-bytes case where you don't actually want to skip. The two scenarios are indistinguishable inside
publish.tsalone.Push events on develop alone can't escape this state, because
dry_run: truesemantic-release will keep emitting the same beta version until the prior tag is in a form it recognises. The fix is to make the manual re-cut path (workflow_dispatch with an explicit version) reliable and obvious, and to make the silent-no-op visible.Expected Behavior
version=<next-unused>and have it actually publish (no need to remember to untick dry-run).Summary
release.yml— flipworkflow_dispatch.dry_rundefault fromtruetofalse. Document workflow_dispatch as the manual re-cut path with a top-of-trigger comment. Theversioninput description now explicitly says it must be unique on npm.release-shared.yml— explicitPush version tagstep in the publish job, afterpnpm publishand beforesoftprops/action-gh-release. Idempotent: skips push if the tag is already on origin (e.g. previous-run replay). Without this, any failure between npm and softprops leaves origin without the tag for a version that's already live on npm.publish.ts—publishPackagenow returns"published" | "skipped"; the script printsPublished: N, Skipped: M.and, whenpublished === 0, a multi-line[warn]that names the version and tells the operator to re-cut via workflow_dispatch. We can't auto-detect stale-bytes vs downstream-failure recovery here, but the warning is enough signal for a human reviewing the run to decide.Out of scope: replacing
dry_run: truesemantic-release with a real release pipeline (so push events advance prerelease versions automatically). The current minimalreleaseconfig (onlycommit-analyzer) is part of the same root cause but reshaping it touches branch-protection, the GH-App push token, and how main → develop fast-forwards interact with channel tracking — separate change.Test Plan
cd apps/cli && pnpm check:all(types, lint, fmt, knip) passes.bun build apps/cli/scripts/publish.tsparses cleanly.bunx js-yamlround-trips both modified workflow files.channel=beta,version=2.99.0-beta.2,dry_runleft at the new default (false). Confirm: 9 packages publish,v2.99.0-beta.2lands on origin, GH release + brew + scoop converge,Published: 9, Skipped: 0summary line in publish-job logs.2.99.0-beta.2a second time. Confirm: 9 packages skip, the[warn] No packages were publishedblock fires, tag-push step skips (already on origin), GH release / brew / scoop are idempotent.Fixes the failure mode in workflow run 25501868685.