Merge pull request #2755 from pikasTech/fix/2721-release-plan-catalog-authority

fix: 从 GitOps authority 生成手动发布计划
This commit is contained in:
Lyon
2026-07-21 15:43:42 +08:00
committed by GitHub
6 changed files with 36 additions and 2 deletions
+1
View File
@@ -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 已通过和用户对当次生产发布的明确授权。
@@ -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
@@ -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 发送 webhookPaC 从同一 source authority 创建 commit-pinned PipelineRun。触发前必须先审阅当前输入的 plan;计划输入或范围已变化时必须重新 plan。禁止 CLI 直建或裸创建 PipelineRun、由 PR 合并回调 trigger,或为兼容旧自动链保留第二入口。
@@ -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");
@@ -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
)
@@ -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 ?? "",