refactor(ops): standardize image-based production delivery
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import process from "node:process";
|
||||
|
||||
const rootDir = process.cwd();
|
||||
|
||||
const rules = [
|
||||
{
|
||||
file: "packages/api/src/sse/event-bus.ts",
|
||||
required: [],
|
||||
forbidden: [
|
||||
{ pattern: /\bRoleSseAudience\b/, message: "role-based SSE audience types must not reappear" },
|
||||
{ pattern: /\broleAudience\s*\(/, message: "role-derived SSE audiences must not be emitted" },
|
||||
{ pattern: /\bBROADCAST_SENT\b/, message: "broadcast SSE event resurrection needs explicit architecture review" },
|
||||
],
|
||||
},
|
||||
{
|
||||
file: "packages/api/src/sse/subscription-policy.ts",
|
||||
required: [
|
||||
{
|
||||
pattern: /\bderiveUserSseSubscription\b/,
|
||||
message: "subscription derivation must stay centralized in deriveUserSseSubscription",
|
||||
},
|
||||
],
|
||||
forbidden: [
|
||||
{ pattern: /\broleAudience\s*\(/, message: "subscription policy must not derive role audiences" },
|
||||
],
|
||||
},
|
||||
{
|
||||
file: "apps/web/src/app/api/sse/timeline/route.ts",
|
||||
required: [
|
||||
{
|
||||
pattern: /\bderiveUserSseSubscription\s*\(/,
|
||||
message: "timeline SSE route must derive audiences server-side from the authenticated user",
|
||||
},
|
||||
],
|
||||
forbidden: [
|
||||
{ pattern: /\bsearchParams\b/, message: "timeline SSE route must not accept client-provided audience scoping" },
|
||||
{ pattern: /\baudience\b/, message: "timeline SSE route must not parse raw audience values from the client" },
|
||||
],
|
||||
},
|
||||
{
|
||||
file: "docker-compose.prod.yml",
|
||||
required: [
|
||||
{
|
||||
pattern: /image:\s+\$\{APP_IMAGE:\?set APP_IMAGE\}/,
|
||||
message: "production compose must deploy the immutable app image",
|
||||
},
|
||||
{
|
||||
pattern: /image:\s+\$\{MIGRATOR_IMAGE:\?set MIGRATOR_IMAGE\}/,
|
||||
message: "production compose must deploy the immutable migrator image",
|
||||
},
|
||||
{
|
||||
pattern: /http:\/\/localhost:3000\/api\/ready/,
|
||||
message: "production compose must gate app health on the readiness endpoint",
|
||||
},
|
||||
{
|
||||
pattern: /RATE_LIMIT_BACKEND:\s+\$\{RATE_LIMIT_BACKEND:-redis\}/,
|
||||
message: "production compose must intentionally pin the Redis-backed rate-limit path",
|
||||
},
|
||||
],
|
||||
forbidden: [
|
||||
{ pattern: /\bbuild:/, message: "production compose must not build application images on the host" },
|
||||
],
|
||||
},
|
||||
{
|
||||
file: ".github/workflows/release-image.yml",
|
||||
required: [
|
||||
{
|
||||
pattern: /push:\s*\n\s*branches:\s*\[main\]/,
|
||||
message: "image releases must build automatically on pushes to main",
|
||||
},
|
||||
{
|
||||
pattern: /workflow_dispatch:/,
|
||||
message: "image release must remain manually callable for rebuilds and tag overrides",
|
||||
},
|
||||
{
|
||||
pattern: /target:\s+runner/,
|
||||
message: "release workflow must keep publishing the runner image",
|
||||
},
|
||||
{
|
||||
pattern: /target:\s+migrator/,
|
||||
message: "release workflow must keep publishing the migrator image",
|
||||
},
|
||||
],
|
||||
forbidden: [],
|
||||
},
|
||||
{
|
||||
file: ".github/workflows/deploy-staging.yml",
|
||||
required: [
|
||||
{
|
||||
pattern: /docker-compose\.prod\.yml tooling\/deploy/,
|
||||
message: "staging deploy must ship the canonical production compose bundle",
|
||||
},
|
||||
],
|
||||
forbidden: [],
|
||||
},
|
||||
{
|
||||
file: ".github/workflows/deploy-prod.yml",
|
||||
required: [
|
||||
{
|
||||
pattern: /docker-compose\.prod\.yml tooling\/deploy/,
|
||||
message: "production deploy must ship the canonical production compose bundle",
|
||||
},
|
||||
],
|
||||
forbidden: [],
|
||||
},
|
||||
{
|
||||
file: "tooling/deploy/deploy-compose.sh",
|
||||
required: [
|
||||
{
|
||||
pattern: /COMPOSE_FILE="\$\{COMPOSE_FILE:-docker-compose\.prod\.yml\}"/,
|
||||
message: "deploy script must default to the canonical production compose file",
|
||||
},
|
||||
{
|
||||
pattern: /READY_URL="\$\{READY_URL:-http:\/\/127\.0\.0\.1:\$\{APP_HOST_PORT:-3000\}\/api\/ready\}"/,
|
||||
message: "deploy script must wait on the readiness endpoint",
|
||||
},
|
||||
{
|
||||
pattern: /docker compose -f "\$\{COMPOSE_FILE\}" config -q/,
|
||||
message: "deploy script must validate the rendered compose file before pulling images",
|
||||
},
|
||||
],
|
||||
forbidden: [],
|
||||
},
|
||||
];
|
||||
|
||||
const violations = [];
|
||||
|
||||
for (const rule of rules) {
|
||||
const absolutePath = path.join(rootDir, rule.file);
|
||||
const source = await readFile(absolutePath, "utf8");
|
||||
|
||||
for (const requirement of rule.required) {
|
||||
if (!requirement.pattern.test(source)) {
|
||||
violations.push(`${rule.file}: missing guardrail anchor: ${requirement.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const forbidden of rule.forbidden) {
|
||||
if (forbidden.pattern.test(source)) {
|
||||
violations.push(`${rule.file}: forbidden pattern matched: ${forbidden.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (violations.length > 0) {
|
||||
console.error("Architecture guardrail check failed:");
|
||||
for (const violation of violations) {
|
||||
console.error(`- ${violation}`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log("Architecture guardrails passed.");
|
||||
Reference in New Issue
Block a user