diff --git a/.agents/skills/unidesk-cicd/SKILL.md b/.agents/skills/unidesk-cicd/SKILL.md index 2b3b34af..a984f3f7 100644 --- a/.agents/skills/unidesk-cicd/SKILL.md +++ b/.agents/skills/unidesk-cicd/SKILL.md @@ -71,6 +71,7 @@ bun scripts/cli.ts hwlab nodes control-plane legacy-cicd --help - 当前 GitOps catalog 落后 source 时,plan 必须把累计发布欠账与本次 base 到 source 的请求范围分开显示; - 有效范围大于请求范围时标记 `expanded`,`release trigger` 必须在发送 webhook 前拒绝; - 计划范围扩大时先修复 source range、源码、YAML 或项目 planner,再重新 plan; + - artifact catalog 必须从 owning YAML 声明、且与 Argo/promotion 相同的 GitOps read URL 与 branch 恢复;禁止用 source mirror 中的同名 branch 代替 GitOps authority; - 审阅通过后运行 `release trigger --confirm`;该命令只向 PaC 发送 webhook,由 PaC 创建 PipelineRun; - 禁止裸创建 PipelineRun、由 CLI 直建 PipelineRun,或恢复 Gitea 自动 hook; - L3 继续要求对应 L2 已通过和用户对当次生产发布的明确授权。 diff --git a/config/platform-infra/pipelines-as-code.yaml b/config/platform-infra/pipelines-as-code.yaml index 27181539..cb054067 100644 --- a/config/platform-infra/pipelines-as-code.yaml +++ b/config/platform-infra/pipelines-as-code.yaml @@ -1001,6 +1001,7 @@ templates: params: git_read_url: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-HWLAB.git git_write_url: http://git-mirror-write.devops-infra.svc.cluster.local:8080/pikasTech/HWLAB.git + gitops_read_url: http://git-mirror-http.devops-infra.svc.cluster.local:8080/pikasTech/HWLAB.git source_branch: v0.3 gitops_branch: v0.3-gitops source_snapshot_prefix: refs/unidesk/snapshots/gitea-actions/hwlab-node-runtime/v0.3 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 6eb8a8c5..e4e61d3a 100644 --- a/project-management/PJ2026-01/specs/PJ2026-010601-controlled-release.md +++ b/project-management/PJ2026-01/specs/PJ2026-010601-controlled-release.md @@ -146,6 +146,13 @@ L2 与 L3 发布必须使用 `plan -> 人工审阅 -> trigger` 的受控手动 `plan` 必须是只读操作,并针对精确 target、lane 和 source commit 输出:env identity 及 reuse/build 判定、待构建镜像数量与服务列表、待 rollout 服务、全部受影响服务、范围基线和相对基线新增项。`plan` 不得同步 mirror、创建 snapshot、修改 branch、创建 PipelineRun 或写入运行面。 +`plan` 恢复 artifact catalog 时必须满足: + +- 读取 owning YAML 声明的 GitOps read URL 与 branch; +- 与 Argo 和 promotion 使用同一 GitOps authority; +- Source mirror 只用于读取 source commit,不得代替 GitOps authority; +- catalog 无法读取、来源不一致或落后于已部署 revision 时,停止触发并返回重新 plan 的只读引导。 + 范围相对 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/scripts/src/platform-infra-pipelines-as-code-bootstrap.test.ts b/scripts/src/platform-infra-pipelines-as-code-bootstrap.test.ts index ee5cde54..16779b7d 100644 --- a/scripts/src/platform-infra-pipelines-as-code-bootstrap.test.ts +++ b/scripts/src/platform-infra-pipelines-as-code-bootstrap.test.ts @@ -316,6 +316,12 @@ test("L2/L3 release exposes plan first and trigger only sends the PaC webhook", expect(planAction).toContain("expandedServices"); expect(planAction).toContain("buildServices"); expect(planAction).toContain("rolloutServices"); + expect(planAction).toContain('gitops_read_url=$(cluster_service_url "$UNIDESK_PAC_MANUAL_GITOPS_READ_URL")'); + expect(planAction).toContain('HWLAB_GIT_READ_URL="$gitops_read_url"'); + expect(planAction).not.toContain('HWLAB_GIT_READ_URL="$UNIDESK_PAC_REPOSITORY_URL"'); + const pac = readPacConfig({ consumerId: "hwlab-nc01-v03", selectDefault: true }); + expect(pac.repositories.find((repository) => repository.id === "hwlab-nc01-v03")?.params.gitops_read_url) + .toBe("http://git-mirror-http.devops-infra.svc.cluster.local:8080/pikasTech/HWLAB.git"); expect(triggerAction).toContain("release-plan-scope-expanded"); expect(triggerAction).not.toContain("kubectl create"); expect(triggerAction).not.toContain("PipelineRun"); diff --git a/scripts/src/platform-infra-pipelines-as-code-remote.sh b/scripts/src/platform-infra-pipelines-as-code-remote.sh index 4d688c0b..4923dd6b 100644 --- a/scripts/src/platform-infra-pipelines-as-code-remote.sh +++ b/scripts/src/platform-infra-pipelines-as-code-remote.sh @@ -115,6 +115,22 @@ prepare_gitea_api_base() { export UNIDESK_PAC_GITEA_API_BASE_URL } +cluster_service_url() { + value=$1 + hostport=$(printf '%s' "$value" | sed -n 's#^https\?://\([^/]*\).*$#\1#p') + host=${hostport%%:*} + case "$host" in + *.svc.cluster.local) + service=${host%%.*} + rest=${host#*.} + namespace=${rest%%.*} + cluster_ip=$(kubectl -n "$namespace" get service "$service" -o jsonpath='{.spec.clusterIP}') + printf '%s' "$value" | sed "s#$host#$cluster_ip#" + ;; + *) printf '%s' "$value" ;; + esac +} + gitea_api() { method="$1" path="$2" @@ -242,11 +258,12 @@ manual_release_plan_json() { return 1 fi ln -s "$UNIDESK_PAC_MANUAL_NODE_MODULES" "$tmp/source/node_modules" + gitops_read_url=$(cluster_service_url "$UNIDESK_PAC_MANUAL_GITOPS_READ_URL") ( 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_GIT_READ_URL="$gitops_read_url" \ HWLAB_SERVICES="$UNIDESK_PAC_MANUAL_SERVICE_IDS" \ node scripts/ci/restore-artifact-catalog.mjs ) diff --git a/scripts/src/platform-infra-pipelines-as-code.ts b/scripts/src/platform-infra-pipelines-as-code.ts index af37b0e0..8261e588 100644 --- a/scripts/src/platform-infra-pipelines-as-code.ts +++ b/scripts/src/platform-infra-pipelines-as-code.ts @@ -1630,6 +1630,7 @@ async function manualRelease(config: UniDeskConfig, action: "plan" | "trigger", runtimeService: params.runtime_deployment ?? consumer.id, planningNodeModules, artifactCatalogPath: hwlabLane?.catalogPath ?? "", + gitopsReadUrl: params.gitops_read_url ?? params.git_read_url ?? repository.cloneUrl, gitopsBranch: params.gitops_branch ?? hwlabLane?.gitopsBranch ?? "", serviceIds: hwlabLane?.serviceIds ?? [], registryPrefix: hwlabLane?.registryPrefix ?? "", @@ -2423,7 +2424,7 @@ export class PacReleaseManifestRenderError extends Error { } } -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 { +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; gitopsReadUrl: 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); @@ -2460,6 +2461,7 @@ function remoteScript(action: "apply" | "status" | "history" | "diagnose-regress 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_READ_URL: manualRelease?.gitopsReadUrl ?? "", UNIDESK_PAC_MANUAL_GITOPS_BRANCH: manualRelease?.gitopsBranch ?? "", UNIDESK_PAC_MANUAL_SERVICE_IDS: (manualRelease?.serviceIds ?? []).join(","), UNIDESK_PAC_MANUAL_REGISTRY_PREFIX: manualRelease?.registryPrefix ?? "",