Files
HiveGrid-Wireframes/hivegrid-cloud/scripts/verify-gitea-ci-workflow.mjs
T

1139 lines
45 KiB
JavaScript

#!/usr/bin/env node
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import {
CI_RUNNER_LABEL,
dockerImageTag,
HOST_OVERRIDE_VALIDATION_PATTERN as INFRA_HOST_OVERRIDE_VALIDATION_PATTERN,
HOST_OVERRIDE_WRITE_PATTERN as INFRA_HOST_OVERRIDE_WRITE_PATTERN,
} from "./infra-config.mjs";
const WORKFLOW_PATH = ".gitea/workflows/ci.yml";
const SENTRY_REUSABLE_WORKFLOW_PATH = ".gitea/workflows/_sentry-release.yml";
const REQUIRED_PNPM_VERSION = "10.33.0";
const WEB_SOURCEMAPS_ARTIFACT_NAME = "web-client-sourcemaps";
const WEB_SOURCEMAPS_PATH = "apps/web/build/client";
const WEB_SOURCEMAPS_VALIDATE_COMMAND = "test -n \"$(find apps/web/build/client -type f -name '*.map' -print -quit)\"";
const REQUIRED_COMMANDS_IN_ORDER = [
"pnpm run verify:github-ci-workflow",
"pnpm run verify:gitea-ci-workflow",
"pnpm run verify:deploy-workflow",
"pnpm run verify:deploy-readiness",
"pnpm run verify:ci-baseline",
"pnpm run verify:source-health",
"pnpm run verify:eslint-baseline",
"pnpm run verify:env-drift",
"pnpm run verify:health-baseline",
"pnpm run verify:api-inventory",
"pnpm run verify:tenant-isolation-report",
"pnpm run verify:sidebar-routes",
"pnpm run verify:i18n",
"pnpm run verify:tenant-auth-baseline",
"pnpm run verify:onboarding-flow",
"pnpm run verify:phase-c-boundaries",
"pnpm run verify:core-build-without-plugins-smoke",
"pnpm run verify:removed-modules",
"pnpm run verify:ui-visual",
"pnpm run test:hot-swap",
"pnpm run verify:a11y-coverage",
"pnpm run test:a11y",
"pnpm run verify:porting-docs-baseline",
"pnpm run verify:production-docs",
"pnpm run verify:release-docs",
"pnpm run verify:docs-links",
"pnpm typecheck",
"pnpm lint",
"pnpm run test:foundation",
"pnpm run test:packages",
"pnpm run test:mobile",
"pnpm test",
"pnpm run build:ci",
"pnpm run verify:bundle-size",
"pnpm run verify:production-seed-artifact",
];
const SERVICE_JOB_CONTRACTS = {
api: {
label: "api",
commands: [
"pnpm run verify:openapi-security",
"pnpm --filter @toolctrl/api typecheck",
"pnpm exec vitest run apps/api/src",
"pnpm --filter @toolctrl/api build",
],
},
worker: {
label: "worker",
commands: [
"pnpm --filter @toolctrl/worker typecheck",
"pnpm --filter @toolctrl/worker exec vitest run",
"pnpm run verify:workflow-outbox-native",
"pnpm --filter @toolctrl/worker build",
"pnpm --filter @toolctrl/worker health",
],
},
};
const DATABASE_JOB = "database";
const DATABASE_STATUS_COMMAND = "pnpm run verify:db-migrate-status:staging";
const DATABASE_GENERATE_COMMAND = "pnpm run db:generate";
const STAGING_SSH_HOST_VAR = "${{ vars.COOLIFY_STAGING_SSH_HOST }}";
const STAGING_SSH_USER_VAR = "${{ vars.COOLIFY_STAGING_SSH_USER }}";
const STAGING_WEB_RESOURCE_UUID_VAR = "${{ vars.COOLIFY_STAGING_WEB_RESOURCE_UUID }}";
const STAGING_SSH_KEY_SECRET = "${{ secrets.COOLIFY_STAGING_SSH_KEY }}";
const FORBIDDEN_DATABASE_MUTATION_PATTERN = /\b(?:prisma\s+migrate\s+deploy|migrate:deploy|db:deploy|db:push|prisma\s+db\s+push|seed)\b/i;
const DOCKER_SMOKE_JOB = "docker-smoke";
const DOCKER_SMOKE_NEEDS = ["quality", "api", "worker", "database"];
const DOCKER_SMOKE_TARGETS = ["web", "api", "worker"];
const DOCKER_WEB_TAG = dockerImageTag("web");
const DOCKER_API_TAG = dockerImageTag("api");
const DOCKER_WORKER_TAG = dockerImageTag("worker");
const DOCKER_WEB_ARTIFACT_COMMAND = `docker run --rm --entrypoint /bin/sh ${DOCKER_WEB_TAG} -lc 'test -f /myapp/apps/web/build/server/index.js && test -d /myapp/apps/web/build/client && test -f /myapp/build/server-runtime/server.js'`;
const DOCKER_WEB_RUNTIME_COMMAND = `bash scripts/docker-web-runtime-smoke.sh ${DOCKER_WEB_TAG}`;
const DOCKER_API_RUNTIME_COMMAND = `bash scripts/docker-api-runtime-smoke.sh ${DOCKER_API_TAG}`;
const DOCKER_WORKER_RUNTIME_COMMAND = `bash scripts/docker-worker-runtime-smoke.sh ${DOCKER_WORKER_TAG}`;
const DOCKER_SMOKE_REVISION_ARG_PATTERN = /(?:^|\s)--build-arg(?:=|\s+)APP_REVISION=ci-smoke-revision(?:\s|$)/;
const REQUIRED_RUNNER = CI_RUNNER_LABEL;
const FORBIDDEN_E2E_PATTERN = /\b(playwright|cypress|browser|test:e2e|e2e)\b/i;
const FORBIDDEN_DOCKER_PUBLISH_PATTERN = /\b(?:docker\s+(?:login|push)|docker\s+buildx\s+build[^\n]*--push|docker\s+build[^\n]*--push|publish)\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;
function readWorkflow(rootDir) {
return fs.readFileSync(path.join(rootDir, WORKFLOW_PATH), "utf8");
}
function readOptionalFile(rootDir, relativePath) {
const absolutePath = path.join(rootDir, relativePath);
if (!fs.existsSync(absolutePath)) {
return null;
}
return fs.readFileSync(absolutePath, "utf8");
}
function escapeRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function includesLineMatching(content, pattern) {
return content.split(/\r?\n/).some((line) => pattern.test(line));
}
function checkWorkflowExists(rootDir) {
return fs.existsSync(path.join(rootDir, WORKFLOW_PATH));
}
function getJobSection(content, jobName) {
const lines = content.split(/\r?\n/);
const jobStartPattern = new RegExp(`^ ${jobName}:\\s*$`);
const startIndex = lines.findIndex((line) => jobStartPattern.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+/.test(section);
}
function hasCorepackSetup(section) {
return /\bcorepack\s+enable\b/i.test(section);
}
function hasPinnedPnpm(section) {
return new RegExp(`\\bcorepack\\s+prepare\\s+pnpm@${REQUIRED_PNPM_VERSION.replaceAll(".", "\\.")}\\s+--activate\\b`).test(section);
}
function hasFrozenInstall(section) {
return /pnpm\s+install\s+--frozen-lockfile\b/.test(section);
}
function hasPnpmStoreSetup(section) {
return /pnpm\s+config\s+set\s+store-dir\b/.test(section) || /path:\s*\.pnpm-store\b/.test(section);
}
function checkWorkflowConcurrency(content) {
if (
/^concurrency:\s*$/m.test(content) &&
/^\s+group:\s*\$\{\{\s*github\.workflow\s*\}\}-\$\{\{\s*github\.event\.pull_request\.number\s*\|\|\s*github\.ref\s*\}\}\s*$/m.test(content) &&
/^\s+cancel-in-progress:\s*true\s*$/m.test(content)
) {
return { passes: [`${WORKFLOW_PATH}: CI concurrency cancels superseded pull request and branch runs`], failures: [], observations: [] };
}
return {
passes: [],
failures: [`${WORKFLOW_PATH}: CI workflow must define concurrency by workflow plus PR/ref with cancel-in-progress: true`],
observations: [],
};
}
function hasNeedsQuality(section) {
return getNeedsValues(section).includes("quality");
}
function getNeedsValues(section) {
const inlineNeeds = section.match(/^ needs:\s*\[([^\]]+)\]\s*$/m);
if (inlineNeeds) {
return inlineNeeds[1]
.split(",")
.map((value) => value.trim().replace(/^['\"]|['\"]$/g, ""))
.filter(Boolean);
}
const singleNeeds = section.match(/^ needs:\s*([A-Za-z0-9_-]+)\s*$/m);
if (singleNeeds) {
return [singleNeeds[1]];
}
const blockNeeds = section.match(/^ needs:\s*\n((?: -\s*[^\n]+\n?)+)/m);
if (blockNeeds) {
return blockNeeds[1]
.split(/\r?\n/)
.map((line) => line.match(/^ -\s*([^\s#]+)/)?.[1]?.replace(/^['\"]|['\"]$/g, ""))
.filter(Boolean);
}
return [];
}
function hasDockerBuildxSetup(section) {
return /uses:\s*docker\/setup-buildx-action@v\d+/.test(section);
}
function getRunCommands(section) {
const lines = section.split(/\r?\n/);
const commands = [];
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();
const lineNumber = index + 1;
if (/^[|>][+-]?$/.test(value)) {
const blockLines = [];
for (let blockIndex = index + 1; blockIndex < lines.length; blockIndex += 1) {
const blockLine = lines[blockIndex];
const blockIndent = blockLine.match(/^(\s*)/)?.[1].length ?? 0;
if (blockLine.trim() !== "" && blockIndent <= runIndent) {
break;
}
blockLines.push(blockLine.trim());
index = blockIndex;
}
commands.push({ lineNumber, command: blockLines.join("\n") });
continue;
}
commands.push({ lineNumber, command: value });
}
return commands;
}
function stripExecutableShellLine(line) {
return line.replace(/\s+#.*$/, "").trim();
}
function getExecutableRunCommandEntries(section) {
return getRunCommands(section).flatMap(({ lineNumber, command }) =>
command
.split(/\r?\n/)
.map((line, offset) => ({ lineNumber: lineNumber + offset, command: stripExecutableShellLine(line) }))
.filter(({ command }) => command.length > 0 && !command.startsWith("#")),
);
}
function getExecutableRunCommandBlocks(section) {
return getRunCommands(section)
.map(({ command }) =>
command
.split(/\r?\n/)
.map((line) => stripExecutableShellLine(line))
.filter((line) => line.length > 0 && !line.startsWith("#"))
.join("\n"),
)
.filter(Boolean);
}
function getAllStepBlocks(section) {
const lines = section.split(/\r?\n/);
const steps = [];
for (let index = 0; index < lines.length; index += 1) {
const stepMatch = lines[index].match(/^(\s*)-\s+/);
if (!stepMatch) {
continue;
}
const stepIndent = stepMatch[1].length;
const stepLines = [lines[index]];
for (let inner = index + 1; inner < lines.length; inner += 1) {
const candidate = lines[inner];
const indent = candidate.match(/^(\s*)/)?.[1].length ?? 0;
if (candidate.trim().length > 0 && indent <= stepIndent && /^\s*-\s+/.test(candidate)) {
break;
}
stepLines.push(candidate);
index = inner;
}
const text = stepLines.join("\n");
steps.push(text);
}
return steps;
}
function getStepBlocks(section) {
return getAllStepBlocks(section).filter((step) => /^\s*run:\s*/m.test(step));
}
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 inner = index + 1; inner < lines.length; inner += 1) {
const candidate = lines[inner];
const indent = candidate.match(/^(\s*)/)?.[1].length ?? 0;
if (candidate.trim().length > 0 && indent <= runIndent) {
break;
}
commandLines.push(candidate);
}
return commandLines.join("\n");
}
return "";
}
function getExecutableShellLines(runText) {
return runText
.split(/\r?\n/)
.map((line) => stripExecutableShellLine(line))
.filter(Boolean);
}
function getStepForRunCommand(section, predicate) {
return (
getStepBlocks(section).find((step) => {
const lines = getExecutableShellLines(getRunTextFromStep(step));
const block = lines.join("\n");
return lines.some((line) => predicate(line)) || predicate(block);
}) ?? ""
);
}
function hasUploadSourcemapsArtifactStep(section) {
return getAllStepBlocks(section).some(
(step) =>
/uses:\s*actions\/upload-artifact@v4/i.test(step) &&
new RegExp(`^\\s*name:\\s*${escapeRegExp(WEB_SOURCEMAPS_ARTIFACT_NAME)}\\s*$`, "m").test(step) &&
new RegExp(`^\\s*path:\\s*${escapeRegExp(WEB_SOURCEMAPS_PATH)}\\s*$`, "m").test(step) &&
/^\s*if-no-files-found:\s*error\s*$/m.test(step) &&
/^\s*retention-days:\s*7\s*$/m.test(step),
);
}
function hasReusableSourcemapsArtifactContract(content) {
return (
/^\s+sourcemaps-artifact:\s*$/m.test(content) &&
/uses:\s*actions\/download-artifact@v4/i.test(content) &&
/^\s*if:\s*\$\{\{\s*inputs\.sourcemaps-artifact\s*!=\s*'none'\s*\}\}\s*$/m.test(content) &&
/^\s*name:\s*\$\{\{\s*inputs\.sourcemaps-artifact\s*\}\}\s*$/m.test(content) &&
/^\s*path:\s*\$\{\{\s*inputs\.sourcemaps-path\s*\}\}\s*$/m.test(content)
);
}
function matchesExecutableCommand(executableCommand, expectedCommand) {
return executableCommand === expectedCommand;
}
function countExecutableCommandOccurrences(section, expectedCommand) {
return getExecutableRunCommandEntries(section).filter(({ command }) => command === expectedCommand).length;
}
function hasDockerTargetBuild(section, target) {
return getExecutableRunCommandEntries(section).some(
({ command }) => /\bdocker\s+(?:buildx\s+build|build)\b/.test(command) && new RegExp(`(?:^|\\s)--target(?:=|\\s+)${target}(?:\\s|$)`).test(command),
);
}
function getDockerTargetBuild(section, target) {
return getExecutableRunCommandEntries(section).find(
({ command }) => /\bdocker\s+(?:buildx\s+build|build)\b/.test(command) && new RegExp(`(?:^|\\s)--target(?:=|\\s+)${target}(?:\\s|$)`).test(command),
)?.command;
}
function hasDockerComposeConfig(section) {
return getExecutableRunCommandEntries(section).some(({ command }) => /\bdocker\s+compose\s+config\b/.test(command));
}
function hasWebDockerArtifactValidation(section) {
return getExecutableRunCommandEntries(section).some(({ command }) => command === DOCKER_WEB_ARTIFACT_COMMAND);
}
function hasWebDockerRuntimeValidation(section) {
return getExecutableRunCommandEntries(section).some(({ command }) => command === DOCKER_WEB_RUNTIME_COMMAND);
}
function hasApiDockerRuntimeValidation(section) {
return getExecutableRunCommandEntries(section).some(({ command }) => command === DOCKER_API_RUNTIME_COMMAND);
}
function hasWorkerDockerRuntimeValidation(section) {
return getExecutableRunCommandEntries(section).some(({ command }) => command === DOCKER_WORKER_RUNTIME_COMMAND);
}
function hasParallelDockerTargetBuilds(section) {
return getRunCommands(section).some(({ command }) => {
const hasEveryTarget = DOCKER_SMOKE_TARGETS.every((target) =>
new RegExp(`\\bdocker\\s+(?:buildx\\s+build|build)\\b[^\\n]*--target(?:=|\\s+)${target}(?:\\s|$)[^\\n]*&`, "u").test(command),
);
const capturesEveryPid = DOCKER_SMOKE_TARGETS.every((target) => new RegExp(`\\b${target}_pid=\\$!`, "u").test(command));
const waitsForEveryPid = DOCKER_SMOKE_TARGETS.every((target) => new RegExp(`\\bwait\\s+"\\$${target}_pid"`, "u").test(command));
return hasEveryTarget && capturesEveryPid && waitsForEveryPid;
});
}
function includesCommand(section, command) {
return getExecutableRunCommandEntries(section).some(({ command: executableCommand }) => matchesExecutableCommand(executableCommand, command));
}
function getRequiredStepSofteningReason(step) {
if (!step) {
return "";
}
if (/^\s*continue-on-error:\s*true\s*$/im.test(step) || /^\s*if:\s*(?:always\(\)|\$\{\{\s*always\(\)\s*\}\})\s*$/im.test(step)) {
return "continue-on-error or always()";
}
const lines = getExecutableShellLines(getRunTextFromStep(step));
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 getRequiredCommandSofteningReason(section, command) {
const step = getStepForRunCommand(section, (line) => line.includes(command));
return getRequiredStepSofteningReason(step);
}
function getRunCommandLines(section) {
return getExecutableRunCommandBlocks(section).map((command) => `run: ${command}`);
}
function hasStepLevelEnvValue(section, command, envName, envValue) {
const lines = section.split(/\r?\n/);
const commandIndex = lines.findIndex((line) => /^\s*run:\s*/.test(line) && line.includes(command));
if (commandIndex === -1) {
return false;
}
const commandIndent = lines[commandIndex].match(/^(\s*)/)?.[1].length ?? 0;
const stepIndent = Math.max(commandIndent - 2, 0);
let nextStepIndex = lines.length;
for (let index = commandIndex + 1; index < lines.length; index += 1) {
const line = lines[index];
if (new RegExp(`^\\s{${stepIndent}}-\\s+`).test(line)) {
nextStepIndex = index;
break;
}
}
const stepRemainder = lines.slice(commandIndex + 1, nextStepIndex).join("\n");
const escapedEnvValue = envValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
return new RegExp(`^\\s*${envName}:\\s*${escapedEnvValue}\\s*$`, "m").test(stepRemainder);
}
function hasJobLevelEnvValue(section, envName) {
const firstStepIndex = section.search(/^ steps:\s*$/m);
const beforeSteps = firstStepIndex === -1 ? section : section.slice(0, firstStepIndex);
return new RegExp(`^\\s*${envName}:\\s*`, "m").test(beforeSteps);
}
export function checkWorkflowTriggers(content) {
const passes = [];
const failures = [];
if (includesLineMatching(content, /^\s*push\s*:/m)) {
failures.push(`${WORKFLOW_PATH}: trigger "push" is forbidden; main pushes are reserved for the deploy workflow`);
} else {
passes.push(`${WORKFLOW_PATH}: trigger "push" is disabled so deploy is not queued behind CI on main`);
}
if (includesLineMatching(content, /^\s*pull_request\s*:/m)) {
passes.push(`${WORKFLOW_PATH}: trigger "pull_request" is defined`);
} else {
failures.push(`${WORKFLOW_PATH}: missing required trigger "pull_request"`);
}
if (includesLineMatching(content, /^\s*workflow_dispatch\s*:/m)) {
passes.push(`${WORKFLOW_PATH}: trigger "workflow_dispatch" is defined`);
} else {
failures.push(`${WORKFLOW_PATH}: missing required trigger "workflow_dispatch" for manual CI runs`);
}
return { passes, failures, observations: [] };
}
export function checkPnpmSetup(content) {
const passes = [];
const failures = [];
const qualitySection = getJobSection(content, "quality");
if (!qualitySection) {
failures.push(`${WORKFLOW_PATH}: missing required quality job "quality"`);
return { passes, failures, observations: [] };
}
if (hasCorepackSetup(qualitySection)) {
passes.push(`${WORKFLOW_PATH}: Corepack setup is present`);
} else {
failures.push(`${WORKFLOW_PATH}: missing Corepack setup`);
}
if (hasPinnedPnpm(qualitySection)) {
passes.push(`${WORKFLOW_PATH}: pnpm@${REQUIRED_PNPM_VERSION} is pinned`);
} else {
failures.push(`${WORKFLOW_PATH}: pnpm must be pinned to pnpm@${REQUIRED_PNPM_VERSION}`);
}
if (hasFrozenInstall(qualitySection)) {
passes.push(`${WORKFLOW_PATH}: command "pnpm install --frozen-lockfile" is present`);
} else {
failures.push(`${WORKFLOW_PATH}: missing required command "pnpm install --frozen-lockfile"`);
}
return { passes, failures, observations: [] };
}
export function checkOrderedQualityGates(content) {
const passes = [];
const failures = [];
const qualitySection = getJobSection(content, "quality") ?? content;
const executableCommands = getExecutableRunCommandEntries(qualitySection);
let previousIndex = -1;
for (const command of REQUIRED_COMMANDS_IN_ORDER) {
const commandIndex = executableCommands.findIndex((entry) => matchesExecutableCommand(entry.command, command));
const commandOccurrences = countExecutableCommandOccurrences(qualitySection, command);
const softeningReason = getRequiredCommandSofteningReason(qualitySection, command);
if (commandIndex === -1) {
if (softeningReason) {
failures.push(`${WORKFLOW_PATH}: command "${command}" must not use ${softeningReason}`);
} else {
failures.push(`${WORKFLOW_PATH}: missing required command "${command}"`);
}
continue;
}
if (commandOccurrences > 1) {
failures.push(`${WORKFLOW_PATH}: command "${command}" must run exactly once to avoid redundant CI gates`);
continue;
}
if (commandIndex < previousIndex) {
failures.push(`${WORKFLOW_PATH}: command "${command}" must run after previous quality gates`);
continue;
}
if (softeningReason) {
failures.push(`${WORKFLOW_PATH}: command "${command}" must not use ${softeningReason}`);
continue;
}
passes.push(`${WORKFLOW_PATH}: command "${command}" is ordered`);
previousIndex = commandIndex;
}
const sourcemapsValidationStep = getStepForRunCommand(qualitySection, (line) => line === WEB_SOURCEMAPS_VALIDATE_COMMAND);
if (sourcemapsValidationStep) {
const softeningReason = getRequiredStepSofteningReason(sourcemapsValidationStep);
if (softeningReason) {
failures.push(`${WORKFLOW_PATH}: web source-map validation must not use ${softeningReason}`);
} else {
passes.push(`${WORKFLOW_PATH}: web source-map files are validated before Sentry artifact upload`);
}
} else {
failures.push(`${WORKFLOW_PATH}: missing web source-map validation before Sentry artifact upload`);
}
if (hasUploadSourcemapsArtifactStep(qualitySection)) {
passes.push(`${WORKFLOW_PATH}: web source maps are uploaded as artifact "${WEB_SOURCEMAPS_ARTIFACT_NAME}"`);
} else {
failures.push(`${WORKFLOW_PATH}: must upload ${WEB_SOURCEMAPS_PATH} as artifact "${WEB_SOURCEMAPS_ARTIFACT_NAME}" with if-no-files-found: error`);
}
return { passes, failures, observations: [] };
}
export function checkSentrySourcemapWorkflow(rootDir, content) {
const passes = [];
const failures = [];
const sentryReusableContent = readOptionalFile(rootDir, SENTRY_REUSABLE_WORKFLOW_PATH);
if (!sentryReusableContent) {
failures.push(`${SENTRY_REUSABLE_WORKFLOW_PATH}: missing reusable Sentry release workflow`);
} else if (hasReusableSourcemapsArtifactContract(sentryReusableContent)) {
passes.push(`${SENTRY_REUSABLE_WORKFLOW_PATH}: reusable workflow downloads requested source-map artifacts before upload`);
} else {
failures.push(`${SENTRY_REUSABLE_WORKFLOW_PATH}: reusable workflow must accept sourcemaps-artifact and download it to inputs.sourcemaps-path`);
}
const sentryWebSection = getJobSection(content, "sentry-release-web");
if (!sentryWebSection) {
failures.push(`${WORKFLOW_PATH}: missing Sentry web release job "sentry-release-web"`);
} else if (
new RegExp(`^\\s*sourcemaps-path:\\s*${escapeRegExp(WEB_SOURCEMAPS_PATH)}\\s*$`, "m").test(sentryWebSection) &&
new RegExp(`^\\s*sourcemaps-artifact:\\s*${escapeRegExp(WEB_SOURCEMAPS_ARTIFACT_NAME)}\\s*$`, "m").test(sentryWebSection)
) {
passes.push(`${WORKFLOW_PATH}: Sentry web release consumes artifact "${WEB_SOURCEMAPS_ARTIFACT_NAME}"`);
} else {
failures.push(`${WORKFLOW_PATH}: Sentry web release must pass sourcemaps-path ${WEB_SOURCEMAPS_PATH} and sourcemaps-artifact ${WEB_SOURCEMAPS_ARTIFACT_NAME}`);
}
return { passes, failures, observations: [] };
}
export function checkServiceJobs(content) {
const passes = [];
const failures = [];
for (const [jobName, contract] of Object.entries(SERVICE_JOB_CONTRACTS)) {
const section = getJobSection(content, jobName);
if (!section) {
failures.push(`${WORKFLOW_PATH}: missing required service job "${jobName}"`);
continue;
}
passes.push(`${WORKFLOW_PATH}: service job "${jobName}" is defined`);
if (hasNeedsQuality(section)) {
failures.push(`${WORKFLOW_PATH}: service job "${jobName}" must not wait for "quality"; scoped service checks should run in parallel`);
} else {
passes.push(`${WORKFLOW_PATH}: service job "${jobName}" runs independently of "quality"`);
}
if (hasCheckoutStep(section)) {
passes.push(`${WORKFLOW_PATH}: service job "${jobName}" checks out the repository`);
} else {
failures.push(`${WORKFLOW_PATH}: service job "${jobName}" missing checkout step`);
}
if (hasCorepackSetup(section)) {
passes.push(`${WORKFLOW_PATH}: service job "${jobName}" enables Corepack`);
} else {
failures.push(`${WORKFLOW_PATH}: service job "${jobName}" missing Corepack setup`);
}
if (hasPinnedPnpm(section)) {
passes.push(`${WORKFLOW_PATH}: service job "${jobName}" pins pnpm@${REQUIRED_PNPM_VERSION}`);
} else {
failures.push(`${WORKFLOW_PATH}: service job "${jobName}" must pin pnpm@${REQUIRED_PNPM_VERSION}`);
}
if (hasFrozenInstall(section)) {
passes.push(`${WORKFLOW_PATH}: service job "${jobName}" installs with frozen lockfile`);
} else {
failures.push(`${WORKFLOW_PATH}: service job "${jobName}" missing command "pnpm install --frozen-lockfile"`);
}
for (const command of contract.commands) {
if (includesCommand(section, command)) {
const softeningReason = getRequiredCommandSofteningReason(section, command);
if (softeningReason) {
failures.push(`${WORKFLOW_PATH}: service job "${jobName}" command "${command}" must not use ${softeningReason}`);
} else {
passes.push(`${WORKFLOW_PATH}: service job "${jobName}" includes scoped command "${command}"`);
}
} else {
const softeningReason = getRequiredCommandSofteningReason(section, command);
if (softeningReason) {
failures.push(`${WORKFLOW_PATH}: service job "${jobName}" command "${command}" must not use ${softeningReason}`);
} else {
failures.push(`${WORKFLOW_PATH}: service job "${jobName}" missing scoped command "${command}"`);
}
}
}
}
return { passes, failures, observations: [] };
}
export function checkDatabaseJob(content) {
const passes = [];
const failures = [];
const section = getJobSection(content, DATABASE_JOB);
if (!section) {
return {
passes,
failures: [`${WORKFLOW_PATH}: missing required database job "${DATABASE_JOB}"`],
observations: [],
};
}
passes.push(`${WORKFLOW_PATH}: database job "${DATABASE_JOB}" is defined`);
if (hasNeedsQuality(section)) {
failures.push(`${WORKFLOW_PATH}: database job "${DATABASE_JOB}" must not wait for "quality"; database verification should run in parallel`);
} else {
passes.push(`${WORKFLOW_PATH}: database job "${DATABASE_JOB}" runs independently of "quality"`);
}
if (hasCheckoutStep(section)) {
passes.push(`${WORKFLOW_PATH}: database job "${DATABASE_JOB}" checks out the repository`);
} else {
failures.push(`${WORKFLOW_PATH}: database job "${DATABASE_JOB}" missing checkout step`);
}
if (hasCorepackSetup(section)) {
passes.push(`${WORKFLOW_PATH}: database job "${DATABASE_JOB}" enables Corepack`);
} else {
failures.push(`${WORKFLOW_PATH}: database job "${DATABASE_JOB}" missing Corepack setup`);
}
if (hasPinnedPnpm(section)) {
passes.push(`${WORKFLOW_PATH}: database job "${DATABASE_JOB}" pins pnpm@${REQUIRED_PNPM_VERSION}`);
} else {
failures.push(`${WORKFLOW_PATH}: database job "${DATABASE_JOB}" must pin pnpm@${REQUIRED_PNPM_VERSION}`);
}
if (hasFrozenInstall(section)) {
passes.push(`${WORKFLOW_PATH}: database job "${DATABASE_JOB}" installs with frozen lockfile`);
} else {
failures.push(`${WORKFLOW_PATH}: database job "${DATABASE_JOB}" missing command "pnpm install --frozen-lockfile"`);
}
if (includesCommand(section, DATABASE_GENERATE_COMMAND)) {
const softeningReason = getRequiredCommandSofteningReason(section, DATABASE_GENERATE_COMMAND);
if (softeningReason) {
failures.push(`${WORKFLOW_PATH}: database job "${DATABASE_JOB}" command "${DATABASE_GENERATE_COMMAND}" must not use ${softeningReason}`);
} else {
passes.push(`${WORKFLOW_PATH}: database job "${DATABASE_JOB}" includes command "${DATABASE_GENERATE_COMMAND}"`);
}
} else {
const softeningReason = getRequiredCommandSofteningReason(section, DATABASE_GENERATE_COMMAND);
if (softeningReason) {
failures.push(`${WORKFLOW_PATH}: database job "${DATABASE_JOB}" command "${DATABASE_GENERATE_COMMAND}" must not use ${softeningReason}`);
} else {
failures.push(`${WORKFLOW_PATH}: database job "${DATABASE_JOB}" missing command "${DATABASE_GENERATE_COMMAND}"`);
}
}
if (includesCommand(section, DATABASE_STATUS_COMMAND)) {
const softeningReason = getRequiredCommandSofteningReason(section, DATABASE_STATUS_COMMAND);
if (softeningReason) {
failures.push(`${WORKFLOW_PATH}: database job "${DATABASE_JOB}" command "${DATABASE_STATUS_COMMAND}" must not use ${softeningReason}`);
} else {
passes.push(`${WORKFLOW_PATH}: database job "${DATABASE_JOB}" includes command "${DATABASE_STATUS_COMMAND}"`);
}
} else {
const softeningReason = getRequiredCommandSofteningReason(section, DATABASE_STATUS_COMMAND);
if (softeningReason) {
failures.push(`${WORKFLOW_PATH}: database job "${DATABASE_JOB}" command "${DATABASE_STATUS_COMMAND}" must not use ${softeningReason}`);
} else {
failures.push(`${WORKFLOW_PATH}: database job "${DATABASE_JOB}" missing command "${DATABASE_STATUS_COMMAND}"`);
}
}
if (hasStepLevelEnvValue(section, DATABASE_STATUS_COMMAND, "COOLIFY_STAGING_SSH_HOST", STAGING_SSH_HOST_VAR)) {
passes.push(`${WORKFLOW_PATH}: database job "${DATABASE_JOB}" scopes staging SSH host to the migrate-status step`);
} else {
failures.push(`${WORKFLOW_PATH}: database job "${DATABASE_JOB}" must set COOLIFY_STAGING_SSH_HOST: \${{ vars.COOLIFY_STAGING_SSH_HOST }} on the migrate-status step`);
}
if (hasStepLevelEnvValue(section, DATABASE_STATUS_COMMAND, "COOLIFY_STAGING_SSH_USER", STAGING_SSH_USER_VAR)) {
passes.push(`${WORKFLOW_PATH}: database job "${DATABASE_JOB}" scopes staging SSH user to the migrate-status step`);
} else {
failures.push(`${WORKFLOW_PATH}: database job "${DATABASE_JOB}" must set COOLIFY_STAGING_SSH_USER: \${{ vars.COOLIFY_STAGING_SSH_USER }} on the migrate-status step`);
}
if (hasStepLevelEnvValue(section, DATABASE_STATUS_COMMAND, "COOLIFY_STAGING_WEB_RESOURCE_UUID", STAGING_WEB_RESOURCE_UUID_VAR)) {
passes.push(`${WORKFLOW_PATH}: database job "${DATABASE_JOB}" scopes staging web resource UUID to the migrate-status step`);
} else {
failures.push(`${WORKFLOW_PATH}: database job "${DATABASE_JOB}" must set COOLIFY_STAGING_WEB_RESOURCE_UUID: \${{ vars.COOLIFY_STAGING_WEB_RESOURCE_UUID }} on the migrate-status step`);
}
if (hasStepLevelEnvValue(section, DATABASE_STATUS_COMMAND, "COOLIFY_STAGING_SSH_KEY", STAGING_SSH_KEY_SECRET)) {
passes.push(`${WORKFLOW_PATH}: database job "${DATABASE_JOB}" scopes staging SSH key to the migrate-status step`);
} else {
failures.push(`${WORKFLOW_PATH}: database job "${DATABASE_JOB}" must set COOLIFY_STAGING_SSH_KEY: \${{ secrets.COOLIFY_STAGING_SSH_KEY }} on the migrate-status step`);
}
if (section.includes("secrets.DATABASE_URL")) {
failures.push(`${WORKFLOW_PATH}: database job "${DATABASE_JOB}" must not require direct DATABASE_URL access to private Coolify databases`);
} else {
passes.push(`${WORKFLOW_PATH}: database job "${DATABASE_JOB}" verifies private Coolify databases without exposing DATABASE_URL`);
}
if (hasJobLevelEnvValue(section, "DATABASE_URL")) {
failures.push(`${WORKFLOW_PATH}: database job "${DATABASE_JOB}" must not set DATABASE_URL at job scope`);
} else {
passes.push(`${WORKFLOW_PATH}: database job "${DATABASE_JOB}" does not expose DATABASE_URL at job scope`);
}
const forbiddenMutationLine = getRunCommandLines(section).find((line) => FORBIDDEN_DATABASE_MUTATION_PATTERN.test(line));
if (forbiddenMutationLine) {
failures.push(`${WORKFLOW_PATH}: database job "${DATABASE_JOB}" contains forbidden deploy/seed/schema mutation command: ${forbiddenMutationLine.trim()}`);
} else {
passes.push(`${WORKFLOW_PATH}: database job "${DATABASE_JOB}" contains no deploy/seed/schema mutation commands`);
}
const forbiddenE2eLine = getRunCommandLines(section).find((line) => FORBIDDEN_E2E_PATTERN.test(line));
if (forbiddenE2eLine) {
failures.push(`${WORKFLOW_PATH}: database job "${DATABASE_JOB}" contains forbidden E2E/browser command: ${forbiddenE2eLine.trim()}`);
} else {
passes.push(`${WORKFLOW_PATH}: database job "${DATABASE_JOB}" contains no E2E/browser commands`);
}
return { passes, failures, observations: [] };
}
export function checkDockerSmokeJob(content) {
const passes = [];
const failures = [];
const section = getJobSection(content, DOCKER_SMOKE_JOB);
if (!section) {
return {
passes,
failures: [`${WORKFLOW_PATH}: missing required Docker smoke job "${DOCKER_SMOKE_JOB}"`],
observations: [],
};
}
passes.push(`${WORKFLOW_PATH}: Docker smoke job "${DOCKER_SMOKE_JOB}" is defined`);
const needs = getNeedsValues(section);
for (const requiredNeed of DOCKER_SMOKE_NEEDS) {
if (needs.includes(requiredNeed)) {
passes.push(`${WORKFLOW_PATH}: Docker smoke job "${DOCKER_SMOKE_JOB}" needs "${requiredNeed}"`);
} else {
failures.push(`${WORKFLOW_PATH}: Docker smoke job "${DOCKER_SMOKE_JOB}" must declare needs including "${requiredNeed}"`);
}
}
if (hasCheckoutStep(section)) {
passes.push(`${WORKFLOW_PATH}: Docker smoke job "${DOCKER_SMOKE_JOB}" checks out the repository`);
} else {
failures.push(`${WORKFLOW_PATH}: Docker smoke job "${DOCKER_SMOKE_JOB}" missing checkout step`);
}
if (hasCorepackSetup(section)) {
failures.push(`${WORKFLOW_PATH}: Docker smoke job "${DOCKER_SMOKE_JOB}" must not run Corepack setup`);
} else {
passes.push(`${WORKFLOW_PATH}: Docker smoke job "${DOCKER_SMOKE_JOB}" does not run Corepack setup`);
}
if (hasPinnedPnpm(section)) {
failures.push(`${WORKFLOW_PATH}: Docker smoke job "${DOCKER_SMOKE_JOB}" must not run "corepack prepare pnpm@${REQUIRED_PNPM_VERSION}"`);
} else {
passes.push(`${WORKFLOW_PATH}: Docker smoke job "${DOCKER_SMOKE_JOB}" does not run "corepack prepare pnpm@${REQUIRED_PNPM_VERSION}"`);
}
if (hasPnpmStoreSetup(section)) {
failures.push(`${WORKFLOW_PATH}: Docker smoke job "${DOCKER_SMOKE_JOB}" must not configure or cache the pnpm store`);
} else {
passes.push(`${WORKFLOW_PATH}: Docker smoke job "${DOCKER_SMOKE_JOB}" does not configure or cache the pnpm store`);
}
if (hasFrozenInstall(section)) {
failures.push(`${WORKFLOW_PATH}: Docker smoke job "${DOCKER_SMOKE_JOB}" must not run "pnpm install --frozen-lockfile"`);
} else {
passes.push(`${WORKFLOW_PATH}: Docker smoke job "${DOCKER_SMOKE_JOB}" does not run "pnpm install --frozen-lockfile"`);
}
if (hasDockerBuildxSetup(section)) {
passes.push(`${WORKFLOW_PATH}: Docker smoke job "${DOCKER_SMOKE_JOB}" sets up Docker Buildx`);
} else {
failures.push(`${WORKFLOW_PATH}: Docker smoke job "${DOCKER_SMOKE_JOB}" missing Docker Buildx setup`);
}
for (const target of DOCKER_SMOKE_TARGETS) {
if (hasDockerTargetBuild(section, target)) {
const targetBuild = getDockerTargetBuild(section, target) ?? "";
const targetBuildPattern = (line) => /\bdocker\s+(?:buildx\s+build|build)\b/.test(line) && new RegExp(`(?:^|\\s)--target(?:=|\\s+)${target}(?:\\s|$)`).test(line);
const softeningReason = getRequiredStepSofteningReason(getStepForRunCommand(section, targetBuildPattern));
if (softeningReason) {
failures.push(`${WORKFLOW_PATH}: Docker smoke job "${DOCKER_SMOKE_JOB}" build for target "${target}" must not use ${softeningReason}`);
} else if (!DOCKER_SMOKE_REVISION_ARG_PATTERN.test(targetBuild)) {
failures.push(`${WORKFLOW_PATH}: Docker smoke job "${DOCKER_SMOKE_JOB}" build for target "${target}" must pass --build-arg APP_REVISION=ci-smoke-revision`);
} else {
passes.push(`${WORKFLOW_PATH}: Docker smoke job "${DOCKER_SMOKE_JOB}" builds Docker target "${target}"`);
}
} else {
failures.push(`${WORKFLOW_PATH}: Docker smoke job "${DOCKER_SMOKE_JOB}" missing Docker build for target "${target}"`);
}
}
if (hasParallelDockerTargetBuilds(section)) {
passes.push(`${WORKFLOW_PATH}: Docker smoke job "${DOCKER_SMOKE_JOB}" builds Docker targets in parallel`);
} else {
failures.push(`${WORKFLOW_PATH}: Docker smoke job "${DOCKER_SMOKE_JOB}" must build web, api, and worker Docker targets in one parallel bash step`);
}
if (hasDockerComposeConfig(section)) {
const softeningReason = getRequiredCommandSofteningReason(section, "docker compose config");
if (softeningReason) {
failures.push(`${WORKFLOW_PATH}: Docker smoke job "${DOCKER_SMOKE_JOB}" command "docker compose config" must not use ${softeningReason}`);
} else {
passes.push(`${WORKFLOW_PATH}: Docker smoke job "${DOCKER_SMOKE_JOB}" validates Docker Compose config`);
}
} else {
const softeningReason = getRequiredCommandSofteningReason(section, "docker compose config");
if (softeningReason) {
failures.push(`${WORKFLOW_PATH}: Docker smoke job "${DOCKER_SMOKE_JOB}" command "docker compose config" must not use ${softeningReason}`);
} else {
failures.push(`${WORKFLOW_PATH}: Docker smoke job "${DOCKER_SMOKE_JOB}" missing command "docker compose config"`);
}
}
if (hasWebDockerArtifactValidation(section)) {
const softeningReason = getRequiredCommandSofteningReason(section, DOCKER_WEB_ARTIFACT_COMMAND);
if (softeningReason) {
failures.push(`${WORKFLOW_PATH}: Docker smoke job "${DOCKER_SMOKE_JOB}" command "${DOCKER_WEB_ARTIFACT_COMMAND}" must not use ${softeningReason}`);
} else {
passes.push(`${WORKFLOW_PATH}: Docker smoke job "${DOCKER_SMOKE_JOB}" validates web Docker artifacts`);
}
} else {
const softeningReason = getRequiredCommandSofteningReason(section, DOCKER_WEB_ARTIFACT_COMMAND);
if (softeningReason) {
failures.push(`${WORKFLOW_PATH}: Docker smoke job "${DOCKER_SMOKE_JOB}" command "${DOCKER_WEB_ARTIFACT_COMMAND}" must not use ${softeningReason}`);
} else {
failures.push(`${WORKFLOW_PATH}: Docker smoke job "${DOCKER_SMOKE_JOB}" missing command "${DOCKER_WEB_ARTIFACT_COMMAND}"`);
}
}
if (hasWebDockerRuntimeValidation(section)) {
const softeningReason = getRequiredCommandSofteningReason(section, DOCKER_WEB_RUNTIME_COMMAND);
if (softeningReason) {
failures.push(`${WORKFLOW_PATH}: Docker smoke job "${DOCKER_SMOKE_JOB}" command "${DOCKER_WEB_RUNTIME_COMMAND}" must not use ${softeningReason}`);
} else {
passes.push(`${WORKFLOW_PATH}: Docker smoke job "${DOCKER_SMOKE_JOB}" validates web Docker runtime`);
}
} else {
const softeningReason = getRequiredCommandSofteningReason(section, DOCKER_WEB_RUNTIME_COMMAND);
if (softeningReason) {
failures.push(`${WORKFLOW_PATH}: Docker smoke job "${DOCKER_SMOKE_JOB}" command "${DOCKER_WEB_RUNTIME_COMMAND}" must not use ${softeningReason}`);
} else {
failures.push(`${WORKFLOW_PATH}: Docker smoke job "${DOCKER_SMOKE_JOB}" missing command "${DOCKER_WEB_RUNTIME_COMMAND}"`);
}
}
if (hasApiDockerRuntimeValidation(section)) {
const softeningReason = getRequiredCommandSofteningReason(section, DOCKER_API_RUNTIME_COMMAND);
if (softeningReason) {
failures.push(`${WORKFLOW_PATH}: Docker smoke job "${DOCKER_SMOKE_JOB}" command "${DOCKER_API_RUNTIME_COMMAND}" must not use ${softeningReason}`);
} else {
passes.push(`${WORKFLOW_PATH}: Docker smoke job "${DOCKER_SMOKE_JOB}" validates API Docker runtime`);
}
} else {
const softeningReason = getRequiredCommandSofteningReason(section, DOCKER_API_RUNTIME_COMMAND);
if (softeningReason) {
failures.push(`${WORKFLOW_PATH}: Docker smoke job "${DOCKER_SMOKE_JOB}" command "${DOCKER_API_RUNTIME_COMMAND}" must not use ${softeningReason}`);
} else {
failures.push(`${WORKFLOW_PATH}: Docker smoke job "${DOCKER_SMOKE_JOB}" missing command "${DOCKER_API_RUNTIME_COMMAND}"`);
}
}
if (hasWorkerDockerRuntimeValidation(section)) {
const softeningReason = getRequiredCommandSofteningReason(section, DOCKER_WORKER_RUNTIME_COMMAND);
if (softeningReason) {
failures.push(`${WORKFLOW_PATH}: Docker smoke job "${DOCKER_SMOKE_JOB}" command "${DOCKER_WORKER_RUNTIME_COMMAND}" must not use ${softeningReason}`);
} else {
passes.push(`${WORKFLOW_PATH}: Docker smoke job "${DOCKER_SMOKE_JOB}" validates worker Docker runtime`);
}
} else {
const softeningReason = getRequiredCommandSofteningReason(section, DOCKER_WORKER_RUNTIME_COMMAND);
if (softeningReason) {
failures.push(`${WORKFLOW_PATH}: Docker smoke job "${DOCKER_SMOKE_JOB}" command "${DOCKER_WORKER_RUNTIME_COMMAND}" must not use ${softeningReason}`);
} else {
failures.push(`${WORKFLOW_PATH}: Docker smoke job "${DOCKER_SMOKE_JOB}" missing command "${DOCKER_WORKER_RUNTIME_COMMAND}"`);
}
}
const forbiddenLine = getRunCommandLines(section).find((line) => FORBIDDEN_DOCKER_PUBLISH_PATTERN.test(line));
if (forbiddenLine) {
failures.push(`${WORKFLOW_PATH}: Docker smoke job "${DOCKER_SMOKE_JOB}" contains forbidden publish command: ${forbiddenLine.trim()}`);
} else {
passes.push(`${WORKFLOW_PATH}: Docker smoke job "${DOCKER_SMOKE_JOB}" contains no registry login/push/publish commands`);
}
return { passes, failures, observations: [] };
}
export function checkWorkflowRunners(content) {
const passes = [];
const failures = [];
const runnerLines = content
.split(/\r?\n/)
.map((line, index) => ({ line, lineNumber: index + 1 }))
.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 checkForbiddenE2eCommands(content) {
const passes = [];
const failures = [];
const forbiddenLine = getRunCommandLines(content).find((line) => FORBIDDEN_E2E_PATTERN.test(line));
if (forbiddenLine) {
failures.push(`${WORKFLOW_PATH}: forbidden E2E/browser command found: ${forbiddenLine.trim()}`);
} else {
passes.push(`${WORKFLOW_PATH}: no forbidden E2E/browser commands are present`);
}
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 TOOLCTRL_GITEA_HOST_IP`);
} else {
passes.push(`${WORKFLOW_PATH}: internal host override avoids the reserved GITEA_ variable prefix`);
}
const hostWrites = [...content.matchAll(HOST_OVERRIDE_WRITE_PATTERN)].length;
const validations = [...content.matchAll(HOST_OVERRIDE_VALIDATION_PATTERN)].length;
if (hostWrites > validations) {
failures.push(`${WORKFLOW_PATH}: TOOLCTRL_GITEA_HOST_IP must be validated before writing to /etc/hosts`);
} else {
passes.push(`${WORKFLOW_PATH}: internal host override validates TOOLCTRL_GITEA_HOST_IP before /etc/hosts writes`);
}
return { passes, failures, observations: [] };
}
export function verifyGiteaCiWorkflow(rootDir = process.cwd()) {
if (!checkWorkflowExists(rootDir)) {
return { passes: [], failures: [`${WORKFLOW_PATH}: missing required Gitea CI workflow file`], observations: [] };
}
let content;
try {
content = readWorkflow(rootDir);
} catch (error) {
return { passes: [], failures: [`${WORKFLOW_PATH}: failed to read workflow: ${error.message}`], observations: [] };
}
const checks = [
checkWorkflowTriggers(content),
checkWorkflowConcurrency(content),
checkPnpmSetup(content),
checkWorkflowRunners(content),
checkOrderedQualityGates(content),
checkServiceJobs(content),
checkDatabaseJob(content),
checkDockerSmokeJob(content),
checkSentrySourcemapWorkflow(rootDir, content),
checkForbiddenE2eCommands(content),
checkGiteaVariableNames(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 formatGiteaCiWorkflowResult(result) {
const status = result.failures.length > 0 ? "FAIL" : "PASS";
return [
`${status} Gitea CI 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 = verifyGiteaCiWorkflow(process.cwd());
const output = formatGiteaCiWorkflowResult(result);
if (result.failures.length > 0) {
process.stderr.write(output);
process.exitCode = 1;
} else {
process.stdout.write(output);
}
}