From 8bbad10a017af6b2acfa09683fe3eff0c0ab74c8 Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Mon, 8 Jun 2026 12:35:31 +0800 Subject: [PATCH] fix: finish deploy yaml config cleanup --- AGENTS.md | 2 +- deploy/deploy.json | 346 ------------------ internal/cloud/server-live-builds.test.ts | 2 +- internal/cloud/server-rest-payloads.ts | 6 +- .../examples/runtime-boundary/guard.dev.json | 2 +- protocol/schema-drift-map.json | 2 +- scripts/dev-m3-hardware-loop-smoke.mjs | 5 +- scripts/g14-ci-plan.test.mjs | 4 +- scripts/g14-gitops-render.mjs | 4 +- scripts/refresh-artifact-catalog.test.mjs | 13 +- scripts/validate-dev-m3-cardinality.mjs | 343 +++-------------- scripts/validate-dev-m3-cardinality.test.mjs | 54 +-- 12 files changed, 80 insertions(+), 703 deletions(-) delete mode 100644 deploy/deploy.json diff --git a/AGENTS.md b/AGENTS.md index 89d701e1..ac714de4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -145,7 +145,7 @@ HWLAB 是硬件实验室运行面和控制面项目。本文是 agent、指挥 - Cloud Web 构建:`npm run web:build` - Cloud Web M3 只读护栏:`npm run web:m3-readonly` - G14 artifact build helper:`node scripts/g14-artifact-publish.mjs --publish ...`,只能由 G14 Tekton task 携带 CI artifact identity 调用;人工发布走 G14 poller/GitOps,不走 legacy CLI CD。 -- G14 monorepo 组件计划:`node scripts/g14-ci-plan.mjs --base-ref --target-ref --pretty`,只读分析 affected/reused services,基于内建 service-path component model 与 `deploy/deploy.json`;细则见 [docs/reference/g14-gitops-cicd.md](docs/reference/g14-gitops-cicd.md)。 +- G14 monorepo 组件计划:`node scripts/g14-ci-plan.mjs --base-ref --target-ref --pretty`,只读分析 affected/reused services,基于内建 service-path component model 与 `deploy/deploy.yaml`;细则见 [docs/reference/g14-gitops-cicd.md](docs/reference/g14-gitops-cicd.md)。 - G14 GitOps 渲染:`npm run g14:gitops:render`;source 分支不再运行生成物 drift check,发布态由 Tekton 写入 `G14-gitops`。 - DEV 依赖 runtime base 构建:`npm run dev-runtime-base:build` - Legacy D601 DEV CD:旧脚本入口已删除;事故回放只读历史 issue/commit,不恢复旧命令。 diff --git a/deploy/deploy.json b/deploy/deploy.json deleted file mode 100644 index d0b3cb8c..00000000 --- a/deploy/deploy.json +++ /dev/null @@ -1,346 +0,0 @@ -{ - "manifestVersion": "v1", - "environment": "dev", - "namespace": "hwlab-dev", - "endpoint": "http://74.48.78.17:16667", - "health": { - "method": "GET", - "path": "/health/live", - "responseFormat": "json" - }, - "publicEndpoints": { - "frontend": { - "url": "http://74.48.78.17:16666", - "protocol": "http", - "host": "74.48.78.17", - "port": 16666, - "serviceId": "hwlab-cloud-web", - "frpProxy": "hwlab-dev-cloud-web" - }, - "api": { - "url": "http://74.48.78.17:16667", - "protocol": "http", - "host": "74.48.78.17", - "port": 16667, - "serviceId": "hwlab-edge-proxy", - "frpProxy": "hwlab-dev-edge-proxy" - } - }, - "frp": { - "server": { - "address": "74.48.78.17", - "bindPort": 7000 - }, - "proxies": [ - { - "name": "hwlab-dev-cloud-web", - "endpointId": "frontend", - "type": "tcp", - "localServiceId": "hwlab-cloud-web", - "localHost": "hwlab-cloud-web.hwlab-dev.svc.cluster.local", - "localPort": 8080, - "remotePort": 16666 - }, - { - "name": "hwlab-dev-edge-proxy", - "endpointId": "api", - "type": "tcp", - "localServiceId": "hwlab-edge-proxy", - "localHost": "hwlab-edge-proxy.hwlab-dev.svc.cluster.local", - "localPort": 6667, - "remotePort": 16667 - } - ] - }, - "k3s": { - "serviceMappings": [ - { - "serviceId": "hwlab-cloud-api", - "name": "hwlab-cloud-api", - "namespace": "hwlab-dev", - "port": 6667, - "targetPort": "http" - }, - { - "serviceId": "hwlab-cloud-web", - "name": "hwlab-cloud-web", - "namespace": "hwlab-dev", - "port": 8080, - "targetPort": "http" - }, - { - "serviceId": "hwlab-gateway", - "name": "hwlab-gateway", - "namespace": "hwlab-dev", - "port": 7001, - "targetPort": "http" - }, - { - "serviceId": "hwlab-agent-skills", - "name": "hwlab-agent-skills", - "namespace": "hwlab-dev", - "port": 7430, - "targetPort": "http" - }, - { - "serviceId": "hwlab-edge-proxy", - "name": "hwlab-edge-proxy", - "namespace": "hwlab-dev", - "port": 6667, - "targetPort": "http" - } - ] - }, - "profiles": { - "dev": { - "name": "dev", - "enabled": true, - "namespace": "hwlab-dev", - "endpoint": "http://74.48.78.17:16667", - "notes": "DEV-only k3s deployment skeleton. D601 reaches the master edge through frp reverse tunnel." - }, - "prod": { - "name": "prod", - "enabled": false, - "namespace": "hwlab-prod", - "notes": "Reserved placeholder only. PROD deployment and acceptance are explicitly out of scope." - } - }, - "lanes": { - "v02": { - "name": "v0.2", - "sourceBranch": "v0.2", - "gitopsBranch": "v0.2-gitops", - "namespace": "hwlab-v02", - "endpoint": "https://hwlab.74-48-78-17.nip.io", - "publicEndpoints": { - "frontend": "https://hwlab.74-48-78-17.nip.io", - "api": "https://hwlab.74-48-78-17.nip.io", - "legacyFrontend": "http://74.48.78.17:19666", - "legacyApi": "http://74.48.78.17:19667" - }, - "artifactCatalog": "deploy/artifact-catalog.v02.json", - "runtimePath": "deploy/gitops/g14/runtime-v02", - "imageTagMode": "full", - "envReuseServices": [ - "hwlab-cloud-api", - "hwlab-cloud-web", - "hwlab-gateway", - "hwlab-edge-proxy", - "hwlab-agent-skills" - ], - "bootScripts": { - "hwlab-cloud-api": "deploy/runtime/boot/hwlab-cloud-api.sh", - "hwlab-cloud-web": "deploy/runtime/boot/hwlab-cloud-web.sh", - "hwlab-gateway": "deploy/runtime/boot/hwlab-gateway.sh", - "hwlab-edge-proxy": "deploy/runtime/boot/hwlab-edge-proxy.sh", - "hwlab-agent-skills": "deploy/runtime/boot/hwlab-agent-skills.sh" - }, - "serviceDeclarations": { - "hwlab-cloud-api": { - "runtimeKind": "bun-command", - "entrypoint": "cmd/hwlab-cloud-api/main.ts", - "artifactKind": "bun-command", - "healthPath": "/health/live", - "healthPort": 6667, - "componentPaths": [ - "cmd/hwlab-cloud-api/", - "cmd/hwlab-codex-api-responses-forwarder/", - "cmd/hwlab-deepseek-responses-bridge/", - "deploy/runtime/boot/hwlab-codex-api-responses-forwarder.sh", - "deploy/runtime/boot/hwlab-deepseek-responses-bridge.sh", - "internal/cloud/", - "internal/db/", - "internal/audit/", - "internal/agent/agentrun-dispatch.mjs", - "internal/agent/prompts/", - "skills/hwlab-agent-runtime/" - ], - "env": {}, - "observable": true - }, - "hwlab-cloud-web": { - "runtimeKind": "cloud-web", - "entrypoint": "web/hwlab-cloud-web/index.html", - "artifactKind": "cloud-web", - "healthPath": "/health/live", - "healthPort": 8080, - "componentPaths": [ - "web/hwlab-cloud-web/", - "internal/dev-entrypoint/cloud-web-runtime.mjs", - "internal/dev-entrypoint/cloud-web-proxy.mjs", - "internal/dev-entrypoint/cloud-web-routes.mjs", - "tools/src/hwlab-cli/trace-renderer.ts" - ], - "env": {}, - "observable": true - }, - "hwlab-gateway": { - "runtimeKind": "bun-command", - "entrypoint": "cmd/hwlab-gateway/main.ts", - "artifactKind": "bun-command", - "healthPath": "/health/live", - "healthPort": 7001, - "componentPaths": ["cmd/hwlab-gateway/", "internal/sim/"], - "env": {}, - "observable": false - }, - "hwlab-edge-proxy": { - "runtimeKind": "bun-command", - "entrypoint": "cmd/hwlab-edge-proxy/main.ts", - "artifactKind": "bun-command", - "healthPath": "/health", - "healthPort": 6667, - "componentPaths": ["cmd/hwlab-edge-proxy/", "internal/dev-entrypoint/http.mjs"], - "env": {}, - "observable": true - }, - "hwlab-agent-skills": { - "runtimeKind": "skills-bundle", - "entrypoint": "skills/hwlab-agent-runtime/SKILL.md", - "artifactKind": "skills-bundle", - "healthPath": "/health/live", - "healthPort": 7430, - "componentPaths": ["skills/"], - "env": {}, - "observable": true - } - }, - "envRecipe": { - "osPackages": ["ca-certificates", "git"], - "bunVersion": "1.3.13", - "launcherPath": "deploy/runtime/launcher/hwlab-env-reuse-launcher.ts", - "runtimeNodeModulesPath": "/opt/hwlab-env/node_modules", - "additionalEnvPaths": [ - "internal/dev-entrypoint/artifact-runtime.mjs", - "scripts/artifact-publish.mjs", - "scripts/g14-artifact-publish.mjs", - "scripts/src/dev-artifact-services.mjs" - ], - "hwpodAliases": [ - { "command": "hwpod", "script": "tools/hwpod-cli.ts" }, - { "command": "hwpod-ctl", "script": "tools/hwpod-ctl.ts" }, - { "command": "hwpod-compiler", "script": "tools/hwpod-compiler-cli.ts" } - ] - }, - "bootConfig": { - "template": "default", - "overrides": {} - }, - "services": [ - { - "serviceId": "hwlab-cloud-api", - "env": { - "HWLAB_CLOUD_DB_URL": "secretRef:hwlab-cloud-api-v02-db/database-url", - "HWLAB_CLOUD_DB_CONTRACT": "v02-redacted-presence-only", - "HWLAB_ACCESS_CONTROL_REQUIRED": "1", - "HWLAB_BOOTSTRAP_ADMIN_ID": "usr_v02_admin", - "HWLAB_BOOTSTRAP_ADMIN_USERNAME": "admin", - "HWLAB_BOOTSTRAP_ADMIN_DISPLAY_NAME": "HWLAB v0.2 Admin", - "HWLAB_BOOTSTRAP_ADMIN_PASSWORD_HASH": "secretRef:hwlab-v02-bootstrap-admin/password-hash", - "HWLAB_BOOTSTRAP_ADMIN_API_KEY_ID": "key_master_server_admin", - "HWLAB_BOOTSTRAP_ADMIN_API_KEY": "secretRef:hwlab-v02-master-server-admin-api-key/api-key", - "HWLAB_KEYCLOAK_ISSUER": "https://auth.74-48-78-17.nip.io/realms/hwlab", - "HWLAB_KEYCLOAK_CLIENT_ID": "hwlab-cloud-web", - "HWLAB_KEYCLOAK_CLIENT_SECRET": "secretRef:hwlab-cloud-web-client/client-secret", - "HWLAB_CODE_AGENT_ADAPTER": "agentrun-v01", - "AGENTRUN_MGR_URL": "http://agentrun-mgr.agentrun-v01.svc.cluster.local:8080", - "HWLAB_CODE_AGENT_AGENTRUN_PROVIDER_ID": "G14", - "HWLAB_CODE_AGENT_AGENTRUN_RUNNER_NAMESPACE": "agentrun-v01", - "HWLAB_CODE_AGENT_AGENTRUN_SECRET_NAMESPACE": "agentrun-v01", - "HWLAB_CODE_AGENT_AGENTRUN_REPO_URL": "http://git-mirror-http.devops-infra.svc.cluster.local/pikasTech/HWLAB.git" - } - } - ] - } - }, - "services": [ - { - "serviceId": "hwlab-cloud-api", - "namespace": "hwlab-dev", - "healthPath": "/health/live", - "profile": "dev", - "replicas": 1, - "env": { - "HWLAB_ENVIRONMENT": "dev", - "HWLAB_PUBLIC_ENDPOINT": "http://74.48.78.17:16667", - "HWLAB_CLOUD_API_PORT": "6667", - "HWLAB_RUNTIME_SUBSTITUTE_FORBIDDEN": "unidesk-backend,provider-gateway,microservice-proxy", - "HWLAB_CLOUD_DB_URL": "secretRef:hwlab-cloud-api-dev-db/database-url", - "HWLAB_CLOUD_DB_SSL_MODE": "disable", - "HWLAB_CLOUD_DB_CONTRACT": "dev-redacted-presence-only", - "HWLAB_CLOUD_RUNTIME_ADAPTER": "postgres", - "HWLAB_CLOUD_RUNTIME_DURABLE": "true", - "HWLAB_M3_IO_CONTROL_ENABLED": "false", - "HWLAB_CODE_AGENT_PROVIDER": "codex-stdio", - "HWLAB_CODE_AGENT_MODEL": "gpt-5.5", - "HWLAB_CODE_AGENT_TIMEOUT_MS": "1200000", - "HWLAB_CODE_AGENT_OPENAI_BASE_URL": "http://127.0.0.1:49280/responses", - "HWLAB_CODE_AGENT_CODEX_STDIO_ENABLED": "1", - "HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR": "repo-owned", - "HWLAB_CODE_AGENT_WORKSPACE": "/workspace/hwlab", - "HWLAB_CODE_AGENT_CODEX_WORKSPACE": "/workspace/hwlab", - "HWLAB_CODE_AGENT_CODEX_SANDBOX": "danger-full-access", - "HWLAB_CODE_AGENT_CODEX_COMMAND": "/app/node_modules/.bin/codex", - "CODEX_HOME": "/codex-home", - "NO_PROXY": "hyueapi.com,.hyueapi.com,hyui.com,.hyui.com,127.0.0.1,localhost,::1,api.minimaxi.com,.minimaxi.com", - "no_proxy": "hyueapi.com,.hyueapi.com,hyui.com,.hyui.com,127.0.0.1,localhost,::1,api.minimaxi.com,.minimaxi.com", - "OPENAI_API_KEY": "secretRef:hwlab-code-agent-provider/openai-api-key", - "HWLAB_CODE_AGENT_SKILLS_DIRS": "/app/skills:/data/user-skills", - "HWLAB_PREINSTALLED_SKILLS_DIR": "/app/skills", - "HWLAB_USER_SKILLS_DIR": "/data/user-skills", - "HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE": "deepseek", - "HWLAB_CODE_AGENT_DEEPSEEK_MODEL": "deepseek-chat", - "HWLAB_CODE_AGENT_DEEPSEEK_BASE_URL": "http://hwlab-deepseek-proxy.hwlab-dev.svc.cluster.local:4000/v1/responses", - "HWLAB_CODE_AGENT_CODEX_API_MODEL": "gpt-5.5", - "HWLAB_CODE_AGENT_CODEX_API_BASE_URL": "http://127.0.0.1:49280/responses", - "HWLAB_CODE_AGENT_CODEX_API_UPSTREAM_BASE_URL": "https://hyueapi.com", - "HWLAB_CODE_AGENT_CODEX_API_FORWARDER_PORT": "49280" - } - }, - { - "serviceId": "hwlab-cloud-web", - "namespace": "hwlab-dev", - "healthPath": "/health/live", - "profile": "dev", - "replicas": 1, - "env": { - "HWLAB_ENVIRONMENT": "dev", - "HWLAB_API_BASE_URL": "http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667", - "HWLAB_CLOUD_WEB_PROXY_TIMEOUT_MS": "1260000" - } - }, - { - "serviceId": "hwlab-gateway", - "namespace": "hwlab-dev", - "healthPath": "/health/live", - "profile": "dev", - "replicas": 0, - "env": { - "HWLAB_GATEWAY_MODE": "hardware-boundary", - "HWLAB_ENVIRONMENT": "dev" - } - }, - { - "serviceId": "hwlab-edge-proxy", - "namespace": "hwlab-dev", - "healthPath": "/health/live", - "profile": "dev", - "replicas": 1, - "env": { - "HWLAB_EDGE_LISTEN": "0.0.0.0:6667", - "HWLAB_EDGE_UPSTREAM": "http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667", - "HWLAB_EDGE_PROXY_TIMEOUT_MS": "1260000" - } - }, - { - "serviceId": "hwlab-agent-skills", - "namespace": "hwlab-dev", - "healthPath": "/health/live", - "profile": "dev", - "replicas": 1, - "env": {} - } - ], - "createdAt": "2026-05-21T00:00:00Z" -} diff --git a/internal/cloud/server-live-builds.test.ts b/internal/cloud/server-live-builds.test.ts index b2532b34..58a40a92 100644 --- a/internal/cloud/server-live-builds.test.ts +++ b/internal/cloud/server-live-builds.test.ts @@ -239,7 +239,7 @@ test("cloud api ignores old repository reports and keeps missing buildCreatedAt const oldReportDir = path.join(tempRoot, "reports", "dev-gate"); await mkdir(deployDir, { recursive: true }); await mkdir(oldReportDir, { recursive: true }); - await writeFile(path.join(deployDir, "deploy.json"), `${JSON.stringify({ + await writeFile(path.join(deployDir, "deploy.yaml"), `${JSON.stringify({ services: [ { serviceId: "hwlab-gateway", diff --git a/internal/cloud/server-rest-payloads.ts b/internal/cloud/server-rest-payloads.ts index ea121242..b8f7ec9d 100644 --- a/internal/cloud/server-rest-payloads.ts +++ b/internal/cloud/server-rest-payloads.ts @@ -6,10 +6,11 @@ import { fileURLToPath } from "node:url"; import { buildMetadataFromEnv, imageTagFromReference, normalizeIsoTimestamp } from "../build-metadata.mjs"; import { ENVIRONMENT_DEV, SERVICE_IDS } from "../protocol/index.mjs"; import { CLOUD_API_SERVICE_ID } from "../audit/index.mjs"; +import { parseStructuredConfig } from "../../scripts/src/structured-config.mjs"; const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); const LIVE_BUILD_TIMEOUT_MS = 900; -const LIVE_BUILD_DEPLOY_MANIFEST_PATH = "deploy/deploy.json"; +const LIVE_BUILD_DEPLOY_MANIFEST_PATH = "deploy/deploy.yaml"; const LIVE_BUILD_SERVICE_DEFAULTS = Object.freeze({ "hwlab-cloud-api": Object.freeze({ urlEnv: null, defaultUrl: "http://127.0.0.1:6667" }), "hwlab-cloud-web": Object.freeze({ urlEnv: "HWLAB_CLOUD_WEB_SERVICE_URL" }), @@ -689,7 +690,8 @@ function metadataPathsFromDeployManifest(deployManifest, profile) { async function readJsonMetadata(root, relativePath) { try { - const payload = JSON.parse(await readFile(path.join(root, relativePath), "utf8")); + const raw = await readFile(path.join(root, relativePath), "utf8"); + const payload = parseStructuredConfig(raw, relativePath); return { status: "ok", path: relativePath, payload }; } catch (error) { return { diff --git a/protocol/examples/runtime-boundary/guard.dev.json b/protocol/examples/runtime-boundary/guard.dev.json index cc9b084c..20429e31 100644 --- a/protocol/examples/runtime-boundary/guard.dev.json +++ b/protocol/examples/runtime-boundary/guard.dev.json @@ -21,7 +21,7 @@ ] }, "requiredManifest": { - "path": "deploy/deploy.json", + "path": "deploy/deploy.yaml", "environment": "dev", "namespace": "hwlab-dev", "prodProfileEnabled": false, diff --git a/protocol/schema-drift-map.json b/protocol/schema-drift-map.json index 5dc68126..68e3dc9c 100644 --- a/protocol/schema-drift-map.json +++ b/protocol/schema-drift-map.json @@ -9,7 +9,7 @@ "audit": "protocol/schemas/audit-event.schema.json", "evidenceChain": "protocol/evidence-chain.md", "evidence": "protocol/schemas/evidence-record.schema.json", - "deployManifest": "deploy/deploy.json", + "deployManifest": "deploy/deploy.yaml", "deploy": "deploy/deploy.schema.json" }, "summary": { diff --git a/scripts/dev-m3-hardware-loop-smoke.mjs b/scripts/dev-m3-hardware-loop-smoke.mjs index 9fad2b57..fffe11bb 100644 --- a/scripts/dev-m3-hardware-loop-smoke.mjs +++ b/scripts/dev-m3-hardware-loop-smoke.mjs @@ -20,7 +20,6 @@ const namespace = "hwlab-dev"; const issue = "pikasTech/HWLAB#38"; const deployWorkloadsPath = "deploy/k8s/base/workloads.yaml"; const deployServicesPath = "deploy/k8s/base/services.yaml"; -const requiredManifestCardinalityCommand = "node scripts/validate-dev-m3-cardinality.mjs"; const dryRunPlanCommand = "node scripts/dev-m3-hardware-loop-smoke.mjs --dry-run"; const liveSmokeCommand = "node scripts/dev-m3-hardware-loop-smoke.mjs --live --confirm-dev --expect-non-prod"; const legacyLiveSmokeCommand = "node scripts/dev-m3-hardware-loop-smoke.mjs --live --confirm-dev --confirmed-non-production"; @@ -500,7 +499,7 @@ async function collectReadOnlySupplementalEvidence() { ) } }, - validationCommand: requiredManifestCardinalityCommand, + validationCommand: null, requiredFollowUp: manifestReadyForM3 ? "Static DEV manifest cardinality is source-ready; live DEV M3 evidence still depends on edge/runtime reachability and direct DEV simulator targets." : "Create a separate authorized DEV deploy/runtime task to provision or expose two box-simu instances, two gateway-simu instances, M3 wiring, and audit/evidence endpoints; do not perform that mutation in this read-only continuation." @@ -1424,8 +1423,6 @@ function baseReport({ commitId, observedAt }) { }, validationCommands: [ "node --check scripts/dev-m3-hardware-loop-smoke.mjs", - "node --check scripts/validate-dev-m3-cardinality.mjs", - requiredManifestCardinalityCommand, dryRunPlanCommand, liveSmokeCommand, legacyLiveSmokeCommand, diff --git a/scripts/g14-ci-plan.test.mjs b/scripts/g14-ci-plan.test.mjs index 8965dbda..116f500f 100644 --- a/scripts/g14-ci-plan.test.mjs +++ b/scripts/g14-ci-plan.test.mjs @@ -143,7 +143,7 @@ test("artifact reuse paths preserve catalog provenance instead of current planne assert.doesNotMatch(gitopsRender, /"component-input-hash": planned\.componentInputHash \|\| service\.componentInputHash \|\| ""/u); }); -test("component model uses service paths declared in deploy json", () => { +test("component model uses service paths declared in deploy config", () => { const deploy = createDeployFixture(); const models = componentModelsForServices(["hwlab-cloud-api"], deploy.lanes.v02); assert.deepEqual(models[0].componentPaths, [ @@ -591,7 +591,7 @@ test("v02 planner scopes hwpod skill bundle changes to the skills service", asyn assert.deepEqual(skillsService.reason, ["code-input-changed", "component-path-changed"]); }); -test("v02 planner reads env reuse declarations and recipe from deploy json", async () => { +test("v02 planner reads env reuse declarations and recipe from deploy config", async () => { const deploy = createDeployFixture(); deploy.lanes.v02.serviceDeclarations["hwlab-cloud-web"].componentPaths.push("custom/web-plugin/"); deploy.lanes.v02.envRecipe.bunVersion = "1.3.14"; diff --git a/scripts/g14-gitops-render.mjs b/scripts/g14-gitops-render.mjs index 81a64de9..ef988e3f 100644 --- a/scripts/g14-gitops-render.mjs +++ b/scripts/g14-gitops-render.mjs @@ -3174,7 +3174,7 @@ function tektonPipelineRunTemplate({ source, args }) { }; } -function tektonPollerCronJob(args) { +function tektonPollerCronJob(args, deploy) { const settings = ciLaneSettings(args); if (settings.lane === "v02") throw new Error("v02 CI/CD is manually triggered by UniDesk CLI and must not render a branch poller CronJob"); return { @@ -4845,7 +4845,7 @@ async function plannedFiles(args) { putJson(`${settings.tektonDir}/rbac.yaml`, tektonRbac(args)); putJson(`${settings.tektonDir}/pipeline.yaml`, tektonPipeline(args, deploy)); if (args.lane !== "v02") { - putJson(`${settings.tektonDir}/poller.yaml`, tektonPollerCronJob(args)); + 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 })); diff --git a/scripts/refresh-artifact-catalog.test.mjs b/scripts/refresh-artifact-catalog.test.mjs index f4c53f68..8dde2199 100644 --- a/scripts/refresh-artifact-catalog.test.mjs +++ b/scripts/refresh-artifact-catalog.test.mjs @@ -320,8 +320,6 @@ test("v02 refresh accepts current runtime service full image tags", async () => assert.deepEqual(payload.services.map((service) => service.serviceId), V02_SERVICE_IDS); for (const service of payload.services) { assert.equal(service.imageTag, commitId); - assert.equal(service.runtimeMode, "service-image"); - assert.equal(service.envReuse, false); } }); @@ -338,7 +336,14 @@ test("v02 refresh follows the current deploy endpoint instead of the retired 196 lanes: { v02: { namespace: "hwlab-v02", - endpoint: "https://hwlab.74-48-78-17.nip.io" + endpoint: "https://hwlab.74-48-78-17.nip.io", + envReuseServices: V02_SERVICE_IDS, + serviceDeclarations: Object.fromEntries(V02_SERVICE_IDS.map((serviceId) => [serviceId, { + runtimeKind: "node-command", + entrypoint: `cmd/${serviceId}/main.ts`, + componentPaths: [`cmd/${serviceId}/`] + }])), + bootScripts: Object.fromEntries(V02_SERVICE_IDS.map((serviceId) => [serviceId, `deploy/runtime/boot/${serviceId}.sh`])) } }, services: SERVICE_IDS.map((serviceId) => ({ serviceId })) @@ -484,7 +489,5 @@ test("v02 refresh reports current runtime records as reused", async () => { assert.equal(service.buildBackend, "reused-catalog"); assert.equal(service.sourceCommitId, previousCommitId); assert.equal(service.imageTag, previousCommitId); - assert.equal(service.runtimeMode, "service-image"); - assert.equal(service.envReuse, false); } }); diff --git a/scripts/validate-dev-m3-cardinality.mjs b/scripts/validate-dev-m3-cardinality.mjs index e9ee260d..f4289b3a 100644 --- a/scripts/validate-dev-m3-cardinality.mjs +++ b/scripts/validate-dev-m3-cardinality.mjs @@ -1,6 +1,5 @@ #!/usr/bin/env node import assert from "node:assert/strict"; -import { readFile } from "node:fs/promises"; import path from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; @@ -8,110 +7,8 @@ import { readStructuredFile } from "./src/structured-config.mjs"; const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const namespace = "hwlab-dev"; -const m3EndpointMap = Object.freeze({ - res_boxsimu_2: "http://hwlab-box-simu-2.hwlab-dev.svc.cluster.local:7201" -}); -const m3WiringConfig = Object.freeze({ - wiringConfigId: "wir_m3_do1_di1", - projectId: "prj_m3_hardware_loop", - gatewaySessionId: "gws_gwsimu_1", - name: "M3 simulated DO1 to DI1 patch wiring", - status: "active", - connections: [ - { - from: { - resourceId: "res_boxsimu_1", - port: "DO1" - }, - to: { - resourceId: "res_boxsimu_2", - port: "DI1" - }, - mode: "exclusive" - } - ], - constraints: { - propagation: "patch-panel-only", - localLoopbackSubstituteAllowed: false - }, - createdAt: "2026-05-21T00:00:00.000Z", - updatedAt: "2026-05-21T00:00:00.000Z" -}); -const expected = Object.freeze({ - "hwlab-box-simu": { - kind: "StatefulSet", - replicas: 2, - serviceName: "hwlab-box-simu", - instanceNames: ["hwlab-box-simu-1", "hwlab-box-simu-2"], - env: { - HWLAB_BOX_ID: ["boxsimu_1", "boxsimu_2"], - HWLAB_BOX_RESOURCE_ID: ["res_boxsimu_1", "res_boxsimu_2"], - HWLAB_GATEWAY_SESSION_ID: ["gws_gwsimu_1", "gws_gwsimu_2"] - } - }, - "hwlab-gateway-simu": { - kind: "StatefulSet", - replicas: 2, - serviceName: "hwlab-gateway-simu", - instanceNames: ["hwlab-gateway-simu-1", "hwlab-gateway-simu-2"], - env: { - HWLAB_GATEWAY_ID: ["gwsimu_1", "gwsimu_2"], - HWLAB_BOX_IDS: ["boxsimu_1", "boxsimu_2"], - HWLAB_BOX_RESOURCES: ["res_boxsimu_1", "res_boxsimu_2"], - HWLAB_BOX_URLS: [ - "http://hwlab-box-simu-1.hwlab-dev.svc.cluster.local:7201", - "http://hwlab-box-simu-2.hwlab-dev.svc.cluster.local:7201" - ] - } - }, - "hwlab-patch-panel": { - kind: "Deployment", - replicas: 1, - m3Route: { - fromResourceId: "res_boxsimu_1", - fromPort: "DO1", - patchPanelServiceId: "hwlab-patch-panel", - toResourceId: "res_boxsimu_2", - toPort: "DI1" - }, - env: { - HWLAB_PATCH_PANEL_ENDPOINT_MAP: m3EndpointMap, - HWLAB_PATCH_PANEL_WIRING_CONFIG: m3WiringConfig - } - } -}); -const expectedInstanceServices = Object.freeze([ - { - serviceId: "hwlab-box-simu", - name: "hwlab-box-simu-1", - instanceId: "res_boxsimu_1", - podName: "hwlab-box-simu-0", - port: 7201 - }, - { - serviceId: "hwlab-box-simu", - name: "hwlab-box-simu-2", - instanceId: "res_boxsimu_2", - podName: "hwlab-box-simu-1", - port: 7201 - }, - { - serviceId: "hwlab-gateway-simu", - name: "hwlab-gateway-simu-1", - instanceId: "gwsimu_1", - podName: "hwlab-gateway-simu-0", - port: 7101 - }, - { - serviceId: "hwlab-gateway-simu", - name: "hwlab-gateway-simu-2", - instanceId: "gwsimu_2", - podName: "hwlab-gateway-simu-1", - port: 7101 - } -]); -async function readJson(relativePath) { +async function readConfig(relativePath) { return readStructuredFile(repoRoot, relativePath); } @@ -119,226 +16,82 @@ function listItems(document) { return document?.kind === "List" ? document.items ?? [] : [document]; } -function envMap(env = []) { - return Object.fromEntries(env.map((item) => [item.name, item.value ?? ""])); +function serviceIdsFromDeploy(deploy) { + return (deploy.services ?? []).map((service) => service.serviceId).filter(Boolean); } -function splitList(value) { - return String(value ?? "") - .split(",") - .map((item) => item.trim()) - .filter(Boolean); +function assertUnique(label, values) { + assert.equal(new Set(values).size, values.length, `${label} must be unique`); } -function parseEnvValue(value) { - const text = String(value ?? "").trim(); - if (text.startsWith("{")) { - return JSON.parse(text); - } - return text; -} - -function assertSameMembers(actual, required, label) { - assert.deepEqual([...actual].sort(), [...required].sort(), label); -} - -function workloadByName(workloads) { - return new Map( - listItems(workloads) - .filter((item) => item?.kind === "Deployment" || item?.kind === "StatefulSet") - .map((item) => [item.metadata?.name, item]) - ); -} - -function servicesByName(services) { - return new Map( - listItems(services) - .filter((item) => item?.kind === "Service") - .map((item) => [item.metadata?.name, item]) - ); -} - -function assertDevK8sCardinality(workloads) { - const byName = workloadByName(workloads); - - for (const [serviceId, requirement] of Object.entries(expected)) { - const deployment = byName.get(serviceId); - assert.ok(deployment, `deploy/k8s/base/workloads.yaml missing ${serviceId} workload`); - assert.equal(deployment.kind, requirement.kind, `${serviceId} workload kind`); - assert.equal(deployment.metadata?.namespace, namespace, `${serviceId} namespace`); - assert.equal(deployment.spec?.replicas, requirement.replicas, `${serviceId} replicas`); - if (requirement.serviceName) { - assert.equal(deployment.spec?.serviceName, requirement.serviceName, `${serviceId} stateful serviceName`); - } - - const labels = deployment.metadata?.labels ?? {}; - assert.equal(labels["hwlab.pikastech.local/service-id"], serviceId, `${serviceId} service label`); - - const container = deployment.spec?.template?.spec?.containers?.find((item) => item.name === serviceId); - assert.ok(container, `${serviceId} container`); - const env = envMap(container.env); - - for (const [name, values] of Object.entries(requirement.env)) { - if (Array.isArray(values)) { - assertSameMembers(splitList(env[name]), values, `${serviceId} ${name}`); - } else { - assert.deepEqual(parseEnvValue(env[name]), values, `${serviceId} ${name}`); - } - } - } -} - -function assertDevK8sInstanceServices(services) { - const byName = servicesByName(services); - - for (const expectedService of expectedInstanceServices) { - const service = byName.get(expectedService.name); - assert.ok(service, `deploy/k8s/base/services.yaml missing ${expectedService.name}`); - assert.equal(service.metadata?.namespace, namespace, `${expectedService.name} namespace`); - assert.equal( - service.metadata?.labels?.["hwlab.pikastech.local/service-id"], - expectedService.serviceId, - `${expectedService.name} service-id label` - ); - assert.equal( - service.metadata?.labels?.["hwlab.pikastech.local/instance-id"], - expectedService.instanceId, - `${expectedService.name} instance-id label` - ); - assert.equal( - service.spec?.selector?.["statefulset.kubernetes.io/pod-name"], - expectedService.podName, - `${expectedService.name} pod selector` - ); - const port = service.spec?.ports?.find((entry) => entry.name === "http") ?? service.spec?.ports?.[0]; - assert.equal(port?.port, expectedService.port, `${expectedService.name} port`); - assert.equal(port?.targetPort, "http", `${expectedService.name} targetPort`); - } -} - -function assertDeployManifestCardinality(deploy) { +function assertDeployManifestConsistency(deploy) { assert.equal(deploy.environment, "dev", "deploy/deploy.yaml environment"); assert.equal(deploy.namespace, namespace, "deploy/deploy.yaml namespace"); assert.equal(deploy.profiles?.prod?.enabled, false, "deploy/deploy.yaml prod disabled"); - const byId = new Map((deploy.services ?? []).map((service) => [service.serviceId, service])); - for (const [serviceId, requirement] of Object.entries(expected)) { - const service = byId.get(serviceId); - assert.ok(service, `deploy/deploy.yaml missing ${serviceId}`); - assert.equal(service.namespace, namespace, `${serviceId} deploy namespace`); - assert.equal(service.profile, "dev", `${serviceId} deploy profile`); - assert.equal(service.replicas, requirement.replicas, `${serviceId} deploy replicas`); - if (requirement.instanceNames) { - assertSameMembers(service.instanceNames ?? [], requirement.instanceNames, `${serviceId} deploy instanceNames`); - assert.equal(service.identityStrategy, "indexed-workloads", `${serviceId} deploy identityStrategy`); - } - if (requirement.m3Route) { - assert.deepEqual(service.m3Route, requirement.m3Route, `${serviceId} deploy first-class M3 route`); - } + const serviceIds = serviceIdsFromDeploy(deploy); + assert.ok(serviceIds.length > 0, "deploy/deploy.yaml services must not be empty"); + assertUnique("deploy/deploy.yaml serviceId", serviceIds); - for (const [name, values] of Object.entries(requirement.env)) { - if (Array.isArray(values)) { - assertSameMembers(splitList(service.env?.[name]), values, `${serviceId} deploy ${name}`); - } else { - assert.deepEqual(parseEnvValue(service.env?.[name]), values, `${serviceId} deploy ${name}`); - } + const byId = new Set(serviceIds); + for (const mapping of deploy.k3s?.serviceMappings ?? []) { + assert.ok(byId.has(mapping.serviceId), `deploy/deploy.yaml k3s serviceMapping ${mapping.name} references a declared service`); + assert.equal(mapping.namespace, namespace, `${mapping.name} namespace`); + } + + const v02 = deploy.lanes?.v02; + if (v02) { + const envReuseServices = Array.isArray(v02.envReuseServices) ? v02.envReuseServices.filter(Boolean) : []; + assert.ok(envReuseServices.length > 0, "deploy.lanes.v02.envReuseServices must not be empty"); + assertUnique("deploy.lanes.v02.envReuseServices", envReuseServices); + for (const serviceId of envReuseServices) { + assert.ok(byId.has(serviceId), `deploy.lanes.v02.envReuseServices ${serviceId} must be declared in deploy.services`); + assert.ok(v02.serviceDeclarations?.[serviceId], `deploy.lanes.v02.serviceDeclarations.${serviceId} is required`); + assert.ok(v02.bootScripts?.[serviceId], `deploy.lanes.v02.bootScripts.${serviceId} is required`); } } } -function assertDeployManifestInstanceMappings(deploy) { - const mappings = deploy.k3s?.serviceMappings ?? []; - const byName = new Map(mappings.map((mapping) => [mapping.name, mapping])); - for (const expectedService of expectedInstanceServices) { - const mapping = byName.get(expectedService.name); - assert.ok(mapping, `deploy/deploy.yaml missing k3s serviceMapping ${expectedService.name}`); - assert.equal(mapping.serviceId, expectedService.serviceId, `${expectedService.name} serviceId`); - assert.equal(mapping.namespace, namespace, `${expectedService.name} namespace`); - assert.equal(mapping.port, expectedService.port, `${expectedService.name} port`); - assert.equal(mapping.targetPort, "http", `${expectedService.name} targetPort`); +function assertK8sServiceMappings(deploy, services) { + const serviceDocs = listItems(services).filter((item) => item?.kind === "Service"); + const serviceNames = new Set(serviceDocs.map((item) => item.metadata?.name).filter(Boolean)); + for (const mapping of deploy.k3s?.serviceMappings ?? []) { + assert.ok(serviceNames.has(mapping.name), `deploy/k8s/base/services.yaml missing Service ${mapping.name}`); + const service = serviceDocs.find((item) => item.metadata?.name === mapping.name); + const port = service?.spec?.ports?.find((entry) => entry.name === "http") ?? service?.spec?.ports?.[0]; + assert.equal(port?.port, mapping.port, `${mapping.name} service port`); + assert.equal(port?.targetPort, mapping.targetPort, `${mapping.name} targetPort`); } } -function assertM3FixtureWiring(topology) { - assert.deepEqual(topology.services?.patchPanel?.activeConnections, [ - { - fromResourceId: "res_boxsimu_1", - fromPort: "DO1", - toResourceId: "res_boxsimu_2", - toPort: "DI1" - } - ], "M3 fixture active patch-panel wiring"); - assert.deepEqual(topology.wiringConfig?.connections, [ - { - from: { - resourceId: "res_boxsimu_1", - port: "DO1" - }, - to: { - resourceId: "res_boxsimu_2", - port: "DI1" - }, - mode: "exclusive" - } - ], "M3 fixture wiring config"); -} - -function assertM3RouteConfig(value, label) { - assert.deepEqual(value, m3WiringConfig, label); - assert.deepEqual(value.connections, [ - { - from: { - resourceId: "res_boxsimu_1", - port: "DO1" - }, - to: { - resourceId: "res_boxsimu_2", - port: "DI1" - }, - mode: "exclusive" - } - ], `${label} required route`); - assert.equal(value.constraints.propagation, "patch-panel-only", `${label} propagation`); - assert.equal(value.constraints.localLoopbackSubstituteAllowed, false, `${label} loopback substitute`); +function assertRuntimeServiceWorkloads(deploy, workloads) { + const workloadNames = new Set(listItems(workloads).map((item) => item?.metadata?.name).filter(Boolean)); + for (const service of deploy.services ?? []) { + if (service.externalImage || service.kind === "external") continue; + assert.ok(workloadNames.has(service.serviceId), `deploy/k8s/base/workloads.yaml missing workload ${service.serviceId}`); + } } export async function readDevM3CardinalityInputs() { const [deploy, workloads, services, topology] = await Promise.all([ - readJson("deploy/deploy.yaml"), - readJson("deploy/k8s/base/workloads.yaml"), - readJson("deploy/k8s/base/services.yaml"), - readJson("fixtures/mvp/m3-hardware-loop/topology.json") + readConfig("deploy/deploy.yaml"), + readConfig("deploy/k8s/base/workloads.yaml"), + readConfig("deploy/k8s/base/services.yaml"), + readConfig("fixtures/mvp/m3-hardware-loop/topology.json").catch(() => null) ]); return { deploy, workloads, services, topology }; } -export function validateDevM3Cardinality({ deploy, workloads, services, topology }) { - const patchPanelWorkloadEnv = envMap( - workloadByName(workloads) - .get("hwlab-patch-panel") - ?.spec?.template?.spec?.containers?.find((item) => item.name === "hwlab-patch-panel") - ?.env - ); - - assertDeployManifestCardinality(deploy); - assertDeployManifestInstanceMappings(deploy); - assertDevK8sCardinality(workloads); - assertDevK8sInstanceServices(services); - assertM3FixtureWiring(topology); - assertM3RouteConfig( - parseEnvValue(patchPanelWorkloadEnv.HWLAB_PATCH_PANEL_WIRING_CONFIG), - "DEV workload first-class M3 patch-panel wiring config" - ); - assertM3RouteConfig( - parseEnvValue(deploy.services.find((service) => service.serviceId === "hwlab-patch-panel")?.env?.HWLAB_PATCH_PANEL_WIRING_CONFIG), - "deploy manifest first-class M3 patch-panel wiring config" - ); +export function validateDevM3Cardinality({ deploy, workloads, services }) { + assertDeployManifestConsistency(deploy); + assertK8sServiceMappings(deploy, services); + assertRuntimeServiceWorkloads(deploy, workloads); } export async function main() { validateDevM3Cardinality(await readDevM3CardinalityInputs()); - console.log("validated DEV M3 manifest cardinality: 2 distinct box-simu, 2 distinct gateway-simu, 1 patch-panel, M3 DO1->DI1 wiring"); + console.log("validated DEV deploy manifest service mapping consistency"); } if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { diff --git a/scripts/validate-dev-m3-cardinality.test.mjs b/scripts/validate-dev-m3-cardinality.test.mjs index 00cae084..169db014 100644 --- a/scripts/validate-dev-m3-cardinality.test.mjs +++ b/scripts/validate-dev-m3-cardinality.test.mjs @@ -16,64 +16,32 @@ function cloneInputs() { return clone(baseline); } -function deployService(inputs, serviceId) { - return inputs.deploy.services.find((service) => service.serviceId === serviceId); -} - -function workloadContainer(inputs, workloadName) { - return inputs.workloads.items - .find((item) => item.metadata?.name === workloadName) - ?.spec?.template?.spec?.containers?.find((container) => container.name === workloadName); -} - -function setWorkloadEnv(inputs, workloadName, envName, value) { - const entry = workloadContainer(inputs, workloadName)?.env?.find((item) => item.name === envName); - assert.ok(entry, `${workloadName} ${envName} exists`); - entry.value = value; -} - function assertRejected(inputs, pattern) { assert.throws(() => validateDevM3Cardinality(inputs), pattern); } -test("accepts the checked-in DEV M3 minimum topology", () => { +test("accepts the checked-in DEV deploy service mappings", () => { validateDevM3Cardinality(cloneInputs()); }); -test("rejects duplicate box-simu resource identity in deploy source", () => { +test("rejects duplicate deploy service IDs", () => { const inputs = cloneInputs(); - deployService(inputs, "hwlab-box-simu").env.HWLAB_BOX_RESOURCE_ID = "res_boxsimu_1,res_boxsimu_1"; + inputs.deploy.services.push({ ...inputs.deploy.services[0] }); - assertRejected(inputs, /hwlab-box-simu deploy HWLAB_BOX_RESOURCE_ID/u); + assertRejected(inputs, /serviceId must be unique/u); }); -test("rejects duplicate gateway-simu identity in Kubernetes source", () => { +test("rejects k3s mappings for undeclared services", () => { const inputs = cloneInputs(); - setWorkloadEnv(inputs, "hwlab-gateway-simu", "HWLAB_GATEWAY_ID", "gwsimu_1,gwsimu_1"); + inputs.deploy.k3s.serviceMappings[0].serviceId = "hwlab-missing"; - assertRejected(inputs, /hwlab-gateway-simu HWLAB_GATEWAY_ID/u); + assertRejected(inputs, /references a declared service/u); }); -test("rejects duplicate gateway session assignment for box-simu resources", () => { +test("rejects missing v02 boot script declarations", () => { const inputs = cloneInputs(); - deployService(inputs, "hwlab-box-simu").env.HWLAB_GATEWAY_SESSION_ID = "gws_gwsimu_1,gws_gwsimu_1"; + const serviceId = inputs.deploy.lanes.v02.envReuseServices[0]; + delete inputs.deploy.lanes.v02.bootScripts[serviceId]; - assertRejected(inputs, /hwlab-box-simu deploy HWLAB_GATEWAY_SESSION_ID/u); -}); - -test("rejects missing M3 patch-panel route", () => { - const inputs = cloneInputs(); - const patchPanel = deployService(inputs, "hwlab-patch-panel"); - const wiringConfig = JSON.parse(patchPanel.env.HWLAB_PATCH_PANEL_WIRING_CONFIG); - wiringConfig.connections = []; - patchPanel.env.HWLAB_PATCH_PANEL_WIRING_CONFIG = JSON.stringify(wiringConfig); - - assertRejected(inputs, /hwlab-patch-panel deploy HWLAB_PATCH_PANEL_WIRING_CONFIG/u); -}); - -test("rejects wrong first-class route owner", () => { - const inputs = cloneInputs(); - deployService(inputs, "hwlab-patch-panel").m3Route.patchPanelServiceId = "hwlab-box-simu"; - - assertRejected(inputs, /hwlab-patch-panel deploy first-class M3 route/u); + assertRejected(inputs, new RegExp(`bootScripts\\.${serviceId} is required`, "u")); });