Files
buzz-sheet/tests/deployment-contract.test.ts
T
gunshiz be2648cab3
CI / Verify (push) Successful in 2m4s
CI / Build immutable images and deploy (push) Successful in 3m51s
fix(deploy) : roll out web replicas without surge capacity
2026-10-09 19:20:23 +07:00

251 lines
11 KiB
TypeScript

import { readFile } from "node:fs/promises";
import { describe, expect, it } from "vitest";
async function repositoryFile(path: string): Promise<string> {
return readFile(new URL(`../${path}`, import.meta.url), "utf8");
}
describe("production deployment contract", () => {
it("defines a resilient two-replica web workload", async () => {
const deployment = await repositoryFile("k8s/base/deployment.yaml");
expect(deployment).toContain("replicas: 2");
expect(deployment).toContain("maxSurge: 0");
expect(deployment).toContain("maxUnavailable: 1");
expect(deployment.match(/path: \/api\/healthz\n/gu)).toHaveLength(3);
expect(deployment).toContain("kubernetes.io/arch: arm64");
expect(deployment).toMatch(
/requests:\s+cpu: 500m\s+memory: 1Gi\s+limits:\s+cpu: "1"\s+memory: 2Gi/u,
);
expect(deployment).toContain("runAsNonRoot: true");
expect(deployment).toContain("runAsUser: 1000");
expect(deployment).toContain("runAsGroup: 1000");
expect(deployment).toContain("readOnlyRootFilesystem: true");
expect(deployment).toContain('drop: ["ALL"]');
});
it("exposes only the app through the requested edge-TLS host", async () => {
const [service, ingress] = await Promise.all([
repositoryFile("k8s/base/service.yaml"),
repositoryFile("k8s/base/ingress.yaml"),
]);
expect(service).toContain("type: ClusterIP");
expect(service).toContain("port: 3000");
expect(service).toContain("targetPort: http");
expect(ingress).toContain("ingressClassName: traefik");
expect(ingress).toContain("host: guide.sudloh.com");
expect(ingress).toContain(
'traefik.ingress.kubernetes.io/read-timeout: "200"',
);
expect(ingress).not.toContain("secretName:");
expect(ingress).not.toContain("router.tls");
});
it("keeps availability and scaling bounds explicit", async () => {
const [hpa, pdb] = await Promise.all([
repositoryFile("k8s/base/hpa.yaml"),
repositoryFile("k8s/base/pdb.yaml"),
]);
expect(hpa).toContain("minReplicas: 2");
expect(hpa).toContain("maxReplicas: 6");
expect(hpa).toContain("averageUtilization: 70");
expect(hpa).toContain("name: memory");
expect(hpa).toContain("averageUtilization: 75");
expect(pdb).toContain("minAvailable: 1");
});
it("uses externally supplied secrets and does not provision data stores", async () => {
const manifestPaths = [
"k8s/base/namespace.yaml",
"k8s/base/configmap.yaml",
"k8s/base/deployment.yaml",
"k8s/base/worker-deployment.yaml",
"k8s/base/discord-worker-deployment.yaml",
"k8s/base/service.yaml",
"k8s/base/ingress.yaml",
"k8s/base/hpa.yaml",
"k8s/base/pdb.yaml",
"k8s/base/ci-rbac.yaml",
"k8s/base/network-policy.yaml",
"k8s/migration/job.yaml",
];
const manifests = (
await Promise.all(manifestPaths.map(repositoryFile))
).join("\n---\n");
expect(manifests).toContain("name: buzz-sheet-env");
expect(manifests).not.toMatch(/kind: (Secret|StatefulSet|PersistentVolumeClaim)/u);
expect(manifests).not.toContain("resources: [\"secrets\"]");
expect(manifests).not.toContain("kind: ClusterRole");
expect(manifests).not.toContain("kind: ClusterRoleBinding");
expect(manifests).toContain("kind: NetworkPolicy");
expect(manifests).toContain("key: DATABASE_URL");
expect(manifests).not.toContain("secretRef:");
});
it("packages non-root Bun application and migration targets", async () => {
const dockerfile = await repositoryFile("Dockerfile");
expect(dockerfile).toContain("FROM ${BUN_IMAGE} AS migration");
expect(dockerfile).toContain("FROM ${BUN_IMAGE} AS app");
expect(dockerfile).toMatch(/ARG BUN_IMAGE=oven\/bun:1\.3\.14-alpine@sha256:[a-f0-9]{64}/u);
expect(dockerfile.match(/^USER 1000:1000$/gmu)).toHaveLength(2);
expect(dockerfile).toContain('ENTRYPOINT ["bun", "migrate.js"]');
expect(dockerfile).toContain('CMD ["bun", "server.js"]');
expect(dockerfile).toContain("ARG BASE_URL");
expect(dockerfile).toContain("ENV BASE_URL=${BASE_URL}");
expect(dockerfile).toContain("ARG NEXT_PUBLIC_GOOGLE_ADSENSE_CLIENT_ID");
expect(dockerfile).toContain(
"ENV NEXT_PUBLIC_GOOGLE_ADSENSE_CLIENT_ID=${NEXT_PUBLIC_GOOGLE_ADSENSE_CLIENT_ID}",
);
expect(dockerfile).toContain("backend/discord.ts");
expect(dockerfile).toMatch(
/bun build scripts\/outbox-worker\.ts[\s\S]*?--conditions=react-server[\s\S]*?--outfile=dist\/outbox-worker\.js/u,
);
expect(dockerfile).toMatch(
/bun build backend\/discord\.ts[\s\S]*?--conditions=react-server[\s\S]*?--outfile=dist\/discord\.js/u,
);
expect(dockerfile).toContain("./worker/discord.js");
});
it("runs the outbox worker with server-only module resolution locally", async () => {
const packageJson = JSON.parse(await repositoryFile("package.json"));
expect(packageJson.scripts["worker:outbox"]).toBe(
"bun --conditions=react-server scripts/outbox-worker.ts",
);
});
it("verifies first, publishes immutable images, and migrates before rollout", async () => {
const workflow = await repositoryFile(".gitea/workflows/ci.yml");
expect(workflow).toContain("bun install --frozen-lockfile");
expect(workflow).toContain("bun run test");
expect(workflow).toContain("bun run typecheck");
expect(workflow).toContain("bun run lint");
expect(workflow).toContain("kubectl kustomize k8s/");
expect(workflow).toContain("needs: verify");
expect(workflow).not.toContain("pull_request:");
expect(workflow).toContain("--insecure-skip-tls-verify=true");
expect(workflow).toContain("--target app");
expect(workflow).toContain('--build-arg BASE_URL="$BASE_URL"');
expect(workflow).toContain(
'--build-arg NEXT_PUBLIC_GOOGLE_ADSENSE_CLIENT_ID="$NEXT_PUBLIC_GOOGLE_ADSENSE_CLIENT_ID"',
);
expect(workflow).toContain('\\"BASE_URL\\":\\"$BASE_URL\\"');
expect(workflow).toContain("--target migration");
expect(workflow).not.toContain("platforms: linux/arm64");
expect(workflow).not.toMatch(/docker\/setup-qemu-action|docker\/setup-buildx-action|docker\/login-action/iu);
expect(workflow).toContain(
"registry.neko-piranha.ts.net/astral/buzz-sheet",
);
expect(workflow).toContain("migrate-$REVISION");
expect(workflow).toContain("deployment/buzz-sheet-discord-worker");
expect(workflow).toContain('"maxSurge":0,"maxUnavailable":1');
const patchRolloutStrategy = workflow.indexOf("patch deployment buzz-sheet");
expect(patchRolloutStrategy).toBeGreaterThan(-1);
expect(patchRolloutStrategy).toBeLessThan(workflow.indexOf("set image"));
const deleteMigrationJob = workflow.indexOf(
'delete job buzz-sheet-migrate --ignore-not-found',
);
const createMigrationJob = workflow.indexOf(
'create --validate=false -f "$migration_manifest"',
);
expect(deleteMigrationJob).toBeGreaterThan(-1);
expect(deleteMigrationJob).toBeLessThan(createMigrationJob);
expect(workflow.indexOf("condition=complete")).toBeLessThan(
workflow.indexOf("set image"),
);
expect(workflow).not.toMatch(/playwright|chromium/iu);
});
it("mounts the deployment stream and offers a full reload for new releases", async () => {
const [layout, notifier, route] = await Promise.all([
repositoryFile("app/layout.tsx"),
repositoryFile("components/deployment-update-notifier.tsx"),
repositoryFile("app/api/active/route.ts"),
]);
expect(layout).toContain("<DeploymentUpdateNotifier");
expect(notifier).toContain('new EventSource("/api/active")');
expect(notifier).toContain("window.location.reload()");
expect(notifier).toContain('label: "รีโหลด"');
expect(notifier).toContain('source.addEventListener("deployment-status"');
expect(route).toContain('"deployment-status"');
});
it("announces incoming deployments without blocking releases", async () => {
const [workflow, deployment] = await Promise.all([
repositoryFile(".gitea/workflows/ci.yml"),
repositoryFile("k8s/base/deployment.yaml"),
]);
expect(workflow).toContain("Announce incoming deployment");
expect(workflow).toContain("continue-on-error: true");
expect(workflow).toContain('status\\":\\"deploying');
expect(workflow).toContain("/api/deployments/status");
expect(deployment).toContain("DEPLOYMENT_WEBHOOK_SECRET");
expect(deployment).toContain("optional: true");
});
});
describe("environment template contract", () => {
it("documents every externally supplied production secret", async () => {
const environmentExample = await repositoryFile(".env.example");
const requiredKeys = [
"DATABASE_URL",
"BETTER_AUTH_URL",
"BETTER_AUTH_SECRET",
"REDIS_URL",
"S3_ENDPOINT",
"S3_BUCKET",
"S3_ACCESS_KEY_ID",
"S3_SECRET_ACCESS_KEY",
"NEXT_SERVER_ACTIONS_ENCRYPTION_KEY",
"NEXT_DEPLOYMENT_ID",
"DEPLOYMENT_WEBHOOK_SECRET",
"DISCORD_BOT_TOKEN",
"DISCORD_CHANNEL_ID",
"DISCORD_LOG_CHANNEL_ID",
];
for (const key of requiredKeys) {
expect(environmentExample).toMatch(new RegExp(`^${key}=`, "mu"));
}
expect(environmentExample).not.toContain("BUZZ_DEMO_MODE");
expect(environmentExample).toContain(
"NEXT_PUBLIC_GOOGLE_ADSENSE_CLIENT_ID=ca-pub-9687404323559597",
);
});
it("runs Discord catalog sync as an isolated worker", async () => {
const [deployment, rbac] = await Promise.all([
repositoryFile("k8s/base/discord-worker-deployment.yaml"),
repositoryFile("k8s/base/ci-rbac.yaml"),
]);
expect(deployment).toContain("name: buzz-sheet-discord-worker");
expect(deployment).toContain("type: Recreate");
expect(deployment).toContain('command: ["bun", "worker/discord.js"]');
expect(deployment).toContain("replicas: 1");
expect(deployment).toContain("readOnlyRootFilesystem: true");
expect(deployment).toContain('drop: ["ALL"]');
expect(rbac).toContain("buzz-sheet-discord-worker");
});
it("checks Lunaris for every new message in the configured Discord channel", async () => {
const worker = await repositoryFile("backend/discord.ts");
expect(worker).toContain("client.on(Events.MessageCreate, handleMessage)");
expect(worker).toContain("if (message.channelId !== channelId) return;");
expect(worker).not.toContain("message.author.id === client.user?.id");
expect(worker).not.toContain("parseLunarisVersionChange");
expect(worker).toContain("const details = { version, attempt: attempt + 1, messageUrl };");
expect(worker).not.toContain("channel.messages.fetch");
expect(worker).not.toContain('source: "history"');
});
});