Merge pull request #1972 from pikasTech/fix/pac-runtime-gitops-verify-marker-condition
Pipelines as Code CI / hwlab-web-probe-sentinel-nc01- Success
Pipelines as Code CI / platform-infra-gitea-nc01- Success
Pipelines as Code CI / unidesk-host- Success

fix(pac): runtime GitOps verify marker 不受 willRun 条件吞没
This commit is contained in:
Lyon
2026-07-14 07:28:12 +08:00
committed by GitHub
3 changed files with 82 additions and 12 deletions
@@ -336,10 +336,16 @@ function injectRuntimeGitopsGuard(pipeline) {
const step = steps.find((item) => typeof recordOrNull(item)?.script === "string" && item.script.includes("scripts/gitops-render.mjs"));
if (!step) throw new Error("runtime GitOps guard injection failed: gitops-promote render step missing");
const originalScript = String(step.script);
let script = patchNoBuildNoRolloutSkip(originalScript);
const noBuildSkipPatched = script !== originalScript;
let script = patchNoBuildNoRolloutCondition(originalScript);
const noBuildConditionPatched = script !== originalScript;
const beforeSkipPatch = script;
script = patchNoBuildNoRolloutSkip(script);
const noBuildSkipPatched = script !== beforeSkipPatch;
const beforeInjection = script;
script = injectRuntimeGitopsCommands(script);
if (hasWillRunGitopsPromoteCoupling(script)) {
throw new Error("runtime GitOps guard injection failed: no-build-no-rollout marker remains coupled to willRunGitopsPromote");
}
if (hasNoBuildNoRolloutEarlyExit(script)) {
throw new Error("runtime GitOps guard injection failed: no-build-no-rollout-plan early exit remains after patch");
}
@@ -354,12 +360,20 @@ function injectRuntimeGitopsGuard(pipeline) {
present: true,
configMapName: runtimeGitopsConfigMapName,
stepName: stringOrNull(step.name),
noBuildConditionPatched,
noBuildSkipPatched,
postprocessInjected,
verifyInjected,
};
}
function patchNoBuildNoRolloutCondition(script) {
return String(script).replace(
/const willRunGitopsPromote = plan\.ciCdPlan && plan\.ciCdPlan\.willRunGitopsPromote === true;\nprocess\.exit\(!willRunGitopsPromote && buildServices\.length === 0 && affectedServices\.length === 0 \? 0 : 1\);/u,
"process.exit(buildServices.length === 0 && affectedServices.length === 0 ? 0 : 1);",
);
}
function patchNoBuildNoRolloutSkip(script) {
return String(script).replace(
/(^|\n)([ \t]*)echo '\{"event":"gitops-promote","status":"skipped","reason":"no-build-no-rollout-plan"\}'(?:\s*>\s*&2)?\n[ \t]*exit 0(?=\n|$)/u,
@@ -367,6 +381,10 @@ function patchNoBuildNoRolloutSkip(script) {
);
}
function hasWillRunGitopsPromoteCoupling(script) {
return /process\.exit\(!willRunGitopsPromote && buildServices\.length === 0 && affectedServices\.length === 0 \? 0 : 1\);/u.test(String(script));
}
function hasNoBuildNoRolloutEarlyExit(script) {
return /echo '\{"event":"gitops-promote","status":"skipped","reason":"no-build-no-rollout-plan"\}'(?:\s*>\s*&2)?\n[ \t]*exit 0(?=\n|$)/u.test(String(script));
}
@@ -54,10 +54,16 @@ function injectRuntimeGitopsGuard(pipeline) {
if (!step) throw new Error("runtime GitOps guard injection failed: gitops-promote render step missing");
const originalScript = String(step.script);
let script = patchNoBuildNoRolloutSkip(originalScript);
const noBuildSkipPatched = script !== originalScript;
let script = patchNoBuildNoRolloutCondition(originalScript);
const noBuildConditionPatched = script !== originalScript;
const beforeSkipPatch = script;
script = patchNoBuildNoRolloutSkip(script);
const noBuildSkipPatched = script !== beforeSkipPatch;
const beforeInjection = script;
script = injectRuntimeGitopsCommands(script);
if (hasWillRunGitopsPromoteCoupling(script)) {
throw new Error("runtime GitOps guard injection failed: no-build-no-rollout marker remains coupled to willRunGitopsPromote");
}
if (hasNoBuildNoRolloutEarlyExit(script)) {
throw new Error("runtime GitOps guard injection failed: no-build-no-rollout-plan early exit remains after patch");
}
@@ -70,12 +76,20 @@ function injectRuntimeGitopsGuard(pipeline) {
present: true,
configMapName,
stepName: typeof step.name === "string" ? step.name : null,
noBuildConditionPatched,
noBuildSkipPatched,
postprocessInjected: !hasPostprocess(beforeInjection) && postprocessPresent,
verifyInjected: !hasVerify(beforeInjection) && verifyPresent,
};
}
function patchNoBuildNoRolloutCondition(script) {
return String(script).replace(
/const willRunGitopsPromote = plan\.ciCdPlan && plan\.ciCdPlan\.willRunGitopsPromote === true;\nprocess\.exit\(!willRunGitopsPromote && buildServices\.length === 0 && affectedServices\.length === 0 \? 0 : 1\);/u,
"process.exit(buildServices.length === 0 && affectedServices.length === 0 ? 0 : 1);",
);
}
function patchNoBuildNoRolloutSkip(script) {
return String(script).replace(
/(^|\n)([ \t]*)echo '\{"event":"gitops-promote","status":"skipped","reason":"no-build-no-rollout-plan"\}'(?:\s*>\s*&2)?\n[ \t]*exit 0(?=\n|$)/u,
@@ -83,6 +97,10 @@ function patchNoBuildNoRolloutSkip(script) {
);
}
function hasWillRunGitopsPromoteCoupling(script) {
return /process\.exit\(!willRunGitopsPromote && buildServices\.length === 0 && affectedServices\.length === 0 \? 0 : 1\);/u.test(String(script));
}
function hasNoBuildNoRolloutEarlyExit(script) {
return /echo '\{"event":"gitops-promote","status":"skipped","reason":"no-build-no-rollout-plan"\}'(?:\s*>\s*&2)?\n[ \t]*exit 0(?=\n|$)/u.test(String(script));
}
@@ -281,26 +281,60 @@ describe("HWLAB YAML-owned Pipeline provenance", () => {
name: "gitops-promote",
taskSpec: { steps: [{
name: "render",
script: "node scripts/run-bun.mjs scripts/gitops-render.mjs --use-deploy-images\nnode scripts/run-bun.mjs scripts/gitops-render.mjs --use-deploy-images --check\n",
script: [
"if [ -s /workspace/source/affected-services.json ] && node - /workspace/source/affected-services.json <<'NODE'",
"const fs = require(\"node:fs\");",
"const plan = JSON.parse(fs.readFileSync(process.argv[2], \"utf8\"));",
"const buildServices = Array.isArray(plan.buildServices) ? plan.buildServices : [];",
"const affectedServices = Array.isArray(plan.affectedServices) ? plan.affectedServices : [];",
"const willRunGitopsPromote = plan.ciCdPlan && plan.ciCdPlan.willRunGitopsPromote === true;",
"process.exit(!willRunGitopsPromote && buildServices.length === 0 && affectedServices.length === 0 ? 0 : 1);",
"NODE",
"then",
" printf 'false' > /tekton/results/runtime-ready-required || true",
" echo '{\"event\":\"gitops-promote\",\"status\":\"skipped\",\"reason\":\"no-build-no-rollout-plan\"}'",
" exit 0",
"fi",
"node scripts/run-bun.mjs scripts/gitops-render.mjs --use-deploy-images",
"node scripts/run-bun.mjs scripts/gitops-render.mjs --use-deploy-images --check",
"",
].join("\n"),
}] },
}] },
}));
const overlayBase64 = Buffer.from(JSON.stringify(overlay), "utf8").toString("base64");
const postprocessInput = [
"set -eu",
`render_dir=${JSON.stringify(temporary)}`,
`overlay_b64=${JSON.stringify(overlayBase64)}`,
...nodeRuntimePipelinePostprocessScript(),
].join("\n");
const result = spawnSync("sh", [], {
cwd: rootPath(),
input: [
"set -eu",
`render_dir=${JSON.stringify(temporary)}`,
`overlay_b64=${JSON.stringify(overlayBase64)}`,
...nodeRuntimePipelinePostprocessScript(),
].join("\n"),
input: postprocessInput,
encoding: "utf8",
timeout: 30_000,
});
expect(result.status, result.stderr).toBe(0);
const pipeline = Bun.YAML.parse(readFileSync(pipelinePath, "utf8")) as Record<string, any>;
const guardedPipelineText = readFileSync(pipelinePath, "utf8");
const repeated = spawnSync("node", [
resolve(temporary, ".unidesk-runtime-gitops", "runtime-gitops-pipeline-guard.mjs"),
"--pipeline", pipelinePath,
], {
cwd: rootPath(),
env: { ...process.env, UNIDESK_RUNTIME_GITOPS_OVERLAY_B64: overlayBase64 },
encoding: "utf8",
timeout: 30_000,
});
expect(repeated.status, repeated.stderr).toBe(0);
expect(readFileSync(pipelinePath, "utf8")).toBe(guardedPipelineText);
const pipeline = Bun.YAML.parse(guardedPipelineText) as Record<string, any>;
const scripts = pipeline.spec.tasks.flatMap((task: Record<string, any>) => task.taskSpec.steps.map((step: Record<string, any>) => String(step.script ?? "")));
expect(Math.max(...scripts.map((script: string) => Buffer.byteLength(script)))).toBeLessThanOrEqual(budget as number);
expect(scripts.join("\n")).toContain("process.exit(buildServices.length === 0 && affectedServices.length === 0 ? 0 : 1);");
expect(scripts.join("\n")).not.toContain("!willRunGitopsPromote");
expect(scripts.join("\n")).toContain("no-build-no-rollout-plan-gitops-verify");
expect(scripts.join("\n")).not.toContain('"status":"skipped","reason":"no-build-no-rollout-plan"');
expect(scripts.join("\n")).toContain("UNIDESK_RUNTIME_GITOPS_OVERLAY_FILE=/etc/unidesk-cicd-runtime-gitops/runtime-gitops-overlay.json");
expect(scripts.join("\n")).not.toContain("NODE_UNIDESK_RUNTIME_GITOPS_POSTPROCESS");
expect(scripts.join("\n")).not.toContain("NODE_UNIDESK_RUNTIME_GITOPS_VERIFY");