Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
b8facde
feat(cli): add pending-migration reconciliation for legacy db push
claude Jun 24, 2026
d7bb356
feat(cli): add seed-file ops for legacy db push
claude Jun 24, 2026
d207d56
feat(cli): add vault upsert + migration/globals apply loops for db push
claude Jun 24, 2026
6853ef4
feat(cli): implement native db push handler
claude Jun 24, 2026
3eead57
test(cli): integration tests for native db push
claude Jun 24, 2026
4a9eee4
test(cli): expand db push coverage to 98.7% branch
claude Jun 24, 2026
daf361f
docs(cli): document db push side effects and mark it ported
claude Jun 24, 2026
16214c0
feat(cli): implement native db reset remote path
claude Jun 25, 2026
2c227b1
docs(cli): document db reset side effects and mark remote path ported
claude Jun 25, 2026
e3f9168
docs(cli): add CLI-1325 Stage 3 handoff for db start / db reset --local
claude Jun 25, 2026
9f547c5
feat(cli): implement native db start via hidden Go bootstrap seam
avallete Jun 25, 2026
633a647
feat(cli): implement native db reset --local path
avallete Jun 25, 2026
5968f83
test(cli): add db start / db reset legacy e2e smokes
avallete Jun 25, 2026
0897515
docs(cli): remove completed CLI-1325 Stage 3 handoff
avallete Jun 25, 2026
e592005
test(cli): add gated real-Docker live e2e for db start / db reset --l…
avallete Jun 25, 2026
b8cd172
Merge remote-tracking branch 'origin/develop' into avallete/exciting-…
avallete Jun 26, 2026
0437f44
test(cli-e2e): add live db start / db reset --local real-stack coverage
avallete Jun 26, 2026
73252bd
test(cli-e2e): cover db reset remote leg; drop env-gated local live test
avallete Jun 26, 2026
d5ff357
chore(cli-go): sync generated API types with latest spec
avallete Jun 26, 2026
597371b
fix(cli): address db reset/start review feedback
avallete Jun 26, 2026
a09132a
Merge branch 'develop' into avallete/exciting-noyce-078f0e
avallete Jun 26, 2026
c972723
Merge branch 'develop' into avallete/exciting-noyce-078f0e
avallete Jun 29, 2026
9a1d1c5
chore(cli-go): sync generated API types with latest spec
avallete Jun 29, 2026
5d6f0c7
Merge branch 'develop' into avallete/exciting-noyce-078f0e
avallete Jun 29, 2026
f55f01b
refactor(cli): hoist seed-buckets core to legacy/shared
avallete Jun 29, 2026
7d08832
test(cli): make db reset git-branch test deterministic under CI
avallete Jun 29, 2026
6ac9e50
fix(cli): RESET ALL only on migration-apply path + consume --password…
avallete Jun 30, 2026
26d6bec
test(cli-e2e): canonicalize postgres connection-error stderr in parit…
avallete Jun 30, 2026
0522eed
Merge branch 'develop' into avallete/exciting-noyce-078f0e
avallete Jun 30, 2026
84b8ef6
fix(cli): port SetConnectSuggestion + address db push/reset parity re…
avallete Jun 30, 2026
c89153d
fix(cli): seed lock_timeout, migrations-enabled env override, atomic …
avallete Jun 30, 2026
3ea1c5a
fix(cli): seed-enabled env override, reset --version glob, delegated …
avallete Jun 30, 2026
7701e4b
fix(cli): run finalizers on bootstrap child failure (revert process.e…
avallete Jun 30, 2026
15429c9
Merge remote-tracking branch 'origin/develop' into avallete/exciting-…
avallete Jun 30, 2026
b32d7f2
fix(cli): empty SUPABASE_* override is unset; scope seed-table lock t…
avallete Jun 30, 2026
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
81 changes: 81 additions & 0 deletions apps/cli-e2e/src/tests/live/db-reset-start.live.e2e.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { mkdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect } from "vitest";
import { TARGET } from "../env.ts";
import { testLive } from "./live-context.ts";

// Real-backend live coverage for the native `db start` / `db reset` ports.
//
// `db start` / `db reset` live only in the `go` reference and the `ts-legacy`
// port (the `next` shell has no `db` group), so skip the `ts-next` target.
//
// The live suite runs serially (`fileParallelism: false`, `maxWorkers: 1`), so the
// destructive remote reset below is safe against the throwaway per-run project.

// --- Local leg: db start + db reset --local against the real Docker socket -----
// Exercises the hidden `db __db-bootstrap` Go seam end-to-end — the boundary the
// in-process integration suites mock. The start → already-running → reset cycle
// runs in one test so it shares a single booted stack, and `finally` stops it
// (legacy proxies `stop` to Go) so the run never leaves containers behind.
describe.skipIf(TARGET === "ts-next")("db start / db reset --local (live, local Docker)", () => {
testLive(
"db start boots, is idempotent, and db reset --local recreates",
{ timeout: 600_000 },
async ({ run }) => {
try {
const start = await run(["db", "start"]);
expect(start.exitCode, start.stderr).toBe(0);
// Go tees bootstrap progress to stderr (mode-independent).
expect(`${start.stdout}${start.stderr}`).toMatch(/Starting database|Initialising schema/i);

// Second start is a no-op: the db is already running, exit 0.
const again = await run(["db", "start"]);
expect(again.exitCode, again.stderr).toBe(0);
expect(`${again.stdout}${again.stderr}`).toMatch(/already[\s-]running/i);

// Local reset recreates the container and prints the git-branch line.
const reset = await run(["db", "reset", "--local"]);
expect(reset.exitCode, reset.stderr).toBe(0);
expect(reset.stderr).toContain("on branch ");
} finally {
await run(["stop", "--no-backup"]).catch(() => undefined);
}
},
);
});

// --- Remote leg: db reset against the staging project over the session pooler ---
// Exercises the native remote reset path (drop user schemas → apply local
// migrations → seed) against a real Postgres, no Docker. `--yes` auto-accepts the
// confirmation prompt (the non-interactive default is decline). Mutates the
// throwaway project's schema — deleted on teardown. The IPv4 session pooler
// `dbUrl` is used because the direct host is IPv6-only and unreachable from
// IPv4-only CI runners.
describe.skipIf(TARGET === "ts-next")("db reset (live, remote session pooler)", () => {
testLive(
"resets the remote schema and re-applies a local migration",
{ timeout: 600_000 },
async ({ run, dbUrl, workspace }) => {
const migrations = join(workspace.path, "supabase", "migrations");
mkdirSync(migrations, { recursive: true });
writeFileSync(
join(migrations, "20240101000000_e2e_reset.sql"),
"create table if not exists e2e_reset (id int);\n",
);

const reset = await run(["db", "reset", "--db-url", dbUrl, "--yes"]);
expect(reset.exitCode, reset.stderr).toBe(0);
expect(reset.stderr).toContain("Resetting remote database");
// A real connection failure must never be mistaken for a benign outcome.
expect(`${reset.stdout}${reset.stderr}`, "db reset hit a connection error").not.toMatch(
/dial|no route|connection refused|could not connect|server closed the connection|i\/o timeout/i,
);

// The migration history shows the re-applied version → proves the drop +
// migrate ran against the remote database.
const listed = await run(["migration", "list", "--db-url", dbUrl]);
expect(listed.exitCode, listed.stderr).toBe(0);
expect(listed.stdout).toContain("20240101000000");
},
);
});
74 changes: 74 additions & 0 deletions apps/cli-go/cmd/db.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cmd

import (
"context"
"errors"
"fmt"
"os"
Expand Down Expand Up @@ -270,6 +271,71 @@ var (
},
}

bootstrapMode string
bootstrapSqlPaths []string
bootstrapFromBackup string
bootstrapVersion string
bootstrapNoSeed bool

// dbBootstrapCmd is a hidden seam used by the native-TypeScript `db start` and
// `db reset --local` commands to drive the container-bootstrap primitives that
// are not yet ported to TypeScript: creating/recreating the local Postgres
// container, applying the initial schema, and the storage health gate. The TS
// caller orchestrates everything else (the "already running?" check and its
// message, version/last resolution, bucket seeding, the git-branch "Finished…"
// line, telemetry, and --output-format shaping); the seam stays in Go only for
// the Docker lifecycle. It mirrors the existing db __shadow seam: it carries no
// db-url/local/linked target flags, so it loads supabase/config.toml explicitly
// (the root PersistentPreRunE only loads it when a target flag is set). Progress
// goes to stderr; the only stdout output is a single machine-parseable marker
// for --mode await-storage ("ready" or "absent").
dbBootstrapCmd = &cobra.Command{
Use: "__db-bootstrap",
Hidden: true,
Short: "Internal: container bootstrap for the native db start / db reset commands",
RunE: func(cmd *cobra.Command, args []string) error {
fsys := afero.NewOsFs()
if err := flags.LoadConfig(fsys); err != nil {
return err
}
switch bootstrapMode {
case "start":
// Mirror start.Run minus the "already running?" check, which the TS
// caller performs (and prints "Postgres database is already running.").
if err := start.StartDatabase(cmd.Context(), bootstrapFromBackup, fsys, os.Stderr); err != nil {
if rmErr := utils.DockerRemoveAll(context.Background(), os.Stderr, utils.Config.ProjectId); rmErr != nil {
fmt.Fprintln(os.Stderr, rmErr)
}
return err
}
return nil
case "recreate":
// The PG14/PG15 container-recreate half of local db reset. The TS
// caller has already printed "Resetting local database…" and validated
// the flags. Apply the same seed handling as `db reset` (dbResetCmd):
// `--no-seed` disables the seed, `--sql-paths` overrides the seed paths,
// before MigrateAndSeed runs inside the recreate.
if err := applyDbResetSeedFlags(bootstrapNoSeed, bootstrapSqlPaths); err != nil {
return err
}
return reset.RecreateLocalDatabase(cmd.Context(), bootstrapVersion, fsys)
case "await-storage":
ready, err := reset.AwaitStorageReady(cmd.Context())
if err != nil {
return err
}
if ready {
fmt.Println("ready")
} else {
fmt.Println("absent")
}
return nil
default:
return fmt.Errorf("unknown bootstrap mode: %s", bootstrapMode)
}
},
}

dbRemoteCmd = &cobra.Command{
Hidden: true,
Use: "remote",
Expand Down Expand Up @@ -620,6 +686,14 @@ func init() {
shadowFlags.StringSliceVarP(&shadowSchema, "schema", "s", []string{}, "Comma separated list of schema to include.")
shadowFlags.StringVar(&shadowProjectRef, "project-ref", "", "Linked project ref, so the shadow merges the matching [remotes.<ref>] config override.")
dbCmd.AddCommand(dbShadowCmd)
// Build hidden container-bootstrap seam command (native db start / db reset)
bootstrapFlags := dbBootstrapCmd.Flags()
bootstrapFlags.StringVar(&bootstrapMode, "mode", "start", "Bootstrap mode: start, recreate, or await-storage.")
bootstrapFlags.StringVar(&bootstrapFromBackup, "from-backup", "", "Path to a logical backup file (start mode).")
bootstrapFlags.StringVar(&bootstrapVersion, "version", "", "Reset up to the specified version (recreate mode).")
bootstrapFlags.BoolVar(&bootstrapNoSeed, "no-seed", false, "Skip the seed script after recreate (recreate mode).")
bootstrapFlags.StringArrayVar(&bootstrapSqlPaths, "sql-paths", nil, "Override [db.seed].sql_paths for the recreate (recreate mode).")
dbCmd.AddCommand(dbBootstrapCmd)
// Build remote command
remoteFlags := dbRemoteCmd.PersistentFlags()
remoteFlags.StringSliceVarP(&schema, "schema", "s", []string{}, "Comma separated list of schema to include.")
Expand Down
32 changes: 32 additions & 0 deletions apps/cli-go/internal/db/reset/reset.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,38 @@ func toLogMessage(version string) string {
return "..."
}

// RecreateLocalDatabase is the container-lifecycle half of a local `db reset`,
// exposed for the native-TypeScript `db reset --local` seam (cmd db __db-bootstrap).
// It performs the PG14/PG15 branch — recreate the db container/volume, init schema,
// migrate + seed, and restart the satellite containers — WITHOUT the leading
// "Resetting local database…" line, which the TS caller prints itself. Mirrors
// resetDatabase (above) minus that message.
func RecreateLocalDatabase(ctx context.Context, version string, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error {
if utils.Config.Db.MajorVersion <= 14 {
return resetDatabase14(ctx, version, fsys, options...)
}
return resetDatabase15(ctx, version, fsys, options...)
}

// AwaitStorageReady mirrors the storage-health gate that local `db reset` runs
// before seeding buckets (Run, above): if the storage container exists but is not
// healthy, wait up to 30s for it. It reports whether the storage container exists
// so the native-TypeScript caller knows whether to run the (already-ported) bucket
// seeding. Any inspect error is treated as "storage not running" → false, matching
// Go's `err == nil` gate, which silently skips buckets on any inspect failure.
func AwaitStorageReady(ctx context.Context) (bool, error) {
resp, err := utils.Docker.ContainerInspect(ctx, utils.StorageId)
if err != nil {
return false, nil
}
if resp.State.Health == nil || resp.State.Health.Status != types.Healthy {
if err := start.WaitForHealthyService(ctx, 30*time.Second, utils.StorageId); err != nil {
return false, err
}
}
return true, nil
}

func resetDatabase14(ctx context.Context, version string, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error {
if err := recreateDatabase(ctx, options...); err != nil {
return err
Expand Down
Loading
Loading