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
28 changes: 17 additions & 11 deletions apps/cli/src/legacy/commands/gen/types/types.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ import {
resolvePgmetaImage,
} from "./types.shared.ts";

type ChildProcessOptions = ChildProcess.CommandOptions;

const mapProjectTypesError = mapLegacyHttpError({
networkError: LegacyGenTypesNetworkError,
statusError: LegacyGenTypesUnexpectedStatusError,
Expand Down Expand Up @@ -307,13 +309,11 @@ export const legacyGenTypes = Effect.fn("legacy.gen.types")(function* (flags: Le
"node",
"dist/server/server.js",
];
const child = yield* spawner.spawn(
ChildProcess.make("docker", args, {
stdin: "ignore",
stdout: "pipe",
stderr: "pipe",
}),
);
const child = yield* spawnContainerProcess(args, {
stdin: "ignore",
stdout: "pipe",
stderr: "pipe",
});

const [exitCode] = yield* Effect.all(
[
Expand All @@ -336,20 +336,21 @@ export const legacyGenTypes = Effect.fn("legacy.gen.types")(function* (flags: Le
// We only need the exit code and stderr (Go uses Docker's ContainerInspect API,
// which reads no stdout). Discard stdout so the inspect JSON can never fill the
// pipe buffer and deadlock the unconsumed stream.
const child = yield* spawner.spawn(
ChildProcess.make("docker", ["container", "inspect", localDbContainerId(projectId)], {
const child = yield* spawnContainerProcess(
["container", "inspect", localDbContainerId(projectId)],
Comment thread
jgoux marked this conversation as resolved.
{
stdin: "ignore",
stdout: "ignore",
stderr: "pipe",
}),
},
);
const [exitCode, stderr] = yield* Effect.all([
child.exitCode.pipe(Effect.map(Number)),
collectByteStream(child.stderr),
]);
if (exitCode !== 0) {
const message = stderr.trim();
if (message.includes("No such container")) {
if (message.toLowerCase().includes("no such container")) {
return yield* Effect.fail(new Error("supabase start is not running."));
}
return yield* Effect.fail(
Expand All @@ -363,6 +364,11 @@ export const legacyGenTypes = Effect.fn("legacy.gen.types")(function* (flags: Le
}),
);

const spawnContainerProcess = (args: ReadonlyArray<string>, options: ChildProcessOptions) =>
spawner
.spawn(ChildProcess.make("docker", args, options))
.pipe(Effect.catch(() => spawner.spawn(ChildProcess.make("podman", args, options))));

yield* Effect.gen(function* () {
if (flags.local) {
const loaded = yield* loadConfig();
Expand Down
159 changes: 158 additions & 1 deletion apps/cli/src/legacy/commands/gen/types/types.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { describe, expect, it } from "@effect/vitest";
import { BunServices } from "@effect/platform-bun";
import { ChildProcessSpawner } from "effect/unstable/process";
import { CliOutput, Command } from "effect/unstable/cli";
import { Deferred, Effect, Exit, Layer, Option, Sink, Stdio, Stream } from "effect";
import { Deferred, Effect, Exit, Layer, Option, PlatformError, Sink, Stdio, Stream } from "effect";
import {
LEGACY_GLOBAL_FLAGS,
LegacyDebugFlag,
Expand Down Expand Up @@ -263,6 +263,78 @@ function mockSequentialChildProcessSpawner(
};
}

function mockDockerMissingChildProcessSpawner(
steps: ReadonlyArray<{
readonly exitCode?: number;
readonly stdout?: ReadonlyArray<string>;
readonly stderr?: ReadonlyArray<string>;
}>,
) {
const encoder = new TextEncoder();
const spawned: Array<{ command: string; args: ReadonlyArray<string> }> = [];
let stepIndex = 0;

const layer = Layer.succeed(
ChildProcessSpawner.ChildProcessSpawner,
ChildProcessSpawner.make((command) =>
Effect.gen(function* () {
const cmd = command._tag === "StandardCommand" ? command.command : "";
const args = command._tag === "StandardCommand" ? command.args : [];
spawned.push({ command: cmd, args });

if (cmd === "docker") {
return yield* Effect.fail(
PlatformError.systemError({
_tag: "NotFound",
module: "ChildProcess",
method: "spawn",
description: "docker not found",
}),
);
}

const step = steps[Math.min(stepIndex, steps.length - 1)];
stepIndex += 1;
const exitDeferred = yield* Deferred.make<ChildProcessSpawner.ExitCode>();

yield* Effect.forkDetach(
Effect.gen(function* () {
yield* Effect.sleep("10 millis");
yield* Deferred.succeed(
exitDeferred,
ChildProcessSpawner.ExitCode(step?.exitCode ?? 0),
);
}),
);

const stdoutBytes = (step?.stdout ?? []).map((line) => encoder.encode(`${line}\n`));
const stderrBytes = (step?.stderr ?? []).map((line) => encoder.encode(`${line}\n`));

return ChildProcessSpawner.makeHandle({
pid: ChildProcessSpawner.ProcessId(3000 + spawned.length),
stdout: Stream.fromIterable(stdoutBytes),
stderr: Stream.fromIterable(stderrBytes),
all: Stream.empty,
exitCode: Deferred.await(exitDeferred),
isRunning: Effect.succeed(false),
stdin: Sink.drain,
kill: () => Effect.void,
unref: Effect.succeed(Effect.void),
getInputFd: () => Sink.drain,
getOutputFd: () => Stream.empty,
});
}),
),
);

return {
layer,
get spawned() {
return spawned;
},
};
}

async function withSslProbeServer<T>(
run: (port: number) => Promise<T>,
response: "N" | "S" = "N",
Expand Down Expand Up @@ -697,6 +769,55 @@ describe("legacy gen types", () => {
}),
);

it.live("falls back to podman when the docker executable is missing for local generation", () =>
Effect.tryPromise({
try: () =>
withSslProbeServer(async (port) => {
const workdir = mkdtempSync(join(tmpdir(), "supabase-gen-types-local-podman-"));
writeConfig(
workdir,
[
'project_id = "demo"',
"",
"[api]",
'schemas = ["public"]',
"",
"[db]",
`port = ${port}`,
].join("\n"),
);
const child = mockDockerMissingChildProcessSpawner([
{ exitCode: 0 },
{ exitCode: 0, stdout: ["export type Database = {};"] },
]);
const { layer, out } = setup({
workdir,
childLayer: child.layer,
});

await Effect.runPromise(
legacyGenTypes(defaultFlags({ local: true })).pipe(Effect.provide(layer)),
);

expect(out.stdoutText).toContain("export type Database = {};");
expect(child.spawned[0]).toEqual({
command: "docker",
args: ["container", "inspect", "supabase_db_demo"],
});
expect(child.spawned[1]).toEqual({
command: "podman",
args: ["container", "inspect", "supabase_db_demo"],
});
expect(child.spawned[2]?.command).toBe("docker");
expect(child.spawned[2]?.args).toContain("run");
expect(child.spawned[3]?.command).toBe("podman");
expect(child.spawned[3]?.args).toContain("run");
expect(child.spawned[3]?.args).toContain("supabase_network_demo");
}),
catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))),
}),
);

it.live("uses sanitized local docker ids and env-backed local db passwords", () =>
Effect.tryPromise({
try: () =>
Expand Down Expand Up @@ -1080,6 +1201,42 @@ describe("legacy gen types", () => {
});
});

it.live("keeps not-running parity when podman reports the local db container is missing", () => {
const workdir = mkdtempSync(join(tmpdir(), "supabase-gen-types-local-podman-missing-"));
writeConfig(
workdir,
['project_id = "demo"', "", "[api]", 'schemas = ["public"]', "", "[db]", "port = 54321"].join(
"\n",
),
);
const child = mockDockerMissingChildProcessSpawner([
{
exitCode: 1,
stderr: ['Error: inspecting object: no such container "supabase_db_demo"'],
},
]);
const { layer } = setup({
workdir,
childLayer: child.layer,
});

return Effect.gen(function* () {
const exit = yield* legacyGenTypes(defaultFlags({ local: true })).pipe(
Effect.provide(layer),
Effect.exit,
);

expect(Exit.isFailure(exit)).toBe(true);
if (Exit.isFailure(exit)) {
expect(String(exit.cause)).toContain("supabase start is not running.");
}
expect(child.spawned).toEqual([
{ command: "docker", args: ["container", "inspect", "supabase_db_demo"] },
{ command: "podman", args: ["container", "inspect", "supabase_db_demo"] },
]);
});
});

it.live(
"preserves inspect failure details when local db inspection fails for other reasons",
() => {
Expand Down
Loading