Skip to content

feat(web-ui): PR status panel with live CI checks, review, and merge state#579

Merged
frankbria merged 2 commits intomainfrom
feat/570-pr-status-panel
Apr 13, 2026
Merged

feat(web-ui): PR status panel with live CI checks, review, and merge state#579
frankbria merged 2 commits intomainfrom
feat/570-pr-status-panel

Conversation

@frankbria
Copy link
Copy Markdown
Owner

@frankbria frankbria commented Apr 13, 2026

Closes #570

Summary

  • Backend (github_integration.py): CICheck dataclass + get_pr_ci_checks() + get_pr_review_status() — 3 total GitHub API calls (1 for PR raw data, 2 gathered in parallel for CI and reviews)
  • Backend (pr_v2.py): GET /api/v2/pr/status?workspace_path=...&pr_number=N endpoint with CICheckResponse + PRStatusResponse models; placed before /{pr_number} to avoid routing conflicts
  • Frontend (PRStatusPanel.tsx): SWR-polling Card rendered below CommitPanel when prNumber > 0; stops polling when merge_state is merged or closed; maps CI conclusions, review status, and merge state to Badge variants
  • Types + API client updated with CICheck, PRStatusResponse, prApi.getStatus()

Test plan

  • uv run pytest tests/ui/test_pr_status.py -v — 10/10 pass (mocked GitHub, no live API calls)
  • npm test -- --testPathPattern=PRStatusPanel — 19/19 pass
  • Full suite: uv run pytest && uv run ruff check . — clean
  • Full suite: npm test — 747/747 pass, npm run build — succeeds

Acceptance criteria

  • PR Status panel appears after PR creation (prNumber > 0)
  • CI checks list renders with correct status labels and badge variants
  • Review and merge state badges render
  • Polling stops after PR is merged/closed (refreshInterval returns 0)
  • npm test and uv run pytest pass for the new backend endpoint

Summary by CodeRabbit

  • New Features

    • Added a PR status API that returns CI checks, aggregated review status, merge state and PR metadata.
    • Added a PR Status Panel to the review sidebar showing CI checks, review badges, and merge-state badges.
    • Added client API and types to retrieve typed PR status; polling refreshes every 30s until merged/closed.
  • Tests

    • Added backend and frontend tests covering success, error cases, polling behavior, and refresh interval logic.

…state (#570)

Adds a live PR Status panel to the Review page that polls GitHub every 30 s
after a PR is created, surfacing CI check results, review status, and merge
state without leaving the app.

Backend:
- github_integration.py: add CICheck dataclass, get_pr_ci_checks(), and
  get_pr_review_status() methods (asyncio-parallel, 3 total GitHub API calls)
- pr_v2.py: add CICheckResponse + PRStatusResponse Pydantic models and
  GET /api/v2/pr/status endpoint (placed before /{pr_number} to avoid routing
  conflict); reuses _make_request for a single /pulls/{N} fetch then gathers
  CI checks and reviews in parallel

Frontend:
- types/index.ts: CICheck + PRStatusResponse interfaces
- api.ts: prApi.getStatus(workspacePath, prNumber)
- PRStatusPanel.tsx: SWR-polling Card component; refreshInterval=0 when
  merge_state is merged/closed; Badge variants map CI conclusions, review
  status, and merge state; loading skeleton while data pending
- review/page.tsx: render PRStatusPanel below CommitPanel when prNumber > 0

Tests:
- tests/ui/test_pr_status.py: 10 pytest tests (mocked GitHub, no live calls)
- PRStatusPanel.test.tsx: 19 Jest tests using jest.mock('swr') pattern
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 13, 2026

Walkthrough

Added a live PR Status feature: backend GET /api/v2/pr/status aggregates PR metadata, CI check runs, and review status from GitHub; frontend polls this endpoint every 30s via a new PRStatusPanel until the PR is merged or closed.

Changes

Cohort / File(s) Summary
Backend: GitHub integration
codeframe/git/github_integration.py
Added CICheck dataclass and methods get_pr_ci_checks(pr_number, head_sha=None) and get_pr_review_status(pr_number) to fetch/normalize commit check-runs and aggregate review states.
Backend: PR status endpoint
codeframe/ui/routers/pr_v2.py
Added CICheckResponse and PRStatusResponse models and implemented GET /api/v2/pr/status which fetches PR payload, concurrently gathers CI checks and review status, derives merge_state, and maps GitHub errors to HTTP responses.
Backend: Tests
tests/ui/test_pr_status.py
New test suite covering success scenarios (merge_state derivation, concurrent fetches, serialization) and error paths (validation, GitHub client/config errors, 404/other API errors).
Frontend: Types & API
web-ui/src/types/index.ts, web-ui/src/lib/api.ts
Added CICheck and PRStatusResponse TS interfaces and extended prApi with getStatus(workspacePath, prNumber) to call the new backend endpoint.
Frontend: PR Status UI
web-ui/src/components/review/PRStatusPanel.tsx, web-ui/src/app/review/page.tsx
New PRStatusPanel component (SWR polling every 30s, stops on merged/closed) and integrated it into ReviewPage sidebar beneath CommitPanel.
Frontend: Component tests
web-ui/src/__tests__/components/review/PRStatusPanel.test.tsx
Added Jest tests mocking SWR and API to validate loading/loaded/error states, badge mappings, CI check rendering, and SWR refreshInterval behavior.

Sequence Diagram

sequenceDiagram
    participant Browser as Browser / ReviewPage
    participant Panel as PRStatusPanel (SWR)
    participant Backend as GET /api/v2/pr/status
    participant GitHub as GitHub API

    Browser->>Panel: Mount with prNumber, workspacePath
    Panel->>Backend: GET /api/v2/pr/status?workspace_path=...&pr_number=...
    activate Backend
    Backend->>GitHub: GET /repos/.../pulls/{pr_number}
    GitHub-->>Backend: PR payload (head.sha, merged_at, state)

    par Concurrent fetches
        Backend->>GitHub: GET /repos/.../commits/{head_sha}/check-runs
        GitHub-->>Backend: check_runs list
    and
        Backend->>GitHub: GET /repos/.../pulls/{pr_number}/reviews
        GitHub-->>Backend: reviews list
    end

    Backend-->>Panel: PRStatusResponse (ci_checks, review_status, merge_state, pr_url, pr_number)
    deactivate Backend

    Panel->>Panel: Render badges + CI checks list
    loop Every 30s while merge_state == "open"
        Panel->>Backend: Poll PR status
        Backend->>GitHub: Fetch latest PR/checks/reviews
        Backend-->>Panel: Updated PRStatusResponse
    end
    alt merge_state == "merged" or "closed"
        Panel->>Panel: Stop polling (refreshInterval = 0)
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 Hopping through code with a twitch and a cheer,
I fetch checks and reviews so the panel is near,
Badges blink, statuses sing, polling through night,
Till merge or close ends the little race-flight,
Cheers — the PR's story now clear and bright!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 58.62% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding a PR status panel with live CI checks, review status, and merge state visibility, which is the core feature of this PR.
Linked Issues check ✅ Passed All key requirements from issue #570 are met: PR Status panel appears after PR creation, CI checks list renders with statuses, review and merge badges render, polling stops at merge/close, and tests pass.
Out of Scope Changes check ✅ Passed All changes directly support the PR status feature. Backend adds CI check and review status helpers; frontend adds panel component and integration; types and API client are extended to support the new endpoint.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/570-pr-status-panel

Comment @coderabbitai help to get the list of available commands and usage tips.

@claude
Copy link
Copy Markdown

claude bot commented Apr 13, 2026

PR Review — feat(web-ui): PR status panel with live CI checks, review, and merge state

Overall this is a solid, well-scoped implementation. The parallel API calls, stop-when-done polling, and test coverage are all good. A few issues worth addressing before merge:


Bug — Dismissed reviews treated as active (github_integration.py)

get_pr_review_status() does not filter out DISMISSED reviews. GitHub returns all reviews including ones the reviewer later dismissed. If a reviewer requested changes, then dismissed their review after the author force-pushed to address the comments, the method would still return "changes_requested".

Also, COMMENTED reviews are not actionable and should be excluded from the aggregation.

# current — considers all review states including DISMISSED and COMMENTED
has_changes_requested = any(r.get("state") == "CHANGES_REQUESTED" for r in reviews)
has_approved = any(r.get("state") == "APPROVED" for r in reviews)

# safer — only count actionable states
ACTIONABLE = {"APPROVED", "CHANGES_REQUESTED"}
active_reviews = [r for r in reviews if r.get("state") in ACTIONABLE]
has_changes_requested = any(r.get("state") == "CHANGES_REQUESTED" for r in active_reviews)
has_approved = any(r.get("state") == "APPROVED" for r in active_reviews)

Robustness — Non-list response from GitHub not guarded (github_integration.py)

get_pr_review_status() does reviews = reviews or [], which handles None, but if _make_request returns a dict (e.g. an unexpected response shape), the any(r.get("state") == ...) generator would iterate over dict keys (strings), and "somekey".get("state") raises AttributeError. Compare with get_pr_ci_checks() which already does isinstance(data, dict) for similar defensive handling.

reviews = await self._make_request(...)
if not isinstance(reviews, list):
    reviews = []

Design — Endpoint accesses private internals of GitHubIntegration (pr_v2.py)

The endpoint reaches into client._make_request, client.owner, and client.repo_name directly — bypassing the class interface:

# pr_v2.py:~170
pr_raw = await client._make_request(
    "GET",
    f"/repos/{client.owner}/{client.repo_name}/pulls/{pr_number}",
)

This couples the router tightly to the implementation details of GitHubIntegration. A public get_pr_details(pr_number) method on GitHubIntegration (returning the raw dict or a PRDetails-like object) would be cleaner and keep the change consistent with how get_pr_ci_checks() and get_pr_review_status() are structured.


Pagination silently truncates results

GitHub's check-runs and reviews endpoints both default to 30 results per page and do not auto-paginate. Repos with many CI checks (monorepos, large pipelines) will silently miss checks beyond the first 30 without any indication in the UI. At minimum, pass per_page=100 to push the limit, and consider adding a ?page= loop or at least surfacing a truncation warning in the response when total_count > len(check_runs) (the check-runs response includes total_count).


TypeScript types are loose (web-ui/src/types/index.ts)

The string fields use comments to document the allowed values, but TypeScript won't enforce them. This means a typo in the frontend (e.g. merge_state === "Merged") won't be caught at compile time.

// current
status: string;        // "queued" | "in_progress" | "completed"
conclusion: string | null;
review_status: string; // "approved" | "changes_requested" | "pending"
merge_state: string;   // "open" | "merged" | "closed"

// better
status: 'queued' | 'in_progress' | 'completed';
conclusion: 'success' | 'failure' | 'neutral' | 'cancelled' | 'timed_out' | 'action_required' | 'skipped' | null;
review_status: 'approved' | 'changes_requested' | 'pending';
merge_state: 'open' | 'merged' | 'closed';

The BadgeVariant union and REVIEW_BADGE/MERGE_BADGE lookup tables in PRStatusPanel.tsx are already written this way — the type definitions should match.


Minor — pr_url is fetched but not surfaced in the panel

PRStatusResponse includes pr_url, which is retrieved via a GitHub API call, but PRStatusPanel.tsx never renders it. Either add a clickable link (e.g. "View on GitHub" below the badges) or drop pr_url from the response to avoid the unused field.


Positive notes

  • Parallel CI + review fetching via asyncio.gather is the right call — avoids two sequential round trips.
  • refreshInterval returning 0 once merged/closed is clean; the SWR pattern is idiomatic.
  • Placing /status before /{pr_number} in the router to avoid the routing conflict is correct and the comment explains it.
  • The w-80 max-h-48 overflow-y-auto on the CI check list handles long check lists gracefully.
  • Test coverage (10 backend + 19 frontend) is thorough, with SHA-forwarding assertion in test_head_sha_forwarded_to_ci_checks being a nice detail.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@codeframe/git/github_integration.py`:
- Around line 424-431: The reviews payload from self._make_request may not be a
list of dicts, so before computing has_changes_requested and has_approved (and
before any use of reviews) validate/coerce the response: ensure reviews is a
list (default to []) and filter its items to dicts only (e.g., keep only items
where isinstance(item, dict)); then compute has_changes_requested =
any(r.get("state") == "CHANGES_REQUESTED" for r in filtered_reviews) and
has_approved = any(r.get("state") == "APPROVED" for r in filtered_reviews) so
.get is only called on dict objects and unexpected payload shapes are guarded.
- Around line 397-409: The code that builds CICheck instances from check_runs
can raise KeyError when GitHub returns partial entries; update the list
construction in the function that calls self._make_request so each run is
validated (ensure isinstance(run, dict) and that "name" and "status" exist)
before indexing, skip or provide safe defaults for malformed entries, use
run.get("conclusion") as already done, and optionally log or increment a metric
for skipped runs; this hardens the CICheck creation (referencing
variables/functions: self._make_request, data, check_runs, run, CICheck).

In `@codeframe/ui/routers/pr_v2.py`:
- Around line 174-176: The code directly indexes pr_raw to derive head_sha,
pr_url, and merge_state which will raise KeyError on malformed responses; before
using pr_raw["head"]["sha"], pr_raw["html_url"], and
pr_raw.get("merged_at")/["state"], explicitly validate the required fields exist
(e.g., ensure "head" is present and is a dict with "sha", and "html_url" and
"state" keys exist), or use safe lookups like pr_raw.get("head", {}).get("sha")
and pr_raw.get("html_url") and if any required value is missing raise a clear,
controlled error/HTTPException (with a helpful message and status code) or
return a validation error response rather than letting a KeyError bubble up;
make this check immediately before computing head_sha, pr_url, and merge_state
so callers receive deterministic error handling.
- Around line 167-213: The GitHubIntegration client returned by
_get_github_client() is never closed; wrap its usage in an async context manager
or ensure it's closed in a finally block to avoid leaking httpx connections.
Replace the direct assignment client = _get_github_client() with either "async
with _get_github_client() as client:" around the logic that calls
client.get_pr_ci_checks and client.get_pr_review_status, or add a try/finally
where you call await client.aclose() in the finally; ensure this runs for all
code paths including the GitHubAPIError and generic Exception handlers so the
GitHubIntegration/httpx AsyncClient is always closed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 75ab9776-1995-4b91-b868-036f5275823e

📥 Commits

Reviewing files that changed from the base of the PR and between 6166e0f and fbced41.

📒 Files selected for processing (8)
  • codeframe/git/github_integration.py
  • codeframe/ui/routers/pr_v2.py
  • tests/ui/test_pr_status.py
  • web-ui/src/__tests__/components/review/PRStatusPanel.test.tsx
  • web-ui/src/app/review/page.tsx
  • web-ui/src/components/review/PRStatusPanel.tsx
  • web-ui/src/lib/api.ts
  • web-ui/src/types/index.ts

Comment on lines +424 to +431
reviews = await self._make_request(
"GET",
f"/repos/{self.owner}/{self.repo_name}/pulls/{pr_number}/reviews",
)
reviews = reviews or []

has_changes_requested = any(r.get("state") == "CHANGES_REQUESTED" for r in reviews)
has_approved = any(r.get("state") == "APPROVED" for r in reviews)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Guard review payload type before state aggregation.

At Line 430-Line 431, r.get(...) assumes each review item is a dict. If reviews is non-list or mixed, this can raise and mask the real upstream issue.

Suggested hardening
-        reviews = await self._make_request(
+        reviews_raw = await self._make_request(
             "GET",
             f"/repos/{self.owner}/{self.repo_name}/pulls/{pr_number}/reviews",
         )
-        reviews = reviews or []
+        reviews = reviews_raw if isinstance(reviews_raw, list) else []
 
-        has_changes_requested = any(r.get("state") == "CHANGES_REQUESTED" for r in reviews)
-        has_approved = any(r.get("state") == "APPROVED" for r in reviews)
+        has_changes_requested = any(
+            isinstance(r, dict) and r.get("state") == "CHANGES_REQUESTED"
+            for r in reviews
+        )
+        has_approved = any(
+            isinstance(r, dict) and r.get("state") == "APPROVED"
+            for r in reviews
+        )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codeframe/git/github_integration.py` around lines 424 - 431, The reviews
payload from self._make_request may not be a list of dicts, so before computing
has_changes_requested and has_approved (and before any use of reviews)
validate/coerce the response: ensure reviews is a list (default to []) and
filter its items to dicts only (e.g., keep only items where isinstance(item,
dict)); then compute has_changes_requested = any(r.get("state") ==
"CHANGES_REQUESTED" for r in filtered_reviews) and has_approved =
any(r.get("state") == "APPROVED" for r in filtered_reviews) so .get is only
called on dict objects and unexpected payload shapes are guarded.

Comment on lines +167 to +213
client = _get_github_client()

# Single call to get PR state, URL, and head SHA.
pr_raw = await client._make_request(
"GET",
f"/repos/{client.owner}/{client.repo_name}/pulls/{pr_number}",
)
head_sha: str = pr_raw["head"]["sha"]
pr_url: str = pr_raw["html_url"]
merge_state: str = "merged" if pr_raw.get("merged_at") else pr_raw["state"]

# Fetch CI checks and reviews in parallel (2 more GitHub API calls).
ci_checks, review_status = await asyncio.gather(
client.get_pr_ci_checks(pr_number, head_sha=head_sha),
client.get_pr_review_status(pr_number),
)

return PRStatusResponse(
ci_checks=[
CICheckResponse(name=c.name, status=c.status, conclusion=c.conclusion)
for c in ci_checks
],
review_status=review_status,
merge_state=merge_state,
pr_url=pr_url,
pr_number=pr_number,
)

except GitHubAPIError as e:
if e.status_code == 404:
raise HTTPException(
status_code=404,
detail=api_error("PR not found", ErrorCodes.NOT_FOUND, f"No PR #{pr_number}"),
)
raise HTTPException(
status_code=e.status_code,
detail=api_error("GitHub API error", ErrorCodes.EXECUTION_FAILED, e.message),
)
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to get PR #{pr_number} status: {e}", exc_info=True)
raise HTTPException(
status_code=500,
detail=api_error("Failed to get PR status", ErrorCodes.EXECUTION_FAILED, str(e)),
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Close GitHubIntegration client in this endpoint to prevent connection leaks.

At Line 167, a new client is created but never closed. Since GitHubIntegration wraps httpx.AsyncClient, repeated polling can accumulate open connections.

Suggested fix
 async def get_pr_status(
@@
 ) -> PRStatusResponse:
@@
-    try:
-        client = _get_github_client()
+    client: Optional[GitHubIntegration] = None
+    try:
+        client = _get_github_client()
@@
     except Exception as e:
         logger.error(f"Failed to get PR #{pr_number} status: {e}", exc_info=True)
         raise HTTPException(
             status_code=500,
             detail=api_error("Failed to get PR status", ErrorCodes.EXECUTION_FAILED, str(e)),
         )
+    finally:
+        if client is not None:
+            await client.close()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codeframe/ui/routers/pr_v2.py` around lines 167 - 213, The GitHubIntegration
client returned by _get_github_client() is never closed; wrap its usage in an
async context manager or ensure it's closed in a finally block to avoid leaking
httpx connections. Replace the direct assignment client = _get_github_client()
with either "async with _get_github_client() as client:" around the logic that
calls client.get_pr_ci_checks and client.get_pr_review_status, or add a
try/finally where you call await client.aclose() in the finally; ensure this
runs for all code paths including the GitHubAPIError and generic Exception
handlers so the GitHubIntegration/httpx AsyncClient is always closed.

Comment on lines +174 to +176
head_sha: str = pr_raw["head"]["sha"]
pr_url: str = pr_raw["html_url"]
merge_state: str = "merged" if pr_raw.get("merged_at") else pr_raw["state"]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Validate required PR fields before deriving status fields.

Line 174-Line 176 directly indexes pr_raw; malformed-but-200 responses will raise KeyError and return generic 500s. Prefer explicit validation and a controlled error.

Suggested fix
-        head_sha: str = pr_raw["head"]["sha"]
-        pr_url: str = pr_raw["html_url"]
-        merge_state: str = "merged" if pr_raw.get("merged_at") else pr_raw["state"]
+        head_sha = (pr_raw.get("head") or {}).get("sha") if isinstance(pr_raw, dict) else None
+        pr_url = pr_raw.get("html_url") if isinstance(pr_raw, dict) else None
+        pr_state = pr_raw.get("state") if isinstance(pr_raw, dict) else None
+        if not head_sha or not pr_url or pr_state not in {"open", "closed"}:
+            raise HTTPException(
+                status_code=502,
+                detail=api_error(
+                    "Invalid GitHub PR payload",
+                    ErrorCodes.EXECUTION_FAILED,
+                    f"PR #{pr_number} missing required fields",
+                ),
+            )
+        merge_state: str = "merged" if pr_raw.get("merged_at") else pr_state
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codeframe/ui/routers/pr_v2.py` around lines 174 - 176, The code directly
indexes pr_raw to derive head_sha, pr_url, and merge_state which will raise
KeyError on malformed responses; before using pr_raw["head"]["sha"],
pr_raw["html_url"], and pr_raw.get("merged_at")/["state"], explicitly validate
the required fields exist (e.g., ensure "head" is present and is a dict with
"sha", and "html_url" and "state" keys exist), or use safe lookups like
pr_raw.get("head", {}).get("sha") and pr_raw.get("html_url") and if any required
value is missing raise a clear, controlled error/HTTPException (with a helpful
message and status code) or return a validation error response rather than
letting a KeyError bubble up; make this check immediately before computing
head_sha, pr_url, and merge_state so callers receive deterministic error
handling.

Skip entries that are not dicts or are missing required name/status fields,
logging a warning for malformed objects rather than raising KeyError.

Addresses CodeRabbit feedback on PR #579.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
codeframe/git/github_integration.py (1)

429-436: ⚠️ Potential issue | 🟡 Minor

Guard review entries before calling .get() in aggregation.

Line 435 and Line 436 assume every reviews item is a dict. Mixed payloads can raise AttributeError and fail /api/v2/pr/status. Coerce to a list and filter dicts first.

Suggested fix
-        reviews = await self._make_request(
+        reviews_raw = await self._make_request(
             "GET",
             f"/repos/{self.owner}/{self.repo_name}/pulls/{pr_number}/reviews",
         )
-        reviews = reviews or []
+        reviews = reviews_raw if isinstance(reviews_raw, list) else []
+        reviews = [r for r in reviews if isinstance(r, dict)]
 
         has_changes_requested = any(r.get("state") == "CHANGES_REQUESTED" for r in reviews)
         has_approved = any(r.get("state") == "APPROVED" for r in reviews)
#!/bin/bash
# Verify the current implementation still calls .get on untyped review entries.
# Expected result before fix: generator expressions on Line 435/436 use r.get(...) over raw reviews.
rg -n -C3 'has_changes_requested|has_approved|reviews = await self\._make_request|r\.get\("state"\)' codeframe/git/github_integration.py
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codeframe/git/github_integration.py` around lines 429 - 436, The aggregation
assumes every element in reviews is a dict, which can raise AttributeError;
before computing has_changes_requested and has_approved, coerce reviews to a
list and filter to dicts only (e.g., replace direct generator over reviews with
a filtered list comprehension), then compute has_changes_requested and
has_approved from that filtered collection; update the block around the reviews
variable (result of self._make_request for the PR reviews) so you only call
.get("state") on items confirmed to be dicts.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@codeframe/git/github_integration.py`:
- Around line 429-436: The aggregation assumes every element in reviews is a
dict, which can raise AttributeError; before computing has_changes_requested and
has_approved, coerce reviews to a list and filter to dicts only (e.g., replace
direct generator over reviews with a filtered list comprehension), then compute
has_changes_requested and has_approved from that filtered collection; update the
block around the reviews variable (result of self._make_request for the PR
reviews) so you only call .get("state") on items confirmed to be dicts.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c2326f9b-cc88-417d-bc15-4a40362e3198

📥 Commits

Reviewing files that changed from the base of the PR and between fbced41 and 77b0ceb.

📒 Files selected for processing (1)
  • codeframe/git/github_integration.py

@claude
Copy link
Copy Markdown

claude bot commented Apr 13, 2026

Review: PR Status Panel (Phase 4A)

Good overall structure — the three-layer separation (GitHub integration → FastAPI endpoint → SWR-polling component) is clean, test coverage is solid, and the asyncio.gather() for parallel CI+review fetches is the right approach. A few issues worth addressing before merge:


1. httpx client leak in get_pr_status (blocking)

_get_github_client() returns a GitHubIntegration that wraps an httpx.AsyncClient. None of the code paths (success, GitHubAPIError, or generic Exception) close it. Under load this leaks file descriptors. CodeRabbit flagged this too.

Fix — wrap in try/finally:

client = _get_github_client()
try:
    # ... all the gather logic
finally:
    await client.close()

2. Unsafe key access on pr_raw (blocking)

# pr_v2.py ~line 165-167
head_sha: str = pr_raw["head"]["sha"]
pr_url:   str = pr_raw["html_url"]
merge_state: str = "merged" if pr_raw.get("merged_at") else pr_raw["state"]

If GitHub returns a partial response, these raise KeyError and surface as an unhandled 500. The CI check path already uses .get() correctly — apply the same pattern here, and raise a clear 502 if required fields are absent. (CodeRabbit flagged this as well.)


3. Missing isinstance guard in get_pr_review_status

# github_integration.py ~line 84-85
has_changes_requested = any(r.get("state") == "CHANGES_REQUESTED" for r in reviews)

reviews or [] coerces to a list but doesn't filter non-dict items. A non-dict element will raise AttributeError on .get(). get_pr_ci_checks already does this correctly with if not isinstance(run, dict): continue — use the same guard:

filtered = [r for r in reviews if isinstance(r, dict)]

4. SWR key doesn't URL-encode workspacePath (new)

// PRStatusPanel.tsx
const swrKey = `/api/v2/pr/status?workspace_path=${workspacePath}&pr_number=${prNumber}`;

The actual API call goes through prApi.getStatus() which uses axios params (auto-encoded), so for paths with spaces or special characters the SWR cache key and the network request won't match. SWR may re-fetch unnecessarily or serve stale data.

Fix:

const swrKey = `/api/v2/pr/status?workspace_path=${encodeURIComponent(workspacePath)}&pr_number=${prNumber}`;

Minor observations

  • Type safety: review_status and merge_state are typed as plain string in types/index.ts. Union literal types ('open' | 'merged' | 'closed') would catch typos at compile time and let you drop the ?? MERGE_BADGE.open fallbacks in the badge maps.

  • Test gap: TestGetPrStatusErrors covers 404 and 503 but not the except Exception as e branch. A test passing a ValueError to verify it returns a clean 500 would complete the error-handling coverage.


Items 1-3 are the same concerns CodeRabbit raised; I'm seconding them as real production risks. Item 4 is new. Everything else is non-blocking — the overall structure and test coverage are solid.

@frankbria frankbria merged commit 71cbb25 into main Apr 13, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Phase 4A] PR Status: live CI checks, review status, and merge state

1 participant