diff --git a/AGENTS.md b/AGENTS.md index bfd89634..f6ad4ff2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,7 +12,7 @@ These are product/architecture constraints — not a ban on editing existing cod - Single Next.js app, three route groups: `(frontend)` public site, `(payload)` admin, `(campaign)` internal campaign tool. No separate Rust service or frontend app for `/campanha`; it is modeled with Payload collections. The campaign roles are `coordinator` (label "Coordenador Geral"), `advisor` ("Assessor"), `leader` ("Liderança"), and `candidate` ("Candidato", unrestricted visibility), all on the separate auth collection `campaignUser` (see "Campaign auth" below). An advisor sees and manages ONLY the municipalities they administer; a leader is locked down to the supporter contact tool. - **2026-07-23 remodel (DEPLOYED to production 2026-07-23): "Município" is the operational unit** (it replaced "Praça", which replaced "Núcleo Eleitoral"). Municípios are PREDEFINED (seeded, geography read-only): 435 rows — one per Bahia municipality (416), except Salvador (19 zone Municípios, ZE 1–19); Camaçari is a single whole municipality (ZE 170/171 aggregated); shared/leaky TSE zones are cut at the municipal boundary. Code identifier: `municipality`. Leaderships are one record per person (`contact` UNIQUE) linked to N municipalities and N organizations; vote pledges are per leadership×municipality with `declaredVotes` (staff-entered, leader-visible) vs `estimatedVotes` (three scenarios, staff-only, hidden from the leader). Campaign production data was reset by the remodel migrations (public-site data untouched). Master plan: `docs/plans/remodelagem-municipios.md` (supersedes `remodelagem-pracas.md`). Details: "Campaign Municípios model" below. -- Production is self-hosted on the homeserver (`jorgesolla1313.com.br`, behind the Cloudflare tunnel; container/database `teqo-1313`/`teqo_1313`). **There is no Vercel deploy anymore** — the Vercel deploy job, `vercel-promote.yml`, the Vercel scripts/`vercel.json` and the `VERCEL_*` repo secrets were removed in OPS50 (2026-08-17). The CI (`ci.yml` on `main`) is the verifier — full suite green — **plus the deploy step (OPS53, 2026-08-17; windowed since OPS65, 2026-08-18)**: `ci.yml` no longer fires on every push — it runs in a **fixed 30-minute window** (`schedule */30` + `workflow_dispatch`), where a cheap gate job (`check`, host runner, no pnpm) reads the SHA deployed on the homeserver from `~/stack/docker-compose.yml` over SSH, diffs it against `main` HEAD and classifies "production change" using `.dockerignore` as the source of truth (docs/`.agents`/`.env*` changes skip; `scripts/`, `tests/`, `.forgejo/`, `AGENTS.md` count as production — conservative). No production change → suite + deploy skipped, run stays green; production change → full suite + deploy in the same window; dispatch always runs the full pipeline. The `deploy` job (runs-on `host` on the workstation runner) streams `scripts/deploy-homeserver.sh` to the homeserver over SSH; the remote script is idempotent ("already deployed" → skip), builds the standalone image at the pushed SHA (BuildKit secrets from `~/stack/teqo-1313.env`), pushes it to the local registry (`localhost:5000`), applies pending migrations through the compose maintenance service `teqo-1313-migrate` **before** the rollout, and recreates the container. Merge em `main` com mudança de produção verde == site atualizado em até ~1 h (janela de 30 min + suite + deploy). Runbook (rollback, falhas conhecidas): `docs/ops/teqo-1313-deploy.md`. **The agent-pool supervisor workflow was killed in OPS65** (the `*/10` tick cost 144 full `pnpm install`/day on the workstation; `agent-pool.yml` is gone, pool scripts/skills stay dormant). **The production database lives on the homeserver Postgres (`teqo_1313`, migrated from Neon in OPS51, 2026-08-17)** — the full production content (contacts, signatures, subscriptions, campaign) was restored there via `pg_dump`/`pg_restore` (dump in `/srv/hdd/backups/teqo-neon-pre-migracao/`); Neon remains alive and untouched as the documented rollback (re-point `DATABASE_URL` in `~/stack/teqo-1313.env`). Media moved to the homeserver Garage S3 in OPS52 (2026-08-18): the `media` collection uploads via `@payloadcms/storage-s3` when the `S3_*` envs are set, and Payload proxies the files through `/api/media/file/...` (the bucket stays private). Production env lives in `~/stack/teqo-1313.env` (bucket `teqo-media`, endpoint `http://host.docker.internal:3900` — o container alcança o Garage no host via `extra_hosts: host-gateway` + regras ufw `allow from 10.0.11.0/24 to any port 3900` (INPUT e route/FWD); a workstation/tailnet usa `http://100.119.220.31:3900`, key `teqo-key`); a partial `S3_*` config aborts the boot, and no `S3_*` means local disk (dev/test). The 40 pre-existing media rows are RECOVERED (OPS52-media, 2026-08-18): 38/40 objects restaurados no bucket a partir do WordPress (38 capas servindo 200 em `/api/media/file/...`; exceções sem fonte: `fim-escala-6x1.jpg` e `jorgesolla.jpg`) — re-executar via `pnpm media:recover` (restaura objetos sem tocar no DB; runbook em `docs/plans/ops52-media-recuperar-arquivos-impl.md`; desde OPS52-media-guard o reconcile exige `MEDIA_RECOVER_CONFIRM=1` — guard de intenção explícita para escrita no bucket, `--dry-run`/`--verify` seguem sem flag); the URLs themselves are the same contract the proxy now serves. Never point dev/test databases at the homeserver prod DB either — the local guards (`guard-dev-db`, `assertTestDatabase`) still enforce `localhost`/`_test`-suffixed targets. +- Production is self-hosted on the homeserver (`jorgesolla1313.com.br`, behind the Cloudflare tunnel; container/database `teqo-1313`/`teqo_1313`). **There is no Vercel deploy anymore** — the Vercel deploy job, `vercel-promote.yml`, the Vercel scripts/`vercel.json` and the `VERCEL_*` repo secrets were removed in OPS50 (2026-08-17). The CI (`ci.yml` on `main`) is the verifier — full suite green — **plus the deploy step (OPS53, 2026-08-17; windowed since OPS65, 2026-08-18)**: `ci.yml` no longer fires on every push — it runs in a **fixed 30-minute window** (`schedule */30` + `workflow_dispatch`), where a cheap gate job (`check`, host runner, no pnpm) reads the SHA deployed on the homeserver from `~/stack/docker-compose.yml` over SSH, diffs it against `main` HEAD and classifies "production change" using `.dockerignore` as the source of truth (docs/`.agents`/`.env*` changes skip; `scripts/`, `tests/`, `.forgejo/`, `AGENTS.md` count as production — conservative). No production change → suite + deploy skipped, run stays green; production change → full suite + deploy in the same window; dispatch always runs the full pipeline. The `deploy` job (runs-on `host` on the workstation runner) streams `scripts/deploy-homeserver.sh` to the homeserver over SSH; the remote script is idempotent ("already deployed" → skip), builds the migrator image first, applies pending migrations through the compose maintenance service `teqo-1313-migrate` **before the runner build** (OPS66: the static build reads the new schema — build-after-migrate, never the reverse), builds the standalone image at the pushed SHA (BuildKit secrets from `~/stack/teqo-1313.env`), pushes it to the local registry (`localhost:5000`), and recreates the container. Merge em `main` com mudança de produção verde == site atualizado em até ~1 h (janela de 30 min + suite + deploy). Runbook (rollback, falhas conhecidas): `docs/ops/teqo-1313-deploy.md`. **The agent-pool supervisor workflow was killed in OPS65** (the `*/10` tick cost 144 full `pnpm install`/day on the workstation; `agent-pool.yml` is gone, pool scripts/skills stay dormant). **The production database lives on the homeserver Postgres (`teqo_1313`, migrated from Neon in OPS51, 2026-08-17)** — the full production content (contacts, signatures, subscriptions, campaign) was restored there via `pg_dump`/`pg_restore` (dump in `/srv/hdd/backups/teqo-neon-pre-migracao/`); Neon remains alive and untouched as the documented rollback (re-point `DATABASE_URL` in `~/stack/teqo-1313.env`). Media moved to the homeserver Garage S3 in OPS52 (2026-08-18): the `media` collection uploads via `@payloadcms/storage-s3` when the `S3_*` envs are set, and Payload proxies the files through `/api/media/file/...` (the bucket stays private). Production env lives in `~/stack/teqo-1313.env` (bucket `teqo-media`, endpoint `http://host.docker.internal:3900` — o container alcança o Garage no host via `extra_hosts: host-gateway` + regras ufw `allow from 10.0.11.0/24 to any port 3900` (INPUT e route/FWD); a workstation/tailnet usa `http://100.119.220.31:3900`, key `teqo-key`); a partial `S3_*` config aborts the boot, and no `S3_*` means local disk (dev/test). The 40 pre-existing media rows are RECOVERED (OPS52-media, 2026-08-18): 38/40 objects restaurados no bucket a partir do WordPress (38 capas servindo 200 em `/api/media/file/...`; exceções sem fonte: `fim-escala-6x1.jpg` e `jorgesolla.jpg`) — re-executar via `pnpm media:recover` (restaura objetos sem tocar no DB; runbook em `docs/plans/ops52-media-recuperar-arquivos-impl.md`; desde OPS52-media-guard o reconcile exige `MEDIA_RECOVER_CONFIRM=1` — guard de intenção explícita para escrita no bucket, `--dry-run`/`--verify` seguem sem flag); the URLs themselves are the same contract the proxy now serves. Never point dev/test databases at the homeserver prod DB either — the local guards (`guard-dev-db`, `assertTestDatabase`) still enforce `localhost`/`_test`-suffixed targets. - Donations are NOT handled in this app — they run through QueroApoiar (`apoiar.me/jorgesolla`, TSE-homologated). The site only needs a "Doar" link/CTA out to that URL. - **Embasamento de produto/domínio:** `docs/research/` guarda o compêndio de literatura (campanha de DF na Bahia, geografia do voto, cartografia eleitoral) e o relatório de discovery aprovado (2026-07-21) com o playbook dado→decisão. Para métricas, análises territoriais, mapa e priorização em `/campanha`, consulte-o antes de inventar — o kernel: a disputa de DF é conta de quociente fragmentada; leitura útil é relativa e local (captura, LQ, quantis, cobertura de meta por pledges), nunca % estadual absoluto. diff --git a/Dockerfile b/Dockerfile index 43175c74..df28d81c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -32,7 +32,9 @@ ENV NEXT_PUBLIC_SITE_URL=$NEXT_PUBLIC_SITE_URL ENV NEXT_TELEMETRY_DISABLED=1 # Static generation reads Payload data. BuildKit mounts keep credentials out of -# layers and image history; migrations run separately through the migrator. +# layers and image history; migrations run separately through the migrator — +# the deploy script applies them BEFORE this build (OPS66: static routes read +# the new schema, so the build must see a migrated DB). RUN --mount=type=secret,id=database_url,env=DATABASE_URL \ --mount=type=secret,id=payload_secret,env=PAYLOAD_SECRET \ NEXT_OUTPUT_STANDALONE=1 \ diff --git a/docs/CHANGELOG-AGENTS.md b/docs/CHANGELOG-AGENTS.md index 3e3b019a..8b535dad 100644 --- a/docs/CHANGELOG-AGENTS.md +++ b/docs/CHANGELOG-AGENTS.md @@ -24,6 +24,8 @@ Conteúdo movido do `AGENTS.md` em 2026-07-30 (fatiamento do paradigma de agente **PUB5** — slogan da campanha "Mais saúde mais futuro" no hero e no rodapé da home (jorgesolla1313.com.br), no lugar de "Um mandato do tamanho da Bahia" (discurso do mandato vigente; o novo é da campanha 2026 — saúde é o tema-âncora do candidato). Troca de copy cirúrgica, sem redesenho nem migration: h1 do hero em duas linhas uppercase "MAIS SAÚDE / MAIS FUTURO" com `aria-label` acompanhando ("MAIS SAÚDE MAIS FUTURO"), segunda linha já em `font-black` (anatomia existente), e o rodapé em sentence case com ponto ("Mais saúde mais futuro."). Testes que pinavam a string antiga atualizados (`campaignHero.unit`, `campaignHome.unit`, `frontend.e2e`). Fora de escopo decidido no gate: cabeçalho de artigos ("O mandato do tamanho da Bahia") e docs históricos. Verificação: unit 2269 + int 791 verdes, e2e frontend 8/8 em prod mode (`E2E_PROD=1`, `.next-e2e`), lint/format/typecheck/cycles/knip/build verdes; render conferido por screenshot + DOM (hero duas linhas, rodapé com ponto). `/simplify` 3 reviewers: sem P0/P1; P2 corrigidos no impl plan (redação + line refs); débitos descartados por serem convenções pré-existentes ou decisões do gate. +**Recently resolved (2026-08-19):** **OPS66** — deadlock de migrations em caminho estático: o deploy buildava o runner **antes** do migrate e, se uma migration criasse tabela lida em geração estática (caso S2, 2026-08-18: `social_feed_settings` na home), o `next build` falhava para sempre (`relation ... does not exist`) e o migrate nunca rodava — 4 deploys seguidos vermelhos até o migrator manual. Fix em `scripts/deploy-homeserver.sh` (reordenação de fases, sem custo novo — migrate e build do migrator já rodavam em todo deploy): **build do migrator** (o estágio não roda `next build` — builda contra o schema antigo) → push/tag do migrator → swap do compose (backup + trap na mesma posição) → **migrate** via `docker compose run teqo-1313-migrate` (caminho canônico do serviço de maintenance, agora com a imagem do SHA novo) → **build do runner** contra o banco JÁ migrado → push/tag → rollout → healthcheck → smoke. Alternativas rejeitadas: `payload migrate` dentro do build do runner (efeito colateral de escrita em prod a partir de um processo de build; contraria o desenho deliberado do Dockerfile) e `docker run` direto do migrator com env-file (duplicaria a definição do serviço de maintenance — caminho paralelo ao canônico). Caveat do rollback ajustado no runbook: migrate antes do build do runner deixa o banco à frente do código se o build falhar depois (mesma classe do caveat atual — migrations append-only e revisadas). Testes: `tests/unit/deployScript.unit.spec.ts` com pin de ordem completo (migrator build < swap < migrate < runner build < rollout) + invariante novo do Dockerfile (estágio migrator nunca roda `next build`). Runbook `docs/ops/teqo-1313-deploy.md` atualizado (fluxo, caveat, falha conhecida nova). Dockerfile/AGENTS.md com a precisão build-after-migrate. + **Recently resolved (2026-08-18):** **OPS52** — a media saiu do Vercel Blob para o Garage S3 do homeserver (bucket `teqo-media` + key `teqo-key`, já existentes no Garage v2.1.0). O plugin `@payloadcms/storage-vercel-blob` (e o `@vercel/blob` do seed) foi trocado por `@payloadcms/storage-s3` na config, no padrão R2 da doc do Payload (`endpoint` + `forcePathStyle: true`, region `garage`). **Decisão de arquitetura (revisão A→B durante a execução, gate com o humano):** o Garage v2.1.0 **não suporta acesso anônimo na API S3** (sem `--public-read`; source: "when we support anonymous access in the future"; testado: `anonymous`/`anon` → `NoSuchAccessKey`), então o bucket fica **privado** e a app **serve a media por proxy** — sem `disablePayloadAccessControl`, o `staticHandler` do plugin streama o objeto do Garage em `/api/media/file/` (range + etag/304 nativos) — o **mesmo contrato de URL relativa que as 40 rows pré-existentes já têm** (a recuperação dos arquivos é a Issue #10/OPS52-media). **Fail-closed:** `resolveS3StorageEnv` (`src/utilities/mediaStorage.ts`, unit-testado) — envs `S3_*` parciais → throw no boot com a lista exata de faltantes; nenhuma → storage local (dev/test). Envs novas: `S3_BUCKET`, `S3_ENDPOINT`, `S3_REGION` (default `garage`), `S3_ACCESS_KEY_ID`, `S3_SECRET_ACCESS_KEY` (`.env.example`, `.env.test`, worktree.mjs com cópia all-or-nothing). Seed de posts: pre-delete do Blob removido (o S3 sobrescreve o key determinístico nativamente — idempotência intacta). Prova de contrato real: PUT/GET/DELETE via SDK contra o Garage (tailnet `http://100.119.220.31:3900`), depois media criada pelo Payload local com S3 ativo → `media.url` relativa + `GET /api/media/file/...` → 200 image/png; bucket devolvido a 0 objects e `.env.local` do worktree limpo (sem credenciais de prod). Sem migration (config-only; importMap regenerado sem `VercelBlobClientUploadHandler`). Runbook de cutover registrado no impl plan: setar as 5 `S3_*` no `~/stack/teqo-1313.env` (endpoint tailnet) antes do próximo deploy (OPS53). Repo: `docs/plans/ops52-blob-vercel-para-garage-impl.md`; infra-solla `STATE.md` atualizado à parte. **Recently resolved (2026-08-18):** **OPS52-media** — os objetos de media de produção (as ~40 capas dos posts, que existiam como rows com URL relativa `/api/media/file/` mas sem arquivo em lugar nenhum) são recuperáveis do WordPress de origem com `pnpm media:recover` (`scripts/recover-media.mjs`, novo): lê as rows de `media` + `post.coverImage` (mapeamento exato row→slug; fallback pelo filename determinístico `.`), resolve a capa com a MESMA lógica do seed (featured media do REST `_embed`, senão 1ª imagem inline do corpo — só 7/43 posts têm featured), baixa e faz `PutObject` no bucket das envs `S3_*` com key = `filename` da row, **sem nunca escrever no DB** (rows e URLs intocadas; o staticHandler do Payload passa a servir). Extraído para `scripts/lib/wpArticles.mjs` o fetch ao vivo do WordPress (seed-posts agora importa de lá — sem twin; unit-testado). Fail-closed: exige as 4 `S3_*` completas (`resolveS3StorageEnv` reusado; recuperação nunca vai pro storage local), `ALLOW_REMOTE_DB=true` explícito para DB remoto + echo do alvo (DB/bucket/endpoint), e timeouts curtos no SDK (`@smithy/node-http-handler`, 5s/15s) — endpoint inalcançável falha em segundos em vez de pendurar (a rota pública de media de prod pendurava >25s; evidência de possível falta de rota container→Garage, diagnosticada no runbook do impl plan). Modos: reconcile (padrão, upload idempotente por overwrite), `--dry-run` (plano + `headObject`), `--verify` (HEAD da URL pública por filename — o aceite da Issue, sem credencial). Runbook de execução no homeserver: `docs/plans/ops52-media-recuperar-arquivos-impl.md`. diff --git a/docs/changelog/2026-08-19-ops66.md b/docs/changelog/2026-08-19-ops66.md new file mode 100644 index 00000000..bc85e480 --- /dev/null +++ b/docs/changelog/2026-08-19-ops66.md @@ -0,0 +1 @@ +**Recently resolved (2026-08-19):** **OPS66** — deadlock de migrations em caminho estático: o deploy buildava o runner **antes** do migrate e, se uma migration criasse tabela lida em geração estática (caso S2, 2026-08-18: `social_feed_settings` na home), o `next build` falhava para sempre (`relation ... does not exist`) e o migrate nunca rodava — 4 deploys seguidos vermelhos até o migrator manual. Fix em `scripts/deploy-homeserver.sh` (reordenação de fases, sem custo novo — migrate e build do migrator já rodavam em todo deploy): **build do migrator** (o estágio não roda `next build` — builda contra o schema antigo) → push/tag do migrator → swap do compose (backup + trap na mesma posição) → **migrate** via `docker compose run teqo-1313-migrate` (caminho canônico do serviço de maintenance, agora com a imagem do SHA novo) → **build do runner** contra o banco JÁ migrado → push/tag → rollout → healthcheck → smoke. Alternativas rejeitadas: `payload migrate` dentro do build do runner (efeito colateral de escrita em prod a partir de um processo de build; contraria o desenho deliberado do Dockerfile) e `docker run` direto do migrator com env-file (duplicaria a definição do serviço de maintenance — caminho paralelo ao canônico). Caveat do rollback ajustado no runbook: migrate antes do build do runner deixa o banco à frente do código se o build falhar depois (mesma classe do caveat atual — migrations append-only e revisadas). Testes: `tests/unit/deployScript.unit.spec.ts` com pin de ordem completo (migrator build < swap < migrate < runner build < rollout) + invariante novo do Dockerfile (estágio migrator nunca roda `next build`). Runbook `docs/ops/teqo-1313-deploy.md` atualizado (fluxo, caveat, falha conhecida nova). Dockerfile/AGENTS.md com a precisão build-after-migrate. diff --git a/docs/ops/teqo-1313-deploy.md b/docs/ops/teqo-1313-deploy.md index 8d0eb115..8d4e4191 100644 --- a/docs/ops/teqo-1313-deploy.md +++ b/docs/ops/teqo-1313-deploy.md @@ -30,16 +30,19 @@ da suite/deploy (~15–25 min cada; tipicamente < 1 h no total). (ex.: dispatch duplicado), sai verde "already deployed" sem rebuild. 5. O script, no **homeserver**: guarda de HEAD (`main` remoto == SHA do job, senão skip) → `flock` (serializa) → guard "already deployed" (revision do - container) → build da imagem standalone + migrator (BuildKit, secrets do - `~/stack/teqo-1313.env`, `--network host` com proxy socat - `teqo-1313-build-proxy` na `stack_default` para o build alcançar o - `postgres` — o proxy é criado idempotentemente pelo script) → push em - `localhost:5000` → swap dos tags de imagem no `~/stack/docker-compose.yml` - (backup antes) → **migrations** - (`docker compose --profile maintenance run --rm teqo-1313-migrate -rebuild`) e `docker compose up -d`, ou espere um commit novo | -| Gate `gate` vermelho (erro de git/`.dockerignore`) | SHA deployado ilegível para o git (ex.: história reescrita) ou `.dockerignore` corrompido no checkout | Fail-open: suite roda; deploy pula (nunca deploy às cegas). Corrigir a causa e esperar a próxima janela (auto-cura). Se `ssh homeserver` estiver fora, o gate lê revision vazia e roda tudo (verde) — checar tailnet se a suite roda sem deploy recorrente | -| Sem revision no container (label ausente no compose) | O compose do homeserver não define `org.opencontainers.image.revision` (deploy pré-OPS53 ou edição manual) | Degradação conservadora: toda janela roda suite + rebuild (verde). Corrigir adicionando o label ao serviço `teqo-1313` no compose e re-deployando | -| Todo job docker quebra com `exec: "node": executable file not found in $PATH` | `runner.envs.PATH` do `~/.forgejo-runner/config.yaml` (workstation) com paths só de host (nvm/bun) — o container perde o node do toolcache (`/opt/acttoolcache`); edição do config exige o toolcache primeiro (ver comentário no próprio config) | Corrigir o PATH no config (toolcache primeiro — ver comentário no próprio arquivo) + `systemctl --user restart forgejo-runner` (incidente 2026-08-17) | -| 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 que vai no pipe do `bash -s`; 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 | -| `checkout@v5` falha no job `host` | Label host do act_runner | Fallback: baixar o script direto do Forgejo (`curl -s https://git.solla.dev/fsolla/teqo/raw/branch/main/scripts/deploy-homeserver.sh`) | -| 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 | +| Sintoma | Causa | Tratamento | +| ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Job verde sem deploy ("stale run") | Push mais novo venceu; o deploy antigo saiu da fila | Nada a fazer — o run do SHA novo deploya | +| 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: teqo-1313:-rebuild`) e `docker compose up -d`, ou espere um commit novo | +| Gate `gate` vermelho (erro de git/`.dockerignore`) | SHA deployado ilegível para o git (ex.: história reescrita) ou `.dockerignore` corrompido no checkout | Fail-open: suite roda; deploy pula (nunca deploy às cegas). Corrigir a causa e esperar a próxima janela (auto-cura). Se `ssh homeserver` estiver fora, o gate lê revision vazia e roda tudo (verde) — checar tailnet se a suite roda sem deploy recorrente | +| Sem revision no container (label ausente no compose) | O compose do homeserver não define `org.opencontainers.image.revision` (deploy pré-OPS53 ou edição manual) | Degradação conservadora: toda janela roda suite + rebuild (verde). Corrigir adicionando o label ao serviço `teqo-1313` no compose e re-deployando | +| Todo job docker quebra com `exec: "node": executable file not found in $PATH` | `runner.envs.PATH` do `~/.forgejo-runner/config.yaml` (workstation) com paths só de host (nvm/bun) — o container perde o node do toolcache (`/opt/acttoolcache`); edição do config exige o toolcache primeiro (ver comentário no próprio config) | Corrigir o PATH no config (toolcache primeiro — ver comentário no próprio arquivo) + `systemctl --user restart forgejo-runner` (incidente 2026-08-17) | +| 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 que vai no pipe do `bash -s`; 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 | +| `checkout@v5` falha no job `host` | Label host do act_runner | Fallback: baixar o script direto do Forgejo (`curl -s https://git.solla.dev/fsolla/teqo/raw/branch/main/scripts/deploy-homeserver.sh`) | +| 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 | ## Segurança (decisão deliberada) diff --git a/docs/plans/ops66-deploy-build-le-schema-novo-antes-da-migrate-impl.md b/docs/plans/ops66-deploy-build-le-schema-novo-antes-da-migrate-impl.md new file mode 100644 index 00000000..f4430760 --- /dev/null +++ b/docs/plans/ops66-deploy-build-le-schema-novo-antes-da-migrate-impl.md @@ -0,0 +1,73 @@ +# Impl: OPS66 — deploy: build de geração estática lê schema novo antes do passo migrate (deadlock de migrations em caminho estático) + +Status: aprovado +Atualizado em: 2026-08-19 +Issue: #68 +Intenção: docs/plans/ops66-deploy-build-le-schema-novo-antes-da-migrate.md +Appetite restante: herdado (~0,5 dia) + +## Leitura da intenção + +- **Outcome:** nenhuma migration nova pode mais travar o deploy para sempre por criar tabela lida em geração estática — o schema novo chega ao build do runner antes do `next build`. +- **O que NÃO negociar:** ordem migrate antes do build; nada de auto-migrate dentro da imagem do runner (o Dockerfile chama `next build` direto de propósito); não tocar migrations já aplicadas; não mudar arquitetura de geração estática. +- **O que reavaliar:** COMO rodar o migrate antes do build — o serviço canônico `teqo-1313-migrate` do compose depende do swap dos tags de imagem, que hoje acontece só depois do build. A intenção sugere "rodar o migrate antes do build" sem fixar o mecanismo. + +## Abordagem recomendada + +Reordenar as fases do `scripts/deploy-homeserver.sh`: o migrator (estágio que NÃO roda `next build` — não depende do schema) builda primeiro; o swap do compose (com backup) e o migrate via serviço de maintenance acontecem antes do build do runner. + +```mermaid +flowchart LR + G[HEAD guard + flock + already-deployed] --> B[Migrator: build + push + tag] + B --> S[compose swap com backup — trap de rollback] + S --> M[migrate: compose run teqo-1313-migrate — imagem nova] + M --> R[Runner: build + push + tag — contra o banco migrado] + R --> U[rollout + healthcheck + smoke] +``` + +**Opções consideradas:** A | B | C +**Recomendação:** A — reordenar o script movendo o migrate para antes do build do runner, mantendo o `compose run teqo-1313-migrate` como caminho canônico (o swap passa a ser feito logo após o build do migrator). É a opção 1 da própria intenção, sem custo adicional (o migrate e o build do migrator já rodavam em todo deploy — só mudam de posição) e sem caminho paralelo ao serviço de maintenance. +**Rejeitadas:** + +- **B — `payload migrate` pré-check dentro do build do runner:** efeito colateral de escrita no banco de prod a partir de um processo de build; contradiz o desenho deliberado do Dockerfile ("migrations run separately through the migrator") e a decisão (B) do OPS53. Qualquer build CI em ambiente errado migraria o banco de prod. +- **C — `docker run --rm --network stack_default --env-file ~/stack/teqo-1313.env localhost:5000/teqo-1313-migrator:` antes do build do runner** (o comando manual de recuperação do incidente): evita o swap precoce, mas duplica a definição do serviço de maintenance do compose (envs/networks/labels) e cria caminho paralelo ao serviço canônico que OPS51/53 e o runbook documentam. O swap precoce (A) é inofensivo: nada referencia o tag do runner até o `compose up -d`, que só roda depois do push. + +### Componentes / mudanças + +- **`scripts/deploy-homeserver.sh`**: mover o bloco `build_image migrator` + push/tag do migrator para ANTES do swap; mover o bloco swap (backup + sed + greps + trap) para antes do migrate; mover `build_image runner` + push/tag do runner para DEPOIS do migrate. Comentário no passo migrate explicando o porquê (OPS66). +- **`tests/unit/deployScript.unit.spec.ts`**: pin de ordem completo — migrator build < swap < migrate < runner build < rollout (substitui o pin atual migrate < rollout). +- **`docs/ops/teqo-1313-deploy.md`**: fluxo do passo 5 reescrito; caveat do rollback ajustado ("migrate roda antes do build do runner"); nova linha em Falhas conhecidas: build falha com `relation ... does not exist` (sintoma do incidente S2). +- **`Dockerfile`**: estender o comentário do estágio `builder` (migrations são aplicadas pelo deploy antes deste build — OPS66). +- **`AGENTS.md`**: precisão de uma linha no parágrafo do deploy (migrations aplicadas antes do build do runner). +- **Migration:** sem migration. +- **Access / Consent:** sem mudança. +- **UI:** sem mudança. + +### Dados → forma + +n/a (ops script). + +## Fases verificáveis + +1. **Script + teste** — reordenar `deploy-homeserver.sh`; atualizar `deployScript.unit.spec.ts` com os pins de ordem; `bash -n` + unit verde. +2. **Docs** — runbook (fluxo + caveat + falha conhecida), comentário no Dockerfile, AGENTS.md, changelog (`pnpm changelog:build` + `changelog:check`). +3. **Gates** — `pnpm gate:fast` na iteração; `pnpm push` para a entrega. + +## Rabbit holes / Não escopo (engenharia) + +- Não alterar o Dockerfile além do comentário (estágio migrator/builder ficam como estão). +- Não mexer no `.forgejo/workflows/ci.yml` (job deploy continua streamando o script — a mudança é toda no script). +- Não criar infra nova no homeserver (compose, proxy socat, service de maintenance ficam como estão). +- Não reescrever o histórico de changelog/AGENTS além da entrada nova + precisão da linha do deploy. + +## Riscos e mitigação + +- **Forward migration em build falho:** migrate antes do build do runner — se o build falhar depois do migrate, o banco fica à frente do código rodando. Mesma classe do caveat atual (rollout falho pós-migrate); migrations são append-only e revisadas antes do deploy (checklist AGENTS). Documentado no runbook. +- **Compose órfão em kill duro entre swap e push:** o compose referencia `teqo-1313:` ainda não existente. Backup `docker-compose.yml.pre-$SHA` existe; o próximo deploy re-swapa; restore manual documentado no runbook. Mesma classe do hazard atual (kill entre swap e rollout). +- **Migrate sem migrations pendentes:** `payload migrate` vira no-op (~1–2 min) — custo que já existia em todo deploy, só deslocado. + +## Aceite de engenharia + +- [x] Aceite de produto da intenção ainda coberto (migrate antes do build do runner) +- [x] Invariantes AGENTS/engineering-standards (sem caminho paralelo: compose run segue canônico; sem auto-migrate no build) +- [x] Testes previstos (unit pin de ordem no deployScript.unit.spec.ts) diff --git a/scripts/deploy-homeserver.sh b/scripts/deploy-homeserver.sh index 00ebe972..135a93e4 100644 --- a/scripts/deploy-homeserver.sh +++ b/scripts/deploy-homeserver.sh @@ -7,12 +7,14 @@ # # Flow: HEAD guard (only the current main HEAD deploys) -> flock # serialization -> workspace fetch at -> docker login (local registry) -# -> build of the runner + migrator stages (BuildKit secrets, compose -# network) -> push to localhost:5000 -> compose image-tag swap (with backup) -# -> migrate via the maintenance service (BEFORE the rollout) -> compose up -# -> healthcheck wait -> smoke. Any failure after the swap rolls back to the -# previous compose + image; failures before it leave the running site -# untouched and the job red. +# -> build of the MIGRATOR stage (it never runs `next build`, so it builds +# even against the old schema) -> push/tag of the migrator -> compose +# image-tag swap (with backup) -> migrate via the maintenance service +# (BEFORE the runner build — static generation reads the NEW schema, OPS66) +# -> build of the runner stage (BuildKit secrets, compose network) -> +# push/tag -> compose up -> healthcheck wait -> smoke. Any failure after the +# swap rolls back to the previous compose + image; failures before it leave +# the running site untouched and the job red. # # Environment defaults assume the homeserver layout: stack under # $HOME/stack, repo cloned from the local Forgejo. Secrets are sourced from @@ -130,15 +132,14 @@ build_image() { } cd "$WORKSPACE_DIR" -build_image runner "localhost:5000/teqo-1313:$SHA" + +# The migrator stage never runs `next build`, so it builds fine against the +# OLD schema — unlike the runner, whose static generation reads Payload data +# and therefore needs the migrations applied first (OPS66). build_image migrator "localhost:5000/teqo-1313-migrator:$SHA" -say "pushing images to localhost:5000" -docker push "localhost:5000/teqo-1313:$SHA" -docker push "localhost:5000/teqo-1313-migrator:$SHA" - # The compose references bare `teqo-1313:` tags with pull_policy: never. -docker tag "localhost:5000/teqo-1313:$SHA" "teqo-1313:$SHA" +docker push "localhost:5000/teqo-1313-migrator:$SHA" docker tag "localhost:5000/teqo-1313-migrator:$SHA" "teqo-1313-migrator:$SHA" # --- compose swap (backup first; failures from here roll back) ---------- @@ -163,7 +164,12 @@ rollback() { } trap 'rollback "unexpected failure"' ERR -# --- migrate (before the rollout) --------------------------------------- +# --- migrate (BEFORE the runner build — OPS66) -------------------------- +# The runner build statically generates pages that read Payload data; a +# migration that creates a table a static route reads would otherwise +# deadlock the deploy (build fails -> migrate never runs -> build fails...). +# The migrator image above is already swapped into the compose, so this +# maintenance service runs the migrations of the NEW sha against prod. cd "$STACK_DIR" say "applying pending migrations (maintenance service teqo-1313-migrate)" @@ -172,8 +178,17 @@ say "applying pending migrations (maintenance service teqo-1313-migrate)" # exits right after the migrate step, skipping the rollout). docker compose --profile maintenance run --rm teqo-1313-migrate { expect(script).toContain('127.0.0.1:5433') }) - it('applies migrations through the maintenance service before the rollout', () => { + it('applies migrations BEFORE the runner build — static generation reads the new schema (OPS66)', () => { + // OPS66: a migration that creates a table a static route reads used to + // deadlock the deploy (runner build failed -> migrate never ran -> build + // failed again). The migrator stage never runs `next build`, so it must + // build first; the compose swap feeds the new migrator image to the + // maintenance service; then migrate runs; only then the runner builds + // against the migrated DB. + const migratorBuildIndex = script.indexOf('build_image migrator') + const swapIndex = script.indexOf('compose swap') + const migrateIndex = script.indexOf('teqo-1313-migrate') + const runnerBuildIndex = script.indexOf('build_image runner') // The rollback helper also contains a `compose up -d` (before the migrate // step); the REAL rollout is the last occurrence. - const migrateIndex = script.indexOf('teqo-1313-migrate') const upIndex = script.lastIndexOf('docker compose up -d teqo-1313') - expect(migrateIndex).toBeGreaterThan(-1) - expect(upIndex).toBeGreaterThan(-1) - expect(migrateIndex).toBeLessThan(upIndex) + for (const i of [migratorBuildIndex, swapIndex, migrateIndex, runnerBuildIndex, upIndex]) { + expect(i).toBeGreaterThan(-1) + } + expect(migratorBuildIndex).toBeLessThan(swapIndex) + expect(swapIndex).toBeLessThan(migrateIndex) + expect(migrateIndex).toBeLessThan(runnerBuildIndex) + expect(runnerBuildIndex).toBeLessThan(upIndex) }) it('smokes the deployed surface with the prod revalidate secret', () => { @@ -76,4 +89,18 @@ describe('scripts/deploy-homeserver.sh (OPS53 deploy pipeline)', () => { expect(script).toContain('echo "$REGISTRY_PASSWORD" | docker login') expect(script).not.toContain('--password "$REGISTRY_PASSWORD"') }) + + it('Dockerfile: the migrator stage never runs `next build` (it builds against the old schema)', () => { + // The invariant OPS66 depends on: the migrator image can always be built, + // even before the new migrations exist in the DB. + const dockerfile = readFileSync(join(repoRoot, 'Dockerfile'), 'utf8') + const migratorStage = dockerfile.slice( + dockerfile.indexOf('AS migrator'), + dockerfile.indexOf('AS builder'), + ) + expect(migratorStage).toContain('CMD ["pnpm", "migrate"]') + expect(migratorStage).not.toContain('next build') + const builderStage = dockerfile.slice(dockerfile.indexOf('AS builder')) + expect(builderStage).toContain('next build') + }) })