




















@@ -92,6 +92,55 @@ function maybeGhcrImage(value) {
9292return typeof value === "string" && value.startsWith("ghcr.io/") ? value : "";
9393}
949495+const TRUSTED_WORKFLOW_INPUTS = new Map([
96+["package_artifact_run_id", "packageArtifactRunId"],
97+["package_artifact_name", "packageArtifactName"],
98+["docker_e2e_bare_image", "bareImage"],
99+["docker_e2e_functional_image", "functionalImage"],
100+["published_upgrade_survivor_baseline", "publishedUpgradeSurvivorBaseline"],
101+["published_upgrade_survivor_baselines", "publishedUpgradeSurvivorBaselines"],
102+["published_upgrade_survivor_scenarios", "publishedUpgradeSurvivorScenarios"],
103+]);
104+105+const REUSE_INPUT_KEYS = [
106+"packageArtifactRunId",
107+"packageArtifactName",
108+"bareImage",
109+"functionalImage",
110+"workflowRef",
111+"publishedUpgradeSurvivorBaseline",
112+"publishedUpgradeSurvivorBaselines",
113+"publishedUpgradeSurvivorScenarios",
114+];
115+116+const WORKFLOW_INPUT_RE = /(?:^|\s)-f\s+([a-z0-9_]+)=('([^']*)'|[^\s]+)/gu;
117+const WORKFLOW_REF_RE = /(?:^|\s)--ref\s+('([^']*)'|[^\s]+)/u;
118+119+function trustedReuseInputsFromCommand(command) {
120+const text = String(command ?? "");
121+if (!/^\s*gh\s+workflow\s+run\s/u.test(text)) {
122+return {};
123+}
124+const inputs = {};
125+const refValue = text.match(WORKFLOW_REF_RE);
126+if (refValue) {
127+inputs.workflowRef = (refValue[2] ?? refValue[1] ?? "").replace(/^'/u, "").replace(/'$/u, "");
128+}
129+for (const match of text.matchAll(WORKFLOW_INPUT_RE)) {
130+const target = TRUSTED_WORKFLOW_INPUTS.get(match[1]);
131+const value = (match[3] ?? match[2] ?? "").replace(/^'/u, "").replace(/'$/u, "");
132+if (!target || !value) {
133+continue;
134+}
135+const normalized =
136+target === "bareImage" || target === "functionalImage" ? maybeGhcrImage(value) : value;
137+if (normalized) {
138+inputs[target] = normalized;
139+}
140+}
141+return inputs;
142+}
143+95144function reuseInputsFromJson(parsed) {
96145const packageArtifactRunId = parsed.github?.runId || "";
97146if (!packageArtifactRunId) {
@@ -107,12 +156,11 @@ function reuseInputsFromJson(parsed) {
107156}
108157109158function sameReuseInputs(left, right) {
110-return (
111-(left?.packageArtifactRunId || "") === (right?.packageArtifactRunId || "") &&
112-(left?.packageArtifactName || "") === (right?.packageArtifactName || "") &&
113-(left?.bareImage || "") === (right?.bareImage || "") &&
114-(left?.functionalImage || "") === (right?.functionalImage || "")
115-);
159+return REUSE_INPUT_KEYS.every((key) => (left?.[key] || "") === (right?.[key] || ""));
160+}
161+162+function reuseInputsKey(inputs) {
163+return JSON.stringify(REUSE_INPUT_KEYS.map((key) => inputs?.[key] || ""));
116164}
117165118166function commonReuseInputs(entries) {
@@ -124,8 +172,25 @@ function commonReuseInputs(entries) {
124172return inputs.every((input) => sameReuseInputs(first, input)) ? first : {};
125173}
126174175+function groupByReuseInputs(entries) {
176+const groups = new Map();
177+for (const entry of entries) {
178+const key = reuseInputsKey(entry.reuseInputs);
179+const group = groups.get(key);
180+if (group) {
181+group.push(entry);
182+} else {
183+groups.set(key, [entry]);
184+}
185+}
186+return [...groups.values()];
187+}
188+127189function ghWorkflowCommand(lanes, ref, workflow, reuseInputs = {}) {
128-const workflowRef = process.env.OPENCLAW_DOCKER_E2E_WORKFLOW_REF || process.env.GITHUB_REF_NAME;
190+const workflowRef =
191+reuseInputs.workflowRef ||
192+process.env.OPENCLAW_DOCKER_E2E_WORKFLOW_REF ||
193+process.env.GITHUB_REF_NAME;
129194const releasePath = lanes.some(laneNeedsReleasePath);
130195const fields = [
131196"gh workflow run",
@@ -159,6 +224,30 @@ function ghWorkflowCommand(lanes, ref, workflow, reuseInputs = {}) {
159224if (reuseInputs.functionalImage) {
160225fields.push("-f", `docker_e2e_functional_image=${shellQuote(reuseInputs.functionalImage)}`);
161226}
227+if (reuseInputs.publishedUpgradeSurvivorBaseline) {
228+fields.push(
229+"-f",
230+`published_upgrade_survivor_baseline=${shellQuote(
231+ reuseInputs.publishedUpgradeSurvivorBaseline,
232+ )}`,
233+);
234+}
235+if (reuseInputs.publishedUpgradeSurvivorBaselines) {
236+fields.push(
237+"-f",
238+`published_upgrade_survivor_baselines=${shellQuote(
239+ reuseInputs.publishedUpgradeSurvivorBaselines,
240+ )}`,
241+);
242+}
243+if (reuseInputs.publishedUpgradeSurvivorScenarios) {
244+fields.push(
245+"-f",
246+`published_upgrade_survivor_scenarios=${shellQuote(
247+ reuseInputs.publishedUpgradeSurvivorScenarios,
248+ )}`,
249+);
250+}
162251return fields.join(" ");
163252}
164253@@ -169,20 +258,31 @@ function failureName(failure) {
169258function failedEntryFromRecord(failure, file, ref, workflow, reuseInputs) {
170259const lane = failureName(failure);
171260const targetable = failure.targetable !== false;
261+const workflowInputs = {
262+ ...trustedReuseInputsFromCommand(failure.ghWorkflowCommand),
263+ ...reuseInputs,
264+};
172265return {
173-ghWorkflowCommand: targetable
174- ? failure.ghWorkflowCommand || ghWorkflowCommand([lane], ref, workflow, reuseInputs)
175- : "",
176266 lane,
177267localRerunCommand: failure.rerunCommand,
178268logFile: failure.logFile,
179- reuseInputs,
269+reuseInputs: workflowInputs,
180270source: file,
181271status: failure.status,
182272 targetable,
183273};
184274}
185275276+function mergeReuseInputs(left = {}, right = {}) {
277+const merged = { ...left };
278+for (const [key, value] of Object.entries(right)) {
279+if (value) {
280+merged[key] = value;
281+}
282+}
283+return merged;
284+}
285+186286function detectRepo() {
187287return run("gh", ["repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"]).trim();
188288}
@@ -222,7 +322,18 @@ function failedLaneEntriesFromJson(file, ref, workflow) {
222322function mergeByLane(entries) {
223323const byLane = new Map();
224324for (const entry of entries) {
225-if (!byLane.has(entry.lane)) {
325+const existing = byLane.get(entry.lane);
326+if (existing) {
327+byLane.set(entry.lane, {
328+ ...existing,
329+ ...entry,
330+localRerunCommand: existing.localRerunCommand || entry.localRerunCommand,
331+logFile: existing.logFile || entry.logFile,
332+reuseInputs: mergeReuseInputs(existing.reuseInputs, entry.reuseInputs),
333+source: existing.source || entry.source,
334+targetable: existing.targetable !== false && entry.targetable !== false,
335+});
336+} else {
226337byLane.set(entry.lane, entry);
227338}
228339}
@@ -288,22 +399,33 @@ function printEntries(entries, ref, workflow, runValue) {
288399console.log(`Failed Docker E2E entries: ${entries.map((entry) => entry.lane).join(", ")}`);
289400if (workflowEntries.length > 0) {
290401console.log("");
291-console.log("Combined GitHub rerun:");
292-console.log(
293-ghWorkflowCommand(
294-workflowEntries.map((entry) => entry.lane),
295-ref,
296-workflow,
297-commonReuseInputs(workflowEntries),
298-),
299-);
300-console.log("");
301-console.log("Per-lane GitHub reruns:");
302-for (const entry of workflowEntries) {
402+const workflowGroups = groupByReuseInputs(workflowEntries);
403+if (workflowGroups.length === 1) {
404+console.log("Combined GitHub rerun:");
303405console.log(
304-`- ${entry.lane}: ${entry.ghWorkflowCommand || ghWorkflowCommand([entry.lane], ref, workflow)}`,
406+ghWorkflowCommand(
407+workflowEntries.map((entry) => entry.lane),
408+ref,
409+workflow,
410+commonReuseInputs(workflowEntries),
411+),
305412);
413+} else {
414+console.log("Combined GitHub reruns:");
415+for (const group of workflowGroups) {
416+const lanes = group.map((entry) => entry.lane);
417+console.log(
418+`- ${lanes.join(", ")}: ${ghWorkflowCommand(lanes, ref, workflow, group[0]?.reuseInputs)}`,
419+);
420+}
306421}
422+console.log("");
423+console.log("Per-lane GitHub reruns:");
424+for (const entry of workflowEntries) {
425+console.log(
426+`- ${entry.lane}: ${ghWorkflowCommand([entry.lane], ref, workflow, entry.reuseInputs)}`,
427+);
428+}
307429} else {
308430console.log("");
309431console.log("No targetable failed Docker E2E lanes found.");
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。