Skip to content

feat(cli): port branches commands to native TypeScript#5374

Merged
Coly010 merged 4 commits into
developfrom
cli/port-branches-command
May 28, 2026
Merged

feat(cli): port branches commands to native TypeScript#5374
Coly010 merged 4 commits into
developfrom
cli/port-branches-command

Conversation

@Coly010
Copy link
Copy Markdown
Contributor

@Coly010 Coly010 commented May 28, 2026

What

Replaces the Phase 0 Go-proxy shims for all eight supabase branches subcommands (list, create, get, update, pause, unpause, delete, disable) with native Effect handlers under apps/cli/src/legacy/commands/branches/. Behavior, output formatting, exit codes, and telemetry are 1:1 with apps/cli-go/internal/branches/*.

Why

Fifth batch in the Go-to-TS port (CLI-1289). Removing the subprocess fork-exec from the hot path of every supabase branches invocation is the immediate motivation; the broader goal is shrinking the Go-binary surface so the TS shell can ship standalone.

Highlights

Per-family colocated helpers

  • branches.errors.ts — tagged errors for every HTTP call (15 pairs) plus validation/cancellation errors.
  • branches.format.ts — list-table and get-table renderers, toStandardEnvs env-map projection (POSTGRES_URL / SUPABASE_URL / SUPABASE_<NAME>_KEY / SUPABASE_JWT_SECRET), Go-byte-exact toPostgresUrl and parsePoolerConnectionString.
  • branches.resolver.tsGetBranchProjectRef equivalent (project-ref / UUID / named-lookup dispatch). Exports the shared LEGACY_BRANCH_PROJECT_REF_PATTERN and LEGACY_BRANCH_UUID_PATTERN.
  • branches.prompt.ts — TTY list-picker and non-TTY stdin prompt with git-branch default; emits Selected branch ID: <ref> to stderr after a TTY pick. Cyan-styles the git-branch hint and surfaces the Create your first branch with: supabase branches create suggestion when branching is disabled.

Hoisted shared infrastructure

  • shared/git/git-branch.tsdetectGitBranch (reads $GITHUB_HEAD_REF, then walks up CWD reading .git/HEAD). next/branches/create switches to this helper in the same change.
  • legacy/shared/legacy-profile.ts — consolidates profile → project_host, profile → dashboard_url, and (profile, org_slug) → billing_url lookups that were duplicated between formatters and the upgrade-suggest helper.
  • legacy/shared/legacy-upgrade-suggest.ts — replays Go's SuggestUpgradeOnError + TrackUpgradeSuggested flow. Fires cli_upgrade_suggested with {feature_key, org_slug} on 4xx-gated entitlement failures from create (branching_limit) and update (branching_persistent). Writes the bold billing-URL suggestion to stderr in text mode only.

Notable Go-parity decisions

  • create / update use Option<boolean> for --persistent and --with-data so explicit-false reaches the API. Mirrors Go's cmdFlags.Changed("persistent") check; previously these would have been silently dropped.
  • create confirm prompt fires unconditionally (TTY or non-TTY) — Go's PromptYesNo defaults to true on EOF, the non-interactive Output layer matches that.
  • update upgrade-suggest passes the resolved branchRef (Go parity with update.go:26); the test path uses a new mockLegacyPlatformApiService direct-service mock that sidesteps the upstream branch_id_or_ref oneOf schema constraint.
  • Pooler warning sanitizes the parse error so the connection string (pooler user/host/port) is never echoed to stderr.
  • WARNING: prefix is yellow-styled to match utils.Yellow; the git-branch default in the prompt label is cyan to match utils.Aqua.

Tests

  • 56 integration tests (handler-level, mocked Effect services) + 34 unit tests.
  • All 982 unit + integration tests pass.
  • New direct-service mock (mockLegacyPlatformApiService) and legacyStatusCodeFailure helper in tests/helpers/legacy-mocks.ts for handler tests that need to bypass schema validation.

Updated docs/go-cli-porting-status.md — eight branches * rows flipped from wrapped to ported. Each subcommand directory has a refreshed SIDE_EFFECTS.md.

Fixes CLI-1289

@Coly010 Coly010 requested a review from a team as a code owner May 28, 2026 08:33
@Coly010 Coly010 self-assigned this May 28, 2026
Coly010 added 4 commits May 28, 2026 13:01
Replaces the Phase 0 Go-proxy shims for `supabase branches {list, create, get,
update, pause, unpause, delete, disable}` with native Effect handlers under
`apps/cli/src/legacy/commands/branches/`. Behavior, output formatting, exit
codes, and telemetry are 1:1 with `apps/cli-go/internal/branches/*`.

- Per-family helpers: `branches.errors.ts`, `branches.format.ts`,
  `branches.resolver.ts`, `branches.prompt.ts`.
- Shared hoist: `shared/git/git-branch.ts` (also adopted by
  `next/branches/create`), `legacy/shared/legacy-profile.ts` (project_host /
  dashboard_url / billing URL lookups), and `legacy/shared/legacy-upgrade-suggest.ts`
  (fires `cli_upgrade_suggested` PostHog event matching Go's `TrackUpgradeSuggested`).
- `create` and `update` use `Option<boolean>` for `--persistent` / `--with-data`
  so explicit-false demote requests reach the API (mirrors Go's
  `cmdFlags.Changed("persistent")`).
- Pooler warning sanitizes the parse error to avoid leaking the connection
  string (host/user) to stderr.
- 982 unit + integration tests passing; types/lint/fmt/knip clean.

Fixes CLI-1289
The OpenAPI spec models `branch_id_or_ref` as `oneOf [project-ref, uuid]`,
but OpenAPI 3.0's `format: "uuid"` is a hint, not validation. The generator
pipeline left the UUID branch unconstrained (`Schema.String` with only a
format annotation), so a 20-letter project ref matched BOTH branches and
the union rejected with "Expected exactly one member to match" — breaking
every `branches get/update/pause/unpause/delete` invocation against a real
project ref returned from the resolver lookup (caught by branches e2e
parity tests).

Adds the canonical RFC 4122 pattern to every `format: "uuid"` string both
in the generator (sanitizeOpenApiSchema) and inline in the 8 affected
`branch_id_or_ref` schemas in contracts.ts. The branches become mutually
exclusive and 20-letter project refs route deterministically.

Also fixes a unit test that depended on the buggy permissive matching.
Two e2e parity failures observed against the bundled `dist/supabase-legacy`:

1. `branches get/create/update/...` panicked with "Service not found:
   supabase/legacy/CliConfig" — `legacy-upgrade-suggest.ts` and
   `get.handler.ts` both yield `LegacyCliConfig` directly, but
   `legacyManagementApiRuntimeLayer` only piped `legacyCliConfigLayer`
   into sub-layers via `Layer.provide`, which does not share to siblings
   inside `Layer.mergeAll`. Expose `legacyCliConfigLayer` at the top
   level of the merged layer so handler-scope `yield* LegacyCliConfig`
   resolves. Matches the CLAUDE.md parity-checklist item 5 footgun.

2. `branches get --output json` exit code 1 due to a schema-decode
   failure on the pooler fixture: the recorded scenario was missing
   the deprecated-but-still-required `connectionString` (camelCase)
   field. The Go proxy never re-decoded the response; the native TS
   handler does. Add the field to mirror the production API shape.
The schema fix for `branch_id_or_ref`'s `oneOf` union added a real RFC 4122
pattern check to the UUID branch, so the placeholder string "branch-ref"
no longer validates against either branch. Update every test fixture and
the `platform-examples` placeholder to use a 20-letter project ref
("abcdefghijklmnopqrst") that matches the project-ref branch:

- packages/api: api-client unit test that drove the unified execute path
- platform-examples.ts placeholder + its unit test + integration test
- platform-input.unit.test.ts prompt mock that asserted validate() accepts
  the value

User-facing example commands now display a valid placeholder, so anyone
who copies the example past the placeholder substitution prompt gets a
schema-valid command.
@Coly010 Coly010 force-pushed the cli/port-branches-command branch from 9fe7172 to c0c786d Compare May 28, 2026 12:01
Copy link
Copy Markdown
Contributor

@jgoux jgoux left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, big one!

@Coly010 Coly010 merged commit a0adb4d into develop May 28, 2026
11 checks passed
@Coly010 Coly010 deleted the cli/port-branches-command branch May 28, 2026 12:22
Coly010 added a commit that referenced this pull request May 28, 2026
…-HTTP fix

After rebasing onto develop, the branches port (#5374) had already landed a
shared `legacy/shared/legacy-upgrade-suggest.ts` helper covering the same
`cli_upgrade_suggested` telemetry path my SSO commit duplicated. Reconcile
to use the develop-side helper (`legacySuggestUpgrade` with an opts-object
argument shape) and delete the SSO-only `legacy/telemetry/legacy-upgrade-suggested.ts`
+ test.

Three pieces had to merge:

1. The shared helper's *behaviour* now writes the bold billing-URL
   suggestion to stderr in text mode (Go's `CmdSuggestion` parity) on top
   of firing the telemetry event. SSO inherits this for free — my prior
   helper only emitted the event and was missing this Go-parity
   side-effect.

2. The helper now uses raw `HttpClient` for the project + entitlements
   GETs (my fix from a few commits ago). Develop's version went through
   the typed `api.v1.getProject` which strict-decodes — the cli-e2e replay
   fixtures embed the literal 15-char `__PROJECT_REF__` placeholder in
   response bodies, failing `ref: isMinLength(20)`. With `Effect.option`
   swallowing the decode error, the entitlements GET would silently be
   skipped and the test would never observe the gated path. Same
   workaround used by `legacy-linked-project-cache.layer.ts`.

3. `branches/update`'s upgrade-gate integration test was built around
   `mockLegacyPlatformApiService` (typed-client mock) — its assertion
   pattern `apiMock.requests.find(r => r.method === "getProject")` no
   longer applies now that the helper bypasses the typed client. Rewrote
   that one test to use `mockLegacyPlatformApi` with a handler that
   routes the three URLs (`PATCH /v1/branches/<id>`, `GET /v1/projects/<ref>`,
   `GET /v1/organizations/<slug>/entitlements`) and assert against the
   recorded request URLs. As a bonus the rewrite verifies the stderr
   suggestion now emits ("Upgrade your plan:"), which the previous test
   couldn't reach.

Six SSO handler call sites updated to the opts-object signature.
SSO integration tests updated to use `mockAnalytics` from
`tests/helpers/mocks.ts` (the existing shared helper) instead of the
parallel `mockLegacyAnalytics` I had added — same shape, cleaner.

Verified locally:
- `nx run-many ...` (types + lint + fmt + knip) green.
- `bun run test:core`: 1127 in-process tests pass.
- Full `sso.e2e.test.ts` cli-e2e suite (52 tests) passes against
  `CLI_HARNESS_TARGET=ts-legacy`.
- `legacy-upgrade-suggest.unit.test.ts` (8 tests, develop-originated) still passes.
jgoux added a commit that referenced this pull request May 28, 2026
## Summary

Promotes `supabase orgs list` and `supabase orgs create` from Phase 0
Go-binary proxies to native TypeScript implementations in the legacy
shell. The handlers reuse the shared infrastructure introduced by the
[branches port](#5374) — HTTP error
mapper, Go-compatible JSON/YAML/TOML/env encoders, and Glamour-rendered
pretty tables — so no new shared helpers were needed.

Go source mirrored 1:1: `apps/cli-go/internal/orgs/list/list.go` and
`apps/cli-go/internal/orgs/create/create.go`. Error templates, output
mode behaviour, and the `Created organization: <id>` preamble byte-match
the Go CLI.

## Notable parity rules

- **`orgs list --output env`** is rejected with the byte-exact Go
message `--output env flag is not supported`.
- **`orgs list --output toml`** wraps the array as `[[organizations]]`
(Go uses a TOML envelope for arrays).
- **`orgs create --output env`** _is_ supported here, unlike on list —
matches Go's `EncodeOutput` flattening of a single object into `ID=…
NAME=… SLUG=…`.
- **`Created organization: <id>`** preamble fires before every Go-output
mode (json / yaml / toml / env / pretty) but is omitted from
`--output-format json` / `stream-json`, where the message is delivered
as a structured event field instead.
- **Telemetry** flushes in `Effect.ensuring` on success and failure
(CLAUDE.md PersistentPostRun parity rule). No linked-project cache write
— these are user-level commands without a `--project-ref`.
- **Parent description** matches Go exactly: `Manage Supabase
organizations` (no trailing period).

## Tests

- 3 unit tests for `renderOrgsListTable` (header-only, multi-row,
literal `|` passthrough).
- 32 integration tests covering every output mode for both subcommands,
the env-not-supported rejection, `--output`-over-`--output-format`
precedence, network and HTTP-503 error paths (including the `format !==
"text"` spinner-undefined branch), and telemetry flush on both success
and failure.

E2E coverage deliberately omitted — these are pure Management-API CRUD
commands with no subprocess-specific behaviour to validate (per
CLAUDE.md testing policy).

## Reviewer-relevant context

- The orgs handlers never resolve a project ref but still use
`legacyManagementApiRuntimeLayer`, which constructs the project-ref
resolver lazily. The orgs handlers simply don't yield those services.
This is intentional: keeping a single shared runtime layer is cheaper
than maintaining a user-vs-project layer split.
- `orgs.errors.ts` and `orgs.format.ts` are colocated at the family
root, not hoisted to `legacy/shared/`. Per CLAUDE.md
hoist-before-duplicate, the threshold is "used across ≥2 families" —
branches has its own `branches.format.ts`/`branches.errors.ts`, and
these are the second instance of the pattern, not yet a third.
- The Glamour table renders API-supplied strings (`id`, `name`) without
ANSI/control-character stripping. This is strict Go parity — both
`SIDE_EFFECTS.md` files document the deliberate pass-through under
"Security Notes" so a future renderer-level sanitization fix has a clear
pointer.

Refs CLI-1290.

Co-authored-by: Julien Goux <hi@jgoux.dev>
pull Bot pushed a commit to limerencel/tldraw that referenced this pull request May 29, 2026
…review (tldraw#8964)

In order to stop the "Deploy .com to preview" workflow from failing on
the "Create Supabase preview branch" step, this PR makes the
branch-provisioning wait loop tolerant of the Supabase CLI's output
while a branch is still being created.

The Supabase CLI
[v2.102.0](https://github.com/supabase/cli/releases/tag/v2.102.0)
[ported the `branches` commands from Go to native
TypeScript](supabase/cli#5374). Because our
workflow installs the CLI with `version: latest`, CI picked this up
automatically. The rewrite changed what `supabase branches get -o json`
writes to **stdout** while a branch is still provisioning: the old (Go)
CLI wrote empty stdout there, but the new (TS) CLI renders a non-JSON
error/status line. The workflow piped that straight into an unguarded
`jq`, which exited with code 5 on the parse error, and since the step
runs under `set -e` the whole step aborted on the first loop iteration —
before the retry loop could wait for the branch to become ready.

This was a CLI behavior change, not a regression in our code: the script
had been unchanged since the [Supabase preview-DB
migration](tldraw#8066), and the same
workflow passed on older CLI versions.

The fix adds `2>/dev/null || true` to `supabase branches get` and to
both `jq` calls so that, while the branch is provisioning, non-JSON /
non-zero output is treated as "not ready yet" and the loop keeps
retrying as it was always meant to. Once the branch is ready the CLI
returns valid JSON, `jq` extracts `POSTGRES_URL`, and the loop breaks
normally. Field names (`POSTGRES_URL`, `POSTGRES_URL_NON_POOLING`) are
unchanged in the new CLI, so no other adjustments are needed, and the
CLI stays on `latest`.

### Change type

- [x] `other` (CI)

### Test plan

1. Open a PR that triggers the "Deploy .com to preview" workflow with a
brand-new preview branch (one that has to provision from scratch).
2. Confirm the "Create Supabase preview branch" step logs "Waiting for
branch to be ready (i/24)..." while provisioning and then succeeds,
instead of failing with `jq: parse error` / exit code 5.

### Code changes

| Section        | LOC change |
| -------------- | ---------- |
| Config/tooling | +8 / -3    |
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