import { readFileSync, readdirSync } from "node:fs"; const directory = new URL("../deploy/ack/", import.meta.url); const repository = new URL("../", import.meta.url); const files = readdirSync(directory).filter((name) => name.endsWith(".yaml")).sort(); assert(!files.includes("migration-job.yaml"), "ACK must not ship the removed Node migration Job"); assert(!files.includes("worker.yaml"), "ACK must not ship the removed Node worker Deployment"); assert(files.includes("namespace.yaml"), "ACK must include the Namespace manifest required by first deployment"); for (const file of files) { const text = readFileSync(new URL(file, directory), "utf8"); assert(text.includes("apiVersion:"), `${file}: missing apiVersion`); assert(text.includes("kind:"), `${file}: missing kind`); assert(!text.includes("server-snippet"), `${file}: must not depend on disabled snippet annotations`); } const web = read("web.yaml"); assert(/^\s*replicas: 1\s*$/m.test(web), "Web must default to one replica for the first deployment"); assert(web.includes("path: /healthz"), "Web must probe the static Nginx health endpoint"); assert(!/^\s*env(?:From)?:\s*$/m.test(web), "Web must not receive runtime environment configuration"); assert(!web.includes("secretKeyRef:"), "Web must not receive application secrets"); assert(web.includes("runAsNonRoot: true"), "Web must run as a non-root user"); assert(web.includes("runAsUser: 101"), "Web must run as the unprivileged Nginx user"); assert(web.includes("fsGroup: 101"), "Web temporary volume must be writable by unprivileged Nginx"); assert(web.includes("readOnlyRootFilesystem: true"), "Web root filesystem must be read-only"); assert(!web.includes("ZHINIAN_DATA_BACKEND"), "Web must not select a database backend in production"); assert(!web.includes("DATABASE_URL"), "Web must not receive DATABASE_URL in production"); assert(!web.includes("zhinian-web-db"), "Web must not hold RDS credentials in production"); assert(!web.includes("rds-ca"), "Web must not mount the RDS CA in production"); assert(!web.includes("ZHINIAN_AUTH_SESSION_SECRET"), "Web must not receive the Go session signing secret"); assert(!web.includes("ZHINIAN_GO_INTERNAL_BASE_URL"), "Web must not receive an internal Go URL"); const goApi = read("go-api.yaml"); assert(/^\s*replicas: 1\s*$/m.test(goApi), "Go API must default to one replica until object storage is shared"); assert(goApi.includes("path: /api/ready"), "Go API must use database-aware readiness"); assert(goApi.includes("runAsNonRoot: true"), "Go API must run as a non-root user"); assert(goApi.includes("name: zhinian-go-runtime"), "Go API must consume the Go runtime ConfigMap"); assert(goApi.includes("name: zhinian-go-auth"), "Go API must own the browser session signing Secret"); assert(!goApi.includes("name: zhinian-web-auth"), "Go API must not reference the removed Web auth Secret"); assert(goApi.includes("name: zhinian-go-bootstrap"), "Go API must receive bootstrap administrator credentials"); assert(!goApi.includes("rds-ca"), "Go API must not mount an RDS CA when PostgreSQL TLS is disabled"); assert(!goApi.includes("/etc/zhinian/rds"), "Go API must not retain the removed RDS CA path"); assert(!/\bTLS\b/i.test(goApi), "Go API manifest must not retain PostgreSQL TLS configuration"); assert(!goApi.includes("DATABASE_SSL_MODE"), "Go API must not receive a separate database SSL mode"); assert(!goApi.includes("DATABASE_CA_CERT_PATH"), "Go API must not receive a database CA path"); const secrets = read("secrets.example.yaml"); assert(secrets.includes("name: zhinian-go-auth"), "Example secrets must name Go as the session Secret owner"); assert(!secrets.includes("name: zhinian-web-auth"), "Example secrets must not retain the removed Web auth Secret"); assert(!secrets.includes("zhinian-rds-ca"), "Example secrets must not define the removed RDS CA Secret"); assert(!secrets.includes("sslrootcert"), "Example DATABASE_URL values must not reference an RDS CA"); assert(!secrets.includes("verify-full"), "Example DATABASE_URL values must not request TLS"); assert(!/\bTLS\b/i.test(secrets), "Example secrets must not retain PostgreSQL TLS configuration"); assert((secrets.match(/sslmode=disable/g) ?? []).length === 2, "Migration and Go DATABASE_URL examples must disable TLS"); const namespace = read("namespace.yaml"); assert(namespace.includes("kind: Namespace"), "namespace.yaml must define a Namespace"); assert(namespace.includes("name: zhinian"), "namespace.yaml must create the zhinian Namespace"); const configMap = read("configmap.yaml"); assert(!configMap.includes("name: zhinian-runtime"), "Static Web must not have a runtime ConfigMap"); assert((configMap.match(/^kind: ConfigMap$/gm) ?? []).length === 1, "Only the Go runtime ConfigMap should remain"); assert(configMap.includes("ZHINIAN_GO_EMBEDDED_WORKER: \"true\""), "Go runtime ConfigMap must embed the WorkerLoop"); assert(configMap.includes("GO_BACKEND_HOST: 0.0.0.0"), "Go runtime ConfigMap must listen on the Pod interface"); assert(!configMap.includes("DATABASE_SSL_MODE"), "ConfigMaps must not carry database TLS settings"); assert(!configMap.includes("DATABASE_CA_CERT_PATH"), "ConfigMaps must not carry database CA paths"); const ingress = read("ingress.yaml"); assert(ingress.includes("path: /api/internal/worker"), "Ingress must intercept the internal worker prefix"); assert(ingress.includes("name: zhinian-public-deny"), "Ingress must route the internal prefix away from the workloads"); assert(ingress.includes("name: zhinian-go-api"), "Ingress must route backend paths to the Go API Service"); assert(ingress.includes("path: /uploads"), "Ingress must route /uploads to the Go API"); assert(ingress.includes("path: /generated-results"), "Ingress must route /generated-results to the Go API"); assert(ingress.includes("name: zhinian-web"), "Ingress must route pages/static paths to Web"); const service = read("service.yaml"); assert(service.includes("name: zhinian-public-deny"), "selectorless deny Service is required"); const goService = goApi.match( /kind: Service\s+metadata:\s+name:\s+(\S+)[\s\S]*?ports:\s+- name: http\s+port:\s+(\d+)\s+targetPort:\s+(\d+)/, ); assert(goService, "Go API Service must declare an HTTP name, port, and targetPort"); const [, serviceName, servicePort, targetPort] = goService; assert(serviceName === "zhinian-go-api", "Go API Service name must match the Ingress backend"); assert(targetPort === servicePort, "Go API Service targetPort must match its port"); const nextConfig = readRepository("next.config.ts"); assert(nextConfig.includes('output: "export"'), "Next must emit a static export"); assert(nextConfig.includes("unoptimized: true"), "Static export must not depend on the Next image optimizer"); assert(!nextConfig.includes("serverActions"), "Static frontend must not enable Server Actions"); const dockerfile = readRepository("Dockerfile"); assert(dockerfile.includes("nginxinc/nginx-unprivileged:"), "Web runner must use unprivileged Nginx"); assert(dockerfile.includes("/app/out"), "Web runner must copy the static export"); assert(dockerfile.includes("ENTRYPOINT []"), "Web runner must skip image entrypoint mutations on a read-only root filesystem"); assert(!dockerfile.includes("next start"), "Production Web must not start a Next server"); assert(!dockerfile.includes("/app/.next"), "Production Web must not copy Next server artifacts"); const dockerIgnore = readRepository(".dockerignore"); assert(dockerIgnore.includes("!deploy/nginx.conf"), "Docker build context must include the Nginx runtime config"); const nginx = readRepository("deploy/nginx.conf"); assert(nginx.includes("listen 3000;"), "Static Nginx must preserve the Web service port"); assert(nginx.includes("location = /healthz"), "Static Nginx must expose /healthz"); assert(nginx.includes("api|uploads|generated-results"), "Static Nginx must reject Go-owned paths"); assert(nginx.includes("immutable"), "Hashed Next assets must be cached immutably"); assert(nginx.includes("no-cache, no-store, must-revalidate"), "HTML navigation must not be cached"); const packageJson = JSON.parse(readRepository("package.json")); assert(!packageJson.scripts.start, "package scripts must not advertise a Next production server"); assert(!packageJson.scripts["start:server"], "package scripts must not advertise a public Next production server"); assert(!packageJson.scripts.worker, "package scripts must not advertise the removed Node worker"); assert(!packageJson.scripts["worker:once"], "package scripts must not advertise the removed one-shot Node worker"); assert(!packageJson.scripts.health, "package scripts must not probe the removed Next API server"); const deploymentDocs = readRepository("docs/DEPLOYMENT.md"); assert( deploymentDocs.includes( "kubectl apply -f deploy/ack/namespace.yaml\n\n" + "kubectl apply --dry-run=server -f deploy/ack/secrets.production.yaml\n" + "kubectl apply --dry-run=server -f deploy/ack/configmap.yaml", ), "Deployment docs must create the Namespace before dry-running namespaced Secrets and resources", ); assert( deploymentDocs.includes( "kubectl apply -f deploy/ack/secrets.production.yaml\n" + "kubectl apply -f deploy/ack/configmap.yaml", ), "Deployment docs must publish Secrets before other namespaced resources", ); assert( deploymentDocs.includes("当前 ACK 清单不注入 `ZHINIAN_BOOTSTRAP_ADMIN_NAME`"), "Deployment docs must not claim the optional bootstrap administrator name is injected", ); assert( deploymentDocs.includes("Pod 重建或滚动升级同样会永久丢失上传文件和生成结果"), "Deployment docs must disclose emptyDir data loss across Go Pod replacement", ); assert(deploymentDocs.includes("不使用 TLS"), "Deployment docs must disclose plaintext PostgreSQL transport"); assert(!deploymentDocs.includes("sslrootcert"), "Deployment docs must not instruct operators to mount an RDS CA"); assert(!deploymentDocs.includes("verify-full"), "Deployment docs must not instruct operators to enable PostgreSQL TLS"); const gitIgnore = readRepository(".gitignore"); assert( gitIgnore.split("\n").includes("/deploy/ack/secrets.production.yaml"), "The populated production Secret manifest must be ignored with an exact repository-root rule", ); console.log(`ACK manifest assertions passed (${files.length} files)`); function read(file) { return readFileSync(new URL(file, directory), "utf8").replace(/\r\n/g, "\n"); } function readRepository(file) { return readFileSync(new URL(file, repository), "utf8").replace(/\r\n/g, "\n"); } function assert(condition, message) { if (!condition) throw new Error(message); }