Add shared read-only gh CLI helper for maintenance scripts#1162
Add shared read-only gh CLI helper for maintenance scripts#1162yanyishuai wants to merge 1 commit into
Conversation
|
Warning Review limit reached
Next review available in: 38 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (5)
📝 WalkthroughWalkthroughAdds Changesgh CLI consolidation
Possibly related PRs
🚥 Pre-merge checks | ✅ 5 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (5 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 68a5ae58-d37a-4cf6-88f1-2b438ee1f62a
📒 Files selected for processing (6)
AGENTS.mdscripts/check_bounty_issue_states.pyscripts/check_live_bounty_closing_refs.pyscripts/gh_cli.pyscripts/pr_queue_health.pytests/test_gh_cli.py
| ./.venv/bin/python -m pytest | ||
| ./.venv/bin/python -m ruff format --check . | ||
| ./.venv/bin/python -m ruff check . | ||
| ./.venv/bin/python -m mypy app | ||
| ./.venv/bin/python scripts/template_text_smoke.py --render | ||
| ./.venv/bin/python -m pytest tests/test_gh_cli.py -q |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
Consolidate or clarify the dual pytest invocations.
Line 38 already runs the full test suite, which would include tests/test_gh_cli.py. Line 43 repeats just that file with -q, making the check list slower without clear benefit. Either remove line 43 if the full suite covers it, or add a brief comment explaining why the focused run is needed (e.g., "quick smoke for gh_cli changes").
If kept, consider swapping order so the faster targeted run comes first.
| def _run_gh(args: list[str]) -> None: | ||
| """Run gh for maintainer --fix paths (may mutate GitHub state).""" | ||
| command = " ".join(args) | ||
| try: | ||
| subprocess.run( | ||
| args, | ||
| check=True, | ||
| capture_output=True, | ||
| text=True, | ||
| encoding="utf-8", | ||
| errors="replace", | ||
| timeout=GH_TIMEOUT_SECONDS, | ||
| ) | ||
| except subprocess.TimeoutExpired as exc: | ||
| raise RuntimeError(f"gh command timed out after {GH_TIMEOUT_SECONDS}s: {command}") from exc | ||
| except subprocess.CalledProcessError as exc: | ||
| raise RuntimeError( | ||
| "gh command failed " | ||
| f"(exit {exc.returncode}): {command}\n" | ||
| f"stdout:\n{exc.stdout or exc.output or ''}\n" | ||
| f"stderr:\n{exc.stderr or ''}" | ||
| ) from exc | ||
|
|
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Handle missing gh on the --fix path.
_run_gh still lets FileNotFoundError escape, so --fix will raise a traceback instead of the standardized missing-gh RuntimeError that the new shared helper returns for live read-only commands.
Proposed fix
except subprocess.TimeoutExpired as exc:
raise RuntimeError(f"gh command timed out after {GH_TIMEOUT_SECONDS}s: {command}") from exc
+ except FileNotFoundError as exc:
+ raise RuntimeError(
+ "GitHub CLI executable 'gh' was not found; install gh and ensure it is on PATH "
+ "before using --fix"
+ ) from exc
except subprocess.CalledProcessError as exc:
raise RuntimeError(
"gh command failed "
f"(exit {exc.returncode}): {command}\n"
f"stdout:\n{exc.stdout or exc.output or ''}\n"📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| def _run_gh(args: list[str]) -> None: | |
| """Run gh for maintainer --fix paths (may mutate GitHub state).""" | |
| command = " ".join(args) | |
| try: | |
| subprocess.run( | |
| args, | |
| check=True, | |
| capture_output=True, | |
| text=True, | |
| encoding="utf-8", | |
| errors="replace", | |
| timeout=GH_TIMEOUT_SECONDS, | |
| ) | |
| except subprocess.TimeoutExpired as exc: | |
| raise RuntimeError(f"gh command timed out after {GH_TIMEOUT_SECONDS}s: {command}") from exc | |
| except subprocess.CalledProcessError as exc: | |
| raise RuntimeError( | |
| "gh command failed " | |
| f"(exit {exc.returncode}): {command}\n" | |
| f"stdout:\n{exc.stdout or exc.output or ''}\n" | |
| f"stderr:\n{exc.stderr or ''}" | |
| ) from exc | |
| def _run_gh(args: list[str]) -> None: | |
| """Run gh for maintainer --fix paths (may mutate GitHub state).""" | |
| command = " ".join(args) | |
| try: | |
| subprocess.run( | |
| args, | |
| check=True, | |
| capture_output=True, | |
| text=True, | |
| encoding="utf-8", | |
| errors="replace", | |
| timeout=GH_TIMEOUT_SECONDS, | |
| ) | |
| except subprocess.TimeoutExpired as exc: | |
| raise RuntimeError(f"gh command timed out after {GH_TIMEOUT_SECONDS}s: {command}") from exc | |
| except FileNotFoundError as exc: | |
| raise RuntimeError( | |
| "GitHub CLI executable 'gh' was not found; install gh and ensure it is on PATH " | |
| "before using --fix" | |
| ) from exc | |
| except subprocess.CalledProcessError as exc: | |
| raise RuntimeError( | |
| "gh command failed " | |
| f"(exit {exc.returncode}): {command}\n" | |
| f"stdout:\n{exc.stdout or exc.output or ''}\n" | |
| f"stderr:\n{exc.stderr or ''}" | |
| ) from exc |
🧰 Tools
🪛 ast-grep (0.44.0)
[error] 126-134: Use of unsanitized data to create processes
Context: subprocess.run(
args,
check=True,
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=GH_TIMEOUT_SECONDS,
)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
(os-system-unsanitized-data)
[error] 126-134: Command coming from incoming request
Context: subprocess.run(
args,
check=True,
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=GH_TIMEOUT_SECONDS,
)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
(subprocess-from-request)
| @@ -0,0 +1,54 @@ | |||
| from __future__ import annotations | |||
|
|
|||
| import json | |||
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Remove the unused json import.
CI is already failing ruff check with F401 on Line 3, so this file will not pass the required lint gate until it is dropped. As per coding guidelines, "Check code quality and linting with ruff check .".
🧰 Tools
🪛 GitHub Actions: CI / 0_Quality, readiness, docs, and image checks.txt
[error] 3-3: ruff check: F401 json imported but unused. Remove unused import: json
🪛 GitHub Actions: CI / Quality, readiness, docs, and image checks
[error] 3-3: Ruff (F401): json imported but unused. help: Remove unused import: json
Sources: Coding guidelines, Pipeline failures
| def test_run_gh_json_parses_stdout(monkeypatch) -> None: | ||
| def fake_run(*args, **kwargs): | ||
| return subprocess.CompletedProcess(args[0], 0, stdout='{"ok": true}', stderr="") | ||
|
|
||
| monkeypatch.setattr(gh_cli.subprocess, "run", fake_run) | ||
| assert gh_cli.run_gh_json(["gh", "issue", "list"]) == {"ok": True} | ||
|
|
||
|
|
||
| def test_run_gh_json_reports_missing_gh(monkeypatch) -> None: | ||
| def missing_gh(*args, **kwargs): | ||
| raise FileNotFoundError("gh") | ||
|
|
||
| monkeypatch.setattr(gh_cli.subprocess, "run", missing_gh) | ||
|
|
||
| with pytest.raises(RuntimeError, match="GitHub CLI executable 'gh' was not found"): | ||
| gh_cli.run_gh_json(["gh", "issue", "list"]) | ||
|
|
||
|
|
||
| def test_run_gh_json_reports_nonzero_exit(monkeypatch) -> None: | ||
| def failing(*args, **kwargs): | ||
| raise subprocess.CalledProcessError(1, args[0], output="", stderr="boom") | ||
|
|
||
| monkeypatch.setattr(gh_cli.subprocess, "run", failing) | ||
|
|
||
| with pytest.raises(RuntimeError, match="gh command failed"): | ||
| gh_cli.run_gh_json(["gh", "issue", "list"]) | ||
|
|
||
|
|
||
| def test_assert_read_only_gh_command_blocks_mutating_api() -> None: | ||
| with pytest.raises(RuntimeError, match="refusing non-read-only gh api command"): | ||
| gh_cli.assert_read_only_gh_command(["gh", "api", "-X", "POST", "repos/x/y"]) | ||
|
|
||
|
|
||
| def test_assert_read_only_gh_command_blocks_issue_comment() -> None: | ||
| with pytest.raises(RuntimeError, match="refusing non-read-only gh command"): | ||
| gh_cli.assert_read_only_gh_command(["gh", "issue", "comment", "1"]) | ||
|
|
||
|
|
||
| def test_run_gh_returns_raw_stdout(monkeypatch) -> None: | ||
| def fake_run(*args, **kwargs): | ||
| return subprocess.CompletedProcess(args[0], 0, stdout="plain\n", stderr="") | ||
|
|
||
| monkeypatch.setattr(gh_cli.subprocess, "run", fake_run) | ||
| assert gh_cli.run_gh(["gh", "issue", "list"]) == "plain\n" |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Add a timeout regression for the new helper.
run_gh has a dedicated TimeoutExpired → RuntimeError path, but this suite never exercises it. That leaves one of the new negative paths unproven. As per coding guidelines, "Add or update tests for changed behavior." As per path instructions, "Focus on whether tests prove the changed behavior and include negative, replay, boundary, or regression cases where relevant."
Sources: Coding guidelines, Path instructions
|
Bounty #1009 review for current head Verdict: not merge-ready yet because Evidence: the file now imports both This is separate from the existing unused |
qingfeng312
left a comment
There was a problem hiding this comment.
Reviewed current head fd369977dae5adeae872fce70901fb826acfba69.
The branch gets through formatting but fails the lint gate in CI run 28314670389. ruff check . reports four fixable issues:
scripts/check_live_bounty_closing_refs.py:5: unusedsubprocessscripts/pr_queue_health.py:6: unusedsubprocessscripts/pr_queue_health.py:16: unusedDEFAULT_GH_TIMEOUT_SECONDS as GH_TIMEOUT_SECONDStests/test_gh_cli.py:3: unusedjson
Since the PR is meant to add a shared read-only gh CLI helper, keeping stale imports in the migrated call sites and test file leaves the quality gate red.
Please remove the unused imports or run Ruff's fixer and push the resulting small cleanup.
Scope checked: CI log, current PR metadata, CodeRabbit status, and changed-file list only. No wallet, treasury, payout, private data, credentials, or external mutation paths were exercised.
fd36997 to
1bca92f
Compare
qingfeng312
left a comment
There was a problem hiding this comment.
Reviewed current head 1bca92fb9b7cd74232a2c41ebc9baee0a4ef7def.
This still does not implement the stated shared-helper scope. The PR body says it adds scripts/gh_cli.py and tests/test_gh_cli.py, but the current diff only rewrites scripts/check_bounty_issue_states.py, scripts/check_live_bounty_closing_refs.py, and scripts/pr_queue_health.py; there is no shared helper module or targeted helper test. The changed scripts still keep local subprocess.run wrappers, and the hosted quality/readiness check is failing.
Please add the shared read-only gh helper and its tests, then migrate the scripts to call that helper with a focused diff.
e59bf5a to
c1cbedb
Compare
qingfeng312
left a comment
There was a problem hiding this comment.
Reviewed current head c1cbedb9b5c5689cb1f28ffd574e024b66e301c9.
Requesting changes because pytest cannot collect the suite on this head. The hosted quality gate run 28493734643 reports:
tests/test_check_bounty_issue_states.py:ModuleNotFoundError: No module named 'scripts.source_args'tests/test_check_live_bounty_closing_refs.py:ModuleNotFoundError: No module named 'scripts.source_args'tests/test_claim_inventory.py:ModuleNotFoundError: No module named 'scripts.public_json_fetch'
This PR migrates several scripts toward shared helpers, but the current diff does not include every helper required by the imported tests/modules. Please include the missing helper modules in the same branch, or keep those imports out of scope until the helper PRs land.
|
@qingfeng312 — proactive CRLF cleanup on this branch. Normalized LF line endings (no functional changes) in:
Should pass |
c1cbedb to
3278d80
Compare
|
@qingfeng312 — added missing |
c0077ad to
2dcbb9c
Compare
2dcbb9c to
22b6e2e
Compare
22b6e2e to
89efd0a
Compare
be611cd to
b5efea3
Compare
|
@qingfeng312 — pytest / AGENTS.md fully green on Status: logic + tests pass; diff minimized to PR-scoped files. Likely needs a maintainer-side Happy to push a follow-up formatting-only commit if you can point at the repo's pinned ruff version. Wallet: |
b5efea3 to
53b53f4
Compare
53b53f4 to
cde1521
Compare
cde1521 to
7b0a33a
Compare
7b0a33a to
8e0fa26
Compare
|
@qingfeng312 — CI fully green on latest head for bounty #1140. Fixes on current head (
Please recheck when convenient. Wallet: |
taherdhanera
left a comment
There was a problem hiding this comment.
Reviewed current head 8e0fa266e1058b277dc1ca95f8133dc5109ab95f. This still needs changes before merge.
The focused tests pass, but the current diff does not satisfy #1140's stated acceptance scope yet:
-
No existing maintenance script adopts the new helper on this head. The diff only adds
scripts/gh_cli.py,scripts/public_json_fetch.py,scripts/source_args.py, and two test files. The issue asks for "a shared helper" plus "at least some current duplicated maintenance scripts adopt it". Current source still has local wrappers in representative scripts such asscripts/check_bounty_issue_states.py,scripts/check_live_bounty_closing_refs.py,scripts/pr_queue_health.py,scripts/claim_inventory.py,scripts/proposed_work_triage.py,scripts/review_bounty_candidates.py, andscripts/submission_quality_gate.py. -
The read-only guard is incomplete for
gh api.gh apiswitches toPOSTautomatically when parameters are added, and GitHub's own help givesgh api repos/{owner}/{repo}/issues/123/comments -f body='Hi from CLI'as a comment-posting example. On this head,assert_read_only_gh_command()still allows these mutating forms:
['gh', 'api', 'repos/x/y/issues', '-f', 'title=test']['gh', 'api', 'repos/x/y/issues', '--field', 'title=test']['gh', 'api', 'repos/x/y/issues', '--raw-field', 'title=test']['gh', 'api', 'repos/x/y/issues', '--method=POST']['gh', 'api', 'repos/x/y/issues', '-XPOST']
It only rejects the split-token form --method POST. A shared read-only helper should either reject payload-bearing gh api flags unless --method GET is explicit, and handle combined method forms, or avoid advertising the helper as read-only enforcement.
Validation run:
python -m pytest tests\test_gh_cli.py tests\test_public_json_fetch.py -q-> 11 passed.python -m ruff check scripts\gh_cli.py scripts\public_json_fetch.py scripts\source_args.py tests\test_gh_cli.py tests\test_public_json_fetch.py-> passed.python -m ruff format --check scripts\gh_cli.py scripts\public_json_fetch.py scripts\source_args.py tests\test_gh_cli.py tests\test_public_json_fetch.py-> passed.git diff --check origin/main...HEAD-> clean.
Please migrate a small representative set of existing scripts to the helper, and tighten the gh api read-only enforcement/tests before merge.
Summary
Implements proposed work for #1140.
Closes #1140
Solana wallet for bounty payout: Do4v7foHJvRJLpRRoGaVPWX6DDEjX3yTK7J91gpwUQpE
Summary by CodeRabbit
New Features
Bug Fixes
Tests