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
19 changes: 10 additions & 9 deletions apps/cli/src/legacy/commands/gen/signing-key/SIDE_EFFECTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@

## Files Read

| Path | Format | When |
| ----------------------------------------------- | --------------------------- | ------------------------------------------------------------------------------------ |
| `supabase/config.toml` / `supabase/config.json` | TOML / JSON | always when present in the active workdir; used to discover `auth.signing_keys_path` |
| `<resolved signing_keys_path>` | JSON array of JWKs | when `auth.signing_keys_path` is configured; loaded before overwrite or append |
| git ignore rules | git metadata / ignore files | best-effort after a successful write when the resulting file contains exactly 1 key |
| Path | Format | When |
| ----------------------------------------------- | --------------------------- | ------------------------------------------------------------------------------------- |
| `supabase/config.toml` / `supabase/config.json` | TOML / JSON | always when present in the active workdir; used to discover `auth.signing_keys_path` |
| `<resolved signing_keys_path>` | JSON array of JWKs | when `auth.signing_keys_path` is configured; loaded before overwrite or append |
| git ignore rules | git metadata / ignore files | best-effort after a successful write when the resulting file contains exactly 1 key |
| `<workdir>/supabase/.env*`, `<workdir>/.env*` | dotenv | always, to resolve `SUPABASE_YES` (CLI-1878; Go's `flags.LoadConfig`/`loadNestedEnv`) |

## Files Written

Expand All @@ -22,9 +23,9 @@

## Environment Variables

| Variable | Purpose | Required? |
| -------------- | ---------------------------------------------------------------------------------- | --------- |
| `SUPABASE_YES` | Auto-confirms the overwrite prompt, same as `--yes` (Go's `viper.GetBool("YES")`). | No |
| Variable | Purpose | Required? |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------- |
| `SUPABASE_YES` | Auto-confirms the overwrite prompt, same as `--yes` (Go's `viper.GetBool("YES")`). Read from the shell env OR the project `.env`/`.env.local`/`.env.<env>[.local]` files (shell wins; CLI-1878). | No |

## Exit Codes

Expand Down Expand Up @@ -57,7 +58,7 @@ Same as `--output-format json` above.

- `--algorithm` accepts `ES256` (default, recommended) or `RS256`.
- `--append` appends the new key to an existing keys file instead of overwriting.
- The overwrite prompt honors `SUPABASE_YES` and an explicit `--yes=false` override, matching Go's `viper.GetBool("YES")` precedence (flag wins over env; an omitted flag falls back to the env var). On non-TTY stdin, a piped `y`/`n` line is read within a 100ms timeout and honored before falling back to the default (`y`), matching Go's `Console.ReadLine`/`PromptYesNo` — a piped answer other than an exact `y`/`yes`/`n`/`no` (case-insensitive) also falls back to the default.
- The overwrite prompt honors `SUPABASE_YES` (shell env or the project `.env`/`.env.local`/`.env.<env>[.local]` files, shell wins) and an explicit `--yes=false` override, matching Go's `viper.GetBool("YES")` precedence (flag wins over env; an omitted flag falls back to the env var, resolved after `flags.LoadConfig` loads the project env — CLI-1878). On non-TTY stdin, a piped `y`/`n` line is read within a 100ms timeout and honored before falling back to the default (`y`), matching Go's `Console.ReadLine`/`PromptYesNo` — a piped answer other than an exact `y`/`yes`/`n`/`no` (case-insensitive) also falls back to the default.
- `auth.signing_keys_path` is resolved relative to the active `supabase/config.toml` or `supabase/config.json`.
- Generated keys are JWKs, not PEM files.
- No network or Management API calls are involved.
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";

import { LegacyCliConfig } from "../../../config/legacy-cli-config.service.ts";
import { findGitRootPath } from "../../../../shared/git/git-root.ts";
import { legacyLoadProjectEnv } from "../../../shared/legacy-db-config.toml-read.ts";
import { LegacyDebugLogger } from "../../../shared/legacy-debug-logger.service.ts";
import { legacyPromptYesNo } from "../../../shared/legacy-prompt-yes-no.ts";
import { LegacyTelemetryState } from "../../../telemetry/legacy-telemetry-state.service.ts";
import { legacyResolveYes } from "../../../../shared/legacy/global-flags.ts";
import { legacyResolveYesWithProjectEnv } from "../../../../shared/legacy/global-flags.ts";
import { Output } from "../../../../shared/output/output.service.ts";
import { Tty } from "../../../../shared/runtime/tty.service.ts";
import type { LegacyGenSigningKeyFlags } from "./signing-key.command.ts";
Expand Down Expand Up @@ -255,13 +256,21 @@ export const legacyGenSigningKey = Effect.fn("legacy.gen.signing-key")(function*
const telemetryState = yield* LegacyTelemetryState;
const output = yield* Output;
const tty = yield* Tty;
const yes = yield* legacyResolveYes;
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const emphasize = (text: string) => styleIfTty(tty.stdoutIsTty, "bold", text);
const warnText = (text: string) => styleIfTty(tty.stdoutIsTty, "yellow", text);

return yield* Effect.gen(function* () {
// Go's `flags.LoadConfig` (`signingkeys.go:99`) loads the project `.env` files before the
// overwrite prompt reads `viper.GetBool("YES")` (`console.PromptYesNo`, `signingkeys.go:130`),
// so a `SUPABASE_YES` set only in `supabase/.env` must auto-confirm here too. Resolved inside
// this block (not above it) so a malformed/unreadable `.env` still flushes telemetry below,
// matching Go: telemetry attaches in root's `PersistentPreRunE` (`cmd/root.go:131-155`)
// before this command's own `RunE` runs `flags.LoadConfig`, so `service.Capture` still fires
// in Go even when that load fails.
const projectEnv = yield* legacyLoadProjectEnv(fs, path, cliConfig.workdir);
const yes = yield* legacyResolveYesWithProjectEnv(projectEnv);
// Match Go's order: LoadConfig validates the configured signing-keys file before any key is
// generated, so a broken config fails fast without doing throwaway crypto work.
const signingKeysConfig = yield* loadSigningKeysConfig(cliConfig.workdir);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,49 @@ describe("legacy gen signing-key integration", () => {
);
});

it.live(
"auto-confirms from SUPABASE_YES in the project .env, even with a piped 'n' (CLI-1878)",
() => {
// SUPABASE_YES lives only in supabase/.env, not the shell. Go's `flags.LoadConfig`
// (`signingkeys.go:99`) loads the project `.env` files before the overwrite prompt reads
// `viper.GetBool("YES")` (`signingkeys.go:130`), so the overwrite auto-confirms and the
// piped `n` is never consumed — same precedence as the shell-env case above.
//
// Defensively clear a shell SUPABASE_YES: this test must prove the project-.env source
// specifically, not accidentally pass because a prior test in this file left the shell
// env set (the sibling shell-env tests above save/restore theirs).
const prev = process.env["SUPABASE_YES"];
delete process.env["SUPABASE_YES"];
const { layer } = setup({ stdinIsTty: false, pipedAnswer: "n" });
return Effect.gen(function* () {
yield* Effect.tryPromise(() =>
writeConfig('[auth]\nsigning_keys_path = "./signing_keys.json"\n'),
);
yield* Effect.tryPromise(() =>
writeFile(join(tempRoot.current, "supabase", "signing_keys.json"), "[]\n"),
);
yield* Effect.tryPromise(() =>
writeFile(join(tempRoot.current, "supabase", ".env"), "SUPABASE_YES=true\n"),
);

yield* legacyGenSigningKey({ algorithm: "ES256", append: false });

const saved = yield* Effect.tryPromise(() =>
readFile(join(tempRoot.current, "supabase", "signing_keys.json"), "utf8"),
);
const parsed = JSON.parse(saved) as ReadonlyArray<Record<string, unknown>>;
expect(parsed).toHaveLength(1);
}).pipe(
Effect.ensuring(
Effect.sync(() => {
if (prev !== undefined) process.env["SUPABASE_YES"] = prev;
}),
),
Effect.provide(layer),
);
},
);

it.live("an explicit --yes=false overrides SUPABASE_YES and honors a piped 'n'", () => {
const prev = process.env["SUPABASE_YES"];
process.env["SUPABASE_YES"] = "1";
Expand Down Expand Up @@ -662,4 +705,28 @@ describe("legacy gen signing-key integration", () => {
expect(telemetry?.flushed).toBe(true);
}).pipe(Effect.provide(layer));
});

it.live("flushes telemetry state even when the project .env is malformed (Codex review)", () => {
// Go attaches the telemetry service in root's `PersistentPreRunE` (cmd/root.go:131-155),
// before this command's own `RunE` runs `flags.LoadConfig` (signingkeys.go:99), so
// `service.Capture` still fires even when that project-.env load fails. The project-env
// resolution here must live inside the `Effect.ensuring(telemetryState.flush)`-wrapped
// block for the same reason — locks in that fix.
const { layer, telemetry } = setup({ trackTelemetry: true });
return Effect.gen(function* () {
yield* Effect.tryPromise(() =>
mkdir(join(tempRoot.current, "supabase"), { recursive: true }),
);
yield* Effect.tryPromise(() =>
writeFile(join(tempRoot.current, "supabase", ".env"), "!=broken\n"),
);

const exit = yield* Effect.exit(legacyGenSigningKey({ algorithm: "ES256", append: false }));
expect(Exit.isFailure(exit)).toBe(true);
if (Exit.isFailure(exit)) {
expect(JSON.stringify(exit.cause)).toContain("LegacyDbConfigLoadError");
}
expect(telemetry?.flushed).toBe(true);
}).pipe(Effect.provide(layer));
});
});
19 changes: 11 additions & 8 deletions apps/cli/src/legacy/commands/migration/fetch/SIDE_EFFECTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

## Files Read

| Path | Format | When |
| -------------------------- | ---------- | ------------------------------------------------- |
| `~/.supabase/access-token` | plain text | when `SUPABASE_ACCESS_TOKEN` unset and `--linked` |
| Path | Format | When |
| --------------------------------------------- | ---------- | ------------------------------------------------------------------ |
| `~/.supabase/access-token` | plain text | when `SUPABASE_ACCESS_TOKEN` unset and `--linked` |
| `<workdir>/supabase/.env*`, `<workdir>/.env*` | dotenv | always, to resolve `SUPABASE_YES` (CLI-1878; Go's `loadNestedEnv`) |

## Files Written

Expand All @@ -20,9 +21,10 @@

## Environment Variables

| Variable | Purpose | Required? |
| ----------------------- | ------------------------------ | ------------------------------------------------------- |
| `SUPABASE_ACCESS_TOKEN` | auth token for `--linked` mode | no (falls back to keyring → `~/.supabase/access-token`) |
| Variable | Purpose | Required? |
| ----------------------- | --------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `SUPABASE_ACCESS_TOKEN` | auth token for `--linked` mode | no (falls back to keyring → `~/.supabase/access-token`) |
| `SUPABASE_YES` | auto-confirm the overwrite prompt | no — read from the shell env OR the project `.env`/`.env.local`/`.env.<env>[.local]` files (shell wins; CLI-1878) |

## Exit Codes

Expand Down Expand Up @@ -54,8 +56,9 @@ Same structured `files` result delivered as an NDJSON `result` event.

- When the migrations directory is non-empty, prompts
`Do you want to overwrite existing files in supabase/migrations directory?`
(default **YES**). Declining exits non-zero (`context canceled`). `--yes`
auto-confirms; a non-interactive / machine-output run takes the default (YES).
(default **YES**). Declining exits non-zero (`context canceled`). `--yes` or
`SUPABASE_YES` (shell env or project `.env`) auto-confirms; a non-interactive /
machine-output run takes the default (YES).

## Notes

Expand Down
18 changes: 16 additions & 2 deletions apps/cli/src/legacy/commands/migration/fetch/fetch.handler.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { Effect, FileSystem, Option, Path } from "effect";

import { LegacyDnsResolverFlag, legacyResolveYes } from "../../../../shared/legacy/global-flags.ts";
import {
LegacyDnsResolverFlag,
legacyResolveYesWithProjectEnv,
} from "../../../../shared/legacy/global-flags.ts";
import { CliArgs } from "../../../../shared/cli/cli-args.service.ts";
import { Output } from "../../../../shared/output/output.service.ts";
import { LegacyCliConfig } from "../../../config/legacy-cli-config.service.ts";
import { LegacyProjectRefResolver } from "../../../config/legacy-project-ref.service.ts";
import { legacyBold } from "../../../shared/legacy-colors.ts";
import { LegacyDbConfigResolver } from "../../../shared/legacy-db-config.service.ts";
import { LegacyDbConnection } from "../../../shared/legacy-db-connection.service.ts";
import { legacyLoadProjectEnv } from "../../../shared/legacy-db-config.toml-read.ts";
import { resolveLegacyDbTargetFlags } from "../../../shared/legacy-db-target-flags.ts";
import { legacyReadMigrationTable } from "../../../shared/legacy-migration-history.ts";
import { LegacyLinkedProjectCache } from "../../../telemetry/legacy-linked-project-cache.service.ts";
Expand All @@ -31,8 +35,10 @@ const runFetch = Effect.fnUntraced(function* (
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const dnsResolver = yield* LegacyDnsResolverFlag;
const yes = yield* legacyResolveYes; // --yes OR SUPABASE_YES (Go viper AutomaticEnv, root.go:318-334).

// Flag-group mutual-exclusion first: cobra's `MarkFlagsMutuallyExclusive` validates at
// parse time, ahead of the root `PersistentPreRunE` (same ordering as `migration down`/
// `repair`).
if (target.setFlags.length > 1) {
return yield* Effect.fail(
new LegacyMigrationTargetFlagsError({
Expand All @@ -55,6 +61,14 @@ const runFetch = Effect.fnUntraced(function* (
dnsResolver,
});

// Go loads the project .env via loadNestedEnv INSIDE ParseDatabaseConfig (config.go:701),
// i.e. after the parse-time flag-group validation above — so a SUPABASE_YES set only in
// supabase/.env auto-confirms, but a flag conflict still surfaces before any .env read.
// Resolve --yes against the project env here, not just process.env (root.go:318-334).
// Same ordering as `migration down`/`repair`.
const projectEnv = yield* legacyLoadProjectEnv(fs, path, cliConfig.workdir);
const yes = yield* legacyResolveYesWithProjectEnv(projectEnv);

// Linked fetch caches the project ref on success (Go's `PersistentPostRun`). The ref is
// loaded now (pre-run), but the cache write is attached to the body via `Effect.ensuring`,
// so a declined prompt returns before it runs — matching Go (PostRun is skipped on a
Expand Down
Loading
Loading