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
77 changes: 77 additions & 0 deletions apps/api/src/orchestrator/__tests__/railpack-config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { describe, test, expect } from "bun:test";
import { generateDynamicRailpackJson } from "../railpack-config-utils";
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, readFileSync, existsSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";

const setupWorkspace = () => {
const dir = mkdtempSync(join(tmpdir(), "dequel-railpack-config-"));
mkdirSync(join(dir, "client"), { recursive: true });
return { dir, cleanup: () => rmSync(dir, { recursive: true, force: true }) };
};

describe("generateDynamicRailpackJson static subdirectory detection", () => {
test("configures dequel-serve.js when index.html exists without package.json", async () => {
const { dir, cleanup } = setupWorkspace();
try {
writeFileSync(join(dir, "client", "index.html"), "<html></html>");
const logs: string[] = [];
await generateDynamicRailpackJson(
dir,
"client",
"static",
null,
null,
async (line) => logs.push(line),
);
const serveScript = join(dir, "dequel-serve.js");
const railpackJson = join(dir, "railpack.json");
expect(existsSync(serveScript)).toBe(true);
expect(existsSync(railpackJson)).toBe(true);
const parsedConfig = JSON.parse(readFileSync(railpackJson, "utf8"));
expect(parsedConfig.deploy?.startCommand).toBe("bun dequel-serve.js");
} finally {
cleanup();
}
});

test("does not overwrite a user-provided Staticfile", async () => {
const { dir, cleanup } = setupWorkspace();
try {
writeFileSync(join(dir, "client", "index.html"), "<html></html>");
writeFileSync(join(dir, "Staticfile"), "root: public\n");
await generateDynamicRailpackJson(
dir,
"client",
"railpack",
null,
null,
async () => {},
);
expect(readFileSync(join(dir, "Staticfile"), "utf8")).toBe("root: public\n");
} finally {
cleanup();
}
});

test("does not emit Staticfile when package.json exists in source dir", async () => {
const { dir, cleanup } = setupWorkspace();
try {
writeFileSync(join(dir, "client", "index.html"), "<html></html>");
writeFileSync(join(dir, "client", "package.json"), JSON.stringify({ name: "app", scripts: { start: "node dist/index.js" } }));
const logs: string[] = [];
await generateDynamicRailpackJson(
dir,
"client",
"railpack",
null,
null,
async (line) => logs.push(line),
);
expect(existsSync(join(dir, "Staticfile"))).toBe(false);
expect(logs.some((l) => l.includes("Detected static site"))).toBe(false);
} finally {
cleanup();
}
});
});
7 changes: 3 additions & 4 deletions apps/api/src/orchestrator/pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,9 @@ import {
listAllDatabases,
updateDeploymentStatus,
updateDeploymentCommitSha,
listVolumes,
} from "../db/repo";
import { listEnvironmentVariablesForDeploy } from "../db/repo";
import { listVolumes } from "../db/repo";
import { filterBuildEnvVars } from "../utils/env-filter";
import { logBus } from "./log-bus";
import { DeploymentQueue } from "./queue";
import { buildWithRailpack, CancelledError } from "./railpack";
Expand Down Expand Up @@ -407,7 +406,7 @@ export class PipelineOrchestrator {
const cacheKey =
deployment.projectId ||
deploymentId;
const envVars = deployment.projectId ? await listEnvironmentVariablesForDeploy(deployment.projectId, "production") : [];
const envVars = deployment.projectId ? await listEnvironmentVariablesForDeploy(deployment.projectId, deployment.environment ?? "production") : [];
if (project?.buildType === "compose") {
const envMap: Record<string, string> = {};
for (const v of envVars) envMap[v.key] = v.value;
Expand Down Expand Up @@ -444,7 +443,7 @@ export class PipelineOrchestrator {
installCommand: project?.installCommand,
outputDir: project?.outputDir,
startCommand: project?.startCommand,
environmentVariables: filterBuildEnvVars(envVars),
environmentVariables: envVars,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
signal: controller.signal,
clearCache: deployment.clearCache
},
Expand Down
158 changes: 54 additions & 104 deletions apps/api/src/orchestrator/railpack-config-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,39 +140,21 @@ export const generateDynamicRailpackJson = async (

let configured = false;

// 1. Check if user provided manual build / start / install command overrides
if (buildCommandOverride || startCommandOverride || installCommandOverride) {
await onLog("Applying custom build/start/install settings");
if (installCommandOverride) {
config.steps.install = {
commands: [
cleanSourceDir
? `cd ${cleanSourceDir} && ${installCommandOverride}`
: installCommandOverride,
],
};
}
if (buildCommandOverride) {
config.steps.build = {
commands: [
cleanSourceDir
? `cd ${cleanSourceDir} && ${buildCommandOverride}`
: buildCommandOverride,
],
};
if (cleanSourceDir && !(await Bun.file(join(workspace, "package.json")).exists())) {
const sourcePkgExists = await Bun.file(join(workspace, cleanSourceDir, "package.json")).exists();
if (sourcePkgExists) {
await Bun.write(join(workspace, "package.json"), JSON.stringify({
name: "dequel-monorepo-root",
private: true,
workspaces: [cleanSourceDir]
}, null, 2));
}
if (startCommandOverride) {
config.deploy.startCommand = cleanSourceDir
? `cd ${cleanSourceDir} && ${startCommandOverride}`
: startCommandOverride;
}
configured = true;
}

const hasPackageJson = await Bun.file(
join(buildDir, "package.json"),
).exists();
if (hasPackageJson && !configured) {
if (hasPackageJson) {
try {
const packageJson = await Bun.file(
join(buildDir, "package.json"),
Expand Down Expand Up @@ -238,7 +220,7 @@ export const generateDynamicRailpackJson = async (
if (scripts.build) {
const buildCmds = [
cleanSourceDir
? `cd ${cleanSourceDir} && ${pm} run build`
? `[ -d "${cleanSourceDir}" ] && cd -- "${cleanSourceDir}" && ${pm} run build`
: `${pm} run build`,
];
if (hasNext) {
Expand All @@ -260,81 +242,7 @@ export const generateDynamicRailpackJson = async (
}
}

const hasServerScript = !!scripts.server;
const isStatic = projectType === "static" || (!scripts.start && !hasServerScript && (scripts.build || hasPackageJson));

if (isStatic) {
const serveScript = `
const fs = require("fs");
const path = require("path");
const PORT = Number(process.env.PORT || 3000);
const cleanSourceDir = "${cleanSourceDir}";
const cleanOutputDir = "${cleanOutputDir}";
let staticDir = ".";
const candidates = [
...(cleanOutputDir ? [path.join(cleanSourceDir, cleanOutputDir), cleanOutputDir] : []),
path.join(cleanSourceDir, "dist"),
path.join(cleanSourceDir, "build"),
path.join(cleanSourceDir, "out"),
path.join(cleanSourceDir, ".next/server/app"),
path.join(cleanSourceDir, ".next/server/pages"),
path.join(cleanSourceDir, "public"),
"dist",
"build",
"out",
".next/server/app",
".next/server/pages",
"public",
"."
];
for (const dir of candidates) {
const fullPath = path.join(process.cwd(), dir);
if (fs.existsSync(fullPath) && fs.statSync(fullPath).isDirectory()) {
if (fs.existsSync(path.join(fullPath, "index.html"))) {
staticDir = dir;
break;
}
}
}
console.log("Serving static directory:", staticDir, "on port", PORT);
Bun.serve({
port: PORT,
async fetch(req) {
const url = new URL(req.url);
let decodedPathname = "/";
try {
decodedPathname = decodeURIComponent(url.pathname);
} catch {
decodedPathname = url.pathname;
}
let filePath = path.join(staticDir, decodedPathname);
if (decodedPathname.endsWith("/")) {
filePath = path.join(filePath, "index.html");
}
let file = Bun.file(filePath);
if (await file.exists()) {
return new Response(file, {
headers: {
"content-type": file.type || "application/octet-stream"
}
});
}
const fallbackPath = path.join(staticDir, "index.html");
const fallbackFile = Bun.file(fallbackPath);
if (await fallbackFile.exists()) {
return new Response(fallbackFile, {
headers: {
"content-type": fallbackFile.type || "text/html"
}
});
}
return new Response("Not Found", { status: 404 });
}
});
`;
await Bun.write(join(workspace, "dequel-serve.js"), serveScript);
config.deploy.startCommand = "bun dequel-serve.js";
} else if (scripts.start) {
if (scripts.start) {
config.deploy.startCommand =
cleanSourceDir
? `cd ${cleanSourceDir} && ${pm} run start`
Expand All @@ -359,6 +267,8 @@ Bun.serve({
}
}



const hasCargoToml = await Bun.file(
join(buildDir, "Cargo.toml"),
).exists();
Expand Down Expand Up @@ -606,18 +516,23 @@ Bun.serve({
configured = true;
}

if (!configured && projectType === "static") {
if (projectType === "static" || (!configured && ((await Bun.file(join(buildDir, "index.html")).exists()) || (await Bun.file(join(workspace, "index.html")).exists())))) {
const serveScript = `
const fs = require("fs");
const path = require("path");
const PORT = Number(process.env.PORT || 3000);
const cleanSourceDir = "${cleanSourceDir}";
const cleanOutputDir = "${cleanOutputDir}";
let staticDir = ".";
const candidates = [
...(cleanOutputDir ? [path.join(cleanSourceDir, cleanOutputDir), cleanOutputDir] : []),
path.join(cleanSourceDir, "dist"),
path.join(cleanSourceDir, "build"),
path.join(cleanSourceDir, "out"),
path.join(cleanSourceDir, ".next/server/app"),
path.join(cleanSourceDir, ".next/server/pages"),
path.join(cleanSourceDir, "public"),
cleanSourceDir || ".",
"dist",
"build",
"out",
Expand Down Expand Up @@ -670,6 +585,17 @@ Bun.serve({
});
`;
await Bun.write(join(workspace, "dequel-serve.js"), serveScript);
const rootPkgPath = join(workspace, "package.json");
if (!(await Bun.file(rootPkgPath).exists())) {
await Bun.write(rootPkgPath, JSON.stringify({
name: "dequel-static-app",
private: true,
scripts: {
start: "bun dequel-serve.js"
}
}, null, 2));
}
config.steps.build = config.steps.build || { commands: [] };
config.deploy.startCommand = "bun dequel-serve.js";
configured = true;
}
Expand All @@ -680,6 +606,30 @@ Bun.serve({
);
}

if (buildCommandOverride || startCommandOverride || installCommandOverride) {
await onLog("Applying custom build/start/install settings");
if (installCommandOverride) {
const hasCustomInstall = installCommandOverride !== "npm install" && installCommandOverride !== "npm ci" && installCommandOverride !== "pnpm install" && installCommandOverride !== "yarn install" && installCommandOverride !== "bun install";
if (hasCustomInstall) {
config.steps.install = config.steps.install || {};
config.steps.install.commands = [installCommandOverride];
}
}
if (buildCommandOverride) {
config.steps.build = config.steps.build || {};
config.steps.build.commands = [
cleanSourceDir
? `cd ${cleanSourceDir} && ${buildCommandOverride}`
: buildCommandOverride,
];
}
if (startCommandOverride) {
config.deploy.startCommand = cleanSourceDir
? `cd ${cleanSourceDir} && ${startCommandOverride}`
: startCommandOverride;
}
}
Comment on lines +609 to +631

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Split the TypeScript files that exceed the repository size limit.

  • apps/api/src/orchestrator/railpack-config-utils.ts#L609-L631: extract static-server configuration and command-override generation into feature-specific modules.
  • apps/api/src/orchestrator/pipeline.ts#L446-L449: extract build preparation and deployment execution responsibilities into feature-specific modules.

As per coding guidelines, “No file should be above 500 lines of code; if it is, refactor and split into smaller files with proper feature grouping in a folder rather than scattered across the codebase”.

📍 Affects 2 files
  • apps/api/src/orchestrator/railpack-config-utils.ts#L609-L631 (this comment)
  • apps/api/src/orchestrator/pipeline.ts#L446-L449
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/orchestrator/railpack-config-utils.ts` around lines 609 - 631,
The files exceed the repository’s 500-line limit and need feature-based
extraction. In apps/api/src/orchestrator/railpack-config-utils.ts lines 609-631,
extract static-server configuration and command-override generation into
dedicated feature-specific modules, preserving the existing behavior around
install, build, and start overrides. In apps/api/src/orchestrator/pipeline.ts
lines 446-449, extract build preparation and deployment execution
responsibilities into appropriate feature-specific modules; update imports and
callers so both original flows remain unchanged.

Source: Coding guidelines


await Bun.write(
configPath,
JSON.stringify(config, null, 2),
Expand Down
Loading
Loading