feat(github): contar repositorios proprios (publicos e privados) pela API no servidor
Consulta /user/repos com token fine-grained somente leitura de metadados (GITHUB_STATS_TOKEN), pagina, exclui forks e arquivados e devolve null se o token faltar ou a API falhar. Renovacao diaria via revalidate de 86400 s. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
1 parent
b2f3899dc2
commit
4ba955e2c3
2 files changed
+170
No files matched your search
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* Contagem de repositórios próprios no GitHub (públicos + privados), consultada só no servidor.
|
||||
* O token (GITHUB_STATS_TOKEN, fine-grained, somente leitura de metadados) nunca recebe prefixo NEXT_PUBLIC_
|
||||
* e este módulo só é importado por componentes de servidor: ele não chega ao navegador.
|
||||
*/
|
||||
|
||||
export const GITHUB_STATS_REVALIDATE_SECONDS = 24 * 60 * 60;
|
||||
|
||||
const REPOS_ENDPOINT = "https://api.github.com/user/repos";
|
||||
const PAGE_SIZE = 100;
|
||||
const MAX_PAGES = 20;
|
||||
const REQUEST_TIMEOUT_MS = 10_000;
|
||||
|
||||
export interface GithubRepoCounts {
|
||||
total: number;
|
||||
public: number;
|
||||
private: number;
|
||||
}
|
||||
|
||||
interface RepoSummary {
|
||||
private: boolean;
|
||||
fork: boolean;
|
||||
archived: boolean;
|
||||
}
|
||||
|
||||
type CachedRequestInit = RequestInit & { next: { revalidate: number } };
|
||||
type FetchLike = (url: string, init: CachedRequestInit) => Promise<Response>;
|
||||
|
||||
/** Só repositórios meus: forks e arquivados ficam de fora. */
|
||||
export function countOwnRepos(repos: RepoSummary[]): GithubRepoCounts {
|
||||
let publicCount = 0;
|
||||
let privateCount = 0;
|
||||
for (const repo of repos) {
|
||||
if (repo.fork || repo.archived) continue;
|
||||
if (repo.private) privateCount += 1;
|
||||
else publicCount += 1;
|
||||
}
|
||||
return {
|
||||
total: publicCount + privateCount,
|
||||
public: publicCount,
|
||||
private: privateCount,
|
||||
};
|
||||
}
|
||||
|
||||
export async function loadGithubRepoCounts(
|
||||
token: string | undefined = process.env.GITHUB_STATS_TOKEN,
|
||||
fetchImpl: FetchLike = fetch,
|
||||
): Promise<GithubRepoCounts | null> {
|
||||
if (!token) return null;
|
||||
|
||||
const repos: RepoSummary[] = [];
|
||||
try {
|
||||
for (let page = 1; page <= MAX_PAGES; page += 1) {
|
||||
const url = `${REPOS_ENDPOINT}?affiliation=owner&visibility=all&per_page=${PAGE_SIZE}&page=${page}`;
|
||||
const response = await fetchImpl(url, {
|
||||
headers: {
|
||||
accept: "application/vnd.github+json",
|
||||
authorization: `Bearer ${token}`,
|
||||
"user-agent": "evandro-dev-portfolio",
|
||||
"x-github-api-version": "2022-11-28",
|
||||
},
|
||||
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
||||
next: { revalidate: GITHUB_STATS_REVALIDATE_SECONDS },
|
||||
});
|
||||
if (!response.ok) return null;
|
||||
|
||||
const batch = (await response.json()) as RepoSummary[];
|
||||
if (!Array.isArray(batch)) return null;
|
||||
repos.push(
|
||||
...batch.map(({ private: isPrivate, fork, archived }) => ({
|
||||
private: isPrivate,
|
||||
fork,
|
||||
archived,
|
||||
})),
|
||||
);
|
||||
if (batch.length < PAGE_SIZE) return countOwnRepos(repos);
|
||||
}
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user