feat(pr-followup): link a PR to its issue from commit messages when the body has none - #923
Conversation
…he body has none extractLinkedIssue read only the PR title and body. A PR whose body carries no reference is never linked, so it has no linked-issue health, is never enqueued for a fix, and a CHANGES_REQUESTED review on it is never acted on — the PR sits blocked indefinitely with nothing watching it. That is not hypothetical: misospace/pr-reviewer-action#535 and misospace/llmkube-images#284 both opened with an empty body while the commit that did the work said 'Fixes #534' and 'Fixes #277'. The link existed; we were not looking where it survived. Fall back to the PR's commit messages, fetched only when title and body yield nothing. Commits are matched more strictly than the body — a closing keyword is required, since a bare '#123' in a commit is as likely to reference a prior PR as to declare what this one fixes, and a wrong link is worse than none. Claude-Session: https://claude.ai/code/session_01YSuDvZq9ncvyX85Uzx3cQh
There was a problem hiding this comment.
AI Automated Review
Full PR review.
Analysis engine: MiniMax-M2.7@https://litellm.jory.dev/v1 (anthropic) — primary route
Review: PR PR 923 — Link PR to issue from commit messages when body has none
Summary
This PR adds a fallback to extract the linked issue from a PR's commit messages when the title and body contain no issue reference. The primary change is in extractLinkedIssue (now accepts optional commitMessages) and the sync route's ingestion loop (fetches commits only on the fallback path, wrapped in try/catch). All 6 changed files are library or test files; no new routes are introduced.
Change-by-Change Findings
| File | What changed | Assessment |
|---|---|---|
src/lib/pr-followup-ingestion.ts |
extractLinkedIssue now accepts `commitMessages?: readonly (string |
null |
src/lib/github-prs.ts |
New fetchPullRequestCommitMessages function using the existing fetchPaginated helper. |
Uses existing patterns; no new HTTP client. |
src/app/api/pr-followup/sync/route.ts |
In the bot-PR ingestion loop, falls back to fetchPullRequestCommitMessages wrapped in try/catch when extractLinkedIssue returns null. |
Best-effort fallback; failures degrade to unlinked (status quo). |
src/lib/github.ts |
Re-exports fetchPullRequestCommitMessages. |
Trivial re-export for barrel API. |
src/lib/github-facades.test.ts |
Facade export list updated. | Expected; confirms API contract. |
src/lib/pr-followup-ingestion.test.ts |
6 new tests covering body precedence, keyword variants, bare-reference rejection, first-match ordering, and empty/null commit lists. | Comprehensive coverage for the new path. |
Required Checks — Explicitly Addressed
1. verify route access controls are in place
✅ Verified. The sync route (src/app/api/pr-followup/sync/route.ts) was already present and already calls authorizeRequest from @/lib/auth. This PR makes no changes to authentication or middleware. The existing bearer-token enforcement on the route remains intact.
2. check for unintended public endpoints
✅ Verified. This PR introduces zero new routes. It adds only library functions and a best-effort fallback inside the existing POST /api/pr-followup/sync handler. No route.ts files are created; no route configurations are changed.
Standards Compliance
Per the AGENTS.md conventions, this PR:
- Returns appropriate HTTP status codes (unchanged; existing route behavior preserved)
- Uses
try/catchfor the external call (best-effort pattern consistent with the codebase) - Follows the existing PR sync loop pattern in the codebase
- All tests pass (2493 passed, 5 skipped per PR body; CI confirms)
- Lint, typecheck, and build all clean (CI confirms)
Linked Issue Fit
The PR body references two live examples:
misospace/pr-reviewer-action#535— PR body empty, commitFixes #534misospace/llmkube-images#284— PR body empty, commitFixes #277
The implementation addresses these cases: when title and body carry no #NNN, it now searches commit messages for closing keywords (fixes/closes/resolves + variants), matching only when the keyword is present (avoiding false positives from bare references). The test at line 1094 of pr-followup-ingestion.test.ts explicitly validates the #534 case.
CI Check Results
All 11 CI checks passed (Tests, Typecheck, Lint, Build, Docker Build, smoke, Database integration, Database migrations, npm audit, Coverage, Docker Build (MCP)).
Unknowns / Needs Verification
None. The corpus is complete: diff shows all changed code, CI confirms tests pass, no open questions about auth or endpoint exposure.
Summary
extractLinkedIssuefalls back to the PR's commit messages when the title and body carry no issue reference.Why
An unlinked PR drops out of follow-up completely: no linked-issue health, nothing enqueued, so a
CHANGES_REQUESTEDreview on it is never acted on and the PR sits blocked with nothing watching it.Two live examples where the link existed and we were not looking where it survived:
Fixes #534Fixes #277#535 has all checks green,
CHANGES_REQUESTED, an empty PR-fix queue, and no workload — because nothing ever linked it to #534.Matching rule
The body keeps its existing loose rule (first
#NNNanywhere). Commits require a closing keyword —fixes/closes/resolvesand their variants. A bare#123in a commit message is as likely to reference a prior PR as to declare what this one fixes, and a wrong link is worse than no link.Cost
One extra request to
/pulls/N/commits, only when title and body yield nothing. Wrapped in a try/catch: an unlinked PR is the status quo, not a failure.Verification
npx vitest run— 2493 passed, 5 skipped (6 new, covering body precedence, keyword variants, bare-reference rejection, first-match ordering, and empty/null commit lists)https://claude.ai/code/session_01YSuDvZq9ncvyX85Uzx3cQh