185 lines
5.6 KiB
TypeScript
185 lines
5.6 KiB
TypeScript
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
import { dirname, join, resolve } from "node:path";
|
|
import { generateDocker } from "./docker.ts";
|
|
|
|
export const DEPLOY_TARGETS = [
|
|
"docker",
|
|
"kubernetes",
|
|
"systemd",
|
|
"railway",
|
|
"render",
|
|
"fly",
|
|
] as const;
|
|
export type DeployTarget = (typeof DEPLOY_TARGETS)[number];
|
|
|
|
const ENVIRONMENT = `# Copy to .env.production and replace every required value.
|
|
NODE_ENV=production
|
|
PORT=3000
|
|
HOST=0.0.0.0
|
|
DATABASE_URL=postgres://USER:PASSWORD@HOST:5432/DB
|
|
SESSION_SECRET=REPLACE_WITH_AT_LEAST_32_RANDOM_BYTES
|
|
# OTEL_EXPORTER_OTLP_ENDPOINT=https://collector.example.com
|
|
`;
|
|
|
|
const OPERATIONS = `# WRNexus deployment operations
|
|
|
|
- Liveness: \`GET /healthz\`
|
|
- Readiness: \`GET /readyz\` (includes registered dependency checks)
|
|
- Migrations: run \`bunx wrnexus db migrate --profile=production\` once per release before scaling.
|
|
- Shutdown: the Bun production server drains on SIGTERM/SIGINT.
|
|
- Assets: \`dist/public\` files are content-addressed and may be cached immutably by a CDN.
|
|
- Secrets: provide \`DATABASE_URL\` and \`SESSION_SECRET\` through the platform secret store; never commit production env files.
|
|
- Logs: stdout/stderr are structured for platform collection. Configure OTLP for centralized telemetry.
|
|
- Scaling: start with 250m CPU/256Mi memory, use readiness probes, and scale horizontally from request latency and CPU.
|
|
`;
|
|
|
|
const KUBERNETES = `apiVersion: v1
|
|
kind: Service
|
|
metadata:
|
|
name: wrnexus
|
|
spec:
|
|
selector: { app: wrnexus }
|
|
ports: [{ name: http, port: 80, targetPort: 3000 }]
|
|
---
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: wrnexus
|
|
spec:
|
|
replicas: 2
|
|
selector: { matchLabels: { app: wrnexus } }
|
|
template:
|
|
metadata: { labels: { app: wrnexus } }
|
|
spec:
|
|
containers:
|
|
- name: app
|
|
image: ghcr.io/OWNER/APP:latest
|
|
ports: [{ containerPort: 3000 }]
|
|
envFrom: [{ secretRef: { name: wrnexus-secrets } }]
|
|
livenessProbe: { httpGet: { path: /healthz, port: 3000 }, initialDelaySeconds: 5 }
|
|
readinessProbe: { httpGet: { path: /readyz, port: 3000 }, initialDelaySeconds: 5 }
|
|
resources:
|
|
requests: { cpu: 250m, memory: 256Mi }
|
|
limits: { cpu: "1", memory: 512Mi }
|
|
lifecycle: { preStop: { exec: { command: ["sh", "-c", "sleep 5"] } } }
|
|
terminationGracePeriodSeconds: 30
|
|
---
|
|
apiVersion: batch/v1
|
|
kind: Job
|
|
metadata:
|
|
name: wrnexus-migrate
|
|
spec:
|
|
template:
|
|
spec:
|
|
restartPolicy: Never
|
|
containers:
|
|
- name: migrate
|
|
image: ghcr.io/OWNER/APP:latest
|
|
command: ["bunx", "wrnexus", "db", "migrate", "--profile=production"]
|
|
envFrom: [{ secretRef: { name: wrnexus-secrets } }]
|
|
`;
|
|
|
|
const SYSTEMD = `[Unit]
|
|
Description=WRNexus application
|
|
After=network-online.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
WorkingDirectory=/srv/wrnexus
|
|
EnvironmentFile=/etc/wrnexus/wrnexus.env
|
|
ExecStartPre=/usr/bin/bunx wrnexus db migrate --profile=production
|
|
ExecStart=/usr/bin/bun dist/server.js
|
|
Restart=on-failure
|
|
RestartSec=5
|
|
TimeoutStopSec=30
|
|
User=wrnexus
|
|
Group=wrnexus
|
|
NoNewPrivileges=true
|
|
PrivateTmp=true
|
|
ProtectSystem=strict
|
|
ReadWritePaths=/srv/wrnexus
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
`;
|
|
|
|
const NGINX = `server {
|
|
listen 80;
|
|
server_name example.com;
|
|
location /assets/ { root /srv/wrnexus/dist/public; expires 1y; add_header Cache-Control "public, immutable"; }
|
|
location / { proxy_pass http://127.0.0.1:3000; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Request-ID $request_id; }
|
|
}
|
|
`;
|
|
|
|
const RAILWAY = `[build]
|
|
builder = "DOCKERFILE"
|
|
|
|
[deploy]
|
|
startCommand = "bun dist/server.js"
|
|
healthcheckPath = "/readyz"
|
|
restartPolicyType = "ON_FAILURE"
|
|
preDeployCommand = ["bunx wrnexus db migrate --profile=production"]
|
|
`;
|
|
|
|
const RENDER = `services:
|
|
- type: web
|
|
name: wrnexus
|
|
runtime: docker
|
|
healthCheckPath: /readyz
|
|
preDeployCommand: bunx wrnexus db migrate --profile=production
|
|
envVars:
|
|
- key: DATABASE_URL
|
|
sync: false
|
|
- key: SESSION_SECRET
|
|
sync: false
|
|
`;
|
|
|
|
const FLY = `app = "wrnexus-app"
|
|
primary_region = "bom"
|
|
|
|
[build]
|
|
dockerfile = "Dockerfile"
|
|
[env]
|
|
PORT = "3000"
|
|
[http_service]
|
|
internal_port = 3000
|
|
force_https = true
|
|
auto_stop_machines = "stop"
|
|
auto_start_machines = true
|
|
min_machines_running = 1
|
|
[[http_service.checks]]
|
|
path = "/readyz"
|
|
interval = "15s"
|
|
timeout = "2s"
|
|
[deploy]
|
|
release_command = "bunx wrnexus db migrate --profile=production"
|
|
`;
|
|
|
|
function write(root: string, relative: string, content: string, files: string[]): void {
|
|
const path = join(root, relative);
|
|
if (existsSync(path)) return;
|
|
mkdirSync(dirname(path), { recursive: true });
|
|
writeFileSync(path, content, "utf8");
|
|
files.push(relative);
|
|
}
|
|
|
|
export function generateDeployment(appRoot: string, target: string): string[] {
|
|
if (!DEPLOY_TARGETS.includes(target as DeployTarget))
|
|
throw new Error(`WRN-DEPLOY-TARGET: use ${DEPLOY_TARGETS.join(" | ")}.`);
|
|
const root = resolve(appRoot);
|
|
const files: string[] = [];
|
|
if (target === "docker" || ["kubernetes", "railway", "render", "fly"].includes(target))
|
|
generateDocker(root);
|
|
write(root, ".env.production.example", ENVIRONMENT, files);
|
|
write(root, "deploy/README.md", OPERATIONS, files);
|
|
if (target === "kubernetes") write(root, "deploy/kubernetes.yaml", KUBERNETES, files);
|
|
if (target === "systemd") {
|
|
write(root, "deploy/wrnexus.service", SYSTEMD, files);
|
|
write(root, "deploy/nginx.conf", NGINX, files);
|
|
}
|
|
if (target === "railway") write(root, "railway.toml", RAILWAY, files);
|
|
if (target === "render") write(root, "render.yaml", RENDER, files);
|
|
if (target === "fly") write(root, "fly.toml", FLY, files);
|
|
return files;
|
|
}
|