Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2488,7 +2488,7 @@ describe("readDbConfig auth.Enabled validation", () => {
it.effect("rejects the removed linkedin provider, naming linkedin_oidc", () =>
failsWith(
["[auth.external.linkedin]", "enabled = true"],
"[auth.external.linkedin] in supabase/config.toml is no longer supported. Use [auth.external.linkedin_oidc] instead.",
"auth.external.linkedin in supabase/config.toml is no longer supported. Use auth.external.linkedin_oidc instead.",
),
);

Expand Down
6 changes: 5 additions & 1 deletion apps/cli/src/command-internal/resolved-config-context.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { CliConfig } from "@supabase/config";
import { CliConfigRemovedKeyError, type CliConfig } from "@supabase/config";
import { Effect, Option, Predicate } from "effect";

import { CliConfigKeys } from "../config/cli-config-keys.ts";
Expand Down Expand Up @@ -68,6 +68,10 @@ export const resolvePasskeyWebauthn = Effect.fn("ResolvedConfigContext.passkeyWe
return { passkeyEnabled, webauthn };
});

/** The remediation a config load failure carries, when it has one. */
export const configLoadFailureSuggestion = (cause: unknown): string | undefined =>
cause instanceof CliConfigRemovedKeyError ? cause.suggestion : undefined;

/** The failure text commands report when the resolved config cannot be loaded. */
export const describeConfigLoadFailure = (cause: unknown): string =>
Predicate.isTagged(cause, "CliConfigParseError") && "cause" in cause
Expand Down
12 changes: 11 additions & 1 deletion apps/cli/src/command-internal/stack-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { CLI_VERSION } from "../shared/cli/version.ts";
import { decryptSecretMap } from "../shared/config/vault-decrypt.ts";
import { toEdgeRuntimeSecretsEnv } from "../shared/functions/functions-config.ts";
import {
configLoadFailureSuggestion,
describeConfigLoadFailure,
loadResolvedConfigContext,
resolvePasskeyWebauthn,
Expand Down Expand Up @@ -75,12 +76,17 @@ import {
/** A config error suitable for a stack command's user-facing boundary. */
export class StackConfigError extends Data.TaggedError("StackConfigError")<{
readonly message: string;
readonly suggestion?: string;
}> {
get [ErrorActionabilityId](): CliErrorActionabilityDeclaration {
return actionability.invalidConfig;
}
}

/** The remediation a stack config failure carries, for the command error that reports it. */
export const stackConfigSuggestion = (error: unknown): string | undefined =>
error instanceof StackConfigError ? error.suggestion : undefined;

interface StackStartConfig {
/** The artifact pins every creation of this command carries. */
readonly selection: ArtifactSelection;
Expand Down Expand Up @@ -290,7 +296,11 @@ export const loadStackConfig = Effect.fn("StackConfig.load")(
Option.fromNullishOr(opts?.projectRef),
).pipe(
Effect.mapError(
(cause) => new StackConfigError({ message: describeConfigLoadFailure(cause) }),
(cause) =>
new StackConfigError({
message: describeConfigLoadFailure(cause),
suggestion: configLoadFailureSuggestion(cause),
}),
),
);
const { originAt } = resolvedConfig.materialized;
Expand Down
38 changes: 19 additions & 19 deletions apps/cli/src/commands/config/push/SIDE_EFFECTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,25 +136,25 @@ cannot be resolved from either the read or the file — reported in their own

## Exit Codes

| Code | Condition |
| ---- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `0` | success, **including** a push with nothing to write (the project already matches, so nothing is asked) |
| `1` | resolved `--workdir`/`SUPABASE_WORKDIR` doesn't exist or isn't a directory (`ConfigPushWorkdirError`) — beats target resolution, the config load, and every network call |
| `1` | an explicit `--workdir`/`SUPABASE_WORKDIR` holds no project config (`ConfigPushLoadConfigError`) — checked again, right before target resolution, so a typo'd workdir fails without a branch-lookup round trip; a DEFAULTED workdir with no linked project instead fails with the not-linked error below |
| `1` | `HostedMutationDeclinedError` — the consent question was declined (rendered as `context canceled`); nothing was written, though the diff and cost lines were already printed |
| `1` | `HostedMutationConsentRequiredError` — there is something to write, no `--yes`/shell `SUPABASE_YES`, and the run cannot prompt (stdin not a TTY, redirected stdout, or machine output); nothing was written |
| `1` | `--remote` names a branch that doesn't exist, isn't provisioned yet, or fails to resolve (network/status failure); or names a branch by name while no project is linked, or the linked parent ref is invalid (CLI-2289) |
| `1` | malformed or missing `config.toml`/`config.json` |
| `1` | `config.toml`/`config.json` uses a removed key (`[inbucket]`, `experimental.orioledb_version` unless empty, `[auth.external.linkedin]`, `[auth.external.slack]`, at the top level or in a `[remotes.*]` block), or `SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION` is set to a non-empty value — the message names every removed key with its replacement; nothing is pushed |
| `1` | a flag, `SUPABASE_*` variable, project `.env*` file or config value fails to decode (`CliConfigValueError`) — names the source that supplied it; surfaced as-is, not wrapped in `ConfigPushLoadConfigError` |
| `1` | an `encrypted:` (dotenvx) secret anywhere in the document cannot be decrypted (`CliConfigValueError`, see below) |
| `1` | invalid `auth.email.*.content_path` (missing/unreadable template file, or a path that resolves outside the project root) |
| `1` | two `[remotes.*]` blocks declare the same `project_id` as the target ref |
| `1` | list-addons failure (network or non-200) |
| `1` | effective-project-config read failure (network, decode, or unexpected status) — one failure mode now covers what used to be six independent per-service read failures |
| `1` | the API's effective project configuration fails to parse |
| `1` | the effective-project-config response carried no block at all (`ConfigPushConfigEmptyError`) — nothing was pushed |
| `1` | any per-service update failure or webhook-enable failure (network or unexpected status) |
| Code | Condition |
| ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `0` | success, **including** a push with nothing to write (the project already matches, so nothing is asked) |
| `1` | resolved `--workdir`/`SUPABASE_WORKDIR` doesn't exist or isn't a directory (`ConfigPushWorkdirError`) — beats target resolution, the config load, and every network call |
| `1` | an explicit `--workdir`/`SUPABASE_WORKDIR` holds no project config (`ConfigPushLoadConfigError`) — checked again, right before target resolution, so a typo'd workdir fails without a branch-lookup round trip; a DEFAULTED workdir with no linked project instead fails with the not-linked error below |
| `1` | `HostedMutationDeclinedError` — the consent question was declined (rendered as `context canceled`); nothing was written, though the diff and cost lines were already printed |
| `1` | `HostedMutationConsentRequiredError` — there is something to write, no `--yes`/shell `SUPABASE_YES`, and the run cannot prompt (stdin not a TTY, redirected stdout, or machine output); nothing was written |
| `1` | `--remote` names a branch that doesn't exist, isn't provisioned yet, or fails to resolve (network/status failure); or names a branch by name while no project is linked, or the linked parent ref is invalid (CLI-2289) |
| `1` | malformed or missing `config.toml`/`config.json` |
| `1` | `config.toml`/`config.json` uses a removed key (`inbucket`, `experimental.orioledb_version` unless empty, `auth.external.linkedin`, `auth.external.slack`, at the top level or in a `remotes.*` block), or `SUPABASE_EXPERIMENTAL_ORIOLEDB_VERSION` is set to a non-empty value — the message names every removed key with its replacement; nothing is pushed |
| `1` | a flag, `SUPABASE_*` variable, project `.env*` file or config value fails to decode (`CliConfigValueError`) — names the source that supplied it; surfaced as-is, not wrapped in `ConfigPushLoadConfigError` |
| `1` | an `encrypted:` (dotenvx) secret anywhere in the document cannot be decrypted (`CliConfigValueError`, see below) |
| `1` | invalid `auth.email.*.content_path` (missing/unreadable template file, or a path that resolves outside the project root) |
| `1` | two `[remotes.*]` blocks declare the same `project_id` as the target ref |
| `1` | list-addons failure (network or non-200) |
| `1` | effective-project-config read failure (network, decode, or unexpected status) — one failure mode now covers what used to be six independent per-service read failures |
| `1` | the API's effective project configuration fails to parse |
| `1` | the effective-project-config response carried no block at all (`ConfigPushConfigEmptyError`) — nothing was pushed |
| `1` | any per-service update failure or webhook-enable failure (network or unexpected status) |

## Output

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2150,7 +2150,7 @@ secret = "${DOTENVX_ENCRYPTED_VALUE}"
Effect.catchTag("ConfigPushLoadConfigError", (error) => Effect.succeed(error.message)),
);
expect(message).toBe(
"[auth.external.slack] in supabase/config.toml is no longer supported. Use [auth.external.slack_oidc] instead.",
"auth.external.slack in supabase/config.toml is no longer supported. Use auth.external.slack_oidc instead.",
);
expect(api.requests).toHaveLength(0);
}).pipe(Effect.provide(layer));
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/src/commands/db/start/start.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1950,7 +1950,7 @@ describe("db start", () => {
const message = Cause.pretty(exit.cause);
expect(message).toContain("DbConfigLoadError");
expect(message).toContain(
"[inbucket] in supabase/config.toml is no longer supported. Use [local_smtp] instead.",
"inbucket in supabase/config.toml is no longer supported. Use local_smtp instead.",
);
}
expect(child.spawned.some((s) => s.args[0] === "create")).toBe(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import type { PreparedRelease } from "@supabase/stack/internal/artifacts";
import { Effect, Path, Ref, Schema } from "effect";
import { FetchHttpClient } from "effect/http";
import { OutputFlag } from "../../../../command-internal/global-flags.ts";
import { loadStackConfig } from "../../../../command-internal/stack-config.ts";
import {
loadStackConfig,
stackConfigSuggestion,
} from "../../../../command-internal/stack-config.ts";
import {
LockResolutionError,
lockedResolution,
Expand Down Expand Up @@ -103,6 +106,7 @@ export const stackInstall = Effect.fn("experimental.stack.install")(function* (
: new StackCommandInstallError({
reason: Schema.is(LockResolutionError)(error) ? "lock" : "invalid-config",
message: error.message,
suggestion: stackConfigSuggestion(error),
cause: error,
}),
),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { defaultArtifactSelection } from "@supabase/stack/defaults";
import { Effect, Ref, Schema } from "effect";
import { OutputFlag } from "../../../../command-internal/global-flags.ts";
import { loadStackConfig } from "../../../../command-internal/stack-config.ts";
import {
loadStackConfig,
stackConfigSuggestion,
} from "../../../../command-internal/stack-config.ts";
import {
LockResolutionError,
outdatedServices,
Expand Down Expand Up @@ -79,6 +82,7 @@ export const stackOutdated = Effect.fn("experimental.stack.outdated")(function*
new StackCommandOutdatedError({
reason: Schema.is(LockResolutionError)(error) ? "lock" : "invalid-config",
message: error.message,
suggestion: stackConfigSuggestion(error),
cause: error,
}),
),
Expand Down
Loading
Loading