-
Notifications
You must be signed in to change notification settings - Fork 516
feat(cli): add config diff command #6295
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
kanadgupta
wants to merge
13
commits into
develop
from
kanadgupta/cli-2156-add-supabase-config-diff-to-the-cli
Open
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
ae8e1f2
feat(config): add config diff classification core (CLI-2156)
kanadgupta 8d98fcb
feat(config): suppress zero-valued remotes on optional-key paths; add…
kanadgupta 03d80f9
feat(cli): add config diff command (CLI-2156)
kanadgupta 24607dc
feat(cli): reject legacy -o on config diff (CLI-2156)
kanadgupta 766182f
Merge branch 'develop' into kanadgupta/cli-2156-add-supabase-config-d…
kanadgupta cae9c14
refactor(config): rebuild config diff on the CLI-2230 registry (CLI-2…
kanadgupta a32ada7
fix(api): make v2 project-config response blocks and keys optional
kanadgupta 3f23ca8
fix(config): rebuild the diff classifier on registry-declared semantics
kanadgupta 04a174a
fix(cli): make config diff invocable and honor the -o/--output flag
kanadgupta fba78b7
refactor(cli): fold config diff's --target into a branch-accepting --…
kanadgupta 27b62ef
fix(cli): harden config diff output and machine payload contract
kanadgupta 977e300
test(cli): close the config diff suite's structural blind spots
kanadgupta a4dc035
docs(cli): publish config diff reference and finalize ADR 0022
kanadgupta File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| # supabase-config-diff | ||
|
|
||
| Shows the configuration differences between the local `supabase/config.toml` and the effective configuration of a remote project or branch. Read-only: it never modifies the local file or any remote configuration. | ||
|
|
||
| Pass `--project-ref` to compare against a specific project, or the name (or UUID) of a branch of the currently linked project — values that are exactly 20 lowercase letters are always treated as project refs. Without it, the linked project is the target. When the target ref matches a `[remotes.*]` block's `project_id`, that block's merged config is the local side of the comparison. | ||
|
|
||
| Each difference is classified as `update` (the file declares a value that differs remotely), `remote-only` (the remote differs while the file is silent — the shown local value is the schema default a `config push` would write), or `local-only` (the file declares a value the remote did not report). `(unset)` means the local side has no value at all; `(not returned)` means the response did not carry the property. Secret values are never compared — the platform only reports digests — and are listed in a masked-credentials note instead, as are declared properties that `config push` cannot communicate. | ||
|
|
||
| Local values are shown as the configuration your file would produce once pushed, not its literal spelling: a duration written as `"1m"` renders as `"1m0s"`, and byte sizes are shown in the units you wrote. | ||
|
|
||
| With `--exit-code`, the command exits `2` when any difference is found, keeping exit `1` for errors — so scripts can distinguish drift from failure. Machine-readable output is available through `--output-format json|stream-json` (a versioned payload with per-change paths as segment arrays) or the global `-o json|yaml|toml|env` flag. |
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,9 @@ | ||
| import { Command } from "effect/unstable/cli"; | ||
| import { legacyConfigDiffCommand } from "./diff/diff.command.ts"; | ||
| import { legacyConfigPushCommand } from "./push/push.command.ts"; | ||
|
|
||
| export const legacyConfigCommand = Command.make("config").pipe( | ||
| Command.withDescription("Manage Supabase project configurations."), | ||
| Command.withShortDescription("Manage project configurations"), | ||
| Command.withSubcommands([legacyConfigPushCommand]), | ||
| Command.withSubcommands([legacyConfigDiffCommand, legacyConfigPushCommand]), | ||
| ); |
124 changes: 124 additions & 0 deletions
124
apps/cli/src/legacy/commands/config/diff/SIDE_EFFECTS.md
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,124 @@ | ||
| # `supabase config diff` | ||
|
|
||
| Read-only comparison between the local `supabase/config.toml` and the effective | ||
| configuration the Management API reports for a target project or branch. | ||
| Classifies every remotely-managed property as `update` / `remote_only` / | ||
| `local_only` (unmanaged local-only properties are never reported). **Never | ||
| writes `config.toml` or any remote configuration.** | ||
|
|
||
| ## Files Read | ||
|
|
||
| | Path | Format | When | | ||
| | ---------------------------------------------- | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ||
| | `<workdir>/supabase/config.toml` | TOML | always, before any network call (missing file or parse error aborts, exit 1); re-read after target resolution when the file declares `[remotes.*]`, to apply the matching overlay | | ||
| | `<workdir>/supabase/.env`, `.env.local` | dotenv | always, to resolve `env(VAR)` references inside `config.toml` | | ||
| | `<workdir>/supabase/.temp/project-ref` | plain text | project-ref fallback (flag → `SUPABASE_PROJECT_ID` → this file); parent-ref for a branch-name `--project-ref` | | ||
| | `<workdir>/supabase/.temp/linked-project.json` | JSON | existence check only, for the telemetry cache write below | | ||
| | `~/.supabase/access-token` | plain text (token string) | when `SUPABASE_ACCESS_TOKEN` unset and keyring unavailable | | ||
|
|
||
| ## Files Written | ||
|
|
||
| | Path | Format | When | | ||
| | ---------------------------------------------- | ------ | ---------------------------------------------------------------------- | | ||
| | `<workdir>/supabase/.temp/linked-project.json` | JSON | `Effect.ensuring` after run (success **and** failure), if ref resolved | | ||
| | `~/.supabase/telemetry.json` | JSON | `Effect.ensuring` after run (success **and** failure) | | ||
|
|
||
| **No writes to `supabase/config.toml` or `supabase/config.json`** — covered by | ||
| an integration test asserting mtime and contents are unchanged after a run | ||
| that finds differences. | ||
|
|
||
| ## API Routes | ||
|
|
||
| All Bearer-authenticated, all read-only. | ||
|
|
||
| | # | Purpose | Method | Path | Success | Notes | | ||
| | --- | ----------------------- | ------ | ------------------------------------ | ------- | --------------------------------------------------------------------- | | ||
| | 0a | branch by UUID | GET | `/v1/branches/{branch_id}` | 200 | only when `--project-ref` is a UUID; needs no linked project | | ||
| | 0b | branch by name | GET | `/v1/projects/{ref}/branches/{name}` | 200 | only when `--project-ref` is not a ref/UUID; 404 → "branch not found" | | ||
| | 1 | effective remote config | GET | `/v2/projects/{ref}/config` | 200 | always (after target resolution) | | ||
|
|
||
| ## Environment Variables | ||
|
|
||
| | Variable | Purpose | Required? | | ||
| | ----------------------- | --------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | | ||
| | `SUPABASE_PROJECT_ID` | project ref (flag → this → `.temp/project-ref` → prompt) | no | | ||
| | `SUPABASE_ACCESS_TOKEN` | auth token (bypasses credential file/keyring lookup) | no (falls back to keyring → `~/.supabase/access-token`) | | ||
| | `SUPABASE_PROFILE` | API profile selection | no | | ||
| | `env(VAR)` references | interpolated into `config.toml` values at load; a change on an env-resolved property names the variable in the output | no | | ||
|
|
||
| ## Exit Codes | ||
|
|
||
| Drift has its own exit code (`2`), distinct from every failure (`1`), so | ||
| `config diff --exit-code` scripts can tell "config drifted" from "token | ||
| expired" without parsing output (`terraform plan -detailed-exitcode`'s | ||
| convention; `1` stays the CLI-wide failure code). | ||
|
|
||
| | Code | Condition | | ||
| | ---- | ------------------------------------------------------------------------------ | | ||
| | `0` | success — including when differences are found, unless `--exit-code` is passed | | ||
| | `2` | `--exit-code` passed and at least one difference found | | ||
| | `1` | missing or malformed `supabase/config.toml` | | ||
| | `1` | unknown branch (branch-name `--project-ref` 404) | | ||
| | `1` | two `[remotes.*]` blocks declare the same `project_id` as the target ref | | ||
| | `1` | remote config read failure (network or unexpected status) | | ||
|
|
||
| ## Output | ||
|
|
||
| Diagnostics on **stderr**: `Comparing against …` (resolved target + local | ||
| scope, i.e. `[remotes.<name>]` or `base config`) before the fetch, then | ||
| `Comparison scope: <blocks>` listing the blocks the response carried (missing | ||
| blocks are called out). The payload is on **stdout**. | ||
|
|
||
| ### `--output-format text` | ||
|
|
||
| One block per difference (`<path> [update|remote-only|local-only]` with | ||
| `local:`/`remote:` lines; unset renders `(unset)` / `(not returned)`, an | ||
| undeclared path with a schema default renders `<value> (schema default — not | ||
| declared in config.toml)`, env-resolved values append `(from env VAR, …)`), | ||
| then a summary count line — `No config differences found.` when clean — | ||
| followed by a `Note: … (masked by the API): …` line when the file sets masked | ||
| secrets and a `Note: … cannot be pushed and … not compared: …` line for | ||
| declared properties push cannot communicate. Every non-constant string | ||
| (path segments, env-var names, remotes/branch names) is sanitized against | ||
| control characters before rendering. | ||
|
|
||
| ### `--output-format json` / `stream-json` | ||
|
|
||
| `output.success(message, payload)` — the message carries the masked/unmanaged | ||
| caveats too, so echoing it never claims "in sync" while masked values may have | ||
| drifted. The payload contains `schema_version` (integer version of THIS | ||
| payload contract, currently `1`), `config_schema` (the file's `$schema` URL), | ||
| `target` (`project_ref`, optional `branch`, `local_scope`), `scope` | ||
| (`{present, missing}` block lists — the block set is owned by | ||
| `@supabase/config`), `changes[]` (`path` as a SEGMENT ARRAY — a record key may | ||
| contain a `.` — plus `class`, `declared`, `local`, `remote`, optional | ||
| `env_variables[]`; unset sides are `null`), `masked[]` and `unmanaged[]` | ||
| (segment-array paths), and `counts` (per class + `total`). | ||
|
|
||
| ### `-o/--output` (legacy machine formats) | ||
|
|
||
| Honored, and takes priority over `--output-format` (Legacy Shell Invariant | ||
| #6): `-o json|yaml|toml|env` encodes the same structured payload the | ||
| `--output-format json` envelope carries (TOML omits `null`-valued entries — | ||
| TOML has no null; env flattens to SCREAMING_SNAKE keys with arrays collapsing | ||
| to empty strings, the established `godotenv` shape). stdout is payload-pure in | ||
| every machine mode; diagnostics stay on stderr. `-o pretty` (and no `-o`) | ||
| falls through to `--output-format` handling. | ||
|
|
||
| ## Notes | ||
|
|
||
| - Run from the project root (or pass `--workdir`); `config.toml` is read relative to it. | ||
| - **Local operand per target (ADR 0018/0022):** when the resolved target ref matches a | ||
| `[remotes.<name>]` block's `project_id`, the local side is that branch's merged | ||
| effective config; otherwise the base config. The echoed scope line always says which. | ||
| - **Masked credentials:** secret-valued managed properties (the platform returns an HMAC, | ||
| never plaintext; the registry's `isSecret` rows) are treated as "present, unknown" — never | ||
| reported as differences and never counted for `--exit-code`; they are surfaced via the | ||
| masked note / `masked[]`. | ||
| - **Values are convergence projections (ADR 0021):** both sides are normalized through | ||
| `@supabase/config`'s `fromConfigDocument`/`fromApiProjectConfig`, so a reported "local" | ||
| value is what pushing the file would produce hosted (canonicalized durations/byte sizes, | ||
| push-gated omissions), not necessarily the file's literal spelling. | ||
| - **Partial responses:** a managed property the response does not carry is `local_only` | ||
| when the file declares it and silent otherwise; a missing block is called out on the | ||
| scope line rather than treated as an error. | ||
Oops, something went wrong.
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This claim (and ADR 0022's "partially-populated responses degrade … instead of an error") doesn't hold:
V2GetProjectConfigOutputmakes all six blocks — and their keys — required, so a missing block or key fails the typed decode inside the API client before any of this leniency runs. Confirmed live: staging doesn't returnstorage.database_pool_modeyet, and the command hard-fails on every invocation with— i.e. the command is currently broken against staging, and a permission-truncated response (the case the ADR names) surfaces as an opaque SchemaError. Consequences: the scope line's "(not returned: …)" branch is unreachable in production (it prints the constant six-block list on every run), and two
diff.format.unit.test.tscases exercise unreachable states.Pick one: loosen the contract (make blocks/keys optional, matching
auth's leniency — the stated intent) and keep the scope machinery, or delete the scope machinery and correct this doc + ADR 0022. Don't leave the doc asserting behaviour the contract forbids.