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
@@ -1,5 +1,5 @@
import { existsSync, readFileSync, readdirSync, realpathSync } from "node:fs";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";

import { describe, expect, it } from "@effect/vitest";
Expand Down Expand Up @@ -139,6 +139,9 @@ vi.mock("../../../../shared/functions/deploy.ts", async () => {

const tempRoot = useLegacyTempWorkdir("supabase-functions-serve-int-");

// Root bypasses POSIX permission bits, so chmod-based failure tests can't run there.
const isRoot = typeof process.getuid === "function" && process.getuid() === 0;

const { legacyFunctionsServe } = await import("./serve.handler.ts");

interface LogProcessBehavior {
Expand Down Expand Up @@ -2748,4 +2751,72 @@ describe("legacy functions serve integration", () => {
).toHaveLength(0);
});
});

it.live("surfaces the real filesystem error when the fallback env file is unreadable", () => {
return Effect.gen(function* () {
yield* Effect.promise(() =>
writeProjectConfig(['project_id = "test-project"', ""].join("\n")),
);
yield* Effect.promise(() =>
writeFunctionFile("hello", "index.ts", 'Deno.serve(() => new Response("hello"))\n'),
);
// A directory at the fallback path makes the read fail with a non-ENOENT error (EISDIR).
yield* Effect.promise(() =>
mkdir(join(tempRoot.current, "supabase", "functions", ".env"), { recursive: true }),
);

const { layer } = setupServe();
const error = yield* legacyFunctionsServe(baseFlags()).pipe(
Effect.provide(layer),
Effect.flip,
);

expect(error).toBeInstanceOf(Error);
if (error instanceof Error) {
expect(error.message).toContain("EISDIR");
expect(error.message).not.toContain("An error occurred in Effect.tryPromise");
}
expect(
deployMockState.runCalls.filter(
(call) => call.command === "docker" && call.args[0] === "run",
),
).toHaveLength(0);
});
});

it.live.skipIf(isRoot)(
"surfaces the real filesystem error when the env staging dir cannot be created",
() => {
return Effect.gen(function* () {
yield* Effect.promise(() =>
writeProjectConfig(['project_id = "test-project"', ""].join("\n")),
);
yield* Effect.promise(() =>
writeFunctionFile("hello", "index.ts", 'Deno.serve(() => new Response("hello"))\n'),
);
// A read-only parent makes the per-container staging-dir mkdir fail with EACCES.
const stagingRoot = join(tempRoot.current, "supabase", ".temp", "start-secrets");
yield* Effect.promise(() => mkdir(stagingRoot, { recursive: true }));
yield* Effect.promise(() => chmod(stagingRoot, 0o555));

const { layer } = setupServe();
const error = yield* legacyFunctionsServe(baseFlags()).pipe(
Effect.provide(layer),
Effect.flip,
Effect.ensuring(Effect.promise(() => chmod(stagingRoot, 0o755))),
);

expect(error).toBeInstanceOf(Error);
if (error instanceof Error) {
expect(error.message).toContain("EACCES");
expect(error.message).not.toContain("An error occurred in Effect.tryPromise");
}
expect(
deployMockState.runCalls.filter(
(call) => call.command === "docker" && call.args[0] === "run",
),
).toHaveLength(0);
});
},
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -814,7 +814,13 @@ describe("legacy gen types", () => {
// Effect parser produces for this argv (both `local` and `linked` parse as
// independently true, since its tokenizer is unaware of pflag's value
// consumption — CLI-1982); only the pflag-faithful scan can tell them apart.
const { layer } = setup({ args: ["gen", "types", "-s", "--linked", "--local"] });
// `childExitCode: 1` fails the local target's `container inspect`, keeping the
// downstream failure deterministic before the real SSL probe can reach whatever
// is listening on the local db port.
const { layer } = setup({
args: ["gen", "types", "-s", "--linked", "--local"],
childExitCode: 1,
});

return Effect.gen(function* () {
const exit = yield* legacyGenTypes(defaultFlags({ local: true, linked: true })).pipe(
Expand All @@ -824,6 +830,7 @@ describe("legacy gen types", () => {

expect(Exit.isFailure(exit)).toBe(true);
if (Exit.isFailure(exit)) {
expect(String(exit.cause)).toContain("failed to inspect service");
expect(String(exit.cause)).not.toContain("if any flags in the group");
}
});
Expand Down
84 changes: 43 additions & 41 deletions apps/cli/src/shared/functions/serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -812,17 +812,19 @@ const parseCustomEnvFile = Effect.fnUntraced(function* (

if (Option.isNone(envFileFlag)) {
const fallbackPath = join(projectRoot, fallbackEnvFilePath);
const exists = yield* Effect.tryPromise(() =>
readFile(fallbackPath, "utf8").then(
(contents) => ({ contents, path: fallbackPath }),
(error) => {
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
return undefined;
}
throw error;
},
),
);
const exists = yield* Effect.tryPromise({
try: () =>
readFile(fallbackPath, "utf8").then(
(contents) => ({ contents, path: fallbackPath }),
(error) => {
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
return undefined;
}
throw error;
},
),
catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))),
});
if (exists === undefined) {
return yield* toEnvEntries({});
}
Expand Down Expand Up @@ -1031,19 +1033,19 @@ const loadServeProjectEnvironment = Effect.fnUntraced(function* (projectRoot: st
for (const dir of [paths.supabaseDir, paths.projectRoot]) {
for (const filename of loadDefaultEnvFilenames(env)) {
const envPath = join(dir, filename);
const contents = yield* Effect.tryPromise(() =>
readFile(envPath, "utf8").then(
(value) => value,
(error) => {
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
return undefined;
}
throw error;
},
),
).pipe(
Effect.mapError((cause) => (cause instanceof Error ? cause : new Error(String(cause)))),
);
const contents = yield* Effect.tryPromise({
try: () =>
readFile(envPath, "utf8").then(
(value) => value,
(error) => {
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
return undefined;
}
throw error;
},
),
catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))),
});
if (contents === undefined) {
continue;
}
Expand Down Expand Up @@ -1611,19 +1613,20 @@ export const startEdgeRuntimeContainer = Effect.fn("functions.startEdgeRuntimeCo
try: () => validateDockerMultilineEnvNames(multilineDockerEnv),
catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))),
});
const dockerEnvFile = yield* Effect.tryPromise(() =>
writeDockerEnvFile(singleLineDockerEnv, join(stagingDir, "env")),
);
const dockerEnvFile = yield* Effect.tryPromise({
try: () => writeDockerEnvFile(singleLineDockerEnv, join(stagingDir, "env")),
catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))),
});
const multilineEnvDir = "/root/.supabase/multiline-env";
const dockerMultilineEnvScript = yield* Effect.tryPromise(() =>
writeDockerMultilineEnvScript(
multilineDockerEnv,
multilineEnvDir,
join(stagingDir, "multiline-env"),
),
).pipe(
Effect.mapError((cause) => (cause instanceof Error ? cause : new Error(String(cause)))),
);
const dockerMultilineEnvScript = yield* Effect.tryPromise({
try: () =>
writeDockerMultilineEnvScript(
multilineDockerEnv,
multilineEnvDir,
join(stagingDir, "multiline-env"),
),
catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))),
});

const labels = dockerProjectLabels(projectId);
const runtimeCommand = [
Expand All @@ -1636,11 +1639,10 @@ export const startEdgeRuntimeContainer = Effect.fn("functions.startEdgeRuntimeCo
...(input.debug ? ["--verbose"] : []),
];
const serveMainTemplate = yield* Effect.promise(() => getLegacyFunctionsServeMainTemplate());
const serveMainTemplateFile = yield* Effect.tryPromise(() =>
writeServeMainTemplateFile(serveMainTemplate, join(stagingDir, "main")),
).pipe(
Effect.mapError((cause) => (cause instanceof Error ? cause : new Error(String(cause)))),
);
const serveMainTemplateFile = yield* Effect.tryPromise({
try: () => writeServeMainTemplateFile(serveMainTemplate, join(stagingDir, "main")),
catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))),
});
const containerProjectRoot = toDockerPath(input.projectRoot);
const command = [
"run",
Expand Down
Loading