# 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 1. GitHub → Actions → **Deploy (manual)** → Run workflow (ref: `main`). 2. Job `verify` (hosted `ubuntu-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. 3. Se `verify` verde, o job `deploy` (`needs: [verify]`, `runs-on: [self-hosted, homeserver]`) roda no **runner do GitHub instalado no homeserver** e executa `scripts/deploy-homeserver.sh ` **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). 4. O script, no **homeserver**: guarda de HEAD (`git ls-remote` de `TEQO_REPO_URL` — default `https://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 roda `next build` — builda mesmo contra o schema antigo; BuildKit, secrets do `~/stack/teqo-1313.env`) → push do migrator em `localhost: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 `. ```bash 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- 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: # Cuidado (janela de transição pós-INF13): backups gerados ANTES do fix # usam tags bare (`image: teqo-1313:`); se o backup escolhido tiver # tag sem prefixo, recrie o alias antes de subir: # docker tag localhost:5000/teqo-1313: teqo-1313: # 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:-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:`) 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:`) 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:` (+ `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:` 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-backup` no Forgejo (`mirror: true`), criado via `POST /api/v1/repos/migrate` com `clone_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-remote` do `teqo-backup` deve retornar o mesmo SHA de `refs/heads/main` do GitHub; conferir em `mirror_updated` o último sync. - **O repo `fsolla/teqo` original do Forgejo** fica **congelado** no ponto da convergência (OPS75) — arquivo histórico, sem mais uso. ## Segurança (decisão deliberada) - O job `deploy` usa `runs-on: [self-hosted, homeserver]` — executa como o usuário do runner no homeserver. Está **apenas** no `deploy.yml` (dispatch manual), nunca no `ci-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). `main` só anda por PR mergeado com CI verde; o deploy só roda com `verify` full 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. ## 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. ```bash 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). ```bash # 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) ```bash 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: ```bash # 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 → deploy - `docs/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"