mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 09:20:43 +00:00
120 lines
3.5 KiB
JavaScript
120 lines
3.5 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import { execFileSync } from "node:child_process";
|
|
|
|
function parseTime(value) {
|
|
if (!value || value === "0001-01-01T00:00:00Z") {
|
|
return null;
|
|
}
|
|
const parsed = Date.parse(value);
|
|
return Number.isFinite(parsed) ? parsed : null;
|
|
}
|
|
|
|
function secondsBetween(start, end) {
|
|
return start !== null && end !== null ? Math.round((end - start) / 1000) : null;
|
|
}
|
|
|
|
function formatSeconds(value) {
|
|
return value === null ? "" : `${value}s`;
|
|
}
|
|
|
|
export function summarizeRunTimings(run, limit = 15) {
|
|
const created = parseTime(run.createdAt);
|
|
const updated = parseTime(run.updatedAt);
|
|
const jobs = (run.jobs ?? [])
|
|
.filter((job) => !job.name?.startsWith("matrix."))
|
|
.map((job) => {
|
|
const started = parseTime(job.startedAt);
|
|
const completed = parseTime(job.completedAt);
|
|
return {
|
|
conclusion: job.conclusion ?? "",
|
|
durationSeconds: secondsBetween(started, completed),
|
|
name: job.name,
|
|
queueSeconds: secondsBetween(created, started),
|
|
status: job.status,
|
|
};
|
|
});
|
|
const byDuration = [...jobs]
|
|
.filter((job) => job.durationSeconds !== null)
|
|
.toSorted((left, right) => right.durationSeconds - left.durationSeconds)
|
|
.slice(0, limit);
|
|
const byQueue = [...jobs]
|
|
.filter((job) => job.queueSeconds !== null && (job.durationSeconds ?? 0) > 5)
|
|
.toSorted((left, right) => right.queueSeconds - left.queueSeconds)
|
|
.slice(0, limit);
|
|
const badJobs = jobs.filter(
|
|
(job) => job.conclusion && !["success", "skipped", "cancelled"].includes(job.conclusion),
|
|
);
|
|
|
|
return {
|
|
byDuration,
|
|
byQueue,
|
|
conclusion: run.conclusion ?? "",
|
|
status: run.status ?? "",
|
|
wallSeconds: secondsBetween(created, updated),
|
|
badJobs,
|
|
};
|
|
}
|
|
|
|
function getLatestCiRunId() {
|
|
const raw = execFileSync(
|
|
"gh",
|
|
["run", "list", "--branch", "main", "--workflow", "CI", "--limit", "1", "--json", "databaseId"],
|
|
{ encoding: "utf8" },
|
|
);
|
|
const runs = JSON.parse(raw);
|
|
const runId = runs[0]?.databaseId;
|
|
if (!runId) {
|
|
throw new Error("No CI runs found on main");
|
|
}
|
|
return String(runId);
|
|
}
|
|
|
|
function loadRun(runId) {
|
|
return JSON.parse(
|
|
execFileSync(
|
|
"gh",
|
|
["run", "view", runId, "--json", "status,conclusion,createdAt,updatedAt,jobs"],
|
|
{
|
|
encoding: "utf8",
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
function printSection(title, jobs, metric) {
|
|
console.log(title);
|
|
for (const job of jobs) {
|
|
console.log(
|
|
`${String(job.name).padEnd(48)} ${formatSeconds(job[metric]).padStart(6)} queue=${formatSeconds(job.queueSeconds).padStart(6)} ${job.status}/${job.conclusion}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
async function main() {
|
|
const args = process.argv.slice(2);
|
|
const limitIndex = args.indexOf("--limit");
|
|
const limit =
|
|
limitIndex === -1 ? 15 : Math.max(1, Number.parseInt(args[limitIndex + 1] ?? "", 10) || 15);
|
|
const runId =
|
|
args.find((arg, index) => index !== limitIndex && index !== limitIndex + 1) ??
|
|
getLatestCiRunId();
|
|
const summary = summarizeRunTimings(loadRun(runId), limit);
|
|
|
|
console.log(
|
|
`CI run ${runId}: ${summary.status}/${summary.conclusion} wall=${formatSeconds(summary.wallSeconds)}`,
|
|
);
|
|
printSection("\nSlowest jobs", summary.byDuration, "durationSeconds");
|
|
printSection("\nLongest queues", summary.byQueue, "queueSeconds");
|
|
if (summary.badJobs.length > 0) {
|
|
console.log("\nFailed jobs");
|
|
for (const job of summary.badJobs) {
|
|
console.log(`${job.name} ${job.status}/${job.conclusion}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
await main();
|
|
}
|