From ddde5b8d370f8402111f2a6743ccd6d94817febd Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 29 May 2026 04:28:34 +0800 Subject: [PATCH] fix: harden v02 bun runtime rollout --- docs/reference/spec-v02-cicd.md | 3 + docs/reference/spec-v02-services.md | 4 +- scripts/artifact-publish.mjs | 37 ++++++- scripts/g14-gitops-render.mjs | 157 ++++++++++++++++++++------- scripts/g14-gitops-render.test.ts | 31 ++++++ scripts/refresh-artifact-catalog.mjs | 57 ++++++++-- 6 files changed, 229 insertions(+), 60 deletions(-) diff --git a/docs/reference/spec-v02-cicd.md b/docs/reference/spec-v02-cicd.md index 3c0eefbe..b5ad3edc 100644 --- a/docs/reference/spec-v02-cicd.md +++ b/docs/reference/spec-v02-cicd.md @@ -159,6 +159,7 @@ CI/CD 内部由 branch poller、PipelineRun、component planner、BuildKit publi - `v0.2-gitops` 中存在 `deploy/artifact-catalog.v02.json` 与 `deploy/gitops/g14/runtime-v02/**`。 - `argocd/hwlab-g14-v02` 指向 `v0.2-gitops:deploy/gitops/g14/runtime-v02`,sync revision 与目标 GitOps revision 对齐。 - `hwlab-v02` 中长驻 workload ready,没有把 DEV/PROD namespace 当成 `v0.2` 通过证据。 +- `runtime-ready` 必须以 workload Pod template 的 source commit 判断 Argo 刷新,并在 Argo 刷新超时或 workload 未就绪时失败;不得把 CrashLoop、未刷新或公网不可用的 runtime 标成绿色。 - `http://74.48.78.17:19666/` 返回 `v0.2` Cloud Web。 - `http://74.48.78.17:19667/health/live` 返回 `v0.2` runtime health,payload 中的 namespace、revision 或 runtime identity 能与 `hwlab-v02`/`v0.2` 对齐。 @@ -188,6 +189,8 @@ GitOps branch 已更新、source branch render 通过、PipelineRun 名称存在 | --- | --- | --- | | v02 独立 source/GitOps/runtime lane | 已实现 | `v0.2`、`v0.2-gitops`、`hwlab-v02` 和 `runtime-v02` 已固定。 | | Tekton poller/pipeline/promotion | 已实现 | 通过 `hwlab-v02-*` 对象和 GitOps promotion 管理。 | +| v02 runtime readiness fail-closed | 已实现 | `runtime-ready` 读取 workload Pod template source commit,timeout 或未就绪会让 PipelineRun 失败。 | +| v02 裁撤服务不进入发布面 | 已实现 | v02 Tekton build service set、artifact catalog 和 runtime render 只包含保留服务;v02 Argo 开启 prune 清理旧 live 对象;裁撤服务仍可保留在 DEV legacy 源码中。 | | Argo v02 Application | 已实现 | `hwlab-g14-v02` 指向 v02 GitOps path 和 namespace。 | | FRP `19666/19667` 入口 | 已实现 | 由 `hwlab-v02-frpc` 与 master frps allowlist 共同提供。 | | SecretRef 独立与 provider 验收 | 已实现/持续约束 | SecretRef 已独立;验收必须做真实短连接聊天。 | diff --git a/docs/reference/spec-v02-services.md b/docs/reference/spec-v02-services.md index 6bbb8ac2..10d9cd4c 100644 --- a/docs/reference/spec-v02-services.md +++ b/docs/reference/spec-v02-services.md @@ -183,7 +183,7 @@ hwlab-patch-panel ## T2 -阅读 docs/reference/spec-v02-services.md,然后用 cli 手动测试以下内容:从 `deploy/gitops/g14/runtime-v02` 读取 Deployment、StatefulSet、Service 和 Job 模板,确认每个保留服务和稳定外部服务都有对应 spec;裁撤对象即使 runtime 暂存,也不能作为新开发权威入口。 +阅读 docs/reference/spec-v02-services.md,然后用 cli 手动测试以下内容:从 `deploy/gitops/g14/runtime-v02` 读取 Deployment、StatefulSet、Service 和 Job 模板,确认每个保留服务和稳定外部服务都有对应 spec;确认 `hwlab-router`、`hwlab-tunnel-client`、`hwlab-gateway-simu`、`hwlab-box-simu` 和 `hwlab-patch-panel` 不再出现在 v0.2 runtime desired state 或 v0.2 artifact catalog 中。 ## T3 @@ -200,7 +200,7 @@ hwlab-patch-panel | v0.2 总体依赖方向 | 已实现 | 本文定义浏览器、API、device、provider 和 CI/CD 链路。 | | 保留服务均有 spec | 已实现 | 本文服务总表列出当前保留服务和 spec 文件。 | | 稳定外部服务纳入 spec | 已实现 | Postgres、Codex API forwarder/hyueapi、DeepSeek/Moon Bridge、FRP 已独立成文或交叉引用权威规格。 | -| `simu`、接线盘、router、tunnel-client 裁撤 | 已实现 | 本文记录裁撤口径,不建立对应单服务 spec。 | +| `simu`、接线盘、router、tunnel-client 裁撤 | 已实现 | 本文记录裁撤口径;v0.2 render、artifact catalog、Tekton build service set 和 cloud-api 运行时 env 不再包含裁撤对象。 | | Bun + TypeScript 迁移边界 | 已实现 | 本文区分第一阶段、后续、暂不迁移和裁撤集合。 | | spec 作为开发和测试权威 | 已实现 | AGENTS.md 规格区提供顶级索引。 | diff --git a/scripts/artifact-publish.mjs b/scripts/artifact-publish.mjs index 91777382..04a05047 100644 --- a/scripts/artifact-publish.mjs +++ b/scripts/artifact-publish.mjs @@ -73,6 +73,22 @@ const servicePorts = new Map([ ["hwlab-agent-skills", 7430] ]); +const v02RuntimeServiceIds = Object.freeze([ + "hwlab-cloud-api", + "hwlab-cloud-web", + "hwlab-agent-mgr", + "hwlab-agent-worker", + "hwlab-device-pod", + "hwlab-gateway", + "hwlab-edge-proxy", + "hwlab-cli", + "hwlab-agent-skills" +]); + +function serviceIdsForLane(lane) { + return lane === "v02" ? v02RuntimeServiceIds : SERVICE_IDS; +} + function parseArgs(argv) { const args = { mode: "preflight", @@ -128,6 +144,7 @@ function parseArgs(argv) { assert.ok(["g14", "v02"].includes(args.lane), `unknown artifact lane ${args.lane}`); assert.ok(["short", "full"].includes(args.imageTagMode), `unknown image tag mode ${args.imageTagMode}`); + if (!args.servicesExplicit) args.services = [...serviceIdsForLane(args.lane)]; return args; } @@ -574,7 +591,7 @@ function assertV02Contracts(catalog, deployManifest) { assert.deepEqual(catalog.allowedProfiles, ["v02"], "catalog.allowedProfiles must only allow v02"); assert.deepEqual(catalog.forbiddenProfiles, ["dev", "prod"], "catalog.forbiddenProfiles must forbid dev/prod"); const catalogIds = (catalog.services ?? []).map((service) => service.serviceId); - assert.deepEqual(catalogIds, SERVICE_IDS, "v02 catalog services must match frozen service IDs"); + assert.deepEqual(catalogIds, serviceIdsForLane("v02"), "v02 catalog services must match retained service IDs"); for (const service of catalog.services ?? []) { assert.equal(service.profile, "v02", `${service.serviceId}.profile must be v02`); assert.equal(service.namespace, "hwlab-v02", `${service.serviceId}.namespace must be hwlab-v02`); @@ -614,7 +631,7 @@ function artifactCatalogSkeleton({ args, commitId }) { }, allowedProfiles: [artifactEnvironment(args)], forbiddenProfiles: args.lane === "v02" ? ["dev", "prod"] : ["prod"], - services: SERVICE_IDS.map((serviceId) => ({ + services: serviceIdsForLane(args.lane).map((serviceId) => ({ serviceId, commitId: tag, sourceCommitId: commitId, @@ -634,6 +651,15 @@ function artifactCatalogSkeleton({ args, commitId }) { }; } +function normalizeCatalogForLane(catalog, lane) { + if (!catalog || lane !== "v02") return catalog; + const allowed = new Set(serviceIdsForLane(lane)); + return { + ...catalog, + services: (catalog.services ?? []).filter((service) => allowed.has(service.serviceId)) + }; +} + function parseRegistryPrefix(prefix) { const normalized = prefix.replace(/\/+$/u, ""); const [hostPort, ...pathParts] = normalized.split("/"); @@ -1184,7 +1210,7 @@ if (runtimeKind === "node-command" && entrypoint) { } function dockerfile(baseImage, port, labels = []) { - const dependencyTimingScript = `started_at=$(node -e "console.log(Date.now())"); status=succeeded; if [ -d /opt/hwlab-node-runtime-base/node_modules ]; then cp -a /opt/hwlab-node-runtime-base/node_modules ./node_modules; fi && if [ -f package-lock.json ]; then npm install --omit=dev --include=optional --ignore-scripts; else npm install --omit=dev --include=optional --ignore-scripts; fi && if [ "\${HWLAB_ARTIFACT_KIND:-}" = "bun-command" ]; then npm install --no-save --omit=dev --include=optional --ignore-scripts bun@1.3.13; fi && codex_version="$(node -p "require('./node_modules/@openai/codex/package.json').version")" && case "$(uname -m)" in x86_64|amd64) test -e node_modules/@openai/codex-linux-x64 || npm install --omit=dev --include=optional --ignore-scripts "@openai/codex-linux-x64@npm:@openai/codex@\${codex_version}-linux-x64" ;; aarch64|arm64) test -e node_modules/@openai/codex-linux-arm64 || npm install --omit=dev --include=optional --ignore-scripts "@openai/codex-linux-arm64@npm:@openai/codex@\${codex_version}-linux-arm64" ;; esac || status=failed; finished_at=$(node -e "console.log(Date.now())"); node -e "console.log(JSON.stringify({event:'g14-cicd-build-step-timing',stage:'dependency-install',status:process.argv[1],durationMs:Number(process.argv[3])-Number(process.argv[2]),source:'dockerfile-run'}))" "$status" "$started_at" "$finished_at"; test "$status" = succeeded`; + const dependencyTimingScript = `started_at=$(node -e "console.log(Date.now())"); status=succeeded; if [ -d /opt/hwlab-node-runtime-base/node_modules ]; then cp -a /opt/hwlab-node-runtime-base/node_modules ./node_modules; fi && if [ -f package-lock.json ]; then npm install --omit=dev --include=optional --ignore-scripts; else npm install --omit=dev --include=optional --ignore-scripts; fi && if [ "\${HWLAB_ARTIFACT_KIND:-}" = "bun-command" ]; then npm install --no-save --omit=dev --include=optional --ignore-scripts bun@1.3.13 && test -x /app/node_modules/.bin/bun && ln -sf /app/node_modules/.bin/bun /usr/local/bin/bun; fi && codex_version="$(node -p "require('./node_modules/@openai/codex/package.json').version")" && case "$(uname -m)" in x86_64|amd64) test -e node_modules/@openai/codex-linux-x64 || npm install --omit=dev --include=optional --ignore-scripts "@openai/codex-linux-x64@npm:@openai/codex@\${codex_version}-linux-x64" ;; aarch64|arm64) test -e node_modules/@openai/codex-linux-arm64 || npm install --omit=dev --include=optional --ignore-scripts "@openai/codex-linux-arm64@npm:@openai/codex@\${codex_version}-linux-arm64" ;; esac || status=failed; finished_at=$(node -e "console.log(Date.now())"); node -e "console.log(JSON.stringify({event:'g14-cicd-build-step-timing',stage:'dependency-install',status:process.argv[1],durationMs:Number(process.argv[3])-Number(process.argv[2]),source:'dockerfile-run'}))" "$status" "$started_at" "$finished_at"; test "$status" = succeeded`; return [ `FROM ${baseImage}`, "WORKDIR /app", @@ -1210,7 +1236,7 @@ function dockerfile(baseImage, port, labels = []) { "COPY tools ./tools", "COPY skills ./skills", "COPY deploy ./deploy", - "RUN mkdir -p /workspace /codex-home && rm -rf /workspace/hwlab && ln -s /app /workspace/hwlab && chmod -R a+rwX /app /workspace /codex-home && test -x /app/node_modules/.bin/codex && /app/node_modules/.bin/codex --version >/tmp/hwlab-codex-version.txt && (test -x /app/node_modules/@openai/codex-linux-x64/vendor/x86_64-unknown-linux-musl/codex/codex || test -x /app/node_modules/@openai/codex-linux-arm64/vendor/aarch64-unknown-linux-musl/codex/codex)", + "RUN mkdir -p /workspace /codex-home && rm -rf /workspace/hwlab && ln -s /app /workspace/hwlab && if [ \"${HWLAB_ARTIFACT_KIND:-}\" = \"bun-command\" ]; then test -x /usr/local/bin/bun && /usr/local/bin/bun --version >/tmp/hwlab-bun-version.txt; fi && chmod -R a+rwX /app /workspace /codex-home && test -x /app/node_modules/.bin/codex && /app/node_modules/.bin/codex --version >/tmp/hwlab-codex-version.txt && (test -x /app/node_modules/@openai/codex-linux-x64/vendor/x86_64-unknown-linux-musl/codex/codex || test -x /app/node_modules/@openai/codex-linux-arm64/vendor/aarch64-unknown-linux-musl/codex/codex)", `RUN node -e "const fs=require('node:fs'); fs.writeFileSync('/usr/local/bin/hwlab-dev-artifact-runtime.mjs', Buffer.from('${runtimeScriptBase64()}', 'base64')); fs.chmodSync('/usr/local/bin/hwlab-dev-artifact-runtime.mjs', 0o755);"`, "ARG HWLAB_ENVIRONMENT", "ARG HWLAB_SERVICE_ID", @@ -2200,6 +2226,7 @@ async function main() { let catalog = await readJsonIfPresent(args.catalogPath, null); const catalogWasMissing = !catalog; if (!catalog && args.lane === "v02") catalog = artifactCatalogSkeleton({ args, commitId }); + catalog = normalizeCatalogForLane(catalog, args.lane); assert.ok(catalog, `${args.catalogPath} is required for ${args.lane} artifact publish`); const shortCommit = imageTagForCommit(args, commitId); let effectiveCatalogPath = args.catalogPath; @@ -2217,7 +2244,7 @@ async function main() { artifactCatalogPath: effectiveCatalogPath, registryPrefix: args.registryPrefix, baseImage: args.baseImage ?? undefined, - ...(args.servicesExplicit ? { services: args.services } : {}) + services: args.services }); const services = enrichServicesWithCiPlan(await resolveServices(args.services, catalog), ciPlan); const catalogByServiceId = new Map((catalog?.services ?? []).map((service) => [service.serviceId, service])); diff --git a/scripts/g14-gitops-render.mjs b/scripts/g14-gitops-render.mjs index 2d150702..355a105d 100644 --- a/scripts/g14-gitops-render.mjs +++ b/scripts/g14-gitops-render.mjs @@ -43,7 +43,29 @@ const defaultServiceIds = Object.freeze([ "hwlab-cli", "hwlab-agent-skills" ]); -const defaultServicesParam = defaultServiceIds.join(","); +const v02RuntimeServiceIds = Object.freeze([ + "hwlab-cloud-api", + "hwlab-cloud-web", + "hwlab-agent-mgr", + "hwlab-agent-worker", + "hwlab-device-pod", + "hwlab-gateway", + "hwlab-edge-proxy", + "hwlab-cli", + "hwlab-agent-skills" +]); +const v02RemovedServiceIds = new Set([ + "hwlab-router", + "hwlab-tunnel-client", + "hwlab-gateway-simu", + "hwlab-box-simu", + "hwlab-patch-panel" +]); +const v02RemovedServiceEnvNames = new Set([ + "HWLAB_M3_GATEWAY_SIMU_1_URL", + "HWLAB_M3_GATEWAY_SIMU_2_URL", + "HWLAB_M3_PATCH_PANEL_URL" +]); const moonBridgeSourceRef = process.env.HWLAB_G14_MOONBRIDGE_REF || "1b99888d3dae889b79ee602cb875c7907f7e76f2"; const moonBridgeImage = process.env.HWLAB_G14_MOONBRIDGE_IMAGE || `${defaultRegistryPrefix}/moonbridge:${moonBridgeSourceRef.slice(0, 12)}`; const deepSeekProfileModel = process.env.HWLAB_G14_DEEPSEEK_PROFILE_MODEL || "deepseek-chat"; @@ -414,6 +436,22 @@ function artifactEndpoint(args) { return args.lane === "v02" ? defaultV02RuntimeEndpoint : defaultRuntimeEndpoint; } +function serviceIdsForLane(lane) { + return lane === "v02" ? v02RuntimeServiceIds : defaultServiceIds; +} + +function serviceIdsForProfile(profile) { + return profile === "v02" ? v02RuntimeServiceIds : defaultServiceIds; +} + +function servicesParamForLane(lane) { + return serviceIdsForLane(lane).join(","); +} + +function isV02RemovedServiceId(serviceId) { + return v02RemovedServiceIds.has(serviceId); +} + function artifactCatalogSkeleton({ args, source }) { const environment = artifactEnvironment(args); const namespace = args.lane === "v02" ? "hwlab-v02" : "hwlab-dev"; @@ -435,7 +473,7 @@ function artifactCatalogSkeleton({ args, source }) { }, allowedProfiles: [environment], forbiddenProfiles: args.lane === "v02" ? ["dev", "prod"] : ["prod"], - services: defaultServiceIds.map((serviceId) => ({ + services: serviceIdsForLane(args.lane).map((serviceId) => ({ serviceId, profile: environment, namespace, @@ -585,8 +623,16 @@ function applyDeployEnv(envList, deployService, namespace, profile = "dev") { } } +function removeV02LegacySimulatorEnv(envList) { + if (!Array.isArray(envList)) return envList; + return envList.filter((entry) => !v02RemovedServiceEnvNames.has(entry?.name)); +} + function deployServicesForProfile(deploy, profile) { - const services = new Map((deploy.services ?? []).map((service) => [service.serviceId, cloneJson(service)])); + const allowed = new Set(serviceIdsForProfile(profile)); + const services = new Map((deploy.services ?? []) + .filter((service) => allowed.has(service.serviceId)) + .map((service) => [service.serviceId, cloneJson(service)])); const laneServices = profile === "v02" && Array.isArray(deploy.lanes?.v02?.services) ? deploy.lanes.v02.services : []; @@ -613,6 +659,7 @@ function transformWorkloads({ workloads, deploy, catalog, source, registryPrefix "hwlab.pikastech.local/environment": profileLabel, "hwlab.pikastech.local/profile": profileLabel }; + result.items = asItems(result, "workloads").filter((item) => !(profile === "v02" && isV02RemovedServiceId(serviceIdForWorkload(item, null)))); for (const item of asItems(result, "workloads")) { item.metadata.namespace = namespace; label(item.metadata, { @@ -674,6 +721,7 @@ function transformWorkloads({ workloads, deploy, catalog, source, registryPrefix if (Object.hasOwn(entry, "value")) entry.value = namespaceScopedHost(entry.value, namespace); } applyDeployEnv(container.env, deployService, namespace, profile); + if (profile === "v02") container.env = removeV02LegacySimulatorEnv(container.env); upsertEnv(container.env, "HWLAB_ENVIRONMENT", profileLabel); upsertEnv(container.env, "HWLAB_COMMIT_ID", runtimeCommit); upsertEnv(container.env, "HWLAB_IMAGE", image); @@ -689,6 +737,7 @@ function transformWorkloads({ workloads, deploy, catalog, source, registryPrefix } if (serviceId === "hwlab-cloud-api") { syncCodeAgentWorkspaceInitContainer(podTemplate.spec.initContainers, { image, runtimeCommit, runtimeImageTag }); + if (profile === "v02") upsertEnv(container.env, "HWLAB_M3_IO_CONTROL_ENABLED", "false"); upsertEnv(container.env, "HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE", "deepseek"); upsertEnv(container.env, "HWLAB_CODE_AGENT_DEEPSEEK_MODEL", deepSeekProfileModel); upsertEnv(container.env, "HWLAB_CODE_AGENT_DEEPSEEK_BASE_URL", deepSeekBaseUrl(namespace)); @@ -759,6 +808,14 @@ function transformListNamespace(value, namespace, labels, annotations) { return result; } +function transformServices({ services, namespace, labels, annotations, profile = "dev" }) { + const result = transformListNamespace(services, namespace, labels, annotations); + if (result.kind === "List") { + result.items = (result.items ?? []).filter((item) => !(profile === "v02" && isV02RemovedServiceId(serviceIdForWorkload(item, null)))); + } + return result; +} + function shellSingleQuote(value) { return `'${String(value).replace(/'/g, `'"'"'`)}'`; } @@ -934,7 +991,7 @@ function prepareSourceScript() { " git fetch --depth 1 origin \"$(params.gitops-branch)\" >/dev/null 2>&1 || true", " if git cat-file -e FETCH_HEAD:\"$catalog_path\" 2>/dev/null; then", " git show FETCH_HEAD:\"$catalog_path\" > /tmp/hwlab-gitops-artifact-catalog.json", - " if node -e 'const fs=require(\"fs\"); const ids=(doc)=>(doc.services||[]).map((s)=>s.serviceId); const gitops=JSON.parse(fs.readFileSync(process.argv[1],\"utf8\")); const deploy=JSON.parse(fs.readFileSync(process.argv[2],\"utf8\")); if (JSON.stringify(ids(gitops)) !== JSON.stringify(ids(deploy))) process.exit(42);' /tmp/hwlab-gitops-artifact-catalog.json deploy/deploy.json; then", + " if node -e 'const fs=require(\"fs\"); const ids=(doc)=>(doc.services||[]).map((s)=>s.serviceId); const selected=(process.argv[3]||\"\").split(\",\").filter(Boolean); const gitops=JSON.parse(fs.readFileSync(process.argv[1],\"utf8\")); const deploy=JSON.parse(fs.readFileSync(process.argv[2],\"utf8\")); const expected=selected.length ? selected : ids(deploy); if (JSON.stringify(ids(gitops)) !== JSON.stringify(expected)) process.exit(42);' /tmp/hwlab-gitops-artifact-catalog.json deploy/deploy.json \"$(params.services)\"; then", " cp /tmp/hwlab-gitops-artifact-catalog.json \"$catalog_path\"", " echo '{\"event\":\"gitops-artifact-catalog\",\"phase\":\"prepare-source\",\"status\":\"loaded\",\"branch\":\"'\"$(params.gitops-branch)\"'\"}'", " else", @@ -947,14 +1004,16 @@ function prepareSourceScript() { " echo '{\"event\":\"gitops-artifact-catalog\",\"phase\":\"prepare-source\",\"status\":\"seed\",\"reason\":\"gitops-branch-missing\"}'", "fi", "if [ ! -s \"$catalog_path\" ] && [ \"$(params.lane)\" = \"v02\" ]; then", - " node - \"$catalog_path\" \"$(params.revision)\" \"$(params.registry-prefix)\" \"$(params.image-tag-mode)\" <<'NODE'", + " node - \"$catalog_path\" \"$(params.revision)\" \"$(params.registry-prefix)\" \"$(params.image-tag-mode)\" \"$(params.services)\" <<'NODE'", "const fs = require('node:fs');", "const path = require('node:path');", - "const [catalogPath, revision, registryPrefix, imageTagMode] = process.argv.slice(2);", + "const [catalogPath, revision, registryPrefix, imageTagMode, selectedServices] = process.argv.slice(2);", "const deploy = JSON.parse(fs.readFileSync('deploy/deploy.json', 'utf8'));", "const tag = imageTagMode === 'full' ? revision : revision.slice(0, 7);", "const namespace = 'hwlab-v02';", - "const services = (deploy.services || []).map((service) => ({ serviceId: service.serviceId, profile: 'v02', namespace, commitId: tag, sourceCommitId: revision, image: `${registryPrefix}/${service.serviceId}:${tag}`, imageTag: tag, digest: null, buildBackend: 'contract-skeleton' }));", + "const selected = (selectedServices || '').split(',').filter(Boolean);", + "const serviceIds = selected.length ? selected : (deploy.services || []).map((service) => service.serviceId);", + "const services = serviceIds.map((serviceId) => ({ serviceId, profile: 'v02', namespace, commitId: tag, sourceCommitId: revision, image: `${registryPrefix}/${serviceId}:${tag}`, imageTag: tag, digest: null, buildBackend: 'contract-skeleton' }));", "fs.mkdirSync(path.dirname(catalogPath), { recursive: true });", "fs.writeFileSync(catalogPath, JSON.stringify({ catalogVersion: 'v1', kind: 'hwlab-artifact-catalog', environment: 'v02', profile: 'v02', namespace, endpoint: 'http://74.48.78.17:19667', commitId: tag, artifactState: 'contract-skeleton', publish: { registryPrefix, sourceCommitId: revision, imageTag: tag, publishedAt: null }, allowedProfiles: ['v02'], forbiddenProfiles: ['dev', 'prod'], services }, null, 2) + '\\n');", "NODE", @@ -1025,7 +1084,7 @@ node - <<'NODE' const fs = require("node:fs"); const plan = JSON.parse(fs.readFileSync("/workspace/source/ci-plan.json", "utf8")); const selected = String(process.env.HWLAB_SELECTED_SERVICES || "").split(",").map((item) => item.trim()).filter(Boolean); -const allServices = ${JSON.stringify(defaultServiceIds)}; +const allServices = selected.length > 0 ? selected : ${JSON.stringify(defaultServiceIds)}; const affected = new Set(plan.affectedServices || []); const selectedSet = new Set(selected); const entries = allServices.map((serviceId) => ({ serviceId, selected: selectedSet.has(serviceId), affected: selectedSet.has(serviceId) && affected.has(serviceId) })); @@ -1714,19 +1773,19 @@ function serviceResultEnvName(serviceId, field) { return `HWLAB_SERVICE_RESULT_${serviceId.toUpperCase().replace(/[^A-Z0-9]+/gu, "_")}_${field.toUpperCase().replace(/[^A-Z0-9]+/gu, "_")}`; } -function serviceResultParams() { - return defaultServiceIds.flatMap((serviceId) => serviceResultFields.map((field) => ({ name: serviceResultParamName(serviceId, field) }))); +function serviceResultParams(serviceIds = defaultServiceIds) { + return serviceIds.flatMap((serviceId) => serviceResultFields.map((field) => ({ name: serviceResultParamName(serviceId, field), default: "" }))); } -function serviceResultParamValues() { - return defaultServiceIds.flatMap((serviceId) => serviceResultFields.map((field) => ({ +function serviceResultParamValues(serviceIds = defaultServiceIds) { + return serviceIds.flatMap((serviceId) => serviceResultFields.map((field) => ({ name: serviceResultParamName(serviceId, field), value: `$(tasks.${buildTaskName(serviceId)}.results.${field})` }))); } -function serviceResultEnv() { - return defaultServiceIds.flatMap((serviceId) => serviceResultFields.map((field) => ({ +function serviceResultEnv(serviceIds = defaultServiceIds) { + return serviceIds.flatMap((serviceId) => serviceResultFields.map((field) => ({ name: serviceResultEnvName(serviceId, field), value: `$(params.${serviceResultParamName(serviceId, field)})` }))); @@ -1776,7 +1835,7 @@ function buildkitSidecar() { }; } -function planArtifactsTask() { +function planArtifactsTask({ serviceIds = defaultServiceIds } = {}) { return { name: "plan-artifacts", runAfter: primitiveValidationTaskNames(), @@ -1788,7 +1847,7 @@ function planArtifactsTask() { { name: "registry-prefix" }, { name: "services" } ], - results: defaultServiceIds.map((serviceId) => ({ name: affectedResultName(serviceId), description: `${serviceId} changed according to g14-ci-plan` })), + results: serviceIds.map((serviceId) => ({ name: affectedResultName(serviceId), description: `${serviceId} changed according to g14-ci-plan` })), workspaces: [{ name: "source" }], steps: [{ name: "plan", @@ -1849,10 +1908,10 @@ function perServiceBuildTask(serviceId) { }; } -function collectArtifactsTask() { +function collectArtifactsTask({ serviceIds = defaultServiceIds } = {}) { return { name: "collect-artifacts", - runAfter: defaultServiceIds.map(buildTaskName), + runAfter: serviceIds.map(buildTaskName), workspaces: [{ name: "source", workspace: "source" }], taskSpec: { params: [ @@ -1863,10 +1922,10 @@ function collectArtifactsTask() { { name: "registry-prefix" }, { name: "services" }, { name: "base-image" }, - ...serviceResultParams() + ...serviceResultParams(serviceIds) ], workspaces: [{ name: "source" }], - steps: [{ name: "collect", image: ciToolsRunnerImage, env: [...proxyEnv(), ...serviceResultEnv()], script: collectArtifactsScript() }] + steps: [{ name: "collect", image: ciToolsRunnerImage, env: [...proxyEnv(), ...serviceResultEnv(serviceIds)], script: collectArtifactsScript() }] }, params: [ { name: "revision", value: "$(params.revision)" }, @@ -1876,7 +1935,7 @@ function collectArtifactsTask() { { name: "registry-prefix", value: "$(params.registry-prefix)" }, { name: "services", value: "$(params.services)" }, { name: "base-image", value: "$(params.base-image)" }, - ...serviceResultParamValues() + ...serviceResultParamValues(serviceIds) ] }; } @@ -1982,7 +2041,11 @@ async function runtimeItems() { } function runtimeSourceCommit(item) { - return item.metadata?.labels?.["hwlab.pikastech.local/source-commit"] || item.metadata?.annotations?.["hwlab.pikastech.local/source-commit"] || null; + return item.spec?.template?.metadata?.labels?.["hwlab.pikastech.local/source-commit"] || + item.spec?.template?.metadata?.annotations?.["hwlab.pikastech.local/source-commit"] || + item.metadata?.labels?.["hwlab.pikastech.local/source-commit"] || + item.metadata?.annotations?.["hwlab.pikastech.local/source-commit"] || + null; } function readyWorkload(item) { @@ -2014,6 +2077,7 @@ async function waitForArgoRefresh() { } if (Date.now() - startedAt >= timeoutMs) { emit({ stage: "argo-refresh", status: "timeout", durationMs: Date.now() - startedAt, workloadCount: runtime.items.length, pending: pending.slice(0, 8) }); + process.exitCode = 1; return { observed: false, skipped: false }; } await sleep(pollMs); @@ -2035,6 +2099,7 @@ async function waitForWorkloads() { } if (Date.now() - startedAt >= timeoutMs) { emit({ stage: "workload-ready", status: "timeout", durationMs: Date.now() - startedAt, workloadCount: runtime.items.length, blocked: blocked.slice(0, 8) }); + process.exitCode = 1; return; } await sleep(pollMs); @@ -2046,6 +2111,7 @@ async function waitForWorkloads() { await waitForWorkloads(); })().catch((error) => { emit({ stage: "runtime-ready", status: "failed", reason: error.message, durationMs: 0 }); + process.exitCode = 1; }); NODE `; @@ -2076,11 +2142,12 @@ function runtimeReadyTask({ profile = "dev" } = {}) { }; } -function imagePublishTaskSet() { +function imagePublishTaskSet(args = { lane: "g14" }) { + const serviceIds = serviceIdsForLane(args.lane); return [ - planArtifactsTask(), - ...defaultServiceIds.map(perServiceBuildTask), - collectArtifactsTask() + planArtifactsTask({ serviceIds }), + ...serviceIds.map(perServiceBuildTask), + collectArtifactsTask({ serviceIds }) ]; } @@ -2114,7 +2181,7 @@ function tektonPipeline(args = { lane: "g14" }) { { name: "runtime-path", type: "string", default: runtimePath }, { name: "revision", type: "string", description: "Full git commit SHA; the only source truth for image tags." }, { name: "registry-prefix", type: "string", default: defaultRegistryPrefix }, - { name: "services", type: "string", default: defaultServicesParam }, + { name: "services", type: "string", default: servicesParamForLane(args.lane) }, { name: "base-image", type: "string", default: defaultDevBaseImage } ], workspaces: [ @@ -2137,6 +2204,7 @@ function tektonPipeline(args = { lane: "g14" }) { { name: "catalog-path" }, { name: "image-tag-mode" }, { name: "registry-prefix" }, + { name: "services" }, { name: "revision" } ], workspaces: [{ name: "source" }, { name: "git-ssh" }], @@ -2150,11 +2218,12 @@ function tektonPipeline(args = { lane: "g14" }) { { name: "catalog-path", value: "$(params.catalog-path)" }, { name: "image-tag-mode", value: "$(params.image-tag-mode)" }, { name: "registry-prefix", value: "$(params.registry-prefix)" }, + { name: "services", value: "$(params.services)" }, { name: "revision", value: "$(params.revision)" } ] }, ...primitiveValidationTasks.map(primitiveValidationTask), - ...imagePublishTaskSet(), + ...imagePublishTaskSet(args), { name: "gitops-promote", runAfter: ["collect-artifacts"], @@ -2296,7 +2365,7 @@ function tektonPollerCronJob(args) { { name: "SOURCE_BRANCH", value: args.sourceBranch }, { name: "GITOPS_BRANCH", value: args.gitopsBranch }, { name: "REGISTRY_PREFIX", value: args.registryPrefix }, - { name: "SERVICES", value: defaultServicesParam }, + { name: "SERVICES", value: servicesParamForLane(args.lane) }, { name: "BASE_IMAGE", value: defaultDevBaseImage } ], volumeMounts: [{ name: "git-ssh", mountPath: "/workspace/git-ssh", readOnly: true }], @@ -2474,12 +2543,12 @@ function argoApplication(args, profile = "dev") { project, source: { repoURL: args.sourceRepo, targetRevision: args.gitopsBranch, path: `deploy/gitops/g14/${runtimePath}` }, destination: { server: "https://kubernetes.default.svc", namespace }, - syncPolicy: { automated: { prune: false, selfHeal: true }, syncOptions: ["CreateNamespace=true", "ApplyOutOfSyncOnly=true"] } + syncPolicy: { automated: { prune: profile === "v02", selfHeal: true }, syncOptions: ["CreateNamespace=true", "ApplyOutOfSyncOnly=true"] } } }; } -function g14FrpcManifest({ profile = "dev" } = {}) { +function g14FrpcManifest({ profile = "dev", source = null } = {}) { const namespace = namespaceNameForProfile(profile); const profileLabel = runtimeLabelForProfile(profile); const deploymentName = profile === "v02" ? "hwlab-v02-frpc" : profile === "prod" ? "hwlab-g14-prod-frpc" : "hwlab-g14-frpc"; @@ -2494,6 +2563,8 @@ function g14FrpcManifest({ profile = "dev" } = {}) { "hwlab.pikastech.local/gitops-target": gitopsTargetForProfile(profile), "hwlab.pikastech.local/profile": profileLabel }; + const sourceCommitLabel = source?.full ? { "hwlab.pikastech.local/source-commit": source.full } : {}; + const renderedLabels = { ...labels, ...sourceCommitLabel }; return { apiVersion: "v1", kind: "List", @@ -2504,7 +2575,7 @@ function g14FrpcManifest({ profile = "dev" } = {}) { metadata: { name: configName, namespace, - labels + labels: renderedLabels }, data: { "frpc.toml": `serverAddr = "74.48.78.17" @@ -2533,13 +2604,13 @@ remotePort = ${edgeRemotePort} metadata: { name: deploymentName, namespace, - labels + labels: renderedLabels }, spec: { replicas: 1, selector: { matchLabels: { "app.kubernetes.io/name": deploymentName } }, template: { - metadata: { labels }, + metadata: { labels: renderedLabels, annotations: sourceCommitLabel }, spec: { containers: [{ name: "frpc", @@ -2569,10 +2640,12 @@ function deepSeekProxyManifest({ profile = "dev", source, registryPrefix = defau "app.kubernetes.io/part-of": "hwlab", "hwlab.pikastech.local/environment": runtimeLabelForProfile(profile), "hwlab.pikastech.local/gitops-target": gitopsTargetForProfile(profile), - "hwlab.pikastech.local/service-id": "hwlab-deepseek-proxy" + "hwlab.pikastech.local/service-id": "hwlab-deepseek-proxy", + "hwlab.pikastech.local/source-commit": source.full }; const templateAnnotations = { "hwlab.pikastech.local/bridge-service-id": bridgeServiceId, + "hwlab.pikastech.local/source-commit": source.full, "hwlab.pikastech.local/bridge-source-commit": bridgeCommit, "hwlab.pikastech.local/bridge-image-tag": bridgeImageTag, "hwlab.pikastech.local/bridge-image": bridgeImage @@ -2940,7 +3013,7 @@ function deviceAgent71FreqManifest({ profile = "dev", source, registryPrefix, ca }; } -function v02PostgresManifest({ migrationSql }) { +function v02PostgresManifest({ migrationSql, source }) { const namespace = "hwlab-v02"; const name = "hwlab-v02-postgres"; const labels = { @@ -2948,8 +3021,10 @@ function v02PostgresManifest({ migrationSql }) { "app.kubernetes.io/part-of": "hwlab", "hwlab.pikastech.local/environment": "v02", "hwlab.pikastech.local/gitops-target": "v02", - "hwlab.pikastech.local/profile": "v02" + "hwlab.pikastech.local/profile": "v02", + "hwlab.pikastech.local/source-commit": source.full }; + const templateAnnotations = { "hwlab.pikastech.local/source-commit": source.full }; return { apiVersion: "v1", kind: "List", @@ -2975,7 +3050,7 @@ function v02PostgresManifest({ migrationSql }) { replicas: 1, selector: { matchLabels: { "app.kubernetes.io/name": name } }, template: { - metadata: { labels }, + metadata: { labels, annotations: templateAnnotations }, spec: { containers: [{ name: "postgres", @@ -3129,13 +3204,13 @@ async function plannedFiles(args) { putJson(`${runtimePath}/kustomization.yaml`, runtimeKustomization({ profile, includeDeviceAgent })); putJson(`${runtimePath}/namespace.yaml`, runtimeNamespace); putJson(`${runtimePath}/code-agent-codex-config.yaml`, transformListNamespace(config, namespace, profileLabels, annotations)); - putJson(`${runtimePath}/services.yaml`, transformListNamespace(services, namespace, profileLabels, annotations)); + putJson(`${runtimePath}/services.yaml`, transformServices({ services, namespace, labels: profileLabels, annotations, profile })); putJson(`${runtimePath}/health-contract.yaml`, transformHealthContract(health, namespace, profileLabels, annotations, endpoints.runtimeEndpoint, endpoints.webEndpoint, profile)); putJson(`${runtimePath}/workloads.yaml`, transformWorkloads({ workloads, deploy, catalog: artifactCatalog, source, registryPrefix: args.registryPrefix, runtimeEndpoint: endpoints.runtimeEndpoint, webEndpoint: endpoints.webEndpoint, profile, useDeployImages: args.useDeployImages })); putJson(`${runtimePath}/deepseek-proxy.yaml`, deepSeekProxyManifest({ profile, source, registryPrefix: args.registryPrefix, catalog: artifactCatalog, useDeployImages: args.useDeployImages })); - if (profile === "v02") putJson(`${runtimePath}/postgres.yaml`, v02PostgresManifest({ migrationSql })); + if (profile === "v02") putJson(`${runtimePath}/postgres.yaml`, v02PostgresManifest({ migrationSql, source })); if (includeDeviceAgent) putJson(`${runtimePath}/device-agent-71-freq.yaml`, deviceAgent71FreqManifest({ profile, source, registryPrefix: args.registryPrefix, catalog: artifactCatalog, useDeployImages: args.useDeployImages })); - putJson(`${runtimePath}/g14-frpc.yaml`, g14FrpcManifest({ profile })); + putJson(`${runtimePath}/g14-frpc.yaml`, g14FrpcManifest({ profile, source })); } return { files, source }; } diff --git a/scripts/g14-gitops-render.test.ts b/scripts/g14-gitops-render.test.ts index 6019c9c2..0e9121ab 100644 --- a/scripts/g14-gitops-render.test.ts +++ b/scripts/g14-gitops-render.test.ts @@ -24,6 +24,37 @@ test("v02 render follows TypeScript runtime checks and does not self-patch boots assert.match(pipeline, /127\.0\.0\.1:5000\/hwlab\/hwlab-ci-node-tools:node22-alpine-bun-v1/u); assert.match(pipeline, /node scripts\/run-bun\.mjs test cmd\/hwlab-codex-api-responses-forwarder\/main\.test\.ts/u); assert.doesNotMatch(pipeline, /cmd\/hwlab-codex-api-responses-forwarder\/main\.mjs/u); + assert.match(pipeline, /process\.exitCode = 1/u); + assert.match(pipeline, /item\.spec\?\.template\?\.metadata\?\.labels\?\.\[\\"hwlab\.pikastech\.local\/source-commit\\"\]/u); + + const removedServiceIds = [ + "hwlab-router", + "hwlab-tunnel-client", + "hwlab-gateway-simu", + "hwlab-box-simu", + "hwlab-patch-panel" + ]; + for (const serviceId of removedServiceIds) { + assert.doesNotMatch(pipeline, new RegExp(`build-${serviceId}`, "u")); + } + + const workloads = await readFile(path.join(outDir, "runtime-v02", "workloads.yaml"), "utf8"); + const services = await readFile(path.join(outDir, "runtime-v02", "services.yaml"), "utf8"); + for (const serviceId of removedServiceIds) { + assert.doesNotMatch(workloads, new RegExp(serviceId, "u")); + assert.doesNotMatch(services, new RegExp(serviceId, "u")); + } + + assert.doesNotMatch(pipeline, /hwlab-gateway-simu-status/u); + assert.match(pipeline, /hwlab-cloud-api-status/u); + + const application = await readFile(path.join(outDir, "argocd", "application-v02.yaml"), "utf8"); + assert.match(application, /"prune": true/u); + + for (const generatedFile of ["deepseek-proxy.yaml", "postgres.yaml", "g14-frpc.yaml"]) { + const content = await readFile(path.join(outDir, "runtime-v02", generatedFile), "utf8"); + assert.match(content, /"hwlab\.pikastech\.local\/source-commit"/u); + } const reconciler = await readFile(path.join(outDir, "tekton-v02", "control-plane-reconciler.yaml"), "utf8"); assert.match(reconciler, /cross-namespace-rbac-bootstrap-managed/u); diff --git a/scripts/refresh-artifact-catalog.mjs b/scripts/refresh-artifact-catalog.mjs index 8f3a82d8..cfc50c9a 100644 --- a/scripts/refresh-artifact-catalog.mjs +++ b/scripts/refresh-artifact-catalog.mjs @@ -25,6 +25,22 @@ const commitPattern = /^[a-f0-9]{7,40}$/; const publishReadyStatuses = new Set(["published", "reused"]); +const v02RuntimeServiceIds = Object.freeze([ + "hwlab-cloud-api", + "hwlab-cloud-web", + "hwlab-agent-mgr", + "hwlab-agent-worker", + "hwlab-device-pod", + "hwlab-gateway", + "hwlab-edge-proxy", + "hwlab-cli", + "hwlab-agent-skills" +]); + +function serviceIdsForLane(lane) { + return lane === "v02" ? v02RuntimeServiceIds : SERVICE_IDS; +} + function parseArgs(argv) { const args = { lane: defaultLane, @@ -186,7 +202,7 @@ function artifactCatalogSkeleton({ args, target, registryPrefix = defaultRegistr }, allowedProfiles: [environment], forbiddenProfiles: args.lane === "v02" ? ["dev", "prod"] : ["prod"], - services: SERVICE_IDS.map((serviceId) => ({ + services: serviceIdsForLane(args.lane).map((serviceId) => ({ serviceId, commitId: target.imageTag, sourceCommitId: target.commitId, @@ -253,7 +269,16 @@ function assertV02DeployAndCatalog(deploy, catalog) { assert.equal(catalog.endpoint, "http://74.48.78.17:19667", "catalog endpoint must be the v02 API endpoint"); assert.deepEqual(catalog.allowedProfiles, ["v02"], "catalog allowedProfiles must only allow v02"); assert.deepEqual(catalog.forbiddenProfiles, ["dev", "prod"], "catalog forbiddenProfiles must forbid dev/prod"); - assert.deepEqual((catalog.services ?? []).map((service) => service.serviceId), SERVICE_IDS, "catalog services must cover frozen service IDs in order"); + assert.deepEqual((catalog.services ?? []).map((service) => service.serviceId), serviceIdsForLane("v02"), "catalog services must cover retained v0.2 service IDs in order"); +} + +function normalizeCatalogForLane(catalog, lane) { + if (!catalog || lane !== "v02") return catalog; + const allowed = new Set(serviceIdsForLane(lane)); + return { + ...catalog, + services: (catalog.services ?? []).filter((service) => allowed.has(service.serviceId)) + }; } function assertLaneDeployAndCatalog(args, deploy, catalog) { @@ -261,12 +286,12 @@ function assertLaneDeployAndCatalog(args, deploy, catalog) { return assertDevOnlyDeployAndCatalog(deploy, catalog); } -function publishRecordsFromReport(report, target) { +function publishRecordsFromReport(report, target, serviceIds = SERVICE_IDS) { const artifactPublish = report.artifactPublish; assert.ok(artifactPublish && typeof artifactPublish === "object", "publish report missing artifactPublish"); assert.equal(artifactPublish.status, "published", "publish report artifactPublish.status must be published"); assert.ok(commitMatchesTarget(artifactPublish.sourceCommitId, target), "publish report sourceCommitId must match target ref"); - assert.equal(artifactPublish.serviceCount, SERVICE_IDS.length, "publish report must cover every frozen service"); + assert.equal(artifactPublish.serviceCount, serviceIds.length, "publish report must cover every service for the selected lane"); assert.ok(artifactPublish.publishPlan && typeof artifactPublish.publishPlan === "object", "publish report missing artifactPublish.publishPlan"); assert.equal(artifactPublish.publishPlan.version, "v2", "publish report plan version must be v2"); @@ -278,8 +303,8 @@ function publishRecordsFromReport(report, target) { .map((service) => service.serviceId); assert.deepEqual( [...requiredServiceIds, ...disabledServiceIds].sort(), - [...SERVICE_IDS].sort(), - "publish plan must cover every frozen service" + [...serviceIds].sort(), + "publish plan must cover every service for the selected lane" ); assert.equal( (artifactPublish.publishedCount ?? 0) + (artifactPublish.reusedCount ?? 0), @@ -288,8 +313,9 @@ function publishRecordsFromReport(report, target) { ); const records = new Map(); + const allowed = new Set(serviceIds); for (const service of artifactPublish.services ?? []) { - assert.ok(SERVICE_IDS.includes(service.serviceId), `unknown service ${service.serviceId} in publish report`); + assert.ok(allowed.has(service.serviceId), `unknown service ${service.serviceId} in publish report`); const image = parseTaggedImage(service.image, service.serviceId); if (!requiredServiceIds.includes(service.serviceId)) { assert.equal(service.artifactRequired, false, `${service.serviceId} disabled service must not be required`); @@ -332,7 +358,7 @@ function publishRecordsFromReport(report, target) { return { records, requiredServiceIds, disabledServiceIds }; } -function refreshDocuments({ deploy, catalog, target, publishRecords, serviceInventory, provenancePath, registryPrefix }) { +function refreshDocuments({ deploy, catalog, target, publishRecords, serviceInventory, provenancePath, registryPrefix, serviceIds = SERVICE_IDS }) { const published = Boolean(publishRecords); const records = publishRecords?.records ?? null; const requiredIds = new Set(publishRecords?.requiredServiceIds ?? serviceInventory.requiredServiceIds); @@ -340,6 +366,7 @@ function refreshDocuments({ deploy, catalog, target, publishRecords, serviceInve const deployByService = new Map(deploy.services.map((service) => [service.serviceId, service])); const inventoryByService = new Map(serviceInventory.services.map((service) => [service.serviceId, service])); const refreshedServices = []; + const nextCatalogServices = []; catalog.commitId = target.imageTag; catalog.artifactState = published ? "published" : "contract-skeleton"; @@ -354,7 +381,7 @@ function refreshDocuments({ deploy, catalog, target, publishRecords, serviceInve : "Artifact identity was refreshed to this source commit, but no publish report proved registry digests." }; - for (const serviceId of SERVICE_IDS) { + for (const serviceId of serviceIds) { const deployService = deployByService.get(serviceId); const catalogService = catalogByService.get(serviceId); assert.ok(deployService, `${serviceId} missing from deploy manifest`); @@ -406,8 +433,11 @@ function refreshDocuments({ deploy, catalog, target, publishRecords, serviceInve artifactRequired: catalogService.artifactRequired, notPublishedReason: catalogService.notPublishedReason }); + nextCatalogServices.push(catalogService); } + catalog.services = nextCatalogServices; + return refreshedServices; } @@ -431,12 +461,14 @@ async function main() { ]); let catalog = await readJsonIfPresent(args.catalogPath, null); if (!catalog && args.lane === "v02") catalog = artifactCatalogSkeleton({ args, target, registryPrefix: publishReport?.artifactPublish?.registryPrefix ?? defaultRegistryPrefix }); + catalog = normalizeCatalogForLane(catalog, args.lane); assert.ok(catalog, `${args.catalogPath} is required for ${args.lane} catalog refresh`); assertLaneDeployAndCatalog(args, deploy, catalog); - const publishRecords = publishReport ? publishRecordsFromReport(publishReport, target) : null; + const serviceIds = serviceIdsForLane(args.lane); + const publishRecords = publishReport ? publishRecordsFromReport(publishReport, target, serviceIds) : null; const serviceInventory = publishReport?.artifactPublish?.serviceInventory ?? serviceInventoryFromServices( - await resolveDevArtifactServices(repoRoot, SERVICE_IDS, SERVICE_IDS) + await resolveDevArtifactServices(repoRoot, serviceIds, SERVICE_IDS) ); const services = refreshDocuments({ deploy, @@ -445,7 +477,8 @@ async function main() { publishRecords, serviceInventory, provenancePath: publishReportProvenance(publishReport, args.publishReportPath ?? defaultPublishReportPath), - registryPrefix: publishReport?.artifactPublish?.registryPrefix ?? defaultRegistryPrefix + registryPrefix: publishReport?.artifactPublish?.registryPrefix ?? defaultRegistryPrefix, + serviceIds }); if (args.write) {