diff --git a/.agents/skills/unidesk-cicd/SKILL.md b/.agents/skills/unidesk-cicd/SKILL.md index 8ed29632..d3aedbf9 100644 --- a/.agents/skills/unidesk-cicd/SKILL.md +++ b/.agents/skills/unidesk-cicd/SKILL.md @@ -2,7 +2,7 @@ name: unidesk-cicd description: >- UniDesk CI/CD 控制面,覆盖 PaC consumer 首发 bootstrap、Tekton/Argo、GitOps、 - git-mirror、PR 自动交付、Secret、observability、CI tools image、PipelineRun 清理、 + git-mirror、L2/L3 手动计划发布、Secret、observability、CI tools image、PipelineRun 清理、 Tekton 大对象与 Kine/SQLite 控制面退化、AgentRun 与 HWLAB 部署, 以及 branch-follower 退役只读诊断。 用户提到 CI/CD、deploy、rollout、PipelineRun、PaC、bootstrap、GitOps、Tekton、 @@ -17,9 +17,9 @@ description: >- HWLAB G14 和 AgentRun CI/CD 的受控入口。任何 PR 监控、Tekton/Argo、git-mirror、Secret、observability、CI tools image、PipelineRun 清理或 AgentRun 部署都必须走 `bun scripts/cli.ts`。 - `$unidesk-devlevel` 用 L2 Development 和 L3 Production 描述两种集群部署方式: - - 本 skill 负责这两种方式的自动交付、运行面操作和事故处理; + - 本 skill 负责这两种方式的手动计划发布、运行面操作和事故处理; - L0 Function 与 L1 Native 不使用 CI/CD。 - - 能在 L0/L1 复现和修复的问题,不用反复 L2/L3 rollout 代替快速小回环;修复后再通过正常自动链回归对应集群方式。 + - 能在 L0/L1 复现和修复的问题,不用反复 L2/L3 rollout 代替快速小回环;需要集群回归时必须先 plan,再手动触发对应集群方式。 - L2 可由代理按任务需要自行判断。 - L3 必须先完成对应 L2 回归,L2 通过后才请求用户当次明确授权,获授权后再滚动到生产。 @@ -47,6 +47,8 @@ bun scripts/cli.ts platform-infra gitea mirror status --target JD01 bun scripts/cli.ts platform-infra gitea mirror webhook status --target JD01 bun scripts/cli.ts platform-infra pipelines-as-code bootstrap --target NC01 --consumer --dry-run bun scripts/cli.ts platform-infra pipelines-as-code bootstrap --target NC01 --consumer --confirm +bun scripts/cli.ts platform-infra pipelines-as-code release plan --target NC01 --consumer --source-commit [--base-commit ] +bun scripts/cli.ts platform-infra pipelines-as-code release trigger --target NC01 --consumer --source-commit [--base-commit ] --confirm bun scripts/cli.ts platform-infra pipelines-as-code status --target JD01 bun scripts/cli.ts platform-infra pipelines-as-code status --target JD01 --consumer hwlab-jd01-v03 bun scripts/cli.ts platform-infra pipelines-as-code history --target JD01 --limit 10 @@ -63,6 +65,16 @@ bun scripts/cli.ts agentrun control-plane legacy-cicd --help bun scripts/cli.ts hwlab nodes control-plane legacy-cicd --help ``` +- L2/L3 发布触发: + - PR merge、push 和 branch update 只更新 source authority,不创建 PipelineRun; + - 先运行 `release plan`,检查本次请求范围、累计有效范围、env reuse、待构建镜像数量与列表、rollout 范围和全部受影响服务; + - 当前 GitOps catalog 落后 source 时,plan 必须把累计发布欠账与本次 base 到 source 的请求范围分开显示; + - 有效范围大于请求范围时标记 `expanded`,`release trigger` 必须在发送 webhook 前拒绝; + - 计划范围扩大时先修复 source range、源码、YAML 或项目 planner,再重新 plan; + - 审阅通过后运行 `release trigger --confirm`;该命令只向 PaC 发送 webhook,由 PaC 创建 PipelineRun; + - 禁止裸创建 PipelineRun、由 CLI 直建 PipelineRun,或恢复 Gitea 自动 hook; + - L3 继续要求对应 L2 已通过和用户对当次生产发布的明确授权。 + - 节点级只读状态: - 优先用 `cicd status --node `; - 该入口在目标侧一次批量读取 PipelineRun、Argo Application 与 Deployment,再按 owning YAML consumer 投影,禁止为每个 consumer 并发启动完整 `status`; @@ -125,7 +137,7 @@ bun scripts/cli.ts hwlab nodes control-plane legacy-cicd --help - 确认后把同一命令改为 `--confirm`; - 入口从 Gitea 与 PaC owning YAML 精确解析仓库; - 入口只初始化空 Gitea 仓库和 PaC/Tekton/GitOps 控制面; - - 随后合并 source PR,作为唯一交付触发; + - 随后合并 source PR,再通过 `release plan` 和 `release trigger --confirm` 手动发布; - 详细最短路径见 [references/gitea-pac.md](references/gitea-pac.md)。 按职责读取拆分后的 reference: @@ -208,7 +220,7 @@ bun scripts/cli.ts hwlab nodes control-plane legacy-cicd --help - 当前业务问题域内的 CI/CD 故障必须直接闭环: - 用户要求修复、发布或回归某个业务时,选中 consumer 自有的 owning YAML 配置片段、独立 namespace/lane、独立 source artifact、消费仓 `.tekton` 制品和该业务运行配置均属于主任务授权范围; - - 确认故障位于上述范围后,直接完成根因修复、正常 PR 自动交付和原业务入口回归,不得只登记 issue 后停工; + - 确认故障位于上述范围后,直接完成根因修复、PR 合并、手动 plan/trigger 发布和原业务入口回归,不得只登记 issue 后停工; - 修复仍须保持最小影响面,并按 L0、L1、L2 的适用层级逐级回归;不得借域内授权修改共享控制面或其他 consumer; - 只有致因落入下述公共服务面,或修复必须改变跨 consumer 共享对象时,才转为公共服务面授权判定。 - 未经用户明确授权,不得修改 CI/CD 公共服务面: @@ -220,12 +232,12 @@ bun scripts/cli.ts hwlab nodes control-plane legacy-cicd --help - 用户只说“修复某个 consumer 的 CI/CD”不等于授权修改公共服务面;授权必须明确指向共享 PaC、Gitea、git-mirror、Tekton/Argo bootstrap 或其他公共组件; - consumer 私有修复不得夹带公共服务重构、其他 consumer 配置、共享模板清理或无关 PR;无法在私有边界内修复时按阻塞处理,不得扩大作用域。 - 生产 CI/CD 事故必须优先回退: - - 把自动链连续失败、多个 consumer 同时失败、生产发布停摆、错误变更持续扩散或需要人工补链才能交付,按生产事故处理; + - 把共享发布链连续失败、多个 consumer 同时失败、生产发布停摆、错误变更持续扩散或需要受控入口之外补链才能交付,按生产事故处理; - 立即停止与恢复无关的合并、bootstrap、apply、手工同步、补跑和并行架构改造,只保留只读取证与回退动作; - 先锁定首个失败事件、最后一个成功事件、共同失败指纹和两者之间的变更集,优先证明“哪一个变更导致失败”,禁止从最新错误状态直接开始连续试修; - 最近变更与失败强相关且回退不会造成不可逆数据损坏时,必须先回退该变更,恢复最后已知健康基线;不得以“顺手完善”“已经改了一半”或“继续修可能更快”为由保留错误基础; - - 回退也必须通过最小单一职责 PR 和正常自动事件交付,禁止用直接 Gitea push、人工 PipelineRun、Argo sync 或运行面热补伪造恢复; - - 只有回退后的正常事件已经证明自动链恢复,才允许从健康基线重新设计前向修复;前向修复必须拆成可独立验证、可独立回退的最小变更; + - 回退也必须通过最小单一职责 PR、`release plan` 审阅和受控手动 webhook 交付,禁止用直接 Gitea push、裸建 PipelineRun、Argo sync 或运行面热补伪造恢复; + - 只有回退后的受控发布已经证明链路恢复,才允许从健康基线重新设计前向修复;前向修复必须拆成可独立验证、可独立回退的最小变更; - 回退存在不可逆 schema、数据迁移或外部状态风险时,先停止继续发布并保全证据,再选择已有受控恢复路径;不得在没有恢复边界的情况下继续叠加代码修复。 - PaC 公共 status renderer 故障必须先区分交付失败与观察失败: - 先用 PipelineRun、Argo 与 runtime 独立事实确认自动交付是否成功,不得把 `parse-failure` 或 status timeout 直接表述为交付链失败; @@ -235,25 +247,25 @@ bun scripts/cli.ts hwlab nodes control-plane legacy-cicd --help - PK01 的 CI/CD 默认只允许只读诊断: - 普通 PR merge、自动滚动、环境恢复或跨服务交付不构成 PK01 变更授权; - 只有用户在当前请求中明确要求修改 PK01,才允许提交或推广会改变 PK01 版本、配置、Secret 绑定、边缘路由、容器或运行面状态的 source 变更。 -- PaC migrated consumer 的唯一正式 CI/CD 触发是目标 source branch 的 GitHub PR merge。合并后由 GitHub webhook -> Gitea controlled mirror + immutable snapshot ref -> Gitea webhook -> PaC -> Tekton -> GitOps/Argo -> runtime 自动滚动;不得要求主代理、子代理或操作者再执行 apply、closeout、trigger、sync、flush 或补跑。 +- PaC migrated consumer 的唯一正式 CI/CD 触发是 `release plan` 审阅后由 CLI 手动发送 PaC webhook。GitHub PR merge 只更新 Gitea controlled mirror 与 immutable snapshot ref,不创建 PipelineRun;禁止恢复 Gitea 自动 hook、直接 Gitea push、裸建 PipelineRun、Argo sync 或受控入口之外的补跑。 - PaC consumer 首次引导必须满足以下约束: - 默认使用 `pipelines-as-code bootstrap --dry-run|--confirm`,不再人工串联 Gitea bootstrap 与 PaC apply; - `apply` 必须在任何 release、RBAC、Secret、Repository CR、Argo 或 webhook 写入前确认 YAML 匹配的 Gitea 仓库已存在; - - source PR 合并后不得再次调用 bootstrap 或 apply;正常验收不串联 mirror/status/history,只有失败归因才逐层下钻。 -- 自动链路不通时必须修复自动链自身,并通过修复 PR 合并产生的新正常事件验收: + - source PR 合并后不得再次调用 bootstrap 或 apply;先执行 `release plan`,范围准确后才执行 `release trigger --confirm`。 +- 发布链路不通时必须修复链路自身,并通过修复 PR 对应的手动 plan/trigger 验收: - 已发生错误或首次部署尚未跑通时,允许使用 YAML 已声明的受控单步入口调试具体阶段; - 单步调试必须限定目标、输入和阶段,保留结构化结果,并且不得改写 source/ref authority、伪造成功状态或替代完整交付; - - 单步确认的修复必须回写 owning YAML、renderer 或源码,再由正常 source PR 合并事件跑通自动 CI/CD 链; - - 只有自动链从 source event、artifact、GitOps/Argo 到 runtime 原入口全部收敛,才能把首次部署或故障修复判为完成; + - 单步确认的修复必须回写 owning YAML、renderer 或源码,再由正常 source PR 合并、plan 和手动 webhook 跑通 CI/CD 链; + - 只有发布链从手动 source event、artifact、GitOps/Argo 到 runtime 原入口全部收敛,才能把首次部署或故障修复判为完成; - 禁止人工 mirror sync、直接 Gitea push、人工 PipelineRun、`trigger-current` 或其他手段补齐当前交付; - 调试或临时恢复确需改变运行面时,允许按 `$unidesk-daddev` P2 实施最小、可逆 patch, 但不得修改 source/ref authority、伪造交付成功或作为最终验收; - P2 结论必须收敛到 owning YAML、controller 或源码, - 再由修复 PR 的正常自动事件交付;临时 patch 必须撤销或被声明式交付覆盖; + 再由修复 PR 的手动 plan/trigger 交付;临时 patch 必须撤销或被声明式交付覆盖; - 只读 status/history/events/logs/debug-step 仍是默认定位入口, `closeout` 只属于显式 compatibility diagnostics,不得进入默认观察或恢复路径。 -- CI/CD、GitOps、rollout、PipelineRun、Argo、git-mirror 和 AgentRun 的观察、诊断与 legacy 控制必须走受控 CLI;不要用裸 `kubectl`、`argo`、`tkn`、`curl` 当正式控制入口。PaC migrated consumer 仍只由 GitHub PR merge 触发,不得因“必须走 CLI”而额外调用写命令。 +- CI/CD、GitOps、rollout、PipelineRun、Argo、git-mirror 和 AgentRun 的观察、诊断与控制必须走受控 CLI;不要用裸 `kubectl`、`argo`、`tkn`、`curl` 当正式控制入口。PaC migrated consumer 只允许 `release plan` 后由 `release trigger --confirm` 发送 webhook。 - CI/CD 校验默认不得阻塞交付: - 除非用户明确要求某项校验成为门禁,禁止新增会让 Pipeline、artifact、GitOps promote、Argo reconcile、runtime rollout 或 `/health` closeout 失败的配置一致性、版本、契约、schema、preflight 或跨对象校验; - 全局配置、非选中 target/consumer、跨 consumer 一致性和版本/契约漂移只输出结构化 typed warning,并固定 `blocking=false`; @@ -262,11 +274,11 @@ bun scripts/cli.ts hwlab nodes control-plane legacy-cicd --help - 当前选中对象缺少渲染必需输入、Secret/权限不成立、目标无法唯一解析或 mutation target 不安全时,仍在 mutation 前 fail-closed;这类损害预防不得扩展成全局配置一致性门禁。 - CI/CD source authority 只能来自 YAML 声明的 Kubernetes 托管 source authority: - legacy lane 由受控命令在 k8s 内同步并创建不可变 `refs/unidesk/snapshots/.../` stage ref; - - Gitea/PaC migrated lane 由 GitHub PR merge 自动驱动 GitHub webhook bridge、Gitea controlled mirror 与 immutable snapshot ref,禁止合并后人工同步或创建 snapshot; + - Gitea/PaC migrated lane 由 GitHub PR merge 更新 GitHub webhook bridge、Gitea controlled mirror 与 immutable snapshot ref,禁止合并后人工同步或创建 snapshot;该 source 更新不触发 PipelineRun; - build/status/publish 只消费对应 snapshot,host worktree、本地 `git fetch/pull`、可变 branch ref 或 Pipeline 内直连 GitHub 都不能作为 authoritative source。 - CLI 必须组合 `config/platform-infra/gitea.yaml` 与 `config/platform-infra/pipelines-as-code.yaml`,按 consumer、node、lane、upstream repository、branch 和 Gitea repository 精确解析 delivery authority。不得用 URL 片段或 repo 专属条件判断迁移;当前选中对象零匹配或多匹配时返回 `unknown`、`mutation=false` 并在 mutation 前 fail-closed,非选中对象和全局一致性错误只进入 `blocking=false` warning。 - PaC 与 `unknown` authority 的 help、plan、status、失败态 `Next`、`REPAIR` 和实际执行 guard 都不得包含或执行 mutation command。`trigger-current|refresh|sync|flush` 只在 YAML 精确解析为 `legacy-manual` 后进入旧实现,且只从 `legacy-cicd` / `legacy-ops` scoped help 发现;平台 bootstrap、Secret 与配置维护使用独立 scoped help,不得充当 source delivery recovery。 -- JD01/NC01 `agentrun--v02`、`sentinel--v03`、`hwlab--v03` 的交付收口由自动链自行完成并写入状态。仅在显式调查自动链故障时读取 `cicd status --node ` 或 `platform-infra pipelines-as-code status|history --target --consumer `;主代理与子代理不得把 `closeout` 当作 PR 合并后的人工步骤。`cicd branch-follower` 和 `cicd gitea-actions-poc` 对这些 consumer 只保留历史/迁移只读用途。 +- JD01/NC01 `agentrun--v02`、`sentinel--v03`、`hwlab--v03` 在手动 webhook 后由 PaC/Tekton/GitOps 自动收敛并写入状态。仅在显式调查发布链故障时读取 `cicd status --node ` 或 `platform-infra pipelines-as-code status|history --target --consumer `;`cicd branch-follower` 和 `cicd gitea-actions-poc` 只保留历史/迁移只读用途。 - PaC `.tekton` 文件必须用 Repository CR 的 target/node 参数隔离 JD01/NC01,避免同一个 Gitea push 在一个 target cluster 内额外创建另一个 target 的 PipelineRun。`history --id` 必须按运行面 provenance 和实际 PipelineRun prefix 唯一归属 consumer;零匹配、多匹配或显式 consumer 不一致均 fail-closed,不得回退到默认 consumer 或默认 node。 - PaC source artifact 必须遵循以下边界: - 由 `config/platform-infra/pipelines-as-code.yaml#consumers[].sourceArtifact` 显式声明; @@ -326,7 +338,7 @@ bun scripts/cli.ts hwlab nodes control-plane legacy-cicd --help - 只有同一 deliveryId 的 exact-after immutable snapshot 与 authority branch 经 atomic push 后重新读取 refs 证明一致,才能进入 `committed`; - 同 deliveryId 与同 payload 必须幂等,异 payload 必须返回 `409`;PVC 不可用或容量满必须返回 `503` 并使 readiness=false; - inbox worker 使用 YAML 声明的有界 backoff 自动重试,重启后恢复 `accepted` / `processing`;Caddy 只可在请求尚未持久化时做小于 10 秒的 body-safe 有界重试。 -- Durable inbox 只是 webhook delivery 的持久接收与重试 journal,不是业务 source/ref 的第二份真相,也不得被 PaC、Tekton、GitOps、Argo 或 runtime 当作源码/read model。最终 source authority 仍只有 Gitea authority branch 与 immutable snapshot。默认只使用 `platform-infra gitea mirror webhook status` 观察 accepted -> committed 分层。会 POST hook test 的 mutation 测试入口已经删除;连通性只能通过真正只读的 status、GET 与 readiness 观察,不能制造伪 push。平台 bootstrap 必须先落入 Git-backed YAML/controller/source,并由正常 PR merge 的自动事件验收;禁止 Gitea -> GitHub 回写、轮询/read-model fallback、Gitea Actions/act_runner fallback 或第二 source authority。 +- Durable inbox 只是 webhook delivery 的持久接收与重试 journal,不是业务 source/ref 的第二份真相,也不得被 PaC、Tekton、GitOps、Argo 或 runtime 当作源码/read model。最终 source authority 仍只有 Gitea authority branch 与 immutable snapshot。默认只使用 `platform-infra gitea mirror webhook status` 观察 accepted -> committed 分层。会 POST hook test 的 mutation 测试入口已经删除;连通性只能通过真正只读的 status、GET 与 readiness 观察。平台 bootstrap 必须先落入 Git-backed YAML/controller/source,并由 PR 合并后的 source authority 状态、手动 plan 和手动 webhook 验收;禁止 Gitea -> GitHub 回写、轮询/read-model fallback、Gitea Actions/act_runner fallback 或第二 source authority。 - Gitea webhook/public-edge 自动链变更的 closeout: - 必须观察修复 PR 合并后的真实 delivery、rollout 和只读 status/GET/readiness 证据; - Secret、ConfigMap 或 public-edge 候选已生成不等于 connector Pod 与公网 route 已加载新配置; @@ -336,8 +348,8 @@ bun scripts/cli.ts hwlab nodes control-plane legacy-cicd --help - `cicd branch-follower` 对当前 PaC consumer 已退役,只保留 `status|events|logs` 与只读 `debug-step` 历史诊断。默认入口、任意 `--config` 和混合/未知 authority 都不能恢复 `apply|run-once|state-write` 等写能力,也不能猜测默认 node。 - k8s 运行面从拉取已构建镜像开始必须 0 Docker;CI 构建面可以使用 YAML 声明的原生构建工具,但不得把 Docker socket、Docker daemon 或 host Docker 带入运行面。 - 正式 CI/CD、publish、image build 和 rollout 必须走 Tekton Task/Pipeline/PipelineRun 承担 CI,并通过 GitOps/Argo 承担部署收敛;普通 Kubernetes Job 只允许用于 bounded helper、source sync、diagnostic、cleanup 或 bootstrap,不得作为正式发布、镜像构建或 rollout 入口。 -- 未迁移的 legacy CI/CD 若仍保留人工入口,必须由单一受控命令完成 source sync、构建、发布、GitOps/Argo 收敛、runtime provenance 校验和 `/health` 端点验证。PaC migrated consumer 不得使用该人工入口,其唯一交付触发是 GitHub PR merge。 -- CI/CD 端到端 wall-clock 目标及预算只由 owning YAML 声明;PaC migrated consumer 从 GitHub PR merge 事件计时,legacy lane 才从操作者触发受控命令计时,均到 runtime ready 且 `/health` 端点验证完成为止。 +- 未迁移的 legacy CI/CD 若仍保留人工入口,必须由单一受控命令完成 source sync、构建、发布、GitOps/Argo 收敛、runtime provenance 校验和 `/health` 端点验证。PaC migrated consumer 不得使用该 legacy 入口,只允许 `release plan` 后执行 `release trigger --confirm`。 +- CI/CD 端到端 wall-clock 目标及预算只由 owning YAML 声明;PaC migrated consumer 从 CLI 手动 webhook 事件计时,legacy lane 从操作者触发受控命令计时,均到 runtime ready 且 `/health` 端点验证完成为止。 - CI/CD validation 阶段只能验证部署对象的 `/health` 端点和必要 provenance;禁止在 CI/CD gate 中运行 web-probe、Playwright、远程浏览器截图、用户路径 E2E 或等价重型业务探针。业务/用户入口验证只能作为发布后的独立 post-deploy validation 证据,不得阻塞 CI/CD 一键交付。 - 产品功能配置 schema 的 CI/CD 合同: - repo 内唯一约定路径固定为 `config/feature-config.schema.json`; @@ -366,9 +378,9 @@ bun scripts/cli.ts hwlab nodes control-plane legacy-cicd --help - 指定 `--consumer` 或可唯一归属的 `--id` 时只读取单 consumer 快路径; - 输出 `config.scope=target-summary|single-consumer`,不得静默退回默认 consumer。 - 退役 branch-follower 排障只允许使用 `state-read|controller-source|status-read|decide`、`status|events|logs` 等只读边界;`state-write`、gate、`run-once` 和自动 loop 不再是当前 PaC consumer 的修复或验收入口。 -- 自动链单步测不通时,应在 Git-backed YAML、controller 或源码中修复根因,并由修复 PR 合并产生的新正常 webhook/PaC 事件复测;不得用 branch-follower 写状态或补跑当前交付。 +- 发布链单步测不通时,应在 Git-backed YAML、planner、controller 或源码中修复根因,并由修复 PR 合并后的新 plan 和手动 webhook/PaC 事件复测;不得用 branch-follower 写状态或补跑当前交付。 - CI/CD 排障中再次踩到已暴露的运行面坑、工具误用、镜像假设或状态可见性缺口时,必须先把长期规则写入本 skill/reference,再继续只读定位。 -- CI/CD 运行面验收必须在目标 NODE/k8s 内计算短摘要;本机只用于源码阅读、编辑和静态合同测试,不能替代正常 PR merge 自动事件产生的目标运行面证据。 +- CI/CD 运行面验收必须在目标 NODE/k8s 内计算短摘要;本机只用于源码阅读、编辑和静态合同测试,不能替代受控手动 webhook 产生的目标运行面证据。 - CI tools image 默认必须包含 `kubectl`: - `kubectl` 与其他构建工具统一由 owning YAML 的 tools image Dockerfile 声明并随镜像交付; - 镜像构建必须执行 `kubectl version --client` 自检; diff --git a/.agents/skills/unidesk-cicd/references/agentrun.md b/.agents/skills/unidesk-cicd/references/agentrun.md index adaea3c7..f847b0c5 100644 --- a/.agents/skills/unidesk-cicd/references/agentrun.md +++ b/.agents/skills/unidesk-cicd/references/agentrun.md @@ -86,7 +86,7 @@ AgentRun YAML-only lane closeout 必须同时看当前 k8s git-mirror source sna ## JD01/NC01 v0.2 Gitea / Pipelines-as-Code lane -JD01/NC01 `agentrun--v02` 已迁移为单一路径:GitHub PR merge -> GitHub webhook -> Gitea controlled mirror -> immutable snapshot -> Gitea webhook -> Pipelines-as-Code -> Tekton -> GitOps/Argo -> k8s runtime。不要保留 Gitea Actions、act_runner、legacy branch-follower 或第二套 trigger fallback。CLI 必须组合 `config/platform-infra/gitea.yaml` 与 `config/platform-infra/pipelines-as-code.yaml` 精确解析 consumer;未知、歧义或 source identity drift 都必须 `mutation=false` 并 fail-closed。 +JD01/NC01 `agentrun--v02` 已迁移为单一路径:GitHub PR merge -> Gitea controlled mirror/immutable snapshot -> CLI release plan -> CLI 手动 PaC webhook -> Tekton -> GitOps/Argo -> k8s runtime。不要保留 Gitea Actions、act_runner、legacy branch-follower 或第二套 trigger fallback。CLI 必须组合 `config/platform-infra/gitea.yaml` 与 `config/platform-infra/pipelines-as-code.yaml` 精确解析 consumer;未知、歧义或 source identity drift 都必须 `mutation=false` 并 fail-closed。 - Gitea 只作为受控 mirror/source authority 和 PaC webhook 源: - 公开 Web UI 是 `https://gitea.hwpod.com`,由 NC01 public-edge 直连 ClusterIP; diff --git a/.agents/skills/unidesk-cicd/references/branch-follower.md b/.agents/skills/unidesk-cicd/references/branch-follower.md index 4719ca4e..ebb446c7 100644 --- a/.agents/skills/unidesk-cicd/references/branch-follower.md +++ b/.agents/skills/unidesk-cicd/references/branch-follower.md @@ -4,7 +4,7 @@ SPEC: PJ2026-01060703 CI/CD branch follower draft-2026-07-03-p0-branch-follower ## 当前边界 -`config/cicd-branch-followers.yaml` 中现有 follower 已全部迁移到 GitHub PR merge 驱动的 Gitea + Pipelines-as-Code 自动链。`cicd branch-follower` 不再是 delivery authority,只保留退役状态和历史对象的只读诊断。 +`config/cicd-branch-followers.yaml` 中现有 follower 已全部迁移到 Gitea source authority + CLI 手动 PaC webhook 发布链。`cicd branch-follower` 不再是 delivery authority,只保留退役状态和历史对象的只读诊断。 默认入口只允许: diff --git a/.agents/skills/unidesk-cicd/references/control-plane.md b/.agents/skills/unidesk-cicd/references/control-plane.md index bd7acc02..081f7ba8 100644 --- a/.agents/skills/unidesk-cicd/references/control-plane.md +++ b/.agents/skills/unidesk-cicd/references/control-plane.md @@ -97,4 +97,4 @@ PipelineRun 失败或长时间未完成时,先按定点 status/history 和 bou 小范围 PR 触发 120s 时必须看 plan artifacts 的 `affectedServices/buildServices/reusedServices`:如果 source diff 很小却出现所有 envreuse 服务都在 `buildServices` 且 `reusedServices=[]`,优先怀疑 current GitOps artifact catalog 没有 hydrate 到 source plan 阶段,而不是继续盲目重跑 PipelineRun。 -Migrated consumer 出现 registry half-state、GitOps 超预算或 runtime 未对齐时,只用 PaC `status|history`、PipelineRun id、只读 sentinel image status 和 bounded logs 下钻。不得把 PaC closeout、GitOps flush、async mutation job 或 consumer 专用 publish 放入 Next;定位后修 owning YAML、controller 或源码,并用新 PR merge 自动事件验收。 +Migrated consumer 出现 registry half-state、GitOps 超预算或 runtime 未对齐时,只用 PaC `status|history`、PipelineRun id、只读 sentinel image status 和 bounded logs 下钻。不得把 PaC closeout、GitOps flush、async mutation job 或 consumer 专用 publish 放入 Next;定位后修 owning YAML、planner、controller 或源码,并用新 PR merge 后的 plan 与手动 webhook 验收。 diff --git a/.agents/skills/unidesk-cicd/references/git-mirror.md b/.agents/skills/unidesk-cicd/references/git-mirror.md index 967edcf9..58787aaa 100644 --- a/.agents/skills/unidesk-cicd/references/git-mirror.md +++ b/.agents/skills/unidesk-cicd/references/git-mirror.md @@ -2,7 +2,7 @@ ## 默认只读入口 -Gitea/PaC migrated consumer 的 mirror、authority branch、immutable snapshot 与 GitOps publication 全由 GitHub PR merge 自动链拥有。默认只允许读取: +Gitea/PaC migrated consumer 的 mirror、authority branch 与 immutable snapshot 由 GitHub PR merge 更新;GitOps publication 只在手动 plan/trigger 后发生。默认只允许读取: ```bash bun scripts/cli.ts platform-infra gitea mirror status --target @@ -13,7 +13,7 @@ bun scripts/cli.ts agentrun git-mirror status --node --lane 已迁移或 `unknown` authority 必须在远端调用或异步 Job 创建前 fail-closed。状态和失败 `Next` 只能指向 status/history 与自动链修复引用,不得建议 mirror sync/flush、直接 Gitea push、host git、fixed workspace 或第二套 source resolver。 -自动链故障必须修 `config/platform-infra/gitea.yaml`、`config/platform-infra/pipelines-as-code.yaml`、webhook bridge、受控 mirror worker 或源码,并通过修复 PR 合并产生的新正常事件验收。不得使用 branch-follower、人工 PipelineRun 或 legacy mirror mutation 补齐当前交付。 +发布链故障必须修 `config/platform-infra/gitea.yaml`、`config/platform-infra/pipelines-as-code.yaml`、planner、webhook bridge、受控 mirror worker 或源码,并通过修复 PR 合并后的 plan 与手动 webhook 验收。不得使用 branch-follower、裸建 PipelineRun 或 legacy mirror mutation 补齐当前交付。 ## Legacy scope diff --git a/.agents/skills/unidesk-cicd/references/gitea-pac.md b/.agents/skills/unidesk-cicd/references/gitea-pac.md index aa9aee58..5c483cea 100644 --- a/.agents/skills/unidesk-cicd/references/gitea-pac.md +++ b/.agents/skills/unidesk-cicd/references/gitea-pac.md @@ -3,12 +3,12 @@ Gitea/Pipelines-as-Code 迁移后的 CI/CD 只有一条正式路径: ```text -GitHub PR merge -> GitHub webhook bridge -> Gitea controlled mirror and immutable snapshot refs -> Gitea Repository webhook -> Pipelines-as-Code -> Tekton PipelineRun -> GitOps/Argo -> k8s runtime +GitHub PR merge -> GitHub webhook bridge -> Gitea controlled mirror and immutable snapshot refs -> CLI release plan -> CLI manual PaC webhook -> Tekton PipelineRun -> GitOps/Argo -> k8s runtime ``` -GitHub 是唯一上游写入权威。目标 source branch 的 GitHub PR merge 是唯一正式交付触发事件;合并后必须由 webhook、mirror、PaC、Tekton、GitOps/Argo 自动推进到运行面,无需主代理、子代理或操作者执行任何 apply、closeout、trigger、sync、flush 或补跑。状态与历史命令只能观察和诊断,不能推进交付。 +GitHub 是唯一上游写入权威。目标 source branch 的 GitHub PR merge 只更新 Gitea source authority,不创建 PipelineRun。操作者必须先执行 `release plan`,审阅本次请求范围、累计有效范围、env reuse、镜像构建和 rollout;范围准确后再执行 `release trigger --confirm`,由 CLI 手动发送 PaC webhook。禁止恢复 Gitea 自动 hook、直接创建 PipelineRun 或用 apply、sync、flush 补跑。 -自动链任一环节不通时,任务目标是修复该环节的 YAML、controller 或源码,并以修复 PR 合并后产生的新自动链事件作为验收证据。禁止用 Gitea Actions、`act_runner`、branch-follower、host worktree、Pipeline 内直连 GitHub、直接 Gitea push、人工 mirror sync、人工 PipelineRun、`trigger-current`、伪 push、hook test 或自定义脚本补齐当前交付。`config/cicd-gitea-actions-poc.yaml` 只保留只读历史材料。会 POST Gitea hook `/tests` 的 `webhook-test` 已从 Gitea source-authority 与 PaC 公共 CLI、远端执行器移除,因为它会制造交付事件,不是只读连通性诊断。 +发布链任一环节不通时,任务目标是修复该环节的 YAML、controller、planner 或源码,并重新执行 plan;范围准确后才允许手动 webhook 验收。禁止用 Gitea Actions、`act_runner`、branch-follower、host worktree、Pipeline 内直连 GitHub、直接 Gitea push、人工 mirror sync、裸建 PipelineRun、`trigger-current`、hook test 或自定义脚本补齐当前交付。`config/cicd-gitea-actions-poc.yaml` 只保留只读历史材料。 ## Webhook bridge 自举保护 @@ -78,9 +78,10 @@ bun scripts/cli.ts platform-infra pipelines-as-code bootstrap --target -- - YAML 零匹配或多匹配时在本地拒绝,不猜测 repository; - `gitea mirror bootstrap` 未带 `--confirm` 时输出可读 dry-run 与精确 confirm 命令,不再返回空白拒绝。 - 完成 `bootstrap --confirm` 后合并 source PR: - - GitHub PR merge 是唯一 delivery 触发; - - 不再调用 bootstrap、apply、mirror sync、PipelineRun、Argo refresh 或 closeout; - - 正常链路不串联 mirror status、webhook status、PaC status 与 history。 + - GitHub PR merge 只更新 source authority; + - 执行 `release plan` 并审阅范围; + - plan 未扩大时执行 `release trigger --confirm`; + - 不再调用 bootstrap、apply、mirror sync、裸建 PipelineRun、Argo refresh 或 closeout。 - 只有出现自动链故障或用户明确要求验收时才读取状态: - 首选一次 `cicd status --node ` 获取 node 摘要; - 只对失败 consumer 使用一次 `pipelines-as-code status --consumer `; @@ -180,7 +181,7 @@ bun scripts/cli.ts platform-infra pipelines-as-code source-artifact verify-runti - consumer 声明 `sourceArtifact` 时,在最终 cache-hit commit 建立 clean exact-commit worktree; - 只执行一次 `source-artifact verify-runtime`,完成 artifact、embedded PipelineRun 与 provenance 对齐。 - 禁止项: - - 整个流程只使用两次正常 PR merge 自动事件; + - 两个样本分别使用一次 PR merge、一次 plan 和一次手动 webhook; - 不创建人工 PipelineRun; - 不执行 mirror sync、Argo sync、补链或第二触发。 @@ -188,7 +189,7 @@ bun scripts/cli.ts platform-infra pipelines-as-code source-artifact verify-runti ## 按需只读调查 -自动链必须在没有任何操作者命令的情况下完成交付并记录 terminal 状态。以下入口只在显式调查自动链故障时按需使用,不是 PR 合并后的默认顺序、交付前置或“下一步”: +手动 webhook 发出后,PaC、Tekton、GitOps/Argo 必须自动完成后续交付并记录 terminal 状态。以下入口只在显式调查发布链故障时按需使用: - `bun scripts/cli.ts platform-infra gitea mirror status --target ` - `bun scripts/cli.ts platform-infra gitea mirror webhook status --target ` diff --git a/.agents/skills/unidesk-cicd/references/incident-recovery.md b/.agents/skills/unidesk-cicd/references/incident-recovery.md index 89cc1595..f98ed571 100644 --- a/.agents/skills/unidesk-cicd/references/incident-recovery.md +++ b/.agents/skills/unidesk-cicd/references/incident-recovery.md @@ -83,7 +83,7 @@ ## 恢复验收 -- 回退 PR 合并后,只观察该合并产生的新正常自动事件: +- 回退 PR 合并后,先执行 release plan;范围准确时手动 trigger,再观察新 PaC 事件: - webhook/Gitea delivery 已提交; - PaC 外层 PipelineRun 成功; - Tekton terminal roles 成功; @@ -271,7 +271,7 @@ - CI/CD OTel span/event 记录 source commit、PipelineRun、GitOps commit、Argo revision、 runtime digest、阶段时间和对象压力 warning; - exporter 或 provenance 漂移只标记 evidence gap,禁止改变业务成功终态。 -- 长效修复必须由正常 PR 自动事件验收: +- 长效修复必须由 PR 合并后的 plan 与手动 webhook 事件验收: - 新 PipelineRun 成功且对象显著减小; - GitOps commit、Argo revision、runtime digest 与 source identity 对齐; - PipelineRun 执行和后续 compaction 窗口内 node、API、lease、kubelet 与业务 endpoint 保持健康; diff --git a/.agents/skills/unidesk-cicd/references/platform-ops.md b/.agents/skills/unidesk-cicd/references/platform-ops.md index 1bca3e21..5ad5bf3d 100644 --- a/.agents/skills/unidesk-cicd/references/platform-ops.md +++ b/.agents/skills/unidesk-cicd/references/platform-ops.md @@ -69,7 +69,7 @@ bun scripts/cli.ts platform-infra wechat-archive wcf-host-status|collector-plan| - 其他平台服务继续遵循各自 owning YAML 与专项 skill。 - Gitea mirror 和 Pipelines-as-Code 的 source-of-truth: - 分别为 `config/platform-infra/gitea.yaml` 与 `config/platform-infra/pipelines-as-code.yaml`; - - migrated consumer 的唯一交付触发是 GitHub PR merge; + - migrated consumer 的唯一交付触发是 `release plan` 审阅后由 CLI 手动发送 PaC webhook; - 默认入口只有 Gitea/PaC status、history 与只读 debug。 - 平台维护入口边界: - `closeout` 只在 PaC compatibility diagnostics scope 中保留为只读历史观察; @@ -118,7 +118,7 @@ bun scripts/cli.ts hwlab g14 control-plane cleanup-released-pvs \ - 大型 PipelineRun 造成 Kine/SQLite 压力时: - cleanup 只属于控制面快速缓解; - - 长期修复必须消除 renderer 中重复内联的 `taskSpec`,并通过正常 PR 自动事件验收; + - 长期修复必须消除 renderer 中重复内联的 `taskSpec`,并通过 PR 合并后的 plan 与手动 webhook 验收; - 根因判定、k3s 紧急恢复和防复发规则见 [incident-recovery.md](incident-recovery.md#tekton-大对象与-kinesqlite-控制面退化)。 ## Rollout 记录 diff --git a/.agents/skills/unidesk-cicd/references/pr-monitor.md b/.agents/skills/unidesk-cicd/references/pr-monitor.md index 5d99aedc..5bcd30ea 100644 --- a/.agents/skills/unidesk-cicd/references/pr-monitor.md +++ b/.agents/skills/unidesk-cicd/references/pr-monitor.md @@ -26,6 +26,6 @@ bun scripts/cli.ts hwlab g14 monitor-prs --lane v02 [--once] [--dry-run] bun scripts/cli.ts hwlab g14 monitor-prs --lane v03 [--once] [--dry-run] ``` -只监控 base=`v0.3` 的 PR。Ready PR 可经 UniDesk `gh pr merge` 合并;合并本身是 PaC 唯一交付触发。Monitor 之后只能读取 PaC status/history、Argo、runtime `/health` 与 provenance,并在自动链失败或超时时创建/更新 failure issue;不得触发 runtime CD、Git mirror flush、人工 PipelineRun 或其他补跑。 +只监控 base=`v0.3` 的 PR。Ready PR 可经 UniDesk `gh pr merge` 合并;合并只更新 source authority,不创建 PipelineRun。需要 L2/L3 发布时必须另行执行 `release plan`,范围准确后再由 CLI 手动发送 PaC webhook;Monitor 不得自行触发发布。 CI/CD validation 只允许使用部署对象的 `/health` 端点和必要 provenance;禁止在 CI/CD gate 中运行 web-probe、Playwright、远程浏览器截图或用户路径 E2E。public health probe 必须使用 `config/hwlab-node-lanes.yaml` 选中 node/lane 的 formal public URL;裸 IP、FRP 端口和 legacy 端口只作为边缘诊断证据,不能作为 CI/CD 验收口径。 diff --git a/.agents/skills/unidesk-devlevel/SKILL.md b/.agents/skills/unidesk-devlevel/SKILL.md index 437d6592..7a883d81 100644 --- a/.agents/skills/unidesk-devlevel/SKILL.md +++ b/.agents/skills/unidesk-devlevel/SKILL.md @@ -101,7 +101,8 @@ description: >- ## L2 Development -- 通过项目正常 CI/CD 把目标版本滚动到开发集群。 +- 通过项目受控手动 CI/CD 把目标版本滚动到开发集群。 +- PR merge、push 和 branch update 不自动进入 L2;先通过 `$unidesk-cicd` 的 release plan 审阅 env reuse、镜像构建数和范围,再手动发送 PaC webhook。 - 代理可以根据功能和问题是否需要开发集群真实运行面,自行选择并执行 L2。 - CLI 显式使用开发集群 `--over-api`,访问 dev K8s API 和 Worker。 - Web 使用 `$unidesk-webdev` 与 owning YAML 选择的 development semantic origin。 @@ -110,7 +111,8 @@ description: >- ## L3 Production -- 通过项目正式发布方式把目标版本部署到生产集群。 +- 通过项目受控手动发布方式把目标版本部署到生产集群。 +- L3 同样先 plan 后手动发送 PaC webhook,禁止 PR merge 自动发布。 - CLI 使用生产 `--over-api`,访问 prod K8s API 和 Worker。 - Web 使用 `$unidesk-webdev` 与 owning YAML 选择的 production semantic origin。 - 适合生产发布、生产环境问题复现和生产入口检查。 diff --git a/docs/reference/platform-infra.md b/docs/reference/platform-infra.md index da2328fa..dbe2633d 100644 --- a/docs/reference/platform-infra.md +++ b/docs/reference/platform-infra.md @@ -195,16 +195,16 @@ - `config/platform-infra/pipelines-as-code.yaml` 拥有 PaC Repository 与 consumer 配置; - repo URL、snapshot、webhook、token sourceRef 和 PaC params 不得隐藏在代码特例中。 - Delivery authority resolver 必须组合上述两份 YAML,并按 consumer id、node、lane、upstream repository、branch 和 Gitea repository 精确关联。不得通过 URL 片段、repo 名称前缀或仓库专属 `if` 推断迁移状态。零匹配、多匹配和配置错误都必须返回 `unknown`、`mutation=false` 并停止生成或执行写操作。 -- PaC consumer 的唯一正式触发是目标 source branch 的 GitHub PR merge。完整自动链固定为:GitHub PR merge -> GitHub webhook -> Gitea controlled mirror -> immutable snapshot -> Gitea webhook -> PaC/Pipelines-as-Code -> Tekton -> GitOps/Argo -> runtime 运行面。 +- PaC consumer 的唯一正式触发是 CLI 在 `release plan` 审阅后手动发送的 PaC webhook。GitHub PR merge 只更新 Gitea controlled mirror 与 immutable snapshot,不创建 PipelineRun;后续链路固定为 Tekton -> GitOps/Argo -> runtime 运行面。 - PR 合并后的交付边界: - 主代理、子代理和操作者不得用 `apply`、`closeout`、`trigger-current`、 `refresh`、`sync`、`flush`、`webhook-test`、人工 PipelineRun、 直接 Gitea push、本地 mirror 写入或其他补跑完成当前交付; - 自动链不通时必须修 owning YAML、controller 或源码, - 并只用修复 PR 合并产生的新正常自动事件验收; + 并只用修复 PR 合并后重新 plan、手动 trigger 产生的新事件验收; - 必要的运行面调试或临时恢复可以走 `$unidesk-daddev` P2 最小可逆 patch; - patch 不得修改 source/ref authority、制造交付成功或替代终态; - - 成立的变化必须写回 owning YAML/源码并经正常 PR 自动交付; + - 成立的变化必须写回 owning YAML/源码,经正常 PR 合并后重新 plan,并通过受控手动 webhook 交付; - 临时 patch 随后撤销或由声明式交付覆盖。 - Migrated consumer 的默认 help、status、`Next` 与 `REPAIR` 只能给 `status`、`history`、`events`、`logs`、只读单步下钻以及本节稳定引用。CLI 必须省略 mutation command,而不是只给命令加警告文字。`unknown` authority 同样 fail-closed。 - 旧 `trigger-current`、`refresh` 和 mirror `sync|flush` 只允许在 owning YAML 明确解析为 `legacy-manual` 后执行,并且只在显式 `legacy-cicd` 或 `legacy-ops` scoped help 中可发现。平台 bootstrap、Secret 与配置维护属于独立职责,只在 `platform-bootstrap` 或 `platform-maintenance` scoped help 中展示,不能作为 source delivery recovery。 @@ -216,10 +216,11 @@ - 默认 PaC 入口是 `bun scripts/cli.ts platform-infra pipelines-as-code status|history --target `。`status` 应显示 webhook、PipelineRun/TaskRun duration、image status、env identity、digest、GitOps commit、Argo revision 和 runtime provenance;`history` 必须读取 target node 上的实时对象、显式报告 read error,并支持 consumer 与 PipelineRun id 下钻。`history --id` 必须通过运行面 provenance 与 PipelineRun prefix 唯一解析实际 consumer;零匹配、多匹配或显式 consumer 不一致都 fail-closed,不能回退到默认 consumer 或默认 node。 - 最新一次端到端耗时使用 `bun scripts/cli.ts platform-infra pipelines-as-code delivery-timing --target --consumer ` 一次查询。命令从两份 owning YAML 解析 source repository、branch 与 consumer,按 PipelineRun source commit 关联 GitHub PR `mergedAt`,并投影 trigger wait、PipelineRun、端到端耗时、Argo/runtime closeout 与证据缺口;缺失关联时保持 `partial/unknown`,不得使用 commit 时间冒充 `mergedAt`,且始终 `mutation=false`。 - Sentinel 内部 publish capability 由 `config/platform-infra/pipelines-as-code.yaml#capabilities.sentinelInternalPublish` 单一拥有,默认 `enabled=false`。Pod env、ownerReferences、ServiceAccount、label 与 annotation 不能证明 creator;#1769 的 admission-owned provenance 与最小权限边界落地前不得启用。`.tekton` 继续走既有自动 `publish-current`,不能退化成人工 publish。 -- PaC 观察必须把 PipelineRun 分类为外层 `outer-pac-event`、历史内层 `inner-deterministic-publish` 或 `unknown`。只有唯一外层 PaC push event 能驱动 latest、status、debug-step 与兼容 closeout;Web sentinel 的 source、image、artifact、GitOps 步骤必须直接位于该 outer PipelineRun,禁止再创建 nested PipelineRun。历史内层运行只作为未绑定执行观察,缺少 admission-owned 父子证据时标记 `unproven`,禁止按名称前缀推断关系。 +- PaC 观察必须把 PipelineRun 分类为外层 `outer-pac-event`、历史内层 `inner-deterministic-publish` 或 `unknown`。只有由受控 CLI 在 release plan 审阅后手动发送的外层 PaC push event 能驱动 latest、status、debug-step 与兼容 closeout;PR merge、branch update 和 Gitea 自动 hook 不得发送该事件。Web sentinel 的 source、image、artifact、GitOps 步骤必须直接位于该 outer PipelineRun,禁止再创建 nested PipelineRun。历史内层运行只作为未绑定执行观察,缺少 admission-owned 父子证据时标记 `unproven`,禁止按名称前缀推断关系。 - CI/CD OTel endpoint、service/resource 属性、采样与 exporter timeout/failure policy 由 `config/platform-infra/pipelines-as-code.yaml` 引用现有 `config/platform-infra/observability.yaml` 并负责渲染。outer PaC 在最早可控 step 生成 W3C trace context,后续 source、image、artifact、GitOps 与 helper 复用同一 traceId;export 失败或超时只输出 `warning=true`、`blocking=false` 的有界事件,不改变 PipelineRun、TaskRun 或业务 step 终态。 - `config/platform-infra/pipelines-as-code.yaml` 可以声明多个 repository 和 consumer。`cicd status --node ` 用于 node 汇总,`history --target ` 用于全 consumer 审计,`status --target --consumer ` 用于 consumer 只读状态;不得跨 repository 混合 PipelineRun 或 env reuse 证据。 - PaC Repository CR `spec.url` 必须匹配 Gitea webhook payload 的 public repository URL;ClusterIP/service URL 只属于 `cloneUrl` 与 `params.git_read_url`。Repository params 必须使用该 consumer 的 node-specific snapshot prefix。 +- L2/L3 发布固定使用 `platform-infra pipelines-as-code release plan` 后再 `release trigger --confirm`。plan 必须分别显示本次 base 到 source 的请求范围、基于当前 GitOps catalog 的累计有效范围、env reuse、镜像构建数量与列表、rollout 服务和全部影响范围;trigger 只发送 PaC webhook,由 PaC 创建 PipelineRun。有效范围大于请求范围时必须标记 `expanded`,trigger 在发送 webhook 前拒绝;先修复 source range、planner、源码或 owning YAML,不得先触发后依赖资源限流兜底。 - 私有 Gitea source checkout 必须复用 PaC Repository 已声明的 Secret: - source-artifact binding 从 repository 直接投影 `secretName` 与 `tokenKey`,不得在 consumer params 或业务 owning YAML 重复声明; - Tekton step 通过 `secretKeyRef` 和临时 `GIT_ASKPASS` 读取 token,禁止把 token 拼入 Git URL、PipelineRun 参数或日志; diff --git a/scripts/src/agentrun/control-plane.ts b/scripts/src/agentrun/control-plane.ts index 01f21fe8..9cb52986 100644 --- a/scripts/src/agentrun/control-plane.ts +++ b/scripts/src/agentrun/control-plane.ts @@ -34,7 +34,7 @@ import { } from "../agentrun-manifests"; import { sha256Fingerprint } from "../platform-infra-ops-library"; import { - pacAutomaticDeliveryFix, + pacManualReleaseFix, pacReadOnlyNext, resolveCicdDeliveryAuthority, type CicdDeliveryAuthority, @@ -305,7 +305,7 @@ export async function controlPlanePlan(_config: UniDeskConfig, options: StatusOp ? { status: statusCommand, history: `bun scripts/cli.ts platform-infra pipelines-as-code history --target ${spec.nodeId} --limit 10`, - fixAutomaticDelivery: pacAutomaticDeliveryFix(deliveryAuthority), + fixManualRelease: pacManualReleaseFix(deliveryAuthority), valuesPrinted: false, } : { @@ -456,13 +456,13 @@ export async function statusPipelinesAsCodeLane(config: UniDeskConfig, options: }; const nextAction = aligned ? { code: "none", summary: "PaC/Gitea migrated lane aligned; no action required", command: null } - : { code: "inspect-automatic-delivery", summary: `PaC/Gitea automatic delivery is blocked: ${blockers.join(", ")}`, command: pacStatusCommand(spec, consumerId, true) }; + : { code: "inspect-manual-release", summary: `PaC/Gitea manual release is blocked: ${blockers.join(", ")}`, command: pacStatusCommand(spec, consumerId, true) }; const migration = { migrated: true, sourceAuthority: "gitea-pipelines-as-code", consumerId, configRefs: authority.consumer.configRefs, - triggerPath: "GitHub PR merge -> GitHub webhook -> Gitea mirror/snapshot -> Gitea webhook -> Pipelines-as-Code -> Tekton -> GitOps/Argo -> k8s runtime", + triggerPath: "GitHub PR merge -> Gitea mirror/snapshot -> CLI release plan -> CLI manual PaC webhook -> Tekton -> GitOps/Argo -> k8s runtime", legacyGitMirrorDisposition: "migration-readonly", legacyTriggerCurrentDisabled: true, valuesPrinted: false, @@ -562,7 +562,7 @@ export async function statusPipelinesAsCodeLane(config: UniDeskConfig, options: pacStatus: pacStatusCommand(spec, consumerId), pacHistory: record(pacReadOnlyNext(spec.nodeId, consumerId)).history, giteaMirrorStatus: giteaMirrorStatusCommand(spec), - fixAutomaticDelivery: pacAutomaticDeliveryFix(authority), + fixManualRelease: pacManualReleaseFix(authority), }, valuesPrinted: false, }; @@ -623,7 +623,7 @@ function unknownDeliveryAuthorityStatus(options: StatusOptions, target: { config }, next: { statusFull, - fixAutomaticDelivery: pacAutomaticDeliveryFix(authority), + fixManualRelease: pacManualReleaseFix(authority), }, valuesPrinted: false, }; diff --git a/scripts/src/agentrun/entry.ts b/scripts/src/agentrun/entry.ts index 411562cb..7041e48f 100644 --- a/scripts/src/agentrun/entry.ts +++ b/scripts/src/agentrun/entry.ts @@ -104,7 +104,7 @@ export function agentRunHelp(): unknown { resources: ["task/qt", "attempt", "run", "command/cmd", "runnerjob/rjob", "session/ses", "aipodspec/aps"], description: "Operate AgentRun through Kubernetes-style resource verbs. Human output is compact by default; -o json|yaml returns the UniDesk render-only client schema, --raw exposes the REST envelope, and target-trans uses the selected Target k3s service proxy plus a host directory probe for Artificer target tasks.", legacyCompatibility: "queue/runs/commands/runner/sessions/aipod-specs remain compatibility groups; new commander work should use get/describe/events/logs/result/ack/cancel/retry/dispatch/create/apply/send. sessions turn/steer are removed; use send only.", - cicdBoundary: "NC01 PaC consumer 仅由 GitHub PR merge 自动触发;默认帮助只给 status/history。legacy 与平台维护写入口只在显式 scoped help 中展示。", + cicdBoundary: "NC01 PaC consumer 的 PR merge 只更新 source authority;发布必须先 release plan,再由 CLI 手动发送 PaC webhook。legacy 与平台维护写入口只在显式 scoped help 中展示。", }; } @@ -367,7 +367,7 @@ export function agentRunHelpText(args: string[]): string { "Scoped maintenance help:", " bun scripts/cli.ts agentrun control-plane platform-maintenance --help", " bun scripts/cli.ts agentrun control-plane legacy-cicd --help", - "PaC migrated consumer 的唯一触发是 GitHub PR merge;自动链故障只用 status/history 调查并修 owning YAML/controller/source。", + "PaC migrated consumer 的唯一触发是 release plan 后由 CLI 手动发送 PaC webhook;发布链故障先用 status/history 调查,再修 owning YAML/planner/controller/source。", ].join("\n"); } if (verb === "provider-profile") return providerProfileHelp(); diff --git a/scripts/src/agentrun/git-mirror.ts b/scripts/src/agentrun/git-mirror.ts index b402da07..d994ce52 100644 --- a/scripts/src/agentrun/git-mirror.ts +++ b/scripts/src/agentrun/git-mirror.ts @@ -31,7 +31,7 @@ import { type AgentRunArtifactService, } from "../agentrun-manifests"; import { sha256Fingerprint } from "../platform-infra-ops-library"; -import { pacAutomaticDeliveryFix, resolveCicdDeliveryAuthority } from "../cicd-delivery-authority"; +import { pacManualReleaseFix, resolveCicdDeliveryAuthority } from "../cicd-delivery-authority"; import type { GitMirrorStatusOptions } from "./options"; import { readGitMirrorStatus } from "./rest-bridge"; @@ -548,7 +548,7 @@ export function renderGitMirrorStatusEnvelope( : deliveryAuthority.kind === "unknown" ? { status: `bun scripts/cli.ts agentrun control-plane status --node ${spec.nodeId} --lane ${spec.lane}`, - ...(expanded ? { fixAutomaticDelivery: pacAutomaticDeliveryFix(deliveryAuthority) } : {}), + ...(expanded ? { fixManualRelease: pacManualReleaseFix(deliveryAuthority) } : {}), valuesPrinted: false, } : { diff --git a/scripts/src/cicd-branch-follower-authority.ts b/scripts/src/cicd-branch-follower-authority.ts index dfdc2ca7..45e3fd68 100644 --- a/scripts/src/cicd-branch-follower-authority.ts +++ b/scripts/src/cicd-branch-follower-authority.ts @@ -1,6 +1,6 @@ // Responsibility: fail-closed delivery authority and safe navigation for the retired branch-follower surface. import { - pacAutomaticDeliveryFix, + pacManualReleaseFix, pacNodeReadOnlyNext, resolveCicdDeliveryAuthority, type CicdDeliveryAuthority, @@ -115,8 +115,8 @@ export function branchFollowerReadOnlyNext( status: `bun scripts/cli.ts cicd branch-follower status${suffix}`, events: `bun scripts/cli.ts cicd branch-follower events${suffix}`, logs: `bun scripts/cli.ts cicd branch-follower logs${suffix}`, - fixAutomaticDelivery: authority.kind === "unknown" - ? pacAutomaticDeliveryFix(authorityFromUnknown(authority)) + fixManualRelease: authority.kind === "unknown" + ? pacManualReleaseFix(authorityFromUnknown(authority)) : { reference: "docs/reference/platform-infra.md#gitea-与-pipelines-as-code-边界", mutation: false }, valuesPrinted: false, }; diff --git a/scripts/src/cicd-branch-follower.ts b/scripts/src/cicd-branch-follower.ts index 7e131522..ebbdb26f 100644 --- a/scripts/src/cicd-branch-follower.ts +++ b/scripts/src/cicd-branch-follower.ts @@ -135,7 +135,7 @@ function branchFollowerConfigMutationGuard(options: ParsedOptions): Record { status: `bun scripts/cli.ts cicd branch-follower status --follower ${follower.id}`, events: `bun scripts/cli.ts cicd branch-follower events --follower ${follower.id}`, logs: `bun scripts/cli.ts cicd branch-follower logs --follower ${follower.id}`, - fixAutomaticDelivery: "docs/reference/platform-infra.md#gitea-与-pipelines-as-code-边界", + fixManualRelease: "docs/reference/platform-infra.md#gitea-与-pipelines-as-code-边界", }; if (follower.nativeStatus.tekton !== null) next.pipelineRuns = `bun scripts/cli.ts cicd branch-follower events --follower ${follower.id}`; if (follower.nativeStatus.argo !== null) next.argoApplication = `bun scripts/cli.ts cicd branch-follower events --follower ${follower.id}`; diff --git a/scripts/src/cicd-delivery-authority.test.ts b/scripts/src/cicd-delivery-authority.test.ts index 30b347d8..f9dc8f28 100644 --- a/scripts/src/cicd-delivery-authority.test.ts +++ b/scripts/src/cicd-delivery-authority.test.ts @@ -148,9 +148,9 @@ describe("YAML 组合的 CI/CD delivery authority", () => { })).toMatchObject({ allowed: true }); }); - test("只读 Next 只有 status/history 和稳定自动链修复引用", () => { + test("只读 Next 只有 status/history 和稳定手动发布修复引用", () => { const next = pacReadOnlyNext("NC01", "hwlab-nc01-v03"); - expect(Object.keys(next).sort()).toEqual(["fixAutomaticDelivery", "history", "status", "valuesPrinted"]); + expect(Object.keys(next).sort()).toEqual(["fixManualRelease", "history", "status", "valuesPrinted"]); expect(JSON.stringify(next)).not.toMatch(mutationCommand); expect(PAC_AUTOMATIC_DELIVERY_REFERENCE).toBe("docs/reference/platform-infra.md#gitea-与-pipelines-as-code-边界"); expect(PAC_AUTOMATIC_DELIVERY_REFERENCE).not.toContain("issues/"); @@ -201,7 +201,7 @@ describe("migrated CLI 执行 guard 与提示清理", () => { expect(result).toMatchObject({ ok: false, mode: "pac-pr-merge-mutation-blocked", mutation: false }); expect(Object.keys(result.next as Record).sort()).toEqual([ "controlPlaneStatus", - "fixAutomaticDelivery", + "fixManualRelease", "history", "pacHistory", "pacStatus", @@ -507,7 +507,7 @@ describe("migrated CLI 执行 guard 与提示清理", () => { ] as Record[]; for (const result of results) { const next = result.next as Record; - expect(Object.keys(next).sort()).toEqual(["fixAutomaticDelivery", "history", "status", "valuesPrinted"]); + expect(Object.keys(next).sort()).toEqual(["fixManualRelease", "history", "status", "valuesPrinted"]); expect(JSON.stringify(next)).not.toMatch(mutationCommand); } }); @@ -601,7 +601,7 @@ describe("migrated CLI 执行 guard 与提示清理", () => { const next = { status: "bun scripts/cli.ts web-probe sentinel control-plane status --node NC01 --lane v03", history: "bun scripts/cli.ts platform-infra pipelines-as-code history --target NC01 --consumer sentinel-nc01-v03", - fixAutomaticDelivery: PAC_AUTOMATIC_DELIVERY_REFERENCE, + fixManualRelease: PAC_AUTOMATIC_DELIVERY_REFERENCE, }; for (const renderedText of [ renderPublishCurrentResult({ command: "fixture publish", ok: false, next }), @@ -609,7 +609,7 @@ describe("migrated CLI 执行 guard 与提示清理", () => { ]) { expect(renderedText).toContain("status:"); expect(renderedText).toContain("history:"); - expect(renderedText).toContain("fix-automatic-delivery:"); + expect(renderedText).toContain("fix-manual-release:"); expect(renderedText).not.toMatch(/pac-closeout:|manual-recovery:|trigger-current:|apply:|sync:|flush:/u); } }); @@ -636,7 +636,7 @@ describe("migrated CLI 执行 guard 与提示清理", () => { mode: "noncanonical-branch-follower-config-mutation-blocked", }); expect(Object.keys((JSON.parse(forged.renderedText) as { next: Record }).next).sort()).toEqual([ - "fixAutomaticDelivery", + "fixManualRelease", "status", "valuesPrinted", ]); @@ -654,7 +654,7 @@ describe("migrated CLI 执行 guard 与提示清理", () => { } as FollowerSpec], } as BranchFollowerRegistry; const unknownNext = branchFollowerReadOnlyNext(unknownRegistry, options); - expect(Object.keys(unknownNext).sort()).toEqual(["events", "fixAutomaticDelivery", "logs", "status", "valuesPrinted"]); + expect(Object.keys(unknownNext).sort()).toEqual(["events", "fixManualRelease", "logs", "status", "valuesPrinted"]); expect(JSON.stringify(unknownNext)).not.toContain("JD01"); const catalog = readCicdDeliveryAuthorityCatalog(); @@ -669,7 +669,7 @@ describe("migrated CLI 执行 guard 与提示清理", () => { } as FollowerSpec; }); const multiNodeNext = branchFollowerReadOnlyNext({ followers: consumers } as BranchFollowerRegistry, options); - expect(Object.keys(multiNodeNext).sort()).toEqual(["events", "fixAutomaticDelivery", "logs", "status", "valuesPrinted"]); + expect(Object.keys(multiNodeNext).sort()).toEqual(["events", "fixManualRelease", "logs", "status", "valuesPrinted"]); expect(JSON.stringify(multiNodeNext)).not.toContain("--target JD01"); }); diff --git a/scripts/src/cicd-delivery-authority.ts b/scripts/src/cicd-delivery-authority.ts index b5865220..3db3cc8d 100644 --- a/scripts/src/cicd-delivery-authority.ts +++ b/scripts/src/cicd-delivery-authority.ts @@ -268,7 +268,7 @@ export function decideCicdDeliveryMutation(authority: CicdDeliveryAuthority, con ? pacReadOnlyNext(authority.consumer.node, authority.consumer.consumerId) : { status: context.statusCommand, - fixAutomaticDelivery: pacAutomaticDeliveryFix(authority), + fixManualRelease: pacManualReleaseFix(authority), valuesPrinted: false, }; return { @@ -283,7 +283,7 @@ export function decideCicdDeliveryMutation(authority: CicdDeliveryAuthority, con node: context.node, lane: context.lane, reason: authority.kind === "pac-pr-merge" - ? "该 lane 已迁移到 PaC;GitHub PR merge 是唯一交付触发,CLI 不执行人工推进。" + ? "该 lane 已迁移到 PaC 手动发布;GitHub PR merge 只更新 source authority,请使用 release plan 和 release trigger。" : "无法从 owning YAML 唯一确认 delivery authority;CLI 已 fail-closed。", deliveryAuthority: authority, next, @@ -305,16 +305,16 @@ export function gitRepositoryIdentity(remoteUrl: string): string | null { } } -export function pacAutomaticDeliveryFix(authority: CicdDeliveryAuthority): Record { +export function pacManualReleaseFix(authority: CicdDeliveryAuthority): Record { const configRefs = authority.kind === "pac-pr-merge" ? authority.consumer.configRefs : authority.kind === "unknown" ? authority.configRefs : [authority.declaredConfigRef]; - return automaticDeliveryFix(configRefs, authority.kind === "unknown" ? "fix-delivery-authority" : "fix-automatic-delivery"); + return manualReleaseFix(configRefs, authority.kind === "unknown" ? "fix-delivery-authority" : "fix-manual-release"); } export function pacReadOnlyNext(targetId: string, consumerId: string): Record { return { status: `bun scripts/cli.ts platform-infra pipelines-as-code status --target ${targetId} --consumer ${consumerId}`, history: `bun scripts/cli.ts platform-infra pipelines-as-code history --target ${targetId} --consumer ${consumerId}`, - fixAutomaticDelivery: automaticDeliveryFix([pacConfigPath, giteaConfigPath], "fix-automatic-delivery"), + fixManualRelease: manualReleaseFix([pacConfigPath, giteaConfigPath], "fix-manual-release"), valuesPrinted: false, }; } @@ -323,17 +323,17 @@ export function pacNodeReadOnlyNext(targetId: string): Record { return { status: `bun scripts/cli.ts cicd status --node ${targetId}`, history: `bun scripts/cli.ts platform-infra pipelines-as-code history --target ${targetId} --limit 10`, - fixAutomaticDelivery: automaticDeliveryFix([pacConfigPath, giteaConfigPath], "fix-automatic-delivery"), + fixManualRelease: manualReleaseFix([pacConfigPath, giteaConfigPath], "fix-manual-release"), valuesPrinted: false, }; } -function automaticDeliveryFix(configRefs: readonly string[], code: "fix-automatic-delivery" | "fix-delivery-authority"): Record { +function manualReleaseFix(configRefs: readonly string[], code: "fix-manual-release" | "fix-delivery-authority"): Record { return { code, reference: PAC_AUTOMATIC_DELIVERY_REFERENCE, configRefs, - instruction: "修复 owning YAML、controller 或源码中的自动链缺陷,并以新的正常 GitHub PR merge 验收;不得人工补齐当前交付。", + instruction: "修复 owning YAML、planner、controller 或源码,重新执行 release plan;只有范围准确时才执行 release trigger --confirm。", mutation: false, valuesPrinted: false, }; diff --git a/scripts/src/cicd-help.ts b/scripts/src/cicd-help.ts index bfb05694..35b0d2b1 100644 --- a/scripts/src/cicd-help.ts +++ b/scripts/src/cicd-help.ts @@ -64,11 +64,11 @@ export function buildCicdHelp( ], config: configPath, spec, - description: "退役 branch-follower 的只读历史状态与有界下钻入口;当前交付只观察 PaC 自动链。", + description: "退役 branch-follower 的只读历史状态与有界下钻入口;当前发布先 plan,再由 CLI 手动发送 PaC webhook。", next: { status: "bun scripts/cli.ts platform-infra pipelines-as-code status --target JD01", history: "bun scripts/cli.ts platform-infra pipelines-as-code history --target JD01 --limit 10", - fixAutomaticDelivery: "docs/reference/platform-infra.md#gitea-与-pipelines-as-code-边界", + fixManualRelease: "docs/reference/platform-infra.md#gitea-与-pipelines-as-code-边界", }, }; } diff --git a/scripts/src/cicd-job-runtime-drilldown.ts b/scripts/src/cicd-job-runtime-drilldown.ts index 97b861b6..59e75318 100644 --- a/scripts/src/cicd-job-runtime-drilldown.ts +++ b/scripts/src/cicd-job-runtime-drilldown.ts @@ -167,7 +167,7 @@ function missingPolicyPayload(action: string, follower: FollowerSpec, registry: parsedDownstreamCliOutput: false, next: { status: `bun scripts/cli.ts cicd branch-follower status --follower ${follower.id}`, - fixAutomaticDelivery: "docs/reference/platform-infra.md#gitea-与-pipelines-as-code-边界", + fixManualRelease: "docs/reference/platform-infra.md#gitea-与-pipelines-as-code-边界", }, }; } diff --git a/scripts/src/cicd-node-status.test.ts b/scripts/src/cicd-node-status.test.ts index 496462f8..cfca2684 100644 --- a/scripts/src/cicd-node-status.test.ts +++ b/scripts/src/cicd-node-status.test.ts @@ -5,7 +5,7 @@ import { compactNodeStatus, renderNodeStatus } from "./cicd-node-status"; const next = { status: "bun scripts/cli.ts cicd status --node NC01", history: "bun scripts/cli.ts platform-infra pipelines-as-code history --target NC01", - fixAutomaticDelivery: { reference: ".agents/skills/unidesk-cicd/SKILL.md" }, + fixManualRelease: { reference: ".agents/skills/unidesk-cicd/SKILL.md" }, }; function result(status: "ready" | "warning" | "blocked", consumers: Array>): Record { diff --git a/scripts/src/cicd-node-status.ts b/scripts/src/cicd-node-status.ts index 58c8e425..9b01db7e 100644 --- a/scripts/src/cicd-node-status.ts +++ b/scripts/src/cicd-node-status.ts @@ -14,7 +14,7 @@ interface NodeStatusOptions { export function cicdNodeStatusHelp(): Record { return { command: "cicd status --node ", - description: "只读汇总节点上每个 Pipelines-as-Code consumer 的自动交付状态。", + description: "只读汇总节点上每个 Pipelines-as-Code consumer 的手动发布与后续交付状态。", usage: [ "bun scripts/cli.ts cicd status --node NC01", "bun scripts/cli.ts cicd status --node NC01 --full", @@ -162,7 +162,7 @@ export function renderNodeStatus(result: Record, full: boolean) "NEXT", ` status: ${stringValue(record(result.next).status)}`, ` history: ${stringValue(record(result.next).history)}`, - ` fix-automatic-delivery: ${stringValue(record(record(result.next).fixAutomaticDelivery).reference)}`, + ` fix-manual-release: ${stringValue(record(record(result.next).fixManualRelease).reference)}`, ]; return { ok: result.ok !== false, command: "cicd status", renderedText: lines.join("\n"), contentType: "text/plain" }; } diff --git a/scripts/src/cicd-render.ts b/scripts/src/cicd-render.ts index 6518d79c..147dc8de 100644 --- a/scripts/src/cicd-render.ts +++ b/scripts/src/cicd-render.ts @@ -67,7 +67,7 @@ function renderPlanHuman(payload: Record): string { `status: ${next?.status ?? "-"}`, `events: ${next?.events ?? "-"}`, `logs: ${next?.logs ?? "-"}`, - `fix-automatic-delivery: ${asOptionalRecord(next?.fixAutomaticDelivery)?.reference ?? next?.fixAutomaticDelivery ?? "-"}`, + `fix-manual-release: ${asOptionalRecord(next?.fixManualRelease)?.reference ?? next?.fixManualRelease ?? "-"}`, "", ].join("\n"); } @@ -153,7 +153,7 @@ function renderStatusHuman(payload: Record, _options: ParsedOpt `status: ${next?.status ?? "-"}`, `events: ${next?.events ?? "-"}`, `logs: ${next?.logs ?? "-"}`, - `fix-automatic-delivery: ${asOptionalRecord(next?.fixAutomaticDelivery)?.reference ?? next?.fixAutomaticDelivery ?? "-"}`, + `fix-manual-release: ${asOptionalRecord(next?.fixManualRelease)?.reference ?? next?.fixManualRelease ?? "-"}`, "", ].filter((line) => line !== "").join("\n"); } diff --git a/scripts/src/cicd-taskrun-drilldown.ts b/scripts/src/cicd-taskrun-drilldown.ts index 02a0052b..1f6f64cb 100644 --- a/scripts/src/cicd-taskrun-drilldown.ts +++ b/scripts/src/cicd-taskrun-drilldown.ts @@ -36,7 +36,7 @@ export async function runBranchFollowerTaskRunDrillDown( parsedDownstreamCliOutput: false, next: { status: `bun scripts/cli.ts cicd branch-follower status --follower ${follower.id}`, - fixAutomaticDelivery: "docs/reference/platform-infra.md#gitea-与-pipelines-as-code-边界", + fixManualRelease: "docs/reference/platform-infra.md#gitea-与-pipelines-as-code-边界", }, }; } diff --git a/scripts/src/cicd.ts b/scripts/src/cicd.ts index 92c3eec6..dbe09dcf 100644 --- a/scripts/src/cicd.ts +++ b/scripts/src/cicd.ts @@ -22,7 +22,7 @@ export function cicdHelp(): unknown { primaryHistory: "bun scripts/cli.ts platform-infra pipelines-as-code history --target JD01 --limit 10", archived: "cicd gitea-actions-poc", deprecated: "cicd branch-follower", - note: "当前 source/trigger authority 是 GitHub PR merge -> Gitea mirror -> Pipelines-as-Code -> Tekton -> Argo/k8s runtime;Gitea Actions POC 与 branch-follower 仅保留只读历史观察。", + note: "当前 source authority 由 GitHub PR merge -> Gitea mirror 更新;发布触发固定为 CLI release plan -> 手动 PaC webhook -> Tekton -> Argo/k8s runtime。Gitea Actions POC 与 branch-follower 仅保留只读历史观察。", }, subcommands: [ cicdNodeStatusHelp(), diff --git a/scripts/src/help.ts b/scripts/src/help.ts index edd0b5c2..37ef422c 100644 --- a/scripts/src/help.ts +++ b/scripts/src/help.ts @@ -78,7 +78,7 @@ export function rootHelp(): unknown { { command: "hwlab nodes control-plane|git-mirror|hwpod-preinstall|secret|test-accounts --node NC01 --lane v03", description: "Operate the NC01 HWLAB v0.3 development and deployment lane from YAML source truth." }, { command: "agentrun get|describe|events|logs|result|ack|cancel|dispatch|create|apply|send|native-development|control-plane|git-mirror", description: "Use AgentRun resource primitives and the YAML-first L1 manager lifecycle with low-noise human output by default; session follow-up uses send only and the server decides internal steer vs turn." }, { command: "platform-infra sub2api|langbot|n8n|webterm|wechat-archive|hyperv-vm|k3s-cluster ...", description: "Deploy YAML-controlled platform services, Hyper-V Ubuntu nodes and multi-host k3s clusters without printing secrets." }, - { command: "pikaoa attachments-backup ...", description: "管理 PikaOA 生产附件离机备份控制面;开发与生产运行面由 PaC 自动交付。" }, + { command: "pikaoa attachments-backup ...", description: "管理 PikaOA 生产附件离机备份控制面;开发与生产运行面通过 PaC plan 后手动发布。" }, { command: "secrets plan|sync|status", description: "Plan, push and inspect YAML-declared local secret source keys to Kubernetes Secrets without printing or reverse-engineering values." }, { command: "platform-db postgres plan|status|export-secrets|apply|swap", description: "Manage YAML-declared host-native PostgreSQL and its node swap capacity through bounded plans, asynchronous mutation, status, TLS and redacted secret output." }, { command: "hwlab cd audit --env dev | hwlab cd status --env dev | hwlab cd apply --env dev --dry-run", description: "Archived HWLAB DEV CD diagnostics only; current HWLAB rollout uses NC01 GitOps." }, diff --git a/scripts/src/hwlab-node-help.ts b/scripts/src/hwlab-node-help.ts index 20117c1d..520542eb 100644 --- a/scripts/src/hwlab-node-help.ts +++ b/scripts/src/hwlab-node-help.ts @@ -75,7 +75,7 @@ export function hwlabNodeHelp(scope: "legacy-cicd" | "platform-maintenance" | nu "control-plane cleanup-runs deletes terminal PipelineRuns; cleanup-released-pvs deletes only orphaned hwlab-ci PipelineRun PVCs with no active pod mounts and Released local-path/Delete PVs.", "cleanup-legacy-docker-images is a transitional legacy-cache GC for Docker images matching YAML allowlisted repositories; it protects container-referenced images, does not run prune, and does not touch Docker volumes.", "cleanup-legacy-docker-registry-volume removes only an exited legacy Docker registry container and its unique /var/lib/registry volume after the YAML-declared k8s node-local-registry is ready.", - "PaC consumer 只由 GitHub PR merge 触发;默认帮助只给只读 status/history,自动链故障必须修 owning YAML、controller 或源码。", + "PaC consumer 的 PR merge 只更新 source authority;发布必须先 release plan,再由 CLI 手动发送 PaC webhook。默认帮助只给只读 status/history。", "旧交付与平台维护写入口分别只在 legacy-cicd、platform-maintenance scoped help 中展示。", "control-plane status reads its total budget, heartbeat cadence, termination grace period, and probe switches from config/hwlab-node-control-plane.yaml; --json keeps stdout as one compact JSON document while progress remains on stderr.", ], diff --git a/scripts/src/hwlab-node-web-sentinel-cicd-shared.ts b/scripts/src/hwlab-node-web-sentinel-cicd-shared.ts index 2d58f86b..b54939cf 100644 --- a/scripts/src/hwlab-node-web-sentinel-cicd-shared.ts +++ b/scripts/src/hwlab-node-web-sentinel-cicd-shared.ts @@ -857,12 +857,12 @@ function renderSentinelNext(next: Record): string { const status = commandText(next.pacStatus) ?? commandText(next.status) ?? commandText(next.controlPlaneStatus); const controlPlaneStatus = commandText(next.controlPlaneStatus); const history = commandText(next.pacHistory) ?? commandText(next.history); - const fix = commandText(next.fixAutomaticDelivery) ?? commandText(record(next.fixAutomaticDelivery).reference); + const fix = commandText(next.fixManualRelease) ?? commandText(record(next.fixManualRelease).reference); const rows: Array<[string, unknown]> = [ ["status", status], ["control-plane-status", controlPlaneStatus === status ? null : controlPlaneStatus], ["history", history], - ["fix-automatic-delivery", fix], + ["fix-manual-release", fix], ["pipeline-run", next.pipelineRunStatus], ["events", next.events], ["logs", next.logs], @@ -908,7 +908,7 @@ function renderSentinelRecoveryNext(recoveryNext: Record): stri const lines = commandRows([ ["status", recoveryNext.nextStatus ?? recoveryNext.pacStatus ?? recoveryNext.status], ["history", recoveryNext.pacHistory ?? recoveryNext.history], - ["fix-automatic-delivery", commandText(recoveryNext.fixAutomaticDelivery) ?? commandText(record(recoveryNext.fixAutomaticDelivery).reference)], + ["fix-manual-release", commandText(recoveryNext.fixManualRelease) ?? commandText(record(recoveryNext.fixManualRelease).reference)], ["git-mirror", recoveryNext.gitMirrorStatus], ["pac-closeout", recoveryNext.pacCloseout], ["manual-recovery", recoveryNext.publishCurrent], diff --git a/scripts/src/hwlab-node-web-sentinel-cicd.ts b/scripts/src/hwlab-node-web-sentinel-cicd.ts index aa5d3bb8..bf5baf36 100644 --- a/scripts/src/hwlab-node-web-sentinel-cicd.ts +++ b/scripts/src/hwlab-node-web-sentinel-cicd.ts @@ -2225,7 +2225,7 @@ function sentinelObservedStatusDiagnosis(state: SentinelCicdState, value: unknow : "one or more source, registry, GitOps, Argo, runtime or cadence checks did not pass"; const reason = legacyDelivery ? legacyReason - : `Web sentinel automatic delivery observation is not aligned (${code}); inspect read-only status/history and fix the owning YAML, controller, or source.`; + : `Web sentinel manual release observation is not aligned (${code}); inspect read-only status/history, fix the owning YAML, planner, controller, or source, then rerun release plan.`; const next = publishCurrentNext(state); const controlNext = controlPlaneNext(state, "trigger-current"); const shouldRetryPublish = code === "sentinel-publish-half-state-registry-missing" || code === "sentinel-gitops-manifest-not-updated"; @@ -2252,7 +2252,7 @@ function sentinelObservedStatusDiagnosis(state: SentinelCicdState, value: unknow pipelineRun, deliveryAuthority, warning: !legacyDelivery - ? `自动链未对齐;检查 ${next.pacStatus ?? next.status} 与 ${next.pacHistory ?? next.status},按 ${next.fixAutomaticDelivery} 修复后用新的正常 GitHub PR merge 验收。` + ? `发布链未对齐;检查 ${next.pacStatus ?? next.status} 与 ${next.pacHistory ?? next.status},按 ${next.fixManualRelease} 修复后重新 plan,范围准确时再手动 trigger。` : code === "sentinel-publish-half-state-registry-missing" ? `source mirror already exposes ${short(state.sourceHead.commit)} but registry tag ${state.image.tag} is missing; run ${next.pacCloseout} and inspect ${next.pacHistory}.` : code === "sentinel-git-mirror-not-in-sync" @@ -2279,7 +2279,7 @@ function sentinelObservedStatusDiagnosis(state: SentinelCicdState, value: unknow status: next.status, pacStatus: next.pacStatus, pacHistory: next.pacHistory, - fixAutomaticDelivery: next.fixAutomaticDelivery, + fixManualRelease: next.fixManualRelease, valuesRedacted: true, }, valuesRedacted: true, @@ -2729,7 +2729,7 @@ function controlPlaneRecoveryNext(state: SentinelCicdState, ok: boolean, publish const deliveryAuthority = webProbeSentinelDeliveryAuthority(state.spec); if (deliveryAuthority.kind !== "legacy-manual") { return { - reason: "平台维护后自动交付仍未对齐;只读检查 sentinel/PaC 状态并修复自动链,不得人工 publish、sync 或 flush。", + reason: "平台维护后发布链仍未对齐;只读检查 sentinel/PaC 状态并修复 owning YAML、planner 或 controller,重新 plan;不得裸 publish、sync 或 flush。", ...sentinelReadOnlyNext(state, deliveryAuthority), valuesRedacted: true, }; @@ -2773,7 +2773,7 @@ function controlPlaneDrillDown(state: SentinelCicdState, pipelineRun: unknown, f : `${pacBase} history --target ${state.spec.nodeId} --id ${pipelineRunName} --full`, consumerStatus: `${pacBase} status --target ${state.spec.nodeId} --consumer sentinel-${state.spec.nodeId.toLowerCase()}-${state.spec.lane}`, controlPlaneStatus: next.controlPlaneStatus ?? next.status, - fixAutomaticDelivery: next.fixAutomaticDelivery ?? PAC_AUTOMATIC_DELIVERY_REFERENCE, + fixManualRelease: next.fixManualRelease ?? PAC_AUTOMATIC_DELIVERY_REFERENCE, valuesRedacted: true, }; if (deliveryAuthority.kind !== "legacy-manual") return readOnly; diff --git a/scripts/src/hwlab-node/delivery-authority.ts b/scripts/src/hwlab-node/delivery-authority.ts index 499720b9..7bcd57d6 100644 --- a/scripts/src/hwlab-node/delivery-authority.ts +++ b/scripts/src/hwlab-node/delivery-authority.ts @@ -1,7 +1,7 @@ import { decideCicdDeliveryMutation, gitRepositoryIdentity, - pacAutomaticDeliveryFix, + pacManualReleaseFix, pacReadOnlyNext, resolveCicdDeliveryAuthority, type CicdDeliveryAuthority, @@ -54,7 +54,7 @@ export function hwlabNodeDeliveryNext( if (authority.kind === "unknown") { return { runtimeStatusFull, - fixAutomaticDelivery: pacAutomaticDeliveryFix(authority), + fixManualRelease: pacManualReleaseFix(authority), valuesPrinted: false, }; } @@ -79,7 +79,7 @@ export function hwlabNodePlanNext( return { status, history: `bun scripts/cli.ts platform-infra pipelines-as-code history --target ${target.node} --limit 10`, - fixAutomaticDelivery: pacAutomaticDeliveryFix(authority), + fixManualRelease: pacManualReleaseFix(authority), valuesPrinted: false, }; } diff --git a/scripts/src/hwlab-node/render.ts b/scripts/src/hwlab-node/render.ts index 8de465b6..3ba7ef46 100644 --- a/scripts/src/hwlab-node/render.ts +++ b/scripts/src/hwlab-node/render.ts @@ -450,7 +450,7 @@ export function nodeRuntimeVisiblePipelineRunDiagnostics( ? diagnostics.failureSummary ?? null : { ...failureSummary, - nextAction: "检查失败 TaskRun 与有界日志,修复 owning YAML、controller 或源码中的自动链缺陷,再以新的正常 GitHub PR merge 验收;不得人工 rerun。", + nextAction: "检查失败 TaskRun 与有界日志,修复 owning YAML、planner、controller 或源码,重新执行 release plan;范围准确时再手动 trigger,不得人工 rerun。", }, next: null, }; diff --git a/scripts/src/hwlab-node/sentinel-delivery-authority.ts b/scripts/src/hwlab-node/sentinel-delivery-authority.ts index 6f0e642b..ee29a573 100644 --- a/scripts/src/hwlab-node/sentinel-delivery-authority.ts +++ b/scripts/src/hwlab-node/sentinel-delivery-authority.ts @@ -113,9 +113,9 @@ export function renderBlockedSentinelDeliveryMutation( valuesRedacted: true, }; const next = record(result.next); - const fixReference = typeof next.fixAutomaticDelivery === "string" - ? next.fixAutomaticDelivery - : record(next.fixAutomaticDelivery).reference ?? PAC_AUTOMATIC_DELIVERY_REFERENCE; + const fixReference = typeof next.fixManualRelease === "string" + ? next.fixManualRelease + : record(next.fixManualRelease).reference ?? PAC_AUTOMATIC_DELIVERY_REFERENCE; return Object.assign( rendered(false, command, [ "WEB SENTINEL DELIVERY BLOCKED", @@ -123,7 +123,7 @@ export function renderBlockedSentinelDeliveryMutation( `status=${text(next.status)}`, `history=${text(next.history ?? next.pacHistory)}`, `fix=${text(fixReference)}`, - "GitHub PR merge 是唯一正式交付触发;修复自动链后用新的正常合并事件验收。", + "GitHub PR merge 只更新 source authority;修复后重新执行 release plan,范围准确时再手动 trigger。", ].join("\n")), result, ); @@ -139,7 +139,7 @@ export function sentinelReadOnlyNextForSpec( const base = { status: controlPlaneStatus, controlPlaneStatus, - fixAutomaticDelivery: PAC_AUTOMATIC_DELIVERY_REFERENCE, + fixManualRelease: PAC_AUTOMATIC_DELIVERY_REFERENCE, }; if (authority.kind !== "pac-pr-merge") return base; const pacHistory = `bun scripts/cli.ts platform-infra pipelines-as-code history --target ${authority.consumer.node} --consumer ${authority.consumer.consumerId} --limit 10`; diff --git a/scripts/src/platform-infra-gitea-render.ts b/scripts/src/platform-infra-gitea-render.ts index 6525415c..cf36bf59 100644 --- a/scripts/src/platform-infra-gitea-render.ts +++ b/scripts/src/platform-infra-gitea-render.ts @@ -20,7 +20,7 @@ export function renderMirrorPlan(result: Record): RenderedCliRe ` status: ${stringValue(next.status)}`, ` full: ${stringValue(next.statusFull)}`, ` webhook-status: ${stringValue(next.webhookStatus)}`, - ` fix-automatic-delivery: ${stringValue(next.fixAutomaticDelivery)}`, + ` fix-manual-release: ${stringValue(next.fixManualRelease)}`, "", "Disclosure: credentials are summarized by presence/fingerprint only.", ]); @@ -266,7 +266,7 @@ export function renderPlan(result: Record): RenderedCliResult { ` validate: ${stringValue(next.validate)}`, ` mirror-status: ${stringValue(next.mirrorStatus)}`, ` webhook-status: ${stringValue(next.webhookStatus)}`, - ` fix-automatic-delivery: ${stringValue(next.fixAutomaticDelivery)}`, + ` fix-manual-release: ${stringValue(next.fixManualRelease)}`, "", "Boundary: Gitea is internal ClusterIP source authority for GH-1560; CI trigger authority is Pipelines-as-Code, not Gitea Actions/act_runner.", "Disclosure: Secret values are not printed; this stage does not create runner credentials.", diff --git a/scripts/src/platform-infra-gitea.ts b/scripts/src/platform-infra-gitea.ts index b8cf2253..f784dea9 100644 --- a/scripts/src/platform-infra-gitea.ts +++ b/scripts/src/platform-infra-gitea.ts @@ -204,7 +204,7 @@ export function giteaHelp(scope?: string): Record { "bun scripts/cli.ts platform-infra gitea mirror webhook status --target [--repo ] [--delivery-id ] [--full|--raw]", ], scopedHelp: ["bun scripts/cli.ts platform-infra gitea help platform-bootstrap"], - boundary: "PR merge 是唯一 delivery 触发;默认入口只读观察和校验,不能充当合并后的恢复动作。", + boundary: "PR merge 只更新 Gitea source authority,不创建 PipelineRun;发布必须先执行 PaC release plan,再由 CLI 手动发送 webhook。默认入口只读观察和校验。", }; } @@ -1622,7 +1622,7 @@ function mirrorReadOnlyNextCommands(targetId: string): Record { statusFull: `bun scripts/cli.ts platform-infra gitea mirror status --target ${targetId} --full`, webhookStatus: `bun scripts/cli.ts platform-infra gitea mirror webhook status --target ${targetId}`, webhookStatusFull: `bun scripts/cli.ts platform-infra gitea mirror webhook status --target ${targetId} --full`, - fixAutomaticDelivery: PAC_AUTOMATIC_DELIVERY_REFERENCE, + fixManualRelease: PAC_AUTOMATIC_DELIVERY_REFERENCE, }; } @@ -1988,7 +1988,7 @@ function giteaReadOnlyNextCommands(targetId: string): Record { validate: `bun scripts/cli.ts platform-infra gitea validate --target ${targetId}`, mirrorStatus: `bun scripts/cli.ts platform-infra gitea mirror status --target ${targetId}`, webhookStatus: `bun scripts/cli.ts platform-infra gitea mirror webhook status --target ${targetId}`, - fixAutomaticDelivery: PAC_AUTOMATIC_DELIVERY_REFERENCE, + fixManualRelease: PAC_AUTOMATIC_DELIVERY_REFERENCE, }; } diff --git a/scripts/src/platform-infra-pipelines-as-code-bootstrap.test.ts b/scripts/src/platform-infra-pipelines-as-code-bootstrap.test.ts index 487a065c..e0eac767 100644 --- a/scripts/src/platform-infra-pipelines-as-code-bootstrap.test.ts +++ b/scripts/src/platform-infra-pipelines-as-code-bootstrap.test.ts @@ -27,6 +27,13 @@ function pacConfigDocument(): Record { return structuredClone(materializeYamlComposition(source, { label: "config/platform-infra/pipelines-as-code.yaml" }).value); } +test("PaC bootstrap next requires manual release plan after source merge", () => { + const source = readFileSync(resolve(import.meta.dir, "platform-infra-pipelines-as-code-bootstrap.ts"), "utf8"); + expect(source).toContain("source-pr-merge-then-plan"); + expect(source).toContain("release plan"); + expect(source).not.toContain("由自动链继续交付"); +}); + test("PaC bootstrap uses one exact YAML-owned Gitea repository", () => { const selected = resolvePacBootstrapMirrorRepository(fixtureConfig([mirror]), "nc01", { id: "widgets-nc01", @@ -269,12 +276,35 @@ test("PaC automatic webhook disable is target-scoped and does not touch Kubernet expect(Number((plan as any).plan.repositoryCount)).toBeGreaterThan(1); const remote = readFileSync(resolve(import.meta.dir, "platform-infra-pipelines-as-code-remote.sh"), "utf8"); const actionStart = remote.indexOf("disable_automatic_webhooks_action()"); - const action = remote.slice(actionStart, remote.indexOf("repository_manifest()", actionStart)); + const action = remote.slice(actionStart, remote.indexOf("manual_release_plan_json()", actionStart)); expect(action).toContain("gitea_api DELETE"); expect(action).not.toContain("kubectl"); expect(action).not.toContain("ensure_webhook"); }); +test("L2/L3 release exposes plan first and trigger only sends the PaC webhook", async () => { + await expect(runPlatformInfraPipelinesAsCodeCommand({} as never, [ + "release", "trigger", "--target", "NC01", "--consumer", "hwlab-nc01-v03", + "--source-commit", "a".repeat(40), + ])).rejects.toThrow("release trigger requires --confirm after reviewing release plan"); + const remote = readFileSync(resolve(import.meta.dir, "platform-infra-pipelines-as-code-remote.sh"), "utf8"); + const planStart = remote.indexOf("manual_release_plan_json()"); + const actionStart = remote.indexOf("manual_release_action()"); + const planAction = remote.slice(planStart, actionStart); + const triggerAction = remote.slice(actionStart, remote.indexOf("repository_manifest()", actionStart)); + expect(triggerAction).toContain("manual_release_plan_json"); + expect(triggerAction).toContain("X-Gitea-Event-Type: push"); + expect(planAction).toContain("imageBuildCount"); + expect(planAction).toContain("envReuse"); + expect(planAction).toContain("requestedScope"); + expect(planAction).toContain("expandedServices"); + expect(planAction).toContain("buildServices"); + expect(planAction).toContain("rolloutServices"); + expect(triggerAction).toContain("release-plan-scope-expanded"); + expect(triggerAction).not.toContain("kubectl create"); + expect(triggerAction).not.toContain("PipelineRun"); +}); + test("PaC release manifest renders YAML-owned watcher startup backlog probes", () => { const pac = readPacConfig({ consumerId: "hwlab-nc01-v03", selectDefault: true }); const source = readFileSync(resolve(import.meta.dir, "fixtures/pac/release-leading-comment-only.yaml"), "utf8"); diff --git a/scripts/src/platform-infra-pipelines-as-code-bootstrap.ts b/scripts/src/platform-infra-pipelines-as-code-bootstrap.ts index 62add1aa..3b807d0c 100644 --- a/scripts/src/platform-infra-pipelines-as-code-bootstrap.ts +++ b/scripts/src/platform-infra-pipelines-as-code-bootstrap.ts @@ -188,7 +188,7 @@ function bootstrapNext(result: Record, blockers: Record[]): string { diff --git a/scripts/src/platform-infra-pipelines-as-code-remote.sh b/scripts/src/platform-infra-pipelines-as-code-remote.sh index 5e412cd0..96064f71 100644 --- a/scripts/src/platform-infra-pipelines-as-code-remote.sh +++ b/scripts/src/platform-infra-pipelines-as-code-remote.sh @@ -223,6 +223,136 @@ EOF printf '{"ok":true,"mutation":true,"repositoryCount":%s,"removedWebhookCount":%s,"automaticWebhookEnabled":false,"valuesPrinted":false}\n' "$repository_count" "$removed_count" } +manual_release_plan_json() { + tmp=$(mktemp -d) + trap 'rm -rf "$tmp"' EXIT INT TERM + git clone --quiet --no-checkout "$UNIDESK_PAC_REPOSITORY_URL" "$tmp/source" + git -C "$tmp/source" checkout --quiet --detach "$UNIDESK_PAC_MANUAL_SOURCE_COMMIT" + source_commit=$(git -C "$tmp/source" rev-parse HEAD) + [ "$source_commit" = "$UNIDESK_PAC_MANUAL_SOURCE_COMMIT" ] + base_commit=${UNIDESK_PAC_MANUAL_BASE_COMMIT:-} + if [ -z "$base_commit" ]; then base_commit=$(git -C "$tmp/source" rev-parse "$source_commit^"); fi + git -C "$tmp/source" cat-file -e "$base_commit^{commit}" + changed_paths=$(git -C "$tmp/source" diff --name-only "$base_commit" "$source_commit") + if [ "$UNIDESK_PAC_MANUAL_RENDERER" = "hwlab-runtime-lane" ]; then + if [ ! -d "$UNIDESK_PAC_MANUAL_NODE_MODULES" ]; then + printf '%s\n' "HWLAB planner node_modules is missing at the YAML-selected workspace" >&2 + return 1 + fi + ln -s "$UNIDESK_PAC_MANUAL_NODE_MODULES" "$tmp/source/node_modules" + ( + cd "$tmp/source" + HWLAB_CATALOG_PATH="$UNIDESK_PAC_MANUAL_ARTIFACT_CATALOG_PATH" \ + HWLAB_GITOPS_BRANCH="$UNIDESK_PAC_MANUAL_GITOPS_BRANCH" \ + HWLAB_GIT_READ_URL="$UNIDESK_PAC_REPOSITORY_URL" \ + HWLAB_SERVICES="$UNIDESK_PAC_MANUAL_SERVICE_IDS" \ + node scripts/ci/restore-artifact-catalog.mjs + ) + domain_plan=$(cd "$tmp/source" && node scripts/ci-plan.mjs \ + --lane "$UNIDESK_PAC_CONSUMER_LANE" \ + --base-ref "$base_commit" \ + --target-ref "$source_commit" \ + --artifact-catalog "$UNIDESK_PAC_MANUAL_ARTIFACT_CATALOG_PATH" \ + --services "$UNIDESK_PAC_MANUAL_SERVICE_IDS" \ + --registry-prefix "$UNIDESK_PAC_MANUAL_REGISTRY_PREFIX" \ + --base-image "$UNIDESK_PAC_MANUAL_BASE_IMAGE") + else + domain_plan="" + fi + SOURCE_COMMIT="$source_commit" BASE_COMMIT="$base_commit" CHANGED_PATHS="$changed_paths" DOMAIN_PLAN="$domain_plan" node <<'NODE' +const changedPaths = (process.env.CHANGED_PATHS || '').split('\n').filter(Boolean); +const domain = process.env.DOMAIN_PLAN ? JSON.parse(process.env.DOMAIN_PLAN) : null; +const configuredImages = JSON.parse(process.env.UNIDESK_PAC_MANUAL_IMAGE_REPOSITORIES_JSON || '[]'); +const affectedServices = domain?.affectedServices || (changedPaths.length > 0 ? [process.env.UNIDESK_PAC_MANUAL_RUNTIME_SERVICE].filter(Boolean) : []); +const rolloutServices = domain?.rolloutServices || affectedServices; +const buildServices = domain?.buildServices || (changedPaths.length > 0 ? configuredImages : []); +const requestedServices = domain + ? (domain.services || []) + .filter((service) => (service.changedPaths || []).length > 0 || service.runtimeConfigChanged === true) + .map((service) => service.serviceId) + : (changedPaths.length > 0 ? [process.env.UNIDESK_PAC_MANUAL_RUNTIME_SERVICE].filter(Boolean) : []); +const expandedServices = affectedServices.filter((serviceId) => !requestedServices.includes(serviceId)); +const envGroups = Array.isArray(domain?.envArtifactGroups) ? domain.envArtifactGroups : []; +const envReuse = domain ? { + status: envGroups.length > 0 ? 'planned' : 'not-configured', + groups: envGroups, + reusedServices: domain.reusedServices || [], + reusedServiceCount: Number(domain.serviceReusedCount || 0), + buildSkippedServices: domain.imageBuildSkippedServices || [], +} : { status: 'not-configured', groups: [], reusedServices: [], reusedServiceCount: 0, buildSkippedServices: [] }; +const plan = { + sourceCommit: process.env.SOURCE_COMMIT, + baseCommit: process.env.BASE_COMMIT, + sourceBranch: process.env.UNIDESK_PAC_MANUAL_SOURCE_BRANCH, + changedPathCount: changedPaths.length, + changedPaths, + artifactCatalog: domain?.artifactCatalog || null, + requestedScope: { + affectedServiceCount: requestedServices.length, + affectedServices: requestedServices, + }, + envReuse, + imageBuildCount: buildServices.length, + buildServices, + rolloutServiceCount: rolloutServices.length, + rolloutServices, + affectedServiceCount: affectedServices.length, + affectedServices, + scopeReview: { + required: true, + status: expandedServices.length > 0 ? 'expanded' : 'exact', + expanded: expandedServices.length > 0, + expandedServiceCount: expandedServices.length, + expandedServices, + instruction: expandedServices.length > 0 + ? 'fix the planner, source range, or owning YAML before trigger' + : 'review the explicit build and rollout scope before trigger', + }, + valuesPrinted: false, +}; +process.stdout.write(JSON.stringify(plan)); +NODE +} + +manual_release_action() { + plan=$(manual_release_plan_json) + if [ "$UNIDESK_PAC_ACTION" = "manual-release-plan" ]; then + printf '{"ok":true,"mutation":false,"plan":%s,"valuesPrinted":false}\n' "$plan" + return + fi + scope_expanded=$(PLAN_JSON="$plan" node -e 'const plan=JSON.parse(process.env.PLAN_JSON); process.stdout.write(plan.scopeReview?.expanded === true ? "1" : "0");') + if [ "$scope_expanded" = "1" ]; then + printf '{"ok":false,"mutation":false,"error":"release-plan-scope-expanded","plan":%s,"webhook":null,"valuesPrinted":false}\n' "$plan" + return 1 + fi + payload=$(PLAN_JSON="$plan" node <<'NODE' +const crypto = require('node:crypto'); +const plan = JSON.parse(process.env.PLAN_JSON); +const byKind = { added: [], removed: [], modified: plan.changedPaths }; +const owner = process.env.UNIDESK_PAC_GITEA_OWNER; +const repo = process.env.UNIDESK_PAC_GITEA_REPO; +const url = process.env.UNIDESK_PAC_REPOSITORY_URL.replace(/\/+$/u, ''); +const commit = { id: plan.sourceCommit, message: 'manual L2/L3 release', url: `${url}/commit/${plan.sourceCommit}`, added: byKind.added, removed: byKind.removed, modified: byKind.modified, author: { name: 'unidesk-cli', email: '', username: 'unidesk-cli' }, committer: { name: 'unidesk-cli', email: '', username: 'unidesk-cli' } }; +const user = { id: 0, login: 'unidesk-cli', full_name: 'UniDesk CLI', email: '', avatar_url: '', username: 'unidesk-cli' }; +process.stdout.write(JSON.stringify({ ref: `refs/heads/${plan.sourceBranch}`, before: plan.baseCommit, after: plan.sourceCommit, compare_url: `${url}/compare/${plan.baseCommit}...${plan.sourceCommit}`, commits: [commit], head_commit: commit, repository: { id: 0, owner: { ...user, login: owner, username: owner }, name: repo, full_name: `${owner}/${repo}`, private: true, html_url: url, clone_url: `${url}.git`, default_branch: plan.sourceBranch }, pusher: user, sender: user })); +NODE +) + signature=$(PAYLOAD="$payload" node -e 'const crypto=require("node:crypto"); process.stdout.write(crypto.createHmac("sha256", process.env.UNIDESK_PAC_WEBHOOK_SECRET).update(process.env.PAYLOAD).digest("hex"));') + service_ip=$(kubectl -n "$UNIDESK_PAC_RELEASE_NAMESPACE" get service "$UNIDESK_PAC_CONTROLLER_SERVICE_NAME" -o jsonpath='{.spec.clusterIP}') + response_tmp=$(mktemp) + delivery="unidesk-manual-$(date +%s)-$$" + http_code=$(curl -sS -o "$response_tmp" -w '%{http_code}' -X POST "http://$service_ip:$UNIDESK_PAC_CONTROLLER_SERVICE_PORT" \ + -H 'Content-Type: application/json' \ + -H 'X-Gitea-Event-Type: push' \ + -H "X-Gitea-Delivery: $delivery" \ + -H "X-Gitea-Signature: $signature" \ + --data-binary "$payload") + case "$http_code" in 2*) ok=true ;; *) ok=false ;; esac + rm -f "$response_tmp" + printf '{"ok":%s,"mutation":true,"plan":%s,"webhook":{"event":"push","delivery":"%s","httpStatus":%s,"sourceCommit":"%s","valuesPrinted":false},"valuesPrinted":false}\n' "$ok" "$plan" "$(json_string "$delivery")" "$http_code" "$(json_string "$UNIDESK_PAC_MANUAL_SOURCE_COMMIT")" + [ "$ok" = true ] +} + repository_manifest() { cat < --consumer --source-commit [--base-commit ] [--confirm]", valuesPrinted: false }; + const options = parseManualReleaseOptions(args.slice(2)); + if (releaseAction === "plan" && options.confirm) throw new Error("release plan does not accept --confirm"); + if (releaseAction === "trigger" && !options.confirm) throw new Error("release trigger requires --confirm after reviewing release plan"); + return await manualRelease(config, releaseAction, options); + } if (action === "apply") { const options = parseApplyOptions(args.slice(1)); const result = await apply(config, options); @@ -584,11 +598,13 @@ function help(scope: string | null): Record { }; } return { - command: "platform-infra pipelines-as-code plan|status|history|diagnose-regression|delivery-timing|delivery-observe|debug-step|source-artifact", + command: "platform-infra pipelines-as-code plan|release|status|history|diagnose-regression|delivery-timing|delivery-observe|debug-step|source-artifact", configTruth: configLabel, usage: [ "bun scripts/cli.ts platform-infra pipelines-as-code plan --target JD01", "bun scripts/cli.ts platform-infra pipelines-as-code automatic-webhooks disable --target [--confirm]", + "bun scripts/cli.ts platform-infra pipelines-as-code release plan --target --consumer --source-commit [--base-commit ]", + "bun scripts/cli.ts platform-infra pipelines-as-code release trigger --target --consumer --source-commit [--base-commit ] --confirm", "bun scripts/cli.ts platform-infra pipelines-as-code status --target JD01 [--json|--full|--raw]", "bun scripts/cli.ts platform-infra pipelines-as-code history --target JD01 [--consumer hwlab-jd01-v03] [--limit 10]", "bun scripts/cli.ts platform-infra pipelines-as-code diagnose-regression --target NC01 --consumer agentrun-nc01-v02 [--limit 20] [--json|--full]", @@ -601,7 +617,7 @@ function help(scope: string | null): Record { ], maintenanceHelp: "bun scripts/cli.ts platform-infra pipelines-as-code help platform-bootstrap", compatibilityHelp: "bun scripts/cli.ts platform-infra pipelines-as-code help compatibility-diagnostics", - boundary: "唯一正式触发链:GitHub PR merge -> GitHub webhook bridge -> Gitea mirror/snapshot -> Pipelines-as-Code -> Tekton -> GitOps/Argo/k8s runtime。", + boundary: "唯一正式发布链:GitHub PR merge -> Gitea source authority -> CLI release plan -> CLI 手动 PaC webhook -> Tekton -> GitOps/Argo/k8s runtime。", }; } @@ -1571,6 +1587,78 @@ async function disableAutomaticWebhooks(config: UniDeskConfig, options: ApplyOpt }; } +async function manualRelease(config: UniDeskConfig, action: "plan" | "trigger", options: ManualReleaseOptions): Promise> { + const pac = readPacConfig({ consumerId: options.consumerId }); + const target = resolveTarget(pac, options.targetId); + const consumer = resolveConsumer(pac, options.consumerId); + if (consumer.node.toLowerCase() !== target.id.toLowerCase()) throw new Error(`consumer ${consumer.id} belongs to ${consumer.node}, not ${target.id}`); + const repository = resolveRepository(pac, consumer.repositoryRef); + const params = consumerParams(repository, consumer); + const imageRepositories = [...new Set(Object.entries(params) + .filter(([key, value]) => key.endsWith("image_repository") && value.length > 0) + .map(([, value]) => value))]; + const hwlabLane = consumer.sourceArtifact?.renderer === "hwlab-runtime-lane" + ? hwlabRuntimeLaneSpecForNode(consumer.lane, consumer.node) + : null; + const planningNodeModules = hwlabLane === null ? "" : `${hwlabLane.workspace}/node_modules`; + const secrets = ensureSecrets(pac, false); + const remoteAction = action === "plan" ? "manual-release-plan" : "manual-release-trigger"; + const result = await capture(config, target.route, ["sh"], remoteScript(remoteAction, pac, target, repository, consumer, { ...options, dryRun: action === "plan", wait: false }, secrets, "", [consumer], false, [repository], { + sourceCommit: options.sourceCommit, + baseCommit: options.baseCommit, + sourceBranch: params.source_branch ?? "", + imageRepositories, + runtimeService: params.runtime_deployment ?? consumer.id, + planningNodeModules, + artifactCatalogPath: hwlabLane?.catalogPath ?? "", + gitopsBranch: params.gitops_branch ?? hwlabLane?.gitopsBranch ?? "", + serviceIds: hwlabLane?.serviceIds ?? [], + registryPrefix: hwlabLane?.registryPrefix ?? "", + baseImage: hwlabLane?.baseImage ?? "", + }), { runtimeTimeoutMs: action === "plan" ? 120_000 : 150_000 }); + const remote = parseJsonOutput(result.stdout) ?? {}; + const fullReleasePlan = remote.plan && typeof remote.plan === "object" ? remote.plan as Record : null; + const scopeExpanded = fullReleasePlan?.scopeReview?.expanded === true; + const releasePlan = action === "trigger" && typeof remote.error === "string" && fullReleasePlan !== null + ? { + sourceCommit: fullReleasePlan.sourceCommit ?? null, + baseCommit: fullReleasePlan.baseCommit ?? null, + artifactCatalog: fullReleasePlan.artifactCatalog === null ? null : { + status: fullReleasePlan.artifactCatalog?.status ?? null, + authority: fullReleasePlan.artifactCatalog?.authority ?? null, + catalogSourceCommitId: fullReleasePlan.artifactCatalog?.catalogSourceCommitId ?? null, + coverage: fullReleasePlan.artifactCatalog?.coverage ?? null, + }, + requestedScope: fullReleasePlan.requestedScope ?? null, + imageBuildCount: fullReleasePlan.imageBuildCount ?? null, + rolloutServiceCount: fullReleasePlan.rolloutServiceCount ?? null, + affectedServiceCount: fullReleasePlan.affectedServiceCount ?? null, + scopeReview: fullReleasePlan.scopeReview ?? null, + } + : fullReleasePlan; + return { + ok: result.exitCode === 0 && remote.ok === true, + action: `platform-infra-pipelines-as-code-release-${action}`, + mutation: action === "trigger" && remote.mutation === true, + mode: action, + target: targetSummary(target), + consumer: consumerObservationSummary(consumer), + repository: repositorySummary(repository), + error: typeof remote.error === "string" ? remote.error : undefined, + releasePlan, + webhook: action === "trigger" ? remote.webhook ?? null : null, + remote: result.exitCode === 0 ? undefined : { exitCode: result.exitCode, stderrTail: result.stderr.slice(-1200) }, + next: action === "plan" + ? scopeExpanded + ? `fix the source range, planner, or owning YAML, then rerun: bun scripts/cli.ts platform-infra pipelines-as-code release plan --target ${target.id} --consumer ${consumer.id} --source-commit ${options.sourceCommit}${options.baseCommit === null ? "" : ` --base-commit ${options.baseCommit}`}` + : `bun scripts/cli.ts platform-infra pipelines-as-code release trigger --target ${target.id} --consumer ${consumer.id} --source-commit ${options.sourceCommit}${options.baseCommit === null ? "" : ` --base-commit ${options.baseCommit}`} --confirm` + : typeof remote.error === "string" + ? `fix the source range, planner, or owning YAML, then rerun: bun scripts/cli.ts platform-infra pipelines-as-code release plan --target ${target.id} --consumer ${consumer.id} --source-commit ${options.sourceCommit}${options.baseCommit === null ? "" : ` --base-commit ${options.baseCommit}`}` + : `bun scripts/cli.ts platform-infra pipelines-as-code closeout --target ${target.id} --consumer ${consumer.id} --source-commit ${options.sourceCommit} --wait`, + valuesPrinted: false, + }; +} + async function bootstrap(config: UniDeskConfig, options: ApplyOptions): Promise> { const pac = readPacConfig({ consumerId: options.consumerId, selectDefault: true }); const target = resolveTarget(pac, options.targetId); @@ -1649,7 +1737,8 @@ function bootstrapResult( warnings, next: options.confirm ? { - deliveryTrigger: `Merge the prepared GitHub PR for ${mirror.upstream.repository}@${mirror.upstream.branch}; the automatic chain owns mirror, PaC, Tekton, GitOps, Argo, and runtime convergence.`, + sourceMerge: `Merge the prepared GitHub PR for ${mirror.upstream.repository}@${mirror.upstream.branch}; merge only updates source authority and does not create a PipelineRun.`, + releasePlan: `After merge, run bun scripts/cli.ts platform-infra pipelines-as-code release plan --target ${target.id} --consumer ${consumer.id} --source-commit ; run release trigger --confirm only when scopeReview.status is exact.`, investigate: `bun scripts/cli.ts platform-infra pipelines-as-code status --target ${target.id} --consumer ${consumer.id}`, } : { @@ -2284,7 +2373,7 @@ export class PacReleaseManifestRenderError extends Error { } } -function remoteScript(action: "apply" | "status" | "history" | "diagnose-regression" | "debug-step" | "disable-automatic-webhooks", pac: PacConfig, target: PacTarget, repository: PacRepository, consumer: PacConsumer, options: ApplyOptions | HistoryOptions, secrets: SecretMaterial, releaseManifest: string, historyConsumers: PacConsumer[] = [consumer], collectArtifactLogs = false, webhookRepositories: PacRepository[] = [repository]): string { +function remoteScript(action: "apply" | "status" | "history" | "diagnose-regression" | "debug-step" | "disable-automatic-webhooks" | "manual-release-plan" | "manual-release-trigger", pac: PacConfig, target: PacTarget, repository: PacRepository, consumer: PacConsumer, options: ApplyOptions | HistoryOptions, secrets: SecretMaterial, releaseManifest: string, historyConsumers: PacConsumer[] = [consumer], collectArtifactLogs = false, webhookRepositories: PacRepository[] = [repository], manualRelease?: { sourceCommit: string; baseCommit: string | null; sourceBranch: string; imageRepositories: string[]; runtimeService: string; planningNodeModules: string; artifactCatalogPath: string; gitopsBranch: string; serviceIds: readonly string[]; registryPrefix: string; baseImage: string }): string { const webhookUrl = `${pac.gitea.internalBaseUrl.replace(/\/+$/u, "").replace(/gitea-http\.[^.]+\.svc\.cluster\.local:3000/u, `${pac.release.controllerServiceName}.${pac.release.namespace}.svc.cluster.local:${pac.release.controllerServicePort}`)}`; const admissionIdentity = pacAdmissionDesiredIdentity(target.id); const rbacIdentity = pacConsumerRbacDesiredIdentity(target.id); @@ -2298,9 +2387,11 @@ function remoteScript(action: "apply" | "status" | "history" | "diagnose-regress UNIDESK_PAC_TARGET_ID: target.id, UNIDESK_PAC_TARGET_NAMESPACE: consumer.namespace, UNIDESK_PAC_CONSUMER_ID: consumer.id, + UNIDESK_PAC_CONSUMER_LANE: consumer.lane, UNIDESK_PAC_RELEASE_NAMESPACE: pac.release.namespace, UNIDESK_PAC_RELEASE_MANIFEST_B64: Buffer.from(releaseManifest, "utf8").toString("base64"), UNIDESK_PAC_CONTROLLER_SERVICE_NAME: pac.release.controllerServiceName, + UNIDESK_PAC_CONTROLLER_SERVICE_PORT: String(pac.release.controllerServicePort), UNIDESK_PAC_WAIT_TIMEOUT_SECONDS: String(pac.release.waitTimeoutSeconds), UNIDESK_PAC_READ_STEP_TIMEOUT_SECONDS: String(Math.max(1, Math.ceil(pac.observability.readOnlyCapture.timeoutMs / 1_000))), UNIDESK_PAC_DRY_RUN: "dryRun" in options && options.dryRun ? "1" : "0", @@ -2311,6 +2402,18 @@ function remoteScript(action: "apply" | "status" | "history" | "diagnose-regress UNIDESK_PAC_GITEA_OWNER: repository.owner, UNIDESK_PAC_GITEA_REPO: repository.repo, UNIDESK_PAC_WEBHOOK_REPOSITORIES_JSON: JSON.stringify(webhookRepositories.map((item) => ({ id: item.id, owner: item.owner, repo: item.repo }))), + UNIDESK_PAC_MANUAL_SOURCE_COMMIT: manualRelease?.sourceCommit ?? "", + UNIDESK_PAC_MANUAL_BASE_COMMIT: manualRelease?.baseCommit ?? "", + UNIDESK_PAC_MANUAL_SOURCE_BRANCH: manualRelease?.sourceBranch ?? "", + UNIDESK_PAC_MANUAL_IMAGE_REPOSITORIES_JSON: JSON.stringify(manualRelease?.imageRepositories ?? []), + UNIDESK_PAC_MANUAL_RUNTIME_SERVICE: manualRelease?.runtimeService ?? "", + UNIDESK_PAC_MANUAL_RENDERER: consumer.sourceArtifact?.renderer ?? "generic", + UNIDESK_PAC_MANUAL_NODE_MODULES: manualRelease?.planningNodeModules ?? "", + UNIDESK_PAC_MANUAL_ARTIFACT_CATALOG_PATH: manualRelease?.artifactCatalogPath ?? "", + UNIDESK_PAC_MANUAL_GITOPS_BRANCH: manualRelease?.gitopsBranch ?? "", + UNIDESK_PAC_MANUAL_SERVICE_IDS: (manualRelease?.serviceIds ?? []).join(","), + UNIDESK_PAC_MANUAL_REGISTRY_PREFIX: manualRelease?.registryPrefix ?? "", + UNIDESK_PAC_MANUAL_BASE_IMAGE: manualRelease?.baseImage ?? "", UNIDESK_PAC_WEBHOOK_URL: webhookUrl, UNIDESK_PAC_WEBHOOK_SECRET: secrets.webhookSecret, UNIDESK_PAC_REPOSITORY_NAME: repository.name, @@ -3328,7 +3431,7 @@ export function renderStatus(result: Record): RenderedCliResult "NEXT", ` full: ${stringValue(record(result.next).status)} --full`, ` all-history: ${stringValue(record(result.next).history).replace(/ --consumer [^ ]+/u, "")} --limit 10`, - ` fix-automatic-delivery: ${stringValue(record(record(result.next).fixAutomaticDelivery).reference)}`, + ` fix-manual-release: ${stringValue(record(record(result.next).fixManualRelease).reference)}`, ]; return rendered(result, "platform-infra pipelines-as-code status", lines); } @@ -3691,6 +3794,39 @@ function parseApplyOptions(args: string[]): ApplyOptions { return { ...parseCommonOptions(commonArgs), confirm, dryRun: dryRun || !confirm, wait }; } +function parseManualReleaseOptions(args: string[]): ManualReleaseOptions { + let targetId: string | null = null; + let consumerId: string | null = null; + let sourceCommit: string | null = null; + let baseCommit: string | null = null; + let confirm = false; + let full = false; + let raw = false; + let json = false; + for (let index = 0; index < args.length; index += 1) { + const arg = args[index]; + if (arg === "--target") targetId = optionValue(args, ++index, arg); + else if (arg === "--consumer") consumerId = optionValue(args, ++index, arg); + else if (arg === "--source-commit") sourceCommit = optionValue(args, ++index, arg); + else if (arg === "--base-commit") baseCommit = optionValue(args, ++index, arg); + else if (arg === "--confirm") confirm = true; + else if (arg === "--full") full = true; + else if (arg === "--raw") raw = true; + else if (arg === "--json") json = true; + else throw new Error(`unsupported release option: ${arg}`); + } + if (consumerId === null) throw new Error("release requires --consumer "); + if (sourceCommit === null || !/^[0-9a-f]{40}$/iu.test(sourceCommit)) throw new Error("release requires --source-commit <40-hex-sha>"); + if (baseCommit !== null && !/^[0-9a-f]{40}$/iu.test(baseCommit)) throw new Error("--base-commit must be a 40-hex SHA"); + return { targetId, consumerId, sourceCommit: sourceCommit.toLowerCase(), baseCommit: baseCommit?.toLowerCase() ?? null, confirm, full, raw, json }; +} + +function optionValue(args: readonly string[], index: number, option: string): string { + const value = args[index]; + if (value === undefined || value.startsWith("--")) throw new Error(`${option} requires a value`); + return value; +} + function parseCloseoutOptions(args: string[]): CloseoutOptions { const commonArgs: string[] = []; let sourceCommit: string | null = null; diff --git a/scripts/src/platform-infra-sub2rank.ts b/scripts/src/platform-infra-sub2rank.ts index b1122cab..05d83ec3 100644 --- a/scripts/src/platform-infra-sub2rank.ts +++ b/scripts/src/platform-infra-sub2rank.ts @@ -72,7 +72,7 @@ function plan(options: OpsCommonOptions): Record { sourceRemoteMatches: sub2rank.application.sourceRemoteMatches, publication: { supported: sub2rank.delivery.enabled, - authority: "GitHub PR merge -> Gitea snapshot -> PaC -> Tekton -> GitOps -> Argo", + authority: "GitHub PR merge -> Gitea snapshot -> CLI release plan -> CLI manual PaC webhook -> Tekton -> GitOps -> Argo", branch: delivery.sourceBranch, pipeline: delivery.pipeline.name, },