Skip to content

feat(integrations): execute GitHub issue import + task traceability (#565)#609

Merged
frankbria merged 20 commits into
mainfrom
feature/issue-565-github-issues-import
Jun 4, 2026
Merged

feat(integrations): execute GitHub issue import + task traceability (#565)#609
frankbria merged 20 commits into
mainfrom
feature/issue-565-github-issues-import

Conversation

@frankbria
Copy link
Copy Markdown
Owner

@frankbria frankbria commented Jun 4, 2026

Summary

Implements #565 (Phase 5.5): execute the GitHub issue import wired up in #563/#564, and add task↔issue traceability.

  • Import: POST /api/v2/integrations/github/import turns selected issues into CodeFRAME tasks — title verbatim, body as description (with a best-effort **Labels:** footer), linked via github_issue_number + external_url. The #564 import modal's "Import Selected" now calls it, shows in-progress state, an in-modal error on failure, and a board summary banner (N created · M skipped) on success.
  • Traceability: imported tasks show an "Imported from GitHub #N" badge (links to the issue) in the task detail modal, plus a "Close GitHub issue when task is marked DONE" checkbox.
  • Auto-close: when an opted-in task reaches DONE — via the web UI, CLI, or agent/batch — the linked issue is closed (with a "Completed via CodeFRAME" comment). Implemented in core (tasks.update_status) so all completion paths are covered.
  • Dedup: a UNIQUE(workspace_id, external_url) index + URL-keyed checks prevent duplicate imports (atomic across concurrent requests; repo-aware so the same issue number in a different repo is still importable).

Acceptance Criteria

  • Import creates tasks with correct titles and descriptions
  • Imported tasks show the GitHub issue badge with correct link
  • Auto-close checkbox works end-to-end (web UI + CLI/agent completion)
  • Duplicate import protection (checks the source issue URL, not just the number)
  • npm test and uv run pytest pass

Test Plan

  • Unit/integration tests written first (TDD) — backend (core + routers) and frontend (components + api)
  • All tests passing — backend 2445 passed; frontend 955 passed + npm run build clean
  • Linting clean (ruff, eslint on changed files)
  • Internal review (advisory) — superseded by cross-family pass below
  • Cross-family review pass: codex — 14 iterative passes; every Critical/Major finding fixed or rebutted
  • Test mutation sanity check (tests assert outcomes, not just non-crash)

Known Limitations / Intentionally Deferred

  • Single GitHub PAT (machine-wide). Auto-close uses the one GIT_GITHUB credential (the [Phase 5.1] Settings page: API key management #555/[Phase 5.5] GitHub Issues import: repository connection (PAT auth) #563 model). If a workspace is reconnected to a different repo whose token lacks access to an older imported repo, that older task's auto-close will fail (logged, best-effort). Per-repo credential storage is a broader change than [Phase 5.5] GitHub Issues import: execute import and task traceability #565.
  • Auto-close completion vs. CLI exit latency (deliberate tradeoff). In sync/CLI contexts the close runs on a non-daemon thread so a short-lived process waits for the close to complete (reliable close > exit speed). Worst case adds up to the 10s client timeout when GitHub is slow/unreachable; zero added latency in the common case. In the server it schedules on the running event loop (no thread, non-blocking). Reverting to a daemon thread would silently abandon closes on CLI exit — rejected.
  • All-or-nothing import on fetch. A single stale/inaccessible/PR issue number fails the whole batch (4xx); partial-success reporting was not added. Already-created tasks are rolled back on a mid-create DB error, and the unique index makes any retry idempotent.
  • Standalone PATCH /issues/{n}/close endpoint dropped. The issue listed it, but it was unused and (lacking task context) would target the wrong repo after a reconnect. Auto-close via the core dispatch — which uses each task's source repo from external_url — fully covers the requirement.

Implementation Notes

Adapted from the issue's (stale, ~2mo old) Traycer plan: schema lives in core/workspace.py (not the removed persistence/schema_manager.py); issue ops added to core/github_issues_service.py (not the legacy PR class); the existing github_integrations_v2.py router was extended rather than adding a new one; frontend is web-ui/ (not legacy/web-ui/). Dedup reuses the existing github_issue_number column and keys on the full issue URL.

Closes #565

Summary by CodeRabbit

Release Notes

  • New Features

    • Import GitHub issues directly as tasks using a new dedicated import modal
    • Auto-close linked GitHub issues when their corresponding tasks are marked as complete (optional per-task setting)
    • View GitHub issue badges and links directly within task details
  • Improvements

    • Enhanced task interfaces and responses with GitHub issue metadata and traceability information

Test User added 17 commits June 4, 2026 06:50
Add external_url + auto_close_github_issue columns to the v2 tasks table
(both _init_database and _ensure_schema_upgrades), persist github_issue_number
through create(), and add get_by_github_issue_number() (duplicate-import
protection) and update_auto_close() helpers.
Backend for executing the GitHub issue import:
- core/github_issues_service: get_issue() + close_issue()
- POST /api/v2/integrations/github/import (dedupe via github_issue_number,
  labels footer in description) and PATCH /issues/{number}/close
- tasks_v2: expose github_issue_number/external_url/auto_close_github_issue on
  TaskResponse, accept auto_close toggle on PATCH, fire fire-and-forget issue
  close on the DONE transition (failures swallowed)
- tests disable the shared rate limiter to avoid ip:testclient bucket bleed
- integrationsApi.importIssues/closeIssue + tasksApi.updateGitHubSettings
- GitHubIssueBadge component (links a task to its source issue)
- TaskBoardView: real import flow with in-progress state + summary banner
  (N created · M skipped) and board refresh
- TaskDetailModal: GitHub badge + 'close issue when DONE' checkbox (optimistic)
- Task type extended with github_issue_number/external_url/auto_close_github_issue
Address cross-family (codex) review:
- P1: move GitHub issue auto-close from the HTTP PATCH path into core
  tasks.update_status (the single DONE chokepoint), so agent/batch (via
  runtime.complete_run) and CLI completions also close the linked issue.
  Fire-and-forget on a daemon thread, fully guarded — mirrors the existing
  blockers/conductor webhook dispatch pattern.
- P2: de-duplicate imports on the full issue URL (get_by_external_url) instead
  of the bare issue number, so the same number in a different connected repo is
  still importable. Import now fetches then de-dupes by html_url.
…se (#565)

Address second codex review pass:
- Persist auto_close_github_issue before the status transition in update_task so
  a single PATCH {status: DONE, auto_close_github_issue: true} closes the issue
  (the core DONE dispatch now reads the fresh flag).
- Run the auto-close thread non-daemon so a short-lived CLI process waits for
  the GitHub close at exit (bounded by the ~15s client timeout) instead of
  abandoning it. The server response still returns without joining.
…ide-effects (#565)

Third codex review pass:
- P1: auto-close now targets the task's source repo parsed from external_url,
  not the workspace's current connection — completing an old imported task
  after reconnecting to a different repo no longer closes the wrong issue.
- P2: import is two-phase (fetch+dedupe all, then create) so a mid-batch fetch
  failure leaves no partial tasks behind.
- P2: update_task pre-validates the status transition before any mutation, so a
  rejected PATCH no longer persists the auto-close flag (and illegal
  transitions now return a proper 400).
…565)

Fourth codex review pass:
- De-dupe issue numbers within a single /import payload so a repeated number
  creates one task (was: one per occurrence).
- Reject pull-request numbers in get_issue (NotAnIssueError -> 422), matching
  list_issues which already excludes PRs — a PR can no longer be imported as a
  task.
- Remove the unused standalone PATCH /issues/{n}/close endpoint + its frontend
  client. It was dead code and, lacking task context, would close the wrong
  repo's issue after a reconnect. Auto-close runs through the core dispatch,
  which correctly targets each task's source repo (external_url).
Fifth codex review pass:
- Auto-close is now context-aware (mirrors WebhookNotificationService): in the
  server it schedules on the running event loop (no thread to join at
  shutdown); only in sync/CLI contexts does it use a non-daemon thread so the
  close completes before the process exits. Bounded by a 10s timeout so a hung
  GitHub call never stalls a CLI for long. Resolves the daemon-vs-completion
  tension from the prior pass.
- Add a UNIQUE index on tasks(workspace_id, external_url) so duplicate-import
  protection is atomic: concurrent imports (double-submit / two tabs) can no
  longer both create a task for the same issue. The import catches the
  IntegrityError and records the loser as skipped.
Sixth codex review pass:
- close_issue: posting the completion comment is now best-effort. A locked
  issue or comment-disabled repo no longer aborts before the close — the close
  (the operation that matters) always proceeds.
- update_task: enabling auto-close on a task that is ALREADY DONE now closes the
  issue immediately (new tasks.autoclose_if_done helper), instead of silently
  doing nothing until some future re-completion.
#565)

Seventh codex review pass:
- The late opt-in close (for already-DONE tasks) now only fires when the PATCH
  is NOT also changing status. A combined {status: READY, auto_close: true}
  request reopens the task without closing the issue — previously it closed the
  issue while reopening the task (opposite of intent).

Rebuttal (out of scope): auto-close resolves the PAT from the single
machine-wide GIT_GITHUB credential (the #555/#563 model). After reconnecting a
workspace to a different repo, closing an older imported task whose repo needs a
different PAT may 403/404 (logged, best-effort). Per-repo credential storage is
a broader change than #565; documented as a Known Limitation.
…ure (#565)

Eighth codex review pass:
- get_issue now raises IssueNotFoundError on a 404 (distinct from upstream
  GitHubConnectError), and the import maps it to 404 instead of 502 — a
  stale/typo'd issue number is reported as a client error, not a server failure.
- TaskBoardView: the post-import board refresh (mutate) runs after the import
  resolves and outside its try/catch, so a transient /api/v2/tasks refresh error
  no longer flips a successful import into a 'Failed to import' banner.
Ninth codex review pass:
- get_issue: a 404 on the issue endpoint now probes the repo to distinguish a
  genuinely missing issue (-> IssueNotFoundError -> 404) from a broken
  integration (renamed/deleted repo or rotated token -> connect/auth error), so
  a real auth/repo failure is no longer reported to the user as a stale issue
  number. The probe only runs on the 404 path.
- update_task: apply the status transition BEFORE persisting the auto-close
  flag, so a concurrently-rejected transition (now a 409) leaves no persisted
  side effect. The explicit late-opt-in close now fires on a newly-enabled flag
  (false->true) when the task is DONE — double-fire-safe with the core
  DONE-transition close.
Tenth codex review pass:
- Import phase 2 now rolls back already-created tasks if a create fails with an
  unexpected DB error (e.g. OperationalError on a locked DB), preserving the
  all-or-nothing contract. (The URL-unique index still makes a retry idempotent.)
- A malformed saved repo slug (parse_repo ValueError) now returns 409 from
  /import, consistent with the browse endpoint, instead of a 500.
…back) (#565)

Eleventh codex review pass:
- Persist the auto-close flag BEFORE the status transition again, so the core
  DONE dispatch reads the value requested in THIS PATCH: {status: DONE, opt-out}
  no longer closes the issue (and {status: DONE, opt-in} still does).
- Restore the pass-9 atomicity guarantee differently: if the transition is
  rejected by a concurrent change, roll the flag back to its original value
  (409), so a failed request leaves no side effect.
- Late opt-in (false->true on an already-DONE task, no transition) still closes
  explicitly; double-fire and re-close are avoided by gating on
  (newly-enabled AND no transition in this request).
Twelfth codex review pass:
- Import failures now render inline INSIDE the import modal (new importError
  prop) and keep it open, so the error is visible and the selection is
  preserved for a retry — previously a board-level banner sat hidden behind the
  open dialog.
- After a successful import, the 60s issue-browse cache for the repo is
  invalidated, so reopening the modal no longer offers just-imported issues as
  selectable (they would now be skipped as duplicates).
Thirteenth codex review pass:
- Always invalidate the repo's issue-browse cache after an import, including
  all-skipped imports (issues created by another tab/process), so the modal
  never offers already-imported issues as selectable within the 60s TTL.

Rebuttal (P2, daemon thread — re-raised across passes 3/5/13): the auto-close
runs on a non-daemon thread in sync/CLI contexts BY DESIGN. For a CLI process to
RELIABLY close the issue it must wait for the close; abandoning it (daemon=True)
is a silent feature failure (the pass-3 finding). The wait is bounded by the 10s
client timeout and is zero in the common case (fast GitHub completes before the
command exits). Documented as a Known Limitation rather than reverted.
…ects) (#565)

Fourteenth codex review pass:
- get_issue 404 repo-probe: a degraded probe response (429/5xx) now raises
  GitHubConnectError instead of IssueNotFoundError, so a GitHub outage during
  import isn't misreported as a stale issue number.
- close_issue: a 3xx redirect (moved/renamed/transferred repo) is now treated as
  a failure — httpx doesn't follow redirects, so the PATCH wasn't applied and
  the issue is still open; we no longer report a silent success.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 4, 2026

Review Change Stack

Warning

Review limit reached

@frankbria, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 13 minutes and 36 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 31077165-06ae-4728-a45b-7cd925bb06e8

📥 Commits

Reviewing files that changed from the base of the PR and between b229206 and a15fa9c.

📒 Files selected for processing (5)
  • CLAUDE.md
  • codeframe/core/tasks.py
  • tests/conftest.py
  • tests/core/test_task_github_traceability.py
  • tests/ui/test_github_integrations_v2.py

Walkthrough

This PR implements end-to-end GitHub issue import and auto-close for CodeFRAME tasks. It adds GitHub service functions to fetch and close issues, extends the task model to track GitHub linkage and auto-close preferences, introduces an import API endpoint with deduplication and rollback semantics, updates task API responses and logic, and provides complete frontend UI for importing issues and toggling auto-close behavior.

Changes

GitHub Issue Import and Auto-Close Feature

Layer / File(s) Summary
GitHub issue service: get and close
codeframe/core/github_issues_service.py, tests/core/test_github_issues_service.py
New exception types (NotAnIssueError, IssueNotFoundError) distinguish PR vs. missing issues. get_issue() fetches single issue details and rejects PRs; on 404 it probes repo access to decide between missing issue vs. auth/token failure. close_issue() optionally posts a comment then patches issue state to closed, treating 3xx as failure. Tests cover PR rejection, field normalization, nuanced error mapping, and close ordering.
Task model and storage for GitHub linkage
codeframe/core/tasks.py, codeframe/core/workspace.py, tests/core/test_task_github_traceability.py
Task now stores github_issue_number, external_url, and auto_close_github_issue toggle. Database schema adds columns and a unique index on (workspace_id, external_url) for deduplication. New helpers: get_by_external_url() for lookup, update_auto_close() for toggling, autoclose_if_done() for late opt-in. update_status() dispatches best-effort background close via guarded async/thread execution when transitioning to DONE with opt-in. Tests verify defaults, persistence, uniqueness, dispatch gating, and missing PAT handling.
Issue import API endpoint and orchestration
codeframe/ui/routers/github_integrations_v2.py, tests/ui/test_github_integrations_v2.py
POST /api/v2/integrations/github/import validates connection, fetches selected issues, deduplicates within request and against existing imports, rejects PRs, creates tasks with all-or-nothing rollback on mid-create failure, treats sqlite3.IntegrityError as concurrent-import skip, invalidates issue cache after import, and returns created/skipped counts. Tests cover prerequisites, label augmentation, skipping, cache invalidation, error mapping (409 for bad repo, 404 for missing issues, 502 for fetch failures), rollback on create failure, and re-import after repo reconnection.
Task API response and update enhancements
codeframe/ui/routers/tasks_v2.py, tests/ui/test_v2_routers_integration.py
TaskResponse adds GitHub fields and centralized from_task() mapping used by list and detail endpoints. UpdateTaskRequest accepts auto_close_github_issue. update_task validates inputs early, persists auto-close before status change, rolls back flag on concurrent transition failure, and explicitly triggers autoclose_if_done() for late opt-in on already-DONE tasks. Tests verify response fields, PATCH toggle behavior, DONE dispatch triggering, and edge cases (reopen with opt-in, already-DONE opt-out, invalid transitions).
Frontend UI components for import and auto-close
web-ui/src/components/tasks/GitHubIssueBadge.tsx, web-ui/src/components/tasks/GitHubIssueImportModal.tsx, web-ui/src/components/tasks/TaskBoardView.tsx, web-ui/src/components/tasks/TaskDetailModal.tsx, web-ui/src/__tests__/components/tasks/*
GitHubIssueBadge renders a linked badge for imported issues. GitHubIssueImportModal now accepts importing state and importError to show progress and inline errors. TaskBoardView implements full async import flow, builds created/skipped summary, refreshes board via SWR mutate() on success, and shows dismissible banner. TaskDetailModal renders issue badge and auto-close checkbox that calls API to persist toggle. Tests cover rendering, link propagation, error display, import state UI, and API integration.
Frontend API client methods and type definitions
web-ui/src/lib/api.ts, web-ui/src/types/index.ts
Task type adds optional github_issue_number, external_url, and auto_close_github_issue fields. New import types: ImportedTaskSummary and GitHubImportResponse. tasksApi.updateGitHubSettings() patches auto-close preference. integrationsApi.importIssues() posts selected issue numbers and returns import response.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant TaskBoard
  participant ImportModal
  participant IntegrationsAPI
  participant TasksService
  participant GitHubAPI

  User->>TaskBoard: Click "Import from GitHub"
  TaskBoard->>ImportModal: Show modal
  User->>ImportModal: Select issues, click Import
  ImportModal->>IntegrationsAPI: POST /import with [issue_numbers]
  
  IntegrationsAPI->>TasksService: Validate connection
  IntegrationsAPI->>GitHubAPI: get_issue() for each number
  alt Issues fetched
    GitHubAPI-->>IntegrationsAPI: Issue details
    IntegrationsAPI->>IntegrationsAPI: Deduplicate & skip PRs
    IntegrationsAPI->>TasksService: create() for each unique
    alt Create succeeds
      TasksService-->>IntegrationsAPI: Created tasks
      IntegrationsAPI->>IntegrationsAPI: Invalidate cache
      IntegrationsAPI-->>ImportModal: {created, skipped, total}
      ImportModal->>TaskBoard: Close + trigger mutate()
      TaskBoard->>TaskBoard: Refresh board
    else Create fails mid-batch
      TasksService-->>IntegrationsAPI: Error
      IntegrationsAPI->>TasksService: Rollback created
      IntegrationsAPI-->>ImportModal: Import error
    end
  else Fetch fails
    GitHubAPI-->>IntegrationsAPI: Error (502)
    IntegrationsAPI-->>ImportModal: No tasks created
  end

  User->>TaskBoard: Mark task DONE with auto-close enabled
  TaskBoard->>TasksService: update_status(DONE) + auto_close=true
  TasksService->>GitHubAPI: close_issue() (background)
  GitHubAPI-->>TasksService: 200 OK
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • frankbria/codeframe#608: Directly precedes this PR by establishing the GitHub issue browser/import UI components and router foundation that this PR extends with actual import and auto-close backend implementation.
  • frankbria/codeframe#439: Related at the data model level through shared use of github_issue_number for GitHub issue linkage, though that PR detects externally closed issues while this PR auto-closes them.

Poem

🐰 A rabbit's leap to GitHub's shore,
Where issues dance and tasks explore,
Import them swift, close them fast,
Each completed task a task at last!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 53.39% 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 summarizes the main feature: GitHub issue import execution and task traceability, with issue reference for tracking. It is concise and clearly conveys the primary changes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/issue-565-github-issues-import

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

@claude
Copy link
Copy Markdown

claude Bot commented Jun 4, 2026

Code Review -- PR #609: GitHub Issue Import + Task Traceability (#565)

This PR completes Phase 5.5 by wiring up the import execution that was stubbed in #564 and adding full task-issue traceability. The scope, structure, and test coverage are all strong. A few targeted notes below.


Strengths

  • All-or-nothing import + unique-index dedup is the right call. The two-phase fetch-then-create design means a bad issue number fails cleanly without leaving phantom tasks that would confuse retries.
  • _repo_from_issue_url for auto-close targeting -- pulling the source repo out of the task's own external_url instead of the live workspace connection is the correct future-proof design. This is a subtle but important correctness choice.
  • Pre-validation before mutation in update_task -- persisting auto_close_github_issue before the DONE transition (so core sees the opt-in flag) and rolling it back on InvalidTransitionError is exactly right for the combined {status: DONE, auto_close: true} case.
  • TaskResponse.from_task() factory -- good cleanup; eliminates six copies of the same field-mapping pattern.
  • Test coverage -- 3xx redirect detection on close, repo-probe disambiguation for 404, concurrent-import race via the unique index, late opt-in on already-DONE tasks, and combined opt-in/opt-out/DONE in one request. That is the full edge-case matrix.

Issues

1. Connection leak in get_by_external_url (and get)

If cursor.execute or fetchone raises (e.g. OperationalError: database is locked), the connection leaks. get_by_external_url opens a connection without a try/finally block. The existing get() has the same pattern, but the new function could reasonably use the same try/finally structure used by create and update_auto_close. Low probability in practice, but worth fixing for consistency.

2. Test fragility around credential resolution in TestAutoCloseDispatch

test_done_dispatches_when_opted_in (and two sibling tests) mock _close_issue_background but rely on CredentialManager().get_credential(CredentialProvider.GIT_GITHUB) returning a non-falsy value so the dispatch actually reaches the mock. The only setup is monkeypatch.setenv("GITHUB_TOKEN", "ghp_token").

If CredentialManager does not fall back to the GITHUB_TOKEN env var on this code path, the dispatch silently short-circuits at the "no stored PAT" guard and calls stays empty -- giving a green test that is not actually testing what it claims. The explicit mock in test_done_skips_when_no_pat shows you know this guard exists. A comment explaining how the env var makes the credential resolve (or switching to a direct mock of CredentialManager.get_credential to return a fixture PAT) would make the test intent clear and remove the env-dependency.

3. asyncio/threading awareness in a core module

_close_issue_background in tasks.py calls asyncio.get_running_loop() to detect whether it is in an async context. The architecture rule is that core/** must not import FastAPI/HTTP frameworks -- not that it must be synchronous -- so this does not technically violate it. But it does make the core module context-aware in a way that feels like it belongs in an adapter layer. The comment explicitly models it on WebhookNotificationService.send_event_background, which lives in ui/ for good reason. If the dispatch logic ever needs to change (different timeouts per context, cancellation, etc.), having it in tasks.py makes it harder to test or swap.

Not asking for a refactor now -- the PR description documents this tradeoff and it is clearly deliberate -- but it is worth a # TODO: consider moving background dispatch to a caller-provided callback comment so future authors know it is a known pressure point.

4. Minor: update_auto_close mutates the fetched object directly

The function calls get(), then sets task.auto_close_github_issue = auto_close in-place before returning. This works correctly today but is surprising -- a reader would expect either a re-read from the DB or the function to document that it returns the same object with the field patched. A one-line comment or a return get(workspace, task_id) at the end would clarify intent.


Nitpicks

  • GitHubIssueBadge.tsx: issueUrl goes straight into href. GitHub API always returns HTTPS URLs so this is safe in practice, but a guard that the URL starts with https:// would be easy belt-and-suspenders. Not blocking.
  • importIssues in api.ts: the comment uses // style while all neighbours use /** */ JSDoc. Trivial consistency nit.
  • The seen_urls: set[str] in the import endpoint nicely handles intra-payload dedup of repeated issue numbers. The comment calling it out is appreciated -- it is easy to miss why both the set-check and the DB lookup are needed.

Summary

The implementation is correct, well-tested, and architecturally consistent with the existing patterns. The two items I would address before merge are the credential-resolution fragility in the dispatch tests (so the tests are clearly verifying what they claim) and the connection-leak edge case in get_by_external_url. Everything else is minor or documented tradeoff.

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: 5

🧹 Nitpick comments (1)
codeframe/core/tasks.py (1)

228-270: ⚡ Quick win

Make late opt-in auto-close part of update_auto_close().

Right now this helper only flips the persisted flag. For an already-DONE task, callers have to remember to also invoke autoclose_if_done(), which makes the late-opt-in contract easy to miss across entry points.

♻️ Suggested consolidation
 def update_auto_close(
     workspace: Workspace,
     task_id: str,
     auto_close: bool,
 ) -> Task:
@@
     task = get(workspace, task_id)
     if not task:
         raise ValueError(f"Task not found: {task_id}")
+    was_auto_close = task.auto_close_github_issue
 
     now = _utc_now().isoformat()
@@
     task.auto_close_github_issue = auto_close
     task.updated_at = datetime.fromisoformat(now)
+    if auto_close and not was_auto_close:
+        autoclose_if_done(workspace, task)
 
     return task
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@codeframe/core/tasks.py` around lines 228 - 270, The update_auto_close
function currently only flips the persisted auto_close flag; modify it so that
after successfully updating the DB and updating the Task object (in
update_auto_close), if auto_close is being turned on and the task is already in
DONE state, immediately invoke the existing autoclose_if_done helper to perform
the late opt-in auto-close flow; locate update_auto_close, get(), and
autoclose_if_done() and add the call (or import) so the DB commit happens first,
then set task.auto_close_github_issue = auto_close, task.updated_at = ..., and
if auto_close and task.status == "DONE" (or Task.Status.DONE) call
autoclose_if_done(workspace, task) (or the appropriate autoclose_if_done
signature) to apply the late opt-in behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@codeframe/core/github_issues_service.py`:
- Around line 300-323: get_issue currently parses resp.json() even for 3xx
responses and only raises on >=400; adjust get_issue to treat any status_code >=
300 as an error (consistent with close_issue) before calling resp.json(), e.g.
call _raise_for_status or explicitly check resp.status_code >= 300 and raise an
appropriate integration/error (not NotAnIssueError) so redirects/3xx are not
parsed into empty issue payloads; keep the existing PR check ("pull_request" ->
NotAnIssueError) and field defaults but ensure they only run after confirming
resp is a successful 2xx response.

In `@codeframe/ui/routers/github_integrations_v2.py`:
- Around line 98-101: ImportRequest.issue_numbers currently allows zero/negative
values; update its type to enforce item-level >=1 validation so bad payloads
fail at the API boundary. Replace list[int] with a constrained integer type for
list items (e.g., use pydantic.conint(ge=1) or pydantic.types.PositiveInt) and
import the required symbol, keeping the Field min_length=1 for the list itself;
update any type hints/usages that reference ImportRequest.issue_numbers
accordingly.

In `@codeframe/ui/routers/tasks_v2.py`:
- Around line 309-344: The handler currently uses truthiness checks so a
provided empty string status ({"status": ""}) is treated as omitted; change
presence checks to use "is not None" when deciding to fetch current (i.e., if
body.status is not None or body.auto_close_github_issue is not None) and when
validating status use "if body.status is not None:"; inside that branch
explicitly reject empty strings (e.g., if body.status == "" raise the same
400/api_error as invalid status), then attempt TaskStatus(body.status.upper())
and use can_transition(current.status, new_status) as before; references:
body.status, body.auto_close_github_issue, tasks.get, current, TaskStatus,
can_transition.

In `@web-ui/src/components/tasks/TaskBoardView.tsx`:
- Around line 131-137: The catch block currently calls setImportError(...) even
if the import modal was closed while the request was in flight; guard that call
by checking the modal's open state (e.g., an isImportModalOpen prop/state or a
mounted/ref flag) before calling setImportError, and also clear any stale import
error when the modal is opened (add a useEffect or open handler that calls
setImportError('') when the import modal becomes open) so users don't see prior
errors on reopen; update the catch to only setImportError if the modal is still
open and/or component is mounted.

In `@web-ui/src/components/tasks/TaskDetailModal.tsx`:
- Around line 133-146: handleToggleAutoClose can issue overlapping PATCHes
causing out-of-order persistence; add a local in-flight guard (e.g.,
isUpdatingAutoClose state ref/boolean) and early-return when an update is
already in progress, clear any existing error before starting, perform the
optimistic setTask update, await tasksApi.updateGitHubSettings, and only on
catch revert to the previous value and setError; ensure you reset the in-flight
flag in finally. Apply the identical pattern to the other GitHub-setting toggle
handler in this component.

---

Nitpick comments:
In `@codeframe/core/tasks.py`:
- Around line 228-270: The update_auto_close function currently only flips the
persisted auto_close flag; modify it so that after successfully updating the DB
and updating the Task object (in update_auto_close), if auto_close is being
turned on and the task is already in DONE state, immediately invoke the existing
autoclose_if_done helper to perform the late opt-in auto-close flow; locate
update_auto_close, get(), and autoclose_if_done() and add the call (or import)
so the DB commit happens first, then set task.auto_close_github_issue =
auto_close, task.updated_at = ..., and if auto_close and task.status == "DONE"
(or Task.Status.DONE) call autoclose_if_done(workspace, task) (or the
appropriate autoclose_if_done signature) to apply the late opt-in behavior.
🪄 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: ccb495d4-ec5f-4570-a4ab-c4c1259af34b

📥 Commits

Reviewing files that changed from the base of the PR and between 4010f38 and b229206.

📒 Files selected for processing (19)
  • codeframe/core/github_issues_service.py
  • codeframe/core/tasks.py
  • codeframe/core/workspace.py
  • codeframe/ui/routers/github_integrations_v2.py
  • codeframe/ui/routers/tasks_v2.py
  • tests/core/test_github_issues_service.py
  • tests/core/test_task_github_traceability.py
  • tests/ui/test_github_integrations_v2.py
  • tests/ui/test_v2_routers_integration.py
  • web-ui/src/__tests__/components/tasks/GitHubIssueBadge.test.tsx
  • web-ui/src/__tests__/components/tasks/GitHubIssueImportModal.test.tsx
  • web-ui/src/__tests__/components/tasks/TaskBoardView.import565.test.tsx
  • web-ui/src/__tests__/components/tasks/TaskDetailModal.test.tsx
  • web-ui/src/components/tasks/GitHubIssueBadge.tsx
  • web-ui/src/components/tasks/GitHubIssueImportModal.tsx
  • web-ui/src/components/tasks/TaskBoardView.tsx
  • web-ui/src/components/tasks/TaskDetailModal.tsx
  • web-ui/src/lib/api.ts
  • web-ui/src/types/index.ts

Comment on lines +300 to +323
_raise_for_status(resp.status_code, context="get issue")

raw = resp.json()
if not isinstance(raw, dict):
raw = {}
# The issues endpoint also returns pull requests (a PR is an issue with a
# ``pull_request`` member). Reject them so the import stays consistent
# with ``list_issues`` (which excludes PRs) and never links a PR as an
# issue.
if "pull_request" in raw:
raise NotAnIssueError(f"#{number} is a pull request, not an issue.")
labels_raw = raw.get("labels") or []
labels = [
(lbl.get("name") if isinstance(lbl, dict) else str(lbl))
for lbl in labels_raw
]
labels = [n for n in labels if n]
return {
"number": int(raw.get("number", number)),
"title": str(raw.get("title") or ""),
"body": str(raw.get("body") or ""),
"labels": labels,
"html_url": str(raw.get("html_url") or ""),
}
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 | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="codeframe/core/github_issues_service.py"

# Show surrounding definitions for get_issue, close_issue, and _raise_for_status
rg -n "async def (get_issue|close_issue)\b|def _raise_for_status\b" "$file"
python3 - <<'PY'
import re, itertools, pathlib
path = pathlib.Path("codeframe/core/github_issues_service.py")
txt = path.read_text(encoding="utf-8")
def show_context(name, window=80):
    # find def line
    m = re.search(rf"(async\s+def\s+{name}\b|def\s+{name}\b)", txt)
    if not m:
        print("Missing:", name); return
    start = txt.rfind("\n", 0, m.start())
    # compute line numbers
    lines = txt.splitlines()
    # map index to line
    idx = m.start()
    line_no = txt[:idx].count("\n")+1
    a=max(1,line_no-window)
    b=min(len(lines), line_no+window)
    for i in range(a,b+1):
        print(f"{i}:{lines[i-1]}")
    print("\n"+"="*60+"\n")
show_context("_raise_for_status", 120)
show_context("get_issue", 160)
show_context("close_issue", 160)
PY

# Also quickly check for any explicit redirect-follow settings or status handling in the client call sites.
rg -n "redirect|allow_redirect|3xx|status_code >|>= 300|>=300" "$file"

Repository: frankbria/codeframe

Length of output: 33619


Handle redirect/3xx responses in get_issue() instead of parsing them as an issue payload.

close_issue() already treats resp.status_code >= 300 as a failure, but get_issue() only raises on >= 400 and then proceeds to resp.json() and defaults missing fields—so a 3xx response can produce empty/incorrect imported issue data instead of a clear integration error.

🛠️ Suggested guard
         if resp.status_code == 404:
             try:
                 repo_resp = await client.get(
                     f"{GITHUB_API_BASE}/repos/{owner}/{name}", headers=_headers(pat)
                 )
@@
             if repo_resp.status_code >= 300:
                 raise GitHubConnectError(
                     f"GitHub repo check returned status {repo_resp.status_code}."
                 )
             raise IssueNotFoundError(f"Issue #{number} was not found in '{repo}'.")
         _raise_for_status(resp.status_code, context="get issue")
+        if resp.status_code >= 300:
+            raise GitHubConnectError(
+                f"GitHub get issue returned status {resp.status_code}; "
+                "issue details were not fetched."
+            )
 
         raw = resp.json()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@codeframe/core/github_issues_service.py` around lines 300 - 323, get_issue
currently parses resp.json() even for 3xx responses and only raises on >=400;
adjust get_issue to treat any status_code >= 300 as an error (consistent with
close_issue) before calling resp.json(), e.g. call _raise_for_status or
explicitly check resp.status_code >= 300 and raise an appropriate
integration/error (not NotAnIssueError) so redirects/3xx are not parsed into
empty issue payloads; keep the existing PR check ("pull_request" ->
NotAnIssueError) and field defaults but ensure they only run after confirming
resp is a successful 2xx response.

Comment on lines +98 to +101
class ImportRequest(BaseModel):
issue_numbers: list[int] = Field(
..., min_length=1, description="GitHub issue numbers to import as tasks"
)
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 | ⚡ Quick win

Reject non-positive issue numbers at the API boundary.

issue_numbers currently accepts 0 and negative values, so malformed payloads still go through the GitHub fetch path and fail later as lookup errors instead of a validation error. Add an item-level >= 1 constraint here so bad requests are rejected before any external call.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@codeframe/ui/routers/github_integrations_v2.py` around lines 98 - 101,
ImportRequest.issue_numbers currently allows zero/negative values; update its
type to enforce item-level >=1 validation so bad payloads fail at the API
boundary. Replace list[int] with a constrained integer type for list items
(e.g., use pydantic.conint(ge=1) or pydantic.types.PositiveInt) and import the
required symbol, keeping the Field min_length=1 for the list itself; update any
type hints/usages that reference ImportRequest.issue_numbers accordingly.

Comment on lines +309 to +344
if body.status or body.auto_close_github_issue is not None:
current = tasks.get(workspace, task_id)
if current is None:
raise HTTPException(
status_code=404,
detail=api_error(
"Task not found",
ErrorCodes.NOT_FOUND,
f"No task with id {task_id}",
),
)

new_status: Optional[TaskStatus] = None
if body.status:
try:
new_status = TaskStatus(body.status.upper())
tasks.update_status(workspace, task_id, new_status)
except ValueError as e:
if "Invalid status" in str(e) or "not a valid" in str(e).lower():
raise HTTPException(
status_code=400,
detail=api_error(
f"Invalid status: {body.status}",
ErrorCodes.VALIDATION_ERROR,
f"Valid values: {[s.value for s in TaskStatus]}",
),
)
# Status transition error
except ValueError:
raise HTTPException(
status_code=400,
detail=api_error("Invalid status transition", ErrorCodes.INVALID_STATE, str(e)),
detail=api_error(
f"Invalid status: {body.status}",
ErrorCodes.VALIDATION_ERROR,
f"Valid values: {[s.value for s in TaskStatus]}",
),
)
if not can_transition(current.status, new_status):
raise HTTPException(
status_code=400,
detail=api_error(
"Invalid status transition",
ErrorCodes.INVALID_STATE,
f"Cannot transition from {current.status.value} to "
f"{new_status.value}",
),
)

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 | ⚡ Quick win

Reject empty status values instead of treating them as omitted.

These truthiness checks let {"status": ""} skip validation entirely, so the handler can return 200 while silently ignoring a malformed status update. Use is not None for the presence checks, then validate the provided value.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@codeframe/ui/routers/tasks_v2.py` around lines 309 - 344, The handler
currently uses truthiness checks so a provided empty string status ({"status":
""}) is treated as omitted; change presence checks to use "is not None" when
deciding to fetch current (i.e., if body.status is not None or
body.auto_close_github_issue is not None) and when validating status use "if
body.status is not None:"; inside that branch explicitly reject empty strings
(e.g., if body.status == "" raise the same 400/api_error as invalid status),
then attempt TaskStatus(body.status.upper()) and use
can_transition(current.status, new_status) as before; references: body.status,
body.auto_close_github_issue, tasks.get, current, TaskStatus, can_transition.

Comment on lines +131 to +137
} catch (err) {
const apiErr = err as ApiError;
// Keep the modal open and show the error inline there, so the user sees
// it (a board-level banner would sit behind the dialog) and keeps their
// selection for a retry.
setImportError(apiErr.detail || 'Failed to import issues from GitHub');
} finally {
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 | ⚡ Quick win

Clear stale import errors on reopen after close-during-import.

At Line 136, setImportError(...) still runs if the modal was closed while the import request was in flight. On the next open, users can see a stale prior error before retrying.

Suggested fix
-        {githubConnected && (
-          <Button
-            variant="outline"
-            onClick={() => setImportModalOpen(true)}
-          >
+        {githubConnected && (
+          <Button
+            variant="outline"
+            onClick={() => {
+              setImportError(null);
+              setImportModalOpen(true);
+            }}
+          >
             <GithubIcon className="mr-2 h-4 w-4" />
             Import from GitHub
           </Button>
         )}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-ui/src/components/tasks/TaskBoardView.tsx` around lines 131 - 137, The
catch block currently calls setImportError(...) even if the import modal was
closed while the request was in flight; guard that call by checking the modal's
open state (e.g., an isImportModalOpen prop/state or a mounted/ref flag) before
calling setImportError, and also clear any stale import error when the modal is
opened (add a useEffect or open handler that calls setImportError('') when the
import modal becomes open) so users don't see prior errors on reopen; update the
catch to only setImportError if the modal is still open and/or component is
mounted.

Comment on lines +133 to +146
// Toggle "close GitHub issue when DONE" for imported tasks (#565). Optimistic:
// flip locally, then persist; revert on failure.
const handleToggleAutoClose = async (checked: boolean) => {
if (!task) return;
const previous = task.auto_close_github_issue ?? false;
setTask({ ...task, auto_close_github_issue: checked });
try {
await tasksApi.updateGitHubSettings(workspacePath, task.id, checked);
} catch (err) {
const apiErr = err as ApiError;
setError(apiErr.detail || 'Failed to update GitHub setting');
setTask({ ...task, auto_close_github_issue: previous });
}
};
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 | ⚡ Quick win

Prevent overlapping auto-close writes to avoid persisted state drift.

The toggle can dispatch concurrent PATCH requests; if responses arrive out of order, the saved auto_close_github_issue can end up opposite to the final UI selection. Gate updates while one request is in flight, and clear stale errors before retry.

Suggested fix
@@
   const [isUpdating, setIsUpdating] = useState(false);
+  const [isUpdatingGitHubSetting, setIsUpdatingGitHubSetting] = useState(false);
@@
   const handleToggleAutoClose = async (checked: boolean) => {
-    if (!task) return;
+    if (!task || isUpdatingGitHubSetting) return;
     const previous = task.auto_close_github_issue ?? false;
-    setTask({ ...task, auto_close_github_issue: checked });
+    setError(null);
+    setTask((prev) => (prev ? { ...prev, auto_close_github_issue: checked } : prev));
+    setIsUpdatingGitHubSetting(true);
     try {
       await tasksApi.updateGitHubSettings(workspacePath, task.id, checked);
     } catch (err) {
       const apiErr = err as ApiError;
       setError(apiErr.detail || 'Failed to update GitHub setting');
-      setTask({ ...task, auto_close_github_issue: previous });
+      setTask((prev) => (prev ? { ...prev, auto_close_github_issue: previous } : prev));
+    } finally {
+      setIsUpdatingGitHubSetting(false);
     }
   };
@@
                   <Checkbox
                     checked={task.auto_close_github_issue ?? false}
                     onCheckedChange={(c) => handleToggleAutoClose(c === true)}
+                    disabled={isUpdatingGitHubSetting}
                     aria-label="Close GitHub issue when task is marked DONE"
                   />

Also applies to: 237-243

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-ui/src/components/tasks/TaskDetailModal.tsx` around lines 133 - 146,
handleToggleAutoClose can issue overlapping PATCHes causing out-of-order
persistence; add a local in-flight guard (e.g., isUpdatingAutoClose state
ref/boolean) and early-return when an update is already in progress, clear any
existing error before starting, perform the optimistic setTask update, await
tasksApi.updateGitHubSettings, and only on catch revert to the previous value
and setError; ensure you reset the in-flight flag in finally. Apply the
identical pattern to the other GitHub-setting toggle handler in this component.

Test User added 2 commits June 4, 2026 10:16
The @rate_limit_* decorators bind the limiter at router-import time and the
limits library keeps counters in a process-shared in-memory store, so the fixed
ip:testclient AI bucket (connect/import = 20/min) is shared across the whole
test suite and exhausts in the full v2 run — 429ing /connect and surfacing as
spurious 409s in the GitHub import tests (green in isolation, red in CI).

Disable the global default limiter once in the root conftest, BEFORE any router
import, so every route imports unwrapped. Verified with the exact CI invocation
(pytest tests/ --ignore=tests/e2e -m v2): 2949 passed. Rate-limit-specific tests
build their own explicit Limiter and are unaffected; the per-file workaround is
removed.
…565)

Address advisory review feedback:
- Wrap the get_by_external_url DB connection in try/finally so it can't leak on
  a query error (matches the create/update_auto_close pattern).
- Comment the GITHUB_TOKEN -> CredentialManager.get_credential resolution in the
  auto-close dispatch tests so the test intent is explicit.

(Other advisory notes declined: in-place mutation in update_auto_close matches
every sibling update_* in the module; a TODO comment on the context-aware
dispatch is disallowed by project convention and the tradeoff is documented in
the PR body.)
@claude
Copy link
Copy Markdown

claude Bot commented Jun 4, 2026

Code Review — PR #609: GitHub Issue Import + Task Traceability

Overall this is a solid, well-structured implementation of Phase 5.5. The architecture is correct — auto-close lives in core/tasks.py, which is the single chokepoint every DONE transition flows through (CLI, HTTP, agent/batch). Test coverage is thorough and the two-phase import design (fetch-all then create) correctly enforces the all-or-nothing contract. A few items worth surfacing:


Bug / Correctness

autoclose_if_done late opt-in triggers an extra DB read (tasks_v2.py):

if (
    body.auto_close_github_issue
    and original_auto_close is False
    and new_status is None
):
    tasks.autoclose_if_done(workspace, tasks.get(workspace, task_id))

current is already in scope at this point and has just had its auto_close_github_issue flag updated by update_auto_close. tasks.get adds an unnecessary round-trip. Since autoclose_if_done only reads task.status, task.auto_close_github_issue, task.github_issue_number, and task.external_url — all correct on current after the mutation — you can update current.auto_close_github_issue in memory and pass it directly, skipping the re-read.


Test Reliability

GITHUB_TOKEN env-var coupling in core dispatch tests (tests/core/test_task_github_traceability.py):

Tests in TestAutoCloseDispatch use monkeypatch.setenv("GITHUB_TOKEN", "ghp_token") to satisfy the PAT check inside _dispatch_github_autoclose:

pat = CredentialManager().get_credential(CredentialProvider.GIT_GITHUB)
if not pat:
    return  # skip

This works only if CredentialManager reads GITHUB_TOKEN from the environment for the GIT_GITHUB provider. That coupling is implicit — if the env-var mapping ever changes, these tests silently become no-ops (calls is always []), masking a regression. test_done_skips_when_no_pat correctly patches CredentialManager.get_credential directly. Consider doing the same for the positive cases so the tests are independent of the credential store's env-var wiring.


Fragile Pattern (pre-existing, extended by this PR)

_row_to_task positional bounds checks (tasks.py):

external_url=row[19] if len(row) > 19 else None,
auto_close_github_issue=bool(row[20]) if len(row) > 20 and row[20] is not None else False,

This extends the existing len(row) > 17 pattern for hierarchical_id. It works today but any column reordering breaks it silently. Pre-existing technical debt — not a blocker for this PR, but each addition increases the surface area. Worth tracking for a future refactor to use sqlite3.Row or column-name indexing.


Test Infrastructure Change (global effect)

tests/conftest.py rate-limit disable:

Setting RATE_LIMIT_ENABLED=false at module level disables rate limiting for the entire test suite. The in-code comment explains the motivation clearly (the AI bucket at 20/min is shared across the full run; the import endpoint uses @rate_limit_ai()). This is the right fix. Confirm that tests explicitly asserting 429 behavior construct their own Limiter instance (the comment says they do — just confirming the claim holds for tests/ui/test_v2_routers_integration.py::TestRateLimiting).


Minor / Nit

Rollback test re-raises server exception through TestClient (test_create_failure_rolls_back_partial_import):

with pytest.raises(_sqlite3.OperationalError):
    client.post(...)

Relies on FastAPI TestClient's default raise_server_exceptions=True. It works and correctly verifies the rollback (empty task list after the error), but is worth being aware of if this pattern is refactored later.


Positives worth calling out

  • The two-phase import (fetch-all then create-all) correctly prevents partial imports on fetch failure; concurrent race is handled correctly via the sqlite3.IntegrityError path.
  • Source-repo targeting from external_url (not the live workspace connection) is exactly right — completing old imported tasks closes the correct issue even after a workspace reconnect.
  • _close_issue_background context detection (running loop vs. sync/CLI) mirrors the webhook dispatch pattern cleanly; the non-daemon thread choice for CLI is well-justified.
  • Deduplication keyed on full URL (not issue number) properly handles multi-repo workspaces.
  • Pre-validation before mutation in update_task is a meaningful improvement over the old error-path behavior.

Verdict: Approve with the note on the GITHUB_TOKEN coupling. The extra DB read in the late opt-in path is minor but easy to fix. The token coupling is the one test reliability concern worth addressing before merge.

@claude
Copy link
Copy Markdown

claude Bot commented Jun 4, 2026

Code Review - feat(integrations): GitHub Issue Import + Task Traceability (#565)

Overview

This PR completes Phase 5.5 cleanly: import endpoint, auto-close dispatch, dedup via unique index, and traceability fields are all wired up end-to-end. Architecture is sound, test coverage is excellent, and design decisions are well-documented. A few items worth discussing before merge:


Medium - Address Before Merge

1. Sequential issue fetching

import_issues() fetches GitHub issues one-by-one in a loop. For 20+ issues this serializes 20 HTTP round-trips before any task is created. It is worth adding a comment documenting the intentional tradeoff: all-or-nothing fetch semantics require serialization (concurrent fetch + any error would require per-item rollback). Future contributors who try to parallelize this need to know the error contract changes.

2. autoclose_if_done called with potentially-None task

In tasks_v2.py, the late opt-in path calls:

tasks.autoclose_if_done(workspace, tasks.get(workspace, task_id))

tasks.get() returns Optional[Task]. In the unlikely case of a concurrent delete between update_auto_close and this get, autoclose_if_done receives None and hits task.status with an AttributeError. Simple fix:

updated = tasks.get(workspace, task_id)
if updated is not None:
    tasks.autoclose_if_done(workspace, updated)

Low - Worth Noting

3. Dead-code path in get_issue repo probe

Within the 404 probe block in github_issues_service.py:

if repo_resp.status_code >= 400:
    raise GitHubConnectError(...)
if repo_resp.status_code >= 300:  # <- can never fire
    raise GitHubConnectError(...)

The second >= 300 check is unreachable since >= 400 catches everything above 300 first. A single >= 300 guard would be cleaner.

4. Import rollback propagates unhandled exception (intentional but implicit)

The except Exception: ... raise in import_issues propagates the raw DB exception as an unhandled 500, which is why test_create_failure_rolls_back_partial_import uses pytest.raises(OperationalError) rather than asserting a status code. This is correct behavior - a brief inline comment ("re-raise as 500 after rollback; client can retry idempotently") would help future readers.


Positives

  • Excellent test coverage: dispatch gating, concurrent-delete race, rollback, cache invalidation, late opt-in edge cases, and more.
  • Clean error hierarchy: NotAnIssueError/IssueNotFoundError as non-subclasses of GitHubConnectError correctly encodes the client-error vs. upstream-failure distinction.
  • Dedup keyed on full URL (not issue number) correctly handles workspace reconnection to a different repo.
  • Context-aware background dispatch (async loop vs. non-daemon thread) mirrors the webhook pattern and is well-explained.
  • All-or-nothing fetch before create: a single stale issue number aborts cleanly with no partial state.
  • PAT never surfaced in any API response.
  • target="_blank" rel="noopener noreferrer" on the badge link - correct security practice.

Summary

Items 1-2 are worth addressing before merge; 3-4 are low-priority polish. The core implementation is solid and test coverage is thorough. No blocking issues found.

Update CLAUDE.md Current Focus: #565 (import execution + task traceability +
auto-close) is implemented and merged-ready; document the endpoint behavior,
URL-keyed dedup + unique index, core-level auto-close targeting the source repo,
and the single-PAT known limitation.
@claude
Copy link
Copy Markdown

claude Bot commented Jun 4, 2026

Code Review — PR #609: GitHub Issue Import + Task Traceability (#565)

Overall assessment: This is a well-executed, production-ready feature. Architecture is clean, edge cases are covered, and test coverage is excellent. A few observations below.


Architecture / Design — Strengths

  • Two-phase import (fetch-all then create-all) cleanly prevents partial state: any fetch error aborts before a single task is written.
  • All-or-nothing rollback on mid-create DB error is correctly implemented. The sqlite3.IntegrityError path (concurrent duplicate) is handled inline and excluded from the outer rollback catch — exactly right.
  • Source-repo from external_url (not the workspace's live connection) is the correct decision for auto-close targeting, and it's tested explicitly.
  • Context-aware async dispatch (asyncio.get_running_loop() to schedule vs. non-daemon thread) mirrors the existing webhook dispatch pattern and is well-reasoned.
  • TaskResponse.from_task() eliminates the duplicated manual mapping in list + detail endpoints.

Issues

1. Stale docstring in test file

tests/core/test_task_github_traceability.py, module docstring line 6 references get_by_github_issue_number but the actual helper is get_by_external_url. Low risk (tests are correct), but worth fixing before merge.

2. Late opt-in path re-reads the task redundantly

In tasks_v2.py, the late opt-in block calls:

tasks.autoclose_if_done(workspace, tasks.get(workspace, task_id))

This triggers a second DB read immediately after update_auto_close already returned the updated Task. Passing the return value of update_auto_close directly would avoid the extra round-trip. Correctness is unaffected — this is a minor efficiency note.

3. Sequential GitHub API fetches in import endpoint

Issues are fetched one at a time in a for loop. Fine for typical browse-and-select imports (small batches), but for bulk imports of 20+ issues it adds noticeable latency. A TODO comment noting asyncio.gather as a future optimization would be useful, or keep as-is given the current scope.

4. setActionError(null) cleared at import start

handleImportIssues in TaskBoardView.tsx calls setActionError(null) at the top. If an unrelated board-level error is already displayed, starting an import silently clears it. Consider clearing only the import-specific state (importError, importSummary) here and leaving actionError alone.


Correctness Confirmations (things that look subtle but are right)

  • UNIQUE(workspace_id, external_url) with NULL distinctness: SQLite treats NULLs as distinct, so non-imported tasks are unaffected. The comment in workspace.py explains this correctly.
  • Pre-validate then mutate: Status validation including can_transition runs before update_auto_close persists. On a rejected transition the auto-close rollback is correctly gated on original_auto_close is not None.
  • Combined DONE + opt-in ordering: update_auto_close persists before update_status so the core dispatch reads the freshly-set flag. Tested and correct.
  • conftest.py rate-limiter fix: setdefault("RATE_LIMIT_ENABLED", "false") before router import is the right fix for AI-bucket exhaustion in the full test suite, and setdefault correctly lets an explicit env override win.

Nits

  • _repo_from_issue_url is exercised indirectly through dispatch tests but a direct unit test for the 4-segment path parse and the None fallback would be a cheap safety net against future refactoring.
  • The _AUTOCLOSE_TIMEOUT = 10.0 constant is passed by value as the timeout kwarg to close_issue. Renaming it _AUTOCLOSE_CLIENT_TIMEOUT would make the purpose clearer at the call site. Very minor.

Summary

Two items to address before merge:

  1. Fix stale docstring in tests/core/test_task_github_traceability.py (get_by_github_issue_number should be get_by_external_url).
  2. (Recommended) Clear only import-specific state in handleImportIssues rather than the shared actionError banner.

Everything else is correct or a minor efficiency/style note. Test coverage is comprehensive and the architecture decisions are well-reasoned.

@frankbria frankbria merged commit 5553157 into main Jun 4, 2026
11 checks passed
@frankbria frankbria deleted the feature/issue-565-github-issues-import branch June 4, 2026 17:35
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 5.5] GitHub Issues import: execute import and task traceability

1 participant