fix(cli): mark platform binaries executable in pnpm publish tarball#5201
Merged
Conversation
pnpm publish only sets +x on files declared in `bin` (or `publishConfig.executableFiles`); other files default to 0644 in the tarball. Since the bin field was removed from the platform packages in 0067c4e, the switch to `pnpm publish` in #5199 ships `bin/supabase` and `bin/supabase-go` non-executable, breaking `npx supabase@beta` with EACCES on first invocation of the CLI binary, and on every LegacyGoProxy shell-out to supabase-go. Add `publishConfig.executableFiles` to all 8 platform packages so the tarballed binaries are 0755. Verified with `pnpm pack`: source files at 0644 are packed as -rwxr-xr-x. `executableFiles` is a no-op for entries not present in the tarball, so alpha (next-shell) builds — which don't produce supabase-go — are unaffected.
…me (#5202) ## What kind of change does this PR introduce? Bug fix. ## What is the current behavior? The `beta` channel of the new TS-based release pipeline (introduced in #39) ships brew/scoop artifacts that **install the binary as `supabase-beta` instead of `supabase`**. This is a breaking change vs. the historical Go CLI behavior, where `Formula/supabase-beta.rb` and `supabase-beta.json` always installed a binary named `supabase` — only the formula/manifest filename + Ruby class differed between channels. Concretely, the currently-published `v2.99.0-beta.1` artifacts contain: - `supabase/homebrew-tap` → `Formula/supabase-beta.rb`: ```ruby def install bin.install "supabase" => "supabase-beta" bin.install "supabase-go" if File.exist?("supabase-go") end test do assert_match version.to_s, shell_output("#{bin}/supabase-beta --version") end ``` - `supabase/scoop-bucket` → `supabase-beta.json`: ```json "bin": [["supabase.exe", "supabase-beta"]] ``` So users on the beta channel suddenly have to invoke `supabase-beta ...` instead of `supabase ...` after a `brew upgrade` / `scoop update`, breaking every existing script, CI job, and shell alias. ### Root cause PR #39 added a `--name` flag to `apps/cli/scripts/update-{homebrew,scoop}.ts` to support PoC validation against user-owned forks (e.g. `supabase-shim-poc`). To avoid clashing with reviewers' already-installed `supabase` CLI during PoC testing, the same flag also **renamed the installed binary** when `--name != "supabase"`. When `release.yml` started passing `--name supabase-beta` for the beta channel, that PoC-only side-effect leaked into production. Compare with the Go CLI's `tools/publish/main.go` and the previous `Formula/supabase-beta.rb` (`v2.98.2`), which both installed `bin.install "supabase"` regardless of channel. ## What is the new behavior? `--name` controls only: - Homebrew formula filename (`supabase.rb` vs `supabase-beta.rb`) and Ruby class name (`Supabase` vs `SupabaseBeta`) - Scoop manifest filename (`supabase.json` vs `supabase-beta.json`) - Commit message The installed binary is **always** `supabase` / `supabase.exe`, matching the Go CLI's historical behavior. Stable and beta still coexist as separate formulas / manifests in the same tap / bucket — users just choose one or the other (Homebrew detects the `supabase` binary collision the same way it did with the Go CLI). ### Verification (dry-run, `--name supabase-beta`) `Formula/supabase-beta.rb`: ```ruby class SupabaseBeta < Formula desc "Supabase CLI" ... def install bin.install "supabase" bin.install "supabase-go" if File.exist?("supabase-go") end test do assert_match version.to_s, shell_output("#{bin}/supabase --version") end end ``` `supabase-beta.json`: ```json "bin": ["supabase.exe"] ``` Both match the Go CLI's `v2.98.2`-era output exactly. ## Additional context ### Files changed - `apps/cli/scripts/update-homebrew.ts` — drop the rename branch; always `bin.install "supabase"`; `brew test` invokes `#{bin}/supabase`. - `apps/cli/scripts/update-scoop.ts` — drop the alias-tuple branch; `binEntry` is always `"supabase.exe"`. - `apps/cli/docs/release-process.md` — Ring 2 PoC section now notes that the PoC formula installs a `supabase` binary, so reviewers must `brew uninstall supabase` / `scoop uninstall supabase` first if they have the official CLI installed. Validation snippets updated to invoke `supabase --version` after `brew install supabase-shim-poc`. - `docs/adr/0011-cli-release-and-distribution-strategy.md` — Implementation progress §B updated to reflect that `--name` only controls the filename/class, not the installed binary name. ### Hotfix note for already-published `v2.99.0-beta.1` The next beta release pushed via `release-shared.yml` will overwrite `Formula/supabase-beta.rb` and `supabase-beta.json` with the corrected shape. If we want to unblock current beta users **before** the next beta cuts, we'd need to either: 1. Manually edit `Formula/supabase-beta.rb` + `supabase-beta.json` on `main` in `supabase/homebrew-tap` / `supabase/scoop-bucket` to match the new template, or **done** 2. Run a manual `release.yml` `workflow_dispatch` with `channel=beta` against the `v2.99.0-beta.1` version to republish the brew/scoop side only. Out of scope for this PR; flagging for the release operator. ### Test plan - [x] `pnpm check:all` in `apps/cli` (types, lint, fmt, knip) - [x] `pnpm test:core` in `apps/cli` (unit + integration) - [x] Local dry-run of `update-homebrew.ts --name supabase-beta` — generated formula matches the Go CLI's `v2.98.2` `Formula/supabase-beta.rb` shape. - [x] Local dry-run of `update-scoop.ts --name supabase-beta` — generated manifest uses `"bin": ["supabase.exe"]`. - [x] Local dry-run with default `--name supabase` (stable path) — formula + manifest unchanged from prior correct behavior. - [ ] Once merged + a beta release cuts, verify `brew install supabase/homebrew-tap/supabase-beta && supabase --version` resolves to the new beta version on macOS. - [ ] Verify `scoop install supabase-beta && supabase --version` on Windows.
avallete
approved these changes
May 7, 2026
5 tasks
Coly010
added a commit
that referenced
this pull request
May 7, 2026
## Current Behavior
The release-CI smoke test for the npm/Verdaccio sub-test reports `[npm]
Error: ShellError: Failed with exit code 1` on linux + macos with no
further detail. The `await $` invocation in
`apps/cli/tests/helpers/npm-registry.ts` swallows stderr on non-zero
exit, so the actual cause is invisible.
Worse: even when the test passes, it has been doing so for a false
reason. With the diagnostics added in this PR, two regressions surface
that have been masked for some time:
1. `npm install` ignores the test project's `.npmrc` in environments
where pnpm has set `npm_config_*` env vars. The install silently
resolves `supabase` against `registry.npmjs.org` instead of the local
Verdaccio, fetching the public 2.x CLI (76 transitive deps). The version
regex `/^\d+\.\d+\.\d+/` matches whatever that package prints, so the
smoke test reports PASS while exercising none of our build artifacts.
The recent CI failure is the public package's postinstall now failing on
the runners — the symptom, not the cause.
2. Verdaccio is configured with `uplinks: {}`, so even when the local
registry is hit, transitive deps from the umbrella's runtime
`dependencies` (`@clack/prompts`, `effect`, `ink`, `react`, …) 404.
Those deps are bundled into `dist/supabase.js` at build time but `npm
install` still resolves them.
`b2b397af`'s `publishConfig.executableFiles` fix was correct —
diagnostics confirm tarball entries land at mode `0755`. The smoke test
just never reached them.
Smoke tests also currently only run post-merge in `release-shared.yml`.
Packaging regressions therefore slip through PR review and only surface
when a release is cut — 2.99.0-beta.1 being the most recent example.
## New Behavior
**Diagnostics (`2beff61a`)**
- Capture stdout / stderr / exit-code on the verify spawn instead of
letting Bun's `\$` collapse them into an opaque \`ShellError\`.
- After publish, \`tar -tvf\` each platform tarball from Verdaccio
storage and log the \`bin/\` mode bits — directly answers whether
\`executableFiles\` is being honoured.
- After install, dump the resolved tree: \`.bin/supabase\`'s symlink
target + mode, the umbrella shim's mode, every unpacked
\`@supabase/cli-*/bin/\` entry, and each platform package's
\`name@version\`.
- On verify failure, retry against the platform binary directly to
isolate "shim broken" vs "binary broken".
- \`runCli\` (already in \`release-shell.ts\`) is now the single spawn
primitive for both checks; \`verifyExpectedShell\`'s failure detail
includes captured stderr.
- Smoke-test runners log \`e.stack\` + \`e.stdout\` / \`e.stderr\`
instead of \`\${e}\` on every catch.
**Fix (`2beff61a`)**
- Pass \`--registry \${verdaccio_url}\` explicitly to \`npm install\`.
The CLI flag wins over env vars and \`.npmrc\`, so the install can no
longer fall through to the public registry.
- Pin \`supabase\` and \`@supabase/*\` to local-only resolution in
Verdaccio's \`packages\` config; configure an \`npmjs\` uplink for
everything else so the umbrella's bundled-in runtime deps still resolve.
Mirrors what a real \`npm install supabase\` does today.
**PR-CI smoke job (`2b9e2bee`)**
\`.github/workflows/smoke-test-pr.yml\` is a thin caller that reuses
\`release-shared.yml\` with \`dry_run: true\`. The shared workflow
already gates publish jobs on \`!inputs.dry_run\`, so only \`build\` +
\`smoke-test\` actually run.
- Triggers on \`pull_request\` events with paths under \`apps/cli/**\`,
plus the workflow files themselves so changes to the workflow re-trigger
on the PR that introduced them.
- Synthesises a PR-scoped version (\`0.0.0-pr-\${PR_NUMBER}\`) so
concurrent PRs do not collide on the build artifact name.
- Skips drafts and cancels superseded runs.
## Test plan
- [x] \`nx run supabase:check:all\` (types/lint/fmt/knip).
- [x] Local smoke run with stub binaries: \`0.0.1-smoke\` resolves to
the local umbrella (not the public 2.x CLI), platform binary mode is
\`0755\`, \`supabase --version\` returns \`0.0.1-smoke\`.
- [ ] Release-CI \`smoke-test\` matrix passes on this branch's run.
- [ ] On this PR, the new \`Smoke Test (PR) / smoke / build\` and four
\`Smoke Test (PR) / smoke / smoke-test (...)\` jobs appear and pass.
- [ ] Confirm \`publish\` / \`publish-homebrew\` / \`publish-scoop\`
jobs from the called workflow report skipped on this PR.
## Related Issue(s)
Follow-up to #5199, #5200, #5201.
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
After #5199 switched the npm publisher from
bun publishtopnpm publish,npx supabase@betafails on first run:Per pnpm docs: “by default, for portability reasons, no files except those listed in the bin field will be marked as executable in the resulting package archive.”
The platform packages used to have
"bin": { "supabase": "bin/supabase" }, but it was removed in 0067c4e (“remove bin field for binary only packages”).bun publishpreserved source file modes so this was fine;pnpm publishdoes not, so the published tarballs shipbin/supabaseandbin/supabase-goat mode0644and EACCES on consumer install.The smoke-test job in
.github/workflows/release-shared.ymlre-applieschmod +xafter artifact download, which is why CI smoke goes green; the publish job has no such step, so production tarballs ship without the bit.Both binaries are affected:
supabase(the bun-compiled main binary, failing in the trace above) andsupabase-go(whichLegacyGoProxyshell-outs would hit on every Phase-0 wrapped legacy command).New Behavior
Add
publishConfig.executableFilesto all 8 platformpackage.jsonfiles. This is pnpm’s documented mechanism for flagging files as executable in the published tarball without creating abinsymlink — so we don’t reintroduce thesupabasebin name collision with the umbrellaapps/cli/package.jsonthat motivated 0067c4e.supabase-gois only built for the legacy shell, butexecutableFilesis a no-op for files not present in the tarball, so listing it unconditionally is safe — alpha (next-shell) tarballs simply won’t containbin/supabase-goand pnpm skips it silently.Verified locally with
pnpm pack: stub source files at mode0644are packed as-rwxr-xr-x(0755):Test plan
ubuntu-latest,macos-latest,macos-15-intel,windows-latest.npx supabase@beta --helpsucceeds on darwin-arm64 with no EACCES.npx supabase@beta orgs list(a Phase-0 wrapped legacy command exercisingsupabase-goviaLegacyGoProxy) succeeds without EACCES.@supabase/cli-darwin-arm64@<version>tarball on npm and confirmbin/supabaseandbin/supabase-goare mode0755.