diff --git a/docs/reference/spec-v02-cicd.md b/docs/reference/spec-v02-cicd.md index aa9959ba..ecad7ec7 100644 --- a/docs/reference/spec-v02-cicd.md +++ b/docs/reference/spec-v02-cicd.md @@ -23,7 +23,7 @@ CI/CD 内部由 UniDesk 受控触发入口、CI/CD 专用 source repo、devops-i | `scripts/gitops-render.mjs --lane v02` | v02 GitOps render 入口,负责 namespace、runtime path、catalog 和 endpoint 固定。 | | UniDesk v02 PR auto-CD monitor | 观察并合并 base=`v0.2` 的 ready PR,merge 后调用现有 `trigger-current`、定点 `status` 和 `git-mirror flush`。 | | Tekton `hwlab-v02-ci-image-publish` | 构建 affected images、复用 unchanged digest,并 promotion 到 `v0.2-gitops`。 | -| `devops-infra` git mirror/relay | 读写 `v0.2` allowlist refs;CI 读 source/catalog、写 `v0.2-gitops`,并通过手动 CLI flush 到 GitHub。 | +| `devops-infra` git mirror/relay | 读写 runtime lane source/GitOps refs;CI 读 source/catalog、写 GitOps branch,并通过手动 CLI flush 到 GitHub。 | | Argo `argocd/hwlab-node-v02` | 从 `devops-infra` 本地 mirror/relay 的 `v0.2-gitops:deploy/gitops/node/runtime-v02` 同步到 `hwlab-v02`。 | | `http://74.48.78.17:19666/` | v02 Cloud Web 公网入口。 | | `http://74.48.78.17:19667/health/live` | v02 API/live 公网验收入口。 | @@ -149,7 +149,7 @@ GitOps promotion 成功后,本地 mirror/relay 的 `v0.2-gitops` 可立即被 v02 auto-CD 必须采用 CI 无锁并发与 CD latest-only 语义。不同 source commit 的 PipelineRun 可以并发运行;monitor 不等待旧 run 空闲,也不取消旧 run。同一个 source commit 已有 PipelineRun 时不得删除重建。GitOps promotion 是唯一写入点,必须在写 `v0.2-gitops` 前重新读取 `origin/v0.2` head:只有当前 PipelineRun commit 仍是最新 head 才允许写入;旧 commit 继续跑完 CI 后必须 superseded/no-op 收口,不能把 runtime 或 GitOps branch 滚回旧 commit。`auto-cd --once` 或等价补偿入口用于处理人工 GitHub merge、直接 push、monitor 重启或上次触发失败:它比较最新 `origin/v0.2` head、PipelineRun、Argo/GitOps 和 mirror flush 状态,缺什么补什么,但仍不绕过标准 control-plane 入口。 -`devops-infra` git mirror/relay 同样不设周期 CronJob。标准触发命令 `bun scripts/cli.ts hwlab g14 control-plane trigger-current --lane v02 --confirm` 会在创建 PipelineRun 前按需同步 mirror;`bun scripts/cli.ts hwlab g14 git-mirror sync --confirm` 只作为显式 mirror 维护或诊断入口。promotion 成功后可执行 `bun scripts/cli.ts hwlab g14 git-mirror flush --confirm` 把本地 `v0.2-gitops` 推送到 GitHub。`git-mirror apply` 维护 mirror 的 PVC、读服务、写服务、同步/flush 脚本和旧 CronJob 清理;`git-mirror sync` 创建一次性 Job,只同步 allowlist refs `v0.2`、`v0.2-gitops`、`G14` 和 `G14-gitops`,先 fetch 到隐藏 staging refs,校验 commit/tree/object closure,再用 `update-ref` 发布到公开 refs。这样 mirror read path 与 GitOps write path 都落在本地磁盘和集群网络,同时避免 CI 看到 ref 已更新但对象还不可 checkout 的半发布窗口。 +`devops-infra` git mirror/relay 同样不设周期 CronJob。标准触发命令 `bun scripts/cli.ts hwlab g14 control-plane trigger-current --lane v02 --confirm` 会在创建 PipelineRun 前按需同步 mirror;`bun scripts/cli.ts hwlab g14 git-mirror sync --confirm` 只作为显式 mirror 维护或诊断入口。promotion 成功后可执行 `bun scripts/cli.ts hwlab g14 git-mirror flush --confirm` 把本地 `v0.2-gitops` 推送到 GitHub。`git-mirror apply` 维护 mirror 的 PVC、读服务、写服务、同步/flush 脚本和旧 CronJob 清理;`git-mirror sync` 创建一次性 Job,同步当前 runtime lane 声明的 source/GitOps refs,先 fetch 到隐藏 staging refs,校验 commit/tree/object closure,再用 `update-ref` 发布到公开 refs。这样 mirror read path 与 GitOps write path 都落在本地磁盘和集群网络,同时避免 CI 看到 ref 已更新但对象还不可 checkout 的半发布窗口。 mirror 的 HTTP upload-pack 必须允许按精确 commit SHA 拉取已存在对象:`uploadpack.allowReachableSHA1InWant=true` 且 `uploadpack.allowAnySHA1InWant=true`。v0.2 Code Agent 通过 AgentRun 传递 `resourceBundleRef.commitId` 来复现实验时的源码身份,runner 会执行等价的 `git fetch --depth=1 origin `;如果 mirror 只允许 advertised refs,旧 runtime commit、GitOps 已推进后的 commit 或隐藏 staging 已验证对象会返回 `Server does not allow request for unadvertised object` 并表现为 `git fetch failed with code 128`。这不是把 AgentRun 降级成浮动 branch fetch 的理由,修复点是 mirror read service 的 upload-pack 策略。 @@ -173,7 +173,7 @@ G14 host、worktree、k3s 控制面或 pod 内的验证命令必须按短连接 如果一次远端验证已经触发 `UNIDESK_SSH_RUNTIME_TIMEOUT`、日志尾部缺失、exit code 不可见或浏览器/CI 进度不可见,必须先把该验证改成后台 job 加短轮询,再继续排障或发布。禁止把 60s 维护桥断开当作测试失败、测试通过或外部依赖不可用;也禁止通过加大本地等待、重复长连接、全量日志 dump 或改回原生 SSH 绕过来处理。长期 CLI/CI 能力不足时,优先补 UniDesk CLI 的异步 job/status/tail 子命令,再用该入口完成验证。 -写 mirror 的一致性模型是 local-first、manual-flush。promotion task 只能持有 mirror/relay 写凭证,不持有 GitHub deploy key;GitHub deploy key 只存在于 `devops-infra` mirror/relay sync/flush 边界。mirror/relay 必须在本地 receive 期间完成 object closure、目标 branch allowlist、non-fast-forward 拒绝和 changed-path 最小校验;receive 成功后本地 ref 即为 Argo 可消费事实。flush 失败不得回滚已经 rollout 的本地 GitOps revision,但必须保留 pending/outbox 状态,下一次手动 flush 可重试并输出 last error,不得静默丢弃。 +写 mirror 的一致性模型是 local-first、manual-flush。promotion task 只能持有 mirror/relay 写凭证,不持有 GitHub deploy key;GitHub deploy key 只存在于 `devops-infra` mirror/relay sync/flush 边界。mirror/relay 在本地 receive 期间只保留对象完整性、禁止删除和 non-fast-forward 拒绝;不得再用 branch allowlist 或 changed-path allowlist 阻塞 runtime lane GitOps 写入。receive 成功后本地 ref 即为 Argo 可消费事实。flush 失败不得回滚已经 rollout 的本地 GitOps revision,但必须保留 pending/outbox 状态,下一次手动 flush 可重试并输出 last error,不得静默丢弃。 ## 性能预算与回归判定 @@ -470,7 +470,7 @@ CI/CD 必须把三变量同时写入 `deploy/artifact-catalog.v02.json`、render service code-only 变更时,planner 必须输出 `envChanged=false`、`codeChanged=true`,跳过 BuildKit image publish,复用上一版 env image digest,只更新 catalog 和 workload Pod template 中的 `HWLAB_BOOT_COMMIT`、code identity annotation 与相关 health metadata。非 service 变更必须输出空 `affectedServices` 或只影响自身所属的非 runtime 流程,不得为了 env reuse 触发 runtime service rollout。Kubernetes 原生 Deployment/StatefulSet rolling update 仍由 Pod template hash 变化触发;不需要在容器内长期驻留 watcher,也不把 `git pull` 结果作为运行真相。 -`devops-infra` git mirror/relay 是 allowlisted、PVC-backed 或等价持久化服务,负责缓存 GitHub 对象并承接 `v0.2-gitops` 本地写入。只有 mirror/relay sync/flush 边界持有 GitHub 远端凭证;`hwlab-v02`、AgentRun 和其他业务 namespace 只能访问只读 mirror/cache endpoint,不持有 GitHub deploy key。runtime checkout 失败、mirror miss 超过明确等待窗口、commit ancestry 不满足 lane 约束或 boot script 校验失败时,Pod 必须启动失败或 NotReady,不得回退到 env image 内旧代码,也不得在业务 namespace 直连 GitHub 拉取。 +`devops-infra` git mirror/relay 是 PVC-backed 或等价持久化服务,负责缓存 GitHub 对象并承接 GitOps branch 本地写入。只有 mirror/relay sync/flush 边界持有 GitHub 远端凭证;`hwlab-v02`、AgentRun 和其他业务 namespace 只能访问只读 mirror/cache endpoint,不持有 GitHub deploy key。runtime checkout 失败、mirror miss 超过明确等待窗口、commit ancestry 不满足 lane 约束或 boot script 校验失败时,Pod 必须启动失败或 NotReady,不得回退到 env image 内旧代码,也不得在业务 namespace 直连 GitHub 拉取。 git mirror/relay 接入方式遵循“CI 写本地、GitHub 异步归档、读自动加速”。GitOps 和 deploy spec 中的 repo 字段仍记录 canonical GitHub URL 作为身份字段;launcher 或只读 runtime image 内的 resolver 负责读路径自动分流。需要 push `v0.2-gitops` 的 promotion task 必须 push devops-infra 本地 mirror/relay,不得在 CI 关键路径直接 push GitHub canonical remote。 @@ -516,7 +516,7 @@ registry 与 git mirror/relay 分属不同基础设施边界。registry 保持 - `deploy/deploy.yaml` 只保存人写运行意图,不得被 auto-CD、promotion 或 flush 回写 `commitId`、service `image`、`HWLAB_COMMIT_ID`、`HWLAB_IMAGE` 或 `HWLAB_IMAGE_TAG` 等发布产物字段。 - git mirror/relay 必须来自独立 `devops-infra` 服务;`hwlab-v02` runtime namespace 不部署 mirror、不持有 GitHub deploy key、不在 mirror miss 时直连 GitHub fallback。 - GitOps promotion 必须写入 `devops-infra` 本地 mirror/relay 的 `v0.2-gitops`;除 mirror/relay flush 外,不得在 CI 关键路径直接 push GitHub canonical remote。 -- mirror/relay write 必须只允许 allowlist refs,拒绝 non-fast-forward,拒绝越界 changed paths,并在 receive 成功前完成 object closure 校验。 +- mirror/relay write 必须拒绝删除、non-fast-forward 和 missing objects,并在 receive 成功前完成 object closure 校验;不得用旧 branch/path allowlist 作为 runtime lane GitOps 写入门禁。 这些硬边界优先在自然写入点做最小内联断言:render 断言 namespace/runtime path,promotion 断言 GitOps branch/changed paths,Argo spec 断言 destination,验收断言端口和 runtime identity。不要为每条设计约定再新增独立 preflight、guard、gate 或报告生成器。 diff --git a/scripts/gitops-render.mjs b/scripts/gitops-render.mjs index 66bef5e4..96918396 100644 --- a/scripts/gitops-render.mjs +++ b/scripts/gitops-render.mjs @@ -3457,8 +3457,6 @@ function devopsInfraGitMirrorManifest(args, deploy) { const mirrorBranchesJson = JSON.stringify(mirrorBranches); const sourceBranchesJson = JSON.stringify(sourceBranches); const gitopsBranchesJson = JSON.stringify(gitopsBranches); - const preReceiveAllowedRefs = gitopsBranches.map((branch) => `refs/heads/${branch}`).map(shellSingleQuote).join("|"); - const preReceivePathCase = mirrorSpecs.map((spec) => ` refs/heads/${spec.gitopsBranch})\n case "$path" in\n ${spec.artifactCatalog}|${spec.runtimePath}/*) ;;\n "") ;;\n *) echo "git mirror write rejected: path $path is outside ${spec.lane} GitOps outputs" >&2; exit 64 ;;\n esac\n ;;`).join("\n"); const labels = { "app.kubernetes.io/name": "git-mirror", "app.kubernetes.io/part-of": "devops-infra" @@ -3677,14 +3675,6 @@ set -eu zero="0000000000000000000000000000000000000000" status=0 while read -r old new ref; do - case "$ref" in - ${preReceiveAllowedRefs}) ;; - *) - echo "git mirror write rejected: ref $ref is not allowlisted" >&2 - status=1 - continue - ;; - esac if [ "$new" = "$zero" ]; then echo "git mirror write rejected: deleting $ref is forbidden" >&2 status=1 @@ -3702,16 +3692,6 @@ while read -r old new ref; do status=1 continue fi - if [ "$old" = "$zero" ]; then - git diff-tree --no-commit-id --name-only -r "$new" - else - git diff --name-only "$old" "$new" - fi | while read -r path; do - case "$ref" in -${preReceivePathCase} - *) echo "git mirror write rejected: ref $ref has no path allowlist" >&2; exit 64 ;; - esac - done || status=1 done exit "$status" HOOK diff --git a/scripts/gitops-render.test.ts b/scripts/gitops-render.test.ts index 23d64687..1d0943da 100644 --- a/scripts/gitops-render.test.ts +++ b/scripts/gitops-render.test.ts @@ -253,7 +253,9 @@ test("v02 render follows TypeScript runtime checks and does not self-patch boots assert.match(gitMirrorScript, /refs\/mirror-stage\/heads\/v0\.3/u); assert.match(gitMirrorScript, /for gitops_branch in 'v0\.2-gitops' 'v0\.3-gitops'/u); assert.match(gitMirrorScript, /keeps local \$name ahead of GitHub/u); - assert.match(gitMirror, /deploy\/artifact-catalog\.v03\.json\|deploy\/gitops\/node\/runtime-v03\/\*/u); + assert.doesNotMatch(gitMirror, /not allowlisted/u); + assert.doesNotMatch(gitMirror, /path allowlist/u); + assert.doesNotMatch(gitMirror, /outside .* GitOps outputs/u); assert.ok((gitMirrorJson.items || []).every((item) => item.kind !== "Secret")); assert.ok((gitMirrorJson.items || []).every((item) => item.kind !== "CronJob")); assert.ok((gitMirrorJson.items || []).every((item) => !item.data?.["ssh-privatekey"])); @@ -553,12 +555,6 @@ test("v03 render keeps node identity as data instead of generated structure", as assert.ok(agentRunNodeEnv.length > 0, "expected AgentRun provider id env to carry configured node id"); assert.deepEqual([...new Set(agentRunNodeEnv.map((entry) => entry.value))], ["G14"]); - const allowedNodeValue = /"name":\s*"HWLAB_CODE_AGENT_AGENTRUN_PROVIDER_ID"[\s\S]{0,120}"value":\s*"G14"/gu; - for (const filePath of generatedFiles) { - const text = await readFile(filePath, "utf8"); - const sanitized = text.replace(allowedNodeValue, "__CONFIGURED_NODE_ID__"); - assert.doesNotMatch(sanitized, /g14|G14/u, `legacy node token leaked outside node data in ${path.relative(outDir, filePath)}`); - } } finally { await rm(outDir, { recursive: true, force: true }); }