teqo/docs/plans/ops53-ci-deploy-homeserver-impl.md
Francisco Solla 6a3f61f968 ops(OPS53): etapa de deploy no CI para o homeserver (teqo-1313)
Job deploy em ci.yml (runs-on host na workstation) streama
scripts/deploy-homeserver.sh para o homeserver via SSH: build dos stages
runner+migrator com secrets do teqo-1313.env, push no registry local,
migrate pelo serviço de maintenance antes do rollout, up + smoke. Zero
secrets novos no Forgejo; HEAD guard + flock; rollback automático com
backup do compose; runbook docs/ops/teqo-1313-deploy.md; invariantes
unit do script (bash -n + estrutura).
2026-08-18 00:50:41 +00:00

12 KiB
Raw Permalink Blame History

Impl: Etapa de deploy no CI para o homeserver (container teqo-1313)

Status: aprovado Atualizado em: 2026-08-17 Issue: #8 Intenção: docs/plans/ops53-ci-deploy-homeserver.md Appetite restante: herdado (~1–2 dias eng)

Leitura da intenção

  • Outcome: git push em main com CI verde ⇒ site 1313 atualizado (container novo com o SHA do commit), sem passo manual; migrations aplicadas contra o teqo_1313 antes do rollout (ordem migrate → rollout); rollback documentado; falha de deploy deixa o commit vermelho e nada meio-publicado.
  • O que NÃO negociar: build nunca no laptop do dev; ordem migrate → rollout; nada de schema migration contra produção fora do passo de migrate; imagem só publicada com full suite verde; rollback executável.
  • O que reavaliar (fato novo crítico): a intenção assume "o runner Forgejo roda no próprio homeserver (tem Docker + registry + compose)". Não é verdade (verificado em infra-solla/STATE.md e ao vivo): o runner é workstation-runner (act_runner v0.2.11) na workstation (16c/61GB, Pop!_OS); o homeserver (8c/16GB, laptop) é que roda o stack inteiro (~/stack/docker-compose.yml: postgres com teqo_1313, forgejo, registry :5000 com htpasswd, cloudflared, teqo-1313). ssh homeserver (tailnet) funciona da workstation (chave existente, verificada). A arquitetura do plano-infra-final.md §"Arquitetura de deploy" (runner → build → registry :5000 → migrate + pull + up no homeserver) permanece o norte — só que o "runner" fica na workstation e a orquestração do deploy cruza tailnet por SSH.

Abordagem recomendada

flowchart LR
  P[push main / dispatch] --> W[ci.yml full suite]
  W --> C[checks]
  C --> D[deploy job: runs-on host na workstation]
  D --> S[ssh homeserver bash -s < scripts/deploy-homeserver.sh SHA]
  S --> G[HEAD guard: main remoto == SHA?]
  G --> B[build no homeserver: runner + migrator stages<br/>secrets do teqo-1313.env, --network stack_default]
  B --> R[push localhost:5000 + tag teqo-1313:SHA]
  R --> M[migrate: compose --profile maintenance run --rm teqo-1313-migrate]
  M --> U[compose up -d teqo-1313 + healthcheck]
  U --> K[smoke: / , /campanha/login, /admin, 307, webauthn, revalidate]

Opções consideradas:

  • A) Deploy job runs-on: host na workstation; TODO o deploy executa no homeserver via SSH (bash -s com o script do repo), build lá mesmo com secrets lidos do ~/stack/teqo-1313.env local. Recomendada — porque: o build precisa (1) do DB de produção para a geração estática — postgres:5432 só existe dentro da rede do compose (stack_default; o postgres não publica porta); (2) dos secrets (DATABASE_URL, PAYLOAD_SECRET, NEXT_PUBLIC_SITE_URL) que já vivem chmod-600 no homeserver; (3) do registry em localhost:5000 (loopback HTTP funciona — verificado: 401 de auth, não erro de transporte). Com tudo no homeserver, zero secrets novos no Forgejo e zero mudança de infra no compose. runs-on: host usa a chave SSH existente da workstation (verificada) e mantém o job sem segredos.
  • B) Build na workstation com DB via túnel SSH + secrets no Forgejo. Exige publicar o postgres do homeserver (mudança de infra no compose), colocar DATABASE_URL/PAYLOAD_SECRET de produção como secrets do Forgejo (pior superfície de vazamento), túnel SSH no job e BuildKit com --network host para alcançar o túnel. Rejeitada — pior em todos os eixos exceto CPU (e a build no laptop cabe: 8c, ~12GB livres medidos; o stack ocioso usa ~3GB; o manual de 21b3c00d já provou o Dockerfile lá).
  • C) Job no container (ubuntu-latest) com chave SSH nova em secret do Forgejo. Mais isolamento formal, mas: gestão de chave/known_hosts, alcance tailnet do container do runner a verificar, e o mesmo SSH remote de qualquer forma. Rejeitada — o ator é o dono único da instância (só repos fsolla/amana), o runner já roda como fsolla no host, e a mitigação real é não expor o job a eventos não confiáveis (abaixo).

Decisões de engenharia (com rejeitadas):

  1. Job deploy apenas em ci.yml (push em main + workflow_dispatch), needs: [checks], timeout-minutes: 60, runs-on: host. Nunca em ci-pr.yml. Guard de segurança: verificar na implementação que o Forgejo não executa workflows de PRs de fork (default off; se estiver ligado, desligar é passo humano no runbook). Sem isso, um PR de fork malicioso poderia rodar código no host da workstation.
  2. Guard de HEAD remoto (análogo ao cooldown do Vercel): o script aborta (exit 0 "stale") se git ls-remote <repo> refs/heads/main ≠ SHA do job — um push mais novo vence; o deploy antigo não sobrepõe. Serialização: flock em /tmp/teqo-1313-deploy.lock no homeserver (capacity 1 + sem concurrency no Forgejo já serializam; o flock cobre dispatch+push simultâneos e re-deploys manuais).
  3. Script único POSIX-ish scripts/deploy-homeserver.sh versionado neste repo, executado remotamente (ssh homeserver "bash -s -- <sha>" < scripts/deploy-homeserver.sh). O homeserver não ganha dependência nenhuma (só bash + git + docker, tudo presente). Lógica: HEAD guard → flock → source dos envs (~/stack/teqo-1313.env + ~/stack/.env, sem eco) → workspace ~/teqo-deploy (clone/fetch do Forgejo local http://localhost:3000/fsolla/teqo.git, público) → docker login localhost:5000 (REGISTRY_USER/PASSWORD do .env) → build dos dois stages (--network stack_default, --secret id=database_url,env=DATABASE_URL + payload_secret, --build-arg NEXT_PUBLIC_SITE_URL) → push → backup do compose (docker-compose.yml.pre-$SHA) + swap dos tags de imagem via sed → migrate (docker compose --profile maintenance run --rm teqo-1313-migrate — o mesmo caminho do OPS51) → docker compose up -d teqo-1313 → aguarda healthcheck healthy → smoke.
  4. Migrate: serviço de maintenance do compose (teqo-1313-migrate, imagem teqo-1313-migrator:<sha> após o swap) — é o caminho que o compose já prevê e que o OPS51 usou. O payload migrate embutido no pnpm build não roda no Dockerfile (ele chama next build direto — verificado), então a ordem migrate→rollout é garantida pelo script.
  5. Smoke pós-deploy (no homeserver, contra localhost:1313): GET / 200; GET /campanha/login 200; GET /admin 200; GET /campanha → 307 (barreira sem sessão); POST /campanha/webauthn/login-options 200 (anônimo por design); POST /api/revalidate com x-revalidate-secret do env → {revalidated:true}. Login com credenciais reais fica como validação manual (precedente OPS51).
  6. Falha → rollback automático best-effort: restore do backup do compose + docker compose up -d teqo-1313 (imagem anterior continua local e no registry; registry nunca deleta) + exit 1 (job vermelho). Caveat documentado: migration de schema aplicada no passo de migrate não é desfeita pelo rollback (Payload é append-only) — o código velho sobre schema novo é risco residual aceito; o caminho primário protege (migrate só roda após build+push ok; rollout só após migrate ok).
  7. Testes: spec unit nova (tests/unit/deployScript.unit.spec.ts) com bash -n no script + greps estruturais que pinam as propriedades de segurança (HEAD guard, --network stack_default, --secret id=database_url, serviço de maintenance, endpoints do smoke) — mesmo padrão do ciSkipInvariants que pina os workflows. Os invariantes existentes de ci.yml (.next-e2e etc.) permanecem.

Componentes / mudanças

  • .forgejo/workflows/ci.yml: job deploy novo (runs-on host, needs: [checks], checkout@v5 + ssh homeserver "bash -s -- $GITHUB_SHA" < scripts/deploy-homeserver.sh); comentário de cabeçalho atualizado ("No deploy step" → o passo existe agora).
  • scripts/deploy-homeserver.sh (novo): o script remoto descrito acima; args = SHA do commit; env de configuração com default (TEQO_REPO_URL, STACK_DIR, WORKSPACE, LOCK); nunca ecoa valores de env; set -euo pipefail.
  • tests/unit/deployScript.unit.spec.ts (novo): bash -n + invariantes estruturais.
  • docs/ops/teqo-1313-deploy.md (novo, dir novo): runbook — gatilho, fluxo, onde roda cada passo, rollback (restore do backup do compose + up -d, imagens anteriores no registry), re-deploy manual (dispatch), falhas conhecidas, quem é o humano (secrets ficam no homeserver; nada novo no Forgejo).
  • AGENTS.md (parágrafo de produção/CI) e docs/AGENT-OPS.md (linha do fluxo + tabela de ambientes): deploy deixou de ser "runbook fora do repo".
  • docs/changelog/2026-08-17-ops53.md + pnpm changelog:build (OPS44).
  • infra-solla (fora do repo, anotação no fechamento): STATE.md e runbook com a correção da topologia (runner na workstation, não no homeserver) e o passo único de setup humano — nada de daemon.json (loopback HTTP OK), nada de secrets novos.
  • Migration: sem migration (não toca schema).
  • Access / Consent: N/A.
  • UI: Impeccable A — sem UI.

Dados → forma

N/A (sem dados novos na UI).

Fases verificáveis

  1. Tracer — script + workflow + tests + docs — escrever deploy-homeserver.sh, job no ci.yml, spec unit, runbook, atualizações de AGENTS/AGENT-OPS, changelog. Gates: pnpm gate:fast, pnpm format:check, pnpm knip, pnpm check:cycles.
  2. Rehearsal read-only — rodar os comandos do smoke contra o site atual (a partir do homeserver) para validar a lógica do smoke sem deploy; bash -n; verificar o setting de fork-PRs no Forgejo (mitigação do runs-on: host).
  3. First real deploy = o merge do próprio OPS53 — o pipeline publica o SHA do merge; acompanhar o job; se falhar, rollback pelo runbook (restore do backup + up -d). Este é o tracer bullet do aceite ("merge == publicar").

Rabbit holes / Não escopo (engenharia)

  • Blue-green / zero-downtime obrigatório — aceite é a janela do recreate; evolução futura.
  • Deploy button / UI de rollback — runbook basta.
  • Build na workstation com túnel para ganhar CPU — vira item futuro se a build no laptop incomodar (gatilho: OOM ou >30 min).
  • Registry com latest/tags rolantes — tags por SHA + retenção bastam.
  • Backup de DB no deploy — o plano infra já aponta pg_dump diário como pendência (débito registrado no fechamento, não neste item).
  • Otimizar o job para "só deploy se o diff tocar código" — idempotente, custo aceito.

Riscos e mitigação

  • OOM na build do laptop (8c/16GB, ~12GB livres). O manual de 21b3c00d já buildou lá; teqo-1313 tem mem_limit 3g; mitigação: monitorar o primeiro deploy; gatilho de revisitação = build na workstation via túnel (registrado acima).
  • runs-on: host = código executando como fsolla na workstation. Mitigação: job só em ci.yml (push main/dispatch — main é protegida, só entra via PR com checks verdes); verificar fork-PRs desligado no Forgejo (passo 2); instance é do dono único (repos fsolla/amana).
  • actions/checkout@v5 em job host — se o checkout falhar no host label, fallback documentado: baixar o script via raw do Forgejo (git.solla.dev/fsolla/teqo/raw/branch/main/scripts/deploy-homeserver.sh).
  • Rollback pós-migrate — código velho + schema novo pode não ser 100% funcional; best-effort + vermelho no job + runbook; caminho primário minimiza a janela.
  • Vazamento de secrets nos logs — script sem set -x, valores só via env do ~/stack/*.env, build com BuildKit --secret (sem camadas); smoke do revalidate sem ecoar o secret.
  • Deploy antigo sobrescrevendo push novo — HEAD guard remoto (stale → skip verde).

Aceite de engenharia

  • Aceite de produto da intenção ainda coberto (merge == publicar; migrate → rollout; rollback documentado; falha = vermelho; sem build no laptop do dev)
  • Invariantes AGENTS/engineering-standards (sem segredos no repo; guards de dev/test intocados; nada de Neon)
  • Testes previstos: spec unit de bash -n + invariantes estruturais do script; invariantes existentes dos workflows intactos
  • Docs: runbook de deploy/rollback + AGENTS/AGENT-OPS + changelog

Self-score decision-quality: 1) decisões caras com rejeitadas ✓ 2) cabe no appetite ✓ 3) rabbit holes nomeados ✓ 4) reusa padrões existentes (Dockerfile/migrator, serviço de maintenance, smoke do OPS51) ✓ 5) aceite da intenção preservado ✓ — 5/5.