teqo-backup/docs/postmortems/2026-09-18-github-token-401.md

10 KiB

Post-mortem: PAT fine-grained expirado no GITHUB_TOKEN do shell quebra pnpm issue/agent:*/gh (e drive-by: main RED por conflito semântico de rebase)

Template do /bug-fix. Preencha com fatos apurados; o que não for apurado fica "não apurado" — nunca invente.

Registro

Campo Valor
Data do post-mortem 2026-09-18
Severidade média (ferramental local; o drive-by, porém, bloqueava staging/produção — ver "O bug")
Ambiente dev (workstation) + CI/deploy (drive-by)
Issue(s) sem Issue (fluxo /bug-fix; o registro é o post-mortem)
PR do fix #1174
Detectado por log (sessão de agente c194, antes de 2026-09-18) e humano (2026-09-18)

Timeline

Momento Data/hora Evento
Início provável 2026-08-20 PAT fine-grained github_pat_11ACYUP… criado no cutover OPS71 e exportado em ~/.bashrc:194, com o comentário "rotacionar após o cutover"; nunca rotacionado. Expira ~2026-09 (validade default de 30d)
Detecção antes de 2026-09-18 (log) / 2026-09-18 (humano) Log ~/.local/state/teqo/agent-sessions/c194-…log:74: gh issue view → HTTP 401: Bad credentials. Depois o humano reproduziu pnpm issue next → 401
Correção mergeada 2026-09-18 não apurado à época da escrita — merge deste PR (a confirmar)
Deploy não apurado à época da escrita o drive-by é o que desbloqueia o deploy de main, vermelho desde C193 no job verify/Typecheck do run 35394524888; staging/produção a confirmar após o merge
Verificado em prod não se aplica (bug local de ferramental) —

O bug

pnpm issue next, rodado de ~/Code/teqo, falhava com [agent:issue] GitHub API GET /repos/fsolla/teqo/issues → 401: {"message": "Bad credentials", ...}. O sintoma atingia toda a ferramentaria local que fala com o GitHub — pnpm issue, agent:*, scripts/github-pr.mjs — além do próprio gh, que já havia falhado numa sessão de agente recente (log c194-…log:74). Não era runtime de produção; dados de produção intactos.

No mesmo PR saiu um drive-by de outro bug: main estava RED desde C193 (bdaf3c7c) — o job verify/Typecheck do Deploy run 35394524888 falhava com TS2305, bloqueando staging e produção. O 401 e o main RED são independentes; estão neste post-mortem porque a correção saiu no mesmo PR. Sintoma — não a causa.

Causa-raiz

5-whys (401):

  1. A API rejeitava o token com "Bad credentials".
  2. O ambiente exportava um PAT fine-grained expirado; scripts/lib/github-api.mjs:80 lê process.env.GITHUB_TOKEN e usava esse valor.
  3. O PAT nunca foi rotacionado: o lembrete vivia só no comentário de ~/.bashrc:194 ("rotacionar após o cutover"), fora de qualquer tracker; PATs fine-grained expiram por padrão em 30d.
  4. A expiração foi silenciosa — nenhum alerta; o primeiro sinal foi um 401 cru e enganoso.
  5. O valor inválido no ambiente sombreava a credencial válida do keyring do gh (gho_…, conta fsolla), porque o gh honra GITHUB_TOKEN acima do keyring.

Evidência: ~/.bashrc:194 (pré-fix, PAT adicionado no OPS71 em 2026-08-20); scripts/lib/github-api.mjs:80. Reprodução: gh auth status acusava o token de env inválido; env -u GITHUB_TOKEN gh api /repos/fsolla/teqo funcionava; GITHUB_TOKEN=$(env -u GITHUB_TOKEN gh auth token) pnpm issue next funcionava; GITHUB_TOKEN=ghp_invalid_token pnpm issue next → 401.

Causa-raiz do drive-by (main RED): C192 e C193 passaram verdes individualmente, mas o auto-merge por rebase combinou os dois num commit que não foi re-checado; C193 renomeou canReadSpeechCatalog → canReadCommunicationCatalog, e src/utilities/speech/speechPageData.ts manteve o import/call site antigo. Conflito semântico que o merge textual não detecta; a primeira prova foi o verify do deploy — que só detecta o dano depois de main já estar vermelha.

Correção

(1) Ambiente (fora do repo): ~/.bashrc agora deriva GITHUB_TOKEN de env -u GITHUB_TOKEN gh auth token quando não setado — escolha do humano; auto-renova enquanto existir o login do gh no keyring.

(2) Repo: scripts/lib/github-api.mjs passa a anexar dica acionável aos erros 401 (REST e GraphQL): token rejeitado — GITHUB_TOKEN inválido/expirado ou revogado? Rode \gh auth status` e rotacione o PAT (um token inválido no ambiente também sombreia o login do `gh`).Dois testes unitários fixam o comportamento (REST 401 sem retry + GraphQL 401) emtests/unit/githubApi.unit.spec.ts; docs/AGENT-OPS.md:124documenta a expiração de PAT e a recuperação viagh`.

(3) Drive-by, mesmo PR: src/utilities/speech/speechPageData.ts atualizado para importar/chamar canReadCommunicationCatalog — resolve a causa do TS2305 e desbloqueia o deploy.

Verificação

  • Teste de regressão: testes de 401 em tests/unit/githubApi.unit.spec.ts — prova RED pré-fix: o módulo (cópia de HEAD) emitia GitHub API GET … → 401: {"message":"Bad credentials"} sem dica; pós-fix inclui a dica
  • Suíte: pnpm test:unit 364 arquivos / 3869 testes passaram (inclui os 2 novos); pnpm test:int 94 arquivos / 838 passaram (2 skipped), após um flake pré-existente e não relacionado de ordem/estado (testDatabaseLease.int.spec.ts, verde isolado e em rerun limpo); lint/format/knip/cycles 0; pnpm typecheck 0; build 0
  • Diff sem segredos; GITHUB_TOKEN=ghp_invalid_token pnpm issue next sai 1 com a dica acionável; env -u GITHUB_TOKEN bash -ic 'pnpm issue next' imprime a Issue #1154 (read-only, confirmada ainda ready/unassigned)
  • CI: PR #1174 — a confirmar no merge
  • Prod: não se aplica (bug local)

Prevenção

Estratégia Custo Estado
Dica acionável no 401 (REST + GraphQL) + 2 testes de regressão + nota em docs/AGENT-OPS.md barata implementada agora (commit/PR)
scripts/github-doctor.mjs espelhando db-doctor.mjs (GET /user, imprime login/método), chamado no início de pnpm issue/agent:*/sessões work-issue — pegaria ESTE bug no início do run barata documentada — não implementada neste fluxo
Ciclo de vida de credencial via GitHub App installation token + alerta de expiração em sops/age em vez de .bashrc — previne a classe cara documentada — não implementada neste fluxo (candidata a Issue futura)
Guard de conflito semântico de rebase para main (branch protection strict=true re-checando o head rebaseado, ou GitHub Merge Queue) — teria pego o main RED de C192+C193 antes do merge cara documentada — não implementada neste fluxo (candidata a Issue futura)

Estratégia implementada: dica acionável no erro 401 (em vez do "Bad credentials" cru) com testes unitários que a fixam, mais a nota operacional em AGENT-OPS.md; o drive-by restaura o typecheck de main.

Estratégia documentada (cara): GitHub App installation token com expiração alertada via sops/age; e guard de re-checagem do head rebaseado (branch protection estrita ou Merge Queue) — nota-se que o verify pós-merge do deploy só detecta o dano depois de main vermelha. Também registrado: não fazer fallback silencioso para gh auth token dentro de github-api.mjs (OPS77 manteve resolução explícita de PAT; fallback silencioso mascara expiração e age como identidade errada).

Lições

Expiração de token é silenciosa: o lembrete existia só como comentário de dotfile fora de qualquer tracker, e o primeiro sinal foi um 401 cru. Uma variável de ambiente inválida sombreou uma credencial válida do keyring, produzindo um erro enganoso (a culpa parecia do gh/API, não do shell). E dois PRs individualmente verdes podem combinar vermelho em main via auto-merge por rebase, sem re-check do commit rebaseado — a primeira prova é o verify do deploy, que bloqueia produção até o fix. Trilha de evidências: ~/.bashrc:194 (pré-fix), scripts/lib/github-api.mjs:80, commit C193 bdaf3c7c, deploy run 35394524888.