Skip to content

fix(ci): make release re-cut reliable after stale-bytes runs#5209

Merged
Coly010 merged 2 commits into
developfrom
fix/release-recovery-path
May 7, 2026
Merged

fix(ci): make release re-cut reliable after stale-bytes runs#5209
Coly010 merged 2 commits into
developfrom
fix/release-recovery-path

Conversation

@Coly010
Copy link
Copy Markdown
Contributor

@Coly010 Coly010 commented May 7, 2026

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:

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.ts alone.

Push events on develop alone can't escape this state, because dry_run: true semantic-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

  • Operators recovering from a stale-bytes run can dispatch the Release workflow with version=<next-unused> and have it actually publish (no need to remember to untick dry-run).
  • The version git tag lands on origin as soon as bytes are on npm, even if a downstream step (GitHub release / brew / scoop) fails.
  • An all-skipped publish run prints a loud warning advising re-cut, instead of looking identical to a clean success.

Summary

  • release.yml — flip workflow_dispatch.dry_run default from true to false. Document workflow_dispatch as the manual re-cut path with a top-of-trigger comment. The version input description now explicitly says it must be unique on npm.
  • release-shared.yml — explicit Push version tag step in the publish job, after pnpm publish and before softprops/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.tspublishPackage now returns "published" | "skipped"; the script prints Published: N, Skipped: M. and, when published === 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: true semantic-release with a real release pipeline (so push events advance prerelease versions automatically). The current minimal release config (only commit-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.ts parses cleanly.
  • bunx js-yaml round-trips both modified workflow files.
  • Manual smoke (after merge): trigger Release workflow_dispatch with channel=beta, version=2.99.0-beta.2, dry_run left at the new default (false). Confirm: 9 packages publish, v2.99.0-beta.2 lands on origin, GH release + brew + scoop converge, Published: 9, Skipped: 0 summary line in publish-job logs.
  • Manual no-op smoke (optional): dispatch the same 2.99.0-beta.2 a second time. Confirm: 9 packages skip, the [warn] No packages were published block fires, tag-push step skips (already on origin), GH release / brew / scoop are idempotent.

Fixes the failure mode in workflow run 25501868685.

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.
@Coly010 Coly010 requested a review from a team as a code owner May 7, 2026 15:25
Comment thread .github/workflows/release.yml Outdated
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.
@Coly010 Coly010 merged commit ef1b13a into develop May 7, 2026
16 checks passed
@Coly010 Coly010 deleted the fix/release-recovery-path branch May 7, 2026 15:43
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants