#!/usr/bin/env node // SPEC: PJ2026-01060703 CI/CD branch follower native HWLAB control-plane refresh. // Responsibility: render and apply the HWLAB node Tekton Pipeline from a k8s git-mirror snapshot. import { spawnSync } from "node:child_process"; import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import https from "node:https"; import { createRequire } from "node:module"; import { tmpdir } from "node:os"; import path from "node:path"; const startedAt = Date.now(); const sourceCommit = requiredEnv("SOURCE_COMMIT"); const sourceStageRef = requiredEnv("SOURCE_STAGE_REF"); const gitReadUrl = requiredEnv("GIT_READ_URL"); const fieldManager = requiredEnv("FIELD_MANAGER"); const tektonNamespace = requiredEnv("TEKTON_NAMESPACE"); const kubeRequestTimeoutSeconds = requiredEnvPositiveNumber("KUBE_REQUEST_TIMEOUT_SECONDS"); const overlay = JSON.parse(Buffer.from(requiredEnv("HWLAB_RENDER_OVERLAY_B64"), "base64").toString("utf8")); const workDir = mkdtempSync(path.join(tmpdir(), `hwlab-control-plane-${sourceCommit.slice(0, 12)}-`)); const repoDir = path.join(workDir, "repo"); const renderDir = path.join(workDir, "render"); const depsDir = path.join(workDir, "deps"); try { checkoutSnapshot(); prepareYamlDependency(); applyDeployOverlay(); renderControlPlane(); await applyPipeline(); emit({ ok: true, status: "applied" }); } finally { rmSync(workDir, { recursive: true, force: true }); } function checkoutSnapshot() { run("git", ["init", repoDir], { cwd: "/" }); run("git", ["-C", repoDir, "remote", "add", "origin", gitReadUrl]); run("git", ["-C", repoDir, "fetch", "--depth", "1", "origin", sourceStageRef]); run("git", ["-C", repoDir, "checkout", "--detach", "FETCH_HEAD"]); const head = capture("git", ["-C", repoDir, "rev-parse", "HEAD"]).trim(); if (head !== sourceCommit) throw new Error(`snapshot checkout mismatch: expected ${sourceCommit}, got ${head}`); } function prepareYamlDependency() { mkdirSync(depsDir, { recursive: true }); writeFileSync(path.join(depsDir, "package.json"), JSON.stringify({ private: true, dependencies: {} }), "utf8"); if (canResolveYaml()) return; const registry = requiredOverlayString("npmRegistry"); const timeoutMs = String(requiredPositiveNumber("npmFetchTimeoutMs")); const retries = String(requiredNonNegativeNumber("npmRetries")); const yamlSpec = yamlDependencySpec(); const env = renderEnv({ npm_config_registry: registry, BUN_CONFIG_REGISTRY: registry, npm_config_fetch_timeout: timeoutMs, npm_config_fetch_retries: retries, }); if (commandExists("bun")) { const result = runOptional("bun", ["add", "--no-save", "--ignore-scripts", "--registry", registry, yamlSpec], { cwd: depsDir, env }); if (result.status === 0 && canResolveYaml()) return; } run("npm", ["install", "--package-lock=false", "--no-save", "--ignore-scripts", "--no-audit", "--no-fund", "--omit=dev", "--registry", registry, yamlSpec], { cwd: depsDir, env }); if (!canResolveYaml()) throw new Error("yaml dependency remains unresolved after install"); } function yamlDependencySpec() { const pkg = JSON.parse(readFileSync(path.join(repoDir, "package.json"), "utf8")); const version = pkg.dependencies?.yaml || pkg.devDependencies?.yaml; if (typeof version !== "string" || version.length === 0) throw new Error("package.json must declare yaml dependency for gitops render"); return `yaml@${version}`; } function canResolveYaml() { const result = runOptional("node", ["-e", "require.resolve('yaml')"], { cwd: depsDir, stdio: "ignore" }); return result.status === 0; } function applyDeployOverlay() { const YAML = yamlModule(); const deployPath = path.join(repoDir, "deploy/deploy.yaml"); const doc = YAML.parse(readFileSync(deployPath, "utf8")); doc.nodes = doc.nodes || {}; doc.nodes[overlay.nodeId] = { ...(doc.nodes[overlay.nodeId] || {}), gitopsRoot: overlay.gitopsRoot, sourceRepo: overlay.gitUrl }; doc.lanes = doc.lanes || {}; const lane = doc.lanes[overlay.lane] || {}; const downloadStack = { ...(lane.envRecipe?.downloadStack || {}), httpProxy: overlay.dockerProxyHttp, httpsProxy: overlay.dockerProxyHttps, noProxy: overlay.dockerNoProxyList, }; doc.lanes[overlay.lane] = { ...lane, node: overlay.nodeId, sourceBranch: overlay.sourceBranch, gitopsBranch: overlay.gitopsBranch, namespace: overlay.runtimeNamespace, endpoint: overlay.publicApiUrl, publicEndpoints: { frontend: overlay.publicWebUrl, api: overlay.publicApiUrl }, artifactCatalog: overlay.catalogPath, runtimePath: overlay.runtimePath, imageTagMode: "full", sourceRepo: overlay.gitUrl, externalPostgres: overlay.externalPostgres, observability: overlay.observability, envRecipe: { ...(lane.envRecipe || {}), downloadStack }, }; if (overlay.runtimeStore !== undefined) doc.lanes[overlay.lane].runtimeStore = overlay.runtimeStore; if (overlay.codeAgentRuntime !== undefined) doc.lanes[overlay.lane].codeAgentRuntime = overlay.codeAgentRuntime; if (overlay.deployYamlGitMirror !== undefined) doc.lanes[overlay.lane].gitMirror = overlay.deployYamlGitMirror; writeFileSync(deployPath, YAML.stringify(doc), "utf8"); } function renderControlPlane() { run("node", [ "scripts/run-bun.mjs", "scripts/gitops-render.mjs", "--lane", overlay.lane, "--node", overlay.nodeId, "--gitops-root", overlay.gitopsRoot, "--catalog-path", overlay.catalogPath, "--image-tag-mode", "full", "--source-revision", sourceCommit, "--source-repo", overlay.gitUrl, "--source-branch", overlay.sourceBranch, "--gitops-branch", overlay.gitopsBranch, "--git-read-url", overlay.gitReadUrl, "--git-write-url", overlay.gitWriteUrl, "--registry-prefix", overlay.registryPrefix, "--runtime-endpoint", overlay.publicApiUrl, "--web-endpoint", overlay.publicWebUrl, "--out", renderDir, ], { cwd: repoDir, env: renderEnv() }); } async function applyPipeline() { const pipelinePath = path.join(renderDir, overlay.tektonDir, "pipeline.yaml"); if (!existsSync(pipelinePath)) throw new Error(`rendered Pipeline missing: ${pipelinePath}`); const YAML = yamlModule(); const pipeline = YAML.parse(readFileSync(pipelinePath, "utf8")); if (!pipeline || typeof pipeline !== "object") throw new Error(`rendered Pipeline is not an object: ${pipelinePath}`); const renderedPipelineName = pipeline?.metadata?.name; if (typeof renderedPipelineName !== "string" || renderedPipelineName.length === 0) { throw new Error(`rendered Pipeline metadata.name missing: ${pipelinePath}`); } const pipelineName = requiredOverlayString("pipelineName"); pipeline.metadata = pipeline.metadata && typeof pipeline.metadata === "object" ? pipeline.metadata : {}; pipeline.metadata.name = pipelineName; const pipelineText = YAML.stringify(pipeline); await kubeRequest( "PATCH", `/apis/tekton.dev/v1/namespaces/${encodeURIComponent(tektonNamespace)}/pipelines/${encodeURIComponent(pipelineName)}?fieldManager=${encodeURIComponent(fieldManager)}&force=true`, pipelineText, "application/apply-patch+yaml", ); } function yamlModule() { const requireFromDeps = createRequire(path.join(depsDir, "package.json")); return requireFromDeps("yaml"); } function kubeRequest(method, requestPath, body, contentType = "application/json") { const host = requiredEnv("KUBERNETES_SERVICE_HOST"); const port = requiredEnvPositiveNumber("KUBERNETES_SERVICE_PORT"); const token = readFileSync("/var/run/secrets/kubernetes.io/serviceaccount/token", "utf8").trim(); const ca = readFileSync("/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"); return new Promise((resolve, reject) => { const payload = typeof body === "string" ? body : JSON.stringify(body); const req = https.request({ host, port, path: requestPath, method, ca, headers: { authorization: `Bearer ${token}`, "content-type": contentType, "content-length": Buffer.byteLength(payload), }, }, (res) => { let text = ""; res.setEncoding("utf8"); res.on("data", (chunk) => { text += chunk; }); res.on("end", () => { if ((res.statusCode || 0) < 200 || (res.statusCode || 0) >= 300) { reject(new Error(`kube api ${method} ${requestPath} status ${res.statusCode}: ${tail(text, 1200)}`)); return; } resolve(text); }); }); req.setTimeout(kubeRequestTimeoutSeconds * 1000, () => req.destroy(new Error(`kube api ${method} ${requestPath} timed out after ${kubeRequestTimeoutSeconds}s`))); req.on("error", reject); req.write(payload); req.end(); }); } function renderEnv(extra = {}) { const noProxy = overlay.noProxy || process.env.NO_PROXY || process.env.no_proxy || ""; return { ...process.env, HTTP_PROXY: overlay.proxyHttp || process.env.HTTP_PROXY || "", HTTPS_PROXY: overlay.proxyHttps || process.env.HTTPS_PROXY || "", ALL_PROXY: overlay.proxyAll || process.env.ALL_PROXY || "", NO_PROXY: noProxy, http_proxy: overlay.proxyHttp || process.env.http_proxy || "", https_proxy: overlay.proxyHttps || process.env.https_proxy || "", all_proxy: overlay.proxyAll || process.env.all_proxy || "", no_proxy: noProxy, HWLAB_NODE_CI_TOOLS_IMAGE: overlay.toolsImage || process.env.HWLAB_NODE_CI_TOOLS_IMAGE || "", HWLAB_NODE_BUILDKIT_IMAGE: overlay.buildkitSidecarImage || process.env.HWLAB_NODE_BUILDKIT_IMAGE || "", ...extra, }; } function run(command, args, options = {}) { const result = runOptional(command, args, options); if (result.status !== 0) throw new Error(failedCommandMessage(command, args, result)); return result; } function capture(command, args) { const result = runOptional(command, args, { cwd: repoDir, encoding: "utf8", stdio: ["ignore", "pipe", "inherit"] }); if (result.status !== 0) throw new Error(failedCommandMessage(command, args, result)); return result.stdout || ""; } function runOptional(command, args, options = {}) { return spawnSync(command, args, { cwd: options.cwd || repoDir, env: options.env || process.env, encoding: options.encoding || "utf8", stdio: options.stdio || "inherit", }); } function commandExists(command) { return runOptional("sh", ["-lc", `command -v ${command} >/dev/null 2>&1`], { cwd: repoDir, stdio: "ignore" }).status === 0; } function failedCommandMessage(command, args, result) { const signal = result.signal ? ` signal=${result.signal}` : ""; const error = result.error ? ` error=${result.error.message}` : ""; const stderr = typeof result.stderr === "string" && result.stderr.length > 0 ? ` stderr=${tail(result.stderr, 1000)}` : ""; return `${command} ${args.join(" ")} failed with exit ${result.status}${signal}${error}${stderr}`; } function tail(value, maxChars) { return value.length <= maxChars ? value : value.slice(value.length - maxChars); } function requiredEnv(name) { const value = process.env[name]; if (!value) throw new Error(`${name} is required`); return value; } function requiredEnvPositiveNumber(name) { const value = Number(requiredEnv(name)); if (!Number.isFinite(value) || value <= 0) throw new Error(`${name} must be a positive number`); return Math.floor(value); } function requiredOverlayString(name) { const value = overlay[name]; if (typeof value !== "string" || value.length === 0) throw new Error(`overlay.${name} is required`); return value; } function requiredPositiveNumber(name) { const value = Number(overlay[name]); if (!Number.isFinite(value) || value <= 0) throw new Error(`overlay.${name} must be a positive number`); return Math.floor(value); } function requiredNonNegativeNumber(name) { const value = Number(overlay[name]); if (!Number.isFinite(value) || value < 0) throw new Error(`overlay.${name} must be a non-negative number`); return Math.floor(value); } function emit(extra) { process.stdout.write(`${JSON.stringify({ ...extra, sourceCommit, sourceStageRef, pipeline: overlay.pipelineName, namespace: tektonNamespace, elapsedMs: Date.now() - startedAt, sourceAuthority: "k8s-git-mirror-snapshot", statusAuthority: "kubernetes-api-serviceaccount", parsedDownstreamCliOutput: false, })}\n`); }