From 45376daef5a9101a25e425f54054b89d6bdf3a5a Mon Sep 17 00:00:00 2001 From: Andrew Valleteau Date: Wed, 14 Oct 2026 10:53:16 +0000 Subject: [PATCH 01/11] fix(cli): name the failing platform operation in defect telemetry (#7137) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary **TL;DR** When a command dies on an unclassified Effect `PlatformError` (a filesystem or stdio failure whose reason is not `NotFound`, `PermissionDenied`, or `BadArgument`), telemetry now names the reason and the Effect operation that raised it. Today every such failure collapses into one `error:Defect:PlatformError` fingerprint, which recent prereleases report on `status` / `stack status` for the new local stack without any way to tell which call fails. ### Before ```mermaid flowchart LR A["FileSystem / Stdio call fails"] --> B["PlatformError
(Busy, Unknown, ...)"] B -->|orDie| C["error:Defect:PlatformError"] ``` ### After ```mermaid flowchart LR A["FileSystem / Stdio call fails"] --> B["PlatformError
(Busy, Unknown, ...)"] B -->|orDie| C["error:Defect:PlatformError:
Busy:FileSystem.readFile"] ``` ### Why The failures cluster right after `start` while other commands run in parallel, and a retry succeeds, which points at a race on shared state or on stdout. Several call sites turn a `PlatformError` into a defect, so the fingerprint has to carry the operation before the cause can be fixed. ### What changed - A `PlatformError` defect fingerprints as `error:Defect:PlatformError::.`, for example `…:Busy:FileSystem.readFile`. - Only the closed reason tag and Effect's module and method names are read, as data descriptors; `pathOrDescriptor` and `description` never reach telemetry. If any part is not a plain identifier, the fingerprint stays `error:Defect:PlatformError`. - `PlatformError` failures that are already classified (`not_found`, `filesystem`, `bad_argument`) are unchanged. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 --- .../shared/telemetry/error-actionability.ts | 57 +++++++++++++++++-- .../error-actionability.unit.test.ts | 35 +++++++++++- 2 files changed, 86 insertions(+), 6 deletions(-) diff --git a/apps/cli/src/shared/telemetry/error-actionability.ts b/apps/cli/src/shared/telemetry/error-actionability.ts index 2e02b1749b..1eb66717fc 100644 --- a/apps/cli/src/shared/telemetry/error-actionability.ts +++ b/apps/cli/src/shared/telemetry/error-actionability.ts @@ -1,4 +1,5 @@ import { Cause, Option, Predicate } from "effect"; +import type { PlatformError } from "effect"; import type { CliError as EffectCliError } from "effect/cli"; import type { StackError, StackFailureKind } from "@supabase/stack/effect"; @@ -1158,10 +1159,55 @@ const defectSystemErrorCodes = new Set([ "EXDEV", ]); +// Data descriptors only, so reading a defect never runs a getter. +const ownValue = (record: object, key: string): unknown => + Object.getOwnPropertyDescriptor(record, key)?.value; + +const platformErrorReasons: ReadonlySet = new Set([ + "AlreadyExists", + "BadResource", + "Busy", + "InvalidData", + "NotFound", + "PermissionDenied", + "TimedOut", + "UnexpectedEof", + "Unknown", + "WouldBlock", + "WriteZero", +]); + +// The Effect platform services, and the CLI's stdin reader, that raise PlatformError for local I/O. +const platformErrorModules: ReadonlySet = new Set([ + "ChildProcess", + "Crypto", + "FileSystem", + "Path", + "Stdin", + "Stdio", + "Terminal", +]); + +/** + * Names a PlatformError's closed reason and the platform module and method that raised it. The + * method is an identifier those modules pass as a literal, never a caller value. + */ +function platformErrorOperation(error: Error): string | undefined { + const reason = ownValue(error, "reason"); + if (!isErrorRecord(reason)) return undefined; + const tag = ownValue(reason, "_tag"); + const module = ownValue(reason, "module"); + const method = ownValue(reason, "method"); + if (typeof tag !== "string" || !platformErrorReasons.has(tag)) return undefined; + if (typeof module !== "string" || !platformErrorModules.has(module)) return undefined; + const name = typeof method === "string" ? safeIdentifier(method) : undefined; + return name === undefined ? undefined : `${tag}:${module}.${name}`; +} + /** * Names an unclassified defect by its class's prototype tag and a known system error code, or by its type - * for a non-error value. Instance fields other than an errno code are never read, so only - * source-owned identifiers reach telemetry. + * for a non-error value. A PlatformError adds its closed reason and known raising operation. No + * other instance field is read, so only source-owned identifiers reach telemetry. */ function defectIdentity(defect: unknown): string { try { @@ -1169,8 +1215,11 @@ function defectIdentity(defect: unknown): string { if (!isErrorRecord(error)) return typeof error; if (!(error instanceof Error)) return "object"; const base = readStableTaggedPrototypeName(error, true) ?? "Error"; - // A data descriptor, so reading the code never runs a getter. - const code = Object.getOwnPropertyDescriptor(error, "code")?.value; + if (base === "PlatformError") { + const operation = platformErrorOperation(error); + if (operation !== undefined) return `${base}:${operation}`; + } + const code = ownValue(error, "code"); return typeof code === "string" && defectSystemErrorCodes.has(code) ? `${base}:${code}` : base; } catch { return "Unreadable"; diff --git a/apps/cli/src/shared/telemetry/error-actionability.unit.test.ts b/apps/cli/src/shared/telemetry/error-actionability.unit.test.ts index 6c9fa5b4eb..246b9baaa0 100644 --- a/apps/cli/src/shared/telemetry/error-actionability.unit.test.ts +++ b/apps/cli/src/shared/telemetry/error-actionability.unit.test.ts @@ -1,4 +1,4 @@ -import { Cause, Data } from "effect"; +import { Cause, Data, PlatformError } from "effect"; import { CliError } from "effect/cli"; import { describe, expect, it } from "vitest"; import { SupabaseApiInputError, markSupabaseApiInputErrorAsUserInput } from "@supabase/api/effect"; @@ -297,7 +297,17 @@ describe("classifyCliCauseActionability", () => { }); }); - it("names an unclassified defect by its source-owned tag and errno code, never instance data", () => { + const platformDefectWith = (field: "_tag" | "module" | "method", value: string) => { + const reason = new PlatformError.SystemError({ + _tag: "Busy", + module: "FileSystem", + method: "readFile", + }); + Object.defineProperty(reason, field, { value }); + return new PlatformError.PlatformError(reason); + }; + + it("names an unclassified defect by its source-owned tag, errno code, or platform operation, never instance data", () => { const secret = "privateToken"; class NamespaceDefect extends Data.TaggedError("Namespace.NamespaceError")<{ readonly message: string; @@ -323,6 +333,27 @@ describe("classifyCliCauseActionability", () => { "error:Defect:Error", ], [secret, "error:Defect:string"], + [ + new PlatformError.PlatformError( + new PlatformError.SystemError({ + _tag: "Busy", + module: "FileSystem", + method: "readFile", + pathOrDescriptor: secret, + description: secret, + }), + ), + "error:Defect:PlatformError:Busy:FileSystem.readFile", + ], + [ + new PlatformError.PlatformError( + new PlatformError.SystemError({ _tag: "WouldBlock", module: "Stdin", method: "read" }), + ), + "error:Defect:PlatformError:WouldBlock:Stdin.read", + ], + [platformDefectWith("_tag", secret), "error:Defect:PlatformError"], + [platformDefectWith("module", secret), "error:Defect:PlatformError"], + [platformDefectWith("method", "a b"), "error:Defect:PlatformError"], ]; for (const [defect, fingerprint] of cases) { From 2930f6d069ef8cb71f4eeecb495fb18f44a8a765 Mon Sep 17 00:00:00 2001 From: Wen Bo Xie Date: Wed, 14 Oct 2026 11:02:55 +0000 Subject: [PATCH 02/11] docs(cli): port pg-delta reference fixes into db diff and db pull overlays (#7130) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary The published CLI reference describes pg-delta, but its source in this repo still describes migra. The next release would regenerate the reference and revert the September fixes made directly in supabase/supabase. This PR moves those fixes into the overlays so the generator produces the corrected text. Three files under `apps/cli/docs/` change. No code changes. - `db diff`: projects with `[experimental.pgdelta] enabled = true` use pg-delta. Other projects run migra. Storage buckets are data, so no engine diffs them. The known-miss list (publications, `security_invoker` views) now applies to migra only, since pg-delta tracks both. - `db pull`: initial-pull behavior is stated per engine. pg-delta diffs against an empty shadow database. migra seeds the migration with `pg_dump`. `--diff-engine pg-delta` is the single-run opt-in. Added the managed-schemas paragraph (triggers on `auth.users`, RLS on `storage.*`). Added a caveat for `--declarative` on projects without pg-delta: it also sets `[db.migrations].schema_paths` and breaks `db diff`. - `db pull` examples: the CLI no longer prints "auth and storage schemas are excluded" or "Try rerunning with --debug". The examples show current output. The custom-schemas example uses `public,extensions` instead of `auth,storage`. Verification: ran `bun scripts/generate-docs-spec.ts` and diffed the `db diff` and `db pull` sections against the yaml on supabase/supabase `master`. No stale sentence remains. The only other differences are flag descriptions, which come from the command code. ## Linked issue Closes # - [x] The linked issue is **open** and carries the `open-for-contribution` label (or I'm a Supabase maintainer). ## Checklist - [x] The PR title follows [Conventional Commits](https://www.conventionalcommits.org/) (e.g. `fix(cli): …`). - [ ] Tests added or updated for the change. - [ ] From the repository root, `pnpm check:all` passes; relevant package tests pass for every touched workspace, and `pnpm types:check` passes for each touched TypeScript workspace (or workspace declaring it). --------- Co-authored-by: avallete Co-authored-by: Claude Opus 5.5 Co-authored-by: Andrew Valleteau --- apps/cli/docs/supabase/db/diff.md | 5 ++--- apps/cli/docs/supabase/db/pull.md | 11 +++++++++-- apps/cli/docs/templates/examples.yaml | 19 +++++++++---------- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/apps/cli/docs/supabase/db/diff.md b/apps/cli/docs/supabase/db/diff.md index 6ad22f7e1a..eec0081f3e 100644 --- a/apps/cli/docs/supabase/db/diff.md +++ b/apps/cli/docs/supabase/db/diff.md @@ -4,7 +4,7 @@ Diffs schema changes made to the local or remote database. Requires the local development stack to be running when diffing against the local database. To diff against a remote or self-hosted database, specify the `--linked` or `--db-url` flag respectively. -Runs [djrobstep/migra](https://github.com/djrobstep/migra) in a container to compare schema differences between the target database and a shadow database. The shadow database is created by applying migrations in local `supabase/migrations` directory in a separate container. Output is written to stdout by default. For convenience, you can also save the schema diff as a new migration file by passing in `-f` flag. +Compares schema differences between the target database and a shadow database. Projects with `[experimental.pgdelta] enabled = true` use the bundled pg-delta engine. Other projects run the legacy [djrobstep/migra](https://github.com/djrobstep/migra) engine in a container, unless you pass `--use-pg-delta` for a single run. The shadow database is created by applying migrations in local `supabase/migrations` directory in a separate container. Output is written to stdout by default. For convenience, you can also save the schema diff as a new migration file by passing in `-f` flag. Explicit `--from`/`--to` mode always uses pg-delta. In this mode, `-f` is ignored and stdout (or `--output`) is a flattened representation for review, not a portable apply script. Do not apply it directly with plain `psql -f`: transactional units can contain `SET LOCAL` preambles that only take effect inside a transaction, while plans that mix transactional and non-transactional units cannot safely be wrapped in one transaction. To create an applicable migration, use normal target mode with `supabase db diff -f `, then apply it through `supabase db reset` locally or `supabase db push` against the linked project. These paths preserve the plan's per-unit transaction semantics. @@ -14,8 +14,7 @@ Projects created by a recent `supabase init` default to the pg-delta diff engine With the bundled pg-delta engine, diff SQL defaults to uppercase keywords, indent 2, a maximum width of 180, trailing commas, and column/key alignment, matching its declarative export. When `-f` writes migrations, execution-aware transaction semantics are preserved as ordered per-unit files; non-transactional units carry a directive that the CLI apply path honors. Flattened review output retains the rendered SQL and preambles, but not the unit boundaries supplied to a migration runner. Configure overrides with `[experimental.pgdelta] format_options`, or set `format_options = "null"` to emit raw, unformatted statements. -While the diff command is able to capture most schema changes, there are cases where it is known to fail. Currently, this could happen if you schema contains: +Storage buckets are data, not schema, so no diff engine captures them. Under the legacy migra engine, the diff can also miss: - Changes to publication -- Changes to storage buckets - Views with `security_invoker` attributes diff --git a/apps/cli/docs/supabase/db/pull.md b/apps/cli/docs/supabase/db/pull.md index c0b917a4c5..dc64ac764e 100644 --- a/apps/cli/docs/supabase/db/pull.md +++ b/apps/cli/docs/supabase/db/pull.md @@ -8,9 +8,14 @@ Requires your local project to be linked to a remote database by running `supaba Optionally, a new row can be inserted into the migration history table to reflect the current state of the remote database. -If no entries exist in the migration history table, the default diff engine uses `pg_dump` to capture all contents of the remote schemas you have created. Otherwise, this command will only diff schema changes against the remote database, similar to running `db diff --linked`. +If no entries exist in the migration history table, the behavior depends on the diff engine. Under `pg-delta`, the CLI diffs the remote database against an empty shadow database. Under the legacy `migra` engine, the CLI uses `pg_dump` to capture all contents of the remote schemas you have created. Otherwise the command behaves like `db diff --linked` and only diffs schema changes against the remote database. -Pass `--diff-engine pg-delta` to keep the migration-file `db pull` workflow while using pg-delta for the shadow diff step. On initial pull, pg-delta replaces `pg_dump` and produces the full migration from the shadow diff alone. Pass `--declarative` to switch to the declarative pg-delta export workflow instead. +Pass `--diff-engine pg-delta` to use pg-delta for a single run on a project that hasn't enabled it. The migration-file workflow is unchanged. On an initial pull, the full migration comes from the shadow diff alone. Pass `--declarative` to export the selected database into the declarative schema tree instead of creating a migration. The tree lives in `supabase/schemas` by default. Change the location with `experimental.pgdelta.declarative_schema_path`. + +On a project without pg-delta enabled, don't use `--declarative`. The export also points `[db.migrations].schema_paths` at the tree it writes. The next local `db diff` applies those files in lexicographic order without resolving dependencies between them, so it fails when a file sorts before an object it depends on. If you've already run it, enable pg-delta, or undo both changes: + +1. Delete the files the export wrote to the declarative directory (`supabase/schemas/` by default). Files that were already there are left in place. +2. Restore `[db.migrations].schema_paths` in `config.toml` to its previous value, or remove it if the export added it. pg-delta plans are execution-aware: when a plan crosses a transaction boundary — for example `ALTER TYPE ... ADD VALUE` followed by a statement that uses the new enum value, which cannot run in the same transaction — `db pull` writes one ordered migration file per plan unit instead of a single file (for example `_remote_schema_schema_changes.sql` and `_remote_schema_after_enum_values.sql`), each recorded in the migration history. The common case (a single unit) still produces exactly one `_remote_schema.sql` file. @@ -18,6 +23,8 @@ By default the emitted SQL is formatted with the same settings the declarative e When `[experimental.pgdelta] enabled = true` (the default for projects created by a recent `supabase init`), the migration-file `db pull` workflow uses pg-delta for the shadow diff step by default; it does not switch to declarative output. Existing projects without the section are unaffected and keep using migra. To fall back to the legacy migra engine, set `enabled = false` under `[experimental.pgdelta]`, or pass `--diff-engine migra` for a single run. +Under the pg-delta engine, the pull captures your own customizations in Supabase-managed schemas automatically. Examples are triggers on managed tables like `auth.users` and RLS policies on `storage.objects` and `storage.buckets`. Under the legacy migra engine, the initial pull's `pg_dump` skips managed schemas. The diff pass that follows appends your trigger and RLS policy changes there to the same migration. If an older CLI reports that `auth` and `storage` were excluded, capture existing customizations there with `--schema auth,storage`. + When pulling from a remote database with `--db-url`, prefer a direct connection (`db..supabase.co:5432`) over the connection pooler so pg-delta can introspect the full catalog reliably. ## Debugging empty pg-delta pulls diff --git a/apps/cli/docs/templates/examples.yaml b/apps/cli/docs/templates/examples.yaml index 8d0e70222d..f49ca5269f 100644 --- a/apps/cli/docs/templates/examples.yaml +++ b/apps/cli/docs/templates/examples.yaml @@ -236,29 +236,28 @@ supabase-db-pull: Update remote migration history table? [Y/n] Repaired migration history: [20240414044403] => applied Finished supabase db pull. - The auth and storage schemas are excluded. Run supabase db pull --schema auth,storage again to diff them. - id: local-studio name: Local studio code: supabase db pull --local response: | Connecting to local database... - Setting up initial schema.... - Creating custom roles supabase/roles.sql... + Creating shadow database... + Seeding globals from roles.sql... Applying migration 20240414044403_remote_schema.sql... + Diffing schemas... No schema changes found - The auth and storage schemas are excluded. Run supabase db pull --schema auth,storage again to diff them. - exit status 1 + The remote database is already in sync with your local migrations — nothing to pull. - id: custom-schemas name: Custom schemas - code: supabase db pull --schema auth,storage + code: supabase db pull --schema public,extensions response: | Connecting to remote database... - Setting up initial schema.... - Creating custom roles supabase/roles.sql... + Creating shadow database... + Seeding globals from roles.sql... Applying migration 20240414044403_remote_schema.sql... + Diffing schemas: public,extensions No schema changes found - Try rerunning the command with --debug to troubleshoot the error. - exit status 1 + The remote database is already in sync with your local migrations — nothing to pull. supabase-db-push: - id: basic-usage name: Basic usage From ddb5c6e76dbf24457a10337edbd511a76d78a3e8 Mon Sep 17 00:00:00 2001 From: "supabase-cli-releaser[bot]" <246109035+supabase-cli-releaser[bot]@users.noreply.github.com> Date: Wed, 14 Oct 2026 12:52:16 +0000 Subject: [PATCH 03/11] chore(stack): bump storage to v1.82.1-r0 (#7153) This bumps storage from v1.82.0-r0 to v1.82.1-r0. https://github.com/supabase/slim-services/releases/tag/storage-v1.82.1-r0 Planned after supabase/slim-services published v1.82.1-r0. This branch is rewritten from develop whenever a newer relevant release arrives. Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- apps/cli-go/pkg/config/templates/Dockerfile | 2 +- apps/cli/src/shared/services/Dockerfile | 2 +- packages/stack/src/Artifacts.ts | 18 +++++++++--------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/apps/cli-go/pkg/config/templates/Dockerfile b/apps/cli-go/pkg/config/templates/Dockerfile index 8ddea5a6ac..563c96ccd7 100644 --- a/apps/cli-go/pkg/config/templates/Dockerfile +++ b/apps/cli-go/pkg/config/templates/Dockerfile @@ -35,7 +35,7 @@ FROM supabase/gotrue:v2.197.0 AS gotrue FROM supabase/realtime:v2.143.4 AS realtime -FROM supabase/storage-api:v1.82.0 AS storage +FROM supabase/storage-api:v1.82.1 AS storage FROM supabase/logflare:1.53.0 AS logflare # One-shot job images (not started as long-running containers); differ's alias diff --git a/apps/cli/src/shared/services/Dockerfile b/apps/cli/src/shared/services/Dockerfile index 8ddea5a6ac..563c96ccd7 100644 --- a/apps/cli/src/shared/services/Dockerfile +++ b/apps/cli/src/shared/services/Dockerfile @@ -35,7 +35,7 @@ FROM supabase/gotrue:v2.197.0 AS gotrue FROM supabase/realtime:v2.143.4 AS realtime -FROM supabase/storage-api:v1.82.0 AS storage +FROM supabase/storage-api:v1.82.1 AS storage FROM supabase/logflare:1.53.0 AS logflare # One-shot job images (not started as long-running containers); differ's alias diff --git a/packages/stack/src/Artifacts.ts b/packages/stack/src/Artifacts.ts index 184195ea71..3ad3e92387 100644 --- a/packages/stack/src/Artifacts.ts +++ b/packages/stack/src/Artifacts.ts @@ -267,23 +267,23 @@ const definitions: Readonly> = { storage: definition( "storage", { - upstreamVersion: "v1.82.0", + upstreamVersion: "v1.82.1", revision: 0, image: - "ghcr.io/supabase/cli/storage:v1.82.0-r0@sha256:c71adb01633ce0147162f53fd5d4fa861fcf2707586707f6d88acfaa8db2eeb4", - upstreamImage: "supabase/storage-api:v1.82.0", + "ghcr.io/supabase/cli/storage:v1.82.1-r0@sha256:68c0f36557430219eb318df22ee817915992f2d5c976971e32574c3fe429be3c", + upstreamImage: "supabase/storage-api:v1.82.1", natives: { "darwin-arm64": { - archive: "e225cd6ce14cd8ee1ede02c8e3b14e366ae2342920d904b6bd30727cbc1fd6ab", - manifest: "b2e126721482284b60cb32d82b530e17e9603cbe95cbfcc35dcb7fcedad0696d", + archive: "69a3308f1633db400bc718ee363aaa3d0d8ae846f52af4ea3a61d4cf04064e69", + manifest: "37ac0f369085e88952fde5c7493e2291d9e1034ade6c763509900e4cafa7d978", }, "linux-amd64": { - archive: "de14d47658694a3d084d26bd12c0d2ec8ff30a7abbc1dcdcbc7fa1b098010360", - manifest: "5de5ef546a2f579b6c2dd025eaf9052013849218db80449b87feda23b8d8db59", + archive: "f8716410f5997cf2a89cd3fe13565fe21b6bb239ef0be9818be385c0b397afba", + manifest: "ca82ac18ccccd91ec3370d65497c7f17c938f8601d7982a5fb48893ad5c1b159", }, "linux-arm64": { - archive: "3e436461a05a18579da27b3734df0c367195731046c54044a0f81e442dccec39", - manifest: "a9eed25be05723dc34a2f7240cf8127c3d3c85fbfb82fae903f470122debf2fa", + archive: "45e623ac2913dc0aa2e11c3b58ea437227c51bc8e55a5184a2736fb1c13ab49e", + manifest: "024d11f58d90d54ebe62f55b88f75d5b0e541f1612211f5bf6a351472c6cb466", }, }, }, From d68f6d098f8e65a6f2909087fa443bae284eb405 Mon Sep 17 00:00:00 2001 From: Andrew Valleteau Date: Wed, 14 Oct 2026 13:17:09 +0000 Subject: [PATCH 04/11] fix(cli): wait for a concurrent Docker prune in stop (#7143) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `supabase stop` no longer fails when another stack on the same Docker daemon is stopping at the same moment. Docker runs one prune at a time per daemon and rejects a concurrent container, volume, or network prune with `a prune operation is already running`, even when the label filters differ. `stop` now runs that prune again while Docker reports the conflict, with a short jittered wait and a bounded number of attempts; any other prune failure still fails immediately. The container CLI helper now also collects stderr so the conflict can be recognised. Split out of #7129, which runs CLI e2e files concurrently and depends on this. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 --- .../cli/src/command-internal/container-cli.ts | 18 +++-- .../src/command-internal/docker-remove-all.ts | 56 ++++++++++++--- .../commands/stop/stop.integration.test.ts | 72 +++++++++++++++++++ 3 files changed, 131 insertions(+), 15 deletions(-) diff --git a/apps/cli/src/command-internal/container-cli.ts b/apps/cli/src/command-internal/container-cli.ts index 8b054fa922..c8ccc780a1 100644 --- a/apps/cli/src/command-internal/container-cli.ts +++ b/apps/cli/src/command-internal/container-cli.ts @@ -241,11 +241,11 @@ export function runContainerCliExpectSuccess( } /** - * Like {@link containerCliExitCode}, but also returns the child's stdout — reading it instead of - * ignoring it avoids the process hanging on an unread pipe. `podmanArgs` behaves the same as on + * Like {@link containerCliExitCode}, but also returns the child's stdout and stderr — reading them + * instead of ignoring them avoids the process hanging on an unread pipe. `podmanArgs` behaves the same as on * {@link containerCliExitCode}. */ -export const containerCliExitCodeAndStdout = ( +export const containerCliOutput = ( spawner: Spawner, args: ReadonlyArray, podmanArgs?: ReadonlyArray, @@ -255,7 +255,7 @@ export const containerCliExitCodeAndStdout = ( const options = { stdin: "ignore", stdout: "pipe", - stderr: "ignore", + stderr: "pipe", } satisfies ChildProcess.CommandOptions; const handle = yield* spawnRuntime(spawner, dockerRuntime, args, options).pipe( Effect.catch(() => @@ -268,11 +268,15 @@ export const containerCliExitCodeAndStdout = ( ); // Read stdout concurrently with the exit code: Node's "exit" event can fire before a fast // process's stdio finishes draining, so awaiting the exit code first would race the stream. - const [exitCode, stdout] = yield* Effect.all( - [handle.exitCode.pipe(Effect.map(Number)), collectText(handle.stdout)], + const [exitCode, stdout, stderr] = yield* Effect.all( + [ + handle.exitCode.pipe(Effect.map(Number)), + collectText(handle.stdout), + collectText(handle.stderr), + ], { concurrency: "unbounded" }, ); - return { exitCode, stdout }; + return { exitCode, stdout, stderr }; }), ); diff --git a/apps/cli/src/command-internal/docker-remove-all.ts b/apps/cli/src/command-internal/docker-remove-all.ts index 54f4a9fa58..40cc21333e 100644 --- a/apps/cli/src/command-internal/docker-remove-all.ts +++ b/apps/cli/src/command-internal/docker-remove-all.ts @@ -1,4 +1,4 @@ -import { Data, Effect, Result } from "effect"; +import { Data, Effect, Result, Schedule } from "effect"; import type { ChildProcessSpawner } from "effect/process/ChildProcessSpawner"; import { @@ -8,7 +8,7 @@ import { } from "../shared/telemetry/error-actionability.ts"; import { containerCliExitCode, - containerCliExitCodeAndStdout, + containerCliOutput, describeContainerCliFailure, dockerSupportsVolumePruneAllFlag, } from "./container-cli.ts"; @@ -91,6 +91,42 @@ const reportPruned = (debug: boolean, label: string, stdout: string) => globalThis.process.stderr.write(`${label} [${parsePrunedNames(stdout).join(" ")}]\n`); }); +/** + * Docker runs one prune at a time per daemon and rejects concurrent ones, even with different + * label filters, so stacks stopping at the same moment wait for each other's prune. + */ +const pruneWaitingForConcurrentPrune = Effect.fn("DockerRemoveAll.prune")(function* ( + spawner: Spawner, + args: ReadonlyArray, + podmanArgs?: ReadonlyArray, +) { + let attempts = 0; + const result = yield* containerCliOutput(spawner, args, podmanArgs).pipe( + Effect.tap(() => + Effect.sync(() => { + attempts += 1; + }), + ), + Effect.repeat({ + while: (output) => + output.exitCode !== 0 && /a prune operation is already running/iu.test(output.stderr), + schedule: Schedule.spaced("250 millis").pipe(Schedule.jittered), + times: 40, + }), + ); + yield* Effect.annotateCurrentSpan({ + "docker.prune.kind": args[0] ?? "", + "docker.prune.attempts": attempts, + "docker.prune.exit_code": result.exitCode, + }); + return result; +}); + +const pruneFailureMessage = (kind: string, stderr: string) => { + const detail = stderr.trim(); + return detail.length > 0 ? `failed to prune ${kind}: ${detail}` : `failed to prune ${kind}`; +}; + /** Every failure {@link dockerRemoveAll} can produce. */ export type DockerRemoveAllError = | DockerRemoveAllListError @@ -152,7 +188,7 @@ export const dockerRemoveAll = ( // Collects stdout instead of ignoring it — reading the pipe avoids the same hang, and the // report backs the `--debug` output below. - const containerPrune = yield* containerCliExitCodeAndStdout(spawner, [ + const containerPrune = yield* pruneWaitingForConcurrentPrune(spawner, [ "container", "prune", "--force", @@ -168,7 +204,7 @@ export const dockerRemoveAll = ( ); if (containerPrune.exitCode !== 0) { return yield* new DockerRemoveAllContainerPruneError({ - message: "failed to prune containers", + message: pruneFailureMessage("containers", containerPrune.stderr), }); } yield* reportPruned(debug, "Pruned containers:", containerPrune.stdout); @@ -182,7 +218,7 @@ export const dockerRemoveAll = ( // Podman has no `--all` flag on `volume prune` at all, but already prunes every unused // volume by default, so omitting `--all` there is a lossless fallback. const dockerSupportsAll = yield* dockerSupportsVolumePruneAllFlag(spawner); - const volumePrune = yield* containerCliExitCodeAndStdout( + const volumePrune = yield* pruneWaitingForConcurrentPrune( spawner, [ "volume", @@ -202,12 +238,14 @@ export const dockerRemoveAll = ( ), ); if (volumePrune.exitCode !== 0) { - return yield* new DockerRemoveAllVolumePruneError({ message: "failed to prune volumes" }); + return yield* new DockerRemoveAllVolumePruneError({ + message: pruneFailureMessage("volumes", volumePrune.stderr), + }); } yield* reportPruned(debug, "Pruned volumes:", volumePrune.stdout); } - const networkPrune = yield* containerCliExitCodeAndStdout(spawner, [ + const networkPrune = yield* pruneWaitingForConcurrentPrune(spawner, [ "network", "prune", "--force", @@ -222,7 +260,9 @@ export const dockerRemoveAll = ( ), ); if (networkPrune.exitCode !== 0) { - return yield* new DockerRemoveAllNetworkPruneError({ message: "failed to prune networks" }); + return yield* new DockerRemoveAllNetworkPruneError({ + message: pruneFailureMessage("networks", networkPrune.stderr), + }); } // Established output text: singular "network", unlike the container/volume reports. yield* reportPruned(debug, "Pruned network:", networkPrune.stdout); diff --git a/apps/cli/src/commands/stop/stop.integration.test.ts b/apps/cli/src/commands/stop/stop.integration.test.ts index 1ae4855c3f..d96b48ad17 100644 --- a/apps/cli/src/commands/stop/stop.integration.test.ts +++ b/apps/cli/src/commands/stop/stop.integration.test.ts @@ -5,6 +5,7 @@ import { Deferred, Effect, Exit, + Fiber, FileSystem, Layer, Option, @@ -14,6 +15,7 @@ import { Stream, } from "effect"; import { ChildProcess, ChildProcessSpawner } from "effect/process"; +import { TestClock } from "effect/testing"; import { vi } from "vitest"; import { mockOutput } from "../../../tests/helpers/mocks.ts"; @@ -616,6 +618,76 @@ describe("stop integration", () => { }).pipe(Effect.provide(BunServices.layer)), ); + const PRUNE_RUNNING = "Error response from daemon: a prune operation is already running"; + + const networkPruneRoute = (failures: number, stderr: string) => { + const route = defaultRoute(); + let remaining = failures; + return (args: ReadonlyArray): RouteResult => { + if (args[0] === "network" && args[1] === "prune" && remaining > 0) { + remaining -= 1; + return { exitCode: 1, stderr: [stderr] }; + } + return route(args); + }; + }; + + /** Each retry sleeps only after the previous attempt, so the clock steps until `stop` settles. */ + const joinAdvancingClock = (fiber: Fiber.Fiber) => + TestClock.adjust("100 millis").pipe( + Effect.repeat({ while: () => fiber.pollUnsafe() === undefined }), + Effect.andThen(Fiber.join(fiber)), + ); + + const networkPrunes = (spawned: ReadonlyArray) => + spawned.filter((s) => s.args[0] === "network" && s.args[1] === "prune"); + + it.effect("waits for another stack's prune instead of failing", () => + Effect.gen(function* () { + const { layer, child } = yield* setup({ + configuredProjectId: "demo", + route: networkPruneRoute(2, PRUNE_RUNNING), + }); + const fiber = yield* stop(flags()).pipe(Effect.provide(layer), Effect.forkChild); + yield* joinAdvancingClock(fiber); + expect(networkPrunes(child.spawned)).toHaveLength(3); + }).pipe(Effect.provide(BunServices.layer)), + ); + + it.effect("fails once another stack's prune outlasts the retry budget", () => + Effect.gen(function* () { + const { layer, child } = yield* setup({ + configuredProjectId: "demo", + route: networkPruneRoute(Number.POSITIVE_INFINITY, PRUNE_RUNNING), + }); + const fiber = yield* stop(flags()).pipe(Effect.provide(layer), Effect.exit, Effect.forkChild); + const exit = yield* joinAdvancingClock(fiber); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(Cause.pretty(exit.cause)).toContain("StopNetworkPruneError"); + expect(Cause.pretty(exit.cause)).toContain("a prune operation is already running"); + } + expect(networkPrunes(child.spawned)).toHaveLength(41); + }).pipe(Effect.provide(BunServices.layer)), + ); + + it.effect("fails on the first unrelated prune error", () => + Effect.gen(function* () { + const { layer, child } = yield* setup({ + configuredProjectId: "demo", + route: networkPruneRoute(1, "Error response from daemon: permission denied"), + }); + const exit = yield* Effect.exit(stop(flags()).pipe(Effect.provide(layer))); + expect(Exit.isFailure(exit)).toBe(true); + if (Exit.isFailure(exit)) { + expect(Cause.pretty(exit.cause)).toContain( + "failed to prune networks: Error response from daemon: permission denied", + ); + } + expect(networkPrunes(child.spawned)).toHaveLength(1); + }).pipe(Effect.provide(BunServices.layer)), + ); + it.effect("deletes data volumes with --no-backup", () => Effect.gen(function* () { const { layer, child } = yield* setup({ From d47510fbd9cf0537020256dee5090a8247e6793b Mon Sep 17 00:00:00 2001 From: Andrew Valleteau Date: Wed, 14 Oct 2026 13:32:52 +0000 Subject: [PATCH 05/11] ci: speed up tests with sticky disks, parallel e2e and on-demand Windows and macOS jobs (#7129) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cuts CI time and cost: dependency and Docker caches come from Blacksmith sticky disks that only protected branches write, more end-to-end work runs in parallel, and PRs into `develop` run Windows and macOS jobs only when they touch platform-sensitive code. ## Gains ### Whole run Measured from every successful `test.yml` run between Oct 10 and Oct 14. Run time counts from run creation to the last job finishing, so it includes queueing. Values are median [range]; every run below ran the Windows and macOS jobs. | | Other branches, macOS native on `macos-latest` (Oct 10–13, 132 runs) | Other branches, macOS native on Blacksmith macOS since #7126 (Oct 13–14, 52 runs) | This PR, warm disks (7 runs) | This PR, empty disks (2 runs) | | --- | --- | --- | --- | --- | | Run time | 8.5 min [4.5–22.6] | 5.0 min [3.9–9.4] | 3.3 min [2.7–4.8] | 3.4–4.1 min | | Slowest job | 8.1 min [4.4–11.1] | 4.6 min [3.6–7.9] | 2.8 min [2.5–3.4] | 3.1–3.3 min | | macOS native scenarios | 8.1 min [4.4–11.1], one job | 3.4 min [2.9–7.9], one job | 2.1–3.4 min, slowest of up to four jobs | 1.7–1.9 min, slowest of four jobs | | Total job time | 62 min, 20 jobs | 65 min, 26 jobs | 44–56 min, 29–33 jobs | 58–62 min, 33 jobs | The macOS runner change in #7126 brought most of the drop between the two baselines. On top of it, this PR brings a run to about 3 minutes once disks are warm: the slowest Linux job goes from 4.3 min [3.6–5.0] on other branches since Oct 14 to 2.7 min [2.5–3.3], and Linux is now what bounds the run. One warm run took 15.9 min while its slowest job took 2.8 min; the rest was runner queueing, so it is left out of the range above. About half of PRs into `develop` will also skip the Windows and macOS jobs. ### Warm vs empty disks Measured on this PR by running the same commit twice: once with empty sticky disks, then again after the fill workflow populated them. | Linux jobs (Blacksmith) | Warm vs empty disks | | --- | --- | | Setup | ~80% faster | | Code quality | ~40% faster | | Unit tests | ~40% faster | | Integration shards (average) | ~30% faster | | CLI e2e shards (average) | ~25% faster | | Stack e2e, native (average) | ~25% faster | | Stack e2e, Docker (average) | ~20% faster | | Stack e2e, Podman (average) | ~15% faster | Compared with the base branch, Windows port tests setup is ~60% faster and macOS port tests setup ~40% faster. Splitting the declarative sync e2e into two files and letting its commands reuse the shadow baseline cache makes the slowest of those files ~65% faster than the original single file. Splitting the macOS native scenarios into four jobs and caching their native artifacts makes the slowest of them ~35% faster than the single job it replaces; with a warm cache the isolation job is ~45% faster than without. On warm disks every job finished in under three minutes; remaining time is test work rather than setup. ## Changes **Sticky disks.** On Blacksmith Linux runners, `test.yml` jobs mount the pnpm store, Go caches, and (for container jobs) Docker's image store from Blacksmith sticky disks instead of downloading them through `actions/cache`. Test jobs always mount them read-only; only the fill workflow commits, after pruning unused pnpm packages, containers, and volumes. Native stack jobs also mount the native artifact cache. Jobs that mount the Docker image disk stop Docker in a final step so the disk unmounts cleanly; the fill workflow also prunes containers and volumes first, even after a failure. Other workflows, including release builds, keep `actions/cache`. **Disk refill.** `fill-sticky-disks.yml` runs on pushes to protected branches, daily, and on demand. It installs dependencies, downloads the stack's native service artifacts, pulls both the `supabase start` images and the stack package's catalog images, so every disk stays warm. Default-branch fills also drop artifacts and images of older versions. It is the only writer, so Blacksmith's sticky-disk branch protection can be enabled. The disk keys are new, so the first fill after merge seeds every disk from protected-branch code. **Parallel CLI e2e.** CLI e2e files run four at a time in CI (`SUPABASE_E2E_WORKERS`); local runs stay serial. Each worker takes free ports only from its own slot, so a port one worker released is never handed to another. Files that cannot share the daemon run one at a time afterwards in an `e2e-exclusive` project: `start.health`, which tags a broken Mailpit image, and the files that provision pg-delta shadow databases on ports the CLI picks itself. Concurrent `supabase stop` teardowns rely on the prune retry from #7143. **Lighter Windows and macOS setup.** Port tests install only node, bun, and pnpm through mise instead of the full toolchain. Windows also skips the pnpm store archive because downloading packages is faster there than unpacking it. **Split slow e2e work.** The declarative sync e2e runs as two files that shard independently, and their commands share one home with the shipped shadow baseline cache on, so only the first sync provisions a cold shadow. macOS native scenarios run as separate lifecycle, confinement, idle, and isolation jobs, each restoring its native artifacts through `actions/cache`. **Windows and macOS on demand.** On PRs into `develop` or `next`, the Windows and macOS port tests and macOS native scenarios run only when the PR touches platform-sensitive paths (stack host, namespace, port, process, internal and artifact code, the CLI commands the Windows job covers, the lockfile, root config, and the test workflow). They always run in the merge queue, on PRs into `main` or `v*.x`, on `next` release runs, and when the `run-ci` label is added, which now also applies to ready PRs into `develop` and `next`. Replaying the last 50 merged PRs, about half would skip them. The summary jobs fail if change detection fails, so a skipped platform job never hides an error. **Concurrent whole-stack scenarios.** Stack e2e jobs run their scenarios concurrently. The write-confinement scenario watches the whole temp tree, so it runs in its own `confinement` job per runtime. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 --- .github/actions/setup/action.yml | 143 ++++++++++- .github/workflows/fill-sticky-disks.yml | 89 +++++++ .github/workflows/run-ci.yml | 11 +- .github/workflows/test.yml | 153 +++++++++++- apps/cli/package.json | 2 +- apps/cli/scripts/refresh-e2e-timings.ts | 7 +- .../scripts/refresh-e2e-timings.unit.test.ts | 6 +- .../schema/declarative/sync/sync.e2e.test.ts | 225 ++---------------- .../declarative/sync/sync.jobs.e2e.test.ts | 89 +++++++ apps/cli/tests/e2e-sequencer.ts | 5 +- apps/cli/tests/e2e-timings.json | 3 +- apps/cli/tests/helpers/cli.ts | 28 ++- .../cli/tests/helpers/declarative-sync-e2e.ts | 181 ++++++++++++++ apps/cli/vitest.config.ts | 41 +++- packages/stack/package.json | 1 + .../stack/scripts/prepare-test-artifacts.ts | 26 ++ .../stack/src/Network.integration.test.ts | 8 +- 17 files changed, 778 insertions(+), 240 deletions(-) create mode 100644 .github/workflows/fill-sticky-disks.yml create mode 100644 apps/cli/src/commands/db/schema/declarative/sync/sync.jobs.e2e.test.ts create mode 100644 apps/cli/tests/helpers/declarative-sync-e2e.ts create mode 100644 packages/stack/scripts/prepare-test-artifacts.ts diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index a05bc49616..8e503fe8f6 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -20,20 +20,134 @@ inputs: that never run Go. required: false default: "true" + tools: + description: >- + Space-separated mise tools to install instead of every tool in + mise.toml. Skipping the Go toolchain keeps the restored mise cache small. + required: false + default: "" + pnpm-store-cache: + description: >- + Whether to restore the pnpm store through actions/cache. Where the store + archive restores slower than the registry downloads it, disable this. + required: false + default: "true" + sticky-disks: + description: >- + Whether to mount dependency caches from Blacksmith sticky disks on + Blacksmith Linux runners. Disks are read-only unless commit-sticky-disks + is set. + required: false + default: "false" + commit-sticky-disks: + description: >- + Whether this job writes its sticky disks back. Only the fill workflow + sets it, so test jobs never race it with partial snapshots. + required: false + default: "false" + docker-images: + description: >- + Whether to keep Docker's image store on a Blacksmith sticky disk so + stack images are not pulled again on every job. Requires sticky-disks. + Callers must stop Docker in a final if: always() step so the disk + unmounts cleanly. + required: false + default: "false" + native-artifacts: + description: >- + Whether to keep the stack tests' native service artifacts on a + Blacksmith sticky disk. Requires sticky-disks. + required: false + default: "false" runs: using: "composite" steps: + # Sticky disks are shared by every run in the repository, so only the fill + # job on trusted events (pushes, schedules, manual runs) commits; pull request + # and merge queue code could otherwise plant dependencies or images that + # trusted runs later execute. + # Committing runs prune what the current lockfile no longer uses; bump the + # -vN key segment to reset a disk outright. + - name: Detect sticky disks + id: sticky + if: inputs.sticky-disks == 'true' + shell: bash + env: + EVENT_NAME: ${{ github.event_name }} + COMMIT: ${{ inputs.commit-sticky-disks }} + run: | + if [ "$RUNNER_OS" != Linux ]; then + exit 0 + fi + if [ -z "${BLACKSMITH_RUNNER_MESSAGE_WAIT_MS:-}" ]; then + echo "::notice::Sticky disks requested but this is not a Blacksmith runner; using actions/cache." + exit 0 + fi + echo "enabled=true" >> "$GITHUB_OUTPUT" + case "$COMMIT:$EVENT_NAME" in + true:push | true:schedule | true:workflow_dispatch) echo "commit=on-change" >> "$GITHUB_OUTPUT" ;; + *) echo "commit=false" >> "$GITHUB_OUTPUT" ;; + esac + + - name: Stop Docker before mounting its image disk + if: inputs.docker-images == 'true' && steps.sticky.outputs.enabled == 'true' + shell: bash + run: sudo systemctl stop docker.socket docker.service + + - name: Mount Docker image disk + if: inputs.docker-images == 'true' && steps.sticky.outputs.enabled == 'true' + uses: useblacksmith/stickydisk@b6cb58d1d31d6d32a25248fab12493d831f7bd48 # v1.7.2 + with: + key: ${{ github.repository }}-docker-images-v3-${{ runner.arch }} + path: /var/lib/docker + commit: ${{ steps.sticky.outputs.commit }} + + - name: Restart Docker on the image disk + if: inputs.docker-images == 'true' && steps.sticky.outputs.enabled == 'true' + shell: bash + run: | + sudo systemctl start docker.socket docker.service + docker info --format '{{.DockerRootDir}} {{.Driver}}' + docker image ls --format '{{.Repository}}:{{.Tag}}' | wc -l + + - name: Locate native artifact cache + id: native-artifacts + if: inputs.native-artifacts == 'true' && steps.sticky.outputs.enabled == 'true' + shell: bash + run: echo "path=${XDG_CACHE_HOME:-$HOME/.cache}/supabase-stack-artifacts" >> "$GITHUB_OUTPUT" + + - name: Mount native artifact disk + if: inputs.native-artifacts == 'true' && steps.sticky.outputs.enabled == 'true' + uses: useblacksmith/stickydisk@b6cb58d1d31d6d32a25248fab12493d831f7bd48 # v1.7.2 + with: + key: ${{ github.repository }}-stack-artifacts-v2-${{ runner.arch }} + path: ${{ steps.native-artifacts.outputs.path }} + commit: ${{ steps.sticky.outputs.commit }} + - name: Install toolchains uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4.3.0 with: version: 2026.9.0 + install_args: ${{ inputs.tools }} + cache_key: ${{ inputs.tools != '' && format('mise-{0}-{1}-{2}-{3}', runner.os, runner.arch, inputs.tools, hashFiles('mise.toml', 'mise.lock', '.node-version', '.bun-version', 'package.json')) || '' }} - name: Resolve pnpm store path if: inputs.dependency-cache == 'true' id: pnpm-store shell: bash - run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT" + run: | + echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT" + + # A sticky disk mounts a block-level snapshot in seconds, while + # actions/cache downloads and untars the whole store on every job. + - name: Mount pnpm store disk + if: inputs.dependency-cache == 'true' && steps.sticky.outputs.enabled == 'true' + uses: useblacksmith/stickydisk@b6cb58d1d31d6d32a25248fab12493d831f7bd48 # v1.7.2 + with: + key: ${{ github.repository }}-pnpm-store-v3-${{ runner.arch }} + path: ${{ steps.pnpm-store.outputs.path }} + commit: ${{ steps.sticky.outputs.commit }} # Cache only the content-addressable half of the store (package files plus # the SQLite index), never the global virtual store under links/. That @@ -48,7 +162,7 @@ runs: # so restores never match the earlier whole-store archives, which still # carry a links/ tree. - name: Configure pnpm dependency cache - if: inputs.dependency-cache == 'true' + if: inputs.dependency-cache == 'true' && inputs.pnpm-store-cache == 'true' && steps.sticky.outputs.enabled != 'true' uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: | @@ -83,8 +197,24 @@ runs: echo "mod=$(go env GOMODCACHE)" >> "$GITHUB_OUTPUT" echo "build=$(go env GOCACHE)" >> "$GITHUB_OUTPUT" + - name: Mount Go module disk + if: inputs.dependency-cache == 'true' && inputs.go-cache == 'true' && steps.sticky.outputs.enabled == 'true' + uses: useblacksmith/stickydisk@b6cb58d1d31d6d32a25248fab12493d831f7bd48 # v1.7.2 + with: + key: ${{ github.repository }}-go-mod-v3-${{ runner.arch }} + path: ${{ steps.go-cache.outputs.mod }} + commit: ${{ steps.sticky.outputs.commit }} + + - name: Mount Go build disk + if: inputs.dependency-cache == 'true' && inputs.go-cache == 'true' && steps.sticky.outputs.enabled == 'true' + uses: useblacksmith/stickydisk@b6cb58d1d31d6d32a25248fab12493d831f7bd48 # v1.7.2 + with: + key: ${{ github.repository }}-go-build-v3-${{ runner.arch }} + path: ${{ steps.go-cache.outputs.build }} + commit: ${{ steps.sticky.outputs.commit }} + - name: Configure Go dependency cache - if: inputs.dependency-cache == 'true' && inputs.go-cache == 'true' + if: inputs.dependency-cache == 'true' && inputs.go-cache == 'true' && steps.sticky.outputs.enabled != 'true' uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: | @@ -116,3 +246,10 @@ runs: echo "//firewall.depthfirst.com/npm/:_authToken=${DEPENDENCY_FIREWALL_TOKEN}" } > "$npmrc" NPM_CONFIG_USERCONFIG="$npmrc" pnpm install --frozen-lockfile + + # Pruning keeps only this checkout's packages, so only the default branch + # prunes; other branches would evict each other's dependencies. + - name: Prune pnpm store disk + if: inputs.dependency-cache == 'true' && steps.sticky.outputs.commit == 'on-change' && github.ref_name == (github.event.repository.default_branch || 'develop') + shell: bash + run: pnpm store prune diff --git a/.github/workflows/fill-sticky-disks.yml b/.github/workflows/fill-sticky-disks.yml new file mode 100644 index 0000000000..2440f388d1 --- /dev/null +++ b/.github/workflows/fill-sticky-disks.yml @@ -0,0 +1,89 @@ +name: Fill sticky disks + +# Test jobs mount the pnpm, Go and Docker image caches from Blacksmith sticky +# disks but pull request runs never commit them. This workflow is the trusted +# writer: it runs on protected branches, which are the only refs Blacksmith's +# sticky-disk branch protection lets commit. +on: + push: + branches: + - develop + - next + - main + schedule: + - cron: "23 4 * * *" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + # Every ref writes the same repository-wide disks, so fills run one at a time. + group: fill-sticky-disks + cancel-in-progress: false + +jobs: + fill: + name: Fill sticky disks + # Manual dispatches from other branches would commit unreviewed caches. + if: contains(fromJSON('["refs/heads/develop", "refs/heads/next", "refs/heads/main"]'), github.ref) + runs-on: blacksmith-8vcpu-ubuntu-2404 + timeout-minutes: 60 + steps: + - name: Checkout + uses: useblacksmith/checkout@6fd481652155169ed4d2f25ebaf97464f685175f # v1.0.0-beta + + - name: Setup + uses: ./.github/actions/setup + with: + dependency-firewall-token: ${{ secrets.DF_FIREWALL_TOKEN }} + sticky-disks: "true" + commit-sticky-disks: "true" + docker-images: "true" + native-artifacts: "true" + + # The nightly fill clears artifacts of older service versions. + - name: Download native stack artifacts + run: | + if [ "$GITHUB_EVENT_NAME" = schedule ]; then + rm -rf "${XDG_CACHE_HOME:-$HOME/.cache}/supabase-stack-artifacts/"* + fi + pnpm --filter @supabase/stack prepare:test-artifacts + + - name: Build CLI + run: pnpm exec turbo run supabase#build + + # Starting a default project pulls every image `supabase start` uses. + - name: Pull CLI images + env: + SUPABASE_GO_BINARY: ${{ github.workspace }}/apps/cli-go/supabase-go + SUPABASE_CLI_BINARY_OVERRIDE: ${{ github.workspace }}/apps/cli/dist/supabase + run: | + project="$(mktemp -d)" + cli="$GITHUB_WORKSPACE/apps/cli/dist/supabase.js" + cd "$project" + node "$cli" init + node "$cli" start + # The nightly fill drops images of older versions; the next step pulls the catalog back. + if [ "$GITHUB_EVENT_NAME" = schedule ]; then + docker image prune --all --force + fi + node "$cli" stop --no-backup + + - name: Pull stack package images + run: | + pnpm --silent --filter @supabase/stack prepare:test-artifacts --images | + xargs --max-procs 4 --max-args 1 docker pull --quiet + + # Also runs after a failed step so leftover stack containers and volumes never persist. + - name: Prune Docker before saving its image disk + if: always() + run: | + docker ps --all --quiet | xargs --no-run-if-empty docker rm --force + docker system prune --force + docker volume prune --all --force + + # The image disk only commits once nothing holds /var/lib/docker open. + - name: Stop Docker so its image disk can be saved + if: always() + run: sudo systemctl stop docker.socket docker.service diff --git a/.github/workflows/run-ci.yml b/.github/workflows/run-ci.yml index 61b40a0065..bf39a18cb6 100644 --- a/.github/workflows/run-ci.yml +++ b/.github/workflows/run-ci.yml @@ -1,9 +1,9 @@ name: run-ci -# Opt-in full develop CI for PRs that Test.yml does not already cover: stacked -# PRs (base is not develop, main, next or v*.x) and drafts. Ready PRs into -# those stay on the existing workflows so required check names and concurrency -# are unchanged. +# Opt-in full CI, Windows and macOS included, for drafts, stacked PRs, and +# ready PRs into develop or next. Ready PRs into main or v*.x already run +# everything through Test.yml. On ready PRs the Linux jobs run a second time +# alongside Test.yml's own run. # # Add the `run-ci` label to start the suite; remove it to cancel in-progress # runs via this workflow's concurrency group. Other labels do not retrigger. @@ -43,7 +43,7 @@ jobs: !startsWith(github.head_ref, 'release-notes/') && contains(github.event.pull_request.labels.*.name, 'run-ci') && (github.event.pull_request.draft || - (github.base_ref != 'develop' && github.base_ref != 'main' && github.base_ref != 'next' && + (github.base_ref != 'main' && !(startsWith(github.base_ref, 'v') && endsWith(github.base_ref, '.x')))) && ((github.event.action != 'labeled' && github.event.action != 'unlabeled') || @@ -51,5 +51,6 @@ jobs: uses: ./.github/workflows/test.yml with: force: true + full: true secrets: DF_FIREWALL_TOKEN: ${{ secrets.DF_FIREWALL_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b823b5702b..d88906c01b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,7 +5,9 @@ name: Test # # Default path: ready (non-draft) PRs targeting `develop`, `next`, `main` or a # `v*.x` maintenance branch, plus the merge queue for `develop` and `next`. -# `run-ci.yml` calls this workflow for drafts and stacked PRs. +# `run-ci.yml` calls this workflow for drafts and stacked PRs. Windows and macOS +# jobs run on PRs into develop or next only for platform-sensitive paths, and +# always in the merge queue, on PRs into main or v*.x, and under `run-ci`. on: pull_request: types: @@ -31,6 +33,10 @@ on: description: Run the suite even when the PR is a draft (used by run-ci.yml) type: boolean default: false + full: + description: Run the Windows and macOS jobs regardless of the changed paths (used by run-ci.yml) + type: boolean + default: false secrets: DF_FIREWALL_TOKEN: required: true @@ -55,6 +61,7 @@ jobs: github.event.pull_request.draft == false) name: Check code quality runs-on: blacksmith-8vcpu-ubuntu-2404 + timeout-minutes: 15 steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -65,6 +72,7 @@ jobs: uses: ./.github/actions/setup with: dependency-firewall-token: ${{ secrets.DF_FIREWALL_TOKEN }} + sticky-disks: "true" - name: Unlock keyring (for cli-go keyring tests) uses: t1m0thyj/unlock-keyring@cbcf205c879ebd86add70bab3a6abfcce59a5cae # v1.2.0 @@ -90,6 +98,7 @@ jobs: github.event.pull_request.draft == false) name: Run unit tests runs-on: blacksmith-4vcpu-ubuntu-2404 + timeout-minutes: 15 steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -100,6 +109,7 @@ jobs: uses: ./.github/actions/setup with: dependency-firewall-token: ${{ secrets.DF_FIREWALL_TOKEN }} + sticky-disks: "true" go-cache: "false" - name: Run unit tests @@ -113,6 +123,7 @@ jobs: github.event.pull_request.draft == false) name: Run integration tests (${{ matrix.name }}) runs-on: blacksmith-8vcpu-ubuntu-2404 + timeout-minutes: 15 strategy: fail-fast: false matrix: @@ -133,7 +144,9 @@ jobs: uses: ./.github/actions/setup with: dependency-firewall-token: ${{ secrets.DF_FIREWALL_TOKEN }} + sticky-disks: "true" go-cache: "false" + docker-images: "true" - name: Run integration tests run: >- @@ -151,14 +164,68 @@ jobs: src/port-reservation.e2e.test.ts --passWithNoTests=false - test-stack-ports: + # Releases /var/lib/docker so the image disk unmounts cleanly. + - name: Stop Docker + if: always() + run: sudo systemctl stop docker.socket docker.service + + platform-changes: if: | !startsWith(github.head_ref, 'release-notes/') && (github.event_name == 'merge_group' || inputs.force || github.event.pull_request.draft == false) + name: Detect platform-sensitive changes + runs-on: ubuntu-latest + timeout-minutes: 5 + outputs: + run: ${{ steps.detect.outputs.run }} + steps: + # The merge queue and PRs into main or v*.x run every platform; only PRs + # into develop or next defer Windows and macOS to the merge queue. + - name: Detect + id: detect + env: + GH_TOKEN: ${{ github.token }} + EVENT: ${{ github.event_name }} + BASE_REF: ${{ github.base_ref }} + FULL: ${{ inputs.full }} + REPO: ${{ github.repository }} + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + PATTERN: >- + ^(pnpm-lock\.yaml|pnpm-workspace\.yaml|package\.json|mise\.(toml|lock)|\.node-version|\.bun-version|turbo\.json|\.github/workflows/test\.yml|\.github/actions/ + |packages/stack/(package\.json|vitest[^/]*|tests/|src/(host|namespace|identity|storage|internal)/ + |src/(Artifacts|HostProcess|Ports|Network|Sweep|Owner|Proxy|HttpProxy|StackNamespace|effect)[.]|src/whole-stack\.native) + |apps/cli/src/commands/(db/reset|experimental/stack/start)/) + run: | + if [ "$EVENT" != "pull_request" ] || [ "$FULL" = "true" ] || [ "$BASE_REF" = "main" ] || [[ "$BASE_REF" == v*.x ]]; then + echo "run=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + pattern=$(tr -d ' \n' <<<"$PATTERN") + # The compare API lists at most 300 files; a larger diff runs every platform. + files=$(gh api "repos/$REPO/compare/$BASE_SHA...$HEAD_SHA" --jq 'if (.files | length) >= 300 then "package.json" else .files[].filename end') + matches=$(grep -E "$pattern" <<<"$files" || true) + if [ -n "$matches" ]; then + printf 'Platform-sensitive changes:\n%s\n' "$matches" + echo "run=true" >> "$GITHUB_OUTPUT" + else + echo "No platform-sensitive changes; Windows and macOS run in the merge queue. Add the run-ci label to run them here." + echo "run=false" >> "$GITHUB_OUTPUT" + fi + + test-stack-ports: + if: | + !startsWith(github.head_ref, 'release-notes/') && + (github.event_name == 'merge_group' || + inputs.force || + github.event.pull_request.draft == false) && + needs.platform-changes.outputs.run == 'true' + needs: platform-changes name: Run stack port tests (${{ matrix.os }}) runs-on: ${{ matrix.os }} + timeout-minutes: 15 strategy: fail-fast: false matrix: @@ -176,6 +243,9 @@ jobs: with: dependency-firewall-token: ${{ secrets.DF_FIREWALL_TOKEN }} go-cache: "false" + tools: node bun pnpm + # Windows unpacks the store archive slower than it downloads the packages. + pnpm-store-cache: ${{ matrix.os != 'windows-latest' }} - name: Run stack integration tests working-directory: packages/stack @@ -216,16 +286,26 @@ jobs: !startsWith(github.head_ref, 'release-notes/') && (github.event_name == 'merge_group' || inputs.force || - github.event.pull_request.draft == false) - name: Run stack native scenarios (${{ matrix.os }}) + github.event.pull_request.draft == false) && + needs.platform-changes.outputs.run == 'true' + needs: platform-changes + name: Run stack native scenarios (${{ matrix.os }}, ${{ matrix.suite }}) runs-on: ${{ matrix.os }} - timeout-minutes: 45 + timeout-minutes: 15 strategy: fail-fast: false matrix: # Native artifacts exist for macOS and Linux only, and macOS runners have # no Docker; Linux coverage for both backends comes from test-stack-e2e. os: [blacksmith-6vcpu-macos-latest] + suite: [lifecycle, confinement, idle, isolation] + # Concurrent scenarios slow each other on macOS runners, so the + # heaviest ones run alone. + include: + - { suite: lifecycle, file: lifecycle, pattern: "^(?!.*writes stay confined)" } + - { suite: confinement, file: lifecycle, pattern: "writes stay confined" } + - { suite: idle, file: idle-parallel, pattern: "^(?!.*parallel stack isolation)" } + - { suite: isolation, file: idle-parallel, pattern: "parallel stack isolation" } steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -238,9 +318,21 @@ jobs: dependency-firewall-token: ${{ secrets.DF_FIREWALL_TOKEN }} go-cache: "false" + # Keyed per suite so each job saves the artifacts it actually prepared. + - name: Cache native stack artifacts + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/.cache/supabase-stack-artifacts + key: stack-artifacts-${{ runner.os }}-${{ runner.arch }}-${{ matrix.suite }}-${{ hashFiles('packages/stack/src/Artifacts.ts') }} + - name: Run stack native whole-stack scenarios working-directory: packages/stack - run: pnpm test:e2e:native-scenarios --passWithNoTests=false + run: >- + pnpm test:e2e:run + src/whole-stack.native.${{ matrix.file }}.e2e.test.ts + --testNamePattern='${{ matrix.pattern }}' + --sequence.concurrent + --passWithNoTests=false test-summary: if: | @@ -250,11 +342,16 @@ jobs: inputs.force || github.event.pull_request.draft == false) name: Run unit and integration tests - needs: [test-unit, test-integration, test-stack-ports] + needs: [platform-changes, test-unit, test-integration, test-stack-ports] runs-on: ubuntu-latest + timeout-minutes: 5 steps: - name: Verify unit and integration tests succeeded run: | + if [ "${{ needs.platform-changes.result }}" != "success" ]; then + echo "::error ::Platform change detection did not succeed: ${{ needs.platform-changes.result }}" + exit 1 + fi if [ "${{ needs.test-unit.result }}" = "failure" ] || [ "${{ needs.test-unit.result }}" = "cancelled" ] || \ [ "${{ needs.test-integration.result }}" = "failure" ] || [ "${{ needs.test-integration.result }}" = "cancelled" ]; then echo "::error ::Unit or integration tests failed: unit=${{ needs.test-unit.result }}, integration=${{ needs.test-integration.result }}, stack-ports=${{ needs.test-stack-ports.result }}" @@ -274,6 +371,7 @@ jobs: github.event.pull_request.draft == false) name: Run CLI end-to-end tests (${{ matrix.name }}) runs-on: blacksmith-8vcpu-ubuntu-2404 + timeout-minutes: 15 strategy: fail-fast: false matrix: @@ -294,6 +392,8 @@ jobs: uses: ./.github/actions/setup with: dependency-firewall-token: ${{ secrets.DF_FIREWALL_TOKEN }} + sticky-disks: "true" + docker-images: "true" - name: Cache Go CLI binary id: cache-go-binary @@ -320,6 +420,7 @@ jobs: run: pnpm exec turbo run test:e2e:run --only --filter=supabase -- --shard=${{ matrix.shard }}/${{ matrix.shards }} env: SUPABASE_GO_BINARY: ${{ github.workspace }}/apps/cli-go/supabase-go + SUPABASE_E2E_WORKERS: "4" - name: Run cli-e2e end-to-end tests if: matrix.suite == 'cli-e2e' @@ -338,6 +439,11 @@ jobs: retention-days: 7 if-no-files-found: warn + # Releases /var/lib/docker so the image disk unmounts cleanly. + - name: Stop Docker + if: always() + run: sudo systemctl stop docker.socket docker.service + test-stack-e2e: if: | !startsWith(github.head_ref, 'release-notes/') && @@ -346,11 +452,14 @@ jobs: github.event.pull_request.draft == false) name: Run stack end-to-end tests (${{ matrix.runtime }}, ${{ matrix.suite }}) runs-on: blacksmith-8vcpu-ubuntu-2404 + timeout-minutes: 15 strategy: fail-fast: false matrix: runtime: [native, docker, podman] - suite: [lifecycle, idle-parallel] + # Confinement watches the whole temp tree, so it gets a job of its own + # instead of sharing one with concurrently running scenarios. + suite: [lifecycle, idle-parallel, confinement] env: SUPABASE_STACK_TEST_ENGINE: ${{ matrix.runtime == 'podman' && 'podman' || 'docker' }} steps: @@ -361,7 +470,10 @@ jobs: uses: ./.github/actions/setup with: dependency-firewall-token: ${{ secrets.DF_FIREWALL_TOKEN }} + sticky-disks: "true" go-cache: "false" + docker-images: ${{ matrix.runtime == 'docker' }} + native-artifacts: ${{ matrix.runtime == 'native' }} - name: Setup Podman if: matrix.runtime == 'podman' @@ -370,7 +482,9 @@ jobs: - name: Run stack end-to-end tests run: >- pnpm --filter @supabase/stack test:e2e:run - src/whole-stack.${{ matrix.runtime }}.${{ matrix.suite }}.e2e.test.ts + src/whole-stack.${{ matrix.runtime }}.${{ matrix.suite == 'idle-parallel' && 'idle-parallel' || 'lifecycle' }}.e2e.test.ts + --testNamePattern='${{ matrix.suite == 'confinement' && 'writes stay confined' || matrix.suite == 'lifecycle' && '^(?!.*writes stay confined)' || '.' }}' + --sequence.concurrent --passWithNoTests=false - name: Run public stack end-to-end tests @@ -380,6 +494,11 @@ jobs: src/public.e2e.test.ts --passWithNoTests=false + # Releases /var/lib/docker so the image disk unmounts cleanly. + - name: Stop Docker + if: always() && matrix.runtime == 'docker' + run: sudo systemctl stop docker.socket docker.service + test-stack-podman-integration: if: | !startsWith(github.head_ref, 'release-notes/') && @@ -388,6 +507,7 @@ jobs: github.event.pull_request.draft == false) name: Run stack Podman integration tests runs-on: blacksmith-8vcpu-ubuntu-2404 + timeout-minutes: 15 env: SUPABASE_STACK_TEST_ENGINE: podman steps: @@ -398,6 +518,7 @@ jobs: uses: ./.github/actions/setup with: dependency-firewall-token: ${{ secrets.DF_FIREWALL_TOKEN }} + sticky-disks: "true" go-cache: "false" - name: Setup Podman @@ -437,11 +558,23 @@ jobs: inputs.force || github.event.pull_request.draft == false) name: Run end-to-end tests - needs: [test-e2e, test-stack-e2e, test-stack-native-scenarios, test-stack-podman-integration] + needs: + [ + platform-changes, + test-e2e, + test-stack-e2e, + test-stack-native-scenarios, + test-stack-podman-integration, + ] runs-on: ubuntu-latest + timeout-minutes: 5 steps: - name: Verify all shards succeeded run: | + if [ "${{ needs.platform-changes.result }}" != "success" ]; then + echo "::error ::Platform change detection did not succeed: ${{ needs.platform-changes.result }}" + exit 1 + fi if [ "${{ needs.test-e2e.result }}" = "failure" ] || [ "${{ needs.test-e2e.result }}" = "cancelled" ] || \ [ "${{ needs.test-stack-e2e.result }}" = "failure" ] || [ "${{ needs.test-stack-e2e.result }}" = "cancelled" ] || \ [ "${{ needs.test-stack-native-scenarios.result }}" = "failure" ] || [ "${{ needs.test-stack-native-scenarios.result }}" = "cancelled" ] || \ diff --git a/apps/cli/package.json b/apps/cli/package.json index ded242f8d8..d9360d9de9 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -37,7 +37,7 @@ "test:integration": "pnpm exec turbo run supabase#test:integration:run --", "test:integration:run": "bun --bun vitest run --project integration --coverage.reportsDirectory=coverage/integration", "test:e2e": "pnpm exec turbo run supabase#build && pnpm exec turbo run supabase#test:e2e:run --only --", - "test:e2e:run": "bun --bun vitest run --project e2e", + "test:e2e:run": "bun --bun vitest run --project e2e --project e2e-exclusive", "test:live": "bun --bun vitest run --project live", "test:smoke": "bun run tests/smoke-test.ts", "types:check": "tsc --noEmit -p tsconfig.types.json" diff --git a/apps/cli/scripts/refresh-e2e-timings.ts b/apps/cli/scripts/refresh-e2e-timings.ts index dd5396410b..a52667501c 100644 --- a/apps/cli/scripts/refresh-e2e-timings.ts +++ b/apps/cli/scripts/refresh-e2e-timings.ts @@ -26,7 +26,7 @@ export interface MergeOptions { readonly warn?: (message: string) => void; } -const PROJECT_PREFIX = "e2e:"; +const PROJECT_PREFIXES = ["e2e:", "e2e-exclusive:"]; function major(version: string): string { return version.split(".")[0] ?? version; @@ -48,10 +48,11 @@ export function mergeTimings( ); } for (const [key, result] of cache.results) { - if (!key.startsWith(PROJECT_PREFIX)) { + const prefix = PROJECT_PREFIXES.find((candidate) => key.startsWith(candidate)); + if (prefix === undefined) { continue; } - const path = key.slice(PROJECT_PREFIX.length); + const path = key.slice(prefix.length); if (result.failed) { warn(`skipping failed ${path}`); continue; diff --git a/apps/cli/scripts/refresh-e2e-timings.unit.test.ts b/apps/cli/scripts/refresh-e2e-timings.unit.test.ts index 8571e883f7..b7c11ffae2 100644 --- a/apps/cli/scripts/refresh-e2e-timings.unit.test.ts +++ b/apps/cli/scripts/refresh-e2e-timings.unit.test.ts @@ -13,6 +13,7 @@ describe("mergeTimings", () => { [ cache( ["e2e:src/start.e2e.test.ts", { duration: 1200, failed: false }], + ["e2e-exclusive:src/health.e2e.test.ts", { duration: 900, failed: false }], ["unit:src/start.unit.test.ts", { duration: 5, failed: false }], [":src/start.integration.test.ts", { duration: 40, failed: false }], ), @@ -20,7 +21,10 @@ describe("mergeTimings", () => { { vitestVersion }, ); - expect(timings).toEqual({ "src/start.e2e.test.ts": 1200 }); + expect(timings).toEqual({ + "src/health.e2e.test.ts": 900, + "src/start.e2e.test.ts": 1200, + }); }); test("skips failed entries and reports each one", () => { diff --git a/apps/cli/src/commands/db/schema/declarative/sync/sync.e2e.test.ts b/apps/cli/src/commands/db/schema/declarative/sync/sync.e2e.test.ts index d630f7428f..b54627296f 100644 --- a/apps/cli/src/commands/db/schema/declarative/sync/sync.e2e.test.ts +++ b/apps/cli/src/commands/db/schema/declarative/sync/sync.e2e.test.ts @@ -1,142 +1,16 @@ import { BunServices } from "@effect/platform-bun"; import { describe, expect, it } from "@effect/vitest"; -import { Data, Effect, FileSystem, Path } from "effect"; +import { Effect, FileSystem, Path } from "effect"; +import { requireCliSuccess, runSupabaseEffect } from "../../../../../../tests/helpers/cli.ts"; import { - makeTempCliStackProject, - overrideStackPorts, - requireCliSuccess, - runSupabaseEffect, -} from "../../../../../../tests/helpers/cli.ts"; - -const CLI_COMMAND_TIMEOUT_MS = 60_000; -const STACK_START_TIMEOUT_MS = 280_000; -const CLEANUP_TIMEOUT_MS = 120_000; -const LIFECYCLE_MARGIN_MS = 30_000; -const CLEANUP_HOOK_TIMEOUT_MS = CLEANUP_TIMEOUT_MS + LIFECYCLE_MARGIN_MS; -const SCENARIO_COMMAND_TIMEOUT_MS = 280_000; -const BEFORE_ALL_TIMEOUT_MS = CLI_COMMAND_TIMEOUT_MS + STACK_START_TIMEOUT_MS + LIFECYCLE_MARGIN_MS; -const SCENARIO_TIMEOUT_MS = 900_000; -const TEST_TIMEOUT_MS = BEFORE_ALL_TIMEOUT_MS + SCENARIO_TIMEOUT_MS + CLEANUP_HOOK_TIMEOUT_MS; - -class SyncE2eSetupError extends Data.TaggedError("SyncE2eSetupError")<{ - readonly message: string; - readonly cause?: unknown; -}> {} - -const initialDesiredSchema = `create type public.account_state as enum ('pending', 'active'); - -create table public.disposable_note ( - id bigint generated by default as identity primary key, - body text not null -); - -create view public.auth_user_emails as -select id, email -from auth.users; -`; - -function commandFailure(result: { stdout: string; stderr: string }): string { - return `stdout:\n${result.stdout}\nstderr:\n${result.stderr}`; -} - -const migrationFiles = (projectDir: string) => - Effect.gen(function* () { - const fs = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const migrationsDir = path.join(projectDir, "supabase", "migrations"); - return (yield* fs.exists(migrationsDir)) - ? (yield* fs.readDirectory(migrationsDir)).filter((file) => file.endsWith(".sql")).sort() - : []; - }); - -const withTempStackProject = ( - use: (project: Awaited>) => Effect.Effect, -): Effect.Effect => - Effect.acquireUseRelease( - Effect.tryPromise({ - try: () => makeTempCliStackProject("sb-pgdelta-next-e2e-"), - catch: (cause) => - new SyncE2eSetupError({ message: "temp stack project setup failed", cause }), - }), - use, - (project) => - Effect.tryPromise({ - try: () => project.cleanup(), - catch: (cause) => - new SyncE2eSetupError({ message: "temp stack project cleanup failed", cause }), - }).pipe(Effect.ignore), - ); - -const setUpProject = (projectDir: string) => - Effect.gen(function* () { - const fs = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const init = yield* runSupabaseEffect(["init"], { - cwd: projectDir, - exitTimeoutMs: CLI_COMMAND_TIMEOUT_MS, - }); - requireCliSuccess(init, "init setup"); - yield* Effect.tryPromise({ - try: () => overrideStackPorts(projectDir), - catch: (cause) => - new SyncE2eSetupError({ message: `failed to override ports in ${projectDir}`, cause }), - }); - - const configPath = path.join(projectDir, "supabase", "config.toml"); - const config = yield* fs.readFileString(configPath); - if (!config.includes("[experimental.pgdelta]\nenabled = true")) { - return yield* new SyncE2eSetupError({ - message: "init setup did not enable experimental pg-delta in config.toml", - }); - } - yield* fs.writeFileString( - configPath, - config.replace( - '# declarative_schema_path = "./schemas"', - 'declarative_schema_path = "./schemas"', - ), - ); - - const schemasDir = path.join(projectDir, "supabase", "schemas"); - yield* fs.makeDirectory(schemasDir, { recursive: true }); - yield* fs.writeFileString(path.join(schemasDir, "public.sql"), initialDesiredSchema); - const extensionsDir = path.join(schemasDir, "cluster", "extensions"); - yield* fs.makeDirectory(extensionsDir, { recursive: true }); - for (const extension of ["pg_net", "pgcrypto", "uuid-ossp"]) { - yield* fs.writeFileString( - path.join(extensionsDir, `${extension}.sql`), - `CREATE EXTENSION IF NOT EXISTS "${extension}" WITH SCHEMA "extensions";\n`, - ); - } - // Both install into a fixed schema, so no `WITH SCHEMA` clause. - for (const extension of ["pg_cron", "pgmq"]) { - yield* fs.writeFileString( - path.join(extensionsDir, `${extension}.sql`), - `CREATE EXTENSION IF NOT EXISTS "${extension}";\n`, - ); - } - - const start = yield* runSupabaseEffect( - [ - "start", - "--exclude", - "studio", - "--exclude", - "logflare", - "--exclude", - "vector", - "--exclude", - "gotrue", - "--exclude", - "realtime", - "--exclude", - "storage-api", - ], - { cwd: projectDir, exitTimeoutMs: STACK_START_TIMEOUT_MS }, - ); - requireCliSuccess(start, "start setup"); - }); + commandFailure, + migrationFiles, + scenarioCommandOptions, + setUpProject, + TEST_TIMEOUT_MS, + withTempStackProject, +} from "../../../../../../tests/helpers/declarative-sync-e2e.ts"; describe("db schema declarative sync (e2e)", () => { it.live( @@ -160,10 +34,7 @@ describe("db schema declarative sync (e2e)", () => { "initial_declarative", "--experimental", ], - { - cwd: projectDir, - exitTimeoutMs: SCENARIO_COMMAND_TIMEOUT_MS, - }, + scenarioCommandOptions(projectDir), ); expect(sync.exitCode, commandFailure(sync)).toBe(0); @@ -180,84 +51,18 @@ describe("db schema declarative sync (e2e)", () => { /CREATE\s+(?:SCHEMA|TABLE)\s+(?:IF\s+NOT\s+EXISTS\s+)?["']?(?:auth|storage|realtime)["']?/iu, ); - const reset = yield* runSupabaseEffect(["db", "reset", "--local", "--no-seed"], { - cwd: projectDir, - exitTimeoutMs: SCENARIO_COMMAND_TIMEOUT_MS, - }); + const reset = yield* runSupabaseEffect( + ["db", "reset", "--local", "--no-seed"], + scenarioCommandOptions(projectDir), + ); requireCliSuccess(reset, "db reset setup"); const converged = yield* runSupabaseEffect( ["db", "schema", "declarative", "sync", "--no-apply", "--experimental"], - { - cwd: projectDir, - exitTimeoutMs: SCENARIO_COMMAND_TIMEOUT_MS, - }, + scenarioCommandOptions(projectDir), ); expect(converged.exitCode, commandFailure(converged)).toBe(0); expect(`${converged.stdout}${converged.stderr}`).toContain("No schema changes found"); - - // Extension-managed objects on the converged tree. The stack teardown after every test - // means this continues in the same test rather than a second one. - const jobsPath = path.join(projectDir, "supabase", "schemas", "jobs.sql"); - const runSync = (name: string) => - runSupabaseEffect( - [ - "db", - "schema", - "declarative", - "sync", - "--no-apply", - "--name", - name, - "--experimental", - ], - { - cwd: projectDir, - exitTimeoutMs: SCENARIO_COMMAND_TIMEOUT_MS, - }, - ); - // A next-engine plan may span several ordered migration files; read every - // file a sync added rather than only the last one. - const syncAndReadSql = (name: string) => - Effect.gen(function* () { - const before = new Set(yield* migrationFiles(projectDir)); - const result = yield* runSync(name); - expect(result.exitCode, commandFailure(result)).toBe(0); - const added = (yield* migrationFiles(projectDir)).filter((file) => !before.has(file)); - expect(added.length, "sync did not write a migration").toBeGreaterThan(0); - return { - result, - sql: (yield* Effect.forEach(added, (file) => - fs.readFileString(path.join(projectDir, "supabase", "migrations", file)), - )).join("\n"), - }; - }); - - yield* fs.writeFileString( - jobsPath, - [ - "select cron.schedule('nightly_cleanup', '0 3 * * *', $$delete from public.disposable_note$$);", - "select pgmq.create('emails');", - "", - ].join("\n"), - ); - // pg-delta renders a job through `cron.schedule_in_database(...)`. - const added = yield* syncAndReadSql("add_jobs"); - expect(added.sql).toMatch(/cron\.schedule(?:_in_database)?\('nightly_cleanup'/); - expect(added.sql).toContain("pgmq.create('emails')"); - - // Rename the job and drop the queue — this must not be refused as a legacy export. - yield* fs.writeFileString( - jobsPath, - "select cron.schedule('weekly_cleanup', '0 3 * * 0', $$delete from public.disposable_note$$);\n", - ); - const removed = yield* syncAndReadSql("rename_job_drop_queue"); - expect(`${removed.result.stdout}${removed.result.stderr}`).not.toContain( - "legacy pg-delta export", - ); - expect(removed.sql).toContain("cron.unschedule('nightly_cleanup')"); - expect(removed.sql).toMatch(/cron\.schedule(?:_in_database)?\('weekly_cleanup'/); - expect(removed.sql).toContain("pgmq.drop_queue('emails')"); }), ).pipe(Effect.provide(BunServices.layer)), TEST_TIMEOUT_MS, diff --git a/apps/cli/src/commands/db/schema/declarative/sync/sync.jobs.e2e.test.ts b/apps/cli/src/commands/db/schema/declarative/sync/sync.jobs.e2e.test.ts new file mode 100644 index 0000000000..59e26b4101 --- /dev/null +++ b/apps/cli/src/commands/db/schema/declarative/sync/sync.jobs.e2e.test.ts @@ -0,0 +1,89 @@ +import { BunServices } from "@effect/platform-bun"; +import { describe, expect, it } from "@effect/vitest"; +import { Effect, FileSystem, Path } from "effect"; + +import { runSupabaseEffect } from "../../../../../../tests/helpers/cli.ts"; +import { + commandFailure, + convergeInitialSchema, + migrationFiles, + scenarioCommandOptions, + setUpProject, + TEST_TIMEOUT_MS, + withTempStackProject, +} from "../../../../../../tests/helpers/declarative-sync-e2e.ts"; + +describe("db schema declarative sync extension jobs (e2e)", () => { + it.live( + "adds, renames, and drops extension-managed jobs and queues", + () => + withTempStackProject((project) => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const projectDir = project.dir; + yield* setUpProject(projectDir); + yield* convergeInitialSchema(projectDir); + + const jobsPath = path.join(projectDir, "supabase", "schemas", "jobs.sql"); + const runSync = (name: string) => + runSupabaseEffect( + [ + "db", + "schema", + "declarative", + "sync", + "--no-apply", + "--name", + name, + "--experimental", + ], + scenarioCommandOptions(projectDir), + ); + // A next-engine plan may span several ordered migration files; read every + // file a sync added rather than only the last one. + const syncAndReadSql = (name: string) => + Effect.gen(function* () { + const before = new Set(yield* migrationFiles(projectDir)); + const result = yield* runSync(name); + expect(result.exitCode, commandFailure(result)).toBe(0); + const added = (yield* migrationFiles(projectDir)).filter((file) => !before.has(file)); + expect(added.length, "sync did not write a migration").toBeGreaterThan(0); + return { + result, + sql: (yield* Effect.forEach(added, (file) => + fs.readFileString(path.join(projectDir, "supabase", "migrations", file)), + )).join("\n"), + }; + }); + + yield* fs.writeFileString( + jobsPath, + [ + "select cron.schedule('nightly_cleanup', '0 3 * * *', $$delete from public.disposable_note$$);", + "select pgmq.create('emails');", + "", + ].join("\n"), + ); + // pg-delta renders a job through `cron.schedule_in_database(...)`. + const added = yield* syncAndReadSql("add_jobs"); + expect(added.sql).toMatch(/cron\.schedule(?:_in_database)?\('nightly_cleanup'/); + expect(added.sql).toContain("pgmq.create('emails')"); + + // Rename the job and drop the queue — this must not be refused as a legacy export. + yield* fs.writeFileString( + jobsPath, + "select cron.schedule('weekly_cleanup', '0 3 * * 0', $$delete from public.disposable_note$$);\n", + ); + const removed = yield* syncAndReadSql("rename_job_drop_queue"); + expect(`${removed.result.stdout}${removed.result.stderr}`).not.toContain( + "legacy pg-delta export", + ); + expect(removed.sql).toContain("cron.unschedule('nightly_cleanup')"); + expect(removed.sql).toMatch(/cron\.schedule(?:_in_database)?\('weekly_cleanup'/); + expect(removed.sql).toContain("pgmq.drop_queue('emails')"); + }), + ).pipe(Effect.provide(BunServices.layer)), + TEST_TIMEOUT_MS, + ); +}); diff --git a/apps/cli/tests/e2e-sequencer.ts b/apps/cli/tests/e2e-sequencer.ts index 576b9e16bd..0ce50c1f43 100644 --- a/apps/cli/tests/e2e-sequencer.ts +++ b/apps/cli/tests/e2e-sequencer.ts @@ -4,6 +4,7 @@ import { BaseSequencer, type TestSpecification } from "vitest/node"; /** Bun `--timings` file of measured e2e file durations, refreshed from CI artifacts. */ const TIMINGS_FILE = "tests/e2e-timings.json"; +const E2E_PROJECTS = new Set(["e2e", "e2e-exclusive"]); /** Duration assumed for every file while no timing has been measured yet. */ export const DEFAULT_DURATION_MS = 30_000; @@ -92,13 +93,13 @@ export function readTimings(root: string): Readonly> | un } /** - * Shards the e2e project by measured duration. Other projects, and the e2e project while + * Shards the e2e projects by measured duration. Other projects, and the e2e projects while * the timings file is absent, keep Vitest's hash split. */ export class E2eSequencer extends BaseSequencer { override async shard(files: TestSpecification[]): Promise { const { root, shard } = this.ctx.config; - const timings = files.every((spec) => spec.project.name === "e2e") + const timings = files.every((spec) => E2E_PROJECTS.has(spec.project.name)) ? readTimings(root) : undefined; if (shard === undefined || timings === undefined) { diff --git a/apps/cli/tests/e2e-timings.json b/apps/cli/tests/e2e-timings.json index 0594a390ef..620d433cef 100644 --- a/apps/cli/tests/e2e-timings.json +++ b/apps/cli/tests/e2e-timings.json @@ -17,7 +17,8 @@ "src/commands/db/push/push.e2e.test.ts": 543, "src/commands/db/reset/reset.e2e.test.ts": 229, "src/commands/db/reset/reset.stack.e2e.test.ts": 9406, - "src/commands/db/schema/declarative/sync/sync.e2e.test.ts": 134676, + "src/commands/db/schema/declarative/sync/sync.e2e.test.ts": 57895, + "src/commands/db/schema/declarative/sync/sync.jobs.e2e.test.ts": 66192, "src/commands/db/start/start.e2e.test.ts": 17188, "src/commands/db/start/start.roles.e2e.test.ts": 13597, "src/commands/encryption/encryption.e2e.test.ts": 412, diff --git a/apps/cli/tests/helpers/cli.ts b/apps/cli/tests/helpers/cli.ts index 9b2d6b9931..e76a2e37df 100644 --- a/apps/cli/tests/helpers/cli.ts +++ b/apps/cli/tests/helpers/cli.ts @@ -173,7 +173,23 @@ export const withTempHome = ( }), ); -function pickFreePort(): Promise { +/** + * Concurrent e2e workers each keep only ports in their own slot, so a port one worker picked and + * released before Docker binds it is never handed to another worker. Slots skip the low bit + * because Linux hands out mostly odd ports for `listen(0)`. + */ +const e2eWorkers = Number(process.env.SUPABASE_E2E_WORKERS || "1"); +const e2eWorkerSlot = Number(process.env.VITEST_POOL_ID || "1") % e2eWorkers; + +async function pickFreePort(): Promise { + for (let attempt = 0; attempt < 1000; attempt++) { + const port = await pickReleasedPort(); + if (Math.floor(port / 2) % e2eWorkers === e2eWorkerSlot) return port; + } + throw new Error(`No free port for e2e worker slot ${e2eWorkerSlot} of ${e2eWorkers}`); +} + +function pickReleasedPort(): Promise { return new Promise((resolve, reject) => { const server = createServer(); server.listen(0, "127.0.0.1", () => { @@ -276,6 +292,14 @@ export async function makeTempCliStackProject( return stackProject; } +/** Concurrent e2e files each run a stack, and Docker names its containers after the project id. */ +const projectIdFor = (dir: string) => + `e2e-${path + .basename(dir) + .toLowerCase() + .replace(/[^a-z0-9]/g, "") + .slice(-8)}`; + export async function makeTempStackProject(prefix = "supabase-stack-e2e-") { const project = await makeTempProject(prefix); const ports = { @@ -304,7 +328,7 @@ export async function makeTempStackProject(prefix = "supabase-stack-e2e-") { await writeFile( path.join(supabaseDir, "config.toml"), [ - 'project_id = "e2e"', + `project_id = "${projectIdFor(project.dir)}"`, "", "[api]", `port = ${ports.apiPort}`, diff --git a/apps/cli/tests/helpers/declarative-sync-e2e.ts b/apps/cli/tests/helpers/declarative-sync-e2e.ts new file mode 100644 index 0000000000..bc09358ff2 --- /dev/null +++ b/apps/cli/tests/helpers/declarative-sync-e2e.ts @@ -0,0 +1,181 @@ +import { Data, Effect, FileSystem, Path } from "effect"; + +import { + makeTempCliStackProject, + overrideStackPorts, + requireCliSuccess, + runSupabaseEffect, +} from "./cli.ts"; + +const CLI_COMMAND_TIMEOUT_MS = 60_000; +const STACK_START_TIMEOUT_MS = 280_000; +const CLEANUP_TIMEOUT_MS = 120_000; +const LIFECYCLE_MARGIN_MS = 30_000; +const CLEANUP_HOOK_TIMEOUT_MS = CLEANUP_TIMEOUT_MS + LIFECYCLE_MARGIN_MS; +const SCENARIO_COMMAND_TIMEOUT_MS = 280_000; +const BEFORE_ALL_TIMEOUT_MS = CLI_COMMAND_TIMEOUT_MS + STACK_START_TIMEOUT_MS + LIFECYCLE_MARGIN_MS; +const SCENARIO_TIMEOUT_MS = 900_000; +export const TEST_TIMEOUT_MS = + BEFORE_ALL_TIMEOUT_MS + SCENARIO_TIMEOUT_MS + CLEANUP_HOOK_TIMEOUT_MS; + +class SyncE2eSetupError extends Data.TaggedError("SyncE2eSetupError")<{ + readonly message: string; + readonly cause?: unknown; +}> {} + +const initialDesiredSchema = `create type public.account_state as enum ('pending', 'active'); + +create table public.disposable_note ( + id bigint generated by default as identity primary key, + body text not null +); + +create view public.auth_user_emails as +select id, email +from auth.users; +`; + +export function commandFailure(result: { stdout: string; stderr: string }): string { + return `stdout:\n${result.stdout}\nstderr:\n${result.stderr}`; +} + +export const migrationFiles = (projectDir: string) => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const migrationsDir = path.join(projectDir, "supabase", "migrations"); + return (yield* fs.exists(migrationsDir)) + ? (yield* fs.readDirectory(migrationsDir)).filter((file) => file.endsWith(".sql")).sort() + : []; + }); + +export const withTempStackProject = ( + use: (project: Awaited>) => Effect.Effect, +): Effect.Effect => + Effect.acquireUseRelease( + Effect.tryPromise({ + try: () => makeTempCliStackProject("sb-pgdelta-next-e2e-"), + catch: (cause) => + new SyncE2eSetupError({ message: "temp stack project setup failed", cause }), + }), + use, + (project) => + Effect.tryPromise({ + try: () => project.cleanup(), + catch: (cause) => + new SyncE2eSetupError({ message: "temp stack project cleanup failed", cause }), + }).pipe(Effect.ignore), + ); + +/** Scenario commands share one home with the shipped shadow cache on, so only the first one provisions a cold shadow. */ +export const scenarioCommandOptions = (projectDir: string) => ({ + cwd: projectDir, + exitTimeoutMs: SCENARIO_COMMAND_TIMEOUT_MS, + home: `${projectDir}/.supabase-home`, + env: { SUPABASE_SHADOW_CACHE: undefined }, +}); + +export const setUpProject = (projectDir: string) => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + yield* fs.makeDirectory(path.join(projectDir, ".supabase-home")); + const init = yield* runSupabaseEffect(["init"], { + cwd: projectDir, + exitTimeoutMs: CLI_COMMAND_TIMEOUT_MS, + }); + requireCliSuccess(init, "init setup"); + yield* Effect.tryPromise({ + try: () => overrideStackPorts(projectDir), + catch: (cause) => + new SyncE2eSetupError({ message: `failed to override ports in ${projectDir}`, cause }), + }); + + const configPath = path.join(projectDir, "supabase", "config.toml"); + const config = yield* fs.readFileString(configPath); + if (!config.includes("[experimental.pgdelta]\nenabled = true")) { + return yield* new SyncE2eSetupError({ + message: "init setup did not enable experimental pg-delta in config.toml", + }); + } + yield* fs.writeFileString( + configPath, + config.replace( + '# declarative_schema_path = "./schemas"', + 'declarative_schema_path = "./schemas"', + ), + ); + + const schemasDir = path.join(projectDir, "supabase", "schemas"); + yield* fs.makeDirectory(schemasDir, { recursive: true }); + yield* fs.writeFileString(path.join(schemasDir, "public.sql"), initialDesiredSchema); + const extensionsDir = path.join(schemasDir, "cluster", "extensions"); + yield* fs.makeDirectory(extensionsDir, { recursive: true }); + for (const extension of ["pg_net", "pgcrypto", "uuid-ossp"]) { + yield* fs.writeFileString( + path.join(extensionsDir, `${extension}.sql`), + `CREATE EXTENSION IF NOT EXISTS "${extension}" WITH SCHEMA "extensions";\n`, + ); + } + // Both install into a fixed schema, so no `WITH SCHEMA` clause. + for (const extension of ["pg_cron", "pgmq"]) { + yield* fs.writeFileString( + path.join(extensionsDir, `${extension}.sql`), + `CREATE EXTENSION IF NOT EXISTS "${extension}";\n`, + ); + } + + const start = yield* runSupabaseEffect( + [ + "start", + "--exclude", + "studio", + "--exclude", + "logflare", + "--exclude", + "vector", + "--exclude", + "gotrue", + "--exclude", + "realtime", + "--exclude", + "storage-api", + ], + { cwd: projectDir, exitTimeoutMs: STACK_START_TIMEOUT_MS }, + ); + requireCliSuccess(start, "start setup"); + }); + +/** Writes the initial declarative migration, resets onto it, and requires a converged plan. */ +export const convergeInitialSchema = (projectDir: string) => + Effect.gen(function* () { + const sync = yield* runSupabaseEffect( + [ + "db", + "schema", + "declarative", + "sync", + "--no-apply", + "--name", + "initial_declarative", + "--experimental", + ], + scenarioCommandOptions(projectDir), + ); + requireCliSuccess(sync, "initial declarative sync"); + const reset = yield* runSupabaseEffect( + ["db", "reset", "--local", "--no-seed"], + scenarioCommandOptions(projectDir), + ); + requireCliSuccess(reset, "db reset setup"); + const converged = yield* runSupabaseEffect( + ["db", "schema", "declarative", "sync", "--no-apply", "--experimental"], + scenarioCommandOptions(projectDir), + ); + requireCliSuccess(converged, "converged declarative sync"); + if (!`${converged.stdout}${converged.stderr}`.includes("No schema changes found")) { + return yield* new SyncE2eSetupError({ + message: `initial schema did not converge:\n${commandFailure(converged)}`, + }); + } + }); diff --git a/apps/cli/vitest.config.ts b/apps/cli/vitest.config.ts index 1c20786369..531aefc51c 100644 --- a/apps/cli/vitest.config.ts +++ b/apps/cli/vitest.config.ts @@ -1,8 +1,32 @@ import { readFileSync } from "node:fs"; import { defaultClientConditions, defaultServerConditions } from "vite"; -import { defineConfig } from "vitest/config"; +import { configDefaults, defineConfig } from "vitest/config"; import { E2eSequencer } from "./tests/e2e-sequencer.ts"; +/** E2e files that run concurrently; each stack project picks free ports and its own project id. */ +const e2eWorkers = Number(process.env.SUPABASE_E2E_WORKERS || "1"); +if (!Number.isSafeInteger(e2eWorkers) || e2eWorkers < 1) { + throw new Error( + `SUPABASE_E2E_WORKERS must be a positive integer, got "${process.env.SUPABASE_E2E_WORKERS}"`, + ); +} + +/** + * E2e files that run alone: `start.health` tags a broken image under the tag every stack's + * Mailpit resolves to, and the rest provision pg-delta shadow databases on host ports the CLI + * picks and releases before Docker binds them, which a concurrent stack could take. + */ +const E2E_EXCLUSIVE_FILES = [ + "src/commands/start/start.health.e2e.test.ts", + "src/command-internal/db-bootstrap/shadow-cache.e2e.test.ts", + "src/commands/db/diff/diff.e2e.test.ts", + "src/commands/db/diff/diff.declarative.e2e.test.ts", + "src/commands/db/pull/pull.e2e.test.ts", + "src/commands/db/schema/declarative/sync/sync.e2e.test.ts", + "src/commands/db/schema/declarative/sync/sync.jobs.e2e.test.ts", + "src/commands/db/start/start.roles.e2e.test.ts", +]; + function dockerfileTextPlugin() { return { name: "dockerfile-text-loader", @@ -75,8 +99,23 @@ export default defineConfig({ test: { name: "e2e", include: ["**/*.e2e.test.ts"], + exclude: [...configDefaults.exclude, ...E2E_EXCLUSIVE_FILES], + fileParallelism: e2eWorkers > 1, + maxWorkers: e2eWorkers, + setupFiles: ["tests/e2e-setup.ts"], + globalSetup: ["../../packages/stack/tests/docker-volume-run.ts"], + testTimeout: 120_000, + hookTimeout: 120_000, + }, + }, + { + test: { + // Runs after the e2e project, one file at a time. + name: "e2e-exclusive", + include: E2E_EXCLUSIVE_FILES, fileParallelism: false, maxWorkers: 1, + sequence: { groupOrder: 2 }, setupFiles: ["tests/e2e-setup.ts"], globalSetup: ["../../packages/stack/tests/docker-volume-run.ts"], testTimeout: 120_000, diff --git a/packages/stack/package.json b/packages/stack/package.json index a8ba06429f..95313fb0f9 100644 --- a/packages/stack/package.json +++ b/packages/stack/package.json @@ -18,6 +18,7 @@ }, "scripts": { "generate": "bun run scripts/generate-functions-bootstrap.ts", + "prepare:test-artifacts": "bun run scripts/prepare-test-artifacts.ts", "test": "pnpm run test:unit && pnpm run test:integration", "test:unit": "pnpm exec turbo run @supabase/stack#test:unit:run --", "test:unit:run": "bun --bun vitest run --project unit", diff --git a/packages/stack/scripts/prepare-test-artifacts.ts b/packages/stack/scripts/prepare-test-artifacts.ts new file mode 100644 index 0000000000..fd182c98f4 --- /dev/null +++ b/packages/stack/scripts/prepare-test-artifacts.ts @@ -0,0 +1,26 @@ +import { NodeHttpClient, NodeRuntime, NodeServices } from "@effect/platform-node"; +import { Console, Effect, Layer } from "effect"; +import { artifactServiceKinds, catalogPins, prepareNativeArtifact } from "../src/Artifacts.ts"; +import { testArtifactCacheRoot } from "../tests/artifact-cache.ts"; + +/** + * Downloads every default native artifact into the shared test cache, as CI fills its disk. + * With `--images`, prints every catalog container image instead so CI can pull them. + */ +const prepareNative = Effect.forEach( + [...artifactServiceKinds(), "vector" as const], + (service) => + prepareNativeArtifact({ service }, testArtifactCacheRoot).pipe( + Effect.tap((artifact) => Console.log(`${service} ${artifact.version}`)), + ), + { concurrency: "unbounded", discard: true }, +).pipe(Effect.withSpan("Stack.prepareNativeArtifacts")); + +const printImages = Effect.forEach(catalogPins(), ({ pin }) => Console.log(pin.image), { + discard: true, +}); + +(process.argv.includes("--images") ? printImages : prepareNative).pipe( + Effect.provide(Layer.mergeAll(NodeServices.layer, NodeHttpClient.layerNodeHttp)), + NodeRuntime.runMain, +); diff --git a/packages/stack/src/Network.integration.test.ts b/packages/stack/src/Network.integration.test.ts index d0cb69bdfa..71ea4f4d3a 100644 --- a/packages/stack/src/Network.integration.test.ts +++ b/packages/stack/src/Network.integration.test.ts @@ -598,12 +598,18 @@ it.live("addresses docker runtime endpoints through the stack host alias", () => ).pipe(Effect.provide(NodeServices.layer)), ); +/** Ports Windows runners listen on: RDP, WinRM, Delivery Optimization and Windows services. */ +const windowsHeldPorts = new Set([3389, 5040, 5357, 5985, 5986, 7680]); + /** * A pinned port below the stack's native and auto port ranges and every OS ephemeral range, so * neither another test's auto allocation nor an outbound socket can already hold it. It stays * under 9000, where other files pin fixed ports. */ -const pinnedPort = () => 1024 + (Math.abs(Hash.string(randomUUID())) % (9_000 - 1024)); +const pinnedPort = (): number => { + const port = 1024 + (Math.abs(Hash.string(randomUUID())) % (9_000 - 1024)); + return windowsHeldPorts.has(port) ? pinnedPort() : port; +}; it.live("addresses a pinned endpoint from its configured port without a registry row", () => Effect.scoped( From 8a549dffa6bc5c88069c61c1f55ef55ad9b82ae2 Mon Sep 17 00:00:00 2001 From: Andrew Valleteau Date: Wed, 14 Oct 2026 15:16:30 +0000 Subject: [PATCH 06/11] fix(cli): warn that deprecated config, env vars and import maps are removed in v3 (#7154) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## TL;DR Users still relying on config keys, env vars or file conventions that v3 removes now get a warning on stable that says so. `SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION` was silently ignored after the rename to `SUPABASE_DB_ORIOLEDB_VERSION`; it now works as a deprecated alias with a warning. ## Before ```mermaid flowchart LR A["SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION set"] --> B[Ignored silently] --> C[Plain Postgres image] ``` ## After ```mermaid flowchart LR A["SUPABASE_DB_ORIOLEDB_VERSION set?"] -- yes --> D[OrioleDB image] A -- no --> E["SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION set?"] E -- yes --> F["Warn: removed in v3"] --> D E -- no --> G[config.toml value] ``` ## Why The v3 removals in CLI-2597 need a stable release that warns first. Several existing warnings did not mention v3, `INBUCKET_URL` in `status` had no warning, and a project using the old OrioleDB env var started plain Postgres with no message. ## What changed - `SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION` is used when `SUPABASE_DB_ORIOLEDB_VERSION` is unset and prints a deprecation warning; the new variable wins when both are set. Resolution lives in one helper used by every reader. - Warnings for `[inbucket]` / `[remotes.*.inbucket]`, `experimental.orioledb_version`, the `linkedin` / `slack` auth providers and `import_map.json` in `functions deploy` now say they will be removed in v3 and name the replacement. - `supabase status` warns that `INBUCKET_URL` is deprecated in favour of `MAILPIT_URL` for output formats that include it. - All warnings go to stderr; machine-readable stdout is unchanged. - Also touches `packages/config` warning wording, so this cuts an `@supabase/config` release. ## Terminal captures Recorded with vhs from source (`bun apps/cli/src/main.ts`) against a fresh `supabase init` project whose `config.toml` sets `[inbucket]`, `experimental.orioledb_version` and `[auth.external.slack]`. Each GIF runs `supabase stop` twice (it loads the config without a running stack): first with `SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION` unset, then with it set inline on the second run only. Before (merge-base with develop): ![before-stop](https://gist.githubusercontent.com/avallete/12c7231c768ee9188655db9110b3a5dd/raw/before-stop.gif) After: ![after-stop](https://gist.githubusercontent.com/avallete/12c7231c768ee9188655db9110b3a5dd/raw/after-stop.gif) Closes CLI-2601 Closes CLI-2569 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 --- .../db-bootstrap/bootstrap-config.ts | 3 +- .../db-bootstrap/reset-local-database.ts | 4 +- .../command-internal/db-config.toml-read.ts | 49 +++++++--- apps/cli/src/command-internal/db-pull-run.ts | 9 +- .../command-internal/local-config-values.ts | 54 ++++++++++- .../command-internal/local-project-context.ts | 22 ++++- apps/cli/src/command-internal/seed-buckets.ts | 4 - apps/cli/src/command-internal/stack-config.ts | 6 +- .../cli/src/command-internal/status-values.ts | 10 ++ .../src/command-internal/test-db.handler.ts | 10 +- .../src/commands/bootstrap/SIDE_EFFECTS.md | 19 ++-- .../commands/bootstrap/bootstrap.handler.ts | 7 +- apps/cli/src/commands/db/diff/SIDE_EFFECTS.md | 34 ++++--- apps/cli/src/commands/db/diff/diff.handler.ts | 21 ++++- .../commands/db/diff/diff.integration.test.ts | 23 +++++ apps/cli/src/commands/db/dump/SIDE_EFFECTS.md | 2 + apps/cli/src/commands/db/dump/dump.handler.ts | 7 +- .../commands/db/dump/dump.integration.test.ts | 23 +++++ apps/cli/src/commands/db/pull/SIDE_EFFECTS.md | 2 + .../commands/db/pull/pull.integration.test.ts | 42 +++++++++ apps/cli/src/commands/db/push/SIDE_EFFECTS.md | 15 +-- apps/cli/src/commands/db/push/push.handler.ts | 7 +- .../cli/src/commands/db/reset/SIDE_EFFECTS.md | 1 + .../src/commands/db/reset/reset.handler.ts | 7 +- .../declarative/generate/SIDE_EFFECTS.md | 19 ++-- .../declarative/generate/generate.handler.ts | 6 ++ .../generate/generate.integration.test.ts | 40 ++++++++ .../schema/declarative/sync/sync.handler.ts | 8 +- .../cli/src/commands/db/start/SIDE_EFFECTS.md | 1 + .../db/start/start.integration.test.ts | 40 +++++++- .../experimental/stack/start/start.handler.ts | 37 ++++---- .../stack/start/start.integration.test.ts | 47 +++++++++- .../commands/functions/deploy/SIDE_EFFECTS.md | 1 + .../deploy/deploy.integration.test.ts | 55 +++++++++++ .../src/commands/gen/types/SIDE_EFFECTS.md | 29 +++--- .../src/commands/gen/types/types.handler.ts | 6 +- .../commands/migration/down/SIDE_EFFECTS.md | 9 +- .../commands/migration/down/down.handler.ts | 7 +- .../src/commands/migration/up/SIDE_EFFECTS.md | 9 +- .../src/commands/migration/up/up.handler.ts | 6 +- .../migration/up/up.integration.test.ts | 19 ++++ .../cli/src/commands/services/SIDE_EFFECTS.md | 16 ++-- .../commands/services/services-local-stack.ts | 5 +- .../src/commands/services/services.handler.ts | 6 +- apps/cli/src/commands/status/SIDE_EFFECTS.md | 6 ++ .../cli/src/commands/status/status.handler.ts | 11 +++ .../status/status.integration.test.ts | 91 ++++++++++++------- apps/cli/src/commands/test/db/SIDE_EFFECTS.md | 17 ++-- apps/cli/src/shared/functions/deploy.ts | 2 +- packages/config/src/io.ts | 10 +- packages/config/src/io.unit.test.ts | 16 ++-- 51 files changed, 714 insertions(+), 186 deletions(-) diff --git a/apps/cli/src/command-internal/db-bootstrap/bootstrap-config.ts b/apps/cli/src/command-internal/db-bootstrap/bootstrap-config.ts index 577eb95193..fec6e16175 100644 --- a/apps/cli/src/command-internal/db-bootstrap/bootstrap-config.ts +++ b/apps/cli/src/command-internal/db-bootstrap/bootstrap-config.ts @@ -19,6 +19,7 @@ import { envOverride, envOverrideBool, envOverrideMajorVersion, + envOverrideOrioleDBVersion, envOverrideRealtimeIpVersion, envOverrideRealtimeMaxHeaderLength, InvalidRealtimeIpVersionEnvOverrideError, @@ -115,7 +116,7 @@ export const resolveDbBootstrapConfig = ( // precedence as `majorVersion` applies to each. const orioledbVersion = remoteWins("db.orioledb_version") ? config.db.orioledb_version - : envOverride("SUPABASE_DB_ORIOLEDB_VERSION", config.db.orioledb_version, projectEnvValues); + : envOverrideOrioleDBVersion(config.db.orioledb_version, projectEnvValues); const s3Host = remoteWins("experimental.s3_host") ? config.experimental.s3_host : envOverride("SUPABASE_EXPERIMENTAL_S3_HOST", config.experimental.s3_host, projectEnvValues); diff --git a/apps/cli/src/command-internal/db-bootstrap/reset-local-database.ts b/apps/cli/src/command-internal/db-bootstrap/reset-local-database.ts index c0bbef152d..695b632193 100644 --- a/apps/cli/src/command-internal/db-bootstrap/reset-local-database.ts +++ b/apps/cli/src/command-internal/db-bootstrap/reset-local-database.ts @@ -129,7 +129,8 @@ export const resetLocalDatabase = Effect.fn("DbBootstrap.resetLocalDatabase")(fu return yield* new ResetLocalDbNotRunningError({ message: "The local stack is not running." }); const catalog = yield* Effect.serviceOption(StackCatalogSetup); if (Option.isNone(catalog)) return yield* resetFailed("stack catalog setup is unavailable"); - const config = yield* loadStackConfig(workdir).pipe( + const context = yield* loadLocalProjectContext(workdir, (message) => resetFailed(message)); + const config = yield* loadStackConfig(workdir, { context }).pipe( Effect.mapError((cause) => resetFailed(cause.message)), ); const toml = yield* readDbToml(fs, path, workdir); @@ -227,7 +228,6 @@ export const resetLocalDatabase = Effect.fn("DbBootstrap.resetLocalDatabase")(fu // The database is already rebuilt, so any typed seeding failure only warns; defects and // interruption still propagate. yield* Effect.gen(function* () { - const context = yield* loadLocalProjectContext(workdir, (message) => resetFailed(message)); if (!hasConfiguredBuckets(context.config)) return; const capability = storage === undefined ? undefined : yield* storage.status; const decision = classifyStorageCapability(capability); diff --git a/apps/cli/src/command-internal/db-config.toml-read.ts b/apps/cli/src/command-internal/db-config.toml-read.ts index 210e213d84..ac0c9112fb 100644 --- a/apps/cli/src/command-internal/db-config.toml-read.ts +++ b/apps/cli/src/command-internal/db-config.toml-read.ts @@ -25,9 +25,14 @@ import { import { DbConfigLoadError } from "./db-config.errors.ts"; import { recordOrioleDbTelemetry, selectsOrioleDb } from "./db-image.ts"; import { parseDotEnv } from "./dotenv.ts"; -import { strToArr } from "./local-config-values.ts"; +import { + deprecatedOrioleDBVersionEnvWarning, + readOrioleDBVersionEnvOverride, + strToArr, +} from "./local-config-values.ts"; import { ramInBytes } from "./size-units.ts"; import { collectDotenvPrivateKeys, decryptSecret, isEncryptedSecret } from "./vault-decrypt.ts"; +import { Output } from "../shared/output/output.service.ts"; /** Resolves a config `env(VAR)` reference: shell env first, then project `.env`. */ type EnvLookup = (name: string) => string | undefined; @@ -1046,6 +1051,7 @@ const ENV_OVERRIDE_NAMES = [ "SUPABASE_DB_SHADOW_PORT", "SUPABASE_DB_MAJOR_VERSION", "SUPABASE_DB_ORIOLEDB_VERSION", + "SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION", "SUPABASE_EDGE_RUNTIME_DENO_VERSION", "SUPABASE_EXPERIMENTAL_WEBHOOKS_ENABLED", "SUPABASE_EXPERIMENTAL_PGDELTA_ENABLED", @@ -1068,6 +1074,34 @@ const ENV_OVERRIDE_NAMES = [ type EnvOverrideName = (typeof ENV_OVERRIDE_NAMES)[number]; +/** + * The `SUPABASE_*` override lookup `readDbToml` exposes as `envLookup`: a non-empty shell value + * wins, then a non-empty value from the project `.env` files. + */ +export const envOverrideLookup = Effect.fnUntraced(function* ( + projectEnv: Readonly>, +) { + const shellOverrideEnv = yield* resolveShellEnv(ENV_OVERRIDE_NAMES); + return (name: EnvOverrideName): string | undefined => { + const fromShell = shellOverrideEnv.get(name); + if (fromShell !== undefined && fromShell.length > 0) return fromShell; + const fromFile = projectEnv[name]; + return fromFile !== undefined && fromFile.length > 0 ? fromFile : undefined; + }; +}); + +/** + * Prints the `SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION` deprecation WARN on stderr when `envLookup` + * finds it set. Commands call it once after their main config read; a command that loads the + * local project context gets the WARN from that load instead. + */ +export const warnDeprecatedOrioleDBVersionEnv = Effect.fnUntraced(function* ( + envLookup: DbTomlValues["envLookup"], +) { + const warning = yield* deprecatedOrioleDBVersionEnvWarning(envLookup); + if (warning !== undefined) yield* (yield* Output).raw(`${warning}\n`, "stderr"); +}); + /** * Reads `/supabase/config.toml` (db subtree + project id) and the linked * `/supabase/.temp/pooler-url`. `fs`/`path` are passed in so the resolver @@ -1133,16 +1167,9 @@ const readDbTomlCore = Effect.fnUntraced(function* ( // the deprecation warning. doc = asRecord(normalizeDeprecatedOrioleDBVersion(doc).document); } - const shellOverrideEnv = yield* resolveShellEnv(ENV_OVERRIDE_NAMES); // `SUPABASE_DB_*` env vars override the matching `[db]` field before the TOML - // value/default. An empty env value is ignored, and the project `.env` files are - // loaded into the environment first, so consult both. - const envOverride = (name: EnvOverrideName): string | undefined => { - const fromShell = shellOverrideEnv.get(name); - if (fromShell !== undefined && fromShell.length > 0) return fromShell; - const fromFile = projectEnv[name]; - return fromFile !== undefined && fromFile.length > 0 ? fromFile : undefined; - }; + // value/default. + const envOverride = yield* envOverrideLookup(projectEnv); const shellRefEnv = yield* resolveShellEnv([ ...envRefNames(doc), ...Object.keys(asRecord(doc?.["remotes"]) ?? {}).map(remoteProjectIdEnvName), @@ -1314,7 +1341,7 @@ const readDbTomlCore = Effect.fnUntraced(function* ( const expandString = (value: unknown): Option.Option => typeof value === "string" ? nonEmptyString(expandEnv(value, lookup)) : Option.none(); const orioledbVersionRaw = - (remoteWins("db.orioledb_version") ? undefined : envOverride("SUPABASE_DB_ORIOLEDB_VERSION")) ?? + (remoteWins("db.orioledb_version") ? undefined : readOrioleDBVersionEnvOverride(envOverride)) ?? db?.["orioledb_version"]; const orioledbVersion = expandString(orioledbVersionRaw); // The defaults-only fallback says nothing about the project's engine. diff --git a/apps/cli/src/command-internal/db-pull-run.ts b/apps/cli/src/command-internal/db-pull-run.ts index 5cfa421d89..88448adb7b 100644 --- a/apps/cli/src/command-internal/db-pull-run.ts +++ b/apps/cli/src/command-internal/db-pull-run.ts @@ -19,7 +19,12 @@ import { ipv6Suggestion, isIPv6ConnectivityError } from "./connect-errors.ts"; import { DbConfigResolver } from "./db-config.service.ts"; import { resolveDbImage } from "./db-image.ts"; import { DbConnection, type PgConnInput } from "./db-connection.service.ts"; -import { loadProjectEnv, readDbToml, resolveDeclarativeDir } from "./db-config.toml-read.ts"; +import { + loadProjectEnv, + readDbToml, + resolveDeclarativeDir, + warnDeprecatedOrioleDBVersionEnv, +} from "./db-config.toml-read.ts"; import type { DbConnType } from "./db-target-flags.ts"; import { makeDir } from "./make-dir.ts"; import { toPostgresURL } from "./postgres-url.ts"; @@ -264,6 +269,8 @@ export const runDbPull = Effect.fn("db.pull.run")(function* ( if (toml.appliedRemote !== undefined) { yield* output.raw(`Loading config override: [remotes.${toml.appliedRemote}]\n`, "stderr"); } + // Without the deprecated export, the shadow inputs' project-context load below prints it. + if (useExperimentalExport) yield* warnDeprecatedOrioleDBVersionEnv(toml.envLookup); const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; const runtimeInfo = yield* RuntimeInfo; diff --git a/apps/cli/src/command-internal/local-config-values.ts b/apps/cli/src/command-internal/local-config-values.ts index ff9807efa4..8240608de6 100644 --- a/apps/cli/src/command-internal/local-config-values.ts +++ b/apps/cli/src/command-internal/local-config-values.ts @@ -15,7 +15,7 @@ import { defaultPublishableKey, defaultSecretKey, } from "../shared/stack-constants.ts"; -import { Effect, Option, Schema } from "effect"; +import { Context, Effect, Option, Ref, Schema } from "effect"; import * as Base64Url from "effect/encoding/Base64Url"; import { @@ -1020,6 +1020,58 @@ export function envOverrideMajorVersion( ); } +const ORIOLEDB_VERSION_ENV = "SUPABASE_DB_ORIOLEDB_VERSION"; +const DEPRECATED_ORIOLEDB_VERSION_ENV = "SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION"; + +/** + * Reads the `db.orioledb_version` override through `read`, falling back to the deprecated + * `SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION` alias while `SUPABASE_DB_ORIOLEDB_VERSION` is unset. + */ +export const readOrioleDBVersionEnvOverride = ( + read: ( + name: typeof ORIOLEDB_VERSION_ENV | typeof DEPRECATED_ORIOLEDB_VERSION_ENV, + ) => string | undefined, +): string | undefined => read(ORIOLEDB_VERSION_ENV) ?? read(DEPRECATED_ORIOLEDB_VERSION_ENV); + +/** `SUPABASE_DB_ORIOLEDB_VERSION` or its deprecated alias — see {@link envOverride}. */ +export function envOverrideOrioleDBVersion( + configured: string | undefined, + projectEnvValues: Readonly> | undefined, +): string | undefined { + return ( + readOrioleDBVersionEnvOverride((name) => envOverride(name, undefined, projectEnvValues)) ?? + configured + ); +} + +/** + * Set by {@link withSingleOrioleDBVersionEnvWarning}; while unset, every config read that finds the + * deprecated OrioleDB env alias prints its WARN. + */ +const OrioleDBVersionEnvWarningPrinted = Context.Reference>>( + "supabase/cli/OrioleDBVersionEnvWarningPrinted", + { defaultValue: () => Option.none() }, +); + +/** Runs `effect` so its config reads print the deprecated OrioleDB env WARN at most once. */ +export const withSingleOrioleDBVersionEnvWarning = (effect: Effect.Effect) => + Effect.flatMap(Ref.make(false), (printed) => + Effect.provideService(effect, OrioleDBVersionEnvWarningPrinted, Option.some(printed)), + ); + +/** + * The deprecation warning to print when `read` finds `SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION` set, + * unless an enclosing {@link withSingleOrioleDBVersionEnvWarning} already printed it. + */ +export const deprecatedOrioleDBVersionEnvWarning = Effect.fnUntraced(function* ( + read: (name: typeof DEPRECATED_ORIOLEDB_VERSION_ENV) => string | undefined, +) { + if (read(DEPRECATED_ORIOLEDB_VERSION_ENV) === undefined) return undefined; + const printed = yield* OrioleDBVersionEnvWarningPrinted; + if (Option.isSome(printed) && (yield* Ref.getAndSet(printed.value, true))) return undefined; + return `WARN: ${DEPRECATED_ORIOLEDB_VERSION_ENV} is deprecated and will be removed in v3, use ${ORIOLEDB_VERSION_ENV} instead.`; +}); + /** `SUPABASE_EDGE_RUNTIME_DENO_VERSION` — see {@link envOverrideUint}. */ export function envOverrideDenoVersion( configured: number, diff --git a/apps/cli/src/command-internal/local-project-context.ts b/apps/cli/src/command-internal/local-project-context.ts index 821b770ed2..d758c13f71 100644 --- a/apps/cli/src/command-internal/local-project-context.ts +++ b/apps/cli/src/command-internal/local-project-context.ts @@ -5,12 +5,17 @@ import { type CliConfig, } from "@supabase/config/effect"; import { loadCliConfig } from "@supabase/config/internal"; -import { Config, Crypto, Effect, FileSystem, Option, Path, Schema } from "effect"; +import { Config, Console, Crypto, Effect, FileSystem, Option, Path, Schema } from "effect"; import { recordOrioleDbTelemetry } from "./db-image.ts"; import { resolveLocalProjectId, sanitizeProjectId } from "./docker-ids.ts"; import { getHostname } from "./hostname.ts"; -import { envOverride, envOverrideMajorVersion } from "./local-config-values.ts"; +import { + deprecatedOrioleDBVersionEnvWarning, + envOverride, + envOverrideMajorVersion, + envOverrideOrioleDBVersion, +} from "./local-config-values.ts"; import { resolveProjectEnvironmentValues } from "./project-environment.ts"; import { RuntimeInfo } from "../shared/runtime/runtime-info.service.ts"; @@ -84,6 +89,16 @@ export const loadLocalProjectContext = ( }).pipe( Effect.mapError((cause) => mapConfigLoadError(`failed to read config: ${String(cause)}`)), ); + // Printed beside the loader's deprecated-config WARNs, on the same real console, so it shares + // their once-per-context-load cadence. + const orioleDBEnvWarning = yield* deprecatedOrioleDBVersionEnvWarning((name) => + envOverride(name, undefined, projectEnvValues), + ); + if (orioleDBEnvWarning !== undefined) { + yield* Console.error(orioleDBEnvWarning).pipe( + Effect.provideService(Console.Console, globalThis.console), + ); + } const config = loaded?.config ?? (yield* Schema.decodeEffect(CliConfigSchema)({}).pipe( @@ -129,8 +144,7 @@ export const loadLocalProjectContext = ( /** Records OrioleDB selection for commands whose only local config read is this context. */ export const recordLocalProjectOrioleDbTelemetry = (context: LocalProjectContext) => Effect.try(() => ({ - version: envOverride( - "SUPABASE_DB_ORIOLEDB_VERSION", + version: envOverrideOrioleDBVersion( context.config.db.orioledb_version, context.projectEnvValues, ), diff --git a/apps/cli/src/command-internal/seed-buckets.ts b/apps/cli/src/command-internal/seed-buckets.ts index 1a10b1df71..48cf490acf 100644 --- a/apps/cli/src/command-internal/seed-buckets.ts +++ b/apps/cli/src/command-internal/seed-buckets.ts @@ -34,10 +34,6 @@ import { import { SeedConfigLoadError } from "../commands/seed/buckets/buckets.errors.ts"; import { bucketObjectKey } from "../commands/seed/buckets/buckets.upload.ts"; -// Re-exported so `stack start` can report a project-context config-load failure with the -// same tag `seedBucketsRun` itself fails with, without importing across command boundaries. -export { SeedConfigLoadError }; - const CONFIG_PATH = "supabase/config.toml"; const UPLOAD_CONCURRENCY = 5; diff --git a/apps/cli/src/command-internal/stack-config.ts b/apps/cli/src/command-internal/stack-config.ts index 4964171688..cad545d5a0 100644 --- a/apps/cli/src/command-internal/stack-config.ts +++ b/apps/cli/src/command-internal/stack-config.ts @@ -33,6 +33,7 @@ import { envOverrideEdgeRuntimePolicy, envOverrideMaxClientConn, envOverrideMajorVersion, + envOverrideOrioleDBVersion, envOverridePort, envOverridePoolMode, envOverrideRealtimeIpVersion, @@ -132,6 +133,8 @@ const section = (document: Readonly> | undefined, name: export interface StackEndpointSetting { readonly configPath: string; readonly envVar: string; + /** A deprecated alias of `envVar`, honored only while `envVar` is unset. */ + readonly deprecatedEnvVar?: string; } // Shared by `createCreations` and `stackEndpointSetting`; a new endpoint also needs an entry in @@ -233,6 +236,7 @@ export const stackMajorVersionSetting: StackEndpointSetting = { export const stackOrioledbVersionSetting: StackEndpointSetting = { configPath: "db.orioledb_version", envVar: "SUPABASE_DB_ORIOLEDB_VERSION", + deprecatedEnvVar: "SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION", }; const authProviderNames = [ @@ -727,7 +731,7 @@ const resolveEffectiveCliConfig = ( port: resolvedPort("SUPABASE_DB_PORT", db.port, "db.port", env), major_version: envOverrideMajorVersion(db.major_version, env), health_timeout: envOverride("SUPABASE_DB_HEALTH_TIMEOUT", db.health_timeout, env), - orioledb_version: envOverride("SUPABASE_DB_ORIOLEDB_VERSION", db.orioledb_version, env), + orioledb_version: envOverrideOrioleDBVersion(db.orioledb_version, env), settings: resolveDbSettingsEnvOverrides(db.settings, env), pooler: resolvedPooler, }, diff --git a/apps/cli/src/command-internal/status-values.ts b/apps/cli/src/command-internal/status-values.ts index 5f5e1d6d36..ff8240a8cd 100644 --- a/apps/cli/src/command-internal/status-values.ts +++ b/apps/cli/src/command-internal/status-values.ts @@ -423,6 +423,16 @@ export function statusValuesFromState( return { values, names, local }; } +/** The stderr warning for the deprecated `INBUCKET_URL` field, when {@link statusValuesFromState} emits it. */ +export function deprecatedStatusFieldWarning( + state: StatusState, + overrides: ReadonlyMap, +): string | undefined { + if (!state.inbucketEnabled) return undefined; + const nameFor = (field: StatusField) => overrides.get(field.fieldKey) ?? field.defaultName; + return `WARN: ${nameFor(INBUCKET_URL)} is deprecated and will be removed in v3, use ${nameFor(MAILPIT_URL)} instead.\n`; +} + /** * Convenience wrapper combining {@link resolveStatusLocalState}, {@link gateStatusState}, and * {@link statusValuesFromState} in one call — used by tests needing only a single override diff --git a/apps/cli/src/command-internal/test-db.handler.ts b/apps/cli/src/command-internal/test-db.handler.ts index 0180e26e4f..178b0ac7d0 100644 --- a/apps/cli/src/command-internal/test-db.handler.ts +++ b/apps/cli/src/command-internal/test-db.handler.ts @@ -7,7 +7,12 @@ import { CliArgs } from "../shared/cli/cli-args.service.ts"; import { CommandSettings } from "../config/command-settings.service.ts"; import { TelemetryState } from "../telemetry/telemetry-state.service.ts"; import { DbConfigResolver } from "./db-config.service.ts"; -import { readDbToml } from "./db-config.toml-read.ts"; +import { + envOverrideLookup, + loadProjectEnv, + readDbToml, + warnDeprecatedOrioleDBVersionEnv, +} from "./db-config.toml-read.ts"; import { DbConnection, type PgConnInput } from "./db-connection.service.ts"; import { LocalDbRunningError } from "./db-bootstrap/local-db-running.ts"; import { DockerRun } from "./docker-run.service.ts"; @@ -159,6 +164,9 @@ export const testDb = Effect.fn("test.db")(function* (flags: TestDbFlags) { dnsResolver, linkedProjectRef: flags.projectRef, }); + yield* warnDeprecatedOrioleDBVersionEnv( + yield* envOverrideLookup(yield* loadProjectEnv(fs, path, cliSettings.workdir)), + ); const args = buildPgProveArgs({ paths: flags.paths, diff --git a/apps/cli/src/commands/bootstrap/SIDE_EFFECTS.md b/apps/cli/src/commands/bootstrap/SIDE_EFFECTS.md index cc35f8e1bf..54ac163ec0 100644 --- a/apps/cli/src/commands/bootstrap/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/bootstrap/SIDE_EFFECTS.md @@ -61,15 +61,16 @@ neither branch ever reaches the temp-login-role/Management-API path a passwordle ## Environment Variables -| Variable | Purpose | Required? | -| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------- | -| `SUPABASE_WORKDIR` | target dir (`--workdir` flag → env → prompt → cwd) | no | -| `SUPABASE_DB_PASSWORD` | DB password (`-p` flag → env → prompt/generate) | no | -| `GITHUB_TOKEN` | raise the GitHub API rate limit for template fetch | no | -| `SUPABASE_ACCESS_TOKEN` | auth bypass for ensure-login | no | -| `SUPABASE_PROFILE` | profile name/path (env → `~/.supabase/profile` → `supabase`) | no | -| `SUPABASE_YES` | auto-confirm the native push step's prompts, read project-`.env`-aware like the standalone `db push` | no | -| `SUPABASE_EXPERIMENTAL_STACK` | blank/`scratch` path only; when `1`, persists `[experimental] stack = true` and omits Docker-era default ports. Empty is unset; other non-empty values fail closed | no | +| Variable | Purpose | Required? | +| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------- | +| `SUPABASE_WORKDIR` | target dir (`--workdir` flag → env → prompt → cwd) | no | +| `SUPABASE_DB_PASSWORD` | DB password (`-p` flag → env → prompt/generate) | no | +| `GITHUB_TOKEN` | raise the GitHub API rate limit for template fetch | no | +| `SUPABASE_ACCESS_TOKEN` | auth bypass for ensure-login | no | +| `SUPABASE_PROFILE` | profile name/path (env → `~/.supabase/profile` → `supabase`) | no | +| `SUPABASE_YES` | auto-confirm the native push step's prompts, read project-`.env`-aware like the standalone `db push` | no | +| `SUPABASE_EXPERIMENTAL_STACK` | blank/`scratch` path only; when `1`, persists `[experimental] stack = true` and omits Docker-era default ports. Empty is unset; other non-empty values fail closed | no | +| `SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION` | deprecated alias of `SUPABASE_DB_ORIOLEDB_VERSION`, used only while that is unset; when set, stderr gets `WARN: SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION is deprecated and will be removed in v3, use SUPABASE_DB_ORIOLEDB_VERSION instead.` | no | ## Exit Codes diff --git a/apps/cli/src/commands/bootstrap/bootstrap.handler.ts b/apps/cli/src/commands/bootstrap/bootstrap.handler.ts index 3a535824a6..3b06020987 100644 --- a/apps/cli/src/commands/bootstrap/bootstrap.handler.ts +++ b/apps/cli/src/commands/bootstrap/bootstrap.handler.ts @@ -26,7 +26,11 @@ import { getProjectApiKeys } from "../../command-internal/get-api-keys.ts"; import { sanitizeErrorBody } from "../../command-internal/http-errors.ts"; import type { ConnectSuggestionContext } from "../../command-internal/connect-errors.ts"; import { resolveLinkedConn } from "../../command-internal/db-config.layer.ts"; -import { checkDbToml, loadProjectEnv } from "../../command-internal/db-config.toml-read.ts"; +import { + checkDbToml, + loadProjectEnv, + warnDeprecatedOrioleDBVersionEnv, +} from "../../command-internal/db-config.toml-read.ts"; import { dbPushCore } from "../../command-internal/db-push-core.ts"; import { linkServicesCore } from "../../command-internal/link-services-core.ts"; import { projectCreateCore } from "../../command-internal/project-create-core.ts"; @@ -229,6 +233,7 @@ export const bootstrap = Effect.fn("bootstrap")(function* ( if (toml.appliedRemote !== undefined) { yield* output.raw(`Loading config override: [remotes.${toml.appliedRemote}]\n`, "stderr"); } + yield* warnDeprecatedOrioleDBVersionEnv(toml.envLookup); yield* linkServicesCore({ ref: projectRef, diff --git a/apps/cli/src/commands/db/diff/SIDE_EFFECTS.md b/apps/cli/src/commands/db/diff/SIDE_EFFECTS.md index d342688c2f..705fe8a15c 100644 --- a/apps/cli/src/commands/db/diff/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/db/diff/SIDE_EFFECTS.md @@ -103,20 +103,22 @@ of this command's own target resolve, ahead of the differ container. ## Environment Variables -| Variable | Purpose | Required? | -| ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | -| `SUPABASE_ACCESS_TOKEN` | auth for `--linked` | no | -| `SUPABASE_DB_PASSWORD` | remote DB password (linked) | no | -| `SUPABASE_DB_SHADOW_PORT` | shadow container's host port (`db.shadow_port`) — NOT `SUPABASE_DB_PORT`, which the shadow never reads | no | -| `SUPABASE_DB_MAJOR_VERSION` / `SUPABASE_DB_HEALTH_TIMEOUT` / `SUPABASE_DB_SETTINGS_*` | shadow container-config overrides, same as `db start`/`db reset` | no | -| `SUPABASE_PROJECT_ID` | overrides the shadow container's project id/labels, same as `db start`/`db reset` (`utils.DbId`); ALSO the linked-ref resolution fallback `--project-ref` supersedes — see Notes for the narrower scope of the flag | no | -| `SUPABASE_NETWORK_ID` (`--network-id`) | forces the shadow container/network onto an existing Docker network | no | -| `SUPABASE_HOME` | overrides the `~/.supabase` root used for the shadow baseline cache (and other CLI state) | no | -| `SUPABASE_SHADOW_CACHE` | shadow baseline cache; on by default, opt-out (`0`/`false`); the shadow's post-baseline state is saved under a managed snapshot key and restored into the next run's fresh stack database (see Notes) | no | -| `SUPABASE_EXPERIMENTAL_PG_DELTA` | force pg-delta engine | no | -| `PGDELTA_DEBUG` | pg-delta debug capture | no | -| `SUPABASE_SSL_DEBUG` | migra SSL debug logging | no | -| `SUPABASE_INTERNAL_IMAGE_REGISTRY` | overrides the differ's / shadow's image registry (shell **or** project `.env`, passed to each image operation) | no | +| Variable | Purpose | Required? | +| ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------- | +| `SUPABASE_ACCESS_TOKEN` | auth for `--linked` | no | +| `SUPABASE_DB_PASSWORD` | remote DB password (linked) | no | +| `SUPABASE_DB_SHADOW_PORT` | shadow container's host port (`db.shadow_port`) — NOT `SUPABASE_DB_PORT`, which the shadow never reads | no | +| `SUPABASE_DB_MAJOR_VERSION` / `SUPABASE_DB_HEALTH_TIMEOUT` / `SUPABASE_DB_SETTINGS_*` | shadow container-config overrides, same as `db start`/`db reset` | no | +| `SUPABASE_PROJECT_ID` | overrides the shadow container's project id/labels, same as `db start`/`db reset` (`utils.DbId`); ALSO the linked-ref resolution fallback `--project-ref` supersedes — see Notes for the narrower scope of the flag | no | +| `SUPABASE_NETWORK_ID` (`--network-id`) | forces the shadow container/network onto an existing Docker network | no | +| `SUPABASE_HOME` | overrides the `~/.supabase` root used for the shadow baseline cache (and other CLI state) | no | +| `SUPABASE_SHADOW_CACHE` | shadow baseline cache; on by default, opt-out (`0`/`false`); the shadow's post-baseline state is saved under a managed snapshot key and restored into the next run's fresh stack database (see Notes) | no | +| `SUPABASE_EXPERIMENTAL_PG_DELTA` | force pg-delta engine | no | +| `PGDELTA_DEBUG` | pg-delta debug capture | no | +| `SUPABASE_SSL_DEBUG` | migra SSL debug logging | no | +| `SUPABASE_INTERNAL_IMAGE_REGISTRY` | overrides the differ's / shadow's image registry (shell **or** project `.env`, passed to each image operation) | no | +| `SUPABASE_DB_ORIOLEDB_VERSION` | overrides `db.orioledb_version`, selecting the OrioleDB shadow image | no | +| `SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION` | deprecated alias of `SUPABASE_DB_ORIOLEDB_VERSION`, used only while that is unset; when set, stderr gets `WARN: SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION is deprecated and will be removed in v3, use SUPABASE_DB_ORIOLEDB_VERSION instead.` | no | `SUPABASE_DB_SHADOW_PORT`/`SUPABASE_NETWORK_ID`/`--network-id`/`SUPABASE_PROJECT_ID`/ `SUPABASE_DB_HEALTH_TIMEOUT` all apply to `--use-pgadmin` too — its shadow is provisioned @@ -227,7 +229,9 @@ transaction metadata. dropped. - `--use-pg-schema` rebuilds the argv and exec's the bundled Go binary (its side effects are Go's); the Go child's telemetry is disabled so the single - `cli_command_executed` event comes from this TS command. + `cli_command_executed` event comes from this TS command. When only the deprecated + `SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION` alias is set (shell or project `.env`), its value + is passed to the child as `SUPABASE_DB_ORIOLEDB_VERSION` and the deprecation WARN prints once. - Explicit `--from`/`--to` mode always uses pg-delta and writes the flattened review representation to `--output` (or stdout). It ignores `--file`; normal mode retains per-unit migration files for the CLI apply paths. diff --git a/apps/cli/src/commands/db/diff/diff.handler.ts b/apps/cli/src/commands/db/diff/diff.handler.ts index f77c4627cd..f486062ee0 100644 --- a/apps/cli/src/commands/db/diff/diff.handler.ts +++ b/apps/cli/src/commands/db/diff/diff.handler.ts @@ -14,8 +14,11 @@ import { CommandSettings } from "../../../config/command-settings.service.ts"; import { ProjectRefResolver } from "../../../config/project-ref.service.ts"; import { aqua, yellow } from "../../../command-internal/colors.ts"; import { + envOverrideLookup, + loadProjectEnv, readDbToml, resolveDeclarativeDir, + warnDeprecatedOrioleDBVersionEnv, type DbTomlValues, } from "../../../command-internal/db-config.toml-read.ts"; import { DbConfigResolver } from "../../../command-internal/db-config.service.ts"; @@ -357,6 +360,8 @@ export const dbDiff = Effect.fn("db.diff")(function* (flags: DbDiffFlags) { }); const source = yield* resolveRef(from); const desired = yield* resolveRef(to); + // A `migrations` ref provisions a shadow whose project-context load prints this itself. + if (migrationsToml === undefined) yield* warnDeprecatedOrioleDBVersionEnv(cfg.envLookup); const explicitCtx: PgDeltaContext = { projectId: resolvePgDeltaProjectId(path, cliSettings.projectId, cfg, cliSettings.workdir), cwd: cliSettings.workdir, @@ -429,7 +434,21 @@ export const dbDiff = Effect.fn("db.diff")(function* (flags: DbDiffFlags) { // every mode). The delegated Go `db diff --use-pg-schema` still prints its own // experimental warning; this is additive, not a replacement, so don't drop it. yield* output.raw(`${warnPgSchemaDeprecated}\n`, "stderr"); - const env = { SUPABASE_TELEMETRY_DISABLED: "1" }; + const envLookup = yield* envOverrideLookup( + yield* loadProjectEnv(fs, path, cliSettings.workdir), + ); + yield* warnDeprecatedOrioleDBVersionEnv(envLookup); + // The Go child does not know the deprecated alias, so hand it the resolved value. + const orioleDBVersionAlias = + envLookup("SUPABASE_DB_ORIOLEDB_VERSION") === undefined + ? envLookup("SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION") + : undefined; + const env = { + SUPABASE_TELEMETRY_DISABLED: "1", + ...(orioleDBVersionAlias === undefined + ? {} + : { SUPABASE_DB_ORIOLEDB_VERSION: orioleDBVersionAlias }), + }; // In machine-output mode the child's stdout is captured and re-emitted as a structured // envelope, so scripted callers get valid JSON instead of the raw SQL. The delegated // child owns any `--file` write, so the written path isn't introspectable here (`file: diff --git a/apps/cli/src/commands/db/diff/diff.integration.test.ts b/apps/cli/src/commands/db/diff/diff.integration.test.ts index 6d010518ca..df51d52bb1 100644 --- a/apps/cli/src/commands/db/diff/diff.integration.test.ts +++ b/apps/cli/src/commands/db/diff/diff.integration.test.ts @@ -1382,6 +1382,29 @@ describe("db diff", () => { }, ); + it.effect( + "hands the deprecated OrioleDB version alias to the --use-pg-schema Go child under its new name and warns once", + () => { + const s = setup(tmp.current, { + files: { "supabase/.env": "SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION=17.11.0.002\n" }, + }); + return Effect.gen(function* () { + yield* dbDiff(flags({ usePgSchema: Option.some(true) })); + const warning = + "WARN: SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION is deprecated and will be removed in v3, use SUPABASE_DB_ORIOLEDB_VERSION instead."; + expect( + stderr(s.out) + .split("\n") + .filter((line) => line === warning), + ).toHaveLength(1); + expect(s.proxyCalls[0]?.env).toEqual({ + SUPABASE_TELEMETRY_DISABLED: "1", + SUPABASE_DB_ORIOLEDB_VERSION: "17.11.0.002", + }); + }).pipe(Effect.provide(s.layer)); + }, + ); + it.effect("does not print the --use-pg-schema deprecation warning on other diff paths", () => { const s = setup(tmp.current, { diffSql: "create table g ();\n" }); return Effect.gen(function* () { diff --git a/apps/cli/src/commands/db/dump/SIDE_EFFECTS.md b/apps/cli/src/commands/db/dump/SIDE_EFFECTS.md index 9a545e46f7..3f5b57cacf 100644 --- a/apps/cli/src/commands/db/dump/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/db/dump/SIDE_EFFECTS.md @@ -38,6 +38,8 @@ image), to stdout or `--file`. | ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `SUPABASE_DB_PASSWORD` (`DB_PASSWORD` viper key; `--password`/`-p` overrides) | remote DB password | | `SUPABASE_ACCESS_TOKEN` | `--linked` auth | +| `SUPABASE_DB_ORIOLEDB_VERSION` | overrides `db.orioledb_version`, selecting the OrioleDB `pg_dump` image | +| `SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION` | deprecated alias of `SUPABASE_DB_ORIOLEDB_VERSION`, used only while that is unset; when set, stderr gets `WARN: SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION is deprecated and will be removed in v3, use SUPABASE_DB_ORIOLEDB_VERSION instead.` | | `BITBUCKET_CLONE_DIR` | (no-op for dump — no `--security-opt` is set) | | `SUPABASE_INTERNAL_IMAGE_REGISTRY` | rewrite the pg image registry for compose dumps; stack dumps use the catalog image pin unchanged | | `SUPABASE_USE_SLIM_IMAGES` | resolve the current Postgres pin from the slim `ghcr.io/supabase/cli` builds (`true`/`1` enable); majors 13/15 use `15.14.1.167` when the flag is on; historical pins, PG14, OrioleDB tags the catalog does not pin, and flag-off `15.8.1.085` stay on docker.io | diff --git a/apps/cli/src/commands/db/dump/dump.handler.ts b/apps/cli/src/commands/db/dump/dump.handler.ts index 0b39683db8..e6f8a77786 100644 --- a/apps/cli/src/commands/db/dump/dump.handler.ts +++ b/apps/cli/src/commands/db/dump/dump.handler.ts @@ -7,7 +7,11 @@ import { LinkedProjectCache } from "../../../telemetry/linked-project-cache.serv import { TelemetryState } from "../../../telemetry/telemetry-state.service.ts"; import { DbConfigResolver } from "../../../command-internal/db-config.service.ts"; import type { DbConnType } from "../../../command-internal/db-target-flags.ts"; -import { loadProjectEnv, readDbToml } from "../../../command-internal/db-config.toml-read.ts"; +import { + loadProjectEnv, + readDbToml, + warnDeprecatedOrioleDBVersionEnv, +} from "../../../command-internal/db-config.toml-read.ts"; import { parseConnectionString } from "../../../command-internal/db-config.parse.ts"; import { resolveDbImage } from "../../../command-internal/db-image.ts"; import { @@ -187,6 +191,7 @@ export const dbDump = Effect.fn("db.dump")(function* (flags: DbDumpFlags) { // Read before the dry-run print so an invalid merged config fails rather than // silently printing a script. const tomlValues = yield* readDbToml(fs, path, cliSettings.workdir, linkedRef); + yield* warnDeprecatedOrioleDBVersionEnv(tomlValues.envLookup); const backend = yield* currentStackBackend; const managedProject = diff --git a/apps/cli/src/commands/db/dump/dump.integration.test.ts b/apps/cli/src/commands/db/dump/dump.integration.test.ts index 44e45ab7ff..a4fd320525 100644 --- a/apps/cli/src/commands/db/dump/dump.integration.test.ts +++ b/apps/cli/src/commands/db/dump/dump.integration.test.ts @@ -748,6 +748,29 @@ describe("db dump integration", () => { }).pipe(Effect.provide(layer)); }); + it.effect( + "warns once on stderr that SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION is removed in v3, keeping stdout clean", + () => { + const { layer, out } = setup({ + isLocal: true, + stdout: "CREATE SCHEMA public;\n", + env: { SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION: "17.11.0.002" }, + }); + const consoleErrors: Array = []; + const errorSpy = vi.spyOn(console, "error").mockImplementation((...args) => { + consoleErrors.push(args.map((a) => String(a)).join(" ")); + }); + return Effect.gen(function* () { + yield* dbDump(flags({ local: Option.some(true) })); + const warning = + "WARN: SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION is deprecated and will be removed in v3, use SUPABASE_DB_ORIOLEDB_VERSION instead."; + const stderrLines = [...out.stderrText.split("\n"), ...consoleErrors]; + expect(stderrLines.filter((line) => line === warning)).toHaveLength(1); + expect(out.stdoutText).toBe("CREATE SCHEMA public;\n"); + }).pipe(Effect.provide(layer), Effect.ensuring(Effect.sync(() => errorSpy.mockRestore()))); + }, + ); + it.effect("dumps only data with column inserts", () => { const { layer, out, docker } = setup({ isLocal: true, stdout: "INSERT INTO ...;\n" }); return Effect.gen(function* () { diff --git a/apps/cli/src/commands/db/pull/SIDE_EFFECTS.md b/apps/cli/src/commands/db/pull/SIDE_EFFECTS.md index 1ba637c263..a4cc4e29be 100644 --- a/apps/cli/src/commands/db/pull/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/db/pull/SIDE_EFFECTS.md @@ -133,6 +133,8 @@ at all, so nothing is cached for it. | `SUPABASE_DB_PASSWORD` | remote DB password (overridden by `-p`) | no | | `SUPABASE_DB_SHADOW_PORT` | shadow container's host port (`db.shadow_port`) — NOT `SUPABASE_DB_PORT`, which the shadow never reads | no | | `SUPABASE_DB_MAJOR_VERSION` / `SUPABASE_DB_HEALTH_TIMEOUT` / `SUPABASE_DB_SETTINGS_*` | shadow container-config overrides, same as `db start`/`db reset` | no | +| `SUPABASE_DB_ORIOLEDB_VERSION` | overrides `db.orioledb_version`, selecting the OrioleDB shadow and initial-migra `pg_dump` images | no | +| `SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION` | deprecated alias of `SUPABASE_DB_ORIOLEDB_VERSION`, used only while that is unset; when set, stderr gets `WARN: SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION is deprecated and will be removed in v3, use SUPABASE_DB_ORIOLEDB_VERSION instead.` | no | | `SUPABASE_PROJECT_ID` | overrides the shadow container's project id/labels, same as `db start`/`db reset` (`utils.DbId`); ALSO the linked-ref resolution fallback `--project-ref` supersedes — see Notes for the narrower scope of the flag | no | | `SUPABASE_NETWORK_ID` (`--network-id`) | forces the shadow and initial-migra `pg_dump` containers onto an existing Docker network | no | | `SUPABASE_USE_SLIM_IMAGES` | resolves the current-pin shadow Postgres, `pg_dump`, PG15+ realtime/storage/auth migrate-job images (migration-style cold shadow), and (for migra) the edge-runtime image from the slim `ghcr.io/supabase/cli` builds (`true`/`1` enable); majors 13/15 use `15.14.1.167` when the flag is on; historical pins, PG14, OrioleDB tags the catalog does not pin, flag-off `15.8.1.085`, and `deno_version = 1` stay on docker.io | no | diff --git a/apps/cli/src/commands/db/pull/pull.integration.test.ts b/apps/cli/src/commands/db/pull/pull.integration.test.ts index 5499474b82..c2ff521ae2 100644 --- a/apps/cli/src/commands/db/pull/pull.integration.test.ts +++ b/apps/cli/src/commands/db/pull/pull.integration.test.ts @@ -1893,6 +1893,48 @@ describe("db pull", () => { }, ); + it.effect.each([ + { + mode: "the deprecated --experimental export", + opts: { experimental: true, edgeStdout: EXPORT_JSON }, + }, + { + mode: "a migration pull", + opts: { + migrations: ["20240101000000"], + remoteVersions: ["20240101000000"], + edgeStdout: "create table remote ();\n", + yes: true, + }, + }, + ])( + "warns once on stderr that SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION is removed in v3 for $mode", + ({ opts }) => { + const s = setup(tmp.current, opts); + const consoleErrors: Array = []; + const errorSpy = vi.spyOn(console, "error").mockImplementation((...args) => { + consoleErrors.push(args.map((a) => String(a)).join(" ")); + }); + const [name, value] = ["SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION", "17.11.0.002"] as const; + // The project-context load reads `process.env`; `readDbToml` reads `Config`. + return withEnvVar( + name, + value, + withConfigEnv( + { [name]: value }, + Effect.gen(function* () { + yield* dbPull(flags()); + const warning = + "WARN: SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION is deprecated and will be removed in v3, use SUPABASE_DB_ORIOLEDB_VERSION instead."; + const stderrLines = [...streamText(s.out, "stderr").split("\n"), ...consoleErrors]; + expect(stderrLines.filter((line) => line === warning)).toHaveLength(1); + expect(streamText(s.out, "stdout")).not.toContain(warning); + }), + ), + ).pipe(Effect.provide(s.layer), Effect.ensuring(Effect.sync(() => errorSpy.mockRestore()))); + }, + ); + it.effect( "an experimental pull in json mode reports a declarative export with no history repair", () => { diff --git a/apps/cli/src/commands/db/push/SIDE_EFFECTS.md b/apps/cli/src/commands/db/push/SIDE_EFFECTS.md index 03616d31b0..1f5ac260ac 100644 --- a/apps/cli/src/commands/db/push/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/db/push/SIDE_EFFECTS.md @@ -42,13 +42,14 @@ before migrations unless `--skip-vault` is set. ## Environment Variables -| Variable | Purpose | Required? | -| ----------------------- | ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | -| `SUPABASE_ACCESS_TOKEN` | auth token for the `--linked` resolver path | no (falls back to keyring → `~/.supabase/access-token`) | -| `SUPABASE_DB_PASSWORD` | password for the linked/remote connection | no (`--password`/`-p` takes precedence) | -| `SUPABASE_YES` | auto-confirm prompts | no (also `--yes`) | -| `SUPABASE_PROJECT_ID` | linked-ref resolution override, superseded by `--project-ref` when set (same precedence position) — see Notes | no | -| `DOTENV_PRIVATE_KEY*` | decrypts `encrypted:` config secrets; `[db.vault]` values are not decrypted with `--skip-vault` | no | +| Variable | Purpose | Required? | +| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------- | +| `SUPABASE_ACCESS_TOKEN` | auth token for the `--linked` resolver path | no (falls back to keyring → `~/.supabase/access-token`) | +| `SUPABASE_DB_PASSWORD` | password for the linked/remote connection | no (`--password`/`-p` takes precedence) | +| `SUPABASE_YES` | auto-confirm prompts | no (also `--yes`) | +| `SUPABASE_PROJECT_ID` | linked-ref resolution override, superseded by `--project-ref` when set (same precedence position) — see Notes | no | +| `DOTENV_PRIVATE_KEY*` | decrypts `encrypted:` config secrets; `[db.vault]` values are not decrypted with `--skip-vault` | no | +| `SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION` | deprecated alias of `SUPABASE_DB_ORIOLEDB_VERSION`, used only while that is unset; when set, stderr gets `WARN: SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION is deprecated and will be removed in v3, use SUPABASE_DB_ORIOLEDB_VERSION instead.` | no | ## Exit Codes diff --git a/apps/cli/src/commands/db/push/push.handler.ts b/apps/cli/src/commands/db/push/push.handler.ts index 86c6059145..4f7f1d57a8 100644 --- a/apps/cli/src/commands/db/push/push.handler.ts +++ b/apps/cli/src/commands/db/push/push.handler.ts @@ -7,7 +7,11 @@ import { Output } from "../../../shared/output/output.service.ts"; import { CommandSettings } from "../../../config/command-settings.service.ts"; import { ProjectRefResolver } from "../../../config/project-ref.service.ts"; import { DbConfigResolver } from "../../../command-internal/db-config.service.ts"; -import { checkDbToml, loadProjectEnv } from "../../../command-internal/db-config.toml-read.ts"; +import { + checkDbToml, + loadProjectEnv, + warnDeprecatedOrioleDBVersionEnv, +} from "../../../command-internal/db-config.toml-read.ts"; import { dbPushCore } from "../../../command-internal/db-push-core.ts"; import { resolveDbTargetFlags } from "../../../command-internal/db-target-flags.ts"; import { LinkedProjectCache } from "../../../telemetry/linked-project-cache.service.ts"; @@ -85,6 +89,7 @@ export const dbPush = Effect.fn("db.push")(function* (flags: DbPushFlags) { if (toml.appliedRemote !== undefined) { yield* output.raw(`Loading config override: [remotes.${toml.appliedRemote}]\n`, "stderr"); } + yield* warnDeprecatedOrioleDBVersionEnv(toml.envLookup); const cfg = yield* resolver.resolve({ dbUrl: flags.dbUrl, diff --git a/apps/cli/src/commands/db/reset/SIDE_EFFECTS.md b/apps/cli/src/commands/db/reset/SIDE_EFFECTS.md index b257f7b8df..4a4eee0f95 100644 --- a/apps/cli/src/commands/db/reset/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/db/reset/SIDE_EFFECTS.md @@ -178,6 +178,7 @@ the whole reset** (not just "skip buckets"). | `SUPABASE_AUTH_JWT_SECRET` / `SUPABASE_AUTH_SERVICE_ROLE_KEY` | local path: override `auth.jwt_secret` / `auth.service_role_key` (shell or project dotenv, `encrypted:` decrypted) — feeds the recreated Postgres container (jwt secret), the fresh-database setup jobs (both) and the legacy-backend bucket-seed Storage service-role key, same as `seed buckets`. Not consulted on the stack backend, which uses the stack's own service-role JWT | no | | `SUPABASE_NETWORK_ID` (`--network-id`) | forces the recreated container/network onto an existing Docker network — legacy backend only | no | | `SUPABASE_EXPERIMENTAL_STACK` | `1`/`0` selects the stack backend for the local path, overriding `experimental.stack` | no (→ `experimental.stack` → legacy) | +| `SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION` | deprecated alias of `SUPABASE_DB_ORIOLEDB_VERSION`, used only while that is unset; when set, stderr gets `WARN: SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION is deprecated and will be removed in v3, use SUPABASE_DB_ORIOLEDB_VERSION instead.` | no | ## Connection loss during migration apply diff --git a/apps/cli/src/commands/db/reset/reset.handler.ts b/apps/cli/src/commands/db/reset/reset.handler.ts index 0c3c17235e..e86008eca9 100644 --- a/apps/cli/src/commands/db/reset/reset.handler.ts +++ b/apps/cli/src/commands/db/reset/reset.handler.ts @@ -15,7 +15,11 @@ import { aqua, yellow } from "../../../command-internal/colors.ts"; import { resolveResetSeedConfig } from "../../../command-internal/db-bootstrap/db-setup.ts"; import { resetLocalDatabase } from "../../../command-internal/db-bootstrap/reset-local-database.ts"; import { DbConfigResolver } from "../../../command-internal/db-config.service.ts"; -import { checkDbToml, loadProjectEnv } from "../../../command-internal/db-config.toml-read.ts"; +import { + checkDbToml, + loadProjectEnv, + warnDeprecatedOrioleDBVersionEnv, +} from "../../../command-internal/db-config.toml-read.ts"; import { DbConnection } from "../../../command-internal/db-connection.service.ts"; import { applyMigrations, applySchemaFiles } from "../../../command-internal/migration-apply.ts"; import { parseMigrationVersion } from "../../../command-internal/migration-timestamp.format.ts"; @@ -226,6 +230,7 @@ export const dbReset = Effect.fn("db.reset")(function* (flags: DbResetFlags) { if (toml.appliedRemote !== undefined) { yield* output.raw(`Loading config override: [remotes.${toml.appliedRemote}]\n`, "stderr"); } + yield* warnDeprecatedOrioleDBVersionEnv(toml.envLookup); const vaultSecrets = toml.vault; // Prompt (default false) → cancel, then reset everything. diff --git a/apps/cli/src/commands/db/schema/declarative/generate/SIDE_EFFECTS.md b/apps/cli/src/commands/db/schema/declarative/generate/SIDE_EFFECTS.md index 30ed404763..a8fff1d00e 100644 --- a/apps/cli/src/commands/db/schema/declarative/generate/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/db/schema/declarative/generate/SIDE_EFFECTS.md @@ -41,15 +41,16 @@ formatting without disabling safe compaction. ## Environment Variables -| Variable | Purpose | Required? | -| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | -| `SUPABASE_ACCESS_TOKEN` | auth token for `--linked` | no | -| `DB_PASSWORD` | password for `--linked` / `--db-url` | no | -| `SUPABASE_HOME` | overrides the `~/.supabase` root (access token and other CLI state) | no | -| `PGDELTA_DEBUG` | bundled-engine debug artifacts | no | -| `SUPABASE_SERVICES_HOSTNAME` | local DB host for `--local` | no | -| `DOCKER_HOST` | tcp daemon host used as the local DB host fallback | no | -| `SUPABASE_USE_SLIM_IMAGES` | resolves the expected local `db` image for the stale-container guard from the slim `ghcr.io/supabase/cli` builds (`true`/`1` enable); majors 13/15 use `15.14.1.167` when the flag is on; historical pins, PG14, OrioleDB tags the catalog does not pin, and flag-off `15.8.1.085` stay on docker.io | no | +| Variable | Purpose | Required? | +| ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | +| `SUPABASE_ACCESS_TOKEN` | auth token for `--linked` | no | +| `DB_PASSWORD` | password for `--linked` / `--db-url` | no | +| `SUPABASE_HOME` | overrides the `~/.supabase` root (access token and other CLI state) | no | +| `PGDELTA_DEBUG` | bundled-engine debug artifacts | no | +| `SUPABASE_SERVICES_HOSTNAME` | local DB host for `--local` | no | +| `DOCKER_HOST` | tcp daemon host used as the local DB host fallback | no | +| `SUPABASE_USE_SLIM_IMAGES` | resolves the expected local `db` image for the stale-container guard from the slim `ghcr.io/supabase/cli` builds (`true`/`1` enable); majors 13/15 use `15.14.1.167` when the flag is on; historical pins, PG14, OrioleDB tags the catalog does not pin, and flag-off `15.8.1.085` stay on docker.io | no | +| `SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION` | deprecated alias of `SUPABASE_DB_ORIOLEDB_VERSION`, used only while that is unset; when set, stderr gets `WARN: SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION is deprecated and will be removed in v3, use SUPABASE_DB_ORIOLEDB_VERSION instead.` | no | ## Exit Codes diff --git a/apps/cli/src/commands/db/schema/declarative/generate/generate.handler.ts b/apps/cli/src/commands/db/schema/declarative/generate/generate.handler.ts index e173023860..229e3603d2 100644 --- a/apps/cli/src/commands/db/schema/declarative/generate/generate.handler.ts +++ b/apps/cli/src/commands/db/schema/declarative/generate/generate.handler.ts @@ -15,10 +15,12 @@ import { loadProjectEnv, readDbToml, resolveDeclarativeDir, + warnDeprecatedOrioleDBVersionEnv, } from "../../../../../command-internal/db-config.toml-read.ts"; import { LinkedProjectCache } from "../../../../../telemetry/linked-project-cache.service.ts"; import { TelemetryState } from "../../../../../telemetry/telemetry-state.service.ts"; import { listLocalMigrations } from "../../../../../command-internal/migration-list.ts"; +import { withSingleOrioleDBVersionEnvWarning } from "../../../../../command-internal/local-config-values.ts"; import { isPgDeltaDebugEnabled, resolvePgDeltaProjectId, @@ -73,6 +75,7 @@ export const dbSchemaDeclarativeGenerate = Effect.fn("db.schema.declarative.gene yield* Effect.gen(function* () { const baseToml = yield* readDbToml(fs, path, cliSettings.workdir); + yield* warnDeprecatedOrioleDBVersionEnv(baseToml.envLookup); // Gate before the mutex check below (see `requirePgDelta`'s doc comment for why), and on the // base config: a remote `experimental.pgdelta.enabled = true` must not enable a // base-disabled command without `--experimental`. @@ -263,6 +266,9 @@ export const dbSchemaDeclarativeGenerate = Effect.fn("db.schema.declarative.gene yield* warnPreservedUnmanagedDeclarativeFiles(declarativeDirRel, written); yield* output.raw(declarativeSchemaWrittenLine(declarativeDirRel), "stderr"); }).pipe( + // Starting or resetting the local database loads the project context, which would print the + // OrioleDB env WARN again. + withSingleOrioleDBVersionEnvWarning, // Writes the linked-project cache for any resolved ref, on success and failure. Only // explicit `--linked` resolves a ref here; the cache layer no-ops when the file exists, the // token is missing, or the GET is non-200. diff --git a/apps/cli/src/commands/db/schema/declarative/generate/generate.integration.test.ts b/apps/cli/src/commands/db/schema/declarative/generate/generate.integration.test.ts index fa89590112..c2455eb972 100644 --- a/apps/cli/src/commands/db/schema/declarative/generate/generate.integration.test.ts +++ b/apps/cli/src/commands/db/schema/declarative/generate/generate.integration.test.ts @@ -1187,6 +1187,46 @@ describe("db schema declarative generate integration", () => { }).pipe(Effect.provide(s.layer)); }); + it.effect( + "smart mode: warns once about SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION when the local reset reloads the config", + () => { + const s = setup(tmp.current, { + experimental: true, + stdinIsTty: true, + yes: true, + promptSelectResponses: ["local"], + }); + const consoleErrors: Array = []; + const errorSpy = vi.spyOn(console, "error").mockImplementation((...args) => { + consoleErrors.push(args.map((a) => String(a)).join(" ")); + }); + return Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + yield* seedMigration(tmp.current); + yield* fs.writeFileString( + path.join(tmp.current, "supabase", "config.toml"), + 'project_id = "test"\n', + ); + yield* fs.writeFileString( + path.join(tmp.current, "supabase", ".env"), + "SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION=17.11.0.002\n", + ); + yield* dbSchemaDeclarativeGenerate(flags()); + expect(localResetCreateArgs(s.child.spawned)).not.toBeUndefined(); + const warning = + "WARN: SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION is deprecated and will be removed in v3, use SUPABASE_DB_ORIOLEDB_VERSION instead."; + const stderrLines = [ + ...s.out.rawChunks + .filter((c) => c.stream === "stderr") + .flatMap((c) => c.text.split("\n")), + ...consoleErrors, + ]; + expect(stderrLines.filter((line) => line === warning)).toHaveLength(1); + }).pipe(Effect.provide(s.layer), Effect.ensuring(Effect.sync(() => errorSpy.mockRestore()))); + }, + ); + it.effect("smart mode: forwards --network-id to the local reset", () => { const s = setup(tmp.current, { experimental: true, diff --git a/apps/cli/src/commands/db/schema/declarative/sync/sync.handler.ts b/apps/cli/src/commands/db/schema/declarative/sync/sync.handler.ts index 4e09fc904e..9f91ea660c 100644 --- a/apps/cli/src/commands/db/schema/declarative/sync/sync.handler.ts +++ b/apps/cli/src/commands/db/schema/declarative/sync/sync.handler.ts @@ -1,5 +1,6 @@ import { Clock, DateTime, Effect, FileSystem, Option, Path, Ref, Result } from "effect"; +import { withSingleOrioleDBVersionEnvWarning } from "../../../../../command-internal/local-config-values.ts"; import { DnsResolverFlag, resolveExperimentalWithProjectEnv, @@ -85,7 +86,7 @@ const DEFAULT_SYNC_NAME = "declarative_sync"; const formatTimestamp = (millis: number): string => DateTime.formatIso(DateTime.makeUnsafe(millis)).replace(/\D/g, "").slice(0, 14); -export const dbSchemaDeclarativeSync = Effect.fn("db.schema.declarative.sync")(function* ( +const runDbSchemaDeclarativeSync = Effect.fn("db.schema.declarative.sync")(function* ( flags: DbSchemaDeclarativeSyncFlags, ) { const output = yield* Output; @@ -694,6 +695,11 @@ const declarativeDirHasFiles = Effect.fnUntraced(function* ( }); /** Connects once and applies the ordered migration files. */ +// Starting or resetting the local database loads the project context, which would print the +// OrioleDB env WARN again. +export const dbSchemaDeclarativeSync = (flags: DbSchemaDeclarativeSyncFlags) => + withSingleOrioleDBVersionEnvWarning(runDbSchemaDeclarativeSync(flags)); + const applyMigrationToLocal = Effect.fn("db.schema.declarative.sync.apply")(function* ( local: { host: string; port: number; password: string; dnsResolver: "native" | "https" }, migrationPaths: ReadonlyArray, diff --git a/apps/cli/src/commands/db/start/SIDE_EFFECTS.md b/apps/cli/src/commands/db/start/SIDE_EFFECTS.md index e9ac097bef..bd093e1e8e 100644 --- a/apps/cli/src/commands/db/start/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/db/start/SIDE_EFFECTS.md @@ -131,6 +131,7 @@ API request over the active context's local unix socket / named pipe: | `SUPABASE_DB_HEALTH_TIMEOUT` | overrides `db.health_timeout` | no | | `SUPABASE_DB_SETTINGS_*` | overrides individual `[db.settings]` fields | no | | `SUPABASE_DB_ORIOLEDB_VERSION` | overrides `db.orioledb_version` (image + env) | no | +| `SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION` | deprecated alias of `SUPABASE_DB_ORIOLEDB_VERSION`, used only while that is unset; when set, stderr gets `WARN: SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION is deprecated and will be removed in v3, use SUPABASE_DB_ORIOLEDB_VERSION instead.` | no | | `SUPABASE_EXPERIMENTAL_S3_{HOST,REGION,ACCESS_KEY,SECRET_KEY}` | OrioleDB S3 env overrides | no | | `SUPABASE_REALTIME_ENABLED` | gates the fresh-volume realtime migrate job | no | | `SUPABASE_REALTIME_IP_VERSION` / `_MAX_HEADER_LENGTH` | realtime migrate job env overrides | no | diff --git a/apps/cli/src/commands/db/start/start.integration.test.ts b/apps/cli/src/commands/db/start/start.integration.test.ts index 2819f8661e..2c80574d04 100644 --- a/apps/cli/src/commands/db/start/start.integration.test.ts +++ b/apps/cli/src/commands/db/start/start.integration.test.ts @@ -1468,7 +1468,7 @@ describe("db start", () => { yield* dbStart(DEFAULT_FLAGS).pipe(Effect.provide(layer)); const inbucketWarnings = warnings.filter((m) => m.includes( - "WARN: config section [inbucket] is deprecated. Please use [local_smtp] instead.", + "WARN: config section [inbucket] is deprecated and will be removed in v3, use [local_smtp] instead.", ), ); expect(inbucketWarnings).toHaveLength(1); @@ -1476,6 +1476,44 @@ describe("db start", () => { }, ); + it.effect.each([ + { + with: "alone", + env: { SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION: "17.11.0.002" }, + expectedTag: "17.11.0.002-orioledb", + }, + { + with: "and SUPABASE_DB_ORIOLEDB_VERSION (wins)", + env: { + SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION: "17.11.0.002", + SUPABASE_DB_ORIOLEDB_VERSION: "17.11.0.003", + }, + expectedTag: "17.11.0.003-orioledb", + }, + ])( + "selects the OrioleDB image with SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION $with, warning once that it is removed in v3", + ({ env, expectedTag }) => { + for (const [name, value] of Object.entries(env)) vi.stubEnv(name, value); + const { layer, child } = setup({ route: freshVolumeRoute(defaultRoute()) }); + const warnings: Array = []; + const errorSpy = vi.spyOn(console, "error").mockImplementation((...args) => { + warnings.push(args.map((a) => String(a)).join(" ")); + }); + return Effect.gen(function* () { + yield* dbStart(DEFAULT_FLAGS).pipe(Effect.provide(layer)); + const image = createArgs(child.spawned)?.find((arg) => arg.includes("supabase/postgres:")); + expect(image).toContain(expectedTag); + expect( + warnings.filter( + (m) => + m === + "WARN: SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION is deprecated and will be removed in v3, use SUPABASE_DB_ORIOLEDB_VERSION instead.", + ), + ).toHaveLength(1); + }).pipe(Effect.ensuring(Effect.sync(() => errorSpy.mockRestore()))); + }, + ); + it.effect("fails on a malformed auth duration field even when the db is already running", () => { const { layer, out } = setup({ configContents: 'project_id = "test"\n[auth.email]\nmax_frequency = "not-a-duration"\n', diff --git a/apps/cli/src/commands/experimental/stack/start/start.handler.ts b/apps/cli/src/commands/experimental/stack/start/start.handler.ts index 0ffdd6b979..20cbe2b796 100644 --- a/apps/cli/src/commands/experimental/stack/start/start.handler.ts +++ b/apps/cli/src/commands/experimental/stack/start/start.handler.ts @@ -52,14 +52,11 @@ import { projectCatalogOverlay, } from "../../../../command-internal/stack-bootstrap.ts"; import { stackStorageCredentialsFor } from "../../../../command-internal/stack-storage.ts"; -import { - hasConfiguredBuckets, - SeedConfigLoadError, - seedBucketsRun, -} from "../../../../command-internal/seed-buckets.ts"; +import { hasConfiguredBuckets, seedBucketsRun } from "../../../../command-internal/seed-buckets.ts"; import { loadLocalProjectContext } from "../../../../command-internal/local-project-context.ts"; import { loadStackConfig, + StackConfigError, stackEndpointSetting, stackMajorVersionSetting, stackOrioledbVersionSetting, @@ -200,10 +197,14 @@ const stackAcquireError = ( const loadStartConfig = Effect.fn("experimental.stack.start.loadConfig")( function* (projectRoot: string, fs: FileSystem.FileSystem, path: Path.Path) { - const config = yield* loadStackConfig(projectRoot); + const context = yield* loadLocalProjectContext( + projectRoot, + (message) => new StackConfigError({ message }), + ); + const config = yield* loadStackConfig(projectRoot, { context }); const keys = yield* config.keys; const toml = yield* readDbToml(fs, path, projectRoot); - return { config, keys, toml }; + return { context, config, keys, toml }; }, Effect.mapError( (error) => @@ -261,9 +262,9 @@ const settingKeyLabel = ( setting: StackEndpointSetting, projectEnvValues: Readonly>, ): string => - envOverride(setting.envVar, undefined, projectEnvValues) !== undefined - ? setting.envVar - : formatConfigPath(setting.configPath); + [setting.envVar, setting.deprecatedEnvVar].find( + (name) => name !== undefined && envOverride(name, undefined, projectEnvValues) !== undefined, + ) ?? formatConfigPath(setting.configPath); /** * One setting behind an incompatible path, reported as the JSON/stream-json error envelope's @@ -559,10 +560,9 @@ export const stackStart = Effect.fn("experimental.stack.start")(function* (flags StepDown: (stepDown) => output.info(stepDown.message), NotNeeded: () => Effect.void, }); - const configBeforeCreate = - target.id === undefined || !target.hostRunning - ? yield* loadStartConfig(target.projectRoot, fs, path) - : undefined; + // Every project-context load prints the config deprecation WARNs, so it happens at most once. + const startConfig = yield* Effect.cached(loadStartConfig(target.projectRoot, fs, path)); + if (target.id === undefined || !target.hostRunning) yield* startConfig; if (target.id === undefined) yield* ensurePostgresUser; const stateRoot = path.join(settings.supabaseHome, "stacks"); const cacheRoot = path.join(settings.supabaseHome, "cache", "stack"); @@ -603,7 +603,7 @@ export const stackStart = Effect.fn("experimental.stack.start")(function* (flags // configuration that cannot resolve only fails a start whose owner was not running. const resolveForReplan = (stackId: string) => Effect.gen(function* () { - const loaded = configBeforeCreate ?? (yield* loadStartConfig(target.projectRoot, fs, path)); + const loaded = yield* startConfig; return yield* resolveRequested(stackId, loaded.config); }); const requestedForReplan = @@ -801,8 +801,7 @@ export const stackStart = Effect.fn("experimental.stack.start")(function* (flags message: "A standalone database exists outside the saved stack composition", suggestion: "Destroy the standalone database before starting this stack.", }); - const { config, keys, toml } = - configBeforeCreate ?? (yield* loadStartConfig(target.projectRoot, fs, path)); + const { context, config, keys, toml } = yield* startConfig; // Reuses the creations already resolved for the re-plan above instead of reading the // Functions dotenv a second time; only a new or already-running stack has none yet. const requested = requestedForReplan ?? (yield* resolveRequested(stack.id, config)); @@ -1031,10 +1030,6 @@ export const stackStart = Effect.fn("experimental.stack.start")(function* (flags if (initialComposition) { const storage = members.find((instance) => instance.service === "storage"); if (storage !== undefined) { - const context = yield* loadLocalProjectContext( - target.projectRoot, - (message) => new SeedConfigLoadError({ message }), - ); if (hasConfiguredBuckets(context.config)) { yield* Effect.annotateCurrentSpan({ "stack.storage_seeded": true }); yield* storage.start.pipe( diff --git a/apps/cli/src/commands/experimental/stack/start/start.integration.test.ts b/apps/cli/src/commands/experimental/stack/start/start.integration.test.ts index c82c6e5df6..2409dcba09 100644 --- a/apps/cli/src/commands/experimental/stack/start/start.integration.test.ts +++ b/apps/cli/src/commands/experimental/stack/start/start.integration.test.ts @@ -1,6 +1,7 @@ import { generateKeyPairSync } from "node:crypto"; import { BunServices } from "@effect/platform-bun"; import { describe, expect, it } from "@effect/vitest"; +import { vi } from "vitest"; import { Deferred, Effect, @@ -2117,6 +2118,7 @@ describe("experimental stack start", () => { before: "[db]\nmajor_version = 15\n", after: `[db]\norioledb_version = "${orioledb}"\n`, env: undefined, + envVar: "SUPABASE_DB_ORIOLEDB_VERSION", changes: `[db] major_version: saved 15, requested 17; [db] orioledb_version: saved unset, requested ${orioledb}`, revert: "Revert the settings listed to their saved values", }, @@ -2125,9 +2127,19 @@ describe("experimental stack start", () => { before: "", after: "", env: orioledb, + envVar: "SUPABASE_DB_ORIOLEDB_VERSION", changes: `SUPABASE_DB_ORIOLEDB_VERSION: saved unset, requested ${orioledb}`, revert: "Revert SUPABASE_DB_ORIOLEDB_VERSION to its saved value", }, + { + name: "the deprecated SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION when it alone switches a saved stock stack to OrioleDB", + before: "", + after: "", + env: orioledb, + envVar: "SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION", + changes: `SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION: saved unset, requested ${orioledb}`, + revert: "Revert SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION to its saved value", + }, ]) it.effect(`names ${target.name}`, () => Effect.gen(function* () { @@ -2147,7 +2159,7 @@ describe("experimental stack start", () => { ); const error = yield* target.env === undefined ? restart - : withEnvVar("SUPABASE_DB_ORIOLEDB_VERSION", target.env, restart); + : withEnvVar(target.envVar, target.env, restart); expect(error).toMatchObject({ reason: "invalid-config", @@ -2159,6 +2171,39 @@ describe("experimental stack start", () => { }).pipe(Effect.provide(BunServices.layer)), ); + it.effect( + "warns once about SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION on a first start that includes storage", + () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const root = yield* fs.makeTempDirectoryScoped({ prefix: "stack-start-orioledb-alias-" }); + yield* fs.makeDirectory(`${root}/supabase`, { recursive: true }); + yield* fs.writeFileString(`${root}/supabase/config.toml`, 'project_id = "alias"\n'); + yield* fs.writeFileString( + `${root}/supabase/.env`, + `SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION=${orioledb}\n`, + ); + const fixture = fakeStack(); + const consoleErrors: Array = []; + const errorSpy = vi.spyOn(console, "error").mockImplementation((...args) => { + consoleErrors.push(args.map((a) => String(a)).join(" ")); + }); + yield* stackStart(flags()).pipe( + Effect.provide(layers(root, fixture)), + Effect.ensuring(Effect.sync(() => errorSpy.mockRestore())), + ); + const services = yield* fixture.stack.services.list; + expect(services.map(({ service }) => service)).toContain("storage"); + expect( + consoleErrors.filter( + (line) => + line === + "WARN: SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION is deprecated and will be removed in v3, use SUPABASE_DB_ORIOLEDB_VERSION instead.", + ), + ).toHaveLength(1); + }).pipe(Effect.provide(BunServices.layer)), + ); + it.effect("offers no revert to an OrioleDB build the catalog no longer pins", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; diff --git a/apps/cli/src/commands/functions/deploy/SIDE_EFFECTS.md b/apps/cli/src/commands/functions/deploy/SIDE_EFFECTS.md index 834397acf9..854be67649 100644 --- a/apps/cli/src/commands/functions/deploy/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/functions/deploy/SIDE_EFFECTS.md @@ -101,6 +101,7 @@ The `--output`/`-o` flag does not change deploy output. - Requires a linked project unless `--project-ref` is provided. - Bundles locally with Docker by default (`--use-docker` defaults to true and is hidden); `--use-api` selects server-side bundling, and a stopped Docker daemon falls back to it after a `WARNING: Docker is not running`. - Local Docker bundling mounts existing local values declared under an import map's `scopes` read-only, including targets outside the nearest Git root; each such out-of-root mount prints a `WARN` naming the host path. The mounted target itself is bound as declared; imports reached from inside an out-of-root target are not additionally bound. API source uploads retain their existing source-root restrictions. +- A Function resolved through a per-function `import_map.json` (no `deno.json`/`deno.jsonc` beside it, no configured or `--import-map` override) prints `WARNING: import_map.json is deprecated and will be removed in v3, use deno.json instead. Functions using it: , ...` to stderr once per deploy; stdout is unaffected. - `--use-api`, `--use-docker`, and `--legacy-bundle` are mutually exclusive deploy modes. - `--prune` deletes deployed Functions that are not present locally after a confirmation prompt; global `--yes` skips the prompt. diff --git a/apps/cli/src/commands/functions/deploy/deploy.integration.test.ts b/apps/cli/src/commands/functions/deploy/deploy.integration.test.ts index c1d2ee5864..74db96f0fb 100644 --- a/apps/cli/src/commands/functions/deploy/deploy.integration.test.ts +++ b/apps/cli/src/commands/functions/deploy/deploy.integration.test.ts @@ -380,6 +380,61 @@ describe("functions deploy", () => { }).pipe(Effect.provide(layer), Effect.ensuring(removeTempRoot)); }); + it.effect("warns on stderr that a per-function import_map.json is removed in v3", () => { + const out = mockOutput({ format: "text" }); + const api = mockCommandPlatformApi({ + handler: (request) => { + if (request.method === "GET") { + return Effect.succeed(jsonResponse(request, 200, [])); + } + return Effect.succeed( + jsonResponse(request, 201, { + id: "function-id", + slug: "hello-world", + name: "hello-world", + status: "ACTIVE", + version: 2, + created_at: 1_687_423_025_152, + updated_at: 1_687_423_025_152, + verify_jwt: true, + import_map: true, + entrypoint_path: "supabase/functions/hello-world/index.ts", + import_map_path: "supabase/functions/hello-world/import_map.json", + }), + ); + }, + }); + const layer = Layer.mergeAll( + buildTestRuntime({ + out, + api, + cliSettings: mockCommandSettings({ workdir: tempRoot.current }), + runtimeInfo: mockRuntimeInfo({ cwd: tempRoot.current }), + }), + Layer.succeed(YesFlag, false), + Stdio.layerTest({ + args: Effect.succeed(["functions", "deploy", "hello-world", "--use-api"]), + }), + ); + + return Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + yield* writeCliConfig(tempRoot.current); + yield* writeLocalFunction(tempRoot.current, "hello-world"); + const functionDir = path.join(tempRoot.current, "supabase", "functions", "hello-world"); + yield* fs.remove(path.join(functionDir, "deno.json")); + yield* fs.writeFileString(path.join(functionDir, "import_map.json"), '{"imports":{}}'); + + yield* functionsDeploy(baseFlags); + + expect(out.stderrText).toContain( + "WARNING: import_map.json is deprecated and will be removed in v3, use deno.json instead. Functions using it: hello-world\n", + ); + expect(out.stdoutText).not.toContain("import_map.json"); + }).pipe(Effect.provide(layer), Effect.ensuring(removeTempRoot)); + }); + it.effect("loads project config from the resolved workdir", () => { const callerDir = `${tempRoot.current}/caller`; const out = mockOutput({ format: "text" }); diff --git a/apps/cli/src/commands/gen/types/SIDE_EFFECTS.md b/apps/cli/src/commands/gen/types/SIDE_EFFECTS.md index 5db2ed9e28..ea0a319e33 100644 --- a/apps/cli/src/commands/gen/types/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/gen/types/SIDE_EFFECTS.md @@ -76,20 +76,21 @@ way `--db-url` does. ## Environment Variables -| Variable | Purpose | Required? | -| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `SUPABASE_ACCESS_TOKEN` | auth token for linked/project-id mode | no (falls back to keyring → `~/.supabase/access-token`) | -| `SUPABASE_PROJECT_ID` | local Docker container and network project ID | no (falls back to the workdir name) | -| `SUPABASE_DB_PORT` | local database port | no (defaults to `54322`) | -| `SUPABASE_DB_MAJOR_VERSION` | local PostgreSQL major version | no (defaults to `17`) | -| `SUPABASE_API_SCHEMAS` | local schemas used when `--schema` is omitted | no (defaults to `public,graphql_public`) | -| `SUPABASE_ENV` | selects nested dotenv files for local generation and `--db-url` resolution | no (defaults to `development`) | -| `PGHOST`, `PGPORT`, `PGUSER`, `PGPASSWORD`, `PGDATABASE`, `PGSSLMODE`, `PGSSLROOTCERT`, `PGSSLCERT`, `PGSSLKEY`, `PGSSLPASSWORD`, `PGCONNECT_TIMEOUT`, `PGSERVICE`, `PGSERVICEFILE`, `PGAPPNAME` | libpq connection-setting fallbacks `--db-url` consults for whatever the DSN itself omits | no — each applies only when the DSN, a resolved `service`, and (where applicable) `[db].password` leave the setting unset | -| `SUPABASE_PROFILE` | built-in profile name or YAML file path | no (falls back to `~/.supabase/profile` -> `supabase`) | -| `SUPABASE_DB_PASSWORD` | database password for `--local` and the `--linked` workdir project | no (defaults to `postgres`; **ignored** for ad-hoc `--project-id`, which always mints a temporary login role) | -| `SUPABASE_SERVICES_HOSTNAME` | host used to reach the local database on the legacy Docker Compose stack | no (defaults to `127.0.0.1`) | -| `SUPABASE_WORKDIR` | working directory `supabase/config.toml`/`config.json` is read from (`--workdir` takes priority) | no — when unset, the CLI walks up from cwd looking for `supabase/config.toml`; when SET (flag or env) the directory is used exactly as given and **no ancestor is searched** | -| `PATH`, `PATHEXT`, `ComSpec` | find the tool of an out-of-process `--lang`; `PATHEXT` and `ComSpec` matter on Windows only | no (`PATHEXT` defaults to `.COM;.EXE;.BAT;.CMD`, `ComSpec` to `cmd.exe`; without `PATH` the tool is reported as not installed) | +| Variable | Purpose | Required? | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `SUPABASE_ACCESS_TOKEN` | auth token for linked/project-id mode | no (falls back to keyring → `~/.supabase/access-token`) | +| `SUPABASE_PROJECT_ID` | local Docker container and network project ID | no (falls back to the workdir name) | +| `SUPABASE_DB_PORT` | local database port | no (defaults to `54322`) | +| `SUPABASE_DB_MAJOR_VERSION` | local PostgreSQL major version | no (defaults to `17`) | +| `SUPABASE_API_SCHEMAS` | local schemas used when `--schema` is omitted | no (defaults to `public,graphql_public`) | +| `SUPABASE_ENV` | selects nested dotenv files for local generation and `--db-url` resolution | no (defaults to `development`) | +| `PGHOST`, `PGPORT`, `PGUSER`, `PGPASSWORD`, `PGDATABASE`, `PGSSLMODE`, `PGSSLROOTCERT`, `PGSSLCERT`, `PGSSLKEY`, `PGSSLPASSWORD`, `PGCONNECT_TIMEOUT`, `PGSERVICE`, `PGSERVICEFILE`, `PGAPPNAME` | libpq connection-setting fallbacks `--db-url` consults for whatever the DSN itself omits | no — each applies only when the DSN, a resolved `service`, and (where applicable) `[db].password` leave the setting unset | +| `SUPABASE_PROFILE` | built-in profile name or YAML file path | no (falls back to `~/.supabase/profile` -> `supabase`) | +| `SUPABASE_DB_PASSWORD` | database password for `--local` and the `--linked` workdir project | no (defaults to `postgres`; **ignored** for ad-hoc `--project-id`, which always mints a temporary login role) | +| `SUPABASE_SERVICES_HOSTNAME` | host used to reach the local database on the legacy Docker Compose stack | no (defaults to `127.0.0.1`) | +| `SUPABASE_WORKDIR` | working directory `supabase/config.toml`/`config.json` is read from (`--workdir` takes priority) | no — when unset, the CLI walks up from cwd looking for `supabase/config.toml`; when SET (flag or env) the directory is used exactly as given and **no ancestor is searched** | +| `PATH`, `PATHEXT`, `ComSpec` | find the tool of an out-of-process `--lang`; `PATHEXT` and `ComSpec` matter on Windows only | no (`PATHEXT` defaults to `.COM;.EXE;.BAT;.CMD`, `ComSpec` to `cmd.exe`; without `PATH` the tool is reported as not installed) | +| `SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION` | deprecated alias of `SUPABASE_DB_ORIOLEDB_VERSION`, used only while that is unset; when set, stderr gets `WARN: SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION is deprecated and will be removed in v3, use SUPABASE_DB_ORIOLEDB_VERSION instead.` | no | ## Exit Codes diff --git a/apps/cli/src/commands/gen/types/types.handler.ts b/apps/cli/src/commands/gen/types/types.handler.ts index 03158cc590..7d600165da 100644 --- a/apps/cli/src/commands/gen/types/types.handler.ts +++ b/apps/cli/src/commands/gen/types/types.handler.ts @@ -26,7 +26,10 @@ import { } from "../../../command-internal/db-config.service.ts"; import type { DbConfigFlags } from "../../../command-internal/db-config.types.ts"; import { poolerConfigFromConnectionString } from "../../../command-internal/db-config.parse.ts"; -import { readDbToml } from "../../../command-internal/db-config.toml-read.ts"; +import { + readDbToml, + warnDeprecatedOrioleDBVersionEnv, +} from "../../../command-internal/db-config.toml-read.ts"; import { getHostname } from "../../../command-internal/hostname.ts"; import type { DbConnectError } from "../../../command-internal/db-connection.errors.ts"; import type { PgConnInput } from "../../../command-internal/db-connection.service.ts"; @@ -606,6 +609,7 @@ export const genTypes = Effect.fn("gen.types")(function* (flags: GenTypesFlags) "stack.backend": backend.kind, }); const config = yield* readDbToml(fs, path, cliSettings.workdir); + yield* warnDeprecatedOrioleDBVersionEnv(config.envLookup); const projectEnvValues = Object.fromEntries( Object.entries(config.projectEnv).filter(([key]) => key !== "SUPABASE_DB_PASSWORD"), ); diff --git a/apps/cli/src/commands/migration/down/SIDE_EFFECTS.md b/apps/cli/src/commands/migration/down/SIDE_EFFECTS.md index 9f491d8124..9833dc7379 100644 --- a/apps/cli/src/commands/migration/down/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/migration/down/SIDE_EFFECTS.md @@ -22,10 +22,11 @@ ## Environment Variables -| Variable | Purpose | Required? | -| ------------------------ | --------------------------------------------------------------------------------- | ------------------------------------------------------- | -| `SUPABASE_ACCESS_TOKEN` | auth token for `--linked` mode | no (falls back to keyring → `~/.supabase/access-token`) | -| `DOTENV_PRIVATE_KEY[_*]` | dotenvx private key(s) to decrypt `encrypted:` `[db.vault]` secrets before upsert | no (required only if a `[db.vault]` value is encrypted) | +| Variable | Purpose | Required? | +| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------- | +| `SUPABASE_ACCESS_TOKEN` | auth token for `--linked` mode | no (falls back to keyring → `~/.supabase/access-token`) | +| `DOTENV_PRIVATE_KEY[_*]` | dotenvx private key(s) to decrypt `encrypted:` `[db.vault]` secrets before upsert | no (required only if a `[db.vault]` value is encrypted) | +| `SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION` | deprecated alias of `SUPABASE_DB_ORIOLEDB_VERSION`, used only while that is unset; when set, stderr gets `WARN: SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION is deprecated and will be removed in v3, use SUPABASE_DB_ORIOLEDB_VERSION instead.` | no | ## Exit Codes diff --git a/apps/cli/src/commands/migration/down/down.handler.ts b/apps/cli/src/commands/migration/down/down.handler.ts index bd1ee18ff1..3a58e5abc5 100644 --- a/apps/cli/src/commands/migration/down/down.handler.ts +++ b/apps/cli/src/commands/migration/down/down.handler.ts @@ -10,7 +10,11 @@ import { Output } from "../../../shared/output/output.service.ts"; import { CommandSettings } from "../../../config/command-settings.service.ts"; import { ProjectRefResolver } from "../../../config/project-ref.service.ts"; import { aqua, bold, yellow } from "../../../command-internal/colors.ts"; -import { loadProjectEnv, readDbToml } from "../../../command-internal/db-config.toml-read.ts"; +import { + loadProjectEnv, + readDbToml, + warnDeprecatedOrioleDBVersionEnv, +} from "../../../command-internal/db-config.toml-read.ts"; import { DbConfigResolver } from "../../../command-internal/db-config.service.ts"; import { DbConnection } from "../../../command-internal/db-connection.service.ts"; import { resolveDbTargetFlags } from "../../../command-internal/db-target-flags.ts"; @@ -83,6 +87,7 @@ const runDown = Effect.fnUntraced(function* ( const ref = Option.getOrUndefined(cfg.ref ?? Option.none()); const toml = yield* readDbToml(fs, path, cliSettings.workdir, ref); + yield* warnDeprecatedOrioleDBVersionEnv(toml.envLookup); yield* Effect.scoped( Effect.gen(function* () { diff --git a/apps/cli/src/commands/migration/up/SIDE_EFFECTS.md b/apps/cli/src/commands/migration/up/SIDE_EFFECTS.md index 8a700a4c12..7069137693 100644 --- a/apps/cli/src/commands/migration/up/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/migration/up/SIDE_EFFECTS.md @@ -22,10 +22,11 @@ ## Environment Variables -| Variable | Purpose | Required? | -| ------------------------ | --------------------------------------------------------------------------------- | ------------------------------------------------------- | -| `SUPABASE_ACCESS_TOKEN` | auth token for `--linked` mode | no (falls back to keyring → `~/.supabase/access-token`) | -| `DOTENV_PRIVATE_KEY[_*]` | dotenvx private key(s) to decrypt `encrypted:` `[db.vault]` secrets before upsert | no (required only if a `[db.vault]` value is encrypted) | +| Variable | Purpose | Required? | +| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------- | +| `SUPABASE_ACCESS_TOKEN` | auth token for `--linked` mode | no (falls back to keyring → `~/.supabase/access-token`) | +| `DOTENV_PRIVATE_KEY[_*]` | dotenvx private key(s) to decrypt `encrypted:` `[db.vault]` secrets before upsert | no (required only if a `[db.vault]` value is encrypted) | +| `SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION` | deprecated alias of `SUPABASE_DB_ORIOLEDB_VERSION`, used only while that is unset; when set, stderr gets `WARN: SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION is deprecated and will be removed in v3, use SUPABASE_DB_ORIOLEDB_VERSION instead.` | no | ## Exit Codes diff --git a/apps/cli/src/commands/migration/up/up.handler.ts b/apps/cli/src/commands/migration/up/up.handler.ts index 6fbb4b1d45..bfd54f5450 100644 --- a/apps/cli/src/commands/migration/up/up.handler.ts +++ b/apps/cli/src/commands/migration/up/up.handler.ts @@ -6,7 +6,10 @@ import { Output } from "../../../shared/output/output.service.ts"; import { CommandSettings } from "../../../config/command-settings.service.ts"; import { ProjectRefResolver } from "../../../config/project-ref.service.ts"; import { bold } from "../../../command-internal/colors.ts"; -import { readDbToml } from "../../../command-internal/db-config.toml-read.ts"; +import { + readDbToml, + warnDeprecatedOrioleDBVersionEnv, +} from "../../../command-internal/db-config.toml-read.ts"; import { DbConfigResolver } from "../../../command-internal/db-config.service.ts"; import { DbConnection } from "../../../command-internal/db-connection.service.ts"; import { resolveDbTargetFlags } from "../../../command-internal/db-target-flags.ts"; @@ -70,6 +73,7 @@ const runUp = Effect.fnUntraced(function* ( }); const ref = Option.getOrUndefined(cfg.ref ?? Option.none()); const toml = yield* readDbToml(fs, path, cliSettings.workdir, ref); + yield* warnDeprecatedOrioleDBVersionEnv(toml.envLookup); yield* Effect.annotateCurrentSpan({ "db.conn_type": target.connType ?? "local", "db.is_local": cfg.isLocal, diff --git a/apps/cli/src/commands/migration/up/up.integration.test.ts b/apps/cli/src/commands/migration/up/up.integration.test.ts index b7510a90b9..aa125df69b 100644 --- a/apps/cli/src/commands/migration/up/up.integration.test.ts +++ b/apps/cli/src/commands/migration/up/up.integration.test.ts @@ -310,6 +310,25 @@ describe("migration up", () => { }).pipe(Effect.provide(layer)); }); + it.effect( + "warns once on stderr that SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION is removed in v3", + () => { + const { layer, out } = setup(tmp.current); + return Effect.gen(function* () { + yield* writeProjectFile( + tmp.current, + ".env", + "SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION=17.11.0.002\n", + ); + yield* migrationUp(flags()); + const warning = + "WARN: SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION is deprecated and will be removed in v3, use SUPABASE_DB_ORIOLEDB_VERSION instead."; + expect(out.stderrText.split("\n").filter((line) => line === warning)).toHaveLength(1); + expect(out.stdoutText).not.toContain(warning); + }).pipe(Effect.provide(layer)); + }, + ); + it.effect("surfaces an apply failure", () => { const { layer } = setup(tmp.current, { remote: [], failApply: true }); return Effect.gen(function* () { diff --git a/apps/cli/src/commands/services/SIDE_EFFECTS.md b/apps/cli/src/commands/services/SIDE_EFFECTS.md index 63b4d68c7b..1588e49944 100644 --- a/apps/cli/src/commands/services/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/services/SIDE_EFFECTS.md @@ -42,13 +42,15 @@ Tenant calls send `apikey: ` and additionally ## Environment Variables -| Variable | Purpose | Required? | -| ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | -| `SUPABASE_ACCESS_TOKEN` | auth token for Management API linked-version checks | no (falls back to keyring, then `~/.supabase/access-token`) | -| `SUPABASE_PROFILE` | built-in profile name or YAML file path | no (falls back to `~/.supabase/profile` -> `supabase`) | -| `SUPABASE_USE_SLIM_IMAGES` | Ambient `process.env` only (`true`/`1` enable). Rewrites legacy current-pin `SERVICE IMAGE`/`name` fields to `ghcr.io/supabase/cli/`; Kong stays on docker.io. Majors 13/15 list the slim `15.14.1.167` pin when enabled; flag-off keeps `15.8.1.085`. Ignored by the stack backend. | no | -| `SUPABASE_EXPERIMENTAL_STACK` | Selects the stack backend (`1`) or legacy backend (`0`); unset or empty uses `experimental.stack`. Other values fail routing. | no | -| `SUPABASE_DB_MAJOR_VERSION` | Stack backend PostgreSQL major override; supported values are 15 and 17. | no | +| Variable | Purpose | Required? | +| ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | +| `SUPABASE_ACCESS_TOKEN` | auth token for Management API linked-version checks | no (falls back to keyring, then `~/.supabase/access-token`) | +| `SUPABASE_PROFILE` | built-in profile name or YAML file path | no (falls back to `~/.supabase/profile` -> `supabase`) | +| `SUPABASE_USE_SLIM_IMAGES` | Ambient `process.env` only (`true`/`1` enable). Rewrites legacy current-pin `SERVICE IMAGE`/`name` fields to `ghcr.io/supabase/cli/`; Kong stays on docker.io. Majors 13/15 list the slim `15.14.1.167` pin when enabled; flag-off keeps `15.8.1.085`. Ignored by the stack backend. | no | +| `SUPABASE_EXPERIMENTAL_STACK` | Selects the stack backend (`1`) or legacy backend (`0`); unset or empty uses `experimental.stack`. Other values fail routing. | no | +| `SUPABASE_DB_MAJOR_VERSION` | Stack backend PostgreSQL major override; supported values are 15 and 17. | no | +| `SUPABASE_DB_ORIOLEDB_VERSION` | legacy backend: overrides `db.orioledb_version`, selecting the listed OrioleDB Postgres image | no | +| `SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION` | deprecated alias of `SUPABASE_DB_ORIOLEDB_VERSION`, used only while that is unset; when set, stderr gets `WARN: SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION is deprecated and will be removed in v3, use SUPABASE_DB_ORIOLEDB_VERSION instead.` | no | ## Exit Codes diff --git a/apps/cli/src/commands/services/services-local-stack.ts b/apps/cli/src/commands/services/services-local-stack.ts index 3833a3075e..902fd7ec94 100644 --- a/apps/cli/src/commands/services/services-local-stack.ts +++ b/apps/cli/src/commands/services/services-local-stack.ts @@ -6,8 +6,8 @@ import { import { Effect, Result } from "effect"; import { loadLocalProjectContext } from "../../command-internal/local-project-context.ts"; import { - envOverride, envOverrideMajorVersion, + envOverrideOrioleDBVersion, } from "../../command-internal/local-config-values.ts"; import { stackDatabaseVersion } from "../../command-internal/stack-database-version.ts"; import { upstreamVersionFromTag } from "../../shared/services/services.shared.ts"; @@ -40,8 +40,7 @@ export const stackServiceVersions = Effect.fn("services.stackServiceVersions")(f throw new Error(`unsupported PostgreSQL major version: ${value}`); return stackDatabaseVersion({ major_version: value, - orioledb_version: envOverride( - "SUPABASE_DB_ORIOLEDB_VERSION", + orioledb_version: envOverrideOrioleDBVersion( context.success.config.db.orioledb_version, context.success.projectEnvValues, ), diff --git a/apps/cli/src/commands/services/services.handler.ts b/apps/cli/src/commands/services/services.handler.ts index 1b93ca1823..648fb10b5a 100644 --- a/apps/cli/src/commands/services/services.handler.ts +++ b/apps/cli/src/commands/services/services.handler.ts @@ -7,7 +7,10 @@ import { } from "../../config/project-ref.service.ts"; import { LinkedProjectCache } from "../../telemetry/linked-project-cache.service.ts"; import { TelemetryState } from "../../telemetry/telemetry-state.service.ts"; -import { readDbToml } from "../../command-internal/db-config.toml-read.ts"; +import { + readDbToml, + warnDeprecatedOrioleDBVersionEnv, +} from "../../command-internal/db-config.toml-read.ts"; import { resolveDbImage } from "../../command-internal/db-image.ts"; import { resolveEdgeRuntimeImage } from "../../command-internal/edge-runtime-image.ts"; import { readServiceVersionOverrides } from "../../command-internal/service-version-overrides.ts"; @@ -146,6 +149,7 @@ export const services = Effect.fn("services")(function* (_flags: ServicesFlags) output.raw(`${error.message}\n`, "stderr").pipe(Effect.as(null)), ), ); + if (tomlValues !== null) yield* warnDeprecatedOrioleDBVersionEnv(tomlValues.envLookup); const serviceVersions = tomlValues === null ? {} diff --git a/apps/cli/src/commands/status/SIDE_EFFECTS.md b/apps/cli/src/commands/status/SIDE_EFFECTS.md index 602836ae79..9530f57632 100644 --- a/apps/cli/src/commands/status/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/status/SIDE_EFFECTS.md @@ -213,6 +213,12 @@ headers); colors are stripped on non-TTY/piped output. bracketed list, e.g. `[supabase_storage_test supabase_studio_test]`) whenever one of the 13 expected service containers isn't in the running set. +Whenever a machine format (`-o env|json|toml|yaml`, or `--output-format json|stream-json`) +emits the deprecated `INBUCKET_URL` field, stderr gets +`WARN: INBUCKET_URL is deprecated and will be removed in v3, use MAILPIT_URL instead.` (both +names follow `--override-name`). stdout stays payload-only, and the pretty table, which never +shows `INBUCKET_URL`, prints no warning. + ### `-o env` `KEY="VALUE"` lines (unquoted for integer-looking values), one per resolved field, sorted by diff --git a/apps/cli/src/commands/status/status.handler.ts b/apps/cli/src/commands/status/status.handler.ts index d9933715d1..6eb5f46efe 100644 --- a/apps/cli/src/commands/status/status.handler.ts +++ b/apps/cli/src/commands/status/status.handler.ts @@ -45,6 +45,7 @@ import { import { renderStatusPretty } from "../../command-internal/status-pretty.ts"; import { STATUS_FIELDS, + deprecatedStatusFieldWarning, gateStatusState, resolveStatusLocalState, statusContainerIds, @@ -237,6 +238,16 @@ export const status = Effect.fn("status")(function* (flags: StatusFlags) { // with the linked-state extension, never the other way round. const valuesWithLinkedState = { ...linkedStateGoFields(linkedState), ...values }; + // `INBUCKET_URL` appears only in the machine formats, never the pretty table. + const machineFormat = + goFmt === undefined + ? output.format === "json" || output.format === "stream-json" + : goFmt !== "pretty"; + const deprecationWarning = deprecatedStatusFieldWarning(state, overrides); + if (machineFormat && deprecationWarning !== undefined) { + yield* output.raw(deprecationWarning, "stderr"); + } + if (goFmt === "env") { yield* output.raw(encodeEnv(valuesWithLinkedState) + "\n"); return; diff --git a/apps/cli/src/commands/status/status.integration.test.ts b/apps/cli/src/commands/status/status.integration.test.ts index 854ebe8421..0b9d3cfeb2 100644 --- a/apps/cli/src/commands/status/status.integration.test.ts +++ b/apps/cli/src/commands/status/status.integration.test.ts @@ -443,6 +443,9 @@ function setupFailureEnvelope(opts: FailureEnvelopeOpts) { return { workdir, layer, stdio, processControl }; } +const INBUCKET_WARNING = + "WARN: INBUCKET_URL is deprecated and will be removed in v3, use MAILPIT_URL instead.\n"; + describe("status integration", () => { it.effect("shows the running stack as a pretty table", () => { const { layer, out } = setup(); @@ -457,6 +460,7 @@ describe("status integration", () => { expect(out.stdoutText).toContain("📦 Storage (S3)"); expect(out.stdoutText).toContain("postgresql://postgres:postgres@"); expect(out.stderrText).not.toContain("Stopped services:"); + expect(out.stderrText).not.toContain("INBUCKET_URL"); }).pipe(Effect.provide(layer)); }); @@ -1009,15 +1013,21 @@ content_path = "./supabase/templates/password_changed_notification.html" }).pipe(Effect.provide(layer)); }); - it.effect("outputs env vars with -o env", () => { - const { layer, out } = setup({ goOutput: Option.some("env") }); - return Effect.gen(function* () { - yield* writeConfig(); - yield* status(flags()); - expect(out.stdoutText).toContain('API_URL="http://127.0.0.1:54321"'); - expect(out.stdoutText).toContain("DB_URL="); - }).pipe(Effect.provide(layer)); - }); + it.effect( + "outputs env vars with -o env, warning on stderr that INBUCKET_URL is removed in v3", + () => { + const { layer, out } = setup({ goOutput: Option.some("env") }); + return Effect.gen(function* () { + yield* writeConfig(); + yield* status(flags()); + expect(out.stdoutText).toContain('API_URL="http://127.0.0.1:54321"'); + expect(out.stdoutText).toContain("DB_URL="); + expect(out.stdoutText).toContain("INBUCKET_URL="); + expect(out.stdoutText).not.toContain("deprecated"); + expect(out.stderrText).toContain(INBUCKET_WARNING); + }).pipe(Effect.provide(layer)); + }, + ); it.effect("prints API_URL but no REST_URL with -o env when [api] enabled = false", () => { const { layer, out } = setup({ @@ -2103,32 +2113,47 @@ content_path = "./supabase/templates/password_changed_notification.html" }, ); - it.effect("--output-format json, not linked: linked_project is null", () => { - const { layer, out } = setup({ format: "json" }); - return Effect.gen(function* () { - yield* writeConfig(); - yield* status(flags()); - const success = out.messages.find((m) => m.type === "success"); - expect(success?.data).toMatchObject({ linked_project: null }); - }).pipe(Effect.provide(layer)); - }); + it.effect( + "--output-format json, not linked: linked_project is null, INBUCKET_URL warning on stderr only", + () => { + const { layer, out } = setup({ format: "json" }); + return Effect.gen(function* () { + yield* writeConfig(); + yield* status(flags()); + const success = out.messages.find((m) => m.type === "success"); + expect(success?.data).toMatchObject({ + linked_project: null, + INBUCKET_URL: expect.any(String), + }); + expect(out.stderrText).toBe(INBUCKET_WARNING); + expect(out.stdoutText).toBe(""); + }).pipe(Effect.provide(layer)); + }, + ); - it.effect("--output-format stream-json, branch-linked: nests linked_project (smoke)", () => { - const { layer, out, workdir } = setup({ - format: "stream-json", - branches: { ok: [LINKED_BRANCH] }, - }); - return Effect.gen(function* () { - yield* writeConfig(); - yield* writeProjectRefFile(workdir, LINKED_BRANCH_REF); - yield* writeLinkedProjectCacheFile(workdir, LINKED_PARENT_REF, { name: "Parent Project" }); - yield* status(flags()); - const success = out.messages.find((m) => m.type === "success"); - expect(success?.data).toMatchObject({ - linked_project: { project_ref: LINKED_BRANCH_REF }, + it.effect( + "--output-format stream-json, branch-linked: nests linked_project (smoke), INBUCKET_URL warning on stderr only", + () => { + const { layer, out, workdir } = setup({ + format: "stream-json", + branches: { ok: [LINKED_BRANCH] }, }); - }).pipe(Effect.provide(layer)); - }); + return Effect.gen(function* () { + yield* writeConfig(); + yield* writeProjectRefFile(workdir, LINKED_BRANCH_REF); + yield* writeLinkedProjectCacheFile(workdir, LINKED_PARENT_REF, { + name: "Parent Project", + }); + yield* status(flags()); + const success = out.messages.find((m) => m.type === "success"); + expect(success?.data).toMatchObject({ + linked_project: { project_ref: LINKED_BRANCH_REF }, + }); + expect(out.stderrText).toBe(INBUCKET_WARNING); + expect(out.stdoutText).toBe(""); + }).pipe(Effect.provide(layer)); + }, + ); describe("json/stream-json FAILURE envelope carries linked_project (CLI-2167 follow-up)", () => { it.effect( diff --git a/apps/cli/src/commands/test/db/SIDE_EFFECTS.md b/apps/cli/src/commands/test/db/SIDE_EFFECTS.md index c870ff5185..056307b146 100644 --- a/apps/cli/src/commands/test/db/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/test/db/SIDE_EFFECTS.md @@ -49,14 +49,15 @@ Managed stack `--local` prove runs the catalog `pg_prove` tool through the ownin ## Environment Variables -| Variable | Purpose | Required? | -| ---------------------------- | -------------------------------------------------------------------- | --------------------------------------------- | -| `SUPABASE_DB_PASSWORD` | `--linked`: skip temporary login-role creation | no | -| `SUPABASE_ACCESS_TOKEN` | `--linked`: Management API auth | no (falls back to keyring/file) | -| `SUPABASE_SERVICES_HOSTNAME` | `--local`: overrides the local DB host (dev-container/remote Docker) | no (defaults via `DOCKER_HOST` → `127.0.0.1`) | -| `DOCKER_HOST` | `--local`: tcp daemon host used when no services-hostname override | no | -| `BITBUCKET_CLONE_DIR` | when set, omit `--security-opt label:disable` (Bitbucket rejects it) | no | -| `DEBUG` / `--debug` | append `--verbose` to `pg_prove` | no | +| Variable | Purpose | Required? | +| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------- | +| `SUPABASE_DB_PASSWORD` | `--linked`: skip temporary login-role creation | no | +| `SUPABASE_ACCESS_TOKEN` | `--linked`: Management API auth | no (falls back to keyring/file) | +| `SUPABASE_SERVICES_HOSTNAME` | `--local`: overrides the local DB host (dev-container/remote Docker) | no (defaults via `DOCKER_HOST` → `127.0.0.1`) | +| `DOCKER_HOST` | `--local`: tcp daemon host used when no services-hostname override | no | +| `BITBUCKET_CLONE_DIR` | when set, omit `--security-opt label:disable` (Bitbucket rejects it) | no | +| `DEBUG` / `--debug` | append `--verbose` to `pg_prove` | no | +| `SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION` | deprecated alias of `SUPABASE_DB_ORIOLEDB_VERSION`, used only while that is unset; when set, stderr gets `WARN: SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION is deprecated and will be removed in v3, use SUPABASE_DB_ORIOLEDB_VERSION instead.` | no | ## Exit Codes diff --git a/apps/cli/src/shared/functions/deploy.ts b/apps/cli/src/shared/functions/deploy.ts index 3dd5ec5d15..5458c5737a 100644 --- a/apps/cli/src/shared/functions/deploy.ts +++ b/apps/cli/src/shared/functions/deploy.ts @@ -2212,7 +2212,7 @@ export const resolveFunctionConfigs = Effect.fn("functions.deploy.resolveConfigs if (seenDeprecatedImportMap.size > 0) { yield* output.raw( - `WARNING: Functions using deprecated import_map.json (please migrate to deno.json): ${[...seenDeprecatedImportMap].join(", ")}\n`, + `WARNING: import_map.json is deprecated and will be removed in v3, use deno.json instead. Functions using it: ${[...seenDeprecatedImportMap].join(", ")}\n`, "stderr", ); } diff --git a/packages/config/src/io.ts b/packages/config/src/io.ts index 82bffc9e6f..80e4eb2d3c 100644 --- a/packages/config/src/io.ts +++ b/packages/config/src/io.ts @@ -586,14 +586,14 @@ export const loadCliConfigFile = Effect.fn("CliConfig.loadFile")(function* ( for (const section of deprecatedSections) { const replacement = section.replace(/inbucket$/, "local_smtp"); yield* Console.error( - `WARN: config section [${section}] is deprecated. Please use [${replacement}] instead.`, + `WARN: config section [${section}] is deprecated and will be removed in v3, use [${replacement}] instead.`, ).pipe(Effect.provideService(Console.Console, globalThis.console)); } for (const path of deprecatedOrioleDBPaths) { const replacement = path.replace(/experimental\.orioledb_version$/, "db.orioledb_version"); - yield* Console.error(`WARN: ${path} is deprecated. Please use ${replacement} instead.`).pipe( - Effect.provideService(Console.Console, globalThis.console), - ); + yield* Console.error( + `WARN: ${path} is deprecated and will be removed in v3, use ${replacement} instead.`, + ).pipe(Effect.provideService(Console.Console, globalThis.console)); } // Substitute `env(VAR)` references against `.env`/`.env.local`/ambient env before schema @@ -667,7 +667,7 @@ export const loadCliConfigFile = Effect.fn("CliConfig.loadFile")(function* ( if (goViperCompat) { for (const ext of deprecatedProviders) { yield* Console.error( - `WARN: disabling deprecated "${ext}" provider. Please use [auth.external.${ext}_oidc] instead`, + `WARN: disabling deprecated "${ext}" provider. It will be removed in v3, use [auth.external.${ext}_oidc] instead.`, ).pipe(Effect.provideService(Console.Console, globalThis.console)); } } diff --git a/packages/config/src/io.unit.test.ts b/packages/config/src/io.unit.test.ts index a6a219c264..c71f9c7d7a 100644 --- a/packages/config/src/io.unit.test.ts +++ b/packages/config/src/io.unit.test.ts @@ -2329,7 +2329,7 @@ port = 12345 expect( warnings.some((m) => m.includes( - "WARN: config section [inbucket] is deprecated. Please use [local_smtp] instead.", + "WARN: config section [inbucket] is deprecated and will be removed in v3, use [local_smtp] instead.", ), ), ).toBe(true); @@ -2391,7 +2391,7 @@ port = 33333 expect( warnings.some((m) => m.includes( - "WARN: config section [remotes.staging.inbucket] is deprecated. Please use [remotes.staging.local_smtp] instead.", + "WARN: config section [remotes.staging.inbucket] is deprecated and will be removed in v3, use [remotes.staging.local_smtp] instead.", ), ), ).toBe(true); @@ -2457,7 +2457,7 @@ orioledb_version = "15.1.0.150" expect( warnings.some((m) => m.includes( - "WARN: experimental.orioledb_version is deprecated. Please use db.orioledb_version instead.", + "WARN: experimental.orioledb_version is deprecated and will be removed in v3, use db.orioledb_version instead.", ), ), ).toBe(true); @@ -2495,7 +2495,7 @@ orioledb_version = "15.1.0.150" expect( warnings.some((m) => m.includes( - "WARN: experimental.orioledb_version is deprecated. Please use db.orioledb_version instead.", + "WARN: experimental.orioledb_version is deprecated and will be removed in v3, use db.orioledb_version instead.", ), ), ).toBe(true); @@ -2518,7 +2518,7 @@ orioledb_version = "15.1.0.150" expect( warnings.some((m) => m.includes( - "WARN: experimental.orioledb_version is deprecated. Please use db.orioledb_version instead.", + "WARN: experimental.orioledb_version is deprecated and will be removed in v3, use db.orioledb_version instead.", ), ), ).toBe(true); @@ -2603,7 +2603,7 @@ orioledb_version = "15.1.0.150" expect( warnings.some((m) => m.includes( - "WARN: remotes.staging.experimental.orioledb_version is deprecated. Please use remotes.staging.db.orioledb_version instead.", + "WARN: remotes.staging.experimental.orioledb_version is deprecated and will be removed in v3, use remotes.staging.db.orioledb_version instead.", ), ), ).toBe(true); @@ -2654,7 +2654,7 @@ enabled = true expect( warnings.some((m) => m.includes( - 'WARN: disabling deprecated "slack" provider. Please use [auth.external.slack_oidc] instead', + 'WARN: disabling deprecated "slack" provider. It will be removed in v3, use [auth.external.slack_oidc] instead.', ), ), ).toBe(true); @@ -2675,7 +2675,7 @@ enabled = true expect( warnings.some((m) => m.includes( - 'WARN: disabling deprecated "linkedin" provider. Please use [auth.external.linkedin_oidc] instead', + 'WARN: disabling deprecated "linkedin" provider. It will be removed in v3, use [auth.external.linkedin_oidc] instead.', ), ), ).toBe(true); From 8bdd6ee2c264d39b4667340eaacb8d4e8978976f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 14 Oct 2026 15:49:06 +0000 Subject: [PATCH 07/11] chore(repo): format the develop-into-next merge --- apps/cli/src/config/cli-config-keys.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/cli/src/config/cli-config-keys.ts b/apps/cli/src/config/cli-config-keys.ts index d711408cc4..43e3fd47ef 100644 --- a/apps/cli/src/config/cli-config-keys.ts +++ b/apps/cli/src/config/cli-config-keys.ts @@ -177,9 +177,7 @@ export const cliConfigSchemaKeyDefs = (root: SchemaAST.Objects): ReadonlyArray Date: Wed, 14 Oct 2026 15:50:12 +0000 Subject: [PATCH 08/11] chore(repo): fix the develop-into-next merge --- .../src/command-internal/db-bootstrap/reset-local-database.ts | 2 +- .../src/commands/functions/deploy/deploy.integration.test.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/cli/src/command-internal/db-bootstrap/reset-local-database.ts b/apps/cli/src/command-internal/db-bootstrap/reset-local-database.ts index 169d5e1288..3713e3f4b2 100644 --- a/apps/cli/src/command-internal/db-bootstrap/reset-local-database.ts +++ b/apps/cli/src/command-internal/db-bootstrap/reset-local-database.ts @@ -125,7 +125,7 @@ export const resetLocalDatabase = Effect.fn("DbBootstrap.resetLocalDatabase")(fu const catalog = yield* Effect.serviceOption(StackCatalogSetup); if (Option.isNone(catalog)) return yield* resetFailed("stack catalog setup is unavailable"); const context = yield* loadLocalProjectContext(workdir, (message) => resetFailed(message)); - const config = yield* loadStackConfig(workdir, { context }).pipe( + const config = yield* loadStackConfig(workdir).pipe( Effect.mapError((cause) => resetFailed(cause.message)), ); const toml = yield* readDbToml(fs, path, workdir); diff --git a/apps/cli/src/commands/functions/deploy/deploy.integration.test.ts b/apps/cli/src/commands/functions/deploy/deploy.integration.test.ts index fa80f081e1..24a9cc0449 100644 --- a/apps/cli/src/commands/functions/deploy/deploy.integration.test.ts +++ b/apps/cli/src/commands/functions/deploy/deploy.integration.test.ts @@ -410,6 +410,7 @@ describe("functions deploy", () => { }, }); const layer = Layer.mergeAll( + cliConfigValuesTestLayer, buildTestRuntime({ out, api, From 45e899dad980a710fc621f2f417ae8aef0141252 Mon Sep 17 00:00:00 2001 From: avallete Date: Wed, 14 Oct 2026 17:54:59 +0200 Subject: [PATCH 09/11] ci: drop Go CLI steps from develop workflows for next next removed apps/cli-go, so the Go build, cache, and keyring steps synced from develop fail. Co-Authored-By: Claude Opus 5.5 --- .github/actions/setup/action.yml | 43 +------------------------ .github/workflows/fill-sticky-disks.yml | 3 +- .github/workflows/github-scripts-ci.yml | 2 +- .github/workflows/test.yml | 25 -------------- 4 files changed, 3 insertions(+), 70 deletions(-) diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index 8e503fe8f6..1bfebaa530 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -14,16 +14,10 @@ inputs: fails with a path validation error. required: false default: "true" - go-cache: - description: >- - Whether to restore the Go module and build caches. Disable this in jobs - that never run Go. - required: false - default: "true" tools: description: >- Space-separated mise tools to install instead of every tool in - mise.toml. Skipping the Go toolchain keeps the restored mise cache small. + mise.toml. required: false default: "" pnpm-store-cache: @@ -189,41 +183,6 @@ runs: path: ${{ steps.pnpm-cache.outputs.path }}/lockfile-verified.jsonl key: pnpm-verify-${{ runner.os }}-${{ inputs.dependency-firewall-token != '' && 'firewall' || 'public' }}-${{ hashFiles('pnpm-lock.yaml', 'pnpm-workspace.yaml') }} - - name: Resolve Go cache paths - if: inputs.dependency-cache == 'true' && inputs.go-cache == 'true' - id: go-cache - shell: bash - run: | - echo "mod=$(go env GOMODCACHE)" >> "$GITHUB_OUTPUT" - echo "build=$(go env GOCACHE)" >> "$GITHUB_OUTPUT" - - - name: Mount Go module disk - if: inputs.dependency-cache == 'true' && inputs.go-cache == 'true' && steps.sticky.outputs.enabled == 'true' - uses: useblacksmith/stickydisk@b6cb58d1d31d6d32a25248fab12493d831f7bd48 # v1.7.2 - with: - key: ${{ github.repository }}-go-mod-v3-${{ runner.arch }} - path: ${{ steps.go-cache.outputs.mod }} - commit: ${{ steps.sticky.outputs.commit }} - - - name: Mount Go build disk - if: inputs.dependency-cache == 'true' && inputs.go-cache == 'true' && steps.sticky.outputs.enabled == 'true' - uses: useblacksmith/stickydisk@b6cb58d1d31d6d32a25248fab12493d831f7bd48 # v1.7.2 - with: - key: ${{ github.repository }}-go-build-v3-${{ runner.arch }} - path: ${{ steps.go-cache.outputs.build }} - commit: ${{ steps.sticky.outputs.commit }} - - - name: Configure Go dependency cache - if: inputs.dependency-cache == 'true' && inputs.go-cache == 'true' && steps.sticky.outputs.enabled != 'true' - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: | - ${{ steps.go-cache.outputs.mod }} - ${{ steps.go-cache.outputs.build }} - key: go-cache-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('apps/cli-go/go.mod', 'apps/cli-go/go.sum') }} - restore-keys: | - go-cache-${{ runner.os }}-${{ runner.arch }}- - - name: Install dependencies shell: bash env: diff --git a/.github/workflows/fill-sticky-disks.yml b/.github/workflows/fill-sticky-disks.yml index 2440f388d1..5e25630251 100644 --- a/.github/workflows/fill-sticky-disks.yml +++ b/.github/workflows/fill-sticky-disks.yml @@ -1,6 +1,6 @@ name: Fill sticky disks -# Test jobs mount the pnpm, Go and Docker image caches from Blacksmith sticky +# Test jobs mount the pnpm and Docker image caches from Blacksmith sticky # disks but pull request runs never commit them. This workflow is the trusted # writer: it runs on protected branches, which are the only refs Blacksmith's # sticky-disk branch protection lets commit. @@ -56,7 +56,6 @@ jobs: # Starting a default project pulls every image `supabase start` uses. - name: Pull CLI images env: - SUPABASE_GO_BINARY: ${{ github.workspace }}/apps/cli-go/supabase-go SUPABASE_CLI_BINARY_OVERRIDE: ${{ github.workspace }}/apps/cli/dist/supabase run: | project="$(mktemp -d)" diff --git a/.github/workflows/github-scripts-ci.yml b/.github/workflows/github-scripts-ci.yml index c37e557139..ee6a98f27c 100644 --- a/.github/workflows/github-scripts-ci.yml +++ b/.github/workflows/github-scripts-ci.yml @@ -33,7 +33,7 @@ jobs: test: name: Test and type-check runs-on: ubuntu-latest - # The shared setup installs the full workspace + Go toolchain via mise, which + # The shared setup installs the full workspace toolchain via mise, which # runs ~9-10 min; a 10-minute cap raced the install and got cancelled on a # cold cache. 20 gives that install headroom. (This check is heavier than it # needs to be for two scripts — slimming the setup is a possible follow-up.) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d88906c01b..050fc254b4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -73,9 +73,6 @@ jobs: with: dependency-firewall-token: ${{ secrets.DF_FIREWALL_TOKEN }} sticky-disks: "true" - - name: Unlock keyring (for cli-go keyring tests) - uses: t1m0thyj/unlock-keyring@cbcf205c879ebd86add70bab3a6abfcce59a5cae # v1.2.0 - - name: Check code quality run: pnpm run check:all @@ -110,7 +107,6 @@ jobs: with: dependency-firewall-token: ${{ secrets.DF_FIREWALL_TOKEN }} sticky-disks: "true" - go-cache: "false" - name: Run unit tests run: pnpm run test:unit @@ -145,7 +141,6 @@ jobs: with: dependency-firewall-token: ${{ secrets.DF_FIREWALL_TOKEN }} sticky-disks: "true" - go-cache: "false" docker-images: "true" - name: Run integration tests @@ -242,7 +237,6 @@ jobs: uses: ./.github/actions/setup with: dependency-firewall-token: ${{ secrets.DF_FIREWALL_TOKEN }} - go-cache: "false" tools: node bun pnpm # Windows unpacks the store archive slower than it downloads the packages. pnpm-store-cache: ${{ matrix.os != 'windows-latest' }} @@ -316,7 +310,6 @@ jobs: uses: ./.github/actions/setup with: dependency-firewall-token: ${{ secrets.DF_FIREWALL_TOKEN }} - go-cache: "false" # Keyed per suite so each job saves the artifacts it actually prepared. - name: Cache native stack artifacts @@ -395,19 +388,6 @@ jobs: sticky-disks: "true" docker-images: "true" - - name: Cache Go CLI binary - id: cache-go-binary - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: apps/cli-go/supabase-go - key: go-cli-${{ runner.os }}-${{ hashFiles('apps/cli-go/**/*.go', - 'apps/cli-go/go.mod', 'apps/cli-go/go.sum') }} - - - name: Build Go CLI - if: steps.cache-go-binary.outputs.cache-hit != 'true' - run: go build -o supabase-go . - working-directory: apps/cli-go - # The e2e harness invokes `node apps/cli/dist/supabase.js` with # `SUPABASE_CLI_BINARY_OVERRIDE` pointing at the compiled binary in # `apps/cli/dist/`. Build the CLI explicitly before invoking every @@ -419,14 +399,11 @@ jobs: if: matrix.suite == 'cli' run: pnpm exec turbo run test:e2e:run --only --filter=supabase -- --shard=${{ matrix.shard }}/${{ matrix.shards }} env: - SUPABASE_GO_BINARY: ${{ github.workspace }}/apps/cli-go/supabase-go SUPABASE_E2E_WORKERS: "4" - name: Run cli-e2e end-to-end tests if: matrix.suite == 'cli-e2e' run: pnpm exec turbo run test:e2e:run --only --filter=@supabase/cli-e2e - env: - SUPABASE_GO_BINARY: ${{ github.workspace }}/apps/cli-go/supabase-go # Vitest writes its results cache under a sha1-named directory. The refresh script # (apps/cli/scripts/refresh-e2e-timings.ts) merges these into tests/e2e-timings.json. @@ -471,7 +448,6 @@ jobs: with: dependency-firewall-token: ${{ secrets.DF_FIREWALL_TOKEN }} sticky-disks: "true" - go-cache: "false" docker-images: ${{ matrix.runtime == 'docker' }} native-artifacts: ${{ matrix.runtime == 'native' }} @@ -519,7 +495,6 @@ jobs: with: dependency-firewall-token: ${{ secrets.DF_FIREWALL_TOKEN }} sticky-disks: "true" - go-cache: "false" - name: Setup Podman uses: ./.github/actions/setup-podman From bfe734e750fd147f1a0f07b06b2afb99711012b0 Mon Sep 17 00:00:00 2001 From: avallete Date: Wed, 14 Oct 2026 18:01:10 +0200 Subject: [PATCH 10/11] chore(repo): drop develop's v3 removal warnings on next next already removed the deprecated config, env vars and import maps that #7154 warns about, so its warnings, env alias and tests do not apply here. Co-Authored-By: Claude Opus 5.5 --- .../db-bootstrap/reset-local-database.ts | 2 +- apps/cli/src/command-internal/seed-buckets.ts | 4 + .../cli/src/command-internal/status-values.ts | 10 -- .../src/commands/bootstrap/SIDE_EFFECTS.md | 19 ++-- apps/cli/src/commands/db/diff/SIDE_EFFECTS.md | 32 +++---- apps/cli/src/commands/db/dump/SIDE_EFFECTS.md | 4 +- .../commands/db/dump/dump.integration.test.ts | 23 ----- apps/cli/src/commands/db/pull/SIDE_EFFECTS.md | 4 +- .../commands/db/pull/pull.integration.test.ts | 42 --------- apps/cli/src/commands/db/push/SIDE_EFFECTS.md | 21 ++--- .../cli/src/commands/db/reset/SIDE_EFFECTS.md | 3 +- .../declarative/generate/SIDE_EFFECTS.md | 19 ++-- .../generate/generate.integration.test.ts | 40 -------- .../cli/src/commands/db/start/SIDE_EFFECTS.md | 1 - .../db/start/start.integration.test.ts | 40 +------- .../experimental/stack/start/start.handler.ts | 38 ++++---- .../stack/start/start.integration.test.ts | 47 +--------- .../commands/functions/deploy/SIDE_EFFECTS.md | 3 +- .../deploy/deploy.integration.test.ts | 56 ------------ .../src/commands/gen/types/SIDE_EFFECTS.md | 3 +- .../commands/migration/down/SIDE_EFFECTS.md | 9 +- .../src/commands/migration/up/SIDE_EFFECTS.md | 9 +- .../migration/up/up.integration.test.ts | 19 ---- .../cli/src/commands/services/SIDE_EFFECTS.md | 18 ++-- apps/cli/src/commands/status/SIDE_EFFECTS.md | 6 -- .../cli/src/commands/status/status.handler.ts | 11 --- .../status/status.integration.test.ts | 91 +++++++------------ apps/cli/src/commands/test/db/SIDE_EFFECTS.md | 17 ++-- .../src/config/cli-config-key-annotations.ts | 8 -- apps/cli/src/config/cli-config-keys.ts | 3 - .../cli/src/config/cli-config-values.layer.ts | 11 --- apps/cli/src/shared/functions/deploy.ts | 2 +- packages/config/src/io.stages.unit.test.ts | 4 +- packages/config/src/io.ts | 8 +- packages/config/src/io.unit.test.ts | 16 ++-- 35 files changed, 147 insertions(+), 496 deletions(-) diff --git a/apps/cli/src/command-internal/db-bootstrap/reset-local-database.ts b/apps/cli/src/command-internal/db-bootstrap/reset-local-database.ts index 3713e3f4b2..6d36e9a426 100644 --- a/apps/cli/src/command-internal/db-bootstrap/reset-local-database.ts +++ b/apps/cli/src/command-internal/db-bootstrap/reset-local-database.ts @@ -124,7 +124,6 @@ export const resetLocalDatabase = Effect.fn("DbBootstrap.resetLocalDatabase")(fu return yield* new ResetLocalDbNotRunningError({ message: "The local stack is not running." }); const catalog = yield* Effect.serviceOption(StackCatalogSetup); if (Option.isNone(catalog)) return yield* resetFailed("stack catalog setup is unavailable"); - const context = yield* loadLocalProjectContext(workdir, (message) => resetFailed(message)); const config = yield* loadStackConfig(workdir).pipe( Effect.mapError((cause) => resetFailed(cause.message)), ); @@ -223,6 +222,7 @@ export const resetLocalDatabase = Effect.fn("DbBootstrap.resetLocalDatabase")(fu // The database is already rebuilt, so any typed seeding failure only warns; defects and // interruption still propagate. yield* Effect.gen(function* () { + const context = yield* loadLocalProjectContext(workdir, (message) => resetFailed(message)); if (!hasConfiguredBuckets(context.config)) return; const capability = storage === undefined ? undefined : yield* storage.status; const decision = classifyStorageCapability(capability); diff --git a/apps/cli/src/command-internal/seed-buckets.ts b/apps/cli/src/command-internal/seed-buckets.ts index 6f1a304b97..957a41dad7 100644 --- a/apps/cli/src/command-internal/seed-buckets.ts +++ b/apps/cli/src/command-internal/seed-buckets.ts @@ -34,6 +34,10 @@ import { import { SeedConfigLoadError } from "../commands/seed/buckets/buckets.errors.ts"; import { bucketObjectKey } from "../commands/seed/buckets/buckets.upload.ts"; +// Re-exported so `stack start` can report a project-context config-load failure with the +// same tag `seedBucketsRun` itself fails with, without importing across command boundaries. +export { SeedConfigLoadError }; + const CONFIG_PATH = "supabase/config.toml"; const UPLOAD_CONCURRENCY = 5; diff --git a/apps/cli/src/command-internal/status-values.ts b/apps/cli/src/command-internal/status-values.ts index 5b7e0485a0..074ee335ef 100644 --- a/apps/cli/src/command-internal/status-values.ts +++ b/apps/cli/src/command-internal/status-values.ts @@ -377,16 +377,6 @@ export function statusValuesFromState( return { values, names, local }; } -/** The stderr warning for the deprecated `INBUCKET_URL` field, when {@link statusValuesFromState} emits it. */ -export function deprecatedStatusFieldWarning( - state: StatusState, - overrides: ReadonlyMap, -): string | undefined { - if (!state.inbucketEnabled) return undefined; - const nameFor = (field: StatusField) => overrides.get(field.fieldKey) ?? field.defaultName; - return `WARN: ${nameFor(INBUCKET_URL)} is deprecated and will be removed in v3, use ${nameFor(MAILPIT_URL)} instead.\n`; -} - /** * Convenience wrapper combining {@link resolveStatusLocalState}, {@link gateStatusState}, and * {@link statusValuesFromState} in one call — used by tests needing only a single override diff --git a/apps/cli/src/commands/bootstrap/SIDE_EFFECTS.md b/apps/cli/src/commands/bootstrap/SIDE_EFFECTS.md index b5d6b53680..4e42e962a8 100644 --- a/apps/cli/src/commands/bootstrap/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/bootstrap/SIDE_EFFECTS.md @@ -61,16 +61,15 @@ neither branch ever reaches the temp-login-role/Management-API path a passwordle ## Environment Variables -| Variable | Purpose | Required? | -| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------- | -| `SUPABASE_WORKDIR` | target dir (`--workdir` flag → env → prompt → cwd) | no | -| `SUPABASE_DB_PASSWORD` | DB password (`-p` flag → env → prompt/generate) | no | -| `GITHUB_TOKEN` | raise the GitHub API rate limit for template fetch | no | -| `SUPABASE_ACCESS_TOKEN` | auth bypass for ensure-login | no | -| `SUPABASE_PROFILE` | profile name/path (env → `~/.supabase/profile` → `supabase`) | no | -| `SUPABASE_YES` | auto-confirm the native push step's prompts, read project-`.env`-aware like the standalone `db push` | no | -| `SUPABASE_EXPERIMENTAL_STACK` | blank/`scratch` path only; when `1`, persists `[experimental] stack = true` and omits Docker-era default ports. Empty is unset; non-boolean values fail closed | no | -| `SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION` | deprecated alias of `SUPABASE_DB_ORIOLEDB_VERSION`, used only while that is unset; when set, stderr gets `WARN: SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION is deprecated and will be removed in v3, use SUPABASE_DB_ORIOLEDB_VERSION instead.` | no | +| Variable | Purpose | Required? | +| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | +| `SUPABASE_WORKDIR` | target dir (`--workdir` flag → env → prompt → cwd) | no | +| `SUPABASE_DB_PASSWORD` | DB password (`-p` flag → env → prompt/generate) | no | +| `GITHUB_TOKEN` | raise the GitHub API rate limit for template fetch | no | +| `SUPABASE_ACCESS_TOKEN` | auth bypass for ensure-login | no | +| `SUPABASE_PROFILE` | profile name/path (env → `~/.supabase/profile` → `supabase`) | no | +| `SUPABASE_YES` | auto-confirm the native push step's prompts, read project-`.env`-aware like the standalone `db push` | no | +| `SUPABASE_EXPERIMENTAL_STACK` | blank/`scratch` path only; when `1`, persists `[experimental] stack = true` and omits Docker-era default ports. Empty is unset; non-boolean values fail closed | no | ## Exit Codes diff --git a/apps/cli/src/commands/db/diff/SIDE_EFFECTS.md b/apps/cli/src/commands/db/diff/SIDE_EFFECTS.md index 97ce08b522..c01caa7cac 100644 --- a/apps/cli/src/commands/db/diff/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/db/diff/SIDE_EFFECTS.md @@ -103,22 +103,20 @@ of this command's own target resolve, ahead of the differ container. ## Environment Variables -| Variable | Purpose | Required? | -| ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------- | -| `SUPABASE_ACCESS_TOKEN` | auth for `--linked` | no | -| `SUPABASE_DB_PASSWORD` | remote DB password (linked); ignored for a target other than the linked project (see Notes) | no | -| `SUPABASE_DB_SHADOW_PORT` | shadow container's host port (`db.shadow_port`) — NOT `SUPABASE_DB_PORT`, which the shadow never reads | no | -| `SUPABASE_DB_MAJOR_VERSION` / `SUPABASE_DB_HEALTH_TIMEOUT` / `SUPABASE_DB_SETTINGS_*` | shadow container-config overrides, same as `db start`/`db reset` | no | -| `SUPABASE_PROJECT_ID` | overrides the shadow container's project id/labels, same as `db start`/`db reset` (`utils.DbId`); ALSO the linked-ref resolution fallback `--project-ref` supersedes — see Notes for the narrower scope of the flag | no | -| `SUPABASE_NETWORK_ID` (`--network-id`) | forces the shadow container/network onto an existing Docker network | no | -| `SUPABASE_HOME` | overrides the `~/.supabase` root used for the shadow baseline cache (and other CLI state) | no | -| `SUPABASE_SHADOW_CACHE` | shadow baseline cache; on by default, opt-out (`0`/`false`); the shadow's post-baseline state is saved under a managed snapshot key and restored into the next run's fresh stack database (see Notes) | no | -| `SUPABASE_EXPERIMENTAL_PGDELTA_ENABLED` | overrides `[experimental.pgdelta].enabled` (default true; `false` selects migra); a value that is not a boolean fails the command | no | -| `PGDELTA_DEBUG` | pg-delta debug capture | no | -| `SUPABASE_SSL_DEBUG` | migra SSL debug logging | no | -| `SUPABASE_INTERNAL_IMAGE_REGISTRY` | overrides the differ's / shadow's image registry (shell **or** project `.env`, passed to each image operation) | no | -| `SUPABASE_DB_ORIOLEDB_VERSION` | overrides `db.orioledb_version`, selecting the OrioleDB shadow image | no | -| `SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION` | deprecated alias of `SUPABASE_DB_ORIOLEDB_VERSION`, used only while that is unset; when set, stderr gets `WARN: SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION is deprecated and will be removed in v3, use SUPABASE_DB_ORIOLEDB_VERSION instead.` | no | +| Variable | Purpose | Required? | +| ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | +| `SUPABASE_ACCESS_TOKEN` | auth for `--linked` | no | +| `SUPABASE_DB_PASSWORD` | remote DB password (linked); ignored for a target other than the linked project (see Notes) | no | +| `SUPABASE_DB_SHADOW_PORT` | shadow container's host port (`db.shadow_port`) — NOT `SUPABASE_DB_PORT`, which the shadow never reads | no | +| `SUPABASE_DB_MAJOR_VERSION` / `SUPABASE_DB_HEALTH_TIMEOUT` / `SUPABASE_DB_SETTINGS_*` | shadow container-config overrides, same as `db start`/`db reset` | no | +| `SUPABASE_PROJECT_ID` | overrides the shadow container's project id/labels, same as `db start`/`db reset` (`utils.DbId`); ALSO the linked-ref resolution fallback `--project-ref` supersedes — see Notes for the narrower scope of the flag | no | +| `SUPABASE_NETWORK_ID` (`--network-id`) | forces the shadow container/network onto an existing Docker network | no | +| `SUPABASE_HOME` | overrides the `~/.supabase` root used for the shadow baseline cache (and other CLI state) | no | +| `SUPABASE_SHADOW_CACHE` | shadow baseline cache; on by default, opt-out (`0`/`false`); the shadow's post-baseline state is saved under a managed snapshot key and restored into the next run's fresh stack database (see Notes) | no | +| `SUPABASE_EXPERIMENTAL_PGDELTA_ENABLED` | overrides `[experimental.pgdelta].enabled` (default true; `false` selects migra); a value that is not a boolean fails the command | no | +| `PGDELTA_DEBUG` | pg-delta debug capture | no | +| `SUPABASE_SSL_DEBUG` | migra SSL debug logging | no | +| `SUPABASE_INTERNAL_IMAGE_REGISTRY` | overrides the differ's / shadow's image registry (shell **or** project `.env`, passed to each image operation) | no | `SUPABASE_DB_SHADOW_PORT`/`SUPABASE_NETWORK_ID`/`--network-id`/`SUPABASE_PROJECT_ID`/ `SUPABASE_DB_HEALTH_TIMEOUT` all apply to `--use-pgadmin` too — its shadow is provisioned @@ -211,7 +209,7 @@ transaction metadata. ## Notes / Delegation -- **Config value precedence** (ADR 0032): explicit flag > shell env > project `.env*` > config +- **Config value precedence** (ADR 0031): explicit flag > shell env > project `.env*` > config (`config.json` over `config.toml`) > default. The whole config is decoded up front, so an invalid value fails the command. A `--linked` target also applies a matching `[remotes.*]` block. The linked-database password env is withheld when the target differs from diff --git a/apps/cli/src/commands/db/dump/SIDE_EFFECTS.md b/apps/cli/src/commands/db/dump/SIDE_EFFECTS.md index dbbf096635..85b29e5b94 100644 --- a/apps/cli/src/commands/db/dump/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/db/dump/SIDE_EFFECTS.md @@ -38,8 +38,6 @@ image), to stdout or `--file`. | ---------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `SUPABASE_DB_PASSWORD` (`--password`/`-p` overrides) | remote DB password; ignored for a target other than the linked project (see Notes) | | `SUPABASE_ACCESS_TOKEN` | `--linked` auth | -| `SUPABASE_DB_ORIOLEDB_VERSION` | overrides `db.orioledb_version`, selecting the OrioleDB `pg_dump` image | -| `SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION` | deprecated alias of `SUPABASE_DB_ORIOLEDB_VERSION`, used only while that is unset; when set, stderr gets `WARN: SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION is deprecated and will be removed in v3, use SUPABASE_DB_ORIOLEDB_VERSION instead.` | | `BITBUCKET_CLONE_DIR` | (no-op for dump — no `--security-opt` is set) | | `SUPABASE_INTERNAL_IMAGE_REGISTRY` | rewrite the pg image registry for compose dumps; stack dumps use the catalog image pin unchanged | | `SUPABASE_USE_SLIM_IMAGES` | resolve the current Postgres pin from the slim `ghcr.io/supabase/cli` builds (`true`/`1` enable); majors 13/15 use `15.14.1.167` when the flag is on; historical pins, PG14, OrioleDB tags the catalog does not pin, and flag-off `15.8.1.085` stay on docker.io | @@ -85,7 +83,7 @@ shell inherits the suppressing variables and is missed. ## Notes -- **Config value precedence** (ADR 0032): explicit flag > shell env > project `.env*` > config +- **Config value precedence** (ADR 0031): explicit flag > shell env > project `.env*` > config (`config.json` over `config.toml`; a matched `[remotes.*]` block over the base document on `--linked`) > default. The linked-database password env is withheld when the target differs from `.temp/project-ref`: stderr gets `Not sending SUPABASE_DB_PASSWORD to : this directory is linked to . Using a temporary login role instead (needs supabase login or SUPABASE_ACCESS_TOKEN). Pass --password to use a password for .` and a temporary login role is minted. `--password` is rejected with diff --git a/apps/cli/src/commands/db/dump/dump.integration.test.ts b/apps/cli/src/commands/db/dump/dump.integration.test.ts index 9cce1659be..2b051ae9e5 100644 --- a/apps/cli/src/commands/db/dump/dump.integration.test.ts +++ b/apps/cli/src/commands/db/dump/dump.integration.test.ts @@ -778,29 +778,6 @@ describe("db dump integration", () => { }).pipe(Effect.provide(layer)); }); - it.effect( - "warns once on stderr that SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION is removed in v3, keeping stdout clean", - () => { - const { layer, out } = setup({ - isLocal: true, - stdout: "CREATE SCHEMA public;\n", - env: { SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION: "17.11.0.002" }, - }); - const consoleErrors: Array = []; - const errorSpy = vi.spyOn(console, "error").mockImplementation((...args) => { - consoleErrors.push(args.map((a) => String(a)).join(" ")); - }); - return Effect.gen(function* () { - yield* dbDump(flags({ local: Option.some(true) })); - const warning = - "WARN: SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION is deprecated and will be removed in v3, use SUPABASE_DB_ORIOLEDB_VERSION instead."; - const stderrLines = [...out.stderrText.split("\n"), ...consoleErrors]; - expect(stderrLines.filter((line) => line === warning)).toHaveLength(1); - expect(out.stdoutText).toBe("CREATE SCHEMA public;\n"); - }).pipe(Effect.provide(layer), Effect.ensuring(Effect.sync(() => errorSpy.mockRestore()))); - }, - ); - it.effect("dumps only data with column inserts", () => { const { layer, out, docker } = setup({ isLocal: true, stdout: "INSERT INTO ...;\n" }); return Effect.gen(function* () { diff --git a/apps/cli/src/commands/db/pull/SIDE_EFFECTS.md b/apps/cli/src/commands/db/pull/SIDE_EFFECTS.md index 8d186c8d67..6024c02c57 100644 --- a/apps/cli/src/commands/db/pull/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/db/pull/SIDE_EFFECTS.md @@ -131,8 +131,6 @@ at all, so nothing is cached for it. | `SUPABASE_DB_PASSWORD` | remote DB password (overridden by `-p`); ignored for a target other than the linked project (see Notes) | no | | `SUPABASE_DB_SHADOW_PORT` | shadow container's host port (`db.shadow_port`) — NOT `SUPABASE_DB_PORT`, which the shadow never reads | no | | `SUPABASE_DB_MAJOR_VERSION` / `SUPABASE_DB_HEALTH_TIMEOUT` / `SUPABASE_DB_SETTINGS_*` | shadow container-config overrides, same as `db start`/`db reset` | no | -| `SUPABASE_DB_ORIOLEDB_VERSION` | overrides `db.orioledb_version`, selecting the OrioleDB shadow and initial-migra `pg_dump` images | no | -| `SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION` | deprecated alias of `SUPABASE_DB_ORIOLEDB_VERSION`, used only while that is unset; when set, stderr gets `WARN: SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION is deprecated and will be removed in v3, use SUPABASE_DB_ORIOLEDB_VERSION instead.` | no | | `SUPABASE_PROJECT_ID` | overrides the shadow container's project id/labels, same as `db start`/`db reset` (`utils.DbId`); ALSO the linked-ref resolution fallback `--project-ref` supersedes — see Notes for the narrower scope of the flag | no | | `SUPABASE_NETWORK_ID` (`--network-id`) | forces the shadow and initial-migra `pg_dump` containers onto an existing Docker network | no | | `SUPABASE_USE_SLIM_IMAGES` | resolves the current-pin shadow Postgres, `pg_dump`, PG15+ realtime/storage/auth migrate-job images (migration-style cold shadow), and (for migra) the edge-runtime image from the slim `ghcr.io/supabase/cli` builds (`true`/`1` enable); majors 13/15 use `15.14.1.167` when the flag is on; historical pins, PG14, OrioleDB tags the catalog does not pin, flag-off `15.8.1.085`, and `deno_version = 1` stay on docker.io | no | @@ -178,7 +176,7 @@ Progress strings still go to stderr; stdout carries a single structured envelope ## Notes -- **Config value precedence** (ADR 0032): explicit flag > shell env > project `.env*` > config +- **Config value precedence** (ADR 0031): explicit flag > shell env > project `.env*` > config (`config.json` over `config.toml`) > default. The whole config is decoded up front, so an invalid value fails the command. A `--linked` target also applies a matching `[remotes.*]` block. The linked-database password env is withheld when the target differs from diff --git a/apps/cli/src/commands/db/pull/pull.integration.test.ts b/apps/cli/src/commands/db/pull/pull.integration.test.ts index c5e250ebb4..400fb7e7d1 100644 --- a/apps/cli/src/commands/db/pull/pull.integration.test.ts +++ b/apps/cli/src/commands/db/pull/pull.integration.test.ts @@ -1898,48 +1898,6 @@ describe("db pull", () => { }, ); - it.effect.each([ - { - mode: "the deprecated --experimental export", - opts: { experimental: true, edgeStdout: EXPORT_JSON }, - }, - { - mode: "a migration pull", - opts: { - migrations: ["20240101000000"], - remoteVersions: ["20240101000000"], - edgeStdout: "create table remote ();\n", - yes: true, - }, - }, - ])( - "warns once on stderr that SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION is removed in v3 for $mode", - ({ opts }) => { - const s = setup(tmp.current, opts); - const consoleErrors: Array = []; - const errorSpy = vi.spyOn(console, "error").mockImplementation((...args) => { - consoleErrors.push(args.map((a) => String(a)).join(" ")); - }); - const [name, value] = ["SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION", "17.11.0.002"] as const; - // The project-context load reads `process.env`; `readDbToml` reads `Config`. - return withEnvVar( - name, - value, - withConfigEnv( - { [name]: value }, - Effect.gen(function* () { - yield* dbPull(flags()); - const warning = - "WARN: SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION is deprecated and will be removed in v3, use SUPABASE_DB_ORIOLEDB_VERSION instead."; - const stderrLines = [...streamText(s.out, "stderr").split("\n"), ...consoleErrors]; - expect(stderrLines.filter((line) => line === warning)).toHaveLength(1); - expect(streamText(s.out, "stdout")).not.toContain(warning); - }), - ), - ).pipe(Effect.provide(s.layer), Effect.ensuring(Effect.sync(() => errorSpy.mockRestore()))); - }, - ); - it.effect( "an experimental pull in json mode reports a declarative export with no history repair", () => { diff --git a/apps/cli/src/commands/db/push/SIDE_EFFECTS.md b/apps/cli/src/commands/db/push/SIDE_EFFECTS.md index d3f2c88dec..720dafdcad 100644 --- a/apps/cli/src/commands/db/push/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/db/push/SIDE_EFFECTS.md @@ -43,16 +43,15 @@ before migrations unless `--skip-vault` is set. ## Environment Variables -| Variable | Purpose | Required? | -| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------- | -| `SUPABASE_ACCESS_TOKEN` | auth token for the `--linked` resolver path | no (falls back to keyring → `~/.supabase/access-token`) | -| `SUPABASE_DB_PASSWORD` | password for the linked/remote connection; ignored for a target other than the linked project (see Notes) | no (`--password`/`-p` takes precedence) | -| `SUPABASE_DB_SEED_ENABLED` | overrides `[db.seed].enabled`, including a matched `[remotes.*]` block; lifts a `false` but does not make push seed without `--include-seed` | no | -| `SUPABASE_*` config overrides | any `config.toml` key an env name exists for (e.g. `SUPABASE_DB_MIGRATIONS_ENABLED`); see Notes | no | -| `SUPABASE_YES` | auto-confirm prompts | no (also `--yes`) | -| `SUPABASE_PROJECT_ID` | linked-ref resolution override, superseded by `--project-ref` when set (same precedence position) — see Notes | no | -| `DOTENV_PRIVATE_KEY*` | decrypts `encrypted:` config secrets; `[db.vault]` values are not decrypted with `--skip-vault` | no | -| `SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION` | deprecated alias of `SUPABASE_DB_ORIOLEDB_VERSION`, used only while that is unset; when set, stderr gets `WARN: SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION is deprecated and will be removed in v3, use SUPABASE_DB_ORIOLEDB_VERSION instead.` | no | +| Variable | Purpose | Required? | +| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | +| `SUPABASE_ACCESS_TOKEN` | auth token for the `--linked` resolver path | no (falls back to keyring → `~/.supabase/access-token`) | +| `SUPABASE_DB_PASSWORD` | password for the linked/remote connection; ignored for a target other than the linked project (see Notes) | no (`--password`/`-p` takes precedence) | +| `SUPABASE_DB_SEED_ENABLED` | overrides `[db.seed].enabled`, including a matched `[remotes.*]` block; lifts a `false` but does not make push seed without `--include-seed` | no | +| `SUPABASE_*` config overrides | any `config.toml` key an env name exists for (e.g. `SUPABASE_DB_MIGRATIONS_ENABLED`); see Notes | no | +| `SUPABASE_YES` | auto-confirm prompts | no (also `--yes`) | +| `SUPABASE_PROJECT_ID` | linked-ref resolution override, superseded by `--project-ref` when set (same precedence position) — see Notes | no | +| `DOTENV_PRIVATE_KEY*` | decrypts `encrypted:` config secrets; `[db.vault]` values are not decrypted with `--skip-vault` | no | ## Exit Codes @@ -108,7 +107,7 @@ stdout is payload-only. A single `result` object is emitted: target is a hard error rather than a silently discarded flag (deliberately stricter than `SUPABASE_PROJECT_ID`, which simply goes unused on a non-linked target). -- **Config value precedence** (ADR 0032): explicit flag > shell env > project +- **Config value precedence** (ADR 0031): explicit flag > shell env > project `.env*` > config (`config.json` over `config.toml`; a matched `[remotes.*]` block over the base document) > default. The whole config is decoded up front, so an invalid value fails the command before any connection. diff --git a/apps/cli/src/commands/db/reset/SIDE_EFFECTS.md b/apps/cli/src/commands/db/reset/SIDE_EFFECTS.md index b0f28afd04..9a11723cdc 100644 --- a/apps/cli/src/commands/db/reset/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/db/reset/SIDE_EFFECTS.md @@ -176,7 +176,6 @@ the whole reset** (not just "skip buckets"). | `SUPABASE_AUTH_JWT_SECRET` / `SUPABASE_AUTH_SERVICE_ROLE_KEY` | local path: override `auth.jwt_secret` / `auth.service_role_key` (shell or project dotenv, `encrypted:` decrypted) — feeds the recreated Postgres container (jwt secret), the fresh-database setup jobs (both) and the legacy-backend bucket-seed Storage service-role key, same as `seed buckets`. Not consulted on the stack backend, which uses the stack's own service-role JWT | no | | `SUPABASE_NETWORK_ID` (`--network-id`) | forces the recreated container/network onto an existing Docker network — legacy backend only | no | | `SUPABASE_EXPERIMENTAL_STACK` | `1`/`0` selects the stack backend for the local path, overriding `experimental.stack` | no (→ `experimental.stack` → legacy) | -| `SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION` | deprecated alias of `SUPABASE_DB_ORIOLEDB_VERSION`, used only while that is unset; when set, stderr gets `WARN: SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION is deprecated and will be removed in v3, use SUPABASE_DB_ORIOLEDB_VERSION instead.` | no | ## Connection loss during migration apply @@ -274,7 +273,7 @@ to those defaults (the usual outcome for an interactive terminal). standalone outside the per-file transaction batch, with the same non-atomic flush behaviour as `db push` — see `db push`'s SIDE_EFFECTS Notes (supabase/cli#5139, adopted into TS in PR supabase/cli#5671). -- **Config value precedence** (ADR 0032): explicit flag > shell env > project +- **Config value precedence** (ADR 0031): explicit flag > shell env > project `.env*` > config (`config.json` over `config.toml`; a matched `[remotes.*]` block over the base document) > default. The linked-database password env is withheld when the target differs from `.temp/project-ref`: stderr gets diff --git a/apps/cli/src/commands/db/schema/declarative/generate/SIDE_EFFECTS.md b/apps/cli/src/commands/db/schema/declarative/generate/SIDE_EFFECTS.md index 4c847b6add..77b4593596 100644 --- a/apps/cli/src/commands/db/schema/declarative/generate/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/db/schema/declarative/generate/SIDE_EFFECTS.md @@ -41,16 +41,15 @@ formatting without disabling safe compaction. ## Environment Variables -| Variable | Purpose | Required? | -| ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | -| `SUPABASE_ACCESS_TOKEN` | auth token for `--linked` | no | -| `DB_PASSWORD` | password for `--linked` / `--db-url` | no | -| `SUPABASE_HOME` | overrides the `~/.supabase` root (access token and other CLI state) | no | -| `PGDELTA_DEBUG` | bundled-engine debug artifacts | no | -| `SUPABASE_SERVICES_HOSTNAME` | local DB host for `--local` | no | -| `DOCKER_HOST` | tcp daemon host used as the local DB host fallback | no | -| `SUPABASE_USE_SLIM_IMAGES` | resolves the expected local `db` image for the stale-container guard from the slim `ghcr.io/supabase/cli` builds (`true`/`1` enable); majors 13/15 use `15.14.1.167` when the flag is on; historical pins, PG14, OrioleDB tags the catalog does not pin, and flag-off `15.8.1.085` stay on docker.io | no | -| `SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION` | deprecated alias of `SUPABASE_DB_ORIOLEDB_VERSION`, used only while that is unset; when set, stderr gets `WARN: SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION is deprecated and will be removed in v3, use SUPABASE_DB_ORIOLEDB_VERSION instead.` | no | +| Variable | Purpose | Required? | +| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | +| `SUPABASE_ACCESS_TOKEN` | auth token for `--linked` | no | +| `DB_PASSWORD` | password for `--linked` / `--db-url` | no | +| `SUPABASE_HOME` | overrides the `~/.supabase` root (access token and other CLI state) | no | +| `PGDELTA_DEBUG` | bundled-engine debug artifacts | no | +| `SUPABASE_SERVICES_HOSTNAME` | local DB host for `--local` | no | +| `DOCKER_HOST` | tcp daemon host used as the local DB host fallback | no | +| `SUPABASE_USE_SLIM_IMAGES` | resolves the expected local `db` image for the stale-container guard from the slim `ghcr.io/supabase/cli` builds (`true`/`1` enable); majors 13/15 use `15.14.1.167` when the flag is on; historical pins, PG14, OrioleDB tags the catalog does not pin, and flag-off `15.8.1.085` stay on docker.io | no | ## Exit Codes diff --git a/apps/cli/src/commands/db/schema/declarative/generate/generate.integration.test.ts b/apps/cli/src/commands/db/schema/declarative/generate/generate.integration.test.ts index 77596f9bad..c5c3d6e89d 100644 --- a/apps/cli/src/commands/db/schema/declarative/generate/generate.integration.test.ts +++ b/apps/cli/src/commands/db/schema/declarative/generate/generate.integration.test.ts @@ -1231,46 +1231,6 @@ describe("db schema declarative generate integration", () => { }).pipe(Effect.provide(s.layer)); }); - it.effect( - "smart mode: warns once about SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION when the local reset reloads the config", - () => { - const s = setup(tmp.current, { - experimental: true, - stdinIsTty: true, - yes: true, - promptSelectResponses: ["local"], - }); - const consoleErrors: Array = []; - const errorSpy = vi.spyOn(console, "error").mockImplementation((...args) => { - consoleErrors.push(args.map((a) => String(a)).join(" ")); - }); - return Effect.gen(function* () { - const fs = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - yield* seedMigration(tmp.current); - yield* fs.writeFileString( - path.join(tmp.current, "supabase", "config.toml"), - 'project_id = "test"\n', - ); - yield* fs.writeFileString( - path.join(tmp.current, "supabase", ".env"), - "SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION=17.11.0.002\n", - ); - yield* dbSchemaDeclarativeGenerate(flags()); - expect(localResetCreateArgs(s.child.spawned)).not.toBeUndefined(); - const warning = - "WARN: SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION is deprecated and will be removed in v3, use SUPABASE_DB_ORIOLEDB_VERSION instead."; - const stderrLines = [ - ...s.out.rawChunks - .filter((c) => c.stream === "stderr") - .flatMap((c) => c.text.split("\n")), - ...consoleErrors, - ]; - expect(stderrLines.filter((line) => line === warning)).toHaveLength(1); - }).pipe(Effect.provide(s.layer), Effect.ensuring(Effect.sync(() => errorSpy.mockRestore()))); - }, - ); - it.effect("smart mode: forwards --network-id to the local reset", () => { const s = setup(tmp.current, { experimental: true, diff --git a/apps/cli/src/commands/db/start/SIDE_EFFECTS.md b/apps/cli/src/commands/db/start/SIDE_EFFECTS.md index 8c81adeffd..5010c0a5dd 100644 --- a/apps/cli/src/commands/db/start/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/db/start/SIDE_EFFECTS.md @@ -131,7 +131,6 @@ API request over the active context's local unix socket / named pipe: | `SUPABASE_DB_HEALTH_TIMEOUT` | overrides `db.health_timeout` | no | | `SUPABASE_DB_SETTINGS_*` | overrides individual `[db.settings]` fields | no | | `SUPABASE_DB_ORIOLEDB_VERSION` | overrides `db.orioledb_version` (image + env) | no | -| `SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION` | deprecated alias of `SUPABASE_DB_ORIOLEDB_VERSION`, used only while that is unset; when set, stderr gets `WARN: SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION is deprecated and will be removed in v3, use SUPABASE_DB_ORIOLEDB_VERSION instead.` | no | | `SUPABASE_EXPERIMENTAL_S3_{HOST,REGION,ACCESS_KEY,SECRET_KEY}` | OrioleDB S3 env overrides | no | | `SUPABASE_REALTIME_ENABLED` | gates the fresh-volume realtime migrate job | no | | `SUPABASE_REALTIME_IP_VERSION` / `_MAX_HEADER_LENGTH` | realtime migrate job env overrides | no | diff --git a/apps/cli/src/commands/db/start/start.integration.test.ts b/apps/cli/src/commands/db/start/start.integration.test.ts index 9f67c1bf17..a60c87747e 100644 --- a/apps/cli/src/commands/db/start/start.integration.test.ts +++ b/apps/cli/src/commands/db/start/start.integration.test.ts @@ -1455,7 +1455,7 @@ describe("db start", () => { yield* dbStart(DEFAULT_FLAGS).pipe(Effect.provide(layer)); const inbucketWarnings = warnings.filter((m) => m.includes( - "WARN: config section [inbucket] is deprecated and will be removed in v3, use [local_smtp] instead.", + "WARN: config section [inbucket] is deprecated. Please use [local_smtp] instead.", ), ); expect(inbucketWarnings).toHaveLength(1); @@ -1463,44 +1463,6 @@ describe("db start", () => { }, ); - it.effect.each([ - { - with: "alone", - env: { SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION: "17.11.0.002" }, - expectedTag: "17.11.0.002-orioledb", - }, - { - with: "and SUPABASE_DB_ORIOLEDB_VERSION (wins)", - env: { - SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION: "17.11.0.002", - SUPABASE_DB_ORIOLEDB_VERSION: "17.11.0.003", - }, - expectedTag: "17.11.0.003-orioledb", - }, - ])( - "selects the OrioleDB image with SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION $with, warning once that it is removed in v3", - ({ env, expectedTag }) => { - for (const [name, value] of Object.entries(env)) vi.stubEnv(name, value); - const { layer, child } = setup({ route: freshVolumeRoute(defaultRoute()) }); - const warnings: Array = []; - const errorSpy = vi.spyOn(console, "error").mockImplementation((...args) => { - warnings.push(args.map((a) => String(a)).join(" ")); - }); - return Effect.gen(function* () { - yield* dbStart(DEFAULT_FLAGS).pipe(Effect.provide(layer)); - const image = createArgs(child.spawned)?.find((arg) => arg.includes("supabase/postgres:")); - expect(image).toContain(expectedTag); - expect( - warnings.filter( - (m) => - m === - "WARN: SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION is deprecated and will be removed in v3, use SUPABASE_DB_ORIOLEDB_VERSION instead.", - ), - ).toHaveLength(1); - }).pipe(Effect.ensuring(Effect.sync(() => errorSpy.mockRestore()))); - }, - ); - it.effect("fails on a malformed auth duration field even when the db is already running", () => { const { layer, out } = setup({ configContents: 'project_id = "test"\n[auth.email]\nmax_frequency = "not-a-duration"\n', diff --git a/apps/cli/src/commands/experimental/stack/start/start.handler.ts b/apps/cli/src/commands/experimental/stack/start/start.handler.ts index db3e60a0a8..665f8229a2 100644 --- a/apps/cli/src/commands/experimental/stack/start/start.handler.ts +++ b/apps/cli/src/commands/experimental/stack/start/start.handler.ts @@ -52,7 +52,11 @@ import { projectCatalogOverlay, } from "../../../../command-internal/stack-bootstrap.ts"; import { stackStorageCredentialsFor } from "../../../../command-internal/stack-storage.ts"; -import { hasConfiguredBuckets, seedBucketsRun } from "../../../../command-internal/seed-buckets.ts"; +import { + hasConfiguredBuckets, + SeedConfigLoadError, + seedBucketsRun, +} from "../../../../command-internal/seed-buckets.ts"; import { describeConfigLoadFailure, loadResolvedConfigContext, @@ -61,7 +65,6 @@ import { CliConfigKeys } from "../../../../config/cli-config-keys.ts"; import type { CliConfigMaterialized } from "../../../../config/cli-config-values.service.ts"; import { loadStackConfig, - StackConfigError, stackEndpointKey, stackMajorVersionKey, type StackEndpointKey, @@ -200,15 +203,10 @@ const stackAcquireError = ( const loadStartConfig = Effect.fn("experimental.stack.start.loadConfig")( function* (projectRoot: string, fs: FileSystem.FileSystem, path: Path.Path) { - const context = yield* loadResolvedConfigContext(projectRoot).pipe( - Effect.mapError( - (cause) => new StackConfigError({ message: describeConfigLoadFailure(cause) }), - ), - ); const config = yield* loadStackConfig(projectRoot); const keys = yield* config.keys; const toml = yield* readDbToml(fs, path, projectRoot); - return { context, config, keys, toml }; + return { config, keys, toml }; }, Effect.mapError( (error) => @@ -266,10 +264,9 @@ const settingKeyLabel = ( setting: StackEndpointKey, originAt: CliConfigMaterialized["originAt"], ): string => { - const origin = originAt(setting.path); - // Names the variable that actually won, so a deprecated alias is reported as itself. - return origin.tier === "shell" || origin.tier === "projectEnv" - ? origin.envName + const { tier } = originAt(setting.path); + return tier === "shell" || tier === "projectEnv" + ? (setting.env[0] ?? formatConfigPath(setting.path)) : formatConfigPath(setting.path); }; @@ -566,9 +563,10 @@ export const stackStart = Effect.fn("experimental.stack.start")(function* (flags StepDown: (stepDown) => output.info(stepDown.message), NotNeeded: () => Effect.void, }); - // Every project-context load prints the config deprecation WARNs, so it happens at most once. - const startConfig = yield* Effect.cached(loadStartConfig(target.projectRoot, fs, path)); - if (target.id === undefined || !target.hostRunning) yield* startConfig; + const configBeforeCreate = + target.id === undefined || !target.hostRunning + ? yield* loadStartConfig(target.projectRoot, fs, path) + : undefined; if (target.id === undefined) yield* ensurePostgresUser; const stateRoot = path.join(settings.supabaseHome, "stacks"); const cacheRoot = path.join(settings.supabaseHome, "cache", "stack"); @@ -609,7 +607,7 @@ export const stackStart = Effect.fn("experimental.stack.start")(function* (flags // configuration that cannot resolve only fails a start whose owner was not running. const resolveForReplan = (stackId: string) => Effect.gen(function* () { - const loaded = yield* startConfig; + const loaded = configBeforeCreate ?? (yield* loadStartConfig(target.projectRoot, fs, path)); return yield* resolveRequested(stackId, loaded.config); }); const requestedForReplan = @@ -807,7 +805,8 @@ export const stackStart = Effect.fn("experimental.stack.start")(function* (flags message: "A standalone database exists outside the saved stack composition", suggestion: "Destroy the standalone database before starting this stack.", }); - const { context, config, keys, toml } = yield* startConfig; + const { config, keys, toml } = + configBeforeCreate ?? (yield* loadStartConfig(target.projectRoot, fs, path)); // Reuses the creations already resolved for the re-plan above instead of reading the // Functions dotenv a second time; only a new or already-running stack has none yet. const requested = requestedForReplan ?? (yield* resolveRequested(stack.id, config)); @@ -1036,6 +1035,11 @@ export const stackStart = Effect.fn("experimental.stack.start")(function* (flags if (initialComposition) { const storage = members.find((instance) => instance.service === "storage"); if (storage !== undefined) { + const context = yield* loadResolvedConfigContext(target.projectRoot).pipe( + Effect.mapError( + (cause) => new SeedConfigLoadError({ message: describeConfigLoadFailure(cause) }), + ), + ); if (hasConfiguredBuckets(context.config)) { yield* Effect.annotateCurrentSpan({ "stack.storage_seeded": true }); yield* storage.start.pipe( diff --git a/apps/cli/src/commands/experimental/stack/start/start.integration.test.ts b/apps/cli/src/commands/experimental/stack/start/start.integration.test.ts index 989886bb51..9b10e80ec9 100644 --- a/apps/cli/src/commands/experimental/stack/start/start.integration.test.ts +++ b/apps/cli/src/commands/experimental/stack/start/start.integration.test.ts @@ -1,7 +1,6 @@ import { generateKeyPairSync } from "node:crypto"; import { BunServices } from "@effect/platform-bun"; import { describe, expect, it } from "@effect/vitest"; -import { vi } from "vitest"; import { Deferred, Effect, @@ -2120,7 +2119,6 @@ describe("experimental stack start", () => { before: "[db]\nmajor_version = 15\n", after: `[db]\norioledb_version = "${orioledb}"\n`, env: undefined, - envVar: "SUPABASE_DB_ORIOLEDB_VERSION", changes: `[db] major_version: saved 15, requested 17; [db] orioledb_version: saved unset, requested ${orioledb}`, revert: "Revert the settings listed to their saved values", }, @@ -2129,19 +2127,9 @@ describe("experimental stack start", () => { before: "", after: "", env: orioledb, - envVar: "SUPABASE_DB_ORIOLEDB_VERSION", changes: `SUPABASE_DB_ORIOLEDB_VERSION: saved unset, requested ${orioledb}`, revert: "Revert SUPABASE_DB_ORIOLEDB_VERSION to its saved value", }, - { - name: "the deprecated SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION when it alone switches a saved stock stack to OrioleDB", - before: "", - after: "", - env: orioledb, - envVar: "SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION", - changes: `SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION: saved unset, requested ${orioledb}`, - revert: "Revert SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION to its saved value", - }, ]) it.effect(`names ${target.name}`, () => Effect.gen(function* () { @@ -2161,7 +2149,7 @@ describe("experimental stack start", () => { ); const error = yield* target.env === undefined ? restart - : withEnvVar(target.envVar, target.env, restart); + : withEnvVar("SUPABASE_DB_ORIOLEDB_VERSION", target.env, restart); expect(error).toMatchObject({ reason: "invalid-config", @@ -2173,39 +2161,6 @@ describe("experimental stack start", () => { }).pipe(Effect.provide(BunServices.layer)), ); - it.effect( - "warns once about SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION on a first start that includes storage", - () => - Effect.gen(function* () { - const fs = yield* FileSystem.FileSystem; - const root = yield* fs.makeTempDirectoryScoped({ prefix: "stack-start-orioledb-alias-" }); - yield* fs.makeDirectory(`${root}/supabase`, { recursive: true }); - yield* fs.writeFileString(`${root}/supabase/config.toml`, 'project_id = "alias"\n'); - yield* fs.writeFileString( - `${root}/supabase/.env`, - `SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION=${orioledb}\n`, - ); - const fixture = fakeStack(); - const consoleErrors: Array = []; - const errorSpy = vi.spyOn(console, "error").mockImplementation((...args) => { - consoleErrors.push(args.map((a) => String(a)).join(" ")); - }); - yield* stackStart(flags()).pipe( - Effect.provide(layers(root, fixture)), - Effect.ensuring(Effect.sync(() => errorSpy.mockRestore())), - ); - const services = yield* fixture.stack.services.list; - expect(services.map(({ service }) => service)).toContain("storage"); - expect( - consoleErrors.filter( - (line) => - line === - "WARN: SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION is deprecated and will be removed in v3, use SUPABASE_DB_ORIOLEDB_VERSION instead.", - ), - ).toHaveLength(1); - }).pipe(Effect.provide(BunServices.layer)), - ); - it.effect("offers no revert to an OrioleDB build the catalog no longer pins", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; diff --git a/apps/cli/src/commands/functions/deploy/SIDE_EFFECTS.md b/apps/cli/src/commands/functions/deploy/SIDE_EFFECTS.md index 4d0fd784f4..a28e9fe980 100644 --- a/apps/cli/src/commands/functions/deploy/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/functions/deploy/SIDE_EFFECTS.md @@ -90,7 +90,7 @@ The `--output`/`-o` flag does not change deploy output. ## Notes -- **Config value precedence** (ADR 0032): explicit flag > shell env > project `.env*` > config +- **Config value precedence** (ADR 0031): explicit flag > shell env > project `.env*` > config (`config.json` over `config.toml`) > default. The whole config is decoded up front, so an invalid value, including an unparsable `SUPABASE_*` override, fails the command. - If no function name is provided, deploys all functions found in `supabase/functions/`. @@ -102,7 +102,6 @@ The `--output`/`-o` flag does not change deploy output. - Requires a linked project unless `--project-ref` is provided. - Bundles locally with Docker by default (`--use-docker` defaults to true and is hidden); `--use-api` selects server-side bundling, and a stopped Docker daemon falls back to it after a `WARNING: Docker is not running`. - Local Docker bundling mounts existing local values declared under an import map's `scopes` read-only, including targets outside the nearest Git root; each such out-of-root mount prints a `WARN` naming the host path. The mounted target itself is bound as declared; imports reached from inside an out-of-root target are not additionally bound. API source uploads retain their existing source-root restrictions. -- A Function resolved through a per-function `import_map.json` (no `deno.json`/`deno.jsonc` beside it, no configured or `--import-map` override) prints `WARNING: import_map.json is deprecated and will be removed in v3, use deno.json instead. Functions using it: , ...` to stderr once per deploy; stdout is unaffected. - `--use-api`, `--use-docker`, and `--legacy-bundle` are mutually exclusive deploy modes. - `--prune` deletes deployed Functions that are not present locally after a confirmation prompt; global `--yes` skips the prompt. diff --git a/apps/cli/src/commands/functions/deploy/deploy.integration.test.ts b/apps/cli/src/commands/functions/deploy/deploy.integration.test.ts index 24a9cc0449..88b81cfb6b 100644 --- a/apps/cli/src/commands/functions/deploy/deploy.integration.test.ts +++ b/apps/cli/src/commands/functions/deploy/deploy.integration.test.ts @@ -385,62 +385,6 @@ describe("functions deploy", () => { }).pipe(Effect.provide(layer), Effect.ensuring(removeTempRoot)); }); - it.effect("warns on stderr that a per-function import_map.json is removed in v3", () => { - const out = mockOutput({ format: "text" }); - const api = mockCommandPlatformApi({ - handler: (request) => { - if (request.method === "GET") { - return Effect.succeed(jsonResponse(request, 200, [])); - } - return Effect.succeed( - jsonResponse(request, 201, { - id: "function-id", - slug: "hello-world", - name: "hello-world", - status: "ACTIVE", - version: 2, - created_at: 1_687_423_025_152, - updated_at: 1_687_423_025_152, - verify_jwt: true, - import_map: true, - entrypoint_path: "supabase/functions/hello-world/index.ts", - import_map_path: "supabase/functions/hello-world/import_map.json", - }), - ); - }, - }); - const layer = Layer.mergeAll( - cliConfigValuesTestLayer, - buildTestRuntime({ - out, - api, - cliSettings: mockCommandSettings({ workdir: tempRoot.current }), - runtimeInfo: mockRuntimeInfo({ cwd: tempRoot.current }), - }), - Layer.succeed(YesFlag, false), - Stdio.layerTest({ - args: Effect.succeed(["functions", "deploy", "hello-world", "--use-api"]), - }), - ); - - return Effect.gen(function* () { - const fs = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - yield* writeCliConfig(tempRoot.current); - yield* writeLocalFunction(tempRoot.current, "hello-world"); - const functionDir = path.join(tempRoot.current, "supabase", "functions", "hello-world"); - yield* fs.remove(path.join(functionDir, "deno.json")); - yield* fs.writeFileString(path.join(functionDir, "import_map.json"), '{"imports":{}}'); - - yield* functionsDeploy(baseFlags); - - expect(out.stderrText).toContain( - "WARNING: import_map.json is deprecated and will be removed in v3, use deno.json instead. Functions using it: hello-world\n", - ); - expect(out.stdoutText).not.toContain("import_map.json"); - }).pipe(Effect.provide(layer), Effect.ensuring(removeTempRoot)); - }); - it.effect("loads project config from the resolved workdir", () => { const callerDir = `${tempRoot.current}/caller`; const out = mockOutput({ format: "text" }); diff --git a/apps/cli/src/commands/gen/types/SIDE_EFFECTS.md b/apps/cli/src/commands/gen/types/SIDE_EFFECTS.md index 24a47105d1..1443573e1f 100644 --- a/apps/cli/src/commands/gen/types/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/gen/types/SIDE_EFFECTS.md @@ -89,7 +89,6 @@ way `--db-url` does. | `SUPABASE_SERVICES_HOSTNAME` | host used to reach the local database on the legacy Docker Compose stack; read from the shell env first, then project `.env*` | no (defaults to the shell `DOCKER_HOST`'s tcp host, else `127.0.0.1`) | | `SUPABASE_WORKDIR` | working directory `supabase/config.toml`/`config.json` is read from (`--workdir` takes priority) | no — when unset, the CLI walks up from cwd looking for `supabase/config.toml` or `supabase/config.json`; when SET (flag or env) the directory is used exactly as given and **no ancestor is searched** | | `PATH`, `PATHEXT`, `ComSpec` | find the tool of an out-of-process `--lang`; `PATHEXT` and `ComSpec` matter on Windows only | no (`PATHEXT` defaults to `.COM;.EXE;.BAT;.CMD`, `ComSpec` to `cmd.exe`; without `PATH` the tool is reported as not installed) | -| `SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION` | deprecated alias of `SUPABASE_DB_ORIOLEDB_VERSION`, used only while that is unset; when set, stderr gets `WARN: SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION is deprecated and will be removed in v3, use SUPABASE_DB_ORIOLEDB_VERSION instead.` | no | ## Exit Codes @@ -139,7 +138,7 @@ Not applicable. shell and nested dotenv overrides, matching the CLI. The database password is the `[db].password` value (default `postgres`); `SUPABASE_DB_PASSWORD` has no effect, and the command has no `--password` flag. -- **Config value precedence** (ADR 0032): explicit flag > shell env > project `.env*` > config +- **Config value precedence** (ADR 0031): explicit flag > shell env > project `.env*` > config (`config.json` over `config.toml`; a matched `[remotes.*]` block over the base document on `--linked`/`--project-id`) > default. The whole config is decoded up front where it is loaded, so an invalid value fails the command. diff --git a/apps/cli/src/commands/migration/down/SIDE_EFFECTS.md b/apps/cli/src/commands/migration/down/SIDE_EFFECTS.md index c4540b1392..b9a89f0d10 100644 --- a/apps/cli/src/commands/migration/down/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/migration/down/SIDE_EFFECTS.md @@ -22,11 +22,10 @@ ## Environment Variables -| Variable | Purpose | Required? | -| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------- | -| `SUPABASE_ACCESS_TOKEN` | auth token for `--linked` mode | no (falls back to keyring → `~/.supabase/access-token`) | -| `DOTENV_PRIVATE_KEY[_*]` | dotenvx private key(s) to decrypt `encrypted:` `[db.vault]` secrets before upsert | no (required only if a `[db.vault]` value is encrypted) | -| `SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION` | deprecated alias of `SUPABASE_DB_ORIOLEDB_VERSION`, used only while that is unset; when set, stderr gets `WARN: SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION is deprecated and will be removed in v3, use SUPABASE_DB_ORIOLEDB_VERSION instead.` | no | +| Variable | Purpose | Required? | +| ------------------------ | --------------------------------------------------------------------------------- | ------------------------------------------------------- | +| `SUPABASE_ACCESS_TOKEN` | auth token for `--linked` mode | no (falls back to keyring → `~/.supabase/access-token`) | +| `DOTENV_PRIVATE_KEY[_*]` | dotenvx private key(s) to decrypt `encrypted:` `[db.vault]` secrets before upsert | no (required only if a `[db.vault]` value is encrypted) | ## Exit Codes diff --git a/apps/cli/src/commands/migration/up/SIDE_EFFECTS.md b/apps/cli/src/commands/migration/up/SIDE_EFFECTS.md index f87eabcfc0..b1557d03d7 100644 --- a/apps/cli/src/commands/migration/up/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/migration/up/SIDE_EFFECTS.md @@ -22,11 +22,10 @@ ## Environment Variables -| Variable | Purpose | Required? | -| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------- | -| `SUPABASE_ACCESS_TOKEN` | auth token for `--linked` mode | no (falls back to keyring → `~/.supabase/access-token`) | -| `DOTENV_PRIVATE_KEY[_*]` | dotenvx private key(s) to decrypt `encrypted:` `[db.vault]` secrets before upsert | no (required only if a `[db.vault]` value is encrypted) | -| `SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION` | deprecated alias of `SUPABASE_DB_ORIOLEDB_VERSION`, used only while that is unset; when set, stderr gets `WARN: SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION is deprecated and will be removed in v3, use SUPABASE_DB_ORIOLEDB_VERSION instead.` | no | +| Variable | Purpose | Required? | +| ------------------------ | --------------------------------------------------------------------------------- | ------------------------------------------------------- | +| `SUPABASE_ACCESS_TOKEN` | auth token for `--linked` mode | no (falls back to keyring → `~/.supabase/access-token`) | +| `DOTENV_PRIVATE_KEY[_*]` | dotenvx private key(s) to decrypt `encrypted:` `[db.vault]` secrets before upsert | no (required only if a `[db.vault]` value is encrypted) | ## Exit Codes diff --git a/apps/cli/src/commands/migration/up/up.integration.test.ts b/apps/cli/src/commands/migration/up/up.integration.test.ts index 24c79f8c54..8372615e18 100644 --- a/apps/cli/src/commands/migration/up/up.integration.test.ts +++ b/apps/cli/src/commands/migration/up/up.integration.test.ts @@ -312,25 +312,6 @@ describe("migration up", () => { }).pipe(Effect.provide(layer)); }); - it.effect( - "warns once on stderr that SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION is removed in v3", - () => { - const { layer, out } = setup(tmp.current); - return Effect.gen(function* () { - yield* writeProjectFile( - tmp.current, - ".env", - "SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION=17.11.0.002\n", - ); - yield* migrationUp(flags()); - const warning = - "WARN: SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION is deprecated and will be removed in v3, use SUPABASE_DB_ORIOLEDB_VERSION instead."; - expect(out.stderrText.split("\n").filter((line) => line === warning)).toHaveLength(1); - expect(out.stdoutText).not.toContain(warning); - }).pipe(Effect.provide(layer)); - }, - ); - it.effect("surfaces an apply failure", () => { const { layer } = setup(tmp.current, { remote: [], failApply: true }); return Effect.gen(function* () { diff --git a/apps/cli/src/commands/services/SIDE_EFFECTS.md b/apps/cli/src/commands/services/SIDE_EFFECTS.md index 71870da0af..70ca8c048c 100644 --- a/apps/cli/src/commands/services/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/services/SIDE_EFFECTS.md @@ -40,15 +40,13 @@ Tenant calls send `apikey: ` and additionally ## Environment Variables -| Variable | Purpose | Required? | -| ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | -| `SUPABASE_ACCESS_TOKEN` | auth token for Management API linked-version checks | no (falls back to keyring, then `~/.supabase/access-token`) | -| `SUPABASE_PROFILE` | built-in profile name or YAML file path | no (falls back to `~/.supabase/profile` -> `supabase`) | -| `SUPABASE_USE_SLIM_IMAGES` | Ambient `process.env` only (`true`/`1` enable). Rewrites legacy current-pin `SERVICE IMAGE`/`name` fields to `ghcr.io/supabase/cli/`; Kong stays on docker.io. Majors 13/15 list the slim `15.14.1.167` pin when enabled; flag-off keeps `15.8.1.085`. Ignored by the stack backend. | no | -| `SUPABASE_EXPERIMENTAL_STACK` | Selects the stack backend (`1`/`true`) or legacy backend (`0`/`false`); unset or empty uses `experimental.stack`. Non-boolean values fail routing. | no | -| `SUPABASE_DB_MAJOR_VERSION` | Stack backend PostgreSQL major override; supported values are 15 and 17. | no | -| `SUPABASE_DB_ORIOLEDB_VERSION` | legacy backend: overrides `db.orioledb_version`, selecting the listed OrioleDB Postgres image | no | -| `SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION` | deprecated alias of `SUPABASE_DB_ORIOLEDB_VERSION`, used only while that is unset; when set, stderr gets `WARN: SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION is deprecated and will be removed in v3, use SUPABASE_DB_ORIOLEDB_VERSION instead.` | no | +| Variable | Purpose | Required? | +| ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | +| `SUPABASE_ACCESS_TOKEN` | auth token for Management API linked-version checks | no (falls back to keyring, then `~/.supabase/access-token`) | +| `SUPABASE_PROFILE` | built-in profile name or YAML file path | no (falls back to `~/.supabase/profile` -> `supabase`) | +| `SUPABASE_USE_SLIM_IMAGES` | Ambient `process.env` only (`true`/`1` enable). Rewrites legacy current-pin `SERVICE IMAGE`/`name` fields to `ghcr.io/supabase/cli/`; Kong stays on docker.io. Majors 13/15 list the slim `15.14.1.167` pin when enabled; flag-off keeps `15.8.1.085`. Ignored by the stack backend. | no | +| `SUPABASE_EXPERIMENTAL_STACK` | Selects the stack backend (`1`/`true`) or legacy backend (`0`/`false`); unset or empty uses `experimental.stack`. Non-boolean values fail routing. | no | +| `SUPABASE_DB_MAJOR_VERSION` | Stack backend PostgreSQL major override; supported values are 15 and 17. | no | ## Exit Codes @@ -85,7 +83,7 @@ NDJSON success event with the same `{ services: [...] }` payload. ## Notes -- Config values resolve as flag > shell env > project `.env*` > config (`config.json` over `config.toml`) > default (ADR 0032). An invalid config value or `SUPABASE_*` override fails the command with exit 1, naming its source. +- Config values resolve as flag > shell env > project `.env*` > config (`config.json` over `config.toml`) > default (ADR 0031). An invalid config value or `SUPABASE_*` override fails the command with exit 1, naming its source. - Backend selection follows canonical experimental-feature routing: `SUPABASE_EXPERIMENTAL_STACK=1|0` takes precedence over `experimental.stack`; unset or empty uses project config, and an invalid environment value fails. Output fields and serializers stay the same. - The legacy backend uses its baked-in service matrix and honors its existing config/version overrides. The stack backend lists services from the installed CLI artifact catalog using canonical `ghcr.io/supabase/cli/...` image names and catalog versions, reported as the upstream version without a slim revision suffix (`-rN`). Native and Docker runtimes use the same catalog versions. Since the catalog belongs to the installed CLI, an older launched CLI or a newer/mirrored stack image may differ from this inventory. The command does not inspect running containers, image pulls, service health, or live stack state. - For stack mode, PostgreSQL uses the configured major version or `SUPABASE_DB_MAJOR_VERSION` (15 or 17). An unreadable config file or an unsupported PostgreSQL major warns with the cause and falls back to default catalog versions; an invalid config value fails the command; absent config uses defaults. Legacy image pins, slim-image rewriting, and remote image overrides do not affect stack results. diff --git a/apps/cli/src/commands/status/SIDE_EFFECTS.md b/apps/cli/src/commands/status/SIDE_EFFECTS.md index 16227791df..059e811863 100644 --- a/apps/cli/src/commands/status/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/status/SIDE_EFFECTS.md @@ -212,12 +212,6 @@ headers); colors are stripped on non-TTY/piped output. bracketed list, e.g. `[supabase_storage_test supabase_studio_test]`) whenever one of the 13 expected service containers isn't in the running set. -Whenever a machine format (`-o env|json|toml|yaml`, or `--output-format json|stream-json`) -emits the deprecated `INBUCKET_URL` field, stderr gets -`WARN: INBUCKET_URL is deprecated and will be removed in v3, use MAILPIT_URL instead.` (both -names follow `--override-name`). stdout stays payload-only, and the pretty table, which never -shows `INBUCKET_URL`, prints no warning. - ### `-o env` `KEY="VALUE"` lines (unquoted for integer-looking values), one per resolved field, sorted by diff --git a/apps/cli/src/commands/status/status.handler.ts b/apps/cli/src/commands/status/status.handler.ts index afec4a2600..8dc01cfa69 100644 --- a/apps/cli/src/commands/status/status.handler.ts +++ b/apps/cli/src/commands/status/status.handler.ts @@ -47,7 +47,6 @@ import { import { renderStatusPretty } from "../../command-internal/status-pretty.ts"; import { STATUS_FIELDS, - deprecatedStatusFieldWarning, gateStatusState, resolveStatusLocalState, statusContainerIds, @@ -249,16 +248,6 @@ export const status = Effect.fn("status")(function* (flags: StatusFlags) { // with the linked-state extension, never the other way round. const valuesWithLinkedState = { ...linkedStateFields(linkedState), ...values }; - // `INBUCKET_URL` appears only in the machine formats, never the pretty table. - const machineFormat = - outputFlagFormat === undefined - ? output.format === "json" || output.format === "stream-json" - : outputFlagFormat !== "pretty"; - const deprecationWarning = deprecatedStatusFieldWarning(state, overrides); - if (machineFormat && deprecationWarning !== undefined) { - yield* output.raw(deprecationWarning, "stderr"); - } - if (outputFlagFormat === "env") { yield* output.raw(encodeEnv(valuesWithLinkedState) + "\n"); return; diff --git a/apps/cli/src/commands/status/status.integration.test.ts b/apps/cli/src/commands/status/status.integration.test.ts index d05367109d..84bbeb6c7b 100644 --- a/apps/cli/src/commands/status/status.integration.test.ts +++ b/apps/cli/src/commands/status/status.integration.test.ts @@ -446,9 +446,6 @@ function setupFailureEnvelope(opts: FailureEnvelopeOpts) { return { workdir, layer, stdio, processControl }; } -const INBUCKET_WARNING = - "WARN: INBUCKET_URL is deprecated and will be removed in v3, use MAILPIT_URL instead.\n"; - describe("status integration", () => { it.effect("shows the running stack as a pretty table", () => { const { layer, out } = setup(); @@ -463,7 +460,6 @@ describe("status integration", () => { expect(out.stdoutText).toContain("📦 Storage (S3)"); expect(out.stdoutText).toContain("postgresql://postgres:postgres@"); expect(out.stderrText).not.toContain("Stopped services:"); - expect(out.stderrText).not.toContain("INBUCKET_URL"); }).pipe(Effect.provide(layer)); }); @@ -1027,21 +1023,15 @@ content_path = "./supabase/templates/password_changed_notification.html" }).pipe(Effect.provide(layer)); }); - it.effect( - "outputs env vars with -o env, warning on stderr that INBUCKET_URL is removed in v3", - () => { - const { layer, out } = setup({ outputFlag: Option.some("env") }); - return Effect.gen(function* () { - yield* writeConfig(); - yield* status(flags()); - expect(out.stdoutText).toContain('API_URL="http://127.0.0.1:54321"'); - expect(out.stdoutText).toContain("DB_URL="); - expect(out.stdoutText).toContain("INBUCKET_URL="); - expect(out.stdoutText).not.toContain("deprecated"); - expect(out.stderrText).toContain(INBUCKET_WARNING); - }).pipe(Effect.provide(layer)); - }, - ); + it.effect("outputs env vars with -o env", () => { + const { layer, out } = setup({ outputFlag: Option.some("env") }); + return Effect.gen(function* () { + yield* writeConfig(); + yield* status(flags()); + expect(out.stdoutText).toContain('API_URL="http://127.0.0.1:54321"'); + expect(out.stdoutText).toContain("DB_URL="); + }).pipe(Effect.provide(layer)); + }); it.effect("prints API_URL but no REST_URL with -o env when [api] enabled = false", () => { const { layer, out } = setup({ @@ -2146,47 +2136,32 @@ content_path = "./supabase/templates/password_changed_notification.html" }, ); - it.effect( - "--output-format json, not linked: linked_project is null, INBUCKET_URL warning on stderr only", - () => { - const { layer, out } = setup({ format: "json" }); - return Effect.gen(function* () { - yield* writeConfig(); - yield* status(flags()); - const success = out.messages.find((m) => m.type === "success"); - expect(success?.data).toMatchObject({ - linked_project: null, - INBUCKET_URL: expect.any(String), - }); - expect(out.stderrText).toBe(INBUCKET_WARNING); - expect(out.stdoutText).toBe(""); - }).pipe(Effect.provide(layer)); - }, - ); + it.effect("--output-format json, not linked: linked_project is null", () => { + const { layer, out } = setup({ format: "json" }); + return Effect.gen(function* () { + yield* writeConfig(); + yield* status(flags()); + const success = out.messages.find((m) => m.type === "success"); + expect(success?.data).toMatchObject({ linked_project: null }); + }).pipe(Effect.provide(layer)); + }); - it.effect( - "--output-format stream-json, branch-linked: nests linked_project (smoke), INBUCKET_URL warning on stderr only", - () => { - const { layer, out, workdir } = setup({ - format: "stream-json", - branches: { ok: [LINKED_BRANCH] }, + it.effect("--output-format stream-json, branch-linked: nests linked_project (smoke)", () => { + const { layer, out, workdir } = setup({ + format: "stream-json", + branches: { ok: [LINKED_BRANCH] }, + }); + return Effect.gen(function* () { + yield* writeConfig(); + yield* writeProjectRefFile(workdir, LINKED_BRANCH_REF); + yield* writeLinkedProjectCacheFile(workdir, LINKED_PARENT_REF, { name: "Parent Project" }); + yield* status(flags()); + const success = out.messages.find((m) => m.type === "success"); + expect(success?.data).toMatchObject({ + linked_project: { project_ref: LINKED_BRANCH_REF }, }); - return Effect.gen(function* () { - yield* writeConfig(); - yield* writeProjectRefFile(workdir, LINKED_BRANCH_REF); - yield* writeLinkedProjectCacheFile(workdir, LINKED_PARENT_REF, { - name: "Parent Project", - }); - yield* status(flags()); - const success = out.messages.find((m) => m.type === "success"); - expect(success?.data).toMatchObject({ - linked_project: { project_ref: LINKED_BRANCH_REF }, - }); - expect(out.stderrText).toBe(INBUCKET_WARNING); - expect(out.stdoutText).toBe(""); - }).pipe(Effect.provide(layer)); - }, - ); + }).pipe(Effect.provide(layer)); + }); describe("json/stream-json FAILURE envelope carries linked_project (CLI-2167 follow-up)", () => { it.effect( diff --git a/apps/cli/src/commands/test/db/SIDE_EFFECTS.md b/apps/cli/src/commands/test/db/SIDE_EFFECTS.md index 71d9a6f593..2e6f79da54 100644 --- a/apps/cli/src/commands/test/db/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/test/db/SIDE_EFFECTS.md @@ -49,15 +49,14 @@ Managed stack `--local` prove runs the catalog `pg_prove` tool through the ownin ## Environment Variables -| Variable | Purpose | Required? | -| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------- | -| `SUPABASE_DB_PASSWORD` | `--linked`: skip temporary login-role creation | no | -| `SUPABASE_ACCESS_TOKEN` | `--linked`: Management API auth | no (falls back to keyring/file) | -| `SUPABASE_SERVICES_HOSTNAME` | `--local`: overrides the local DB host (dev-container/remote Docker) | no (defaults via `DOCKER_HOST` → `127.0.0.1`) | -| `DOCKER_HOST` | `--local`: tcp daemon host used when no services-hostname override | no | -| `BITBUCKET_CLONE_DIR` | when set, omit `--security-opt label:disable` (Bitbucket rejects it) | no | -| `DEBUG` / `--debug` | append `--verbose` to `pg_prove` | no | -| `SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION` | deprecated alias of `SUPABASE_DB_ORIOLEDB_VERSION`, used only while that is unset; when set, stderr gets `WARN: SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION is deprecated and will be removed in v3, use SUPABASE_DB_ORIOLEDB_VERSION instead.` | no | +| Variable | Purpose | Required? | +| ---------------------------- | -------------------------------------------------------------------- | --------------------------------------------- | +| `SUPABASE_DB_PASSWORD` | `--linked`: skip temporary login-role creation | no | +| `SUPABASE_ACCESS_TOKEN` | `--linked`: Management API auth | no (falls back to keyring/file) | +| `SUPABASE_SERVICES_HOSTNAME` | `--local`: overrides the local DB host (dev-container/remote Docker) | no (defaults via `DOCKER_HOST` → `127.0.0.1`) | +| `DOCKER_HOST` | `--local`: tcp daemon host used when no services-hostname override | no | +| `BITBUCKET_CLONE_DIR` | when set, omit `--security-opt label:disable` (Bitbucket rejects it) | no | +| `DEBUG` / `--debug` | append `--verbose` to `pg_prove` | no | ## Exit Codes diff --git a/apps/cli/src/config/cli-config-key-annotations.ts b/apps/cli/src/config/cli-config-key-annotations.ts index 0f5de3755a..f4183202bb 100644 --- a/apps/cli/src/config/cli-config-key-annotations.ts +++ b/apps/cli/src/config/cli-config-key-annotations.ts @@ -64,14 +64,6 @@ export const CLI_CONFIG_ENV_EXCLUDED: Readonly> = { "experimental.orioledb_version": "deprecated; promoted to db.orioledb_version before resolution", }; -/** - * Deprecated env names a schema leaf still honours after its canonical derived name, in order. The - * config loader warns once when one of them is set. - */ -export const CLI_CONFIG_ENV_ALIASES: Readonly>> = { - "db.orioledb_version": ["SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION"], -}; - const emptyString = () => ""; /** diff --git a/apps/cli/src/config/cli-config-keys.ts b/apps/cli/src/config/cli-config-keys.ts index 43e3fd47ef..4d6314f723 100644 --- a/apps/cli/src/config/cli-config-keys.ts +++ b/apps/cli/src/config/cli-config-keys.ts @@ -5,7 +5,6 @@ import { CLI_CONFIG_CODEC_OVERRIDES, CLI_CONFIG_CONTEXT_DEFAULTS, CLI_CONFIG_DOCUMENT_KEYS, - CLI_CONFIG_ENV_ALIASES, CLI_CONFIG_ENV_EXCLUDED, CLI_CONFIG_FAMILIES, CLI_CONFIG_FLAGS, @@ -165,7 +164,6 @@ export const cliConfigSchemaKeyDefs = (root: SchemaAST.Objects): ReadonlyArray = Object.create(null); for (const name of new Set([...Object.keys(projectEnv.values), ...shell.entries().keys()])) { const value = lookupEnv(name); diff --git a/apps/cli/src/shared/functions/deploy.ts b/apps/cli/src/shared/functions/deploy.ts index e0d68e1b74..64dd94307c 100644 --- a/apps/cli/src/shared/functions/deploy.ts +++ b/apps/cli/src/shared/functions/deploy.ts @@ -2211,7 +2211,7 @@ export const resolveFunctionConfigs = Effect.fn("functions.deploy.resolveConfigs if (seenDeprecatedImportMap.size > 0) { yield* output.raw( - `WARNING: import_map.json is deprecated and will be removed in v3, use deno.json instead. Functions using it: ${[...seenDeprecatedImportMap].join(", ")}\n`, + `WARNING: Functions using deprecated import_map.json (please migrate to deno.json): ${[...seenDeprecatedImportMap].join(", ")}\n`, "stderr", ); } diff --git a/packages/config/src/io.stages.unit.test.ts b/packages/config/src/io.stages.unit.test.ts index 8cbeac5d71..773d2a921d 100644 --- a/packages/config/src/io.stages.unit.test.ts +++ b/packages/config/src/io.stages.unit.test.ts @@ -178,8 +178,8 @@ describe("config pipeline stages", () => { ); expect(warnings).toEqual([ - "WARN: config section [inbucket] is deprecated and will be removed in v3, use [local_smtp] instead.", - 'WARN: disabling deprecated "slack" provider. It will be removed in v3, use [auth.external.slack_oidc] instead.', + "WARN: config section [inbucket] is deprecated. Please use [local_smtp] instead.", + 'WARN: disabling deprecated "slack" provider. Please use [auth.external.slack_oidc] instead', ]); expect(errorSpy).not.toHaveBeenCalled(); } finally { diff --git a/packages/config/src/io.ts b/packages/config/src/io.ts index 70aeebf0c1..fefdc66a0f 100644 --- a/packages/config/src/io.ts +++ b/packages/config/src/io.ts @@ -593,14 +593,12 @@ const readAndNormalizeCliConfigFile = Effect.fnUntraced(function* ( for (const section of deprecatedSections) { const replacement = section.replace(/inbucket$/, "local_smtp"); yield* onWarning( - `WARN: config section [${section}] is deprecated and will be removed in v3, use [${replacement}] instead.`, + `WARN: config section [${section}] is deprecated. Please use [${replacement}] instead.`, ); } for (const path of deprecatedOrioleDBPaths) { const replacement = path.replace(/experimental\.orioledb_version$/, "db.orioledb_version"); - yield* onWarning( - `WARN: ${path} is deprecated and will be removed in v3, use ${replacement} instead.`, - ); + yield* onWarning(`WARN: ${path} is deprecated. Please use ${replacement} instead.`); } return { format, content, document, normalized } as const; }); @@ -737,7 +735,7 @@ export const decodeMergedCliConfig = Effect.fn("CliConfig.decodeMerged")(functio if (cliCompat) { for (const ext of deprecatedProviders) { yield* onWarning( - `WARN: disabling deprecated "${ext}" provider. It will be removed in v3, use [auth.external.${ext}_oidc] instead.`, + `WARN: disabling deprecated "${ext}" provider. Please use [auth.external.${ext}_oidc] instead`, ); } } diff --git a/packages/config/src/io.unit.test.ts b/packages/config/src/io.unit.test.ts index a20b68dfcc..981cf95cb1 100644 --- a/packages/config/src/io.unit.test.ts +++ b/packages/config/src/io.unit.test.ts @@ -2327,7 +2327,7 @@ port = 12345 expect( warnings.some((m) => m.includes( - "WARN: config section [inbucket] is deprecated and will be removed in v3, use [local_smtp] instead.", + "WARN: config section [inbucket] is deprecated. Please use [local_smtp] instead.", ), ), ).toBe(true); @@ -2389,7 +2389,7 @@ port = 33333 expect( warnings.some((m) => m.includes( - "WARN: config section [remotes.staging.inbucket] is deprecated and will be removed in v3, use [remotes.staging.local_smtp] instead.", + "WARN: config section [remotes.staging.inbucket] is deprecated. Please use [remotes.staging.local_smtp] instead.", ), ), ).toBe(true); @@ -2455,7 +2455,7 @@ orioledb_version = "15.1.0.150" expect( warnings.some((m) => m.includes( - "WARN: experimental.orioledb_version is deprecated and will be removed in v3, use db.orioledb_version instead.", + "WARN: experimental.orioledb_version is deprecated. Please use db.orioledb_version instead.", ), ), ).toBe(true); @@ -2493,7 +2493,7 @@ orioledb_version = "15.1.0.150" expect( warnings.some((m) => m.includes( - "WARN: experimental.orioledb_version is deprecated and will be removed in v3, use db.orioledb_version instead.", + "WARN: experimental.orioledb_version is deprecated. Please use db.orioledb_version instead.", ), ), ).toBe(true); @@ -2516,7 +2516,7 @@ orioledb_version = "15.1.0.150" expect( warnings.some((m) => m.includes( - "WARN: experimental.orioledb_version is deprecated and will be removed in v3, use db.orioledb_version instead.", + "WARN: experimental.orioledb_version is deprecated. Please use db.orioledb_version instead.", ), ), ).toBe(true); @@ -2601,7 +2601,7 @@ orioledb_version = "15.1.0.150" expect( warnings.some((m) => m.includes( - "WARN: remotes.staging.experimental.orioledb_version is deprecated and will be removed in v3, use remotes.staging.db.orioledb_version instead.", + "WARN: remotes.staging.experimental.orioledb_version is deprecated. Please use remotes.staging.db.orioledb_version instead.", ), ), ).toBe(true); @@ -2652,7 +2652,7 @@ enabled = true expect( warnings.some((m) => m.includes( - 'WARN: disabling deprecated "slack" provider. It will be removed in v3, use [auth.external.slack_oidc] instead.', + 'WARN: disabling deprecated "slack" provider. Please use [auth.external.slack_oidc] instead', ), ), ).toBe(true); @@ -2673,7 +2673,7 @@ enabled = true expect( warnings.some((m) => m.includes( - 'WARN: disabling deprecated "linkedin" provider. It will be removed in v3, use [auth.external.linkedin_oidc] instead.', + 'WARN: disabling deprecated "linkedin" provider. Please use [auth.external.linkedin_oidc] instead', ), ), ).toBe(true); From b068add72868d05e8300392d7e352bc2d7fa0833 Mon Sep 17 00:00:00 2001 From: avallete Date: Wed, 14 Oct 2026 18:04:18 +0200 Subject: [PATCH 11/11] docs(repo): drop stale ADR copies left by the next merge Remove the pre-renumber Go CLI removal and config precedence ADRs and point config precedence references at ADR 0032. Co-Authored-By: Claude Opus 5.5 --- apps/cli/src/commands/db/diff/SIDE_EFFECTS.md | 2 +- apps/cli/src/commands/db/dump/SIDE_EFFECTS.md | 2 +- apps/cli/src/commands/db/pull/SIDE_EFFECTS.md | 2 +- apps/cli/src/commands/db/push/SIDE_EFFECTS.md | 2 +- .../cli/src/commands/db/reset/SIDE_EFFECTS.md | 2 +- .../commands/functions/deploy/SIDE_EFFECTS.md | 2 +- .../src/commands/gen/types/SIDE_EFFECTS.md | 2 +- .../cli/src/commands/services/SIDE_EFFECTS.md | 2 +- docs/adr/0030-go-cli-removal.md | 132 ------------- docs/adr/0031-config-value-precedence.md | 184 ------------------ docs/adr/0032-config-value-precedence.md | 17 +- 11 files changed, 15 insertions(+), 334 deletions(-) delete mode 100644 docs/adr/0030-go-cli-removal.md delete mode 100644 docs/adr/0031-config-value-precedence.md diff --git a/apps/cli/src/commands/db/diff/SIDE_EFFECTS.md b/apps/cli/src/commands/db/diff/SIDE_EFFECTS.md index c01caa7cac..aaed23e195 100644 --- a/apps/cli/src/commands/db/diff/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/db/diff/SIDE_EFFECTS.md @@ -209,7 +209,7 @@ transaction metadata. ## Notes / Delegation -- **Config value precedence** (ADR 0031): explicit flag > shell env > project `.env*` > config +- **Config value precedence** (ADR 0032): explicit flag > shell env > project `.env*` > config (`config.json` over `config.toml`) > default. The whole config is decoded up front, so an invalid value fails the command. A `--linked` target also applies a matching `[remotes.*]` block. The linked-database password env is withheld when the target differs from diff --git a/apps/cli/src/commands/db/dump/SIDE_EFFECTS.md b/apps/cli/src/commands/db/dump/SIDE_EFFECTS.md index 85b29e5b94..cb353316bd 100644 --- a/apps/cli/src/commands/db/dump/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/db/dump/SIDE_EFFECTS.md @@ -83,7 +83,7 @@ shell inherits the suppressing variables and is missed. ## Notes -- **Config value precedence** (ADR 0031): explicit flag > shell env > project `.env*` > config +- **Config value precedence** (ADR 0032): explicit flag > shell env > project `.env*` > config (`config.json` over `config.toml`; a matched `[remotes.*]` block over the base document on `--linked`) > default. The linked-database password env is withheld when the target differs from `.temp/project-ref`: stderr gets `Not sending SUPABASE_DB_PASSWORD to : this directory is linked to . Using a temporary login role instead (needs supabase login or SUPABASE_ACCESS_TOKEN). Pass --password to use a password for .` and a temporary login role is minted. `--password` is rejected with diff --git a/apps/cli/src/commands/db/pull/SIDE_EFFECTS.md b/apps/cli/src/commands/db/pull/SIDE_EFFECTS.md index 6024c02c57..c75c9249e7 100644 --- a/apps/cli/src/commands/db/pull/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/db/pull/SIDE_EFFECTS.md @@ -176,7 +176,7 @@ Progress strings still go to stderr; stdout carries a single structured envelope ## Notes -- **Config value precedence** (ADR 0031): explicit flag > shell env > project `.env*` > config +- **Config value precedence** (ADR 0032): explicit flag > shell env > project `.env*` > config (`config.json` over `config.toml`) > default. The whole config is decoded up front, so an invalid value fails the command. A `--linked` target also applies a matching `[remotes.*]` block. The linked-database password env is withheld when the target differs from diff --git a/apps/cli/src/commands/db/push/SIDE_EFFECTS.md b/apps/cli/src/commands/db/push/SIDE_EFFECTS.md index 720dafdcad..2146375456 100644 --- a/apps/cli/src/commands/db/push/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/db/push/SIDE_EFFECTS.md @@ -107,7 +107,7 @@ stdout is payload-only. A single `result` object is emitted: target is a hard error rather than a silently discarded flag (deliberately stricter than `SUPABASE_PROJECT_ID`, which simply goes unused on a non-linked target). -- **Config value precedence** (ADR 0031): explicit flag > shell env > project +- **Config value precedence** (ADR 0032): explicit flag > shell env > project `.env*` > config (`config.json` over `config.toml`; a matched `[remotes.*]` block over the base document) > default. The whole config is decoded up front, so an invalid value fails the command before any connection. diff --git a/apps/cli/src/commands/db/reset/SIDE_EFFECTS.md b/apps/cli/src/commands/db/reset/SIDE_EFFECTS.md index 9a11723cdc..7d45764baf 100644 --- a/apps/cli/src/commands/db/reset/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/db/reset/SIDE_EFFECTS.md @@ -273,7 +273,7 @@ to those defaults (the usual outcome for an interactive terminal). standalone outside the per-file transaction batch, with the same non-atomic flush behaviour as `db push` — see `db push`'s SIDE_EFFECTS Notes (supabase/cli#5139, adopted into TS in PR supabase/cli#5671). -- **Config value precedence** (ADR 0031): explicit flag > shell env > project +- **Config value precedence** (ADR 0032): explicit flag > shell env > project `.env*` > config (`config.json` over `config.toml`; a matched `[remotes.*]` block over the base document) > default. The linked-database password env is withheld when the target differs from `.temp/project-ref`: stderr gets diff --git a/apps/cli/src/commands/functions/deploy/SIDE_EFFECTS.md b/apps/cli/src/commands/functions/deploy/SIDE_EFFECTS.md index a28e9fe980..44549ccf7b 100644 --- a/apps/cli/src/commands/functions/deploy/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/functions/deploy/SIDE_EFFECTS.md @@ -90,7 +90,7 @@ The `--output`/`-o` flag does not change deploy output. ## Notes -- **Config value precedence** (ADR 0031): explicit flag > shell env > project `.env*` > config +- **Config value precedence** (ADR 0032): explicit flag > shell env > project `.env*` > config (`config.json` over `config.toml`) > default. The whole config is decoded up front, so an invalid value, including an unparsable `SUPABASE_*` override, fails the command. - If no function name is provided, deploys all functions found in `supabase/functions/`. diff --git a/apps/cli/src/commands/gen/types/SIDE_EFFECTS.md b/apps/cli/src/commands/gen/types/SIDE_EFFECTS.md index 1443573e1f..56387fdc9c 100644 --- a/apps/cli/src/commands/gen/types/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/gen/types/SIDE_EFFECTS.md @@ -138,7 +138,7 @@ Not applicable. shell and nested dotenv overrides, matching the CLI. The database password is the `[db].password` value (default `postgres`); `SUPABASE_DB_PASSWORD` has no effect, and the command has no `--password` flag. -- **Config value precedence** (ADR 0031): explicit flag > shell env > project `.env*` > config +- **Config value precedence** (ADR 0032): explicit flag > shell env > project `.env*` > config (`config.json` over `config.toml`; a matched `[remotes.*]` block over the base document on `--linked`/`--project-id`) > default. The whole config is decoded up front where it is loaded, so an invalid value fails the command. diff --git a/apps/cli/src/commands/services/SIDE_EFFECTS.md b/apps/cli/src/commands/services/SIDE_EFFECTS.md index 70ca8c048c..7a4a12a003 100644 --- a/apps/cli/src/commands/services/SIDE_EFFECTS.md +++ b/apps/cli/src/commands/services/SIDE_EFFECTS.md @@ -83,7 +83,7 @@ NDJSON success event with the same `{ services: [...] }` payload. ## Notes -- Config values resolve as flag > shell env > project `.env*` > config (`config.json` over `config.toml`) > default (ADR 0031). An invalid config value or `SUPABASE_*` override fails the command with exit 1, naming its source. +- Config values resolve as flag > shell env > project `.env*` > config (`config.json` over `config.toml`) > default (ADR 0032). An invalid config value or `SUPABASE_*` override fails the command with exit 1, naming its source. - Backend selection follows canonical experimental-feature routing: `SUPABASE_EXPERIMENTAL_STACK=1|0` takes precedence over `experimental.stack`; unset or empty uses project config, and an invalid environment value fails. Output fields and serializers stay the same. - The legacy backend uses its baked-in service matrix and honors its existing config/version overrides. The stack backend lists services from the installed CLI artifact catalog using canonical `ghcr.io/supabase/cli/...` image names and catalog versions, reported as the upstream version without a slim revision suffix (`-rN`). Native and Docker runtimes use the same catalog versions. Since the catalog belongs to the installed CLI, an older launched CLI or a newer/mirrored stack image may differ from this inventory. The command does not inspect running containers, image pulls, service health, or live stack state. - For stack mode, PostgreSQL uses the configured major version or `SUPABASE_DB_MAJOR_VERSION` (15 or 17). An unreadable config file or an unsupported PostgreSQL major warns with the cause and falls back to default catalog versions; an invalid config value fails the command; absent config uses defaults. Legacy image pins, slim-image rewriting, and remote image overrides do not affect stack results. diff --git a/docs/adr/0030-go-cli-removal.md b/docs/adr/0030-go-cli-removal.md deleted file mode 100644 index 45bcf3cbf8..0000000000 --- a/docs/adr/0030-go-cli-removal.md +++ /dev/null @@ -1,132 +0,0 @@ -# 0030. Go CLI Removal - -**Status**: accepted -**Date**: 2026-09-14 - -## Problem Statement - -ADR 0016 described a residual Go delegation surface in `src/legacy/`: a handful of commands still -proxying to the Go binary (`GoProxy`) while the rest of the CLI had already been natively ported to -TypeScript. The remaining deprecated surfaces were removed rather than ported (CLI-2432); each now -exits 1 with replacement guidance: - -- `db remote changes` → `db diff --linked` -- `gen keys` → `projects api-keys` -- `db diff --use-pg-schema` → the default engine or `--use-pg-delta` -- `functions download --legacy-bundle` → `--use-api` -- `db branch create|delete|list|switch` has no local replacement (hosted preview branches via - `supabase branches` are a different feature) - -No command in the TypeScript CLI spawns `supabase-go` anymore. - -That leaves `apps/cli-go/` — roughly 300 tracked files across three Go modules (the root CLI -module, the `fsevents` helper module, and the separately-tagged `pkg` module) — serving nothing. -It still carries its own Go toolchain pin, CI (test/coverage/lint/CodeQL), Dependabot ecosystem, -release packaging (a second binary per platform, signed and archived alongside the TS binary), and -documentation, all to build and ship a binary nothing calls. - -## Decision - -Delete `apps/cli-go/` entirely. The CLI ships as a single compiled binary per platform -(`packages/cli-/bin/supabase`), built by `apps/cli/scripts/build.ts` with no Go -compilation step. - -The one file the Go tree owned that TypeScript still needed — the service-image Dockerfile -manifest parsed by `dockerfileServiceImages` — was already relocated into the TS tree -(`apps/cli/src/shared/services/Dockerfile`) by a prep commit on this branch before the tree was -deleted, so no runtime behavior depends on `apps/cli-go/` by the time it's removed. - -The tree's own `pkg` module (`github.com/supabase/cli/pkg`, a separately-tagged, externally -published Go module) already had a structurally broken publish path before this ADR, not merely -one this ADR chooses not to continue. At `pkg/v1.2.3` (2026-04-16, `ae7642d508`) the module lived -at repo-root `pkg/`, matching Go's subdirectory-module convention that a `pkg/vX.Y.Z` tag resolves -by finding `pkg/go.mod` at the repository root. `cad095718` ("chore(monorepo): move CLI sources -under apps/cli-go/", 2026-05-06) relocated it to `apps/cli-go/pkg/` while keeping the same module -path, so from that point any new `pkg/vX` tag would have been unresolvable — the module path no -longer matched where `go.mod` actually lived. `pkg/v1.2.3` is therefore the last resolvable -version, and was already roughly five months stale relative to `apps/cli-go/pkg`'s actual content -before this deletion. Deleting the tree makes that unresolvability permanent rather than causing -it. The durability guarantee for existing importers rests on two things: `refs/tags/pkg/v*` is -protected against deletion, creation, and force-push by a repository ruleset ("Protect pkg/v* -release tags", id 23327496), and on top of that the Go module proxy (`proxy.golang.org`, the -default `GOPROXY`) caches published module zips independently of this repository's current tree, -so an importer already pinned to a version keeps resolving it through the proxy even without -direct access to the tag. - -## Consequences - -### Positive - -- The repo ships zero Go: no toolchain pin, no CI surface, no packaging step, no documentation - drift between two implementations of the same CLI. -- Release archives, install scripts, and the Homebrew formula carry one binary instead of two, - removing an entire class of "sidecar present but stale/missing" failure mode. -- `cli-go-ci.yml`'s test/coverage/lint/codegen jobs, the per-PR Go module dependency cache in the - shared setup action, and Dependabot's `gomod` ecosystem entry are all gone — but these jobs were - path-filtered to `apps/cli-go/**` and so ran only on PRs that touched the Go tree already; most - ordinary PRs never paid this cost. CodeQL's shape changes rather than shrinking uniformly: its - `go` language matrix entry is gone entirely, including from the `merge_group` trigger it used to - run on, while the remaining `javascript-typescript` entry now runs on every `pull_request` push - — coverage it effectively never had before, since `merge_group` was CodeQL's only unfiltered - trigger — bounded by a new concurrency group so pushes to the same PR no longer queue up - uncancelled analysis runs. - -### Negative - -- No further `pkg` module releases. External importers pinned to existing tags are unaffected, but - anyone needing content newer than `pkg/v1.2.3` has no upgrade path within this module. Two - external importers are known from a code-search during planning: `supabase/terraform-provider-supabase` - (same org, pins `pkg v1.2.3`) and `turbot/steampipe-plugin-supabase` (third-party); both import - only `pkg/api`. Because `pkg/api` is pure `oapi-codegen` output, the concrete unblock for both is - to run `oapi-codegen` in the consuming repo against the Management API OpenAPI document — - the same one `packages/api` already consumes — and drop the dependency on - `github.com/supabase/cli/pkg` entirely. One caveat: the deleted `apps/cli-go/api/README.md` - documented the Go snapshot as generated from **staging** (`https://api.supabase.green/api/v1-yaml`), - while `packages/api` generates from **production** (`https://api.supabase.com`, see - `packages/api/scripts/download-openapi.ts`), so a consumer following this unblock gets a - different — production, not staging — surface than `pkg/api` had, which is worth knowing now - that the source doc explaining that distinction is gone. Beyond these two, this repository has - no visibility into other importers beyond the module proxy's own download counts, which aren't - queried here. -- Go-specific CI ends entirely: Coveralls coverage upload (Go-side) had no TypeScript-side - equivalent and simply stops with this PR — there is no CI-enforced coverage threshold for the - TypeScript CLI today. This is a known gap, not something this PR fixes; it is a candidate for a - follow-up if coverage enforcement is wanted. -- golangci-lint and the `go` CodeQL analysis both end, along with any lint/security coverage they - provided that had no TypeScript-side equivalent (e.g. Go-specific vulnerability classes CodeQL's - `javascript-typescript` matrix entry doesn't check for). -- The four `*.go-payload.ts` output-encoding specs (which drive `-o yaml`/`-o toml` byte-compatible - output) become frozen, hand-maintained contracts with no live Go source left to diff against for - drift. A prior commit on this branch already retargeted their drift test at the OpenAPI schema - instead of Go source, so this is a note about the ongoing nature of that contract, not new work. - `7b469f5b3` (2026-08-12, the CLI-1970 pin) is the last commit with the full pre-shrink Go source - tree intact and remains the closest available provenance reference for that contract's origin, - but it does not reflect `apps/cli-go/pkg/api`'s final state: that package kept regenerating via - the API-sync workflow for roughly another month after that pin, until this PR's tree deletion. - -## Alternatives Considered - -1. **Keep `pkg/` alone as a standalone Go module in this repo.** Rejected — this reintroduces the - entire Go toolchain, CI, and Dependabot surface this PR removes, to serve at most a couple of - external consumers of a single module. -2. **Extract `pkg/` to its own separate repository before deleting it here.** Not chosen for this - PR. Because the publish path was already structurally broken since the May 2026 monorepo move - (see Decision), this would be a restart from the last resolvable state (`pkg/v1.2.3`), not a - continuation of an active release cadence. The module shipped 14 packages (`api`, `cast`, - `config`, `diff`, `fetcher`, `function`, `migration`, `parser`, `pgtest`, `pgxv5`, `queue`, - `storage`, `vault`, plus the root module); "internal" audience is only evidenced for the two - known importers named above, both of which use only `pkg/api` — not for the module as a whole. - -## Related Decisions - -- Supersedes [ADR 0016](0016-legacy-port-completion-and-go-cli-authority-scope.md), whose - residual authority scope no longer applies now that `apps/cli-go/` is gone. -- [ADR 0011](0011-cli-release-and-distribution-strategy.md) and - [ADR 0014](0014-macos-code-signing-and-notarization.md) remain historically accurate records of - decisions made while the Go CLI still existed; they are not updated by this ADR. -- Linear: CLI-2432. - -## See Also - -- [`apps/cli/docs/binary-distribution.md`](../../apps/cli/docs/binary-distribution.md) -- [`apps/cli/docs/release-process.md`](../../apps/cli/docs/release-process.md) diff --git a/docs/adr/0031-config-value-precedence.md b/docs/adr/0031-config-value-precedence.md deleted file mode 100644 index 95c2cd3068..0000000000 --- a/docs/adr/0031-config-value-precedence.md +++ /dev/null @@ -1,184 +0,0 @@ -# 0031. Config value precedence - -**Status**: proposed -**Date**: 2026-10-08 - -## Problem Statement - -Each command family read config values through its own overlay: the db reader, the start and stack -readers, `config push` and the telemetry gate each decided separately how flags, environment -variables, `.env` files, `[remotes.*]` blocks and `config.toml` combine. They disagreed. A matched -`[remotes.*]` block could beat an explicit `SUPABASE_*` variable, a variable could be honoured by -`db push` and ignored by `start`, and a project `.env` value reached some commands only. - -## Decision - -- Every config value resolves in one order: explicit flag, shell environment, project `.env*` - files, config (`config.json` preferred over `config.toml`, a matched `[remotes.*]` block over the - base document), default. One pure function, `pickCliConfigKey`, implements it, and the tier order - is a constant. -- Commands read values through the `CliConfigValues` service. `load({ workdir, projectRef })` - returns a resolved config, memoised per workdir, project ref and flag set within a runtime. The resolved config - decodes the whole config eagerly, so an invalid value fails every command that loads config - unless the caller passes `tolerateInvalid`. Load-time warnings (an env value - overriding a remote, the `[inbucket]` deprecation) print once per runtime, however many - resolved configs it loads. Code that writes config or `.temp` goes through `writeThrough`, which drops - the memo. -- The resolved config surface is: - - `get(key)`: the value and the origin tier that supplied it. - - `loaded`: what the project declares, with every flag, env and secret winner written in and no - defaults. `fileDeclared`: what the config file alone declares, with `env()` resolved and no - flag or `SUPABASE_*` overlay. - - `materialized`: `loaded` plus defaults and normalizers, with `originAt(path)`. `origins`: the - origin of every registry key. - - `appliedRemote`, `hasConfigFile`, `declares(path)`, `familyNames(family)`, `invalid`, - `withheldEnv`, `dotenvPrivateKeys`. - - `envValues(names)`: the non-empty value of each named variable, shell before project `.env*`, - for resolving `env(NAME)` references. - - `projectEnvValues`: the raw project `.env*` record, for names outside the registry only (see - the exceptions). -- The key registry is generated from `CliConfigSchema`. Each leaf gets a path, the env name - `SUPABASE_` plus the upper-snake path, and a codec derived from its type. Hand-written - annotations cover what the schema cannot express: codec overrides, secret - and section-gated keys, context defaults, exclusions and key families. Document-only keys such as - `db.password` have no env tier. -- A flag binds to a key with `key.flag(...)`, declared in the annotations. The flag supplies the - highest tier. `withCliConfigFlags` collects a command's bindings, and two flags assigning the - same key fail with an error. -- Env for a section-gated key applies only when the section exists in the merged document, so - `SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN` does not conjure a provider block. -- Remote selection uses the effective `project_id` of each `[remotes.*]` block, which is - `SUPABASE_REMOTES__PROJECT_ID` when set, else the TOML value. -- Credential scoping: an env source for a key marked `linkedTarget` (the linked database password) - is withheld when the target project differs from the one in `.temp/project-ref`. This changes - which sources are available, not the tier order. The CLI prints a notice naming the ignored - variable; without a password it mints a temporary login role. -- Seed consent: when a target matches a `[remotes.*]` block, a matched block that does not declare - `db.seed.enabled` seeds nothing by default. `db push` and `db reset --linked` ask before seeding - into the matched project (`--yes` or `SUPABASE_YES` skips the prompt); a non-interactive run - without `--yes` exits 1. `db push` still seeds only with `--include-seed`. -- Local SMTP is on when `[auth.email.smtp]` sets `enabled`, or when the table is present and - leaves it out. The schema default alone is off, so a partial table such as one holding only - `pass = "env(SMTP_PASS)"` still loads. `resolveSmtpEnabled` implements the rule for every - reader, and `config push` and `config pull` see the same value. -- Exceptions kept on purpose: - - `CommandSettings` reads `SUPABASE_PROJECT_ID` from the shell only, because it selects the - target project before any config is loaded. - - `resolveExperimentalFeature` ignores remotes and `.env`, because it runs before the target - project is known. - - `experimental.pgdelta` is exempt from section gating: `enabled` defaults to true, so the env - rollback (`SUPABASE_EXPERIMENTAL_PGDELTA_ENABLED=false`) applies while the section is absent. - `SUPABASE_EXPERIMENTAL_PG_DELTA` is not read. - - `db.password` has no env tier: the local database password lives in config, and the linked - password is a separate key with its own env name, flag and scoping. - - Credential scoping covers the linked database password only. `SUPABASE_AUTH_SERVICE_ROLE_KEY` - is not scoped to the linked project. - - Flag ownership covers the names in `CLI_CONFIG_FLAGS` only. A command may declare any other - flag, with or without a config key behind it. - - `resolvedConfig.projectEnvValues` serves names outside the registry: Docker and registry resolution, - the services hostname, Bitbucket detection, `SUPABASE_YES`, `SUPABASE_NETWORK_ID`, and the - project env `functions serve` forwards to the edge runtime. A registry name read from it is a - guard failure. - - `secrets set` loads with `tolerateInvalid`, so an invalid value elsewhere in the config does - not block setting secrets; it reads `edge_runtime` through `resolveCliSubtree`. - - `--include-seed` beats `db.seed.enabled = false` in the base config; the flag is the - highest tier for the decision it names. - - `layeredParseEnv` in `db-config.parse.ts` looks up libpq `PG*` names, which are not registry - keys, so it stays outside the registry. - - The telemetry event catalog lists `SUPABASE_PROJECT_ID` as an environment signal; it records - which variables are set and never resolves a value. -- The pipeline stages live in `@supabase/config/internal` and are not covered by semver. The CLI - composes them: parse, merge a selected remote, overlay values, decode. - -## Rationale - -One implementation of the order removes the class of bug where two readers disagree. Generating -the registry from the schema means a new schema field gets an env override without a second edit. -Eager whole-config decode costs a failure on unrelated invalid values but makes every reader see -the same config. Credential scoping is source availability rather than a different order, so the -order stays the same for every key. - -## Consequences - -### Positive - -- A flag, then env, then `.env`, then config, behaves identically across commands. -- A matched remote no longer shadows an explicit environment variable. -- Four more leaves are overridable from the environment: `auth.sms.otp_expiry`, - `auth.sms.otp_length`, `db.network_restrictions.allowed_cidrs` and - `db.network_restrictions.allowed_cidrs_v6`. - -### Negative - -- Breaking changes, listed in the pull request: an invalid `SUPABASE_EXPERIMENTAL_STACK` - value now fails, `--password` is rejected for commands that - default to the local database, `db reset --linked` can ask a second prompt, an invalid config - value fails every command that loads config, `services` and `functions` read `config.json` - first, and `config push` pushes env-overridden values. -- The remote and credential rules are subtle enough that a reader must consult the resolved config's - `origin` to know why a value won. - -### Guardrails - -- `code-structure.unit.test.ts` fails when: - - a registry env name appears as a string literal outside the foundation files - (`config/cli-config-*.ts`, `shared/config/cli-config-*.ts`), whatever the read pattern; only - the telemetry event catalog is exempt; - - the old overlay identifiers return; - - a foundation file imports from `commands/` or `command-internal/`; - - `CliConfigFlagInputs` is constructed outside `cli-config-flags.ts`; - - a registry-backed flag is declared with a raw `Flag.*` instead of `key.flag`; - - `process.env`, `Bun.env` or an aliased form is read outside the foundation, the entrypoint - and `shared/compute/stacks/**` templates; - - a file outside the foundation imports `loadCliConfig`, `resolveCliConfigSubtree` or - `loadCliProjectEnvironment` from `@supabase/config`, tests included for the last. -- `oxlint` bans `process.env`, `Bun.env`, `globalThis.process`, `env` imported from `node:process`, - `process` or `bun`, and namespace imports of `node:process` across `apps/cli/src`, except the - config provider, the env loader, the entrypoint, `shared/compute/stacks/**` and tests. Importing - `ambientEnvironment` is limited to nine audited files that read terminal, libpq, Docker and proxy - variables, never a registry name: `cli/complete.ts`, `command-internal/colors.ts`, - `command-internal/db-config.parse.ts`, `command-internal/hostname.ts`, - `command-internal/pgpass.ts`, `commands/login/login-claude-hint.ts`, - `commands/start/lib/env-or-default.ts`, `commands/start/services/vector.service.ts` and - `shared/functions/deploy.ts`. A default-import alias of `node:process` is caught by the - code-structure guard instead. -- `CliConfigFlagInputs` is not an allowed runtime service, so a command that reads config values - without `withCliConfigFlags` fails `tsc`. -- Registry unit tests check env-name uniqueness, section gating, and that every - schema leaf is in the registry or explicitly excluded. -- `cli-config-contract.unit.test.ts` walks every registry key and family field and checks that - each resolves from the highest tier that can supply it, with that tier's origin, that a lower - tier wins only when every higher one is unavailable, and that an empty shell variable falls - through. It also pins secret keys and the declared flags to real keys. -- `cli-config-flag-ownership.unit.test.ts` walks the command tree, hidden commands included, and - fails when a flag the registry owns is missing from a command, bound to another key, or bound by - a command that does not declare it. It checks the names in `CLI_CONFIG_FLAGS` only. - -### Adding a key or a flag - -- A new `CliConfigSchema` leaf joins the registry automatically. Add an annotation only for a - non-default codec, a secret, a section gate or an exclusion; a leaf with no codec must - be listed in `CLI_CONFIG_SCHEMA_EXCLUDED` or registry construction throws. -- To bind a flag, add it to `CLI_CONFIG_FLAGS`, declare it with `key.flag` in the command, and - pipe the command config through `withCliConfigFlags`. -- Read the value with `resolvedConfig.get(CliConfigKeys.)`. Never read the env name directly. -- A new exception to any rule above is a decision: record it in this ADR and in the guard's - exemption list together. - -## Alternatives Considered - -1. **Fix each reader to match the others**: keeps the duplicated overlays, so they drift again. -2. **Let remotes override env**: matches the old db behaviour but makes an explicit variable - unable to win over a file. -3. **Reorder tiers for credentials**: hides the order behind per-key exceptions; scoping the - sources keeps one order. - -## Related Decisions - -- [ADR 0020](0020-config-naming-vocabulary.md): the config vocabulary this decision uses. -- [ADR 0021](0021-projectconfig-convergence-semantics.md): the hosted subset that shares the schema. - -## See Also - -- [CLI config loading](../../packages/config/docs/cli-config-loading.md) -- [`apps/cli` agent rules](../../apps/cli/AGENTS.md) diff --git a/docs/adr/0032-config-value-precedence.md b/docs/adr/0032-config-value-precedence.md index cc14a5a856..1914fac638 100644 --- a/docs/adr/0032-config-value-precedence.md +++ b/docs/adr/0032-config-value-precedence.md @@ -21,9 +21,9 @@ variables, `.env` files, `[remotes.*]` blocks and `config.toml` combine. They di returns a resolved config, memoised per workdir, project ref and flag set within a runtime. The resolved config decodes the whole config eagerly, so an invalid value fails every command that loads config unless the caller passes `tolerateInvalid`. Load-time warnings (an env value - overriding a remote, the `[inbucket]` deprecation, a set deprecated env alias) print once per - runtime, however many resolved configs it loads. Code that writes config or `.temp` goes through - `writeThrough`, which drops the memo. + overriding a remote, the `[inbucket]` deprecation) print once per runtime, however many + resolved configs it loads. Code that writes config or `.temp` goes through `writeThrough`, which drops + the memo. - The resolved config surface is: - `get(key)`: the value and the origin tier that supplied it. - `loaded`: what the project declares, with every flag, env and secret winner written in and no @@ -40,11 +40,8 @@ variables, `.env` files, `[remotes.*]` blocks and `config.toml` combine. They di - The key registry is generated from `CliConfigSchema`. Each leaf gets a path, the env name `SUPABASE_` plus the upper-snake path, and a codec derived from its type. Hand-written annotations cover what the schema cannot express: codec overrides, secret - and section-gated keys, context defaults, exclusions, key families and deprecated env aliases. - Document-only keys such as `db.password` have no env tier. -- A deprecated env alias (`CLI_CONFIG_ENV_ALIASES`, e.g. `SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION` - for `db.orioledb_version`) follows the canonical name within each env tier, so the canonical - name wins when both are set. The loader warns once when an alias is set. + and section-gated keys, context defaults, exclusions and key families. Document-only keys such as + `db.password` have no env tier. - A flag binds to a key with `key.flag(...)`, declared in the annotations. The flag supplies the highest tier. `withCliConfigFlags` collects a command's bindings, and two flags assigning the same key fail with an error. @@ -160,8 +157,8 @@ order stays the same for every key. ### Adding a key or a flag - A new `CliConfigSchema` leaf joins the registry automatically. Add an annotation only for a - non-default codec, a secret, a section gate, an exclusion or a deprecated env alias; a leaf with - no codec must be listed in `CLI_CONFIG_SCHEMA_EXCLUDED` or registry construction throws. + non-default codec, a secret, a section gate or an exclusion; a leaf with no codec must + be listed in `CLI_CONFIG_SCHEMA_EXCLUDED` or registry construction throws. - To bind a flag, add it to `CLI_CONFIG_FLAGS`, declare it with `key.flag` in the command, and pipe the command config through `withCliConfigFlags`. - Read the value with `resolvedConfig.get(CliConfigKeys.)`. Never read the env name directly.