Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/scripts/verify-deploy-readiness.test.mjs
T

332 lines
18 KiB
JavaScript

import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { spawnSync } from "node:child_process";
import { afterEach, describe, expect, it } from "vitest";
import { verifyDeployReadiness } from "./verify-deploy-readiness.mjs";
const fixtures = [];
function writeFixtureFile(rootDir, relativePath, content) {
const absolutePath = path.join(rootDir, relativePath);
fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
fs.writeFileSync(absolutePath, content);
}
function createFixture(overrides = {}) {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "deploy-readiness-"));
fixtures.push(rootDir);
for (const relativePath of [
"package.json",
".gitea/workflows/deploy.yml",
".gitea/workflows/ci.yml",
"docs/deployment.md",
"docs/ops/asset-cdn.md",
"docs/observability/sentry-selfhost.md",
"docs/worker.md",
"docs/ops/worker-queues.md",
"scripts/coolify-ensure-worker-groups.mjs",
"apps/worker/scripts/build.mjs",
"apps/worker/src/deploy-health.ts",
"apps/mcp/scripts/build.mjs",
"apps/mcp/src/deploy-health.ts",
"Dockerfile",
"start.sh",
"scripts/verify-migrator-role.mjs",
]) {
writeFixtureFile(rootDir, relativePath, overrides[relativePath] ?? fs.readFileSync(path.join(process.cwd(), relativePath), "utf8"));
}
return rootDir;
}
function packageJsonWithoutWorkflowOutboxTarget(target) {
const packageJson = JSON.parse(fs.readFileSync(path.join(process.cwd(), "package.json"), "utf8"));
packageJson.scripts["verify:workflow-outbox-native"] = packageJson.scripts["verify:workflow-outbox-native"]
.split(/\s+/)
.filter((part) => part !== target)
.join(" ");
return `${JSON.stringify(packageJson, null, 2)}\n`;
}
afterEach(() => {
while (fixtures.length > 0) {
fs.rmSync(fixtures.pop(), { recursive: true, force: true });
}
});
describe("verify-deploy-readiness", () => {
it("accepts the repository deploy readiness contract", () => {
const result = spawnSync(process.execPath, ["scripts/verify-deploy-readiness.mjs"], {
cwd: process.cwd(),
encoding: "utf8",
});
expect(result.status).toBe(0);
expect(result.stdout).toContain("Deploy readiness contract verified.");
});
it("fails when the MCP stage and promote gates stop consuming readiness", () => {
const workflow = fs
.readFileSync(path.join(process.cwd(), ".gitea/workflows/deploy.yml"), "utf8")
.replace('if [ "$RESOURCE_KIND" = "mcp" ]; then', 'if [ "$RESOURCE_KIND" = "api" ]; then');
const result = verifyDeployReadiness(createFixture({ ".gitea/workflows/deploy.yml": workflow }));
expect(result.failures).toContain('.gitea deploy workflow requires if [ "$RESOURCE_KIND" = "mcp" ]; then at least 2 times (found 1)');
});
it("fails when the exact deployment revision is no longer checked out", () => {
const workflow = fs
.readFileSync(path.join(process.cwd(), ".gitea/workflows/deploy.yml"), "utf8")
.replaceAll('git checkout --detach "$IMAGE_TAG"', "git checkout --detach main");
const result = verifyDeployReadiness(createFixture({ ".gitea/workflows/deploy.yml": workflow }));
expect(result.failures).toContain('.gitea deploy workflow missing git checkout --detach "$IMAGE_TAG"');
});
it("fails when staging builds bypass the isolated deploy builder", () => {
const workflow = fs
.readFileSync(path.join(process.cwd(), ".gitea/workflows/deploy.yml"), "utf8")
.replace('docker buildx build --builder "$DEPLOY_BUILDER_NAME" --push --target', "docker buildx build --push --target");
const result = verifyDeployReadiness(createFixture({ ".gitea/workflows/deploy.yml": workflow }));
expect(result.failures).toContain(
'.gitea deploy workflow missing docker buildx build --builder "$DEPLOY_BUILDER_NAME" --push --target'
);
});
it("fails when either Hocuspocus rollout stops consuming Docker readiness", () => {
const workflow = fs
.readFileSync(path.join(process.cwd(), ".gitea/workflows/deploy.yml"), "utf8")
.replace('if [ "$RESOURCE_KIND" = "hocuspocus" ]; then', 'if [ "$RESOURCE_KIND" = "hocuspocus-disabled" ]; then');
const result = verifyDeployReadiness(createFixture({ ".gitea/workflows/deploy.yml": workflow }));
expect(result.failures).toContain('.gitea deploy workflow requires if [ "$RESOURCE_KIND" = "hocuspocus" ]; then at least 2 times (found 1)');
});
it("fails when the migration entrypoint skips the semantic role probe", () => {
const entrypoint = fs.readFileSync(path.join(process.cwd(), "start.sh"), "utf8").replace(" node /myapp/scripts/verify-migrator-role.mjs\n", "");
const result = verifyDeployReadiness(createFixture({ "start.sh": entrypoint }));
expect(result.failures).toContain("migration entrypoint missing node /myapp/scripts/verify-migrator-role.mjs");
});
it("fails when the migration entrypoint skips reviewed Prisma baseline adoption", () => {
const entrypoint = fs
.readFileSync(path.join(process.cwd(), "start.sh"), "utf8")
.replace(" node /myapp/scripts/adopt-prisma-baseline.mjs --adopt\n", "");
const result = verifyDeployReadiness(createFixture({ "start.sh": entrypoint }));
expect(result.failures).toContain("migration entrypoint missing node /myapp/scripts/adopt-prisma-baseline.mjs --adopt");
});
it("fails when the migration entrypoint skips post-migration Prisma schema verification", () => {
const entrypoint = fs
.readFileSync(path.join(process.cwd(), "start.sh"), "utf8")
.replace(" node /myapp/scripts/adopt-prisma-baseline.mjs --verify-current-schema\n", "");
const result = verifyDeployReadiness(createFixture({ "start.sh": entrypoint }));
expect(result.failures).toContain("migration entrypoint missing node /myapp/scripts/adopt-prisma-baseline.mjs --verify-current-schema");
});
it("fails when the migration entrypoint skips the post-migration permission catalog sync", () => {
const entrypoint = fs
.readFileSync(path.join(process.cwd(), "start.sh"), "utf8")
.replace(" node /myapp/packages/db/dist/deploy-permission-catalog-sync.server.js\n", "");
const result = verifyDeployReadiness(createFixture({ "start.sh": entrypoint }));
expect(result.failures).toContain(
"migration entrypoint missing node /myapp/packages/db/dist/deploy-permission-catalog-sync.server.js"
);
});
it("fails when the migrator role probe stops rejecting superusers", () => {
const probe = fs
.readFileSync(path.join(process.cwd(), "scripts/verify-migrator-role.mjs"), "utf8")
.replace("roles[0].isSuperuser !== false", "roles[0].isSuperuser !== true");
const result = verifyDeployReadiness(createFixture({ "scripts/verify-migrator-role.mjs": probe }));
expect(result.failures).toContain("migrator role probe missing roles[0].isSuperuser !== false");
});
it("checks worker runbook matrix rows against the Coolify provisioner contract", () => {
const result = verifyDeployReadiness(createFixture());
expect(result.failures).toEqual([]);
expect(result.passes).toContain("docs/worker.md documents worker-plugin as plugin on health 9105 and admin 9205");
expect(result.passes).toContain("docs/ops/worker-queues.md documents worker-mail as mail on health 9102 and admin 9202");
expect(result.passes).toContain("Coolify worker provisioner sets worker-general OUTBOX_DISPATCHER_REQUIRED=true");
expect(result.passes).toContain("Coolify worker provisioner sets worker-general WORKFLOW_TRIGGER_OUTBOX_REQUIRED=true");
expect(result.passes).toContain("Coolify worker provisioner sets worker-general DATABASE_RUNTIME_ROLE=hivegrid_worker");
expect(result.passes).toContain("Coolify worker provisioner sets worker-general RLS_RUNTIME_MODE=required");
expect(result.passes).toContain("Coolify worker provisioner rejects privileged worker env DIRECT_URL");
expect(result.passes).toContain("Coolify worker provisioner rejects a non-worker DATABASE_URL role");
expect(result.passes).toContain("Coolify worker provisioner detects missing worker-general API_ACCESS_TOKEN");
expect(result.passes).toContain("Coolify worker provisioner detects missing worker-general WORKFLOW_TRIGGER_EXECUTOR_URL");
expect(result.passes).toContain("Coolify worker provisioner binds worker deployment health to the immutable image SHA");
expect(result.passes).toContain("Coolify worker provisioner accepts enabled SHA-bound cmd health configuration");
expect(result.passes).toContain("Coolify worker provisioner detects stale worker health_check_enabled");
expect(result.passes).toContain("Coolify worker provisioner detects stale worker health_check_type");
expect(result.passes).toContain("Coolify worker provisioner detects stale worker health_check_command");
});
it("fails when the worker provisioner validates an unused legacy env source", () => {
const provisioner = fs
.readFileSync(path.join(process.cwd(), "scripts/coolify-ensure-worker-groups.mjs"), "utf8")
.replace(
'const workerEnvSourceUuid = needsProvisioningOrRepair ? resolveWorkerEnvSourceUuid(apps, allowed) : "";',
"const workerEnvSourceUuid = resolveWorkerEnvSourceUuid(apps, allowed);"
);
const result = verifyDeployReadiness(createFixture({ "scripts/coolify-ensure-worker-groups.mjs": provisioner }));
expect(result.failures).toContain(
'Coolify worker provisioner missing const workerEnvSourceUuid = needsProvisioningOrRepair ? resolveWorkerEnvSourceUuid(apps, allowed) : "";'
);
});
it("fails when either stage or promotion stops enforcing the SHA-bound worker command", () => {
const workflow = fs
.readFileSync(path.join(process.cwd(), ".gitea/workflows/deploy.yml"), "utf8")
.replace(
'EXPECTED_HEALTH_COMMAND="node apps/worker/dist/deploy-health.js $IMAGE_TAG $EXPECTED_WORKER_GROUP"',
'EXPECTED_HEALTH_COMMAND="node apps/worker/dist/health.js"'
);
const result = verifyDeployReadiness(createFixture({ ".gitea/workflows/deploy.yml": workflow }));
expect(result.failures).toContain(
'.gitea deploy workflow requires EXPECTED_HEALTH_COMMAND="node apps/worker/dist/deploy-health.js $IMAGE_TAG $EXPECTED_WORKER_GROUP" at least 2 times (found 1)'
);
});
it("fails when the worker build omits the deployment health entrypoint", () => {
const workerBuild = fs.readFileSync(path.join(process.cwd(), "apps/worker/scripts/build.mjs"), "utf8").replace(', "src/deploy-health.ts"', "");
const result = verifyDeployReadiness(createFixture({ "apps/worker/scripts/build.mjs": workerBuild }));
expect(result.failures).toContain('worker build missing "src/deploy-health.ts"');
});
it("fails when worker deployment health stops checking the exact image revision", () => {
const deployHealth = fs
.readFileSync(path.join(process.cwd(), "apps/worker/src/deploy-health.ts"), "utf8")
.replace("imageRevision !== expectedRevision || env.APP_REVISION !== expectedRevision", "!env.APP_REVISION");
const result = verifyDeployReadiness(createFixture({ "apps/worker/src/deploy-health.ts": deployHealth }));
expect(result.failures).toContain(
"worker deployment health entrypoint missing imageRevision !== expectedRevision || env.APP_REVISION !== expectedRevision"
);
});
it("fails when MCP command health stops being bound to the staged revision", () => {
const workflow = fs
.readFileSync(path.join(process.cwd(), ".gitea/workflows/deploy.yml"), "utf8")
.replace(
'EXPECTED_MCP_HEALTH_COMMAND="node apps/mcp/dist/deploy-health.js $IMAGE_TAG"',
'EXPECTED_MCP_HEALTH_COMMAND="node apps/mcp/dist/deploy-health.js staging"'
);
const result = verifyDeployReadiness(createFixture({ ".gitea/workflows/deploy.yml": workflow }));
expect(result.failures).toContain(
'.gitea deploy workflow requires EXPECTED_MCP_HEALTH_COMMAND="node apps/mcp/dist/deploy-health.js $IMAGE_TAG" at least 2 times (found 1)'
);
});
it("fails when image-baked revision proof is removed from a runtime target", () => {
const dockerfile = fs
.readFileSync(path.join(process.cwd(), "Dockerfile"), "utf8")
.replace("RUN printf '%s\\n' \"$APP_REVISION\" > /myapp/.image-revision", "");
const result = verifyDeployReadiness(createFixture({ Dockerfile: dockerfile }));
expect(result.failures).toContain("Dockerfile requires RUN printf '%s\\n' \"$APP_REVISION\" > /myapp/.image-revision at least 3 times (found 2)");
});
it("fails when Gitea CI stops running the workflow outbox native gate", () => {
const ciWorkflow = fs
.readFileSync(path.join(process.cwd(), ".gitea/workflows/ci.yml"), "utf8")
.replace(" - name: Verify workflow outbox native readiness\n run: pnpm run verify:workflow-outbox-native\n", "");
const result = verifyDeployReadiness(createFixture({ ".gitea/workflows/ci.yml": ciWorkflow }));
expect(result.failures).toContain(".gitea CI workflow missing pnpm run verify:workflow-outbox-native");
});
it("fails when Gitea CI stops running the deploy readiness gate", () => {
const ciWorkflow = fs
.readFileSync(path.join(process.cwd(), ".gitea/workflows/ci.yml"), "utf8")
.replace(" - name: Verify deploy readiness\n run: pnpm run verify:deploy-readiness\n", "");
const result = verifyDeployReadiness(createFixture({ ".gitea/workflows/ci.yml": ciWorkflow }));
expect(result.failures).toContain(".gitea CI workflow missing pnpm run verify:deploy-readiness");
});
it("fails when web workflow producer inventory coverage falls out of the workflow outbox native gate", () => {
const target = "apps/web/app/modules/workflowEngine/routes/workflow-engine/workflow-producer-inventory.test.ts";
const result = verifyDeployReadiness(createFixture({ "package.json": packageJsonWithoutWorkflowOutboxTarget(target) }));
expect(result.failures).toContain(`package.json verify:workflow-outbox-native missing ${target}`);
});
it("fails when worker workflow consumer or bootstrap coverage falls out of the workflow outbox native gate", () => {
const consumerTarget = "apps/worker/src/jobs/workflow-trigger.test.ts";
const bootstrapTarget = "apps/worker/src/jobs/workflow-trigger-bootstrap.test.ts";
const boundaryTarget = "apps/worker/src/jobs/workflow-trigger-boundary.test.ts";
const storeTarget = "apps/worker/src/jobs/workflow-trigger-store.test.ts";
const healthTarget = "apps/worker/src/health.test.ts";
const packageJson = JSON.parse(fs.readFileSync(path.join(process.cwd(), "package.json"), "utf8"));
packageJson.scripts["verify:workflow-outbox-native"] = packageJson.scripts["verify:workflow-outbox-native"]
.split(/\s+/)
.filter((part) => ![consumerTarget, bootstrapTarget, boundaryTarget, storeTarget, healthTarget].includes(part))
.join(" ");
const result = verifyDeployReadiness(createFixture({ "package.json": `${JSON.stringify(packageJson, null, 2)}\n` }));
expect(result.failures).toContain(`package.json verify:workflow-outbox-native missing ${consumerTarget}`);
expect(result.failures).toContain(`package.json verify:workflow-outbox-native missing ${bootstrapTarget}`);
expect(result.failures).toContain(`package.json verify:workflow-outbox-native missing ${boundaryTarget}`);
expect(result.failures).toContain(`package.json verify:workflow-outbox-native missing ${storeTarget}`);
expect(result.failures).toContain(`package.json verify:workflow-outbox-native missing ${healthTarget}`);
});
it("fails when the worker runbook drifts from managed Coolify ports", () => {
const workerDocs = fs
.readFileSync(path.join(process.cwd(), "docs/worker.md"), "utf8")
.replace(
"| `worker-mail` | `WORKER_QUEUE_GROUP=mail` | none | `9102` | `9202` |",
"| `worker-mail` | `WORKER_QUEUE_GROUP=mail` | none | `9101` | `9202` |"
);
const result = verifyDeployReadiness(createFixture({ "docs/worker.md": workerDocs }));
expect(result.failures).toContain("docs/worker.md missing worker-mail row with WORKER_QUEUE_GROUP=mail, health port 9102, and admin port 9202");
});
it("fails when canonical worker fail-loud readiness guidance is removed", () => {
const workerQueueDocs = fs
.readFileSync(path.join(process.cwd(), "docs/ops/worker-queues.md"), "utf8")
.replace(/`TRANSACTIONAL_MAIL_OUTBOX_REQUIRED=true`/g, "`TRANSACTIONAL_MAIL_OUTBOX_REQUIRED=false`");
const result = verifyDeployReadiness(createFixture({ "docs/ops/worker-queues.md": workerQueueDocs }));
expect(result.failures).toContain("canonical worker queue docs missing TRANSACTIONAL_MAIL_OUTBOX_REQUIRED=true");
});
it("fails when operator-owned deploy/backlog status markers drift", () => {
const deploymentDocs = fs
.readFileSync(path.join(process.cwd(), "docs/deployment.md"), "utf8")
.replace("B-012 is not a repository code blocker", "B-012 is a repository blocker");
const assetCdnRunbook = fs.readFileSync(path.join(process.cwd(), "docs/ops/asset-cdn.md"), "utf8").replace("remain parked", "are active");
const sentryRunbook = fs
.readFileSync(path.join(process.cwd(), "docs/observability/sentry-selfhost.md"), "utf8")
.replace("B-008 is closed", "B-008 pending");
const result = verifyDeployReadiness(
createFixture({
"docs/deployment.md": deploymentDocs,
"docs/ops/asset-cdn.md": assetCdnRunbook,
"docs/observability/sentry-selfhost.md": sentryRunbook,
})
);
expect(result.failures).toContain("deployment docs missing B-012 is not a repository code blocker");
expect(result.failures).toContain("asset CDN runbook missing remain parked");
expect(result.failures).toContain("Sentry self-host runbook missing B-008 is closed");
});
});