Skip to content

Commit cb8041a

Browse files
authored
feat(cli): rebuild config push as diff-first partial updates (CLI-2313) (#6454)
## Summary `supabase config push` is rebuilt as a **diff-first** command. It reads the project's effective configuration once (`GET /v2/projects/{ref}/config`), runs the same `diffProjectConfig` that `config diff` and `config pull` use, and writes only the properties your `supabase/config.toml` declares *and* that differ from the project — as sparse request bodies to the six existing v1 update endpoints. The previous pipeline (six per-service v1 GETs → rebuild each service's full local subset with hand-coded presence/enabled gates → TOML unified diff → PUT/PATCH the whole subset) is deleted: `push/config-sync/**`, the TOML encoder and diff renderer, and `push.raw-presence.ts`. Linear: CLI-2313. The optional follow-up to swap the six v1 writes for the single v2 PATCH once that endpoint ships is CLI-2317; the `@supabase/config` cleanup this unblocks is CLI-2314 (no `packages/config` changes here). ## What changed - `push.plan.ts` — routes every comparable `ProjectConfig` path to one of six write resources (`api`, `db.settings`, `db.network_restrictions`, `db.ssl_enforcement`, `auth`, `storage`) or to an explicit *unsupported* set (`db.major_version`, `db.pooler.*`, `auth.oauth_server.*`); owns the resource gates, the MFA add-on gates and the resource→response-block mapping. A unit test asserts every registry path is classified, so a new `@supabase/config` row cannot fall through silently. - `push.encoders.ts` — six pure encoders turning routed changes into sparse bodies. Groups the API requires together still ship whole (CIDR lists, storage feature blocks, SMTP, captcha, each auth hook, each OAuth provider, the active SMS provider); **undeclared members of such a group are sent with the project's current value read in the same run**, so they do not change. Only when the API did not return a member's value is the config default sent, and that is disclosed in the confirmation block as `[group-write]`. The storage encoder can emit `{ enabled: false, … }` for `icebergCatalog`/`vectorBuckets`, which the old builder structurally could not. - `push.secrets.ts` — declared secrets are compared as HMAC digests against what the API reports; plaintext is sent only when the digest differs or the remote has no value. Empty values and unresolved `env(...)` references are never sent and are now disclosed before the prompt. - `push.format.ts` — per-service change blocks reuse the family's per-property renderer (`config/config.format.ts`), plus the new notes, the machine-readable summary message and the JSON payload. - `push.handler.ts` — one read, then per-resource encode → confirm → write in the established order. A resource whose block the API did not return is reported `unavailable` and never written; a response with no configuration blocks at all is an error (nothing is written). - Family-root hoists shared by `config diff`, `config pull` and `config push`: `config/config.read-status.ts` (401/403/404 read-status messages) and `config/config.project-config.ts` (the `ProjectConfigParseError` guard). Both handlers now use them instead of private copies. - Test infrastructure: `mockLegacyPlatformApiService` gains `executeRaw` support; the v2 response fixture is shared between the `config diff` and `config push` suites; the cli-e2e replay scenarios and recorded fixtures reflect the new request sequence. - `SIDE_EFFECTS.md` rewritten for the new routes, output, statuses and exit causes. ## Behavior changes (user-visible) 1. **One read instead of six.** `GET /v2/projects/{ref}/config` replaces the six per-service `GET /v1/…` calls. `GET /v1/projects/{ref}/billing/addons` is unchanged. 2. **Undeclared properties are left alone.** Previously a property absent from `config.toml` was written back with its schema default whenever its service was pushed. Now only declared, differing properties are written; members of a required-together group keep their current project value. `config diff` shows the remaining drift, and the push output reports how many remote properties were left untouched. 3. **Requests carry only what changed** (plus the required-together groups above). 4. **Credentials are compared before being sent**; unchanged secrets are not re-sent, and a credential that cannot be sent (empty / unresolved `env(...)`) is shown in the confirmation block. 5. **Per-service change display** is now the same per-property format `config diff` prints (`path [update]`, `local:` / `remote:`) instead of a TOML-style unified diff. Secret values are never printed. 6. **New reporting**: a `Comparison scope:` line on every run; notes for properties with no Management API field, properties that could not be encoded as written (with the reason), declared properties the command does not manage (count), remote-only drift (count), and group members written at a default. 7. **Partial API responses are never written blind.** A resource whose configuration block was not returned is reported `unavailable` and skipped; if no blocks are returned the command exits 1. 8. **JSON / stream-json payload** gains `schema_version: 1`, `services[].changes`, `unsupported`, `unencodable`, `forced`, `unmanaged`, `secrets` (`sent`/`unchanged`/`not_set`/`gated`/`skipped`, reflecting the write that actually happened), `declined_addons`, `remote_only`, `scope`, and a non-empty summary `message`. Existing fields (`project_ref`, `services[].service`, `services[].status`) keep their values; `status` gains `unavailable` and `not_pushable`. 9. **Declining an MFA cost prompt** behaves as before (both `verify_enabled` and `enroll_enabled` are sent as `false` when the project has either enabled) and is now recorded in `declined_addons`. 10. **Command description** now states the hands-off rule and points at `config diff` as the preview. 11. **Unchanged**: command path and flags, prompt wording, `Pushing config to project:` / `Remote <service> config is up to date.` / `Updating <service> service with config:` / `Enabling webhooks for project:` lines, exit codes (declining a prompt still exits 0), files read and written, telemetry. ## Reviewer notes - Storage feature *disable* is encoder-ready but not yet reachable: `@supabase/config` still prunes a disabled `storage.analytics`/`storage.vector` from the local projection (`applyPushUnmanagedOmissions`), so such a change surfaces in the `unmanaged` count today and starts being pushed when CLI-2314 lands. - The one `Effect.die` arm in `legacyConfigProjectConfigTry` is unreachable from a JSON-parsed body (every `@supabase/config` failure is a `ProjectConfigParseError`) and is covered by its unit test rather than the integration suites.
1 parent 0489a3d commit cb8041a

63 files changed

Lines changed: 11439 additions & 9549 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,6 @@ packages/cli-*/bin/
2323

2424
# Turbo
2525
.turbo/
26+
27+
# Transient render dir created by packages/api/scripts/generated-output-sync.unit.test.ts
28+
packages/api/.generated-output-sync-*/
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"method": "GET",
3+
"path": "/v2/projects/__PROJECT_REF__/config",
4+
"query": {},
5+
"headers": {
6+
"accept-encoding": "gzip",
7+
"authorization": "Bearer __ACCESS_TOKEN__",
8+
"host": "localhost:__PORT__",
9+
"user-agent": "SupabaseCLI/"
10+
},
11+
"body": null
12+
}

apps/cli-e2e/fixtures/recorded/GET_v2_projects___PROJECT_REF___config/default.response.json

Lines changed: 347 additions & 0 deletions
Large diffs are not rendered by default.

apps/cli-e2e/fixtures/recorded/PATCH_v1_projects___PROJECT_REF___config_auth/default.request.json

Lines changed: 1 addition & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -5,53 +5,13 @@
55
"headers": {
66
"accept-encoding": "gzip",
77
"authorization": "Bearer __ACCESS_TOKEN__",
8-
"content-length": "1329",
8+
"content-length": "82",
99
"content-type": "application/json",
1010
"host": "localhost:__PORT__",
1111
"user-agent": "SupabaseCLI/2.90.0"
1212
},
1313
"body": {
14-
"disable_signup": false,
15-
"external_anonymous_users_enabled": false,
16-
"external_apple_enabled": false,
17-
"external_email_enabled": true,
18-
"external_phone_enabled": false,
19-
"external_web3_ethereum_enabled": false,
20-
"external_web3_solana_enabled": false,
21-
"jwt_exp": 3600,
22-
"mailer_autoconfirm": true,
23-
"mailer_otp_exp": 3600,
24-
"mailer_otp_length": 6,
25-
"mailer_secure_email_change_enabled": true,
26-
"mfa_max_enrolled_factors": 10,
27-
"mfa_phone_enroll_enabled": false,
28-
"mfa_phone_max_frequency": 5,
29-
"mfa_phone_otp_length": 6,
30-
"mfa_phone_template": "Your code is {{ .Code }}",
31-
"mfa_phone_verify_enabled": false,
32-
"mfa_totp_enroll_enabled": false,
33-
"mfa_totp_verify_enabled": false,
34-
"mfa_web_authn_enroll_enabled": false,
35-
"mfa_web_authn_verify_enabled": false,
36-
"password_min_length": 8,
37-
"password_required_characters": "",
38-
"rate_limit_anonymous_users": 30,
39-
"rate_limit_otp": 30,
40-
"rate_limit_sms_sent": 30,
41-
"rate_limit_token_refresh": 150,
42-
"rate_limit_verify": 30,
43-
"rate_limit_web3": 30,
44-
"refresh_token_rotation_enabled": true,
45-
"security_manual_linking_enabled": false,
46-
"security_refresh_token_reuse_interval": 10,
47-
"security_update_password_require_reauthentication": false,
48-
"sessions_inactivity_timeout": 0,
49-
"sessions_timebox": 0,
5014
"site_url": "https://example.com",
51-
"sms_autoconfirm": false,
52-
"sms_max_frequency": 5,
53-
"sms_template": "Your code is {{ .Code }}",
54-
"smtp_max_frequency": 1,
5515
"uri_allow_list": "https://example.com/callback"
5616
}
5717
}

0 commit comments

Comments
 (0)