# F6 — Deploy & operação (CI/CD · borda WHM · backup/restore)

> **Épico:** `epic_01iyr6v6` · **Depende de:** F5 · **Habilita:** produção · **Plano:** 15
> **Revisão 2026-10-19:** escopo reescrito pela [ADR-042](../adr/ADR-042-sem-retrocompatibilidade-conteudo-migrado-a-mao.md)
> — **sem retrocompatibilidade**, base nova e conteúdo do legado entrado **manualmente, caso a caso**.
> Saíram: dump/restore do PG 10.23, `sys_migration`, `legacy_mapping`, adaptadores por módulo,
> `migrate:legacy` e transição de hash. Ver §"Fora de escopo" abaixo.

## Objetivo

Colocar o V7 em produção de forma **atômica, observável e reversível** numa conta cPanel real, com a
borda do WHM correta, e provar que o banco do **próprio V7** é recuperável (backup, restore em base
limpa e PITR). Conteúdo e usuários entram pelo CMS, com curadoria manual.

## Estado real (medido em 2026-10-19)

O que **já existe** — o plano 15 original prometia mais do que a constituição permite:

| Item | Estado | Evidência |
|---|---|---|
| `.gitlab-ci.yml` | ✅ `check` → `build` → `deploy` (manual), runner shell tag `deploy`, `make check E2E=1` como **único** gate | o arquivo; estágios separados por ferramenta foram substituídos pelo caminho único (ADR-001) |
| Imagem de produção separada | ⛔ **não deve existir** | AGENTS.md: "same image dev/prod"; ADR-031 (base pinada) + `composer install --no-dev` no deploy |
| `./scripts/update_prod.sh` | ✅ lock (`flock`), `--dry-run/--no-pull/--skip-migrate/--branch/--allow-dirty`, 10 fases, JSON log, healthcheck `/up`, rollback via `trap` | `scripts/update_prod.sh`, `wiki/specs/17-up-prod.md` |
| `php artisan optimize` no deploy (sem `storage:link` — ADR-030) | ✅ fase 7 | `scripts/update_prod.sh` |
| **Backup antes de `migrate`** | ✅ fase **5.9** (`pg_dump -Fc` + `pg_restore --list`), retenção `BACKUP_KEEP`, cópia + poda fora do host (`BACKUP_OFFSITE=1`) | `scripts/update_prod.sh`, `core:backup:push`, `core:backup:prune` |
| Restore ensaiado | ✅ `tools/db-restore-drill.sh` (base descartável, `RTO` medido) | medido: 18 tabelas · 27 migrations · 0 pendentes · **9 s** |
| **PITR** (WAL) | ✅ `archive_mode=on` + `tools/pitr-drill.sh` | medido: recupera até o LSN alvo (`base,after`), **sem** `late` · RTO 8–24 s |
| Usuários | ✅ `core:user:create` (Argon2id, um perfil, `suporte` reservado) | `CmsUserCommandTest` (6) |
| Go-live | ✅ `tools/go-live-check.sh` (páginas críticas + arquivos de raiz + headers) | 9/9 OK em dev |
| Runbook de operação | ✅ `wiki/runbooks/deploy-e-recuperacao.md` | deploy, backup, restore, PITR, rollback, WHM, go-live |
| `seo.redirects` (301 curado) | ✅ tabela + `ResolveSeoRedirect` (cache 300 s) + **`seo:redirect`** (console) + testes | `src/services/Seo/**` |
| `sys_migration` / `legacy_mapping` / `migrate:legacy` | ⛔ **não existem e não existirão** | ADR-042 |
| AutoSSL/`ProxyPass` documentados | ⚠️ parcial (ADR-005 + spec 16 descrevem; falta o runbook com o passo a passo do WHM) | `wiki/specs/16-edge-apache-whm.md` |

## Implementado em 2026-10-19 (sessão F6)

- **F6.I2.T1 — backup antes do migrate**: `scripts/update_prod.sh` fase **5.9**; dump em
  `$STORAGE_PATH/backups/db-<UTC>.dump`, validado com `pg_restore --list`; dump vazio/ilegível
  **aborta** o deploy (`backup_db`); retenção por `BACKUP_KEEP` (poda determinística por nome);
  cópia remota opcional `core:backup:push` (confere tamanho no destino). Flag `--backup-only` para
  operação avulsa. Provado ao vivo: dump de **327 825 bytes**, poda 5→2 com `BACKUP_KEEP=2` e
  upload para `s3:backups/…` com tamanho conferido.
- **F6.I2.T2 — restore ensaiado**: `tools/db-restore-drill.sh` restaura o dump numa base descartável
  (`concordia_restore_<ts>`), confere tabelas/`migrations`/`migrate:status` sem pendências, imprime
  `rto_s` em JSON e remove a base. Medido: **9 s**.
- **F6.I3.T1 — 301 curado**: comando `seo:redirect` (`--from/--to/--status/--disable/--remove`,
  lista por padrão), valida origem/destino, invalida o cache do mapa e emite `seo.redirect`; rota viva
  nunca é sombreada (teste existente).
- **F6.I2.T2 — PITR**: `docker/postgres/postgresql.conf` com `wal_level=replica`,
  `archive_mode=on`, `archive_timeout=300` e `archive_command` → `${PGDATA}/wal_archive`;
  `tools/pitr-drill.sh` tira `pg_basebackup`, força `pg_switch_wal`, escreve `late` **depois** do alvo
  e restaura com `recovery_target_lsn` (determinístico: `recovery_target_time` erra por fuso e pela
  corrida do arquivador) + `recovery.signal`; valida `base,after` sem `late` e mede o RTO
  (medido 3×: 8 s, 24 s, 23 s). Produção deve arquivar WAL no S3 (ADR-030) — documentado no runbook.
- **F6.I2.T3 — retenção**: `BACKUP_KEEP` poda o local (determinístico pelo nome) e
  `core:backup:prune --keep=N [--dry-run]` poda a **cópia no S3** (o deploy roda as duas quando
  `BACKUP_OFFSITE=1`); DR documentado no runbook §3 (restore do dump remoto + PITR).
- **F6.I3.T2 — usuários**: `core:user:create` (Argon2id via cast `hashed`, `--role` por slug,
  `--force` para atualizar, senha gerada quando não interativo, perfil `suporte` só para
  `suporte@maxima.inf.br`, `log_event` **sem PII**) — 6 testes.
- **F6.I3.T3 — go-live**: `tools/go-live-check.sh` (9 páginas/arquivos críticos: 200 + `Content-Type`
  + CSP + `X-Content-Type-Options` + `Referrer-Policy` + `Cache-Control`, com `--json`), 9/9 OK.
- **F6.I1.T2 — deploy provado**: `scripts/update_prod.sh` 2× seguidas → 0 (~117 s, mesmas imagens/serviços) e
  falha injetada (Caddyfile inválido no `caddy validate`) → **rollback** com saída 1 e app no ar.
- **F6.I1.T1/T3 — pipeline e runbook**: gate estrutural `DeployPipelineTest` (ordem backup→migrate,
  lock, `trap` de rollback, `optimize`/`optimize:clear`, healthcheck `/up`, CI com `make check E2E=1`
  + deploy manual + artefatos, mesma imagem dev/prod, **zero resquício de ETL** — ADR-042) e runbook
  publicado. `storage/backups/` no `.gitignore` (LGPD: dump tem dado pessoal).
- **Spec 17** atualizada (fase 5.9, flags, `BACKUP_*`, rollback com `optimize:clear`).

## Fora de escopo (ADR-042)

- Dump/restore do **PostgreSQL 10.23** do v5; schema de quarentena; `config/migration.php`.
- Adaptadores de importação (Notícias, Diário Oficial, Licitações/Contratos, Obras, Ouvidoria).
- `legacy_mapping`, `migrate:legacy --dry-run/--only=`, reconciliação por contagem.
- Migração de usuários com transição de hash / shadow auth.
- "Paridade de conteúdo" medida por linhas: vira **checklist de páginas críticas** (F6.I3.T3).

## Issues e tasks

### `F6.I1` — CI/CD e deploy atômico (plano 15) · `task_01cx4ni0`
- **T1** `task_00x46tuw` — `.gitlab-ci.yml` completo e **reprodutível**: `check` (`make check E2E=1`,
  cobertura Cobertura + logs em artefato), assets + `sbom.cdx.json` saem do `check` (sem estágio `build`), `deploy` manual em
  `main` via `scripts/update_prod.sh --no-pull`. **Sem** imagem de produção própria (AGENTS/ADR-031): o deploy é
  `composer install --no-dev --classmap-authoritative` + `optimize` dentro do container.
- **T2** `task_01r20th7` — Deploy atômico + healthcheck + rollback **provados**: `scripts/update_prod.sh` 2×
  termina 0 sem mudar estado; falha injetada no meio do deploy aciona o rollback; `/up` só volta 200
  com o `optimize` aplicado (I2.T1 pluga o backup antes do `migrate`).
- **T3** `task_00ja4pzi` — Borda no WHM (domínio/AutoSSL/`ProxyPass` → `web`) + **runbook de
  operação** (subir, reverter, restaurar backup, limpar cache, renovar certificado, rolar logs) e
  política de retenção de backup.

### `F6.I2` — Base nova: backup, restore e PITR do PG 18 · `task_01wedy29`
- **T1** `task_01e0cyc2` — **Backup antes de migrar**: `pg_dump -Fc` no `scripts/update_prod.sh` (fase antes do
  `artisan migrate`), arquivo nomeado/determinístico, falha do dump **aborta** o deploy, retenção
  (N dias / M GiB) e cópia fora do host (S3/RustFS — ADR-030).
- **T2** `task_01jy6h6i` — **Restore ensaiado em base limpa** (a partir do dump) + **PITR** com WAL
  arquivado: roteiro reproduzível, tempo medido (RTO) e verificação de integridade (`migrate:status`,
  contagens de referência, checksums).
- **T3** `task_01ohn9qn` — **Política de retenção/expurgo** de backups + ensaio de **DR completo**
  (host perdido → container + dump + storage) com checklist e dono. Sem janela de corte de dados:
  a "base nova" já é o corte.

### `F6.I3` — Corte e conteúdo do cliente (manual) · `task_01op1idn`
- **T1** `task_00yjz1b0` — **301 curado à mão**: superfície mínima de operação para `seo.redirects`
  (comando `artisan` de console; UI no backlog) + lista de URLs críticas do cliente + teste de que a
  rota viva nunca é sombreada (ADR-027).
- **T2** `task_007ci8zo` — **Usuários recriados** no CMS (Argon2id, um perfil por usuário — ADR-033/
  ADR-037): checklist de quem entra, com qual perfil, e revisão de `suporte@` (fundador).
- **T3** `task_01xvg7tm` — **Checklist de go-live** (paridade sem contagem): páginas críticas com 200
  + metadata + a11y verdes, `sitemap.xml`/`robots.txt` corretos, `http.not_found` monitorado na
  primeira semana, rollback ensaiado em conta cPanel de homologação.

## Revisão profunda (`_to_revision/`)

As nove tasks de F6 entraram no pipeline de revisão como fichas 46–54 (README + `PROTOCOLO.md` §15).
Sessão de 2026-10-20: **2 altos, 4 médios, 1 baixo corrigidos** com teste vermelho→verde — destaque
para `seo:redirect --disable/--remove` saindo da **trilha de auditoria** (query builder em vez de
model), a espera do WAL no `pitr-drill.sh` que **expirava em silêncio** e a senha por flag em
`core:user:create` sem mínimo. Observações (barra final em `--from`, `PGDATA` fixo no
`restore_command`, `BACKUP_KEEP` cru) ficaram em `risk_01urn5zn`. Evidência: `make check E2E=1`
verde 2×; PITR 11 s; go-live 9/9.

## DoD (binário)

- [x] **Backup antes de `migrate`** existindo e bloqueando o deploy quando falha.
- [x] **Restore em base limpa** validado a partir do dump + **PITR** ensaiado (RTO medido).
- [x] Runbook de operação e política de retenção publicados em `wiki/runbooks/`.
- [x] `grep -rn "sys_migration\|legacy_mapping\|migrate:legacy" src/ config/` **vazio** (ADR-042).
- [x] Deploy idempotente e **reversível em dev**: `scripts/update_prod.sh` 2× → 0 sem mudar estado; falha
      injetada → rollback. *(falta repetir na conta cPanel real)*
- [x] Usuários recriáveis (`core:user:create`), 301 curado (`seo:redirect`), go-live mecânico
      (`tools/go-live-check.sh`).
- [ ] Deploy atômico, observável e reversível em **conta cPanel real** (humano/infra) — mesma
      bateria de `scripts/update_prod.sh` 2× + falha injetada, agora no servidor do cliente.
- [ ] Borda real: domínio + AutoSSL + `ProxyPass`; headers/CSP do ADR-020 em produção.
- [ ] Páginas críticas **do cliente** com 200/metadata/a11y verdes (`go-live-check` + `make check E2E=1`).

## Evidência

```
--note "deploy 2x + rollback injetado em homolog; backup pg_dump antes do migrate; restore em base
limpa + PITR com RTO medido; runbook publicado; páginas críticas 200/metadata/a11y; 301 curados"
```
