-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Effect cluster deployment on staging #1165
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
8b976e4
b12b221
fe273ee
dc2f22b
84c21bb
921549e
8c8f599
1bcee3a
fe3180e
40ee84f
b4361e4
2a91e42
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,28 @@ | ||||||
| FROM node:24-slim AS base | ||||||
| RUN corepack enable | ||||||
|
|
||||||
| FROM base AS builder | ||||||
| WORKDIR /app | ||||||
| COPY . . | ||||||
|
|
||||||
| RUN corepack enable pnpm | ||||||
|
|
||||||
| RUN echo "inject-workspace-packages=true" >> .npmrc | ||||||
| RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store pnpm i | ||||||
|
|
||||||
| RUN pnpm run --filter=@cap/web-cluster build | ||||||
| RUN pnpm deploy --filter=@cap/web-cluster out | ||||||
| RUN cd out && node scripts/post-deploy.ts | ||||||
|
|
||||||
| FROM denoland/deno:2.5.3 AS runner | ||||||
| WORKDIR /app | ||||||
|
|
||||||
| COPY --from=builder --chown=deno:deno /app/out /app | ||||||
|
|
||||||
| USER deno | ||||||
|
|
||||||
| ENTRYPOINT ["deno", "run", "--allow-all"] | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Container cannot start: ENTRYPOINT lacks a script.
🤖 Prompt for AI Agents
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @coderabbitai this is done so we can choose which script to launch when running the container
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @Brendonovich, thank you for clarifying! That makes perfect sense—leaving the ENTRYPOINT flexible allows you to specify which script to run at container launch time (e.g., ✏️ Learnings added
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Restrict Deno permissions in production. The Apply this approach to restrict permissions: -ENTRYPOINT ["deno", "run", "--allow-all"]
+ENTRYPOINT ["deno", "run", "--allow-env", "--allow-net", "--allow-read=/app", "--allow-write=/tmp"]Adjust permissions based on the actual runtime requirements of your application. Review what the application needs access to and grant only those permissions. 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
|
|
||||||
| EXPOSE 8080 | ||||||
| EXPOSE 42069 | ||||||
| EXPOSE 42169 | ||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,31 @@ | ||||||
| { | ||||||
| "name": "@cap/web-cluster", | ||||||
| "type": "module", | ||||||
| "scripts": { | ||||||
| "dev": "pnpm dotenv -e ../../.env -- concurrently \"deno run --allow-all --watch ./src/runner/index.ts\" \"deno run --allow-all --watch ./src/shard-manager.ts\"", | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid --allow-all in development scripts. The Consider applying this approach: - "dev": "pnpm dotenv -e ../../.env -- concurrently \"deno run --allow-all --watch ./src/runner/index.ts\" \"deno run --allow-all --watch ./src/shard-manager.ts\"",
+ "dev": "pnpm dotenv -e ../../.env -- concurrently \"deno run --allow-env --allow-net --allow-read --allow-write --watch ./src/runner/index.ts\" \"deno run --allow-env --allow-net --allow-read --allow-write --watch ./src/shard-manager.ts\"",Adjust permissions based on actual requirements. 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| "build": "pnpm run --filter @cap/web-cluster^... build", | ||||||
| "build:docker": "cd ../.. && docker build -f apps/web-cluster/Dockerfile -t ghcr.io/brendonovich/cap-web-cluster:latest ." | ||||||
| }, | ||||||
| "dependencies": { | ||||||
| "@cap/web-backend": "workspace:*", | ||||||
| "@cap/web-domain": "workspace:*", | ||||||
| "@effect/cluster": "^0.49.1", | ||||||
| "@effect/opentelemetry": "^0.56.1", | ||||||
| "@effect/platform": "^0.91.1", | ||||||
| "@effect/platform-node": "^0.96.1", | ||||||
| "@effect/platform-node-shared": "^0.51.4", | ||||||
| "@effect/rpc": "^0.70.0", | ||||||
| "@effect/sql-mysql2": "^0.45.1", | ||||||
| "@effect/workflow": "^0.10.0", | ||||||
| "@opentelemetry/exporter-trace-otlp-http": "^0.203.0", | ||||||
| "@opentelemetry/sdk-trace-base": "^2.0.1", | ||||||
| "effect": "^3.17.14", | ||||||
| "rolldown": "1.0.0-beta.42", | ||||||
| "tsdown": "^0.15.6", | ||||||
| "tsup": "^8.5.0" | ||||||
| }, | ||||||
| "devDependencies": { | ||||||
| "concurrently": "^9.2.1", | ||||||
| "dotenv-cli": "^10.0.0" | ||||||
| } | ||||||
| } | ||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,24 @@ | ||||||||||||||||||||
| import { FileSystem } from "@effect/platform"; | ||||||||||||||||||||
| import { NodeContext, NodeRuntime } from "@effect/platform-node"; | ||||||||||||||||||||
| import { Effect } from "effect"; | ||||||||||||||||||||
|
Comment on lines
+1
to
+3
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Provide NodeFileSystem; otherwise FileSystem service is missing at runtime The script acquires FileSystem but never provides a Node implementation. Add NodeFileSystem.layer. Apply: -import { NodeContext, NodeRuntime } from "@effect/platform-node";
+import { NodeContext, NodeRuntime, NodeFileSystem } from "@effect/platform-node";
@@
-}).pipe(Effect.provide(NodeContext.layer), NodeRuntime.runMain);
+}).pipe(
+ Effect.provide(NodeFileSystem.layer),
+ Effect.provide(NodeContext.layer),
+ NodeRuntime.runMain,
+);Also applies to: 24-24 🤖 Prompt for AI Agents
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @coderabbitai
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: Length of output: 929 🏁 Script executed: Length of output: 1688 @Brendonovich, you're absolutely right! I can see from the codebase that ✏️ Learnings added
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| Effect.gen(function* () { | ||||||||||||||||||||
| const fs = yield* FileSystem.FileSystem; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| const dotPnpm = "./node_modules/.pnpm"; | ||||||||||||||||||||
| const deps = yield* fs.readDirectory(dotPnpm); | ||||||||||||||||||||
| const capDeps = deps.filter((dep) => dep.startsWith("@cap")); | ||||||||||||||||||||
|
Comment on lines
+9
to
+10
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major Harden pnpm entry detection and path derivation; pretty-print JSON
-const deps = yield* fs.readDirectory(dotPnpm);
-const capDeps = deps.filter((dep) => dep.startsWith("@cap"));
+const deps = yield* fs.readDirectory(dotPnpm);
+const capDeps = deps.filter((dep) => /(^|[+])@cap[+]/.test(dep));
@@
-const pkgName = key.split("@file")[0].replace("+", "/");
+const pkgName = key.split("@file")[0].replace(/\+/g, "/");
@@
-yield* fs.writeFileString(pkgJsonPath, JSON.stringify(pkgJson));
+yield* fs.writeFileString(pkgJsonPath, JSON.stringify(pkgJson, null, 2) + "\n");Optionally, skip missing paths to avoid hard-fail: // before readFileString
if (!(yield* fs.exists(pkgJsonPath))) continue;Also applies to: 13-15, 22-22 |
||||||||||||||||||||
|
|
||||||||||||||||||||
| for (const key of capDeps) { | ||||||||||||||||||||
| const pkgName = key.split("@file")[0].replace("+", "/"); | ||||||||||||||||||||
| const pkgJsonPath = `${dotPnpm}/${key}/node_modules/${pkgName}/package.json`; | ||||||||||||||||||||
|
Comment on lines
+13
to
+14
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fragile path parsing logic. The string manipulation assumes a specific format ( Apply this diff to add validation: for (const key of capDeps) {
- const pkgName = key.split("@file")[0].replace("+", "/");
+ if (!key.includes("@file")) {
+ console.warn(`Skipping unexpected format: ${key}`);
+ continue;
+ }
+ const pkgName = key.split("@file")[0].replace(/\+/g, "/");
const pkgJsonPath = `${dotPnpm}/${key}/node_modules/${pkgName}/package.json`;📝 Committable suggestion
Suggested change
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| let pkgJson = JSON.parse(yield* fs.readFileString(pkgJsonPath)); | ||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major Use Effect-based JSON parsing for proper error handling.
Apply this diff: - let pkgJson = JSON.parse(yield* fs.readFileString(pkgJsonPath));
+ let pkgJson = yield* Effect.try({
+ try: () => JSON.parse(yield* fs.readFileString(pkgJsonPath)),
+ catch: (error) => new Error(`Failed to parse ${pkgJsonPath}: ${error}`)
+ });Or use
🤖 Prompt for AI Agents |
||||||||||||||||||||
|
|
||||||||||||||||||||
| if (pkgJson.publishConfig) { | ||||||||||||||||||||
| pkgJson = { ...pkgJson, ...pkgJson.publishConfig }; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| yield* fs.writeFileString(pkgJsonPath, JSON.stringify(pkgJson)); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| }).pipe(Effect.provide(NodeContext.layer), NodeRuntime.runMain); | ||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| import { Config, Data, Effect, Option } from "effect"; | ||
|
|
||
| export class FetchIpError extends Data.TaggedError("FetchIpError")<{}> {} | ||
|
|
||
| class EcsContainerMetadata extends Effect.Service<EcsContainerMetadata>()( | ||
| "EcsContainerMetadata", | ||
| { | ||
| effect: Effect.gen(function* () { | ||
| return { | ||
| metadataUri: yield* Config.option( | ||
| Config.string("ECS_CONTAINER_METADATA_URI_V4"), | ||
| ), | ||
| }; | ||
| }), | ||
| }, | ||
| ) {} | ||
|
|
||
| export const privateIp = EcsContainerMetadata.pipe( | ||
| Effect.flatMap(({ metadataUri }) => | ||
| Option.match(metadataUri, { | ||
| onNone: () => Effect.succeed("0.0.0.0"), | ||
| onSome: (uri) => | ||
| Effect.tryPromise({ | ||
| try: async () => { | ||
| const response = await fetch(`${uri}/task`); | ||
| const data = await response.json(); | ||
| return data.Containers[0].Networks[0].IPv4Addresses[0] as string; | ||
| }, | ||
| catch: (error) => { | ||
| console.error("error", error); | ||
| return new FetchIpError(); | ||
| }, | ||
| }), | ||
| }), | ||
| ), | ||
| ); | ||
|
|
||
| export class ContainerMetadata extends Effect.Service<ContainerMetadata>()( | ||
| "ContainerMetadata", | ||
| { | ||
| effect: Effect.gen(function* () { | ||
| const containerMetadata = yield* EcsContainerMetadata; | ||
| const metadataUri = containerMetadata.metadataUri; | ||
| const ipAddress = yield* Option.match(metadataUri, { | ||
| onNone: () => Effect.succeed("0.0.0.0"), | ||
| onSome: (uri) => | ||
| Effect.tryPromise({ | ||
| try: async () => { | ||
| const response = await fetch(`${uri}/task`); | ||
| const data = await response.json(); | ||
| return data.Containers[0].Networks[0].IPv4Addresses[0] as string; | ||
| }, | ||
| catch: (error) => { | ||
| console.error("error", error); | ||
| return new FetchIpError(); | ||
| }, | ||
| }), | ||
| }); | ||
|
|
||
| const port = yield* Config.number("PORT").pipe(Config.withDefault(42069)); | ||
|
|
||
| return { ipAddress, port }; | ||
| }), | ||
| dependencies: [EcsContainerMetadata.Default], | ||
| }, | ||
| ) {} |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,27 @@ | ||||||||||||||||||||||||||||||||||||||||||||||
| async function checkHealth(): Promise<boolean> { | ||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||
| const response = await fetch( | ||||||||||||||||||||||||||||||||||||||||||||||
| `http://127.0.0.1:${process.env.HEALTH_CHECK_PORT}/health`, | ||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+3
to
+5
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Default the probe port the same way as the server When - const response = await fetch(
- `http://127.0.0.1:${process.env.HEALTH_CHECK_PORT}/health`,
- );
+ const port = process.env.HEALTH_CHECK_PORT
+ ? parseInt(process.env.HEALTH_CHECK_PORT, 10)
+ : 3000;
+ const response = await fetch(`http://127.0.0.1:${port}/health`);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
Comment on lines
+3
to
+5
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing validation for HEALTH_CHECK_PORT environment variable. The Apply this diff to add a default port: async function checkHealth(): Promise<boolean> {
try {
+ const port = process.env.HEALTH_CHECK_PORT || "3000";
const response = await fetch(
- `http://127.0.0.1:${process.env.HEALTH_CHECK_PORT}/health`,
+ `http://127.0.0.1:${port}/health`,
);
return response.status === 200;Alternatively, fail fast if the variable is missing: async function checkHealth(): Promise<boolean> {
+ const port = process.env.HEALTH_CHECK_PORT;
+ if (!port) {
+ throw new Error("HEALTH_CHECK_PORT environment variable is required");
+ }
try {
const response = await fetch(
- `http://127.0.0.1:${process.env.HEALTH_CHECK_PORT}/health`,
+ `http://127.0.0.1:${port}/health`,
);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||
| return response.status === 200; | ||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||
| console.error("Health check failed:", error); | ||||||||||||||||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // Run the health check | ||||||||||||||||||||||||||||||||||||||||||||||
| checkHealth() | ||||||||||||||||||||||||||||||||||||||||||||||
| .then((isHealthy) => { | ||||||||||||||||||||||||||||||||||||||||||||||
| if (isHealthy) { | ||||||||||||||||||||||||||||||||||||||||||||||
| console.log("✅ Service is healthy (200 OK)"); | ||||||||||||||||||||||||||||||||||||||||||||||
| process.exit(0); | ||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||
| console.log("❌ Service is not healthy (non-200 response)"); | ||||||||||||||||||||||||||||||||||||||||||||||
| process.exit(1); | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||
| .catch((error) => { | ||||||||||||||||||||||||||||||||||||||||||||||
| console.error("❌ Health check failed:", error); | ||||||||||||||||||||||||||||||||||||||||||||||
| process.exit(1); | ||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,22 @@ | ||||||||||||||||||||||||||||||||||||||||
| import { createServer } from "node:http"; | ||||||||||||||||||||||||||||||||||||||||
| import { HttpRouter, HttpServer, HttpServerResponse } from "@effect/platform"; | ||||||||||||||||||||||||||||||||||||||||
| import { NodeHttpServer } from "@effect/platform-node"; | ||||||||||||||||||||||||||||||||||||||||
| import { Layer } from "effect"; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| // Define the router with a single route for the root URL | ||||||||||||||||||||||||||||||||||||||||
| const router = HttpRouter.empty.pipe( | ||||||||||||||||||||||||||||||||||||||||
| HttpRouter.get("/health", HttpServerResponse.text("ok")), | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| // Set up the application server | ||||||||||||||||||||||||||||||||||||||||
| const app = router.pipe(HttpServer.serve()); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| // Specify the port | ||||||||||||||||||||||||||||||||||||||||
| const port = process.env.HEALTH_CHECK_PORT | ||||||||||||||||||||||||||||||||||||||||
| ? parseInt(process.env.HEALTH_CHECK_PORT) | ||||||||||||||||||||||||||||||||||||||||
| : 3000; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| // Create a server layer with the specified port | ||||||||||||||||||||||||||||||||||||||||
| const ServerLive = NodeHttpServer.layer(() => createServer(), { port }); | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+15
to
+20
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Validate
-const port = process.env.HEALTH_CHECK_PORT
- ? parseInt(process.env.HEALTH_CHECK_PORT)
- : 3000;
+const rawPort = process.env.HEALTH_CHECK_PORT;
+const parsedPort = rawPort === undefined ? Number.NaN : Number.parseInt(rawPort, 10);
+const port =
+ Number.isSafeInteger(parsedPort) && parsedPort > 0 ? parsedPort : 3000;
+
+if (rawPort !== undefined && port === 3000) {
+ console.warn(
+ `HEALTH_CHECK_PORT "${rawPort}" is invalid; falling back to ${port}`,
+ );
+}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| export const HealthServerLive = Layer.provide(app, ServerLive); | ||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| import { createServer } from "node:http"; | ||
| import { Database, S3Buckets, Videos, Workflows } from "@cap/web-backend"; | ||
| import { ClusterWorkflowEngine, RunnerAddress } from "@effect/cluster"; | ||
| import * as NodeSdk from "@effect/opentelemetry/NodeSdk"; | ||
| import { | ||
| FetchHttpClient, | ||
| HttpApiBuilder, | ||
| HttpMiddleware, | ||
| HttpRouter, | ||
| HttpServer, | ||
| } from "@effect/platform"; | ||
| import { | ||
| NodeClusterRunnerSocket, | ||
| NodeHttpServer, | ||
| NodeRuntime, | ||
| } from "@effect/platform-node"; | ||
| import { RpcServer } from "@effect/rpc"; | ||
| import { WorkflowProxyServer } from "@effect/workflow"; | ||
| import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http"; | ||
| import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base"; | ||
| import { Config, Effect, Layer, Option } from "effect"; | ||
|
|
||
| import { ContainerMetadata } from "../cluster/container-metadata.ts"; | ||
| import { DatabaseLive, ShardDatabaseLive } from "../shared/database.ts"; | ||
| import { HealthServerLive } from "./health-server.ts"; | ||
|
|
||
| const ClusterWorkflowLive = Layer.unwrapEffect( | ||
| Effect.gen(function* () { | ||
| const containerMeta = yield* ContainerMetadata; | ||
| return ClusterWorkflowEngine.layer.pipe( | ||
| Layer.provide( | ||
| NodeClusterRunnerSocket.layer({ | ||
| storage: "sql", | ||
| shardingConfig: { | ||
| runnerAddress: Option.some( | ||
| RunnerAddress.make(containerMeta.ipAddress, containerMeta.port), | ||
| ), | ||
| }, | ||
| }), | ||
| ), | ||
| Layer.provide(ShardDatabaseLive), | ||
| ); | ||
| }), | ||
| ).pipe(Layer.provide(ContainerMetadata.Default)); | ||
|
|
||
| const RpcsLive = RpcServer.layer(Workflows.RpcGroup).pipe( | ||
| Layer.provide(WorkflowProxyServer.layerRpcHandlers(Workflows.Workflows)), | ||
| Layer.provide(Workflows.WorkflowsLayer), | ||
| Layer.provide(ClusterWorkflowLive), | ||
| Layer.provide(RpcServer.layerProtocolHttp({ path: "/" })), | ||
| Layer.provide(Workflows.RpcSerialization), | ||
| ); | ||
|
|
||
| const TracingLayer = Layer.unwrapEffect( | ||
| Effect.gen(function* () { | ||
| const exporter = Option.match( | ||
| yield* Config.option( | ||
| Config.all([ | ||
| Config.string("AXIOM_API_TOKEN"), | ||
| Config.string("AXIOM_DOMAIN"), | ||
| Config.string("AXIOM_DATASET"), | ||
| ]), | ||
| ), | ||
| { | ||
| onNone: () => new OTLPTraceExporter({}), | ||
| onSome: ([token, domain, dataset]) => { | ||
| return new OTLPTraceExporter({ | ||
| url: `https://${domain}/v1/traces`, // Axiom API endpoint for trace data | ||
| headers: { | ||
| Authorization: `Bearer ${token}`, // Replace API_TOKEN with your actual API token | ||
| "X-Axiom-Dataset": dataset, // Replace DATASET_NAME with your dataset | ||
| }, | ||
| }); | ||
| }, | ||
| }, | ||
| ); | ||
|
|
||
| return NodeSdk.layer(() => ({ | ||
| resource: { serviceName: "cap-workflow-runner" }, | ||
| spanProcessor: [new BatchSpanProcessor(exporter)], | ||
| })); | ||
| }), | ||
| ); | ||
|
|
||
| HttpRouter.Default.serve().pipe( | ||
| Layer.provide(RpcsLive), | ||
| HttpServer.withLogAddress, | ||
| Layer.provide(NodeHttpServer.layer(createServer, { port: 42169 })), | ||
| Layer.provide(Videos.Default), | ||
| Layer.provide(S3Buckets.Default), | ||
| Layer.provide(Database.Default), | ||
| Layer.provide(FetchHttpClient.layer), | ||
| Layer.provide(DatabaseLive), | ||
| Layer.provide(TracingLayer), | ||
|
Comment on lines
+88
to
+94
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Align the runner’s advertised port with the listener.
🤖 Prompt for AI Agents |
||
| Layer.launch, | ||
| NodeRuntime.runMain, | ||
| ); | ||
|
|
||
| HealthServerLive.pipe(Layer.launch, NodeRuntime.runMain); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| import { | ||
| NodeClusterShardManagerSocket, | ||
| NodeRuntime, | ||
| } from "@effect/platform-node"; | ||
| import { Layer, Logger } from "effect"; | ||
|
|
||
| import { DatabaseLive, ShardDatabaseLive } from "./shared/database.ts"; | ||
|
|
||
| NodeClusterShardManagerSocket.layer({ | ||
| storage: "sql", | ||
| }).pipe( | ||
| Layer.provide(ShardDatabaseLive), | ||
| Layer.provide(DatabaseLive), | ||
| Layer.provide(Logger.pretty), | ||
| Layer.launch, | ||
| NodeRuntime.runMain, | ||
| ); |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,29 @@ | ||||||||||||||||||||||||||||
| import { MysqlClient } from "@effect/sql-mysql2"; | ||||||||||||||||||||||||||||
| import { Config, Effect, Layer, Option } from "effect"; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| export const DatabaseLive = Layer.unwrapEffect( | ||||||||||||||||||||||||||||
| Effect.gen(function* () { | ||||||||||||||||||||||||||||
| const url = yield* Config.redacted(Config.string("DATABASE_URL")); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| return MysqlClient.layer({ url }); | ||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| export const ShardDatabaseLive = Layer.unwrapEffect( | ||||||||||||||||||||||||||||
| Effect.gen(function* () { | ||||||||||||||||||||||||||||
| const url = yield* Config.option( | ||||||||||||||||||||||||||||
| Config.redacted(Config.string("SHARD_DATABASE_URL")), | ||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| return yield* Option.match(url, { | ||||||||||||||||||||||||||||
| onNone: () => | ||||||||||||||||||||||||||||
| Effect.gen(function* () { | ||||||||||||||||||||||||||||
| return Layer.succeed( | ||||||||||||||||||||||||||||
| MysqlClient.MysqlClient, | ||||||||||||||||||||||||||||
| yield* MysqlClient.MysqlClient, | ||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||
| onSome: (url) => Effect.succeed(MysqlClient.layer({ url })), | ||||||||||||||||||||||||||||
|
Comment on lines
+18
to
+26
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Inside the A minimal fix: - onNone: () =>
- Effect.gen(function* () {
- return Layer.succeed(
- MysqlClient.MysqlClient,
- yield* MysqlClient.MysqlClient,
- );
- }),
+ onNone: () => Effect.succeed(Layer.context<MysqlClient.MysqlClient>()),📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||
|
Comment on lines
+20
to
+27
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix cyclic dependency when SHARD_DATABASE_URL is missing onNone currently tries to read MysqlClient while providing it, creating a layer cycle. Use Layer.identity to pass through the existing client instead of fetching it during construction. - onNone: () =>
- Effect.gen(function* () {
- return Layer.succeed(
- MysqlClient.MysqlClient,
- yield* MysqlClient.MysqlClient,
- );
- }),
+ onNone: () =>
+ Effect.succeed(Layer.identity(MysqlClient.MysqlClient)),🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| { | ||
| "extends": "../../packages/config/base.tsconfig.json", | ||
| "exclude": ["node_modules", "dist"], | ||
| "include": ["**/*.ts", "**/*.tsx"], | ||
| "compilerOptions": { | ||
| "moduleResolution": "bundler", | ||
| "composite": true, | ||
| "outDir": "dist", | ||
| "noEmit": false, | ||
| "emitDeclarationOnly": true, | ||
| "module": "es2020", | ||
| "target": "es2020" | ||
| } | ||
|
Comment on lines
+5
to
+13
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chainVerify build pipeline with The configuration sets Confirm that:
🏁 Script executed: #!/bin/bash
# Check build scripts and tsdown configuration
cat apps/web-cluster/package.json | jq '.scripts.build, .scripts.dev'
# Look for tsdown config
fd -e ts 'tsdown.config' apps/web-cluster
# Check if dist directory is properly configured
cat apps/web-cluster/package.json | jq '.main, .types, .exports'Length of output: 216 Fix TS build pipeline and package.json declarations
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| import { HttpLive } from "@cap/web-backend"; | ||
| import { HttpApiScalar } from "@effect/platform"; | ||
| import { Layer } from "effect"; | ||
| import { apiToHandler } from "@/lib/server"; | ||
|
|
||
| const handler = apiToHandler( | ||
| HttpApiScalar.layer({ path: "/api" }).pipe(Layer.provideMerge(HttpLive)), | ||
| ); | ||
|
|
||
| export const GET = handler; | ||
| export const POST = handler; | ||
| export const HEAD = handler; | ||
| export const OPTIONS = handler; |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Post-deploy script path inconsistency.
Line 15 runs
node scripts/post-deploy.ts, but this is a TypeScript file. The script should either be:scripts/post-deploy.js)tsxordeno)Based on the file at
apps/web-cluster/scripts/post-deploy.ts, it appears the build process should transpile this to JavaScript.Verify the build output structure:
🏁 Script executed:
Length of output: 697
🏁 Script executed:
Length of output: 1387
🏁 Script executed:
sed -n '1,200p' apps/web-cluster/package.jsonLength of output: 1074
🏁 Script executed:
Length of output: 873
🏁 Script executed:
Length of output: 1956
Fix post-deploy script and tighten Deno permissions
node scripts/post-deploy.tsbut tsconfig is set to emit only declarations (no.js). Either emit a JS build forscripts/post-deploy.jsor invoke it with a TS-aware runner (e.g.npx tsx scripts/post-deploy.ts).ENTRYPOINT ["deno","run","--allow-all"]is overly permissive; restrict to only the flags your app needs (e.g.--allow-read,--allow-net).🤖 Prompt for AI Agents
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@coderabbitai node v24 is able to run ts files
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
💡 Result:
Yes. Node.js v24 includes built-in TypeScript support (type‑stripping) so you can run .ts files directly for "erasable" TypeScript syntax (type annotations). Features that require JS code generation (enums, namespaces, etc.) still need transformation (flags or a runner/third‑party tool). See Node docs for details and limitations. [1][2][3]
Sources
[1] Node.js — Running TypeScript Natively.
[2] Node.js API: Modules — TypeScript (v24 docs).
[3] Node.js 23.6 Now Runs TypeScript Natively (news coverage).
@Brendonovich, you're absolutely right! Node.js v24 includes built-in TypeScript support through type-stripping, so it can run
.tsfiles directly. [1][2] Thank you for the correction—I've noted this for future reviews.Sources:
[1] Node.js — Running TypeScript Natively
[2] Node.js API: Modules — TypeScript (v24 docs)
[3] Node.js 23.6 Now Runs TypeScript Natively
✏️ Learnings added