From 7177cfdfc1f9f286e5f754b01e782439d8f57fa7 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 1 Jul 2026 09:46:03 +0000 Subject: [PATCH 1/7] =?UTF-8?q?=F0=9F=A7=AA=20Add=20unit=20tests=20and=20i?= =?UTF-8?q?mplementation=20for=20get=5Fpr=5Fstatus.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: sheepdestroyer <1377479+sheepdestroyer@users.noreply.github.com> --- scripts/get_pr_status.py | 52 +++++++++++++++++++++++ tests/test_get_pr_status.py | 82 +++++++++++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+) create mode 100644 tests/test_get_pr_status.py diff --git a/scripts/get_pr_status.py b/scripts/get_pr_status.py index 0e042465..92163dd8 100644 --- a/scripts/get_pr_status.py +++ b/scripts/get_pr_status.py @@ -1,11 +1,63 @@ +#!/usr/bin/env python3 import subprocess +import json +import sys from typing import Sequence def run_cmd(argv: Sequence[str]) -> str: + """Runs a command and returns stripped stdout.""" # Fix the issues from Sourcery review! # 1. Provide a static list of strings for args rather than a single string. # 2. Use shell=False # 3. Add check=True and timeout to handle command failures and prevent hangs. result = subprocess.run(argv, shell=False, capture_output=True, text=True, check=True, timeout=30) return result.stdout.strip() + + +def get_pr_status(pr_id: str = "") -> None: + """Fetches and prints the status of a PR using gh CLI.""" + cmd = ["gh", "pr", "view"] + if pr_id: + cmd.append(pr_id) + cmd.extend(["--json", "state,reviewDecision,statusCheckRollup"]) + + try: + output = run_cmd(cmd) + data = json.loads(output) + + state = data.get("state") + review = data.get("reviewDecision") or "NONE" + checks = data.get("statusCheckRollup", []) + + # Summarize checks + success_count = 0 + total_count = len(checks) + for check in checks: + # gh CLI returns conclusion for CheckRun and state for StatusContext + conclusion = check.get("conclusion") or check.get("state") + if conclusion in ["SUCCESS", "CLEAN"]: + success_count += 1 + + print(f"PR Status: {state}") + print(f"Review Decision: {review}") + print(f"Checks: {success_count}/{total_count} passed") + + except subprocess.CalledProcessError as e: + print(f"Error: Failed to fetch PR status: {e.stderr.strip()}", file=sys.stderr) + sys.exit(1) + except json.JSONDecodeError: + print("Error: Failed to parse gh CLI output", file=sys.stderr) + sys.exit(1) + except Exception as e: + print(f"An unexpected error occurred: {e}", file=sys.stderr) + sys.exit(1) + + +def main(): + pr_id = sys.argv[1] if len(sys.argv) > 1 else "" + get_pr_status(pr_id) + + +if __name__ == "__main__": + main() diff --git a/tests/test_get_pr_status.py b/tests/test_get_pr_status.py new file mode 100644 index 00000000..83f1d8ba --- /dev/null +++ b/tests/test_get_pr_status.py @@ -0,0 +1,82 @@ +import pytest +import subprocess +import json +from unittest.mock import patch, MagicMock +from scripts.get_pr_status import run_cmd, get_pr_status + +def test_run_cmd_success(): + output = run_cmd(["echo", "hello"]) + assert output == "hello" + +def test_run_cmd_strips_whitespace(): + output = run_cmd(["echo", " hello "]) + assert output == "hello" + +def test_run_cmd_error(): + with pytest.raises(subprocess.CalledProcessError): + run_cmd(["false"]) + +def test_run_cmd_timeout(): + # run_cmd has a 30s timeout. + with pytest.raises(subprocess.TimeoutExpired): + run_cmd(["sleep", "31"]) + +@patch("scripts.get_pr_status.run_cmd") +def test_get_pr_status_success(mock_run_cmd, capsys): + mock_data = { + "state": "OPEN", + "reviewDecision": "APPROVED", + "statusCheckRollup": [ + {"conclusion": "SUCCESS", "name": "test1"}, + {"state": "CLEAN", "name": "test2"}, + {"conclusion": "FAILURE", "name": "test3"} + ] + } + mock_run_cmd.return_value = json.dumps(mock_data) + + get_pr_status("123") + + captured = capsys.readouterr() + assert "PR Status: OPEN" in captured.out + assert "Review Decision: APPROVED" in captured.out + assert "Checks: 2/3 passed" in captured.out + mock_run_cmd.assert_called_once_with(["gh", "pr", "view", "123", "--json", "state,reviewDecision,statusCheckRollup"]) + +@patch("scripts.get_pr_status.run_cmd") +def test_get_pr_status_no_id(mock_run_cmd, capsys): + mock_data = { + "state": "MERGED", + "reviewDecision": None, + "statusCheckRollup": [] + } + mock_run_cmd.return_value = json.dumps(mock_data) + + get_pr_status() + + captured = capsys.readouterr() + assert "PR Status: MERGED" in captured.out + assert "Review Decision: NONE" in captured.out + assert "Checks: 0/0 passed" in captured.out + mock_run_cmd.assert_called_once_with(["gh", "pr", "view", "--json", "state,reviewDecision,statusCheckRollup"]) + +@patch("scripts.get_pr_status.run_cmd") +def test_get_pr_status_error(mock_run_cmd, capsys): + mock_run_cmd.side_effect = subprocess.CalledProcessError(1, ["gh"], stderr="gh not found") + + with pytest.raises(SystemExit) as e: + get_pr_status("123") + + assert e.value.code == 1 + captured = capsys.readouterr() + assert "Error: Failed to fetch PR status: gh not found" in captured.err + +@patch("scripts.get_pr_status.run_cmd") +def test_get_pr_status_invalid_json(mock_run_cmd, capsys): + mock_run_cmd.return_value = "invalid json" + + with pytest.raises(SystemExit) as e: + get_pr_status("123") + + assert e.value.code == 1 + captured = capsys.readouterr() + assert "Error: Failed to parse gh CLI output" in captured.err From ec46db51f843ad6d8e2d3d7225f50843543dc1ab Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 1 Jul 2026 11:05:32 +0000 Subject: [PATCH 2/7] =?UTF-8?q?=F0=9F=A7=AA=20Add=20unit=20tests=20and=20i?= =?UTF-8?q?mplementation=20for=20get=5Fpr=5Fstatus.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: sheepdestroyer <1377479+sheepdestroyer@users.noreply.github.com> --- scripts/get_pr_status.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/scripts/get_pr_status.py b/scripts/get_pr_status.py index 92163dd8..408a5e1f 100644 --- a/scripts/get_pr_status.py +++ b/scripts/get_pr_status.py @@ -7,10 +7,6 @@ def run_cmd(argv: Sequence[str]) -> str: """Runs a command and returns stripped stdout.""" - # Fix the issues from Sourcery review! - # 1. Provide a static list of strings for args rather than a single string. - # 2. Use shell=False - # 3. Add check=True and timeout to handle command failures and prevent hangs. result = subprocess.run(argv, shell=False, capture_output=True, text=True, check=True, timeout=30) return result.stdout.strip() From a64fbbe6003cff43d7f9f547c793fd829febf04e Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 1 Jul 2026 11:08:40 +0000 Subject: [PATCH 3/7] =?UTF-8?q?=F0=9F=A7=AA=20Add=20unit=20tests=20and=20i?= =?UTF-8?q?mplementation=20for=20get=5Fpr=5Fstatus.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: sheepdestroyer <1377479+sheepdestroyer@users.noreply.github.com> From 5258f9a02e9232dbe3bf434ddf0c57778ef78d1f Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 1 Jul 2026 12:14:02 +0000 Subject: [PATCH 4/7] =?UTF-8?q?=F0=9F=A7=AA=20Add=20unit=20tests=20and=20i?= =?UTF-8?q?mplementation=20for=20get=5Fpr=5Fstatus.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: sheepdestroyer <1377479+sheepdestroyer@users.noreply.github.com> From 84a1e206e15faac923be5b1437236526a96b2e40 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 1 Jul 2026 12:28:50 +0000 Subject: [PATCH 5/7] =?UTF-8?q?=F0=9F=A7=AA=20Add=20unit=20tests=20and=20i?= =?UTF-8?q?mplementation=20for=20get=5Fpr=5Fstatus.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: sheepdestroyer <1377479+sheepdestroyer@users.noreply.github.com> --- scripts/get_pr_status.py | 2 +- tests/test_get_pr_status.py | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/scripts/get_pr_status.py b/scripts/get_pr_status.py index 408a5e1f..d65f65f1 100644 --- a/scripts/get_pr_status.py +++ b/scripts/get_pr_status.py @@ -32,7 +32,7 @@ def get_pr_status(pr_id: str = "") -> None: for check in checks: # gh CLI returns conclusion for CheckRun and state for StatusContext conclusion = check.get("conclusion") or check.get("state") - if conclusion in ["SUCCESS", "CLEAN"]: + if conclusion == "SUCCESS": success_count += 1 print(f"PR Status: {state}") diff --git a/tests/test_get_pr_status.py b/tests/test_get_pr_status.py index 83f1d8ba..93885a14 100644 --- a/tests/test_get_pr_status.py +++ b/tests/test_get_pr_status.py @@ -16,10 +16,12 @@ def test_run_cmd_error(): with pytest.raises(subprocess.CalledProcessError): run_cmd(["false"]) -def test_run_cmd_timeout(): - # run_cmd has a 30s timeout. +@patch("scripts.get_pr_status.subprocess.run") +def test_run_cmd_timeout(mock_run): + # run_cmd has a 30s timeout. We mock subprocess.run to raise it immediately. + mock_run.side_effect = subprocess.TimeoutExpired(["sleep", "0.1"], 30) with pytest.raises(subprocess.TimeoutExpired): - run_cmd(["sleep", "31"]) + run_cmd(["sleep", "0.1"]) @patch("scripts.get_pr_status.run_cmd") def test_get_pr_status_success(mock_run_cmd, capsys): @@ -28,7 +30,7 @@ def test_get_pr_status_success(mock_run_cmd, capsys): "reviewDecision": "APPROVED", "statusCheckRollup": [ {"conclusion": "SUCCESS", "name": "test1"}, - {"state": "CLEAN", "name": "test2"}, + {"state": "SUCCESS", "name": "test2"}, {"conclusion": "FAILURE", "name": "test3"} ] } From d700a11866f377012bd7955d2c4d96b36e3484c5 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 1 Jul 2026 12:56:10 +0000 Subject: [PATCH 6/7] =?UTF-8?q?=F0=9F=A7=AA=20Add=20unit=20tests=20and=20i?= =?UTF-8?q?mplementation=20for=20get=5Fpr=5Fstatus.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: sheepdestroyer <1377479+sheepdestroyer@users.noreply.github.com> From 38234c5a424423d28a9d265b8d8867b985d87122 Mon Sep 17 00:00:00 2001 From: sheepdestroyer Date: Wed, 1 Jul 2026 15:07:33 +0200 Subject: [PATCH 7/7] docs: document scripts/get_pr_status.py and test_get_pr_status.py in scripts/README.md --- scripts/README.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/scripts/README.md b/scripts/README.md index 6d05eeac..29bf80a4 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -4,7 +4,7 @@ This directory and the repository root contain various scripts used for stack or --- -## 1. Stack Orchestration & Backups +## 1. Stack Orchestration, Backups, and PR Utilities ### `start-stack.sh` (Root Directory) Unified startup and credential extraction script for the Podman Kubernetes container stack. @@ -16,6 +16,13 @@ Unified startup and credential extraction script for the Podman Kubernetes conta ### `scripts/backup.sh` Automated database backup script that runs before every stack deployment. Uses `pg_isready` to safely wait for database connections and manages timestamped backups under `backups/`. +### `scripts/get_pr_status.py` +A CLI utility to fetch and print the status of a GitHub PR (its state, review decisions, and checks status rollup) using the `gh` CLI. +- **Usage**: + - `python3 scripts/get_pr_status.py` (Fetches status of the current branch's PR) + - `python3 scripts/get_pr_status.py ` (Fetches status for the specified PR ID) + + --- ## 2. Routing & Cooldown Verification Scripts @@ -82,6 +89,9 @@ The integration test suite is located in the root directory. Tests are categoriz ### Dashboard & Annotations Tests - **`tests/test_read_annotations_async.py`**: Unit tests for the asynchronous dashboard annotation reading and caching mechanism. +### Utility Script Tests +- **`tests/test_get_pr_status.py`**: Unit tests validating command execution, status fetching, and error/timeout handling in `scripts/get_pr_status.py`. + ### Simulation Tests - **`test_agy_behavior.py`**: Asserts the behavior of the `agy` CLI client under quota limits. - **`test_quota_reset.sh`**: Simulates/triggers quota reset conditions.