Merge pull request #2742 from pikasTech/fix/2721-release-plan-failure-next
fix(cicd): 修复大计划传输与失败 next
This commit is contained in:
@@ -75,6 +75,7 @@ bun scripts/cli.ts hwlab nodes control-plane legacy-cicd --help
|
||||
- 禁止裸创建 PipelineRun、由 CLI 直建 PipelineRun,或恢复 Gitea 自动 hook;
|
||||
- L3 继续要求对应 L2 已通过和用户对当次生产发布的明确授权。
|
||||
- 每次修改发布入口后,必须复核 `pipelines-as-code --help`、`release --help`、`release plan --help`、`release trigger --help` 与一条动态 `next`:帮助必须完整展示 plan/trigger 的参数、范围审阅字段和 `--confirm` 边界;动态 `next` 只能在范围准确时指向 trigger,范围扩大或触发失败时必须指向修复后重新 plan,不得遗漏或引导自动交付。
|
||||
- release plan 的 changed paths、domain plan 和 webhook payload 等可增长结构化数据必须通过临时文件或 stdin 传递,禁止放入环境变量或命令行参数;默认/`--json` 输出必须有界保留 env reuse、build、rollout、affected 与 scopeReview,完整 changed-path 明细只在显式 `--full|--raw` 披露。
|
||||
|
||||
- 节点级只读状态:
|
||||
- 优先用 `cicd status --node <NODE>`;
|
||||
|
||||
@@ -5,7 +5,7 @@ import { resolve } from "node:path";
|
||||
import type { GiteaConfig, GiteaMirrorRepository } from "./platform-infra-gitea-config";
|
||||
import { renderMirrorBootstrap } from "./platform-infra-gitea-render";
|
||||
import { pacBootstrapYamlMatchFailure, projectPacBootstrapResult, renderPacBootstrap, resolvePacBootstrapMirrorRepository } from "./platform-infra-pipelines-as-code-bootstrap";
|
||||
import { PacReleaseManifestRenderError, parsePacConfigDocument, readPacConfig, renderPacReleaseManifest, runPlatformInfraPipelinesAsCodeCommand, validatePacConfig, validPacConsumers } from "./platform-infra-pipelines-as-code";
|
||||
import { manualReleaseNext, PacReleaseManifestRenderError, parsePacConfigDocument, readPacConfig, renderPacReleaseManifest, runPlatformInfraPipelinesAsCodeCommand, validatePacConfig, validPacConsumers } from "./platform-infra-pipelines-as-code";
|
||||
import { materializeYamlComposition } from "./yaml-composition";
|
||||
|
||||
const mirror: GiteaMirrorRepository = {
|
||||
@@ -318,6 +318,32 @@ test("PaC release scoped help stays on the manual plan-before-trigger path", asy
|
||||
expect(JSON.stringify(trigger)).toContain("先执行 release plan");
|
||||
});
|
||||
|
||||
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, remoteError: false })).toContain("then rerun");
|
||||
expect(manualReleaseNext(...args, { planSucceeded: true, scopeExpanded: true, remoteError: false })).toContain("then rerun");
|
||||
expect(manualReleaseNext(...args, { planSucceeded: true, scopeExpanded: false, remoteError: false })).toContain("release trigger");
|
||||
const remote = readFileSync(resolve(import.meta.dir, "platform-infra-pipelines-as-code-remote.sh"), "utf8");
|
||||
const action = remote.slice(remote.indexOf("manual_release_plan_json()"), remote.indexOf("repository_manifest()"));
|
||||
expect(action).not.toContain('DOMAIN_PLAN="$domain_plan"');
|
||||
expect(action).not.toContain('PLAN_JSON="$plan"');
|
||||
expect(action).not.toContain('PAYLOAD="$payload"');
|
||||
expect(action).toContain('node - "$changed_paths_file" "$domain_plan_file"');
|
||||
expect(action).toContain('--data-binary "@$payload_file"');
|
||||
});
|
||||
|
||||
test("PaC default release plan projection keeps review fields without changed-path payload", () => {
|
||||
const source = readFileSync(resolve(import.meta.dir, "platform-infra-pipelines-as-code.ts"), "utf8");
|
||||
const projector = source.slice(source.indexOf("function compactManualReleasePlan"), source.indexOf("export function manualReleaseNext"));
|
||||
expect(projector).toContain("requestedScope");
|
||||
expect(projector).toContain("envReuse");
|
||||
expect(projector).toContain("buildServices");
|
||||
expect(projector).toContain("rolloutServices");
|
||||
expect(projector).toContain("affectedServices");
|
||||
expect(projector).toContain("scopeReview");
|
||||
expect(projector).not.toContain("changedPaths:");
|
||||
});
|
||||
|
||||
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");
|
||||
|
||||
@@ -233,7 +233,9 @@ manual_release_plan_json() {
|
||||
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")
|
||||
changed_paths_file="$tmp/changed-paths.txt"
|
||||
git -C "$tmp/source" diff --name-only "$base_commit" "$source_commit" >"$changed_paths_file"
|
||||
domain_plan_file="$tmp/domain-plan.json"
|
||||
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
|
||||
@@ -248,20 +250,21 @@ manual_release_plan_json() {
|
||||
HWLAB_SERVICES="$UNIDESK_PAC_MANUAL_SERVICE_IDS" \
|
||||
node scripts/ci/restore-artifact-catalog.mjs
|
||||
)
|
||||
domain_plan=$(cd "$tmp/source" && node scripts/ci-plan.mjs \
|
||||
(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")
|
||||
--base-image "$UNIDESK_PAC_MANUAL_BASE_IMAGE") >"$domain_plan_file"
|
||||
else
|
||||
domain_plan=""
|
||||
printf 'null' >"$domain_plan_file"
|
||||
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;
|
||||
SOURCE_COMMIT="$source_commit" BASE_COMMIT="$base_commit" 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'));
|
||||
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;
|
||||
@@ -320,14 +323,24 @@ manual_release_action() {
|
||||
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");')
|
||||
plan_file=$(mktemp)
|
||||
printf '%s' "$plan" >"$plan_file"
|
||||
scope_expanded=$(node - "$plan_file" <<'NODE'
|
||||
const fs = require('node:fs');
|
||||
const plan = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
|
||||
process.stdout.write(plan.scopeReview?.expanded === true ? '1' : '0');
|
||||
NODE
|
||||
)
|
||||
if [ "$scope_expanded" = "1" ]; then
|
||||
rm -f "$plan_file"
|
||||
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'
|
||||
payload_file=$(mktemp)
|
||||
node - "$plan_file" >"$payload_file" <<'NODE'
|
||||
const fs = require('node:fs');
|
||||
const crypto = require('node:crypto');
|
||||
const plan = JSON.parse(process.env.PLAN_JSON);
|
||||
const plan = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
|
||||
const byKind = { added: [], removed: [], modified: plan.changedPaths };
|
||||
const owner = process.env.UNIDESK_PAC_GITEA_OWNER;
|
||||
const repo = process.env.UNIDESK_PAC_GITEA_REPO;
|
||||
@@ -335,9 +348,14 @@ 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=$(node - "$payload_file" <<'NODE'
|
||||
const fs = require('node:fs');
|
||||
const crypto = require('node:crypto');
|
||||
const payload = fs.readFileSync(process.argv[2]);
|
||||
process.stdout.write(crypto.createHmac('sha256', process.env.UNIDESK_PAC_WEBHOOK_SECRET).update(payload).digest('hex'));
|
||||
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)-$$"
|
||||
@@ -346,9 +364,10 @@ NODE
|
||||
-H 'X-Gitea-Event-Type: push' \
|
||||
-H "X-Gitea-Delivery: $delivery" \
|
||||
-H "X-Gitea-Signature: $signature" \
|
||||
--data-binary "$payload")
|
||||
--data-binary "@$payload_file")
|
||||
case "$http_code" in 2*) ok=true ;; *) ok=false ;; esac
|
||||
rm -f "$response_tmp"
|
||||
rm -f "$plan_file" "$payload_file"
|
||||
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 ]
|
||||
}
|
||||
|
||||
@@ -1638,23 +1638,10 @@ async function manualRelease(config: UniDeskConfig, action: "plan" | "trigger",
|
||||
const remote = parseJsonOutput(result.stdout) ?? {};
|
||||
const fullReleasePlan = remote.plan && typeof remote.plan === "object" ? remote.plan as Record<string, any> : 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;
|
||||
const planSucceeded = result.exitCode === 0 && remote.ok === true && fullReleasePlan !== null;
|
||||
const releasePlan = fullReleasePlan === null || options.full || options.raw
|
||||
? fullReleasePlan
|
||||
: compactManualReleasePlan(fullReleasePlan);
|
||||
return {
|
||||
ok: result.exitCode === 0 && remote.ok === true,
|
||||
action: `platform-infra-pipelines-as-code-release-${action}`,
|
||||
@@ -1667,17 +1654,61 @@ 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: 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`,
|
||||
next: manualReleaseNext(action, target.id, consumer.id, options.sourceCommit, options.baseCommit, {
|
||||
planSucceeded,
|
||||
scopeExpanded,
|
||||
remoteError: typeof remote.error === "string",
|
||||
}),
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
|
||||
function compactManualReleasePlan(plan: Record<string, any>): Record<string, unknown> {
|
||||
return {
|
||||
sourceCommit: plan.sourceCommit ?? null,
|
||||
baseCommit: plan.baseCommit ?? null,
|
||||
sourceBranch: plan.sourceBranch ?? null,
|
||||
changedPathCount: plan.changedPathCount ?? null,
|
||||
artifactCatalog: plan.artifactCatalog === null ? null : {
|
||||
status: plan.artifactCatalog?.status ?? null,
|
||||
authority: plan.artifactCatalog?.authority ?? null,
|
||||
gitopsCommitId: plan.artifactCatalog?.gitopsCommitId ?? null,
|
||||
catalogSourceCommitId: plan.artifactCatalog?.catalogSourceCommitId ?? null,
|
||||
coverage: plan.artifactCatalog?.coverage ?? null,
|
||||
},
|
||||
requestedScope: plan.requestedScope ?? null,
|
||||
envReuse: plan.envReuse ?? null,
|
||||
imageBuildCount: plan.imageBuildCount ?? null,
|
||||
buildServices: plan.buildServices ?? [],
|
||||
rolloutServiceCount: plan.rolloutServiceCount ?? null,
|
||||
rolloutServices: plan.rolloutServices ?? [],
|
||||
affectedServiceCount: plan.affectedServiceCount ?? null,
|
||||
affectedServices: plan.affectedServices ?? [],
|
||||
scopeReview: plan.scopeReview ?? null,
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
|
||||
export function manualReleaseNext(
|
||||
action: "plan" | "trigger",
|
||||
targetId: string,
|
||||
consumerId: string,
|
||||
sourceCommit: string,
|
||||
baseCommit: string | null,
|
||||
outcome: { readonly planSucceeded: boolean; readonly scopeExpanded: boolean; readonly remoteError: boolean },
|
||||
): string {
|
||||
const baseSuffix = baseCommit === null ? "" : ` --base-commit ${baseCommit}`;
|
||||
const plan = `bun scripts/cli.ts platform-infra pipelines-as-code release plan --target ${targetId} --consumer ${consumerId} --source-commit ${sourceCommit}${baseSuffix}`;
|
||||
if (action === "plan") {
|
||||
return outcome.planSucceeded && !outcome.scopeExpanded
|
||||
? `bun scripts/cli.ts platform-infra pipelines-as-code release trigger --target ${targetId} --consumer ${consumerId} --source-commit ${sourceCommit}${baseSuffix} --confirm`
|
||||
: `fix the source range, planner, or owning YAML, then rerun: ${plan}`;
|
||||
}
|
||||
return outcome.remoteError
|
||||
? `fix the source range, planner, or owning YAML, then rerun: ${plan}`
|
||||
: `bun scripts/cli.ts platform-infra pipelines-as-code closeout --target ${targetId} --consumer ${consumerId} --source-commit ${sourceCommit} --wait`;
|
||||
}
|
||||
|
||||
async function bootstrap(config: UniDeskConfig, options: ApplyOptions): Promise<Record<string, unknown>> {
|
||||
const pac = readPacConfig({ consumerId: options.consumerId, selectDefault: true });
|
||||
const target = resolveTarget(pac, options.targetId);
|
||||
|
||||
Reference in New Issue
Block a user