From dd7029b22f7a612624367bb8e6a4053814b23341 Mon Sep 17 00:00:00 2001 From: Masonlet Date: Tue, 9 Jun 2026 17:42:29 -0400 Subject: [PATCH 1/3] feat: add GITHUB_TOKEN support for authenticated API requests --- src/services/github.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/services/github.ts b/src/services/github.ts index e354e0e..7c7ab74 100644 --- a/src/services/github.ts +++ b/src/services/github.ts @@ -18,12 +18,12 @@ type Repo = { full_name: string; }; -async function fetchAllRepos(url: string): Promise { - const repos: Repo[] = []; +async function fetchAllRepos(url: string, options: RequestInit): Promise { let nextUrl: string | null = url; + const repos: Repo[] = []; while (nextUrl) { - const response = await fetch(nextUrl); + const response = await fetch(nextUrl, options); if (!response.ok) throw new Error(`GitHub API error: ${response.status} ${response.statusText}`); repos.push(...await response.json() as Repo[]); nextUrl = parseNextLink(response.headers.get("Link")); @@ -49,9 +49,12 @@ export async function fetchLanguageData(useTestData = false): Promise fetchAllRepos(`https://api.github.com/users/${user}/repos?per_page=100`)), - ...orgs.map( org => fetchAllRepos(`https://api.github.com/orgs/${org}/repos?per_page=100` )) + ...usernames.map(user => fetchAllRepos(`https://api.github.com/users/${user}/repos?per_page=100`, options)), + ...orgs.map( org => fetchAllRepos(`https://api.github.com/orgs/${org}/repos?per_page=100`, options)) ]); const repos = repoArrays.flat(); @@ -59,7 +62,7 @@ export async function fetchLanguageData(useTestData = false): Promise !repo.fork && !ignored.includes(repo.name)); const languageFetches = filteredRepos.map( - repo => fetch(`https://api.github.com/repos/${repo.full_name}/languages`).then(r => r.ok ? r.json() : {}) + repo => fetch(`https://api.github.com/repos/${repo.full_name}/languages`, options).then(r => r.ok ? r.json() : {}) ); const langResults: LanguageBytes[] = await Promise.all(languageFetches); From c2539ff7fba83845b62d3db852d3a9cd97d91998 Mon Sep 17 00:00:00 2001 From: Masonlet Date: Tue, 9 Jun 2026 17:47:29 -0400 Subject: [PATCH 2/3] test: add tests for authenticated fetch headers --- tests/services/github.test.ts | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/tests/services/github.test.ts b/tests/services/github.test.ts index 8f49e16..b5b22a4 100644 --- a/tests/services/github.test.ts +++ b/tests/services/github.test.ts @@ -72,17 +72,17 @@ describe("fetchLanguageData", () => { await fetchLanguageData(); expect(global.fetch).toHaveBeenCalledWith( - "https://api.github.com/users/testuser/repos?per_page=100" + "https://api.github.com/users/testuser/repos?per_page=100", {} ); expect(global.fetch).toHaveBeenCalledWith( - "https://api.github.com/repos/user/repo1/languages" + "https://api.github.com/repos/user/repo1/languages", {} ); expect(global.fetch).not.toHaveBeenCalledWith( - "https://api.github.com/repos/user/repo2/languages" + "https://api.github.com/repos/user/repo2/languages", {} ); expect(global.fetch).not.toHaveBeenCalledWith( - "https://api.github.com/repos/user/ignored-repo/languages" + "https://api.github.com/repos/user/ignored-repo/languages", {} ); }); @@ -149,7 +149,7 @@ describe("fetchLanguageData", () => { const result = await fetchLanguageData(); expect(global.fetch).toHaveBeenCalledWith( - "https://api.github.com/orgs/test-org/repos?per_page=100" + "https://api.github.com/orgs/test-org/repos?per_page=100", {} ); expect(result).toEqual({ TypeScript: 4000 }); }); @@ -169,7 +169,7 @@ describe("fetchLanguageData", () => { .mockResolvedValueOnce(mockResponse({ Python: 500 })); const result = await fetchLanguageData(); - expect(global.fetch).toHaveBeenCalledWith(page2Url); + expect(global.fetch).toHaveBeenCalledWith(page2Url, {}); expect(global.fetch).toHaveBeenCalledTimes(4); expect(result).toEqual({ JavaScript: 1000, Python: 500 }); }); @@ -186,6 +186,21 @@ describe("fetchLanguageData", () => { expect(global.fetch).toHaveBeenCalledTimes(2); expect(result).toEqual({ Go: 300 }); }); + + it("sends Authorization header when GITHUB_TOKEN is set", async () => { + vi.stubEnv("GITHUB_TOKEN", "test-token"); + + mockFetch() + .mockResolvedValueOnce(mockResponse([repos[0]])) + .mockResolvedValueOnce(mockResponse(languages)); + + await fetchLanguageData(); + + expect(global.fetch).toHaveBeenCalledWith( + "https://api.github.com/users/testuser/repos?per_page=100", + { headers: { Authorization: "Bearer test-token" } } + ); + }); }); describe("processLanguageData", () => { From 4ecc53089f87662ec0b6e57800ec2bb238d35866 Mon Sep 17 00:00:00 2001 From: Masonlet Date: Tue, 9 Jun 2026 17:51:19 -0400 Subject: [PATCH 3/3] docs: add GITHUB_TOKEN to README and .env.example --- .env.example | 5 +++-- README.md | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.env.example b/.env.example index 2d97b1a..087d85b 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,4 @@ GITHUB_USERNAMES=your_username(s) -GITHUB_ORGS= -IGNORED_REPOS=repo-name1,repo-name2,repo-name3 +GITHUB_ORGS=your_org(s) +IGNORED_REPOS=ignored_repo(s) +GITHUB_TOKEN= diff --git a/README.md b/README.md index 3559265..2d6bee2 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,7 @@ Copy `.env.example` to `.env`, and update the variables. - `GITHUB_USERNAMES`: Comma-separated GitHub usernames to fetch repositories from. - `GITHUB_ORGS`: Optional comma-separated GitHub organization names to include. - `IGNORED_REPOS`: Optional comma-separated repo names to exclude from the chart. +- `GITHUB_TOKEN`: Optional GitHub personal access token. Raises the API rate limit from 60 to 5000 requests/hour. ### Running Locally ```bash @@ -107,7 +108,7 @@ vercel dev ### Deployment -[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/masonlet/github-top-languages&env=GITHUB_USERNAMES,IGNORED_REPOS&envDescription[GITHUB_USERNAMES]=Comma-separated%20GitHub%20usernames&envDescription[IGNORED_REPOS]=Optional%20comma-separated%20repos%20to%20exclude) +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/masonlet/github-top-languages&env=GITHUB_USERNAMES,GITHUB_TOKEN,IGNORED_REPOS&envDescription[GITHUB_USERNAMES]=Comma-separated%20GitHub%20usernames&envDescription[IGNORED_REPOS]=Optional%20comma-separated%20repos%20to%20exclude&envDescription[GITHUB_TOKEN]=Optional%20GitHub%20personal%20access%20token%20for%20higher%20rate%20limits) > The default endpoint is /api/languages