#!/usr/bin/env node // SPEC: PJ2026-01060505 Workbench Performance; PJ2026-0106 平台运维 import assert from "node:assert/strict"; import { createHash } from "node:crypto"; import { mkdir, readFile, rm, writeFile } from "node:fs/promises"; import path from "node:path"; import { applyRuntimeLaneDeployConfig, argoApplicationName, artifactCatalogSkeleton, attachAccessControlEnv, annotate, cloneJson, defaultBranch, defaultCatalogPath, defaultGitopsBranch, defaultOutDir, defaultRuntimeEndpoint, defaultSourceRepo, defaultV02RuntimeEndpoint, defaultWebEndpoint, ensureObject, generatedPath, gitopsPathForProfile, imageTagForSource, isNamedRuntimeLane, isRuntimeLane, jsonManifest, label, laneRuntimeProfiles, namespaceNameForProfile, observabilityManifest, parseArgs, profileEndpoint, readJson, readJsonIfPresent, repoRoot, resolveSourceRevision, runtimeLabelForProfile, runtimePathForProfile, textFile, transformHealthContract, transformListNamespace, transformServices, transformWorkloads, usage, workbenchRuntimeRedisConfigForProfile } from "./src/gitops-render/core.mjs"; import { registryManifest } from "./src/gitops-render/infra-manifests.mjs"; import { argoApplication, argoProject, deepSeekProxyManifest, deviceAgent71FreqManifest, externalPostgresConfigForLane, externalPostgresManifest, nodeFrpcManifest, opencodeServerManifest, runtimeConfigMapsForProfile, runtimeConfigMapsManifest, runtimePostgresImageForProfile, runtimeSecretPlaneConfigForProfile, runtimeSecretPlaneManifest, v02OpenFgaManifest, v02PostgresManifest, workbenchRuntimeRedisManifest } from "./src/gitops-render/runtime-manifests.mjs"; import { ciLaneSettings, tektonControlPlaneReconcilerCronJob, tektonPipeline, tektonTasks, tektonPipelineRunTemplate, tektonPollerCronJob, tektonRbac } from "./src/gitops-render/tekton-manifests.mjs"; import { CLOUD_CORE_MIGRATIONS } from "../internal/db/schema.ts"; function runtimeKustomization({ profile = "dev", includeDeviceAgent = profile === "dev", externalPostgres = false, workbenchRedis = false, runtimeConfigMaps = false, externalSecrets = false, deploy = null } = {}) { const resources = ["namespace.yaml", "services.yaml", "health-contract.yaml", "workloads.yaml", "deepseek-proxy.yaml"]; if (!isRuntimeLane(profile)) resources.splice(1, 0, "code-agent-codex-config.yaml"); if (runtimeConfigMaps) resources.splice(3, 0, "configmaps.yaml"); if (externalSecrets) resources.splice(resources.indexOf("workloads.yaml"), 0, "external-secrets.yaml"); if (isRuntimeLane(profile)) { if (externalPostgres) resources.push("external-postgres.yaml"); else resources.push("postgres.yaml"); if (workbenchRedis) resources.push("workbench-redis.yaml"); resources.push("openfga.yaml", "observability.yaml", "opencode.yaml"); } if (includeDeviceAgent) resources.push("device-agent-71-freq.yaml"); resources.push("node-frpc.yaml"); return { apiVersion: "kustomize.config.k8s.io/v1beta1", kind: "Kustomization", namespace: namespaceNameForProfile(profile, deploy), resources }; } function readme({ source, args, deploy }) { if (args.lane === "v02") { return `# HWLAB v0.2 GitOps CI/CD This directory is generated by \`scripts/gitops-render.mjs --lane v02\` from source branch \`${args.sourceBranch}\`. - Target: additive node-scoped v0.2 lane only. - CI: Tekton Pipeline \`hwlab-ci/hwlab-v02-ci-image-publish\`. - Git mirror/relay: v0.2 CI reads source/catalog from \`${args.gitReadUrl}\` and writes GitOps promotion to \`${args.gitWriteUrl}\`; canonical repo \`${args.sourceRepo}\` remains the source identity and manual flush upstream. - Artifact contract: runtime images come from \`${args.catalogPath}\`; source branch \`v0.2\` does not track this generated catalog. - Manual trigger: run \`bun scripts/cli.ts hwlab node control-plane trigger-current --lane v02 --confirm\` from UniDesk to create commit-pinned PipelineRun \`hwlab-v02-ci-poll-\` for the current \`origin/v0.2\` commit. - Branch split: source branch \`${args.sourceBranch}\` remains human-authored source; generated desired state is promoted to local mirror branch \`${args.gitopsBranch}\`. - CD: Argo CD Application \`argocd/hwlab-node-v02\` consumes \`${args.gitReadUrl}:${args.gitopsBranch}:deploy/gitops/node/runtime-v02\` and deploys only to namespace \`hwlab-v02\`. - Public preview: FRP exposes v0.2 web \`${args.webEndpoint}\` / edge \`${args.runtimeEndpoint}\` through \`hwlab-v02/hwlab-v02-frpc\`. - node DEV/PROD boundary: this lane does not modify archived DEV/PROD branches, \`hwlab-dev\`, \`hwlab-prod\`, 17666/17667 or 18666/18667. - CronJob policy: v0.2 does not generate \`hwlab-v02-branch-poller\` or \`hwlab-v02-control-plane-reconciler\`; obsolete live CronJobs should be deleted by the UniDesk control-plane apply command. - Gate policy: old DEV/D601/main gates do not enter this lane; new checks stay limited to the fixed branch, namespace, catalog, runtime path and Argo boundaries. `; } if (isNamedRuntimeLane(args.lane)) { const namespace = namespaceNameForProfile(args.lane, deploy); const runtimePath = gitopsPathForProfile(args, args.lane, deploy); return `# HWLAB ${args.lane} GitOps CI/CD This directory is generated by \`scripts/gitops-render.mjs --lane ${args.lane}\` from the owning runtime lane declaration in \`deploy/deploy.yaml\`. - Source: \`${args.sourceRepo}\` branch \`${args.sourceBranch}\` at \`${source.full}\`. - GitOps: generated desired state is promoted to \`${args.gitWriteUrl}\` branch \`${args.gitopsBranch}\`. - Artifact contract: runtime images come from \`${args.catalogPath}\`. - Runtime: Argo CD consumes \`${args.gitReadUrl}:${args.gitopsBranch}:${runtimePath}\` and deploys only to namespace \`${namespace}\`. - Public endpoints: web \`${args.webEndpoint}\`; API \`${args.runtimeEndpoint}\`. - Authority: branch, catalog, runtime path, namespace, public services and endpoints are read from \`deploy/deploy.yaml\`; missing named-lane fields fail closed. `; } return `# HWLAB node GitOps CI/CD This directory is generated from built-in Tekton-native CI primitive declarations plus human-authored \`deploy/deploy.yaml\`, CI-authored \`deploy/artifact-catalog.dev.json\`, and \`deploy/k8s/*\` by \`scripts/gitops-render.mjs\`. - Target: node k3s only. - CI: Tekton Pipeline \`hwlab-ci/hwlab-node-ci-image-publish\`. - Artifact contract: runtime images come from \`deploy/artifact-catalog.dev.json\`; \`deploy/deploy.yaml\` only carries human-authored config such as env and topology. - Branch split: source branch \`${args.sourceBranch}\` remains the watched code branch; generated desired state is promoted to \`${args.gitopsBranch}\`. - CD: Argo CD Applications \`argocd/hwlab-node-dev\` and \`argocd/hwlab-node-prod\` consume \`${args.gitopsBranch}:deploy/gitops/node/runtime-dev\` and \`runtime-prod\`. - Public preview: FRP exposes node DEV web \`${args.webEndpoint}\` / edge \`${args.runtimeEndpoint}\` through \`hwlab-dev/hwlab-node-frpc\`, and node PROD web \`${args.prodWebEndpoint}\` / edge \`${args.prodRuntimeEndpoint}\` through \`hwlab-prod/hwlab-node-prod-frpc\`; D601 keeps \`:16666/:16667\`. - Manual polling: run \`kubectl -n hwlab-ci create job --from=cronjob/hwlab-node-branch-poller hwlab-node-branch-poller-manual-$(date -u +%Y%m%d%H%M%S)\`; this reuses the same poller path and creates a commit-pinned Tekton PipelineRun only when needed. - Control-plane reconcile: CronJob \`hwlab-ci/hwlab-node-control-plane-reconciler\` polls \`${args.sourceBranch}\`, runs \`scripts/gitops-render.mjs\`, and server-side-applies the generated Tekton RBAC/Pipeline/Poller/Reconciler manifests so CI control-plane changes do not require a manual render/apply step. - D601 boundary: this path does not target D601, does not use UniDesk Code Queue, and does not change D601 production traffic. - Concurrency: CI builds are per source commit and do not acquire the legacy DEV CD Lease; Argo CD reconciles the Git desired state. `; } async function plannedFiles(args) { const [deploy, namespaceTemplate, config, services, health, workloads] = await Promise.all([ readJson("deploy/deploy.yaml"), readJson("deploy/k8s/base/namespace.yaml"), readJson("deploy/k8s/base/code-agent-codex-config.yaml"), readJson("deploy/k8s/base/services.yaml"), readJson("deploy/k8s/dev/health-contract.yaml"), readJson("deploy/k8s/base/workloads.yaml") ]); args = applyRuntimeLaneDeployConfig(args, deploy, { defaultBranch, defaultGitopsBranch, defaultCatalogPath, defaultGitopsRoot: defaultOutDir, defaultRuntimeEndpoint, defaultWebEndpoint, defaultV02RuntimeEndpoint, defaultSourceRepo }); await attachAccessControlEnv(deploy); const source = await resolveSourceRevision(args.sourceRevision); source.imageTag = imageTagForSource(args, source); let artifactCatalog = await readJsonIfPresent(args.catalogPath, null); if (!artifactCatalog && isRuntimeLane(args.lane)) artifactCatalog = artifactCatalogSkeleton({ args, deploy, source }); if (!artifactCatalog) artifactCatalog = await readJson(args.catalogPath); const settings = ciLaneSettings(args, deploy); const profiles = laneRuntimeProfiles(args); const migrationSources = isRuntimeLane(args.lane) ? await Promise.all(CLOUD_CORE_MIGRATIONS.map(async (migration) => ({ id: migration.id, path: migration.path, sql: await readFile(path.join(repoRoot, migration.path), "utf8") }))) : []; const metricsSidecarScript = isRuntimeLane(args.lane) ? await readFile(path.join(repoRoot, "internal/dev-entrypoint/metrics-sidecar.mjs"), "utf8") : null; const metricsSidecarSha256 = metricsSidecarScript ? createHash("sha256").update(metricsSidecarScript).digest("hex") : null; const baseLabels = { "hwlab.pikastech.local/gitops-target": settings.gitopsTarget, "hwlab.pikastech.local/source-commit": source.full }; const annotations = { "hwlab.pikastech.local/rendered-by": "scripts/gitops-render.mjs" }; const files = new Map(); const putJson = (relative, value) => files.set(path.join(args.outDir, relative), jsonManifest(value)); const putText = (relative, value) => files.set(path.join(args.outDir, relative), textFile(value)); const publicEndpoints = Object.fromEntries(profiles.map((profile) => { const endpoints = profileEndpoint(args, profile); return [profile, { web: endpoints.webEndpoint, runtime: endpoints.runtimeEndpoint }]; })); const sourceDescriptor = { kind: isRuntimeLane(args.lane) ? `hwlab-${args.lane}-gitops-source` : "hwlab-node-gitops-source", sourceCommit: source.full, sourceShortCommit: source.short, sourceImageTag: source.imageTag, sourceBranch: args.sourceBranch, gitopsBranch: args.gitopsBranch, lane: settings.lane, sourceRepo: args.sourceRepo, gitReadUrl: args.gitReadUrl, gitWriteUrl: args.gitWriteUrl, registryPrefix: args.registryPrefix, runtimePaths: profiles.map((profile) => gitopsPathForProfile(args, profile, deploy)), publicEndpoints, frpDeployments: Object.fromEntries(profiles.map((profile) => [profile, `${namespaceNameForProfile(profile, deploy)}/${isRuntimeLane(profile) ? `hwlab-${profile}-frpc` : profile === "prod" ? "hwlab-node-prod-frpc" : "hwlab-node-frpc"}`])), tektonPipeline: `hwlab-ci/${settings.pipelineName}`, argoApplications: profiles.map((profile) => `argocd/${argoApplicationName(profile)}`) }; if (args.useDeployImages) { sourceDescriptor.renderMode = { imageSource: args.catalogPath, configSource: "deploy/deploy.yaml", mixedDesiredState: true }; } putJson("source.json", sourceDescriptor); putText("README.md", readme({ source, args, deploy })); putJson("registry/registry.yaml", registryManifest()); putJson(`${settings.tektonDir}/rbac.yaml`, tektonRbac(args, deploy)); putJson(`${settings.tektonDir}/tasks.yaml`, { apiVersion: "v1", kind: "List", items: tektonTasks(args, deploy) }); putJson(`${settings.tektonDir}/pipeline.yaml`, tektonPipeline(args, deploy)); if (!isRuntimeLane(args.lane)) { putJson(`${settings.tektonDir}/poller.yaml`, tektonPollerCronJob(args, deploy)); putJson(`${settings.tektonDir}/control-plane-reconciler.yaml`, tektonControlPlaneReconcilerCronJob(args)); } putJson(`${settings.tektonDir}/pipelinerun.sample.yaml`, tektonPipelineRunTemplate({ source, args, deploy })); putJson("argocd/project.yaml", argoProject(args, deploy)); for (const profile of profiles) putJson(`argocd/application-${profile}.yaml`, argoApplication(args, profile, deploy)); for (const profile of profiles) { const namespace = namespaceNameForProfile(profile, deploy); const includeDeviceAgent = profile === "dev"; const externalPostgres = externalPostgresConfigForLane(deploy, profile, args); const workbenchRedis = workbenchRuntimeRedisConfigForProfile(deploy, profile); const runtimeConfigMaps = runtimeConfigMapsForProfile(deploy, profile); const runtimeSecretPlane = await runtimeSecretPlaneConfigForProfile(deploy, profile, args); const profileLabels = { ...baseLabels, "hwlab.pikastech.local/environment": runtimeLabelForProfile(profile), "hwlab.pikastech.local/profile": runtimeLabelForProfile(profile) }; const runtimeNamespace = cloneJson(namespaceTemplate); runtimeNamespace.metadata.name = namespace; label(runtimeNamespace.metadata, profileLabels); annotate(runtimeNamespace.metadata, annotations); const runtimePath = runtimePathForProfile(profile, deploy); const endpoints = profileEndpoint(args, profile); putJson(`${runtimePath}/kustomization.yaml`, runtimeKustomization({ profile, includeDeviceAgent, externalPostgres: Boolean(externalPostgres), workbenchRedis: Boolean(workbenchRedis), runtimeConfigMaps: runtimeConfigMaps.length > 0, externalSecrets: Boolean(runtimeSecretPlane), deploy })); putJson(`${runtimePath}/namespace.yaml`, runtimeNamespace); if (!isRuntimeLane(profile)) putJson(`${runtimePath}/code-agent-codex-config.yaml`, transformListNamespace(config, namespace, profileLabels, annotations)); putJson(`${runtimePath}/services.yaml`, transformServices({ services, namespace, labels: profileLabels, annotations, profile, deploy, nodeId: args.nodeId })); putJson(`${runtimePath}/health-contract.yaml`, transformHealthContract(health, namespace, profileLabels, annotations, endpoints.runtimeEndpoint, endpoints.webEndpoint, profile)); if (runtimeConfigMaps.length > 0) putJson(`${runtimePath}/configmaps.yaml`, runtimeConfigMapsManifest({ configMaps: runtimeConfigMaps, namespace, labels: profileLabels, annotations })); if (runtimeSecretPlane) putJson(`${runtimePath}/external-secrets.yaml`, runtimeSecretPlaneManifest({ config: runtimeSecretPlane, namespace, labels: profileLabels, annotations })); putJson(`${runtimePath}/workloads.yaml`, transformWorkloads({ workloads, deploy, catalog: artifactCatalog, source, sourceBranch: args.sourceBranch, gitReadUrl: args.gitReadUrl, registryPrefix: args.registryPrefix, runtimeEndpoint: endpoints.runtimeEndpoint, webEndpoint: endpoints.webEndpoint, profile, nodeId: args.nodeId, useDeployImages: args.useDeployImages, metricsSidecarSha256 })); putJson(`${runtimePath}/deepseek-proxy.yaml`, deepSeekProxyManifest({ profile, source, sourceBranch: args.sourceBranch, sourceRepo: args.sourceRepo, gitReadUrl: args.gitReadUrl, deploy, registryPrefix: args.registryPrefix, catalog: artifactCatalog, useDeployImages: args.useDeployImages, metricsSidecarSha256 })); if (isRuntimeLane(profile) && externalPostgres) putJson(`${runtimePath}/external-postgres.yaml`, externalPostgresManifest({ profile, config: externalPostgres, source, deploy, migrationSources, catalog: artifactCatalog, registryPrefix: args.registryPrefix, useDeployImages: args.useDeployImages })); if (isRuntimeLane(profile) && !externalPostgres) putJson(`${runtimePath}/postgres.yaml`, v02PostgresManifest({ profile, migrationSources, source, image: runtimePostgresImageForProfile(deploy, profile), deploy })); if (workbenchRedis) putJson(`${runtimePath}/workbench-redis.yaml`, workbenchRuntimeRedisManifest({ profile, config: workbenchRedis, source, deploy })); if (isRuntimeLane(profile)) putJson(`${runtimePath}/openfga.yaml`, v02OpenFgaManifest({ profile, source, deploy })); if (isRuntimeLane(profile)) putJson(`${runtimePath}/observability.yaml`, observabilityManifest({ deploy, profile, namespace, labels: profileLabels, annotations, metricsSidecarScript })); if (isRuntimeLane(profile)) putJson(`${runtimePath}/opencode.yaml`, opencodeServerManifest({ profile, source, deploy, catalog: artifactCatalog, registryPrefix: args.registryPrefix, useDeployImages: args.useDeployImages, sourceBranch: args.sourceBranch, gitReadUrl: args.gitReadUrl, sourceRepo: args.sourceRepo })); if (includeDeviceAgent) putJson(`${runtimePath}/device-agent-71-freq.yaml`, deviceAgent71FreqManifest({ profile, source, registryPrefix: args.registryPrefix, catalog: artifactCatalog, useDeployImages: args.useDeployImages })); putJson(`${runtimePath}/node-frpc.yaml`, nodeFrpcManifest({ profile, source, deploy, args })); } return { files, source }; } async function writeFiles(files) { for (const [filePath, content] of files) { const absolutePath = generatedPath(filePath); await mkdir(path.dirname(absolutePath), { recursive: true }); await writeFile(absolutePath, content); } } async function cleanStaleGeneratedOutput(args) { if (!isRuntimeLane(args.lane)) return; for (const relativePath of [`runtime-${args.lane}`, `tekton-${args.lane}`]) { await rm(generatedPath(path.join(args.outDir, relativePath)), { recursive: true, force: true }); } } async function checkFiles(files) { const mismatches = []; for (const [filePath, expected] of files) { let actual = null; try { actual = await readFile(generatedPath(filePath), "utf8"); } catch (error) { if (error?.code !== "ENOENT") throw error; } if (actual !== expected) mismatches.push(filePath); } return mismatches; } async function main() { let args = parseArgs(process.argv.slice(2)); if (args.help) { console.log(usage()); return; } ensureObject(args, "args"); const { files, source } = await plannedFiles(args); if (args.check) { const mismatches = await checkFiles(files); console.log(JSON.stringify({ ok: mismatches.length === 0, sourceCommit: source.full, outDir: args.outDir, checked: files.size, mismatches }, null, 2)); process.exitCode = mismatches.length === 0 ? 0 : 1; return; } if (args.write) { await cleanStaleGeneratedOutput(args); await writeFiles(files); } console.log(JSON.stringify({ ok: true, sourceCommit: source.full, sourceShort: source.short, outDir: args.outDir, files: [...files.keys()] }, null, 2)); } main().catch((error) => { console.error(error.stack || error.message); process.exitCode = 1; });