























@@ -0,0 +1,259 @@
1+#!/usr/bin/env node
2+// Builds cheap rerun commands from a Docker E2E GitHub run or local summary.
3+// For GitHub runs, the script downloads Docker E2E artifacts, reads
4+// summary/failures JSON, and prints targeted workflow commands that prepare a
5+// fresh OpenClaw tarball for the same ref before running only failed lanes.
6+import { spawnSync } from "node:child_process";
7+import fs from "node:fs";
8+import os from "node:os";
9+import path from "node:path";
10+11+const DEFAULT_WORKFLOW = "openclaw-live-and-e2e-checks-reusable.yml";
12+13+function usage() {
14+return [
15+"Usage:",
16+" node scripts/docker-e2e-rerun.mjs <run-id|summary.json|failures.json> [--repo owner/repo] [--dir output-dir] [--workflow workflow.yml] [--ref ref]",
17+].join("\n");
18+}
19+20+function parseArgs(argv) {
21+const options = {
22+dir: "",
23+input: "",
24+ref: "",
25+repo: "",
26+workflow: DEFAULT_WORKFLOW,
27+};
28+for (let index = 0; index < argv.length; index += 1) {
29+const arg = argv[index];
30+if (arg === "--repo") {
31+options.repo = argv[(index += 1)] ?? "";
32+} else if (arg?.startsWith("--repo=")) {
33+options.repo = arg.slice("--repo=".length);
34+} else if (arg === "--dir") {
35+options.dir = argv[(index += 1)] ?? "";
36+} else if (arg?.startsWith("--dir=")) {
37+options.dir = arg.slice("--dir=".length);
38+} else if (arg === "--workflow") {
39+options.workflow = argv[(index += 1)] ?? "";
40+} else if (arg?.startsWith("--workflow=")) {
41+options.workflow = arg.slice("--workflow=".length);
42+} else if (arg === "--ref") {
43+options.ref = argv[(index += 1)] ?? "";
44+} else if (arg?.startsWith("--ref=")) {
45+options.ref = arg.slice("--ref=".length);
46+} else if (!options.input) {
47+options.input = arg;
48+} else {
49+throw new Error(`unknown argument: ${arg}\n${usage()}`);
50+}
51+}
52+if (!options.input || !options.workflow) {
53+throw new Error(usage());
54+}
55+return options;
56+}
57+58+function run(command, args, options = {}) {
59+const result = spawnSync(command, args, {
60+encoding: "utf8",
61+stdio: options.stdio ?? ["ignore", "pipe", "pipe"],
62+});
63+if (result.status !== 0) {
64+throw new Error(
65+`${command} ${args.join(" ")} failed with ${result.status ?? result.signal}\n${result.stderr}`,
66+);
67+}
68+return result.stdout;
69+}
70+71+function readJson(file) {
72+return JSON.parse(fs.readFileSync(file, "utf8"));
73+}
74+75+function shellQuote(value) {
76+return `'${String(value).replaceAll("'", "'\\''")}'`;
77+}
78+79+function ghWorkflowCommand(lanes, ref, workflow) {
80+return [
81+"gh workflow run",
82+shellQuote(workflow),
83+"-f",
84+`ref=${shellQuote(ref)}`,
85+"-f",
86+"include_repo_e2e=false",
87+"-f",
88+"include_release_path_suites=false",
89+"-f",
90+"include_openwebui=false",
91+"-f",
92+`docker_lanes=${shellQuote(lanes.join(" "))}`,
93+"-f",
94+"include_live_suites=false",
95+"-f",
96+"live_models_only=false",
97+].join(" ");
98+}
99+100+function detectRepo() {
101+return JSON.parse(run("gh", ["repo", "view", "--json", "nameWithOwner"])).nameWithOwner;
102+}
103+104+function findFiles(rootDir, basenames, out = []) {
105+for (const entry of fs.readdirSync(rootDir, { withFileTypes: true })) {
106+const file = path.join(rootDir, entry.name);
107+if (entry.isDirectory()) {
108+findFiles(file, basenames, out);
109+} else if (basenames.has(entry.name)) {
110+out.push(file);
111+}
112+}
113+return out;
114+}
115+116+function failedLaneEntriesFromJson(file, ref, workflow) {
117+const parsed = readJson(file);
118+const source = path.basename(file);
119+if (source === "failures.json" && Array.isArray(parsed.lanes)) {
120+return parsed.lanes
121+.filter((lane) => lane.name)
122+.map((lane) => ({
123+ghWorkflowCommand: lane.ghWorkflowCommand,
124+lane: lane.name,
125+localRerunCommand: lane.rerunCommand,
126+logFile: lane.logFile,
127+source: file,
128+status: lane.status,
129+}));
130+}
131+132+const lanes = Array.isArray(parsed.lanes) ? parsed.lanes : [];
133+return lanes
134+.filter((lane) => lane.status !== 0 && lane.name)
135+.map((lane) => ({
136+ghWorkflowCommand: ghWorkflowCommand([lane.name], ref, workflow),
137+lane: lane.name,
138+localRerunCommand: lane.rerunCommand,
139+logFile: lane.logFile,
140+source: file,
141+status: lane.status,
142+}));
143+}
144+145+function mergeByLane(entries) {
146+const byLane = new Map();
147+for (const entry of entries) {
148+if (!byLane.has(entry.lane)) {
149+byLane.set(entry.lane, entry);
150+}
151+}
152+return [...byLane.values()].toSorted((left, right) => left.lane.localeCompare(right.lane));
153+}
154+155+function downloadDockerArtifacts(runId, repo, outputDir) {
156+fs.mkdirSync(outputDir, { recursive: true });
157+const artifacts = JSON.parse(
158+run("gh", [
159+"api",
160+`repos/${repo}/actions/runs/${runId}/artifacts?per_page=100`,
161+"--jq",
162+".artifacts",
163+]),
164+);
165+const names = artifacts
166+.filter((artifact) => !artifact.expired && artifact.name.startsWith("docker-e2e-"))
167+.map((artifact) => artifact.name);
168+if (names.length === 0) {
169+throw new Error(`No docker-e2e-* artifacts found for run ${runId}`);
170+}
171+for (const name of names) {
172+run(
173+"gh",
174+["run", "download", String(runId), "--repo", repo, "--name", name, "--dir", outputDir],
175+{
176+stdio: "inherit",
177+},
178+);
179+}
180+return names;
181+}
182+183+function runInfo(runId, repo) {
184+return JSON.parse(
185+run("gh", [
186+"run",
187+"view",
188+String(runId),
189+"--repo",
190+repo,
191+"--json",
192+"databaseId,headSha,headBranch,status,conclusion,url,workflowName",
193+]),
194+);
195+}
196+197+function printEntries(entries, ref, workflow, run) {
198+if (run) {
199+console.log(`Run: ${run.url}`);
200+console.log(`Workflow: ${run.workflowName}`);
201+}
202+console.log(`Ref: ${ref}`);
203+console.log(
204+"Targeted GitHub reruns prepare a fresh OpenClaw npm tarball for that ref before lane execution.",
205+);
206+if (entries.length === 0) {
207+console.log("No failed Docker E2E lanes found.");
208+return;
209+}
210+console.log(`Failed lanes: ${entries.map((entry) => entry.lane).join(", ")}`);
211+console.log("");
212+console.log("Combined GitHub rerun:");
213+console.log(
214+ghWorkflowCommand(
215+entries.map((entry) => entry.lane),
216+ref,
217+workflow,
218+),
219+);
220+console.log("");
221+console.log("Per-lane GitHub reruns:");
222+for (const entry of entries) {
223+console.log(
224+`- ${entry.lane}: ${entry.ghWorkflowCommand || ghWorkflowCommand([entry.lane], ref, workflow)}`,
225+);
226+}
227+console.log("");
228+console.log("Local rerun starting points:");
229+for (const entry of entries) {
230+if (entry.localRerunCommand) {
231+console.log(`- ${entry.lane}: ${entry.localRerunCommand}`);
232+}
233+}
234+}
235+236+const options = parseArgs(process.argv.slice(2));
237+const isLocalJson = fs.existsSync(options.input) && fs.statSync(options.input).isFile();
238+if (isLocalJson) {
239+const ref = options.ref || process.env.GITHUB_SHA || "HEAD";
240+printEntries(
241+mergeByLane(failedLaneEntriesFromJson(options.input, ref, options.workflow)),
242+ref,
243+options.workflow,
244+);
245+} else {
246+const repo = options.repo || detectRepo();
247+const run = runInfo(options.input, repo);
248+const ref = options.ref || run.headSha || run.headBranch;
249+const outputDir =
250+options.dir || path.join(os.tmpdir(), `openclaw-docker-e2e-rerun-${options.input}`);
251+const artifactNames = downloadDockerArtifacts(options.input, repo, outputDir);
252+const files = findFiles(outputDir, new Set(["failures.json", "summary.json"]));
253+const entries = mergeByLane(
254+files.flatMap((file) => failedLaneEntriesFromJson(file, ref, options.workflow)),
255+);
256+console.log(`Artifacts: ${artifactNames.join(", ")}`);
257+console.log(`Downloaded: ${outputDir}`);
258+printEntries(entries, ref, options.workflow, run);
259+}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。