From b6beed98c7e32132ebd3aff906efd2cf2ca5274f Mon Sep 17 00:00:00 2001 From: pikastech Date: Fri, 17 Jul 2026 23:37:49 +0200 Subject: [PATCH] fix: classify pending GitHub Actions logs --- scripts/src/gh-actions-runs.test.ts | 7 ++++++- scripts/src/gh/actions-runs.ts | 25 +++++++++++++++++++++++-- scripts/src/gh/types.ts | 1 + 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/scripts/src/gh-actions-runs.test.ts b/scripts/src/gh-actions-runs.test.ts index b8469096..9c251c57 100644 --- a/scripts/src/gh-actions-runs.test.ts +++ b/scripts/src/gh-actions-runs.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test"; -import { redactAndTailActionLog } from "./gh/actions-runs"; +import { isActionLogNotReady, redactAndTailActionLog } from "./gh/actions-runs"; import { withGhDefaultRendered } from "./gh/default-render"; describe("GitHub Actions run diagnostics", () => { @@ -22,6 +22,11 @@ describe("GitHub Actions run diagnostics", () => { expect(redactAndTailActionLog("a".repeat(100), 10, 12)).toBe("a".repeat(12)); }); + test("classifies a pending job log without claiming the repository is missing", () => { + expect(isActionLogNotReady({ status: 404, message: "The specified blob does not exist." })).toBe(true); + expect(isActionLogNotReady({ status: 404, message: "Not Found" })).toBe(false); + }); + test("renders startup failures and bounded logs without hiding evidence", () => { const startup = withGhDefaultRendered(["run", "view", "42"], { ok: true, diff --git a/scripts/src/gh/actions-runs.ts b/scripts/src/gh/actions-runs.ts index a52d7d05..dedebd88 100644 --- a/scripts/src/gh/actions-runs.ts +++ b/scripts/src/gh/actions-runs.ts @@ -1,4 +1,4 @@ -import { commandError, githubRequest, githubTextRequest, isGitHubError, validationError } from "./client"; +import { commandError, errorPayload, githubRequest, githubTextRequest, isGitHubError, validationError } from "./client"; interface WorkflowRun { id: number; @@ -77,6 +77,12 @@ export function redactAndTailActionLog(text: string, lineLimit = 80, charLimit = return redacted.split(/\r?\n/u).slice(-lineLimit).join("\n").slice(-charLimit); } +export function isActionLogNotReady(error: unknown): boolean { + if (typeof error !== "object" || error === null) return false; + const value = error as { status?: unknown; message?: unknown }; + return value.status === 404 && typeof value.message === "string" && value.message.toLowerCase().includes("blob does not exist"); +} + export async function actionRunList(repo: string, token: string, limit: number): Promise> { const parts = splitRepo(repo); if (parts === null) return validationError("run list", repo, "--repo must use owner/name format"); @@ -137,7 +143,22 @@ export async function actionRunLogs(repo: string, token: string, runId: number, if (parts === null) return validationError("run logs", repo, "--repo must use owner/name format"); const [owner, name] = parts; const log = await githubTextRequest(token, `/repos/${owner}/${name}/actions/jobs/${jobId}/logs`); - if (isGitHubError(log)) return commandError("run logs", repo, log, { runId, jobId, mutation: false }); + if (isGitHubError(log)) { + if (isActionLogNotReady(log)) { + return commandError("run logs", repo, errorPayload("action-log-not-ready", "job log is not available yet", { + status: 404, + retryable: true, + details: { runId, jobId, jobStatus: job.status }, + }), { + runId, + jobId, + mutation: false, + retryable: true, + next: `bun scripts/cli.ts gh run view ${runId} --repo ${repo}`, + }); + } + return commandError("run logs", repo, log, { runId, jobId, mutation: false }); + } const logTail = redactAndTailActionLog(log); return { ok: true, diff --git a/scripts/src/gh/types.ts b/scripts/src/gh/types.ts index 78d909ba..98797d88 100644 --- a/scripts/src/gh/types.ts +++ b/scripts/src/gh/types.ts @@ -187,6 +187,7 @@ export type GitHubDegradedReason = | "auth-failed" | "egress-failed" | "github-transient" + | "action-log-not-ready" | "network-proxy-failed" | "permission-denied" | "repo-not-found"