1881 lines
88 KiB
JavaScript
1881 lines
88 KiB
JavaScript
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import assert from "node:assert/strict";
|
|
import { afterEach, describe, it } from "vitest";
|
|
|
|
import {
|
|
checkDbManifest,
|
|
checkDockerfileDepsManifestContract,
|
|
checkDockerignoreContract,
|
|
checkDockerfileApiContract,
|
|
checkDockerfileMcpContract,
|
|
checkHocuspocusDockerfileContract,
|
|
checkDockerfileWebContract,
|
|
checkDockerfileWorkerContract,
|
|
checkDockerComposeWorkerTopology,
|
|
checkRequiredFiles,
|
|
checkStartScript,
|
|
checkWebManifest,
|
|
formatVerificationResult,
|
|
verifyDockerBaseline,
|
|
} from "./verify-docker-baseline.mjs";
|
|
|
|
const VALID_PRODUCTION_DEPS_HELPER = `#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
deploy_web() {
|
|
pnpm --offline --config.inject-workspace-packages=true \\
|
|
--filter @hivegrid/runtime-web deploy --prod /prod/web
|
|
}
|
|
|
|
deploy_api() {
|
|
pnpm --offline --config.inject-workspace-packages=true \\
|
|
--filter @hivegrid/runtime-api deploy --prod --no-optional /prod/api
|
|
}
|
|
|
|
deploy_mcp() {
|
|
pnpm --offline --config.inject-workspace-packages=true \\
|
|
--filter @hivegrid/runtime-mcp deploy --prod --no-optional /prod/mcp
|
|
}
|
|
|
|
deploy_portal_controller() {
|
|
pnpm --offline --config.inject-workspace-packages=true \\
|
|
--filter @hivegrid/runtime-portal-domain-controller deploy --prod --no-optional /prod/portal-domain-controller
|
|
}
|
|
|
|
deploy_worker() {
|
|
pnpm --offline --config.inject-workspace-packages=true \\
|
|
--filter @hivegrid/runtime-worker deploy --prod /prod/worker
|
|
}
|
|
|
|
tasks=(deploy_web deploy_api deploy_mcp deploy_portal_controller deploy_worker)
|
|
targets=(web api mcp portal-domain-controller worker)
|
|
mkdir -p /prod
|
|
|
|
run_wave() {
|
|
local phase="$1"
|
|
local status=0
|
|
local child_status
|
|
local index
|
|
local task
|
|
local -a pids=()
|
|
|
|
for task in "\${tasks[@]}"; do
|
|
"$task" &
|
|
pids+=("$!")
|
|
done
|
|
|
|
for index in "\${!pids[@]}"; do
|
|
if wait "\${pids[$index]}"; then
|
|
continue
|
|
else
|
|
child_status=$?
|
|
fi
|
|
|
|
if [[ "$status" -eq 0 ]]; then
|
|
status="$child_status"
|
|
fi
|
|
done
|
|
|
|
return "$status"
|
|
}
|
|
|
|
initial_status=0
|
|
run_wave offline-first || initial_status=$?
|
|
if [[ "$initial_status" -eq 0 ]]; then
|
|
exit 0
|
|
fi
|
|
|
|
for target in "\${targets[@]}"; do
|
|
rm -rf -- "/prod/\${target}"
|
|
done
|
|
|
|
if pnpm fetch --frozen-lockfile; then
|
|
echo "fetch complete"
|
|
else
|
|
fetch_status=$?
|
|
exit "$fetch_status"
|
|
fi
|
|
|
|
retry_status=0
|
|
run_wave after-fetch || retry_status=$?
|
|
if [[ "$retry_status" -ne 0 ]]; then
|
|
exit "$retry_status"
|
|
fi
|
|
`;
|
|
|
|
const VALID_DB_RUNTIME_BUILD_SCRIPT = `
|
|
import { build } from "esbuild";
|
|
const sharedBuildOptions = { bundle: true, format: "esm", platform: "node" };
|
|
const artifactBuilds = [
|
|
{
|
|
entryPoints: ["packages/db/src/permissions-seed.server.ts"],
|
|
outfile: "packages/db/dist/permissions-seed.server.js",
|
|
packages: "external",
|
|
},
|
|
{
|
|
alias: { "@hivegrid/logger": "./packages/logger/src/index.ts" },
|
|
entryPoints: ["packages/db/src/deploy-permission-catalog-sync.server.ts"],
|
|
outfile: "packages/db/dist/deploy-permission-catalog-sync.server.js",
|
|
packages: "external",
|
|
},
|
|
{
|
|
alias: { "~": "./apps/web/app" },
|
|
banner: { js: "const require = createRequire(import.meta.url);" },
|
|
entryPoints: ["prisma/seed.ts"],
|
|
outfile: "packages/db/dist/production-seed.server.js",
|
|
},
|
|
{
|
|
alias: { "~": "./apps/web/app" },
|
|
entryPoints: ["scripts/seed-e2e-readiness.ts"],
|
|
outfile: "packages/db/dist/e2e-readiness-seed.server.js",
|
|
},
|
|
];
|
|
await Promise.all(artifactBuilds.map((artifact) => build({ ...sharedBuildOptions, ...artifact })));
|
|
`;
|
|
|
|
const VALID_DOCKERFILE = `# syntax=docker/dockerfile:1.19
|
|
FROM node:24-bookworm-slim AS base
|
|
FROM base AS deps
|
|
WORKDIR /myapp
|
|
ADD package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
|
ADD prisma prisma
|
|
COPY apps/api/package.json apps/api/package.json
|
|
COPY apps/hocuspocus/package.json apps/hocuspocus/package.json
|
|
COPY apps/mcp/package.json apps/mcp/package.json
|
|
COPY apps/portal-domain-controller/package.json apps/portal-domain-controller/package.json
|
|
COPY apps/web/package.json apps/web/package.json
|
|
COPY apps/worker/package.json apps/worker/package.json
|
|
COPY packages/db/package.json packages/db/package.json
|
|
COPY runtime/api/package.json runtime/api/package.json
|
|
COPY runtime/hocuspocus/package.json runtime/hocuspocus/package.json
|
|
COPY runtime/mcp/package.json runtime/mcp/package.json
|
|
COPY runtime/portal-domain-controller/package.json runtime/portal-domain-controller/package.json
|
|
COPY runtime/web/package.json runtime/web/package.json
|
|
COPY runtime/worker/package.json runtime/worker/package.json
|
|
RUN --mount=type=cache,id=hivegrid-pnpm-store,target=/pnpm/store \\
|
|
pnpm fetch --frozen-lockfile
|
|
RUN --mount=type=cache,id=hivegrid-pnpm-store,target=/pnpm/store \\
|
|
pnpm install --frozen-lockfile --prod=false --offline
|
|
FROM base AS build
|
|
RUN ./node_modules/.bin/prisma generate --config prisma.config.ts
|
|
RUN node packages/db/scripts/build-runtime-artifacts.mjs
|
|
FROM build AS production-deps-bundle
|
|
RUN --mount=type=cache,id=hivegrid-pnpm-store-v2,target=/pnpm/store,sharing=locked \
|
|
bash scripts/ci/deploy-production-closures.sh
|
|
FROM production-deps-bundle AS web-production-deps
|
|
RUN node /myapp/scripts/prune-prisma-client-runtime.mjs /prod/web/node_modules /myapp/packages/db/generated/prisma/client/internal/class.ts
|
|
RUN SBOM_TARGET=web SBOM_ROOT_PATH=/prod/web SBOM_NODE_MODULES_PATH=/prod/web/node_modules SBOM_OUTPUT_PATH=/prod/web/sbom-pnpm.json node /myapp/scripts/generate-sbom.mjs
|
|
FROM production-deps-bundle AS api-production-deps
|
|
RUN node /myapp/scripts/prune-prisma-client-runtime.mjs /prod/api/node_modules /myapp/packages/db/generated/prisma/client/internal/class.ts
|
|
RUN SBOM_TARGET=api SBOM_ROOT_PATH=/prod/api SBOM_NODE_MODULES_PATH=/prod/api/node_modules SBOM_OUTPUT_PATH=/prod/api/sbom-pnpm.json node /myapp/scripts/generate-sbom.mjs
|
|
FROM production-deps-bundle AS mcp-production-deps
|
|
RUN node /myapp/scripts/prune-prisma-client-runtime.mjs /prod/mcp/node_modules /myapp/packages/db/generated/prisma/client/internal/class.ts
|
|
RUN SBOM_TARGET=mcp SBOM_ROOT_PATH=/prod/mcp SBOM_NODE_MODULES_PATH=/prod/mcp/node_modules SBOM_OUTPUT_PATH=/prod/mcp/sbom-pnpm.json node /myapp/scripts/generate-sbom.mjs
|
|
FROM production-deps-bundle AS portal-domain-controller-production-deps
|
|
RUN SBOM_TARGET=portal-domain-controller SBOM_ROOT_PATH=/prod/portal-domain-controller SBOM_NODE_MODULES_PATH=/prod/portal-domain-controller/node_modules SBOM_OUTPUT_PATH=/prod/portal-domain-controller/sbom-pnpm.json node /myapp/scripts/generate-sbom.mjs
|
|
FROM production-deps-bundle AS worker-production-deps
|
|
RUN node /myapp/scripts/prune-pnpm-runtime-closure.mjs /prod/worker/node_modules prisma typescript
|
|
RUN node /myapp/scripts/prune-prisma-client-runtime.mjs /prod/worker/node_modules /myapp/packages/db/generated/prisma/client/internal/class.ts
|
|
RUN SBOM_TARGET=worker SBOM_ROOT_PATH=/prod/worker SBOM_NODE_MODULES_PATH=/prod/worker/node_modules SBOM_OUTPUT_PATH=/prod/worker/sbom-pnpm.json node /myapp/scripts/generate-sbom.mjs
|
|
FROM build AS web-build
|
|
ARG VITE_SENTRY_DSN_WEB=""
|
|
ENV VITE_SENTRY_DSN_WEB="\${VITE_SENTRY_DSN_WEB}"
|
|
ARG VITE_HOCUSPOCUS_URL=""
|
|
ENV VITE_HOCUSPOCUS_URL="\${VITE_HOCUSPOCUS_URL}"
|
|
RUN node scripts/build-web-surfaces.mjs && node scripts/build-web-server.mjs
|
|
FROM build AS api-build
|
|
RUN node apps/api/scripts/build.mjs
|
|
FROM build AS mcp-build
|
|
RUN node apps/mcp/scripts/build.mjs
|
|
FROM build AS portal-domain-controller-build
|
|
RUN cd apps/portal-domain-controller && node scripts/build.mjs
|
|
FROM build AS worker-build
|
|
RUN cd apps/worker && node scripts/build.mjs
|
|
FROM web-build AS web-runtime-artifacts
|
|
RUN find /myapp/build /myapp/apps/web/build /myapp/apps/web/build-portal -type f -name '*.map' -delete
|
|
FROM api-build AS api-runtime-artifacts
|
|
RUN find /myapp/apps/api/dist -type f -name '*.map' -delete
|
|
FROM mcp-build AS mcp-runtime-artifacts
|
|
RUN find /myapp/apps/mcp/dist -type f -name '*.map' -delete
|
|
FROM worker-build AS worker-runtime-artifacts
|
|
RUN find /myapp/apps/worker/dist -type f -name '*.map' -delete
|
|
FROM base AS worker
|
|
ARG APP_REVISION=unknown
|
|
ENV APP_REVISION="\${APP_REVISION}"
|
|
LABEL org.opencontainers.image.revision="\${APP_REVISION}"
|
|
COPY --exclude=**/*.map --exclude=**/*.d.ts --from=worker-production-deps /prod/worker/node_modules /myapp/node_modules
|
|
COPY --from=worker-production-deps /prod/worker/sbom-pnpm.json /myapp/sbom-pnpm.json
|
|
COPY --from=worker-runtime-artifacts /myapp/apps/worker/dist /myapp/apps/worker/dist
|
|
RUN printf '%s\\n' "$APP_REVISION" > /myapp/.image-revision
|
|
RUN rm -rf /usr/local/lib/node_modules/corepack /usr/local/bin/pnpm /usr/local/bin/yarn
|
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=20s --start-interval=2s --retries=3 CMD node apps/worker/dist/health.js
|
|
CMD [ "node", "apps/worker/dist/index.js" ]
|
|
FROM base AS portal-domain-controller
|
|
COPY --exclude=**/*.map --exclude=**/*.d.ts --from=portal-domain-controller-production-deps /prod/portal-domain-controller/node_modules /myapp/node_modules
|
|
COPY --from=portal-domain-controller-production-deps /prod/portal-domain-controller/sbom-pnpm.json /myapp/sbom-pnpm.json
|
|
RUN rm -rf /usr/local/lib/node_modules/corepack /usr/local/bin/pnpm /usr/local/bin/yarn
|
|
CMD [ "node", "apps/portal-domain-controller/dist/index.js" ]
|
|
FROM base AS api
|
|
ARG APP_REVISION=unknown
|
|
ENV APP_REVISION="\${APP_REVISION}"
|
|
LABEL org.opencontainers.image.revision="\${APP_REVISION}"
|
|
EXPOSE 8080
|
|
COPY --exclude=**/*.map --exclude=**/*.d.ts --from=api-production-deps /prod/api/node_modules /myapp/node_modules
|
|
COPY --from=api-production-deps /prod/api/sbom-pnpm.json /myapp/sbom-pnpm.json
|
|
COPY --from=api-runtime-artifacts /myapp/apps/api/dist /myapp/apps/api/dist
|
|
RUN rm -rf /usr/local/lib/node_modules/corepack /usr/local/bin/pnpm /usr/local/bin/yarn
|
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=20s --start-interval=2s --retries=3 CMD curl -fsS "http://127.0.0.1:\${API_PORT:-\${PORT}}/healthz"
|
|
CMD [ "node", "apps/api/dist/server.js" ]
|
|
FROM base AS mcp
|
|
ARG APP_REVISION=unknown
|
|
ENV APP_REVISION="\${APP_REVISION}"
|
|
LABEL org.opencontainers.image.revision="\${APP_REVISION}"
|
|
EXPOSE 8080
|
|
COPY --exclude=**/*.map --exclude=**/*.d.ts --from=mcp-production-deps /prod/mcp/node_modules /myapp/node_modules
|
|
COPY --from=mcp-production-deps /prod/mcp/sbom-pnpm.json /myapp/sbom-pnpm.json
|
|
COPY --from=mcp-runtime-artifacts /myapp/apps/mcp/dist /myapp/apps/mcp/dist
|
|
RUN printf '%s\\n' "$APP_REVISION" > /myapp/.image-revision
|
|
RUN rm -rf /usr/local/lib/node_modules/corepack /usr/local/bin/pnpm /usr/local/bin/yarn
|
|
USER node
|
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=20s --start-interval=2s --retries=3 CMD node -e "fetch('http://127.0.0.1:\${MCP_PORT:-\${PORT}}/healthz').then((response) => { if (!response.ok) process.exit(1); })"
|
|
CMD [ "node", "apps/mcp/dist/server.js" ]
|
|
FROM base AS web
|
|
ARG APP_REVISION=unknown
|
|
ENV APP_REVISION="\${APP_REVISION}"
|
|
LABEL org.opencontainers.image.revision="\${APP_REVISION}"
|
|
EXPOSE 8080
|
|
COPY --exclude=**/*.map --exclude=**/*.d.ts --from=web-production-deps /prod/web/node_modules /myapp/node_modules
|
|
COPY --from=web-production-deps /prod/web/sbom-pnpm.json /myapp/sbom-pnpm.json
|
|
COPY --from=web-runtime-artifacts /myapp/build /myapp/build
|
|
COPY --from=web-runtime-artifacts /myapp/build/server-runtime /myapp/build/server-runtime
|
|
COPY --from=web-runtime-artifacts /myapp/apps/web/build /myapp/apps/web/build
|
|
COPY --from=web-runtime-artifacts /myapp/apps/web/build-portal /myapp/apps/web/build-portal
|
|
COPY --from=build /myapp/scripts/verify-migrator-role.mjs /myapp/scripts/verify-migrator-role.mjs
|
|
COPY --from=build /myapp/scripts/adopt-prisma-baseline.mjs /myapp/scripts/adopt-prisma-baseline.mjs
|
|
COPY --from=build /myapp/prisma.config.ts /myapp/prisma.config.ts
|
|
COPY --from=build /myapp/packages/db/dist /myapp/packages/db/dist
|
|
COPY --from=build /myapp/prisma /myapp/prisma
|
|
RUN rm -rf /usr/local/lib/node_modules/corepack /usr/local/bin/pnpm /usr/local/bin/yarn
|
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=120s --start-interval=2s --retries=3 CMD node -e "const http=require('node:http');http.get({hostname:'127.0.0.1',port:process.env.PORT,path:'/healthz',headers:{host:process.env.WEB_APP_HOST}},()=>{})"
|
|
ENTRYPOINT [ "./start.sh" ]
|
|
`;
|
|
|
|
const VALID_DOCKER_COMPOSE = `services:
|
|
redis:
|
|
image: redis:7-alpine
|
|
ports:
|
|
- "127.0.0.1:6379:6379"
|
|
clamav:
|
|
image: clamav/clamav:stable
|
|
ports:
|
|
- "127.0.0.1:3310:3310"
|
|
worker-general: &worker-base
|
|
environment:
|
|
- WORKER_QUEUE_GROUP=general
|
|
- WORKER_CONCURRENCY_GENERAL=\${WORKER_CONCURRENCY_GENERAL:-10}
|
|
- WORKER_HEALTH_PORT=9101
|
|
- WORKER_ADMIN_PORT=9201
|
|
- APP_URL=\${APP_URL:-}
|
|
- OUTBOX_DISPATCHER_ENABLED=\${OUTBOX_DISPATCHER_ENABLED:-false}
|
|
- OUTBOX_DISPATCHER_REQUIRED=\${OUTBOX_DISPATCHER_REQUIRED:-false}
|
|
- LAGO_API_URL=\${LAGO_API_URL:-}
|
|
- LAGO_API_KEY=\${LAGO_API_KEY:-}
|
|
- LAGO_TIMEOUT_MS=\${LAGO_TIMEOUT_MS:-10000}
|
|
- MOLLIE_API_KEY=\${MOLLIE_API_KEY:-}
|
|
- MOLLIE_PROFILE_ID=\${MOLLIE_PROFILE_ID:-}
|
|
- MOLLIE_TIMEOUT_MS=\${MOLLIE_TIMEOUT_MS:-10000}
|
|
- BILLING_MOLLIE_WEBHOOK_URL=\${BILLING_MOLLIE_WEBHOOK_URL:-}
|
|
- WORKFLOW_TRIGGER_EXECUTOR_MODE=\${WORKFLOW_TRIGGER_EXECUTOR_MODE:-http}
|
|
- WORKFLOW_TRIGGER_EXECUTOR_URL=\${WORKFLOW_TRIGGER_EXECUTOR_URL:-}
|
|
- API_ACCESS_TOKEN=\${API_ACCESS_TOKEN:-}
|
|
- WORKFLOW_TRIGGER_EXECUTOR_TIMEOUT_MS=\${WORKFLOW_TRIGGER_EXECUTOR_TIMEOUT_MS:-30000}
|
|
- WORKFLOW_TRIGGER_OUTBOX_ENABLED=\${WORKFLOW_TRIGGER_OUTBOX_ENABLED:-true}
|
|
- WORKFLOW_TRIGGER_OUTBOX_CONCURRENCY=\${WORKFLOW_TRIGGER_OUTBOX_CONCURRENCY:-5}
|
|
- WORKFLOW_TRIGGER_OUTBOX_REQUIRED=\${WORKFLOW_TRIGGER_OUTBOX_REQUIRED:-false}
|
|
- SCAN_QUEUE_ENABLED=\${SCAN_QUEUE_ENABLED:-true}
|
|
- CLAMAV_HOST=\${CLAMAV_HOST:-clamav}
|
|
- CLAMAV_PORT=\${CLAMAV_PORT:-3310}
|
|
- STORAGE_ENDPOINT=\${STORAGE_ENDPOINT:-}
|
|
- STORAGE_REGION=\${STORAGE_REGION:-}
|
|
- STORAGE_BUCKET=\${STORAGE_BUCKET:-}
|
|
- STORAGE_ACCESS_KEY_ID=\${STORAGE_ACCESS_KEY_ID:-}
|
|
- STORAGE_SECRET_ACCESS_KEY=\${STORAGE_SECRET_ACCESS_KEY:-}
|
|
- STORAGE_SESSION_TOKEN=\${STORAGE_SESSION_TOKEN:-}
|
|
- STORAGE_FORCE_PATH_STYLE=\${STORAGE_FORCE_PATH_STYLE:-false}
|
|
- STORAGE_QUARANTINE_BUCKET=\${STORAGE_QUARANTINE_BUCKET:-}
|
|
- MEILI_URL=\${MEILI_URL:-}
|
|
- MEILI_API_KEY=\${MEILI_API_KEY:-}
|
|
- EXPORT_RETENTION_QUEUE_ENABLED=\${EXPORT_RETENTION_QUEUE_ENABLED:-false}
|
|
depends_on:
|
|
redis:
|
|
condition: service_healthy
|
|
clamav:
|
|
condition: service_healthy
|
|
healthcheck:
|
|
test: ["CMD", "node", "apps/worker/dist/health.js"]
|
|
worker-mail:
|
|
<<: *worker-base
|
|
environment:
|
|
- WORKER_QUEUE_GROUP=mail
|
|
- WORKER_CONCURRENCY_MAIL=\${WORKER_CONCURRENCY_MAIL:-5}
|
|
- WORKER_HEALTH_PORT=9102
|
|
- WORKER_ADMIN_PORT=9202
|
|
depends_on:
|
|
redis:
|
|
condition: service_healthy
|
|
healthcheck:
|
|
test: ["CMD", "node", "apps/worker/dist/health.js"]
|
|
worker-import:
|
|
<<: *worker-base
|
|
environment:
|
|
- WORKER_QUEUE_GROUP=import
|
|
- WORKER_CONCURRENCY_IMPORT=\${WORKER_CONCURRENCY_IMPORT:-1}
|
|
- WORKER_HEALTH_PORT=9103
|
|
- WORKER_ADMIN_PORT=9203
|
|
- IMPORT_QUEUE_ENABLED=\${IMPORT_QUEUE_ENABLED:-false}
|
|
- IMPORT_QUEUE_RUNTIME_READY=\${IMPORT_QUEUE_RUNTIME_READY:-false}
|
|
- PORTAL_GDPR_EXPORT_QUEUE_ENABLED=\${PORTAL_GDPR_EXPORT_QUEUE_ENABLED:-false}
|
|
- STORAGE_ENDPOINT=\${STORAGE_ENDPOINT:-}
|
|
- STORAGE_REGION=\${STORAGE_REGION:-}
|
|
- STORAGE_BUCKET=\${STORAGE_BUCKET:-}
|
|
- STORAGE_ACCESS_KEY_ID=\${STORAGE_ACCESS_KEY_ID:-}
|
|
- STORAGE_SECRET_ACCESS_KEY=\${STORAGE_SECRET_ACCESS_KEY:-}
|
|
- STORAGE_SESSION_TOKEN=\${STORAGE_SESSION_TOKEN:-}
|
|
- STORAGE_FORCE_PATH_STYLE=\${STORAGE_FORCE_PATH_STYLE:-false}
|
|
- GDPR_EXPORT_SSE_C_KEY_BASE64=\${GDPR_EXPORT_SSE_C_KEY_BASE64:-}
|
|
depends_on:
|
|
redis:
|
|
condition: service_healthy
|
|
healthcheck:
|
|
test: ["CMD", "node", "apps/worker/dist/health.js"]
|
|
worker-webhook:
|
|
<<: *worker-base
|
|
environment:
|
|
- WORKER_QUEUE_GROUP=webhook
|
|
- WORKER_CONCURRENCY_WEBHOOK=\${WORKER_CONCURRENCY_WEBHOOK:-20}
|
|
- WORKER_HEALTH_PORT=9104
|
|
- WORKER_ADMIN_PORT=9204
|
|
- WEB_PUSH_VAPID_SUBJECT=\${WEB_PUSH_VAPID_SUBJECT:-}
|
|
- WEB_PUSH_VAPID_PUBLIC_KEY=\${WEB_PUSH_VAPID_PUBLIC_KEY:-}
|
|
- WEB_PUSH_VAPID_PRIVATE_KEY=\${WEB_PUSH_VAPID_PRIVATE_KEY:-}
|
|
depends_on:
|
|
redis:
|
|
condition: service_healthy
|
|
healthcheck:
|
|
test: ["CMD", "node", "apps/worker/dist/health.js"]
|
|
worker-plugin:
|
|
<<: *worker-base
|
|
environment:
|
|
- WORKER_QUEUE_GROUP=plugin
|
|
- WORKER_CONCURRENCY_PLUGIN=\${WORKER_CONCURRENCY_PLUGIN:-5}
|
|
- WORKER_HEALTH_PORT=9105
|
|
- WORKER_ADMIN_PORT=9205
|
|
depends_on:
|
|
redis:
|
|
condition: service_healthy
|
|
healthcheck:
|
|
test: ["CMD", "node", "apps/worker/dist/health.js"]
|
|
volumes:
|
|
redis-data:
|
|
clamav-db:
|
|
`;
|
|
|
|
const VALID_HOCUSPOCUS_DOCKERFILE = `# syntax=docker/dockerfile:1.19
|
|
FROM node:24-bookworm-slim AS base
|
|
FROM base AS deps
|
|
WORKDIR /myapp
|
|
ADD package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
|
COPY apps/api/package.json apps/api/package.json
|
|
COPY apps/hocuspocus/package.json apps/hocuspocus/package.json
|
|
COPY apps/portal-domain-controller/package.json apps/portal-domain-controller/package.json
|
|
COPY apps/web/package.json apps/web/package.json
|
|
COPY apps/worker/package.json apps/worker/package.json
|
|
COPY packages/db/package.json packages/db/package.json
|
|
COPY runtime/api/package.json runtime/api/package.json
|
|
COPY runtime/hocuspocus/package.json runtime/hocuspocus/package.json
|
|
COPY runtime/mcp/package.json runtime/mcp/package.json
|
|
COPY runtime/portal-domain-controller/package.json runtime/portal-domain-controller/package.json
|
|
COPY runtime/web/package.json runtime/web/package.json
|
|
COPY runtime/worker/package.json runtime/worker/package.json
|
|
COPY patches ./patches
|
|
RUN --mount=type=cache,id=hivegrid-hocuspocus-pnpm-store,target=/pnpm/store \
|
|
pnpm fetch --frozen-lockfile
|
|
RUN --mount=type=cache,id=hivegrid-hocuspocus-pnpm-store,target=/pnpm/store \
|
|
pnpm install --frozen-lockfile --prod=false --offline
|
|
FROM base AS build
|
|
RUN ./node_modules/.bin/prisma generate --config prisma.config.ts
|
|
RUN node apps/hocuspocus/scripts/build.mjs
|
|
FROM build AS hocuspocus-runtime-artifacts
|
|
RUN find /myapp/apps/hocuspocus/dist -type f -name '*.map' -delete
|
|
FROM deps AS hocuspocus-production-deps
|
|
RUN --mount=type=cache,id=hivegrid-hocuspocus-pnpm-store,target=/pnpm/store \\
|
|
pnpm fetch --frozen-lockfile \\
|
|
&& pnpm --offline --config.inject-workspace-packages=true --filter @hivegrid/runtime-hocuspocus deploy --prod --no-optional /prod/hocuspocus
|
|
COPY --from=build /myapp/packages/db/generated/prisma/client/internal/class.ts /myapp/packages/db/generated/prisma/client/internal/class.ts
|
|
COPY scripts/prune-prisma-client-runtime.mjs /myapp/scripts/prune-prisma-client-runtime.mjs
|
|
RUN node /myapp/scripts/prune-prisma-client-runtime.mjs /prod/hocuspocus/node_modules /myapp/packages/db/generated/prisma/client/internal/class.ts
|
|
COPY scripts/generate-sbom.mjs /myapp/scripts/generate-sbom.mjs
|
|
RUN SBOM_TARGET=hocuspocus SBOM_NODE_MODULES_PATH=/prod/hocuspocus/node_modules SBOM_OUTPUT_PATH=/prod/hocuspocus/sbom-pnpm.json node /myapp/scripts/generate-sbom.mjs
|
|
FROM base AS hocuspocus
|
|
COPY --exclude=**/*.map --exclude=**/*.d.ts --from=hocuspocus-production-deps /prod/hocuspocus/node_modules /myapp/node_modules
|
|
COPY --from=hocuspocus-production-deps /prod/hocuspocus/sbom-pnpm.json /myapp/sbom-pnpm.json
|
|
COPY --from=hocuspocus-runtime-artifacts /myapp/apps/hocuspocus/dist /myapp/apps/hocuspocus/dist
|
|
RUN rm -rf /usr/local/lib/node_modules/corepack /usr/local/bin/pnpm /usr/local/bin/yarn
|
|
USER node
|
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --start-interval=2s --retries=3 CMD node -e "require('net').connect(1234, '127.0.0.1')"
|
|
CMD ["node", "apps/hocuspocus/dist/index.js"]
|
|
`;
|
|
|
|
const VALID_DOCKER_COMPOSE_STAGING = `# docker compose -f docker-compose.yml -f docker-compose.staging.yml up postgres pgbouncer api web worker-general worker-mail worker-import worker-webhook worker-plugin
|
|
services:
|
|
migrator:
|
|
entrypoint: ["./start.sh", "migrate"]
|
|
environment:
|
|
- DIRECT_URL=\${MIGRATOR_DIRECT_URL:?required}
|
|
api:
|
|
environment:
|
|
- DATABASE_URL=\${APP_DATABASE_URL:?required}
|
|
- DATABASE_RUNTIME_ROLE=hivegrid_app
|
|
- RLS_RUNTIME_MODE=required
|
|
web:
|
|
environment:
|
|
- DATABASE_URL=\${APP_DATABASE_URL:?required}
|
|
- DATABASE_RUNTIME_ROLE=hivegrid_app
|
|
- RLS_RUNTIME_MODE=required
|
|
- AUTHENTIK_ADMIN_JIT_DATABASE_URL=\${AUTHENTIK_ADMIN_JIT_DATABASE_URL:-}
|
|
- REDIS_URL=\${REDIS_URL:?required}
|
|
- AIRCALL_CLIENT_ID=\${AIRCALL_CLIENT_ID:-}
|
|
- AIRCALL_CLIENT_SECRET=\${AIRCALL_CLIENT_SECRET:-}
|
|
- AIRCALL_REDIRECT_URI=\${AIRCALL_REDIRECT_URI:-}
|
|
- AIRCALL_WEBHOOK_URL=\${AIRCALL_WEBHOOK_URL:-}
|
|
- STORAGE_ENDPOINT=\${STORAGE_ENDPOINT:?required}
|
|
- STORAGE_REGION=\${STORAGE_REGION:?required}
|
|
- STORAGE_BUCKET=\${STORAGE_BUCKET:?required}
|
|
- STORAGE_ACCESS_KEY_ID=\${WEB_STORAGE_ACCESS_KEY_ID:?required}
|
|
- STORAGE_SECRET_ACCESS_KEY=\${WEB_STORAGE_SECRET_ACCESS_KEY:?required}
|
|
- STORAGE_SESSION_TOKEN=\${WEB_STORAGE_SESSION_TOKEN:-}
|
|
- STORAGE_FORCE_PATH_STYLE=\${STORAGE_FORCE_PATH_STYLE:-false}
|
|
- GDPR_EXPORT_SSE_C_KEY_BASE64=\${GDPR_EXPORT_SSE_C_KEY_BASE64:?required}
|
|
- WEB_PUSH_VAPID_PUBLIC_KEY=\${WEB_PUSH_VAPID_PUBLIC_KEY:?required}
|
|
hocuspocus:
|
|
environment:
|
|
- DATABASE_URL=\${APP_DATABASE_URL:?required}
|
|
- DATABASE_RUNTIME_ROLE=hivegrid_app
|
|
- RLS_RUNTIME_MODE=required
|
|
worker-general:
|
|
environment:
|
|
- DATABASE_URL=\${WORKER_DATABASE_URL:?required}
|
|
- DATABASE_RUNTIME_ROLE=hivegrid_worker
|
|
- RLS_RUNTIME_MODE=required
|
|
- SCAN_QUEUE_ENABLED=true
|
|
- STORAGE_ENDPOINT=\${STORAGE_ENDPOINT:?required}
|
|
- STORAGE_REGION=\${STORAGE_REGION:?required}
|
|
- STORAGE_BUCKET=\${STORAGE_BUCKET:?required}
|
|
- STORAGE_ACCESS_KEY_ID=\${WORKER_GENERAL_STORAGE_ACCESS_KEY_ID:?required}
|
|
- STORAGE_SECRET_ACCESS_KEY=\${WORKER_GENERAL_STORAGE_SECRET_ACCESS_KEY:?required}
|
|
- STORAGE_SESSION_TOKEN=\${WORKER_GENERAL_STORAGE_SESSION_TOKEN:-}
|
|
- STORAGE_FORCE_PATH_STYLE=\${STORAGE_FORCE_PATH_STYLE:-false}
|
|
- MEILI_URL=\${MEILI_URL:?required}
|
|
- MEILI_API_KEY=\${MEILI_API_KEY:?required}
|
|
- EXPORT_RETENTION_QUEUE_ENABLED=true
|
|
- OUTBOX_DISPATCHER_ENABLED=true
|
|
- OUTBOX_DISPATCHER_REQUIRED=true
|
|
- LAGO_API_URL=\${LAGO_API_URL:?required}
|
|
- LAGO_API_KEY=\${LAGO_API_KEY:?required}
|
|
- LAGO_TIMEOUT_MS=\${LAGO_TIMEOUT_MS:-10000}
|
|
- MOLLIE_API_KEY=\${MOLLIE_API_KEY:?required}
|
|
- MOLLIE_PROFILE_ID=\${MOLLIE_PROFILE_ID:-}
|
|
- MOLLIE_TIMEOUT_MS=\${MOLLIE_TIMEOUT_MS:-10000}
|
|
- BILLING_MOLLIE_WEBHOOK_URL=\${BILLING_MOLLIE_WEBHOOK_URL:?required}
|
|
- WORKFLOW_TRIGGER_OUTBOX_ENABLED=true
|
|
- WORKFLOW_TRIGGER_OUTBOX_REQUIRED=true
|
|
depends_on:
|
|
pgbouncer:
|
|
condition: service_healthy
|
|
worker-mail:
|
|
environment:
|
|
- DATABASE_URL=\${WORKER_DATABASE_URL:?required}
|
|
- DATABASE_RUNTIME_ROLE=hivegrid_worker
|
|
- RLS_RUNTIME_MODE=required
|
|
depends_on:
|
|
pgbouncer:
|
|
condition: service_healthy
|
|
worker-import:
|
|
environment:
|
|
- DATABASE_URL=\${WORKER_DATABASE_URL:?required}
|
|
- DATABASE_RUNTIME_ROLE=hivegrid_worker
|
|
- RLS_RUNTIME_MODE=required
|
|
- IMPORT_QUEUE_ENABLED=false
|
|
- IMPORT_QUEUE_RUNTIME_READY=false
|
|
- PORTAL_GDPR_EXPORT_QUEUE_ENABLED=true
|
|
- STORAGE_ENDPOINT=\${STORAGE_ENDPOINT:?required}
|
|
- STORAGE_REGION=\${STORAGE_REGION:?required}
|
|
- STORAGE_BUCKET=\${STORAGE_BUCKET:?required}
|
|
- STORAGE_ACCESS_KEY_ID=\${WORKER_IMPORT_STORAGE_ACCESS_KEY_ID:?required}
|
|
- STORAGE_SECRET_ACCESS_KEY=\${WORKER_IMPORT_STORAGE_SECRET_ACCESS_KEY:?required}
|
|
- STORAGE_SESSION_TOKEN=\${WORKER_IMPORT_STORAGE_SESSION_TOKEN:-}
|
|
- STORAGE_FORCE_PATH_STYLE=\${STORAGE_FORCE_PATH_STYLE:-false}
|
|
- GDPR_EXPORT_SSE_C_KEY_BASE64=\${GDPR_EXPORT_SSE_C_KEY_BASE64:?required}
|
|
depends_on:
|
|
pgbouncer:
|
|
condition: service_healthy
|
|
worker-webhook:
|
|
environment:
|
|
- DATABASE_URL=\${WORKER_DATABASE_URL:?required}
|
|
- DATABASE_RUNTIME_ROLE=hivegrid_worker
|
|
- RLS_RUNTIME_MODE=required
|
|
- WEB_PUSH_VAPID_SUBJECT=\${WEB_PUSH_VAPID_SUBJECT:?required}
|
|
- WEB_PUSH_VAPID_PUBLIC_KEY=\${WEB_PUSH_VAPID_PUBLIC_KEY:?required}
|
|
- WEB_PUSH_VAPID_PRIVATE_KEY=\${WEB_PUSH_VAPID_PRIVATE_KEY:?required}
|
|
depends_on:
|
|
pgbouncer:
|
|
condition: service_healthy
|
|
worker-plugin:
|
|
environment:
|
|
- DATABASE_URL=\${WORKER_DATABASE_URL:?required}
|
|
- DATABASE_RUNTIME_ROLE=hivegrid_worker
|
|
- RLS_RUNTIME_MODE=required
|
|
depends_on:
|
|
pgbouncer:
|
|
condition: service_healthy
|
|
volumes:
|
|
postgres-data:
|
|
`;
|
|
|
|
const fixtures = [];
|
|
|
|
function writeFile(rootDir, relativePath, content) {
|
|
const absolutePath = path.join(rootDir, relativePath);
|
|
fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
|
|
fs.writeFileSync(absolutePath, content);
|
|
}
|
|
|
|
function writeJson(rootDir, relativePath, value) {
|
|
writeFile(rootDir, relativePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
}
|
|
|
|
function createFixture(overrides = {}) {
|
|
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "docker-baseline-"));
|
|
fixtures.push(rootDir);
|
|
|
|
writeFile(rootDir, "Dockerfile", overrides.dockerfile ?? VALID_DOCKERFILE);
|
|
writeFile(rootDir, "scripts/ci/deploy-production-closures.sh", overrides.productionDepsHelper ?? VALID_PRODUCTION_DEPS_HELPER);
|
|
writeFile(rootDir, "packages/db/scripts/build-runtime-artifacts.mjs", overrides.dbRuntimeBuildScript ?? VALID_DB_RUNTIME_BUILD_SCRIPT);
|
|
writeFile(rootDir, "apps/hocuspocus/Dockerfile", overrides.hocuspocusDockerfile ?? VALID_HOCUSPOCUS_DOCKERFILE);
|
|
writeFile(rootDir, "docker-compose.yml", overrides.dockerCompose ?? VALID_DOCKER_COMPOSE);
|
|
writeFile(rootDir, "docker-compose.staging.yml", overrides.dockerComposeStaging ?? VALID_DOCKER_COMPOSE_STAGING);
|
|
writeFile(
|
|
rootDir,
|
|
".dockerignore",
|
|
overrides.dockerignore ??
|
|
[
|
|
"node_modules",
|
|
"/docs",
|
|
"/secrets",
|
|
"/.gitea",
|
|
"/.github",
|
|
"/tests",
|
|
"/e2e",
|
|
"**/*.test.ts",
|
|
"**/*.test.tsx",
|
|
"**/*.test.mjs",
|
|
"**/*.test.js",
|
|
"**/*.spec.ts",
|
|
"/scripts/ci/deploy-buildkitd.toml",
|
|
"/scripts/ci/verify-deploy-buildkit-config.mjs",
|
|
"/scripts/verify-gitea-ci-workflow.mjs",
|
|
"/scripts/verify-gitea-deploy-workflow.mjs",
|
|
".cache",
|
|
"**/.cache",
|
|
".eslintcache",
|
|
"**/.eslintcache",
|
|
"coverage",
|
|
"**/coverage",
|
|
".nyc_output",
|
|
"**/.nyc_output",
|
|
"reports",
|
|
"**/reports",
|
|
"public/build",
|
|
"/build",
|
|
"/apps/*/build",
|
|
"apps/*/dist",
|
|
"packages/*/dist",
|
|
"playwright-report",
|
|
"**/playwright-report",
|
|
"test-results",
|
|
"**/test-results",
|
|
"blob-report",
|
|
"**/blob-report",
|
|
"",
|
|
].join("\n")
|
|
);
|
|
writeFile(
|
|
rootDir,
|
|
"start.sh",
|
|
overrides.startScript ??
|
|
[
|
|
"#!/bin/sh",
|
|
"set -eu",
|
|
"",
|
|
'if [ "${1:-}" = "migrate" ]; then',
|
|
' [ -n "${DIRECT_URL:-}" ] || echo "DIRECT_URL is required for the one-shot migrator"',
|
|
' [ -z "${DATABASE_URL+x}" ] || echo "DATABASE_URL must not be forwarded to the one-shot migrator"',
|
|
' echo "AUTHENTIK_ADMIN_JIT_DATABASE_URL belongs only to the web runtime"',
|
|
' echo "MIGRATOR_* aliases must not be forwarded to the one-shot migrator"',
|
|
' echo "seed credentials and flags must not be forwarded to the one-shot migrator"',
|
|
" node /myapp/scripts/verify-migrator-role.mjs",
|
|
" node /myapp/scripts/adopt-prisma-baseline.mjs --adopt",
|
|
' export DATABASE_URL="$DIRECT_URL"',
|
|
" node /myapp/node_modules/prisma/build/index.js migrate deploy --config prisma.config.ts",
|
|
" node /myapp/scripts/adopt-prisma-baseline.mjs --verify-current-schema",
|
|
" node /myapp/packages/db/dist/deploy-permission-catalog-sync.server.js",
|
|
" exit 0",
|
|
"fi",
|
|
"",
|
|
'[ "${RLS_RUNTIME_MODE:-}" = "required" ] || echo "RLS_RUNTIME_MODE=required is mandatory for the web runtime"',
|
|
'[ "${DATABASE_RUNTIME_ROLE:-}" = "hivegrid_app" ] || echo "DATABASE_RUNTIME_ROLE=hivegrid_app is mandatory for the web runtime"',
|
|
'[ -z "${DIRECT_URL+x}" ] || echo "DIRECT_URL must not be forwarded to the web runtime"',
|
|
'echo "MIGRATOR_* credentials must not be forwarded to the web runtime"',
|
|
'echo "AUTHENTIK_ADMIN_JIT_DATABASE_URL must use the dedicated hivegrid_authentik_jit login"',
|
|
'echo "AUTHENTIK_ OAUTH BILLING COOLIFY SSH TRAEFIK CLOUDFLARE DOMAIN_CONTROLLER"',
|
|
'echo "forbidden core session, OAuth, billing, ingress, or domain-controller credential in the portal web runtime"',
|
|
'[ -z "${MIGRATE_ON_BOOT+x}" ] || echo "MIGRATE_ON_BOOT is forbidden"',
|
|
'[ -z "${SEED_ON_BOOT+x}" ] || echo "SEED_ON_BOOT is forbidden"',
|
|
'echo "WEB_RUNTIME_SURFACE must be explicitly set to core, admin, or portal"',
|
|
'echo "HIVEGRID_RUNTIME_SURFACE must match WEB_RUNTIME_SURFACE"',
|
|
'export HIVEGRID_RUNTIME_SURFACE="$WEB_RUNTIME_SURFACE"',
|
|
"exec node build/server-runtime/server.js",
|
|
"",
|
|
].join("\n")
|
|
);
|
|
writeFile(rootDir, "pnpm-workspace.yaml", overrides.workspace ?? "packages:\n - apps/*\n");
|
|
writeJson(
|
|
rootDir,
|
|
"package.json",
|
|
overrides.rootPackage ?? {
|
|
private: true,
|
|
scripts: {
|
|
build: "vite build && node scripts/build-web-server.mjs",
|
|
lint: "eslint .",
|
|
test: "pnpm --filter @hivegrid/web test",
|
|
typecheck: "tsc -b",
|
|
start: "pnpm --filter @hivegrid/web start",
|
|
},
|
|
}
|
|
);
|
|
writeJson(
|
|
rootDir,
|
|
"packages/db/package.json",
|
|
overrides.dbPackage ?? {
|
|
name: "@hivegrid/db",
|
|
scripts: {
|
|
build:
|
|
"esbuild packages/db/src/deploy-permission-catalog-sync.server.ts --bundle --platform=node --format=esm --packages=external --alias:@hivegrid/logger=./packages/logger/src/index.ts --outfile=packages/db/dist/deploy-permission-catalog-sync.server.js",
|
|
},
|
|
}
|
|
);
|
|
writeJson(
|
|
rootDir,
|
|
"apps/web/package.json",
|
|
overrides.webPackage ?? {
|
|
name: "@hivegrid/web",
|
|
scripts: {
|
|
start: "node ../../build/server-runtime/server.js",
|
|
},
|
|
}
|
|
);
|
|
writeJson(
|
|
rootDir,
|
|
"apps/api/package.json",
|
|
overrides.apiPackage ?? {
|
|
name: "@hivegrid/api",
|
|
scripts: {
|
|
build: "tsc -p tsconfig.json",
|
|
start: "node dist/server.js",
|
|
},
|
|
}
|
|
);
|
|
writeJson(
|
|
rootDir,
|
|
"apps/hocuspocus/package.json",
|
|
overrides.hocuspocusPackage ?? {
|
|
name: "@hivegrid/hocuspocus",
|
|
scripts: {
|
|
build: "node scripts/build.mjs",
|
|
start: "node dist/index.js",
|
|
},
|
|
}
|
|
);
|
|
writeJson(rootDir, "apps/portal-domain-controller/package.json", {
|
|
name: "@hivegrid/portal-domain-controller",
|
|
scripts: {
|
|
build: "tsc -p tsconfig.json",
|
|
start: "node dist/index.js",
|
|
},
|
|
});
|
|
writeJson(
|
|
rootDir,
|
|
"apps/worker/package.json",
|
|
overrides.workerPackage ?? {
|
|
name: "@hivegrid/worker",
|
|
scripts: {
|
|
start: "node dist/index.js",
|
|
health: "node dist/health.js",
|
|
},
|
|
}
|
|
);
|
|
for (const target of ["api", "hocuspocus", "mcp", "portal-domain-controller", "web", "worker"]) {
|
|
writeJson(rootDir, `runtime/${target}/package.json`, {
|
|
name: `@hivegrid/runtime-${target}`,
|
|
private: true,
|
|
dependencies: {},
|
|
});
|
|
}
|
|
|
|
if (overrides.remove) {
|
|
for (const relativePath of overrides.remove) {
|
|
fs.rmSync(path.join(rootDir, relativePath), { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
return rootDir;
|
|
}
|
|
|
|
afterEach(() => {
|
|
while (fixtures.length > 0) {
|
|
fs.rmSync(fixtures.pop(), { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
describe("verify-docker-baseline", () => {
|
|
it("passes for a repository with required Docker files and runtime scripts", () => {
|
|
const rootDir = createFixture();
|
|
|
|
const result = verifyDockerBaseline(rootDir);
|
|
|
|
assert.deepEqual(result.failures, []);
|
|
assert.ok(result.passes.some((pass) => pass === "Dockerfile: exists"));
|
|
assert.ok(result.passes.some((pass) => pass === "Dockerfile: deps stage copies all workspace package manifests before install"));
|
|
assert.ok(result.passes.some((pass) => pass === "Dockerfile: deps target fetches lockfile dependencies before offline install"));
|
|
assert.ok(result.passes.some((pass) => pass === "Dockerfile: deps target uses frozen offline dev dependency install"));
|
|
assert.ok(result.passes.some((pass) => pass === "Dockerfile: deps target uses BuildKit pnpm store cache"));
|
|
assert.ok(result.passes.some((pass) => pass === "Dockerfile: production-deps-bundle delegates one locked warm v2 store directly to the closure helper"));
|
|
assert.ok(
|
|
result.passes.some(
|
|
(pass) =>
|
|
pass === "scripts/ci/deploy-production-closures.sh: each wave starts all five closures in parallel, reaps every PID, and returns the first failure"
|
|
)
|
|
);
|
|
assert.ok(
|
|
result.passes.some(
|
|
(pass) =>
|
|
pass ===
|
|
"scripts/ci/deploy-production-closures.sh: retries one complete wave only after exact-target cleanup and one frozen fetch, with no sentinel or fail-open bypass"
|
|
)
|
|
);
|
|
assert.ok(
|
|
result.passes.some(
|
|
(pass) => pass === "scripts/ci/deploy-production-closures.sh: deploy_web deploys exactly the isolated @hivegrid/runtime-web production closure"
|
|
)
|
|
);
|
|
assert.ok(
|
|
result.passes.some(
|
|
(pass) =>
|
|
pass === "Dockerfile: web-production-deps derives from production-deps-bundle and performs only required reachability pruning and SBOM generation"
|
|
)
|
|
);
|
|
assert.ok(
|
|
result.passes.some(
|
|
(pass) =>
|
|
pass ===
|
|
"Dockerfile: portal-domain-controller-production-deps derives from production-deps-bundle and performs only required reachability pruning and SBOM generation"
|
|
)
|
|
);
|
|
assert.ok(result.passes.some((pass) => pass === "Dockerfile: web target copies only its deployed dependency closure without maps or declarations"));
|
|
assert.ok(result.passes.some((pass) => pass === "Dockerfile: web-production-deps keeps only the generated PostgreSQL Prisma runtime"));
|
|
assert.ok(result.passes.some((pass) => pass === "Dockerfile: api target copies only its deployed dependency closure without maps or declarations"));
|
|
assert.ok(result.passes.some((pass) => pass === "Dockerfile: api target removes inherited package-manager executables"));
|
|
assert.ok(result.passes.some((pass) => pass === "Dockerfile: excludes generic whole-workspace production node_modules from runtime images"));
|
|
assert.ok(result.passes.some((pass) => pass === "Dockerfile: defines named web target from base"));
|
|
assert.ok(result.passes.some((pass) => pass === "Dockerfile: copies build/server-runtime into web image"));
|
|
assert.ok(result.passes.some((pass) => pass === "Dockerfile: build target leaves web, api, and worker app builds to split stages"));
|
|
assert.ok(result.passes.some((pass) => pass === "Dockerfile: build target keeps VITE build args out of api/worker cache paths"));
|
|
assert.ok(result.passes.some((pass) => pass === "Dockerfile: web-build target invokes both direct web build entry points"));
|
|
assert.ok(result.passes.some((pass) => pass === "Dockerfile: web-build target forwards VITE_HOCUSPOCUS_URL into the Vite and CSP builds"));
|
|
assert.ok(result.passes.some((pass) => pass === "Dockerfile: api-build target invokes its direct build entry point"));
|
|
assert.ok(result.passes.some((pass) => pass === "Dockerfile: worker-build target invokes its direct cwd-aware build entry point"));
|
|
assert.ok(
|
|
result.passes.some((pass) => pass === "Dockerfile: target-specific runtime artifacts remove web, api, and worker sourcemaps before runtime COPY")
|
|
);
|
|
assert.ok(result.passes.some((pass) => pass === "Dockerfile: web target exposes port 8080"));
|
|
assert.ok(result.passes.some((pass) => pass === "Dockerfile: web target avoids recursive runtime chown"));
|
|
assert.ok(result.passes.some((pass) => pass === "Dockerfile: web target copies prisma.config.ts"));
|
|
assert.ok(result.passes.some((pass) => pass === "Dockerfile: web target copies target-specific node_modules"));
|
|
assert.ok(result.passes.some((pass) => pass === "Dockerfile: web target copies migrator role verifier"));
|
|
assert.ok(result.passes.some((pass) => pass === "Dockerfile: web target copies packages/db production seed artifact"));
|
|
assert.ok(result.passes.some((pass) => pass === "Dockerfile: web target copies prisma"));
|
|
assert.ok(result.passes.some((pass) => pass === "Dockerfile: web target excludes the separate API application"));
|
|
assert.ok(result.passes.some((pass) => pass === "start.sh: execs the production start process for container signal handling"));
|
|
assert.ok(result.passes.some((pass) => pass === "start.sh: fails closed on command errors and unset variables (set -eu)"));
|
|
assert.ok(result.passes.some((pass) => pass === "start.sh: isolates migrate deploy behind the DIRECT_URL-only one-shot mode"));
|
|
assert.ok(result.passes.some((pass) => pass === "start.sh: syncs the permission catalog inside one-shot migrate mode only after successful migrations"));
|
|
assert.ok(result.passes.some((pass) => pass === "start.sh: keeps seeding outside the non-superuser migration authority"));
|
|
assert.ok(result.passes.some((pass) => pass === "start.sh: rejects privileged credentials and RLS bypasses in the long-lived web runtime"));
|
|
assert.ok(result.passes.some((pass) => pass === "Dockerfile: defines named worker target from base"));
|
|
assert.ok(result.passes.some((pass) => pass === "Dockerfile: worker target copies apps/worker/dist"));
|
|
assert.ok(result.passes.some((pass) => pass === "Dockerfile: worker target copies target-specific node_modules"));
|
|
assert.ok(result.passes.some((pass) => pass === "Dockerfile: worker target runtime does not use tsx"));
|
|
assert.ok(result.passes.some((pass) => pass === "Dockerfile: worker target avoids recursive runtime chown"));
|
|
assert.ok(result.passes.some((pass) => pass === "Dockerfile: defines named api target from base"));
|
|
assert.ok(result.passes.some((pass) => pass === "Dockerfile: api target copies target-specific node_modules"));
|
|
assert.ok(result.passes.some((pass) => pass === "Dockerfile: api target exposes port 8080"));
|
|
assert.ok(result.passes.some((pass) => pass === "Dockerfile: api target avoids recursive runtime chown"));
|
|
assert.ok(result.passes.some((pass) => pass === 'package.json: script "typecheck" is defined'));
|
|
assert.ok(result.passes.some((pass) => pass === "package.json: production start script delegates to @hivegrid/web"));
|
|
assert.ok(result.passes.some((pass) => pass === "apps/web/package.json: owns production web server start command"));
|
|
assert.ok(result.passes.some((pass) => pass === "package.json: production start script does not use tsx"));
|
|
assert.ok(result.passes.some((pass) => pass === "apps/api/package.json: production start script uses node"));
|
|
assert.ok(result.passes.some((pass) => pass === 'apps/worker/package.json: script "start" targets dist/index.js'));
|
|
assert.ok(result.passes.some((pass) => pass === 'apps/worker/package.json: script "health" targets dist/health.js'));
|
|
assert.ok(result.passes.some((pass) => pass === "packages/db/package.json: builds the self-contained deploy permission catalog sync artifact"));
|
|
assert.ok(result.passes.some((pass) => pass === "docker-compose.yml: worker-general passes through OUTBOX_DISPATCHER_ENABLED"));
|
|
assert.ok(result.passes.some((pass) => pass === "docker-compose.yml: worker-general passes through OUTBOX_DISPATCHER_REQUIRED"));
|
|
assert.ok(result.passes.some((pass) => pass === "docker-compose.yml: worker-general passes through WORKFLOW_TRIGGER_OUTBOX_REQUIRED"));
|
|
assert.ok(result.passes.some((pass) => pass === "docker-compose.yml: worker-general uses distinct WORKER_ADMIN_PORT=9201"));
|
|
assert.ok(result.passes.some((pass) => pass === "docker-compose.yml: worker-mail uses distinct WORKER_ADMIN_PORT=9202"));
|
|
assert.ok(result.passes.some((pass) => pass === "docker-compose.yml: worker-plugin uses distinct WORKER_ADMIN_PORT=9205"));
|
|
assert.ok(result.passes.some((pass) => pass === "docker-compose.staging.yml: worker-general forces OUTBOX_DISPATCHER_ENABLED=true"));
|
|
assert.ok(result.passes.some((pass) => pass === "docker-compose.staging.yml: worker-general forces OUTBOX_DISPATCHER_REQUIRED=true"));
|
|
assert.ok(result.passes.some((pass) => pass === "docker-compose.staging.yml: worker-general forces WORKFLOW_TRIGGER_OUTBOX_ENABLED=true"));
|
|
assert.ok(result.passes.some((pass) => pass === "docker-compose.staging.yml: worker-general forces WORKFLOW_TRIGGER_OUTBOX_REQUIRED=true"));
|
|
assert.ok(result.passes.some((pass) => pass === "docker-compose.yml: worker-general passes through LAGO_API_KEY for billing reconciliation"));
|
|
assert.ok(
|
|
result.passes.some((pass) => pass === "docker-compose.staging.yml: worker-general configures MOLLIE_PROFILE_ID for required billing reconciliation")
|
|
);
|
|
assert.ok(result.passes.some((pass) => pass === "docker-compose.yml: worker-general owns ClamAV dependency for file scanning"));
|
|
assert.ok(result.passes.some((pass) => pass === "docker-compose.yml: worker-general enables the fail-closed scan queue by default"));
|
|
assert.ok(result.passes.some((pass) => pass === "docker-compose.yml: worker-general passes through STORAGE_BUCKET for scan and preview storage"));
|
|
assert.ok(result.passes.some((pass) => pass === "docker-compose.staging.yml: worker-general forces SCAN_QUEUE_ENABLED=true"));
|
|
assert.ok(
|
|
result.passes.some(
|
|
(pass) => pass === "docker-compose.staging.yml: worker-general forces EXPORT_RETENTION_QUEUE_ENABLED=true for GDPR export hard-delete"
|
|
)
|
|
);
|
|
assert.ok(result.passes.some((pass) => pass === "docker-compose.staging.yml: worker-general requires STORAGE_BUCKET for GDPR export retention"));
|
|
assert.ok(
|
|
result.passes.some((pass) => pass === "docker-compose.staging.yml: web requires GDPR_EXPORT_SSE_C_KEY_BASE64 for encrypted GDPR export downloads")
|
|
);
|
|
assert.ok(result.passes.some((pass) => pass === "docker-compose.staging.yml: web requires REDIS_URL for distributed GDPR export download gates"));
|
|
assert.ok(result.passes.some((pass) => pass === "docker-compose.staging.yml: worker-import forces PORTAL_GDPR_EXPORT_QUEUE_ENABLED=true"));
|
|
assert.ok(
|
|
result.passes.some((pass) => pass === "docker-compose.staging.yml: worker-import requires GDPR_EXPORT_SSE_C_KEY_BASE64 for encrypted GDPR exports")
|
|
);
|
|
assert.ok(result.passes.some((pass) => pass === "docker-compose.yml: worker-mail depends on redis only and does not wait for ClamAV"));
|
|
assert.ok(result.passes.some((pass) => pass === "docker-compose.yml: worker-plugin depends on redis only and does not wait for ClamAV"));
|
|
});
|
|
|
|
it("fails with a path-specific message when Dockerfile is missing", () => {
|
|
const rootDir = createFixture({ remove: ["Dockerfile"] });
|
|
|
|
const result = checkRequiredFiles(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /Dockerfile: missing required Docker baseline file/);
|
|
});
|
|
|
|
it("fails when the deps target omits a workspace package manifest", () => {
|
|
const rootDir = createFixture({
|
|
dockerfile: VALID_DOCKERFILE.replace("COPY apps/api/package.json apps/api/package.json\n", ""),
|
|
});
|
|
|
|
const result = checkDockerfileDepsManifestContract(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /Dockerfile: deps target must copy apps\/api\/package\.json before pnpm install/);
|
|
});
|
|
|
|
it("fails when the compose worker topology omits the plugin worker group", () => {
|
|
const rootDir = createFixture({
|
|
dockerCompose: VALID_DOCKER_COMPOSE.replace(/\n worker-plugin:[\s\S]*?(?=\nvolumes:)/, ""),
|
|
dockerComposeStaging: VALID_DOCKER_COMPOSE_STAGING.replace(/\n worker-plugin:[\s\S]*?(?=\nvolumes:)/, ""),
|
|
});
|
|
|
|
const result = checkDockerComposeWorkerTopology(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /docker-compose\.yml: missing worker-plugin service for WORKER_QUEUE_GROUP=plugin/);
|
|
assert.match(result.failures.join("\n"), /docker-compose\.staging\.yml: missing worker-plugin staging overlay/);
|
|
});
|
|
|
|
it("fails when Hocuspocus restores dev dependencies or copies source maps into the runtime", () => {
|
|
const rootDir = createFixture({
|
|
hocuspocusDockerfile: VALID_HOCUSPOCUS_DOCKERFILE.replace(
|
|
"COPY --exclude=**/*.map --exclude=**/*.d.ts --from=hocuspocus-production-deps /prod/hocuspocus/node_modules /myapp/node_modules",
|
|
"COPY --from=build /myapp/node_modules /myapp/node_modules"
|
|
)
|
|
.replace(
|
|
"COPY --from=hocuspocus-runtime-artifacts /myapp/apps/hocuspocus/dist /myapp/apps/hocuspocus/dist",
|
|
"COPY --from=build /myapp/apps/hocuspocus/dist /myapp/apps/hocuspocus/dist"
|
|
)
|
|
.replace("RUN rm -rf /usr/local/lib/node_modules/corepack /usr/local/bin/pnpm /usr/local/bin/yarn", ""),
|
|
});
|
|
|
|
const result = checkHocuspocusDockerfileContract(rootDir);
|
|
|
|
expect(result.failures.join("\n")).toMatch(/final dependency COPY must use both --exclude/);
|
|
expect(result.failures.join("\n")).toMatch(/strip dist source maps/);
|
|
expect(result.failures.join("\n")).toMatch(/remove inherited Corepack, pnpm, and Yarn executables/);
|
|
});
|
|
|
|
it("fails when Hocuspocus dependency install is invalidated by application source", () => {
|
|
const rootDir = createFixture({
|
|
hocuspocusDockerfile: VALID_HOCUSPOCUS_DOCKERFILE.replace("COPY patches ./patches", "COPY patches ./patches\nADD apps apps\nADD prisma prisma"),
|
|
});
|
|
|
|
const result = checkHocuspocusDockerfileContract(rootDir);
|
|
|
|
expect(result.failures.join("\n")).toMatch(/deps target must not copy source inputs before install/);
|
|
});
|
|
|
|
it("fails when Hocuspocus source rebuilds rerun workspace resolution or unused database seed builds", () => {
|
|
const rootDir = createFixture({
|
|
hocuspocusDockerfile: VALID_HOCUSPOCUS_DOCKERFILE.replace(
|
|
"RUN ./node_modules/.bin/prisma generate --config prisma.config.ts",
|
|
"RUN pnpm exec prisma generate\nRUN pnpm --filter @hivegrid/db build"
|
|
),
|
|
});
|
|
|
|
const result = checkHocuspocusDockerfileContract(rootDir);
|
|
|
|
expect(result.failures.join("\n")).toMatch(/invoke pinned Prisma and Hocuspocus build scripts directly/);
|
|
});
|
|
|
|
it("fails when the shared build routes Prisma generation through pnpm workspace resolution", () => {
|
|
const rootDir = createFixture({
|
|
dockerfile: VALID_DOCKERFILE.replace(
|
|
"RUN ./node_modules/.bin/prisma generate --config prisma.config.ts",
|
|
"RUN pnpm --filter @hivegrid/db prisma:generate"
|
|
),
|
|
});
|
|
|
|
const result = checkDockerfileWebContract(rootDir);
|
|
|
|
expect(result.failures.join("\n")).toMatch(/shared build must invoke \.\/node_modules\/\.bin\/prisma generate/);
|
|
});
|
|
|
|
it("fails when the Docker DB build stops using the direct runtime artifact entry point", () => {
|
|
const rootDir = createFixture({
|
|
dockerfile: VALID_DOCKERFILE.replace("RUN node packages/db/scripts/build-runtime-artifacts.mjs", "RUN pnpm --filter @hivegrid/db build"),
|
|
});
|
|
|
|
const result = checkDockerfileWebContract(rootDir);
|
|
|
|
expect(result.failures.join("\n")).toMatch(/shared build must invoke node packages\/db\/scripts\/build-runtime-artifacts\.mjs directly/);
|
|
});
|
|
|
|
it("fails when Dockerfile syntax cannot support COPY --exclude", () => {
|
|
const rootDir = createFixture({
|
|
dockerfile: VALID_DOCKERFILE.replace("docker/dockerfile:1.19", "docker/dockerfile:1.18"),
|
|
hocuspocusDockerfile: VALID_HOCUSPOCUS_DOCKERFILE.replace("docker/dockerfile:1.19", "docker/dockerfile:1.18"),
|
|
});
|
|
|
|
const failures = verifyDockerBaseline(rootDir).failures.join("\n");
|
|
expect(failures).toMatch(/Dockerfile: must declare docker\/dockerfile syntax 1\.19 or newer/);
|
|
expect(failures).toMatch(/apps\/hocuspocus\/Dockerfile: must declare docker\/dockerfile syntax 1\.19 or newer/);
|
|
});
|
|
|
|
it("fails when locked closure deployment performs metadata tree walks", () => {
|
|
const rootDir = createFixture({
|
|
productionDepsHelper: VALID_PRODUCTION_DEPS_HELPER.replace(
|
|
"--filter @hivegrid/runtime-api deploy --prod --no-optional /prod/api",
|
|
"--filter @hivegrid/runtime-api deploy --prod --no-optional /prod/api\n find /prod/api/node_modules -name '*.map' -delete"
|
|
),
|
|
hocuspocusDockerfile: VALID_HOCUSPOCUS_DOCKERFILE.replace(
|
|
"&& pnpm --offline --config.inject-workspace-packages=true --filter @hivegrid/runtime-hocuspocus deploy --prod --no-optional /prod/hocuspocus",
|
|
"&& pnpm --offline --config.inject-workspace-packages=true --filter @hivegrid/runtime-hocuspocus deploy --prod --no-optional /prod/hocuspocus \\\n && find /prod/hocuspocus/node_modules -name '*.d.ts' -delete"
|
|
),
|
|
});
|
|
|
|
expect(checkDockerfileDepsManifestContract(rootDir).failures.join("\n")).toMatch(/must not run metadata find\/delete work while the locked/);
|
|
expect(checkHocuspocusDockerfileContract(rootDir).failures.join("\n")).toMatch(/without metadata find\/delete work in the locked RUN/);
|
|
});
|
|
|
|
it("fails for each missing DB runtime artifact build contract", () => {
|
|
const artifacts = [
|
|
["packages/db/src/permissions-seed.server.ts", "packages/db/dist/permissions-seed.server.js"],
|
|
["packages/db/src/deploy-permission-catalog-sync.server.ts", "packages/db/dist/deploy-permission-catalog-sync.server.js"],
|
|
["prisma/seed.ts", "packages/db/dist/production-seed.server.js"],
|
|
["scripts/seed-e2e-readiness.ts", "packages/db/dist/e2e-readiness-seed.server.js"],
|
|
];
|
|
|
|
for (const [entryPoint, outfile] of artifacts) {
|
|
const rootDir = createFixture({
|
|
dbRuntimeBuildScript: VALID_DB_RUNTIME_BUILD_SCRIPT.replace(outfile, `${outfile}.missing`),
|
|
});
|
|
const failures = checkDockerfileWebContract(rootDir).failures;
|
|
|
|
expect(failures).toContain(`packages/db/scripts/build-runtime-artifacts.mjs: must build ${outfile} from ${entryPoint}`);
|
|
}
|
|
});
|
|
|
|
it("fails when runtime HEALTHCHECK startup polling loses its fast cadence", () => {
|
|
const rootDir = createFixture({
|
|
dockerfile: VALID_DOCKERFILE.replaceAll("--start-interval=2s ", ""),
|
|
hocuspocusDockerfile: VALID_HOCUSPOCUS_DOCKERFILE.replace("--start-interval=2s ", ""),
|
|
});
|
|
|
|
const result = verifyDockerBaseline(rootDir);
|
|
const failures = result.failures.join("\n");
|
|
|
|
expect(failures).toMatch(/web HEALTHCHECK must use --start-interval=2s with steady --interval=30s and --retries=3/);
|
|
expect(failures).toMatch(/worker HEALTHCHECK must use --start-interval=2s with steady --interval=30s and --retries=3/);
|
|
expect(failures).toMatch(/api HEALTHCHECK must use --start-interval=2s with steady --interval=30s and --retries=3/);
|
|
expect(failures).toMatch(/MCP HEALTHCHECK must use --start-interval=2s with steady --interval=30s and --retries=3/);
|
|
expect(failures).toMatch(/Hocuspocus HEALTHCHECK must use --start-interval=2s with steady --interval=30s and --retries=3/);
|
|
});
|
|
|
|
it("fails when Hocuspocus production dependency deployment depends on application source", () => {
|
|
const rootDir = createFixture({
|
|
hocuspocusDockerfile: VALID_HOCUSPOCUS_DOCKERFILE.replace("FROM deps AS hocuspocus-production-deps", "FROM build AS hocuspocus-production-deps"),
|
|
});
|
|
|
|
const result = checkHocuspocusDockerfileContract(rootDir);
|
|
|
|
expect(result.failures.join("\n")).toMatch(/production-deps must derive from the manifest-only deps stage/);
|
|
});
|
|
|
|
it("fails when Hocuspocus bypasses the generated-client-aware Prisma runtime pruner", () => {
|
|
const rootDir = createFixture({
|
|
hocuspocusDockerfile: VALID_HOCUSPOCUS_DOCKERFILE.replace(
|
|
"RUN node /myapp/scripts/prune-prisma-client-runtime.mjs /prod/hocuspocus/node_modules /myapp/packages/db/generated/prisma/client/internal/class.ts",
|
|
"RUN rm -rf /prod/hocuspocus/node_modules/.pnpm/@prisma+client@*/node_modules/@prisma/client/generator-build"
|
|
),
|
|
});
|
|
|
|
const result = checkHocuspocusDockerfileContract(rootDir);
|
|
|
|
expect(result.failures.join("\n")).toMatch(/generated-client-aware Prisma runtime pruner/);
|
|
});
|
|
|
|
it("fails when deployed runtime closures retain source artifacts or remote services retain package managers", () => {
|
|
const rootDir = createFixture({
|
|
dockerfile: VALID_DOCKERFILE.replace(
|
|
"COPY --exclude=**/*.map --exclude=**/*.d.ts --from=api-production-deps /prod/api/node_modules /myapp/node_modules",
|
|
"COPY --from=api-production-deps /prod/api/node_modules /myapp/node_modules"
|
|
).replaceAll("RUN rm -rf /usr/local/lib/node_modules/corepack /usr/local/bin/pnpm /usr/local/bin/yarn", ""),
|
|
});
|
|
|
|
const result = checkDockerfileDepsManifestContract(rootDir);
|
|
|
|
expect(result.failures.join("\n")).toMatch(/api target dependency COPY must use both --exclude/);
|
|
expect(result.failures.join("\n")).toMatch(/api target must remove inherited Corepack, pnpm, and Yarn executables/);
|
|
});
|
|
|
|
it("fails when the worker keeps optional peer closure orphans", () => {
|
|
const rootDir = createFixture({
|
|
dockerfile: VALID_DOCKERFILE.replace(
|
|
"RUN node /myapp/scripts/prune-pnpm-runtime-closure.mjs /prod/worker/node_modules prisma typescript",
|
|
"RUN rm -rf /prod/worker/node_modules/.pnpm/prisma@* /prod/worker/node_modules/.pnpm/typescript@*"
|
|
),
|
|
});
|
|
|
|
const result = checkDockerfileDepsManifestContract(rootDir);
|
|
|
|
expect(result.failures.join("\n")).toMatch(/prune optional Prisma and TypeScript peer closures by reachability/);
|
|
});
|
|
|
|
it("fails when non-scanning workers inherit the ClamAV dependency", () => {
|
|
const rootDir = createFixture({
|
|
dockerCompose: VALID_DOCKER_COMPOSE.replace(
|
|
/ worker-mail:\n <<: \*worker-base\n environment:[\s\S]*? healthcheck:/,
|
|
` worker-mail:
|
|
<<: *worker-base
|
|
environment:
|
|
- WORKER_QUEUE_GROUP=mail
|
|
- WORKER_CONCURRENCY_MAIL=\${WORKER_CONCURRENCY_MAIL:-5}
|
|
- WORKER_HEALTH_PORT=9102
|
|
- WORKER_ADMIN_PORT=9202
|
|
healthcheck:`
|
|
),
|
|
});
|
|
|
|
const result = checkDockerComposeWorkerTopology(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /docker-compose\.yml: worker-mail must override worker-base depends_on so it does not inherit clamav/);
|
|
});
|
|
|
|
it("fails when the scan worker defaults off or omits its storage contract", () => {
|
|
const rootDir = createFixture({
|
|
dockerCompose: VALID_DOCKER_COMPOSE.replace(
|
|
"SCAN_QUEUE_ENABLED=\${SCAN_QUEUE_ENABLED:-true}",
|
|
"SCAN_QUEUE_ENABLED=\${SCAN_QUEUE_ENABLED:-false}"
|
|
).replace(" - STORAGE_BUCKET=\${STORAGE_BUCKET:-}\n", ""),
|
|
dockerComposeStaging: VALID_DOCKER_COMPOSE_STAGING.replace(" - SCAN_QUEUE_ENABLED=true\n", ""),
|
|
});
|
|
|
|
const result = checkDockerComposeWorkerTopology(rootDir);
|
|
const failures = result.failures.join("\n");
|
|
|
|
expect(failures).toMatch(/default SCAN_QUEUE_ENABLED to true/);
|
|
expect(failures).toMatch(/pass through STORAGE_BUCKET/);
|
|
expect(failures).toMatch(/force SCAN_QUEUE_ENABLED=true/);
|
|
});
|
|
|
|
it("fails when the staging web runtime omits encrypted GDPR export storage", () => {
|
|
const rootDir = createFixture({
|
|
dockerComposeStaging: VALID_DOCKER_COMPOSE_STAGING.replace(" - STORAGE_ENDPOINT=\${STORAGE_ENDPOINT:?required}\n", "").replace(
|
|
" - GDPR_EXPORT_SSE_C_KEY_BASE64=\${GDPR_EXPORT_SSE_C_KEY_BASE64:?required}\n",
|
|
""
|
|
),
|
|
});
|
|
|
|
const result = checkDockerComposeWorkerTopology(rootDir);
|
|
const failures = result.failures.join("\n");
|
|
|
|
expect(failures).toMatch(/web must require STORAGE_ENDPOINT for encrypted GDPR export downloads/);
|
|
expect(failures).toMatch(/web must require GDPR_EXPORT_SSE_C_KEY_BASE64 for encrypted GDPR export downloads/);
|
|
});
|
|
|
|
it("fails when the staging web runtime omits the distributed GDPR download Redis contract", () => {
|
|
const rootDir = createFixture({
|
|
dockerComposeStaging: VALID_DOCKER_COMPOSE_STAGING.replace(" - REDIS_URL=\${REDIS_URL:?required}\n", ""),
|
|
});
|
|
const result = checkDockerComposeWorkerTopology(rootDir);
|
|
expect(result.failures.join("\n")).toMatch(/web must require REDIS_URL for distributed GDPR export download gates/);
|
|
});
|
|
|
|
it("fails when staging GDPR consumers reuse shared storage credential sources", () => {
|
|
const rootDir = createFixture({
|
|
dockerComposeStaging: VALID_DOCKER_COMPOSE_STAGING
|
|
.replace("WEB_STORAGE_ACCESS_KEY_ID:?required", "STORAGE_ACCESS_KEY_ID:?required")
|
|
.replace("WORKER_GENERAL_STORAGE_ACCESS_KEY_ID:?required", "STORAGE_ACCESS_KEY_ID:?required")
|
|
.replace("WORKER_IMPORT_STORAGE_ACCESS_KEY_ID:?required", "STORAGE_ACCESS_KEY_ID:?required"),
|
|
});
|
|
|
|
const result = checkDockerComposeWorkerTopology(rootDir);
|
|
const failures = result.failures.join("\n");
|
|
|
|
expect(failures).toMatch(/web must require STORAGE_ACCESS_KEY_ID for encrypted GDPR export downloads/);
|
|
expect(failures).toMatch(/worker-general must require STORAGE_ACCESS_KEY_ID for GDPR export retention/);
|
|
expect(failures).toMatch(/worker-import must require STORAGE_ACCESS_KEY_ID for encrypted GDPR exports/);
|
|
});
|
|
|
|
it("fails when worker-general omits automatic GDPR export retention storage", () => {
|
|
const rootDir = createFixture({
|
|
dockerCompose: VALID_DOCKER_COMPOSE.replace(" - EXPORT_RETENTION_QUEUE_ENABLED=\${EXPORT_RETENTION_QUEUE_ENABLED:-false}\n", ""),
|
|
dockerComposeStaging: VALID_DOCKER_COMPOSE_STAGING.replace(" - EXPORT_RETENTION_QUEUE_ENABLED=true\n", "").replaceAll(
|
|
" - STORAGE_BUCKET=\${STORAGE_BUCKET:?required}\n",
|
|
""
|
|
),
|
|
});
|
|
|
|
const result = checkDockerComposeWorkerTopology(rootDir);
|
|
const failures = result.failures.join("\n");
|
|
|
|
expect(failures).toMatch(/worker-general must pass through EXPORT_RETENTION_QUEUE_ENABLED/);
|
|
expect(failures).toMatch(/worker-general must force EXPORT_RETENTION_QUEUE_ENABLED=true/);
|
|
expect(failures).toMatch(/worker-general must require STORAGE_BUCKET for GDPR export retention/);
|
|
});
|
|
|
|
it("fails when worker-general omits required tenant hard-delete search cleanup configuration", () => {
|
|
const rootDir = createFixture({
|
|
dockerCompose: VALID_DOCKER_COMPOSE.replace(" - MEILI_URL=\${MEILI_URL:-}\n", ""),
|
|
dockerComposeStaging: VALID_DOCKER_COMPOSE_STAGING.replace(" - MEILI_API_KEY=\${MEILI_API_KEY:?required}\n", ""),
|
|
});
|
|
|
|
const result = checkDockerComposeWorkerTopology(rootDir);
|
|
const failures = result.failures.join("\n");
|
|
|
|
expect(failures).toMatch(/worker-general must pass through MEILI_URL for tenant hard-delete search cleanup/);
|
|
expect(failures).toMatch(/worker-general must require MEILI_API_KEY for tenant hard-delete search cleanup/);
|
|
});
|
|
|
|
it("fails when worker-import omits its dedicated encrypted GDPR export contract", () => {
|
|
const rootDir = createFixture({
|
|
dockerCompose: VALID_DOCKER_COMPOSE.replace(" - GDPR_EXPORT_SSE_C_KEY_BASE64=\${GDPR_EXPORT_SSE_C_KEY_BASE64:-}\n", ""),
|
|
dockerComposeStaging: VALID_DOCKER_COMPOSE_STAGING.replace(" - PORTAL_GDPR_EXPORT_QUEUE_ENABLED=true\n", "").replace(
|
|
" - IMPORT_QUEUE_RUNTIME_READY=false\n - STORAGE_ENDPOINT=\${STORAGE_ENDPOINT:?required}\n",
|
|
" - IMPORT_QUEUE_RUNTIME_READY=false\n"
|
|
),
|
|
});
|
|
|
|
const result = checkDockerComposeWorkerTopology(rootDir);
|
|
const failures = result.failures.join("\n");
|
|
|
|
expect(failures).toMatch(/worker-import must pass through GDPR_EXPORT_SSE_C_KEY_BASE64 for encrypted GDPR exports/);
|
|
expect(failures).toMatch(/worker-import must force PORTAL_GDPR_EXPORT_QUEUE_ENABLED=true/);
|
|
expect(failures).toMatch(/worker-import must require STORAGE_ENDPOINT for encrypted GDPR exports/);
|
|
});
|
|
|
|
it("fails when a worker group omits its distinct admin port", () => {
|
|
const rootDir = createFixture({
|
|
dockerCompose: VALID_DOCKER_COMPOSE.replace(" - WORKER_ADMIN_PORT=9202\n", ""),
|
|
});
|
|
|
|
const result = checkDockerComposeWorkerTopology(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /docker-compose\.yml: worker-mail must set WORKER_ADMIN_PORT=9202 distinct from WORKER_HEALTH_PORT=9102/);
|
|
});
|
|
|
|
it("fails when dependency targets copy full workspace source or restore a generic runtime closure", () => {
|
|
const rootDir = createFixture({
|
|
dockerfile: VALID_DOCKERFILE.replace("COPY apps/api/package.json apps/api/package.json\n", "ADD apps apps\n")
|
|
.replace("FROM production-deps-bundle AS web-production-deps", "FROM build AS web-production-deps")
|
|
.concat("\nFROM base AS production-deps\nCOPY --from=deps /myapp/node_modules /myapp/node_modules\n"),
|
|
});
|
|
|
|
const result = checkDockerfileDepsManifestContract(rootDir);
|
|
|
|
assert.match(
|
|
result.failures.join("\n"),
|
|
/Dockerfile: deps target must copy workspace package manifests, not full apps or packages source, before pnpm install/
|
|
);
|
|
assert.match(result.failures.join("\n"), /Dockerfile: web-production-deps must derive from production-deps-bundle/);
|
|
assert.match(result.failures.join("\n"), /Dockerfile: must not build or copy a generic whole-workspace production node_modules closure/);
|
|
});
|
|
|
|
it("fails when the production dependency bundle does not own one locked warm-v2 helper invocation", () => {
|
|
const rootDir = createFixture({
|
|
dockerfile: VALID_DOCKERFILE.replace("FROM build AS production-deps-bundle", "FROM deps AS production-deps-bundle").replace(
|
|
"id=hivegrid-pnpm-store-v2,target=/pnpm/store,sharing=locked",
|
|
"id=hivegrid-pnpm-store-v3,target=/pnpm/store,sharing=shared"
|
|
),
|
|
});
|
|
|
|
const result = checkDockerfileDepsManifestContract(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /production-deps-bundle must derive from build and contain only one locked hivegrid-pnpm-store-v2 invocation/);
|
|
});
|
|
|
|
it("fails when a target-specific dependency stage bypasses the bundle or adds work beyond prune and SBOM", () => {
|
|
const rootDir = createFixture({
|
|
dockerfile: VALID_DOCKERFILE.replace("FROM production-deps-bundle AS api-production-deps", "FROM build AS api-production-deps").replace(
|
|
"RUN node /myapp/scripts/prune-prisma-client-runtime.mjs /prod/api/node_modules",
|
|
"RUN echo unexpected\nRUN node /myapp/scripts/prune-prisma-client-runtime.mjs /prod/api/node_modules"
|
|
),
|
|
});
|
|
|
|
const result = checkDockerfileDepsManifestContract(rootDir);
|
|
|
|
assert.match(
|
|
result.failures.join("\n"),
|
|
/api-production-deps must derive from production-deps-bundle and contain only its required post-deploy prune and SBOM instructions/
|
|
);
|
|
});
|
|
|
|
it("fails when helper deploy flags differ from the five exact runtime closure commands", () => {
|
|
const rootDir = createFixture({
|
|
productionDepsHelper: VALID_PRODUCTION_DEPS_HELPER.replace(
|
|
"@hivegrid/runtime-portal-domain-controller deploy --prod --no-optional /prod/portal-domain-controller",
|
|
"@hivegrid/runtime-portal-domain-controller deploy --prod /prod/portal-domain-controller"
|
|
).replace("@hivegrid/runtime-worker deploy --prod /prod/worker", "@hivegrid/runtime-worker deploy --prod --no-optional /prod/worker"),
|
|
});
|
|
|
|
const result = checkDockerfileDepsManifestContract(rootDir);
|
|
const failures = result.failures.join("\n");
|
|
|
|
assert.match(failures, /deploy_portal_controller must run exactly .*deploy --prod --no-optional \/prod\/portal-domain-controller/);
|
|
assert.match(failures, /deploy_worker must run exactly .*deploy --prod \/prod\/worker/);
|
|
});
|
|
|
|
it("fails when helper concurrency drops PID collection or complete failure propagation", () => {
|
|
const rootDir = createFixture({
|
|
productionDepsHelper: VALID_PRODUCTION_DEPS_HELPER.replace(' pids+=("$!")\n', "").replace(
|
|
' if [[ "$status" -eq 0 ]]; then\n status="$child_status"\n fi',
|
|
' status="$child_status"'
|
|
),
|
|
});
|
|
|
|
const result = checkDockerfileDepsManifestContract(rootDir);
|
|
|
|
assert.match(
|
|
result.failures.join("\n"),
|
|
/run_wave must collect all five background PIDs, wait for every child, preserve the first non-zero status, and return after all children are reaped/
|
|
);
|
|
});
|
|
|
|
it("fails when the fallback cleans broadly or skips exact target cleanup", () => {
|
|
const rootDir = createFixture({
|
|
productionDepsHelper: VALID_PRODUCTION_DEPS_HELPER.replace('for target in "${targets[@]}"; do\n rm -rf -- "/prod/${target}"\ndone', "rm -rf -- /prod/*"),
|
|
});
|
|
|
|
const result = checkDockerfileDepsManifestContract(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /clean exactly the five \/prod targets on failure/);
|
|
});
|
|
|
|
it("fails when the fallback fetches more than once or skips the complete retry wave", () => {
|
|
const rootDir = createFixture({
|
|
productionDepsHelper: VALID_PRODUCTION_DEPS_HELPER.replace(
|
|
"if pnpm fetch --frozen-lockfile; then",
|
|
"pnpm fetch --frozen-lockfile\nif pnpm fetch --frozen-lockfile; then"
|
|
).replace("run_wave after-fetch || retry_status=$?", "retry_status=0"),
|
|
});
|
|
|
|
const result = checkDockerfileDepsManifestContract(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /fetch once, retry one complete wave/);
|
|
});
|
|
|
|
it("fails when the fallback uses a lockfile sentinel or masks fetch failure", () => {
|
|
const rootDir = createFixture({
|
|
productionDepsHelper: VALID_PRODUCTION_DEPS_HELPER.replace(
|
|
"if pnpm fetch --frozen-lockfile; then",
|
|
"lockfile_hash=sentinel\nif pnpm fetch --frozen-lockfile || true; then"
|
|
),
|
|
});
|
|
|
|
const result = checkDockerfileDepsManifestContract(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /propagate fetch or retry failure without a sentinel/);
|
|
});
|
|
|
|
it("fails when the deps target installs without a BuildKit pnpm store cache", () => {
|
|
const rootDir = createFixture({
|
|
dockerfile: VALID_DOCKERFILE.replace(
|
|
"RUN --mount=type=cache,id=hivegrid-pnpm-store,target=/pnpm/store \\\n pnpm install --frozen-lockfile --prod=false --offline",
|
|
"RUN pnpm install --frozen-lockfile --prod=false"
|
|
),
|
|
});
|
|
|
|
const result = checkDockerfileDepsManifestContract(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /Dockerfile: deps target must use BuildKit cache mount for \/pnpm\/store during pnpm install/);
|
|
});
|
|
|
|
it("fails when the deps target does not fetch before offline install", () => {
|
|
const rootDir = createFixture({
|
|
dockerfile: VALID_DOCKERFILE.replace("RUN --mount=type=cache,id=hivegrid-pnpm-store,target=/pnpm/store \\\n pnpm fetch --frozen-lockfile\n", ""),
|
|
});
|
|
|
|
const result = checkDockerfileDepsManifestContract(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /Dockerfile: deps target must run pnpm fetch --frozen-lockfile with BuildKit cache before install/);
|
|
});
|
|
|
|
it("fails when .dockerignore broadly excludes source build directories", () => {
|
|
const rootDir = createFixture({
|
|
dockerignore: [
|
|
"node_modules",
|
|
"/docs",
|
|
".cache",
|
|
"**/.cache",
|
|
".eslintcache",
|
|
"**/.eslintcache",
|
|
"coverage",
|
|
"**/coverage",
|
|
".nyc_output",
|
|
"**/.nyc_output",
|
|
"reports",
|
|
"**/reports",
|
|
"public/build",
|
|
"/build",
|
|
"**/build",
|
|
"apps/*/dist",
|
|
"packages/*/dist",
|
|
"playwright-report",
|
|
"**/playwright-report",
|
|
"test-results",
|
|
"**/test-results",
|
|
"blob-report",
|
|
"**/blob-report",
|
|
"",
|
|
].join("\n"),
|
|
});
|
|
|
|
const result = checkDockerignoreContract(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /\.dockerignore: must not use broad \*\*\/build/);
|
|
});
|
|
|
|
it("fails when .dockerignore omits generated build ignores", () => {
|
|
const rootDir = createFixture({
|
|
dockerignore: ["node_modules", "apps/*/dist", "packages/*/dist", ""].join("\n"),
|
|
});
|
|
|
|
const result = checkDockerignoreContract(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /\.dockerignore: must ignore generated public\/build/);
|
|
assert.match(result.failures.join("\n"), /\.dockerignore: must ignore generated \/build/);
|
|
assert.match(result.failures.join("\n"), /\.dockerignore: must ignore generated \/apps\/\*\/build/);
|
|
});
|
|
|
|
it("fails when .dockerignore omits Docker context artifact ignores", () => {
|
|
const rootDir = createFixture({
|
|
dockerignore: ["node_modules", "public/build", "/build", "/apps/*/build", "apps/*/dist", "packages/*/dist", ""].join("\n"),
|
|
});
|
|
|
|
const result = checkDockerignoreContract(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /\.dockerignore: must ignore Docker context artifact \/docs/);
|
|
assert.match(result.failures.join("\n"), /\.dockerignore: must ignore Docker context artifact \/secrets/);
|
|
assert.match(result.failures.join("\n"), /\.dockerignore: must ignore Docker context artifact \/\.gitea/);
|
|
assert.match(result.failures.join("\n"), /\.dockerignore: must ignore Docker context artifact \*\*\/\*\.test\.tsx/);
|
|
assert.match(result.failures.join("\n"), /\.dockerignore: must ignore Docker context artifact \*\*\/coverage/);
|
|
assert.match(result.failures.join("\n"), /\.dockerignore: must ignore Docker context artifact \*\*\/reports/);
|
|
assert.match(result.failures.join("\n"), /\.dockerignore: must ignore Docker context artifact \*\*\/blob-report/);
|
|
assert.match(result.failures.join("\n"), /\.dockerignore: must ignore Docker context artifact \*\*\/\.nyc_output/);
|
|
});
|
|
|
|
it("fails when .dockerignore broadly excludes cache or report source directories", () => {
|
|
const rootDir = createFixture({
|
|
dockerignore: [
|
|
"node_modules",
|
|
"/docs",
|
|
".cache",
|
|
"**/.cache",
|
|
"public/build",
|
|
"/build",
|
|
"/apps/*/build",
|
|
"apps/*/dist",
|
|
"packages/*/dist",
|
|
"**/cache",
|
|
"**/report",
|
|
"coverage",
|
|
"**/coverage",
|
|
".nyc_output",
|
|
"**/.nyc_output",
|
|
"reports",
|
|
"**/reports",
|
|
"playwright-report",
|
|
"**/playwright-report",
|
|
"test-results",
|
|
"**/test-results",
|
|
"blob-report",
|
|
"**/blob-report",
|
|
"",
|
|
].join("\n"),
|
|
});
|
|
|
|
const result = checkDockerignoreContract(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /\.dockerignore: must not use broad \*\*\/cache/);
|
|
assert.match(result.failures.join("\n"), /\.dockerignore: must not use broad \*\*\/report/);
|
|
});
|
|
|
|
it("fails when start.sh does not exec the production start process", () => {
|
|
const rootDir = createFixture({ startScript: "#!/bin/sh\npnpm start\n" });
|
|
|
|
const result = checkStartScript(rootDir);
|
|
|
|
assert.match(
|
|
result.failures.join("\n"),
|
|
/start\.sh: must use "exec node build\/server-runtime\/server\.js" so container signals reach the web process without pnpm indirection/
|
|
);
|
|
});
|
|
|
|
it("fails when start.sh drops the one-shot migration and runtime credential gates", () => {
|
|
const rootDir = createFixture({ startScript: "#!/bin/sh\nexec node build/server-runtime/server.js\n" });
|
|
|
|
const result = checkStartScript(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /start\.sh: must set -eu/);
|
|
assert.match(result.failures.join("\n"), /DIRECT_URL-only "migrate" mode/);
|
|
assert.match(result.failures.join("\n"), /must reject SEED_/);
|
|
assert.match(result.failures.join("\n"), /must require hivegrid_app with RLS required/);
|
|
});
|
|
|
|
it("fails when start.sh delegates the one-shot migration to a package manager", () => {
|
|
const rootDir = createFixture({
|
|
startScript: [
|
|
"#!/bin/sh",
|
|
"set -eu",
|
|
'if [ "${1:-}" = "migrate" ]; then',
|
|
' echo "DIRECT_URL is required for the one-shot migrator"',
|
|
' echo "DATABASE_URL must not be forwarded to the one-shot migrator"',
|
|
' echo "seed credentials and flags must not be forwarded to the one-shot migrator"',
|
|
" node /myapp/scripts/verify-migrator-role.mjs",
|
|
" node /myapp/scripts/adopt-prisma-baseline.mjs --adopt",
|
|
' export DATABASE_URL="$DIRECT_URL"',
|
|
" pnpm exec prisma migrate deploy --config prisma.config.ts",
|
|
"fi",
|
|
'echo "RLS_RUNTIME_MODE=required is mandatory for the web runtime"',
|
|
'echo "DATABASE_RUNTIME_ROLE=hivegrid_app is mandatory for the web runtime"',
|
|
'echo "DIRECT_URL must not be forwarded to the web runtime"',
|
|
'echo "MIGRATOR_* credentials must not be forwarded to the web runtime"',
|
|
'echo "MIGRATE_ON_BOOT is forbidden"',
|
|
'echo "SEED_ON_BOOT is forbidden"',
|
|
"exec node build/server-runtime/server.js",
|
|
].join("\n"),
|
|
});
|
|
|
|
const result = checkStartScript(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /without package-manager indirection/);
|
|
});
|
|
|
|
it("fails when start.sh skips the exact migrator role probe", () => {
|
|
const rootDir = createFixture();
|
|
const startPath = path.join(rootDir, "start.sh");
|
|
fs.writeFileSync(startPath, fs.readFileSync(startPath, "utf8").replace(" node /myapp/scripts/verify-migrator-role.mjs\n", ""));
|
|
|
|
const result = checkStartScript(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /verify the exact non-superuser migrator role/);
|
|
});
|
|
|
|
it("fails when start.sh skips post-migration Prisma schema verification", () => {
|
|
const rootDir = createFixture();
|
|
const startPath = path.join(rootDir, "start.sh");
|
|
fs.writeFileSync(
|
|
startPath,
|
|
fs.readFileSync(startPath, "utf8").replace(" node /myapp/scripts/adopt-prisma-baseline.mjs --verify-current-schema\n", "")
|
|
);
|
|
|
|
const result = checkStartScript(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /current schema verification/);
|
|
});
|
|
|
|
it("fails when start.sh verifies the current Prisma schema before migrations", () => {
|
|
const rootDir = createFixture();
|
|
const startPath = path.join(rootDir, "start.sh");
|
|
const verifyLine = " node /myapp/scripts/adopt-prisma-baseline.mjs --verify-current-schema\n";
|
|
const source = fs.readFileSync(startPath, "utf8");
|
|
fs.writeFileSync(startPath, source.replace(verifyLine, "").replace(" node /myapp/scripts/adopt-prisma-baseline.mjs --adopt\n", `$&${verifyLine}`));
|
|
|
|
const result = checkStartScript(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /verify the exact non-superuser migrator role/);
|
|
});
|
|
|
|
it("fails when start.sh skips the post-migration permission catalog sync", () => {
|
|
const rootDir = createFixture();
|
|
const startPath = path.join(rootDir, "start.sh");
|
|
fs.writeFileSync(startPath, fs.readFileSync(startPath, "utf8").replace(" node /myapp/packages/db/dist/deploy-permission-catalog-sync.server.js\n", ""));
|
|
|
|
const result = checkStartScript(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /must run the bundled permission catalog sync/);
|
|
});
|
|
|
|
it("fails when start.sh moves the permission catalog sync outside one-shot migrate mode", () => {
|
|
const rootDir = createFixture();
|
|
const startPath = path.join(rootDir, "start.sh");
|
|
const syncLine = " node /myapp/packages/db/dist/deploy-permission-catalog-sync.server.js\n";
|
|
const source = fs.readFileSync(startPath, "utf8");
|
|
fs.writeFileSync(startPath, source.replace(syncLine, "").replace(" exit 0\nfi\n", ` exit 0\nfi\n${syncLine}`));
|
|
|
|
const result = checkStartScript(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /exactly once inside one-shot migrate mode/);
|
|
});
|
|
|
|
it("fails when the database build omits the deploy permission catalog artifact", () => {
|
|
const rootDir = createFixture({
|
|
dbPackage: {
|
|
name: "@hivegrid/db",
|
|
scripts: { build: "esbuild packages/db/src/permissions-seed.server.ts --bundle --platform=node --format=esm" },
|
|
},
|
|
});
|
|
|
|
const result = checkDbManifest(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /must bundle deploy-permission-catalog-sync\.server\.ts/);
|
|
});
|
|
|
|
it("fails when the root production start script uses tsx", () => {
|
|
const rootDir = createFixture({
|
|
rootPackage: {
|
|
private: true,
|
|
scripts: {
|
|
build: "vite build && node scripts/build-web-server.mjs",
|
|
lint: "eslint .",
|
|
test: "pnpm --filter @hivegrid/web test",
|
|
typecheck: "tsc -b",
|
|
start: "tsx server/server.ts",
|
|
},
|
|
},
|
|
});
|
|
|
|
const result = verifyDockerBaseline(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /package\.json: script "start" must delegate to @hivegrid\/web/);
|
|
assert.match(result.failures.join("\n"), /package\.json: script "start" must not use tsx for web production runtime/);
|
|
});
|
|
|
|
it("fails when Dockerfile does not define the named web target", () => {
|
|
const rootDir = createFixture({
|
|
dockerfile: VALID_DOCKERFILE.replace("FROM base AS web", "FROM base"),
|
|
});
|
|
|
|
const result = checkDockerfileWebContract(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /Dockerfile: must define web target with "FROM base AS web"/);
|
|
});
|
|
|
|
it("fails when the web healthcheck omits the canonical host header", () => {
|
|
const rootDir = createFixture({
|
|
dockerfile: VALID_DOCKERFILE.replace("headers:{host:process.env.WEB_APP_HOST}", "headers:{}"),
|
|
});
|
|
|
|
const result = checkDockerfileWebContract(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /web target HEALTHCHECK must send WEB_APP_HOST for host-aware routing/);
|
|
});
|
|
|
|
it("fails when Dockerfile does not define the named api target", () => {
|
|
const rootDir = createFixture({
|
|
dockerfile: VALID_DOCKERFILE.replace("FROM base AS api", "FROM base"),
|
|
});
|
|
|
|
const result = checkDockerfileApiContract(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /Dockerfile: must define api target with "FROM base AS api"/);
|
|
});
|
|
|
|
it("fails when Dockerfile does not define the named worker target", () => {
|
|
const rootDir = createFixture({
|
|
dockerfile: VALID_DOCKERFILE.replace("FROM base AS worker", "FROM base"),
|
|
});
|
|
|
|
const result = checkDockerfileWorkerContract(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /Dockerfile: must define worker target with "FROM base AS worker"/);
|
|
});
|
|
|
|
it("fails when runtime images use recursive chown", () => {
|
|
const rootDir = createFixture({
|
|
dockerfile: VALID_DOCKERFILE.replace(
|
|
"HEALTHCHECK --interval=30s --timeout=10s --start-period=20s --start-interval=2s --retries=3 CMD curl",
|
|
"RUN chown -R node:node /myapp\nHEALTHCHECK --interval=30s --timeout=10s --start-period=20s --start-interval=2s --retries=3 CMD curl"
|
|
)
|
|
.replace(
|
|
"HEALTHCHECK --interval=30s --timeout=10s --start-period=20s --start-interval=2s --retries=3 CMD node apps/worker",
|
|
"RUN chown -R node:node /myapp\nHEALTHCHECK --interval=30s --timeout=10s --start-period=20s --start-interval=2s --retries=3 CMD node apps/worker"
|
|
)
|
|
.replace('ENTRYPOINT [ "./start.sh" ]', 'RUN chown -R node:node /myapp\nENTRYPOINT [ "./start.sh" ]'),
|
|
});
|
|
|
|
assert.match(checkDockerfileWebContract(rootDir).failures.join("\n"), /web target must not use recursive chown/);
|
|
assert.match(checkDockerfileApiContract(rootDir).failures.join("\n"), /api target must not use recursive chown/);
|
|
assert.match(checkDockerfileWorkerContract(rootDir).failures.join("\n"), /worker target must not use recursive chown/);
|
|
});
|
|
|
|
it("fails when Dockerfile misses the worker dist copy", () => {
|
|
const rootDir = createFixture({
|
|
dockerfile: VALID_DOCKERFILE.replace("COPY --from=worker-runtime-artifacts /myapp/apps/worker/dist /myapp/apps/worker/dist\n", ""),
|
|
});
|
|
|
|
const result = checkDockerfileWorkerContract(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /Dockerfile: worker target must copy \/myapp\/apps\/worker\/dist/);
|
|
});
|
|
|
|
it("fails when Dockerfile worker target uses tsx at runtime", () => {
|
|
const rootDir = createFixture({
|
|
dockerfile: VALID_DOCKERFILE.replace('CMD [ "node", "apps/worker/dist/index.js" ]', 'CMD [ "tsx", "apps/worker/src/index.ts" ]'),
|
|
});
|
|
|
|
const result = checkDockerfileWorkerContract(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /Dockerfile: worker target must invoke node apps\/worker\/dist\/index\.js directly/);
|
|
assert.match(result.failures.join("\n"), /Dockerfile: worker target must not use tsx at runtime/);
|
|
});
|
|
|
|
it("fails when Dockerfile misses the API target-specific dependency closure", () => {
|
|
const rootDir = createFixture({
|
|
dockerfile: VALID_DOCKERFILE.replace(
|
|
"COPY --exclude=**/*.map --exclude=**/*.d.ts --from=api-production-deps /prod/api/node_modules /myapp/node_modules\n",
|
|
""
|
|
),
|
|
});
|
|
|
|
const result = checkDockerfileApiContract(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /Dockerfile: api target must copy \/myapp\/target-specific node_modules/);
|
|
});
|
|
|
|
it("fails when Dockerfile api target uses tsx at runtime", () => {
|
|
const rootDir = createFixture({
|
|
dockerfile: VALID_DOCKERFILE.replace('CMD [ "node", "apps/api/dist/server.js" ]', 'CMD [ "tsx", "apps/api/src/server.ts" ]'),
|
|
});
|
|
|
|
const result = checkDockerfileApiContract(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /Dockerfile: api target must invoke node apps\/api\/dist\/server\.js directly/);
|
|
assert.match(result.failures.join("\n"), /Dockerfile: api target must not use tsx at runtime/);
|
|
});
|
|
|
|
it("fails when Dockerfile misses the server-runtime copy", () => {
|
|
const rootDir = createFixture({
|
|
dockerfile: VALID_DOCKERFILE.replace("COPY --from=web-runtime-artifacts /myapp/build/server-runtime /myapp/build/server-runtime\n", ""),
|
|
});
|
|
|
|
const result = checkDockerfileWebContract(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /Dockerfile: web target must copy \/myapp\/build\/server-runtime from web-runtime-artifacts stage/);
|
|
});
|
|
|
|
it("fails when Dockerfile does not remove sourcemaps before runtime artifact copies", () => {
|
|
const rootDir = createFixture({
|
|
dockerfile: VALID_DOCKERFILE.replace("RUN find /myapp/build /myapp/apps/web/build /myapp/apps/web/build-portal -type f -name '*.map' -delete\n", ""),
|
|
});
|
|
|
|
const result = checkDockerfileWebContract(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /Dockerfile: web\/api\/worker runtime artifact targets must delete their own \*\.map files before runtime COPY/);
|
|
});
|
|
|
|
it("fails when the shared build stage runs app builds instead of split build targets", () => {
|
|
const rootDir = createFixture({
|
|
dockerfile: VALID_DOCKERFILE.replace(
|
|
"RUN node packages/db/scripts/build-runtime-artifacts.mjs\n",
|
|
"RUN node packages/db/scripts/build-runtime-artifacts.mjs\nRUN pnpm build\n"
|
|
),
|
|
});
|
|
|
|
const result = checkDockerfileWebContract(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /Dockerfile: build target must not run root\/web\/api\/worker app builds/);
|
|
});
|
|
|
|
it("fails when the shared build stage owns Vite build args", () => {
|
|
const rootDir = createFixture({
|
|
dockerfile: VALID_DOCKERFILE.replace(
|
|
"RUN ./node_modules/.bin/prisma generate --config prisma.config.ts\n",
|
|
'ARG VITE_SENTRY_DSN_WEB=""\nRUN ./node_modules/.bin/prisma generate --config prisma.config.ts\n'
|
|
),
|
|
});
|
|
|
|
const result = checkDockerfileWebContract(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /Dockerfile: build target must not declare VITE_\* ARG\/ENV values/);
|
|
});
|
|
|
|
it("fails when the web build does not forward the public Hocuspocus URL", () => {
|
|
const rootDir = createFixture({
|
|
dockerfile: VALID_DOCKERFILE.replace('ARG VITE_HOCUSPOCUS_URL=""\n', "").replace('ENV VITE_HOCUSPOCUS_URL="\${VITE_HOCUSPOCUS_URL}"\n', ""),
|
|
});
|
|
|
|
const result = checkDockerfileWebContract(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /Dockerfile: web-build target must declare ARG VITE_HOCUSPOCUS_URL and forward it with ENV/);
|
|
});
|
|
|
|
it("fails when Dockerfile misses required web runtime closure and boot artifacts", () => {
|
|
const rootDir = createFixture({
|
|
dockerfile: VALID_DOCKERFILE.replace(
|
|
"COPY --exclude=**/*.map --exclude=**/*.d.ts --from=web-production-deps /prod/web/node_modules /myapp/node_modules\n",
|
|
""
|
|
)
|
|
.replace("COPY --from=build /myapp/packages/db/dist /myapp/packages/db/dist\n", "")
|
|
.replace("COPY --from=build /myapp/prisma /myapp/prisma\n", ""),
|
|
});
|
|
|
|
const result = checkDockerfileWebContract(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /Dockerfile: web target must copy \/myapp\/target-specific node_modules/);
|
|
assert.match(result.failures.join("\n"), /Dockerfile: web target must copy \/myapp\/packages\/db production seed artifact/);
|
|
assert.match(result.failures.join("\n"), /Dockerfile: web target must copy \/myapp\/prisma/);
|
|
});
|
|
|
|
it("fails when the web target copies the separately deployed API application", () => {
|
|
const rootDir = createFixture({
|
|
dockerfile: VALID_DOCKERFILE.replace(
|
|
"COPY --from=web-runtime-artifacts /myapp/build /myapp/build\n",
|
|
"COPY --from=web-runtime-artifacts /myapp/build /myapp/build\nCOPY --from=build /myapp/apps/api/src /myapp/apps/api/src\n"
|
|
),
|
|
});
|
|
|
|
const result = checkDockerfileWebContract(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /Dockerfile: web target must not copy API source, manifests, or artifacts/);
|
|
});
|
|
|
|
it("fails when the web manifest depends on the separately deployed API application", () => {
|
|
const rootDir = createFixture({
|
|
webPackage: {
|
|
name: "@hivegrid/web",
|
|
scripts: { start: "node ../../build/server-runtime/server.js" },
|
|
dependencies: { "@hivegrid/api": "workspace:*" },
|
|
},
|
|
});
|
|
|
|
const result = checkWebManifest(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /apps\/web\/package\.json: web must not depend on the separately deployed @hivegrid\/api app/);
|
|
});
|
|
|
|
it("checks web runtime copy and entrypoint only inside the web target", () => {
|
|
const rootDir = createFixture({
|
|
dockerfile: `FROM node:24-bookworm-slim AS base
|
|
FROM base AS build
|
|
COPY --from=build /myapp/build/server-runtime /myapp/build/server-runtime
|
|
ENTRYPOINT [ "./start.sh" ]
|
|
FROM base AS web
|
|
COPY --from=build /myapp/build /myapp/build
|
|
`,
|
|
});
|
|
|
|
const result = checkDockerfileWebContract(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /Dockerfile: web target must copy \/myapp\/build\/server-runtime from web-runtime-artifacts stage/);
|
|
assert.match(result.failures.join("\n"), /Dockerfile: web target must keep ENTRYPOINT/);
|
|
});
|
|
|
|
it("fails with the script name and manifest path when root typecheck is missing", () => {
|
|
const rootDir = createFixture({
|
|
rootPackage: {
|
|
private: true,
|
|
scripts: {
|
|
build: "vite build",
|
|
lint: "eslint .",
|
|
test: "pnpm --filter @hivegrid/web test",
|
|
},
|
|
},
|
|
});
|
|
|
|
const result = verifyDockerBaseline(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /package\.json: missing required script "typecheck"/);
|
|
});
|
|
|
|
it("fails with a path-specific message when a workspace package manifest is missing", () => {
|
|
const rootDir = createFixture({ remove: ["apps/api/package.json"] });
|
|
|
|
const result = verifyDockerBaseline(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /apps\/api\/package\.json: missing required Docker baseline file/);
|
|
});
|
|
|
|
it("fails with a path-specific message when the web package manifest is missing", () => {
|
|
const rootDir = createFixture({ remove: ["apps/web/package.json"] });
|
|
|
|
const result = verifyDockerBaseline(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /apps\/web\/package\.json: missing required Docker baseline file/);
|
|
});
|
|
|
|
it("fails when worker production scripts use tsx", () => {
|
|
const rootDir = createFixture({
|
|
workerPackage: {
|
|
name: "@hivegrid/worker",
|
|
scripts: {
|
|
start: "tsx src/index.ts",
|
|
health: "tsx src/health.ts",
|
|
},
|
|
},
|
|
});
|
|
|
|
const result = verifyDockerBaseline(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /apps\/worker\/package\.json: script "start" must contain node for worker production runtime/);
|
|
assert.match(result.failures.join("\n"), /apps\/worker\/package\.json: script "start" must target dist\/index\.js/);
|
|
assert.match(result.failures.join("\n"), /apps\/worker\/package\.json: script "start" must not use tsx for worker production runtime/);
|
|
assert.match(result.failures.join("\n"), /apps\/worker\/package\.json: script "health" must target dist\/health\.js/);
|
|
});
|
|
|
|
it("formats concise PASS, OBSERVE, and FAIL lines", () => {
|
|
const output = formatVerificationResult({
|
|
passes: ["Dockerfile: exists"],
|
|
observations: ["apps/worker/package.json: dev script uses tsx"],
|
|
failures: ['package.json: missing required script "typecheck"'],
|
|
});
|
|
|
|
assert.match(output, /^FAIL Docker baseline verification/m);
|
|
assert.match(output, /^PASS Dockerfile: exists/m);
|
|
assert.match(output, /^OBSERVE apps\/worker\/package\.json: dev script uses tsx/m);
|
|
assert.match(output, /^FAIL package\.json: missing required script "typecheck"/m);
|
|
});
|
|
});
|