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
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: test

test:
cd apps/api && bun test
cd apps/api && bun test --timeout 15000
7 changes: 5 additions & 2 deletions apps/api/src/api/__tests__/domains-status.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ const run = () =>
exec("bun", ["run", "src/api/__tests__/domains-status-runner.ts"], {
cwd: `${import.meta.dir}/../../..`,
timeout: 30000,
env: { ...process.env, TEST_DATABASE_URL: "postgresql://dequel:dequel@localhost:5433/dequel" },
env: {
...process.env,
TEST_DATABASE_URL: process.env.TEST_DATABASE_URL ?? "postgresql://dequel:dequel@localhost:5433/dequel",
},
});

const parse = (stdout: string) => {
Expand All @@ -18,7 +21,7 @@ const parse = (stdout: string) => {

describe("GET /projects/:id/domains/status", () => {
it("returns array and 404 for missing project", async () => {
const { stdout, stderr } = await run();
const { stdout } = await run();
const results = parse(stdout);
expect(results.test1.status).toBe(200);
expect(results.test1.isArray).toBe(true);
Expand Down
13 changes: 9 additions & 4 deletions apps/api/src/api/deployments/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { terminateWithEscalation } from "../../utils/process-exec";
import { isPrivateGitUrl } from "../../utils/validate";
import { created, fail, ok } from "../response";

const dispatchDeployment = async (
export const dispatchDeployment = async (
deployment: Awaited<ReturnType<typeof createDeployment>>,
project: Awaited<ReturnType<typeof getProjectById>>,
server: Awaited<ReturnType<typeof getServerById>>,
Expand Down Expand Up @@ -217,9 +217,9 @@ export const deploymentsRoutes = new Elysia()
set.status = 404;
return fail("Deployment not found");
}
if (original.status !== "running") {
if (original.status === "pending" || original.status === "building" || original.status === "deploying") {
set.status = 400;
return fail("Can only redeploy the currently running deployment");
return fail("Cannot redeploy an in-progress deployment");
}
if (original.sourceType === "image") {
set.status = 400;
Expand All @@ -233,11 +233,16 @@ export const deploymentsRoutes = new Elysia()
set.status = 400;
return fail("Selected deployment server does not exist");
}
let sourceRef = original.sourceRef;
if (original.sourceType === "upload" && project?.buildType === "compose") {
const { ensureValidComposeArchive } = await import("../projects/compose");
sourceRef = await ensureValidComposeArchive(project, original.sourceRef);
}
const deployment = await createDeployment({
projectId: original.projectId || undefined,
serverId: original.serverId,
sourceType: original.sourceType,
sourceRef: original.sourceRef,
sourceRef,
branch: original.branch || project?.repoBranch || undefined,
environment: original.environment || undefined,
});
Expand Down
6 changes: 5 additions & 1 deletion apps/api/src/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Elysia } from "elysia";
import { fixdiagRoutes } from "../fixdiag/routes";
import { rateLimitMiddleware } from "./rate-limit";
import { agentRoutes } from "./agents";
import { alertsRoutes } from "./alerts";
import { apiKeysRoutes } from "./api-keys";
Expand All @@ -15,8 +14,10 @@ import { envVarsRoutes } from "./env-vars";
import { githubRoutes } from "./github";
import { healthRoutes } from "./health";
import { projectsRoutes } from "./projects";
import { projectComposeRoutes } from "./projects/compose";
import { projectStatusRoutes } from "./projects/status";
import { prometheusRoutes } from "./prometheus";
import { rateLimitMiddleware } from "./rate-limit";
import { fail } from "./response";
import { routesRoutes } from "./routes";
import { scalingRoutes } from "./scaling";
Expand All @@ -26,6 +27,7 @@ import { settingsRoutes } from "./settings";
import { llmSettingsRoutes } from "./settings/llm";
import { sharedEnvLinksRoutes, sharedEnvVarsRoutes } from "./shared-env-vars";
import { sshKeysRoutes } from "./ssh-keys";
import { templateRoutes } from "./templates";
import { volumesRoutes } from "./volumes";

const INTERNAL_ERROR =
Expand Down Expand Up @@ -62,7 +64,9 @@ export const apiRoutes = new Elysia({
.use(agentRoutes)
.use(healthRoutes)
.use(projectsRoutes)
.use(projectComposeRoutes)
.use(projectStatusRoutes)
.use(templateRoutes)
.use(deploymentsRoutes)
.use(envVarsRoutes)
.use(sharedEnvVarsRoutes)
Expand Down
59 changes: 59 additions & 0 deletions apps/api/src/api/projects/__tests__/compose.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { describe, expect, it } from "bun:test";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { parse as parseYaml } from "yaml";
import { createZipStore } from "../../../utils/zip-store";

describe("project compose management", () => {
it("creates a zip store containing docker-compose.yml and unzips correctly", async () => {
const tempDir = mkdtempSync(join(process.cwd(), "scratch-compose-test-"));
try {
const composeContent = "services:\n app:\n image: nginx:alpine\n ports:\n - 80:80\n";
const zipBytes = createZipStore([{ name: "docker-compose.yml", content: composeContent }]);
const zipPath = join(tempDir, "compose.zip");
writeFileSync(zipPath, zipBytes);

const { safeSpawn } = await import("../../../utils/process-exec");
const res = await safeSpawn("unzip", ["-p", zipPath, "docker-compose.yml"]);
expect(res.code).toBe(0);
expect(res.stdout).toBe(composeContent.trim());

const parsed = parseYaml(res.stdout) as any;
expect(parsed.services.app.image).toBe("nginx:alpine");
} finally {
rmSync(tempDir, { recursive: true, force: true });
}
});

it("validates compose yaml structure", () => {
const invalidYaml = "services: [bad yaml";
expect(() => parseYaml(invalidYaml)).toThrow();

const validYaml = "services:\n web:\n image: node:20-alpine\n ports:\n - 3000:3000\n";
const parsed = parseYaml(validYaml) as any;
expect(parsed.services).toBeDefined();
expect(parsed.services.web).toBeDefined();
});

it("finds matching template for a project and recovers full compose yaml with version 3.8", async () => {
const { findTemplateForProject, getOrRecoverProjectCompose } = await import("../compose");
const wpProject = {
id: "proj-wp-test",
name: "my-wordpress",
description: "Content management system (template: wordpress)",
composeService: "wordpress",
composePort: 80,
};
const template = findTemplateForProject(wpProject);
expect(template).toBeDefined();
expect(template?.key).toBe("wordpress");

const recovered = await getOrRecoverProjectCompose(wpProject);
expect(recovered).toContain('version: "3.8"');
expect(recovered).toContain("services:");
expect(recovered).toContain("wordpress:");
expect(recovered).toContain("db:");
expect(recovered).toContain("wp-content:");
expect(recovered).toContain("wp-db:");
});
});
Loading
Loading