repositories / dotfiles
dotfiles
bugabingas dorkfiles
owned by admin
pi/agent/skillz/github/github-cli/scripts/gh_failed_run.mjs
Raw#!/usr/bin/env node
import { spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";
const ANSI = [
/\x1b\[[0-9;]*m/g,
/\x1b\][^\x07]*\x07/g,
/\x1b\[[0-9;]*H/g,
/\x1b\[([0-9]+)?J/g,
/\x1b\[([0-9]+)?K/g,
/\r/g,
];
function clean(s) {
return ANSI.reduce((x, re) => x.replace(re, ""), s);
}
function arg(name) {
const i = process.argv.indexOf(name);
return i === -1 ? undefined : process.argv[i + 1];
}
function has(name) {
return process.argv.includes(name);
}
function checkGh() {
const r = spawnSync("gh", ["auth", "status"], { stdio: "ignore" });
return !r.error && r.status === 0;
}
function runJson(args, timeout = 60) {
const r = spawnSync("gh", args, {
encoding: "utf8",
timeout: timeout * 1000,
maxBuffer: 64 * 1024 * 1024,
});
if (r.error?.code === "ETIMEDOUT")
throw new Error(`gh command timed out after ${timeout}s`);
if (r.status !== 0)
throw new Error(
`Error running gh command: gh ${args.join(" ")}\nError: ${r.stderr}`,
);
return r.stdout ? JSON.parse(r.stdout) : {};
}
function repoArgs(repo) {
return repo ? ["--repo", repo] : [];
}
function runById(id, repo) {
try {
return runJson([
"run",
"view",
String(id),
"--json",
"databaseId,number,conclusion,status,createdAt,displayTitle,url,headBranch,headSha,event",
...repoArgs(repo),
]);
} catch {
return null;
}
}
function recentFailed(repo) {
const runs = runJson([
"run",
"list",
"--status",
"failure",
"--limit",
"1",
"--json",
"databaseId,number,conclusion,status,createdAt,displayTitle,url,headBranch,headSha,event",
...repoArgs(repo),
]);
return runs?.[0] ?? null;
}
function failedJobs(id, repo) {
const jobs =
runJson(["run", "view", String(id), "--json", "jobs", ...repoArgs(repo)])
.jobs ?? [];
return jobs.filter(
(j) => !["success", "skipped", null, undefined].includes(j.conclusion),
);
}
function jobLogArgs(runId, jobId, repo) {
return [
"run",
"view",
String(runId),
"--log-failed",
"--job",
String(jobId),
...repoArgs(repo),
];
}
function logs(runId, jobId, repo) {
const r = spawnSync("gh", jobLogArgs(runId, jobId, repo), {
encoding: "utf8",
timeout: 120000,
maxBuffer: 64 * 1024 * 1024,
});
return r.status === 0 ? r.stdout : "";
}
function excerpts(text, max = 50) {
const patterns = [
/\berror\b/i,
/\bfailed\b/i,
/\bfailure\b/i,
/\bexception\b/i,
/\bcannot\b/i,
/\bpanic\b/i,
/Process completed with exit code [1-9]/i,
/\btimeout\b/i,
];
const out = [];
for (const raw of text.split("\n")) {
const line = clean(raw).trim();
if (!line || /^##\[(command|group|endgroup)\]/.test(line)) continue;
if (patterns.some((p) => p.test(line)) && !out.includes(line))
out.push(line);
if (out.length >= max) break;
}
return out;
}
function analyze(repo, runId) {
const run = runId ? runById(runId, repo) : recentFailed(repo);
if (!run)
return {
error: `No failed runs found${runId ? ` for run ${runId}` : ""}`,
repository: repo || "current",
};
const result = {
run: {
number: run.number,
database_id: run.databaseId,
url: run.url,
workflow: run.displayTitle,
conclusion: run.conclusion,
status: run.status,
created_at: run.createdAt,
branch: run.headBranch,
commit: run.headSha,
event: run.event,
},
failed_jobs: [],
repository: repo || "current",
};
for (const job of failedJobs(run.databaseId, repo))
result.failed_jobs.push({
name: job.name,
conclusion: job.conclusion,
status: job.status,
started_at: job.startedAt,
completed_at: job.completedAt,
error_excerpts: excerpts(logs(run.databaseId, job.databaseId, repo)),
});
return result;
}
function main() {
if (!checkGh()) {
console.error("Error: gh CLI is not installed or not in PATH");
console.error("Install it from: https://cli.github.com/");
return 1;
}
const result = analyze(
arg("--repo"),
arg("--run-id") ? Number(arg("--run-id")) : undefined,
);
console.log(JSON.stringify(result, null, has("--pretty") ? 2 : 0));
return 0;
}
export const __test = {
clean,
excerpts,
jobLogArgs,
repoArgs,
};
if (process.argv[1] === fileURLToPath(import.meta.url)) {
try {
process.exit(main());
} catch (e) {
console.error(e.message);
process.exit(1);
}
}