From 96396c4ed853652ea311d006b4ebe6af15b40684 Mon Sep 17 00:00:00 2001 From: Joe Mou Date: Wed, 27 May 2026 14:02:36 -0400 Subject: Add repo-scoped search support for issues and PRs We do not support hybrid issue/PR search. Any searches on the issues endpoint will redirect to an explicit issue-only search. --- src/index.ts | 26 +++++++++++++++++++++++++- src/scraper.test.ts | 15 +++++++++++++++ src/scraper.ts | 20 ++++++++++---------- 3 files changed, 50 insertions(+), 11 deletions(-) (limited to 'src') diff --git a/src/index.ts b/src/index.ts index eb3a0ee..db9915f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -70,7 +70,31 @@ app.get("/:owner/:repo/raw/:branch/:path{.*}", async (c) => { app.get("/:owner/:repo/issues", async (c) => { const { owner, repo } = c.req.param(); - return tryRender(c, "issues.eta", getGitHubIssues(owner, repo)); + const q = c.req.query("q"); + + // GitHub supports combined issue/PR search, apparently for legacy. We just + // redirect to issue-only search. + if (q && !/\bis:issue\b/.test(q)) { + return c.redirect(`/${owner}/${repo}/issues?q=${encodeURIComponent(`is:issue ${q}`)}`); + } + + return tryRender(c, "issues.eta", getGitHubIssues(owner, repo, { q })); +}); + +app.get("/:owner/:repo/search", async (c) => { + const { owner, repo } = c.req.param(); + const q = c.req.query("q") ?? ""; + const type = c.req.query("type") ?? "issues"; + + if (type === "issues") { + return c.redirect(`/${owner}/${repo}/issues?q=${encodeURIComponent(q)}`); + } + + // Redirect unhandled search types (like code, which requires sign in anyway). + c.header("Referrer-Policy", "no-referrer"); + return c.redirect( + `https://github.com/${owner}/${repo}/search?${new URLSearchParams(c.req.query()).toString()}`, + ); }); app.get("/:owner/:repo/commits/:branch/:path{.*}?", async (c) => { diff --git a/src/scraper.test.ts b/src/scraper.test.ts index 42ddd6e..236b0f5 100644 --- a/src/scraper.test.ts +++ b/src/scraper.test.ts @@ -127,6 +127,7 @@ describe("GitHub scraper", () => { assert.strictEqual(data.repo.owner, "actions"); assert.strictEqual(data.repo.name, "deploy-pages"); + assert.strictEqual(data.q, undefined); const issue402 = data.issues.find((issue) => issue.number === 402); assert.ok(issue402); @@ -135,6 +136,20 @@ describe("GitHub scraper", () => { assert.strictEqual(issue402.createdAt, "2025-07-03T19:07:08Z"); }); + it("should fetch filtered issues with q", async () => { + const data = await getGitHubIssues("actions", "deploy-pages", { + q: "is:issue state:closed", + }); + + assert.strictEqual(data.repo.owner, "actions"); + assert.strictEqual(data.repo.name, "deploy-pages"); + assert.strictEqual(data.q, "is:issue state:closed"); + const issue417 = data.issues.find((issue) => issue.number === 417); + assert.ok(issue417); + assert.strictEqual(issue417.title, "Chats"); + assert.ok(data.issues.every((issue) => issue.title !== "")); + }); + it("should fetch commits", async () => { const data = await getGitHubCommits("actions", "deploy-pages", "main", ""); diff --git a/src/scraper.ts b/src/scraper.ts index 04a685b..2412e49 100644 --- a/src/scraper.ts +++ b/src/scraper.ts @@ -258,6 +258,7 @@ interface Issue { export interface GitHubIssues extends GitHubCommon { issues: Issue[]; + q?: string; } export interface GitHubCommits extends GitHubNav { @@ -513,9 +514,14 @@ export async function getGitHubBlob( }); } -export async function getGitHubIssues(owner: string, repo: string): Promise { - const html = await fetchGitHubPage(`${owner}/${repo}/issues`); - +export async function getGitHubIssues( + owner: string, + repo: string, + // We only support is:issue searches (no PR results). + { q }: { q?: string } = {}, +): Promise { + const suffix = q ? `?q=${encodeURIComponent(q)}` : ""; + const html = await fetchGitHubPage(`${owner}/${repo}/issues${suffix}`); const payload = parsePayload(html); let issuesQuery: IssueIndexPageQuery | undefined; @@ -534,13 +540,7 @@ export async function getGitHubIssues(owner: string, repo: string): Promise