Skip to content

feat(cli): port config push#5489

Open
Coly010 wants to merge 7 commits into
developfrom
cli/port-config-push
Open

feat(cli): port config push#5489
Coly010 wants to merge 7 commits into
developfrom
cli/port-config-push

Conversation

@Coly010
Copy link
Copy Markdown
Contributor

@Coly010 Coly010 commented Jun 5, 2026

What

Replaces the Phase-0 Go proxy for supabase config push with a native Effect implementation in the legacy shell (CLI-1305). It pushes the local supabase/config.toml to the linked project's Management API across all five services — api, db (settings / network_restrictions / ssl_enforcement), auth, storage, experimental.webhooks — in Go's order, with the per-service GET → diff → confirm → PATCH/PUT/POST flow from pkg/config/updater.go.

Why

config push was the last config subcommand still proxying to the bundled Go binary. It rests on essentially the whole Go pkg/config package (schema + per-service diff + update-body machinery).

How it stays byte-exact with Go

The output contract is strict 1:1 — same stderr text, same unified-diff bytes, same API routes, same exit codes. The parity-critical engine lives in config-sync/ and is locked by golden fixtures generated from the Go binary:

  • config-sync.diff.ts — port of Go pkg/diff (anchored/patience unified diff).
  • config-sync.toml.ts — BurntSushi-parity TOML encoder driven by explicit per-service ordered field descriptors (ordering, omitempty, depth-1 blank lines, map sorting).
  • config-sync.units.ts / .duration.ts / .secret.ts — ports of docker/go-units (RAMInBytes/BytesSize), time.ParseDuration/Duration.String, and the Secret HMAC-SHA256 hashing.
  • {api,db,storage,experimental,auth}.sync.ts — push-subset projections + FromRemote + diff + update-body, one module per service.

Orchestration

  • push.handler.ts — native orchestrator: cost-aware confirmation prompts, the auth MFA addon cost filter, telemetry + linked-project-cache flush on success and failure, and --output-format json / stream-json structured summaries.
  • push.cost-matrix.ts / push.raw-presence.ts — use raw HTTP / raw TOML where the typed client or decoded config can't express Go's behaviour: the generated addon type is a closed enum (Go uses a plain string), and @supabase/config defaults the nil-pointer sections db.ssl_enforcement / storage.image_transformation / storage.s3_protocol to present, so raw-key detection recovers Go's skip-when-absent semantics.

@supabase/api schema corrections

config push is the first native command to decode the auth and network-restrictions GET responses, which surfaced spots where the generated schema is stricter than the real API. Corrected via the test-guarded openapi-overrides.json + synced contracts.ts:

  • AuthConfigResponse.nimbus_oauth_email_optional → optional. The spec marks it required, but the live API omits the key.
  • AuthConfigResponse.smtp_admin_email / sms_test_otp_valid_until → nullable. The spec marks them nullable: true, but the generated schema dropped Schema.Null for these format-annotated fields; the live API returns them as null.

Two config-push cli-e2e replay scenarios also had a recorded GET /network-restrictions body missing status; it's added to match the live API and the per-endpoint recorded fixtures.

Reviewer notes

  • Secrets: the diff serialises the hash:<sha256> form (Go Secret.MarshalText), but the update body sends the raw plaintext value (Go Secret.Value), gated by hash presence. Covered by a regression test.
  • requestWithAuth is hoisted to legacy/shared/legacy-raw-http.ts and shared with postgres-config.
  • Known gaps (documented in SIDE_EFFECTS.md): a matched [remotes.*] block aborts with exit 1 rather than overlaying a defaulted subtree; encrypted: dotenvx secret decryption is not reproduced (such secrets are skipped, not pushed as ciphertext).
  • docs/go-cli-porting-status.md flips config pushported.

@Coly010 Coly010 requested a review from a team as a code owner June 5, 2026 12:56
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 5, 2026

Supabase CLI preview

npx --yes https://pkg.pr.new/supabase@5489

Preview package for commit 46271f5.

@Coly010 Coly010 self-assigned this Jun 5, 2026
Coly010 added 3 commits June 8, 2026 09:42
Replace the Phase-0 Go proxy for `supabase config push` with a native
Effect implementation in the legacy shell (CLI-1305).

Pushes the local `supabase/config.toml` to the linked project's Management
API across all five services — api, db (settings / network_restrictions /
ssl_enforcement), auth, storage, experimental.webhooks — in Go order, with
per-service GET → diff → confirm → PATCH/PUT/POST flow.

Parity-critical engine (byte-exact against the Go binary, locked by goldens):
- config-sync.diff.ts: port of Go `pkg/diff` anchored/patience unified diff
- config-sync.toml.ts: BurntSushi-parity TOML encoder driven by ordered
  per-service field descriptors
- config-sync.units.ts / .duration.ts / .secret.ts: ramInBytes/bytesSize,
  time.ParseDuration/Duration.String, and Secret HMAC-SHA256 hashing
- {api,db,storage,experimental,auth}.sync.ts: push-subset projections,
  FromRemote, diff, and update-body construction

Orchestration:
- push.handler.ts native orchestrator with cost-aware confirm prompts, the
  auth MFA addon cost filter, telemetry + linked-project cache flush on
  success and failure, and json / stream-json structured summaries
- push.cost-matrix.ts and push.raw-presence.ts use raw HTTP / raw TOML where
  the generated client / decoded config can't express Go's behaviour
  (string addon types; nil-pointer ssl_enforcement / image_transformation /
  s3_protocol skipped when absent from config.toml)

Secrets are sent to the API as their raw plaintext value (Go `Secret.Value`)
while the diff serialises the `hash:` form, mirroring Go exactly.

Hoist `requestWithAuth` to legacy/shared/legacy-raw-http.ts (shared with
postgres-config). Flip docs/go-cli-porting-status.md to `ported`.
- abort with exit 1 when a [remotes.*] block targets the project ref
  instead of silently resetting non-overridden fields to schema defaults
- treat dotenvx encrypted: secrets as unresolved (hash to ''), so the
  ciphertext is gated out of the diff and update body rather than
  overwriting the remote secret
- compute sms_test_otp_valid_until with calendar-exact 10-year offset
  (setUTCFullYear) to match Go's time.Now().UTC().AddDate(10, 0, 0)
- document the CLI-1489 loadProjectConfig tradeoff; drop now-unreachable
  remote-merge branch from raw-presence
- add secret hasher unit tests + remotes-guard and encrypted-secret
  regression coverage
…PI fields

The config-push e2e (reconciles every section / parity) failed once decoding
got past the first service. Two classes of bug, both pre-existing and masked:

1. Over-strict generated @supabase/api schemas crashed on real responses:
   - NetworkRestrictionsResponse marked `status` required; the API omits it.
   - AuthConfigResponse marked `nimbus_oauth_email_optional` required; omitted.
   - `sms_test_otp_valid_until` / `smtp_admin_email` were emitted as
     single-element unions missing `Schema.Null` despite `nullable: true`.
   Relax the two over-strict `required` arrays via openapi-overrides.json
   (mirroring the existing saml override) and sync openapi.json + contracts.ts;
   add the dropped `Schema.Null` to the two nullable fields. These are real
   production decode crashes, not just test issues.

2. The auth update body diverged from Go's ToUpdateAuthConfigBody:
   - captcha / hooks / smtp / external providers were always emitted because
     @supabase/config defaults those pointer sections present. Gate them on
     raw-config presence (extends push.raw-presence to the [auth] subtree),
     and emit only Go's external set (the apple template default + configured
     providers).
   - external_url / jwt_issuer were hard-coded to ""; derive them from the api
     endpoint like Go's config load (api.external_url -> +/auth/v1 -> jwt_issuer).
@Coly010 Coly010 force-pushed the cli/port-config-push branch from b69940b to ceca515 Compare June 8, 2026 09:07
Coly010 added 4 commits June 8, 2026 10:38
…tures

The two config-push replay scenarios embedded a `GET /network-restrictions`
response missing the `status` field, while the live API and the per-endpoint
recorded fixtures both include it. The native config push port decodes the
response against the (correct, strict) generated schema, which requires
`status`, so the stale scenario bodies caused a `Missing key` decode error and
exit 1. Add `"status": "applied"` to match real API shape; no schema override
needed.
…ig push

The auth sync used the Go oapi-codegen enum *constant names* (alphanumerics
with separators stripped, e.g. "…XYZ0123456789", "…01234567891") as the string
values for `password_required_characters`. The real API values contain `:`
separators between character-class groups ("…XYZ:0123456789", etc.).

Consequences: a real remote value never matched, so `FromRemote` always mapped
back to "" → wrong diffs; and `authToUpdateBody` emitted a value the generated
client rejects on encode → the auth PATCH failed before being sent.

Replace the two duplicated switch tables with a single source-of-truth
`PASSWORD_REQUIREMENTS_TO_CHAR` map (values matching the @supabase/api
`V1{Get,Update}AuthServiceConfig` literals) plus its inverse, so the two
directions cannot diverge again. Add regression tests: round-trip per enum,
schema-acceptance of the emitted update-body value, and the unknown-value → ""
fallback. The existing tests repeated the wrong value and are corrected.
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.

1 participant