refactor: remove CI.json from G14 CI

This commit is contained in:
Codex
2026-05-26 23:02:54 +08:00
parent 3b7daa535a
commit 0c21e43d98
12 changed files with 145 additions and 215 deletions
+1 -1
View File
@@ -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 <ref> --target-ref <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 <ref> --target-ref <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,不恢复旧命令。
-41
View File
@@ -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"
]
}
+2 -2
View File
@@ -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. |
+19 -18
View File
@@ -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。
- CITekton 在 G14 k3s 内运行 `hwlab-g14-ci-image-publish` Pipeline读取 `CI.json` 命令,按 source commit 构建镜像并推送到 G14 registry
- CITekton 在 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:<shortCommit>`digest 由 registry 返回,CI report 只作为审计证据,不作为 CD 真相。
- Branch split`G14` 是源码监控分支;`G14-gitops` 是 Tekton promotion 写入的生成分支,保存 `deploy/gitops/g14/**` desired state。
- CDArgo 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_proxypublish 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 <sourceCommit>
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 是 monorepoG14 CI/CD 加速必须按组件输入判断构建和滚动,但不能破坏当前 `CI.json``deploy/deploy.json`全量 source commit render 合同
HWLAB 是 monorepoG14 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.<serviceId>` 覆盖 `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 stateworkload 的 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 Taskchanged 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/<pipelinerun-t
| grep -E 'dependency-(proxy|curl|download)'
```
如果 probe 显示 `proxy-env-missing``proxy-connect-failed``timeout` 或速度长期接近 0,应先按 `/root/docs/vpn-proxy-ops.md` 检查 v2rayN/Hysteria,再重跑 polling。若 probe 正常但后续测试失败,按对应 `CI.json` 命令和业务检查排障。
如果 probe 显示 `proxy-env-missing``proxy-connect-failed``timeout` 或速度长期接近 0,应先按 `/root/docs/vpn-proxy-ops.md` 检查 v2rayN/Hysteria,再重跑 polling。若 probe 正常但后续校验失败,按对应 Tekton task 日志和业务检查排障。
手动 render/apply 只允许作为 bootstrap 或 reconciler 故障抢修;正常路径必须由 `hwlab-g14-control-plane-reconciler` 自动完成 CI 控制面 manifest 更新。
@@ -182,7 +183,7 @@ G14 PR、CI、CD 的判断应按以下顺序收敛真相,越靠前越接近最
- sidecar 监听 `127.0.0.1` 时,Pod-IP `httpGet` probe 失败不等于 sidecar 自身 crash;必须同时对照 sidecar listen 日志和 probe target 语义。
- 真实 live smoke 超时要先排除脚本入口和 timeout 误报;健康的 Codex stdio 冷启动首 token 可能需要数十秒,10 秒级 transport timeout 不能直接判服务故障。
- 不要把顶层 `set -e` 当成 `ci-json` 命令失败传播的充分保证;`CI.json` `a && b && c` 直接内联到顶层 shell,会出现前半失败但 `ci-json` TaskRun 继续成功的假绿。生成器必须把每条 `CI.json` 命令包进独立子 shell 或等价 simple command,让非零退出码真正传播到 TaskRun
- 不要重新引入把多条检查挤成顶层 shell 链的 runner。旧 `CI.json``a && b && c` 一旦直接内联到顶层 shell会出现前半失败但 TaskRun 继续成功的假绿;原语化 CI 的原则是每个检查保持独立 task/result,失败语义直接由 Tekton 负责
## PR -> 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。
+7 -62
View File
@@ -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({
+1 -2
View File
@@ -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",
+13 -15
View File
@@ -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));
+72 -33
View File
@@ -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 }));
@@ -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."
})
+10 -15
View File
@@ -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))
};
});
}
-12
View File
@@ -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})`);
+19 -13
View File
@@ -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")
});