From 7b049ecdbd5aab8a913404b4d53d3c46e92a7f4c Mon Sep 17 00:00:00 2001 From: pikastech Date: Tue, 21 Jul 2026 06:01:46 +0200 Subject: [PATCH] fix(cicd): disable automatic PaC webhooks --- config/platform-infra/pipelines-as-code.yaml | 9 ++++- .../specs/PJ2026-010601-controlled-release.md | 21 +++++++++- ...026-01060106-hwlab-dev-production-lanes.md | 26 ++++++------ ...26-01060107-nc01-ci-resource-governance.md | 18 ++++++--- ...60308-cicd-yaml-first-target-governance.md | 32 ++++++++++----- ...-infra-pipelines-as-code-bootstrap.test.ts | 3 ++ ...platform-infra-pipelines-as-code-remote.sh | 29 +++++--------- .../src/platform-infra-pipelines-as-code.ts | 40 ++++++++++++++----- 8 files changed, 118 insertions(+), 60 deletions(-) diff --git a/config/platform-infra/pipelines-as-code.yaml b/config/platform-infra/pipelines-as-code.yaml index 3f5e7d8a..27181539 100644 --- a/config/platform-infra/pipelines-as-code.yaml +++ b/config/platform-infra/pipelines-as-code.yaml @@ -17,6 +17,7 @@ metadata: - 2165 - 2274 - 2628 + - 2721 defaults: targetId: NC01 consumerId: hwlab-nc01-v03 @@ -24,6 +25,9 @@ display: timeZone: Asia/Shanghai deliveryTiming: endToEndBudgetSeconds: 120 +deliveryTrigger: + mode: manual-plan-confirm + mechanism: pac-webhook observability: configRef: config/platform-infra/observability.yaml readOnlyCapture: @@ -119,12 +123,12 @@ targets: - id: JD01 route: JD01:k3s namespace: agentrun-ci - role: active-cicd-trigger-authority + role: manual-cicd-trigger-authority enabled: true - id: NC01 route: NC01:k3s namespace: agentrun-ci - role: active-cicd-trigger-authority + role: manual-cicd-trigger-authority enabled: true gitea: configRef: "config/platform-infra/gitea.yaml#sourceAuthority.repositories" @@ -137,6 +141,7 @@ gitea: passwordLine: 2 apiUsername: unidesk-admin webhook: + enabled: false secretRoot: /root/.unidesk/.env secretSourceRef: pipelines-as-code.webhook events: diff --git a/project-management/PJ2026-01/specs/PJ2026-010601-controlled-release.md b/project-management/PJ2026-01/specs/PJ2026-010601-controlled-release.md index a4e53a73..6eb8a8c5 100644 --- a/project-management/PJ2026-01/specs/PJ2026-010601-controlled-release.md +++ b/project-management/PJ2026-01/specs/PJ2026-010601-controlled-release.md @@ -58,6 +58,8 @@ | GitOps desired state | GitOps branch 中声明的目标运行面资源状态。 | | runtime live state | 目标 namespace 中实际运行的 workload、image、config、health 和 readiness 状态。 | | env image reuse | 基础执行环境镜像按 env identity 复用,普通业务源码变更只改变 boot commit。 | +| 发布计划 | 对精确 source commit 和目标 lane 的只读发布影响分析,至少包含 env reuse、待构建镜像、rollout 服务和范围变化。 | +| 手动发布触发 | 操作者审阅发布计划后,通过受控 CLI 向 PaC 发送 webhook,由 PaC 创建 commit-pinned PipelineRun 的唯一发布 mutation。 | | 综合联调 | 在真实目标运行面、真实 SecretRef、真实 backend/provider 或服务入口上完成的发布候选验证。 | ## 4. 系统边界和接口 @@ -66,8 +68,8 @@ | 边界项 | 内容 | | --- | --- | -| 外部使用者 | 平台管理员、发布操作人员、服务维护者、CI/CD 自动化任务和需要发布状态的业务模块。 | -| 外部输入 | source commit、deploy 配置、lane 选择、镜像构建请求、GitOps promotion 请求、rollout 请求、SecretRef presence 和验证请求。 | +| 外部使用者 | 平台管理员、发布操作人员、服务维护者和需要发布状态的业务模块。 | +| 外部输入 | source commit、deploy 配置、lane 选择、发布计划请求、带计划指纹的手动触发请求、SecretRef presence 和验证请求。 | | 受控资源 | Tekton PipelineRun、Argo Application、runtime namespace、workload、image、release status、health/readiness 和发布判定结果。 | | 外部输出 | PipelineRun 状态、image digest、promotion revision、Argo sync 状态、runtime readiness、发布候选结论和 redacted 失败信息。 | | 用户接口 | UniDesk CI/CD CLI、服务自有 CLI 发布/验证相关入口、服务 health/readiness、Tekton/Argo 受控状态查询入口。 | @@ -83,6 +85,7 @@ | PJ2026-01060102 | 镜像Promotion | 本规格 6.2 | env image reuse、digest-pinned image、artifact promotion 和 image readiness | Source commit、Containerfile、registry | 需要运行镜像的服务 | | PJ2026-01060103 | 发布判定 | 本规格 6.3 | source/GitOps/runtime 一致性、health/readiness 和 migration/SecretRef presence | CI/CD、GitOps、YAML运维 | 平台管理员、业务模块 | | PJ2026-01060104 | 验证分层 | 本规格 6.4 | 自测试、综合联调、CLI/API 交互和真实运行面通过口径 | 目标 runtime、业务模块测试需求 | 发布决策 | +| PJ2026-01060108 | 手动发布 | 本规格 6.5 | L2/L3 发布计划、范围审阅和带计划指纹的受控手动触发 | 源码同步、YAML运维、镜像Promotion | 全部 L2/L3 runtime 服务 | | PJ2026-01060105 | AgentRun发布 | [PJ2026-01060105 AgentRun发布Lane](PJ2026-01060105-agentrun-v01-release-lane.md) | AgentRun `v0.1` 的发布 lane、Pipeline、runtime namespace 和真实联调细则 | 源码同步、YAML运维、Agent编排 | AgentRun runtime | | PJ2026-01060106 | HWLAB双环境 | [PJ2026-01060106 HWLAB双环境](PJ2026-01060106-hwlab-dev-production-lanes.md) | HWLAB `v0.3` development 与 `release` production 的分支、流水线、公开入口和数据隔离 | 源码同步、YAML运维、Workbench实时权威 | HWLAB runtime | | PJ2026-01060107 | NC01 CI资源治理 | [PJ2026-01060107 NC01 CI资源治理](PJ2026-01060107-nc01-ci-resource-governance.md) | NC01 构建并发、排队、资源合同、稳定去重和核心运行面保护 | 发布流水、YAML运维、CI/CD目标治理 | NC01 CI 与核心服务 | @@ -132,3 +135,17 @@ PipelineRun 成功、Argo Synced、health 可访问或 source check 通过都不 发布流水应区分组件自测试和综合联调:自测试允许 mock,用于快速反馈;综合联调必须在真实目标运行面、真实依赖、真实配置和服务原入口上完成。 mock、fake dependency、source-only、dry-run、缺关键配置时的 skip、只读 health 成功或没有真实业务终态的运行面状态,都不能作为综合联调或发布通过证据。具体服务需要哪些真实依赖和终态,由对应服务专项规格定义。 + +### 6.5 OPS-RELEASE-REQ-005 L2/L3手动计划与触发 + +| 编号 | 短名 | 主责模块 | 关联模块 | +| --- | --- | --- | --- | +| OPS-RELEASE-REQ-005 | 手动发布 | PJ2026-01060108 手动发布 | [源码同步](PJ2026-010602-source-sync.md)、[YAML运维](PJ2026-010603-yaml-first-ops.md)、[NC01 CI资源治理](PJ2026-01060107-nc01-ci-resource-governance.md) | + +L2 与 L3 发布必须使用 `plan -> 人工审阅 -> trigger` 的受控手动链。PR merge、push、branch update、mirror 同步、定时任务和 L0/L1 开发动作只能更新 source authority 或只读状态,不得创建 PipelineRun、构建镜像、推进 GitOps 或触发 rollout。 + +`plan` 必须是只读操作,并针对精确 target、lane 和 source commit 输出:env identity 及 reuse/build 判定、待构建镜像数量与服务列表、待 rollout 服务、全部受影响服务、范围基线和相对基线新增项。`plan` 不得同步 mirror、创建 snapshot、修改 branch、创建 PipelineRun 或写入运行面。 + +范围相对 owning YAML 声明的发布意图扩大时,`plan` 必须明确列出新增项。操作者应先修正源码、配置或 planner,使范围回到预期后重新生成计划;不得通过确认参数、白名单、忽略 warning 或扩大资源预算继续触发。 + +唯一 mutation 是受控手动 `trigger`。它必须显式选择 target、lane、精确 source commit 和 pipeline intent,并向 PaC 发送 webhook;PaC 从同一 source authority 创建 commit-pinned PipelineRun。触发前必须先审阅当前输入的 plan;计划输入或范围已变化时必须重新 plan。禁止 CLI 直建或裸创建 PipelineRun、由 PR 合并回调 trigger,或为兼容旧自动链保留第二入口。 diff --git a/project-management/PJ2026-01/specs/PJ2026-01060106-hwlab-dev-production-lanes.md b/project-management/PJ2026-01/specs/PJ2026-01060106-hwlab-dev-production-lanes.md index 78095709..a5446297 100644 --- a/project-management/PJ2026-01/specs/PJ2026-01060106-hwlab-dev-production-lanes.md +++ b/project-management/PJ2026-01/specs/PJ2026-01060106-hwlab-dev-production-lanes.md @@ -25,7 +25,7 @@ | 关联规格 | [PJ2026-010602 源码同步](PJ2026-010602-source-sync.md)、[PJ2026-010603 YAML运维](PJ2026-010603-yaml-first-ops.md)、[PJ2026-010401080313 Workbench实时权威](PJ2026-010401080313-workbench-realtime-authority.md) | | 规格治理索引 | [规格治理](spec-governance.md) | -本文定义 HWLAB 在 NC01 上的 development/production 双环境。所有可调事实由 owning YAML 控制,自动发布只由目标 source branch 的 PR merge/branch update 驱动,禁止人工补跑 PipelineRun、手工 Argo sync、运行时 patch 或从集群反解配置。 +本文定义 HWLAB 在 NC01 上的 development/production 双环境。所有可调事实由 owning YAML 控制,L2/L3 发布只允许通过受控 `plan` 和手动 `trigger` 执行;PR merge/branch update 不得创建 PipelineRun。禁止裸补跑 PipelineRun、手工 Argo sync、运行时 patch 或从集群反解配置。 ## 2. 目的和范围 @@ -39,7 +39,7 @@ - development 的 NC01 公网 IP HTTP 入口与 production 的 `https://hwlab.pikapython.com` 入口。 - 共用 NC01 host PostgreSQL 服务时,两套独立 database、role、Secret consumer 和 migration ledger。 - 两套独立 Kafka consumer group identity,以及纯 Kafka实时/回放权威不被数据库迁移阻塞的边界。 -- `v0.3` 到 `release` 的受控晋升、自动部署和原入口验证。 +- `v0.3` 到 `release` 的受控晋升、手动计划发布和原入口验证。 ### 2.3 范围外 @@ -54,7 +54,7 @@ | --- | --- | | development lane | source 为 `v0.3`、保留当前开发 namespace、通过 NC01 公网 IP 空闲 HTTP 端口访问的 HWLAB 环境。 | | production lane | source 为 `release`、使用独立 production namespace、通过 `https://hwlab.pikapython.com` 访问的 HWLAB 环境。 | -| 晋升 | 把已在 development 完成原入口验证的精确 commit 纳入 `release`,随后由 production 自动链发布。 | +| 晋升 | 把已在 development 完成原入口验证的精确 commit 纳入 `release`;分支推进不发布,仍需对 production 单独 plan 和手动 trigger。 | | 数据域隔离 | 两个环境不共享 PostgreSQL database、role、Secret consumer 或 migration ledger,任何 schema/data mutation 只作用于选中 lane。 | | 入口所有权 | owning YAML 中唯一声明某 public origin 的 consumer;同一 host/origin 不得同时归 development 和 production。 | @@ -96,7 +96,7 @@ flowchart LR Kafka event 是实时与回放 authority。数据库只保存各 lane 的业务数据和派生读模型;任一数据库 schema 缺失不得改变 Kafka direct publish/live/replay 路径,也不得成为 Cloud API 启动或自动滚动前置。 -### 4.3 晋升与自动发布时序 +### 4.3 晋升与手动发布时序 ```mermaid sequenceDiagram @@ -106,16 +106,18 @@ sequenceDiagram participant R as release participant PC as production CI/CD participant PV as production 原入口验证 - D->>DC: merge/update commit C - DC->>DV: 自动构建、GitOps、rollout + D->>DC: merge/update commit C,不触发发布 + DC->>DC: plan C,审阅 env reuse 与构建范围 + DC->>DV: 手动 trigger C,构建、GitOps、rollout DV-->>D: C 验证通过 D->>R: 受控晋升精确 commit C - R->>PC: branch update 自动触发 - PC->>PV: 自动构建、GitOps、rollout + R->>PC: branch update,不触发发布 + PC->>PC: plan C,审阅 env reuse 与构建范围 + PC->>PV: 获得当次授权后手动 trigger C PV-->>R: digest/source/runtime/入口证据 ``` -初始 `release` 分支只能从完成 P0 纯 Kafka纠偏并在 development 原入口验证通过的 `v0.3` commit 创建。不得从已知强制 PostgreSQL transactional projector 的事故 commit 创建,也不得把 production 自动链的缺失用人工 PipelineRun 或 runtime patch 填补。 +初始 `release` 分支只能从完成 P0 纯 Kafka纠偏并在 development 原入口验证通过的 `v0.3` commit 创建。不得从已知强制 PostgreSQL transactional projector 的事故 commit 创建,也不得绕过 plan 和受控 trigger,用裸 PipelineRun 或 runtime patch 填补发布链。 ## 5. 配置与隔离 @@ -150,11 +152,11 @@ owning YAML 至少声明: ### 6.1 HWLAB-LANE-REQ-001 Development迁移 -`v0.3` 自动链必须继续部署到 development namespace,并迁移到 YAML 声明的 NC01 公网 IP HTTP 入口。迁移完成后 `https://hwlab.pikapython.com` 只指向 production,不再路由到 development。 +`v0.3` 的受控手动发布链必须部署到 development namespace,并迁移到 YAML 声明的 NC01 公网 IP HTTP 入口。迁移完成后 `https://hwlab.pikapython.com` 只指向 production,不再路由到 development。 -### 6.2 HWLAB-LANE-REQ-002 Production自动链 +### 6.2 HWLAB-LANE-REQ-002 Production手动发布链 -`release` 必须拥有独立 PaC consumer、Tekton Pipeline/ServiceAccount、GitOps branch/path、Argo Application、namespace、image provenance 和 release status。`release` update 自动触发完整链,不依赖主代理或值班人员人工补跑。 +`release` 必须拥有独立 PaC consumer、Tekton Pipeline/ServiceAccount、GitOps branch/path、Argo Application、namespace、image provenance 和 release status。`release` update 不得触发完整链;获得当次 L3 授权后,发布人员必须先审阅精确 commit 的 plan,再用受控 trigger 发布,禁止裸补跑。 ### 6.3 HWLAB-LANE-REQ-003 Source与制品对齐 diff --git a/project-management/PJ2026-01/specs/PJ2026-01060107-nc01-ci-resource-governance.md b/project-management/PJ2026-01/specs/PJ2026-01060107-nc01-ci-resource-governance.md index 021d56e6..78e469a5 100644 --- a/project-management/PJ2026-01/specs/PJ2026-01060107-nc01-ci-resource-governance.md +++ b/project-management/PJ2026-01/specs/PJ2026-01060107-nc01-ci-resource-governance.md @@ -31,7 +31,7 @@ 单个 HWLAB PipelineRun 曾同时展开 14 个 `build-services` TaskRun,使 8 核 NC01 的 load、Swap 和 k3s Kine/SQLite 延迟同时上升,并造成核心 API 约四分钟 502。PaC Repository 的 `concurrencyLimit` 只限制 Repository 触发层的并发 PipelineRun,不能限制一个 PipelineRun 内 matrix 展开的 TaskRun 数量。 -治理目标是让每个发布意图只产生一个受控运行,并让单次运行中的构建工作按 YAML 预算进入有界 worker 队列。CI 压力下,k3s 控制面、CoreDNS、Sub2API 和 Public Edge 的可用性优先于构建吞吐。 +治理目标是让 PR merge 和 branch update 不再产生发布运行;每个经人工审阅的发布意图只产生一个受控运行,并让单次运行中的构建工作按 YAML 预算进入有界 worker 队列。CI 压力下,k3s 控制面、CoreDNS、Sub2API 和 Public Edge 的可用性优先于构建吞吐。 ## 3. 架构与数据流 @@ -46,7 +46,7 @@ flowchart LR K --> C[核心运行面保护] ``` -Repository 触发排队和 Pipeline 内构建排队是两个独立层次。前者继续由 PaC Repository `concurrencyLimit` 管理,后者由 renderer 将目标 matrix 收敛为一个受资源约束的 TaskRun,并在 Pod 内按 `maxParallelServices` 执行有界 worker 队列。不得复制大型内联 `taskSpec`,不得把二者合并为同名字段,也不得创建数据库、ConfigMap 或常驻控制器作为第二队列 authority。 +受控手动发布触发和 Pipeline 内构建排队是两个独立层次。前者由发布 `plan`、人工审阅和带计划指纹的 `trigger` 管理,PaC Repository `concurrencyLimit` 只作为运行并发上限;后者由 renderer 将目标 matrix 收敛为一个受资源约束的 TaskRun,并在 Pod 内按 `maxParallelServices` 执行有界 worker 队列。不得复制大型内联 `taskSpec`,不得把二者合并为同名字段,也不得创建数据库、ConfigMap 或常驻控制器作为第二队列 authority。 ## 4. 配置合同 @@ -57,6 +57,8 @@ HWLAB node/lane target 的 `ciResourceGovernance` 至少声明: - `build.stepResources`:构建工具 step 的 CPU/内存 requests 与 limits。 - `build.sidecarResources`:BuildKit sidecar 的 CPU/内存 requests 与 limits。 - `build.priorityClassName`:构建 Pod 的可抢占优先级;必须低于核心运行面。 +- `deliveryTrigger.mode`:L2/L3 固定为 `manual-plan-confirm`,禁止配置自动 push、merge 或 branch follower 触发。 +- `deliveryTrigger.mechanism`:固定为 `pac-webhook`;受控 CLI 发送 webhook,禁止直建 PipelineRun。 - `dedupe.identityFields`:稳定意图键字段,固定为 repository、lane、source commit 和 pipeline intent。 - `coreProtection`:核心服务优先级和最小资源预算的配置引用;各服务事实仍归自身 owning YAML,不复制进 HWLAB lane YAML。 @@ -74,7 +76,7 @@ Renderer 必须只对 YAML 选中的构建 matrix 建立有界 worker 队列。 ### 5.3 CI-RESOURCE-REQ-003 稳定去重 -自动触发的发布意图键固定为 `repository + lane + source commit + pipeline intent`。相同键已处于 queued、running 或 succeeded 时,自动 poll 不得再创建第二次构建。人工重跑必须使用显式受控入口,携带独立、可审计的 rerun intent;不得通过删除旧 PipelineRun、修改 label 或人工创建 PipelineRun 绕过去重。 +手动触发的发布意图键固定为 `repository + lane + source commit + pipeline intent`。相同键已处于 queued、running 或 succeeded 时,受控入口不得再创建第二次构建。人工重跑必须使用显式受控入口,携带独立、可审计的 rerun intent,并先重新 plan;不得通过删除旧 PipelineRun、修改 label 或人工创建 PipelineRun 绕过去重。 ### 5.4 CI-RESOURCE-REQ-004 核心运行面保护 @@ -82,12 +84,16 @@ CoreDNS 和 k3s 控制面使用 Kubernetes 系统关键优先级。Sub2API 和 ### 5.5 CI-RESOURCE-REQ-005 状态与诊断 -`plan`、`status` 和 `diagnosis` 必须在一次输出中披露:YAML configRef、触发层 concurrency、构建层 max parallel、批次数、运行/排队 TaskRun 数、step/sidecar 资源合同、priority class、稳定意图键状态和核心保护摘要。共享读取超时只能投影为 warning 或 unavailable,不得把全部 consumer 伪装为真实失败。 +`plan` 必须针对精确 source commit 在一次只读输出中披露:YAML configRef、env identity、env reuse/build 判定、待构建镜像数量与服务列表、待 rollout 服务、全部受影响服务、范围基线、新增范围、触发层 concurrency、构建层 max parallel、批次数、step/sidecar 资源合同、priority class 和核心保护摘要。`status` 和 `diagnosis` 必须披露运行/排队 TaskRun 数与稳定意图键状态。共享读取超时只能投影为 warning 或 unavailable,不得把全部 consumer 伪装为真实失败。 + +### 5.6 CI-RESOURCE-REQ-006 中心自动触发禁用 + +中心 PaC owning YAML 必须声明 `deliveryTrigger.mode=manual-plan-confirm` 和 `deliveryTrigger.mechanism=pac-webhook`。PaC webhook 接收端继续作为唯一 PipelineRun 创建入口,但 Gitea push hook、PR merge callback、branch follower、poller 和其他自动发送方不得调用它;PR merge 后只允许 source mirror 或 authority 更新。受控 apply 必须删除或禁用 Gitea 自动 hook,并在 status 中把自动发送方启用视为配置漂移。 ## 6. 验收 - L0:配置解析、资源 quantity、requests/limits 关系、matrix 收敛、有界 worker 队列和依赖保持通过轻量验证。 - L1:受控 `plan/status/diagnosis` 显示 YAML 来源、并发预算、批次、资源和保护状态,不依赖裸 Kubernetes 命令。 -- L2:由正常 HWLAB source PR merge 自动触发,禁止人工补 PipelineRun;观察到同一时刻构建 TaskRun 不超过 YAML 预算,相同 source commit 的自动 poll 不产生第二次构建,所有构建 Pod 带资源合同,并且 NC01 Node、CoreDNS、Sub2API、Public Edge 和公开 API 在构建期间保持健康。 +- L2:正常 HWLAB source PR merge 后不产生 PipelineRun。受控 `plan` 显示 env reuse、镜像构建数量、精确构建与 rollout 范围且没有非预期扩大;随后使用手动 `trigger` 向 PaC 发送一次 webhook,并由 PaC 创建一次 PipelineRun。观察到同一时刻构建 TaskRun 不超过 YAML 预算,相同发布意图不产生第二次构建,所有构建 Pod 带资源合同,并且 NC01 Node、CoreDNS、Sub2API、Public Edge 和公开 API 在构建期间保持健康。 -验收失败时保留自动链证据并修复 owning YAML 或 renderer。重启 k3s、删除 Pod/PipelineRun、手工 Argo sync、运行时 patch、mirror flush 或人工补跑均不能作为最终通过证据。 +验收失败时保留计划、手动触发链和运行证据,并修复 owning YAML、planner 或 renderer。重启 k3s、删除 Pod/PipelineRun、手工 Argo sync、运行时 patch、mirror flush 或裸补跑均不能作为最终通过证据。 diff --git a/project-management/PJ2026-01/specs/PJ2026-01060308-cicd-yaml-first-target-governance.md b/project-management/PJ2026-01/specs/PJ2026-01060308-cicd-yaml-first-target-governance.md index afb145fb..cd7c44cd 100644 --- a/project-management/PJ2026-01/specs/PJ2026-01060308-cicd-yaml-first-target-governance.md +++ b/project-management/PJ2026-01/specs/PJ2026-01060308-cicd-yaml-first-target-governance.md @@ -152,9 +152,9 @@ sequenceDiagram participant Argo participant Runtime CLI->>Target: resolve CI/CD target - CLI->>Mirror: read/sync/flush via controlled entry - Mirror-->>CLI: refs and pendingFlush summary - CLI->>Tekton: PipelineRun status or dry-run manifest + CLI->>Mirror: read source authority + Mirror-->>CLI: refs and source summary + CLI->>Tekton: PipelineRun status or read-only plan Tekton-->>CLI: PipelineRun/TaskRun bounded summary CLI->>Argo: Application sync/health status Argo-->>Runtime: desired runtime state @@ -173,15 +173,17 @@ sequenceDiagram participant Mirror as git-mirror cache participant Tekton CLI->>Target: resolve node/lane/sourceAuthority - CLI->>Resolver: ensure or read source snapshot - Resolver->>Mirror: sync/read selected commit and create stageRef - Mirror-->>Resolver: selectedCommit + stageRef + objectPresent - Resolver-->>CLI: sourceSnapshot summary - CLI->>Tekton: create PipelineRun with sourceSnapshot + CLI->>Resolver: read selected commit and calculate plan + Resolver->>Mirror: read selected commit and object presence + Mirror-->>Resolver: selectedCommit + objectPresent + Resolver-->>CLI: env reuse + build scope + CLI->>PaC: manual webhook after plan review + Resolver->>Mirror: create immutable stageRef + PaC->>Tekton: create PipelineRun with sourceSnapshot Tekton->>Mirror: fetch/checkout immutable stageRef or snapshot artifact ``` -正式 CI/CD 的源码身份以 source snapshot 为准。`trigger-current`、image publish、GitOps render 和 closeout 不得再从 operator host 的 dirty/stale worktree、当前 branch tip 或 GitHub direct fetch 推导本轮 source identity。upstream fetch 只允许发生在受控 k8s git-mirror resolver/sync 组件内;PipelineRun 只消费 node-local mirror 的不可变 stageRef、PVC 或 bundle artifact。 +正式 CI/CD 的源码身份以 source snapshot 为准。只读 `plan` 不得同步 mirror、创建 snapshot 或修改运行面;手动 `trigger` 在完成当前 plan 审阅后才可创建本轮不可变 snapshot。`trigger-current`、image publish、GitOps render 和 closeout 不得再从 operator host 的 dirty/stale worktree、当前 branch tip 或 GitHub direct fetch 推导本轮 source identity。upstream fetch 只允许发生在受控 k8s git-mirror resolver/sync 组件内;PipelineRun 只消费 node-local mirror 的不可变 stageRef、PVC 或 bundle artifact。 ## 6. 原子需求 @@ -251,6 +253,18 @@ PipelineRun source step 不得以 mutable `refs/heads/` 作为本轮源 status/closeout 必须同时展示 selected snapshot、PipelineRun snapshot、latest mirror head、GitOps revision 和 runtime provenance。branch 在发布过程中前进只能标记为 `latest-drift` 或 `superseded`,不得把 selected commit 的成功发布误判为本轮失败。 +### 6.11 OPS-TARGET-REQ-011 L2/L3手动触发配置 + +| 编号 | 短名 | 主责模块 | 关联模块 | +| --- | --- | --- | --- | +| OPS-TARGET-REQ-011 | 手动触发 | PJ2026-01060308 YAML目标治理 | [发布流水](PJ2026-010601-controlled-release.md)、[NC01 CI资源治理](PJ2026-01060107-nc01-ci-resource-governance.md) | + +CI/CD owning YAML 必须为 L2/L3 target 声明 `deliveryTrigger.mode=manual-plan-confirm` 和 `deliveryTrigger.mechanism=pac-webhook`。这些字段是发布触发方式的唯一真相;PaC webhook 接收端保留,Gitea 自动 hook 必须移除。代码、环境变量、Repository annotation 和 consumer 私有配置不得恢复自动 push、PR merge、branch update、poll 或 follow 发送行为。 + +只读 `plan` 必须从 YAML 和精确 source commit 解析 env reuse、待构建镜像、rollout 服务、全部受影响服务、范围基线和新增范围。发现新增范围时必须先修复配置或 planner 后重新 plan,不得使用强制参数越过。 + +受控手动 `trigger` 必须同时接收 target、consumer 或 lane、精确 source commit 和 pipeline intent,并向 PaC 发送 webhook;CLI 不得直接创建 PipelineRun。输入、YAML、env identity 或构建/rollout 范围变化后必须重新 plan。PR merge 后不自动 trigger,L0/L1 操作也不得调用该 mutation。 + ### 6.8 OPS-TARGET-REQ-008 k3s server 重启门禁非阻塞 | 编号 | 短名 | 主责模块 | 关联模块 | 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 63b65246..bb2050de 100644 --- a/scripts/src/platform-infra-pipelines-as-code-bootstrap.test.ts +++ b/scripts/src/platform-infra-pipelines-as-code-bootstrap.test.ts @@ -217,6 +217,9 @@ test("PaC apply checks the Gitea repository before dry-run return and cluster mu expect(remote).toContain('"admission":{"applied":%s,"ready":%s}'); expect(remote).toContain('"admissionProvenance":%s'); expect(remote).toContain('"error":"gitea-repository-missing"'); + expect(remote).toContain("remove_automatic_webhooks"); + expect(remote).not.toContain("ensure_webhook"); + expect(remote).not.toContain('"active":true'); const statusAction = remote.slice(remote.indexOf("status_action() {"), remote.indexOf("history_action() {")); const bootstrapReadyAssignment = statusAction.split("\n").find((line) => line.includes("bootstrap_ready=$(")); expect(bootstrapReadyAssignment).toBeDefined(); diff --git a/scripts/src/platform-infra-pipelines-as-code-remote.sh b/scripts/src/platform-infra-pipelines-as-code-remote.sh index a8b9ab4a..3be242ed 100644 --- a/scripts/src/platform-infra-pipelines-as-code-remote.sh +++ b/scripts/src/platform-infra-pipelines-as-code-remote.sh @@ -178,7 +178,7 @@ ensure_token() { printf '%s' "$out" | sed -n 's/.*"sha1"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' } -ensure_webhook() { +remove_automatic_webhooks() { hooks=$(gitea_api GET "repos/$UNIDESK_PAC_GITEA_OWNER/$UNIDESK_PAC_GITEA_REPO/hooks") hook_ids=$(HOOKS_JSON="$hooks" node <<'NODE' const data = JSON.parse(process.env.HOOKS_JSON || '[]'); @@ -186,23 +186,10 @@ const url = process.env.UNIDESK_PAC_WEBHOOK_URL; for (const item of data) if (item?.config?.url === url && item?.id) console.log(item.id); NODE ) - hook_id=$(printf '%s\n' "$hook_ids" | sed -n '1p') - body=$(printf '{"type":"gitea","config":{"url":"%s","content_type":"json","secret":"%s"},"events":["push"],"active":true}' "$UNIDESK_PAC_WEBHOOK_URL" "$UNIDESK_PAC_WEBHOOK_SECRET") - if [ -n "$hook_id" ]; then - if ! gitea_api PATCH "repos/$UNIDESK_PAC_GITEA_OWNER/$UNIDESK_PAC_GITEA_REPO/hooks/$hook_id" "$body" >/dev/null; then - gitea_api DELETE "repos/$UNIDESK_PAC_GITEA_OWNER/$UNIDESK_PAC_GITEA_REPO/hooks/$hook_id" >/dev/null || true - created=$(gitea_api POST "repos/$UNIDESK_PAC_GITEA_OWNER/$UNIDESK_PAC_GITEA_REPO/hooks" "$body") - hook_id=$(printf '%s' "$created" | sed -n 's/.*"id"[[:space:]]*:[[:space:]]*\([0-9][0-9]*\).*/\1/p') - fi - printf '%s\n' "$hook_ids" | sed -n '2,$p' | while IFS= read -r duplicate_id; do - [ -n "$duplicate_id" ] || continue - gitea_api DELETE "repos/$UNIDESK_PAC_GITEA_OWNER/$UNIDESK_PAC_GITEA_REPO/hooks/$duplicate_id" >/dev/null || true - done - else - created=$(gitea_api POST "repos/$UNIDESK_PAC_GITEA_OWNER/$UNIDESK_PAC_GITEA_REPO/hooks" "$body") - hook_id=$(printf '%s' "$created" | sed -n 's/.*"id"[[:space:]]*:[[:space:]]*\([0-9][0-9]*\).*/\1/p') - fi - printf '%s' "$hook_id" + printf '%s\n' "$hook_ids" | while IFS= read -r hook_id; do + [ -n "$hook_id" ] || continue + gitea_api DELETE "repos/$UNIDESK_PAC_GITEA_OWNER/$UNIDESK_PAC_GITEA_REPO/hooks/$hook_id" >/dev/null + done } repository_manifest() { @@ -404,7 +391,8 @@ apply_action() { kubectl -n "$UNIDESK_PAC_ARGO_NAMESPACE" annotate application "$UNIDESK_PAC_ARGO_APPLICATION" argocd.argoproj.io/refresh=hard --overwrite >/dev/null argo_bootstrap=true fi - hook_id=$(ensure_webhook) + remove_automatic_webhooks + hook_id="" ready=false if wait_ready; then ready=true; fi prepare_admission_provenance_state @@ -414,7 +402,8 @@ apply_action() { fi ok=false if [ "$ready" = true ]; then ok=true; fi - printf '{"ok":%s,"mode":"confirmed","mutation":true,"controllerReady":%s,"admission":{"applied":%s,"ready":%s},"admissionProvenance":%s,"argoBootstrap":%s,"preflight":{"repositoryExists":true,"httpStatus":"2xx","repository":"%s/%s","valuesPrinted":false},"webhook":{"present":true,"id":"%s","url":"%s"},"repository":"%s","namespace":"%s","valuesPrinted":false}\n' "$ok" "$ready" "$admission_applied" "$admission_ready" "$UNIDESK_PAC_ADMISSION_STATE_JSON" "$argo_bootstrap" "$(json_string "$UNIDESK_PAC_GITEA_OWNER")" "$(json_string "$UNIDESK_PAC_GITEA_REPO")" "$(json_string "$hook_id")" "$(json_string "$UNIDESK_PAC_WEBHOOK_URL")" "$(json_string "$UNIDESK_PAC_REPOSITORY_NAME")" "$(json_string "$UNIDESK_PAC_TARGET_NAMESPACE")" + webhook_present=false + printf '{"ok":%s,"mode":"confirmed","mutation":true,"controllerReady":%s,"admission":{"applied":%s,"ready":%s},"admissionProvenance":%s,"argoBootstrap":%s,"preflight":{"repositoryExists":true,"httpStatus":"2xx","repository":"%s/%s","valuesPrinted":false},"webhook":{"present":%s,"id":"%s","url":"%s"},"repository":"%s","namespace":"%s","valuesPrinted":false}\n' "$ok" "$ready" "$admission_applied" "$admission_ready" "$UNIDESK_PAC_ADMISSION_STATE_JSON" "$argo_bootstrap" "$(json_string "$UNIDESK_PAC_GITEA_OWNER")" "$(json_string "$UNIDESK_PAC_GITEA_REPO")" "$webhook_present" "$(json_string "$hook_id")" "$(json_string "$UNIDESK_PAC_WEBHOOK_URL")" "$(json_string "$UNIDESK_PAC_REPOSITORY_NAME")" "$(json_string "$UNIDESK_PAC_TARGET_NAMESPACE")" } condition_status() { diff --git a/scripts/src/platform-infra-pipelines-as-code.ts b/scripts/src/platform-infra-pipelines-as-code.ts index 2e5a8970..de1a70b0 100644 --- a/scripts/src/platform-infra-pipelines-as-code.ts +++ b/scripts/src/platform-infra-pipelines-as-code.ts @@ -113,6 +113,10 @@ export interface PacConfig { timeZone: string; }; deliveryTiming: PacDeliveryTimingPolicy; + deliveryTrigger: { + mode: "manual-plan-confirm"; + mechanism: "pac-webhook"; + }; observability: { configRef: string; readOnlyCapture: { @@ -171,6 +175,7 @@ export interface PacConfig { apiUsername: string; }; webhook: { + enabled: false; secretRoot: string; secretSourceRef: string; events: string[]; @@ -616,6 +621,7 @@ export function parsePacConfigDocument( const gitea = y.objectField(root, "gitea", ""); const display = y.objectField(root, "display", ""); const deliveryTiming = y.objectField(root, "deliveryTiming", ""); + const deliveryTrigger = y.objectField(root, "deliveryTrigger", ""); const observability = y.objectField(root, "observability", ""); const readOnlyCapture = y.objectField(observability, "readOnlyCapture", "observability"); const observabilitySampling = y.objectField(observability, "sampling", "observability"); @@ -655,6 +661,10 @@ export function parsePacConfigDocument( endToEndBudgetSeconds: positiveInteger(deliveryTiming, "endToEndBudgetSeconds", "deliveryTiming"), configPath: `${configLabel}#deliveryTiming.endToEndBudgetSeconds`, }, + deliveryTrigger: { + mode: y.enumField(deliveryTrigger, "mode", "deliveryTrigger", ["manual-plan-confirm"] as const), + mechanism: y.enumField(deliveryTrigger, "mechanism", "deliveryTrigger", ["pac-webhook"] as const), + }, observability: { configRef: y.stringField(observability, "configRef", "observability"), readOnlyCapture: { @@ -717,6 +727,7 @@ export function parsePacConfigDocument( apiUsername: y.stringField(admin, "apiUsername", "gitea.admin"), }, webhook: { + enabled: parseFalse(webhook, "enabled", "gitea.webhook"), secretRoot: y.absolutePathField(webhook, "secretRoot", "gitea.webhook"), secretSourceRef: y.sourceRefField(webhook, "secretSourceRef", "gitea.webhook"), events: y.stringArrayField(webhook, "events", "gitea.webhook"), @@ -1254,7 +1265,7 @@ function validateConfig( warnings.push(pacConfigWarning("pac-unselected-consumer-invalid", "consumer", consumer.id, `consumers.${consumer.id}`, error)); } } - if (!config.gitea.webhook.events.includes("push")) throw new Error(`${configLabel}.gitea.webhook.events must include push`); + if (!config.gitea.webhook.events.includes("push")) throw new Error(`${configLabel}.gitea.webhook.events must retain push as the disabled legacy hook identity`); validateTimeZone(config.display.timeZone, `${configLabel}.display.timeZone`); return warnings; } @@ -1474,7 +1485,7 @@ function plan(options: CommonOptions): Record { consumer: consumerObservationSummary(consumer), ciResourceGovernance: ciResourceGovernanceSummary(consumer, repository), secrets, - policy: policyChecks(repository), + policy: policyChecks(pac, repository), warnings: pac.validationWarnings, next: nextCommands(target.id, consumer.id, pac.defaults.consumerId), }; @@ -1604,7 +1615,7 @@ async function status(config: UniDeskConfig, options: CommonOptions, collectArti const next = nextCommands(target.id, consumer.id, pac.defaults.consumerId); const observed = await capturePacReadOnly(config, pac, "status", target, consumer, remoteScript("status", pac, target, repository, consumer, { ...options, confirm: false, dryRun: true, wait: false }, secrets, "", [consumer], collectArtifactLogs), `${stringValue(next.status)} --full`); const { result, parsed, observation } = observed; - const summary = parsed === null ? null : statusSummary(parsed); + const summary = parsed === null ? null : statusSummary(parsed, pac); const ciResourceGovernance = ciResourceGovernanceSummary( consumer, repository, @@ -2503,7 +2514,7 @@ function secretSummaries(pac: PacConfig): Array> { ]; } -function statusSummary(payload: Record): Record { +function statusSummary(payload: Record, pac?: PacConfig): Record { const readError = stringValue(payload.error); const pipelineRuns = arrayRecords(payload.pipelineRuns); const latest = pipelineRuns[0] ?? {}; @@ -2522,6 +2533,8 @@ function statusSummary(payload: Record): Record item.active === true); + const automaticTriggerDrift = pac?.deliveryTrigger.mode === "manual-plan-confirm" && activeWebhooks.length > 0; const rawArtifact = record(payload.artifact); const sourceObservation = record(rawArtifact.sourceObservation); const provenance = record(diagnostics.provenance); @@ -2544,13 +2557,20 @@ function statusSummary(payload: Record): Record 0, + drift: automaticTriggerDrift, + activeWebhookCount: activeWebhooks.length, + configPath: `${configLabel}#deliveryTrigger.mode`, + }, pipelineRuns, latestPipelineRun: latest, taskRuns, @@ -2567,9 +2587,9 @@ function statusSummary(payload: Record): Record> { +function policyChecks(pac: PacConfig, repository: PacRepository): Array> { return [ - { name: "single-path", ok: true, detail: "Gitea webhook -> Pipelines-as-Code -> Tekton -> Argo/k8s runtime; no Gitea Actions/act_runner/branch-follower fallback." }, + { name: "manual-trigger-only", ok: pac.deliveryTrigger.mode === "manual-plan-confirm" && pac.gitea.webhook.enabled === false, detail: "PR merge and push do not create PipelineRuns; the controlled plan/manual trigger path owns L2/L3 release." }, { name: "yaml-source-of-truth", ok: true, detail: `${configLabel} owns PaC release, Repository CR, Gitea webhook and consumer params.` }, { name: "public-repository-url", ok: !isInternalRepositoryMatchUrl(repository.url), detail: "Repository CR url matches the public Gitea webhook payload URL; internal service URLs remain clone/read URLs only." }, { name: "gitea-internal-source", ok: repository.cloneUrl.includes(".svc.cluster.local"), detail: "Tekton source clone uses the internal k3s Gitea service URL." }, @@ -2598,7 +2618,8 @@ function configSummary(pac: PacConfig, consumer: PacConsumer, repository: PacRep release: pac.release, capabilities: capabilitySummary(pac), deliveryTiming: pac.deliveryTiming, - gitea: { configRef: pac.gitea.configRef, internalBaseUrl: pac.gitea.internalBaseUrl, webhookBranch: pac.gitea.webhook.branch }, + deliveryTrigger: pac.deliveryTrigger, + gitea: { configRef: pac.gitea.configRef, internalBaseUrl: pac.gitea.internalBaseUrl, webhookEnabled: pac.gitea.webhook.enabled, webhookBranch: pac.gitea.webhook.branch }, display: pac.display, repositories: pac.repositories.map(repositorySummary), consumers: pac.consumers.map(consumerObservationSummary), @@ -2618,6 +2639,7 @@ function compactConfigSummary(pac: PacConfig, consumer: PacConsumer, repository: sourceUrl: repository.cloneUrl, displayTimeZone: pac.display.timeZone, deliveryTiming: pac.deliveryTiming, + deliveryTrigger: pac.deliveryTrigger, consumer: `${consumer.node}/${consumer.lane}`, consumerId: consumer.id, pipeline: consumer.pipeline, @@ -3083,7 +3105,7 @@ function renderPlan(result: Record): RenderedCliResult { "PATH", ...table(["STAGE", "AUTHORITY", "DETAIL"], [ ["source", "Gitea", stringValue(repository.cloneUrl)], - ["trigger", "Pipelines-as-Code", "Gitea push webhook"], + ["trigger", "Pipelines-as-Code", "controlled manual trigger after reviewed plan"], ["ci", "Tekton", stringValue(consumer.pipeline)], ["cd", "Argo", stringValue(consumer.argoApplication)], ["runtime evidence", "owning YAML", `${stringValue(deliveryObservation.runtimeEvidence)} (${stringValue(deliveryObservation.configPath)})`],