Luigit
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);
	}
}