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
18 changes: 15 additions & 3 deletions packages/stack/src/Service.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
ServiceDestroyed,
ServiceError,
ServiceLaunchError,
ServiceStaleLaunch,
type RuntimeSession,
type ServiceDefinition,
type ServiceInstance,
Expand Down Expand Up @@ -890,7 +891,7 @@ describe("service kernel", () => {
),
);

it.live("cleans an exact runtime after an unexpected exit during health", () =>
it.live("fails readiness with the crash error and cleans the exact runtime", () =>
Effect.scoped(
Effect.gen(function* () {
const fixture = yield* makeFixture;
Expand Down Expand Up @@ -918,7 +919,13 @@ describe("service kernel", () => {
yield* open(plan.removeGate);
yield* Fiber.join(stopping);
yield* Fiber.join(stopped);
expect(Exit.isFailure(yield* Fiber.await(ready))).toBe(true);
const result = yield* Fiber.await(ready);
expect(Exit.isFailure(result)).toBe(true);
if (Exit.isFailure(result)) {
const failure = result.cause.reasons.find(Cause.isFailReason);
expect(failure?.error).toBeInstanceOf(ServiceError);
expect(failure?.error).toMatchObject({ operation: "process", message: "crashed" });
}
expect((yield* fixture.service.get).lifecycle).toBe("stopped");
expect((yield* Ref.get(plan.state)).removed).toBe(true);
expect((yield* fixture.service.get).wakeEnabled).toBe(false);
Expand Down Expand Up @@ -1001,7 +1008,12 @@ describe("service kernel", () => {
yield* Deferred.await(first.removeStarted);
yield* open(first.removeGate);
yield* Fiber.join(stopping);
expect(Exit.isFailure(yield* Fiber.await(oldReady))).toBe(true);
const stale = yield* Fiber.await(oldReady);
expect(Exit.isFailure(stale)).toBe(true);
if (Exit.isFailure(stale))
expect(stale.cause.reasons.find(Cause.isFailReason)?.error).toBeInstanceOf(
ServiceStaleLaunch,
);

yield* open(second.launchGate);
yield* fixture.service.start;
Expand Down
8 changes: 8 additions & 0 deletions packages/stack/src/functions/FunctionsBootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export const makeFunctionsBootstrapOwner = Effect.fn("FunctionsBootstrap.makeOwn
if (input.content.includes("\u0000"))
return yield* failure("Functions bootstrap contains an invalid character");
const target = path.join(root, "index.ts");
const configFile = path.join(root, "deno.json");
return yield* Effect.gen(function* () {
const token = yield* crypto.randomUUIDv4.pipe(
Effect.mapError((cause) =>
Expand All @@ -83,6 +84,13 @@ export const makeFunctionsBootstrapOwner = Effect.fn("FunctionsBootstrap.makeOwn
fs.makeDirectory(root, { recursive: true, mode: 0o700 }),
);
yield* mapFs(root, "secure functions bootstrap directory", fs.chmod(root, 0o700));
// An empty workspace root stops Deno config discovery before any ancestor package.json or
// workspace; a plain `{}` still joins an ancestor Deno workspace and fails membership.
yield* mapFs(
configFile,
"write functions bootstrap config",
fs.writeFileString(configFile, '{"workspace":[]}\n', { mode: 0o600 }),
);
yield* Effect.scoped(
Effect.gen(function* () {
const file = yield* mapFs(
Expand Down
90 changes: 90 additions & 0 deletions packages/stack/src/services/Functions.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,96 @@ describe("service catalog", () => {
}),
).pipe(Effect.provide(Layer.merge(NodeServices.layer, NodeHttpClient.layerNodeHttp))),
);

const ancestors = [
{
name: "a package.json with an unreadable sibling",
setup: (fs: FileSystem.FileSystem, root: string) =>
Effect.gen(function* () {
yield* fs.writeFileString(`${root}/package.json`, '{"name":"home","private":true}');
yield* fs.makeDirectory(`${root}/unreadable`);
yield* fs.chmod(`${root}/unreadable`, 0o000);
yield* Effect.addFinalizer(() =>
fs.chmod(`${root}/unreadable`, 0o755).pipe(Effect.ignore),
);
}),
},
{
name: "a Deno workspace that includes the project functions",
setup: (fs: FileSystem.FileSystem, root: string) =>
fs.writeFileString(`${root}/deno.json`, '{"workspace":["./project/supabase/functions"]}'),
},
];
for (const ancestor of ancestors)
it.live(
`boots below ${ancestor.name} and keeps shared functions deno.json imports`,
() =>
Effect.scoped(
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const client = yield* HttpClient.HttpClient;
const temporaryRoot = `${process.cwd()}/tmp`;
yield* fs.makeDirectory(temporaryRoot, { recursive: true });
const root = yield* fs.makeTempDirectoryScoped({
directory: temporaryRoot,
prefix: "functions-ancestor-",
});
yield* ancestor.setup(fs, root);
const functionsRoot = `${root}/project/supabase/functions`;
yield* fs.makeDirectory(`${functionsRoot}/hello`, { recursive: true });
yield* fs.makeDirectory(`${functionsRoot}/_shared`, { recursive: true });
yield* fs.writeFileString(
`${functionsRoot}/deno.json`,
'{"imports":{"shared-message":"./_shared/message.ts"}}',
);
yield* fs.writeFileString(
`${functionsRoot}/_shared/message.ts`,
'export const message = "shared";',
);
yield* fs.writeFileString(
`${functionsRoot}/hello/index.ts`,
'import { message } from "shared-message"; Deno.serve(() => new Response(message));',
);
const recipe = yield* makeServiceRecipe(
{
service: "functions",
config: {
functionsRoot,
bootstrap: yield* bundleServeMainTemplate,
verifyJwt: false,
},
},
{
...options(root),
stackId: "d".repeat(64),
instanceId: "ancestor",
cacheRoot: "/tmp/supabase-stack-artifacts",
},
);
const logs = yield* Ref.make("");
yield* recipe.logs.pipe(
Stream.runForEach(({ bytes }) =>
Ref.update(logs, (text) => text + new TextDecoder().decode(bytes)),
),
Effect.forkScoped({ startImmediately: true }),
);
const instance = yield* makeService(recipe.definition, {
id: "ancestor",
config: recipe.creation,
});
yield* instance.start;
yield* instance.ready.pipe(
Effect.tapError(() => Ref.get(logs).pipe(Effect.flatMap(Effect.logError))),
);
const endpoint = yield* recipe.endpoint("http");
const response = yield* client.get(`http://${endpoint.host}:${endpoint.port}/hello`);
expect(response.status, yield* Ref.get(logs)).toBe(200);
expect(yield* response.text).toBe("shared");
yield* instance.stop;
}),
).pipe(Effect.provide(Layer.merge(NodeServices.layer, NodeHttpClient.layerNodeHttp))),
{ timeout: 120_000 },
);
});

for (const runtime of ["native", "docker"] as const) {
Expand Down
Loading