From 336a9b2c2c03e10c9aca0ef08b6911db5b1e1ad8 Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 27 May 2026 10:14:24 +0800 Subject: [PATCH] fix: keep G14 generated state on gitops branch --- AGENTS.md | 7 +- deploy/README.md | 14 +- deploy/artifact-catalog.dev.json | 194 +++++++++--------- docs/reference/g14-gitops-cicd.md | 36 ++-- package.json | 1 - scripts/g14-ci-plan.test.mjs | 30 +++ scripts/g14-gitops-render.mjs | 40 ++-- .../src/artifact-runtime-readiness-guard.mjs | 4 +- scripts/src/g14-ci-plan-lib.mjs | 13 +- scripts/validate-artifact-catalog.mjs | 3 +- 10 files changed, 188 insertions(+), 154 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 37b0c998..0cd30fd4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -24,6 +24,11 @@ HWLAB 是硬件实验室运行面和控制面项目。本文是 agent、指挥 - 当前发布只走 G14 k3s Tekton + `G14-gitops` + Argo CD;镜像构建 helper 是 `scripts/g14-artifact-publish.mjs`,desired state 由 `scripts/g14-gitops-render.mjs` 生成,细则见 [docs/reference/g14-gitops-cicd.md](docs/reference/g14-gitops-cicd.md)。 - 发现旧脚本、旧文档或旧 `hwlab-cli cicd` 再次进入 G14 Pipeline、AGENTS 或长期参考时,优先删除旧入口并把调用方改到 G14 GitOps 路径,不再做兼容保留。 +## P0 门禁最小化纪律 + +- 架构迁移时,过时的自检、预检、guard、gate 优先拆除;不要在旧门禁上继续加例外、加复杂度或制造噪声。 +- 新增自检、预检、guard、gate 必须遵循最小原则,只覆盖明确高价值风险;禁止乱加门禁导致系统僵化、难迁移、难调试或产生大量误报和摩擦。 + ## 工作区 - G14 分支固定 source workspace 是 G14 节点上的 `/root/hwlab`,固定使用 `G14` 分支和 `origin git@github.com:pikasTech/HWLAB.git`。在 G14 上进行代码、文档、GitOps render、Tekton/poller/Argo CD 修复或 CI/CD 验证前,必须先确认 `pwd` 为 `/root/hwlab` 且 `git status --short --branch` 为 `G14...origin/G14`;不满足时先停止并修正 workspace。 @@ -77,7 +82,7 @@ HWLAB 是硬件实验室运行面和控制面项目。本文是 agent、指挥 - 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,基于内建 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` +- G14 GitOps 渲染:`npm run g14:gitops:render`;source 分支不再运行生成物 drift check,发布态由 Tekton 写入 `G14-gitops`。 - DEV 依赖 runtime base 构建:`npm run dev-runtime-base:build` - Legacy D601 DEV CD:旧脚本入口已删除;事故回放只读历史 issue/commit,不恢复旧命令。 - CI/CD job 查询:`node tools/hwlab-cli/bin/hwlab-cli.mjs cicd status|logs|report ` diff --git a/deploy/README.md b/deploy/README.md index 302b6dc4..cf138517 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -70,17 +70,19 @@ node scripts/deploy-desired-state-plan.mjs --promotion-commit --check The desired-state plan reads `deploy/deploy.json`, `deploy/artifact-catalog.dev.json`, `deploy/k8s/base/workloads.yaml`, and the optional artifact report snapshot. `deploy/deploy.json` is the human-authored -runtime config truth source. Generated image identity lives in -`deploy/artifact-catalog.dev.json`; GitOps render overlays catalog image fields -and generated env mirrors such as `HWLAB_COMMIT_ID`, `HWLAB_IMAGE`, +runtime config truth source. Published image identity lives in +`G14-gitops:deploy/artifact-catalog.dev.json`; the source branch catalog is only +a seed contract for local checks. GitOps render overlays catalog image fields and +generated env mirrors such as `HWLAB_COMMIT_ID`, `HWLAB_IMAGE`, `HWLAB_IMAGE_TAG`, and `HWLAB_SKILLS_COMMIT_ID` into runtime manifests. The plan blocks if generated artifact identity leaks back into `deploy.json`, or if `hwlab-cloud-api` loses the Code Agent provider Secret ref `hwlab-code-agent-provider/openai-api-key`, the `OPENAI_API_KEY` env name, or -the DEV egress proxy base-url env. Promotion updates must refresh only -`deploy/artifact-catalog.dev.json` from a publish report before render/check; -report files are contextual evidence only and must not override source truth. +the DEV egress proxy base-url env. Promotion updates must refresh only the +workspace catalog from a publish report before render; Tekton writes the +refreshed catalog to `G14-gitops`, not back to the source branch. Report files +are contextual evidence only and must not override human-authored source truth. This is source/dry-run support only. It does not prove a registry image exists, does not build, pull, push, apply, restart, or touch PROD, and must not be used diff --git a/deploy/artifact-catalog.dev.json b/deploy/artifact-catalog.dev.json index 295121c0..d7582bfd 100644 --- a/deploy/artifact-catalog.dev.json +++ b/deploy/artifact-catalog.dev.json @@ -5,13 +5,13 @@ "profile": "dev", "namespace": "hwlab-dev", "endpoint": "http://74.48.78.17:16667", - "commitId": "8bdcff5", - "artifactState": "published", + "commitId": "278bbe1", + "artifactState": "contract-skeleton", "publish": { - "ciPublished": true, - "registryVerified": true, - "provenance": "g14-ci-artifact:hwlab-g14-ci-poll-8bdcff56c580-collect", - "note": "Digest fields were copied from a successful DEV artifact publish report for this source commit." + "ciPublished": false, + "registryVerified": false, + "provenance": "not_available_until_publish", + "note": "Artifact identity was refreshed to this source commit, but no publish report proved registry digests." }, "healthContract": { "method": "GET", @@ -66,11 +66,11 @@ "services": [ { "serviceId": "hwlab-cloud-api", - "commitId": "af46386", - "image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:af46386", - "imageTag": "af46386", - "digest": "sha256:fe724e72e7cea3c93abe17085c4d885c78d34d18eb2a3c01f987d1971e2f4425", - "publishState": "reused", + "commitId": "278bbe1", + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:278bbe1", + "imageTag": "278bbe1", + "digest": "not_published", + "publishState": "skeleton-only", "profile": "dev", "namespace": "hwlab-dev", "healthPath": "/health/live", @@ -78,8 +78,8 @@ "publishEnabled": true, "artifactRequired": true, "artifactScope": "required", - "notPublishedReason": null, - "buildCreatedAt": "2026-05-27T00:14:43.480Z", + "notPublishedReason": "publish_not_run", + "buildCreatedAt": null, "buildSource": null, "componentCommitId": "8d533cb736c3bf30fe97ef0ddf3d24047cad1b78", "componentInputHash": "7498160c36db86db76d3c1b103e397f2b2304248136d3b79eafe1ab69d4abeff", @@ -101,11 +101,11 @@ }, { "serviceId": "hwlab-cloud-web", - "commitId": "af46386", - "image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:af46386", - "imageTag": "af46386", - "digest": "sha256:3f5d4dcd1df4f736293d4223acb8104fd857e847a3a9a0acec4ef031ea333913", - "publishState": "reused", + "commitId": "278bbe1", + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:278bbe1", + "imageTag": "278bbe1", + "digest": "not_published", + "publishState": "skeleton-only", "profile": "dev", "namespace": "hwlab-dev", "healthPath": "/health/live", @@ -113,8 +113,8 @@ "publishEnabled": true, "artifactRequired": true, "artifactScope": "required", - "notPublishedReason": null, - "buildCreatedAt": "2026-05-27T00:14:45.158Z", + "notPublishedReason": "publish_not_run", + "buildCreatedAt": null, "buildSource": null, "componentCommitId": "8d533cb736c3bf30fe97ef0ddf3d24047cad1b78", "componentInputHash": "fdd27830a0cef173fc07a2cab8cd469d67aaff9da12bb0195f188e323854f93a", @@ -136,11 +136,11 @@ }, { "serviceId": "hwlab-agent-mgr", - "commitId": "af46386", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:af46386", - "imageTag": "af46386", - "digest": "sha256:5a55d6199b5bf2db4afa55a0be8cc6eae38f2dcac8a7b374ebbec2af2f36d71c", - "publishState": "reused", + "commitId": "278bbe1", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:278bbe1", + "imageTag": "278bbe1", + "digest": "not_published", + "publishState": "skeleton-only", "profile": "dev", "namespace": "hwlab-dev", "healthPath": "/health/live", @@ -148,8 +148,8 @@ "publishEnabled": true, "artifactRequired": true, "artifactScope": "required", - "notPublishedReason": null, - "buildCreatedAt": "2026-05-27T00:14:44.484Z", + "notPublishedReason": "publish_not_run", + "buildCreatedAt": null, "buildSource": null, "componentCommitId": "a56ea7e33ef67040cc9a8748c3759b4f9c87ca46", "componentInputHash": "ce4aa5c0b74c873d3270a71d9481a8255571b574f024bd5980f18547b4361860", @@ -171,11 +171,11 @@ }, { "serviceId": "hwlab-agent-worker", - "commitId": "af46386", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:af46386", - "imageTag": "af46386", - "digest": "sha256:03f09333657bb37ad059004aabb4484503f79866ba19272d5af9bf611bbb9c5d", - "publishState": "reused", + "commitId": "278bbe1", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:278bbe1", + "imageTag": "278bbe1", + "digest": "not_published", + "publishState": "skeleton-only", "profile": "dev", "namespace": "hwlab-dev", "healthPath": "/health/live", @@ -183,8 +183,8 @@ "publishEnabled": true, "artifactRequired": true, "artifactScope": "required", - "notPublishedReason": null, - "buildCreatedAt": "2026-05-27T00:14:45.112Z", + "notPublishedReason": "publish_not_run", + "buildCreatedAt": null, "buildSource": null, "componentCommitId": "a56ea7e33ef67040cc9a8748c3759b4f9c87ca46", "componentInputHash": "7b426c226ec6f6d4de516f58055f3bf26d911477a88ebeef320066b1903ef40f", @@ -206,11 +206,11 @@ }, { "serviceId": "hwlab-gateway", - "commitId": "af46386", - "image": "127.0.0.1:5000/hwlab/hwlab-gateway:af46386", - "imageTag": "af46386", - "digest": "sha256:1f44666c01f11416fee24b2d671c2ddcea3b5a6c575fe16ad0cf5307315d35e6", - "publishState": "reused", + "commitId": "278bbe1", + "image": "127.0.0.1:5000/hwlab/hwlab-gateway:278bbe1", + "imageTag": "278bbe1", + "digest": "not_published", + "publishState": "skeleton-only", "profile": "dev", "namespace": "hwlab-dev", "healthPath": "/health/live", @@ -218,8 +218,8 @@ "publishEnabled": true, "artifactRequired": true, "artifactScope": "required", - "notPublishedReason": null, - "buildCreatedAt": "2026-05-27T00:14:45.140Z", + "notPublishedReason": "publish_not_run", + "buildCreatedAt": null, "buildSource": null, "componentCommitId": "a56ea7e33ef67040cc9a8748c3759b4f9c87ca46", "componentInputHash": "6c97b4f8e49ba46148dde9126a21f475356bfdd4d6826326799d03d68212002e", @@ -241,11 +241,11 @@ }, { "serviceId": "hwlab-gateway-simu", - "commitId": "af46386", - "image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:af46386", - "imageTag": "af46386", - "digest": "sha256:9b65bfff46f5fa206e7887340b9e7df2647eb4d0c186f9ad9796636551cd121b", - "publishState": "reused", + "commitId": "278bbe1", + "image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:278bbe1", + "imageTag": "278bbe1", + "digest": "not_published", + "publishState": "skeleton-only", "profile": "dev", "namespace": "hwlab-dev", "healthPath": "/health/live", @@ -253,8 +253,8 @@ "publishEnabled": true, "artifactRequired": true, "artifactScope": "required", - "notPublishedReason": null, - "buildCreatedAt": "2026-05-27T00:14:45.127Z", + "notPublishedReason": "publish_not_run", + "buildCreatedAt": null, "buildSource": null, "componentCommitId": "a56ea7e33ef67040cc9a8748c3759b4f9c87ca46", "componentInputHash": "184706b960dc00c23d6aacf35bde93558d422329635752cfb85796c6fe5e4820", @@ -276,11 +276,11 @@ }, { "serviceId": "hwlab-box-simu", - "commitId": "af46386", - "image": "127.0.0.1:5000/hwlab/hwlab-box-simu:af46386", - "imageTag": "af46386", - "digest": "sha256:909fd838bf6fa7dc2b2eb7b2a868bde4d110aab1640272ee63dbcf6ae488b330", - "publishState": "reused", + "commitId": "278bbe1", + "image": "127.0.0.1:5000/hwlab/hwlab-box-simu:278bbe1", + "imageTag": "278bbe1", + "digest": "not_published", + "publishState": "skeleton-only", "profile": "dev", "namespace": "hwlab-dev", "healthPath": "/health/live", @@ -288,8 +288,8 @@ "publishEnabled": true, "artifactRequired": true, "artifactScope": "required", - "notPublishedReason": null, - "buildCreatedAt": "2026-05-27T00:14:45.148Z", + "notPublishedReason": "publish_not_run", + "buildCreatedAt": null, "buildSource": null, "componentCommitId": "a56ea7e33ef67040cc9a8748c3759b4f9c87ca46", "componentInputHash": "1dfe5c35338d049b996a27f628d8d7b64ddacfa51de07e4c1cbd4a0f19c289bf", @@ -311,11 +311,11 @@ }, { "serviceId": "hwlab-patch-panel", - "commitId": "af46386", - "image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:af46386", - "imageTag": "af46386", - "digest": "sha256:5d49ba34c8dc1e71eb5fedc769ea0759b4ab66d7665086fc773c2020fcd11a2b", - "publishState": "reused", + "commitId": "278bbe1", + "image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:278bbe1", + "imageTag": "278bbe1", + "digest": "not_published", + "publishState": "skeleton-only", "profile": "dev", "namespace": "hwlab-dev", "healthPath": "/health/live", @@ -323,8 +323,8 @@ "publishEnabled": true, "artifactRequired": true, "artifactScope": "required", - "notPublishedReason": null, - "buildCreatedAt": "2026-05-27T00:14:48.434Z", + "notPublishedReason": "publish_not_run", + "buildCreatedAt": null, "buildSource": null, "componentCommitId": "a56ea7e33ef67040cc9a8748c3759b4f9c87ca46", "componentInputHash": "4b84d74569d7d847c886695014aa84236c678b19ceeaa7df1f80ff7aa06c00ed", @@ -346,11 +346,11 @@ }, { "serviceId": "hwlab-router", - "commitId": "af46386", - "image": "127.0.0.1:5000/hwlab/hwlab-router:af46386", - "imageTag": "af46386", - "digest": "sha256:074539cf5a9aef2330c6dc8df96d0be76f5fbb940f8d0ad20f1d67cb1f299666", - "publishState": "reused", + "commitId": "278bbe1", + "image": "127.0.0.1:5000/hwlab/hwlab-router:278bbe1", + "imageTag": "278bbe1", + "digest": "not_published", + "publishState": "skeleton-only", "profile": "dev", "namespace": "hwlab-dev", "healthPath": "/health/live", @@ -358,8 +358,8 @@ "publishEnabled": true, "artifactRequired": true, "artifactScope": "required", - "notPublishedReason": null, - "buildCreatedAt": "2026-05-27T00:14:48.571Z", + "notPublishedReason": "publish_not_run", + "buildCreatedAt": null, "buildSource": null, "componentCommitId": "a56ea7e33ef67040cc9a8748c3759b4f9c87ca46", "componentInputHash": "a2e72fca1d0acb7b5db4ecf77ff5ffacae7e897e8d50920cae8b17fcb6e1e6fb", @@ -381,11 +381,11 @@ }, { "serviceId": "hwlab-tunnel-client", - "commitId": "af46386", - "image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:af46386", - "imageTag": "af46386", - "digest": "sha256:f1f200009731ba832dc26258b01c3abb4de711237728a95124822e0d056c70fe", - "publishState": "reused", + "commitId": "278bbe1", + "image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:278bbe1", + "imageTag": "278bbe1", + "digest": "not_published", + "publishState": "skeleton-only", "profile": "dev", "namespace": "hwlab-dev", "healthPath": "/health/live", @@ -393,8 +393,8 @@ "publishEnabled": true, "artifactRequired": true, "artifactScope": "required", - "notPublishedReason": null, - "buildCreatedAt": "2026-05-27T00:14:48.544Z", + "notPublishedReason": "publish_not_run", + "buildCreatedAt": null, "buildSource": null, "componentCommitId": "a56ea7e33ef67040cc9a8748c3759b4f9c87ca46", "componentInputHash": "58f4fc6f1b56c6074cdeae1d33bfe22f54072ef0d45351d3186cdcf15a3f1573", @@ -416,11 +416,11 @@ }, { "serviceId": "hwlab-edge-proxy", - "commitId": "af46386", - "image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:af46386", - "imageTag": "af46386", - "digest": "sha256:b9dbf6f08bfea10ec9ca9c15cd864e96eb37e483fcd8716513dd74533d8b3cf0", - "publishState": "reused", + "commitId": "278bbe1", + "image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:278bbe1", + "imageTag": "278bbe1", + "digest": "not_published", + "publishState": "skeleton-only", "profile": "dev", "namespace": "hwlab-dev", "healthPath": "/health/live", @@ -428,8 +428,8 @@ "publishEnabled": true, "artifactRequired": true, "artifactScope": "required", - "notPublishedReason": null, - "buildCreatedAt": "2026-05-27T00:14:48.521Z", + "notPublishedReason": "publish_not_run", + "buildCreatedAt": null, "buildSource": null, "componentCommitId": "a56ea7e33ef67040cc9a8748c3759b4f9c87ca46", "componentInputHash": "497d88f5f34075f2ba064cdc10d3a80a236e96e4eab4ca71ae3138a751201315", @@ -451,11 +451,11 @@ }, { "serviceId": "hwlab-cli", - "commitId": "af46386", - "image": "127.0.0.1:5000/hwlab/hwlab-cli:af46386", - "imageTag": "af46386", - "digest": "sha256:3908855f68043119452bd40109668c7a163c33bf048ddcf0cc9c1097ef741c5a", - "publishState": "reused", + "commitId": "278bbe1", + "image": "127.0.0.1:5000/hwlab/hwlab-cli:278bbe1", + "imageTag": "278bbe1", + "digest": "not_published", + "publishState": "skeleton-only", "profile": "dev", "namespace": "hwlab-dev", "healthPath": "/health/live", @@ -463,8 +463,8 @@ "publishEnabled": true, "artifactRequired": true, "artifactScope": "required", - "notPublishedReason": null, - "buildCreatedAt": "2026-05-27T00:14:48.479Z", + "notPublishedReason": "publish_not_run", + "buildCreatedAt": null, "buildSource": null, "componentCommitId": "a56ea7e33ef67040cc9a8748c3759b4f9c87ca46", "componentInputHash": "bff91d0ad2c23aceacc494cd4ad84e93d163c9bcae8815ac4018f2a0f37a0617", @@ -486,11 +486,11 @@ }, { "serviceId": "hwlab-agent-skills", - "commitId": "af46386", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:af46386", - "imageTag": "af46386", - "digest": "sha256:8cdf6922fda03703288a796b107f9185c3635f2768bc0bcf9b8272bf33ae7547", - "publishState": "reused", + "commitId": "278bbe1", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:278bbe1", + "imageTag": "278bbe1", + "digest": "not_published", + "publishState": "skeleton-only", "profile": "dev", "namespace": "hwlab-dev", "healthPath": "/health/live", @@ -498,8 +498,8 @@ "publishEnabled": true, "artifactRequired": true, "artifactScope": "required", - "notPublishedReason": null, - "buildCreatedAt": "2026-05-27T00:14:48.439Z", + "notPublishedReason": "publish_not_run", + "buildCreatedAt": null, "buildSource": null, "componentCommitId": "a56ea7e33ef67040cc9a8748c3759b4f9c87ca46", "componentInputHash": "33cc71d1216f9f523499c950c445b6d6d4ac604e8048c1a2c3a9561b3bdce9e8", diff --git a/docs/reference/g14-gitops-cicd.md b/docs/reference/g14-gitops-cicd.md index fae3edfc..491ee043 100644 --- a/docs/reference/g14-gitops-cicd.md +++ b/docs/reference/g14-gitops-cicd.md @@ -6,8 +6,8 @@ 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,由 `scripts/g14-gitops-render.mjs` 直接生成原生 task;最小校验固定为 `repo-reports-guard`、`g14-contract-check`、`codex-api-forwarder-check`,随后按 component plan 做 per-service BuildKit publish 与 GitOps promote;没有 `CI.json` runner、DIND 单任务发布或 Docker fallback。 -- 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。 +- Artifact:镜像使用 commit tag,例如 `127.0.0.1:5000/hwlab/hwlab-cloud-api:`;digest 由 registry 返回,CI report 只作为审计证据,不作为 CD 真相。发布态 artifact catalog 由 Tekton 写入 `G14-gitops:deploy/artifact-catalog.dev.json`。 +- Branch split:`G14` 是源码监控分支,只保存人写源码、声明和 seed contract;`G14-gitops` 是 Tekton promotion 写入的生成分支,保存 `deploy/artifact-catalog.dev.json` 与 `deploy/gitops/g14/**` desired state。CI/CD 不再把 catalog promotion commit 写回 `G14`。 - CD:Argo CD 只消费 `G14-gitops:deploy/gitops/g14/runtime-dev` 与 `deploy/gitops/g14/runtime-prod` 的 Git desired state,不重新构建镜像,不读取 D601 状态,不获取 legacy DEV CD Lease。 - FRP:G14 DEV 通过 `hwlab-dev/hwlab-g14-frpc` 暴露 `17666/17667`,G14 PROD 通过 `hwlab-prod/hwlab-g14-prod-frpc` 暴露 `18666/18667`;master frps 的 `deploy/frp/frps.dev.toml` 与实际 `/etc/frp/frps.toml` 必须同时放行这四个 G14 端口。 - 并行性:不同 source commit 的 CI build 不共享发布锁;并行安全由 immutable commit tag/digest 和 Git desired state 保证。最终运行版本由 Argo CD 当前同步的 Git revision 决定。 @@ -16,6 +16,8 @@ G14 是 HWLAB 当前 DEV/PROD 原生 k8s 与 GitOps 运行面目标。G14 CI/CD `scripts/g14-gitops-render.mjs` 是 G14 专用转换器: +- 架构迁移时,过时的自检、预检、guard、gate 优先删除,不在旧门禁上叠加例外或复杂度;新门禁只允许覆盖明确高价值风险,必须保持最小、低噪声、易迁移。 + - 直接声明 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 验收证据中。 @@ -23,7 +25,7 @@ G14 是 HWLAB 当前 DEV/PROD 原生 k8s 与 GitOps 运行面目标。G14 CI/CD - 生成 `hwlab-g14-branch-poller` CronJob:它使用 G14 集群内的 Git SSH Secret 轮询 `G14` 分支,按 source commit 创建确定命名的 Tekton PipelineRun。 - 生成 `hwlab-g14-control-plane-reconciler` CronJob:它使用同一个 Git SSH Secret 轮询 `G14`,运行 repo 内 `scripts/g14-gitops-render.mjs`,并 server-side apply 生成的 Tekton RBAC、Pipeline、Poller 和 Reconciler manifests;因此 CI 控制面变化应自动进入 G14 k3s,不需要人工长期执行 render/apply。 - 默认输出到 `deploy/gitops/g14/`。 -- 默认 GitOps 生成分支是 `G14-gitops`;Pipeline 成功后把本次 source commit 对应的 `deploy/gitops/g14/**` 推送到该分支,避免把生成提交继续写回 `G14`。 +- 默认 GitOps 生成分支是 `G14-gitops`;Pipeline 成功后把本次 source commit 对应的 `deploy/artifact-catalog.dev.json` 与 `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、BuildKit sidecar 和 publish step 都注入 proxy/no_proxy;服务镜像构建只允许通过 Pod 内 BuildKit Unix socket 直接 push 到 G14 本地 registry,不能回退到 Docker daemon、DIND 或 host Docker。 - `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。脚本启动时必须输出工具与 proxy preflight 结构化日志;缺少工具时应先重建并推送工具镜像,再修改 `HWLAB_G14_CI_TOOLS_IMAGE`/render 默认值,不能回退到 runtime 安装。 @@ -36,9 +38,10 @@ G14 是 HWLAB 当前 DEV/PROD 原生 k8s 与 GitOps 运行面目标。G14 CI/CD ```sh npm run g14:gitops:render -- --source-revision -npm run g14:gitops:check ``` +人工 source workspace 不再对 `deploy/gitops/g14/**` 做生成物对比;这些文件在 source 分支下是忽略的生成物,旧文件和 `source.json` 不是真相。需要看渲染计划时使用 `npm run g14:gitops:render -- --no-write`;正式发布态只看 `G14-gitops`、Argo Application 和 live runtime。 + ## 原生 k8s Tekton + Argo 配置面 vs 已废弃的 CI.json runner 当前 G14 已完全删除 `CI.json` 和 `ci-json` step。下面的对比只保留为迁移理由:Tekton + Argo 是唯一发布控制面;repo-local 校验已经收敛为 render 内建的少量原语 task,而不是再保留一条 shell runner 路线。 @@ -62,13 +65,13 @@ npm run g14:gitops:check HWLAB 是 monorepo,G14 CI/CD 加速必须按组件输入判断构建和滚动,并直接依赖内建 component model、`deploy/deploy.json` 与 per-service artifact catalog;`CI.json` 已删除,不再作为任何 planner 输入。 -- `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 文件。 +- `scripts/g14-ci-plan.mjs` 是只读 planner,默认读取当前 workspace 的 `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 文件。Tekton `prepare-source` 会先从 `G14-gitops` 注入上一轮发布态 catalog,source 分支里的 catalog 只作为 seed contract。 - 服务清单兼容顺序固定为:显式 `--services`、`deploy.services[]`、`deploy.k3s.serviceMappings[]`、`internal/protocol.SERVICE_IDS`。因此旧 `deploy/deploy.json` 形态和当前完整 `deploy.services[]` 形态都必须能被 planner 识别。 - 组件边界固定由 `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,必须结构化阻塞并修复 catalog 或显式扩大 changed service 范围,不能退回 Docker 或 legacy full-build 路线。 -- `scripts/g14-artifact-publish.mjs` 的 publish report 必须携带 planner 的 per-service 元数据;`scripts/refresh-artifact-catalog.mjs` 只把生成的 `commitId`、`image`、`imageTag`、`digest`、`publishState` 和 component provenance 字段复制进 `deploy/artifact-catalog.dev.json`。`deploy/deploy.json` 是人写的 runtime config 真相源,不得被 promotion 回写镜像身份字段。 +- `scripts/g14-artifact-publish.mjs` 默认启用组件级 lazy build:先运行 planner,再只构建/推送 `affectedServices`,`reusedServices` 从 `deploy/artifact-catalog.dev.json` 复用已有 sha256 digest;如果 catalog 没有可验证 digest,planner 直接把该服务列为 affected 并重新发布,不能用旧 guard 阻塞,也不能退回 Docker 或 legacy full-build 路线。 +- `scripts/g14-artifact-publish.mjs` 的 publish report 必须携带 planner 的 per-service 元数据;`scripts/refresh-artifact-catalog.mjs` 只把生成的 `commitId`、`image`、`imageTag`、`digest`、`publishState` 和 component provenance 字段复制进当前 workspace 的 `deploy/artifact-catalog.dev.json`。Tekton promotion 只把刷新后的 catalog 写入 `G14-gitops`;`deploy/deploy.json` 是人写的 runtime config 真相源,不得被 promotion 回写镜像身份字段。 - `scripts/g14-gitops-render.mjs` 只支持混合 desired state:workload 的 container image、`HWLAB_IMAGE`、`HWLAB_IMAGE_TAG` 和 pod template `source-commit` 来自 `deploy/artifact-catalog.dev.json` 的 per-service artifact identity;普通 env、replica、healthPath、profile 等配置来自 `deploy/deploy.json`。全局 GitOps metadata 仍记录本次 source commit。`--legacy-source-images` 与 `HWLAB_G14_USE_DEPLOY_IMAGES=0` 已废弃,不得把所有 workload image 回退渲染为同一个 source commit tag。 -- G14 Tekton promotion 在推送 `G14-gitops` 前,必须用 publish report 刷新 workspace 内的 `deploy/artifact-catalog.dev.json`,并且只在 catalog 变化时以 `chore: promote G14 GitOps source ` 提交回 `G14` source branch;poller 必须跳过这类生成提交,避免自触发循环。promotion 不得自动修改 `deploy/deploy.json` 或 `deploy/k8s/base/workloads.yaml`,这样人写配置不会和 CI 生成身份反复冲突。 +- G14 Tekton promotion 在推送 `G14-gitops` 前,必须用 publish report 刷新 workspace 内的 `deploy/artifact-catalog.dev.json`,然后把刷新后的 catalog 和 rendered GitOps desired state 一起提交到 `G14-gitops`。promotion 不得自动修改或推送 `G14` source branch,也不得自动修改 `deploy/deploy.json` 或 `deploy/k8s/base/workloads.yaml`,这样人写配置不会和 CI 生成身份反复冲突。 - Tekton 并发化只能以 planner 输出作为输入;每个 service 都有独立 TaskRun,changed service 启动 BuildKit,unchanged service 只写 reuse result 并复用 catalog digest,且不能改 pod template,避免无意义 rollout。没有完整 per-service desired state 证据时,必须修复 planner/catalog 证据,不能使用 `--full-build`、`--legacy-source-images`、DIND 或 Docker fallback 回退。 ## 加速判定与当前瓶颈 @@ -85,7 +88,7 @@ G14 CI/CD 加速的第一判定标准不是 PipelineRun 总耗时单点变短, 当前仍然慢的主要位置: - `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 操作单独缓存。 +- `gitops-promote` 仍是大头,当前量级约 100 秒。它包含 source branch freshness check、artifact catalog refresh、render、clone `G14-gitops`、复制 catalog 与 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 判成失败。 @@ -117,9 +120,9 @@ DeepSeek proxy manifest 是 GitOps desired state 的一部分,DEV/PROD 分别 G14 不要求 GitHub webhook 或 GitHub Actions 配置。`hwlab-ci/hwlab-g14-branch-poller` CronJob 每 1 分钟通过 Git SSH 拉取 `G14` HEAD,并用 source commit 的前 12 位生成 PipelineRun 名称 `hwlab-g14-ci-poll-`。 -如果同名 PipelineRun 已存在,poller 直接跳过;如果不存在,则创建新的 PipelineRun。这样可以用 Kubernetes 原生 CronJob、ServiceAccount、RBAC 和 Tekton API 实现无 GitHub webhook 的分支监控,同时避免同一个 commit 被反复派单。历史上的 `chore: promote G14 GitOps source ...` 生成提交会被 poller 跳过。 +如果同名 PipelineRun 已存在,poller 直接跳过;如果不存在,则创建新的 PipelineRun。这样可以用 Kubernetes 原生 CronJob、ServiceAccount、RBAC 和 Tekton API 实现无 GitHub webhook 的分支监控,同时避免同一个 commit 被反复派单。历史上的 `chore: promote G14 GitOps source ...` source 分支生成提交已经废弃;poller 只应该看到人写 source commit。 -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 内构建镜像。 +Pipeline 的标准路径是:`G14` source commit -> Tekton 原语校验 task -> commit-tagged image push 到 G14 registry -> refresh artifact catalog -> render `deploy/gitops/g14/**` -> push catalog 与 GitOps desired state 到 `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 全部成功也不会滚动新镜像。 @@ -169,14 +172,14 @@ G14 PR、CI、CD 的判断应按以下顺序收敛真相,越靠前越接近最 1. live runtime:目标 namespace 的 Deployment/StatefulSet template、Pod ready、`describe`、容器日志和公网 health。 2. Argo desired state:`hwlab-g14-dev` / `hwlab-g14-prod` 的 Application revision、sync、health 和实际 runtime path。 3. Tekton 执行证据:branch-poller 日志、PipelineRun、TaskRun results、`gitops-promote` 终态。 -4. 干净 source workspace:`origin/G14` 当前内容,以及 `npm run g14:gitops:render`、`npm run g14:gitops:check` 和 planner 输出。 +4. 干净 source workspace:`origin/G14` 当前内容,以及 `npm run g14:gitops:render -- --no-write` 和 planner 输出。 5. 对照线索:旧 commit 记忆、坏 worktree、D601 legacy 路径、脚本旧默认值,只能当线索,不能当真相。 常见误判与纠正: - 不要把 `/root/hwlab` 当前 checkout、任意 `/tmp` worktree 或带 conflict marker 的目录当 source truth;只有跟踪 `origin/G14` 的干净 worktree 才能作为当前发布依据。 - 不要按“某个预期 commit 是否出现在 ancestry 中”判断功能是否已经合入;当前 `origin/G14` 实际内容比历史 commit 轨迹更重要。 -- 改了 source 但没先 render/check 时,generated mismatch 只说明 source 与 generated 没对齐,不等于 Tekton 或 Argo 本身坏掉。 +- 改了 source 但没有触发 Tekton promotion 时,`G14-gitops` 仍停在旧 generated state 是正常现象,不等于 Tekton 或 Argo 本身坏掉。 - 不要把 `build-*` TaskRun 名称直接当作镜像重建证据;reuse-only 变更也会扇出同名 TaskRun,必须看 Tekton result 的 `status=reused` 与 `build-backend=reused-catalog`。 - 不要把 `G14-gitops` 分支已更新误判成 DEV 或 PROD 已滚动;只有 Argo Application revision、目标 workload ready 和 live manifest 生效,才算 CD 真实通过。 - 不要把 health payload 中的镜像 commit 当成 runtime manifest commit;runtime-only 修复可能复用旧镜像,只改变 probe、env、annotation 或 sidecar 行为。 @@ -200,14 +203,13 @@ bun scripts/cli.ts ssh G14:/root/hwlab shell 'git status --short --branch && git 2. Source / PR 预检 ```sh -npm run g14:gitops:render -npm run g14:gitops:check +npm run g14:gitops:render -- --no-write node scripts/g14-ci-plan.mjs --base-ref origin/G14 --target-ref HEAD --pretty ``` - 先看当前内容和长期参考,不按旧 commit 记忆判断“功能是否已合入”。 -- GitOps、manifest、poller/reconciler、provider profile 相关改动必须先跑 render/check;planner 用于判断本轮是 docs-only、reuse-only 还是需要真实构建。 -- provider/profile 变更在进入 PR 或 CI 之前,先按 `code-agent-chat-readiness.md` 做目标 Pod 最小闭环;不要把完整 CI/CD 当成 transport 试错工具。 +- GitOps、manifest、poller/reconciler、provider profile 相关改动先跑 render no-write;planner 用于判断本轮是 docs-only、reuse-only 还是需要真实构建。不要在 source 分支对 ignored generated output 跑 drift check。 +- provider/profile 变更在进入 PR 或 CI 之前,先按 `code-agent-chat-readiness.md` 做目标 Pod 最小���环;不要把完整 CI/CD 当成 transport 试错工具。 3. 合并或推送到 `G14` diff --git a/package.json b/package.json index fe8a53cd..c2e93ef7 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,6 @@ "web:build": "node web/hwlab-cloud-web/scripts/build.mjs", "artifact-catalog:refresh-blocked": "node scripts/refresh-artifact-catalog.mjs --target-ref origin/main --blocked", "g14:gitops:render": "node scripts/g14-gitops-render.mjs", - "g14:gitops:check": "node scripts/g14-gitops-render.mjs --check", "cicd:jobs": "node tools/hwlab-cli/bin/hwlab-cli.mjs cicd jobs", "docs:validate:m3-rollout": "node scripts/validate-m3-rollout-runbook.mjs", "d601:k3s:readonly": "node scripts/d601-k3s-readonly-observability.mjs", diff --git a/scripts/g14-ci-plan.test.mjs b/scripts/g14-ci-plan.test.mjs index 2553fd50..d529cf38 100644 --- a/scripts/g14-ci-plan.test.mjs +++ b/scripts/g14-ci-plan.test.mjs @@ -42,6 +42,19 @@ test("G14 CI planner treats docs-only changes as no image build", async () => { assert.equal(plan.changedPathSummary.docsOnly, true); }); +test("planner rebuilds when catalog digest is missing instead of blocking reuse", async () => { + const repo = await createFixtureRepo({ catalog: "missing-digest" }); + await mkdir(path.join(repo, "docs/reference"), { recursive: true }); + await writeFile(path.join(repo, "docs/reference/g14-gitops-cicd.md"), "docs v2\n"); + await git(repo, ["add", "."]); + await git(repo, ["commit", "-m", "change docs"]); + + const plan = await createG14CiPlan({ repoRoot: repo, baseRef: "HEAD~1", targetRef: "HEAD" }); + assert.deepEqual(plan.affectedServices, ["hwlab-cloud-api", "hwlab-cloud-web"]); + assert.equal(plan.imageBuildRequired, true); + assert.equal(plan.services[0].reason.includes("catalog-digest-missing"), true); +}); + test("component model uses built-in service paths", () => { const models = componentModelsForServices(["hwlab-cloud-api"]); assert.deepEqual(models[0].componentPaths, [ @@ -84,6 +97,10 @@ async function createFixtureRepo(options = {}) { await mkdir(path.join(repo, "web/hwlab-cloud-web"), { recursive: true }); await mkdir(path.join(repo, "deploy"), { recursive: true }); await writeFile(path.join(repo, "deploy/deploy.json"), JSON.stringify(createDeployFixture({ deployServices, k3sServiceMappings }), null, 2)); + const catalogMode = options.catalog ?? "ready"; + if (catalogMode !== false) { + await writeFile(path.join(repo, "deploy/artifact-catalog.dev.json"), JSON.stringify(createCatalogFixture(catalogMode), 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)); await writeFile(path.join(repo, "cmd/hwlab-cloud-api/main.mjs"), "console.log('api');\n"); @@ -115,6 +132,19 @@ function createDeployFixture({ deployServices, k3sServiceMappings }) { return deploy; } +function createCatalogFixture(mode) { + const digest = mode === "ready" ? `sha256:${"1".repeat(64)}` : "not_published"; + return { + services: ["hwlab-cloud-api", "hwlab-cloud-web"].map((serviceId) => ({ + serviceId, + commitId: "abc1234", + image: `127.0.0.1:5000/hwlab/${serviceId}:abc1234`, + imageTag: "abc1234", + digest + })) + }; +} + async function git(cwd, args) { return execFileAsync("git", ["-c", "user.name=HWLAB Test", "-c", "user.email=hwlab-test@example.invalid", ...args], { cwd }); } diff --git a/scripts/g14-gitops-render.mjs b/scripts/g14-gitops-render.mjs index b7b5d42d..99cf8b5a 100644 --- a/scripts/g14-gitops-render.mjs +++ b/scripts/g14-gitops-render.mjs @@ -239,20 +239,6 @@ async function resolveSourceRevision(value) { return { full, short: full.slice(0, 7) }; } -async function defaultCheckSourceRevision(args) { - if (!args.check || args.sourceRevision) return args; - try { - const raw = await readFile(generatedPath(path.join(args.outDir, "source.json")), "utf8"); - const parsed = JSON.parse(raw); - if (typeof parsed.sourceCommit === "string" && sourceCommitPattern.test(parsed.sourceCommit)) { - return { ...args, sourceRevision: parsed.sourceCommit }; - } - } catch (error) { - if (error?.code !== "ENOENT") throw error; - } - return args; -} - function jsonManifest(value) { return `${JSON.stringify(value, null, 2)}\n`; } @@ -729,6 +715,17 @@ function prepareSourceScript() { "git config --global --add safe.directory /workspace/source/repo", "git checkout \"$(params.revision)\"", "test \"$(git rev-parse HEAD)\" = \"$(params.revision)\"", + "if git ls-remote --exit-code --heads origin \"$(params.gitops-branch)\" >/dev/null 2>&1; then", + " git fetch --depth 1 origin \"$(params.gitops-branch)\" >/dev/null 2>&1 || true", + " if git cat-file -e FETCH_HEAD:deploy/artifact-catalog.dev.json 2>/dev/null; then", + " git show FETCH_HEAD:deploy/artifact-catalog.dev.json > deploy/artifact-catalog.dev.json", + " echo '{\"event\":\"gitops-artifact-catalog\",\"phase\":\"prepare-source\",\"status\":\"loaded\",\"branch\":\"'\"$(params.gitops-branch)\"'\"}'", + " else", + " echo '{\"event\":\"gitops-artifact-catalog\",\"phase\":\"prepare-source\",\"status\":\"seed\",\"reason\":\"missing-on-gitops-branch\"}'", + " fi", + "else", + " echo '{\"event\":\"gitops-artifact-catalog\",\"phase\":\"prepare-source\",\"status\":\"seed\",\"reason\":\"gitops-branch-missing\"}'", + "fi", "npm ci --ignore-scripts", `echo ${shellSingleQuote(`G14 prepare-source complete; validation task count=${primitiveValidationTasks.length}`)}` ]; @@ -918,17 +915,10 @@ git config --global user.name "HWLAB G14 GitOps Bot" git config --global user.email "hwlab-g14-gitops-bot@users.noreply.github.com" if [ -s /workspace/source/dev-artifacts.json ]; then node scripts/refresh-artifact-catalog.mjs --target-ref "$(params.revision)" --publish-report /workspace/source/dev-artifacts.json - if ! git diff --quiet -- deploy/artifact-catalog.dev.json; then - short="$(printf '%.7s' "$(params.revision)")" - git add deploy/artifact-catalog.dev.json - git commit -m "chore: promote G14 GitOps source $short" - git push origin "HEAD:$(params.source-branch)" - fi fi -source_state_revision="$(git rev-parse HEAD)" node scripts/g14-gitops-render.mjs --source-revision "$(params.revision)" --source-repo "$(params.git-url)" --source-branch "$(params.source-branch)" --gitops-branch "$(params.gitops-branch)" --registry-prefix "$(params.registry-prefix)" --use-deploy-images node scripts/g14-gitops-render.mjs --source-revision "$(params.revision)" --source-repo "$(params.git-url)" --source-branch "$(params.source-branch)" --gitops-branch "$(params.gitops-branch)" --registry-prefix "$(params.registry-prefix)" --use-deploy-images --check -check_source_head before-push "$source_state_revision" +check_source_head before-push workdir="$(mktemp -d)" git clone --no-checkout "$(params.git-url)" "$workdir/gitops" cd "$workdir/gitops" @@ -942,7 +932,8 @@ else fi mkdir -p deploy/gitops cp -a /workspace/source/repo/deploy/gitops/g14 deploy/gitops/g14 -git add deploy/gitops/g14 +cp /workspace/source/repo/deploy/artifact-catalog.dev.json deploy/artifact-catalog.dev.json +git add deploy/artifact-catalog.dev.json deploy/gitops/g14 if git diff --cached --quiet; then echo '{"status":"unchanged","gitopsBranch":"'"$(params.gitops-branch)"'","sourceRevision":"'"$(params.revision)"'"}' exit 0 @@ -1492,6 +1483,7 @@ function tektonPipeline() { params: [ { name: "git-url" }, { name: "source-branch" }, + { name: "gitops-branch" }, { name: "revision" } ], workspaces: [{ name: "source" }, { name: "git-ssh" }], @@ -1500,6 +1492,7 @@ function tektonPipeline() { params: [ { name: "git-url", value: "$(params.git-url)" }, { name: "source-branch", value: "$(params.source-branch)" }, + { name: "gitops-branch", value: "$(params.gitops-branch)" }, { name: "revision", value: "$(params.revision)" } ] }, @@ -2373,7 +2366,6 @@ async function main() { console.log(usage()); return; } - args = await defaultCheckSourceRevision(args); ensureObject(args, "args"); const { files, source } = await plannedFiles(args); if (args.check) { diff --git a/scripts/src/artifact-runtime-readiness-guard.mjs b/scripts/src/artifact-runtime-readiness-guard.mjs index 5cdee0f2..ec9bcdb5 100644 --- a/scripts/src/artifact-runtime-readiness-guard.mjs +++ b/scripts/src/artifact-runtime-readiness-guard.mjs @@ -1186,7 +1186,7 @@ function nextCommands({ canPublish, canApply, blockedReasons }) { return [ "node scripts/deploy-desired-state-plan.mjs --target-ref origin/main --pretty", "node scripts/deploy-desired-state-plan.mjs --promotion-commit --check", - "node scripts/g14-gitops-render.mjs --check", + "node scripts/g14-gitops-render.mjs --no-write", guardCommand ]; } @@ -1199,7 +1199,7 @@ function nextCommands({ canPublish, canApply, blockedReasons }) { ]; } return [ - "node scripts/g14-gitops-render.mjs --check", + "node scripts/g14-gitops-render.mjs --no-write", "node scripts/artifact-runtime-readiness-guard.mjs --target-ref origin/main --check --live --no-report" ]; } diff --git a/scripts/src/g14-ci-plan-lib.mjs b/scripts/src/g14-ci-plan-lib.mjs index 9aa1e8a9..03cf9a0e 100644 --- a/scripts/src/g14-ci-plan-lib.mjs +++ b/scripts/src/g14-ci-plan-lib.mjs @@ -101,7 +101,9 @@ export async function createG14CiPlan(options = {}) { ...runtimeDepMatches, ...buildSystemMatches ].filter((item) => !isTestOnlyPath(item) && !isDocsOnlyPath(item))); - const affected = imageRelevantChangedPaths.length > 0; + const catalogRecord = catalogByServiceId.get(model.serviceId) ?? null; + const catalogReuse = reuseCandidate(catalogRecord, imageRelevantChangedPaths.length > 0); + const affected = imageRelevantChangedPaths.length > 0 || catalogReuse.status === "candidate-no-catalog" || catalogReuse.status === "candidate-unverified-digest"; const componentInputPaths = uniqueSorted([ ...model.componentPaths, ...model.sharedPaths, @@ -119,7 +121,6 @@ export async function createG14CiPlan(options = {}) { runtimeKind: model.runtimeKind, entrypoint: model.entrypoint }); - const catalogRecord = catalogByServiceId.get(model.serviceId) ?? null; services.push({ serviceId: model.serviceId, runtimeKind: model.runtimeKind, @@ -136,7 +137,9 @@ export async function createG14CiPlan(options = {}) { buildArgsHash, changedPaths: imageRelevantChangedPaths, affected, - reason: affected ? reasonForService({ directMatches, sharedMatches, runtimeDepMatches, buildSystemMatches }) : reasonForUnchanged(globalChange), + reason: affected + ? reasonForService({ directMatches, sharedMatches, runtimeDepMatches, buildSystemMatches, catalogReuse }) + : reasonForUnchanged(globalChange), reuse: reuseCandidate(catalogRecord, affected) }); } @@ -315,12 +318,14 @@ async function gitValue(repoRoot, args) { return result.stdout.trim(); } -function reasonForService({ directMatches, sharedMatches, runtimeDepMatches, buildSystemMatches }) { +function reasonForService({ directMatches, sharedMatches, runtimeDepMatches, buildSystemMatches, catalogReuse = null }) { const reasons = []; if (directMatches.some((item) => !isTestOnlyPath(item) && !isDocsOnlyPath(item))) reasons.push("component-path-changed"); if (sharedMatches.some((item) => !isTestOnlyPath(item) && !isDocsOnlyPath(item))) reasons.push("shared-runtime-changed"); if (runtimeDepMatches.some((item) => !isTestOnlyPath(item) && !isDocsOnlyPath(item))) reasons.push("runtime-deps-changed"); if (buildSystemMatches.some((item) => !isTestOnlyPath(item) && !isDocsOnlyPath(item))) reasons.push("build-system-changed"); + if (catalogReuse?.status === "candidate-no-catalog") reasons.push("catalog-record-missing"); + if (catalogReuse?.status === "candidate-unverified-digest") reasons.push("catalog-digest-missing"); return reasons.length ? reasons : ["affected"]; } diff --git a/scripts/validate-artifact-catalog.mjs b/scripts/validate-artifact-catalog.mjs index b237b1a7..2750c53f 100644 --- a/scripts/validate-artifact-catalog.mjs +++ b/scripts/validate-artifact-catalog.mjs @@ -196,7 +196,6 @@ function assertCatalogServices(catalog, deployManifest, catalogMode) { assert.ok(deployService, `${context} missing from deploy manifest`); assertCommitId(service.commitId, `${context} commitId`); - assert.equal(service.commitId, catalog.commitId, `${context} commitId must match catalog`); assert.equal(service.imageTag, service.commitId.slice(0, 7), `${context} imageTag must be the short commit`); assertNoMutableTag(service.imageTag, `${context} imageTag`); @@ -233,7 +232,7 @@ function assertCatalogServices(catalog, deployManifest, catalogMode) { assert.equal(service.digest, "not_published", `${context} digest`); } else { if (service.artifactRequired) { - assert.equal(service.publishState, "published", `${context} publishState`); + assert.ok(["published", "reused"].includes(service.publishState), `${context} publishState`); assert.match(service.digest, digestPattern, `${context} digest`); } else { assert.equal(service.publishState, "skeleton-only", `${context} publishState`);