Skip to content

Add GitHub issues support#1

Open
0xdhrv wants to merge 3 commits into
R44VC0RP:mainfrom
0xdhrv:add-github-issues-support-7948363216448392324
Open

Add GitHub issues support#1
0xdhrv wants to merge 3 commits into
R44VC0RP:mainfrom
0xdhrv:add-github-issues-support-7948363216448392324

Conversation

@0xdhrv

@0xdhrv 0xdhrv commented Feb 19, 2026

Copy link
Copy Markdown

Summary by CodeRabbit

  • New Features

    • Added support for viewing GitHub issues and their associated comments through new API endpoints
    • Enabled filtering to view individual comments within an issue
    • Implemented pagination to handle issues with large comment threads
  • Tests

    • Added comprehensive test coverage for issue viewing and comment filtering
    • Added pagination integration tests to validate multi-page comment handling

This change adds support for fetching and formatting GitHub Issues and their comments, similar to the existing Pull Request support.

Changes:
- Modified `apps/server/lib/github.ts` to include `Issue` and `IssueComment` interfaces and API methods.
- Modified `apps/server/app.ts` to add endpoints for issues and format issue comments.
- Added integration tests in `apps/server/tests/issue.test.ts`.
- Updated `apps/server/package.json` with test script and dependencies.
- Updated root `package.json` with test dependencies.
This change updates the GitHub API client to request 100 items per page for comments and reviews. This prevents issues where comments were silently truncated at the default limit of 30.

Changes:
- Updated `fetchPullRequestComments` to include `per_page=100`.
- Updated `fetchPullRequestReviews` to include `per_page=100`.
- Updated `fetchIssueComments` to include `per_page=100`.
This change implements full pagination support for GitHub API requests using the Link header. This ensures that all comments and reviews are fetched, not just the first 100.

Changes:
- Added `fetchPaginated` helper function in `apps/server/lib/github.ts`.
- Updated `fetchPullRequestComments`, `fetchPullRequestReviews`, and `fetchIssueComments` to use `fetchPaginated`.
- Added `apps/server/tests/pagination.test.ts` to verify pagination logic.
- Increased timeout for pagination tests to handle multiple requests.
@vercel

vercel Bot commented Feb 19, 2026

Copy link
Copy Markdown

@0xdhrv is attempting to deploy a commit to the exon Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Feb 19, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

The pull request introduces GitHub Issues endpoint support to the server, adding new type definitions (Issue, IssueComment), a generic pagination helper for GitHub API requests, issue fetching functions, and HTTP routes with Markdown rendering and error handling.

Changes

Cohort / File(s) Summary
Core GitHub Library
apps/server/lib/github.ts
Added Issue and IssueComment type definitions, GitHubIssueOptions interface, and generic fetchPaginated helper for handling paginated API responses. Introduced fetchIssue and fetchIssueComments functions. Refactored existing fetchPullRequestComments and fetchPullRequestReviews to use the new pagination helper.
API Routes & Handlers
apps/server/app.ts
Added handleIssueComments handler and routes for issue endpoints (GET /:owner/:repo/issues/:issueNumber and GET /:owner/:repo/issues/:issueNumber/:commentNumber). Introduced formatIssueAsMarkdown helper to render issues and comments as Markdown. Updated 404 message and server startup behavior.
Test Dependencies
apps/server/package.json, package.json
Added supertest (^7.2.2) and @types/supertest (^6.0.3) as devDependencies for HTTP testing.
Test Suites
apps/server/tests/issue.test.ts, apps/server/tests/pagination.test.ts
Introduced comprehensive test coverage: issue.test.ts validates issue endpoint behavior with token handling and comment filtering; pagination.test.ts validates multi-page comment aggregation across GitHub API pages.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 Hop! Issues now flow through our server so bright,
Pagination magic fetches comments just right,
From GitHub's vast pages, we gather them all,
With types and with handlers, we answer the call!
Tests bloom in abundance to catch every flaw,
Issue support's here—it's the law!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely describes the main objective of the pull request, which is to add GitHub issues support alongside existing pull request functionality.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (4)
package.json (1)

12-14: Duplicated devDependencies — these already exist in apps/server/package.json.

In a Yarn/npm workspaces monorepo, test dependencies that are only consumed by apps/server should live exclusively in that workspace's package.json. Having them duplicated at the root can cause version drift and confusion about which package actually depends on them. Consider removing them here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` around lines 12 - 14, Remove the duplicated test
devDependencies from the root package.json: delete the "@types/supertest" and
"supertest" entries under "devDependencies" in this file and keep those
dependencies declared only in apps/server/package.json, ensuring the workspace
relies on the workspace-level declaration to avoid version drift and ambiguity.
apps/server/lib/github.ts (2)

122-154: any[] types in Issue interface reduce type safety.

labels and milestone are typed as any[]/any | null. If the shape is known (even partially), consider at least a minimal interface (e.g., { id: number; name: string } for labels) to catch misuse at compile time. Fine to defer if these fields are unused today.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/lib/github.ts` around lines 122 - 154, The Issue interface
currently weakens type safety by using any[] for labels and any | null for
milestone; define small, explicit types (e.g., Label with id:number and
name:string, optional color/string fields; Milestone with id:number,
title:string, optional description/string and due_on/date) and replace labels:
any[] with labels: Label[] and milestone: any | null with milestone: Milestone |
null in the Issue interface (update any related usages/imports and adjust types
in functions that construct or read Issue.labels and Issue.milestone such as
where Issue objects are parsed or returned).

192-243: Consider adding a max-page safeguard to fetchPaginated.

There's no upper bound on the number of pages fetched. A repo with many thousands of comments could cause this loop to make hundreds of sequential API calls, risking rate-limit exhaustion and long request latency. A simple maxPages parameter (with a sensible default, e.g., 50) would prevent runaway pagination.

Proposed fix
-async function fetchPaginated<T>(url: string, token: string): Promise<T[]> {
+async function fetchPaginated<T>(url: string, token: string, maxPages: number = 50): Promise<T[]> {
   let results: T[] = [];
   let nextUrl: string | null = url;
+  let page = 0;
 
-  while (nextUrl) {
+  while (nextUrl && page < maxPages) {
+    page++;
     const response = await fetch(nextUrl, {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/lib/github.ts` around lines 192 - 243, Add a bounded pagination
safeguard to fetchPaginated by adding an optional maxPages parameter (number)
with a sensible default (e.g., 50) to the function signature and tracking a
pagesFetched counter inside the while (nextUrl) loop; increment pagesFetched
each iteration and break or throw a clear error once pagesFetched >= maxPages to
avoid runaway requests, ensuring you still return accumulated results or surface
a descriptive error if the limit is hit. Reference: function fetchPaginated,
variables nextUrl and results, introduce pagesFetched and maxPages.
apps/server/app.ts (1)

483-500: Parallelize independent getIssueComments and getIssue calls.

getIssueComments and getIssue are independent API calls but are executed sequentially, doubling latency for the "all comments" view. Use Promise.all to fetch both concurrently.

Proposed fix
   try {
     const client = new GitHubClient(token);
-    let comments = await client.getIssueComments(owner, repo, issueNum);
-    let issue: Issue | null = null;
 
     if (commentNum !== null) {
+      let comments = await client.getIssueComments(owner, repo, issueNum);
       if (commentNum < 1 || commentNum > comments.length) {
         const errorMarkdown = `# Error 404\n\nComment not found.\n\n**Comment Number:** ${commentNum}\n**Issue:** #${issueNum}\n**Total Comments:** ${comments.length}\n\nComment number must be between 1 and ${comments.length}.`;
         return res.status(404)
           .set("Content-Type", "text/markdown; charset=utf-8")
           .send(errorMarkdown);
       }
       comments = [comments[commentNum - 1]!];
+      const markdown = formatIssueAsMarkdown(owner, repo, issueNum, null, comments);
+      return res.status(200)
+        .set("Content-Type", "text/markdown; charset=utf-8")
+        .send(markdown);
     } else {
-        // Fetch issue details only if not fetching a specific comment (or maybe fetch it anyway?)
-        // Fetching it anyway to include in the full view
-        issue = await client.getIssue(owner, repo, issueNum);
+      const [comments, issue] = await Promise.all([
+        client.getIssueComments(owner, repo, issueNum),
+        client.getIssue(owner, repo, issueNum),
+      ]);
+      const markdown = formatIssueAsMarkdown(owner, repo, issueNum, issue, comments);
+      return res.status(200)
+        .set("Content-Type", "text/markdown; charset=utf-8")
+        .send(markdown);
     }
-
-    const markdown = formatIssueAsMarkdown(owner, repo, issueNum, issue, comments);
-
-    return res.status(200)
-      .set("Content-Type", "text/markdown; charset=utf-8")
-      .send(markdown);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/app.ts` around lines 483 - 500, The code currently calls
client.getIssueComments(...) then awaits client.getIssue(...) sequentially when
commentNum is null; change it to run them in parallel using Promise.all to
reduce latency: for the branch where commentNum === null, call
Promise.all([client.getIssueComments(owner, repo, issueNum),
client.getIssue(owner, repo, issueNum)]) and destructure the results into
comments and issue respectively; keep the existing behavior for the commentNum
!== null branch (only fetch comments), and ensure the surrounding try/catch and
variable names (GitHubClient, getIssueComments, getIssue, comments, issue,
commentNum) are preserved.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/server/app.ts`:
- Around line 263-317: In formatIssueAsMarkdown, add an explicit "no comments"
message when an issue exists but comments is empty: after the block that appends
the issue description (inside function formatIssueAsMarkdown, following the
lines that append `---\n\n` for the issue), check if comments.length === 0 and
if so append `*No comments found for this issue.*\n` (or similar) and return the
markdown; also update the existing condition that currently checks `if
(comments.length === 0 && !issue)` so it only handles the case where there is no
issue and no comments.

In `@apps/server/tests/issue.test.ts`:
- Around line 126-136: The test "GET /owner/repo/issues/123 without token
returns 401" mutates process.env.GITHUB_TOKEN but only restores it after the
assertion, so a thrown error can leak state; update the test to save
originalToken, delete process.env.GITHUB_TOKEN, then execute the
request(app).get("/owner/repo/issues/123").expect(401) inside a try block and
restore process.env.GITHUB_TOKEN in a finally block so the environment is always
restored regardless of test outcome.

---

Nitpick comments:
In `@apps/server/app.ts`:
- Around line 483-500: The code currently calls client.getIssueComments(...)
then awaits client.getIssue(...) sequentially when commentNum is null; change it
to run them in parallel using Promise.all to reduce latency: for the branch
where commentNum === null, call Promise.all([client.getIssueComments(owner,
repo, issueNum), client.getIssue(owner, repo, issueNum)]) and destructure the
results into comments and issue respectively; keep the existing behavior for the
commentNum !== null branch (only fetch comments), and ensure the surrounding
try/catch and variable names (GitHubClient, getIssueComments, getIssue,
comments, issue, commentNum) are preserved.

In `@apps/server/lib/github.ts`:
- Around line 122-154: The Issue interface currently weakens type safety by
using any[] for labels and any | null for milestone; define small, explicit
types (e.g., Label with id:number and name:string, optional color/string fields;
Milestone with id:number, title:string, optional description/string and
due_on/date) and replace labels: any[] with labels: Label[] and milestone: any |
null with milestone: Milestone | null in the Issue interface (update any related
usages/imports and adjust types in functions that construct or read Issue.labels
and Issue.milestone such as where Issue objects are parsed or returned).
- Around line 192-243: Add a bounded pagination safeguard to fetchPaginated by
adding an optional maxPages parameter (number) with a sensible default (e.g.,
50) to the function signature and tracking a pagesFetched counter inside the
while (nextUrl) loop; increment pagesFetched each iteration and break or throw a
clear error once pagesFetched >= maxPages to avoid runaway requests, ensuring
you still return accumulated results or surface a descriptive error if the limit
is hit. Reference: function fetchPaginated, variables nextUrl and results,
introduce pagesFetched and maxPages.

In `@package.json`:
- Around line 12-14: Remove the duplicated test devDependencies from the root
package.json: delete the "@types/supertest" and "supertest" entries under
"devDependencies" in this file and keep those dependencies declared only in
apps/server/package.json, ensuring the workspace relies on the workspace-level
declaration to avoid version drift and ambiguity.

Comment thread apps/server/app.ts
Comment on lines +263 to +317
function formatIssueAsMarkdown(
owner: string,
repo: string,
issueNumber: number,
issue: Issue | null,
comments: IssueComment[]
): string {
// Calculate tokens
let totalTokens = 0;
if (issue && issue.body) {
totalTokens += countTokens(issue.body);
}
comments.forEach(comment => {
totalTokens += countTokens(comment.body);
});

let markdown = `# Issue #${issueNumber} Comments\n\n`;
markdown += `**Repository:** ${owner}/${repo}\n`;
markdown += `**Total Comments:** ${comments.length}\n`;
markdown += `**Total Tokens:** ${totalTokens.toLocaleString()}\n\n`;
markdown += `---\n\n`;

if (issue) {
markdown += `## Issue Description\n\n`;
markdown += `**Title:** ${issue.title}\n`;
markdown += `**Author:** @${issue.user.login}\n`;
markdown += `**Created:** ${new Date(issue.created_at).toLocaleString()}\n`;
markdown += `**State:** ${issue.state}\n`;
markdown += `**[View on GitHub](${issue.html_url})**\n\n`;
markdown += `### Description\n\n`;
markdown += `${issue.body || "*No description*"}\n\n`;
markdown += `---\n\n`;
}

if (comments.length === 0 && !issue) {
markdown += `*No comments found for this issue.*\n`;
return markdown;
}

comments.forEach((comment, index) => {
markdown += `## Comment ${index + 1}\n\n`;
markdown += `**Author:** @${comment.user.login}\n`;
markdown += `**Created:** ${new Date(comment.created_at).toLocaleString()}\n`;
markdown += `**[View on GitHub](${comment.html_url})**\n\n`;

markdown += `### Comment\n\n`;
markdown += `${comment.body}\n\n`;

if (index < comments.length - 1) {
markdown += `---\n\n`;
}
});

return markdown;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Minor: empty comments case has no explicit "no comments" message when issue is present.

When issue is non-null but comments is empty, the output ends after the issue description with no indication that there are no comments. The "no comments" message on line 298 only triggers when issue is also null. Consider adding a brief note for the issue && comments.length === 0 case.

Proposed fix
   if (comments.length === 0 && !issue) {
      markdown += `*No comments found for this issue.*\n`;
      return markdown;
   }
+
+  if (comments.length === 0) {
+     markdown += `*No comments found for this issue.*\n`;
+     return markdown;
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function formatIssueAsMarkdown(
owner: string,
repo: string,
issueNumber: number,
issue: Issue | null,
comments: IssueComment[]
): string {
// Calculate tokens
let totalTokens = 0;
if (issue && issue.body) {
totalTokens += countTokens(issue.body);
}
comments.forEach(comment => {
totalTokens += countTokens(comment.body);
});
let markdown = `# Issue #${issueNumber} Comments\n\n`;
markdown += `**Repository:** ${owner}/${repo}\n`;
markdown += `**Total Comments:** ${comments.length}\n`;
markdown += `**Total Tokens:** ${totalTokens.toLocaleString()}\n\n`;
markdown += `---\n\n`;
if (issue) {
markdown += `## Issue Description\n\n`;
markdown += `**Title:** ${issue.title}\n`;
markdown += `**Author:** @${issue.user.login}\n`;
markdown += `**Created:** ${new Date(issue.created_at).toLocaleString()}\n`;
markdown += `**State:** ${issue.state}\n`;
markdown += `**[View on GitHub](${issue.html_url})**\n\n`;
markdown += `### Description\n\n`;
markdown += `${issue.body || "*No description*"}\n\n`;
markdown += `---\n\n`;
}
if (comments.length === 0 && !issue) {
markdown += `*No comments found for this issue.*\n`;
return markdown;
}
comments.forEach((comment, index) => {
markdown += `## Comment ${index + 1}\n\n`;
markdown += `**Author:** @${comment.user.login}\n`;
markdown += `**Created:** ${new Date(comment.created_at).toLocaleString()}\n`;
markdown += `**[View on GitHub](${comment.html_url})**\n\n`;
markdown += `### Comment\n\n`;
markdown += `${comment.body}\n\n`;
if (index < comments.length - 1) {
markdown += `---\n\n`;
}
});
return markdown;
}
function formatIssueAsMarkdown(
owner: string,
repo: string,
issueNumber: number,
issue: Issue | null,
comments: IssueComment[]
): string {
// Calculate tokens
let totalTokens = 0;
if (issue && issue.body) {
totalTokens += countTokens(issue.body);
}
comments.forEach(comment => {
totalTokens += countTokens(comment.body);
});
let markdown = `# Issue #${issueNumber} Comments\n\n`;
markdown += `**Repository:** ${owner}/${repo}\n`;
markdown += `**Total Comments:** ${comments.length}\n`;
markdown += `**Total Tokens:** ${totalTokens.toLocaleString()}\n\n`;
markdown += `---\n\n`;
if (issue) {
markdown += `## Issue Description\n\n`;
markdown += `**Title:** ${issue.title}\n`;
markdown += `**Author:** @${issue.user.login}\n`;
markdown += `**Created:** ${new Date(issue.created_at).toLocaleString()}\n`;
markdown += `**State:** ${issue.state}\n`;
markdown += `**[View on GitHub](${issue.html_url})**\n\n`;
markdown += `### Description\n\n`;
markdown += `${issue.body || "*No description*"}\n\n`;
markdown += `---\n\n`;
}
if (comments.length === 0 && !issue) {
markdown += `*No comments found for this issue.*\n`;
return markdown;
}
if (comments.length === 0) {
markdown += `*No comments found for this issue.*\n`;
return markdown;
}
comments.forEach((comment, index) => {
markdown += `## Comment ${index + 1}\n\n`;
markdown += `**Author:** @${comment.user.login}\n`;
markdown += `**Created:** ${new Date(comment.created_at).toLocaleString()}\n`;
markdown += `**[View on GitHub](${comment.html_url})**\n\n`;
markdown += `### Comment\n\n`;
markdown += `${comment.body}\n\n`;
if (index < comments.length - 1) {
markdown += `---\n\n`;
}
});
return markdown;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/app.ts` around lines 263 - 317, In formatIssueAsMarkdown, add an
explicit "no comments" message when an issue exists but comments is empty: after
the block that appends the issue description (inside function
formatIssueAsMarkdown, following the lines that append `---\n\n` for the issue),
check if comments.length === 0 and if so append `*No comments found for this
issue.*\n` (or similar) and return the markdown; also update the existing
condition that currently checks `if (comments.length === 0 && !issue)` so it
only handles the case where there is no issue and no comments.

Comment on lines +126 to +136
test("GET /owner/repo/issues/123 without token returns 401", async () => {
// Ensure GITHUB_TOKEN is not set
const originalToken = process.env.GITHUB_TOKEN;
delete process.env.GITHUB_TOKEN;

await request(app)
.get("/owner/repo/issues/123")
.expect(401);

if (originalToken) process.env.GITHUB_TOKEN = originalToken;
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

process.env.GITHUB_TOKEN restoration not guaranteed on test failure.

If the assertion on line 132-133 throws, process.env.GITHUB_TOKEN is never restored, which can leak state into subsequent tests. Wrap in try/finally.

Proposed fix
   test("GET /owner/repo/issues/123 without token returns 401", async () => {
     const originalToken = process.env.GITHUB_TOKEN;
     delete process.env.GITHUB_TOKEN;
 
-    await request(app)
-      .get("/owner/repo/issues/123")
-      .expect(401);
-
-    if (originalToken) process.env.GITHUB_TOKEN = originalToken;
+    try {
+      await request(app)
+        .get("/owner/repo/issues/123")
+        .expect(401);
+    } finally {
+      if (originalToken) process.env.GITHUB_TOKEN = originalToken;
+      else delete process.env.GITHUB_TOKEN;
+    }
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/tests/issue.test.ts` around lines 126 - 136, The test "GET
/owner/repo/issues/123 without token returns 401" mutates
process.env.GITHUB_TOKEN but only restores it after the assertion, so a thrown
error can leak state; update the test to save originalToken, delete
process.env.GITHUB_TOKEN, then execute the
request(app).get("/owner/repo/issues/123").expect(401) inside a try block and
restore process.env.GITHUB_TOKEN in a finally block so the environment is always
restored regardless of test outcome.

@R44VC0RP R44VC0RP force-pushed the main branch 2 times, most recently from 20adc76 to 0cb0c3a Compare March 4, 2026 15:23
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.

1 participant