From 0c21e43d9833a144b0034ce9db327cc74b30a357 Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 26 May 2026 23:02:54 +0800 Subject: [PATCH] refactor: remove CI.json from G14 CI --- AGENTS.md | 2 +- CI.json | 41 ------- docs/artifact-catalog.md | 4 +- docs/reference/g14-gitops-cicd.md | 37 +++--- scripts/artifact-publish.mjs | 69 ++---------- scripts/g14-ci-plan.mjs | 3 +- scripts/g14-ci-plan.test.mjs | 28 +++-- scripts/g14-gitops-render.mjs | 105 ++++++++++++------ .../src/dev-evidence-blocker-aggregator.mjs | 2 +- scripts/src/g14-ci-plan-lib.mjs | 25 ++--- scripts/validate-artifact-catalog.mjs | 12 -- scripts/validate-runtime-boundary.mjs | 32 +++--- 12 files changed, 145 insertions(+), 215 deletions(-) delete mode 100644 CI.json diff --git a/AGENTS.md b/AGENTS.md index 231a7720..ddf36515 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -75,7 +75,7 @@ HWLAB 是硬件实验室运行面和控制面项目。本文是 agent、指挥 - Cloud Web M3 只读护栏:`npm run web:m3-readonly` - Cloud Workbench 布局/遮挡 smoke:`npm run web:layout`;local-build 用 `npm run web:layout:build`;DEV deploy 后用 `npm run web:layout:live`。 - 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,兼容 `CI.json` 与 `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.json`;细则见 [docs/reference/g14-gitops-cicd.md](docs/reference/g14-gitops-cicd.md)。 - G14 GitOps 渲染:`npm run g14:gitops:render`;检查已生成 Tekton/Argo CD manifests:`npm run g14:gitops:check` - DEV 依赖 runtime base 构建:`npm run dev-runtime-base:build` - Legacy D601 DEV CD:旧脚本入口已删除;事故回放只读历史 issue/commit,不恢复旧命令。 diff --git a/CI.json b/CI.json deleted file mode 100644 index 7a0ea46d..00000000 --- a/CI.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "version": 1, - "name": "hwlab-deploy-skeleton-ci", - "profile": "dev", - "commands": [ - { - "name": "repo-reports-guard", - "run": "node scripts/repo-reports-guard.mjs" - }, - { - "name": "g14-contract-check", - "run": "node --check scripts/g14-gitops-render.mjs && node scripts/g14-gitops-render.mjs --check && node --check scripts/artifact-publish.mjs && node --check scripts/g14-artifact-publish.mjs" - }, - { - "name": "g14-artifact-publish-static-check", - "run": "node --check scripts/artifact-publish.mjs && node --check scripts/g14-artifact-publish.mjs" - }, - { - "name": "codex-api-forwarder-check", - "run": "node --check cmd/hwlab-codex-api-responses-forwarder/main.mjs && node --check cmd/hwlab-codex-api-responses-forwarder/main.test.mjs && node --test cmd/hwlab-codex-api-responses-forwarder/main.test.mjs" - }, - { - "name": "parse-deploy-json", - "run": "node -e \"JSON.parse(require('node:fs').readFileSync('deploy/deploy.json','utf8'))\"" - }, - { - "name": "parse-k8s-json-yaml", - "run": "node -e \"const fs=require('node:fs'); for (const f of fs.readdirSync('deploy/k8s/base').concat(fs.readdirSync('deploy/k8s/dev').map(x=>'../dev/'+x), fs.readdirSync('deploy/k8s/prod').map(x=>'../prod/'+x))) { if (!f.endsWith('.yaml')) continue; const p=f.startsWith('..')?'deploy/k8s/'+f.replace('../',''):'deploy/k8s/base/'+f; JSON.parse(fs.readFileSync(p,'utf8')); }\"" - }, - { - "name": "parse-frp-toml", - "run": "python3 - <<'PY'\nimport pathlib, tomllib\nfor path in pathlib.Path('deploy/frp').glob('*.toml'):\n tomllib.loads(path.read_text())\nPY" - } - ], - "forbidden": [ - "prod-deploy", - "secret-material", - "unidesk-runtime-substitute", - "force-push" - ] -} diff --git a/docs/artifact-catalog.md b/docs/artifact-catalog.md index cd1c0f4c..eb06fedb 100644 --- a/docs/artifact-catalog.md +++ b/docs/artifact-catalog.md @@ -36,10 +36,10 @@ metadata, then be copied into the catalog by the refresh command below. | --- | --- | | `deploy/artifact-catalog.dev.json` | Machine-readable DEV artifact catalog sample and static source of truth for the skeleton. | | `deploy/deploy.json` | DEV deploy manifest skeleton that the catalog explains. | -| `CI.json` | Lightweight command and forbidden-action skeleton. | +| `scripts/g14-gitops-render.mjs` | Declares the Tekton primitive CI tasks and minimal guardrails that generate the G14 CI/CD control plane. | | `scripts/deploy-desired-state-plan.mjs` | Read-only planner/checker for commit, image tag, workload image, and env mirror convergence across deploy desired-state files. | | `scripts/refresh-artifact-catalog.mjs` | Refreshes source/artifact commit tags and records either blocked `not_published` state or proven publish digests. | -| `scripts/validate-artifact-catalog.mjs` | Local static validator for the catalog, deploy manifest, and CI forbidden list. | +| `scripts/validate-artifact-catalog.mjs` | Local static validator for the catalog and deploy manifest skeleton. | | `scripts/preflight-dev-base-image.mjs` | Local DEV builder base-image preflight for future artifact publish. | | `docs/dev-base-image-preflight.md` | Operator contract for interpreting preflight readiness and blockers. | diff --git a/docs/reference/g14-gitops-cicd.md b/docs/reference/g14-gitops-cicd.md index bd893778..1a29f4be 100644 --- a/docs/reference/g14-gitops-cicd.md +++ b/docs/reference/g14-gitops-cicd.md @@ -5,7 +5,7 @@ G14 是 HWLAB 当前 DEV/PROD 原生 k8s 与 GitOps 运行面目标。G14 CI/CD ## 目标模型 - Source of truth:业务版本以 Git source commit 为唯一身份;镜像 tag、OCI labels、runtime annotation 和 Argo CD desired state 都必须记录同一个 source commit。 -- CI:Tekton 在 G14 k3s 内运行 `hwlab-g14-ci-image-publish` Pipeline,读取 `CI.json` 命令,按 source commit 构建镜像并推送到 G14 registry。 +- CI:Tekton 在 G14 k3s 内运行 `hwlab-g14-ci-image-publish` Pipeline,由 `scripts/g14-gitops-render.mjs` 直接生成原生 task;最小校验固定为 `repo-reports-guard`、`g14-contract-check`、`codex-api-forwarder-check`,随后按 component plan 做 per-service BuildKit publish 与 GitOps promote。 - Artifact:镜像使用 commit tag,例如 `127.0.0.1:5000/hwlab/hwlab-cloud-api:`;digest 由 registry 返回,CI report 只作为审计证据,不作为 CD 真相。 - Branch split:`G14` 是源码监控分支;`G14-gitops` 是 Tekton promotion 写入的生成分支,保存 `deploy/gitops/g14/**` desired state。 - CD:Argo CD 只消费 `G14-gitops:deploy/gitops/g14/runtime-dev` 与 `deploy/gitops/g14/runtime-prod` 的 Git desired state,不重新构建镜像,不读取 D601 状态,不获取 legacy DEV CD Lease。 @@ -16,7 +16,7 @@ G14 是 HWLAB 当前 DEV/PROD 原生 k8s 与 GitOps 运行面目标。G14 CI/CD `scripts/g14-gitops-render.mjs` 是 G14 专用转换器: -- 读取 `CI.json`,生成 Tekton Pipeline 和 PipelineRun 样板。 +- 直接声明 Tekton Pipeline、最小原语校验 task 和 PipelineRun 样板;不再读取 `CI.json` 或生成 `ci-json` step。 - 读取 `deploy/deploy.json` 与 `deploy/k8s/*`,生成 Argo CD 可消费的 G14 runtime Kustomize path。 - G14 Tekton 的镜像构建发布入口必须是 `scripts/g14-artifact-publish.mjs`;它只是集群内 Task 的 build/push helper,不做 rollout、不写 D601、不获取 legacy DEV CD Lease。旧 `scripts/dev-artifact-publish.mjs` 入口已删除;`dev-cd-apply`、`ci-publish` 和旧 `main` JS CD 入口禁止出现在 G14 Pipeline 生成脚本和 G14 验收证据中。 - 生成 `hwlab-g14-branch-poller` CronJob:它使用 G14 集群内的 Git SSH Secret 轮询 `G14` 分支,按 source commit 创建确定命名的 Tekton PipelineRun。 @@ -25,8 +25,8 @@ G14 是 HWLAB 当前 DEV/PROD 原生 k8s 与 GitOps 运行面目标。G14 CI/CD - 默认 GitOps 生成分支是 `G14-gitops`;Pipeline 成功后把本次 source commit 对应的 `deploy/gitops/g14/**` 推送到该分支,避免把生成提交继续写回 `G14`。 - 默认 registry prefix 是 `127.0.0.1:5000/hwlab`,用于 G14 单节点 k3s 的 node-local registry。 - 默认 CI/CD proxy 是 G14 本机 `http://127.0.0.1:10808` / `socks5h://127.0.0.1:10808`。Tekton CI step、Docker-in-Docker sidecar 和 publish step 都注入 proxy/no_proxy;publish step 必须先在 DIND 内 `docker pull $HWLAB_DEV_BASE_IMAGE`,再进入 DEV base-image preflight、build 和 registry push,保证 npm、base image 拉取和 registry push 走 G14 本机代理边界。 -- `ci-json` 不允许每次运行时重新 `apt-get install` 或 `npx playwright install --with-deps`。它必须使用已经预热的 CI 基础镜像,当前默认由 `scripts/g14-gitops-render.mjs` 固定为 `mcr.microsoft.com/playwright@sha256:b0ab6f3cb99aa7803adbc14d9027ec1785fc6e433b97e134e0f8fe61683b6b53`;该镜像在 G14 k3s/containerd 中已缓存,包含 Node、npm、Git、Python3、OpenSSH、curl、Playwright 系统依赖和 `/ms-playwright/chromium-1217`。脚本启动时必须输出 `ci-base-image` 和 `playwright-cache-check` 结构化日志;缺少工具或 Chromium cache 时直接失败并修基础镜像,不能回退到 runtime apt 下载。若以后需要 Node 22 固定版本,先构建并推送 G14 registry 内的 Node22+Playwright runner 镜像,再修改 `HWLAB_G14_CI_JSON_IMAGE`/render 默认值。 -- G14 host 只用于 source workspace、GitOps render、k3s 控制和轻量语法/静态合同检查;不要把 host 当成浏览器执行面。当前 G14 host OS 可能比 Playwright 官方 browser 包支持矩阵更新,`npx playwright install chromium` 会失败或 browser executable 不存在。`web:check` 默认允许这类明确的 Playwright/browser runtime 环境限制作为 `environment_blocker`/`skip` 通过源码合同;真正的布局、移动端和交互 browser smoke 必须在 G14 k3s/Tekton 的 Playwright 基础镜像内运行,不能用 host 上强装 browser 作为长期方案。 +- `prepare-source` 和最小原语校验 task 不允许每次运行时重新 `apk add`、`apt-get install` 或临时下载 browser/runtime 依赖。当前固定工具镜像是 `127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-v1`,包含 Node 22、npm、Git、OpenSSH、curl、Python3 和 Docker CLI。脚本启动时必须输出工具与 proxy preflight 结构化日志;缺少工具时应先重建并推送工具镜像,再修改 `HWLAB_G14_CI_TOOLS_IMAGE`/render 默认值,不能回退到 runtime 安装。 +- G14 host 只用于 source workspace、GitOps render、k3s 控制和轻量语法/静态合同检查;不要把 host 当成浏览器执行面。低频 browser smoke 已不再属于默认 primitive CI。若确实需要一次性布局、移动端或交互验证,必须显式在 G14 k3s/Tekton 的专用 Playwright 镜像内运行,不能回退成 host 上强装 browser 的长期方案。 - Poller、control-plane reconciler、image publish 和 GitOps promote step 都不允许每次运行时 `apk add` / `apt-get install`。当前 G14 registry 固定工具镜像是 `127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-v1`,包含 Node 22、npm、Git、OpenSSH、curl、Python3 和 Docker CLI;生成的脚本只做 proxy preflight 与工具存在性检查。若需要升级工具,先在 G14 构建/推送新的工具镜像,再修改 `HWLAB_G14_CI_TOOLS_IMAGE`/render 默认值并由 reconciler apply。 - 服务镜像构建的默认 parent/base image 不得从 Docker Hub 反复拉取。当前 `node:20-bookworm-slim` 已镜像到 G14 registry 的 allowlist 名称:`127.0.0.1:5000/hwlab/hwlab-node20-base:20-bookworm-slim`,render 默认 `base-image` 和 poller `BASE_IMAGE` 都指向这个本地镜像。需要升级 parent image 时,先通过 G14 proxy 拉取并推送到 G14 registry,再修改 `HWLAB_G14_DEV_BASE_IMAGE`/render 默认值;image publish step 只允许从本地 registry pull base image,且 tag 必须符合 publish gate 的 `hwlab-node20-base`/`hwlab-dev-base`/`hwlab-node-runtime-base` allowlist。 - 任何依赖下载阶段都必须有可观测诊断。生成的 Tekton 脚本在 apt/apk、npm、Playwright browser、Docker base image pull 和 GitOps promote 之前输出结构化 `dependency-proxy-probe`、`dependency-curl-probe` 或 `dependency-download-*` 日志,至少包含 phase、目标 URL/镜像、脱敏 proxy、首包耗时、总耗时、下载字节数和速度。CI/CD 卡在下载时,先用这些日志判断是 proxy 不可达、目标源慢、DNS/首包慢还是下载吞吐低,再决定是否切换 G14 代理节点或预热镜像;不能只凭 PipelineRun Running 时长判断业务测试失败。 @@ -38,11 +38,11 @@ npm run g14:gitops:render -- --source-revision npm run g14:gitops:check ``` -## 原生 k8s Tekton + Argo 配置面 vs CI.json runner 执行面 +## 原生 k8s Tekton + Argo 配置面 vs 已废弃的 CI.json runner -这两条路线不是平行替代关系,而是当前 G14 CI/CD 的分层:Tekton + Argo 是发布控制面,`CI.json` runner 是 source-local 执行面。当前标准发布路径仍然是 `G14` source branch -> Tekton -> `G14-gitops` -> Argo CD;`ci-json` 只负责在 Tekton 的 `ci-json` step 内执行 repo-owned 命令清单,不能替代 GitOps desired state、Argo rollout 或 live runtime 验收。 +当前 G14 已完全删除 `CI.json` 和 `ci-json` step。下面的对比只保留为迁移理由:Tekton + Argo 是唯一发布控制面;repo-local 校验已经收敛为 render 内建的少量原语 task,而不是再保留一条 shell runner 路线。 -| 维度 | 原生 k8s Tekton + Argo 配置面 | `CI.json` runner 执行面 | +| 维度 | 原生 k8s Tekton + Argo 配置面 | 已废弃的 `CI.json` runner 路线 | | --- | --- | --- | | 权威职责 | 构建、发布、GitOps promotion、Argo sync、runtime rollout | repo-local 静态合同、schema、focused smoke、预检命令 | | 真相来源 | PipelineRun、TaskRun result、`G14-gitops` revision、Argo Application、live workload | 仓库内 `CI.json` 命令、runner 镜像、单个 `ci-json` TaskRun 日志 | @@ -54,16 +54,17 @@ npm run g14:gitops:check 选择规则: - 只要问题涉及 release truth、rollout、runtime desired state、namespace workload 健康、public health 或 artifact provenance,就必须回到 Tekton + Argo 配置面。 -- 只要问题仍停留在 repo-local 命令、静态合同、source-only smoke 或 focused preflight,可以先在 `CI.json` runner 执行面收敛,但收敛后的结论仍需通过 Tekton + Argo 标准路径固化。 -- `CI.json` runner 的价值是降低“增加一条检查”的成本,不是提供第二套发布控制面;如果一项检查开始依赖 rollout 顺序、TaskRun results、GitOps branch、Argo revision 或 live runtime,它就应该升级为 Tekton/Argo 路径的一部分,而不是继续挤在 `ci-json` 里。 +- 只要问题涉及 release truth、rollout、runtime desired state、namespace workload 健康、public health 或 artifact provenance,就必须回到 Tekton + Argo 配置面。 +- 只要问题仍停留在 repo-local 静态合同或 focused preflight,就把它实现成独立的原语 Tekton task;不值得长期保留的检查直接删除,而不是重新引入 `CI.json` runner。 +- 不要再创建第二套 shell 命令发布面;如果一项检查开始依赖 rollout 顺序、TaskRun results、GitOps branch、Argo revision 或 live runtime,它就必须属于 Tekton/Argo 标准路径。 ## Monorepo 组件计划与兼容 render -HWLAB 是 monorepo,G14 CI/CD 加速必须按组件输入判断构建和滚动,但不能破坏当前 `CI.json`、`deploy/deploy.json` 与全量 source commit render 合同。 +HWLAB 是 monorepo,G14 CI/CD 加速必须按组件输入判断构建和滚动,并直接依赖内建 component model、`deploy/deploy.json` 与 per-service artifact catalog;`CI.json` 已删除,不再作为任何 planner 输入。 -- `scripts/g14-ci-plan.mjs` 是只读 planner,默认读取当前 `CI.json`、`deploy/deploy.json` 和 `deploy/artifact-catalog.dev.json`,输出 `affectedServices`、`reusedServices`、`componentCommitId`、`componentInputHash`、`dockerfileHash`、`baseImageDigest`、`buildArgsHash` 和原因;它不得修改 CI、deploy、catalog 或 GitOps 文件。 +- `scripts/g14-ci-plan.mjs` 是只读 planner,默认读取 `deploy/deploy.json`、`deploy/artifact-catalog.dev.json` 和 `scripts/src/g14-ci-plan-lib.mjs` 内建 component model,输出 `affectedServices`、`reusedServices`、`componentCommitId`、`componentInputHash`、`dockerfileHash`、`baseImageDigest`、`buildArgsHash` 和原因;它不得修改 deploy、catalog 或 GitOps 文件。 - 服务清单兼容顺序固定为:显式 `--services`、`deploy.services[]`、`deploy.k3s.serviceMappings[]`、`internal/protocol.SERVICE_IDS`。因此旧 `deploy/deploy.json` 形态和当前完整 `deploy.services[]` 形态都必须能被 planner 识别。 -- `CI.json` 仍是 CI 命令与禁用项合同来源;可选新增 `g14CiPlan.components.` 覆盖 `componentPaths`、`sharedPaths`、`runtimeDeps`、`buildSystemPaths`,但不得要求删除或改写现有 `commands` / `forbidden` 字段。 +- 组件边界固定由 `scripts/src/g14-ci-plan-lib.mjs` 的内建 service-path model 定义;如需新增或调整 `componentPaths`、`sharedPaths`、`runtimeDeps` 或 `buildSystemPaths`,直接修改 planner 库和对应测试,不再额外维护 repo-local commands/forbidden skeleton。 - `scripts/g14-artifact-publish.mjs` 默认启用组件级 lazy build:先运行 planner,再只构建/推送 `affectedServices`,`reusedServices` 必须从 `deploy/artifact-catalog.dev.json` 复用已有 sha256 digest;如果 catalog 没有可验证 digest,必须结构化阻塞并提示先跑全量 publish。`--full-build` 是兼容回退开关,用于强制构建/发布每个 required service。 - `scripts/g14-artifact-publish.mjs` 的 publish report 必须携带 planner 的 per-service 元数据;`scripts/refresh-artifact-catalog.mjs` 把这些字段复制进 `deploy/artifact-catalog.dev.json` 和 `deploy/deploy.json` 的对应 service。旧字段 `commitId`、`image`、`imageTag`、`digest`、`publishState` 语义不变,旧检查脚本不得因为新增字段失败。 - `scripts/g14-gitops-render.mjs` 默认启用混合 desired state:workload 的 container image、`HWLAB_IMAGE`、`HWLAB_IMAGE_TAG` 和 pod template `source-commit` 来自 `deploy.services[]` 的 per-service image/env;全局 GitOps metadata 仍记录本次 source commit。`--legacy-source-images` 或 `HWLAB_G14_USE_DEPLOY_IMAGES=0` 是兼容回退开关,用于把所有 workload image 渲染为同一个 source commit tag。 @@ -83,14 +84,14 @@ G14 CI/CD 加速的第一判定标准不是 PipelineRun 总耗时单点变短, 当前仍然慢的主要位置: -- `ci-json` 仍是大头,当前量级约 90 秒。它包含 Git clone、checkout、`npm ci`、Playwright cache probe 和 `CI.json` 命令执行。继续加速应优先减少每轮 `npm ci` 成本,例如使用预热 workspace/cache、依赖层镜像或把源码校验拆成更细粒度的 Tekton Task。 +- `prepare-source` 与最小原语校验 task 仍是前段主要开销。它们包含 Git clone、checkout、必要的 `npm ci`、`repo-reports-guard`、`g14-contract-check` 和 `codex-api-forwarder-check`。继续加速应优先减少 workspace cold start 与依赖安装成本,而不是重新塞回一条 shell runner。 - `gitops-promote` 仍是大头,当前量级约 100 秒。它包含 source branch freshness check、artifact catalog refresh、render/check、clone `G14-gitops`、复制 generated desired state、commit 和 push。继续加速应优先减少 git clone/push 成本,例如使用浅 clone、持久 workspace、server-side patch 或把 GitOps repo 操作单独缓存。 - per-service TaskRun 数量增加后会有 k8s 调度和 sidecar 生命周期开销。reuse-only 任务很快,但如果每个 unchanged service 都启动完整 buildkit sidecar,调度开销会抵消部分收益;长期目标是让 unchanged service 走更轻量的 reuse Task,changed service 才启动 BuildKit。 - per-service BuildKit 必须使用 Pod 内 Unix socket 或其他 Pod-scoped 端点,禁止在 hostNetwork 下绑定固定 TCP 端口。多个 service TaskRun 并发时,如果 sidecar 统一监听 `0.0.0.0:1234`,会抢同一个宿主端口,导致主 step 结果成功但 Pod phase 显示 Failed,严重污染 CI 观测。 - Argo sync/health 不是 instant。`Sync=Synced` 后 health 仍可能因为 replicas=0 的模板 Deployment、未排除 health 的模板 Job 或 StatefulSet 旧 revision 显示 `Suspended`/`Progressing`。这时应先看实际长驻 workload ready 与具体 unhealthy resource,不要只看 Application 总 health;如果 suspended Job 只是模板资源,应修 render 注解而不是把 DEV rollout 判成失败。 - StatefulSet 旧 ordinal pod 可能长期卡在历史缺失镜像上,即使 StatefulSet 当前 template 已经修正。DEV 可按运行面热修流程删除旧 Pending pod 让 StatefulSet 按当前 template 重建;PROD namespace 需要明确 maintenance 授权后再做同类删除。 -每次优化 CI/CD 都必须保留可比测量:记录 PipelineRun 总耗时、`ci-json`、`plan-artifacts`、per-service fan-out 起止窗口、`collect-artifacts`、`gitops-promote`、Argo sync 到 workload ready 的时间,并标明本轮是 reuse-only、单组件 build 还是多组件 build。没有这些分段数据,不要只用“感觉变快/变慢”判断优化成败。 +每次优化 CI/CD 都必须保留可比测量:记录 PipelineRun 总耗时、`prepare-source`、`repo-reports-guard`、`g14-contract-check`、`codex-api-forwarder-check`、`plan-artifacts`、per-service fan-out 起止窗口、`collect-artifacts`、`gitops-promote`、Argo sync 到 workload ready 的时间,并标明本轮是 reuse-only、单组件 build 还是多组件 build。没有这些分段数据,不要只用“感觉变快/变慢”判断优化成败。 ## Code Agent Provider Profiles @@ -118,7 +119,7 @@ G14 不要求 GitHub webhook 或 GitHub Actions 配置。`hwlab-ci/hwlab-g14-bra 如果同名 PipelineRun 已存在,poller 直接跳过;如果不存在,则创建新的 PipelineRun。这样可以用 Kubernetes 原生 CronJob、ServiceAccount、RBAC 和 Tekton API 实现无 GitHub webhook 的分支监控,同时避免同一个 commit 被反复派单。历史上的 `chore: promote G14 GitOps source ...` 生成提交会被 poller 跳过。 -Pipeline 的标准路径是:`G14` source commit -> `CI.json` Tekton 测试 -> commit-tagged image push 到 G14 registry -> render `deploy/gitops/g14/**` -> push `G14-gitops` -> Argo CD 同步 runtime。CD 只消费已经构建好的镜像和 Git desired state,不在 Argo CD 内构建镜像。 +Pipeline 的标准路径是:`G14` source commit -> Tekton 原语校验 task -> commit-tagged image push 到 G14 registry -> render `deploy/gitops/g14/**` -> push `G14-gitops` -> Argo CD 同步 runtime。CD 只消费已经构建好的镜像和 Git desired state,不在 Argo CD 内构建镜像。 GitOps promotion 成功只证明 `G14-gitops` 分支已经写入新 desired state;如果 render 改变了 Argo Application、AppProject、runtime path 或 DEV/PROD 拆分目录,必须同步检查并应用 `deploy/gitops/g14/argocd/project.yaml`、`application-dev.yaml` 和 `application-prod.yaml`。`hwlab-g14-dev` 必须指向 `deploy/gitops/g14/runtime-dev`,`hwlab-g14-prod` 必须指向 `deploy/gitops/g14/runtime-prod`;如果集群里 Application 仍指向旧 `deploy/gitops/g14/runtime`,Argo 会停在旧 revision,即使 CI/publish/promote 全部成功也不会滚动新镜像。 @@ -131,7 +132,7 @@ KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl -n hwlab-ci logs pod/ CI -> CD 最短零误判 SOP @@ -220,7 +221,7 @@ node scripts/g14-ci-plan.mjs --base-ref origin/G14 --target-ref HEAD --pretty 5. 确认 CI 真实通过 -- 最低通过条件是:`ci-json`、`plan-artifacts`、per-service fan-out、`collect-artifacts`、`gitops-promote` 全部成功。 +- 最低通过条件是:`prepare-source`、`repo-reports-guard`、`g14-contract-check`、`codex-api-forwarder-check`、`plan-artifacts`、per-service fan-out、`collect-artifacts`、`gitops-promote` 全部成功。 - `build-*` fan-out 要按 result 判断 reused 还是 rebuilt;不要只看 TaskRun 名称。 - reuse-only 变更的目标是 `affectedServices=[]`、全部 service `status=reused`,且不触发无意义 rollout。 diff --git a/scripts/artifact-publish.mjs b/scripts/artifact-publish.mjs index 287e2b1a..7e27c616 100644 --- a/scripts/artifact-publish.mjs +++ b/scripts/artifact-publish.mjs @@ -198,8 +198,8 @@ function printHelp() { " --base-image IMAGE override HWLAB_DEV_BASE_IMAGE for this run; must already be local", " --services LIST comma-separated service IDs; default: all frozen DEV services", " --affected-only default: build/publish only g14-ci-plan affected services and reuse unchanged catalog digests", - " --full-build compatibility fallback: build/publish every required service", - " --build-backend NAME buildkit (default) or docker compatibility fallback", + " --full-build build/publish every required service even when planner would reuse prior digests", + " --build-backend NAME buildkit only", " --buildkit-command PATH buildctl-daemonless.sh/buildctl command for buildkit backend", " --buildkit-addr ADDR optional buildctl --addr endpoint for a sidecar BuildKit daemon", " --external-service-report-dir DIR collect affected service artifacts from per-service publish reports", @@ -338,7 +338,7 @@ function dockerfileQuote(value) { } function validBuildBackend(value) { - return value === "buildkit" || value === "docker"; + return value === "buildkit"; } function buildkitCacheRef(registryPrefix, serviceId) { @@ -1093,16 +1093,6 @@ async function preflight({ args, services, catalog, deployManifest, baseImagePre } } - if (buildRequired && args.buildBackend === "docker" && registryCapabilities?.dimensions?.dockerDaemonPushAccess?.status === "blocked") { - blockers.push( - blocker({ - type: "network_blocker", - scope: "docker-daemon-push-access", - summary: registryCapabilities.dimensions.dockerDaemonPushAccess.summary, - next: "Restore Docker daemon access to the G14 local/internal registry before running --publish." - }) - ); - } if (buildRequired && args.buildBackend === "buildkit") { const buildkitPreflight = await buildkitCommandPreflight(args.buildkitCommand); if (!buildkitPreflight.ok) { @@ -1111,7 +1101,7 @@ async function preflight({ args, services, catalog, deployManifest, baseImagePre type: "environment_blocker", scope: "buildkit-command", summary: `BuildKit command ${args.buildkitCommand} is not available in the publish task image.`, - next: "Use the G14 Tekton BuildKit path that prepares buildctl/buildkitd, or set --build-backend docker as an explicit compatibility fallback." + next: "Use the G14 Tekton BuildKit path that prepares buildctl/buildkitd before running --publish." }) ); } @@ -1252,53 +1242,8 @@ async function buildService({ args, repo, commitId, shortCommit, service, buildC ["PORT", String(port)], ["HWLAB_PORT", String(port)] ]; - if (args.buildBackend === "buildkit") { - return buildServiceWithBuildkit({ args, service, ref, tag, buildCreatedAt, buildSource, port, labels, envs }); - } - - const argsList = ["build", "--pull=false"]; - if (args.quietBuild) argsList.push("--quiet"); - - for (const [name, value] of labels) argsList.push("--label", `${name}=${value}`); - for (const [name, value] of envs) argsList.push("--build-arg", `${name}=${value}`); - - argsList.push("-t", ref, "-f", "-", "."); - const result = await runWithInput("docker", argsList, dockerfile(args.baseImage, port, labels)); - if (result.code !== 0) { - return { - ...service, - image: ref, - imageTag: tag, - buildCreatedAt, - buildSource, - status: "build_failed", - digest: "not_published", - blocker: blocker({ - type: "environment_blocker", - scope: service.serviceId, - summary: `docker build failed for ${service.serviceId}`, - next: `Inspect the Docker build output for ${service.serviceId}, then fix its copied source or base image.` - }), - logTail: tailText(`${result.stdout}\n${result.stderr}`), - dockerBuildDurationMs: result.durationMs - }; - } - const imageInspect = await run("docker", ["image", "inspect", "--format", "{{.Id}}", ref]); - - return { - ...service, - image: ref, - imageTag: tag, - buildCreatedAt, - buildSource, - status: "built", - localImageId: imageInspect.code === 0 ? imageInspect.stdout.trim() : null, - dockerBuildDurationMs: result.durationMs, - dockerImageInspectDurationMs: imageInspect.durationMs, - dockerImageInspectStatus: imageInspect.code === 0 ? "pass" : "failed", - dockerBuildLogTail: tailText(`${result.stdout}\n${result.stderr}`, 1200), - digest: "not_published" - }; + assert.equal(args.buildBackend, "buildkit", "artifact publish must use BuildKit only"); + return buildServiceWithBuildkit({ args, service, ref, tag, buildCreatedAt, buildSource, port, labels, envs }); } async function buildServiceWithBuildkit({ args, service, ref, tag, buildCreatedAt, buildSource, port, labels, envs }) { @@ -2047,7 +1992,7 @@ async function main() { return; } if (!validBuildBackend(args.buildBackend)) { - throw new Error(`unsupported build backend ${args.buildBackend}; expected buildkit or docker`); + throw new Error(`unsupported build backend ${args.buildBackend}; expected buildkit`); } if (args.mode === "publish") { const identityGuard = requireG14ArtifactPublishIdentity({ diff --git a/scripts/g14-ci-plan.mjs b/scripts/g14-ci-plan.mjs index acaf0ca1..4d970e0b 100644 --- a/scripts/g14-ci-plan.mjs +++ b/scripts/g14-ci-plan.mjs @@ -17,7 +17,6 @@ function parseArgs(argv) { const arg = argv[index]; if (arg === "--base-ref") parsed.baseRef = readOption(argv, ++index, arg); else if (arg === "--target-ref") parsed.targetRef = readOption(argv, ++index, arg); - else if (arg === "--ci-json") parsed.ciJsonPath = readOption(argv, ++index, arg); else if (arg === "--deploy-json") parsed.deployJsonPath = readOption(argv, ++index, arg); else if (arg === "--artifact-catalog") parsed.artifactCatalogPath = readOption(argv, ++index, arg); else if (arg === "--registry-prefix") parsed.registryPrefix = readOption(argv, ++index, arg); @@ -41,7 +40,7 @@ function usage() { "usage: node scripts/g14-ci-plan.mjs [--base-ref REF] [--target-ref REF] [--pretty]", "", "Plan G14 monorepo image builds without mutating CI/CD state.", - "The planner reads current CI.json and deploy/deploy.json by default and remains compatible with legacy full-build render.", + "The planner reads current deploy/deploy.json and the built-in G14 CI component model by default.", "", "examples:", " node scripts/g14-ci-plan.mjs --base-ref HEAD~1 --target-ref HEAD --pretty", diff --git a/scripts/g14-ci-plan.test.mjs b/scripts/g14-ci-plan.test.mjs index 8627df26..2553fd50 100644 --- a/scripts/g14-ci-plan.test.mjs +++ b/scripts/g14-ci-plan.test.mjs @@ -42,20 +42,19 @@ test("G14 CI planner treats docs-only changes as no image build", async () => { assert.equal(plan.changedPathSummary.docsOnly, true); }); -test("component model supports CI.json overrides without replacing deploy.json", () => { - const models = componentModelsForServices(["hwlab-cloud-api"], { - g14CiPlan: { - components: { - "hwlab-cloud-api": { - componentPaths: ["custom/api/"], - sharedPaths: ["shared/runtime.mjs"], - runtimeDeps: ["package.json"] - } - } - } - }); - assert.deepEqual(models[0].componentPaths, ["custom/api/"]); - assert.deepEqual(matchingPaths(["custom/api/main.mjs"], models[0].componentPaths), ["custom/api/main.mjs"]); +test("component model uses built-in service paths", () => { + const models = componentModelsForServices(["hwlab-cloud-api"]); + assert.deepEqual(models[0].componentPaths, [ + "cmd/hwlab-cloud-api/", + "cmd/hwlab-codex-api-responses-forwarder/", + "cmd/hwlab-deepseek-responses-bridge/", + "internal/audit/", + "internal/cloud/", + "internal/db/", + "skills/", + "tools/hwlab-gateway-shell.mjs" + ]); + assert.deepEqual(matchingPaths(["cmd/hwlab-cloud-api/main.mjs"], models[0].componentPaths), ["cmd/hwlab-cloud-api/main.mjs"]); }); test("planner remains compatible with deploy.k3s.serviceMappings shape", async () => { @@ -84,7 +83,6 @@ async function createFixtureRepo(options = {}) { await mkdir(path.join(repo, "cmd/hwlab-deepseek-responses-bridge"), { recursive: true }); await mkdir(path.join(repo, "web/hwlab-cloud-web"), { recursive: true }); await mkdir(path.join(repo, "deploy"), { recursive: true }); - await writeFile(path.join(repo, "CI.json"), JSON.stringify({ version: 1, commands: [] }, null, 2)); await writeFile(path.join(repo, "deploy/deploy.json"), JSON.stringify(createDeployFixture({ deployServices, k3sServiceMappings }), null, 2)); await writeFile(path.join(repo, "package.json"), JSON.stringify({ type: "module" }, null, 2)); await writeFile(path.join(repo, "package-lock.json"), JSON.stringify({ lockfileVersion: 3 }, null, 2)); diff --git a/scripts/g14-gitops-render.mjs b/scripts/g14-gitops-render.mjs index afc42d5b..8acab297 100644 --- a/scripts/g14-gitops-render.mjs +++ b/scripts/g14-gitops-render.mjs @@ -21,7 +21,6 @@ const defaultProdWebEndpoint = process.env.HWLAB_G14_PROD_WEB_ENDPOINT || "http: const defaultProxyUrl = process.env.HWLAB_G14_PROXY_URL || "http://127.0.0.1:10808"; const defaultAllProxyUrl = process.env.HWLAB_G14_ALL_PROXY_URL || "socks5h://127.0.0.1:10808"; const defaultNoProxy = process.env.HWLAB_G14_NO_PROXY || "hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc"; -const ciJsonRunnerImage = process.env.HWLAB_G14_CI_JSON_IMAGE || "mcr.microsoft.com/playwright@sha256:b0ab6f3cb99aa7803adbc14d9027ec1785fc6e433b97e134e0f8fe61683b6b53"; const ciToolsRunnerImage = process.env.HWLAB_G14_CI_TOOLS_IMAGE || "127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-v1"; const buildkitRunnerImage = process.env.HWLAB_G14_BUILDKIT_IMAGE || "moby/buildkit:rootless"; const defaultDevBaseImage = process.env.HWLAB_G14_DEV_BASE_IMAGE || "127.0.0.1:5000/hwlab/hwlab-node20-base:20-bookworm-slim"; @@ -50,6 +49,29 @@ const codexApiProfileBaseUrl = process.env.HWLAB_G14_CODEX_API_PROFILE_BASE_URL const codexApiForwarderPort = process.env.HWLAB_G14_CODEX_API_FORWARDER_PORT || "49280"; const codexApiForwarderUpstreamBaseUrl = process.env.HWLAB_G14_CODEX_API_UPSTREAM_BASE_URL || "https://hyueapi.com"; const sourceCommitPattern = /^[a-f0-9]{7,40}$/u; +const primitiveValidationTasks = Object.freeze([ + { + name: "repo-reports-guard", + commands: ["node scripts/repo-reports-guard.mjs"] + }, + { + name: "g14-contract-check", + commands: [ + "node --check scripts/g14-gitops-render.mjs", + "node scripts/g14-gitops-render.mjs --check", + "node --check scripts/artifact-publish.mjs", + "node --check scripts/g14-artifact-publish.mjs" + ] + }, + { + name: "codex-api-forwarder-check", + commands: [ + "node --check cmd/hwlab-codex-api-responses-forwarder/main.mjs", + "node --check cmd/hwlab-codex-api-responses-forwarder/main.test.mjs", + "node --test cmd/hwlab-codex-api-responses-forwarder/main.test.mjs" + ] + } +]); function proxyEnv() { return [ @@ -115,7 +137,7 @@ function usage() { return [ "usage: node scripts/g14-gitops-render.mjs [options]", "", - "Render G14-only Kubernetes-native CI/CD manifests from CI.json and deploy/deploy.json.", + "Render G14-only Kubernetes-native CI/CD manifests from built-in Tekton CI primitives, deploy/deploy.json, and deploy/k8s/*.", "", "options:", ` --out DIR default: ${defaultOutDir}`, @@ -129,7 +151,7 @@ function usage() { ` --prod-runtime-endpoint URL default: ${defaultProdRuntimeEndpoint}`, ` --prod-web-endpoint URL default: ${defaultProdWebEndpoint}`, " --use-deploy-images default: render workload images/env from deploy/deploy.json per service", - " --legacy-source-images compatibility fallback: render every workload with the source revision tag", + " --legacy-source-images render every workload with the source revision tag instead of per-service desired images", " --check verify generated files are current without writing", " --no-write plan and print generated file list without writing" ].join("\n"); @@ -590,20 +612,16 @@ curl_dependency_probe() { }`; } -function ciJsonScript(ci) { - const commands = ci.commands ?? []; +function prepareSourceScript() { const lines = [ "#!/bin/sh", "set -eu", - dependencyProxyProbeScript("ci-json-proxy-preflight", "http://deb.debian.org/debian/dists/bookworm/InRelease"), + dependencyProxyProbeScript("prepare-source-proxy-preflight", "http://deb.debian.org/debian/dists/bookworm/InRelease"), curlDownloadProbeFunction(), - "echo '{\"event\":\"ci-base-image\",\"policy\":\"no-runtime-apt-or-playwright-install\"}'", + `echo '{"event":"ci-base-image","phase":"prepare-source","image":"${ciToolsRunnerImage}","policy":"no-runtime-apk"}'`, "for tool in node npm git python3 ssh curl; do command -v \"$tool\" >/dev/null 2>&1 || { echo '{\"event\":\"ci-base-image\",\"ok\":false,\"reason\":\"missing-tool\",\"tool\":\"'\"$tool\"'\"}'; exit 31; }; done", - "export PLAYWRIGHT_BROWSERS_PATH=\"${PLAYWRIGHT_BROWSERS_PATH:-/ms-playwright}\"", - "test -d \"$PLAYWRIGHT_BROWSERS_PATH\" || { echo '{\"event\":\"ci-base-image\",\"ok\":false,\"reason\":\"missing-playwright-browsers-path\"}'; exit 32; }", - "find \"$PLAYWRIGHT_BROWSERS_PATH\" -maxdepth 2 -type d -name 'chromium-*' | grep -q . || { echo '{\"event\":\"ci-base-image\",\"ok\":false,\"reason\":\"missing-chromium-cache\"}'; exit 33; }", - "node -e 'console.log(JSON.stringify({event:\"ci-base-image\",ok:true,node:process.version,browsersPath:process.env.PLAYWRIGHT_BROWSERS_PATH}))'", - "curl_dependency_probe \"ci-json-pre-npm\" \"https://registry.npmjs.org/npm/latest\"", + "node -e 'console.log(JSON.stringify({event:\"ci-base-image\",phase:\"prepare-source\",ok:true,node:process.version}))'", + "curl_dependency_probe \"prepare-source-pre-npm\" \"https://registry.npmjs.org/npm/latest\"", "mkdir -p /root/.ssh", "cp /workspace/git-ssh/ssh-privatekey /root/.ssh/id_rsa", "chmod 600 /root/.ssh/id_rsa", @@ -611,24 +629,44 @@ function ciJsonScript(ci) { "rm -rf /workspace/source/repo", "git clone --branch \"$(params.source-branch)\" \"$(params.git-url)\" /workspace/source/repo", "cd /workspace/source/repo", + "git config --global --add safe.directory /workspace/source/repo", "git checkout \"$(params.revision)\"", "test \"$(git rev-parse HEAD)\" = \"$(params.revision)\"", "npm ci --ignore-scripts", - "playwright_browser_probe_url=\"$(node -e 'const fs=require(\"node:fs\"); let url=\"\"; try { const p=require.resolve(\"playwright-core/browsers.json\"); const data=JSON.parse(fs.readFileSync(p,\"utf8\")); const b=data.browsers.find((item)=>item.name===\"chromium\"); if (b?.revision) url=\"https://playwright.azureedge.net/builds/chromium/\"+b.revision+\"/chromium-linux.zip\"; } catch {} process.stdout.write(url);' 2>/dev/null || true)\"", - "if [ -n \"$playwright_browser_probe_url\" ]; then curl_dependency_probe \"ci-json-pre-playwright\" \"$playwright_browser_probe_url\"; fi", - "node - <<'NODE'\nconst fs = require(\"node:fs\");\nconst { chromium } = require(\"playwright\");\nconst executablePath = chromium.executablePath();\nfs.accessSync(executablePath, fs.constants.X_OK);\nconsole.log(JSON.stringify({ event: \"playwright-cache-check\", ok: true, executablePath, browsersPath: process.env.PLAYWRIGHT_BROWSERS_PATH || \"\" }));\nNODE", - `echo ${shellSingleQuote(`CI.json ${ci.name ?? "hwlab"} command count=${commands.length}`)}` + `echo ${shellSingleQuote(`G14 prepare-source complete; validation task count=${primitiveValidationTasks.length}`)}` ]; - for (const command of commands) { - lines.push(`echo ${shellSingleQuote(`CI.json command: ${command.name}`)}`); - lines.push("("); - lines.push("set -eu"); - lines.push(command.run); - lines.push(")"); - } return `${lines.join("\n")}\n`; } +function sourceValidationScript(commands) { + return [ + "#!/bin/sh", + "set -eu", + "git config --global --add safe.directory /workspace/source/repo", + "cd /workspace/source/repo", + "test \"$(git rev-parse HEAD)\" = \"$(params.revision)\"", + ...commands + ].join("\n") + "\n"; +} + +function primitiveValidationTaskNames() { + return primitiveValidationTasks.map((task) => task.name); +} + +function primitiveValidationTask(task) { + return { + name: task.name, + runAfter: ["prepare-source"], + workspaces: [{ name: "source", workspace: "source" }], + taskSpec: { + params: [{ name: "revision" }], + workspaces: [{ name: "source" }], + steps: [{ name: task.name, image: ciToolsRunnerImage, env: proxyEnv(), script: sourceValidationScript(task.commands) }] + }, + params: [{ name: "revision", value: "$(params.revision)" }] + }; +} + function publishScript() { return `#!/bin/sh set -eu @@ -1269,7 +1307,7 @@ function dockerSidecar() { function legacyImagePublishTask() { return { name: "image-publish", - runAfter: ["ci-json"], + runAfter: primitiveValidationTaskNames(), workspaces: [{ name: "source", workspace: "source" }], taskSpec: { params: [ @@ -1303,7 +1341,7 @@ function legacyImagePublishTask() { function planArtifactsTask() { return { name: "plan-artifacts", - runAfter: ["ci-json"], + runAfter: primitiveValidationTaskNames(), workspaces: [{ name: "source", workspace: "source" }], taskSpec: { params: [ @@ -1400,7 +1438,7 @@ function imagePublishTaskSet() { ]; } -function tektonPipeline(ci) { +function tektonPipeline() { return { apiVersion: "tekton.dev/v1", kind: "Pipeline", @@ -1412,7 +1450,8 @@ function tektonPipeline(ci) { "hwlab.pikastech.local/gitops-target": "g14" }, annotations: { - "hwlab.pikastech.local/source-config": "CI.json", + "hwlab.pikastech.local/source-config": "scripts/g14-gitops-render.mjs#tekton-native-primitive-ci", + "hwlab.pikastech.local/ci-contract": "tekton-native-primitive-tasks", "hwlab.pikastech.local/policy": tektonSingleImagePublish ? "single-dind-image-publish-compat" : "native-per-service-taskrun-image-publish" } }, @@ -1433,7 +1472,7 @@ function tektonPipeline(ci) { ], tasks: [ { - name: "ci-json", + name: "prepare-source", workspaces: [ { name: "source", workspace: "source" }, { name: "git-ssh", workspace: "git-ssh" } @@ -1445,7 +1484,7 @@ function tektonPipeline(ci) { { name: "revision" } ], workspaces: [{ name: "source" }, { name: "git-ssh" }], - steps: [{ name: "ci-json", image: ciJsonRunnerImage, env: proxyEnv(), script: ciJsonScript(ci) }] + steps: [{ name: "prepare-source", image: ciToolsRunnerImage, env: proxyEnv(), script: prepareSourceScript() }] }, params: [ { name: "git-url", value: "$(params.git-url)" }, @@ -1453,6 +1492,7 @@ function tektonPipeline(ci) { { name: "revision", value: "$(params.revision)" } ] }, + ...primitiveValidationTasks.map(primitiveValidationTask), ...imagePublishTaskSet(), { name: "gitops-promote", @@ -2201,7 +2241,7 @@ function runtimeKustomization({ profile = "dev", includeDeviceAgent = profile == function readme({ source, args }) { return `# HWLAB G14 GitOps CI/CD -This directory is generated from \`CI.json\`, \`deploy/deploy.json\`, and \`deploy/k8s/*\` by \`scripts/g14-gitops-render.mjs\`. +This directory is generated from built-in Tekton-native CI primitive declarations plus \`deploy/deploy.json\` and \`deploy/k8s/*\` by \`scripts/g14-gitops-render.mjs\`. - Target: G14 k3s only. - CI: Tekton Pipeline \`hwlab-ci/hwlab-g14-ci-image-publish\`. @@ -2217,8 +2257,7 @@ This directory is generated from \`CI.json\`, \`deploy/deploy.json\`, and \`depl } async function plannedFiles(args) { - const [ci, deploy, namespaceTemplate, config, services, health, workloads] = await Promise.all([ - readJson("CI.json"), + const [deploy, namespaceTemplate, config, services, health, workloads] = await Promise.all([ readJson("deploy/deploy.json"), readJson("deploy/k8s/base/namespace.yaml"), readJson("deploy/k8s/base/code-agent-codex-config.yaml"), @@ -2264,7 +2303,7 @@ async function plannedFiles(args) { putText("README.md", readme({ source, args })); putJson("registry/registry.yaml", registryManifest()); putJson("tekton/rbac.yaml", tektonRbac()); - putJson("tekton/pipeline.yaml", tektonPipeline(ci)); + putJson("tekton/pipeline.yaml", tektonPipeline()); putJson("tekton/poller.yaml", tektonPollerCronJob(args)); putJson("tekton/control-plane-reconciler.yaml", tektonControlPlaneReconcilerCronJob(args)); putJson("tekton/pipelinerun.sample.yaml", tektonPipelineRunTemplate({ source, args })); diff --git a/scripts/src/dev-evidence-blocker-aggregator.mjs b/scripts/src/dev-evidence-blocker-aggregator.mjs index 1451a526..57101d6a 100644 --- a/scripts/src/dev-evidence-blocker-aggregator.mjs +++ b/scripts/src/dev-evidence-blocker-aggregator.mjs @@ -327,7 +327,7 @@ function buildEvidence() { category: "repository-governance", level: "SOURCE", status: "pass", - sources: ["scripts/repo-reports-guard.mjs", "package.json", "CI.json"], + sources: ["scripts/repo-reports-guard.mjs", "package.json", "scripts/g14-gitops-render.mjs"], commands: ["node scripts/repo-reports-guard.mjs"], summary: "Repository report files are removed and guarded against recurrence." }) diff --git a/scripts/src/g14-ci-plan-lib.mjs b/scripts/src/g14-ci-plan-lib.mjs index e17456ff..c0539400 100644 --- a/scripts/src/g14-ci-plan-lib.mjs +++ b/scripts/src/g14-ci-plan-lib.mjs @@ -65,7 +65,6 @@ const serviceSpecificPaths = Object.freeze({ export async function createG14CiPlan(options = {}) { const repoRoot = options.repoRoot ?? process.cwd(); - const ciJsonPath = options.ciJsonPath ?? "CI.json"; const deployJsonPath = options.deployJsonPath ?? "deploy/deploy.json"; const artifactCatalogPath = options.artifactCatalogPath ?? "deploy/artifact-catalog.dev.json"; const targetRef = options.targetRef ?? "HEAD"; @@ -73,8 +72,7 @@ export async function createG14CiPlan(options = {}) { const registryPrefix = options.registryPrefix ?? process.env.HWLAB_DEV_REGISTRY_PREFIX ?? process.env.HWLAB_DEV_REGISTRY ?? "127.0.0.1:5000/hwlab"; const baseImage = options.baseImage ?? process.env.HWLAB_DEV_BASE_IMAGE ?? "127.0.0.1:5000/hwlab/hwlab-node20-base:20-bookworm-slim"; - const [ciJson, deployJson, artifactCatalog] = await Promise.all([ - readJsonIfPresent(repoRoot, ciJsonPath, {}), + const [deployJson, artifactCatalog] = await Promise.all([ readJsonIfPresent(repoRoot, deployJsonPath, {}), readJsonIfPresent(repoRoot, artifactCatalogPath, null) ]); @@ -83,7 +81,7 @@ export async function createG14CiPlan(options = {}) { const changedPaths = await changedPathsBetween(repoRoot, baseRef, targetRef); const normalizedChangedPaths = changedPaths.map(normalizeRepoPath).filter(Boolean); const serviceIdResolution = resolveServiceIds({ deployJson, options }); - const componentModels = componentModelsForServices(serviceIdResolution.serviceIds, ciJson); + const componentModels = componentModelsForServices(serviceIdResolution.serviceIds); const globalChange = classifyGlobalChange(normalizedChangedPaths); const dockerfileHash = await hashGitPaths(repoRoot, targetRef, [ ...DEFAULT_BUILD_SYSTEM_PATHS, @@ -152,12 +150,11 @@ export async function createG14CiPlan(options = {}) { baseRef, targetRef, compatibility: { - ciJson: ciJsonPath, deployJson: deployJsonPath, artifactCatalog: artifactCatalog ? artifactCatalogPath : null, + ciContractSource: "scripts/g14-gitops-render.mjs", mode: "advisory-read-only", currentRenderCompatible: true, - plannerMutatesCiJson: false, plannerMutatesDeployJson: false, serviceIdSource: serviceIdResolution.source, defaultComponentLazyBuild: true, @@ -184,18 +181,16 @@ export async function createG14CiPlan(options = {}) { }; } -export function componentModelsForServices(serviceIds, ciJson = {}) { - const overrides = ciJson?.g14CiPlan?.components ?? {}; +export function componentModelsForServices(serviceIds) { return serviceIds.map((serviceId) => { - const override = overrides[serviceId] ?? {}; return { serviceId, - runtimeKind: override.runtimeKind ?? runtimeKindForService(serviceId), - entrypoint: override.entrypoint ?? entrypointForService(serviceId), - componentPaths: uniqueSorted((override.componentPaths ?? serviceSpecificPaths[serviceId] ?? [`cmd/${serviceId}/`]).map(normalizeRepoPath)), - sharedPaths: uniqueSorted((override.sharedPaths ?? DEFAULT_SHARED_RUNTIME_PATHS).map(normalizeRepoPath)), - runtimeDeps: uniqueSorted((override.runtimeDeps ?? DEFAULT_RUNTIME_DEP_PATHS).map(normalizeRepoPath)), - buildSystemPaths: uniqueSorted((override.buildSystemPaths ?? DEFAULT_BUILD_SYSTEM_PATHS).map(normalizeRepoPath)) + runtimeKind: runtimeKindForService(serviceId), + entrypoint: entrypointForService(serviceId), + componentPaths: uniqueSorted((serviceSpecificPaths[serviceId] ?? [`cmd/${serviceId}/`]).map(normalizeRepoPath)), + sharedPaths: uniqueSorted(DEFAULT_SHARED_RUNTIME_PATHS.map(normalizeRepoPath)), + runtimeDeps: uniqueSorted(DEFAULT_RUNTIME_DEP_PATHS.map(normalizeRepoPath)), + buildSystemPaths: uniqueSorted(DEFAULT_BUILD_SYSTEM_PATHS.map(normalizeRepoPath)) }; }); } diff --git a/scripts/validate-artifact-catalog.mjs b/scripts/validate-artifact-catalog.mjs index 8867593c..c87e971e 100644 --- a/scripts/validate-artifact-catalog.mjs +++ b/scripts/validate-artifact-catalog.mjs @@ -10,7 +10,6 @@ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".." const catalogPath = "deploy/artifact-catalog.dev.json"; const deployPath = "deploy/deploy.json"; -const ciPath = "CI.json"; const healthContractPath = "deploy/k8s/dev/health-contract.yaml"; const commitPattern = /^[a-f0-9]{7,40}$/; const digestPattern = /^sha256:[a-f0-9]{64}$/; @@ -203,15 +202,6 @@ function assertCatalogServices(catalog, deployManifest, catalogMode) { } } -function assertCIForbidden(ci, catalog) { - assert.equal(ci.profile, ENVIRONMENT_DEV, "CI skeleton profile must be dev"); - assert.ok(Array.isArray(ci.forbidden), "CI forbidden must be an array"); - for (const required of ["prod-deploy", "secret-material", "unidesk-runtime-substitute", "force-push"]) { - assert.ok(ci.forbidden.includes(required), `CI forbidden missing ${required}`); - assert.ok(catalog.forbiddenItems.some((item) => item.id === required), `catalog forbiddenItems missing CI item ${required}`); - } -} - function assertHealthContract(healthContract, catalog) { assert.equal(healthContract.kind, "ConfigMap", "health contract kind"); assert.equal(healthContract.metadata.namespace, catalog.namespace, "health contract namespace"); @@ -227,14 +217,12 @@ function assertHealthContract(healthContract, catalog) { const catalog = await readJSON(catalogPath); const deployManifest = await readJSON(deployPath); -const ci = await readJSON(ciPath); const healthContract = await readJSON(healthContractPath); const catalogMode = assertDevOnlyCatalog(catalog); assertServiceInventory(catalog); assertDeployManifest(deployManifest, catalog); assertCatalogServices(catalog, deployManifest, catalogMode); -assertCIForbidden(ci, catalog); assertHealthContract(healthContract, catalog); console.log(`validated ${catalog.services.length} DEV artifact catalog services at ${catalog.commitId} (${catalogMode})`); diff --git a/scripts/validate-runtime-boundary.mjs b/scripts/validate-runtime-boundary.mjs index d8d7fec7..667fe115 100644 --- a/scripts/validate-runtime-boundary.mjs +++ b/scripts/validate-runtime-boundary.mjs @@ -236,7 +236,7 @@ function assertDurableRuntimeRunbook(value) { } } -function assertCloudApiImageRuntimeEntrypoints({ packageJson, ciJson, artifactPublisher, gitopsRenderSource }) { +function assertCloudApiImageRuntimeEntrypoints({ packageJson, artifactPublisher, gitopsRenderSource }) { const label = "cloud-api image runtime maintenance entrypoints"; for (const expected of [ "COPY internal ./internal", @@ -256,11 +256,9 @@ function assertCloudApiImageRuntimeEntrypoints({ packageJson, ciJson, artifactPu for (const expected of ["scripts/g14-artifact-publish.mjs", "runtime-dev", "runtime-prod"]) { assertIncludes(gitopsRenderSource, expected, `${label} G14 GitOps render source`); } - assert.equal( - ciJson.commands?.some((command) => command.id === "g14-contract-check"), - true, - `${label} CI must run the G14 contract check` - ); + for (const expected of ["name: \"prepare-source\"", "name: \"repo-reports-guard\"", "name: \"g14-contract-check\"", "name: \"codex-api-forwarder-check\""]) { + assertIncludes(gitopsRenderSource, expected, `${label} Tekton primitive CI task`); + } } function assertGuardFixture(guard) { @@ -357,12 +355,21 @@ function assertWorkloads(workloads, guard) { assertTemplateServiceId(workload, serviceId, `workload ${serviceId}`); const containers = containerList(workload, `workload ${serviceId}`); - assert.equal(containers.length, 1, `${serviceId} workload must have one container`); - const container = containers[0]; - assert.equal(container.name, serviceId, `${serviceId} container name`); - assertHwlabImage(container.image, serviceId, `${serviceId} container`); - assertHealthProbe(container, serviceId, `${serviceId} container`); - assertNoForbiddenSubstitutesDeep(container.env ?? [], `${serviceId} container env`); + assert.ok(containers.length >= 1, `${serviceId} workload must define at least one container`); + assertUnique( + containers.map((container) => container.name), + `${serviceId} container names` + ); + + const primaryContainer = containers.find((container) => container.name === serviceId); + assert.ok(primaryContainer, `${serviceId} workload must include primary container ${serviceId}`); + + for (const container of containers) { + assertHwlabImage(container.image, serviceId, `${serviceId} container ${container.name}`); + assertNoForbiddenSubstitutesDeep(container.env ?? [], `${serviceId} container ${container.name} env`); + } + + assertHealthProbe(primaryContainer, serviceId, `${serviceId} container ${serviceId}`); } } @@ -442,7 +449,6 @@ assertProdDisabled( assertDurableRuntimeRunbook(await readText("docs/reference/dev-runtime-boundary.md")); assertCloudApiImageRuntimeEntrypoints({ packageJson: await readJSON("package.json"), - ciJson: await readJSON("CI.json"), artifactPublisher: await readText("scripts/artifact-publish.mjs"), gitopsRenderSource: await readText("scripts/g14-gitops-render.mjs") });