Files
pikasTech-HWLAB/scripts/g14-gitops-render.test.ts
T
2026-05-30 12:31:07 +08:00

245 lines
17 KiB
TypeScript

import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import test from "node:test";
function taskByName(pipeline, name) {
const task = pipeline.spec.tasks.find((item) => item.name === name);
assert.ok(task, `missing task ${name}`);
return task;
}
test("v02 render follows TypeScript runtime checks and does not self-patch bootstrap RBAC", async () => {
const outDir = await mkdtemp(path.join(os.tmpdir(), "hwlab-v02-render-test-"));
try {
const head = spawnSync("git", ["rev-parse", "HEAD"], { cwd: process.cwd(), encoding: "utf8" });
assert.equal(head.status, 0, head.stderr || head.stdout);
const sourceRevision = head.stdout.trim();
const staleRuntimeFile = path.join(outDir, "runtime-v02", "stale-gateway-simu.yaml");
const staleTektonFile = path.join(outDir, "tekton-v02", "stale-router.yaml");
await mkdir(path.dirname(staleRuntimeFile), { recursive: true });
await mkdir(path.dirname(staleTektonFile), { recursive: true });
await writeFile(staleRuntimeFile, "kind: Service\nmetadata:\n name: hwlab-gateway-simu\n");
await writeFile(staleTektonFile, "hwlab-router\n");
const render = spawnSync(process.execPath, [
"scripts/g14-gitops-render.mjs",
"--lane", "v02",
"--catalog-path", "deploy/artifact-catalog.v02.json",
"--image-tag-mode", "full",
"--source-branch", "v0.2",
"--gitops-branch", "v0.2-gitops",
"--out", outDir,
"--source-revision", "HEAD"
], { cwd: process.cwd(), encoding: "utf8" });
assert.equal(render.status, 0, render.stderr || render.stdout);
await assert.rejects(() => readFile(staleRuntimeFile, "utf8"), { code: "ENOENT" });
await assert.rejects(() => readFile(staleTektonFile, "utf8"), { code: "ENOENT" });
const pipeline = await readFile(path.join(outDir, "tekton-v02", "pipeline.yaml"), "utf8");
const pipelineJson = JSON.parse(pipeline);
assert.match(pipeline, /127\.0\.0\.1:5000\/hwlab\/hwlab-ci-node-tools:node22-alpine-bun-v1/u);
assert.match(pipeline, /tar -C \/workspace\/source\/repo -cf - \. \| tar -C \/workspace\/service-work\/repo -xo -f -/u);
assert.match(pipeline, /node scripts\/run-bun\.mjs test cmd\/hwlab-codex-api-responses-forwarder\/main\.test\.ts/u);
assert.doesNotMatch(pipeline, /cmd\/hwlab-codex-api-responses-forwarder\/main\.mjs/u);
assert.match(pipeline, /process\.exitCode = 1/u);
assert.match(pipeline, /item\.metadata\?\.labels\?\.\[\\"hwlab\.pikastech\.local\/source-commit\\"\]/u);
const runtimeReady = taskByName(pipelineJson, "runtime-ready");
assert.deepEqual(runtimeReady.when, [{ input: "$(tasks.gitops-promote.results.runtime-ready-required)", operator: "in", values: ["true"] }]);
const runtimeReadyScript = runtimeReady.taskSpec.steps[0].script;
assert.match(runtimeReadyScript, /const pollMs = 1000;/u);
assert.match(runtimeReadyScript, /status: "timeout", reason: "source-commit-refresh-timeout"/u);
assert.match(runtimeReadyScript, /const refresh = await waitForArgoRefresh\(\);\n if \(!refresh\.observed\) return;/u);
const gitopsPromote = taskByName(pipelineJson, "gitops-promote");
assert.ok(gitopsPromote.taskSpec.results.some((result) => result.name === "runtime-ready-required"));
const gitopsPromoteScript = gitopsPromote.taskSpec.steps[0].script;
assert.match(gitopsPromoteScript, /printf 'true' > \/tekton\/results\/runtime-ready-required/u);
assert.match(gitopsPromoteScript, /runtime-identity-only/u);
assert.match(gitopsPromoteScript, /skipped-runtime-unchanged/u);
assert.match(gitopsPromoteScript, /printf 'false' > \/tekton\/results\/runtime-ready-required/u);
assert.match(gitopsPromoteScript, /argo_hard_refresh_v02\(\) \{/u);
assert.match(gitopsPromoteScript, /argocd\.argoproj\.io\/refresh": "hard"/u);
assert.match(gitopsPromoteScript, /applications\/" \+ encodeURIComponent\(application\) \+ "\?fieldManager=hwlab-v02-gitops-promote/u);
assert.match(gitopsPromoteScript, /BatchMode=yes/u);
assert.match(gitopsPromoteScript, /ConnectTimeout=10/u);
assert.match(gitopsPromoteScript, /git_timed "source-head-\$phase" 45 git ls-remote/u);
assert.match(gitopsPromoteScript, /"event":"git-ssh-setup","phase":"gitops-promote","status":"skipped"/u);
assert.match(gitopsPromoteScript, /source_head_url="\$\(params\.git-read-url\)"/u);
assert.match(gitopsPromoteScript, /gitops_write_url="\$\(params\.git-write-url\)"/u);
assert.match(gitopsPromoteScript, /git clone --no-checkout "\$gitops_read_url"/u);
assert.match(gitopsPromoteScript, /git_timed gitops-push 120 git push origin/u);
assert.doesNotMatch(gitopsPromoteScript, /gitops-promote-proxy-preflight/u);
assert.doesNotMatch(gitopsPromoteScript, /for tool in node npm git ssh curl timeout/u);
const prepareSource = taskByName(pipelineJson, "prepare-source");
assert.ok(prepareSource.taskSpec.params.some((param) => param.name === "git-read-url"));
assert.ok(prepareSource.params.some((param) => param.name === "git-read-url" && param.value === "$(params.git-read-url)"));
const prepareSourceScript = prepareSource.taskSpec.steps[0].script;
assert.match(prepareSourceScript, /git_read_url="\$\(params\.git-read-url\)"/u);
assert.match(prepareSourceScript, /"event":"git-ssh-setup","phase":"prepare-source","status":"skipped"/u);
assert.match(prepareSourceScript, /git_timed source-clone 180 git clone --branch "\$\(params\.source-branch\)" "\$git_read_url"/u);
assert.match(prepareSourceScript, /git_timed catalog-ls-remote 45 git ls-remote --exit-code --heads "\$git_read_url"/u);
assert.match(prepareSourceScript, /git remote set-url gitops-catalog "\$git_read_url"/u);
assert.match(prepareSourceScript, /prepare-source-dependencies/u);
assert.doesNotMatch(prepareSourceScript, /npm ci --ignore-scripts/u);
assert.doesNotMatch(prepareSourceScript, /prepare-source-pre-npm/u);
assert.doesNotMatch(prepareSourceScript, /prepare-source-proxy-preflight/u);
assert.doesNotMatch(prepareSourceScript, /dependency-curl-probe-start/u);
const planArtifacts = taskByName(pipelineJson, "plan-artifacts");
assert.doesNotMatch(planArtifacts.taskSpec.steps[0].script, /plan-artifacts-proxy-preflight/u);
const collectArtifacts = taskByName(pipelineJson, "collect-artifacts");
assert.doesNotMatch(collectArtifacts.taskSpec.steps[0].script, /collect-artifacts-proxy-preflight/u);
const pipelineGitReadParam = pipelineJson.spec.params.find((param) => param.name === "git-read-url");
assert.equal(pipelineGitReadParam?.default, "http://git-mirror-http.devops-infra.svc.cluster.local/pikasTech/HWLAB.git");
const pipelineGitWriteParam = pipelineJson.spec.params.find((param) => param.name === "git-write-url");
assert.equal(pipelineGitWriteParam?.default, "http://git-mirror-write.devops-infra.svc.cluster.local/pikasTech/HWLAB.git");
const sourceDescriptor = JSON.parse(await readFile(path.join(outDir, "source.json"), "utf8"));
assert.equal(sourceDescriptor.gitReadUrl, "http://git-mirror-http.devops-infra.svc.cluster.local/pikasTech/HWLAB.git");
assert.equal(sourceDescriptor.gitWriteUrl, "http://git-mirror-write.devops-infra.svc.cluster.local/pikasTech/HWLAB.git");
const gitMirror = await readFile(path.join(outDir, "devops-infra", "git-mirror.yaml"), "utf8");
assert.match(gitMirror, /"name": "devops-infra"/u);
assert.match(gitMirror, /"name": "git-mirror-http"/u);
assert.match(gitMirror, /"name": "git-mirror-write"/u);
assert.match(gitMirror, /"name": "git-mirror-sync-script"/u);
assert.match(gitMirror, /git-receive-pack/u);
assert.match(gitMirror, /last-flush\.json/u);
assert.match(gitMirror, /last-write\.json/u);
assert.match(gitMirror, /ssh:\/\/git@ssh\.github\.com:443\/pikasTech\/HWLAB\.git/u);
assert.doesNotMatch(gitMirror, /"kind": "CronJob"/u);
assert.doesNotMatch(gitMirror, /"name": "git-mirror-hwlab-sync"/u);
const gitMirrorJson = JSON.parse(gitMirror);
const gitMirrorConfigMap = gitMirrorJson.items.find((item) => item.kind === "ConfigMap" && item.metadata?.name === "git-mirror-sync-script");
const gitMirrorScripts = gitMirrorConfigMap?.data ?? {};
const gitMirrorScript = gitMirrorScripts["sync.sh"] ?? "";
assert.match(gitMirrorScript, /refs\/heads\/v0\.2:refs\/mirror-stage\/heads\/v0\.2/u);
assert.match(gitMirrorScript, /refs\/heads\/v0\.2-gitops:refs\/mirror-stage\/heads\/v0\.2-gitops/u);
assert.match(gitMirrorScript, /refs\/heads\/G14:refs\/mirror-stage\/heads\/G14/u);
assert.match(gitMirrorScript, /refs\/heads\/G14-gitops:refs\/mirror-stage\/heads\/G14-gitops/u);
assert.doesNotMatch(gitMirrorScript, /refs\/heads\/\*:refs\/mirror-stage\/heads\/\*/u);
assert.match(gitMirrorScript, /git -C "\$repo_path" update-ref "refs\/heads\/\$name" "\$sha"/u);
assert.match(gitMirrorScript, /git-mirror-sync-timing/u);
assert.match(gitMirrorScript, /emit_timing fetch succeeded/u);
assert.match(gitMirrorScript, /emit_timing total succeeded/u);
assert.match(gitMirrorScript, /keeps local v0\.2-gitops ahead of GitHub/u);
assert.ok((gitMirrorJson.items || []).every((item) => item.kind !== "Secret"));
assert.ok((gitMirrorJson.items || []).every((item) => item.kind !== "CronJob"));
assert.ok((gitMirrorJson.items || []).every((item) => !item.data?.["ssh-privatekey"]));
const scriptCheckDir = path.join(outDir, "script-check");
await mkdir(scriptCheckDir, { recursive: true });
for (const scriptName of ["http.mjs", "sync.sh", "install-hooks.sh", "flush.sh"]) {
assert.ok(gitMirrorScripts[scriptName], `missing git mirror script ${scriptName}`);
await writeFile(path.join(scriptCheckDir, scriptName), gitMirrorScripts[scriptName]);
}
const httpCheck = spawnSync("node", ["--check", path.join(scriptCheckDir, "http.mjs")], { encoding: "utf8" });
assert.equal(httpCheck.status, 0, httpCheck.stderr || httpCheck.stdout);
for (const scriptName of ["sync.sh", "install-hooks.sh", "flush.sh"]) {
const shellCheck = spawnSync("sh", ["-n", path.join(scriptCheckDir, scriptName)], { encoding: "utf8" });
assert.equal(shellCheck.status, 0, `${scriptName}: ${shellCheck.stderr || shellCheck.stdout}`);
}
const removedServiceIds = [
"hwlab-router",
"hwlab-tunnel-client",
"hwlab-gateway-simu",
"hwlab-box-simu",
"hwlab-patch-panel"
];
for (const serviceId of removedServiceIds) {
assert.doesNotMatch(pipeline, new RegExp(`build-${serviceId}`, "u"));
}
const retainedServiceIds = [
"hwlab-cloud-api",
"hwlab-cloud-web",
"hwlab-agent-mgr",
"hwlab-agent-worker",
"hwlab-device-pod",
"hwlab-gateway",
"hwlab-edge-proxy",
"hwlab-agent-skills"
];
for (let index = 0; index < retainedServiceIds.length; index += 1) {
const serviceId = retainedServiceIds[index];
const task = taskByName(pipelineJson, `build-${serviceId}`);
const expectedRunAfter = index === 0 ? ["plan-artifacts"] : [`build-${retainedServiceIds[index - 1]}`];
assert.deepEqual(task.runAfter, expectedRunAfter);
assert.deepEqual(task.when, [{ input: `$(tasks.plan-artifacts.results.affected-${serviceId})`, operator: "in", values: ["true"] }]);
}
const collectScript = collectArtifacts.taskSpec.steps[0].script;
assert.match(collectScript, /--external-service-report-dir \/workspace\/source\/service-results/u);
assert.doesNotMatch(JSON.stringify(collectArtifacts), /tasks\.build-hwlab-device-pod\.results/u);
const devicePodBuildScript = taskByName(pipelineJson, "build-hwlab-device-pod").taskSpec.steps.find((step) => step.name === "publish")?.script ?? "";
assert.match(devicePodBuildScript, /--report "\/workspace\/source\/service-results\/\$\(params\.service-id\)\.json"/u);
const workloads = await readFile(path.join(outDir, "runtime-v02", "workloads.yaml"), "utf8");
const workloadsJson = JSON.parse(workloads);
const services = await readFile(path.join(outDir, "runtime-v02", "services.yaml"), "utf8");
for (const serviceId of removedServiceIds) {
assert.doesNotMatch(workloads, new RegExp(serviceId, "u"));
assert.doesNotMatch(services, new RegExp(serviceId, "u"));
}
assert.match(workloads, /"name": "HWLAB_ACCESS_CONTROL_REQUIRED"[\s\S]{0,80}"value": "1"/u);
assert.match(workloads, /"name": "HWLAB_BOOTSTRAP_ADMIN_PASSWORD_HASH"/u);
assert.match(workloads, /"name": "hwlab-v02-bootstrap-admin"/u);
const devicePod = workloadsJson.items.find((item) => item.metadata?.name === "hwlab-device-pod");
assert.ok(devicePod, "expected v02 hwlab-device-pod workload");
const devicePodEnv = devicePod.spec.template.spec.containers[0].env;
assert.ok(devicePodEnv.some((entry) => entry.name === "HWLAB_CLOUD_API_INTERNAL_URL" && entry.value === "http://hwlab-cloud-api.hwlab-v02.svc.cluster.local:6667"));
assert.ok(devicePodEnv.some((entry) => entry.name === "HWLAB_RUNTIME_MODE" && entry.value === "env-reuse-git-mirror-checkout"));
assert.ok(devicePodEnv.some((entry) => entry.name === "HWLAB_BOOT_REPO" && entry.value === "git@github.com:pikasTech/HWLAB.git"));
assert.ok(devicePodEnv.some((entry) => entry.name === "HWLAB_BOOT_COMMIT" && entry.value === sourceRevision));
assert.ok(devicePodEnv.some((entry) => entry.name === "HWLAB_BOOT_SH" && entry.value === "deploy/runtime/boot/hwlab-device-pod.sh"));
assert.equal(devicePod.spec.template.metadata.annotations["hwlab.pikastech.local/runtime-mode"], "env-reuse-git-mirror-checkout");
assert.equal(devicePod.spec.template.metadata.annotations["hwlab.pikastech.local/boot-commit"], sourceRevision);
const agentWorker = workloadsJson.items.find((item) => item.metadata?.name === "hwlab-agent-worker");
const agentWorkerEnv = agentWorker?.spec?.template?.spec?.containers?.[0]?.env ?? [];
assert.equal(agentWorkerEnv.some((entry) => entry.name === "HWLAB_BOOT_COMMIT"), false, "agent-worker must not be switched to env reuse by device-pod work");
const workloadTemplates = workloadsJson.items.filter((item) => item.spec?.template?.metadata?.labels?.["hwlab.pikastech.local/source-commit"]);
assert.ok(workloadTemplates.length > 0, "expected rendered pod-template source labels");
assert.equal(devicePod.spec.template.metadata.labels["hwlab.pikastech.local/source-commit"], sourceRevision);
assert.equal(devicePod.spec.template.metadata.annotations["hwlab.pikastech.local/source-commit"], sourceRevision);
assert.ok(workloadTemplates.some((item) => item.spec.template.metadata.labels["hwlab.pikastech.local/artifact-source-commit"]), "expected artifact source labels on pod templates");
assert.doesNotMatch(workloads, /HWLAB_GATEWAY_SIMU_URL/u);
assert.doesNotMatch(workloads, /HWLAB_BOX_SIMU_URL/u);
assert.doesNotMatch(workloads, /HWLAB_PATCH_PANEL_URL/u);
assert.doesNotMatch(workloads, /HWLAB_ROUTER_URL/u);
assert.doesNotMatch(workloads, /HWLAB_TUNNEL_CLIENT_URL/u);
assert.doesNotMatch(pipeline, /hwlab-gateway-simu-status/u);
assert.doesNotMatch(pipeline, /tasks\.build-hwlab-cloud-api\.results/u);
assert.match(pipeline, /--external-service-report-dir \/workspace\/source\/service-results/u);
const application = await readFile(path.join(outDir, "argocd", "application-v02.yaml"), "utf8");
assert.match(application, /"prune": true/u);
assert.match(application, /git-mirror-http\.devops-infra\.svc\.cluster\.local/u);
assert.doesNotMatch(application, /git@github\.com:pikasTech\/HWLAB\.git/u);
for (const generatedFile of ["deepseek-proxy.yaml", "postgres.yaml", "g14-frpc.yaml"]) {
const content = await readFile(path.join(outDir, "runtime-v02", generatedFile), "utf8");
assert.match(content, /"hwlab\.pikastech\.local\/source-commit"/u);
}
await assert.rejects(() => readFile(path.join(outDir, "tekton-v02", "control-plane-reconciler.yaml"), "utf8"), { code: "ENOENT" });
await assert.rejects(() => readFile(path.join(outDir, "tekton-v02", "poller.yaml"), "utf8"), { code: "ENOENT" });
const pipelineRunSample = await readFile(path.join(outDir, "tekton-v02", "pipelinerun.sample.yaml"), "utf8");
assert.match(pipelineRunSample, /"name": "git-read-url"[\s\S]{0,140}git-mirror-http\.devops-infra\.svc\.cluster\.local/u);
assert.match(pipelineRunSample, /"name": "git-write-url"[\s\S]{0,140}git-mirror-write\.devops-infra\.svc\.cluster\.local/u);
assert.match(pipelineRunSample, /"name": "revision"[\s\S]{0,80}"value": "[0-9a-f]{40}"/u);
assert.match(pipelineRunSample, /"name": "source-branch"[\s\S]{0,80}"value": "v0\.2"/u);
} finally {
await rm(outDir, { recursive: true, force: true });
}
});