mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-13 06:03:39 -06:00
c70aee247e
* refactor(scripts): migrate JavaScript tools to TypeScript * fix(ci): keep changed-scope preflight zero-install * fix(ci): preserve zero-install script owners * fix(ci): complete script migration follow-through * fix(release): keep stable closeout zero-install * fix(scripts): preserve standalone execution boundaries * fix(scripts): repair standalone loader boundaries * fix(scripts): normalize gateway observation ids * fix(scripts): keep Docker packager standalone * test(scripts): preserve rebase cleanup helpers * test(sessions): use tracked temp directory
591 lines
19 KiB
TypeScript
591 lines
19 KiB
TypeScript
#!/usr/bin/env node
|
|
// Builds cheap rerun commands from a Docker E2E GitHub run or local summary.
|
|
// For GitHub runs, the script downloads Docker E2E artifacts, reads
|
|
// summary/failures JSON, and prints targeted workflow commands for failed
|
|
// lanes, repacking the exact artifact target and reusing GHCR-backed prepared
|
|
// image refs when artifacts expose them.
|
|
import { spawnSync } from "node:child_process";
|
|
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { isRecord } from "@openclaw/normalization-core/record-coerce";
|
|
import { readDockerE2eJsonArtifact } from "./lib/docker-e2e-json-artifacts.mts";
|
|
|
|
const DEFAULT_WORKFLOW = "openclaw-live-and-e2e-checks-reusable.yml";
|
|
|
|
const REUSE_INPUT_KEYS = [
|
|
"bareImage",
|
|
"functionalImage",
|
|
"publishedUpgradeSurvivorBaseline",
|
|
"publishedUpgradeSurvivorBaselines",
|
|
"publishedUpgradeSurvivorScenarios",
|
|
"allowUnreleasedChangelog",
|
|
] as const;
|
|
|
|
type ReuseInputKey = (typeof REUSE_INPUT_KEYS)[number];
|
|
type ReuseInputs = Partial<Record<ReuseInputKey, string>>;
|
|
|
|
function usage() {
|
|
return [
|
|
"Usage:",
|
|
" node --import tsx scripts/docker-e2e-rerun.mts <run-id|summary.json|failures.json> [--repo owner/repo] [--dir output-dir] [--workflow workflow.yml] [--ref ref]",
|
|
].join("\n");
|
|
}
|
|
|
|
function parseArgs(argv: string[]) {
|
|
const options = {
|
|
dir: "",
|
|
help: false,
|
|
input: "",
|
|
ref: "",
|
|
repo: "",
|
|
workflow: DEFAULT_WORKFLOW,
|
|
};
|
|
for (let index = 0; index < argv.length; index += 1) {
|
|
const arg = argv[index]!;
|
|
if (arg === "--help" || arg === "-h") {
|
|
options.help = true;
|
|
} else if (arg === "--repo") {
|
|
options.repo = argv[(index += 1)] ?? "";
|
|
} else if (arg?.startsWith("--repo=")) {
|
|
options.repo = arg.slice("--repo=".length);
|
|
} else if (arg === "--dir") {
|
|
options.dir = argv[(index += 1)] ?? "";
|
|
} else if (arg?.startsWith("--dir=")) {
|
|
options.dir = arg.slice("--dir=".length);
|
|
} else if (arg === "--workflow") {
|
|
options.workflow = argv[(index += 1)] ?? "";
|
|
} else if (arg?.startsWith("--workflow=")) {
|
|
options.workflow = arg.slice("--workflow=".length);
|
|
} else if (arg === "--ref") {
|
|
options.ref = argv[(index += 1)] ?? "";
|
|
} else if (arg?.startsWith("--ref=")) {
|
|
options.ref = arg.slice("--ref=".length);
|
|
} else if (!options.input) {
|
|
options.input = arg;
|
|
} else {
|
|
throw new Error(`unknown argument: ${arg}\n${usage()}`);
|
|
}
|
|
}
|
|
if (options.help) {
|
|
return options;
|
|
}
|
|
if (!options.input || !options.workflow) {
|
|
throw new Error(usage());
|
|
}
|
|
return options;
|
|
}
|
|
|
|
function run(
|
|
command: string,
|
|
args: string[],
|
|
options: { stdio?: "inherit" | ["ignore", "pipe", "pipe"] } = {},
|
|
): string {
|
|
const result = spawnSync(command, args, {
|
|
encoding: "utf8",
|
|
stdio: options.stdio ?? ["ignore", "pipe", "pipe"],
|
|
});
|
|
if (result.status !== 0) {
|
|
throw new Error(
|
|
`${command} ${args.join(" ")} failed with ${result.status ?? result.signal}\n${result.stderr}`,
|
|
);
|
|
}
|
|
return result.stdout;
|
|
}
|
|
|
|
function readJson(file: string) {
|
|
const value = readDockerE2eJsonArtifact(file);
|
|
return isRecord(value) ? value : {};
|
|
}
|
|
|
|
function shellQuote(value: string): string {
|
|
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
}
|
|
|
|
function laneNeedsReleasePath(lane: string): boolean {
|
|
return /^bundled-channel(?:-|$)/u.test(lane);
|
|
}
|
|
|
|
function maybeGhcrImage(value: unknown): string {
|
|
return typeof value === "string" && value.startsWith("ghcr.io/") ? value : "";
|
|
}
|
|
|
|
const TRUSTED_WORKFLOW_INPUTS = new Map<string, ReuseInputKey>([
|
|
["docker_e2e_bare_image", "bareImage"],
|
|
["docker_e2e_functional_image", "functionalImage"],
|
|
["published_upgrade_survivor_baseline", "publishedUpgradeSurvivorBaseline"],
|
|
["published_upgrade_survivor_baselines", "publishedUpgradeSurvivorBaselines"],
|
|
["published_upgrade_survivor_scenarios", "publishedUpgradeSurvivorScenarios"],
|
|
["allow_unreleased_changelog", "allowUnreleasedChangelog"],
|
|
]);
|
|
|
|
const WORKFLOW_INPUT_RE = /(?:^|\s)-f\s+([a-z0-9_]+)=('([^']*)'|[^\s]+)/gu;
|
|
|
|
function trustedReuseInputsFromCommand(command: unknown): ReuseInputs {
|
|
const text = typeof command === "string" ? command : "";
|
|
if (!/^\s*gh\s+workflow\s+run\s/u.test(text)) {
|
|
return {};
|
|
}
|
|
const inputs: ReuseInputs = {};
|
|
for (const match of text.matchAll(WORKFLOW_INPUT_RE)) {
|
|
const input = match[1];
|
|
const target = input ? TRUSTED_WORKFLOW_INPUTS.get(input) : undefined;
|
|
const value = (match[3] ?? match[2] ?? "").replace(/^'/u, "").replace(/'$/u, "");
|
|
if (!target || !value) {
|
|
continue;
|
|
}
|
|
let normalized = value;
|
|
if (target === "bareImage" || target === "functionalImage") {
|
|
normalized = maybeGhcrImage(value);
|
|
} else if (target === "allowUnreleasedChangelog" && value !== "true") {
|
|
normalized = "";
|
|
}
|
|
if (normalized) {
|
|
inputs[target] = normalized;
|
|
}
|
|
}
|
|
return inputs;
|
|
}
|
|
|
|
function reuseInputsFromJson(parsed: Record<string, unknown>): ReuseInputs {
|
|
const images = isRecord(parsed.images) ? parsed.images : {};
|
|
const bareImage = maybeGhcrImage(images.bare);
|
|
const functionalImage = maybeGhcrImage(images.functional);
|
|
const allowUnreleasedChangelog = parsed.allowUnreleasedChangelog === true ? "true" : undefined;
|
|
return {
|
|
...(allowUnreleasedChangelog ? { allowUnreleasedChangelog } : {}),
|
|
...(bareImage ? { bareImage } : {}),
|
|
...(functionalImage ? { functionalImage } : {}),
|
|
};
|
|
}
|
|
|
|
function artifactTargetRef(
|
|
parsed: Record<string, unknown>,
|
|
file: string,
|
|
required: boolean,
|
|
): string {
|
|
const github = isRecord(parsed.github) ? parsed.github : {};
|
|
const values = [parsed.ref, github.selectedSha].filter(
|
|
(value) => value !== undefined && value !== null && value !== "",
|
|
);
|
|
if (
|
|
!values.every(
|
|
(value): value is string => typeof value === "string" && /^[a-f0-9]{40}$/u.test(value),
|
|
)
|
|
) {
|
|
if (required) {
|
|
throw new Error(`${file} has an invalid artifact target ref; expected a full commit SHA`);
|
|
}
|
|
return "";
|
|
}
|
|
const refs = [...new Set(values)];
|
|
if (refs.length > 1) {
|
|
if (required) {
|
|
throw new Error(`${file} has conflicting artifact target refs: ${refs.join(", ")}`);
|
|
}
|
|
return "";
|
|
}
|
|
return refs[0] || "";
|
|
}
|
|
|
|
function discardMismatchedPreparedImages(entry: FailedEntry, explicitRef: string): FailedEntry {
|
|
if (!explicitRef || entry.artifactRef === explicitRef) {
|
|
return entry;
|
|
}
|
|
const reuseInputs = Object.fromEntries(
|
|
Object.entries(entry.reuseInputs ?? {}).filter(
|
|
([key]) => key !== "bareImage" && key !== "functionalImage",
|
|
),
|
|
);
|
|
return { ...entry, reuseInputs };
|
|
}
|
|
|
|
function sameReuseInputs(left: ReuseInputs | undefined, right: ReuseInputs | undefined): boolean {
|
|
return REUSE_INPUT_KEYS.every((key) => (left?.[key] || "") === (right?.[key] || ""));
|
|
}
|
|
|
|
function reuseInputsKey(inputs: ReuseInputs | undefined): string {
|
|
return JSON.stringify(REUSE_INPUT_KEYS.map((key) => inputs?.[key] || ""));
|
|
}
|
|
|
|
function commonReuseInputs(entries: FailedEntry[]): ReuseInputs {
|
|
const first = entries[0]?.reuseInputs;
|
|
return first && entries.every((entry) => sameReuseInputs(first, entry.reuseInputs)) ? first : {};
|
|
}
|
|
|
|
function groupByReuseInputs(entries: FailedEntry[]): FailedEntry[][] {
|
|
const groups = new Map<string, FailedEntry[]>();
|
|
for (const entry of entries) {
|
|
const key = reuseInputsKey(entry.reuseInputs);
|
|
const group = groups.get(key);
|
|
if (group) {
|
|
group.push(entry);
|
|
} else {
|
|
groups.set(key, [entry]);
|
|
}
|
|
}
|
|
return [...groups.values()];
|
|
}
|
|
|
|
function ghWorkflowCommand(
|
|
lanes: string[],
|
|
ref: string,
|
|
workflow: string,
|
|
reuseInputs: ReuseInputs = {},
|
|
): string {
|
|
const workflowRef = process.env.OPENCLAW_DOCKER_E2E_WORKFLOW_REF;
|
|
const releasePath = lanes.some(laneNeedsReleasePath);
|
|
const fields = [
|
|
"gh workflow run",
|
|
shellQuote(workflow),
|
|
...(workflowRef ? ["--ref", shellQuote(workflowRef)] : []),
|
|
"-f",
|
|
`ref=${shellQuote(ref)}`,
|
|
"-f",
|
|
"include_repo_e2e=false",
|
|
"-f",
|
|
`include_release_path_suites=${releasePath ? "true" : "false"}`,
|
|
"-f",
|
|
"include_openwebui=false",
|
|
"-f",
|
|
`docker_lanes=${shellQuote(lanes.join(" "))}`,
|
|
"-f",
|
|
"include_live_suites=false",
|
|
"-f",
|
|
"live_models_only=false",
|
|
];
|
|
if (reuseInputs.bareImage) {
|
|
fields.push("-f", `docker_e2e_bare_image=${shellQuote(reuseInputs.bareImage)}`);
|
|
}
|
|
if (reuseInputs.functionalImage) {
|
|
fields.push("-f", `docker_e2e_functional_image=${shellQuote(reuseInputs.functionalImage)}`);
|
|
}
|
|
if (reuseInputs.bareImage || reuseInputs.functionalImage) {
|
|
fields.push("-f", "shared_image_policy=existing-only");
|
|
}
|
|
if (reuseInputs.allowUnreleasedChangelog === "true") {
|
|
fields.push("-f", "allow_unreleased_changelog=true");
|
|
}
|
|
if (reuseInputs.publishedUpgradeSurvivorBaseline) {
|
|
fields.push(
|
|
"-f",
|
|
`published_upgrade_survivor_baseline=${shellQuote(
|
|
reuseInputs.publishedUpgradeSurvivorBaseline,
|
|
)}`,
|
|
);
|
|
}
|
|
if (reuseInputs.publishedUpgradeSurvivorBaselines) {
|
|
fields.push(
|
|
"-f",
|
|
`published_upgrade_survivor_baselines=${shellQuote(
|
|
reuseInputs.publishedUpgradeSurvivorBaselines,
|
|
)}`,
|
|
);
|
|
}
|
|
if (reuseInputs.publishedUpgradeSurvivorScenarios) {
|
|
fields.push(
|
|
"-f",
|
|
`published_upgrade_survivor_scenarios=${shellQuote(
|
|
reuseInputs.publishedUpgradeSurvivorScenarios,
|
|
)}`,
|
|
);
|
|
}
|
|
return fields.join(" ");
|
|
}
|
|
|
|
function failureName(failure: Record<string, unknown>): string {
|
|
if (typeof failure.name === "string" && failure.name) {
|
|
return failure.name;
|
|
}
|
|
return typeof failure.lane === "string" ? failure.lane : "";
|
|
}
|
|
|
|
function failedEntryFromRecord(
|
|
failure: Record<string, unknown>,
|
|
artifactRef: string,
|
|
reuseInputs: ReuseInputs,
|
|
) {
|
|
const lane = failureName(failure);
|
|
const targetable = failure.targetable !== false;
|
|
const workflowInputs = {
|
|
...trustedReuseInputsFromCommand(failure.ghWorkflowCommand),
|
|
...reuseInputs,
|
|
};
|
|
return {
|
|
artifactRef,
|
|
lane,
|
|
localRerunCommand: typeof failure.rerunCommand === "string" ? failure.rerunCommand : "",
|
|
reuseInputs: workflowInputs,
|
|
targetable,
|
|
};
|
|
}
|
|
|
|
type FailedEntry = ReturnType<typeof failedEntryFromRecord>;
|
|
|
|
function mergeReuseInputs(left: ReuseInputs = {}, right: ReuseInputs = {}): ReuseInputs {
|
|
const merged: ReuseInputs = { ...left };
|
|
for (const key of REUSE_INPUT_KEYS) {
|
|
const value = right[key];
|
|
if (value) {
|
|
merged[key] = value;
|
|
}
|
|
}
|
|
return merged;
|
|
}
|
|
|
|
function detectRepo(): string {
|
|
return run("gh", ["repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"]).trim();
|
|
}
|
|
|
|
function findFiles(rootDir: string, basenames: Set<string>, out: string[] = []): string[] {
|
|
for (const entry of fs.readdirSync(rootDir, { withFileTypes: true })) {
|
|
const file = path.join(rootDir, entry.name);
|
|
if (entry.isDirectory()) {
|
|
findFiles(file, basenames, out);
|
|
} else if (basenames.has(entry.name)) {
|
|
out.push(file);
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function failedLaneEntriesFromJson(file: string, explicitRef = ""): FailedEntry[] {
|
|
const parsed = readJson(file);
|
|
const reuseInputs = reuseInputsFromJson(parsed);
|
|
const source = path.basename(file);
|
|
let failures;
|
|
const lanes = Array.isArray(parsed.lanes) ? parsed.lanes.filter(isRecord) : [];
|
|
if (source === "failures.json") {
|
|
failures = lanes.filter((lane) => failureName(lane));
|
|
} else {
|
|
const recordedFailures = Array.isArray(parsed.failures) ? parsed.failures.filter(isRecord) : [];
|
|
failures =
|
|
recordedFailures.length > 0 ? recordedFailures : lanes.filter((lane) => lane.status !== 0);
|
|
failures = failures.filter((lane) => failureName(lane));
|
|
}
|
|
const needsTargetRef = !explicitRef && failures.some((failure) => failure.targetable !== false);
|
|
const artifactRef = artifactTargetRef(parsed, file, needsTargetRef);
|
|
return failures.map((failure) =>
|
|
discardMismatchedPreparedImages(
|
|
failedEntryFromRecord(failure, artifactRef, reuseInputs),
|
|
explicitRef,
|
|
),
|
|
);
|
|
}
|
|
|
|
function mergeArtifactRefs(left: string, right: string, lane: string): string {
|
|
if (left && right && left !== right) {
|
|
throw new Error(`lane ${lane} has mixed artifact target refs: ${left}, ${right}`);
|
|
}
|
|
return left || right || "";
|
|
}
|
|
|
|
function mergeByLane(entries: FailedEntry[], explicitRef = ""): FailedEntry[] {
|
|
const byLane = new Map<string, FailedEntry>();
|
|
for (const entry of entries) {
|
|
const existing = byLane.get(entry.lane);
|
|
if (existing) {
|
|
byLane.set(entry.lane, {
|
|
...existing,
|
|
...entry,
|
|
artifactRef:
|
|
explicitRef || mergeArtifactRefs(existing.artifactRef, entry.artifactRef, entry.lane),
|
|
localRerunCommand: existing.localRerunCommand || entry.localRerunCommand,
|
|
reuseInputs: mergeReuseInputs(existing.reuseInputs, entry.reuseInputs),
|
|
targetable: existing.targetable && entry.targetable,
|
|
});
|
|
} else {
|
|
byLane.set(entry.lane, { ...entry, artifactRef: explicitRef || entry.artifactRef });
|
|
}
|
|
}
|
|
return [...byLane.values()].toSorted((left, right) => left.lane.localeCompare(right.lane));
|
|
}
|
|
|
|
function resolveTargetRef(entries: FailedEntry[], explicitRef: string): string {
|
|
const targetable = entries.filter((entry) => entry.targetable);
|
|
if (targetable.length === 0) {
|
|
return "";
|
|
}
|
|
if (explicitRef) {
|
|
if (!/^[a-f0-9]{40}$/u.test(explicitRef)) {
|
|
throw new Error("--ref must be the exact lowercase 40-character target SHA");
|
|
}
|
|
return explicitRef;
|
|
}
|
|
const missing = targetable.filter((entry) => !entry.artifactRef);
|
|
if (missing.length > 0) {
|
|
throw new Error(
|
|
`Docker E2E artifacts are missing an exact target ref for: ${missing.map((entry) => entry.lane).join(", ")}; pass --ref explicitly`,
|
|
);
|
|
}
|
|
const refs = [...new Set(targetable.map((entry) => entry.artifactRef).filter(Boolean))];
|
|
if (refs.length > 1) {
|
|
throw new Error(`Docker E2E artifacts contain mixed target refs: ${refs.join(", ")}`);
|
|
}
|
|
return refs[0] || "";
|
|
}
|
|
|
|
function downloadDockerArtifacts(runId: string, repo: string, outputDir: string): string[] {
|
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
const payload: unknown = JSON.parse(
|
|
run("gh", [
|
|
"api",
|
|
`repos/${repo}/actions/runs/${runId}/artifacts?per_page=100`,
|
|
"--jq",
|
|
".artifacts",
|
|
]),
|
|
);
|
|
const artifacts = Array.isArray(payload) ? payload.filter(isRecord) : [];
|
|
const names = artifacts.flatMap((artifact) =>
|
|
artifact.expired !== true &&
|
|
typeof artifact.name === "string" &&
|
|
artifact.name.startsWith("docker-e2e-")
|
|
? [artifact.name]
|
|
: [],
|
|
);
|
|
if (names.length === 0) {
|
|
throw new Error(`No docker-e2e-* artifacts found for run ${runId}`);
|
|
}
|
|
for (const [index, name] of names.entries()) {
|
|
const artifactDir = path.join(
|
|
outputDir,
|
|
`${String(index).padStart(3, "0")}-${safePathSegment(name)}`,
|
|
);
|
|
run("gh", ["run", "download", runId, "--repo", repo, "--name", name, "--dir", artifactDir], {
|
|
stdio: "inherit",
|
|
});
|
|
}
|
|
return names;
|
|
}
|
|
|
|
function runInfo(runId: string, repo: string) {
|
|
const value: unknown = JSON.parse(
|
|
run("gh", [
|
|
"run",
|
|
"view",
|
|
runId,
|
|
"--repo",
|
|
repo,
|
|
"--json",
|
|
"databaseId,headSha,headBranch,status,conclusion,url,workflowName",
|
|
]),
|
|
);
|
|
return {
|
|
url: isRecord(value) && typeof value.url === "string" ? value.url : undefined,
|
|
workflowName:
|
|
isRecord(value) && typeof value.workflowName === "string" ? value.workflowName : undefined,
|
|
};
|
|
}
|
|
|
|
function safePathSegment(value: string): string {
|
|
return (
|
|
value
|
|
.replace(/[^a-zA-Z0-9_.-]+/gu, "-")
|
|
.replace(/^-+|-+$/gu, "")
|
|
.slice(0, 80) || "run"
|
|
);
|
|
}
|
|
|
|
function defaultOutputDir(input: string): string {
|
|
return fs.mkdtempSync(
|
|
path.join(os.tmpdir(), `openclaw-docker-e2e-rerun-${safePathSegment(input)}-`),
|
|
);
|
|
}
|
|
|
|
function printEntries(
|
|
entries: FailedEntry[],
|
|
ref: string,
|
|
workflow: string,
|
|
runValue?: ReturnType<typeof runInfo>,
|
|
): void {
|
|
if (runValue) {
|
|
console.log(`Run: ${runValue.url}`);
|
|
console.log(`Workflow: ${runValue.workflowName}`);
|
|
}
|
|
console.log(`Ref: ${ref}`);
|
|
console.log(
|
|
"Targeted GitHub reruns repack the exact artifact target and reuse GHCR-backed prepared image refs when the downloaded artifacts expose them.",
|
|
);
|
|
if (entries.length === 0) {
|
|
console.log("No failed Docker E2E lanes found.");
|
|
return;
|
|
}
|
|
const workflowEntries = entries.filter((entry) => entry.targetable);
|
|
console.log(`Failed Docker E2E entries: ${entries.map((entry) => entry.lane).join(", ")}`);
|
|
if (workflowEntries.length > 0) {
|
|
console.log("");
|
|
const workflowGroups = groupByReuseInputs(workflowEntries);
|
|
if (workflowGroups.length === 1) {
|
|
console.log("Combined GitHub rerun:");
|
|
console.log(
|
|
ghWorkflowCommand(
|
|
workflowEntries.map((entry) => entry.lane),
|
|
ref,
|
|
workflow,
|
|
commonReuseInputs(workflowEntries),
|
|
),
|
|
);
|
|
} else {
|
|
console.log("Combined GitHub reruns:");
|
|
for (const group of workflowGroups) {
|
|
const lanes = group.map((entry) => entry.lane);
|
|
console.log(
|
|
`- ${lanes.join(", ")}: ${ghWorkflowCommand(lanes, ref, workflow, group[0]?.reuseInputs)}`,
|
|
);
|
|
}
|
|
}
|
|
console.log("");
|
|
console.log("Per-lane GitHub reruns:");
|
|
for (const entry of workflowEntries) {
|
|
console.log(
|
|
`- ${entry.lane}: ${ghWorkflowCommand([entry.lane], ref, workflow, entry.reuseInputs)}`,
|
|
);
|
|
}
|
|
} else {
|
|
console.log("");
|
|
console.log("No targetable failed Docker E2E lanes found.");
|
|
}
|
|
console.log("");
|
|
console.log("Local rerun starting points:");
|
|
for (const entry of entries) {
|
|
if (entry.localRerunCommand) {
|
|
console.log(`- ${entry.lane}: ${entry.localRerunCommand}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
function main(): void {
|
|
const options = parseArgs(process.argv.slice(2));
|
|
if (options.help) {
|
|
console.log(usage());
|
|
return;
|
|
}
|
|
const isLocalJson = fs.existsSync(options.input) && fs.statSync(options.input).isFile();
|
|
if (isLocalJson) {
|
|
const entries = mergeByLane(failedLaneEntriesFromJson(options.input, options.ref), options.ref);
|
|
const ref = resolveTargetRef(entries, options.ref);
|
|
printEntries(entries, ref, options.workflow);
|
|
} else {
|
|
const repo = options.repo || detectRepo();
|
|
const runLocal = runInfo(options.input, repo);
|
|
const outputDir = options.dir || defaultOutputDir(options.input);
|
|
const artifactNames = downloadDockerArtifacts(options.input, repo, outputDir);
|
|
const files = findFiles(outputDir, new Set(["failures.json", "summary.json"]));
|
|
const entries = mergeByLane(
|
|
files.flatMap((file) => failedLaneEntriesFromJson(file, options.ref)),
|
|
options.ref,
|
|
);
|
|
const ref = resolveTargetRef(entries, options.ref);
|
|
console.log(`Artifacts: ${artifactNames.join(", ")}`);
|
|
console.log(`Downloaded: ${outputDir}`);
|
|
printEntries(entries, ref, options.workflow, runLocal);
|
|
}
|
|
}
|
|
|
|
try {
|
|
main();
|
|
} catch (error) {
|
|
console.error(error instanceof Error ? error.message : String(error));
|
|
process.exit(1);
|
|
}
|