A maior parte do que eu escrevo em código está em repositórios privados de clientes. Um gráfico de linguagens que só conta repositórios públicos não diz quase nada sobre o que eu realmente faço no dia a dia.
Eu queria contar os privados. Sem expor nada deles.
As três regras que me impus
- Nenhum token chega ao navegador.
- Nenhum nome, descrição ou URL de repositório privado é publicado.
- Nenhum serviço de terceiro recebe meu token.
A terceira regra elimina os geradores de badge prontos. As duas primeiras eliminam qualquer chamada à API feita pelo cliente.
A forma do dado importa mais que o acesso
A decisão que resolve o problema não é sobre autenticação, é sobre o que fica salvo. Em vez de guardar uma lista de repositórios, eu guardo só o agregado:
{
"summary": { "repositoryCount": 32, "stars": 1 },
"activity": { "commits": 94, "pullRequests": 6, "issues": 0, "reviews": 0 },
"languages": [
{ "name": "TypeScript", "color": "#3178c6", "bytes": 141815, "percentage": 62.71 }
]
}
Um repositório privado contribui com bytes para o total de uma linguagem e com
+1 na contagem. Ele não aparece em lugar nenhum de forma identificável: nem o
nome, nem quando foi atualizado, nem quantas estrelas tem. O número agregado não
é reversível.
Onde o token vive
A coleta roda em um GitHub Action diário, nunca no navegador:
- name: Generate GitHub statistics
run: node scripts/update-github-stats.mjs
env:
GITHUB_STATS_TOKEN: ${{ secrets.GH_STATS_TOKEN || github.token }}
INCLUDE_PRIVATE: ${{ secrets.GH_STATS_TOKEN != '' }}
Duas coisas acontecem aqui que vale destacar.
A primeira é o fallback: se o secret GH_STATS_TOKEN não existe, o workflow usa o
GITHUB_TOKEN temporário do Actions e produz apenas dados públicos. O site nunca
quebra por falta de configuração; ele só fica menos completo.
A segunda é que INCLUDE_PRIVATE é derivado da existência do secret, não escrito
à mão. Não existe estado em que eu peça dados privados sem ter credencial para
isso.
Uma verificação que vale o esforço
A query GraphQL pede viewer { login } junto com os dados. Com isso o script
confere se o token pertence mesmo ao usuário configurado:
if (includePrivate && data.viewer.login.toLowerCase() !== username.toLowerCase()) {
throw new Error("GH_STATS_TOKEN não pertence ao usuário configurado.");
}
Parece paranoia até você trocar um token e passar a publicar silenciosamente a atividade de outra conta. Falhar o build é muito melhor que acertar por acidente.
O que o navegador recebe
Um arquivo JSON estático, commitado no repositório e servido junto com o site.
O cliente faz um fetch e, se algo der errado, cai em um fallback compilado no
bundle:
export function useGitHubStats() {
const [stats, setStats] = useState<GitHubStats>(fallbackGitHubStats);
// ...
}
Nenhuma chave, nenhuma chamada autenticada, nenhuma dependência de terceiro em tempo de execução. A página carrega igual se a API do GitHub estiver fora do ar.
O custo disso
O dado tem até 24 horas de atraso. Para um portfólio, isso é irrelevante, e é um preço pequeno por não ter segredo nenhum rodando no navegador de quem visita.