ops(OPS58): seed de posts com dry-run e taxonomy atualizada — sync de prod com runbook
All checks were successful
Agent PR Ready + auto-merge / ready-automerge (pull_request) Has been skipped
CI (PR) / docs-guards (pull_request) Successful in 9s
CI (PR) / scope (pull_request) Successful in 51s
CI (PR) / plans-only-closes (pull_request) Successful in 11s
CI (PR) / typecheck (pull_request) Has been skipped
CI (PR) / knip (pull_request) Has been skipped
CI (PR) / cycles (pull_request) Has been skipped
CI (PR) / unit (pull_request) Has been skipped
CI (PR) / lint (pull_request) Successful in 1m22s
CI (PR) / format (pull_request) Successful in 1m37s
CI (PR) / int (pull_request) Has been skipped
CI (PR) / build (pull_request) Has been skipped
CI (PR) / e2e (1) (pull_request) Has been skipped
CI (PR) / e2e (2) (pull_request) Has been skipped
CI (PR) / checks (pull_request) Successful in 2s
All checks were successful
Agent PR Ready + auto-merge / ready-automerge (pull_request) Has been skipped
CI (PR) / docs-guards (pull_request) Successful in 9s
CI (PR) / scope (pull_request) Successful in 51s
CI (PR) / plans-only-closes (pull_request) Successful in 11s
CI (PR) / typecheck (pull_request) Has been skipped
CI (PR) / knip (pull_request) Has been skipped
CI (PR) / cycles (pull_request) Has been skipped
CI (PR) / unit (pull_request) Has been skipped
CI (PR) / lint (pull_request) Successful in 1m22s
CI (PR) / format (pull_request) Successful in 1m37s
CI (PR) / int (pull_request) Has been skipped
CI (PR) / build (pull_request) Has been skipped
CI (PR) / e2e (1) (pull_request) Has been skipped
CI (PR) / e2e (2) (pull_request) Has been skipped
CI (PR) / checks (pull_request) Successful in 2s
- scripts/seed-posts.mjs: modo --dry-run (plan-only, echo do alvo, zero escrita) e 4 slugs novos no CLASSIFICATION (3 com tag eleitoral — lock de visibilidade eleitoral do repo) - docs: impl plan com runbook do homeserver, changelog, nota AGENTS.md
This commit is contained in:
parent
71cf477f6e
commit
3a02b844e5
5 changed files with 325 additions and 10 deletions
|
|
@ -60,7 +60,7 @@ The public news/publications system is backed by two collections (both in the `P
|
|||
|
||||
**Self-revalidation on admin edits.** Both collections have an `afterChange` hook. `post` busts its own document tag + the `posts` listing on every publish/update (skipping only the initial draft that the admin create view generates during render). `tag` is broader: on change it re-reads every post referencing that tag (as `category` or in `tags`, passing `req` for transaction safety) and revalidates each one plus the listing — so flipping `hidden` is reflected on the site immediately.
|
||||
|
||||
**Seeding news content (`pnpm db:seed:posts`).** `scripts/seed-posts.mjs` (loaded via `scripts/seed-loader.mjs`) fetches ~39 articles live from jorgesolla.com.br (WordPress REST API, with an HTML-crawl fallback), imports them as `type: 'noticia'` published posts, converts the WP HTML body to Payload Lexical, and uploads cover images through the configured media storage (Garage S3 when the `S3_*` envs are set — the adapter overwrites the deterministic `<slug>.<ext>` key natively, no pre-delete needed; local disk otherwise). The taxonomy (categories + the `eleitoral` control tag, and the per-slug classification) is hardcoded in the script; it reads/writes the `hidden` flag. It is **idempotent by slug** (existing posts are skipped; tags/media are reused by slug/filename) and reuses the same non-local `DATABASE_URL` guard as `pnpm dev` (override only with `ALLOW_REMOTE_DB=true`).
|
||||
**Seeding news content (`pnpm db:seed:posts`).** `scripts/seed-posts.mjs` (loaded via `scripts/seed-loader.mjs`) fetches ~43 articles live from jorgesolla.com.br (WordPress REST API, with an HTML-crawl fallback), imports them as `type: 'noticia'` published posts, converts the WP HTML body to Payload Lexical, and uploads cover images through the configured media storage (Garage S3 when the `S3_*` envs are set — the adapter overwrites the deterministic `<slug>.<ext>` key natively, no pre-delete needed; local disk otherwise). The taxonomy (categories + the `eleitoral` control tag, and the per-slug classification) is hardcoded in the script; it reads/writes the `hidden` flag. It is **idempotent by slug** (existing posts are skipped; tags/media are reused by slug/filename) and reuses the same non-local `DATABASE_URL` guard as `pnpm dev` (override only with `ALLOW_REMOTE_DB=true`). `pnpm db:seed:posts --dry-run` is the plan-only pre-flight (fetches, resolves covers, reports what WOULD be created — no writes). The OPS58 runbook documents the production sync (post-merge, homeserver, stack env, dry-run → sync → bust `posts` cache → verify) — copy it for future syncs (see `docs/plans/ops58-sincronizar-posts-prod-impl.md`).
|
||||
|
||||
### Revalidating after a manual/direct DB change
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,10 @@ Conteúdo movido do `AGENTS.md` em 2026-07-30 (fatiamento do paradigma de agente
|
|||
|
||||
**Recently resolved (2026-08-18):** **OPS56**: o `forgejo-pr-automerge` nunca mais reporta falso positivo "já mergeado" com PR aberto. Raiz confirmada no source do servidor (Forgejo 9.0.3, tag v9.0.3): o endpoint `POST /pulls/{n}/merge` responde **200 com corpo vazio** no sucesso (falhas são 404/405/409/500), e o `request()` do `forgejo-api.mjs` convertia isso em `null` — o `autoMerge()` devolvia `null` cru e o CLI imprimia "já mergeado" (exit 0) independente do estado real do PR; o incidente (PR #27 da OPS52, aberto das 01:59 às 02:45) deixou o merge falhar silenciosamente com o PR aberto. Correção: o veredito de `autoMerge` agora é **re-read do PR** após a tentativa (`{ attempted, merged, pr }`) — 200-vazio → confirma no `GET /pulls/{n}`; POST falhou e o PR segue aberto → **re-throw** (falha ruidosa, exit 1); raça perdida (outro ator mergeou) → "já mergeado" só com o PR realmente mergeado; PR fechado sem merge → "fechado sem merge — skip" honesto. `waitForChecks` passou a esperar o `mergeable` amadurecer (false → throw imediato de conflito; null → segue no poll), mesmo timeout de 30 min. CLI sem shadowing de `pr` (const redeclarada). Testes: 6 specs novas em `forgejoApi.unit.spec.ts` (veredito por re-read, POST 405 + PR aberto → reject, raça concorrente, no-op de já-mergeado, poll de mergeable null, fail-fast de mergeable false). Gates: 2318 unit + 791 int + tsc/lint/prettier/knip/check:cycles verdes. Débito deferido: alargar o gatilho do `agent-pr-ready-automerge.yml` para branches não-`cursor/*` (decisão de ops separada).
|
||||
|
||||
**Recently resolved (2026-08-18):** **OPS58** — o seed de posts de produção foi executado com o runbook documentado: `scripts/seed-posts.mjs` ganhou o modo **`--dry-run`** (plan-only: fetch ao vivo + echo do alvo + relatório do que SERIA criado/saltado, zero escrita — pre-flight padrão OPS52-media antes de tocar o DB de prod; argumento desconhecido morre) e a **taxonomy `CLASSIFICATION` foi atualizada** com os 4 slugs novos do jorgesolla.com.br (43 posts no WP vs 39 mapeados): os 3 de campanha/pré-campanha (convenção estadual do PT, plenária "PT tem que disputar o programa de governo", caravana da Chapada) entraram com `tags: ['eleitoral']` para ficarem sob o controle de visibilidade eleitoral do repo (`isPostVisible` fail-closed), e o embate com Bruno Reis em `politica`. Execução em prod (homeserver, env do stack, `ALLOW_REMOTE_DB=true`): dry-run → sync (create-only, idempotente por slug) → bust do cache `posts` via `/api/revalidate` → verify (`media:recover --verify` para capas + curl nas páginas dos novos). Runbook para as próximas sincronizações: `docs/plans/ops58-sincronizar-posts-prod-impl.md`.
|
||||
|
||||
**Recently resolved (2026-08-18):** **OPS58** — o sync de posts de produção ficou pronto para executar com runbook documentado: `scripts/seed-posts.mjs` ganhou o modo **`--dry-run`** (plan-only: fetch ao vivo + echo do alvo + relatório do que SERIA criado/saltado — zero escrita, pre-flight padrão OPS52-media antes de tocar o DB de prod; argumento desconhecido morre) e a **taxonomy `CLASSIFICATION` foi atualizada** com os 4 slugs novos do jorgesolla.com.br (43 posts no WP vs 39 mapeados): os 3 de campanha/pré-campanha (convenção estadual do PT, plenária "PT tem que disputar o programa de governo", caravana da Chapada) entraram com `tags: ['eleitoral']` para ficarem sob o controle de visibilidade eleitoral do repo (`isPostVisible` fail-closed — critério do tag documentado no seed), e o embate com Bruno Reis em `politica`. A execução em prod (pós-merge, homeserver, env do stack, `ALLOW_REMOTE_DB=true`) segue o runbook: dry-run → sync (create-only, idempotente por slug) → bust do cache `posts` via `/api/revalidate` → verify (`media:recover --verify` para capas + curl nas páginas dos novos). Runbook para as próximas sincronizações: `docs/plans/ops58-sincronizar-posts-prod-impl.md`.
|
||||
|
||||
**Recently resolved (2026-08-17):** **OPS51** — o banco de produção saiu do Neon para o Postgres do homeserver (`teqo_1313`, container `teqo-1313`), sem mudança de config: o `~/stack/teqo-1313.env` já apontava para o banco preparado (criado 16/08 com role própria + cadeia completa de 56 migrations + zero conteúdo — verificado), então restaurar = a produção "apareceu" no container. **Divergência deliberada da intenção (questão A/B validada como B):** rejeitado criar `teqo_prod` novo — o argumento "o banco do 1313 tem vida própria" não se sustenta (o site 1313 É a produção deste repo) e o OPS53 já trata `teqo_1313` como produção. Execução 100% no homeserver (PII nunca tocou a workstation): URL do Neon puxada do Vercel (`vercel env pull`, projeto `jorgesolla`, `DATABASE_URL_UNPOOLED` → `NEON_DATABASE_URL` em `~/stack/.env` chmod 600), `pg_dump -Fc` (Neon PG 17.10) → `/srv/hdd/backups/teqo-neon-pre-migracao/teqo-neon-full-20260817-204800.dump` (5,8 MB, sha256 `f12196f2…`), `pg_restore --no-owner --no-privileges` como a role `teqo_1313` (drop schema + grant, padrão do `db-pull`), verificado: contagens por tabela idênticas ao Neon (1919 contatos, 39 posts, 1487 assinaturas, 1484 subscrições, 35 campaign_users, 392 lideranças, 2 pledges, 435 municípios, 6 atividades; supporter segue 0 — blocker LGPD ainda de pé), sequences no topo (`last_value == max(id)`), objetos com dono `teqo_1313`, `payload migrate` no-op (56 = 56, serviço de maintenance `teqo-1313-migrate`), 3 credenciais WebAuthn + 34 sessions + 5 chaves Consent restauradas (RP ID não mudou — biometria segue valendo). Smoke sem deploy: `/`, artigo com conteúdo restaurado, `/campanha/login`, `/admin`, `POST /api/revalidate` (`posts` + `global_privacy-policy`), WebAuthn `login-options`, barreira 307 sem sessão — verdes; smoke de browser (login, biometria, convites) validado manualmente. **Rollback documentado:** Neon nunca foi escrito (só lido); voltar = re-apontar `DATABASE_URL` do `teqo-1313.env` para o Neon + `compose up -d`. **Débito registrado (→ OPS52):** as 40 rows de `media` têm URLs relativas (`/api/media/file/…`) sem arquivos em lugar nenhum — capas dão 500 no homeserver e **404 no próprio Vercel/produção atual** (estado pré-existente; migração manteve paridade). Repo: plano da intenção concluído, `AGENTS.md` e help de `db:pull`/`check-push-chain` sem a referência ao Neon; infra-solla `STATE.md` atualizado à parte.
|
||||
|
||||
**Recently resolved (2026-08-17):** **OPS53** — o deploy do site 1313 virou etapa do CI: mergir em `main` com a suite verde == site atualizado em `jorgesolla1313.com.br`, sem passo manual. O job `deploy` em `.forgejo/workflows/ci.yml` roda no host do runner (workstation, `runs-on: host`) e streama `scripts/deploy-homeserver.sh` para o homeserver via SSH (`ssh homeserver "bash -s -- <sha>"`); o script remoto (bash puro, homeserver sem dependências novas) faz: guard de HEAD (`git ls-remote` de `main` == SHA do job, senão skip "stale"), `flock` de serialização, workspace `~/teqo-deploy` (clone do Forgejo local), `docker login localhost:5000` (REGISTRY_USER/PASSWORD de `~/stack/.env`), build dos stages `runner`+`migrator` no homeserver (`--network stack_default` para a geração estática alcançar o `postgres:5432` do compose; secrets de `~/stack/teqo-1313.env` via BuildKit, sem camadas/logs), push no registry local, swap dos tags de imagem no compose (com backup `docker-compose.yml.pre-<sha>`), **migrate pelo serviço de maintenance `teqo-1313-migrate` antes do rollout**, `docker compose up -d`, espera do healthcheck e smoke (`/`, `/campanha/login`, `/admin`, barreira 307, WebAuthn `login-options` com body JSON, `POST /api/revalidate` com o secret real — rehearsal validado contra o site vivo). Falha pós-swap → rollback automático best-effort (restore do backup + `up -d` da imagem anterior) + job vermelho. **Divergência deliberada da intenção (fato novo):** a intenção assumia o runner no homeserver; verificado (`infra-solla/STATE.md` + ao vivo) que o runner é a `workstation-runner` na workstation e o stack de produção vive no homeserver — a abordagem A foi corrigida para "job `runs-on: host` na workstation + todo o deploy no homeserver via SSH", o que manteve **zero secrets novos no Forgejo** e zero mudança de infra no compose. Nenhum secret no repo; envs seguem chmod-600 no homeserver. Runbook (rollback manual, falhas conhecidas, decisão de segurança do host label): `docs/ops/teqo-1313-deploy.md` (novo). Guarda estrutural nova: `tests/unit/deployScript.unit.spec.ts` (bash -n + invariantes do pipeline: HEAD guard, BuildKit secrets, ordem migrate→rollout, smoke). AGENTS.md/AGENT-OPS.md atualizados. Primeiro deploy real = o merge deste próprio item (tracer bullet); infra-solla `STATE.md` anotado à parte.
|
||||
|
|
|
|||
1
docs/changelog/2026-08-18-ops58.md
Normal file
1
docs/changelog/2026-08-18-ops58.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
**Recently resolved (2026-08-18):** **OPS58** — o sync de posts de produção ficou pronto para executar com runbook documentado: `scripts/seed-posts.mjs` ganhou o modo **`--dry-run`** (plan-only: fetch ao vivo + echo do alvo + relatório do que SERIA criado/saltado — zero escrita, pre-flight padrão OPS52-media antes de tocar o DB de prod; argumento desconhecido morre) e a **taxonomy `CLASSIFICATION` foi atualizada** com os 4 slugs novos do jorgesolla.com.br (43 posts no WP vs 39 mapeados): os 3 de campanha/pré-campanha (convenção estadual do PT, plenária "PT tem que disputar o programa de governo", caravana da Chapada) entraram com `tags: ['eleitoral']` para ficarem sob o controle de visibilidade eleitoral do repo (`isPostVisible` fail-closed — critério do tag documentado no seed), e o embate com Bruno Reis em `politica`. A execução em prod (pós-merge, homeserver, env do stack, `ALLOW_REMOTE_DB=true`) segue o runbook: dry-run → sync (create-only, idempotente por slug) → bust do cache `posts` via `/api/revalidate` → verify (`media:recover --verify` para capas + curl nas páginas dos novos). Runbook para as próximas sincronizações: `docs/plans/ops58-sincronizar-posts-prod-impl.md`.
|
||||
241
docs/plans/ops58-sincronizar-posts-prod-impl.md
Normal file
241
docs/plans/ops58-sincronizar-posts-prod-impl.md
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
# Impl: OPS58 — Atualizar artigos de produção com os novos posts do jorgesolla.com.br
|
||||
|
||||
Status: aprovado (gate humano 2026-08-18)
|
||||
Atualizado em: 2026-08-18
|
||||
Issue: #43
|
||||
Intenção: docs/plans/ops58-sincronizar-posts-prod.md
|
||||
Appetite restante: herdado (~0,5 dia — execução no homeserver + runbook)
|
||||
|
||||
## Leitura da intenção
|
||||
|
||||
- **Outcome:** todos os artigos do jorgesolla.com.br ainda ausentes em produção
|
||||
criados como notícia publicada; os existentes intocados (create-only, sem
|
||||
duplicata); capas dos novos servindo 200 em `/api/media/file/...`; cache
|
||||
`posts` bustado; procedimento documentado como runbook (padrão OPS52-media).
|
||||
- **O que NÃO negociar:** create-only (nunca edita/sobrescreve conteúdo
|
||||
existente); PII intocada; contrato público de URL intocado; guard
|
||||
`ALLOW_REMOTE_DB=true` explícito (fail-closed mantido); capas dos novos no
|
||||
bucket de prod (env `S3_*` do stack obrigatório — nunca disco local).
|
||||
- **O que reavaliar:** a hipótese "ferramenta existe, só executar" precisa de
|
||||
dois ajustes — (1) o seed não tem modo **plan-only** (pre-flight de prod
|
||||
inexistente; o precedente OPS52-media provou o valor de `--dry-run` no
|
||||
homeserver); (2) a taxonomy hardcoded está **defasada**: o WP tem 43 posts e
|
||||
o `CLASSIFICATION` cobre 39 — os 4 slugs novos incluem 3 de
|
||||
pré-campanha/campanha que, sem a entrada, caem em `politica` **sem o tag
|
||||
`eleitoral`**, furando o controle de visibilidade eleitoral do repo.
|
||||
|
||||
## Evidência coletada (2026-08-18, probes read-only)
|
||||
|
||||
- **E1 — Contagem do WP:** `x-wp-total: 43` (REST `_embed`, paginado —
|
||||
`fetchArticlesFromWordPress` traz todos). `CLASSIFICATION` cobre 39 slugs →
|
||||
**4 slugs novos não mapeados** (caem no default `politica` hoje):
|
||||
- `bruno-reis-foi-covarde-detonou-solla-ao-rebater-o-prefeito-sobre-saude-de-salvador` (2026-08-17 — resposta política a ataque)
|
||||
- `chapada-diamantina-recebe-caravana-liderada-pelo-deputado-jorge-solla` (2026-07-31 — caravana, pré-campanha)
|
||||
- `lula-reforca-o-time-de-jeronimo-wagner-rui-e-jorge-solla-na-convencao-estadual-do-pt` (2026-08-02 — convenção estadual do PT, campanha)
|
||||
- `o-pt-tem-que-disputar-o-programa-de-governo-conclamou-jorge-solla-em-plenaria` (2026-08-11 — plenária, campanha)
|
||||
- **E2 — Risco do lock `eleitoral`:** os 3 de campanha acima sem tag
|
||||
`eleitoral` ficam visíveis no site público mesmo se a equipe puxar o
|
||||
controle `hidden` na janela eleitoral (lock do repo:
|
||||
`isPostVisible` fail-closed esconde só posts com tag/categoria `eleitoral`).
|
||||
- **E3 — Seed sem pre-flight:** `seed-posts.mjs` não ecoa o alvo (host do DB)
|
||||
e não tem modo que planeje sem escrever; `recover-media.mjs` (OPS52) tem
|
||||
ambos — padrão a espelhar (echo do alvo antes de qualquer escrita; modos
|
||||
mutuamente exclusivos; argumento desconhecido morre).
|
||||
- **E4 — Homeserver:** clone do repo em `~/teqo-deploy` (OPS53 — deploy doc;
|
||||
o deploy deixa o clone em **detached HEAD** no SHA do job). Env do stack em
|
||||
`~/stack/teqo-1313.env` com `DATABASE_URL=…@postgres:5432` — o `postgres`
|
||||
só resolve **dentro** da rede `stack_default`; do host, o caminho comprovado
|
||||
é o **proxy socat `teqo-1313-build-proxy`** que o deploy script cria
|
||||
idempotentemente (publicado em `127.0.0.1:5433` no host — é exatamente como
|
||||
o build o usa, `deploy-homeserver.sh:91-108`). O `S3_ENDPOINT=host.docker.internal:3900`
|
||||
do stack só resolve dentro do compose; o Garage roda **no host** (o container
|
||||
alcança via `host-gateway`) → do host, `S3_ENDPOINT=http://127.0.0.1:3900`
|
||||
(a workstation/tailnet usa `http://100.119.220.31:3900`). A forma exata de
|
||||
execução do OPS52-media não está registrada de forma consistente entre os
|
||||
docs (o runbook dela menciona `cd ~/stack`; o deploy doc diz workspace
|
||||
`~/teqo-deploy`) — o pre-flight do runbook abaixo valida a forma escolhida,
|
||||
e a primeira execução registra o que funcionou na Issue.
|
||||
|
||||
## Abordagem recomendada
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[seed-posts.mjs<br/>+ CLASSIFICATION 4 slugs] -->|pnpm db:seed:posts --dry-run| B[pre-flight prod<br/>plano sem escrita]
|
||||
B -->|ok| C[ALLOW_REMOTE_DB=true<br/>pnpm db:seed:posts] --> D[posts novos + capas<br/>Garage S3 prod]
|
||||
D -->|POST /api/revalidate?tag=posts| E[bust cache posts]
|
||||
E -->|media:recover --verify + curl páginas| F[aceite público]
|
||||
```
|
||||
|
||||
**Opções consideradas (forma de execução):**
|
||||
|
||||
- **A — Estender o dono:** `--dry-run` (plan-only) no `scripts/seed-posts.mjs`
|
||||
- classificar os 4 slugs novos no `CLASSIFICATION` + runbook no homeserver.
|
||||
**Recomendada.**
|
||||
- B — Executar o seed como está ("às cegas") e confiar no relatório final —
|
||||
o relatório só diz created/skipped **depois** de já ter escrito em prod;
|
||||
sem echo de alvo, sem chance de revisar "o que será criado" antes.
|
||||
- C — Script novo de pre-flight separado — twin do fetch/lookup do seed;
|
||||
conhecimento vazaria em 2 módulos.
|
||||
|
||||
**Opções consideradas (taxonomy dos 4 slugs):**
|
||||
|
||||
- **A2 — Expandir o `CLASSIFICATION` com os 4 slugs** (3 com
|
||||
`tags: ['eleitoral']`, o bruno-reis em `politica`). **Recomendada.**
|
||||
- B2 — Seed intocado: os 4 caem em `politica` sem tag `eleitoral` + triagem
|
||||
manual pós-sync no admin.
|
||||
- C2 — Bloquear o sync até classificar (abort no unmapped).
|
||||
|
||||
**Recomendação: A + A2** — o dry-run é o pre-flight que o OPS52 provou no
|
||||
homeserver (custo baixo: o seed já tem todo o fetch/lookup; dry-run troca
|
||||
`payload.create` por contagem), e a classificação dos 4 slugs é **aditiva e
|
||||
create-only** (não toca posts existentes — só decide a categoria/tag dos que
|
||||
serão criados). Os 3 slugs de campanha **precisam** do tag `eleitoral` para o
|
||||
lock de visibilidade do repo continuar valendo — registrá-los como `politica`
|
||||
pura seria criar conteúdo de campanha fora do controle eleitoral em plena
|
||||
janela. A manutenção contínua (próximos slugs) segue fora de escopo, como a
|
||||
intenção manda.
|
||||
|
||||
**Rejeitadas:** B (sem pre-flight — escreve antes de informar); C (twin do
|
||||
seed); B2 (fura o lock `eleitoral`; triagem manual pós-sync é pior que a
|
||||
classificação no commit, com os 4 slugs já em mãos); C2 (transforma 4 slugs
|
||||
em gate de conteúdo — a intenção quer o sync desbloqueado).
|
||||
|
||||
### Componentes / mudanças
|
||||
|
||||
- **`scripts/seed-posts.mjs`** (extensão do dono — comportamento default
|
||||
inalterado):
|
||||
- Parse de args no topo (padrão `recover-media.mjs`): só `--dry-run`
|
||||
aceito; argumento desconhecido → die; echo do alvo (`DB host:
|
||||
<host>` — sem senha) antes de qualquer trabalho.
|
||||
- Modo dry-run: executa o fetch + lookups de tags/posts existentes +
|
||||
resolução de covers (`resolveCoverSource`, **sem download** de imagem),
|
||||
e no lugar de cada `payload.create` conta/lista "would create" /
|
||||
"would skip" / unmapped / sem-cover. Zero escrita. Relatório final
|
||||
espelhando o SUMMARY atual.
|
||||
- `CLASSIFICATION`: +4 entradas (E1) — 3 com `tags: ['eleitoral']`.
|
||||
- **`package.json`:** nada novo — `--dry-run` é argumento do script existente.
|
||||
- **Migration:** nenhuma. **Access / Consent:** nenhum. **UI:** N/A
|
||||
(Impeccable A — sem mudança de UI; conteúdo/operação).
|
||||
- **Docs:** runbook de execução em prod neste impl plan (ordem do homeserver);
|
||||
entrada `docs/changelog/2026-08-18-ops58.md` + `pnpm changelog:build`;
|
||||
nota curta no AGENTS.md (§ seeding de conteúdo) apontando o runbook.
|
||||
|
||||
## Fases verificáveis
|
||||
|
||||
1. **Seed (dry-run + taxonomy)** — parse de args, echo do alvo, early-returns;
|
||||
`CLASSIFICATION` +4. Gates locais: `pnpm gate:fast`, `pnpm format:check`,
|
||||
`pnpm exec knip`, `pnpm check:cycles`. Validação de comportamento:
|
||||
`pnpm db:seed:posts --dry-run` contra o DB local do worktree — relatório
|
||||
com "to create"/"skip"/unmapped e **zero** posts/tags/media criados
|
||||
(conferir via contagem antes/depois).
|
||||
2. **Docs + changelog** — runbook completo neste impl plan, nota no AGENTS.md,
|
||||
`docs/changelog/2026-08-18-ops58.md`, `pnpm changelog:build` +
|
||||
`pnpm changelog:check`.
|
||||
3. **Gates finais + merge** — `pnpm gate:fast`, `format:check`, `knip`,
|
||||
`check:cycles`, `pnpm test` (unit+int), `pnpm build` local; PR → CI verde
|
||||
→ merge em `main` (deploy automático OPS53).
|
||||
4. **Execução em prod (pós-merge, runbook — humano):** seção abaixo.
|
||||
|
||||
## Runbook de execução em produção (pós-merge, humano)
|
||||
|
||||
1. **Deploy do merge em `main`** (CI OPS53) — o seed com `--dry-run` e a
|
||||
taxonomy chegam ao homeserver com a imagem do SHA.
|
||||
2. **Pre-flight no homeserver** (no clone do repo `~/teqo-deploy` — o deploy
|
||||
OPS53 deixa o clone em **detached HEAD**, então trazer para `main` primeiro;
|
||||
env do stack sourceado; `postgres` só resolve dentro do compose → usar o
|
||||
**proxy socat do build** em `127.0.0.1:5433`; o Garage roda no host →
|
||||
`S3_ENDPOINT=http://127.0.0.1:3900`):
|
||||
```bash
|
||||
ssh homeserver
|
||||
cd ~/teqo-deploy
|
||||
git fetch origin main && git checkout main && git pull --ff-only origin main
|
||||
pnpm install # se preciso (hooks — padrão ensure-repo-deps)
|
||||
set -a; source ~/stack/teqo-1313.env; set +a
|
||||
export DATABASE_URL="${DATABASE_URL/@postgres:5432/@127.0.0.1:5433}" # proxy socat do build (postgres só resolve dentro do compose)
|
||||
export S3_ENDPOINT="http://127.0.0.1:3900" # Garage roda no host (host.docker.internal só resolve no compose)
|
||||
ALLOW_REMOTE_DB=true pnpm db:seed:posts --dry-run
|
||||
```
|
||||
Esperado: echo do alvo (`DB: 127.0.0.1:5433`), relatório com os 4 slugs
|
||||
novos a criar (`would-create … (cover: url)` — URL presente; o download/upload
|
||||
só acontece no sync), slugs existentes a pular, sem escrita. Se o dry-run
|
||||
falhar com ECONNREFUSED/ENOTFOUND: conferir o proxy (`docker ps | grep
|
||||
teqo-1313-build-proxy` — o deploy script o cria idempotentemente; se
|
||||
ausente, re-dispatchar o deploy ou rodar o trecho que o cria em
|
||||
`scripts/deploy-homeserver.sh`) e registrar a forma que funcionou na Issue
|
||||
(primeira execução do runbook).
|
||||
3. **Sincronizar:**
|
||||
```bash
|
||||
ALLOW_REMOTE_DB=true pnpm db:seed:posts
|
||||
```
|
||||
Esperado: `Posts created : 4` (os novos), `Posts skipped : 39` (ou o número
|
||||
real de já existentes), capas baixadas e uploadadas para o Garage de prod
|
||||
(`S3_ENDPOINT` já reescrito no passo 2). Se o WP ganhar slugs novos entre o
|
||||
pre-flight e aqui, eles entram como `politica` com warning — registrar na
|
||||
Issue e classificar (admin ou follow-up).
|
||||
4. **Bust de cache:**
|
||||
```bash
|
||||
curl -X POST https://jorgesolla1313.com.br/api/revalidate \
|
||||
-H "x-revalidate-secret: $REVALIDATE_SECRET"
|
||||
```
|
||||
Esperado: `200 { revalidated: true, tag: 'posts' }` (o smoke do deploy
|
||||
OPS53 já exercita o endpoint com o secret real — o env está no stack).
|
||||
5. **Aceite:**
|
||||
- Capas: `ALLOW_REMOTE_DB=true pnpm media:recover --verify` — esperado
|
||||
`42/44` filenames 200: 38 das 40 rows pré-existentes (as 2 exceções
|
||||
conhecidas do OPS52 — `fim-escala-6x1.jpg`, `jorgesolla.jpg` — seguem
|
||||
404 e fazem o verify sair com exit 1; aceite: **as únicas falhas são as
|
||||
2 conhecidas**) + as capas novas dos 4 posts (se o dry-run acusou
|
||||
`cover: url` para os 4).
|
||||
- Páginas: `curl -s https://jorgesolla1313.com.br/ | grep <slug-novo>`
|
||||
e `curl -o /dev/null -w "%{http_code}" https://jorgesolla1313.com.br/noticia/politica/<slug-novo>`
|
||||
→ 200 para os 4 novos; conferir também `/artigos` e a seção S1 da home
|
||||
de campanha.
|
||||
6. **Registro:** resumo na Issue #43 (created/skipped, exceções de capa,
|
||||
slugs unmapped adicionais, forma exata de execução usada no passo 2).
|
||||
|
||||
## Rabbit holes / Não escopo (engenharia)
|
||||
|
||||
- **`--verify` de posts em código** — curl no runbook basta; não criar
|
||||
ferramenta para o aceite.
|
||||
- **Upsert de edits do WP / sync agendado / hidden flag** — fora de escopo da
|
||||
intenção; itens separados quando doer.
|
||||
- **Testar o sync ponta a ponta fora do homeserver** — impossível por
|
||||
contrato (sem DB de prod, sem `S3_*`); a prova E2E é o runbook (padrão
|
||||
OPS52 fase 3).
|
||||
- **Unidade de teste para o parse de args do seed** — mesmo padrão do
|
||||
`recover-media.mjs` (parse inline, sem teste dedicado); o write path não
|
||||
muda (create-only) e o dry-run é validado contra o DB local.
|
||||
|
||||
## Riscos e mitigação
|
||||
|
||||
- **R1 — Host do homeserver não alcança `postgres`** (DB só resolve dentro de
|
||||
`stack_default`; a forma comprovada do host é o proxy socat do build em
|
||||
`127.0.0.1:5433`). Mitigação: o pre-flight (dry-run) valida em segundos;
|
||||
se o proxy estiver ausente, o deploy script o recria (re-dispatch); a forma
|
||||
real usada na primeira execução é registrada na Issue.
|
||||
- **R2 — Capa nova não resolve** (WP mudou/removeu a imagem). Mitigação:
|
||||
post criado sem cover → card degrada para a banda cinza existente (sem
|
||||
placeholder); exceção registrada na Issue; `media:recover --verify` acusa
|
||||
(as únicas falhas aceitáveis são as 2 exceções conhecidas do OPS52).
|
||||
- **R3 — Conteúdo eleitoral sem tag `eleitoral`** (se o gate rejeitar A2).
|
||||
Mitigação: triagem manual no admin imediatamente pós-sync (passo 3 do
|
||||
runbook vira passo de classificação obrigatório).
|
||||
- **R4 — `REVALIDATE_SECRET` ausente no env do stack.** Mitigação: o smoke
|
||||
do deploy (OPS53) já testa `/api/revalidate` com o secret real — presença
|
||||
garantida; conferir no pre-flight se o curl der 401/500.
|
||||
- **R5 — WP ganha slugs novos entre o commit e a execução** (fetch é live).
|
||||
Mitigação: relatório final lista unmapped; runbook manda registrar e
|
||||
classificar (admin ou follow-up) — nunca bloqueia o sync.
|
||||
|
||||
## Aceite de engenharia
|
||||
|
||||
- [ ] Aceite de produto da intenção ainda coberto: posts ausentes criados,
|
||||
existentes intocados (create-only), capas 200, cache bustado, runbook
|
||||
documentado (padrão OPS52-media)
|
||||
- [ ] Invariantes AGENTS/engineering-standards: `ALLOW_REMOTE_DB=true`
|
||||
explícito mantido; identificadores em inglês; zero access/Consent/
|
||||
transação/migration tocados; edit the owner (seed estendido, sem twin)
|
||||
- [ ] Testes/validação de domínio: `--dry-run` contra DB local (relatório sem
|
||||
escrita); gates da checklist do AGENTS; prova E2E é o runbook do
|
||||
homeserver
|
||||
- [ ] Docs: runbook de execução em prod + nota AGENTS.md + changelog
|
||||
|
|
@ -21,6 +21,8 @@
|
|||
*
|
||||
* Usage:
|
||||
* pnpm db:seed:posts
|
||||
* pnpm db:seed:posts --dry-run (plan-only: fetches + resolves covers and
|
||||
* reports what WOULD be created — no writes)
|
||||
* (against a remote DB, on purpose:) ALLOW_REMOTE_DB=true pnpm db:seed:posts
|
||||
*/
|
||||
import { convertHTMLToLexical, editorConfigFactory } from '@payloadcms/richtext-lexical'
|
||||
|
|
@ -48,12 +50,29 @@ const config = (await import('../src/payload.config.ts')).default
|
|||
|
||||
const die = dieWithLabel('seed:posts')
|
||||
|
||||
const args = new Set(process.argv.slice(2))
|
||||
const unknown = [...args].filter((a) => a !== '--dry-run')
|
||||
if (unknown.length > 0) die(`argumento desconhecido: ${unknown.join(', ')}`)
|
||||
const dryRun = args.has('--dry-run')
|
||||
|
||||
/** Host do DATABASE_URL para o echo do alvo — die limpo se a URL for inválida. */
|
||||
const echoTargetHost = () => {
|
||||
try {
|
||||
return new URL(process.env.DATABASE_URL).host
|
||||
} catch {
|
||||
die(`DATABASE_URL não é uma connection string válida: ${process.env.DATABASE_URL || '(vazia)'}`)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Taxonomy (Section 7): the categories + control tag, and the per-article map.
|
||||
// Keys are the WordPress slugs of the live articles. Every post gets exactly
|
||||
// one required `category`; `tags` is optional (currently only `eleitoral`).
|
||||
// The `eleitoral` tag marks pre-candidacy / campaign / PGP / party-event
|
||||
// content so Francisco can hide it all with one toggle in the electoral period.
|
||||
// Criterion: content tied to a campaign event or candidacy (convenções,
|
||||
// plenárias, caravanas, filiações, PGP) gets `eleitoral`; political
|
||||
// rebuttals/position-taking without a campaign event stay plain `politica`.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const TAXONOMY_TAGS = [
|
||||
|
|
@ -120,6 +139,21 @@ const CLASSIFICATION = {
|
|||
category: 'politica',
|
||||
tags: ['eleitoral'],
|
||||
},
|
||||
'bruno-reis-foi-covarde-detonou-solla-ao-rebater-o-prefeito-sobre-saude-de-salvador': {
|
||||
category: 'politica',
|
||||
},
|
||||
'chapada-diamantina-recebe-caravana-liderada-pelo-deputado-jorge-solla': {
|
||||
category: 'politica',
|
||||
tags: ['eleitoral'],
|
||||
},
|
||||
'lula-reforca-o-time-de-jeronimo-wagner-rui-e-jorge-solla-na-convencao-estadual-do-pt': {
|
||||
category: 'politica',
|
||||
tags: ['eleitoral'],
|
||||
},
|
||||
'o-pt-tem-que-disputar-o-programa-de-governo-conclamou-jorge-solla-em-plenaria': {
|
||||
category: 'politica',
|
||||
tags: ['eleitoral'],
|
||||
},
|
||||
'clube-2004-celebra-a-democracia-com-o-lancamento-da-pre-candidatura-de-jorge-solla-a-deputado-federal':
|
||||
{ category: 'politica', tags: ['eleitoral'] },
|
||||
'articulada-por-solla-filiacao-de-zilar-portela-ao-pt-marca-lancamento-de-pre-candidatura-a-deputada-estadual':
|
||||
|
|
@ -370,6 +404,12 @@ async function main() {
|
|||
' 2. set DATABASE_URL=postgresql://teqo:teqo@localhost:5432/teqo in .env.local',
|
||||
)
|
||||
|
||||
console.log(
|
||||
`\n[seed:posts] Alvo da execução:\n` +
|
||||
` DB : ${echoTargetHost()}\n` +
|
||||
` Modo : ${dryRun ? 'dry-run (plan-only — nenhuma escrita)' : 'sync (create-only)'}\n`,
|
||||
)
|
||||
|
||||
const payload = await getPayload({ config })
|
||||
const editorConfig = await editorConfigFactory.default({ config: payload.config })
|
||||
|
||||
|
|
@ -379,6 +419,7 @@ async function main() {
|
|||
console.log('[seed:posts] Ensuring taxonomy tags...')
|
||||
const tagIdBySlug = {}
|
||||
let tagsCreated = 0
|
||||
let tagsToCreate = 0
|
||||
for (const tag of TAXONOMY_TAGS) {
|
||||
const found = await payload.find({
|
||||
collection: 'tag',
|
||||
|
|
@ -390,6 +431,10 @@ async function main() {
|
|||
tagIdBySlug[tag.slug] = found.docs[0].id
|
||||
continue
|
||||
}
|
||||
if (dryRun) {
|
||||
tagsToCreate += 1
|
||||
continue
|
||||
}
|
||||
const created = await payload.create({
|
||||
collection: 'tag',
|
||||
data: { name: tag.name, slug: tag.slug, hidden: false },
|
||||
|
|
@ -397,7 +442,11 @@ async function main() {
|
|||
tagIdBySlug[tag.slug] = created.id
|
||||
tagsCreated += 1
|
||||
}
|
||||
console.log(`[seed:posts] ${tagsCreated} tag(s) created, ${TAXONOMY_TAGS.length} total.`)
|
||||
console.log(
|
||||
dryRun
|
||||
? `[seed:posts] ${tagsToCreate} tag(s) to create, ${TAXONOMY_TAGS.length} total.`
|
||||
: `[seed:posts] ${tagsCreated} tag(s) created, ${TAXONOMY_TAGS.length} total.`,
|
||||
)
|
||||
|
||||
// 2. Live fetch (REST API primary, HTML crawl fallback).
|
||||
console.log('[seed:posts] Fetching articles from jorgesolla.com.br (WP REST API)...')
|
||||
|
|
@ -417,6 +466,7 @@ async function main() {
|
|||
// 3. Upsert each article.
|
||||
let postsCreated = 0
|
||||
let postsSkipped = 0
|
||||
let postsToCreate = 0
|
||||
let mediaCreated = 0
|
||||
const unmapped = []
|
||||
|
||||
|
|
@ -429,6 +479,7 @@ async function main() {
|
|||
const tagSlugs = mapping?.tags || []
|
||||
const categoryId = tagIdBySlug[categorySlug]
|
||||
const tagIds = tagSlugs.map((s) => tagIdBySlug[s]).filter(Boolean)
|
||||
const categoryLabel = `[${categorySlug}${tagSlugs.length ? ' | ' + tagSlugs.join(',') : ''}]`
|
||||
|
||||
const existing = await payload.find({
|
||||
collection: 'post',
|
||||
|
|
@ -441,6 +492,15 @@ async function main() {
|
|||
continue
|
||||
}
|
||||
|
||||
if (dryRun) {
|
||||
const cover = resolveCoverSource(article)
|
||||
postsToCreate += 1
|
||||
console.log(
|
||||
`[seed:posts] would-create ${slug} ${categoryLabel} (cover: ${cover ? 'url' : 'sem-cover'})`,
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
const { subtitle, coverUrl, bodyHtml } = processContent(article)
|
||||
|
||||
const cover = await ensureCoverMedia(payload, {
|
||||
|
|
@ -466,18 +526,27 @@ async function main() {
|
|||
},
|
||||
})
|
||||
postsCreated += 1
|
||||
console.log(
|
||||
`[seed:posts] + ${slug} [${categorySlug}${tagSlugs.length ? ' | ' + tagSlugs.join(',') : ''}]`,
|
||||
)
|
||||
console.log(`[seed:posts] + ${slug} ${categoryLabel}`)
|
||||
}
|
||||
|
||||
// 4. Report.
|
||||
const summaryLines = [
|
||||
`Articles fetched : ${articles.length}`,
|
||||
...(dryRun
|
||||
? [
|
||||
`Posts to create : ${postsToCreate}`,
|
||||
`Posts skipped : ${postsSkipped} (already existed)`,
|
||||
`Tags to create : ${tagsToCreate}`,
|
||||
]
|
||||
: [
|
||||
`Posts created : ${postsCreated}`,
|
||||
`Posts skipped : ${postsSkipped} (already existed)`,
|
||||
`Media created : ${mediaCreated}`,
|
||||
`Tags created : ${tagsCreated} (of ${TAXONOMY_TAGS.length})`,
|
||||
]),
|
||||
]
|
||||
console.log('\n[seed:posts] ==================== SUMMARY ====================')
|
||||
console.log(`[seed:posts] Articles fetched : ${articles.length}`)
|
||||
console.log(`[seed:posts] Posts created : ${postsCreated}`)
|
||||
console.log(`[seed:posts] Posts skipped : ${postsSkipped} (already existed)`)
|
||||
console.log(`[seed:posts] Media created : ${mediaCreated}`)
|
||||
console.log(`[seed:posts] Tags created : ${tagsCreated} (of ${TAXONOMY_TAGS.length})`)
|
||||
for (const line of summaryLines) console.log(`[seed:posts] ${line}`)
|
||||
if (unmapped.length > 0) {
|
||||
console.warn(
|
||||
`\n[seed:posts] WARNING: ${unmapped.length} live slug(s) not in the taxonomy map ` +
|
||||
|
|
|
|||
Loading…
Reference in a new issue