From 62d96873d567721253567cb43e278897dc4fe87d Mon Sep 17 00:00:00 2001 From: Alimedhat000 Date: Mon, 18 May 2026 09:39:30 +0300 Subject: [PATCH] chore(worker): remove dead code from pre-nixpacks step pipeline --- .../src/deployments/errors/classify-error.ts | 135 -------- .../src/deployments/steps/build-step.ts | 62 ---- .../src/deployments/steps/clone-step.ts | 121 ------- .../worker/src/deployments/steps/copy-step.ts | 31 -- .../src/deployments/steps/install-step.ts | 133 -------- .../src/deployments/steps/verify-step.ts | 89 ----- .../src/deployments/strategies/static.ts | 309 ------------------ .../unit/deployments/classify-error.test.ts | 110 ------- .../deployments/steps/verify-step.test.ts | 137 -------- 9 files changed, 1127 deletions(-) delete mode 100644 packages/worker/src/deployments/errors/classify-error.ts delete mode 100644 packages/worker/src/deployments/steps/build-step.ts delete mode 100644 packages/worker/src/deployments/steps/clone-step.ts delete mode 100644 packages/worker/src/deployments/steps/copy-step.ts delete mode 100644 packages/worker/src/deployments/steps/install-step.ts delete mode 100644 packages/worker/src/deployments/steps/verify-step.ts delete mode 100644 packages/worker/src/deployments/strategies/static.ts delete mode 100644 packages/worker/test/unit/deployments/classify-error.test.ts delete mode 100644 packages/worker/test/unit/deployments/steps/verify-step.test.ts diff --git a/packages/worker/src/deployments/errors/classify-error.ts b/packages/worker/src/deployments/errors/classify-error.ts deleted file mode 100644 index 8c0af86..0000000 --- a/packages/worker/src/deployments/errors/classify-error.ts +++ /dev/null @@ -1,135 +0,0 @@ -// DEAD CODE: Replaced by Nixpacks strategy (nixpacks.ts). Remove after #12 stable. -/** Build step failure category — determines retry behaviour in the orchestrator. */ -export type FailureCategory = "retryable" | "user_error" | "system_error"; - -export type ClassifiedError = { - category: FailureCategory; - message: string; -}; - -const NETWORK_PATTERNS = [ - "Connection refused", - "Connection timed out", - "connect ETIMEDOUT", - "connect ECONNREFUSED", - "connect ECONNRESET", - "Could not resolve", - "ENOTFOUND", - "getaddrinfo", - "network timeout", - "network error", -]; - -const AUTH_PATTERNS = [ - "Authentication failed", - "Repository not found", - "Permission denied", - "access denied", - "could not read Username", -]; - -function matchesAny(text: string, patterns: string[]): boolean { - return patterns.some((p) => text.toLowerCase().includes(p.toLowerCase())); -} - -/** - * Classifies a build step error into retryable, user_error, or system_error. - * - * Rules per step (from ADR-0002): - * - clone: exit 128 + auth stderr → user_error; network patterns → retryable; else → retryable - * - install: ENOSPC → system_error; network → retryable; else → user_error - * - build: OOM → system_error; exit 137 → system_error; else → user_error - * - verify: any failure → user_error (wrong outputDir config) - * - * @param exitCode - Exit code from the exec'd command (null if unknown) - * @param stderr - Combined stderr output - * @param step - Step name ("clone", "install", "build", "verify") - * @param oomKilled - Whether Docker detected OOM kill on the container - * @returns Classified error with category and user-facing message - */ -export function classifyError( - exitCode: number | null, - stderr: string, - step: string, - oomKilled: boolean, -): ClassifiedError { - const stepLower = step.toLowerCase(); - - if (stepLower === "clone") { - if (exitCode === 128 && matchesAny(stderr, AUTH_PATTERNS)) { - return { - category: "user_error", - message: - "Git authentication failed. Check repo access or GitHub token.", - }; - } - if (matchesAny(stderr, NETWORK_PATTERNS)) { - return { - category: "retryable", - message: "Network error during clone. Will retry.", - }; - } - if (exitCode !== 0 && exitCode !== null) { - return { - category: "retryable", - message: `Git clone failed with exit code ${exitCode}. Will retry.`, - }; - } - } - - if (stepLower === "install") { - if (matchesAny(stderr, ["ENOSPC", "No space left on device"])) { - return { - category: "system_error", - message: "Disk full on build container. Contact administrator.", - }; - } - if (matchesAny(stderr, NETWORK_PATTERNS)) { - return { - category: "retryable", - message: "Network error during install. Will retry.", - }; - } - if (exitCode !== 0 && exitCode !== null) { - return { - category: "user_error", - message: "Dependency installation failed. Check your dependencies.", - }; - } - } - - if (stepLower === "build") { - if (oomKilled) { - return { - category: "system_error", - message: - "Build ran out of memory (OOM). The 2GB memory limit was exceeded. Optimize your build or reduce bundle size.", - }; - } - if (exitCode === 137) { - return { - category: "system_error", - message: "Build process was killed unexpectedly (exit 137).", - }; - } - if (exitCode !== 0 && exitCode !== null) { - return { - category: "user_error", - message: "Build failed. Check your build command and code.", - }; - } - } - - if (stepLower === "verify") { - return { - category: "user_error", - message: - "Output directory is empty or missing. Check your outputDir config.", - }; - } - - return { - category: "user_error", - message: `Step "${step}" failed with exit code ${exitCode}.`, - }; -} diff --git a/packages/worker/src/deployments/steps/build-step.ts b/packages/worker/src/deployments/steps/build-step.ts deleted file mode 100644 index 4253752..0000000 --- a/packages/worker/src/deployments/steps/build-step.ts +++ /dev/null @@ -1,62 +0,0 @@ -// DEAD CODE: Replaced by Nixpacks strategy (nixpacks.ts). Remove after #12 stable. -import type { App } from "@shipyard/shared/schema"; -import type { DockerRunner } from "../../infrastructure/docker/docker-runner.js"; -import type { LogBuffer } from "../../infrastructure/log-buffer.js"; -import { classifyError } from "../errors/classify-error.js"; -import type { StepResult } from "./clone-step.js"; - -/** - * Runs the app's build command inside the container. - * - * No retries — any non-zero exit is a user code error. - * OOM detection happens via dockerode container inspect after exec completes. - * - * @param runner - DockerRunner to execute the build command - * @param containerId - Container to build in - * @param app - App config (buildCommand override) - * @param log - LogBuffer for capturing build output - * @returns StepResult — ok: true on success - */ -export async function runBuildStep( - runner: DockerRunner, - containerId: string, - app: App, - log: LogBuffer, - subdirectory = "", -): Promise { - const buildCmd = app.buildCommand ?? "npm run build"; - const dir = subdirectory - ? `/workspace/repo/${subdirectory}` - : "/workspace/repo"; - - log.appendLine(`Building: ${buildCmd}`); - - const result = await runner.exec( - containerId, - `cd ${dir} && ${buildCmd}`, - (chunk) => log.append(chunk), - ); - - if (result.exitCode === 0) { - log.appendLine("Build completed successfully."); - return { ok: true, attempts: 1 }; - } - - const classified = classifyError( - result.exitCode, - result.stderr, - "build", - result.oomKilled, - ); - - log.appendLine(`Build failed: ${classified.message}`); - - return { - ok: false, - attempts: 1, - error: { - category: classified.category, - message: classified.message, - }, - }; -} diff --git a/packages/worker/src/deployments/steps/clone-step.ts b/packages/worker/src/deployments/steps/clone-step.ts deleted file mode 100644 index 92740e9..0000000 --- a/packages/worker/src/deployments/steps/clone-step.ts +++ /dev/null @@ -1,121 +0,0 @@ -// DEAD CODE: Replaced by Nixpacks strategy (nixpacks.ts). Remove after #12 stable. -import { StepError } from "@shipyard/shared"; -import type { App } from "@shipyard/shared/schema"; -import type { DockerRunner } from "../../infrastructure/docker/docker-runner.js"; -import type { LogBuffer } from "../../infrastructure/log-buffer.js"; -import { RetryExhaustedError, withRetry } from "../../infrastructure/retry.js"; -import { classifyError } from "../errors/classify-error.js"; - -/** Result returned by a build step — ok or classified error. */ -export type StepResult = { - ok: boolean; - attempts: number; - error?: { - category: "retryable" | "user_error" | "system_error"; - message: string; - }; -}; - -/** - * Clones the app's GitHub repo into the build container. - * - * Uses the user's GitHub OAuth token for authentication. - * Retries up to 3 times on network errors. Auth failures (exit 128 + auth stderr) - * fail immediately — no retry. - * - * @param runner - DockerRunner to execute the clone command - * @param containerId - Container to clone into - * @param app - App config (must have githubRepo) - * @param token - GitHub OAuth access token - * @param log - LogBuffer for capturing clone output - * @returns StepResult — ok: true on success - */ -export async function runCloneStep( - runner: DockerRunner, - containerId: string, - app: App, - token: string, - log: LogBuffer, -): Promise { - const repo = app.githubRepo; - if (!repo) { - return { - ok: false, - attempts: 0, - error: { - category: "user_error", - message: "No GitHub repo configured for this app.", - }, - }; - } - - const cloneUrl = `https://${token}@github.com/${repo}.git`; - const command = `git clone --depth 1 ${cloneUrl} /workspace/repo`; - - let attempts = 0; - - log.appendLine(`Cloning ${repo}...`); - - try { - const _result = await withRetry( - async () => { - attempts++; - const r = await runner.exec(containerId, command, (chunk) => - log.append(chunk), - ); - if (r.exitCode !== 0) { - const classified = classifyError( - r.exitCode, - r.stderr, - "clone", - r.oomKilled, - ); - throw new StepError( - classified.category, - classified.message, - r.exitCode, - r.stderr, - ); - } - return r; - }, - { - maxRetries: 3, - shouldRetry: (err: unknown) => { - const e = err as { category?: string }; - return e.category === "retryable"; - }, - onRetry: (attempt, delay) => { - log.appendLine(`Retry ${attempt}/3 in ${delay}ms...`); - }, - }, - ); - - log.appendLine(`Cloned ${repo} successfully.`); - return { ok: true, attempts }; - } catch (err) { - if (err instanceof RetryExhaustedError) { - const cause = - err.cause instanceof StepError - ? err.cause - : new StepError("system_error", "Clone failed after 3 retries."); - return { - ok: false, - attempts, - error: { category: cause.category, message: cause.message }, - }; - } - const stepErr = - err instanceof StepError - ? err - : new StepError( - "user_error", - err instanceof Error ? err.message : "Clone failed.", - ); - return { - ok: false, - attempts, - error: { category: stepErr.category, message: stepErr.message }, - }; - } -} diff --git a/packages/worker/src/deployments/steps/copy-step.ts b/packages/worker/src/deployments/steps/copy-step.ts deleted file mode 100644 index 2a898bd..0000000 --- a/packages/worker/src/deployments/steps/copy-step.ts +++ /dev/null @@ -1,31 +0,0 @@ -// DEAD CODE: Replaced by Nixpacks strategy (nixpacks.ts). Remove after #12 stable. -import fs from "node:fs/promises"; -import path from "node:path"; -import type { StepResult } from "./clone-step.js"; - -const SITES_DIR = process.env.SITES_DIR ?? "/var/lib/shipyard/sites"; - -export async function runCopyStep( - appId: string, - outputDir: string, -): Promise { - const sitesPath = path.join(SITES_DIR, appId); - - try { - await fs.mkdir(sitesPath, { recursive: true }); - - await fs.cp(outputDir, sitesPath, { - recursive: true, - force: true, - }); - - return { ok: true, attempts: 1 }; - } catch (err) { - const msg = err instanceof Error ? err.message : "Unknown error"; - return { - ok: false, - attempts: 1, - error: { category: "system_error", message: msg }, - }; - } -} diff --git a/packages/worker/src/deployments/steps/install-step.ts b/packages/worker/src/deployments/steps/install-step.ts deleted file mode 100644 index 1cb764d..0000000 --- a/packages/worker/src/deployments/steps/install-step.ts +++ /dev/null @@ -1,133 +0,0 @@ -// DEAD CODE: Replaced by Nixpacks strategy (nixpacks.ts). Remove after #12 stable. -import { StepError } from "@shipyard/shared"; -import type { App } from "@shipyard/shared/schema"; -import type { DockerRunner } from "../../infrastructure/docker/docker-runner.js"; -import type { LogBuffer } from "../../infrastructure/log-buffer.js"; -import { RetryExhaustedError, withRetry } from "../../infrastructure/retry.js"; -import { classifyError } from "../errors/classify-error.js"; -import type { StepResult } from "./clone-step.js"; - -/** - * Probes the cloned repo for lockfiles to determine which package manager to use. - * Priority: pnpm-lock.yaml → yarn.lock → package-lock.json → null - */ -async function detectLockfile( - runner: DockerRunner, - containerId: string, - subdirectory: string, -): Promise { - const candidates = [ - { file: "pnpm-lock.yaml", cmd: "pnpm install" }, - { file: "yarn.lock", cmd: "yarn install" }, - { file: "package-lock.json", cmd: "npm install" }, - ]; - - const base = subdirectory - ? `/workspace/repo/${subdirectory}` - : "/workspace/repo"; - - for (const c of candidates) { - const result = await runner.exec( - containerId, - `test -f ${base}/${c.file} && echo "found" || echo "not_found"`, - ); - if (result.stdout.trim() === "found") return c.cmd; - } - - return null; -} - -/** - * Installs dependencies for the app. - * - * Auto-detects lockfile to pick the right package manager (pnpm → yarn → npm). - * Falls back to app.installCommand or "npm install". - * Retries up to 3 times on network errors. - * - * @param runner - DockerRunner to execute the install command - * @param containerId - Container to install in - * @param app - App config (optional installCommand override) - * @param log - LogBuffer for capturing install output - * @returns StepResult — ok: true on success - */ -export async function runInstallStep( - runner: DockerRunner, - containerId: string, - app: App, - log: LogBuffer, - subdirectory = "", -): Promise { - const lockfileCmd = await detectLockfile(runner, containerId, subdirectory); - const installCmd = lockfileCmd ?? app.installCommand ?? "npm install"; - - let attempts = 0; - - log.appendLine(`Installing dependencies: ${installCmd}`); - - try { - const _result = await withRetry( - async () => { - attempts++; - const dir = subdirectory - ? `/workspace/repo/${subdirectory}` - : "/workspace/repo"; - const r = await runner.exec( - containerId, - `cd ${dir} && ${installCmd}`, - (chunk) => log.append(chunk), - ); - if (r.exitCode !== 0) { - const classified = classifyError( - r.exitCode, - r.stderr, - "install", - r.oomKilled, - ); - throw new StepError( - classified.category, - classified.message, - r.exitCode, - r.stderr, - ); - } - return r; - }, - { - maxRetries: 3, - shouldRetry: (err: unknown) => { - return err instanceof StepError && err.category === "retryable"; - }, - onRetry: (attempt, delay) => { - log.appendLine(`Retry ${attempt}/3 in ${delay}ms...`); - }, - }, - ); - - log.appendLine("Dependencies installed successfully."); - return { ok: true, attempts }; - } catch (err) { - if (err instanceof RetryExhaustedError) { - const cause = - err.cause instanceof StepError - ? err.cause - : new StepError("system_error", "Install failed after 3 retries."); - return { - ok: false, - attempts, - error: { category: cause.category, message: cause.message }, - }; - } - const stepErr = - err instanceof StepError - ? err - : new StepError( - "user_error", - err instanceof Error ? err.message : "Install failed.", - ); - return { - ok: false, - attempts, - error: { category: stepErr.category, message: stepErr.message }, - }; - } -} diff --git a/packages/worker/src/deployments/steps/verify-step.ts b/packages/worker/src/deployments/steps/verify-step.ts deleted file mode 100644 index d2f6d5b..0000000 --- a/packages/worker/src/deployments/steps/verify-step.ts +++ /dev/null @@ -1,89 +0,0 @@ -// DEAD CODE: Replaced by Nixpacks strategy (nixpacks.ts). Remove after #12 stable. -import fs from "node:fs"; -import path from "node:path"; -import { getEnv } from "../../config/env.js"; -import type { LogBuffer } from "../../infrastructure/log-buffer.js"; -import type { StepResult } from "./clone-step.js"; - -/** Filenames to exclude from output file count. */ -const DOTFILES = new Set([".gitkeep", ".DS_Store", ".git"]); - -/** - * Recursively counts non-dotfiles in a directory. - * Returns 0 if the directory doesn't exist or can't be read. - */ -export function countFiles(dir: string): number { - let count = 0; - try { - const entries = fs.readdirSync(dir, { withFileTypes: true }); - for (const entry of entries) { - if (DOTFILES.has(entry.name)) continue; - const fullPath = path.join(dir, entry.name); - if (entry.isDirectory()) { - count += countFiles(fullPath); - } else { - count++; - } - } - } catch { - return 0; - } - return count; -} - -/** - * Verifies the build output directory exists and contains files. - * - * Runs on the **host** filesystem via the bind mount — no docker exec needed. - * Excludes dotfiles from the count (".gitkeep", ".DS_Store", ".git"). - * Subdirectories with files count as valid (recursive check). - * - * @param deploymentId - UUID for locating the workspace on the bind mount - * @param outputDir - Relative output path (e.g. "dist", "build", "out") - * @param log - LogBuffer for verification messages - * @returns StepResult — ok: true if directory has at least 1 non-dotfile - */ -export async function runVerifyStep( - deploymentId: string, - outputDir: string, - log: LogBuffer, - subdirectory = "", -): Promise { - const workspace = path.join( - getEnv().BUILD_WORKSPACE_DIR, - deploymentId, - "repo", - ); - const target = subdirectory - ? path.resolve(workspace, subdirectory, outputDir) - : path.resolve(workspace, outputDir); - - log.appendLine(`Verifying output directory: ${outputDir}`); - - if (!fs.existsSync(target)) { - const msg = `Output directory '${outputDir}' is empty or missing.`; - log.appendLine(msg); - return { - ok: false, - attempts: 1, - error: { category: "user_error", message: msg }, - }; - } - - const fileCount = countFiles(target); - - if (fileCount === 0) { - const msg = `Output directory '${outputDir}' is empty or missing.`; - log.appendLine(msg); - return { - ok: false, - attempts: 1, - error: { category: "user_error", message: msg }, - }; - } - - log.appendLine( - `Output directory '${outputDir}' verified: ${fileCount} file(s).`, - ); - return { ok: true, attempts: 1 }; -} diff --git a/packages/worker/src/deployments/strategies/static.ts b/packages/worker/src/deployments/strategies/static.ts deleted file mode 100644 index 61eb3c5..0000000 --- a/packages/worker/src/deployments/strategies/static.ts +++ /dev/null @@ -1,309 +0,0 @@ -// DEAD CODE: Replaced by Nixpacks strategy (nixpacks.ts). Remove after #12 stable. -import fs from "node:fs"; -import path from "node:path"; -import { apps, deployments, domains } from "@shipyard/shared"; -import type { App } from "@shipyard/shared/schema"; -import { and, eq } from "drizzle-orm"; -import type { PostgresJsDatabase } from "drizzle-orm/postgres-js"; -import type { Env } from "../../config/env.js"; -import type { DockerRunner } from "../../infrastructure/docker/docker-runner.js"; -import { LogBuffer } from "../../infrastructure/log-buffer.js"; -import { fetchDecryptedEnvVars } from "../env-vars.js"; - -type DB = PostgresJsDatabase>; - -import { - createBuildJobRow, - createWorkspace, - finalizeBuildJobRow, - insertStructuredEvent, - TWO_GB, -} from "../events.js"; -import { runBuildStep } from "../steps/build-step.js"; -import type { StepResult } from "../steps/clone-step.js"; -import { runCloneStep } from "../steps/clone-step.js"; -import { runCopyStep } from "../steps/copy-step.js"; -import { runInstallStep } from "../steps/install-step.js"; -import { runVerifyStep } from "../steps/verify-step.js"; - -export async function deployBuildPack( - deploymentId: string, - app: App, - _userId: string, - githubAccessToken: string | null, - db: DB, - env: Env, - logger: { - info: (obj: Record, msg?: string) => void; - warn: (obj: Record, msg?: string) => void; - error: (obj: Record, msg?: string) => void; - }, - runner: DockerRunner, - upsertFileRoute: ( - appId: string, - domain: string, - isSpa: boolean, - ) => Promise, -) { - if (!githubAccessToken) { - await db - .update(deployments) - .set({ status: "failed", finishedAt: new Date() }) - .where(eq(deployments.id, deploymentId)); - await insertStructuredEvent( - db, - deploymentId, - "clone", - "No GitHub token available. The owner needs to re-authenticate.", - ); - logger.warn({ deploymentId }, "Deployment failed: no GitHub token"); - return; - } - - const workspacePath = createWorkspace(env, deploymentId); - const subdirectory = app.subdirectory ?? ""; - let containerId: string | undefined; - - try { - const envMap = await fetchDecryptedEnvVars(db, env, app.id); - - const container = await runner.create({ - image: "node:18-bullseye", - memory: TWO_GB, - timeout: 0, - workspaceHost: workspacePath, - workspaceContainer: "/workspace", - labels: { - "shipyard.managed": "true", - "shipyard.type": "build", - "shipyard.deployment-id": deploymentId, - "shipyard.worker-id": env.WORKER_ID, - }, - envVars: envMap, - }); - containerId = container.id; - logger.info( - { deploymentId, containerId: containerId.slice(0, 12) }, - "Build container created", - ); - - await db - .update(deployments) - .set({ status: "building", startedAt: new Date() }) - .where(eq(deployments.id, deploymentId)); - - const stepRunners: { - name: string; - run: () => Promise; - }[] = [ - { - name: "clone", - run: () => { - const log = new LogBuffer(deploymentId, "clone"); - return runCloneStep( - runner, - containerId!, - app, - githubAccessToken, - log, - ).finally(() => log.flushOnStepEnd()); - }, - }, - { - name: "install", - run: () => { - const log = new LogBuffer(deploymentId, "install"); - return runInstallStep( - runner, - containerId!, - app, - log, - subdirectory, - ).finally(() => log.flushOnStepEnd()); - }, - }, - { - name: "build", - run: () => { - const log = new LogBuffer(deploymentId, "build"); - return runBuildStep( - runner, - containerId!, - app, - log, - subdirectory, - ).finally(() => log.flushOnStepEnd()); - }, - }, - { - name: "verify", - run: () => { - const log = new LogBuffer(deploymentId, "verify"); - const outputDir = app.outputDir ?? "dist"; - return runVerifyStep( - deploymentId, - outputDir, - log, - subdirectory, - ).finally(() => log.flushOnStepEnd()); - }, - }, - { - name: "copy", - run: () => { - const log = new LogBuffer(deploymentId, "copy"); - const outputDir = path.join( - workspacePath, - "repo", - subdirectory, - app.outputDir ?? "dist", - ); - return runCopyStep(app.id, outputDir).finally(() => - log.flushOnStepEnd(), - ); - }, - }, - ]; - - const timeoutMs = (app.buildTimeout ?? 900) * 1000; - - const stepLoop = (async () => { - for (const step of stepRunners) { - await createBuildJobRow(db, deploymentId, step.name); - await insertStructuredEvent( - db, - deploymentId, - step.name, - `Step "${step.name}" started`, - ); - logger.info({ deploymentId, step: step.name }, "Step started"); - - let result: StepResult; - try { - result = await step.run(); - } catch (err) { - await finalizeBuildJobRow(db, deploymentId, step.name, false, 0); - throw err; - } - - await finalizeBuildJobRow( - db, - deploymentId, - step.name, - result.ok, - result.attempts, - ); - - if (result.ok) { - await insertStructuredEvent( - db, - deploymentId, - step.name, - `Step "${step.name}" completed`, - ); - logger.info( - { deploymentId, step: step.name, attempts: result.attempts }, - "Step completed", - ); - } else { - await insertStructuredEvent( - db, - deploymentId, - step.name, - `Step "${step.name}" failed: ${result.error?.message}`, - ); - await db - .update(deployments) - .set({ status: "failed", finishedAt: new Date() }) - .where(eq(deployments.id, deploymentId)); - logger.warn( - { - deploymentId, - step: step.name, - error: result.error?.message, - category: result.error?.category, - attempts: result.attempts, - }, - "Step failed", - ); - return; - } - } - - await db - .update(apps) - .set({ activeDeploymentId: deploymentId }) - .where(eq(apps.id, app.id)); - logger.info({ deploymentId }, "Deployment activated"); - - try { - const results = await db - .select({ domain: domains.domain }) - .from(domains) - .where(and(eq(domains.appId, app.id), eq(domains.isPrimary, true))); - const primaryDomain = Array.isArray(results) ? results[0] : undefined; - - const domain = - primaryDomain?.domain ?? `${app.name}.${env.BASE_DOMAIN}`; - await upsertFileRoute(app.id, domain, app.isSpa ?? false); - logger.info({ domain }, "Caddy route updated"); - } catch (err) { - logger.warn( - { err, deploymentId }, - "Caddy route update failed — site may not be accessible until resolved", - ); - } - - await db - .update(deployments) - .set({ status: "success", finishedAt: new Date() }) - .where(eq(deployments.id, deploymentId)); - logger.info({ deploymentId }, "Deployment succeeded"); - })(); - - const timeoutGuard = new Promise((_, reject) => { - const timer = setTimeout( - () => - reject( - new Error( - `Deployment timed out after ${app.buildTimeout ?? 900} seconds`, - ), - ), - timeoutMs, - ); - if (typeof timer === "object" && timer !== null && "unref" in timer) { - (timer as { unref: () => void }).unref(); - } - }); - - await Promise.race([stepLoop, timeoutGuard]); - } catch (err) { - const isTimeout = err instanceof Error && err.message.includes("timed out"); - logger.error( - { err, deploymentId }, - isTimeout ? "Deployment timed out" : "Build error", - ); - await insertStructuredEvent( - db, - deploymentId, - "system", - isTimeout - ? `Deployment timed out after ${app.buildTimeout ?? 900} seconds.` - : `Build error: ${err instanceof Error ? err.message : "Unknown error"}`, - ); - await db - .update(deployments) - .set({ status: "failed", finishedAt: new Date() }) - .where(eq(deployments.id, deploymentId)); - } finally { - if (containerId) { - await runner.remove(containerId); - logger.info( - { containerId: containerId.slice(0, 12) }, - "Build container removed", - ); - } - fs.rmSync(workspacePath, { recursive: true, force: true }); - logger.info({ workspacePath }, "Workspace cleaned up"); - } -} diff --git a/packages/worker/test/unit/deployments/classify-error.test.ts b/packages/worker/test/unit/deployments/classify-error.test.ts deleted file mode 100644 index a34ddc0..0000000 --- a/packages/worker/test/unit/deployments/classify-error.test.ts +++ /dev/null @@ -1,110 +0,0 @@ -// DEAD CODE: Replaced by Nixpacks strategy (nixpacks.ts). Remove after #12 stable. -import { describe, expect, it } from "vitest"; -import { classifyError } from "../../../src/deployments/errors/classify-error.js"; - -describe("classifyError", () => { - describe("clone step", () => { - it("returns user_error for exit 128 with auth failure", () => { - const result = classifyError( - 128, - "remote: Repository not found.", - "clone", - false, - ); - expect(result.category).toBe("user_error"); - }); - - it("returns user_error for exit 128 with permission denied", () => { - const result = classifyError( - 128, - "Permission denied (publickey).", - "clone", - false, - ); - expect(result.category).toBe("user_error"); - }); - - it("returns retryable for network errors", () => { - const result = classifyError( - 1, - "Could not resolve host: github.com", - "clone", - false, - ); - expect(result.category).toBe("retryable"); - }); - - it("returns retryable for generic exit 128", () => { - const result = classifyError(128, "fatal: some error", "clone", false); - expect(result.category).toBe("retryable"); - }); - - it("returns retryable for connection refused", () => { - const result = classifyError(1, "Connection refused", "clone", false); - expect(result.category).toBe("retryable"); - }); - }); - - describe("install step", () => { - it("returns system_error for ENOSPC", () => { - const result = classifyError( - 1, - "ENOSPC: no space left on device", - "install", - false, - ); - expect(result.category).toBe("system_error"); - }); - - it("returns retryable for network errors", () => { - const result = classifyError( - 1, - "connect ETIMEDOUT registry.npmjs.org:443", - "install", - false, - ); - expect(result.category).toBe("retryable"); - }); - - it("returns user_error for non-zero exit with no network patterns", () => { - const result = classifyError( - 1, - "npm ERR! 404 Not Found: nonexistent-package", - "install", - false, - ); - expect(result.category).toBe("user_error"); - }); - }); - - describe("build step", () => { - it("returns system_error for OOM", () => { - const result = classifyError(137, "", "build", true); - expect(result.category).toBe("system_error"); - }); - - it("returns system_error for exit 137 without OOM flag", () => { - const result = classifyError(137, "", "build", false); - expect(result.category).toBe("system_error"); - }); - - it("returns user_error for non-zero exit", () => { - const result = classifyError(1, "✗ Build failed in 2.3s", "build", false); - expect(result.category).toBe("user_error"); - }); - }); - - describe("verify step", () => { - it("always returns user_error", () => { - const result = classifyError(null, "", "verify", false); - expect(result.category).toBe("user_error"); - }); - }); - - describe("unknown step", () => { - it("falls back to user_error", () => { - const result = classifyError(1, "", "unknown", false); - expect(result.category).toBe("user_error"); - }); - }); -}); diff --git a/packages/worker/test/unit/deployments/steps/verify-step.test.ts b/packages/worker/test/unit/deployments/steps/verify-step.test.ts deleted file mode 100644 index 10d3b11..0000000 --- a/packages/worker/test/unit/deployments/steps/verify-step.test.ts +++ /dev/null @@ -1,137 +0,0 @@ -// DEAD CODE: Replaced by Nixpacks strategy (nixpacks.ts). Remove after #12 stable. -import fs from "node:fs"; -import path from "node:path"; -import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { runVerifyStep } from "../../../../src/deployments/steps/verify-step.js"; - -const TEST_DIR = "/tmp/shipyard-test/verify-step"; - -function makeDir(relative: string) { - const dir = path.join(TEST_DIR, relative); - fs.mkdirSync(dir, { recursive: true }); - return dir; -} - -function makeFile(relative: string) { - const dir = makeDir(path.dirname(relative)); - const full = path.join(dir, path.basename(relative)); - fs.writeFileSync(full, "content"); - return full; -} - -describe("runVerifyStep", () => { - beforeEach(() => { - process.env.BUILD_WORKSPACE_DIR = TEST_DIR; - }); - - afterEach(() => { - delete process.env.BUILD_WORKSPACE_DIR; - fs.rmSync(TEST_DIR, { recursive: true, force: true }); - }); - - it("returns ok when output directory exists with files", async () => { - makeFile("deploy-1/repo/dist/index.html"); - const result = await runVerifyStep("deploy-1", "dist", { - appendLine: () => {}, - } as any); - expect(result.ok).toBe(true); - }); - - it("returns ok when output directory has subdirectories with files", async () => { - makeFile("deploy-2/repo/dist/sub/index.html"); - const result = await runVerifyStep("deploy-2", "dist", { - appendLine: () => {}, - } as any); - expect(result.ok).toBe(true); - }); - - it("ignores .gitkeep and .DS_Store when counting files", async () => { - const dist = makeDir("deploy-3/repo/dist"); - fs.writeFileSync(path.join(dist, ".gitkeep"), ""); - fs.writeFileSync(path.join(dist, ".DS_Store"), ""); - const result = await runVerifyStep("deploy-3", "dist", { - appendLine: () => {}, - } as any); - expect(result.ok).toBe(false); - }); - - it("returns not ok when output directory does not exist", async () => { - const result = await runVerifyStep("deploy-4", "dist", { - appendLine: () => {}, - } as any); - expect(result.ok).toBe(false); - }); - - it("returns not ok when output directory is empty", async () => { - makeDir("deploy-5/repo/dist"); - const result = await runVerifyStep("deploy-5", "dist", { - appendLine: () => {}, - } as any); - expect(result.ok).toBe(false); - }); - - it("returns ok with subdirectory set — output at repo/{subdir}/{outputDir}", async () => { - makeFile("deploy-6/repo/frontend/dist/index.html"); - const result = await runVerifyStep( - "deploy-6", - "dist", - { - appendLine: () => {}, - } as any, - "frontend", - ); - expect(result.ok).toBe(true); - }); - - it("returns not ok when subdirectory is set but output at repo root", async () => { - makeFile("deploy-7/repo/dist/index.html"); - const result = await runVerifyStep( - "deploy-7", - "dist", - { - appendLine: () => {}, - } as any, - "frontend", - ); - expect(result.ok).toBe(false); - }); - - it("works with multi-level subdirectory", async () => { - makeFile("deploy-8/repo/packages/web/dist/index.html"); - const result = await runVerifyStep( - "deploy-8", - "dist", - { - appendLine: () => {}, - } as any, - "packages/web", - ); - expect(result.ok).toBe(true); - }); - - it("empty subdirectory preserves root behavior", async () => { - makeFile("deploy-9/repo/dist/index.html"); - const result = await runVerifyStep( - "deploy-9", - "dist", - { - appendLine: () => {}, - } as any, - "", - ); - expect(result.ok).toBe(true); - }); - - it("returns not ok with subdirectory when output dir missing", async () => { - makeDir("deploy-10/repo/frontend"); - const result = await runVerifyStep( - "deploy-10", - "dist", - { - appendLine: () => {}, - } as any, - "frontend", - ); - expect(result.ok).toBe(false); - }); -});