From ba2f3f7529cd22e7bcce60cdcb56016baad20746 Mon Sep 17 00:00:00 2001 From: Niels Mokkenstorm Date: Sat, 7 Feb 2026 15:44:23 +0100 Subject: [PATCH] ci: add provider-agnostic CI pipeline script and production Dockerfile --- apps/server/Dockerfile | 49 +++++++++++++++++++++++++++++++++++++ ci/pipeline.ts | 55 ++++++++++++++++++++++++++++++++++++++++++ ci/tsconfig.json | 11 +++++++++ 3 files changed, 115 insertions(+) create mode 100644 ci/pipeline.ts create mode 100644 ci/tsconfig.json diff --git a/apps/server/Dockerfile b/apps/server/Dockerfile index 0990ed3..d6c9739 100644 --- a/apps/server/Dockerfile +++ b/apps/server/Dockerfile @@ -27,3 +27,52 @@ EXPOSE 3000 WORKDIR /app/apps/server CMD ["pnpm", "dev"] + +# ---- Production build ---- +FROM node:24-alpine AS builder + +RUN apk add --no-cache openssl +RUN corepack enable && corepack prepare pnpm@latest --activate + +WORKDIR /app + +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml lerna.json ./ +COPY packages/ ./packages/ +COPY apps/ ./apps/ + +RUN pnpm install --frozen-lockfile + +WORKDIR /app/apps/server +RUN pnpm exec prisma generate +WORKDIR /app + +RUN pnpm build + +# ---- Production runtime ---- +FROM node:24-alpine AS production + +RUN apk add --no-cache openssl curl +RUN corepack enable && corepack prepare pnpm@latest --activate + +WORKDIR /app + +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml lerna.json ./ +COPY packages/ ./packages/ +COPY apps/server/package.json ./apps/server/ + +RUN pnpm install --frozen-lockfile --prod + +# Copy Prisma schema + generated client +COPY apps/server/prisma/ ./apps/server/prisma/ +COPY --from=builder /app/apps/server/node_modules/.prisma/ ./apps/server/node_modules/.prisma/ +COPY --from=builder /app/node_modules/.prisma/ ./node_modules/.prisma/ + +# Copy compiled output +COPY --from=builder /app/apps/server/dist/ ./apps/server/dist/ +COPY --from=builder /app/packages/*/dist/ ./packages/ + +ENV NODE_ENV=production +EXPOSE 3000 + +WORKDIR /app/apps/server +CMD ["node", "dist/main.js"] diff --git a/ci/pipeline.ts b/ci/pipeline.ts new file mode 100644 index 0000000..cd1cd1b --- /dev/null +++ b/ci/pipeline.ts @@ -0,0 +1,55 @@ +import { execSync } from "node:child_process"; +import { resolve } from "node:path"; + +const root = resolve(import.meta.dirname, ".."); + +type Step = { + name: string; + command: string; +}; + +const steps: Step[] = [ + { name: "Install dependencies", command: "pnpm install --frozen-lockfile" }, + { name: "Generate Prisma client", command: "pnpm prisma:generate" }, + { name: "Lint", command: "pnpm lint" }, + { name: "Typecheck", command: "pnpm typecheck" }, + { name: "Build", command: "pnpm build" }, +]; + +const run = (step: Step): boolean => { + console.log(`\n==> ${step.name}`); + try { + execSync(step.command, { cwd: root, stdio: "inherit" }); + console.log(`==> ${step.name} passed`); + return true; + } catch { + console.error(`==> ${step.name} FAILED`); + return false; + } +}; + +const selectedStep = process.argv[2]; + +const stepsToRun = selectedStep + ? steps.filter((s) => s.name.toLowerCase().includes(selectedStep.toLowerCase())) + : steps; + +if (stepsToRun.length === 0) { + console.error( + `No step matching "${selectedStep}". Available: ${steps.map((s) => s.name).join(", ")}`, + ); + process.exit(1); +} + +console.log( + `Running ${stepsToRun.length} step(s): ${stepsToRun.map((s) => s.name).join(" -> ")}`, +); + +const failed = stepsToRun.find((step) => !run(step)); + +if (failed) { + console.error(`\nPipeline failed at: ${failed.name}`); + process.exit(1); +} + +console.log("\nPipeline passed"); diff --git a/ci/tsconfig.json b/ci/tsconfig.json new file mode 100644 index 0000000..18993d9 --- /dev/null +++ b/ci/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true + }, + "include": ["*.ts"] +} -- 2.51.2