Merge remote-tracking branch 'origin/master'

This commit is contained in:
pikastech
2026-07-21 13:59:23 +02:00
5 changed files with 67 additions and 16 deletions
+2
View File
@@ -69,6 +69,8 @@ bun scripts/cli.ts hwlab nodes control-plane legacy-cicd --help
- PR merge、push 和 branch update 只更新 source authority,不创建 PipelineRun
- 先运行 `release plan`,检查本次请求范围、累计有效范围、env reuse、待构建镜像数量与列表、rollout 范围和全部受影响服务;
- 当前 GitOps catalog 落后 source 时,plan 必须把累计发布欠账与本次 base 到 source 的请求范围分开显示;
- HWLAB `release plan` 省略 `--base-commit` 时必须先恢复 catalog,并使用 `catalogSourceCommitId` 作为范围基线;禁止回退到 source parent
- 显式 `--base-commit` 只用于诊断请求范围;输出必须披露 `rangeBaseline.catalogAligned`expanded 的动态 `next` 必须直接给出 catalog-aligned replan
- 有效范围大于请求范围时标记 `expanded``release trigger` 必须在发送 webhook 前拒绝;
- 计划范围扩大时先修复 source range、源码、YAML 或项目 planner,再重新 plan
- `package.json` 即使同时出现在 runtime deps 与 component paths,也只按运行字段做一次语义判定;scripts-only 变化不得进入服务 changed paths
@@ -152,6 +152,11 @@ L2 与 L3 发布必须使用 `plan -> 人工审阅 -> trigger` 的受控手动
- 与 Argo 和 promotion 使用同一 GitOps authority
- Source mirror 只用于读取 source commit,不得代替 GitOps authority
- catalog 无法读取、来源不一致或落后于已部署 revision 时,停止触发并返回重新 plan 的只读引导。
- 发布范围基线必须来自 catalog source
- 调用方省略 `base commit` 时,必须使用恢复出的 `catalogSourceCommitId`
- 禁止静默回退到 `source commit` 的 parent
- 显式 `base commit` 只覆盖请求范围诊断,输出必须同时披露 catalog source 和两者是否对齐;
- 缺少有效 catalog source 时必须保持只读并 fail closed,不得猜测 parent 或创建 PipelineRun。
范围相对 owning YAML 声明的发布意图扩大时,`plan` 必须明确列出新增项。操作者应先修正源码、配置或 planner,使范围回到预期后重新生成计划;不得通过确认参数、白名单、忽略 warning 或扩大资源预算继续触发。
@@ -323,6 +323,9 @@ test("L2/L3 release exposes plan first and trigger only sends the PaC webhook",
expect(planAction).toContain("requestedScope");
expect(planAction).toContain("service.envChangedPaths");
expect(planAction).toContain("service.codeChangedPaths");
expect(planAction).toContain("rangeBaseline");
expect(planAction).toContain("base_source=catalog-source");
expect(planAction.indexOf("restore-artifact-catalog.mjs")).toBeLessThan(planAction.indexOf("base_source=catalog-source"));
expect(planAction).toContain("expandedServices");
expect(planAction).toContain("buildServices");
expect(planAction).toContain("rolloutServices");
@@ -347,6 +350,7 @@ test("PaC release scoped help stays on the manual plan-before-trigger path", asy
expect(JSON.stringify(root)).toContain("范围非空");
expect(JSON.stringify(release)).toContain("scopeReview.status=exact");
expect(JSON.stringify(release)).toContain("终态 no-op");
expect(JSON.stringify(release)).toContain("rangeBaseline.catalogAligned=true");
expect(JSON.stringify(plan)).toContain("release plan --target <NODE>");
expect(JSON.stringify(plan)).toContain("envReuse");
expect(JSON.stringify(plan)).toContain("计划非空");
@@ -358,13 +362,16 @@ test("PaC release scoped help stays on the manual plan-before-trigger path", asy
test("PaC failed or expanded plan never points next to trigger", () => {
const args = ["plan", "NC01", "hwlab-nc01-v03", "a".repeat(40), "b".repeat(40)] as const;
expect(manualReleaseNext(...args, { planSucceeded: false, scopeExpanded: false, scopeEmpty: false, remoteError: false })).toContain("then rerun");
expect(manualReleaseNext(...args, { planSucceeded: true, scopeExpanded: true, scopeEmpty: false, remoteError: false })).toContain("then rerun");
expect(manualReleaseNext(...args, { planSucceeded: true, scopeExpanded: false, scopeEmpty: false, remoteError: false })).toContain("release trigger");
const noOp = manualReleaseNext(...args, { planSucceeded: true, scopeExpanded: false, scopeEmpty: true, remoteError: false });
expect(manualReleaseNext(...args, { planSucceeded: false, scopeExpanded: false, scopeEmpty: false, catalogBaseCommit: null, remoteError: false })).toContain("then rerun");
const catalogBase = "c".repeat(40);
const expanded = manualReleaseNext(...args, { planSucceeded: true, scopeExpanded: true, scopeEmpty: false, catalogBaseCommit: catalogBase, remoteError: false });
expect(expanded).toContain(`--base-commit ${catalogBase}`);
expect(expanded).not.toContain(`--base-commit ${args[4]}`);
expect(manualReleaseNext(...args, { planSucceeded: true, scopeExpanded: false, scopeEmpty: false, catalogBaseCommit: null, remoteError: false })).toContain("release trigger");
const noOp = manualReleaseNext(...args, { planSucceeded: true, scopeExpanded: false, scopeEmpty: true, catalogBaseCommit: null, remoteError: false });
expect(noOp).toContain("no release trigger required");
expect(noOp).not.toContain("release trigger --target");
const observed = manualReleaseNext("trigger", args[1], args[2], args[3], args[4], { planSucceeded: true, scopeExpanded: false, scopeEmpty: false, remoteError: false });
const observed = manualReleaseNext("trigger", args[1], args[2], args[3], args[4], { planSucceeded: true, scopeExpanded: false, scopeEmpty: false, catalogBaseCommit: null, remoteError: false });
expect(observed).toContain("delivery-observe");
expect(observed).toContain("a".repeat(40));
expect(observed).not.toContain("closeout");
@@ -247,10 +247,7 @@ manual_release_plan_json() {
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_file="$tmp/changed-paths.txt"
git -C "$tmp/source" diff --name-only "$base_commit" "$source_commit" >"$changed_paths_file"
base_source=explicit
domain_plan_file="$tmp/domain-plan.json"
if [ "$UNIDESK_PAC_MANUAL_RENDERER" = "hwlab-runtime-lane" ]; then
if [ ! -d "$UNIDESK_PAC_MANUAL_NODE_MODULES" ]; then
@@ -267,6 +264,24 @@ manual_release_plan_json() {
HWLAB_SERVICES="$UNIDESK_PAC_MANUAL_SERVICE_IDS" \
node scripts/ci/restore-artifact-catalog.mjs
)
if [ -z "$base_commit" ]; then
base_commit=$(node - "$tmp/source/$UNIDESK_PAC_MANUAL_ARTIFACT_CATALOG_PATH.authority.json" <<'NODE'
const fs = require('node:fs');
const path = process.argv[2];
const authority = JSON.parse(fs.readFileSync(path, 'utf8'));
const commit = String(authority.catalogSourceCommitId || '');
if (!/^[0-9a-f]{40}$/u.test(commit)) {
process.stderr.write('HWLAB release plan requires a valid catalogSourceCommitId\n');
process.exit(1);
}
process.stdout.write(commit);
NODE
)
base_source=catalog-source
fi
git -C "$tmp/source" cat-file -e "$base_commit^{commit}"
changed_paths_file="$tmp/changed-paths.txt"
git -C "$tmp/source" diff --name-only "$base_commit" "$source_commit" >"$changed_paths_file"
(cd "$tmp/source" && node scripts/ci-plan.mjs \
--lane "$UNIDESK_PAC_CONSUMER_LANE" \
--base-ref "$base_commit" \
@@ -276,9 +291,16 @@ manual_release_plan_json() {
--registry-prefix "$UNIDESK_PAC_MANUAL_REGISTRY_PREFIX" \
--base-image "$UNIDESK_PAC_MANUAL_BASE_IMAGE") >"$domain_plan_file"
else
if [ -z "$base_commit" ]; then
base_commit=$(git -C "$tmp/source" rev-parse "$source_commit^")
base_source=source-parent
fi
git -C "$tmp/source" cat-file -e "$base_commit^{commit}"
changed_paths_file="$tmp/changed-paths.txt"
git -C "$tmp/source" diff --name-only "$base_commit" "$source_commit" >"$changed_paths_file"
printf 'null' >"$domain_plan_file"
fi
SOURCE_COMMIT="$source_commit" BASE_COMMIT="$base_commit" node - "$changed_paths_file" "$domain_plan_file" <<'NODE'
SOURCE_COMMIT="$source_commit" BASE_COMMIT="$base_commit" BASE_SOURCE="$base_source" node - "$changed_paths_file" "$domain_plan_file" <<'NODE'
const fs = require('node:fs');
const changedPaths = fs.readFileSync(process.argv[2], 'utf8').split('\n').filter(Boolean);
const domain = JSON.parse(fs.readFileSync(process.argv[3], 'utf8'));
@@ -310,6 +332,14 @@ const plan = {
changedPathCount: changedPaths.length,
changedPaths,
artifactCatalog: domain?.artifactCatalog || null,
rangeBaseline: {
mode: process.env.BASE_SOURCE,
baseCommit: process.env.BASE_COMMIT,
catalogSourceCommitId: domain?.artifactCatalog?.catalogSourceCommitId || null,
catalogAligned: domain?.artifactCatalog?.catalogSourceCommitId
? domain.artifactCatalog.catalogSourceCommitId === process.env.BASE_COMMIT
: null,
},
requestedScope: {
affectedServiceCount: requestedServices.length,
affectedServices: requestedServices,
@@ -579,10 +579,10 @@ function help(scope: string | null): Record<string, unknown> {
configTruth: configLabel,
usage: action === "plan" ? [planUsage] : action === "trigger" ? [triggerUsage] : [planUsage, triggerUsage],
boundary: "PR merge 只更新 source authority,不创建 PipelineRun。必须先 plan;仅当 scopeReview.status=exact 且 imageBuildCount、rolloutServiceCount、affectedServiceCount 至少一项非零时才可 trigger。三项均为零是终态 no-op,不发送 webhook。trigger 只发送 PaC webhook,由 PaC 创建 PipelineRun。",
review: ["requestedScope", "effective", "envReuse", "imageBuildCount", "rolloutServiceCount", "affectedServiceCount", "scopeReview"],
review: ["rangeBaseline", "requestedScope", "effective", "envReuse", "imageBuildCount", "rolloutServiceCount", "affectedServiceCount", "scopeReview"],
next: action === "trigger"
? "先执行 release plan,并确认 scopeReview.status=exact 且计划非空;范围扩大时修复 source range、planner、源码或 owning YAML 后重新 plan,三项范围计数均为零时无需 trigger。"
: "先执行 release plan仅在 scopeReview.status=exact 且计划非空执行 release trigger --confirm,三项范围计数均为零时无需 trigger。",
? "先执行 release plan,并确认 rangeBaseline.catalogAligned=true、scopeReview.status=exact 且计划非空;省略 --base-commit 时 HWLAB 使用 catalog source,范围扩大时按 next 重新 plan,三项范围计数均为零时无需 trigger。"
: "先执行 release planHWLAB 默认以 catalog source 为范围基线,确认 rangeBaseline.catalogAligned=true、scopeReview.status=exact 且计划非空后才执行 release trigger --confirm,三项范围计数均为零时无需 trigger。",
mutation: action === "trigger" ? "explicit-confirm-required" : false,
valuesPrinted: false,
};
@@ -1659,10 +1659,13 @@ async function manualRelease(config: UniDeskConfig, action: "plan" | "trigger",
releasePlan,
webhook: action === "trigger" ? remote.webhook ?? null : null,
remote: result.exitCode === 0 ? undefined : { exitCode: result.exitCode, stderrTail: result.stderr.slice(-1200) },
next: manualReleaseNext(action, target.id, consumer.id, options.sourceCommit, options.baseCommit, {
next: manualReleaseNext(action, target.id, consumer.id, options.sourceCommit, typeof fullReleasePlan?.baseCommit === "string" ? fullReleasePlan.baseCommit : options.baseCommit, {
planSucceeded,
scopeExpanded,
scopeEmpty,
catalogBaseCommit: typeof fullReleasePlan?.artifactCatalog?.catalogSourceCommitId === "string"
? fullReleasePlan.artifactCatalog.catalogSourceCommitId
: null,
remoteError: typeof remote.error === "string",
}),
valuesPrinted: false,
@@ -1682,6 +1685,7 @@ function compactManualReleasePlan(plan: Record<string, any>): Record<string, unk
catalogSourceCommitId: plan.artifactCatalog?.catalogSourceCommitId ?? null,
coverage: plan.artifactCatalog?.coverage ?? null,
},
rangeBaseline: plan.rangeBaseline ?? null,
requestedScope: plan.requestedScope ?? null,
envReuse: plan.envReuse ?? null,
imageBuildCount: plan.imageBuildCount ?? null,
@@ -1701,9 +1705,12 @@ export function manualReleaseNext(
consumerId: string,
sourceCommit: string,
baseCommit: string | null,
outcome: { readonly planSucceeded: boolean; readonly scopeExpanded: boolean; readonly scopeEmpty: boolean; readonly remoteError: boolean },
outcome: { readonly planSucceeded: boolean; readonly scopeExpanded: boolean; readonly scopeEmpty: boolean; readonly catalogBaseCommit: string | null; readonly remoteError: boolean },
): string {
const baseSuffix = baseCommit === null ? "" : ` --base-commit ${baseCommit}`;
const nextBaseCommit = outcome.scopeExpanded && outcome.catalogBaseCommit !== null
? outcome.catalogBaseCommit
: baseCommit;
const baseSuffix = nextBaseCommit === null ? "" : " --base-commit " + nextBaseCommit;
const plan = `bun scripts/cli.ts platform-infra pipelines-as-code release plan --target ${targetId} --consumer ${consumerId} --source-commit ${sourceCommit}${baseSuffix}`;
if (outcome.scopeEmpty) return "no release trigger required: plan has zero build, rollout, and affected services";
if (action === "plan") {