28 KiB
Runbook: deploy do site 1313 no homeserver (OPS53 + OPS71 — dispatch manual)
O deploy de produção é uma action manual (workflow_dispatch no GitHub
Actions, .github/workflows/deploy.yml): mergir em main não publica
nada. O operador dispara o deploy quando decide publicar; antes de tocar o
homeserver, o job verify (hosted) roda a suíte full (incl. e2e full).
Gatilho e fluxo
- GitHub → Actions → Deploy (manual) → Run workflow (ref:
main). - Job
verify(hostedubuntu-latest, ~50 min): suíte full sem skips — check-test-locations → lint → format → typecheck → knip → cycles → unit → int (migrate+seed nos services) → build → e2e full. - Se
verifyverde, o jobdeploy(needs: [verify],runs-on: [self-hosted, homeserver]) roda no runner do GitHub instalado no homeserver e executascripts/deploy-homeserver.sh <sha>localmente (sem SSH; o runner conecta outbound ao GitHub — funciona atrás do Cloudflare tunnel; o hosted nunca toca o homeserver; o self-hosted não conta minutos hosted). - O script, no homeserver: guarda de HEAD (
git ls-remotedeTEQO_REPO_URL— defaulthttps://github.com/fsolla/teqo.git, público — == SHA do job, senão "stale run" skip) →flock(serializa) → guard "already deployed" (revision do container rodando) → build do migrator (o estágio migrator não rodanext build— builda mesmo contra o schema antigo; BuildKit, secrets do~/stack/teqo-1313.env) → push do migrator emlocalhost:5000(tag qualificada de registry — INF13: a única ref que o compose referencia) → swap dos tags de imagem no~/stack/docker-compose.yml(backup antes; sed idempotente aceita as formas antiga e qualificada e escreve sempre a qualificada) → migrations (docker compose --profile maintenance run --rm teqo-1313-migrate </dev/null, já com a imagem do SHA novo) → build do runner (contra o banco JÁ migrado — onext buildda geração estática lê o schema novo, OPS66;--network hostcom proxy socatteqo-1313-build-proxynastack_defaultpara alcançar opostgres; o proxy é criado idempotentemente pelo script) → push do runner (tag qualificada de registry) →docker compose up -d teqo-1313→ healthcheck → smoke (/,/campanha/login,/admin, barreira 307, WebAuthn login-options,api/revalidatecom o secret real). - Falha = job vermelho; nada é publicado pela metade (rollback automático após o swap).
Primeiro deploy (verificação ao vivo): depois do cutover OPS71, dispare o
deploy manual e confira no log do job deploy o set-url do workspace
(origem Forgejo → GitHub), o deployed_sha lido do container e o guard
"already deployed" num segundo dispatch do mesmo SHA.
Onde roda cada coisa
| Máquina | Papel |
|---|---|
| GitHub hosted (ubuntu) | CI de PR (ci-pr.yml, job checks) e job verify do deploy.yml (suíte full). Nunca toca o homeserver. |
| homeserver (8c/16GB) | todo o stack (~/stack/docker-compose.yml): postgres teqo_1313, forgejo, registry localhost:5000, cloudflared, teqo-1313 na 127.0.0.1:1313; runner self-hosted do GitHub (labels self-hosted, homeserver; instalado 2026-08-19 — Issue #113 OPS71-INFRA; ~/actions-runner v2.336.0, systemd user actions.runner.fsolla-teqo.teqo-1313-runner.service — systemctl --user status/restart); workspace ~/teqo-deploy (clone de github.com/fsolla/teqo, público) |
| workstation | dev/agentes apenas — o runner do Forgejo foi desligado no cutover (o schedule do ci.yml antigo não tem mais razão; religar é reversível) |
Segredos não ficam no GitHub: o script sourceia ~/stack/teqo-1313.env e
~/stack/.env (chmod 600) no próprio homeserver. O repo GitHub é público — o
clone do workspace não precisa de credencial. No GitHub ficam apenas os
secrets de integração: FORGEJO_API_TOKEN (flips pós-merge) e
CURSOR_API_KEY (archive helper, dormente).
Cutover OPS71 (passos manuais, ordem)
- Remotes locais (config compartilhada pelos worktrees):
git remote set-url origin git@github.com:fsolla/teqo.gitegit remote add forgejo ssh://git@192.168.15.142:2222/fsolla/teqo.git(ouhttps://git.solla.dev/fsolla/teqo.git) — o repo Forgejo congela em main; o tracker (Issues) segue vivo por API. - GitHub repo: secrets
FORGEJO_API_TOKEN+CURSOR_API_KEY(Settings → Secrets and variables → Actions) eAllow auto-merge(Settings → General → Pull Requests; ouPATCH /repos/...comallow_auto_merge: true). Repo é público: em Settings → Actions → General, defina fork PR workflows como não executar (ou exigir aprovação). - PAT local: criar token GitHub com escopo
repoe exportarGITHUB_TOKEN(paranode scripts/github-pr.mjsepnpm configure:branch-protection). GITHUB_TOKEN=… pnpm configure:branch-protection— required checkCI (PR) / checks(literal de match:checks— o GitHub casa pelo nome do check-run; a UI exibe workflow/job; pin PR #742), 0 reviews,enforce_admins: true. Antes do primeiro push (senão o PR mergearia sem required check).- Desligar o runner do Forgejo na workstation (ex.:
systemctl --user stop forgejo-runner) — após o primeiro PR GitHub validado; para o schedule doci.ymlantigo. - Instalar o runner self-hosted do GitHub no homeserver (labels
self-hosted+homeserver; usuário com docker + acesso a~/stack; registrar em infra-solla:STATE.md). - Push do PR do OPS71 → CI verde → auto-merge → flip da Issue no Forgejo → deploy manual de validação.
Rollback do cutover (se o GitHub falhar cedo): religar o runner do Forgejo e
abrir PRs no Forgejo a partir do main congelado — os workflows de
.forgejo/workflows/ foram removidos na OPS71 Fase 2, mas restauram do git
history (git revert do PR da remoção, ou clone do main congelado do Forgejo)
— remotes: git remote set-url origin ssh://git@192.168.15.142:2222/fsolla/teqo.git.
Rollback (do deploy)
A imagem anterior não fica mais local após a limpeza pós-deploy (INF3/F2:
docker builder prune + remoção das tags locais antigas, exceto o SHA em uso)
— o registry localhost:5000 nunca deleta e é a fonte do rollback. O
compose anterior fica em ~/stack/docker-compose.yml.pre-<sha-do-deploy-que-falhou>.
ssh homeserver
cd ~/stack
# 1. apontar o compose de volta para o SHA anterior (o que estava rodando):
# o backup do deploy que falhou é o compose com os tags antigos
ls docker-compose.yml.pre-* # escolha o anterior ao deploy ruim
cp docker-compose.yml.pre-<sha-anterior> docker-compose.yml
# 2. puxar do registry (a imagem antiga já não fica local após a limpeza;
# o compose referencia a tag qualificada — INF13 — e o pull basta):
docker pull localhost:5000/teqo-1313:<sha-anterior>
# Cuidado (janela de transição pós-INF13): backups gerados ANTES do fix
# usam tags bare (`image: teqo-1313:<sha>`); se o backup escolhido tiver
# tag sem prefixo, recrie o alias antes de subir:
# docker tag localhost:5000/teqo-1313:<sha-anterior> teqo-1313:<sha-anterior>
# 3. subir:
docker compose up -d teqo-1313
Caveat: uma migration de schema aplicada no passo de migrate não é desfeita pelo rollback (Payload é append-only). Código velho sobre schema novo pode se comportar mal — o caminho primário minimiza essa janela (migrate só roda após migrator build+push+swap ok; rollout só após runner build ok). Desde o OPS66 o migrate roda antes do build do runner (o build precisa do schema novo): um build do runner que falhe depois do migrate deixa o banco à frente do código — migrations são append-only e revisadas antes do deploy (checklist); a correção se re-mergeia e o próximo deploy completo publica.
Falhas conhecidas
| Sintoma | Causa | Tratamento |
|---|---|---|
| Job verde sem deploy ("stale run") | Push mais novo venceu; o dispatch apontava um SHA que não é mais o HEAD do main do GitHub | Nada a fazer — dispatch novo no main atual |
| Job verde sem deploy ("already deployed") | O container rodando já tem a revision do SHA do job (ex.: workflow_dispatch duplicado da mesma HEAD) |
Nada a fazer — o site já roda esse SHA. Forçar rebuild da MESMA HEAD não é suportado via dispatch: edite o tag da imagem no compose (image: localhost:5000/teqo-1313:<sha>-rebuild) e docker compose up -d, ou espere um commit novo |
Job deploy nunca roda / runner offline |
Runner self-hosted do homeserver não instalado ou parado (passo manual do cutover OPS71) | Instalar/religar o runner (labels self-hosted, homeserver); os PRs e o verify não dependem dele — só o deploy |
Deploy dispara mas o job verify falha |
Regressão real na suíte full | Corrigir e re-dispatchar — o homeserver nunca é tocado |
No such image ao recriar o container fora de deploy (ex.: compose up -d pós-reboot/migração) |
O compose referenciava uma tag local bare (teqo-1313:<sha>) que desapareceu sem recriação (achado INF8/INF13, 24/08: só restava a ref do registry) |
Corrigido no INF13: o compose passa a referenciar a tag qualificada (localhost:5000/teqo-1313:<sha>) a partir do primeiro deploy pós-fix — até lá, produção ainda roda o compose bare (recrie o alias via docker tag se necessário). Recovery geral: docker pull localhost:5000/teqo-1313:<sha> (+ docker tag se o compose em uso tiver tag bare) e docker compose up -d |
| Deploy para logo após o migrate ("Done." e nada mais, EXIT=0) | docker compose run anexa stdin por padrão — o container consome o resto do script; o bash chega a EOF e termina sem rodar o rollout |
O script já usa < /dev/null no run --rm do migrate (não remover); sintoma visto 2026-08-17 no primeiro deploy |
Build falha: network mode "stack_default" not supported by buildkit |
BuildKit (drivers docker e docker-container) recusa rede bridge custom no --network |
O script builda com --network host + proxy socat teqo-1313-build-proxy (na stack_default, publicado em 127.0.0.1:5433, criado idempotentemente com restart: unless-stopped) + DATABASE_URL reescrita para o loopback |
| Build OOM no homeserver | Laptop 8c/16GB com o stack ativo (~12GB livres medidos) | Re-dispatch; se recorrente, item futuro: build na workstation com túnel |
Build do runner falha: relation "..." does not exist no next build |
Migration nova criou tabela lida em geração estática. Pré-OPS66 a ordem era build→migrate e o deploy morria aqui para sempre (incidente 2026-08-18, S2) | Pós-OPS66 não deve ocorrer: migrate roda antes do build do runner. Se reaparecer, confira no log se o passo migrate rodou; recovery manual: docker build --target migrator + docker run --rm --network stack_default --env-file ~/stack/teqo-1313.env localhost:5000/teqo-1313-migrator:<sha> e re-dispatch do workflow |
| Workspace clone sem o SHA novo | O workspace antigo apontava para o Forgejo local (pré-OPS71) e o fetch não achou o SHA | O script agora re-aponta origin para TEQO_REPO_URL (idempotente) antes do fetch — se ainda falhar, git -C ~/teqo-deploy remote -v e corrigir manualmente |
| Migrate falha | Drift/erro de schema | Job vermelho; site segue no container antigo; corrigir e re-mergear |
| Smoke falha pós-up | Regressão de runtime | Rollback automático (restore + up -d) + job vermelho; investigar |
Backup passivo no Forgejo (OPS76-FOLLOWUP, 2026-08-21)
O GitHub é a única fonte (main, branches, PRs, CI, tracker). O Forgejo mantém
um backup passivo via pull mirror nativo: o repo teqo-backup no
Forgejo (https://git.solla.dev/fsolla/teqo-backup) espelha o GitHub
automaticamente — não serve push, não recebe PRs, é somente leitura.
- Repo:
fsolla/teqo-backupno Forgejo (mirror: true), criado viaPOST /api/v1/repos/migratecomclone_addr=https://github.com/fsolla/teqo.git,service=github,mirror=true, intervalo 8h (mirror_interval). - Automação: o próprio Forgejo sincroniza no intervalo (sem workflow).
Force sync manual:
POST /api/v1/repos/fsolla/teqo-backup/mirror-sync(o "Synchronize now" da UI). - Cobertura: espelha todas as branches (54/54 verificadas) e tags; o histórico do mirror reflete o do GitHub (que é a convergência OPS75 + OPS76). Não espelha issues/PRs/labels (GitHub é o tracker).
- Validação:
git ls-remotedoteqo-backupdeve retornar o mesmo SHA derefs/heads/maindo GitHub; conferir emmirror_updatedo último sync. - O repo
fsolla/teqooriginal do Forgejo fica congelado no ponto da convergência (OPS75) — arquivo histórico, sem mais uso.
Segurança (decisão deliberada)
- O job
deployusaruns-on: [self-hosted, homeserver]— executa como o usuário do runner no homeserver. Está apenas nodeploy.yml(dispatch manual), nunca noci-pr.yml; o hosted (verify) não tem acesso ao homeserver e o self-hosted não recebe secrets de produção (as envs vivem no próprio homeserver).mainsó anda por PR mergeado com CI verde; o deploy só roda comverifyfull verde. Fork-PRs não rodam CI (same-repo gate + setting do repo). - Segredos nunca ecoados: sem
set -x, senhas via--password-stdin/ build-secrets; envs só no homeserver.
C149 — Google OAuth da agenda (envs novas + GCP)
O botão "Conectar com o Google" da agenda usa um OAuth client do GCP. Sem as envs, o diálogo cai no runbook antigo da service account — que segue como fallback e não foi removida por esta entrega.
- No GCP (mesmo projeto da service account), crie um OAuth client Web
application com os redirect URIs:
https://jorgesolla1313.com.br/campanha/agenda/google-oauth/callbackhttp://localhost:<porta>/campanha/agenda/google-oauth/callback(dev)
- Publique o app em produção — unverified é aceito (uso interno). Em
"Testing" o refresh token expira em ~7 dias e o estado vira
errona UI. - Adicione ao
~/stack/teqo-1313.env(homeserver, chmod 600):GOOGLE_CALENDAR_OAUTH_CLIENT_IDeGOOGLE_CALENDAR_OAUTH_CLIENT_SECRET. - Deploy normal (
workflow_dispatchdodeploy.yml). O callback monta o redirect a partir deNEXT_PUBLIC_SITE_URL— precisa bater exatamente com o URI registrado no passo 1.
Desconectar no Teqo apaga o refresh token daquele lado; para revogar o acesso
de fato, revogue o app em myaccount.google.com/permissions (a UI orienta o
passo).
OPS79 — última migração da plataforma antiga → nova (vertical campanha)
Operação de dados executada em 2026-08-23. Ver assistência lógica completa:
docs/plans/ops79-ultima-migracao-dados-campanha.md (intenção) e
…campanha-impl.md (impl). A fonte (Neon, plataforma antiga) está congelada
desde o OPS80 — zero escritas desde 2026-08-23 22:30 UTC; a URL antiga segue
viva na Vercel apenas tecnicamente (desligamento de infra é o OPS81).
Descoberta-chave da reconciliação: a vertical campanha JÁ estava 100% íntegra
no teo_1313 após o OPS51 (dump Neon→homeserver) — nenhum dado de operação de
campanha ficou órfão; o time de campanha não escreveu na URL antiga depois do
OPS51. O único residual real entre as plataformas era a signature pública
1486 (e seu contato/subscription), cujo contato originário não existia no
target (o id 2221 do target é OUTRA pessoa, Jorge Solla).
Reconciliador (read-only, repetível)
Compara contagens + junta de IDs das 13 coleções da vertical e o conteúdo semântico das join-tables de relações contra a fonte Neon.
ssh homeserver
cd ~/teqo-deploy
git fetch origin main && git checkout main && git pull --ff-only origin main
pnpm install
set -a; source ~/stack/.env; set +a # fornece NEON_DATABASE_URL
set -a; source ~/stack/teqo-1313.env; set +a # fornece DATABASE_URL (teqo_1313)
# o host `postgres` só resolve dentro da rede stack_default; do host usa-se o
# proxy socat do build (127.0.0.1:5433):
export DATABASE_URL="${DATABASE_URL/@postgres:5432/@127.0.0.1:5433}"
NEON_DATABASE_URL="$NEON_DATABASE_URL" DATABASE_URL="$DATABASE_URL" pnpm ops79:reconcile
Esperado: 19/19 PASS (a municipality_rels reporta +3 absorvida pós-OPS51 —
edições de portfólio na plataforma nova, não-dado). Exit 1 em qualquer nova
divergência. Nunca imprime PII.
Migração do residual (one-off, com guard de confirmação)
Copia a signature 1486 + seu contato + subscription (a transação que o
submitPetitionSignature faz junto), reusando petition/consent já presentes e
criando o contato com id novo (nunca o 2221, ocupado por outra pessoa).
# dry-run (valida referentes e imprime o plano sem escrever):
NEON_DATABASE_URL="$NEON_DATABASE_URL" DATABASE_URL="$DATABASE_URL" pnpm ops79:migrate --dry-run
# aplica (ESCREVE em prod — exige a flag de intenção explícita):
OPS79_MIGRATE_CONFIRM=1 NEON_DATABASE_URL="$NEON_DATABASE_URL" DATABASE_URL="$DATABASE_URL" pnpm ops79:migrate --apply
Resultado registrado em 2026-08-23: contact novo 2225 (Juares Lagimar de
Souza), signature 1486 preservada (petition fim-escala-6x1, consent 2),
subscription 1492; sequences ajustadas (setval com true → próximo id =
último+1). Sequências conferidas após: contact 2225 / signature 1486 /
subscription 1492. Pós-verificação: signature 1485=1485 (Neon×target) e
reconcile 19/19; smoke /, /campanha, /admin 200.
Rollback
A migração é idempotente (fail-closed: aborta se referentes/ids divergirem). Se
precisar desfazer, DELETar as 3 linhas + ajustar sequences de volta:
DELETE FROM signature WHERE id=1486; DELETE FROM subscription WHERE id=1492; DELETE FROM contact_phones WHERE _parent_id=2225; DELETE FROM contact WHERE id=2225;
e setval('contact_id_seq',2224), setval('signature_id_seq',1485),
setval('subscription_id_seq',1491).
OPS84 — Verificação de valores (não só entidades) da migração
Extensão do reconciliador OPS79: compara TODAS as colunas de conteúdo (não só
ids/contagens) das 13 tabelas da vertical campanha entre a fonte congelada
(Neon) e a base nova (teqo_1313). Foco confirmado em vote_pledge (votos
declarados + 3 cenários de estimativa).
Resultado registrado em 2026-08-24: 13/13 tabelas PASS — 0 divergências de conteúdo. A migração de valores está 100% íntegra. Neon e target com dados idênticos em todas as 13 tabelas da vertical.
Verificador de valores (read-only, repetível)
ssh homeserver
cd ~/teqo-deploy && git fetch origin main && git checkout main && git pull --ff-only origin main
pnpm install
set -a; source ~/stack/.env; set +a # fornece NEON_DATABASE_URL
set -a; source ~/stack/teqo-1313.env; set +a # fornece DATABASE_URL (teqo_1313)
export DATABASE_URL="${DATABASE_URL/@postgres:5432/@127.0.0.1:5433}"
# Todas as 13 tabelas (foco em vote_pledge):
NEON_DATABASE_URL="$NEON_DATABASE_URL" DATABASE_URL="$DATABASE_URL" \
pnpm ops84:reconcile-values
# Tabela isolada (debug/iteração):
NEON_DATABASE_URL="$NEON_DATABASE_URL" DATABASE_URL="$DATABASE_URL" \
pnpm ops84:reconcile-values --table vote_pledge
# Com relatório markdown (anexável ao issue):
NEON_DATABASE_URL="$NEON_DATABASE_URL" DATABASE_URL="$DATABASE_URL" \
pnpm ops84:reconcile-values --report /tmp/ops84-report.md
Esperado: 13/13 PASS com 0 content diffs. Exit 1 se houver divergência
de conteúdo. Colunas classified em 3 buckets:
- content — falha o run (aceite: "0 divergência" ou "N listadas")
- derived — hook-derivadas (
declared_at/by,estimated_at/by,created_at,updated_at); informativas, não falham - sensitive — PII; valores nunca impressos, só contados
Colunas jsonb comparadas via col::text (preserva 1 vs 1.0 como
divergência; SQL NULL ≠ JSON null). Zero normalização (trim/round/coalesce).
Fallback: Neon inacessível (OPS81 já executado)
Se o Neon estiver inacessível (OPS81 desligou a infra), usar o dump do OPS51 como base de comparação:
# Restaurar dump em scratch DB local:
createdb teqo_1313_compare_src
pg_restore -d teqo_1313_compare_src \
/srv/hdd/backups/teqo-neon-pre-migracao/teqo-neon-full-20260817-204800.dump
# Apontar NEON_DATABASE_URL para o scratch:
NEON_DATABASE_URL="postgresql://teqo:teqo@localhost:5432/teqo_1313_compare_src" \
DATABASE_URL="$DATABASE_URL" \
pnpm ops84:reconcile-values --report /tmp/ops84-report-dump.md
Nota: dump é pré-freeze (2026-08-17) — deltas legítimos entre 08-17 e 08-23 (quando o Neon foi congelado) aparecem como divergências derivadas. O relatório declara qual base foi usada.
Rollback
Não há escrita; "rollback" = rodar de novo (idempotente, read-only).
Referências
scripts/deploy-homeserver.sh— o script (fonte da verdade do fluxo).github/workflows/deploy.yml— verify → deploydocs/plans/ops53-ci-deploy-homeserver*.md— intenção e decisões (era Forgejo)docs/plans/ops71-ci-github-actions-tracker-forgejo*.md— o cutover- infra-solla:
STATE.md,plano-infra-final.md§"Arquitetura de deploy"