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

2855 lines
143 KiB
JavaScript

#!/usr/bin/env node
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import {
DEPLOY_RUNNER_LABEL,
GIT_HOST,
HOST_OVERRIDE_VALIDATION_PATTERN as INFRA_HOST_OVERRIDE_VALIDATION_PATTERN,
HOST_OVERRIDE_WRITE_PATTERN as INFRA_HOST_OVERRIDE_WRITE_PATTERN,
PREFLIGHT_DATABASE_URL as INFRA_PREFLIGHT_DATABASE_URL,
escapeRegex,
} from "./infra-config.mjs";
const WORKFLOW_PATH = ".gitea/workflows/deploy.yml";
const REQUIRED_RUNNER = DEPLOY_RUNNER_LABEL;
const REQUIRED_TARGETS = ["web", "api", "worker", "mcp", "hocuspocus", "portal-domain-controller"];
const LEGACY_PREFLIGHT_JOB = "deploy-preflight";
const DEPLOY_JOB = "stage-or-promote";
const DEPLOY_BUILDKIT_CONFIG_PATH = "scripts/ci/deploy-buildkitd.toml";
const DEPLOY_BUILDKIT_CONFIG_VERIFIER_PATH = "scripts/ci/verify-deploy-buildkit-config.mjs";
const DEPLOY_BUILDKIT_IMAGE = "moby/buildkit:v0.31.2@sha256:2f5adac4ecd194d9f8c10b7b5d7bceb5186853db1b26e5abd3a657af0b7e26ec";
const DEPLOY_BUILDER_NAME = "hivegrid-deploy-v0312-gc1";
const PREFLIGHT_DATABASE_URL = INFRA_PREFLIGHT_DATABASE_URL;
const DATABASE_URL_SECRET = "${{ secrets.MIGRATOR_DATABASE_URL }}";
const PREFLIGHT_REQUIRED_COMMANDS = [
"node scripts/verify-gitea-deploy-workflow.mjs",
"node scripts/verify-deploy-readiness.mjs",
"node scripts/verify-docker-baseline.mjs",
"node scripts/verify-docker-non-root.mjs",
"node scripts/verify-portal-domain-controller-deploy.mjs",
"node --check scripts/coolify-deploy.mjs",
"node --check scripts/coolify-ensure-worker-groups.mjs",
"node --check scripts/ci/verify-deploy-buildkit-config.mjs",
"bash -n scripts/coolify-prepull-images.sh",
"bash -n scripts/ci/install-cosign.sh",
];
const PREFLIGHT_FORBIDDEN_HEAVY_COMMANDS = [
{ label: "pnpm install --frozen-lockfile", pattern: /\bpnpm\s+install\s+--frozen-lockfile\b/ },
{ label: "pnpm run verify:i18n", pattern: /\bpnpm\s+run\s+verify:i18n\b/ },
{ label: "pnpm run typecheck", pattern: /\bpnpm\s+(?:run\s+)?typecheck\b/ },
{ label: "pnpm run lint", pattern: /\bpnpm\s+(?:run\s+)?lint\b/ },
{ label: "pnpm run test", pattern: /\bpnpm\s+(?:run\s+)?test(?:\s|$)/ },
{ label: "pnpm run test:foundation", pattern: /\bpnpm\s+run\s+test:foundation\b/ },
{ label: "pnpm run test:packages", pattern: /\bpnpm\s+run\s+test:packages\b/ },
{ label: "pnpm run build", pattern: /\bpnpm\s+(?:run\s+)?build\b/ },
{ label: "docker build --target web", pattern: /\bdocker\s+(?:buildx\s+build|build)\b[\s\S]*?(?:^|\s)--target(?:=|\s+)web(?:\s|$)/ },
{ label: "docker build --target api", pattern: /\bdocker\s+(?:buildx\s+build|build)\b[\s\S]*?(?:^|\s)--target(?:=|\s+)api(?:\s|$)/ },
{ label: "docker build --target worker", pattern: /\bdocker\s+(?:buildx\s+build|build)\b[\s\S]*?(?:^|\s)--target(?:=|\s+)worker(?:\s|$)/ },
{ label: "pnpm run verify:docker-runtime-smoke", pattern: /\bpnpm\s+run\s+verify:docker-runtime-smoke\b/ },
{ label: "pnpm run verify:docker-api-runtime-smoke", pattern: /\bpnpm\s+run\s+verify:docker-api-runtime-smoke\b/ },
{ label: "pnpm run verify:docker-worker-runtime-smoke", pattern: /\bpnpm\s+run\s+verify:docker-worker-runtime-smoke\b/ },
];
const USERNAME_SECRET = "${{ secrets.REGISTRY_USER }}";
const PASSWORD_SECRET = "${{ secrets.REGISTRY_PAT }}";
const HOCUSPOCUS_BROWSER_URL_VAR = "${{ vars.VITE_HOCUSPOCUS_URL }}";
const FORBIDDEN_E2E_PATTERN = /\b(playwright|cypress|browser|test:e2e|e2e)\b/i;
const COOLIFY_TOKEN_SECRET = "${{ secrets.COOLIFY_TOKEN }}";
const COOLIFY_API_URL_VAR = "${{ vars.COOLIFY_API_URL }}";
const COOLIFY_RESOURCE_UUIDS_VAR = "${{ vars.COOLIFY_RESOURCE_UUIDS }}";
// Coolify pre/post_deployment_command never fired for docker-image apps. The
// reviewed web image therefore runs once as a DIRECT_URL-only migrator before
// any runtime resource is mutated; the dead hook slots stay cleared.
const WEB_SURFACE_RELEASE_PATTERN =
/coolify-deploy\.mjs\s+set-web-surface-release\s+\\?\s*\n\s*"\$WEB_RESOURCE_UUID"\s+\\?\s*\n\s*"\$ADMIN_RESOURCE_UUID"\s+\\?\s*\n\s*"\$PORTAL_RESOURCE_UUID"\s+\\?\s*\n\s*"\$WEB_IMAGE"\s+\\?\s*\n\s*"\$IMAGE_TAG"\s+\\?\s*\n\s*"\$WEB_IMAGE_DIGEST"/;
const WEB_SURFACE_RELEASE_PROOF_PATTERN =
/coolify-deploy\.mjs\s+assert-web-surface-release\s+\\?\s*\n\s*"\$(?:STAGING|PRODUCTION)_WEB_RESOURCE_UUID"\s+\\?\s*\n\s*"\$(?:STAGING|PRODUCTION)_ADMIN_RESOURCE_UUID"\s+\\?\s*\n\s*"\$(?:STAGING|PRODUCTION)_PORTAL_RESOURCE_UUID"\s+\\?\s*\n\s*"\$WEB_IMAGE"\s+\\?\s*\n\s*"\$IMAGE_TAG"\s+\\?\s*\n\s*"\$ROLLED_OUT_WEB_IMAGE_DIGEST"\s+\\?\s*\n\s*"\$(?:STAGING|PRODUCTION)_WEB_DEPLOYMENT_UUID"\s+\\?\s*\n\s*"\$(?:STAGING|PRODUCTION)_ADMIN_DEPLOYMENT_UUID"\s+\\?\s*\n\s*"\$(?:STAGING|PRODUCTION)_PORTAL_DEPLOYMENT_UUID"/;
const ONE_SHOT_MIGRATOR_PATTERN =
/DIRECT_URL="\$MIGRATOR_DATABASE_URL"\s*\\?\s*docker\s+run\s+--rm[\s\S]*?-e\s+DIRECT_URL\s*\\?[\s\S]*?\/web:\$IMAGE_TAG"\s+migrate/;
const APP_RUNTIME_ASSERTION_PATTERN =
/coolify-deploy\.mjs\s+assert-runtime-db-env\s+(?:"\$WEB_RESOURCE_UUID"\s+hivegrid_app\s+web|"\$ADMIN_RESOURCE_UUID"\s+hivegrid_app\s+admin|"\$PORTAL_RESOURCE_UUID"\s+hivegrid_portal\s+portal|"\$(?:API|MCP|HOCUSPOCUS)_RESOURCE_UUID"\s+hivegrid_app\s+service)/;
const PORTAL_RUNTIME_ASSERTION_PATTERN = /coolify-deploy\.mjs\s+assert-runtime-db-env\s+"\$PORTAL_RESOURCE_UUID"\s+hivegrid_portal\s+portal/;
const WORKER_RUNTIME_ASSERTION_PATTERN = /coolify-deploy\.mjs\s+assert-runtime-db-env\s+"\$WORKER_RESOURCE_UUID"\s+hivegrid_worker\s+service/;
const WEB_PUSH_RUNTIME_ASSERTION_PATTERN = /coolify-deploy\.mjs\s+assert-web-push-env\s+"\$WEB_RESOURCE_UUID"\s+"\$WORKER_WEBHOOK_RESOURCE_UUID"/;
const FORBIDDEN_RUNTIME_DB_FLAG_PATTERN = /\b(?:MIGRATE_ON_BOOT|SEED_ON_BOOT|SEED_ONCE|RLS_RUNTIME_MODE=off)\b/;
const DEMO_PRO_USER_PASSWORD_PATTERN = /\bSEED_DEMO_USER_PASSWORD\b/;
const LITERAL_COOLIFY_URL_PATTERN = /https?:\/\/[^\s'\"]*(?:coolify|webhook)[^\s'\"]*/i;
const LITERAL_PRODUCTION_REGISTRY_PATTERN =
/\b(?:ghcr\.io|registry\.docker\.com|docker\.io|registry\.[A-Za-z0-9.-]+|[A-Za-z0-9.-]+:\d{2,5})\/[A-Za-z0-9._/-]+/i;
const SECRET_SHAPED_LITERAL_PATTERNS = [
/\bghp_[A-Za-z0-9_]{20,}\b/,
/\bgithub_pat_[A-Za-z0-9_]{20,}\b/,
/\bglpat-[A-Za-z0-9_-]{20,}\b/,
/\b(?:password|token|secret)\s*[:=]\s*['\"]?[A-Za-z0-9_./+=-]{16,}/i,
/\b[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}\b/i,
];
const FORBIDDEN_GITEA_HOST_OVERRIDE_PATTERN = /\bGITEA_HOST_IP\b|\bvars\.GITEA_HOST_IP\b/;
const HOST_OVERRIDE_WRITE_PATTERN = INFRA_HOST_OVERRIDE_WRITE_PATTERN;
const HOST_OVERRIDE_VALIDATION_PATTERN = INFRA_HOST_OVERRIDE_VALIDATION_PATTERN;
const REQUIRED_DEPENDENT_IMAGE_TAG_PATCH_RESOURCES = [
"API_RESOURCE_UUID",
"MCP_RESOURCE_UUID",
"HOCUSPOCUS_RESOURCE_UUID",
"WORKER_GENERAL_RESOURCE_UUID",
"WORKER_MAIL_RESOURCE_UUID",
"WORKER_IMPORT_RESOURCE_UUID",
"WORKER_WEBHOOK_RESOURCE_UUID",
"WORKER_PLUGIN_RESOURCE_UUID",
];
const COSIGN_INSTALLER_PATH = "scripts/ci/install-cosign.sh";
const COOLIFY_DEPLOY_HELPER_PATH = "scripts/coolify-deploy.mjs";
const DEPLOY_IGNORED_CI_ONLY_PATHS = Object.freeze([
"scripts/ci/docker-smoke-suite.sh",
"scripts/docker-api-runtime-smoke.sh",
"scripts/docker-hocuspocus-runtime-smoke.sh",
"scripts/docker-mcp-runtime-smoke.sh",
"scripts/docker-portal-domain-controller-runtime-smoke.sh",
"scripts/docker-web-runtime-smoke.sh",
"scripts/docker-worker-runtime-smoke.sh",
"scripts/lib/docker-rls-role-fixture.sh",
"scripts/lib/docker-runtime-smoke.sh",
"scripts/lib/run-command-with-timeout.mjs",
"scripts/verify-gitea-ci-workflow.mjs",
"scripts/verify-portal-domain-controller-deploy.mjs",
]);
function verifiesLiveHealthRevision(stepText) {
return (
/\/healthz/.test(stepText) &&
/HEALTH_JSON="\$\(mktemp\)"/.test(stepText) &&
/payload\.revision/.test(stepText) &&
/payload\.service/.test(stepText) &&
/HEALTH_REVISION="\$\(node -e/.test(stepText) &&
/HEALTH_SERVICE="\$\(node -e/.test(stepText) &&
/\[\s*-n\s+"\$EXPECTED_SERVICE"\s*\]\s*&&\s*\[\s*"\$HEALTH_SERVICE"\s*!=\s*"\$EXPECTED_SERVICE"\s*\]/.test(stepText) &&
/\[\s*"\$HEALTH_REVISION"\s*!=\s*"\$IMAGE_TAG"\s*\]/.test(stepText)
);
}
function verifiesLiveWebSurfaceReadiness(stepText) {
return (
/\/readyz/.test(stepText) &&
/READINESS_JSON="\$\(mktemp\)"/.test(stepText) &&
/if\s+!\s+curl\b[^\n]*--fail[^\n]*"\$HEALTH_URL\/readyz"\s*>\s*"\$READINESS_JSON";\s*then/.test(stepText) &&
/payload\.status/.test(stepText) &&
/payload\.service/.test(stepText) &&
/payload\.surface/.test(stepText) &&
/payload\.revision/.test(stepText) &&
/READINESS_STATUS="\$\(node -e/.test(stepText) &&
/READINESS_SERVICE="\$\(node -e/.test(stepText) &&
/READINESS_SURFACE="\$\(node -e/.test(stepText) &&
/READINESS_REVISION="\$\(node -e/.test(stepText) &&
/\[\s*"\$READINESS_STATUS"\s*!=\s*"ready"\s*\]/.test(stepText) &&
/\[\s*"\$READINESS_REVISION"\s*!=\s*"\$IMAGE_TAG"\s*\]/.test(stepText) &&
/\[\s*-n\s+"\$EXPECTED_SERVICE"\s*\]\s*&&\s*\[\s*"\$READINESS_SERVICE"\s*!=\s*"\$EXPECTED_SERVICE"\s*\]/.test(stepText) &&
/\[\s*-n\s+"\$EXPECTED_SURFACE"\s*\]\s*&&\s*\[\s*"\$READINESS_SURFACE"\s*!=\s*"\$EXPECTED_SURFACE"\s*\]/.test(stepText)
);
}
function readWorkflow(rootDir) {
return fs.readFileSync(path.join(rootDir, WORKFLOW_PATH), "utf8");
}
function checkWorkflowExists(rootDir) {
return fs.existsSync(path.join(rootDir, WORKFLOW_PATH));
}
function getJobSection(content, jobName, analysis) {
const lines = getWorkflowAnalysis(content, analysis).lines;
const startIndex = lines.findIndex((line) => new RegExp(`^ ${jobName}:\\s*$`).test(line));
if (startIndex === -1) return null;
const sectionLines = [lines[startIndex]];
for (let index = startIndex + 1; index < lines.length; index += 1) {
const line = lines[index];
if (/^ [A-Za-z0-9_-]+:\s*$/.test(line)) break;
sectionLines.push(line);
}
return sectionLines.join("\n");
}
function hasCheckoutStep(section) {
return /uses:\s*actions\/checkout@(?:v\d+|[0-9a-f]{40})/i.test(section);
}
function hasJobLevelEnvValue(section, envName, envValue) {
const stepsIndex = section.search(/^ steps:\s*$/m);
const beforeSteps = stepsIndex === -1 ? section : section.slice(0, stepsIndex);
const escaped = envValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
return new RegExp(`^\\s*${envName}:\\s*${escaped}\\s*$`, "m").test(beforeSteps);
}
function getRunBlocksFromLines(lines) {
const blocks = [];
for (let index = 0; index < lines.length; index += 1) {
const line = lines[index];
const multilineRun = line.match(/^(\s*)run:\s*[>|]\s*$/);
if (!multilineRun) {
const inlineRun = line.match(/^\s*run:\s*(.+)$/);
if (inlineRun) {
blocks.push({ line: line.trim(), command: inlineRun[1], lineNumber: index + 1 });
}
continue;
}
const baseIndent = multilineRun[1].length;
const commandLines = [];
for (let nextIndex = index + 1; nextIndex < lines.length; nextIndex += 1) {
const nextLine = lines[nextIndex];
if (nextLine.trim().length === 0) {
commandLines.push("");
continue;
}
const indent = nextLine.match(/^\s*/)[0].length;
if (indent <= baseIndent) {
break;
}
commandLines.push(nextLine.trim());
}
blocks.push({ line: line.trim(), command: commandLines.join("\n"), lineNumber: index + 1 });
}
return blocks;
}
function stripExecutableShellLine(line) {
return line.replace(/\s+#.*$/, "").trim();
}
function getWorkflowLinesFromLines(lines) {
return lines.map((line, index) => ({ line, lineNumber: index + 1 }));
}
function getStepBlocksFromLines(lines) {
const steps = [];
for (let index = 0; index < lines.length; index += 1) {
const stepMatch = lines[index].match(/^(\s*)-\s+name:\s*(.*)$/);
if (!stepMatch) {
continue;
}
const baseIndent = stepMatch[1].length;
const stepLines = [lines[index]];
for (let nextIndex = index + 1; nextIndex < lines.length; nextIndex += 1) {
const nextLine = lines[nextIndex];
if (nextLine.trim().length === 0) {
stepLines.push(nextLine);
continue;
}
const indent = nextLine.match(/^\s*/)[0].length;
if (indent <= baseIndent && /^\s*-\s+name:\s*/.test(nextLine)) {
break;
}
if (indent <= baseIndent && /^\S/.test(nextLine)) {
break;
}
stepLines.push(nextLine);
}
steps.push({ text: stepLines.join("\n"), lineNumber: index + 1 });
}
return steps;
}
function createWorkflowAnalysis(content) {
const lines = content.split(/\r?\n/);
return {
content,
lines,
workflowLines: getWorkflowLinesFromLines(lines),
runBlocks: getRunBlocksFromLines(lines),
stepBlocks: getStepBlocksFromLines(lines),
};
}
function getWorkflowAnalysis(content, analysis) {
return analysis?.content === content ? analysis : createWorkflowAnalysis(content);
}
function getRunBlocks(content, analysis) {
return getWorkflowAnalysis(content, analysis).runBlocks;
}
function getWorkflowLines(content, analysis) {
return getWorkflowAnalysis(content, analysis).workflowLines;
}
function getStepBlocks(content, analysis) {
return getWorkflowAnalysis(content, analysis).stepBlocks;
}
function imageTagPatchLoopIncludesAllDependentResources(stepText) {
const loopLine =
stepText
.split(/\r?\n/)
.map((line) => line.trim())
.find((line) => /^for RESOURCE_UUID in\b/.test(line) && /API_RESOURCE_UUID/.test(line) && /WORKER_/.test(line)) ?? "";
return REQUIRED_DEPENDENT_IMAGE_TAG_PATCH_RESOURCES.every((resourceName) => new RegExp(`\\$${resourceName}\\b`).test(loopLine));
}
function getStepName(stepText) {
return stepText.match(/^\s*-\s+name:\s*(.*)$/m)?.[1]?.trim() ?? "";
}
function getRunTextFromStep(step) {
const lines = step.split(/\r?\n/);
for (let index = 0; index < lines.length; index += 1) {
const runMatch = lines[index].match(/^(\s*)run:\s*(.*)$/);
if (!runMatch) {
continue;
}
const runIndent = runMatch[1].length;
const value = runMatch[2].trim();
if (!/^[|>][+-]?$/.test(value)) {
return value;
}
const commandLines = [];
for (let nextIndex = index + 1; nextIndex < lines.length; nextIndex += 1) {
const nextLine = lines[nextIndex];
const indent = nextLine.match(/^\s*/)[0].length;
if (nextLine.trim().length > 0 && indent <= runIndent) {
break;
}
commandLines.push(nextLine);
}
return commandLines.join("\n");
}
return "";
}
function getExecutableShellLines(runText) {
return runText
.split(/\r?\n/)
.map((line) => stripExecutableShellLine(line))
.filter(Boolean);
}
function listForbiddenPreflightCommands(preflightSection, analysis) {
const matches = new Set();
for (const block of getRunBlocks(preflightSection, analysis)) {
const command = getExecutableShellLines(block.command).join("\n");
for (const { label, pattern } of PREFLIGHT_FORBIDDEN_HEAVY_COMMANDS) {
if (pattern.test(command)) {
matches.add(label);
}
}
}
return [...matches];
}
function getStepForRunCommand(content, predicate, analysis) {
return (
getStepBlocks(content, analysis).find(({ text }) => {
const lines = getExecutableShellLines(getRunTextFromStep(text));
const block = lines.join("\n");
return lines.some((line) => predicate(line)) || predicate(block) || predicate(text);
}) ?? null
);
}
function getRequiredStepSofteningReason(step, { allowAlways = false } = {}) {
if (!step) {
return "";
}
if (/^\s*continue-on-error:\s*true\s*$/im.test(step.text)) {
return "continue-on-error or always()";
}
if (/^\s*if:\s*[^\n]+$/im.test(step.text) && !(allowAlways && /^\s*if:\s*always\(\)\s*$/im.test(step.text))) {
return "conditional execution";
}
const lines = getExecutableShellLines(getRunTextFromStep(step.text));
const block = lines.join("\n");
const lastLine = lines.at(-1) ?? "";
if (/\bset\s+(?:\+e|\+o\s+errexit)\b/.test(block)) {
return "shell success masking";
}
if (/(^|[^|])\|\|\s*(?:true|:|exit\s+0)(?=[\s;)"']|$)/.test(block)) {
return "shell success masking";
}
if (/;\s*(?:true|:|exit\s+0)\s*$/.test(lastLine)) {
return "shell success masking";
}
if (/^(?:true|:|exit\s+0)$/.test(lastLine)) {
return "shell success masking";
}
return "";
}
function hasPushBranchTrigger(content, branch) {
return new RegExp(`on:\\s*[\\s\\S]*?push:\\s*[\\s\\S]*?branches:\\s*\\[[^\\]]*\\b${branch}\\b`, "i").test(content);
}
function hasDockerTargetBuild(content, target, analysis) {
return getRunBlocks(content, analysis).some(({ command }) => {
if (!/\bdocker\s+(?:buildx\s+build|build)\b/.test(command)) {
return false;
}
return new RegExp(`(?:^|\\s)--target(?:=|\\s+)${target}(?:\\s|$)`).test(command);
});
}
function hasDockerTargetLoop(content, target) {
return new RegExp(`for TARGET in [^;\\n]*\\b${escapeRegex(target)}\\b[^;\\n]*; do`).test(content);
}
function getDockerTargetBuildLine(content, target, analysis) {
const block = getRunBlocks(content, analysis).find(({ command }) => {
if (!/\bdocker\s+(?:buildx\s+build|build)\b/.test(command)) {
return false;
}
return (
new RegExp(`(?:^|\\s)--target(?:=|\\s+)${target}(?:\\s|$)`).test(command) ||
(/for TARGET in web api worker/.test(command) && /--target\s+"\$TARGET"/.test(command))
);
});
return block?.lineNumber ?? 0;
}
function hasDockerTargetPush(content, target, analysis) {
return getRunBlocks(content, analysis).some(({ command }) => {
if (!/\bdocker\s+buildx\s+build\b/.test(command) || !/(?:^|\s)--push(?:\s|$)/.test(command)) {
return false;
}
return new RegExp(`(?:^|\\s)--target(?:=|\\s+)${target}(?:\\s|$)`).test(command);
});
}
export function checkWorkflowRunners(content, analysis) {
const passes = [];
const failures = [];
const runnerLines = getWorkflowLines(content, analysis).filter(({ line }) => /^\s*runs-on:\s*/.test(line));
if (runnerLines.length === 0) {
failures.push(`${WORKFLOW_PATH}: missing required runs-on declarations`);
return { passes, failures, observations: [] };
}
for (const { line, lineNumber } of runnerLines) {
const runner = line
.replace(/^\s*runs-on:\s*/, "")
.trim()
.replace(/^['\"]|['\"]$/g, "");
if (runner === REQUIRED_RUNNER) {
passes.push(`${WORKFLOW_PATH}:${lineNumber}: runner is "${REQUIRED_RUNNER}"`);
} else {
failures.push(`${WORKFLOW_PATH}:${lineNumber}: runs-on must be "${REQUIRED_RUNNER}" but found "${runner}"`);
}
}
return { passes, failures, observations: [] };
}
export function checkDeployPreflightContract(content, analysis) {
const passes = [];
const failures = [];
const legacyPreflightSection = getJobSection(content, LEGACY_PREFLIGHT_JOB, analysis);
const deploySection = getJobSection(content, DEPLOY_JOB, analysis);
const deployAnalysis = deploySection ? createWorkflowAnalysis(deploySection) : null;
if (legacyPreflightSection) {
failures.push(`${WORKFLOW_PATH}: preflight and rollout must remain in one retained deploy job; remove "${LEGACY_PREFLIGHT_JOB}"`);
} else {
passes.push(`${WORKFLOW_PATH}: deploy has no second runner acquisition for a separate preflight job`);
}
if (!deploySection) {
failures.push(`${WORKFLOW_PATH}: missing deploy job "${DEPLOY_JOB}"`);
} else {
passes.push(`${WORKFLOW_PATH}: deploy job "${DEPLOY_JOB}" retains preflight and rollout on one runner`);
if (hasCheckoutStep(deploySection)) passes.push(`${WORKFLOW_PATH}: retained deploy job checks out the repository`);
else failures.push(`${WORKFLOW_PATH}: retained deploy job must check out the repository`);
if (hasJobLevelEnvValue(deploySection, "DATABASE_URL", DATABASE_URL_SECRET)) {
failures.push(`${WORKFLOW_PATH}: retained deploy job must not expose secrets.DATABASE_URL at job scope`);
} else {
passes.push(`${WORKFLOW_PATH}: retained deploy job does not expose production database credentials`);
}
const deploySteps = getStepBlocks(deploySection, deployAnalysis);
const preflightStep = deploySteps.find(({ text }) => /\bname:\s*Verify deploy wiring\b/i.test(text));
const exactCheckoutStep = deploySteps.find(({ text }) => /\bname:\s*Verify checkout revision\b/i.test(text));
const installStep = deploySteps.find(({ text }) => /\bname:\s*Install exact-revision dependencies\b/i.test(text));
const loginStep = deploySteps.find(({ text }) => /\bname:\s*Login to Gitea Registry\b/i.test(text));
const buildStep = deploySteps.find(({ text }) => /\bname:\s*Build and push staging images\b/i.test(text));
if (!preflightStep) {
failures.push(`${WORKFLOW_PATH}: retained deploy job is missing the fail-closed "Verify deploy wiring" step`);
} else {
const preflightStepAnalysis = createWorkflowAnalysis(preflightStep.text);
for (const command of PREFLIGHT_REQUIRED_COMMANDS) {
if (!preflightStep.text.includes(command)) {
failures.push(`${WORKFLOW_PATH}: deploy preflight missing command "${command}"`);
continue;
}
const step = getStepForRunCommand(preflightStep.text, (line) => line.includes(command), preflightStepAnalysis);
const softeningReason = getRequiredStepSofteningReason(step);
if (softeningReason) {
failures.push(`${WORKFLOW_PATH}: deploy preflight command "${command}" must not use ${softeningReason}`);
} else {
passes.push(`${WORKFLOW_PATH}: deploy preflight includes command "${command}"`);
}
}
const forbiddenCommands = listForbiddenPreflightCommands(preflightStep.text, preflightStepAnalysis);
if (forbiddenCommands.length > 0) {
failures.push(`${WORKFLOW_PATH}: deploy preflight must stay lightweight and not run heavy CI commands: ${forbiddenCommands.join(", ")}`);
} else {
passes.push(`${WORKFLOW_PATH}: deploy preflight stays lightweight and leaves full CI gates to the CI workflow`);
}
const mutationBoundaryLines = [exactCheckoutStep, installStep, loginStep, buildStep].filter(Boolean).map(({ lineNumber }) => lineNumber);
if (mutationBoundaryLines.length === 4 && mutationBoundaryLines.every((lineNumber) => preflightStep.lineNumber < lineNumber)) {
passes.push(`${WORKFLOW_PATH}: retained deploy preflight verifies the selected workflow before exact-revision checkout, dependency installation, registry credentials, and image publication`);
} else {
failures.push(`${WORKFLOW_PATH}: retained deploy preflight must run before exact-revision checkout, dependency installation, registry credentials, and image publication`);
}
}
}
if (deploySection) {
const deploySteps = getStepBlocks(deploySection, deployAnalysis);
const exactCheckoutStep = deploySteps.find(({ text }) => /\bVerify checkout revision\b/i.test(text));
const exactInstallStep = deploySteps.find(({ text }) => /\bInstall exact-revision dependencies\b/i.test(text));
const productionHealthStep = deploySteps.find(({ text }) => /\bWait for Coolify production health\b/i.test(text));
const dependentDeployStep = deploySteps.find(({ text }) => /\bTrigger Coolify production dependent deploys\b/i.test(text));
const prepareMigrationStatusStep = deploySteps.find(({ text }) => /\bPrepare production migration status checkout\b/i.test(text));
const cacheMigrationStatusStep = deploySteps.find(({ text }) => /\bCache production migration pnpm store\b/i.test(text));
const installMigrationStatusStep = deploySteps.find(({ text }) => /\bInstall production migration dependencies\b/i.test(text));
const migrationStatusStep = deploySteps.find(({ text }) => /\bVerify production migration status after web deploy\b/i.test(text));
const hasExactRevisionCheckout =
exactCheckoutStep &&
/git\s+checkout\s+--detach\s+"\$IMAGE_TAG"/.test(exactCheckoutStep.text) &&
/CHECKOUT_SHA="\$\(git\s+rev-parse\s+HEAD\)"/.test(exactCheckoutStep.text) &&
/\[\s*"\$CHECKOUT_SHA"\s*!=\s*"\$IMAGE_TAG"\s*\]/.test(exactCheckoutStep.text);
const hasExactRevisionInstall =
exactInstallStep &&
exactInstallStep.text.includes("pnpm install --frozen-lockfile --prefer-offline") &&
!exactInstallStep.text.includes(`DATABASE_URL: ${DATABASE_URL_SECRET}`);
const hasPromoteMigrationStatus =
migrationStatusStep &&
/^\s*if:\s*env\.DEPLOY_ACTION\s*==\s*['"]promote['"]\s*$/m.test(migrationStatusStep.text) &&
migrationStatusStep.text.includes("pnpm run verify:db-migrate-status") &&
!migrationStatusStep.text.includes("pnpm install --frozen-lockfile --prefer-offline") &&
migrationStatusStep.text.includes(`DATABASE_URL: ${DATABASE_URL_SECRET}`);
const hasNoRedundantPromoteSetup = !prepareMigrationStatusStep && !cacheMigrationStatusStep && !installMigrationStatusStep;
if (
hasExactRevisionCheckout &&
hasExactRevisionInstall &&
hasPromoteMigrationStatus &&
hasNoRedundantPromoteSetup &&
productionHealthStep &&
dependentDeployStep &&
exactCheckoutStep.lineNumber < exactInstallStep.lineNumber &&
exactInstallStep.lineNumber < productionHealthStep.lineNumber &&
migrationStatusStep.lineNumber > productionHealthStep.lineNumber &&
migrationStatusStep.lineNumber < dependentDeployStep.lineNumber
) {
passes.push(
`${WORKFLOW_PATH}: promote reuses the exact-revision checkout and install for the post-web migration status gate without a second cache or install`
);
} else {
failures.push(
`${WORKFLOW_PATH}: promote must reuse the initial IMAGE_TAG checkout and frozen install, skip redundant migration checkout/cache/install steps, and run the step-scoped migration status gate after production web health and before dependent deploys`
);
}
}
return { passes, failures, observations: [] };
}
export function checkDeploymentConcurrency(content) {
const passes = [];
const failures = [];
if (/^concurrency:\s*$/m.test(content) && /^\s+group:\s*hivegrid-deploy\s*$/m.test(content) && /^\s+cancel-in-progress:\s*false\s*$/m.test(content)) {
passes.push(`${WORKFLOW_PATH}: deployment concurrency serializes staging and production promotion`);
} else {
failures.push(`${WORKFLOW_PATH}: deployment workflow must define concurrency group hivegrid-deploy with cancel-in-progress: false`);
}
return { passes, failures, observations: [] };
}
export function checkDeployPushPathIgnoreContract(content) {
const passes = [];
const failures = [];
const pushTrigger = content.match(/^ push:\s*\n([\s\S]*?)(?=^ workflow_dispatch:)/mu)?.[1] ?? "";
const pathsIgnore = pushTrigger.match(/^ paths-ignore:\s*\n([\s\S]*)$/mu)?.[1] ?? "";
const ignoredScriptPaths = [...pathsIgnore.matchAll(/^\s+-\s+["']?(scripts\/[^"'\s]+)["']?\s*$/gmu)].map((match) => match[1]);
const expected = new Set(DEPLOY_IGNORED_CI_ONLY_PATHS);
const actual = new Set(ignoredScriptPaths);
const missing = DEPLOY_IGNORED_CI_ONLY_PATHS.filter((pathValue) => !actual.has(pathValue));
const unexpected = ignoredScriptPaths.filter((pathValue) => !expected.has(pathValue));
if (missing.length === 0 && unexpected.length === 0 && actual.size === ignoredScriptPaths.length) {
passes.push(`${WORKFLOW_PATH}: deploy skips only the reviewed exact CI/Docker-smoke script paths`);
} else {
failures.push(
`${WORKFLOW_PATH}: deploy paths-ignore must contain only the reviewed exact CI/Docker-smoke script paths` +
`${missing.length > 0 ? `; missing: ${missing.join(", ")}` : ""}` +
`${unexpected.length > 0 ? `; unexpected: ${unexpected.join(", ")}` : ""}`
);
}
if (/^\s+-\s+["']?scripts\/(?:\*\*|\*)/mu.test(pathsIgnore)) {
failures.push(`${WORKFLOW_PATH}: deploy paths-ignore must not use a broad scripts/** or scripts/* pattern`);
} else {
passes.push(`${WORKFLOW_PATH}: deploy paths-ignore has no broad scripts wildcard`);
}
return { passes, failures, observations: [] };
}
export function checkForbiddenCommands(content, analysis) {
const passes = [];
const failures = [];
const e2eBlock = getRunBlocks(content, analysis).find(({ command }) => FORBIDDEN_E2E_PATTERN.test(command.replace(/\bhivegrid\.browser-automation\b/g, "")));
if (e2eBlock) {
failures.push(`${WORKFLOW_PATH}:${e2eBlock.lineNumber}: forbidden E2E/browser command found: ${e2eBlock.line}`);
} else {
passes.push(`${WORKFLOW_PATH}: no forbidden E2E/browser commands are present`);
}
return { passes, failures, observations: [] };
}
export function checkDuplicateCoolifySelectedAssignments(content, analysis) {
const passes = [];
const failures = [];
const selectedAssignmentPattern = /^\s*(SELECTED_COOLIFY_(?:API_URL|TOKEN))=/;
for (const block of getRunBlocks(content, analysis)) {
const counts = new Map();
for (const line of block.command.split("\n")) {
const match = line.match(selectedAssignmentPattern);
if (!match) continue;
counts.set(match[1], (counts.get(match[1]) ?? 0) + 1);
}
for (const [name, count] of counts) {
if (count > 1) {
failures.push(`${WORKFLOW_PATH}:${block.lineNumber}: ${name} is assigned ${count} times in one run block`);
}
}
}
if (failures.length === 0) {
passes.push(`${WORKFLOW_PATH}: Coolify selected environment assignments are not duplicated within run blocks`);
}
return { passes, failures, observations: [] };
}
export function checkRequiredStepHardening(content, analysis) {
const passes = [];
const failures = [];
const requiredSteps = [
{
label: "deployment action resolution",
predicate: (command) => /\bDEPLOY_ACTION=stage\b/.test(command) && /\bDEPLOY_ACTION=promote\b/.test(command),
},
{
label: "exact deployment revision checkout",
predicate: (command) => /git\s+checkout\s+--detach\s+"\$IMAGE_TAG"/.test(command) && /CHECKOUT_SHA="\$\(git rev-parse HEAD\)"/.test(command),
},
{
label: "exact-revision tenant policy contract",
predicate: (command) => /pnpm\s+run\s+verify:tenant-policy-contract/.test(command),
},
{
label: "exact-revision RLS release readiness",
predicate: (command) => /pnpm\s+run\s+verify:rls-release-readiness/.test(command),
},
{
label: "exact-revision live RLS isolation",
predicate: (command) => /pnpm\s+run\s+verify:rls-live/.test(command),
},
{
label: "registry login",
predicate: (command) => /\bdocker\s+login\b/.test(command),
},
{
label: "staged promotion provenance",
predicate: (command) =>
/\bgit\s+merge-base\s+--is-ancestor\s+"\$IMAGE_TAG"\s+origin\/main\b/.test(command) && /\bdocker\s+buildx\s+imagetools\s+inspect\b/.test(command),
},
{
label: "Docker image publication",
predicate: (command) => /\bdocker\s+buildx\s+build\b/.test(command) && /(?:^|\s)--push(?:\s|$)/.test(command),
},
{
label: "one-shot database migration",
predicate: (command) => ONE_SHOT_MIGRATOR_PATTERN.test(command),
},
{
label: "Coolify deploy trigger",
predicate: (command) => /coolify-deploy\.mjs\s+trigger/.test(command),
},
{
label: "production image promotion",
predicate: (command) => /\bdocker\s+buildx\s+imagetools\s+create\b/.test(command) && /--tag\s+"\$IMAGE:main"/.test(command),
},
];
for (const { label, predicate } of requiredSteps) {
const step = getStepForRunCommand(content, predicate, analysis);
if (!step) {
failures.push(`${WORKFLOW_PATH}: required ${label} step is missing`);
continue;
}
const softeningReason = getRequiredStepSofteningReason(step);
if (softeningReason) {
failures.push(`${WORKFLOW_PATH}:${step.lineNumber}: required ${label} step must not use ${softeningReason}`);
} else {
passes.push(`${WORKFLOW_PATH}:${step.lineNumber}: required ${label} step is not softened`);
}
}
return { passes, failures, observations: [] };
}
export function checkExactRevisionRlsGateContract(content, analysis) {
const passes = [];
const failures = [];
const steps = getStepBlocks(content, analysis);
const checkout = steps.find(({ text }) => /name:\s*Verify checkout revision/i.test(text));
const install = steps.find(({ text }) => /name:\s*Install exact-revision dependencies/i.test(text));
const tenantPolicy = getStepForRunCommand(content, (command) => command.includes("pnpm run verify:tenant-policy-contract"), analysis);
const releaseReadiness = getStepForRunCommand(content, (command) => command.includes("pnpm run verify:rls-release-readiness"), analysis);
const liveRls = getStepForRunCommand(content, (command) => command.includes("pnpm run verify:rls-live"), analysis);
const registryLogin = getStepForRunCommand(content, (command) => /\bdocker\s+login\b/.test(command), analysis);
const checkoutIsExact =
checkout &&
/git\s+fetch\s+--no-tags\s+origin\s+main/.test(checkout.text) &&
/git\s+cat-file\s+-e\s+"\$IMAGE_TAG\^\{commit\}"/.test(checkout.text) &&
/git\s+merge-base\s+--is-ancestor\s+"\$IMAGE_TAG"\s+origin\/main/.test(checkout.text) &&
/git\s+checkout\s+--detach\s+"\$IMAGE_TAG"/.test(checkout.text) &&
/\[\s*"\$CHECKOUT_SHA"\s*!=\s*"\$IMAGE_TAG"\s*\]/.test(checkout.text);
if (checkoutIsExact) {
passes.push(`${WORKFLOW_PATH}:${checkout.lineNumber}: stage and promote check out the exact deployment SHA before release gates`);
} else {
failures.push(`${WORKFLOW_PATH}: stage and promote must check out and verify IMAGE_TAG before release gates`);
}
if (install && /pnpm\s+install\s+--frozen-lockfile\s+--prefer-offline/.test(install.text)) {
passes.push(`${WORKFLOW_PATH}:${install.lineNumber}: exact deployment revision installs its frozen dependency graph`);
} else {
failures.push(`${WORKFLOW_PATH}: exact deployment revision must run pnpm install --frozen-lockfile before release gates`);
}
const ordered =
checkout &&
install &&
tenantPolicy &&
releaseReadiness &&
liveRls &&
registryLogin &&
checkout.lineNumber < install.lineNumber &&
install.lineNumber < tenantPolicy.lineNumber &&
tenantPolicy.lineNumber < releaseReadiness.lineNumber &&
releaseReadiness.lineNumber < liveRls.lineNumber &&
liveRls.lineNumber < registryLogin.lineNumber;
if (ordered) {
passes.push(`${WORKFLOW_PATH}: tenant contract, release readiness, and live RLS gate the exact SHA before registry or deploy mutation`);
} else {
failures.push(`${WORKFLOW_PATH}: exact-SHA tenant contract, release readiness, and live RLS gates must complete before registry or deploy mutation`);
}
if (liveRls && /^\s*HIVEGRID_RLS_REQUIRE_MIGRATION_CHAIN_PARITY:\s*["']?1["']?\s*$/m.test(liveRls.text)) {
passes.push(`${WORKFLOW_PATH}:${liveRls.lineNumber}: exact-revision live RLS gate requires strict migration-chain parity`);
} else {
failures.push(`${WORKFLOW_PATH}: exact-revision live RLS gate must set HIVEGRID_RLS_REQUIRE_MIGRATION_CHAIN_PARITY: "1"`);
}
return { passes, failures, observations: [] };
}
export function checkMigratorRoleProbeContract(rootDir) {
const passes = [];
const failures = [];
const startPath = path.join(rootDir, "start.sh");
const verifierPath = path.join(rootDir, "scripts/verify-migrator-role.mjs");
const adopterPath = path.join(rootDir, "scripts/adopt-prisma-baseline.mjs");
const dockerfilePath = path.join(rootDir, "Dockerfile");
if (!fs.existsSync(startPath) || !fs.existsSync(verifierPath) || !fs.existsSync(adopterPath) || !fs.existsSync(dockerfilePath)) {
return {
passes,
failures: [`${WORKFLOW_PATH}: one-shot deploy migration requires start.sh, the migrator role verifier, the Prisma baseline adopter, and Dockerfile`],
observations: [],
};
}
const start = fs.readFileSync(startPath, "utf8");
const verifier = fs.readFileSync(verifierPath, "utf8");
const dockerfile = fs.readFileSync(dockerfilePath, "utf8");
const probeIndex = start.indexOf("node /myapp/scripts/verify-migrator-role.mjs");
const adoptionIndex = start.indexOf("node /myapp/scripts/adopt-prisma-baseline.mjs --adopt");
const migrationIndex = start.indexOf("node /myapp/node_modules/prisma/build/index.js migrate deploy");
const schemaVerificationIndex = start.indexOf("node /myapp/scripts/adopt-prisma-baseline.mjs --verify-current-schema");
const permissionCatalogIndex = start.indexOf("node /myapp/packages/db/dist/deploy-permission-catalog-sync.server.js");
const strict =
probeIndex >= 0 &&
adoptionIndex > probeIndex &&
migrationIndex > adoptionIndex &&
schemaVerificationIndex > migrationIndex &&
permissionCatalogIndex > schemaVerificationIndex &&
/sessionRole\s*!==\s*REQUIRED_MIGRATOR_ROLE/.test(verifier) &&
/isSuperuser\s*!==\s*false/.test(verifier) &&
/bypassesRls\s*!==\s*false/.test(verifier) &&
/hivegrid_rls_definer/.test(verifier) &&
/COPY\s+--from=build\s+\/myapp\/scripts\/verify-migrator-role\.mjs\s+\/myapp\/scripts\/verify-migrator-role\.mjs/.test(dockerfile) &&
/COPY\s+--from=build\s+\/myapp\/scripts\/adopt-prisma-baseline\.mjs\s+\/myapp\/scripts\/adopt-prisma-baseline\.mjs/.test(dockerfile);
if (strict) {
passes.push(
`${WORKFLOW_PATH}: one-shot migrations verify the exact non-superuser migrator role, adopt the baseline, migrate, verify current schema, and then sync permissions`
);
} else {
failures.push(
`${WORKFLOW_PATH}: one-shot migrations must verify the exact non-superuser hivegrid_migrator role, adopt the reviewed baseline, migrate, verify current schema, and only then sync permissions`
);
}
return { passes, failures, observations: [] };
}
export function checkCoolifyMutationPreflightContract(content, analysis) {
const passes = [];
const failures = [];
const steps = getStepBlocks(content, analysis);
for (const environment of ["staging", "production"]) {
const inventory = steps.find(({ text }) => new RegExp(`name:\\s*Validate Coolify ${environment} runtime preflight`, "i").test(text));
const migration = steps.find(({ text }) => new RegExp(`name:\\s*Run ${environment} one-shot database migration`, "i").test(text));
const workers = steps.find(({ text }) => new RegExp(`name:\\s*Ensure Coolify ${environment} worker groups`, "i").test(text));
const mutation = steps.find(({ text }) => new RegExp(`name:\\s*Configure Coolify ${environment} runtime resources`, "i").test(text));
const expectedPrefix = environment === "staging" ? "COOLIFY_STAGING" : "COOLIFY";
const inventoryIsStrict =
inventory &&
/coolify-deploy\.mjs\s+assert-runtime-preflight/.test(inventory.text) &&
/\$REGISTRY\/\$IMAGE_NAMESPACE\/\$IMAGE_PREFIX\/web/.test(inventory.text) &&
/\$REGISTRY\/\$IMAGE_NAMESPACE\/\$IMAGE_PREFIX\/api/.test(inventory.text) &&
/\$REGISTRY\/\$IMAGE_NAMESPACE\/\$IMAGE_PREFIX\/mcp/.test(inventory.text) &&
/\$REGISTRY\/\$IMAGE_NAMESPACE\/\$IMAGE_PREFIX\/hocuspocus/.test(inventory.text) &&
/\$REGISTRY\/\$IMAGE_NAMESPACE\/\$IMAGE_PREFIX\/worker/.test(inventory.text) &&
!FORBIDDEN_RUNTIME_DB_FLAG_PATTERN.test(inventory.text) &&
!/(?:--request\s+(?:PATCH|POST|PUT|DELETE)|\/deploy\?uuid=|coolify-deploy\.mjs\s+(?:set-envs|set-web-surface-release|trigger|configure-(?:mcp-healthcheck|legacy-rollback-healthcheck))|coolify-ensure-worker-groups\.mjs)/.test(
inventory.text
) &&
inventory.text.includes(`${expectedPrefix}_RESOURCE_UUIDS`) &&
inventory.text.includes(`${expectedPrefix}_TOKEN`) &&
inventory.text.includes(`${expectedPrefix}_API_URL`);
if (
inventoryIsStrict &&
migration &&
workers &&
mutation &&
inventory.lineNumber < migration.lineNumber &&
migration.lineNumber < workers.lineNumber &&
workers.lineNumber < mutation.lineNumber
) {
passes.push(
`${WORKFLOW_PATH}:${inventory.lineNumber}: ${environment} read-only eleven-resource runtime preflight runs before migration, worker reconciliation, and deploy mutation`
);
} else {
failures.push(
`${WORKFLOW_PATH}: ${environment} must enforce read-only eleven-resource runtime preflight < one-shot migration < worker reconciliation and deploy metadata mutation`
);
}
const hasImmutableWebAndApiRevision =
mutation &&
/coolify-deploy\.mjs\s+assert-no-app-revision-override\s+"\$WEB_RESOURCE_UUID"/.test(mutation.text) &&
/coolify-deploy\.mjs\s+assert-no-app-revision-override\s+"\$ADMIN_RESOURCE_UUID"/.test(mutation.text) &&
/coolify-deploy\.mjs\s+assert-no-app-revision-override\s+"\$PORTAL_RESOURCE_UUID"/.test(mutation.text) &&
/coolify-deploy\.mjs\s+assert-no-app-revision-override\s+"\$API_RESOURCE_UUID"/.test(mutation.text);
if (hasImmutableWebAndApiRevision) {
passes.push(
`${WORKFLOW_PATH}:${mutation.lineNumber}: ${environment} core web, admin, portal, and API reject mutable Coolify APP_REVISION overrides before mutation`
);
} else {
failures.push(`${WORKFLOW_PATH}: ${environment} web and API must reject APP_REVISION Coolify overrides before deployment metadata is mutated`);
}
const appAssertions = mutation?.text.match(new RegExp(APP_RUNTIME_ASSERTION_PATTERN.source, "g"))?.length ?? 0;
if (
appAssertions === 6 &&
mutation &&
PORTAL_RUNTIME_ASSERTION_PATTERN.test(mutation.text) &&
WORKER_RUNTIME_ASSERTION_PATTERN.test(mutation.text) &&
!FORBIDDEN_RUNTIME_DB_FLAG_PATTERN.test(mutation.text)
) {
passes.push(
`${WORKFLOW_PATH}:${mutation.lineNumber}: ${environment} runtime resources enforce app/portal/worker RLS credentials without boot or seed flags`
);
} else {
failures.push(
`${WORKFLOW_PATH}: ${environment} must verify core web/admin/API/MCP/Hocuspocus as hivegrid_app, portal as hivegrid_portal, and every worker as hivegrid_worker without RLS bypass or boot/seed flags`
);
}
const webPushAssertionIndex = mutation?.text.search(WEB_PUSH_RUNTIME_ASSERTION_PATTERN) ?? -1;
const firstPatchIndex = mutation?.text.indexOf("--request PATCH") ?? -1;
if (mutation && webPushAssertionIndex >= 0 && firstPatchIndex > webPushAssertionIndex) {
passes.push(`${WORKFLOW_PATH}:${mutation.lineNumber}: ${environment} verifies the shared Web Push runtime contract before Coolify mutation`);
} else {
failures.push(`${WORKFLOW_PATH}: ${environment} must verify matching web and webhook-worker VAPID configuration before Coolify mutation`);
}
}
return { passes, failures, observations: [] };
}
export function checkWebSurfacePairContract(rootDir, content, analysis) {
const passes = [];
const failures = [];
const steps = getStepBlocks(content, analysis);
if (/WEB_RESOURCE_UUID=.*apps\.find\([\s\S]*?docker_registry_image_name\s*===\s*process\.env\.WEB_IMAGE/.test(content)) {
failures.push(`${WORKFLOW_PATH}: web resources must not be selected through the first matching image`);
} else {
passes.push(`${WORKFLOW_PATH}: web resources are not selected through an image-first match`);
}
for (const environment of ["staging", "production"]) {
const upper = environment === "staging" ? "STAGING" : "PRODUCTION";
const inventory = steps.find(({ text }) => new RegExp(`name:\\s*Validate Coolify ${environment} runtime preflight`, "i").test(text));
const mutation = steps.find(({ text }) => new RegExp(`name:\\s*Configure Coolify ${environment} runtime resources`, "i").test(text));
const trigger = steps.find(({ text }) => new RegExp(`name:\\s*Trigger Coolify ${environment} deploy`, "i").test(text));
const wait = steps.find(({ text }) => new RegExp(`name:\\s*Wait for Coolify ${environment} health`, "i").test(text));
const resolvesExplicitTrio =
inventory &&
/coolify-deploy\.mjs\s+assert-runtime-preflight/.test(inventory.text) &&
mutation &&
/WEB_SURFACE_RESOURCE_UUIDS="\$\([^\n]*coolify-deploy\.mjs resolve-web-surfaces/.test(mutation.text) &&
/WEB_RESOURCE_UUID="\$\{WEB_SURFACE_RESOURCE_UUIDS%%,\*\}"/.test(mutation.text) &&
/REMAINING_WEB_SURFACE_RESOURCE_UUIDS="\$\{WEB_SURFACE_RESOURCE_UUIDS#\*,\}"/.test(mutation.text) &&
/ADMIN_RESOURCE_UUID="\$\{REMAINING_WEB_SURFACE_RESOURCE_UUIDS%%,\*\}"/.test(mutation.text) &&
/PORTAL_RESOURCE_UUID="\$\{REMAINING_WEB_SURFACE_RESOURCE_UUIDS#\*,\}"/.test(mutation.text);
if (resolvesExplicitTrio) {
passes.push(`${WORKFLOW_PATH}:${mutation.lineNumber}: ${environment} resolves the explicit hivegrid-web/hivegrid-admin/hivegrid-portal trio`);
} else {
failures.push(`${WORKFLOW_PATH}: ${environment} must resolve distinct hivegrid-web, hivegrid-admin, and hivegrid-portal resources by explicit identity`);
}
const trioImmutableRelease =
mutation &&
/WEB_IMAGE_DIGEST="\$\(docker buildx imagetools inspect "\$WEB_IMAGE:\$IMAGE_TAG" --format '\{\{json \.Manifest\.Digest\}\}' \| tr -d '"'\)"/.test(
mutation.text
) &&
/grep -Eq '\^sha256:\[0-9a-f\]\{64\}\$'/.test(mutation.text) &&
WEB_SURFACE_RELEASE_PATTERN.test(mutation.text) &&
new RegExp(`${upper}_ADMIN_RESOURCE_UUID=\\$ADMIN_RESOURCE_UUID`).test(mutation.text) &&
new RegExp(`${upper}_PORTAL_RESOURCE_UUID=\\$PORTAL_RESOURCE_UUID`).test(mutation.text) &&
new RegExp(`${upper}_WEB_IMAGE_DIGEST=\\$WEB_IMAGE_DIGEST`).test(mutation.text) &&
PORTAL_RUNTIME_ASSERTION_PATTERN.test(mutation.text);
if (trioImmutableRelease) {
passes.push(`${WORKFLOW_PATH}:${mutation.lineNumber}: ${environment} patches all three web surfaces only as one full-SHA registry-digest release`);
} else {
failures.push(`${WORKFLOW_PATH}: ${environment} must patch core, admin, and portal together with one full SHA and one verified registry digest`);
}
const webTriggerIndex =
trigger?.text.search(
new RegExp(
`${upper}_WEB_DEPLOYMENT_UUID="\\$\\(node scripts/coolify-deploy\\.mjs trigger "\\$${upper}_WEB_RESOURCE_UUID"\\)"[ \\t]*(?:\\r?\\n|$)`,
),
) ?? -1;
const webWaitIndex =
trigger?.text.search(
new RegExp(
`coolify-deploy\\.mjs\\s+wait\\s+\\\\?\\s*"\\$${upper}_WEB_DEPLOYMENT_UUID"\\s+\\\\?\\s*"Coolify ${environment} hivegrid-web"[ \\t]*(?:\\r?\\n|$)`,
),
) ?? -1;
const adminTriggerIndex =
trigger?.text.search(
new RegExp(
`${upper}_ADMIN_DEPLOYMENT_UUID="\\$\\(node scripts/coolify-deploy\\.mjs trigger "\\$${upper}_ADMIN_RESOURCE_UUID"\\)"[ \\t]*(?:\\r?\\n|$)`,
),
) ?? -1;
const adminWaitIndex =
trigger?.text.search(
new RegExp(
`coolify-deploy\\.mjs\\s+wait\\s+\\\\?\\s*"\\$${upper}_ADMIN_DEPLOYMENT_UUID"\\s+\\\\?\\s*"Coolify ${environment} hivegrid-admin"[ \\t]*(?:\\r?\\n|$)`,
),
) ?? -1;
const portalTriggerIndex =
trigger?.text.search(
new RegExp(
`${upper}_PORTAL_DEPLOYMENT_UUID="\\$\\(node scripts/coolify-deploy\\.mjs trigger "\\$${upper}_PORTAL_RESOURCE_UUID"\\)"[ \\t]*(?:\\r?\\n|$)`,
),
) ?? -1;
const portalWaitIndex =
trigger?.text.search(
new RegExp(
`coolify-deploy\\.mjs\\s+wait\\s+\\\\?\\s*"\\$${upper}_PORTAL_DEPLOYMENT_UUID"\\s+\\\\?\\s*"Coolify ${environment} hivegrid-portal"[ \\t]*(?:\\r?\\n|$)`,
),
) ?? -1;
const deploysSeriallyAndFailFast =
trigger &&
webTriggerIndex >= 0 &&
webWaitIndex > webTriggerIndex &&
adminTriggerIndex > webWaitIndex &&
adminWaitIndex > adminTriggerIndex &&
portalTriggerIndex > adminWaitIndex &&
portalWaitIndex > portalTriggerIndex &&
!/(?:WEB_SURFACE_TRIGGER_FAILED|WEB_SURFACE_WAIT_FAILED)/.test(trigger.text) &&
!/if\s+[^\n]*coolify-deploy\.mjs\s+(?:trigger|wait)/.test(trigger.text);
if (deploysSeriallyAndFailFast) {
passes.push(`${WORKFLOW_PATH}:${trigger.lineNumber}: ${environment} deploys core, admin, and portal serially and fails on the first terminal trigger/wait`);
} else {
failures.push(`${WORKFLOW_PATH}: ${environment} must run trigger/wait for core < admin < portal without failure aggregation`);
}
const verifiesTrioAfterTerminalDeploySuccess =
wait &&
!/coolify-deploy\.mjs\s+wait/.test(wait.text) &&
new RegExp(`${upper}_WEB_DEPLOYMENT_UUID`).test(wait.text) &&
new RegExp(`${upper}_ADMIN_DEPLOYMENT_UUID`).test(wait.text) &&
new RegExp(`${upper}_PORTAL_DEPLOYMENT_UUID`).test(wait.text) &&
/web surface deployment UUIDs are missing/.test(wait.text) &&
/for SURFACE in core admin portal/.test(wait.text) &&
/EXPECTED_SERVICE="hivegrid-web"/.test(wait.text) &&
/EXPECTED_SERVICE="hivegrid-admin"/.test(wait.text) &&
/EXPECTED_SERVICE="hivegrid-portal"/.test(wait.text) &&
/payload\.surface/.test(wait.text) &&
/HEALTH_SURFACE="\$\(node -e/.test(wait.text) &&
/\[ -n "\$EXPECTED_SURFACE" \] && \[ "\$HEALTH_SURFACE" != "\$EXPECTED_SURFACE" \]/.test(wait.text) &&
verifiesLiveWebSurfaceReadiness(wait.text) &&
/EXPECTED_WEB_DIGEST_TAG="sha256-\$\{(?:STAGING|PRODUCTION)_WEB_IMAGE_DIGEST#sha256:\}"/.test(wait.text) &&
/CONFIGURED_IMAGE_TAG/.test(wait.text) &&
/\[ "\$CONFIGURED_IMAGE_TAG" != "\$EXPECTED_WEB_DIGEST_TAG" \]/.test(wait.text);
if (verifiesTrioAfterTerminalDeploySuccess) {
passes.push(`${WORKFLOW_PATH}:${wait.lineNumber}: ${environment} begins live revision polling only after all three serial deployment waits succeeded`);
} else {
failures.push(`${WORKFLOW_PATH}: ${environment} live polling must not absorb deployment wait failures and must verify all three named digest-pinned surfaces`);
}
const provesRolledOutDigest =
wait &&
/ROLLED_OUT_WEB_IMAGE_DIGEST="\$\(docker buildx imagetools inspect "\$WEB_IMAGE:\$IMAGE_TAG" --format '\{\{json \.Manifest\.Digest\}\}' \| tr -d '"'\)"/.test(
wait.text
) &&
new RegExp(`\\[ "\\$ROLLED_OUT_WEB_IMAGE_DIGEST" != "\\$${upper}_WEB_IMAGE_DIGEST" \\]`).test(wait.text) &&
WEB_SURFACE_RELEASE_PROOF_PATTERN.test(wait.text);
if (provesRolledOutDigest) {
passes.push(
`${WORKFLOW_PATH}:${wait.lineNumber}: ${environment} re-resolves the full-SHA registry digest and proves all three completed deployments use that digest`
);
} else {
failures.push(`${WORKFLOW_PATH}: ${environment} must re-resolve the full-SHA digest and bind all three completed web deployments to it`);
}
}
const drain = steps.find(({ text }) => /name:\s*Stop and drain Coolify staging portal-domain-controller/i.test(text));
const controllerAcceptance = steps.find(({ text }) => /name:\s*Trigger and verify Coolify staging portal-domain-controller/i.test(text));
const recovery = steps.find(({ text }) => /name:\s*Restore previous Coolify staging portal-domain-controller release on rollout failure/i.test(text));
const snapshotReleaseIndex = drain?.text.indexOf("snapshot-release") ?? -1;
const recoveryArmedIndex = drain?.text.indexOf("STAGING_PORTAL_DOMAIN_CONTROLLER_RECOVERY_ARMED=true") ?? -1;
const controllerDrainIndex = drain?.text.indexOf("stop-and-drain") ?? -1;
const controllerProofIndex = controllerAcceptance?.text.indexOf("assert-release") ?? -1;
const controllerAcceptedIndex = controllerAcceptance?.text.indexOf("STAGING_PORTAL_DOMAIN_CONTROLLER_RELEASE_ACCEPTED=true") ?? -1;
const recoverySetReleaseIndex = recovery?.text.indexOf("coolify-portal-domain-controller.mjs set-release") ?? -1;
const recoveryTriggerIndex = recovery?.text.indexOf('coolify-deploy.mjs trigger "$STAGING_PORTAL_DOMAIN_CONTROLLER_RESOURCE_UUID"') ?? -1;
const recoveryWaitIndex = recovery?.text.indexOf('coolify-deploy.mjs wait') ?? -1;
const recoveryProofIndex = recovery?.text.indexOf("coolify-portal-domain-controller.mjs assert-release") ?? -1;
const recoverySetReleaseBlock =
recovery && recoverySetReleaseIndex >= 0 && recoveryTriggerIndex > recoverySetReleaseIndex
? recovery.text.slice(recoverySetReleaseIndex, recoveryTriggerIndex)
: "";
const recoveryIsFailureSafe =
drain &&
controllerAcceptance &&
recovery &&
/snapshot-release/.test(drain.text) &&
/STAGING_PORTAL_DOMAIN_CONTROLLER_PREVIOUS_REVISION/.test(drain.text) &&
snapshotReleaseIndex >= 0 &&
recoveryArmedIndex >= 0 &&
controllerDrainIndex >= 0 &&
snapshotReleaseIndex < recoveryArmedIndex &&
recoveryArmedIndex < controllerDrainIndex &&
/STAGING_PORTAL_DOMAIN_CONTROLLER_RELEASE_ACCEPTED=true/.test(controllerAcceptance.text) &&
controllerProofIndex >= 0 &&
controllerAcceptedIndex > controllerProofIndex &&
/^\s*if:\s*always\(\)\s*$/mu.test(recovery.text) &&
/STAGING_PORTAL_DOMAIN_CONTROLLER_RECOVERY_ARMED:-false/.test(recovery.text) &&
/STAGING_PORTAL_DOMAIN_CONTROLLER_RELEASE_ACCEPTED:-false/.test(recovery.text) &&
/STAGING_PORTAL_DOMAIN_CONTROLLER_PREVIOUS_REVISION/.test(recovery.text) &&
/DEPLOY_ACTION" != "stage/.test(recovery.text) &&
/coolify-portal-domain-controller\.mjs\s+set-release/.test(recoverySetReleaseBlock) &&
/"\$STAGING_PORTAL_DOMAIN_CONTROLLER_PREVIOUS_REVISION"/.test(recoverySetReleaseBlock) &&
!/"\$IMAGE_TAG"/.test(recoverySetReleaseBlock) &&
/coolify-deploy\.mjs\s+trigger\s+"\$STAGING_PORTAL_DOMAIN_CONTROLLER_RESOURCE_UUID"/.test(recovery.text) &&
/coolify-deploy\.mjs\s+wait\s+\\?\s*"\$RECOVERY_DEPLOYMENT_UUID"/.test(recovery.text) &&
/coolify-portal-domain-controller\.mjs\s+assert-release[\s\S]*"\$STAGING_PORTAL_DOMAIN_CONTROLLER_PREVIOUS_REVISION"[\s\S]*"\$RECOVERY_DEPLOYMENT_UUID"/.test(
recovery.text
) &&
recoverySetReleaseIndex >= 0 &&
recoveryTriggerIndex > recoverySetReleaseIndex &&
recoveryWaitIndex > recoveryTriggerIndex &&
recoveryProofIndex > recoveryWaitIndex &&
drain.lineNumber < controllerAcceptance.lineNumber &&
controllerAcceptance.lineNumber < recovery.lineNumber;
if (recoveryIsFailureSafe) {
passes.push(`${WORKFLOW_PATH}:${recovery.lineNumber}: staging restores the validated pre-drain controller release after any failure before controller acceptance`);
} else {
failures.push(`${WORKFLOW_PATH}: staging must arm recovery before drain and restore the previous release unless the new controller release is accepted`);
}
const helperPath = path.join(rootDir, COOLIFY_DEPLOY_HELPER_PATH);
const source = fs.existsSync(helperPath) ? fs.readFileSync(helperPath, "utf8") : "";
const explicitIdentity =
/core:\s*["']hivegrid-web["']/.test(source) &&
/admin:\s*["']hivegrid-admin["']/.test(source) &&
/portal:\s*["']hivegrid-portal["']/.test(source) &&
/allowedWebImageApps\.length\s*!==\s*3/.test(source) &&
/coreMatches\.length\s*!==\s*1/.test(source) &&
/adminMatches\.length\s*!==\s*1/.test(source) &&
/portalMatches\.length\s*!==\s*1/.test(source) &&
/new Set\(\[core\.uuid,\s*admin\.uuid,\s*portal\.uuid\]\)\.size\s*!==\s*3/.test(source);
if (explicitIdentity) {
passes.push(`${COOLIFY_DEPLOY_HELPER_PATH}: web surface resolution requires exactly three distinct explicitly named resources`);
} else {
failures.push(`${COOLIFY_DEPLOY_HELPER_PATH}: must fail closed unless exactly hivegrid-web, hivegrid-admin, and hivegrid-portal share the web image on distinct UUIDs`);
}
const pairedMutation =
/async function setWebSurfaceRelease/.test(source) &&
/FULL_GIT_SHA_PATTERN\.test\(revision\)/.test(source) &&
/OCI_DIGEST_PATTERN\.test\(digest\)/.test(source) &&
/for \(const resource of mutations\)/.test(source) &&
/function coolifyDigestTag\(digest\)/.test(source) &&
/return digest\.replace\(["']:["'],\s*["']-["']\)/.test(source) &&
/pre_deployment_command:\s*["']["']/.test(source) &&
/post_deployment_command:\s*["']["']/.test(source) &&
/restoreWebSurfaceReleases\(patched\)/.test(source) &&
/updated\.some\(\(app\) => normalizeCoolifyValue\(app\?\.docker_registry_image_tag\) !== digestTag\)/.test(source);
if (pairedMutation) {
passes.push(`${COOLIFY_DEPLOY_HELPER_PATH}: rollout and operator rollback can mutate only the compensated three-resource SHA/digest trio`);
} else {
failures.push(`${COOLIFY_DEPLOY_HELPER_PATH}: web surface release mutation must patch, reread, and compensate all three resources as one SHA/digest trio`);
}
const completedDigestProof =
/async function assertWebSurfaceRelease/.test(source) &&
/assertWebSurfacePairByUuid\(\s*applications\[0\],\s*applications\[1\],\s*applications\[2\]/.test(source) &&
/docker_registry_image_tag\) !== digestTag/.test(source) &&
/running:healthy/.test(source) &&
/assertCompletedWebSurfaceDeployment\(deployments\[index\]/.test(source) &&
/deployment_uuid/.test(source) &&
/application_name/.test(source) &&
/application_id/.test(source);
if (completedDigestProof) {
passes.push(`${COOLIFY_DEPLOY_HELPER_PATH}: post-rollout proof binds all three healthy named resources and exact completed deployments to one OCI digest`);
} else {
failures.push(`${COOLIFY_DEPLOY_HELPER_PATH}: post-rollout proof must bind all three healthy named resources and exact completed deployments to one OCI digest`);
}
const portalDenylist =
/function isPortalForbiddenEnvKey/.test(source) &&
/key === ["']SESSION_SECRET["']/.test(source) &&
/AUTHENTIK_/.test(source) &&
/OAUTH/.test(source) &&
/BILLING\|LAGO\|MOLLIE\|STRIPE/.test(source) &&
/COOLIFY\|SSH\|TRAEFIK\|INGRESS\|CLOUDFLARE/.test(source) &&
/PORTAL_SERVER_/.test(source) &&
/DOMAIN_CONTROLLER/.test(source) &&
/values\.get\(["']WEB_RUNTIME_SURFACE["']\)\s*!==\s*["']portal["']/.test(source) &&
/values\.get\(["']PORTAL_SESSION_SECRET["']\)/.test(source);
if (portalDenylist) {
passes.push(`${COOLIFY_DEPLOY_HELPER_PATH}: portal runtime rejects core session, OAuth, billing, migrator, and ingress credentials`);
} else {
failures.push(`${COOLIFY_DEPLOY_HELPER_PATH}: portal runtime verifier must enforce its dedicated surface/session and credential denylist`);
}
const portalAuthDependencies =
/function assertPortalAuthDependencies/.test(source) &&
(source.match(/\n\s+assertPortalAuthDependencies\(values\);/g)?.length ?? 0) >= 1 &&
/values\.get\(["']CAPTCHA_PROVIDER["']\)/.test(source) &&
/CAP_INSTANCE_URL/.test(source) &&
/CAP_SITEKEY_LOGIN/.test(source) &&
/CAP_SITE_SECRET_LOGIN/.test(source) &&
/CAP_SITE_SECRET/.test(source) &&
/MCAPTCHA_INSTANCE_URL/.test(source) &&
/MCAPTCHA_SITEKEY_LOGIN/.test(source) &&
/MCAPTCHA_ACCOUNT_SECRET/.test(source) &&
/values\.get\(["']REDIS_URL["']\)/.test(source) &&
/values\.get\(["']BULLMQ_REDIS_URL["']\)/.test(source) &&
/isAllowedCaptchaInstanceUrl/.test(source) &&
/isValidRedisConnectionUrl/.test(source);
if (portalAuthDependencies) {
passes.push(`${COOLIFY_DEPLOY_HELPER_PATH}: portal runtime requires its provider-specific login CAPTCHA and Redis/BullMQ dependencies`);
} else {
failures.push(`${COOLIFY_DEPLOY_HELPER_PATH}: portal runtime verifier must fail closed on incomplete login CAPTCHA or Redis/BullMQ configuration`);
}
const fullRuntimePreflight =
/async function assertRuntimePreflight/.test(source) &&
/WORKER_GROUP_NAMES = Object\.freeze\(\["general", "mail", "import", "webhook", "plugin"\]\)/.test(source) &&
/selectWebSurfacePair\(apps, allowed, webImage\)/.test(source) &&
/selectUniqueAllowedImageApplication\(apps, allowed, apiImage, "API"\)/.test(source) &&
/selectUniqueAllowedImageApplication\(apps, allowed, mcpImage, "MCP"\)/.test(source) &&
/selectUniqueAllowedImageApplication\(apps, allowed, hocuspocusImage, "Hocuspocus"\)/.test(source) &&
/WORKER_QUEUE_GROUP/.test(source) &&
/runtimeResources\.map\(\(\{ app \}\) => app\.uuid\)/.test(source) &&
/assertRuntimeDatabaseEntries\(resource\.entries, resource\.role, resource\.surface\)/.test(source) &&
/runtime APP_REVISION overrides must be removed before migration/.test(source) &&
/hivegrid-web runtime must not contain PORTAL_SESSION_SECRET/.test(source) &&
/explicit positive DATABASE_POOL_MAX/.test(source) &&
/PgBouncer port 6432/.test(source) &&
/same normalized PORTAL_APP_HOST/.test(source) &&
/hivegrid-portal and worker-mail must set APP_PROTOCOL=https/.test(source) &&
/CORE_PUBLIC_HOST_ENV_KEYS/.test(source) &&
/PORTAL_APP_HOST must be distinct from core APP_BASE_DOMAIN, WEB_APP_HOST, ADMIN_APP_HOST, and API_HOST/.test(source) &&
/assertWebPushEntries\(coreRuntime\?\.entries \|\| \[\], webhookWorker\?\.entries \|\| \[\]\)/.test(source);
if (fullRuntimePreflight) {
passes.push(
`${COOLIFY_DEPLOY_HELPER_PATH}: read-only preflight validates all eleven runtime identities, immutable revisions, DB boundaries, pool caps, PgBouncer, portal-link isolation, and Web Push`
);
} else {
failures.push(
`${COOLIFY_DEPLOY_HELPER_PATH}: runtime preflight must validate eleven immutable runtime identities, DB/pool/PgBouncer bounds, session and portal-host isolation, and Web Push`
);
}
return { passes, failures, observations: [] };
}
export function checkBoundedCurlContract(content, analysis) {
const curlLines = getWorkflowLines(content, analysis).filter(({ line }) => /^\s*(?:if\s+!\s+)?curl(?:\s|$)/.test(line));
const unbounded = curlLines.filter(({ line }) => !/--connect-timeout\s+\d+/.test(line) || !/--max-time\s+\d+/.test(line));
if (curlLines.length > 0 && unbounded.length === 0) {
return {
passes: [`${WORKFLOW_PATH}: every direct curl call has connection and total time bounds`],
failures: [],
observations: [],
};
}
return {
passes: [],
failures:
unbounded.length > 0
? unbounded.map(({ lineNumber }) => `${WORKFLOW_PATH}:${lineNumber}: direct curl calls must set --connect-timeout and --max-time`)
: [`${WORKFLOW_PATH}: expected at least one bounded direct curl call`],
observations: [],
};
}
export function checkDatabaseMigrationDeployContract(content, analysis) {
const passes = [];
const failures = [];
const steps = getStepBlocks(content, analysis);
const migrationStep = steps.find(({ text }) => /\bRun staging one-shot database migration\b/i.test(text));
const productionMigrationStep = steps.find(({ text }) => /\bRun production one-shot database migration\b/i.test(text));
const stagingRuntimeStep = steps.find(({ text }) => /\bConfigure Coolify staging runtime resources\b/i.test(text));
const productionRuntimeStep = steps.find(({ text }) => /\bConfigure Coolify production runtime resources\b/i.test(text));
if (DEMO_PRO_USER_PASSWORD_PATTERN.test(content)) {
failures.push(`${WORKFLOW_PATH}: deploy workflow must not create or overwrite the operator-only SEED_DEMO_USER_PASSWORD`);
} else {
passes.push(`${WORKFLOW_PATH}: deploy workflow leaves the operator-only SEED_DEMO_USER_PASSWORD untouched`);
}
if (!migrationStep) {
failures.push(`${WORKFLOW_PATH}: missing staging one-shot database migration step`);
return { passes, failures, observations: [] };
}
if (!productionMigrationStep) {
failures.push(`${WORKFLOW_PATH}: missing production one-shot database migration step`);
}
const migrationContracts = [
["staging", migrationStep, "STAGING_MIGRATOR_DATABASE_URL", "stage"],
["production", productionMigrationStep, "MIGRATOR_DATABASE_URL", "promote"],
];
for (const [environment, step, secretName, action] of migrationContracts) {
if (!step) continue;
const hasScopedSecret = step.text.includes(`MIGRATOR_DATABASE_URL: \${{ secrets.${secretName} }}`);
const strict =
hasScopedSecret &&
ONE_SHOT_MIGRATOR_PATTERN.test(step.text) &&
new RegExp(`DEPLOY_ACTION" != "${action}"`).test(step.text) &&
/if \[ -z "\$\{MIGRATOR_DATABASE_URL:-\}" \]; then/.test(step.text) &&
!/\bSEED_[A-Z0-9_]*\b/.test(step.text) &&
!/RLS_RUNTIME_MODE=off/.test(step.text);
if (strict) {
passes.push(`${WORKFLOW_PATH}:${step.lineNumber}: ${environment} migration is a fail-closed DIRECT_URL-only one-shot container`);
} else {
failures.push(`${WORKFLOW_PATH}:${step.lineNumber}: ${environment} migration must use only its step-scoped migrator secret and immutable web image`);
}
}
for (const [environment, step] of [
["staging", stagingRuntimeStep],
["production", productionRuntimeStep],
]) {
if (!step) {
failures.push(`${WORKFLOW_PATH}: missing Coolify ${environment} runtime resource configuration step`);
continue;
}
const appAssertions = step.text.match(new RegExp(APP_RUNTIME_ASSERTION_PATTERN.source, "g"))?.length ?? 0;
const strict =
WEB_SURFACE_RELEASE_PATTERN.test(step.text) &&
appAssertions === 6 &&
PORTAL_RUNTIME_ASSERTION_PATTERN.test(step.text) &&
WORKER_RUNTIME_ASSERTION_PATTERN.test(step.text) &&
!FORBIDDEN_RUNTIME_DB_FLAG_PATTERN.test(step.text) &&
/--request PATCH/.test(step.text) &&
/docker_registry_image_tag:\s*process\.env\.IMAGE_TAG/.test(step.text);
if (strict) {
passes.push(
`${WORKFLOW_PATH}:${step.lineNumber}: Coolify ${environment} runtime config pairs web surfaces, clears dead hooks, and enforces role-specific RLS credentials`
);
} else {
failures.push(
`${WORKFLOW_PATH}:${step.lineNumber}: Coolify ${environment} runtime config must enforce app/worker RLS credentials without migrator or seed flags`
);
}
if (imageTagPatchLoopIncludesAllDependentResources(step.text)) {
passes.push(`${WORKFLOW_PATH}:${step.lineNumber}: Coolify ${environment} image-tag patch covers API, MCP, Hocuspocus, and every worker group`);
} else {
failures.push(`${WORKFLOW_PATH}:${step.lineNumber}: Coolify ${environment} image-tag patch must cover API, MCP, Hocuspocus, and every worker group`);
}
}
const firstCoolifyStep = steps.find(
({ text }) => /\bTrigger Coolify staging deploy\b/i.test(text) || /coolify-deploy\.mjs\s+trigger/.test(text) || /\bcurl\b[\s\S]*\/deploy\?uuid=/.test(text)
);
const productionCoolifyStep = steps.find(({ text }) => /\bTrigger Coolify production deploy\b/i.test(text));
const lastImageBuildLine = Math.max(...REQUIRED_TARGETS.map((target) => getDockerTargetBuildLine(content, target, analysis)));
if (lastImageBuildLine > 0 && migrationStep.lineNumber > lastImageBuildLine) {
passes.push(`${WORKFLOW_PATH}:${migrationStep.lineNumber}: staging one-shot migration runs after Docker image publication`);
} else {
failures.push(`${WORKFLOW_PATH}:${migrationStep.lineNumber}: staging one-shot migration must run after Docker image publication`);
}
if (firstCoolifyStep && migrationStep.lineNumber < firstCoolifyStep.lineNumber) {
passes.push(`${WORKFLOW_PATH}:${migrationStep.lineNumber}: staging one-shot migration runs before Coolify deploy`);
} else {
failures.push(`${WORKFLOW_PATH}:${migrationStep.lineNumber}: staging one-shot migration must run before Coolify deploy`);
}
if (
productionMigrationStep &&
productionRuntimeStep &&
productionCoolifyStep &&
productionMigrationStep.lineNumber < productionRuntimeStep.lineNumber &&
productionMigrationStep.lineNumber < productionCoolifyStep.lineNumber
) {
passes.push(`${WORKFLOW_PATH}:${productionMigrationStep.lineNumber}: production one-shot migration runs before Coolify mutation and deploy`);
} else if (productionMigrationStep) {
failures.push(`${WORKFLOW_PATH}:${productionMigrationStep.lineNumber}: production one-shot migration must run before Coolify mutation and deploy`);
}
const stagingDeployStep = steps.find(({ text }) => /\bTrigger Coolify staging deploy\b/i.test(text));
const stagingWaitStep = steps.find(({ text }) => /\bWait for Coolify staging health\b/i.test(text));
const stagingDependentDeployStep = steps.find(({ text }) => /\bTrigger Coolify staging dependent deploys\b/i.test(text));
const productionDeployStep = steps.find(({ text }) => /\bTrigger Coolify production deploy\b/i.test(text));
const productionWaitStep = steps.find(({ text }) => /\bWait for Coolify production health\b/i.test(text));
const productionDependentDeployStep = steps.find(({ text }) => /\bTrigger Coolify production dependent deploys\b/i.test(text));
if (stagingDeployStep && /coolify-deploy\.mjs\s+trigger\s+"\$STAGING_WEB_RESOURCE_UUID"/.test(stagingDeployStep.text)) {
passes.push(`${WORKFLOW_PATH}:${stagingDeployStep.lineNumber}: Coolify staging deploy triggers the migration-bearing web app first`);
} else if (stagingDeployStep) {
failures.push(`${WORKFLOW_PATH}:${stagingDeployStep.lineNumber}: Coolify staging deploy must trigger only STAGING_WEB_RESOURCE_UUID first`);
}
if (stagingDeployStep && /deploy\?uuid=\$COOLIFY_STAGING_RESOURCE_UUIDS/.test(stagingDeployStep.text)) {
failures.push(`${WORKFLOW_PATH}:${stagingDeployStep.lineNumber}: Coolify staging deploy must not trigger all resources before migrations complete`);
}
if (
stagingDependentDeployStep &&
stagingWaitStep &&
stagingDependentDeployStep.lineNumber > stagingWaitStep.lineNumber &&
/for RESOURCE_UUID in \$\(printf "%s" "\$STAGING_DEPENDENT_RESOURCE_UUIDS" \| tr ',' ' '\)/.test(stagingDependentDeployStep.text) &&
/coolify-deploy\.mjs\s+trigger\s+"\$RESOURCE_UUID"/.test(stagingDependentDeployStep.text)
) {
passes.push(`${WORKFLOW_PATH}:${stagingDependentDeployStep.lineNumber}: Coolify staging dependent resources deploy after web health`);
} else {
failures.push(`${WORKFLOW_PATH}: Coolify staging dependent resources must deploy only after the web migration app is healthy`);
}
if (
stagingDependentDeployStep &&
(/deploy\?uuid=\$STAGING_DEPENDENT_RESOURCE_UUIDS/.test(stagingDependentDeployStep.text) ||
/coolify-deploy\.mjs\s+trigger\s+"\$STAGING_DEPENDENT_RESOURCE_UUIDS"/.test(stagingDependentDeployStep.text))
) {
failures.push(`${WORKFLOW_PATH}:${stagingDependentDeployStep.lineNumber}: Coolify staging dependent resources must deploy one UUID per request`);
}
if (productionDeployStep && /coolify-deploy\.mjs\s+trigger\s+"\$PRODUCTION_WEB_RESOURCE_UUID"/.test(productionDeployStep.text)) {
passes.push(`${WORKFLOW_PATH}:${productionDeployStep.lineNumber}: Coolify production deploy triggers the migration-bearing web app first`);
} else if (productionDeployStep) {
failures.push(`${WORKFLOW_PATH}:${productionDeployStep.lineNumber}: Coolify production deploy must trigger only PRODUCTION_WEB_RESOURCE_UUID first`);
}
if (productionDeployStep && /deploy\?uuid=\$COOLIFY_RESOURCE_UUIDS/.test(productionDeployStep.text)) {
failures.push(`${WORKFLOW_PATH}:${productionDeployStep.lineNumber}: Coolify production deploy must not trigger all resources before migrations complete`);
}
if (
productionDependentDeployStep &&
productionWaitStep &&
productionDependentDeployStep.lineNumber > productionWaitStep.lineNumber &&
/for RESOURCE_UUID in \$\(printf "%s" "\$PRODUCTION_DEPENDENT_RESOURCE_UUIDS" \| tr ',' ' '\)/.test(productionDependentDeployStep.text) &&
/coolify-deploy\.mjs\s+trigger\s+"\$RESOURCE_UUID"/.test(productionDependentDeployStep.text)
) {
passes.push(`${WORKFLOW_PATH}:${productionDependentDeployStep.lineNumber}: Coolify production dependent resources deploy after web health`);
} else {
failures.push(`${WORKFLOW_PATH}: Coolify production dependent resources must deploy only after the web migration app is healthy`);
}
if (
productionDependentDeployStep &&
(/deploy\?uuid=\$PRODUCTION_DEPENDENT_RESOURCE_UUIDS/.test(productionDependentDeployStep.text) ||
/coolify-deploy\.mjs\s+trigger\s+"\$PRODUCTION_DEPENDENT_RESOURCE_UUIDS"/.test(productionDependentDeployStep.text))
) {
failures.push(`${WORKFLOW_PATH}:${productionDependentDeployStep.lineNumber}: Coolify production dependent resources must deploy one UUID per request`);
}
return { passes, failures, observations: [] };
}
export function checkCoolifyStagingHealthWaitContract(content, analysis) {
const passes = [];
const failures = [];
const steps = getStepBlocks(content, analysis);
const triggerStep = steps.find(
({ text }) => /\bTrigger Coolify staging deploy\b/i.test(text) || /coolify-deploy\.mjs\s+trigger/.test(text) || /\bcurl\b[\s\S]*\/deploy\?uuid=/.test(text)
);
const waitStep = steps.find(({ text }) => /\bWait for Coolify staging health\b/i.test(text));
if (!waitStep) {
failures.push(`${WORKFLOW_PATH}: missing Coolify staging health wait step`);
return { passes, failures, observations: [] };
}
if (/applications\/\$RESOURCE_UUID/.test(waitStep.text) && /running:healthy/.test(waitStep.text)) {
passes.push(`${WORKFLOW_PATH}:${waitStep.lineNumber}: Coolify staging health wait polls application status`);
} else {
failures.push(`${WORKFLOW_PATH}:${waitStep.lineNumber}: Coolify staging health wait must poll application status until running:healthy`);
}
if (verifiesLiveHealthRevision(waitStep.text)) {
passes.push(`${WORKFLOW_PATH}:${waitStep.lineNumber}: Coolify staging health wait verifies live revision when a health URL exists`);
} else {
failures.push(
`${WORKFLOW_PATH}:${waitStep.lineNumber}: Coolify staging health wait must verify /healthz revision against IMAGE_TAG when a health URL exists`
);
}
if (verifiesLiveWebSurfaceReadiness(waitStep.text)) {
passes.push(
`${WORKFLOW_PATH}:${waitStep.lineNumber}: Coolify staging web surfaces fail closed on database readiness and verify service/surface/revision identity`
);
} else {
failures.push(
`${WORKFLOW_PATH}:${waitStep.lineNumber}: Coolify staging web health wait must require /readyz status=ready with matching service, surface, and revision`
);
}
if (/\b(?:echo|printf)\b[^\n]*(?:hivegrid_app|hivegrid_portal)/.test(waitStep.text) || /\b(?:cat|tee)\b[^\n]*\$READINESS_JSON/.test(waitStep.text)) {
failures.push(`${WORKFLOW_PATH}:${waitStep.lineNumber}: Coolify staging readiness proof must not print database role names or readiness payloads`);
} else {
passes.push(`${WORKFLOW_PATH}:${waitStep.lineNumber}: Coolify staging readiness proof remains redacted`);
}
if (/has no health URL; live revision probe skipped["']/.test(waitStep.text)) {
failures.push(`${WORKFLOW_PATH}:${waitStep.lineNumber}: Coolify staging health wait must not pass-open missing health URLs for HTTP resources`);
} else {
passes.push(`${WORKFLOW_PATH}:${waitStep.lineNumber}: Coolify staging health wait fails missing health URLs for HTTP resources`);
}
if (/verify_live_revision/.test(waitStep.text) && !/verify_live_revision\s+"\$APP_JSON"\s+"[^"]+"\s+required/.test(waitStep.text)) {
failures.push(`${WORKFLOW_PATH}:${waitStep.lineNumber}: Coolify staging web health wait must require a live health URL`);
} else {
passes.push(`${WORKFLOW_PATH}:${waitStep.lineNumber}: Coolify staging web health wait requires a live health URL`);
}
if (
/EXPECTED_SERVICE="hivegrid-web"/.test(waitStep.text) &&
/EXPECTED_SERVICE="hivegrid-admin"/.test(waitStep.text) &&
/EXPECTED_SERVICE="hivegrid-portal"/.test(waitStep.text) &&
/for SURFACE in core admin portal/.test(waitStep.text) &&
/verify_live_revision\s+"\$APP_JSON"\s+"[^"]+"\s+required\s+"\$IMAGE_TAG"\s+"\$EXPECTED_SERVICE"\s+"\$SURFACE"/.test(waitStep.text)
) {
passes.push(`${WORKFLOW_PATH}:${waitStep.lineNumber}: Coolify staging health verifies all three web service and surface identities`);
} else {
failures.push(`${WORKFLOW_PATH}:${waitStep.lineNumber}: Coolify staging web health must verify hivegrid-web/core, hivegrid-admin/admin, and hivegrid-portal/portal`);
}
if (/echo\s+["'][^"']*\$RESOURCE_UUID/.test(waitStep.text) || /printf\s+["'][^"']*\$RESOURCE_UUID/.test(waitStep.text)) {
failures.push(`${WORKFLOW_PATH}:${waitStep.lineNumber}: Coolify staging health wait must not print resource UUIDs`);
} else {
passes.push(`${WORKFLOW_PATH}:${waitStep.lineNumber}: Coolify staging health wait redacts resource UUIDs`);
}
if (triggerStep && waitStep.lineNumber > triggerStep.lineNumber) {
passes.push(`${WORKFLOW_PATH}:${waitStep.lineNumber}: Coolify staging health wait runs after deploy trigger`);
} else {
failures.push(`${WORKFLOW_PATH}:${waitStep.lineNumber}: Coolify staging health wait must run after deploy trigger`);
}
return { passes, failures, observations: [] };
}
export function checkCoolifyProductionHealthWaitContract(content, analysis) {
const passes = [];
const failures = [];
const steps = getStepBlocks(content, analysis);
const triggerStep = steps.find(({ text }) => /\bTrigger Coolify production deploy\b/i.test(text));
const waitStep = steps.find(({ text }) => /\bWait for Coolify production health\b/i.test(text));
if (!waitStep) {
failures.push(`${WORKFLOW_PATH}: missing Coolify production health wait step`);
return { passes, failures, observations: [] };
}
if (/applications\/\$RESOURCE_UUID/.test(waitStep.text) && /running:healthy/.test(waitStep.text)) {
passes.push(`${WORKFLOW_PATH}:${waitStep.lineNumber}: Coolify production health wait polls application status`);
} else {
failures.push(`${WORKFLOW_PATH}:${waitStep.lineNumber}: Coolify production health wait must poll application status until running:healthy`);
}
if (verifiesLiveHealthRevision(waitStep.text)) {
passes.push(`${WORKFLOW_PATH}:${waitStep.lineNumber}: Coolify production health wait verifies live revision when a health URL exists`);
} else {
failures.push(
`${WORKFLOW_PATH}:${waitStep.lineNumber}: Coolify production health wait must verify /healthz revision against IMAGE_TAG when a health URL exists`
);
}
if (verifiesLiveWebSurfaceReadiness(waitStep.text)) {
passes.push(
`${WORKFLOW_PATH}:${waitStep.lineNumber}: Coolify production web surfaces fail closed on database readiness and verify service/surface/revision identity`
);
} else {
failures.push(
`${WORKFLOW_PATH}:${waitStep.lineNumber}: Coolify production web health wait must require /readyz status=ready with matching service, surface, and revision`
);
}
if (/\b(?:echo|printf)\b[^\n]*(?:hivegrid_app|hivegrid_portal)/.test(waitStep.text) || /\b(?:cat|tee)\b[^\n]*\$READINESS_JSON/.test(waitStep.text)) {
failures.push(`${WORKFLOW_PATH}:${waitStep.lineNumber}: Coolify production readiness proof must not print database role names or readiness payloads`);
} else {
passes.push(`${WORKFLOW_PATH}:${waitStep.lineNumber}: Coolify production readiness proof remains redacted`);
}
if (/has no health URL; live revision probe skipped["']/.test(waitStep.text)) {
failures.push(`${WORKFLOW_PATH}:${waitStep.lineNumber}: Coolify production health wait must not pass-open missing health URLs for HTTP resources`);
} else {
passes.push(`${WORKFLOW_PATH}:${waitStep.lineNumber}: Coolify production health wait fails missing health URLs for HTTP resources`);
}
if (/verify_live_revision/.test(waitStep.text) && !/verify_live_revision\s+"\$APP_JSON"\s+"[^"]+"\s+required/.test(waitStep.text)) {
failures.push(`${WORKFLOW_PATH}:${waitStep.lineNumber}: Coolify production web health wait must require a live health URL`);
} else {
passes.push(`${WORKFLOW_PATH}:${waitStep.lineNumber}: Coolify production web health wait requires a live health URL`);
}
if (
/EXPECTED_SERVICE="hivegrid-web"/.test(waitStep.text) &&
/EXPECTED_SERVICE="hivegrid-admin"/.test(waitStep.text) &&
/EXPECTED_SERVICE="hivegrid-portal"/.test(waitStep.text) &&
/for SURFACE in core admin portal/.test(waitStep.text) &&
/verify_live_revision\s+"\$APP_JSON"\s+"[^"]+"\s+required\s+"\$IMAGE_TAG"\s+"\$EXPECTED_SERVICE"\s+"\$SURFACE"/.test(waitStep.text)
) {
passes.push(`${WORKFLOW_PATH}:${waitStep.lineNumber}: Coolify production health verifies all three web service and surface identities`);
} else {
failures.push(`${WORKFLOW_PATH}:${waitStep.lineNumber}: Coolify production web health must verify hivegrid-web/core, hivegrid-admin/admin, and hivegrid-portal/portal`);
}
if (/echo\s+["'][^"']*\$RESOURCE_UUID/.test(waitStep.text) || /printf\s+["'][^"']*\$RESOURCE_UUID/.test(waitStep.text)) {
failures.push(`${WORKFLOW_PATH}:${waitStep.lineNumber}: Coolify production health wait must not print resource UUIDs`);
} else {
passes.push(`${WORKFLOW_PATH}:${waitStep.lineNumber}: Coolify production health wait redacts resource UUIDs`);
}
if (triggerStep && waitStep.lineNumber > triggerStep.lineNumber) {
passes.push(`${WORKFLOW_PATH}:${waitStep.lineNumber}: Coolify production health wait runs after deploy trigger`);
} else {
failures.push(`${WORKFLOW_PATH}:${waitStep.lineNumber}: Coolify production health wait must run after deploy trigger`);
}
return { passes, failures, observations: [] };
}
function checkCoolifyDependentHealthWaitContract(content, options, analysis) {
const passes = [];
const failures = [];
const steps = getStepBlocks(content, analysis);
const deployStep = steps.find(({ text }) => new RegExp(`\\b${options.deployStepName}\\b`, "i").test(text));
const waitStep = steps.find(({ text }) => new RegExp(`\\b${options.waitStepName}\\b`, "i").test(text));
if (!waitStep) {
failures.push(`${WORKFLOW_PATH}: missing ${options.label} dependent health wait step`);
return { passes, failures, observations: [] };
}
if (/applications\/\$RESOURCE_UUID/.test(waitStep.text) && /running:healthy/.test(waitStep.text)) {
passes.push(`${WORKFLOW_PATH}:${waitStep.lineNumber}: ${options.label} dependent health wait polls application status`);
} else {
failures.push(`${WORKFLOW_PATH}:${waitStep.lineNumber}: ${options.label} dependent health wait must poll application status until running:healthy`);
}
if (
/DEPLOYMENT_WAIT_PIDS=/.test(waitStep.text) &&
/node scripts\/coolify-deploy\.mjs wait "\$DEPLOYMENT_UUID"[\s\S]*&/.test(waitStep.text) &&
/for PID in \$DEPLOYMENT_WAIT_PIDS; do[\s\S]*wait "\$PID"/.test(waitStep.text)
) {
passes.push(`${WORKFLOW_PATH}:${waitStep.lineNumber}: ${options.label} dependent deployment waits fan out and aggregate failures`);
} else {
failures.push(`${WORKFLOW_PATH}:${waitStep.lineNumber}: ${options.label} dependent deployment waits must fan out with PID aggregation`);
}
const triggerFailureReference = `\${${options.triggerFailureEnv}:-false}`;
if (
deployStep &&
deployStep.text.includes(`${options.triggerFailureEnv}=false`) &&
/if DEPLOYMENT_UUID="\$\(node scripts\/coolify-deploy\.mjs trigger "\$RESOURCE_UUID"\)"; then/.test(deployStep.text) &&
deployStep.text.includes(`${options.deploymentEnv}=\$${options.deploymentEnv}`) &&
deployStep.text.includes(`${options.triggerFailureEnv}=\$${options.triggerFailureEnv}`) &&
waitStep.text.includes(triggerFailureReference) &&
waitStep.text.includes(`One or more ${options.label.replace("Coolify ", "Coolify ")} dependent deploy triggers failed`)
) {
passes.push(`${WORKFLOW_PATH}:${waitStep.lineNumber}: ${options.label} preserves queued deployment UUIDs and fails after partial trigger diagnostics`);
} else {
failures.push(
`${WORKFLOW_PATH}:${waitStep.lineNumber}: ${options.label} must persist queued deployment UUIDs, record trigger failures, wait for diagnostics, and then fail the rollout`
);
}
if (
deployStep &&
/DEPLOYMENT_INDEX=0/.test(deployStep.text) &&
/DEPLOYMENT_INDEX=\$\(\(DEPLOYMENT_INDEX \+ 1\)\)/.test(deployStep.text) &&
/if DEPLOYMENT_UUID="\$\(node scripts\/coolify-deploy\.mjs trigger "\$RESOURCE_UUID"\)"; then[\s\S]*if ! node scripts\/coolify-deploy\.mjs wait "\$DEPLOYMENT_UUID" "Coolify [^"]+ dependent deploy #\$DEPLOYMENT_INDEX"; then/.test(
deployStep.text
) &&
deployStep.text.includes(`${options.triggerFailureEnv}=true`)
) {
passes.push(`${WORKFLOW_PATH}:${deployStep.lineNumber}: ${options.label} serializes dependent deployments before enqueueing the next resource`);
} else {
failures.push(
`${WORKFLOW_PATH}:${deployStep?.lineNumber || waitStep.lineNumber}: ${options.label} must wait for each dependent deployment before enqueueing the next resource`
);
}
if (
new RegExp(`for RESOURCE_UUID in \\$\\(printf "%s" "\\$${options.resourceEnv}" \\| tr ',' ' '\\)`).test(waitStep.text) &&
/check_dependent_resource "\$RESOURCE_UUID" "\$RESOURCE_INDEX" &/.test(waitStep.text) &&
/RESOURCE_WAIT_PIDS=/.test(waitStep.text) &&
/for PID in \$RESOURCE_WAIT_PIDS; do[\s\S]*wait "\$PID"/.test(waitStep.text)
) {
passes.push(`${WORKFLOW_PATH}:${waitStep.lineNumber}: ${options.label} dependent health wait checks resources concurrently with PID aggregation`);
} else {
failures.push(
`${WORKFLOW_PATH}:${waitStep.lineNumber}: ${options.label} dependent health wait must fan out ${options.resourceEnv} one UUID per worker and aggregate failures`
);
}
if (verifiesLiveHealthRevision(waitStep.text)) {
passes.push(`${WORKFLOW_PATH}:${waitStep.lineNumber}: ${options.label} dependent health wait verifies live revision when a health URL exists`);
} else {
failures.push(
`${WORKFLOW_PATH}:${waitStep.lineNumber}: ${options.label} dependent health wait must verify /healthz revision against IMAGE_TAG when a health URL exists`
);
}
if (
new RegExp(`verify_live_revision\\s+"\\$APP_JSON"\\s+"[^"]+"\\s+auto\\s+(?:"\\$IMAGE_TAG"|${options.expectedAlias})\\s+api`).test(waitStep.text) ||
/EXPECTED_SERVICE="api"/.test(waitStep.text) ||
/EXPECTED_SERVICE="\$\{5:-api\}"/.test(waitStep.text)
) {
passes.push(`${WORKFLOW_PATH}:${waitStep.lineNumber}: ${options.label} dependent health wait verifies API service identity`);
} else {
failures.push(`${WORKFLOW_PATH}:${waitStep.lineNumber}: ${options.label} dependent health wait must verify service=api for HTTP dependent resources`);
}
if (
/HTTP_RESOURCE/.test(waitStep.text) &&
/WORKER_RESOURCE/.test(waitStep.text) &&
new RegExp(`${options.label.includes("staging") ? "STAGING" : "PRODUCTION"}_WORKER_RESOURCE_UUIDS`).test(waitStep.text) &&
new RegExp(`${options.label.includes("staging") ? "STAGING" : "PRODUCTION"}_HOCUSPOCUS_RESOURCE_UUID`).test(waitStep.text) &&
/RESOURCE_KIND="hocuspocus"/.test(waitStep.text) &&
/Hocuspocus Docker health and image revision are healthy/.test(waitStep.text) &&
/worker Coolify status and revision-bound command healthcheck are healthy/.test(waitStep.text) &&
/is an HTTP resource but has no health URL/.test(waitStep.text) &&
/CONFIGURED_IMAGE_TAG/.test(waitStep.text) &&
/EXPECTED_IMAGE_ALIAS/.test(waitStep.text) &&
new RegExp(`EXPECTED_IMAGE_ALIAS="\\$\\{4:-${options.expectedAlias}\\}"`).test(waitStep.text) &&
/\[\s*"\$CONFIGURED_IMAGE_TAG"\s*!=\s*"\$EXPECTED_IMAGE_ALIAS"\s*\]/.test(waitStep.text)
) {
passes.push(
`${WORKFLOW_PATH}:${waitStep.lineNumber}: ${options.label} dependent health wait handles HTTP resources, Hocuspocus Docker health, revision-bound worker health, and non-HTTP tags explicitly`
);
} else {
failures.push(
`${WORKFLOW_PATH}:${waitStep.lineNumber}: ${options.label} dependent health wait must fail HTTP resources without health URLs and verify Hocuspocus/worker/non-HTTP revisions explicitly`
);
}
if (/COOLIFY_.*SSH_KEY/.test(waitStep.text) || /docker exec /.test(waitStep.text) || /docker ps --filter name=\$RESOURCE_UUID/.test(waitStep.text)) {
failures.push(
`${WORKFLOW_PATH}:${waitStep.lineNumber}: ${options.label} dependent health wait must not expose SSH secrets or use docker exec from CI logs`
);
} else {
passes.push(`${WORKFLOW_PATH}:${waitStep.lineNumber}: ${options.label} dependent health wait avoids SSH-backed worker probes`);
}
if (
/EXPECTED_HEALTH_COMMAND="node apps\/worker\/dist\/deploy-health\.js \$IMAGE_TAG \$EXPECTED_WORKER_GROUP"/.test(waitStep.text) &&
/\[ -z "\$EXPECTED_WORKER_GROUP" \]/.test(waitStep.text) &&
/CONFIGURED_HEALTH_COMMAND/.test(waitStep.text) &&
/CONFIGURED_HEALTH_TYPE/.test(waitStep.text) &&
/HEALTH_CHECK_ENABLED/.test(waitStep.text) &&
/CUSTOM_HEALTHCHECK_FOUND/.test(waitStep.text) &&
/\[ "\$HEALTH_CHECK_ENABLED" != "true" \]/.test(waitStep.text) &&
/\[ "\$CONFIGURED_HEALTH_TYPE" != "cmd" \]/.test(waitStep.text) &&
/\[ "\$CONFIGURED_HEALTH_COMMAND" != "\$EXPECTED_HEALTH_COMMAND" \]/.test(waitStep.text) &&
/\[ "\$CUSTOM_HEALTHCHECK_FOUND" != "false" \]/.test(waitStep.text)
) {
passes.push(`${WORKFLOW_PATH}:${waitStep.lineNumber}: ${options.label} dependent health wait enforces worker revision and group automatically`);
} else {
failures.push(
`${WORKFLOW_PATH}:${waitStep.lineNumber}: ${options.label} dependent health wait must bind the worker command healthcheck to IMAGE_TAG and EXPECTED_WORKER_GROUP`
);
}
if (
/EXPECTED_MCP_HEALTH_COMMAND="node apps\/mcp\/dist\/deploy-health\.js \$IMAGE_TAG"/.test(waitStep.text) &&
/MCP_HEALTH_CHECK_ENABLED/.test(waitStep.text) &&
/MCP_CUSTOM_HEALTHCHECK_FOUND/.test(waitStep.text) &&
/CONFIGURED_MCP_HEALTH_TYPE/.test(waitStep.text) &&
/CONFIGURED_MCP_HEALTH_COMMAND/.test(waitStep.text) &&
/\[ "\$MCP_HEALTH_CHECK_ENABLED" != "true" \]/.test(waitStep.text) &&
/\[ "\$CONFIGURED_MCP_HEALTH_TYPE" != "cmd" \]/.test(waitStep.text) &&
/\[ "\$CONFIGURED_MCP_HEALTH_COMMAND" != "\$EXPECTED_MCP_HEALTH_COMMAND" \]/.test(waitStep.text) &&
/\[ "\$MCP_CUSTOM_HEALTHCHECK_FOUND" != "false" \]/.test(waitStep.text)
) {
passes.push(`${WORKFLOW_PATH}:${waitStep.lineNumber}: ${options.label} dependent health wait enforces MCP revision-bound command health`);
} else {
failures.push(`${WORKFLOW_PATH}:${waitStep.lineNumber}: ${options.label} dependent health wait must bind MCP command health to IMAGE_TAG`);
}
if (/has no health URL; live revision probe skipped["']/.test(waitStep.text)) {
failures.push(`${WORKFLOW_PATH}:${waitStep.lineNumber}: ${options.label} dependent health wait must not pass-open missing health URLs for HTTP resources`);
} else {
passes.push(`${WORKFLOW_PATH}:${waitStep.lineNumber}: ${options.label} dependent health wait fails missing health URLs for HTTP resources`);
}
if (/echo\s+["'][^"']*\$RESOURCE_UUID/.test(waitStep.text) || /printf\s+["'][^"']*\$RESOURCE_UUID/.test(waitStep.text)) {
failures.push(`${WORKFLOW_PATH}:${waitStep.lineNumber}: ${options.label} dependent health wait must not print resource UUIDs`);
} else {
passes.push(`${WORKFLOW_PATH}:${waitStep.lineNumber}: ${options.label} dependent health wait redacts resource UUIDs`);
}
if (deployStep && waitStep.lineNumber > deployStep.lineNumber) {
passes.push(`${WORKFLOW_PATH}:${waitStep.lineNumber}: ${options.label} dependent health wait runs after dependent deploy trigger`);
} else {
failures.push(`${WORKFLOW_PATH}:${waitStep.lineNumber}: ${options.label} dependent health wait must run after dependent deploy trigger`);
}
return { passes, failures, observations: [] };
}
export function checkCoolifyStagingDependentHealthWaitContract(content, analysis) {
return checkCoolifyDependentHealthWaitContract(
content,
{
label: "Coolify staging",
deployStepName: "Trigger Coolify staging dependent deploys",
waitStepName: "Wait for Coolify staging dependent health",
resourceEnv: "STAGING_DEPENDENT_RESOURCE_UUIDS",
deploymentEnv: "STAGING_DEPENDENT_DEPLOYMENT_UUIDS",
triggerFailureEnv: "STAGING_DEPENDENT_TRIGGER_FAILED",
expectedAlias: "staging",
},
analysis
);
}
export function checkCoolifyProductionDependentHealthWaitContract(content, analysis) {
return checkCoolifyDependentHealthWaitContract(
content,
{
label: "Coolify production",
deployStepName: "Trigger Coolify production dependent deploys",
waitStepName: "Wait for Coolify production dependent health",
resourceEnv: "PRODUCTION_DEPENDENT_RESOURCE_UUIDS",
deploymentEnv: "PRODUCTION_DEPENDENT_DEPLOYMENT_UUIDS",
triggerFailureEnv: "PRODUCTION_DEPENDENT_TRIGGER_FAILED",
expectedAlias: "main",
},
analysis
);
}
export function checkCoolifyTriggerContract(content, analysis) {
const passes = [];
const failures = [];
const steps = getStepBlocks(content, analysis);
const workflowLines = getWorkflowLines(content, analysis);
const coolifySteps = steps.filter(
({ text }) => /\bTrigger Coolify\b/i.test(text) || /coolify-deploy\.mjs\s+trigger/.test(text) || /\bcurl\b[\s\S]*\bdeploy\b/i.test(text)
);
const literalCoolifyLine = workflowLines.find(({ line }) => LITERAL_COOLIFY_URL_PATTERN.test(line));
if (literalCoolifyLine) {
failures.push(
`${WORKFLOW_PATH}:${literalCoolifyLine.lineNumber}: literal Coolify/webhook URL is forbidden; use ${COOLIFY_API_URL_VAR} with ${COOLIFY_TOKEN_SECRET}`
);
} else {
passes.push(`${WORKFLOW_PATH}: no literal Coolify/webhook URLs are present`);
}
if (coolifySteps.length === 0) {
failures.push(`${WORKFLOW_PATH}: missing Coolify trigger command`);
return { passes, failures, observations: [] };
}
const deployTriggerSteps = coolifySteps.filter(({ text }) => /coolify-deploy\.mjs\s+trigger/.test(text) || /\/deploy\?uuid=/.test(text));
for (const step of deployTriggerSteps) {
const isControllerRecovery =
/name:\s*Restore previous Coolify staging portal-domain-controller release on rollout failure/i.test(step.text) &&
/^\s*if:\s*always\(\)\s*$/imu.test(step.text);
const softeningReason = getRequiredStepSofteningReason(step, { allowAlways: isControllerRecovery });
if (softeningReason) {
failures.push(`${WORKFLOW_PATH}:${step.lineNumber}: Coolify deploy trigger must not use ${softeningReason}`);
} else if (isControllerRecovery) {
passes.push(`${WORKFLOW_PATH}:${step.lineNumber}: controller recovery deploy trigger is guarded by the required always() cleanup path`);
} else {
passes.push(`${WORKFLOW_PATH}:${step.lineNumber}: Coolify deploy trigger is not softened`);
}
if (/(?:--request|-X)\s+POST\b/.test(step.text) || /coolify-deploy\.mjs\s+trigger/.test(step.text)) {
passes.push(`${WORKFLOW_PATH}:${step.lineNumber}: Coolify deploy trigger uses POST`);
} else {
failures.push(`${WORKFLOW_PATH}:${step.lineNumber}: Coolify deploy trigger must use POST`);
}
if (
/DEPLOYMENT_UUID="\$\(node scripts\/coolify-deploy\.mjs trigger/.test(step.text) ||
/_DEPLOYMENT_UUID="\$\(node scripts\/coolify-deploy\.mjs trigger/.test(step.text) ||
/\/deploy\?uuid=[^\n]+["']?\s*>\/dev\/null/.test(step.text)
) {
passes.push(`${WORKFLOW_PATH}:${step.lineNumber}: Coolify deploy trigger suppresses raw response output`);
} else {
failures.push(`${WORKFLOW_PATH}:${step.lineNumber}: Coolify deploy trigger must suppress raw response output`);
}
}
const stagingDependentTrigger = steps.find(({ text }) => /name:\s*Trigger Coolify staging dependent deploys/i.test(text));
if (
stagingDependentTrigger &&
/coolify-deploy\.mjs\s+configure-mcp-healthcheck\s+"\$STAGING_MCP_RESOURCE_UUID"\s+"\$IMAGE_TAG"/.test(stagingDependentTrigger.text)
) {
passes.push(`${WORKFLOW_PATH}:${stagingDependentTrigger.lineNumber}: staging MCP configures revision-bound command health before deploy`);
} else {
failures.push(`${WORKFLOW_PATH}: staging MCP must configure revision-bound command health before deploy`);
}
const productionDependentTrigger = steps.find(({ text }) => /name:\s*Trigger Coolify production dependent deploys/i.test(text));
if (
productionDependentTrigger &&
/coolify-deploy\.mjs\s+configure-mcp-healthcheck\s+"\$PRODUCTION_MCP_RESOURCE_UUID"\s+"\$IMAGE_TAG"/.test(productionDependentTrigger.text)
) {
passes.push(`${WORKFLOW_PATH}:${productionDependentTrigger.lineNumber}: production MCP configures revision-bound command health before deploy`);
} else {
failures.push(`${WORKFLOW_PATH}: production MCP must configure revision-bound command health before deploy`);
}
if (hasPushBranchTrigger(content, "main")) {
passes.push(`${WORKFLOW_PATH}: main-branch push trigger is defined for staging deployment`);
} else {
failures.push(`${WORKFLOW_PATH}: deployment workflow must run on push to main`);
}
if (hasPushBranchTrigger(content, "staging")) {
failures.push(`${WORKFLOW_PATH}: staging branch trigger is forbidden; staging must deploy from main artifacts`);
} else {
passes.push(`${WORKFLOW_PATH}: staging branch trigger is not used`);
}
const workflowDispatchBlock = content.match(/workflow_dispatch:\s*([\s\S]*?)\n(?:env:|jobs:)/i)?.[1] ?? "";
if (
/\bworkflow_dispatch:\s*(?:#.*\n\s*)*\n?env:/i.test(content) &&
!/\binputs:/i.test(workflowDispatchBlock) &&
/\bDEPLOY_ACTION=stage\b/.test(content) &&
/\bDEPLOY_ACTION=promote\b/.test(content) &&
/\[ "\$EVENT_NAME" = "workflow_dispatch" \]; then/.test(content)
) {
passes.push(`${WORKFLOW_PATH}: workflow_dispatch is an input-free production promotion while main pushes stage`);
} else {
failures.push(`${WORKFLOW_PATH}: workflow_dispatch must be an input-free production promotion while main pushes stage`);
}
if (
/\[ "\$EVENT_NAME" = "workflow_dispatch" \]; then/.test(content) &&
/Promote must be dispatched from refs\/heads\/main/.test(content) &&
/\[ "\$REF" != "refs\/heads\/main" \]/.test(content)
) {
passes.push(`${WORKFLOW_PATH}: input-free production promotion is restricted to refs/heads/main`);
} else {
failures.push(`${WORKFLOW_PATH}: input-free production promotion must be restricted to refs/heads/main`);
}
const usesApiDeployContract =
content.includes(COOLIFY_TOKEN_SECRET) &&
content.includes(COOLIFY_API_URL_VAR) &&
content.includes(COOLIFY_RESOURCE_UUIDS_VAR) &&
content.includes("${{ vars.COOLIFY_STAGING_API_URL }}") &&
content.includes("${{ secrets.COOLIFY_STAGING_TOKEN }}") &&
content.includes("${{ vars.COOLIFY_STAGING_RESOURCE_UUIDS }}") &&
/Authorization:\s*Bearer\s*\$(?:COOLIFY_TOKEN|COOLIFY_STAGING_TOKEN|SELECTED_COOLIFY_TOKEN)/.test(content) &&
/coolify-deploy\.mjs\s+trigger\s+"\$STAGING_WEB_RESOURCE_UUID"/.test(content) &&
/for RESOURCE_UUID in \$\(printf "%s" "\$STAGING_DEPENDENT_RESOURCE_UUIDS" \| tr ',' ' '\)/.test(content) &&
/coolify-deploy\.mjs\s+trigger\s+"\$PRODUCTION_WEB_RESOURCE_UUID"/.test(content) &&
/for RESOURCE_UUID in \$\(printf "%s" "\$PRODUCTION_DEPENDENT_RESOURCE_UUIDS" \| tr ',' ' '\)/.test(content) &&
!/deploy\?uuid=\$STAGING_DEPENDENT_RESOURCE_UUIDS/.test(content) &&
!/deploy\?uuid=\$PRODUCTION_DEPENDENT_RESOURCE_UUIDS/.test(content);
if (usesApiDeployContract) {
passes.push(`${WORKFLOW_PATH}: Coolify trigger uses production and staging Coolify configuration slots`);
} else {
failures.push(`${WORKFLOW_PATH}: Coolify trigger must use production and staging Coolify configuration slots`);
}
if (!/COOLIFY_STAGING_(?:API_URL|TOKEN).*:-\$COOLIFY_(?:API_URL|TOKEN)/.test(content)) {
passes.push(`${WORKFLOW_PATH}: staging Coolify configuration cannot fall back to production credentials`);
} else {
failures.push(`${WORKFLOW_PATH}: staging Coolify configuration must not fall back to production Coolify URL or token`);
}
const stagingCoolifySteps = steps.filter((step) => /staging/i.test(getStepName(step.text)) && /coolify/i.test(step.text));
const stagingCoolifyLeaks = stagingCoolifySteps.filter((step) =>
/\$\{?\{?\s*(?:vars\.COOLIFY_API_URL|secrets\.COOLIFY_TOKEN)\s*\}?\}?|\$COOLIFY_API_URL\b|\$COOLIFY_TOKEN\b/.test(step.text)
);
if (stagingCoolifyLeaks.length === 0) {
passes.push(`${WORKFLOW_PATH}: staging Coolify steps do not reference production Coolify variables`);
} else {
for (const step of stagingCoolifyLeaks) {
failures.push(`${WORKFLOW_PATH}:${step.lineNumber}: staging Coolify steps must not reference production Coolify URL or token variables`);
}
}
const productionCoolifySteps = steps.filter((step) => /production/i.test(getStepName(step.text)) && /coolify/i.test(step.text));
const productionCoolifyLeaks = productionCoolifySteps.filter((step) =>
/\$\{?\{?\s*(?:vars\.COOLIFY_STAGING_API_URL|secrets\.COOLIFY_STAGING_TOKEN)\s*\}?\}?|\$COOLIFY_STAGING_API_URL\b|\$COOLIFY_STAGING_TOKEN\b/.test(step.text)
);
if (productionCoolifyLeaks.length === 0) {
passes.push(`${WORKFLOW_PATH}: production Coolify steps do not reference staging Coolify variables`);
} else {
for (const step of productionCoolifyLeaks) {
failures.push(`${WORKFLOW_PATH}:${step.lineNumber}: production Coolify steps must not reference staging Coolify URL or token variables`);
}
}
const firstCoolifyLine = Math.min(...coolifySteps.map(({ lineNumber }) => lineNumber));
const lastImageBuildLine = Math.max(...REQUIRED_TARGETS.map((target) => getDockerTargetBuildLine(content, target, analysis)));
if (lastImageBuildLine > 0 && firstCoolifyLine > lastImageBuildLine) {
passes.push(`${WORKFLOW_PATH}: staging Coolify trigger runs after Docker image publication steps`);
} else {
failures.push(`${WORKFLOW_PATH}: staging Coolify trigger must run after all Docker image publication steps`);
}
if (/imagetools\s+create\s+--prefer-index=false\s+--tag\s+"\$IMAGE:main"\s+"\$IMAGE:\$IMAGE_TAG"/.test(content)) {
passes.push(`${WORKFLOW_PATH}: promote preserves the verified full-SHA manifest digest in production aliases without rebuilding`);
} else {
failures.push(`${WORKFLOW_PATH}: promote must preserve the verified full-SHA manifest digest in production aliases without rebuilding`);
}
return { passes, failures, observations: [] };
}
export function checkCoolifyDeployHelper(rootDir) {
const passes = [];
const failures = [];
const helperPath = path.join(rootDir, COOLIFY_DEPLOY_HELPER_PATH);
if (!fs.existsSync(helperPath)) {
return { passes, failures: [`${COOLIFY_DEPLOY_HELPER_PATH}: missing Coolify deploy helper`], observations: [] };
}
const source = fs.readFileSync(helperPath, "utf8");
const triggerSource = source.match(/async function trigger\(resourceUuid\)\s*\{[\s\S]*?\n\}/)?.[0] ?? "";
const deploymentUuidSource = source.match(/function deploymentUuid\(payload, resourceUuid = ["']["']\)\s*\{[\s\S]*?\n\}/)?.[0] ?? "";
const mcpIdentitySource = source.match(/function isExpectedMcpImage\(app\)\s*\{[\s\S]*?\n\}/)?.[0] ?? "";
const legacyKindSource = source.match(/function legacyRollbackResourceKind\(app\)\s*\{[\s\S]*?\n\}/)?.[0] ?? "";
if (/`\/deploy\?uuid=\$\{encodeURIComponent\(resourceUuid\)\}&force=true`/.test(triggerSource) && /method:\s*["']POST["']/.test(triggerSource)) {
passes.push(`${COOLIFY_DEPLOY_HELPER_PATH}: trigger uses the canonical force-deploy endpoint`);
} else {
failures.push(`${COOLIFY_DEPLOY_HELPER_PATH}: trigger must POST /deploy?uuid=<encoded>&force=true`);
}
if (/\/applications\/[^\n]*\/(?:start|restart)\b/.test(triggerSource)) {
failures.push(`${COOLIFY_DEPLOY_HELPER_PATH}: trigger must not use application start/restart as a deployment substitute`);
} else {
passes.push(`${COOLIFY_DEPLOY_HELPER_PATH}: trigger does not use application start/restart`);
}
if (
/Array\.isArray\(payload\.deployments\)/.test(source) &&
/\.find\(\(deployment\)\s*=>\s*deployment\?\.resource_uuid\s*===\s*resourceUuid\)/.test(source) &&
/queuedDeployment\?\.deployment_uuid/.test(source) &&
!/\.find\([^\n]+\)\s*\|\|\s*payload\.deployments\[0\]/.test(source) &&
!/\.find\([^\n]+\)\s*\|\|\s*deployments\[0\]/.test(source)
) {
passes.push(`${COOLIFY_DEPLOY_HELPER_PATH}: trigger matches deployment-array responses to the requested resource`);
} else {
failures.push(`${COOLIFY_DEPLOY_HELPER_PATH}: trigger must fail closed unless deployments contains the requested resource UUID`);
}
if (
/COOLIFY_CUID_PATTERN\s*=\s*\/\^\[a-z\]\[a-z0-9\]\{23\}\$\//.test(source) &&
/requireCoolifyCuid\(resourceUuid,/.test(triggerSource) &&
/COOLIFY_CUID_PATTERN\.test\(value\)\s*\?\s*value/.test(deploymentUuidSource) &&
/requireCoolifyCuid\(uuid,\s*["']Coolify deployment UUID["']\)/.test(source)
) {
passes.push(`${COOLIFY_DEPLOY_HELPER_PATH}: resource and deployment CUIDs are validated before requests, output, and waits`);
} else {
failures.push(`${COOLIFY_DEPLOY_HELPER_PATH}: must validate 24-character Coolify CUIDs before requests, output, and waits`);
}
if (/function envEntries/.test(source) && /Coolify environment response was malformed/.test(source)) {
passes.push(`${COOLIFY_DEPLOY_HELPER_PATH}: malformed Coolify environment payloads fail closed`);
} else {
failures.push(`${COOLIFY_DEPLOY_HELPER_PATH}: malformed Coolify environment payloads must fail closed`);
}
if (
/function runtimeEnvEntries/.test(source) &&
/normalizeCoolifyBoolean\(entry\?\.is_preview\)\s*===\s*["']true["']/.test(source) &&
/runtime environment response contained duplicate non-preview keys/.test(source) &&
/async function setEnvs[\s\S]*?const existing\s*=\s*runtimeEnvEntries\(existingPayload\)/.test(source) &&
/async function configureLegacyRollbackHealthcheck[\s\S]*?const envs\s*=\s*runtimeEnvEntries\(envPayload\)/.test(source)
) {
passes.push(`${COOLIFY_DEPLOY_HELPER_PATH}: runtime env reads ignore preview entries and reject duplicate non-preview keys before mutation`);
} else {
failures.push(
`${COOLIFY_DEPLOY_HELPER_PATH}: set-envs and legacy rollback must canonicalize non-preview runtime env entries and fail closed on duplicates`
);
}
if (
/async function validateRequiredImages/.test(source) &&
/allowed\.has\(app\?\.uuid\)/.test(source) &&
/docker_registry_image_name/.test(source) &&
/matches\.length\s*!==\s*1/.test(source) &&
/requiredApps\.map\(\(app\)\s*=>\s*readApplicationEnvs\(app\.uuid\)/.test(source) &&
/requiredEnvs\.some\(\(envs\)\s*=>\s*envs\.some\(\(entry\)\s*=>\s*envKey\(entry\)\s*===\s*["']APP_REVISION["']\)\)/.test(source) &&
/Coolify applications response was malformed/.test(source)
) {
passes.push(
`${COOLIFY_DEPLOY_HELPER_PATH}: core deploy inventory requires one exact allowlisted application per image and rejects revision overrides before mutation`
);
} else {
failures.push(
`${COOLIFY_DEPLOY_HELPER_PATH}: core deploy inventory must fail closed unless each exact required image has one allowlisted application without an APP_REVISION override`
);
}
if (
/async function assertNoAppRevisionOverride/.test(source) &&
/envs\.some\(\(entry\)\s*=>\s*envKey\(entry\)\s*===\s*["']APP_REVISION["']\)/.test(source) &&
/APP_REVISION override must be removed before deployment/.test(source)
) {
passes.push(`${COOLIFY_DEPLOY_HELPER_PATH}: mutable APP_REVISION overrides are rejected by key presence`);
} else {
failures.push(`${COOLIFY_DEPLOY_HELPER_PATH}: APP_REVISION overrides, including empty values, must fail closed`);
}
if (
/configureMcpHealthcheck/.test(source) &&
/docker_registry_image_tag:\s*expectedRevision/.test(source) &&
/health_check_type:\s*["']cmd["']/.test(source) &&
/health_check_command:\s*`node apps\/mcp\/dist\/deploy-health\.js \$\{expectedRevision\}`/.test(source) &&
/envKey\(entry\)\s*===\s*["']APP_REVISION["']/.test(source) &&
/isExpectedMcpImage\(current\)/.test(source) &&
/Boolean\(expected\)\s*&&\s*actual\s*===\s*expected/.test(mcpIdentitySource) &&
/custom_healthcheck_found/.test(source) &&
/image healthcheck must be absent/.test(source) &&
/mcpHealthConfigDrift\(updated, expectedRevision\)\.length\s*>\s*0/.test(source)
) {
passes.push(`${COOLIFY_DEPLOY_HELPER_PATH}: MCP revision-bound command health is atomically configured and verified`);
} else {
failures.push(`${COOLIFY_DEPLOY_HELPER_PATH}: must atomically configure and verify MCP revision-bound command health`);
}
if (
/configureLegacyRollbackHealthcheck/.test(source) &&
/legacyRollbackResourceKind/.test(source) &&
/docker_registry_image_name/.test(legacyKindSource) &&
/expectedWorkerImageName\(\)/.test(legacyKindSource) &&
/expectedMcpImageName\(\)/.test(legacyKindSource) &&
/image\s*===\s*expectedWorkerImage/.test(legacyKindSource) &&
/image\s*===\s*expectedMcpImage/.test(legacyKindSource) &&
!/app\?\.(?:name|description)\b/.test(legacyKindSource) &&
/envKey\(entry\)\s*===\s*["']APP_REVISION["']/.test(source) &&
/resourceKind\s*===\s*["']worker["']\s*&&\s*!expectedWorkerGroup/.test(source) &&
/health_check_enabled:\s*false/.test(source) &&
/health_check_command:\s*["']["']/.test(source) &&
/legacyRollbackHealthConfigDrift\(updated, expectedRevision\)\.length\s*>\s*0/.test(source)
) {
passes.push(`${COOLIFY_DEPLOY_HELPER_PATH}: legacy MCP/worker rollback atomically disables command health and verifies target identity`);
} else {
failures.push(
`${COOLIFY_DEPLOY_HELPER_PATH}: legacy rollback must reject revision overrides and non-runtime targets, require worker groups, atomically disable command health, and verify persistence`
);
}
if (/AbortSignal\.timeout\(apiTimeoutMs\)/.test(source) && /Coolify API request failed or timed out/.test(source)) {
passes.push(`${COOLIFY_DEPLOY_HELPER_PATH}: Coolify API calls have a bounded redacted timeout`);
} else {
failures.push(`${COOLIFY_DEPLOY_HELPER_PATH}: Coolify API calls must use a bounded redacted timeout`);
}
const pollDelayMatch = source.match(/COOLIFY_DEPLOYMENT_POLL_MS\s*\|\|\s*["'](\d+)["']/);
const pollAttemptsMatch = source.match(/COOLIFY_DEPLOYMENT_WAIT_ATTEMPTS\s*\|\|\s*["'](\d+)["']/);
const pollDelayMs = Number(pollDelayMatch?.[1] || 0);
const pollAttempts = Number(pollAttemptsMatch?.[1] || 0);
if (pollDelayMs === 2_000 && pollAttempts === 225 && pollDelayMs * pollAttempts === 450_000) {
passes.push(`${COOLIFY_DEPLOY_HELPER_PATH}: deployment waits poll every 2 seconds while preserving the 450-second failure budget`);
} else {
failures.push(`${COOLIFY_DEPLOY_HELPER_PATH}: deployment wait defaults must be 2000 ms x 225 attempts to preserve the 450-second failure budget`);
}
return { passes, failures, observations: [] };
}
export function checkRegistryLogin(content) {
const passes = [];
const failures = [];
if (/\bdocker\s+login\b/.test(content)) {
passes.push(`${WORKFLOW_PATH}: registry login command is present`);
} else {
failures.push(`${WORKFLOW_PATH}: missing registry login command`);
}
if (content.includes(USERNAME_SECRET)) {
passes.push(`${WORKFLOW_PATH}: registry login uses REGISTRY_USER secret`);
} else {
failures.push(`${WORKFLOW_PATH}: registry login must use ${USERNAME_SECRET}`);
}
if (content.includes(PASSWORD_SECRET)) {
passes.push(`${WORKFLOW_PATH}: registry login uses REGISTRY_PAT secret`);
} else {
failures.push(`${WORKFLOW_PATH}: registry login must use ${PASSWORD_SECRET}`);
}
return { passes, failures, observations: [] };
}
export function checkDockerTargets(content, analysis) {
const passes = [];
const failures = [];
for (const target of REQUIRED_TARGETS) {
if (hasDockerTargetBuild(content, target, analysis) || hasDockerTargetLoop(content, target)) {
passes.push(`${WORKFLOW_PATH}: builds Docker target "${target}"`);
} else {
failures.push(`${WORKFLOW_PATH}: missing Docker build for target "${target}"`);
}
if (hasDockerTargetPush(content, target, analysis) || (hasDockerTargetLoop(content, target) && /\bdocker\s+buildx\s+build\b[\s\S]*--push/.test(content))) {
passes.push(`${WORKFLOW_PATH}: pushes Docker target "${target}"`);
} else {
failures.push(`${WORKFLOW_PATH}: Docker target "${target}" must be pushed with docker buildx build --push`);
}
}
const buildStep = getStepBlocks(content, analysis).find(({ text }) => /\bBuild and push staging images\b/i.test(getStepName(text)))?.text ?? "";
if (/--build-arg\s+APP_REVISION="\$SHA"/.test(content)) {
passes.push(`${WORKFLOW_PATH}: Docker image publication passes APP_REVISION build arg`);
} else {
failures.push(`${WORKFLOW_PATH}: Docker image publication must pass --build-arg APP_REVISION="$SHA"`);
}
const queriesLegacyBuildCache =
/\bCACHE_REF=/.test(buildStep) ||
/build-cache:\$TARGET/.test(buildStep) ||
/--cache-from\s+"type=registry,ref=\$CACHE_REF"/.test(buildStep);
if (!queriesLegacyBuildCache) {
passes.push(`${WORKFLOW_PATH}: staging Docker builds do not query nonexistent legacy build-cache refs`);
} else {
failures.push(`${WORKFLOW_PATH}: staging Docker builds must not query legacy build-cache refs that the workflow never publishes`);
}
const usesPortableInlineCache =
/--cache-from\s+"type=registry,ref=\$IMAGE:staging"/.test(buildStep) && /--cache-to\s+"type=inline"/.test(buildStep);
if (usesPortableInlineCache) {
passes.push(`${WORKFLOW_PATH}: staging Docker builds import the previous staging image and embed portable inline cache in the immutable SHA image`);
} else {
failures.push(`${WORKFLOW_PATH}: staging Docker builds must import $IMAGE:staging and export type=inline cache with the successful SHA image push`);
}
if (!/--cache-to(?:=|\s+)["']?type=registry(?:[,"'\s]|$)/.test(buildStep)) {
passes.push(`${WORKFLOW_PATH}: staging Docker builds do not use a separate failure-prone registry cache export`);
} else {
failures.push(`${WORKFLOW_PATH}: staging Docker builds must not use a separate type=registry cache-to export`);
}
const usesParallelStagingBuilds =
/\bBUILD_PIDS=\(\)/.test(buildStep) &&
/\bBUILD_TARGETS=\(\)/.test(buildStep) &&
/\)\s*&/.test(buildStep) &&
/\bBUILD_PIDS\+=\("\$!"\)/.test(buildStep) &&
/for INDEX in "\$\{!BUILD_PIDS\[@\]\}"; do/.test(buildStep) &&
/if ! wait "\$\{BUILD_PIDS\[\$INDEX\]\}"; then/.test(buildStep) &&
/\bBUILD_FAILED=1\b/.test(buildStep) &&
/exit "\$BUILD_FAILED"/.test(buildStep);
if (usesParallelStagingBuilds) {
passes.push(`${WORKFLOW_PATH}: staging Docker image builds run in parallel and collect per-target failures`);
} else {
failures.push(`${WORKFLOW_PATH}: staging Docker image builds must run web/api/worker in parallel and collect per-target failures before tagging`);
}
const viteBuildArgPattern = /--build-arg\s+VITE_[A-Z0-9_]+=/;
if (/\bWEB_BUILD_ARGS=\(\)/.test(buildStep) && /if\s+\[\s+"\$TARGET"\s+=\s+"web"\s+\]/.test(buildStep) && viteBuildArgPattern.test(buildStep)) {
passes.push(`${WORKFLOW_PATH}: Vite build args are scoped to the web Docker target`);
} else {
failures.push(`${WORKFLOW_PATH}: Vite build args must be scoped to the web Docker target so api/worker caches stay reusable`);
}
const hocuspocusEnvPattern = new RegExp(`^\\s*VITE_HOCUSPOCUS_URL:\\s*${escapeRegex(HOCUSPOCUS_BROWSER_URL_VAR)}\\s*$`, "m");
const hocuspocusBuildArgPattern = /--build-arg\s+VITE_HOCUSPOCUS_URL="\$VITE_HOCUSPOCUS_URL"/;
if (hocuspocusEnvPattern.test(buildStep) && hocuspocusBuildArgPattern.test(buildStep)) {
passes.push(`${WORKFLOW_PATH}: public Hocuspocus WebSocket URL is passed from a Gitea variable to the web-only Docker build`);
} else {
failures.push(`${WORKFLOW_PATH}: web image build must map vars.VITE_HOCUSPOCUS_URL and pass --build-arg VITE_HOCUSPOCUS_URL to the web target`);
}
if (
/if\s+\[\s+"\$TARGET"\s+=\s+"hocuspocus"\s+\]; then/.test(buildStep) &&
/DOCKERFILE="apps\/hocuspocus\/Dockerfile"/.test(buildStep) &&
/--file\s+"\$DOCKERFILE"/.test(buildStep)
) {
passes.push(`${WORKFLOW_PATH}: Hocuspocus uses its dedicated Dockerfile in the shared immutable-image build loop`);
} else {
failures.push(`${WORKFLOW_PATH}: Hocuspocus must build from apps/hocuspocus/Dockerfile in the shared immutable-image build loop`);
}
return { passes, failures, observations: [] };
}
export function checkIsolatedDeployBuildKit(rootDir, content, analysis) {
const passes = [];
const failures = [];
const deploySection = getJobSection(content, DEPLOY_JOB, analysis);
const deployAnalysis = deploySection ? createWorkflowAnalysis(deploySection) : null;
const steps = deploySection && deployAnalysis ? getStepBlocks(deploySection, deployAnalysis) : [];
const quiesceStep = steps.find(({ text }) => /\bname:\s*Quiesce orphaned deploy BuildKit\b/i.test(text));
const setupStep = steps.find(({ text }) => /\bname:\s*Start isolated deploy BuildKit\b/i.test(text));
const provenanceStep = steps.find(({ text }) => /\bname:\s*Verify staged promotion provenance\b/i.test(text));
const buildStep = steps.find(({ text }) => /\bname:\s*Build and push staging images\b/i.test(text));
const cleanupStep = steps.find(({ text }) => /\bname:\s*Stop isolated deploy BuildKit\b/i.test(text));
const quiesceIndex = steps.indexOf(quiesceStep);
const setupIndex = steps.indexOf(setupStep);
const provenanceIndex = steps.indexOf(provenanceStep);
const buildIndex = steps.indexOf(buildStep);
const cleanupIndex = steps.indexOf(cleanupStep);
const hasPinnedImage = content.includes(`DEPLOY_BUILDKIT_IMAGE: ${DEPLOY_BUILDKIT_IMAGE}`);
const hasPersistentName = content.includes(`DEPLOY_BUILDER_NAME: ${DEPLOY_BUILDER_NAME}`);
if (hasPinnedImage && hasPersistentName) {
passes.push(`${WORKFLOW_PATH}: deploy BuildKit image is digest-pinned and the bounded builder cache is persistent`);
} else {
failures.push(`${WORKFLOW_PATH}: deploy must pin ${DEPLOY_BUILDKIT_IMAGE} and use persistent builder ${DEPLOY_BUILDER_NAME}`);
}
const setupIsolatedBuilder =
setupStep &&
/if:\s*env\.DEPLOY_ACTION\s*==\s*['"]stage['"]/.test(setupStep.text) &&
/docker\s+buildx\s+create/.test(setupStep.text) &&
/--name\s+"\$DEPLOY_BUILDER_NAME"/.test(setupStep.text) &&
/--driver\s+docker-container/.test(setupStep.text) &&
/--driver-opt\s+"image=\$DEPLOY_BUILDKIT_IMAGE"/.test(setupStep.text) &&
/--driver-opt\s+"restart-policy=no"/.test(setupStep.text) &&
new RegExp(`--buildkitd-config\\s+${escapeRegex(DEPLOY_BUILDKIT_CONFIG_PATH)}`).test(setupStep.text) &&
/--bootstrap/.test(setupStep.text) &&
/docker\s+inspect\s+--format\s+'\{\{\.Config\.Image\}\}'\s+"\$BUILDER_CONTAINER"/.test(setupStep.text) &&
/\[\s*"\$ACTUAL_BUILDKIT_IMAGE"\s*!=\s*"\$DEPLOY_BUILDKIT_IMAGE"\s*\]/.test(setupStep.text) &&
/HostConfig\.RestartPolicy\.Name/.test(setupStep.text) &&
/docker\s+buildx\s+inspect\s+--bootstrap\s+"\$DEPLOY_BUILDER_NAME"/.test(setupStep.text) &&
new RegExp(
`docker\\s+exec\\s+"\\$BUILDER_CONTAINER"\\s+cat\\s+\/etc\/buildkit\/buildkitd\\.toml[\\s\\S]*?node\\s+${escapeRegex(DEPLOY_BUILDKIT_CONFIG_VERIFIER_PATH)}\\s+${escapeRegex(DEPLOY_BUILDKIT_CONFIG_PATH)}`
).test(setupStep.text) &&
setupStep.text.indexOf('if docker inspect "$BUILDER_CONTAINER"') >= 0 &&
setupStep.text.indexOf('if docker inspect "$BUILDER_CONTAINER"') < setupStep.text.indexOf('if docker buildx inspect "$DEPLOY_BUILDER_NAME"');
if (setupIsolatedBuilder) {
passes.push(`${WORKFLOW_PATH}: staging reuses or creates the pinned docker-container BuildKit and rejects image drift`);
} else {
failures.push(`${WORKFLOW_PATH}: staging must reuse or create the isolated pinned BuildKit with image-drift validation and ${DEPLOY_BUILDKIT_CONFIG_PATH}`);
}
const setupText = setupStep?.text || "";
const requiredHostIndex = setupText.indexOf('if [ -z "${HIVEGRID_GITEA_HOST_IP:-}" ]; then');
const requiredHostExitIndex = setupText.indexOf("exit 1", requiredHostIndex);
const requiredHostEndIndex = setupText.indexOf("\n fi", requiredHostIndex);
const validatedHostIndex = setupText.indexOf(
'if ! HIVEGRID_GITEA_HOST_IP="$HIVEGRID_GITEA_HOST_IP" node -e \'process.exit(require("node:net").isIP(process.env.HIVEGRID_GITEA_HOST_IP || "") ? 0 : 1)\'; then'
);
const validatedHostExitIndex = setupText.indexOf("exit 1", validatedHostIndex);
const validatedHostEndIndex = setupText.indexOf("\n fi", validatedHostIndex);
const builderExecIndex = setupText.indexOf("docker exec \\\n --env HIVEGRID_GITEA_HOST_IP=");
const staleRemovalIndex = setupText.indexOf('awk -v host="git.tool-ctrl.de" \'', builderExecIndex);
const staleRemovalEndIndex = setupText.indexOf('\' /etc/hosts > "$HOSTS_TMP"', staleRemovalIndex);
const staleRemovalText =
staleRemovalIndex >= 0 && staleRemovalEndIndex > staleRemovalIndex
? setupText.slice(staleRemovalIndex, staleRemovalEndIndex)
: "";
const replaceHostsIndex = setupText.indexOf('cat "$HOSTS_TMP" > /etc/hosts', staleRemovalEndIndex);
const appendHostIndex = setupText.indexOf(
'printf \'%s git.tool-ctrl.de\\n\' "$HIVEGRID_GITEA_HOST_IP" >> /etc/hosts',
replaceHostsIndex
);
const hostCountIndex = setupText.indexOf('BUILDKIT_HOST_COUNT="$(awk ', appendHostIndex);
const exactHostCountIndex = setupText.indexOf('BUILDKIT_EXACT_HOST_COUNT="$(awk ', hostCountIndex);
const hostCountEndIndex = setupText.indexOf("\n", hostCountIndex);
const exactHostCountEndIndex = setupText.indexOf("\n", exactHostCountIndex);
const hostCountText =
hostCountIndex >= 0 && hostCountEndIndex > hostCountIndex ? setupText.slice(hostCountIndex, hostCountEndIndex) : "";
const exactHostCountText =
exactHostCountIndex >= 0 && exactHostCountEndIndex > exactHostCountIndex
? setupText.slice(exactHostCountIndex, exactHostCountEndIndex)
: "";
const exactCountAssertionIndex = setupText.indexOf(
'if [ "$BUILDKIT_HOST_COUNT" -ne 1 ] || [ "$BUILDKIT_EXACT_HOST_COUNT" -ne 1 ]; then',
exactHostCountEndIndex
);
const exactCountExitIndex = setupText.indexOf("exit 1", exactCountAssertionIndex);
const exactCountAssertionEndIndex = setupText.indexOf("fi", exactCountAssertionIndex);
const hasBuilderExecContract =
/docker\s+exec[\s\S]*?--env\s+HIVEGRID_GITEA_HOST_IP="\$HIVEGRID_GITEA_HOST_IP"[\s\S]*?--interactive[\s\S]*?"\$BUILDER_CONTAINER"[\s\S]*?sh\s+-eu\s+<<'HIVEGRID_BUILDKIT_HOST_OVERRIDE'/.test(
setupText
);
const removesEveryStaleHostToken =
/^\s*\{\s*rewritten\s*=\s*""\s*for\s*\(\s*field\s*=\s*1;\s*field\s*<=\s*NF;\s*field\s*\+=\s*1\s*\)/m.test(
staleRemovalText
) &&
/if\s*\(\s*field\s*>\s*1\s*&&\s*\$field\s*==\s*host\s*\)\s*\{\s*continue/.test(staleRemovalText) &&
/rewritten\s*=\s*rewritten\s*==\s*""\s*\?\s*\$field\s*:\s*rewritten\s*"\s*"\s*\$field/.test(staleRemovalText) &&
/if\s*\(\s*rewritten\s*!=\s*""\s*\)\s*\{\s*print\s+rewritten/.test(staleRemovalText);
const countsEveryHostToken =
hostCountText.trim() ===
'BUILDKIT_HOST_COUNT="$(awk -v host="git.tool-ctrl.de" \'{ for (field = 2; field <= NF; field += 1) if ($field == host) count += 1 } END { print count + 0 }\' /etc/hosts)"';
const countsOnlyTheValidatedHostToken =
exactHostCountText.trim() ===
'BUILDKIT_EXACT_HOST_COUNT="$(awk -v host="git.tool-ctrl.de" -v ip="$HIVEGRID_GITEA_HOST_IP" \'$1 == ip { for (field = 2; field <= NF; field += 1) if ($field == host) count += 1 } END { print count + 0 }\' /etc/hosts)"';
const hostOverrideOrderIsSafe =
requiredHostIndex >= 0 &&
requiredHostIndex < requiredHostExitIndex &&
requiredHostExitIndex < requiredHostEndIndex &&
requiredHostEndIndex < validatedHostIndex &&
validatedHostIndex < validatedHostExitIndex &&
validatedHostExitIndex < validatedHostEndIndex &&
validatedHostEndIndex < builderExecIndex &&
builderExecIndex < staleRemovalIndex &&
staleRemovalIndex < staleRemovalEndIndex &&
staleRemovalEndIndex < replaceHostsIndex &&
replaceHostsIndex < appendHostIndex &&
appendHostIndex < hostCountIndex &&
hostCountIndex < exactHostCountIndex &&
exactHostCountIndex < exactCountAssertionIndex &&
exactCountAssertionIndex < exactCountExitIndex &&
exactCountExitIndex < exactCountAssertionEndIndex;
const configuresBuildKitRegistryHost =
hasBuilderExecContract &&
removesEveryStaleHostToken &&
countsEveryHostToken &&
countsOnlyTheValidatedHostToken &&
hostOverrideOrderIsSafe;
if (configuresBuildKitRegistryHost) {
passes.push(`${WORKFLOW_PATH}: isolated BuildKit replaces and verifies its validated registry host mapping`);
} else {
failures.push(`${WORKFLOW_PATH}: staging must require, validate, replace, and verify exactly one git.tool-ctrl.de mapping inside isolated BuildKit`);
}
const quiescesHostContainer =
quiesceStep &&
/docker\s+inspect\s+"\$BUILDER_CONTAINER"/.test(quiesceStep.text) &&
/docker\s+stop\s+"\$BUILDER_CONTAINER"/.test(quiesceStep.text) &&
!/if:\s*env\.DEPLOY_ACTION/.test(quiesceStep.text);
if (quiescesHostContainer && quiesceIndex >= 0 && provenanceIndex > quiesceIndex && provenanceIndex < setupIndex && setupIndex < buildIndex && cleanupIndex === buildIndex + 1) {
passes.push(`${WORKFLOW_PATH}: isolated BuildKit starts only for the staging build window and stops afterward`);
} else {
failures.push(`${WORKFLOW_PATH}: every deploy must quiesce the host builder before gates and order setup/build/stop around the staging build window`);
}
if (buildStep && /docker\s+buildx\s+build\s+--builder\s+"\$DEPLOY_BUILDER_NAME"/.test(buildStep.text)) {
passes.push(`${WORKFLOW_PATH}: every staging image build explicitly selects the isolated deploy builder`);
} else {
failures.push(`${WORKFLOW_PATH}: staging image builds must pass --builder "$DEPLOY_BUILDER_NAME"`);
}
if (
cleanupStep &&
/if:\s*always\(\)/.test(cleanupStep.text) &&
/\[\s*"\$\{DEPLOY_ACTION:-\}"\s*!=\s*"stage"\s*\]/.test(cleanupStep.text) &&
/docker\s+stop\s+"\$BUILDER_CONTAINER"/.test(cleanupStep.text) &&
!/docker\s+buildx\s+rm\b/.test(cleanupStep.text)
) {
passes.push(`${WORKFLOW_PATH}: normal and failed staging builds stop BuildKit without deleting its bounded cache`);
} else {
failures.push(`${WORKFLOW_PATH}: isolated deploy BuildKit must be stopped by an always() stage cleanup without deleting its cache`);
}
const configPath = path.join(rootDir, DEPLOY_BUILDKIT_CONFIG_PATH);
const configVerifierPath = path.join(rootDir, DEPLOY_BUILDKIT_CONFIG_VERIFIER_PATH);
if (fs.existsSync(configVerifierPath)) {
passes.push(`${DEPLOY_BUILDKIT_CONFIG_VERIFIER_PATH}: running builder config verifier exists`);
} else {
failures.push(`${DEPLOY_BUILDKIT_CONFIG_VERIFIER_PATH}: missing running builder config verifier`);
}
if (!fs.existsSync(configPath)) {
failures.push(`${DEPLOY_BUILDKIT_CONFIG_PATH}: missing bounded deploy BuildKit configuration`);
} else {
const config = fs.readFileSync(configPath, "utf8");
const sections = new Map();
let currentSection = "";
for (const rawLine of config.split(/\r?\n/)) {
const line = rawLine.replace(/\s+#.*$/, "").trim();
if (!line) continue;
const sectionMatch = line.match(/^\[([^\]]+)\]$/);
if (sectionMatch) {
currentSection = sectionMatch[1];
if (!sections.has(currentSection)) sections.set(currentSection, new Map());
continue;
}
const assignment = line.match(/^([A-Za-z0-9_-]+)\s*=\s*(.+)$/);
if (assignment && currentSection) sections.get(currentSection)?.set(assignment[1], assignment[2].trim());
}
const history = sections.get("history") ?? new Map();
const oci = sections.get("worker.oci") ?? new Map();
const bounded =
history.get("maxAge") === "86400" &&
history.get("maxEntries") === "20" &&
oci.get("gc") === "true" &&
oci.get("reservedSpace") === '"2GB"' &&
oci.get("maxUsedSpace") === '"30GB"' &&
oci.get("minFreeSpace") === '"100GB"' &&
oci.get("max-parallelism") === "12";
if (bounded) passes.push(`${DEPLOY_BUILDKIT_CONFIG_PATH}: history, cache growth, and BuildKit parallelism are bounded for the shared homeserver`);
else failures.push(`${DEPLOY_BUILDKIT_CONFIG_PATH}: must enforce history 86400s/20 entries and OCI GC at 2GB reserved/30GB max/100GB free with max-parallelism 12`);
}
return { passes, failures, observations: [] };
}
export function checkImageVariables(content, analysis) {
const passes = [];
const failures = [];
if (/\bCONTAINER_IMAGE_PREFIX\b|\bCONTAINER_REGISTRY_NAMESPACE\b|\bCONTAINER_REGISTRY_USERNAME\b|\bCONTAINER_REGISTRY_PASSWORD\b/.test(content)) {
failures.push(`${WORKFLOW_PATH}: legacy generic registry variables are forbidden; use Gitea registry host with REGISTRY_USER and REGISTRY_PAT`);
}
const giteaRegistryHostPattern = new RegExp(`\\bREGISTRY:\\s*${escapeRegex(GIT_HOST)}\\b`);
const giteaRegistryLoginPattern = new RegExp(`\\bdocker\\s+login\\s+${escapeRegex(GIT_HOST)}\\b`);
const usesGiteaRegistry = giteaRegistryHostPattern.test(content) || giteaRegistryLoginPattern.test(content);
const usesNamespaceVariable = /\bIMAGE_NAMESPACE\b/.test(content);
const usesImagePrefix = /\bIMAGE_PREFIX\b/.test(content);
if (usesGiteaRegistry && usesNamespaceVariable && usesImagePrefix) {
passes.push(`${WORKFLOW_PATH}: image references use the Gitea Package Registry namespace and HiveGrid target path`);
} else {
failures.push(`${WORKFLOW_PATH}: image references must use ${GIT_HOST} with IMAGE_NAMESPACE and IMAGE_PREFIX`);
}
if (/\$IMAGE_PREFIX\/\$TARGET/.test(content)) {
passes.push(`${WORKFLOW_PATH}: image paths match existing Coolify hivegrid/{web|api|worker|mcp|hocuspocus} resources`);
} else {
failures.push(`${WORKFLOW_PATH}: image paths must match existing Coolify hivegrid/{web|api|worker|mcp|hocuspocus} resources`);
}
const literalRegistryLine = getWorkflowLines(content, analysis).find(
({ line }) => !line.includes(PREFLIGHT_DATABASE_URL) && LITERAL_PRODUCTION_REGISTRY_PATTERN.test(line)
);
if (literalRegistryLine) {
failures.push(`${WORKFLOW_PATH}:${literalRegistryLine.lineNumber}: literal production registry/image URL is forbidden`);
} else {
passes.push(`${WORKFLOW_PATH}: no literal production registry/image URLs are present`);
}
return { passes, failures, observations: [] };
}
export function checkTagStrategy(content, analysis) {
const passes = [];
const failures = [];
if (/\bSHA:\s*\$\{\{\s*(?:gitea|github)\.sha\s*\}\}/i.test(content) && content.includes('IMAGE_TAG="$SHA"') && content.includes("^[0-9a-f]{40}$")) {
passes.push(`${WORKFLOW_PATH}: full SHA image tag generation is defined`);
} else {
failures.push(`${WORKFLOW_PATH}: missing full SHA image tag generation`);
}
if (
/git\s+checkout\s+--detach\s+"\$IMAGE_TAG"/.test(content) &&
/CHECKOUT_SHA="\$\(git rev-parse HEAD\)"/.test(content) &&
/\[\s*"\$CHECKOUT_SHA"\s*!=\s*"\$IMAGE_TAG"\s*\]/.test(content) &&
/\[\s*"\$DEPLOY_ACTION"\s*=\s*"stage"\s*\]\s*&&\s*\[\s*"\$IMAGE_TAG"\s*!=\s*"\$SHA"\s*\]/.test(content)
) {
passes.push(`${WORKFLOW_PATH}: checked-out source revision is verified against the exact staged or promoted image SHA before publication`);
} else {
failures.push(
`${WORKFLOW_PATH}: deploy workflow must check out IMAGE_TAG and verify git rev-parse HEAD matches the exact staged or promoted SHA before publication`
);
}
const stagingResolverIndex = content.indexOf("name: Resolve live staging release");
const checkoutIndex = content.indexOf("name: Verify checkout revision");
const usesLiveStagingSha =
stagingResolverIndex >= 0 &&
checkoutIndex > stagingResolverIndex &&
/https:\/\/staging-app\.hivegrid\.de\/healthz/.test(content) &&
/payload\.revision\s*\|\|\s*""/.test(content) &&
/STAGING_SERVICE/.test(content) &&
/STAGING_SURFACE/.test(content) &&
/"\$STAGING_SERVICE"\s*!=\s*"hivegrid-web"/.test(content) &&
/"\$STAGING_SURFACE"\s*!=\s*"core"/.test(content) &&
/Promote must be dispatched from refs\/heads\/main/.test(content) &&
/if ! printf '%s' "\$STAGED_SHA" \| grep -Eq '\^\[0-9a-f\]\{40\}\$'/.test(content) &&
/git\s+cat-file\s+-e\s+"\$STAGED_SHA\^\{commit\}"/.test(content) &&
/git\s+merge-base\s+--is-ancestor\s+"\$STAGED_SHA"\s+origin\/main/.test(content) &&
/echo "IMAGE_TAG=\$STAGED_SHA" >> "\$GITHUB_ENV"/.test(content) &&
/operator's audited confirmation that backup\/restore-point readiness/.test(content) &&
!/(?:gitea|github)\.event\.inputs\./.test(content);
if (usesLiveStagingSha) {
passes.push(`${WORKFLOW_PATH}: one-click promote resolves the full live staging SHA with audited backup-readiness intent`);
} else {
failures.push(`${WORKFLOW_PATH}: one-click promote must resolve and validate the full live staging SHA before exact-revision checkout`);
}
const verifiesPromotedImageProvenance =
/fetch-depth:\s*0/.test(content) &&
/\bgit\s+cat-file\s+-e\s+"\$IMAGE_TAG\^\{commit\}"/.test(content) &&
/\bgit\s+merge-base\s+--is-ancestor\s+"\$IMAGE_TAG"\s+origin\/main\b/.test(content) &&
/for TARGET in web api worker mcp hocuspocus portal-domain-controller; do[\s\S]*docker\s+buildx\s+imagetools\s+inspect\s+"\$IMAGE:\$IMAGE_TAG"\s+--format[\s\S]*docker\s+buildx\s+imagetools\s+inspect\s+"\$IMAGE:staging"\s+--format[\s\S]*SHA_DIGEST[\s\S]*STAGING_DIGEST[\s\S]*done/.test(
content
);
if (verifiesPromotedImageProvenance) {
passes.push(`${WORKFLOW_PATH}: promote verifies staged commit ancestry and staging image digests before retagging`);
} else {
failures.push(`${WORKFLOW_PATH}: promote must verify staged commit ancestry and staging image digests before retagging`);
}
const pushesStagingAliasDuringBuild = getRunBlocks(content, analysis).some(({ command }) =>
command.split("\n").some((line) => /\bdocker\s+buildx\s+build\b/.test(line) && /--tag\s+"\$IMAGE:staging"/.test(line))
);
const createsStagingAliasAfterBuild =
/\bdocker\s+buildx\s+imagetools\s+create\s+--prefer-index=false\s+--tag\s+"\$IMAGE:staging"\s+"\$IMAGE:\$IMAGE_TAG"/.test(content);
if (pushesStagingAliasDuringBuild) {
failures.push(`${WORKFLOW_PATH}: staging aliases must be retagged only after all staging target builds succeed`);
} else {
passes.push(`${WORKFLOW_PATH}: staging aliases are not pushed from individual target builds`);
}
if (/--tag\s+"\$IMAGE:\$IMAGE_TAG"/.test(content) && createsStagingAliasAfterBuild && /--tag\s+"\$IMAGE:main"/.test(content)) {
passes.push(`${WORKFLOW_PATH}: full SHA, staging, and promoted main tag generation is present`);
} else {
failures.push(`${WORKFLOW_PATH}: missing full SHA, staging, and promoted main tag generation`);
}
if (/:latest\b/.test(content)) {
failures.push(`${WORKFLOW_PATH}: mutable latest tag is forbidden; use full SHA plus main tags`);
} else {
passes.push(`${WORKFLOW_PATH}: mutable latest tag is not used`);
}
return { passes, failures, observations: [] };
}
export function checkSentryDeployMarkerContract(content, analysis) {
const passes = [];
const failures = [];
const steps = getStepBlocks(content, analysis);
const sentryStep = steps.find(({ text }) => /\bMark Sentry deploy\b/i.test(text));
if (!sentryStep) {
failures.push(`${WORKFLOW_PATH}: missing Sentry deploy marker step`);
return { passes, failures, observations: [] };
}
if (
/\bDEPLOY_ACTION"\s*=\s*"promote"/.test(sentryStep.text) &&
/\bSENTRY_ENVIRONMENT=production\b/.test(sentryStep.text) &&
/RELEASE_SHA="\$\{IMAGE_TAG\}"/.test(sentryStep.text)
) {
passes.push(`${WORKFLOW_PATH}:${sentryStep.lineNumber}: production Sentry deploy marker uses promoted IMAGE_TAG`);
} else {
failures.push(`${WORKFLOW_PATH}:${sentryStep.lineNumber}: production Sentry deploy marker must release IMAGE_TAG for promote`);
}
if (
/\bSENTRY_ENVIRONMENT=staging\b/.test(sentryStep.text) &&
/RELEASE_SHA="\$\{SHA\}"/.test(sentryStep.text) &&
/for PROJECT in api worker web\b/.test(sentryStep.text)
) {
passes.push(`${WORKFLOW_PATH}:${sentryStep.lineNumber}: Sentry deploy marker covers staging SHA and all runtime projects`);
} else {
failures.push(`${WORKFLOW_PATH}:${sentryStep.lineNumber}: Sentry deploy marker must cover staging SHA and api, worker, and web projects`);
}
return { passes, failures, observations: [] };
}
export function checkSecretRedaction(content, analysis) {
const passes = [];
const failures = [];
const workflowLines = getWorkflowLines(content, analysis);
const secretLine = workflowLines.find(({ line }) => {
if (line.includes("${{ secrets.")) {
return false;
}
return SECRET_SHAPED_LITERAL_PATTERNS.some((pattern) => pattern.test(line));
});
if (secretLine) {
failures.push(`${WORKFLOW_PATH}:${secretLine.lineNumber}: literal secret-shaped value is forbidden`);
} else {
passes.push(`${WORKFLOW_PATH}: no literal secret-shaped values are present`);
}
const unsafeSecretEcho = workflowLines.find(({ line }) => {
if (!/\$\{\{\s*secrets\./.test(line)) {
return false;
}
if (/\bdocker\s+login\b/.test(line) && /--password-stdin\b/.test(line)) {
return false;
}
return /\b(?:echo|printf|env|printenv|set\s+-x)\b/.test(line);
});
if (unsafeSecretEcho) {
failures.push(`${WORKFLOW_PATH}:${unsafeSecretEcho.lineNumber}: workflow must not print secret contexts or token environment`);
} else {
passes.push(`${WORKFLOW_PATH}: workflow does not print secret contexts`);
}
const secretEnvNames = new Set();
for (const { line } of workflowLines) {
const match = line.match(/^\s*([A-Z0-9_]+):\s*\$\{\{\s*secrets\.[A-Z0-9_]+\s*\}\}\s*$/);
if (match) {
secretEnvNames.add(match[1]);
}
}
const unsafeSecretEnvLine = workflowLines.find(({ line }) => {
if (/\bset\s+-x\b/.test(line)) {
return true;
}
for (const name of secretEnvNames) {
const envRef = new RegExp(`\\$\\{?${name}\\}?`);
if (!envRef.test(line)) continue;
if (/Authorization:\s*Bearer\s*\$/.test(line) || /\bdocker\s+login\b/.test(line) || /--password-stdin\b/.test(line)) {
return false;
}
if (/\b(?:echo|printf|env|printenv)\b/.test(line) || /[?&](?:token|key|secret)=\$/.test(line)) {
return true;
}
}
return false;
});
if (unsafeSecretEnvLine) {
failures.push(`${WORKFLOW_PATH}:${unsafeSecretEnvLine.lineNumber}: workflow must not print or query-string secret-backed environment variables`);
} else {
passes.push(`${WORKFLOW_PATH}: workflow does not print secret-backed environment variables`);
}
return { passes, failures, observations: [] };
}
export function checkGiteaVariableNames(content) {
const passes = [];
const failures = [];
if (FORBIDDEN_GITEA_HOST_OVERRIDE_PATTERN.test(content)) {
failures.push(`${WORKFLOW_PATH}: GITEA_HOST_IP is forbidden because Gitea reserves the GITEA_ variable prefix; use HIVEGRID_GITEA_HOST_IP`);
} else {
passes.push(`${WORKFLOW_PATH}: internal host override avoids the reserved GITEA_ variable prefix`);
}
const matches = (text, pattern) => [...text.matchAll(new RegExp(pattern.source, pattern.flags))];
const hostWrites = matches(content, HOST_OVERRIDE_WRITE_PATTERN);
const validations = matches(content, HOST_OVERRIDE_VALIDATION_PATTERN);
const failClosedHostValidationPattern =
/^if\s+!\s+HIVEGRID_GITEA_HOST_IP="\$HIVEGRID_GITEA_HOST_IP"\s+node\s+-e\s+'process\.exit\(require\("node:net"\)\.isIP\(process\.env\.HIVEGRID_GITEA_HOST_IP \|\| ""\) \? 0 : 1\)';\s*then[\s\S]*?\bexit\s+1\b[\s\S]*?\bfi\b/;
let runBlockHostWrites = 0;
let everyRunBlockWriteIsPrevalidated = true;
for (const block of getRunBlocks(content)) {
const blockWrites = matches(block.command, HOST_OVERRIDE_WRITE_PATTERN);
if (blockWrites.length === 0) continue;
const blockValidations = matches(block.command, HOST_OVERRIDE_VALIDATION_PATTERN);
runBlockHostWrites += blockWrites.length;
if (
blockValidations.length < blockWrites.length ||
blockWrites.some((write, index) => {
const validation = blockValidations[index];
if (validation?.index === undefined || validation.index > write.index) return true;
const guardStart = block.command.lastIndexOf("if !", validation.index);
if (guardStart < 0) return true;
return !failClosedHostValidationPattern.test(block.command.slice(guardStart, write.index));
})
) {
everyRunBlockWriteIsPrevalidated = false;
}
}
if (
hostWrites.length > validations.length ||
runBlockHostWrites !== hostWrites.length ||
!everyRunBlockWriteIsPrevalidated
) {
failures.push(`${WORKFLOW_PATH}: HIVEGRID_GITEA_HOST_IP must be validated before writing to /etc/hosts`);
} else {
passes.push(`${WORKFLOW_PATH}: internal host override validates HIVEGRID_GITEA_HOST_IP before /etc/hosts writes`);
}
return { passes, failures, observations: [] };
}
export function checkCosignInstaller(rootDir, content) {
const passes = [];
const failures = [];
if (/releases\/latest\/download\/cosign-linux-amd64/.test(content)) {
failures.push(`${WORKFLOW_PATH}: cosign installer must not use mutable releases/latest download URLs`);
} else {
passes.push(`${WORKFLOW_PATH}: cosign install avoids mutable releases/latest download URLs`);
}
if (!/bash\s+scripts\/ci\/install-cosign\.sh/.test(content)) {
failures.push(`${WORKFLOW_PATH}: cosign install must use ${COSIGN_INSTALLER_PATH}`);
} else {
passes.push(`${WORKFLOW_PATH}: cosign install uses ${COSIGN_INSTALLER_PATH}`);
}
const installerPath = path.join(rootDir, COSIGN_INSTALLER_PATH);
if (!fs.existsSync(installerPath)) {
failures.push(`${COSIGN_INSTALLER_PATH}: missing pinned cosign installer`);
return { passes, failures, observations: [] };
}
const installer = fs.readFileSync(installerPath, "utf8");
if (/COSIGN_VERSION="\$\{COSIGN_VERSION:-v[0-9]+\.[0-9]+\.[0-9]+\}"/.test(installer)) {
passes.push(`${COSIGN_INSTALLER_PATH}: cosign version is pinned`);
} else {
failures.push(`${COSIGN_INSTALLER_PATH}: must pin COSIGN_VERSION to a concrete release`);
}
if (/COSIGN_SHA256="\$\{COSIGN_SHA256:-[a-f0-9]{64}\}"/.test(installer) && /sha256sum --check --status/.test(installer)) {
passes.push(`${COSIGN_INSTALLER_PATH}: cosign download is verified by SHA256`);
} else {
failures.push(`${COSIGN_INSTALLER_PATH}: must verify the cosign download with a pinned SHA256`);
}
return { passes, failures, observations: [] };
}
export function verifyGiteaDeployWorkflow(rootDir = process.cwd()) {
if (!checkWorkflowExists(rootDir)) {
return { passes: [], failures: [`${WORKFLOW_PATH}: missing required Gitea deploy workflow file`], observations: [] };
}
let content;
try {
content = readWorkflow(rootDir);
} catch (error) {
return { passes: [], failures: [`${WORKFLOW_PATH}: failed to read workflow: ${error.message}`], observations: [] };
}
const analysis = createWorkflowAnalysis(content);
const checks = [
checkWorkflowRunners(content, analysis),
checkDeployPreflightContract(content, analysis),
checkDeploymentConcurrency(content),
checkDeployPushPathIgnoreContract(content),
checkForbiddenCommands(content, analysis),
checkDuplicateCoolifySelectedAssignments(content, analysis),
checkRequiredStepHardening(content, analysis),
checkExactRevisionRlsGateContract(content, analysis),
checkMigratorRoleProbeContract(rootDir),
checkCoolifyMutationPreflightContract(content, analysis),
checkWebSurfacePairContract(rootDir, content, analysis),
checkBoundedCurlContract(content, analysis),
checkDatabaseMigrationDeployContract(content, analysis),
checkCoolifyDeployHelper(rootDir),
checkCoolifyTriggerContract(content, analysis),
checkCoolifyStagingHealthWaitContract(content, analysis),
checkCoolifyStagingDependentHealthWaitContract(content, analysis),
checkCoolifyProductionHealthWaitContract(content, analysis),
checkCoolifyProductionDependentHealthWaitContract(content, analysis),
checkRegistryLogin(content),
checkIsolatedDeployBuildKit(rootDir, content, analysis),
checkDockerTargets(content, analysis),
checkImageVariables(content, analysis),
checkTagStrategy(content, analysis),
checkSentryDeployMarkerContract(content, analysis),
checkSecretRedaction(content, analysis),
checkGiteaVariableNames(content),
checkCosignInstaller(rootDir, content),
];
return checks.reduce(
(result, check) => ({
passes: [...result.passes, ...check.passes],
failures: [...result.failures, ...check.failures],
observations: [...result.observations, ...check.observations],
}),
{ passes: [], failures: [], observations: [] }
);
}
export function formatGiteaDeployWorkflowResult(result) {
const status = result.failures.length > 0 ? "FAIL" : "PASS";
return [
`${status} Gitea deploy workflow verification`,
...result.passes.map((pass) => `PASS ${pass}`),
...result.observations.map((observation) => `OBSERVE ${observation}`),
...result.failures.map((failure) => `FAIL ${failure}`),
`Summary: ${result.passes.length} passed, ${result.observations.length} observed, ${result.failures.length} failed`,
"",
].join("\n");
}
const isCli = process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
if (isCli) {
const result = verifyGiteaDeployWorkflow(process.cwd());
const output = formatGiteaDeployWorkflowResult(result);
if (result.failures.length > 0) {
process.stderr.write(output);
process.exitCode = 1;
} else {
process.stdout.write(output);
}
}