#!/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=&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); } }