14 KiB
Post-mortem: PAT fine-grained expirado no GITHUB_TOKEN do shell quebra pnpm issue/agent:*/gh — e o mesmo lote de PAT derruba o AUTOMERGE_PAT dos PRs
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 | alta (quebra a ferramentaria local e o auto-merge de todos os PRs; o deploy de main dependia disso) |
| Ambiente | dev (workstation) + CI (GitHub Actions) |
| Issue(s) | #1169 (C201, kind:defect — fechada por este PR) |
| PR do fix | #1175 |
| 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 (~01:31 UTC) | PAT fine-grained do OPS71 criado no cutover: exportado em ~/.bashrc:194 (comentário "rotacionar após o cutover") e gravado como secret AUTOMERGE_PAT (created_at 2026-08-20T01:31:17Z). 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 |
| Detecção (CI) | 2026-09-18T21:39:59Z | Primeira falha do safety net ready-automerge com 401 GraphQL (branch fix/c193-missed-rename); runs seguintes (work/18, C191-…, fix/6) falham igual — o secret AUTOMERGE_PAT está inválido. Sucessos até 20:43:58Z (plan/c199-c200) |
| Correção do C201 | 2026-09-18T21:52:39Z | O rename perdido do C193 é corrigido de forma independente pelo PR #1171 (c7d1fd29), antes deste PR; o deploy run 35398929341 começa no push da main. O drive-by deste PR foi dispensado no rebase (trabalho duplicado) |
| Correção mergeada | 2026-09-18 | PR #1175 (#1169 é fechada na merge); auto-merge bloqueado até a rotação do AUTOMERGE_PAT — merge manual possível com o required check verde |
| Deploy | 2026-09-18T21:52:42Z | run 35398929341 (push c7d1fd29) em andamento no fechamento deste post-mortem: verify full → staging automático → produção após approve humano |
| 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. O bash dos agentes herda o env do opencode serve (não do .bashrc), então os worktrees também quebravam, e os processos de longa duração (opencode serve/web, zed) precisam ser reiniciados para deixar de propagar o valor velho.
Achado no mesmo fluxo (mesma causa-raiz): o secret de repositório AUTOMERGE_PAT — criado na mesma janela do cutover OPS71 — também está inválido. O safety net agent-pr-ready-automerge.yml passou a falhar fail-closed com 401 no enablePullRequestAutoMerge (GraphQL) a partir de 21:39:59Z, então nenhum PR consegue armar auto-merge até a rotação. O valor do secret não é auditável pela API (não apurado se é literalmente o mesmo token do .bashrc; os timestamps do lote e a falha simultânea sustentam a hipótese). Rotação é ação humana (AGENT-OPS: secrets de repo são "só humano").
Causa-raiz
5-whys (401):
- A API rejeitava o token com "Bad credentials".
- O ambiente exportava um PAT fine-grained expirado;
scripts/lib/github-api.mjs:80lêprocess.env.GITHUB_TOKENe usava esse valor. - 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. - A expiração foi silenciosa — nenhum alerta; o primeiro sinal foi um 401 cru e enganoso.
- O valor inválido no ambiente sombreava a credencial válida do keyring do
gh(gho_…, conta fsolla), porque oghhonraGITHUB_TOKENacima 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 com a dica nova.
Para o AUTOMERGE_PAT: secret criado em 2026-08-20T01:31:17Z (lote do OPS71); primeiras falhas do safety net em 2026-09-18T21:39:59Z; o CLI usa o PAT fail-closed (nunca cai para o GITHUB_TOKEN nativo), então o 401 é do secret.
Causa-raiz do C201 (registrada porque o fluxo a encontrou): 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 (run 35394524888) — que só detecta o dano depois de main já estar vermelha.
Correção
(1) Ambiente (fora do repo): ~/.bashrc agora sobrescreve GITHUB_TOKEN com env -u GITHUB_TOKEN gh auth token sempre que o gh responde (antes do override, o valor herdado por sessões antigas venceria) — escolha do humano; auto-renova enquanto existir o login do gh no keyring. Processos de longa duração (opencode serve/web, zed) precisam de restart manual para perder o valor velho herdado.
(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) C201 / #1169: corrigido de forma independente pelo PR #1171 (c7d1fd29, merge 21:52:39Z) antes deste PR; o drive-by equivalente deste branch foi dispensado no rebase. Este PR fecha o item de tracker (#1169), que ficou OPEN sem keyword de fechamento no #1171.
(4) Pendente (humano): rotacionar o secret AUTOMERGE_PAT (PAT de usuário real com pull-requests: write + contents: write) em Settings → Secrets and variables → Actions. Sem ele, o safety net falha fail-closed e nenhum PR arma auto-merge. Enquanto isso, merge manual com o required check verde é o caminho.
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 deHEAD) emitiaGitHub API GET … → 401: {"message":"Bad credentials"}sem dica; pós-fix inclui a dica - Suíte:
pnpm test:unit364 arquivos / 3869 testes passaram (inclui os 2 novos);pnpm test:int94 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 typecheck0; build 0;pnpm push(gate:ci) verde no commit18216c9f - A dica funcionou ao vivo no próprio incidente: o log do safety net em 21:57 exibiu
GitHub GraphQL → 401: token rejeitado — GITHUB_TOKEN inválido/expirado… - Diff sem segredos;
GITHUB_TOKEN=ghp_invalid_token pnpm issue nextsai 1 com a dica acionável;env -u GITHUB_TOKEN bash -ic 'pnpm issue next'imprime a Issue #1154 (read-only, confirmada aindaready/unassigned) - C201:
pnpm typecheckverde namainvia #1171; deploy run 35398929341 em andamento (a confirmar) - CI do PR #1175: a confirmar no merge;
ready-automergefalha pelo secret expirado (item 4 acima) - 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 |
Calendário/alerta de expiração para os PATs (local e AUTOMERGE_PAT) com data de rotação registrada em tracker |
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/secret — 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 C201 foi corrigido por #1171 e este PR fecha o tracker.
Estratégia documentada (cara): GitHub App installation token com expiração alertada via sops/age; guard de re-checagem do head rebaseado (branch protection estrita ou Merge Queue); rotação calendarizada dos PATs — nota-se que o verify pós-merge do deploy só detecta conflito semântico depois de main vermelha, e que o AUTOMERGE_PAT expirado derruba o auto-merge da frota inteira. 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. O mesmo lote de PAT do OPS71 alimentava o ambiente local e o secret AUTOMERGE_PAT — quando expirou, derrubou a ferramentaria e o auto-merge de todos os PRs ao mesmo tempo, o que só apareceu quando o safety net passou a falhar vermelho. 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; no mesmo dia, a correção do C201 foi duplicada por dois fluxos (este branch e o #1171), reforçando a importância do claim/single-flight. Trilha de evidências: ~/.bashrc:194 (pré-fix), scripts/lib/github-api.mjs:80, commit C193 bdaf3c7c, deploy run 35394524888, safety-net run 35399315018.