From b59e59cdbf81a4724bc775d293964faecd3401f4 Mon Sep 17 00:00:00 2001 From: Kelvin Alexander Date: Sat, 11 Jul 2026 20:13:10 -0500 Subject: [PATCH] prs: surface real gh CLI error instead of generic auth message MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _gh() previously collapsed every failure mode (nonzero exit, timeout, bad JSON, missing binary) into a bare None, so fetch_prs() always raised the same 'gh CLI not found or not authenticated' RuntimeError regardless of the actual cause. This made a wrong --repo value (or any other real failure) indistinguishable from an auth problem, leading to hours of debugging auth/PATH/env when the server-side setup was correct the whole time. Now _gh() optionally collects the real stderr/exception detail via an _errors list, and fetch_prs() surfaces it in the exception message. Fully backward compatible — callers that don't pass _errors behave exactly as before. --- graphify/prs.py | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/graphify/prs.py b/graphify/prs.py index cdca478da..605e2522c 100644 --- a/graphify/prs.py +++ b/graphify/prs.py @@ -138,16 +138,28 @@ def _ci_icon(status: str) -> str: # ── GitHub data fetching ────────────────────────────────────────────────────── -def _gh(*args: str) -> list | dict | None: +def _gh(*args: str, _errors: list[str] | None = None) -> list | dict | None: try: result = subprocess.run( ["gh", *args], capture_output=True, text=True, timeout=30 ) if result.returncode != 0: + if _errors is not None: + _errors.append(result.stderr.strip() or f"gh exited with code {result.returncode}") return None return json.loads(result.stdout) - except (subprocess.TimeoutExpired, json.JSONDecodeError, FileNotFoundError): + except FileNotFoundError: + if _errors is not None: + _errors.append("gh CLI not found on PATH") + return None + except subprocess.TimeoutExpired: + if _errors is not None: + _errors.append("gh command timed out after 30s") + return None + except json.JSONDecodeError as e: + if _errors is not None: + _errors.append(f"gh returned non-JSON output: {e}") return None @@ -202,9 +214,15 @@ def fetch_prs(repo: str | None = None, base: str | None = None, limit: int = 50) if repo: args += ["--repo", repo] - raw = _gh(*args) + errors: list[str] = [] + raw = _gh(*args, _errors=errors) if raw is None: - raise RuntimeError("gh CLI not found or not authenticated. Run: gh auth login") + detail = errors[0] if errors else "unknown error" + raise RuntimeError( + f"gh CLI call failed: {detail}\n" + "If this looks like an auth issue, run: gh auth login\n" + "If it looks like a repo/permissions issue, double-check the --repo value." + ) prs = [] for item in raw: