822 lines
58 KiB
TypeScript
822 lines
58 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { spawnSync } from "node:child_process";
|
|
import { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import test from "node:test";
|
|
import vm from "node:vm";
|
|
|
|
function taskByName(pipeline, name) {
|
|
const task = pipeline.spec.tasks.find((item) => item.name === name);
|
|
assert.ok(task, `missing task ${name}`);
|
|
return task;
|
|
}
|
|
|
|
async function collectGeneratedFiles(root) {
|
|
const files = [];
|
|
async function walk(dir) {
|
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
for (const entry of entries) {
|
|
const fullPath = path.join(dir, entry.name);
|
|
if (entry.isDirectory()) await walk(fullPath);
|
|
else if (entry.isFile()) files.push(fullPath);
|
|
}
|
|
}
|
|
await walk(root);
|
|
return files.sort();
|
|
}
|
|
|
|
function collectContainersFromItem(item) {
|
|
const podSpec = item?.spec?.template?.spec ?? item?.spec?.jobTemplate?.spec?.template?.spec ?? item?.spec?.template?.spec;
|
|
return [
|
|
...(podSpec?.initContainers ?? []),
|
|
...(podSpec?.containers ?? [])
|
|
];
|
|
}
|
|
|
|
function extractRuntimeNoopDecisionScript(gitopsPromoteScript) {
|
|
const marker = `runtime_noop_decision="$(node - "$runtime_path" "$old_runtime_snapshot" /workspace/source/affected-services.json <<'NODE'\n`;
|
|
const start = gitopsPromoteScript.indexOf(marker);
|
|
assert.notEqual(start, -1, "missing runtime no-op decision script");
|
|
const rest = gitopsPromoteScript.slice(start + marker.length);
|
|
const end = rest.indexOf("\nNODE\n)");
|
|
assert.notEqual(end, -1, "missing runtime no-op decision terminator");
|
|
return rest.slice(0, end);
|
|
}
|
|
|
|
function runtimeWorkloadsFixture({ runtimeCommit, agentRunBundleCommit }) {
|
|
return {
|
|
items: [{
|
|
kind: "Deployment",
|
|
metadata: {
|
|
name: "hwlab-cloud-api",
|
|
labels: { "hwlab.pikastech.local/source-commit": runtimeCommit },
|
|
annotations: { "hwlab.pikastech.local/boot-commit": runtimeCommit }
|
|
},
|
|
spec: {
|
|
template: {
|
|
metadata: {
|
|
labels: {
|
|
"hwlab.pikastech.local/source-commit": runtimeCommit,
|
|
"hwlab.pikastech.local/artifact-source-commit": runtimeCommit
|
|
},
|
|
annotations: { "hwlab.pikastech.local/boot-commit": runtimeCommit }
|
|
},
|
|
spec: {
|
|
containers: [{
|
|
name: "hwlab-cloud-api",
|
|
env: [
|
|
{ name: "HWLAB_COMMIT_ID", value: runtimeCommit },
|
|
{ name: "HWLAB_GITOPS_SOURCE_COMMIT", value: runtimeCommit },
|
|
{ name: "HWLAB_CODE_AGENT_AGENTRUN_SOURCE_COMMIT", value: agentRunBundleCommit }
|
|
]
|
|
}]
|
|
}
|
|
}
|
|
}
|
|
}]
|
|
};
|
|
}
|
|
|
|
async function runtimeNoopDecision(gitopsPromoteScript, { oldRuntime, newRuntime }) {
|
|
const script = extractRuntimeNoopDecisionScript(gitopsPromoteScript);
|
|
const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-v02-runtime-noop-"));
|
|
try {
|
|
const oldDir = path.join(root, "old-runtime");
|
|
const newDir = path.join(root, "new-runtime");
|
|
const planPath = path.join(root, "affected-services.json");
|
|
await mkdir(oldDir, { recursive: true });
|
|
await mkdir(newDir, { recursive: true });
|
|
await writeFile(path.join(oldDir, "workloads.json"), JSON.stringify(runtimeWorkloadsFixture(oldRuntime), null, 2));
|
|
await writeFile(path.join(newDir, "workloads.json"), JSON.stringify(runtimeWorkloadsFixture(newRuntime), null, 2));
|
|
await writeFile(planPath, JSON.stringify({ buildServices: [], rolloutServices: [] }));
|
|
const result = spawnSync(process.execPath, ["-", newDir, oldDir, planPath], { input: script, encoding: "utf8" });
|
|
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
return result.stdout.trim();
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
function extractNamedFunction(source, name) {
|
|
const start = source.indexOf(`function ${name}`);
|
|
assert.notEqual(start, -1, `missing function ${name}`);
|
|
const bodyStart = source.indexOf("{", start);
|
|
assert.notEqual(bodyStart, -1, `missing function body for ${name}`);
|
|
let depth = 0;
|
|
for (let index = bodyStart; index < source.length; index += 1) {
|
|
const char = source[index];
|
|
if (char === "{") depth += 1;
|
|
else if (char === "}") {
|
|
depth -= 1;
|
|
if (depth === 0) return source.slice(start, index + 1);
|
|
}
|
|
}
|
|
assert.fail(`unterminated function ${name}`);
|
|
}
|
|
|
|
test("opencode APK proxy prefers explicit gitMirror proxyUrl", async () => {
|
|
const source = await readFile("scripts/gitops-render.mjs", "utf8");
|
|
const context = {};
|
|
vm.runInNewContext(`${extractNamedFunction(source, "configString")}
|
|
${extractNamedFunction(source, "opencodeEgressProxyUrlForProfile")}
|
|
globalThis.result = {
|
|
hostRoute: opencodeEgressProxyUrlForProfile({ lanes: { v03: { gitMirror: { egressProxy: { required: true, proxyUrl: "http://10.42.0.1:10808", namespace: "platform-infra", serviceName: "jd01-host-proxy", port: 10808 } } } } }, "v03"),
|
|
serviceFallback: opencodeEgressProxyUrlForProfile({ lanes: { v03: { gitMirror: { egressProxy: { required: true, namespace: "platform-infra", serviceName: "sub2api-egress-proxy", port: 10808 } } } } }, "v03"),
|
|
disabled: opencodeEgressProxyUrlForProfile({ lanes: { v03: { gitMirror: { egressProxy: { required: false, proxyUrl: "http://10.42.0.1:10808" } } } } }, "v03")
|
|
};`, context, { filename: "gitops-render-opencode-proxy.test.mjs" });
|
|
assert.equal(context.result.hostRoute, "http://10.42.0.1:10808");
|
|
assert.equal(context.result.serviceFallback, "http://sub2api-egress-proxy.platform-infra.svc.cluster.local:10808");
|
|
assert.equal(context.result.disabled, "");
|
|
});
|
|
|
|
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/run-bun.mjs",
|
|
"scripts/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);
|
|
const runtimeReadySource = await readFile(path.join(process.cwd(), "scripts/ci/runtime-ready.mjs"), "utf8");
|
|
assert.match(runtimeReadySource, /process\.exitCode = 1/u);
|
|
assert.match(runtimeReadySource, /item\.metadata\?\.labels\?\.\["hwlab\.pikastech\.local\/source-commit"\]/u);
|
|
|
|
assert.ok(!pipelineJson.spec.tasks.some((task) => task.name === "runtime-ready"));
|
|
|
|
const planArtifacts = taskByName(pipelineJson, "plan-artifacts");
|
|
const planArtifactsScript = planArtifacts.taskSpec.steps[0].script;
|
|
assert.match(planArtifactsScript, /node scripts\/ci\/restore-artifact-catalog\.mjs/u);
|
|
assert.match(planArtifactsScript, /HWLAB_GIT_READ_URL="\$\(params\.git-read-url\)"/u);
|
|
assert.ok(planArtifacts.taskSpec.params.some((param) => param.name === "git-read-url"));
|
|
assert.ok(planArtifacts.taskSpec.params.some((param) => param.name === "gitops-branch"));
|
|
assert.ok(planArtifacts.params.some((param) => param.name === "lane" && param.value === "$(params.lane)"));
|
|
assert.ok(pipelineJson.spec.params.some((param) => param.name === "runtime-ready-timeout-ms" && param.default === "60000"));
|
|
|
|
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, /if \[ "\$runtime_lane" = "true" \]; then\s+printf 'false' > \/tekton\/results\/runtime-ready-required\s+else\s+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.equal(await runtimeNoopDecision(gitopsPromoteScript, {
|
|
oldRuntime: {
|
|
runtimeCommit: "1111111111111111111111111111111111111111",
|
|
agentRunBundleCommit: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
|
},
|
|
newRuntime: {
|
|
runtimeCommit: "2222222222222222222222222222222222222222",
|
|
agentRunBundleCommit: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
|
}
|
|
}), "runtime-identity-only");
|
|
assert.match(gitopsPromoteScript, /argo_hard_refresh_runtime_lane\(\) \{/u);
|
|
assert.match(gitopsPromoteScript, /ARGO_APPLICATION="hwlab-node-\$lane" ARGO_FIELD_MANAGER="hwlab-\$lane-gitops-promote"/u);
|
|
assert.match(gitopsPromoteScript, /argocd\.argoproj\.io\/refresh": "hard"/u);
|
|
assert.match(gitopsPromoteScript, /applications\/" \+ encodeURIComponent\(application\) \+ "\?fieldManager=" \+ encodeURIComponent\(fieldManager\)/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.match(prepareSourceScript, /require\.resolve\("yaml"\)/u);
|
|
assert.match(prepareSourceScript, /npm ci --ignore-scripts --no-audit --prefer-offline/u);
|
|
assert.match(prepareSourceScript, /bun install --frozen-lockfile --ignore-scripts/u);
|
|
assert.doesNotMatch(prepareSourceScript, /ci-tools-image-provides-required-node-runtime/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);
|
|
|
|
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, /uploadpack\.allowReachableSHA1InWant true/u);
|
|
assert.match(gitMirrorScript, /uploadpack\.allowAnySHA1InWant true/u);
|
|
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\/v0\.3:refs\/mirror-stage\/heads\/v0\.3/u);
|
|
assert.match(gitMirrorScript, /refs\/heads\/v0\.3-gitops:refs\/mirror-stage\/heads\/v0\.3-gitops/u);
|
|
assert.doesNotMatch(gitMirrorScript, /refs\/heads\/G14:refs\/mirror-stage\/heads\/G14/u);
|
|
assert.doesNotMatch(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, /source_snapshot_ref_prefix='refs\/unidesk\/snapshots\/hwlab-node-runtime'/u);
|
|
assert.match(gitMirrorScript, /source_stage_ref="\$\{source_snapshot_ref_prefix%\/\}\/\$source_branch\/\$source_sha"/u);
|
|
assert.match(gitMirrorScript, /git -C "\$repo_path" update-ref "\$source_stage_ref" "\$source_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, /sourceInSync/u);
|
|
assert.match(gitMirrorScript, /sourceSnapshots/u);
|
|
assert.match(gitMirrorScript, /refs\/unidesk\/snapshots\/hwlab-node-runtime/u);
|
|
assert.match(gitMirrorScript, /refs\/mirror-stage\/heads\/v0\.2/u);
|
|
assert.match(gitMirrorScript, /refs\/mirror-stage\/heads\/v0\.3/u);
|
|
assert.match(gitMirrorScript, /for gitops_branch in 'v0\.2-gitops' 'v0\.3-gitops'/u);
|
|
assert.match(gitMirrorScript, /keeps local \$name ahead of GitHub/u);
|
|
assert.doesNotMatch(gitMirror, /not allowlisted/u);
|
|
assert.doesNotMatch(gitMirror, /path allowlist/u);
|
|
assert.doesNotMatch(gitMirror, /outside .* GitOps outputs/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-agent-mgr",
|
|
"hwlab-agent-worker",
|
|
"hwlab-agent-worker-template",
|
|
"hwlab-code-agent-workspace",
|
|
"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-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}`);
|
|
assert.deepEqual(task.runAfter, ["plan-artifacts"]);
|
|
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.ok(
|
|
taskByName(pipelineJson, "build-hwlab-cloud-web").taskSpec.results.some((result) => result.name === "go-base-image-status"),
|
|
"build task should declare Go base evidence Tekton results so artifact-publish result files are collected"
|
|
);
|
|
const cloudWebBuildScript = taskByName(pipelineJson, "build-hwlab-cloud-web").taskSpec.steps.find((step) => step.name === "publish")?.script ?? "";
|
|
assert.match(cloudWebBuildScript, /node scripts\/artifact-publish\.mjs --publish/u);
|
|
|
|
const artifactPublishSource = await readFile("scripts/artifact-publish.mjs", "utf8");
|
|
assert.match(artifactPublishSource, /\["run", "--cwd", "web\/hwlab-cloud-web", "check"\]/u);
|
|
assert.match(artifactPublishSource, /cloud web source check failed before BuildKit image build/u);
|
|
assert.match(artifactPublishSource, /\/usr\/local\/bin\/hwpod/u);
|
|
assert.doesNotMatch(artifactPublishSource, /Run bun run --cwd web\/hwlab-cloud-web build, fix the static asset build/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");
|
|
const servicesJson = JSON.parse(services);
|
|
const deepseekProxy = JSON.parse(await readFile(path.join(outDir, "runtime-v02", "deepseek-proxy.yaml"), "utf8"));
|
|
const observability = await readFile(path.join(outDir, "runtime-v02", "observability.yaml"), "utf8");
|
|
const observabilityJson = JSON.parse(observability);
|
|
for (const serviceId of removedServiceIds) {
|
|
assert.doesNotMatch(workloads, new RegExp(serviceId, "u"));
|
|
assert.doesNotMatch(services, new RegExp(serviceId, "u"));
|
|
}
|
|
const observableRuntimeServiceIds = [
|
|
"hwlab-agent-skills",
|
|
"hwlab-cloud-api",
|
|
"hwlab-cloud-web",
|
|
"hwlab-edge-proxy"
|
|
];
|
|
for (const serviceId of observableRuntimeServiceIds) {
|
|
const service = servicesJson.items.find((item) => item.kind === "Service" && item.metadata?.name === serviceId);
|
|
assert.ok(service, `missing observable service ${serviceId}`);
|
|
assert.equal(service.metadata.labels["hwlab.pikastech.local/monitoring"], "enabled");
|
|
assert.ok(service.spec.ports.some((port) => port.name === "metrics" && port.port === 9100 && port.targetPort === "metrics"), `missing metrics port for ${serviceId}`);
|
|
const workload = workloadsJson.items.find((item) => item.kind === "Deployment" && item.metadata?.name === serviceId);
|
|
assert.ok(workload, `missing observable workload ${serviceId}`);
|
|
assert.equal(workload.metadata.labels["hwlab.pikastech.local/monitoring"], "enabled");
|
|
const sidecar = workload.spec.template.spec.containers.find((container) => container.name === "hwlab-metrics");
|
|
assert.ok(sidecar, `missing metrics sidecar for ${serviceId}`);
|
|
assert.match(sidecar.image, /hwlab-ci-node-tools:node22-alpine-bun-v1/u);
|
|
assert.deepEqual(sidecar.command, ["node", "/metrics/metrics-sidecar.mjs"]);
|
|
assert.match(workload.spec.template.metadata.annotations["hwlab.pikastech.local/metrics-sidecar-sha256"], /^[a-f0-9]{64}$/u);
|
|
assert.ok(sidecar.env.some((entry) => entry.name === "HWLAB_METRICS_SERVICE_ID" && entry.value === serviceId));
|
|
assert.ok(sidecar.ports.some((port) => port.name === "metrics" && port.containerPort === 9100));
|
|
if (serviceId === "hwlab-cloud-api") {
|
|
assert.ok(sidecar.env.some((entry) => entry.name === "HWLAB_METRICS_EXTRA_URL" && /\/v1\/web-performance\/metrics$/u.test(entry.value)), "cloud-api sidecar should scrape WebUI performance metrics from loopback");
|
|
} else {
|
|
assert.equal(sidecar.env.some((entry) => entry.name === "HWLAB_METRICS_EXTRA_URL"), false, `unexpected extra metrics target for ${serviceId}`);
|
|
}
|
|
if (serviceId === "hwlab-edge-proxy") {
|
|
const container = workload.spec.template.spec.containers.find((entry) => entry.name === "hwlab-edge-proxy");
|
|
assert.equal(container.readinessProbe.httpGet.path, "/health");
|
|
assert.equal(container.livenessProbe.httpGet.path, "/health");
|
|
}
|
|
}
|
|
const deepseekService = deepseekProxy.items.find((item) => item.kind === "Service" && item.metadata?.name === "hwlab-deepseek-proxy");
|
|
assert.equal(deepseekService.metadata.labels["hwlab.pikastech.local/monitoring"], "enabled");
|
|
assert.ok(deepseekService.spec.ports.some((port) => port.name === "metrics" && port.port === 9100));
|
|
const deepseekDeployment = deepseekProxy.items.find((item) => item.kind === "Deployment" && item.metadata?.name === "hwlab-deepseek-proxy");
|
|
assert.ok(deepseekDeployment.spec.template.spec.containers.some((container) => container.name === "hwlab-metrics"));
|
|
assert.equal(
|
|
deepseekDeployment.spec.template.metadata.annotations["hwlab.pikastech.local/metrics-sidecar-sha256"],
|
|
workloadsJson.items.find((item) => item.kind === "Deployment" && item.metadata?.name === "hwlab-cloud-api").spec.template.metadata.annotations["hwlab.pikastech.local/metrics-sidecar-sha256"]
|
|
);
|
|
const observabilityItems = observabilityJson.items ?? [];
|
|
const metricsConfig = observabilityItems.find((item) => item.kind === "ConfigMap" && item.metadata?.name === "hwlab-v02-metrics-sidecar");
|
|
assert.ok(metricsConfig, "missing metrics sidecar ConfigMap");
|
|
assert.match(metricsConfig.data["metrics-sidecar.mjs"], /hwlab_service_up/u);
|
|
const serviceMonitors = observabilityItems.filter((item) => item.kind === "ServiceMonitor");
|
|
assert.deepEqual(serviceMonitors.map((item) => item.metadata.name).sort(), [
|
|
"hwlab-v02-hwlab-agent-skills",
|
|
"hwlab-v02-hwlab-cloud-api",
|
|
"hwlab-v02-hwlab-cloud-web",
|
|
"hwlab-v02-hwlab-deepseek-proxy",
|
|
"hwlab-v02-hwlab-edge-proxy"
|
|
]);
|
|
for (const monitor of serviceMonitors) {
|
|
assert.equal(monitor.metadata.namespace, "hwlab-v02");
|
|
assert.equal(monitor.metadata.labels["hwlab.pikastech.local/monitoring"], "enabled");
|
|
assert.deepEqual(monitor.spec.namespaceSelector.matchNames, ["hwlab-v02"]);
|
|
assert.equal(monitor.spec.selector.matchLabels["hwlab.pikastech.local/gitops-target"], "v02");
|
|
assert.equal(monitor.spec.selector.matchLabels["hwlab.pikastech.local/monitoring"], "enabled");
|
|
assert.deepEqual(monitor.spec.endpoints, [{ port: "metrics", path: "/metrics", interval: "30s", scrapeTimeout: "10s" }]);
|
|
}
|
|
assert.ok(observabilityItems.some((item) => item.kind === "PrometheusRule" && item.metadata?.name === "hwlab-v02-observability"));
|
|
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 cloudApi = workloadsJson.items.find((item) => item.metadata?.name === "hwlab-cloud-api");
|
|
assert.ok(cloudApi, "expected v02 hwlab-cloud-api workload");
|
|
const cloudApiContainer = cloudApi.spec.template.spec.containers.find((container) => container.name === "hwlab-cloud-api");
|
|
const codexForwarder = cloudApi.spec.template.spec.containers.find((container) => container.name === "hwlab-codex-api-forwarder");
|
|
assert.ok(cloudApiContainer, "expected cloud-api app container");
|
|
assert.ok(codexForwarder, "expected codex API forwarder sidecar");
|
|
const cloudApiEnv = cloudApiContainer.env;
|
|
const cloudApiCommit = cloudApiEnv.find((entry) => entry.name === "HWLAB_COMMIT_ID")?.value;
|
|
const cloudApiBootEnv = new Map(cloudApiEnv.map((entry) => [entry.name, entry.value]));
|
|
assert.equal(cloudApiBootEnv.get("HWLAB_RUNTIME_MODE"), "env-reuse-git-mirror-checkout");
|
|
assert.equal(cloudApiBootEnv.get("HWLAB_BOOT_SH"), "deploy/runtime/boot/hwlab-cloud-api.sh");
|
|
const codexForwarderEnv = new Map(codexForwarder.env.map((entry) => [entry.name, entry.value]));
|
|
assert.equal(codexForwarder.command, undefined);
|
|
assert.equal(codexForwarderEnv.get("HWLAB_RUNTIME_MODE"), "env-reuse-git-mirror-checkout");
|
|
assert.equal(codexForwarderEnv.get("HWLAB_BOOT_SH"), "deploy/runtime/boot/hwlab-codex-api-responses-forwarder.sh");
|
|
assert.equal(codexForwarderEnv.get("HWLAB_BOOT_COMMIT"), cloudApiCommit);
|
|
assert.equal(codexForwarderEnv.get("HWLAB_BOOT_READ_URL"), "http://git-mirror-http.devops-infra.svc.cluster.local/pikasTech/HWLAB.git");
|
|
assert.ok(cloudApiEnv.some((entry) => entry.name === "HWLAB_CODE_AGENT_AGENTRUN_SOURCE_COMMIT" && entry.value === cloudApiCommit));
|
|
assert.ok(cloudApiEnv.some((entry) => entry.name === "HWLAB_CODE_AGENT_ADAPTER" && entry.value === "agentrun-v01"));
|
|
assert.equal(cloudApi.spec.template.spec.initContainers?.some((entry) => entry.name === "hwlab-code-agent-workspace-init"), false);
|
|
assert.equal(cloudApi.spec.template.spec.volumes?.some((entry) => /code-agent-codex|code-agent-workspace/u.test(entry.name)), false);
|
|
assert.equal(cloudApiContainer.volumeMounts?.some((entry) => /code-agent-codex|code-agent-workspace/u.test(entry.name)), false);
|
|
assert.equal(cloudApiEnv.some((entry) => entry.name === "HWLAB_CODE_AGENT_PROVIDER" && entry.value === "codex-stdio"), false);
|
|
assert.equal(cloudApiEnv.some((entry) => entry.name === "HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR"), false);
|
|
assert.ok(cloudApiEnv.some((entry) => entry.name === "UNIDESK_MAIN_SERVER_IP" && entry.value === "http://74.48.78.17:18081"));
|
|
assert.ok(cloudApiEnv.some((entry) => entry.name === "HWLAB_KEYCLOAK_ISSUER" && entry.value === "https://auth.74-48-78-17.nip.io/realms/hwlab"));
|
|
assert.ok(cloudApiEnv.some((entry) => entry.name === "HWLAB_KEYCLOAK_CLIENT_ID" && entry.value === "hwlab-cloud-web"));
|
|
assert.ok(cloudApiEnv.some((entry) => entry.name === "HWLAB_KEYCLOAK_CLIENT_SECRET" && entry.valueFrom?.secretKeyRef?.name === "hwlab-cloud-web-client" && entry.valueFrom?.secretKeyRef?.key === "client-secret"));
|
|
assert.ok(cloudApiEnv.some((entry) => entry.name === "HWLAB_OPENFGA_MODE" && entry.value === "enforce"));
|
|
assert.ok(cloudApiEnv.some((entry) => entry.name === "HWLAB_OPENFGA_API_URL" && entry.value === "http://hwlab-openfga.hwlab-v02.svc.cluster.local:8080"));
|
|
assert.ok(cloudApiEnv.some((entry) => entry.name === "HWLAB_OPENFGA_AUTHN_TOKEN" && entry.valueFrom?.secretKeyRef?.name === "hwlab-v02-openfga" && entry.valueFrom?.secretKeyRef?.key === "authn-preshared-key"));
|
|
const deepseekResponsesBridge = deepseekDeployment.spec.template.spec.containers.find((container) => container.name === "responses-bridge");
|
|
assert.ok(deepseekResponsesBridge, "expected DeepSeek responses bridge container");
|
|
const deepseekBridgeEnv = new Map(deepseekResponsesBridge.env.map((entry) => [entry.name, entry.value]));
|
|
assert.equal(deepseekResponsesBridge.command, undefined);
|
|
assert.equal(deepseekBridgeEnv.get("HWLAB_RUNTIME_MODE"), "env-reuse-git-mirror-checkout");
|
|
assert.equal(deepseekBridgeEnv.get("HWLAB_BOOT_SH"), "deploy/runtime/boot/hwlab-deepseek-responses-bridge.sh");
|
|
assert.equal(deepseekBridgeEnv.get("HWLAB_BOOT_COMMIT"), cloudApiCommit);
|
|
assert.equal(deepseekBridgeEnv.get("HWLAB_BOOT_READ_URL"), "http://git-mirror-http.devops-infra.svc.cluster.local/pikasTech/HWLAB.git");
|
|
assert.equal(workloadsJson.items.some((item) => item.metadata?.labels?.["hwlab.pikastech.local/service-id"] === "hwlab-agent-worker"), false);
|
|
const kustomization = await readFile(path.join(outDir, "runtime-v02", "kustomization.yaml"), "utf8");
|
|
assert.doesNotMatch(kustomization, /code-agent-codex-config/u);
|
|
assert.match(kustomization, /openfga\.yaml/u);
|
|
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.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", "openfga.yaml", "node-frpc.yaml"]) {
|
|
const content = await readFile(path.join(outDir, "runtime-v02", generatedFile), "utf8");
|
|
assert.match(content, /"hwlab\.pikastech\.local\/source-commit"/u);
|
|
}
|
|
const frpc = JSON.parse(await readFile(path.join(outDir, "runtime-v02", "node-frpc.yaml"), "utf8"));
|
|
const frpcDeployment = frpc.items.find((item) => item.kind === "Deployment");
|
|
const frpcConfigLabel = frpcDeployment.spec.template.metadata.labels["hwlab.pikastech.local/config-sha256"];
|
|
const frpcConfigAnnotation = frpcDeployment.spec.template.metadata.annotations["hwlab.pikastech.local/config-sha256"];
|
|
assert.equal(frpcDeployment.spec.template.spec.hostNetwork, true);
|
|
assert.equal(frpcDeployment.spec.template.spec.dnsPolicy, "ClusterFirstWithHostNet");
|
|
assert.ok(frpcConfigLabel);
|
|
assert.equal(frpcConfigLabel.length <= 63, true);
|
|
assert.match(frpcConfigAnnotation, /^[a-f0-9]{64}$/u);
|
|
assert.equal(frpcConfigAnnotation.startsWith(frpcConfigLabel), true);
|
|
assert.equal(frpcDeployment.spec.template.metadata.labels["hwlab.pikastech.local/source-commit"], undefined);
|
|
|
|
const postgres = JSON.parse(await readFile(path.join(outDir, "runtime-v02", "postgres.yaml"), "utf8"));
|
|
const postgresStatefulSet = postgres.items.find((item) => item.kind === "StatefulSet");
|
|
const postgresMigrationLabel = postgresStatefulSet.spec.template.metadata.labels["hwlab.pikastech.local/migration-sha256"];
|
|
const postgresMigrationAnnotation = postgresStatefulSet.spec.template.metadata.annotations["hwlab.pikastech.local/migration-sha256"];
|
|
assert.ok(postgresMigrationLabel);
|
|
assert.equal(postgresMigrationLabel.length <= 63, true);
|
|
assert.match(postgresMigrationAnnotation, /^[a-f0-9]{64}$/u);
|
|
assert.equal(postgresMigrationAnnotation.startsWith(postgresMigrationLabel), true);
|
|
assert.equal(postgresStatefulSet.spec.template.metadata.labels["hwlab.pikastech.local/source-commit"], undefined);
|
|
|
|
const openfga = JSON.parse(await readFile(path.join(outDir, "runtime-v02", "openfga.yaml"), "utf8"));
|
|
const openfgaDeployment = openfga.items.find((item) => item.kind === "Deployment" && item.metadata?.name === "hwlab-openfga");
|
|
const openfgaService = openfga.items.find((item) => item.kind === "Service" && item.metadata?.name === "hwlab-openfga");
|
|
const openfgaMigrate = openfga.items.find((item) => item.kind === "Job" && item.metadata?.name === "hwlab-openfga-migrate");
|
|
assert.ok(openfgaDeployment, "expected v02 OpenFGA deployment");
|
|
assert.ok(openfgaService, "expected v02 OpenFGA ClusterIP service");
|
|
assert.ok(openfgaMigrate, "expected v02 OpenFGA migration job");
|
|
assert.equal(openfgaService.spec.type, "ClusterIP");
|
|
assert.equal(openfga.items.some((item) => item.kind === "Ingress"), false);
|
|
assert.equal(openfgaDeployment.spec.template.spec.containers[0].image, "127.0.0.1:5000/hwlab/openfga:v1.17.0");
|
|
assert.deepEqual(openfgaDeployment.spec.template.spec.containers[0].args, ["run"]);
|
|
assert.deepEqual(openfgaMigrate.spec.template.spec.containers[0].args, ["migrate"]);
|
|
const openfgaEnv = openfgaDeployment.spec.template.spec.containers[0].env;
|
|
assert.ok(openfgaEnv.some((entry) => entry.name === "OPENFGA_DATASTORE_ENGINE" && entry.value === "postgres"));
|
|
assert.ok(openfgaEnv.some((entry) => entry.name === "OPENFGA_DATASTORE_URI" && entry.valueFrom?.secretKeyRef?.name === "hwlab-v02-openfga" && entry.valueFrom?.secretKeyRef?.key === "datastore-uri"));
|
|
assert.ok(openfgaEnv.some((entry) => entry.name === "OPENFGA_AUTHN_METHOD" && entry.value === "preshared"));
|
|
assert.ok(openfgaEnv.some((entry) => entry.name === "OPENFGA_AUTHN_PRESHARED_KEYS" && entry.valueFrom?.secretKeyRef?.name === "hwlab-v02-openfga" && entry.valueFrom?.secretKeyRef?.key === "authn-preshared-key"));
|
|
assert.ok(openfgaEnv.some((entry) => entry.name === "OPENFGA_PLAYGROUND_ENABLED" && entry.value === "false"));
|
|
assert.equal(openfgaDeployment.spec.template.spec.containers[0].readinessProbe.httpGet.path, "/healthz");
|
|
assert.equal(openfgaDeployment.spec.template.metadata.labels["hwlab.pikastech.local/source-commit"], sourceRevision);
|
|
|
|
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 });
|
|
}
|
|
});
|
|
|
|
test("v03 render keeps node identity as data instead of generated structure", async () => {
|
|
const outDir = await mkdtemp(path.join(os.tmpdir(), "hwlab-v03-render-test-"));
|
|
const scratchDir = path.join(".tmp", `gitops-render-v03-${process.pid}-${Date.now()}`);
|
|
const staleCatalogPath = path.join(scratchDir, "artifact-catalog.v03.json");
|
|
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 staleRevision = "0".repeat(40);
|
|
await mkdir(scratchDir, { recursive: true });
|
|
await writeFile(staleCatalogPath, JSON.stringify({
|
|
catalogVersion: "v1",
|
|
kind: "hwlab-artifact-catalog",
|
|
environment: "v03",
|
|
profile: "v03",
|
|
namespace: "hwlab-v03",
|
|
commitId: staleRevision.slice(0, 12),
|
|
artifactState: "ready",
|
|
services: [{
|
|
serviceId: "hwlab-cloud-api",
|
|
runtimeMode: "env-reuse-git-mirror-checkout",
|
|
envReuse: true,
|
|
sourceCommitId: staleRevision,
|
|
commitId: staleRevision,
|
|
image: "127.0.0.1:5000/hwlab/hwlab-cloud-api-env:env-stale",
|
|
imageTag: "env-stale",
|
|
digest: `sha256:${"1".repeat(64)}`,
|
|
environmentImage: "127.0.0.1:5000/hwlab/hwlab-cloud-api-env:env-stale",
|
|
environmentDigest: `sha256:${"1".repeat(64)}`,
|
|
environmentInputHash: "e".repeat(64),
|
|
codeInputHash: "c".repeat(64),
|
|
bootRepo: "git@github.com:pikasTech/HWLAB.git",
|
|
bootCommit: staleRevision,
|
|
bootSh: "deploy/runtime/boot/hwlab-cloud-api.sh"
|
|
}]
|
|
}, null, 2));
|
|
const render = spawnSync(process.execPath, [
|
|
"scripts/run-bun.mjs",
|
|
"scripts/gitops-render.mjs",
|
|
"--lane", "v03",
|
|
"--node", "JD01",
|
|
"--catalog-path", staleCatalogPath,
|
|
"--image-tag-mode", "full",
|
|
"--source-branch", "v0.3",
|
|
"--gitops-branch", "v0.3-gitops",
|
|
"--out", outDir,
|
|
"--source-revision", "HEAD"
|
|
], { cwd: process.cwd(), encoding: "utf8" });
|
|
assert.equal(render.status, 0, render.stderr || render.stdout);
|
|
|
|
const generatedFiles = await collectGeneratedFiles(outDir);
|
|
const generatedPaths = generatedFiles.map((filePath) => path.relative(outDir, filePath));
|
|
assert.ok(generatedPaths.includes("runtime-v03/node-frpc.yaml"));
|
|
assert.ok(generatedPaths.includes("argocd/application-v03.yaml"));
|
|
assert.ok(generatedPaths.includes("tekton-v03/pipeline.yaml"));
|
|
assert.ok(generatedPaths.includes("runtime-v03/external-postgres.yaml"));
|
|
assert.ok(generatedPaths.includes("runtime-v03/configmaps.yaml"));
|
|
assert.ok(generatedPaths.includes("runtime-v03/external-secrets.yaml"));
|
|
const pipeline = await readFile(path.join(outDir, "tekton-v03", "pipeline.yaml"), "utf8");
|
|
const pipelineJson = JSON.parse(pipeline);
|
|
assert.match(pipeline, /--external-service-report-dir \/workspace\/source\/service-results/u);
|
|
assert.match(pipeline, /--ci-plan-path \/workspace\/source\/affected-services\.json/u);
|
|
assert.ok(!pipelineJson.spec.tasks.some((task) => task.name === "runtime-ready"));
|
|
const gitopsPromote = taskByName(pipelineJson, "gitops-promote");
|
|
const gitopsPromoteScript = gitopsPromote.taskSpec.steps[0].script;
|
|
assert.match(gitopsPromoteScript, /if \[ "\$runtime_lane" = "true" \]; then\s+printf 'false' > \/tekton\/results\/runtime-ready-required\s+else\s+printf 'true' > \/tekton\/results\/runtime-ready-required/u);
|
|
assert.match(gitopsPromoteScript, /delegated-post-flush-closeout/u);
|
|
assert.ok(!generatedPaths.includes("runtime-v03/postgres.yaml"));
|
|
for (const relativePath of generatedPaths) {
|
|
assert.doesNotMatch(relativePath, /g14/i, `legacy node token leaked into generated path ${relativePath}`);
|
|
}
|
|
|
|
const workloads = await readFile(path.join(outDir, "runtime-v03", "workloads.yaml"), "utf8");
|
|
const workloadsJson = JSON.parse(workloads);
|
|
const configMapsJson = JSON.parse(await readFile(path.join(outDir, "runtime-v03", "configmaps.yaml"), "utf8"));
|
|
const externalSecretsJson = JSON.parse(await readFile(path.join(outDir, "runtime-v03", "external-secrets.yaml"), "utf8"));
|
|
const configMapNames = (configMapsJson.items ?? []).map((item) => item.metadata?.name).sort();
|
|
assert.deepEqual(configMapNames, ["hwlab-v03-hwpod-preinstalled-specs", "hwlab-v03-project-management-bootstrap"]);
|
|
const smokeExternalSecret = (externalSecretsJson.items ?? []).find((item) => item.kind === "ExternalSecret" && item.metadata?.name === "hwlab-secret-plane-smoke");
|
|
assert.ok(smokeExternalSecret, "expected v03 secret-plane smoke ExternalSecret");
|
|
assert.equal(smokeExternalSecret.metadata?.namespace, "hwlab-v03");
|
|
assert.equal(smokeExternalSecret.metadata?.labels?.["hwlab.pikastech.local/secret-plane"], "issue-2277");
|
|
assert.equal(smokeExternalSecret.metadata?.annotations?.["hwlab.pikastech.local/secret-plane-issue"], "pikasTech/HWLAB#2277");
|
|
assert.equal(smokeExternalSecret.spec?.secretStoreRef?.kind, "ClusterSecretStore");
|
|
assert.equal(smokeExternalSecret.spec?.secretStoreRef?.name, "hwlab-secret-plane-vault-cluster");
|
|
assert.equal(smokeExternalSecret.spec?.target?.name, "hwlab-secret-plane-smoke");
|
|
assert.deepEqual(smokeExternalSecret.spec?.data, [{ secretKey: "password", remoteRef: { key: "hwlab-secret-plane/poc", property: "password" } }]);
|
|
const cloudApi = (workloadsJson.items ?? []).find((item) => item.kind === "Deployment" && item.metadata?.name === "hwlab-cloud-api");
|
|
const cloudApiContainer = collectContainersFromItem(cloudApi).find((container) => container.name === "hwlab-cloud-api");
|
|
const cloudApiEnv = new Map((cloudApiContainer?.env ?? []).map((entry) => [entry.name, entry.value]));
|
|
const cloudApiEnvEntries = new Map((cloudApiContainer?.env ?? []).map((entry) => [entry.name, entry]));
|
|
assert.equal(cloudApi?.spec?.template?.metadata?.labels?.["hwlab.pikastech.local/source-commit"], sourceRevision);
|
|
assert.equal(cloudApi?.spec?.template?.metadata?.labels?.["hwlab.pikastech.local/artifact-source-commit"], sourceRevision);
|
|
assert.equal(cloudApi?.spec?.template?.metadata?.annotations?.["hwlab.pikastech.local/source-commit"], sourceRevision);
|
|
assert.equal(cloudApi?.spec?.template?.metadata?.annotations?.["hwlab.pikastech.local/boot-commit"], sourceRevision);
|
|
assert.equal(cloudApiEnv.get("HWLAB_COMMIT_ID"), sourceRevision);
|
|
assert.equal(cloudApiEnv.get("HWLAB_BOOT_COMMIT"), sourceRevision);
|
|
assert.equal(cloudApiEnv.get("HWLAB_GITOPS_SOURCE_COMMIT"), sourceRevision);
|
|
assert.equal(cloudApiEnv.get("HWLAB_CODE_AGENT_AGENTRUN_SOURCE_COMMIT"), sourceRevision);
|
|
assert.equal(cloudApiEnv.get("HWLAB_CLOUD_DB_SSL_MODE"), "require");
|
|
assert.equal(cloudApiEnv.get("HWLAB_KAFKA_ENABLED"), "true");
|
|
assert.equal(cloudApiEnv.get("HWLAB_KAFKA_AGENTRUN_EVENT_CONSUME_ENABLED"), "true");
|
|
assert.equal(cloudApiEnv.get("HWLAB_KAFKA_BOOTSTRAP_SERVERS"), "platform-infra-kafka-kafka-bootstrap.platform-infra.svc.cluster.local:9092");
|
|
assert.equal(cloudApiEnv.get("HWLAB_KAFKA_AGENTRUN_EVENT_TOPIC"), "agentrun.event.v1");
|
|
assert.equal(cloudApiEnv.get("HWLAB_KAFKA_EVENT_TOPIC"), "hwlab.event.v1");
|
|
assert.equal(cloudApiEnv.get("HWLAB_KAFKA_CLIENT_ID"), "hwlab-v03-cloud-api");
|
|
assert.equal(cloudApiEnv.get("HWLAB_KAFKA_AGENTRUN_EVENT_GROUP_ID"), "hwlab-v03-agentrun-event-bridge");
|
|
assert.match(cloudApiEnv.get("HWLAB_ENVIRONMENT_IMAGE") ?? "", /hwlab-cloud-api-env:env-stale/u);
|
|
assert.deepEqual(cloudApiEnvEntries.get("HWLAB_SECRET_PLANE_SMOKE")?.valueFrom?.secretKeyRef, { name: "hwlab-secret-plane-smoke", key: "password", optional: true });
|
|
assert.ok((cloudApi?.spec?.template?.spec?.volumes ?? []).some((volume) => volume.name === "hwpod-preinstalled-specs" && volume.configMap?.name === "hwlab-v03-hwpod-preinstalled-specs"));
|
|
assert.ok((cloudApiContainer?.volumeMounts ?? []).some((mount) => mount.name === "hwpod-preinstalled-specs" && mount.mountPath === "/etc/hwlab/hwpod-specs"));
|
|
assert.ok((cloudApiContainer?.env ?? []).some((entry) => entry.name === "HWLAB_HWPOD_SPEC_REGISTRY_DIRS" && entry.value === "/etc/hwlab/hwpod-specs"));
|
|
const projectManagement = (workloadsJson.items ?? []).find((item) => item.kind === "Deployment" && item.metadata?.name === "hwlab-project-management");
|
|
const projectManagementContainer = collectContainersFromItem(projectManagement).find((container) => container.name === "hwlab-project-management");
|
|
assert.ok((projectManagement?.spec?.template?.spec?.volumes ?? []).some((volume) => volume.name === "project-management-bootstrap" && volume.configMap?.name === "hwlab-v03-project-management-bootstrap"));
|
|
assert.ok((projectManagementContainer?.volumeMounts ?? []).some((mount) => mount.name === "project-management-bootstrap" && mount.mountPath === "/etc/hwlab/project-management"));
|
|
assert.ok((projectManagementContainer?.env ?? []).some((entry) => entry.name === "HWLAB_PROJECT_MANAGEMENT_BOOTSTRAP_SOURCES_PATH" && entry.value === "/etc/hwlab/project-management/sources.json"));
|
|
const externalPostgres = JSON.parse(await readFile(path.join(outDir, "runtime-v03", "external-postgres.yaml"), "utf8"));
|
|
assert.deepEqual((externalPostgres.items ?? []).map((item) => item.kind), ["Service", "EndpointSlice"]);
|
|
const externalPostgresSlice = (externalPostgres.items ?? []).find((item) => item.kind === "EndpointSlice");
|
|
assert.equal(externalPostgresSlice?.metadata?.name, "jd01-pk01-platform-postgres-host");
|
|
assert.equal(externalPostgresSlice?.metadata?.labels?.["kubernetes.io/service-name"], "jd01-pk01-platform-postgres");
|
|
const deepseekProxy = await readFile(path.join(outDir, "runtime-v03", "deepseek-proxy.yaml"), "utf8");
|
|
const deepseekProxyJson = JSON.parse(deepseekProxy);
|
|
const metricsSidecarConfigMaps = [...(workloadsJson.items ?? []), ...(deepseekProxyJson.items ?? [])]
|
|
.filter((item) => item.kind === "Deployment")
|
|
.flatMap((item) => item.spec?.template?.spec?.volumes ?? [])
|
|
.filter((volume) => volume.name === "hwlab-metrics-sidecar")
|
|
.map((volume) => volume.configMap?.name);
|
|
assert.ok(metricsSidecarConfigMaps.length >= 5, "expected v03 observable workloads to mount metrics sidecar config");
|
|
assert.deepEqual([...new Set(metricsSidecarConfigMaps)], ["hwlab-v03-metrics-sidecar"]);
|
|
assert.doesNotMatch(`${workloads}\n${deepseekProxy}`, /hwlab-v02-metrics-sidecar/u);
|
|
const agentRunNodeEnv = (workloadsJson.items ?? [])
|
|
.flatMap((item) => collectContainersFromItem(item))
|
|
.flatMap((container) => container.env ?? [])
|
|
.filter((entry) => entry.name === "HWLAB_CODE_AGENT_AGENTRUN_PROVIDER_ID");
|
|
assert.ok(agentRunNodeEnv.length > 0, "expected AgentRun provider id env to carry configured node id");
|
|
assert.deepEqual([...new Set(agentRunNodeEnv.map((entry) => entry.value))], ["JD01"]);
|
|
|
|
} finally {
|
|
await rm(outDir, { recursive: true, force: true });
|
|
await rm(scratchDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("v03 render includes JD01 secret-plane smoke on JD01 gitops root", { timeout: 20000 }, async () => {
|
|
const outDir = await mkdtemp(path.join(os.tmpdir(), "hwlab-v03-jd01-render-test-"));
|
|
const scratchDir = path.join(".tmp", `gitops-render-v03-jd01-${process.pid}-${Date.now()}`);
|
|
const staleCatalogPath = path.join(scratchDir, "artifact-catalog.v03.json");
|
|
try {
|
|
const staleRevision = "0".repeat(40);
|
|
await mkdir(scratchDir, { recursive: true });
|
|
await writeFile(staleCatalogPath, JSON.stringify({
|
|
catalogVersion: "v1",
|
|
kind: "hwlab-artifact-catalog",
|
|
environment: "v03",
|
|
profile: "v03",
|
|
namespace: "hwlab-v03",
|
|
commitId: staleRevision.slice(0, 12),
|
|
artifactState: "ready",
|
|
services: [{
|
|
serviceId: "hwlab-cloud-api",
|
|
runtimeMode: "env-reuse-git-mirror-checkout",
|
|
envReuse: true,
|
|
sourceCommitId: staleRevision,
|
|
commitId: staleRevision,
|
|
image: "127.0.0.1:5000/hwlab/hwlab-cloud-api-env:env-stale",
|
|
imageTag: "env-stale",
|
|
digest: `sha256:${"1".repeat(64)}`,
|
|
environmentImage: "127.0.0.1:5000/hwlab/hwlab-cloud-api-env:env-stale",
|
|
environmentDigest: `sha256:${"1".repeat(64)}`,
|
|
environmentInputHash: "e".repeat(64),
|
|
codeInputHash: "c".repeat(64),
|
|
bootRepo: "git@github.com:pikasTech/HWLAB.git",
|
|
bootCommit: staleRevision,
|
|
bootSh: "deploy/runtime/boot/hwlab-cloud-api.sh"
|
|
}]
|
|
}, null, 2));
|
|
const render = spawnSync(process.execPath, [
|
|
"scripts/run-bun.mjs",
|
|
"scripts/gitops-render.mjs",
|
|
"--lane", "v03",
|
|
"--catalog-path", staleCatalogPath,
|
|
"--image-tag-mode", "full",
|
|
"--source-branch", "v0.3",
|
|
"--gitops-branch", "v0.3-gitops",
|
|
"--gitops-root", "deploy/gitops/node/jd01",
|
|
"--out", outDir,
|
|
"--source-revision", "HEAD"
|
|
], { cwd: process.cwd(), encoding: "utf8" });
|
|
assert.equal(render.status, 0, render.stderr || render.stdout);
|
|
|
|
const generatedFiles = await collectGeneratedFiles(outDir);
|
|
const generatedPaths = generatedFiles.map((filePath) => path.relative(outDir, filePath));
|
|
assert.ok(generatedPaths.includes("runtime-v03/kustomization.yaml"));
|
|
assert.ok(generatedPaths.includes("runtime-v03/workloads.yaml"));
|
|
assert.ok(generatedPaths.includes("runtime-v03/opencode.yaml"));
|
|
assert.equal(generatedPaths.includes("runtime-v03/external-secrets.yaml"), true);
|
|
const kustomization = await readFile(path.join(outDir, "runtime-v03", "kustomization.yaml"), "utf8");
|
|
assert.match(kustomization, /external-secrets\.yaml/u);
|
|
const externalSecretsJson = JSON.parse(await readFile(path.join(outDir, "runtime-v03", "external-secrets.yaml"), "utf8"));
|
|
const smokeExternalSecret = (externalSecretsJson.items ?? []).find((item) => item.kind === "ExternalSecret" && item.metadata?.name === "hwlab-secret-plane-smoke");
|
|
assert.ok(smokeExternalSecret, "expected JD01 v03 secret-plane smoke ExternalSecret");
|
|
assert.equal(smokeExternalSecret.metadata?.namespace, "hwlab-v03");
|
|
assert.equal(smokeExternalSecret.metadata?.labels?.["hwlab.pikastech.local/secret-plane"], "issue-2277");
|
|
assert.equal(smokeExternalSecret.metadata?.annotations?.["hwlab.pikastech.local/secret-plane-issue"], "pikasTech/HWLAB#2277");
|
|
assert.equal(smokeExternalSecret.spec?.secretStoreRef?.kind, "ClusterSecretStore");
|
|
assert.equal(smokeExternalSecret.spec?.secretStoreRef?.name, "hwlab-secret-plane-vault-cluster");
|
|
assert.equal(smokeExternalSecret.spec?.target?.name, "hwlab-secret-plane-smoke");
|
|
assert.deepEqual(smokeExternalSecret.spec?.data, [{ secretKey: "password", remoteRef: { key: "hwlab-secret-plane/poc", property: "password" } }]);
|
|
|
|
const workloadsJson = JSON.parse(await readFile(path.join(outDir, "runtime-v03", "workloads.yaml"), "utf8"));
|
|
const cloudApi = (workloadsJson.items ?? []).find((item) => item.kind === "Deployment" && item.metadata?.name === "hwlab-cloud-api");
|
|
const cloudApiContainer = collectContainersFromItem(cloudApi).find((container) => container.name === "hwlab-cloud-api");
|
|
const cloudApiEnvEntries = new Map((cloudApiContainer?.env ?? []).map((entry) => [entry.name, entry]));
|
|
assert.deepEqual(cloudApiEnvEntries.get("HWLAB_SECRET_PLANE_SMOKE")?.valueFrom?.secretKeyRef, { name: "hwlab-secret-plane-smoke", key: "password", optional: true });
|
|
const cloudWeb = (workloadsJson.items ?? []).find((item) => item.kind === "Deployment" && item.metadata?.name === "hwlab-cloud-web");
|
|
const cloudWebContainer = collectContainersFromItem(cloudWeb).find((container) => container.name === "hwlab-cloud-web");
|
|
const cloudWebEnvEntries = new Map((cloudWebContainer?.env ?? []).map((entry) => [entry.name, entry]));
|
|
assert.equal(cloudWebEnvEntries.get("HWLAB_CLOUD_WEB_OPENCODE_DEFAULT_PROJECT")?.value, "/workspace");
|
|
const agentRunNodeEnv = (workloadsJson.items ?? [])
|
|
.flatMap((item) => collectContainersFromItem(item))
|
|
.flatMap((container) => container.env ?? [])
|
|
.filter((entry) => entry.name === "HWLAB_CODE_AGENT_AGENTRUN_PROVIDER_ID");
|
|
assert.ok(agentRunNodeEnv.length > 0, "expected JD01 node id to be inferred from gitops root");
|
|
assert.deepEqual([...new Set(agentRunNodeEnv.map((entry) => entry.value))], ["JD01"]);
|
|
|
|
const opencode = JSON.parse(await readFile(path.join(outDir, "runtime-v03", "opencode.yaml"), "utf8"));
|
|
const opencodeDeployment = (opencode.items ?? []).find((item) => item.kind === "Deployment" && item.metadata?.name === "opencode-server");
|
|
assert.ok(opencodeDeployment, "expected opencode-server deployment");
|
|
assert.equal(opencodeDeployment.metadata?.annotations?.["hwlab.pikastech.local/opencode-provider-profile"], "dsflash-go");
|
|
assert.equal(opencodeDeployment.metadata?.annotations?.["hwlab.pikastech.local/agentrun-provider-secret-ref"], "agentrun-v02/agentrun-v01-provider-dsflash-go");
|
|
assert.equal(opencodeDeployment.metadata?.annotations?.["hwlab.pikastech.local/opencode-provider-base-url"], "http://127.0.0.1:4097/v1");
|
|
assert.equal(opencodeDeployment.metadata?.annotations?.["hwlab.pikastech.local/opencode-provider-upstream-base-url"], "https://opencode.ai/zen/go/v1");
|
|
assert.equal(opencodeDeployment.metadata?.annotations?.["hwlab.pikastech.local/opencode-image"], "127.0.0.1:5000/hwlab/opencode:1.17.7-git");
|
|
assert.equal(opencodeDeployment.metadata?.annotations?.["hwlab.pikastech.local/opencode-provider-proxy-boot-sh"], "deploy/runtime/boot/opencode-provider-proxy.sh");
|
|
assert.equal(opencodeDeployment.metadata?.annotations?.["hwlab.pikastech.local/values-printed"], "false");
|
|
assert.match(opencodeDeployment.spec?.template?.metadata?.annotations?.["hwlab.pikastech.local/opencode-config-sha256"] ?? "", /^[a-f0-9]{64}$/u);
|
|
const opencodeGitInit = opencodeDeployment.spec?.template?.spec?.initContainers?.find((container) => container.name === "opencode-workspace-git-init");
|
|
assert.ok(opencodeGitInit, "expected opencode workspace git init container");
|
|
assert.deepEqual(opencodeGitInit.command, ["/bin/sh", "-ec"]);
|
|
assert.match(opencodeGitInit.args?.[0] ?? "", /git -C \/workspace init/u);
|
|
assert.ok(opencodeGitInit.volumeMounts?.some((entry) => entry.name === "workspace" && entry.mountPath === "/workspace"));
|
|
const opencodeContainer = collectContainersFromItem(opencodeDeployment).find((container) => container.name === "opencode-server");
|
|
assert.equal(opencodeContainer?.image, "127.0.0.1:5000/hwlab/opencode:1.17.7-git");
|
|
assert.deepEqual(opencodeContainer?.command, ["/bin/sh", "-ec"]);
|
|
assert.equal(opencodeContainer?.args?.[0], "exec opencode serve --hostname 0.0.0.0 --port 4096");
|
|
assert.doesNotMatch(opencodeContainer?.args?.[0] ?? "", /apk add|dl-cdn\.alpinelinux\.org/u, "opencode startup must not depend on runtime apk repository access");
|
|
const opencodeEnvEntries = new Map((opencodeContainer?.env ?? []).map((entry) => [entry.name, entry]));
|
|
assert.equal(opencodeEnvEntries.get("HTTPS_PROXY")?.value, "http://sub2api-egress-proxy.platform-infra.svc.cluster.local:10808");
|
|
assert.match(opencodeEnvEntries.get("NO_PROXY")?.value ?? "", /\.svc\.cluster\.local/u);
|
|
assert.deepEqual(opencodeEnvEntries.get("OPENCODE_DSFLASH_GO_API_KEY")?.valueFrom?.secretKeyRef, { name: "hwlab-v03-code-agent-provider", key: "opencode-api-key", optional: true });
|
|
const opencodeConfig = JSON.parse(opencodeEnvEntries.get("OPENCODE_CONFIG_CONTENT")?.value ?? "{}");
|
|
assert.equal(opencodeConfig.model, "dsflash-go/deepseek-v4-flash");
|
|
assert.equal(opencodeConfig.small_model, "dsflash-go/deepseek-v4-flash");
|
|
assert.equal(opencodeConfig.provider["dsflash-go"].npm, "@ai-sdk/openai-compatible");
|
|
assert.equal(opencodeConfig.provider["dsflash-go"].options.baseURL, "http://127.0.0.1:4097/v1");
|
|
assert.equal(opencodeConfig.provider["dsflash-go"].options.apiKey, "{env:OPENCODE_DSFLASH_GO_API_KEY}");
|
|
assert.equal(opencodeConfig.provider["dsflash-go"].models["deepseek-v4-flash"].reasoning, false);
|
|
assert.equal(opencodeConfig.provider["dsflash-go"].models["deepseek-v4-flash"].tool_call, true);
|
|
assert.deepEqual(opencodeConfig.provider["dsflash-go"].models["deepseek-v4-flash"].limit, { context: 1000000, output: 384000 });
|
|
const providerProxy = collectContainersFromItem(opencodeDeployment).find((container) => container.name === "opencode-provider-proxy");
|
|
assert.ok(providerProxy, "expected provider proxy sidecar");
|
|
assert.equal(providerProxy.command, undefined);
|
|
const providerProxyEnv = new Map((providerProxy.env ?? []).map((entry) => [entry.name, entry.value]));
|
|
assert.equal(providerProxyEnv.get("HWLAB_BOOT_SH"), "deploy/runtime/boot/opencode-provider-proxy.sh");
|
|
assert.equal(providerProxyEnv.get("HWLAB_BOOT_REF"), "v0.3");
|
|
assert.match(providerProxyEnv.get("HWLAB_BOOT_READ_URL") ?? "", /pikasTech\/HWLAB/u);
|
|
assert.equal(providerProxyEnv.get("HWLAB_OPENCODE_PROVIDER_PROXY_UPSTREAM_BASE_URL"), "https://opencode.ai/zen/go/v1");
|
|
assert.equal(providerProxyEnv.get("HWLAB_OPENCODE_PROVIDER_PROXY_PUBLIC_BASE_PATH"), "/v1");
|
|
assert.equal(providerProxyEnv.get("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT"), "http://otel-collector.platform-infra.svc.cluster.local:4318/v1/traces");
|
|
assert.equal(providerProxyEnv.get("OTEL_SERVICE_NAME"), "opencode-provider-proxy");
|
|
} finally {
|
|
await rm(outDir, { recursive: true, force: true });
|
|
await rm(scratchDir, { recursive: true, force: true });
|
|
}
|
|
});
|