fix(github): eliminate bare gh invocations, harden rate limiter quota tracking, and optimize project reconciliation (#235) - #236
Conversation
… tracking, and optimize project reconciliation (#235)
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved critical and moderate findings affect mutation-budget enforcement, rate-limit accounting, and request handling.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
This PR centralizes GitHub CLI execution through run_gh, strengthens quota-aware pacing and retries, and updates project reconciliation, output handling, tests, dependencies, and documentation.
Changes:
- Adds quota tracking, caching, mutation detection, pagination controls, and rate-managed GitHub operations.
- Updates project reconciliation, PR commands, output utilities, and authentication flows.
- Refreshes tests, documentation, roadmap entries, and task metadata.
File summaries
| File | Reviewed changes and final findings |
|---|---|
uv.lock |
Locks rate-limiting dependencies. |
tests/test_pr_cmd.py |
Updates PR command mocks and readiness coverage. |
tests/test_github_secrets.py |
Updates secret-operation mocks. |
tests/test_github_rate_limiter.py |
Tests quota and parsing behavior. |
tests/test_github_pr_monitor.py |
Updates monitor subprocess mocks. |
tests/test_github_pages.py |
Updates Pages command mocks. |
tests/test_github_milestones.py |
Updates milestone synchronization setup. |
tests/test_github_issues.py |
Updates issue command mocks. |
tests/test_github_client.py |
Updates client command mocks. |
tests/test_github_branch_protection.py |
Updates protection command mocks. |
tests/test_config_commands.py |
Updates authentication command mocks. |
src/devops_cli/output/formatters/panels.py |
Renders raw reviews as Markdown payloads. |
src/devops_cli/output/file_writer.py |
Adds path validation and serialization helpers. |
src/devops_cli/lang/en/messages.py |
Adds terminal PR messages. |
src/devops_cli/github/secrets.py |
Routes secret operations through run_gh. |
src/devops_cli/github/rate_limiter.py |
Adds pacing, caching, quota, retry, async, and pagination logic. L1576 — Moderate (3 votes): Effective request cost can be decremented twice. L1453 — Moderate (1 vote): Invalid utilization input is silently ignored. L1557 — Moderate (1 vote): Retry delay is not capped by max_backoff.L619 — Moderate (1 vote): quota_max_age does not affect cached quota validity.L1398 — Moderate (1 vote): Reaching the page cap does not report truncation. L177 — Moderate (2 votes): Non-dictionary payloads raise an unstructured TypeError.L1196 — Critical (1 vote): Non-POST mutations may be incorrectly cached. L619 — Moderate (2 votes): max_backoff and quota_max_age are stored without affecting behavior.L508 — Moderate (1 vote): Malformed resetAt data raises an unstructured ValueError.L495 — Moderate (1 vote): GraphQL mutations are not detected for mutation pacing and tracking. |
src/devops_cli/github/projects.py |
Adds owner caching and reconciliation controls. L804 — Moderate (2 votes): Issue synchronization lacks a shared mutation budget. L1110 — Critical (3 votes): Mutation limits reset and count items rather than individual API mutations. L1213 — Moderate (1 vote): Non-open states can be provisioned and reconciled. L1213 — Moderate (1 vote): Closed existing board items are excluded from metadata reconciliation. |
src/devops_cli/github/pr_monitor.py |
Routes monitoring requests through run_gh. |
src/devops_cli/github/pages.py |
Routes Pages requests through run_gh. |
src/devops_cli/github/issues.py |
Routes issue requests through run_gh. |
src/devops_cli/github/client.py |
Routes label and milestone requests through run_gh. |
src/devops_cli/github/branch_protection.py |
Routes protection calls through run_gh.L132 — Moderate (1 vote): The project-wide bare- gh elimination claim remains false because release PR creation still uses run_subprocess. |
src/devops_cli/config/defaults.py |
Adds GitHub limiter defaults. |
src/devops_cli/config/constants.py |
Adds GitHub command and mutation constants. |
src/devops_cli/commands/pr.py |
Uses rate-managed PR commands and readiness handling. L731 — Moderate (2 votes): Removing --milestone is an undocumented breaking change.L20 — Moderate (1 vote): Release automation still bypasses the new rate-managed execution. |
src/devops_cli/commands/config.py |
Routes GitHub authentication through run_gh. |
README.md |
Updates PR edit documentation. |
pyproject.toml |
Adds rate-limiting dependencies. |
docs/ROADMAP.md |
Adds API budget protection roadmap work. |
docs/commands/pr.md |
Updates PR edit reference. |
docs/commands/gh.md |
Updates GitHub command reference. |
docs/CLI_REFERENCE.md |
Synchronizes CLI documentation. |
docs/agent/tasks/task-235-project-reconcile-rate-quota-exhaustion.md |
Adds task tracking details. L4 — Nit (2 votes): The task still has PR: TBD and Status: In Progress instead of linking this PR and using In Review. |
docs/agent/tasks/task-233-paginated-url-dry-run-reconciliation.md |
Updates the prior task status. |
Review details
Suppressed comments (10)
src/devops_cli/commands/pr.py:20
- The project-wide elimination claim is incomplete:
src/devops_cli/commands/release.pystill buildsgh pr createand_execute_release_prinvokes_get("run_subprocess")for both the primary and label-fallback commands. Release automation therefore bypasses the new pacing and quota tracking; route both calls throughrun_ghor narrow the PR description.
from devops_cli.github.rate_limiter import run_gh
src/devops_cli/github/branch_protection.py:132
- The PR summary claims all bare
ghinvocations are now routed throughrun_gh, butsrc/devops_cli/commands/release.py:866-873still executesgh pr createthroughrun_subprocess. The project-wide guarantee is therefore not true; include that path in the change or narrow the claim.
res = run_gh(cmd, check=False, quiet=True)
src/devops_cli/github/projects.py:1215
- Although the default is
open, exposingstateand passing it directly to both candidate fetches allows callers to requestallorclosed; those records are then provisioned and reconciled. That bypasses the issue's requirement to restrict candidate provisioning to open issues/PRs. Keep reconciliation fetches fixed toopenor validate/reject non-open states.
issues = _fetch_repository_issues(repo, state=state)
prs = _fetch_repository_prs(repo, state=state)
candidates = issues + prs
src/devops_cli/github/projects.py:1214
- Open-only filtering is applied to both candidate provisioning and field reconciliation. Items that are already on the board but now closed are absent from these API results, so their existing custom fields are never reconciled; keep provisioning limited to open items while separately evaluating metadata for all existing board URLs.
issues = _fetch_repository_issues(repo, state=state)
prs = _fetch_repository_prs(repo, state=state)
src/devops_cli/github/rate_limiter.py:1456
- The
record_utilizationexception handler discardsTypeError/ValueError, so malformed quota state or invalid utilization input is silently ignored. That contradicts the new propagation behavior and can leave the limiter believing it has quota when its accounting failed; let the exception propagate (or convert it to the typed rate-limit error) instead of usingpass.
try:
limiter.record_utilization(resource, cost=cost)
except TypeError, ValueError:
pass
src/devops_cli/github/rate_limiter.py:1559
max_backoffis configurable on the limiter, but this retry path uses the uncapped value fromcalculate_backoff_delay. A future reset timestamp can therefore make a retry sleep far longer than the configured 60-second bound, defeating the bounded-backoff circuit. Cap the computed delay withlimiter.max_backoffbefore sleeping.
backoff = limiter.calculate_backoff_delay(
combined_err, attempt=attempt + 1, subcommand=target_resource
)
src/devops_cli/github/rate_limiter.py:620
- The new
quota_max_agevalue is only stored; quota validity still checks onlyreset_epoch, neverlast_updated. A persisted response can therefore remain valid for hours until reset despite the 300-second maximum age, so pacing and the project safety check may rely on stale quota data. Apply this age when resolving/validating cached state, or remove the unused option.
max_backoff: float = DEFAULT_GH_MAX_BACKOFF_SECONDS,
quota_max_age: float = DEFAULT_GH_QUOTA_MAX_AGE_SECONDS,
src/devops_cli/github/rate_limiter.py:1398
- When the loop receives
max_pagesfull pages, it exits normally and returns a successful combined response without indicating that more pages exist. Paginated issue/project callers can therefore silently lose records beyond the 100-page cap and reconcile an incomplete candidate set. Return an explicit truncation failure/diagnostic when the cap is reached.
while page <= max_pages:
src/devops_cli/github/rate_limiter.py:509
- Malformed
resetAtdata now escapes as a bareValueError, so a bad GitHub response can bypass the structured CLI error taxonomy and surface as an unhandled built-in exception. RaiseGitHubRateLimitError(with bounded structured details) instead, consistent withAGENTS.md:349.
except (ValueError, TypeError) as err:
raise ValueError(f"Failed to parse resetAt timestamp '{reset_at}': {err}") from err
src/devops_cli/github/rate_limiter.py:496
- GraphQL mutations sent as
gh api graphql -f query=mutation ...are not recognized here:_is_api_mutationonly checks HTTP methods, while these commands use the default POST with the mutation in the query body. Project mutations such as_create_project_viewtherefore skipmutation_min_intervaland mutation tracking; detect the GraphQL operation type or pass explicit mutation metadata.
if clean[0] == "api":
return _is_api_mutation(clean[1:])
- Files reviewed: 33/34 changed files
- Comments generated: 8
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…ter caching, and milestone editing (#235) - Pass shared MutationBudget across issue sync, candidate provisioning, and custom field edits to enforce a strict aggregate cap of 25 mutations - Reject all HTTP mutation methods (POST, PUT, PATCH, DELETE) and field arguments in _is_cacheable_api_call - Retain --milestone option on devops pr edit with REST fallback - Raise GitHubRateLimitError instead of bare TypeError on non-dictionary data in QuotaState.from_dict - Enforce max_backoff in calculate_backoff_delay and quota_max_age in QuotaState.is_valid - Account effective_cost during limiter.acquire() and eliminate redundant second decrement in _post_process_run - Update Task 235 tracking to PR #236 and Status: In Review
…n tests (#235) - Remove DEFAULT_GH_MAX_PACING_DELAY_SECONDS and DEFAULT_GH_MAX_BACKOFF_SECONDS to ensure mandatory pauses are never bypassed or truncated - Ensure _acquire_locked pauses for full calculated request delay and calculate_backoff_delay pauses for full duration until reset - Properly mock _resolve_project_owner_arg in project tests to eliminate unmocked rate limit network calls
* feat(release): v0.2.19 * fix(release): draft PR description generator and milestone deliverable resolution (#218) (#219) * feat(release): fix draft PR description generator and milestone deliverable resolution * docs(agent): track task 218 for release draft pr description generator fix * fix(release): prevent duplicate text by formatting milestone issues as clean autolinks * fix(release): isolate milestone cache, dynamic draft checklist, and mock run_gh in tests * fix(ai): support main branch diffing and base resolution in review branch (#220) (#221) * fix(ai): support main branch diffing and base resolution in review branch (#220) * fix(review): propagate target_branch to target_ref and update task 220 status to In Review (#221) * fix(github): treat copilot review as completed when zero unresolved threads remain (#216) (#222) * fix(github): treat copilot review as completed when zero unresolved threads remain (#216) * docs(agent): update task 216 with PR #222 and status In Review * test(github): test monitor_pr across get_pr_monitoring_status boundary with mock threads (#222) * feat(ai): adaptive embedding batch sizing circuit breaker and timeout fallback (#117) (#223) * feat(ai): adaptive embedding batch sizing circuit breaker and timeout fallback (#117) * docs(agent): link PR #223 and transition task 117 to In Review * fix(ai): resolve review feedback on fallback cache poisoning and query key isolation (#117) * perf(ai): high-performance AST context packer with binary search truncation (#118) (#224) * perf(ai): high-performance AST context packer with binary search truncation (#118) * docs(agent): link PR #224 and transition task 118 to In Review * fix(ai): resolve review feedback on full-body binary search, empty AST isolation, and tokenizer prewarming (#118) * fix(ai): accurately evaluate truncation boolean and expand test coverage (#118) * fix(ai): polyglot tree-sitter file size boundary guard and resource containment (#119) (#225) * fix(ai): polyglot tree-sitter file size boundary guard and resource containment (#119) * fix(ai): harden repomap symlink resolution and syntax tuple * test(ci): isolate ci workspace root to tmp_path to prevent unlinking active coverage * perf(ai): parallel async branch and pr review worker pool with semaphore concurrency (#120) (#226) * perf(ai): parallel async branch and pr review worker pool with semaphore concurrency (#120) * docs(agent): link PR #226 to task-120 tracking * fix(ai): address Copilot review feedback on error isolation and sanitization * fix(security): sanitize CodeQL clear-text logging by recording exception type names * feat(ai): llm structured output retry and json schema repair engine (#123) (#230) * feat(ai): llm structured output retry and json schema repair engine (#123) * fix(ci): remove redundant pr merge readiness gate and remediate review findings (#123) (#230) * chore(ci): deduplicate pre-commit code checks and scope hooks to pre-commit stage * chore(ci): real-time quality gate streaming, re-entrant rate limit locking, and repos clone destination (#231) (#232) * chore(ci): real-time quality gate streaming, re-entrant rate limit locking, and project quota safety * feat(repos): parse organization destination from clone url instead of hardcoding standalone * docs(agent): add task-231 tracking for realtime CI, rate limiter locking, and repos clone * docs(agent): link PR #232 to task-231 tracking * fix(gh): isolate disk quota lock yield and add fallback pacing for unknown quotas * docs(agent): document task-231 CI failure remediation and merge readiness * fix(github): resolve paginated url query replacement loop and optimize dry-run reconciliation (#233) (#234) * docs(agent): record PR #232 merge and close task 231 * fix(github): resolve paginated url replacement loop, optimize dry-run project reconciliation, and add pr edit milestone (#233) * docs(agent): update task 233 status to in-review and link PR #234 * fix(github): resolve PR review comments for #234 with milestone resolution, off-board dry-run test, and tuple except syntax * fix: config * fix(github): eliminate bare gh invocations, harden rate limiter quota tracking, and optimize project reconciliation (#235) (#236) * fix(github): eliminate bare gh invocations, harden rate limiter quota tracking, and optimize project reconciliation (#235) * fix(github): replace hardcoded gh CLI strings with CONST_GH_CLI constant (#235) * fix(github): resolve review feedback on mutation budgeting, rate limiter caching, and milestone editing (#235) - Pass shared MutationBudget across issue sync, candidate provisioning, and custom field edits to enforce a strict aggregate cap of 25 mutations - Reject all HTTP mutation methods (POST, PUT, PATCH, DELETE) and field arguments in _is_cacheable_api_call - Retain --milestone option on devops pr edit with REST fallback - Raise GitHubRateLimitError instead of bare TypeError on non-dictionary data in QuotaState.from_dict - Enforce max_backoff in calculate_backoff_delay and quota_max_age in QuotaState.is_valid - Account effective_cost during limiter.acquire() and eliminate redundant second decrement in _post_process_run - Update Task 235 tracking to PR #236 and Status: In Review * fix(github): eliminate delay-bypassing caps and mock external calls in tests (#235) - Remove DEFAULT_GH_MAX_PACING_DELAY_SECONDS and DEFAULT_GH_MAX_BACKOFF_SECONDS to ensure mandatory pauses are never bypassed or truncated - Ensure _acquire_locked pauses for full calculated request delay and calculate_backoff_delay pauses for full duration until reset - Properly mock _resolve_project_owner_arg in project tests to eliminate unmocked rate limit network calls * fix(github): eliminate bare gh invocations, error masking, and inaccurate quota defaults (#237) (#238) * fix(github): eliminate bare gh invocations, error masking, and inaccurate quota defaults (#237) * docs(agent): link PR #238 in task-237 tracking * fix(github): resolve review feedback on error classification, syntax, and sync resiliency (#237) * feat(k8s): minikube gpu detection and dynamic service nodeport reachability fallback (#126) (#239) * feat(k8s): minikube gpu detection and dynamic service nodeport reachability fallback (#126) * docs(agent): update task-126 status to in review with pr #239 * fix(k8s): resolve PR #239 review feedback on valkey normalization, scheme fallback, and sanitization * feat(devcontainer): build from debian:sid with python3.14, uv 0.12.16, and CI caching optimizations * fix(devcontainer): normalize VERSION_CODENAME to trixie for devcontainer feature compatibility * feat(ai): proactive model prewarming and vram eviction governance (#127) (#240) * feat(ai): streaming reasoning think token parser and bounded stream sanitizer (#128) (#241) * feat(ai): streaming reasoning think token parser and bounded stream sanitizer (#128) * docs(task): update task-128 PR link to #241 * fix(ai): resolve review feedback on streaming reasoning and error bounds * feat(ai): high-throughput LLM gateway and distributed model router (#142) (#242) * feat(ai): high-throughput LLM gateway and distributed model router (#142) * docs(agent): update task-142 tracking with PR #242 * fix(ai): remediate review comments on llm gateway, vllm manifests, and router (#142) * fix: address PR #217 comments, remediate uv diagnostics, and add uv-check/lockfile quality gates (#243) * fix: address PR #217 comments, remediate uv diagnostics, upgrade dependencies, and add uv-check/lockfile quality gates * fix(devcontainer): support sys_bin path parameter and add edge-case coverage for shadowed binary reconciliation * fix: remove reconcile_shadowed_user_binaries and debian_sid test, honor table payload options, integrate outdated check into CI
Summary
ghCLI invocations project-wide (branch_protection.py,release.py,auth.py,commands/config.py), routing all GitHub CLI subprocesses through rate-managedrun_ghwith token-bucket pacing and automatic exponential backoff.QuotaStatemetrics default toNoneinstead of0so unknown states never produce false zero quota, and errors during rate limit evaluation are explicitly propagated rather than masked.CONST_GH_NON_API_COMMANDS(auth,version,help) toconstants.pyand marks them rate-limit exempt in_is_rate_limit_exemptto avoid triggering unauthenticated/rate_limitqueries during initial auth setup.extract_json_payloadagainst non-string/mock inputs and preservesrun_subprocessnamespace inrate_limiter.pyfor clean test mocking.