diff --git a/docs/reference/spec-v02-cicd.md b/docs/reference/spec-v02-cicd.md index 97f6aa90..e30e04ae 100644 --- a/docs/reference/spec-v02-cicd.md +++ b/docs/reference/spec-v02-cicd.md @@ -10,7 +10,7 @@ ## 内部架构 -CI/CD 内部由 UniDesk 手动触发入口、PipelineRun、component planner、BuildKit publish、GitOps promotion、Argo sync、GitHub flush 和公网验收构成。`v0.2` 不设置 branch poller、control-plane reconciler 或其他 CronJob;source branch 只保存源码和人写配置;`v0.2-gitops` branch 保存 catalog 和 rendered runtime desired state;live runtime 是最终通过证据。 +CI/CD 内部由 UniDesk 手动触发入口、CI/CD 专用 source repo、devops-infra git mirror、PipelineRun、component planner、BuildKit publish、GitOps promotion、Argo sync、GitHub flush 和公网验收构成。`v0.2` 不设置 branch poller、control-plane reconciler 或其他 CronJob;source branch 只保存源码和人写配置;`v0.2-gitops` branch 保存 catalog 和 rendered runtime desired state;live runtime 是最终通过证据。固定开发 workspace 只服务人工开发和短连接源码工具,不参与 CI/CD source commit 选择。 `v0.2` 的 GitOps 写路径采用真正写 mirror:promotion 先把 `v0.2-gitops` 写入 `devops-infra` 本地 git mirror/relay,Argo CD 从本地 mirror/relay 读取该 revision 并 rollout,CI 关键路径不等待 GitHub push。GitHub `pikasTech/HWLAB` 仍是长期源码与归档上游,但对 `v0.2-gitops` 来说是由 mirror/relay 负责 flush 的异步上游,不再是 promotion task 的同步写入目标。 @@ -38,7 +38,8 @@ CI/CD 内部由 UniDesk 手动触发入口、PipelineRun、component planner、B | 对象 | v0.2 规格 | | --- | --- | | Source branch | `v0.2` | -| Source workspace | `G14:/root/hwlab-v02` | +| Source workspace | `G14:/root/hwlab-v02`,仅用于人工开发、短连接源码工具和可见性对照,不作为 CI/CD source commit 选择入口 | +| CI/CD source repo | `G14:/root/hwlab-v02-cicd.git` bare repo,由 UniDesk control-plane 自动 fetch `origin/v0.2`,再从 commit-pinned detached worktree render/apply 控制面 | | GitOps branch | `v0.2-gitops` | | Artifact catalog | `v0.2-gitops:deploy/artifact-catalog.v02.json` | | Runtime path | `v0.2-gitops:deploy/gitops/g14/runtime-v02` | @@ -64,12 +65,21 @@ CI/CD 内部由 UniDesk 手动触发入口、PipelineRun、component planner、B 1. live runtime:`hwlab-v02` namespace 中 Deployment/StatefulSet template、Pod ready、事件、日志和 `19666/19667` 公网 health。 2. Argo desired state:`argocd/hwlab-g14-v02` 的 revision、sync、health、source branch 和 runtime path。 -3. devops-infra mirror/relay 中的 GitOps branch:`v0.2-gitops` 中的 `deploy/artifact-catalog.v02.json` 与 `deploy/gitops/g14/runtime-v02/**`。 -4. Tekton 执行证据:UniDesk `trigger-current` 返回的 PipelineRun、TaskRun result、`gitops-promote` 终态。 -5. GitHub 上游归档状态:mirror/relay flush 后的 `origin/v0.2-gitops` 与 `origin/v0.2`。 -6. 干净 source workspace:`origin/v0.2`、`deploy/deploy.json`、模板、render 脚本和 `--no-write` 输出。 +3. CI/CD source refs:UniDesk control-plane 专用 bare repo `refs/remotes/origin/v0.2`、devops-infra mirror/relay 的 `refs/heads/v0.2` 与 `refs/mirror-stage/heads/v0.2` 必须共同指向最新 source commit。 +4. devops-infra mirror/relay 中的 GitOps branch:`v0.2-gitops` 中的 `deploy/artifact-catalog.v02.json` 与 `deploy/gitops/g14/runtime-v02/**`。 +5. Tekton 执行证据:UniDesk `trigger-current` 返回的 PipelineRun、TaskRun result、`gitops-promote` 终态。 +6. GitHub 上游归档状态:mirror/relay flush 后的 `origin/v0.2-gitops` 与 `origin/v0.2`。 +7. 固定 source workspace:`/root/hwlab-v02` 的 `HEAD`、dirty 状态和 `origin/v0.2` 只作为人工开发对照线索;即使 workspace 脏或落后,也不得影响 CI/CD source commit 选择。 -旧 commit 记忆、`G14`/`G14-gitops` DEV/PROD 产物、D601 legacy 路径、GitHub 上游尚未 flush 的短暂落后、source branch 中历史 generated 文件和临时 worktree 只能作为线索,不能作为 `v0.2` 发布通过证据。 +旧 commit 记忆、`G14`/`G14-gitops` DEV/PROD 产物、D601 legacy 路径、GitHub 上游尚未 flush 的短暂落后、source branch 中历史 generated 文件、固定开发 workspace 脏状态和临时 worktree 只能作为线索,不能作为 `v0.2` 发布通过证据。 + +## Workspace 与 CI/CD 分离 + +`v0.2` 开发 workspace 和 CI/CD repo 必须分离。`/root/hwlab-v02` 是人工开发、短连接 `hwlab-cli` 和问题复现的固定 workspace;它允许出现并行任务产生的 untracked `.worktree/`、本地 dirty 文件或临时落后状态。CI/CD 不从该 checkout 的 `HEAD`、工作树 clean 状态或本地 branch 读取待发布 commit,也不得因为该 workspace 脏而跳过、误判或复用旧 PipelineRun。 + +UniDesk control-plane 必须使用独立 bare repo `/root/hwlab-v02-cicd.git` 作为 CI/CD source repo:每次 `status`、`apply`、`git-mirror apply` 和 `trigger-current` 先自动 fetch `origin/v0.2` 到 `refs/remotes/origin/v0.2`,再用目标 commit 创建 detached temp worktree 运行 `scripts/g14-gitops-render.mjs --lane v02`。该 repo 没有业务工作树,因此不会被人工开发 dirty 状态污染;如果它不可用,应以 `v02-head-unresolved` 或等价结构化错误失败,而不是回退到固定 workspace HEAD。 + +devops-infra git mirror 仍是 PipelineRun 和 Argo CD 的集群内读写源。`trigger-current --lane v02 --confirm` 在创建 PipelineRun 前必须比较 `expectedSourceHead` 与 mirror `localV02`,不一致时自动执行 bounded `git-mirror sync` Job。`git-mirror status` 必须分别暴露 `localV02`、`githubV02`、`localGitops`、`githubGitops`、`sourceInSync`、`gitopsInSync` 和 `pendingFlush`;`githubInSync` 不得再被理解为只代表 GitOps branch。 ## Source 与 GitOps 分层 @@ -98,7 +108,7 @@ CI/CD 内部由 UniDesk 手动触发入口、PipelineRun、component planner、B 标准链路如下: -1. UniDesk CLI `hwlab g14 control-plane trigger-current --lane v02 --confirm` 解析当前 `origin/v0.2` 完整 source commit SHA,先复核 `devops-infra` mirror 的 `localV02` ref,必要时自动执行一次 bounded manual `git-mirror sync` Job,再创建 commit-pinned `hwlab-v02-ci-poll-` PipelineRun;默认 `--dry-run` 只返回将要创建的 manifest 和 mirror pre-sync 计划。 +1. UniDesk CLI `hwlab g14 control-plane trigger-current --lane v02 --confirm` 通过 `/root/hwlab-v02-cicd.git` 自动 fetch 并解析当前 `origin/v0.2` 完整 source commit SHA,先复核 `devops-infra` mirror 的 `localV02` ref,必要时自动执行一次 bounded manual `git-mirror sync` Job,再创建 commit-pinned `hwlab-v02-ci-poll-` PipelineRun;默认 `--dry-run` 只返回将要创建的 manifest 和 mirror pre-sync 计划。 2. `prepare-source` 通过 `devops-infra` mirror checkout `v0.2` source,并从 mirror 中的 `v0.2-gitops` 读取上一版 `deploy/artifact-catalog.v02.json`。 3. CI/CD 校验只保留最小构建、TypeScript 语义检查、自动单元测试、打包和必要冒烟检查;旧 DEV/D601/main gate、运行时内部证明型校验、健康诊断重断言和历史预检不进入 lane。 4. planner 根据 component input 判断 affected/reused services。 diff --git a/scripts/g14-gitops-render.mjs b/scripts/g14-gitops-render.mjs index d57fb8bc..2735bb04 100644 --- a/scripts/g14-gitops-render.mjs +++ b/scripts/g14-gitops-render.mjs @@ -3264,25 +3264,35 @@ git -C "$repo_path" fsck --connectivity-only --no-dangling >/tmp/hwlab-git-fsck. emit_timing fsck succeeded "$fsck_started_ms" git -C "$repo_path" update-server-info v02_head=$(git -C "$repo_path" rev-parse refs/heads/v0.2 2>/dev/null || true) +v02_github_head=$(git -C "$repo_path" rev-parse refs/mirror-stage/heads/v0.2 2>/dev/null || true) g14_head=$(git -C "$repo_path" rev-parse refs/heads/G14 2>/dev/null || true) +g14_github_head=$(git -C "$repo_path" rev-parse refs/mirror-stage/heads/G14 2>/dev/null || true) v02_gitops_head=$(git -C "$repo_path" rev-parse refs/heads/v0.2-gitops 2>/dev/null || true) v02_gitops_github_head=$(git -C "$repo_path" rev-parse refs/mirror-stage/heads/v0.2-gitops 2>/dev/null || true) published_at=$(date -u +%Y-%m-%dT%H:%M:%SZ) printf '%s\n' "$v02_head" | tee /cache/HWLAB-v0.2.head printf '%s\n' "$published_at" > /cache/HWLAB.last-sync -export v02_head g14_head v02_gitops_head v02_gitops_github_head published_at +export v02_head v02_github_head g14_head g14_github_head v02_gitops_head v02_gitops_github_head published_at node - <<'NODE' | tee /cache/HWLAB.last-sync.json +const v02Head = process.env.v02_head || null; +const v02GithubHead = process.env.v02_github_head || null; +const gitopsHead = process.env.v02_gitops_head || null; +const gitopsGithubHead = process.env.v02_gitops_github_head || null; const payload = { event: "git-mirror-sync", status: "published", repository: "pikasTech/HWLAB", publishedAt: process.env.published_at, - pendingFlush: Boolean(process.env.v02_gitops_head && process.env.v02_gitops_github_head && process.env.v02_gitops_head !== process.env.v02_gitops_github_head), + pendingFlush: Boolean(gitopsHead && gitopsGithubHead && gitopsHead !== gitopsGithubHead), + sourceInSync: Boolean(v02Head && v02GithubHead && v02Head === v02GithubHead), + gitopsInSync: Boolean(gitopsHead && gitopsGithubHead && gitopsHead === gitopsGithubHead), refs: { - "refs/heads/v0.2": process.env.v02_head || null, + "refs/heads/v0.2": v02Head, + "refs/mirror-stage/heads/v0.2": v02GithubHead, "refs/heads/G14": process.env.g14_head || null, - "refs/heads/v0.2-gitops": process.env.v02_gitops_head || null, - "refs/mirror-stage/heads/v0.2-gitops": process.env.v02_gitops_github_head || null + "refs/mirror-stage/heads/G14": process.env.g14_github_head || null, + "refs/heads/v0.2-gitops": gitopsHead, + "refs/mirror-stage/heads/v0.2-gitops": gitopsGithubHead } }; console.log(JSON.stringify(payload)); diff --git a/scripts/g14-gitops-render.test.ts b/scripts/g14-gitops-render.test.ts index 748472d2..48debf6b 100644 --- a/scripts/g14-gitops-render.test.ts +++ b/scripts/g14-gitops-render.test.ts @@ -143,6 +143,8 @@ test("v02 render follows TypeScript runtime checks and does not self-patch boots assert.match(gitMirrorScript, /git-mirror-sync-timing/u); assert.match(gitMirrorScript, /emit_timing fetch succeeded/u); assert.match(gitMirrorScript, /emit_timing total succeeded/u); + assert.match(gitMirrorScript, /sourceInSync/u); + assert.match(gitMirrorScript, /refs\/mirror-stage\/heads\/v0\.2/u); assert.match(gitMirrorScript, /keeps local v0\.2-gitops ahead of GitHub/u); assert.ok((gitMirrorJson.items || []).every((item) => item.kind !== "Secret")); assert.ok((gitMirrorJson.items || []).every((item) => item.kind !== "CronJob"));