chore: initial project import
Some checks failed
CI - Production Readiness / Verify (push) Has been cancelled
Some checks failed
CI - Production Readiness / Verify (push) Has been cancelled
This commit is contained in:
194
scripts/ops-readiness.mjs
Normal file
194
scripts/ops-readiness.mjs
Normal file
@ -0,0 +1,194 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { execSync } from "node:child_process";
|
||||
import { env } from "node:process";
|
||||
|
||||
const requiredVariables = [
|
||||
{ key: "DATABASE_URL", requiredInProduction: true },
|
||||
{ key: "AUTH_SECRET", requiredInProduction: true },
|
||||
{ key: "CAMPAIGN_RETRY_JOB_TOKEN", requiredInProduction: true },
|
||||
{ key: "WHATSAPP_WEBHOOK_VERIFY_TOKEN", requiredInProduction: false },
|
||||
{ key: "WHATSAPP_WEBHOOK_SECRET", requiredInProduction: false },
|
||||
{ key: "NEXT_PUBLIC_APP_URL", requiredInProduction: false },
|
||||
{ key: "APP_URL", requiredInProduction: false },
|
||||
{ key: "OPS_BASE_URL", requiredInProduction: false }
|
||||
];
|
||||
|
||||
const placeholderPatterns = [
|
||||
"change-me",
|
||||
"changeme",
|
||||
"your-",
|
||||
"example",
|
||||
"todo",
|
||||
"placeholder"
|
||||
];
|
||||
|
||||
const isProductionLike = env.NODE_ENV === "production";
|
||||
let hasFailure = false;
|
||||
|
||||
function isPresent(value) {
|
||||
return typeof value === "string" && value.trim().length > 0;
|
||||
}
|
||||
|
||||
function isPlaceholderValue(value) {
|
||||
const lowered = value.toLowerCase();
|
||||
return placeholderPatterns.some((pattern) => lowered.includes(pattern));
|
||||
}
|
||||
|
||||
function validateSecretValue(key, value, expected) {
|
||||
if (!isPresent(value)) {
|
||||
if (expected) {
|
||||
reportIssue("error", `${key} is missing`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (expected && value === expected) {
|
||||
reportIssue("error", `${key} is using production default placeholder`);
|
||||
}
|
||||
|
||||
if (isPlaceholderValue(value)) {
|
||||
reportIssue("warn", `${key} looks like placeholder value`);
|
||||
}
|
||||
}
|
||||
|
||||
function reportIssue(level, message) {
|
||||
if (level === "error") {
|
||||
console.error(`[ERROR] ${message}`);
|
||||
hasFailure = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (level === "warn") {
|
||||
console.warn(`[WARN] ${message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[OK] ${message}`);
|
||||
}
|
||||
|
||||
function checkEnv() {
|
||||
console.log("Checking environment variables...");
|
||||
for (const item of requiredVariables) {
|
||||
const value = env[item.key];
|
||||
const isRequired = item.requiredInProduction && isProductionLike;
|
||||
if (!isPresent(value)) {
|
||||
if (isRequired) {
|
||||
reportIssue("error", `${item.key} is missing`);
|
||||
} else {
|
||||
reportIssue("warn", `${item.key} is missing (optional)`);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (item.key === "AUTH_SECRET") {
|
||||
validateSecretValue(item.key, value, "whatsapp-inbox-dev-secret");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isPlaceholderValue(value)) {
|
||||
reportIssue("warn", `${item.key} has placeholder-like value`);
|
||||
}
|
||||
|
||||
const display = value.replace(/./g, "*").slice(0, 4);
|
||||
reportIssue("ok", `${item.key} set (${display}...)`);
|
||||
}
|
||||
}
|
||||
|
||||
function checkPrismaMigrationStatus() {
|
||||
console.log("Checking Prisma migration status...");
|
||||
try {
|
||||
const output = execSync("npx prisma migrate status --schema prisma/schema.prisma", {
|
||||
encoding: "utf8",
|
||||
stdio: ["pipe", "pipe", "pipe"]
|
||||
});
|
||||
if (/database is up to date/i.test(output)) {
|
||||
reportIssue("ok", "Database schema is up to date");
|
||||
return;
|
||||
}
|
||||
|
||||
if (/No migration found/i.test(output)) {
|
||||
reportIssue("warn", "No migration found for current database");
|
||||
return;
|
||||
}
|
||||
|
||||
reportIssue("warn", `Prisma migrate status output: ${output.trim()}`);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
reportIssue("error", `Prisma migration check failed: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function runHealthCheck() {
|
||||
const baseUrl = (env.OPS_BASE_URL || env.APP_URL || env.NEXT_PUBLIC_APP_URL || "http://localhost:3000").replace(/\/+$/, "");
|
||||
const healthToken = env.HEALTHCHECK_TOKEN?.trim();
|
||||
|
||||
const url = `${baseUrl}/api/health`;
|
||||
const headers = healthToken ? { Authorization: `Bearer ${healthToken}` } : {};
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 12_000);
|
||||
|
||||
try {
|
||||
const response = await fetch(url, { headers, signal: controller.signal });
|
||||
clearTimeout(timeout);
|
||||
if (!response.ok) {
|
||||
reportIssue("error", `Health endpoint returned ${response.status}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = await response.json();
|
||||
if (!payload?.ok) {
|
||||
reportIssue("warn", `Health endpoint warning: ${JSON.stringify(payload)}`);
|
||||
} else {
|
||||
reportIssue("ok", `Health endpoint OK: ${payload.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
clearTimeout(timeout);
|
||||
reportIssue("error", `Health endpoint unreachable: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function runRetryEndpointCheck() {
|
||||
const baseUrl = (env.OPS_BASE_URL || env.APP_URL || env.NEXT_PUBLIC_APP_URL || "http://localhost:3000").replace(/\/+$/, "");
|
||||
const token = env.CAMPAIGN_RETRY_JOB_TOKEN?.trim();
|
||||
if (!token) {
|
||||
reportIssue("warn", "CAMPAIGN_RETRY_JOB_TOKEN missing; campaign job endpoint health check skipped");
|
||||
return;
|
||||
}
|
||||
|
||||
const url = `${baseUrl}/api/jobs/campaign-retry?token=${encodeURIComponent(token)}`;
|
||||
const headers = { Authorization: `Bearer ${token}` };
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 12_000);
|
||||
|
||||
try {
|
||||
const response = await fetch(url, { headers, signal: controller.signal });
|
||||
clearTimeout(timeout);
|
||||
if (!response.ok) {
|
||||
reportIssue("warn", `Campaign retry endpoint returned ${response.status}`);
|
||||
return;
|
||||
}
|
||||
const payload = await response.json();
|
||||
reportIssue("ok", `Campaign retry endpoint reachable: ${payload.ok ? "ok" : "down"}`);
|
||||
} catch (error) {
|
||||
clearTimeout(timeout);
|
||||
reportIssue("warn", `Campaign retry endpoint error: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
checkEnv();
|
||||
checkPrismaMigrationStatus();
|
||||
await runHealthCheck();
|
||||
await runRetryEndpointCheck();
|
||||
|
||||
if (hasFailure) {
|
||||
reportIssue("error", "Readiness check failed.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
reportIssue("ok", "Readiness check completed.");
|
||||
}
|
||||
|
||||
await main();
|
||||
Reference in New Issue
Block a user