Skip to content
Open
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
16 changes: 8 additions & 8 deletions apps/cli/src/next/commands/branches/switch/switch.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,18 +145,18 @@ export const switchBranch = Effect.fn("branches.switch")(function* (opts: {
// TODO: run `supabase pull` against the new branch before restarting the stack
// so the local config reflects the branch's migrations and seed state.
// `pull` does not exist yet.
const launch = stackCheck.value.launch;
const launchConfig =
stackCheck.value.launch === undefined
? toStartStackConfig([], "auto")
launch === undefined
? toStartStackConfig([], undefined)
: withServiceVersions(
toStartStackConfig(
stackCheck.value.launch.excludedServices?.filter(
(service): service is ExcludedStackService =>
excludedStackServices.some((candidate) => candidate === service),
launch.excludedServices?.filter((service): service is ExcludedStackService =>
excludedStackServices.some((candidate) => candidate === service),
) ?? [],
stackCheck.value.launch.mode,
"mode" in launch ? launch.mode : undefined,
),
stackCheck.value.launch.versions,
launch.versions,
);
const loadedProjectConfig = yield* loadProjectConfig(projectHome.projectRoot);

Expand All @@ -166,7 +166,7 @@ export const switchBranch = Effect.fn("branches.switch")(function* (opts: {
projectDir: projectHome.projectRoot,
name: stackName,
portIntents: managedPortIntents(launchConfig, loadedProjectConfig ?? undefined),
...(stackCheck.value.launch !== undefined && { launch: stackCheck.value.launch }),
...(launch !== undefined && { launch }),
...launchConfig,
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ const startFullStack = Effect.fnUntraced(function* (opts: FunctionsDevStackOptio
const serviceVersionContext = yield* resolveServiceVersionContext([], undefined);
const loadedProjectConfig = yield* loadProjectConfig(projectHome.projectRoot);
const stackConfig = withServiceVersions(
toStartStackConfig([], "auto"),
toStartStackConfig([], undefined),
serviceVersionContext.runtimeVersions,
);
const stackLayer = yield* daemonLayer({
Expand All @@ -65,7 +65,6 @@ const startFullStack = Effect.fnUntraced(function* (opts: FunctionsDevStackOptio
name: opts.stack,
edgeRuntime: opts.edgeRuntime,
launch: {
mode: "auto",
versions: serviceVersionContext.pinnedBaseline,
excludedServices: [],
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ import {
} from "../../config/service-version-resolution.ts";

describe("service version overrides", () => {
test("parses and normalizes repeated flag overrides", async () => {
test("parses repeated flag overrides as exact image tags", async () => {
await expect(
Effect.runPromise(
parseServiceVersionOverrides(["postgrest=v14.5", "mailpit=1.30.2", "auth=2.180.0"]),
),
).resolves.toEqual({
postgrest: "14.5",
mailpit: "v1.30.2",
postgrest: "v14.5",
mailpit: "1.30.2",
auth: "2.180.0",
});
});
Expand All @@ -27,8 +27,8 @@ describe("service version overrides", () => {
const candidateBaseline = {
...DEFAULT_VERSIONS,
postgres: "17.6.1.090",
postgrest: "14.5",
auth: "2.187.0",
postgrest: "v14.5",
auth: "v2.187.0",
};

const layer = Layer.mergeAll(
Expand Down Expand Up @@ -68,12 +68,12 @@ describe("service version overrides", () => {
runtimeVersions: {
...candidateBaseline,
postgres: "17.4.1.045",
auth: "2.170.0",
auth: "v2.170.0",
storage: "1.40.0",
},
activeOverrides: [
{ service: "postgres", version: "17.4.1.045", source: "flag" },
{ service: "auth", version: "2.170.0", source: "flag" },
{ service: "auth", version: "v2.170.0", source: "flag" },
{ service: "storage", version: "1.40.0", source: "local" },
],
availableUpdates: [],
Expand Down
18 changes: 11 additions & 7 deletions apps/cli/src/next/commands/start/start.command.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Effect, Layer, Context } from "effect";
import { Effect, Layer, Context, Option } from "effect";
import { loadProjectConfig } from "@supabase/config";
import {
DEFAULT_MANAGED_STACK_NAME,
Expand Down Expand Up @@ -79,14 +79,15 @@ export const serviceVersionFlag = Flag.string("service-version").pipe(

const modeFlag = Flag.choice("mode", startModes).pipe(
Flag.withDescription(
'Stack startup mode. "auto" prefers native binaries and falls back to Docker, "native" requires native-compatible services, and "docker" forces Docker for all services.',
'Stack startup mode. "native" requires native-compatible services and "docker" requires a usable Docker or Podman runtime.',
),
Flag.withDefault("auto" as StartMode),
Flag.optional,
Flag.map(Option.getOrUndefined),
);

interface StartVersionStateShape {
readonly launch: {
readonly mode: StartMode;
readonly mode?: StartMode;
readonly versions: Readonly<Record<string, string>>;
readonly excludedServices: ReadonlyArray<ExcludedStackService>;
};
Expand Down Expand Up @@ -124,7 +125,7 @@ export type StartFlags = CliCommand.Command.Config.Infer<typeof flags>;
export const startCommand = Command.make("start", flags).pipe(
Command.withDescription(
"Start the local Supabase development stack.\n\n" +
"Starts the full local Supabase stack. Use --mode auto (default) to prefer native binaries and fall back to Docker, --mode native to require native-compatible services, or --mode docker to force Docker-backed startup.\n\n" +
"Starts the full local Supabase stack. By default, a usable Docker or Podman runtime selects Docker mode; otherwise the stack uses native mode. Use --mode to require one explicitly.\n\n" +
"Named CLI stacks persist managed runtime state under the Supabase home directory. Use --exclude to skip optional services. Use --detach to run in the background.",
),
Command.withShortDescription("Start local Supabase stack"),
Expand Down Expand Up @@ -219,7 +220,7 @@ export const startCommand = Command.make("start", flags).pipe(
portDocument: portIntents,
});
const launch = {
mode: flags.mode,
...(flags.mode === undefined ? {} : { mode: flags.mode }),
versions: serviceVersionContext.pinnedBaseline,
excludedServices: flags.exclude,
...(existingSummary?.lastNotifiedUpdateFingerprint === undefined
Expand All @@ -242,12 +243,15 @@ export const startCommand = Command.make("start", flags).pipe(
cwd: runtimeInfo.cwd,
name: flags.stack,
});
if (summary.launch === undefined) {
return yield* Effect.die("Managed stack started without persisted launch settings");
}

return {
stackLayer,
startVersionState: StartVersionState.of({
launch: {
mode: flags.mode,
mode: "mode" in summary.launch ? summary.launch.mode : undefined,
versions: serviceVersionContext.pinnedBaseline,
excludedServices: flags.exclude,
},
Expand Down
3 changes: 1 addition & 2 deletions apps/cli/src/next/commands/start/start.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ export const start = Effect.fnUntraced(function* (flags: StartFlags) {
yield* updateManagedLaunch({
...lifecycleInput,
launch: {
mode: launch.mode,
versions: launch.versions,
excludedServices: launch.excludedServices,
lastNotifiedUpdateFingerprint: serviceVersionContext.updateFingerprint,
Expand All @@ -68,7 +67,7 @@ export const start = Effect.fnUntraced(function* (flags: StartFlags) {
}

yield* analytics.capture("cli_stack_started", {
mode: flags.mode,
mode: launch.mode,
detach: flags.detach,
stack: flags.stack,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ describe("start handler", () => {
);
return start({
stack: fixture.stackName,
mode: "auto",
mode: "docker",
exclude: [],
serviceVersion: [],
detach: false,
Expand Down
7 changes: 3 additions & 4 deletions apps/cli/src/next/commands/status/status.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,11 @@ const resolveConfiguredSummary = Effect.fnUntraced(function* (input: {
const current = yield* resolveStackSummary(input);
const loaded = yield* loadProjectConfig(input.projectDir);
const excluded = (current.launch?.excludedServices ?? []).filter(isExcludedStackService);
const mode =
current.launch !== undefined && "mode" in current.launch ? current.launch.mode : undefined;
return yield* resolveStackSummary({
...input,
portDocument: managedPortIntents(
toStartStackConfig(excluded, current.launch?.mode ?? "auto"),
loaded ?? undefined,
),
portDocument: managedPortIntents(toStartStackConfig(excluded, mode), loaded ?? undefined),
});
});

Expand Down
7 changes: 1 addition & 6 deletions apps/cli/src/next/commands/update/update.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,19 +103,14 @@ export const update = Effect.fnUntraced(function* (flags: UpdateFlags) {
);

if (Option.isSome(existingSummary)) {
const persistedLaunch = existingSummary.value.launch ?? {
mode: "auto" as const,
excludedServices: [] as const,
};
yield* updateManagedLaunch({
cacheRoot: cliConfig.supabaseHome,
cwd: runtimeInfo.cwd,
workspacePath: projectHome.projectRoot,
stackName: flags.stack,
launch: {
mode: persistedLaunch.mode,
versions: serviceVersionContext.candidateBaseline,
excludedServices: persistedLaunch.excludedServices,
excludedServices: existingSummary.value.launch?.excludedServices ?? [],
...(existingSummary.value.lastNotifiedUpdateFingerprint === undefined
? {}
: {
Expand Down
7 changes: 3 additions & 4 deletions apps/cli/src/next/config/stack-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,16 @@ export const excludedStackServices = [
export type ExcludedStackService = (typeof excludedStackServices)[number];
export const isExcludedStackService = (value: string): value is ExcludedStackService =>
excludedStackServices.some((candidate) => candidate === value);
export const startModes = ["native", "auto", "docker"] as const;
export const startModes = ["native", "docker"] as const;
export type StartMode = (typeof startModes)[number];

export function toStartStackConfig(
exclude: ReadonlyArray<ExcludedStackService>,
mode: StartMode,
mode?: StartMode,
): StackConfig {
const excluded = new Set(exclude);
return {
mode,
startupMode: "lazy",
...(mode === undefined ? {} : { mode }),
Comment thread
jgoux marked this conversation as resolved.
realtime: excluded.has("realtime") ? false : {},
storage: excluded.has("storage") ? false : {},
imgproxy: excluded.has("imgproxy") || excluded.has("storage") ? false : {},
Expand Down
25 changes: 13 additions & 12 deletions apps/cli/src/next/config/stack-config.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,29 @@ import { describe, expect, it } from "vitest";
import { toStartStackConfig, withServiceVersions } from "./stack-config.ts";

describe("toStartStackConfig", () => {
it("uses lazy service startup with the requested runtime mode", () => {
expect(toStartStackConfig([], "auto")).toMatchObject({
mode: "auto",
startupMode: "lazy",
it("leaves mode unset so the stack package can select the usable runtime", () => {
expect(toStartStackConfig([], undefined)).not.toHaveProperty("mode");
});

it("uses the requested runtime mode and catalog service defaults", () => {
expect(toStartStackConfig([], "docker")).toMatchObject({
mode: "docker",
});
expect(toStartStackConfig([], "docker")).toMatchObject({
mode: "docker",
startupMode: "lazy",
});
expect(toStartStackConfig([], "native")).toMatchObject({
mode: "native",
startupMode: "lazy",
});
});

it("dedupes excluded services when building stack config", () => {
expect(toStartStackConfig(["auth", "auth"], "auto")).toMatchObject({
mode: "auto",
expect(toStartStackConfig(["auth", "auth"], "docker")).toMatchObject({
mode: "docker",
auth: false,
});
expect(toStartStackConfig(["auth", "postgrest"], "auto")).toMatchObject({
mode: "auto",
expect(toStartStackConfig(["auth", "postgrest"], "docker")).toMatchObject({
mode: "docker",
auth: false,
postgrest: false,
});
Expand All @@ -33,7 +34,7 @@ describe("toStartStackConfig", () => {
describe("withServiceVersions", () => {
it("injects linked service versions without re-enabling excluded services", () => {
expect(
withServiceVersions(toStartStackConfig([], "auto"), {
withServiceVersions(toStartStackConfig([], "docker"), {
postgres: "17.6.1.090",
postgrest: "14.5",
auth: "2.187.0",
Expand All @@ -49,7 +50,7 @@ describe("withServiceVersions", () => {
});

expect(
withServiceVersions(toStartStackConfig(["auth", "storage"], "auto"), {
withServiceVersions(toStartStackConfig(["auth", "storage"], "docker"), {
postgres: "17.6.1.090",
auth: "2.187.0",
storage: "1.39.2",
Expand Down
39 changes: 39 additions & 0 deletions apps/cli/src/shared/runtime/stack-e2e-cleanup.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,45 @@ describe("stack e2e cleanup manager", () => {
expect(calls).toEqual(["stop:/tmp/project:/tmp/home", "cleanup-project", "dispose-home"]);
});

it("keeps cleanup best-effort when the associated home cannot be disposed", async () => {
const calls: Array<string> = [];
const manager = createStackE2eCleanupManager(
cleanupEnvironment(calls, {
captureSnapshot: () => ({
managedStacksRootExists: true,
documentFiles: [],
stackDirs: [],
trackedPids: [],
}),
}),
);

manager.registerHome({
dir: "/tmp/home",
dispose: () => {
calls.push("dispose-home");
throw permissionError("home is not removable");
},
});
manager.registerStackProject({
dir: "/tmp/project",
cleanup: async () => {
calls.push("cleanup-project");
},
});
manager.associateHome("/tmp/project", "/tmp/home");

const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
try {
await expect(manager.drain()).resolves.toBeUndefined();
expect(warn).toHaveBeenCalledWith(expect.stringContaining("/tmp/home"));
expect(warn).toHaveBeenCalledWith(expect.stringContaining("home is not removable"));
} finally {
warn.mockRestore();
}
expect(calls).toEqual(["stop:/tmp/project:/tmp/home", "cleanup-project", "dispose-home"]);
});

it("canonicalizes symlinked project and home paths before matching stack state", async () => {
const root = mkdtempSync(join(tmpdir(), "stack-e2e-cleanup-"));
const project = join(root, "project");
Expand Down
3 changes: 3 additions & 0 deletions apps/cli/src/shared/telemetry/error-actionability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -983,6 +983,9 @@ const externalActionabilityByTag: Record<string, ErrorActionabilityAdapter> = {
? { ...actionability.invalidConfig, fingerprint_suffix: "port_allocation" }
: actionability.unknown,
BinaryNotFoundError: () => actionability.invalidConfig,
BinaryManifestError: () => actionability.externalNetwork,
BinaryRuntimeError: () => actionability.externalNetwork,
BinaryHostCompatibilityError: () => actionability.invalidConfig,
DownloadError: () => actionability.externalNetwork,
ChecksumMismatchError: () => ({
...actionability.externalNetwork,
Expand Down
7 changes: 6 additions & 1 deletion apps/cli/tests/helpers/running-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,12 @@ import { CliConfig } from "../../src/next/config/cli-config.service.ts";
import { ProjectHome } from "../../src/next/config/project-home.service.ts";
import { RuntimeInfo } from "../../src/shared/runtime/runtime-info.service.ts";

const launch = { mode: "auto" as const, versions: { postgres: "17.6.1" }, excludedServices: [] };
const launch = {
mode: "docker" as const,
containerRuntime: "docker" as const,
versions: { postgres: "17.6.1" },
excludedServices: [],
};
const portDocument: ManagedPortIntentDocument = {
activeFields: ["apiPort", "dbPort"],
document: {},
Expand Down
6 changes: 5 additions & 1 deletion apps/cli/tests/helpers/stack-e2e-cleanup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -441,7 +441,11 @@ export function createStackE2eCleanupManager(
failures.push(cleanupErrorDetail(project.dir, error));
} finally {
if (home !== undefined) {
home.dispose();
try {
home.dispose();
} catch (error) {
failures.push(cleanupErrorDetail(home.dir, error));
}
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions packages/process-compose/tests/helpers/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ const isOneShotSupervisor = (args: ReadonlyArray<string>): boolean => {
typeof config === "object" &&
config !== null &&
"command" in config &&
config.command === "bash" &&
"args" in config &&
Array.isArray(config.args) &&
config.args[0] === "-c"
((config.command === "bash" && config.args[0] === "-c") ||
((config.command === "docker" || config.command === "podman") && config.args[0] === "exec"))
);
} catch {
return false;
Expand Down
Loading
Loading