mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-12 22:13:05 +00:00
chore(OPS60): guard SEED_MEDIA_CONFIRM para escrita no bucket via db:seed:posts
This commit is contained in:
parent
085a69e6e1
commit
2d402c81a2
7 changed files with 212 additions and 13 deletions
|
|
@ -70,9 +70,10 @@ PROD_DATABASE_URL="<unpooled Neon URL>" pnpm db:pull
|
|||
pnpm db:seed:posts
|
||||
```
|
||||
|
||||
- `scripts/seed-posts.mjs` fetches ~39 articles **live** from jorgesolla.com.br (WordPress REST API, HTML-crawl fallback) and imports them into `post`/`tag`/`media` as published `noticia` posts, converting the WP HTML body to Payload Lexical and uploading covers to Vercel Blob.
|
||||
- `scripts/seed-posts.mjs` fetches ~43 articles **live** from jorgesolla.com.br (WordPress REST API, HTML-crawl fallback) and imports them into `post`/`tag`/`media` as published `noticia` posts, converting the WP HTML body to Payload Lexical and uploading covers through the configured media storage (Garage S3 when the `S3_*` envs are set — the adapter overwrites the deterministic `<slug>.<ext>` key natively; local disk otherwise).
|
||||
- Same safety guard as `pnpm dev`: it refuses a non-local `DATABASE_URL` unless `ALLOW_REMOTE_DB=true`.
|
||||
- **Idempotent by slug** — re-running skips existing posts and reuses tags/media, so it never duplicates. Because the Vercel Blob store is shared across environments, it deletes any orphan blob under the deterministic `<slug>.<ext>` key before uploading, so it stays re-runnable.
|
||||
- **Idempotent by slug** — re-running skips existing posts and reuses tags/media, so it never duplicates.
|
||||
- **Bucket-write guard (OPS60):** with the `S3_*` envs set (e.g. a worktree with the main repo's prod-credentials copy), the sync requires explicit write intent — `SEED_MEDIA_CONFIRM=1` — and refuses with exit 1 before any write without it; `--dry-run` stays flag-free (it never writes).
|
||||
- **Post-seed revalidation (required for prod):** the seed writes from a CLI process outside the deployed runtime, so it does **not** bust production's frozen `posts` cache. After seeding prod content — or after ANY direct-DB write — bust it:
|
||||
|
||||
```bash
|
||||
|
|
|
|||
|
|
@ -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 ~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`).
|
||||
**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). **Since OPS60, sync with the `S3_*` envs set also requires explicit bucket-write intent: `SEED_MEDIA_CONFIRM=1`** (fail-closed, same `isTruthyEnv` pattern as `MEDIA_RECOVER_CONFIRM`; without it the sync refuses with exit 1 before any write — the `S3_*` envs alone must never arm a bucket write, since a worktree with a prod-credentials copy running sync against a local DB would write covers to the prod bucket). `--dry-run` stays flag-free. 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
|
||||
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@ Conteúdo movido do `AGENTS.md` em 2026-07-30 (fatiamento do paradigma de agente
|
|||
|
||||
**Recently resolved (2026-08-18):** **OPS59** — o pre-push local não roda mais e2e: ele roda **uma vez, no CI** (PR e main — gateia merge e deploy). O espelho `scripts/gate-ci.mjs` cortou o bloco e2e da cascata (playwright install, build `.next-e2e`, `test:e2e`) e o preflight de DB passou a depender só de test/build; o readout do classificador mantém `e2e=<mode>` e imprime o que o CI rodará ("e2e runs in CI, not in this gate — OPS59"), com o check `unmapped` duplicado removido (`ci-scope.mjs` já o imprime no stderr). `package.json`, `.husky/pre-push`, `git-push.mjs` e os workflows `.forgejo/*` intocados (CI verificado, não alterado: `ci-pr.yml` e2e → `checks`; `ci.yml` e2e → `checks` → `deploy`). `tests/unit/ciSkipInvariants.unit.spec.ts` tirou `gate-ci.mjs` da lista de arquivos que devem conter `.next-e2e` (o gate local não builda mais e2e; `run-e2e-affected.mjs`/`playwright.config.ts` seguem cobertos — e2e local opcional intacto). Docs sincronizadas: `AGENTS.md` (checklist local item 3 — `test:e2e`/`test:all` saem da obrigatória; "e2e = CI, local opcional `pnpm test:e2e:affected`"), `.agents/rules/engineering-standards.mdc` e `agent-pr-workflow.mdc` (gate descrito como "espelho do ci-pr.yml sem e2e"). Verificado por grep: `AGENT-OPS.md` e skills (`work-issue`, `agent-work-issue`, `worktree-next-issue`, `local-database`) não citavam e2e no pre-push — sem mudança. Anti-goals respeitados: sem flag de "pular e2e" (bypass continua só via `--no-verify` documentado), política "Dono do PR, dono do CI" inalterada. Sem migration/access/Consent/UI. Verificação: `pnpm gate:ci` local (cascata nova full — lint/format/typecheck/knip/cycles/unit/int/build em `teqo_test`; e2e vira print), unit 2286+ verdes; o e2e desta PR roda full no CI por HIGH_RISK (`gate-ci.mjs`).
|
||||
|
||||
**Recently resolved (2026-08-18):** **OPS60** — o sync do `pnpm db:seed:posts` ganhou guard de intenção explícita para escrita no bucket (achado do /simplify da #37): com as envs `S3_*` ativas, o modo sync exige `SEED_MEDIA_CONFIRM=1` (mesmo padrão `isTruthyEnv` do `MEDIA_RECOVER_CONFIRM` da OPS52-media-guard) e recusa com exit 1 antes de qualquer escrita sem a flag — o risco fechado: um worktree com cópia de credenciais `S3_*` de prod (all-or-nothing do `worktree-env.mjs`) rodando sync contra DB local sobrescrevia covers no bucket de **prod** (o seed é idempotente por slug e sobrescreve o key determinístico). Storage local (sem `S3_*` → disk) e `--dry-run` seguem sem flag (não escrevem); echo do alvo ganhou `Bucket`/`Endpoint` quando S3 ativo (paridade com o `media:recover`). Implementação no dono: `isTruthyEnv` (já existente da #37) + `resolveS3StorageEnv` — sem twin. Runbook do OPS58 atualizado com o comando real; nota no AGENTS.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-ops60.md
Normal file
1
docs/changelog/2026-08-18-ops60.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
**Recently resolved (2026-08-18):** **OPS60** — o sync do `pnpm db:seed:posts` ganhou guard de intenção explícita para escrita no bucket (achado do /simplify da #37): com as envs `S3_*` ativas, o modo sync exige `SEED_MEDIA_CONFIRM=1` (mesmo padrão `isTruthyEnv` do `MEDIA_RECOVER_CONFIRM` da OPS52-media-guard) e recusa com exit 1 antes de qualquer escrita sem a flag — o risco fechado: um worktree com cópia de credenciais `S3_*` de prod (all-or-nothing do `worktree-env.mjs`) rodando sync contra DB local sobrescrevia covers no bucket de **prod** (o seed é idempotente por slug e sobrescreve o key determinístico). Storage local (sem `S3_*` → disk) e `--dry-run` seguem sem flag (não escrevem); echo do alvo ganhou `Bucket`/`Endpoint` quando S3 ativo (paridade com o `media:recover`). Implementação no dono: `isTruthyEnv` (já existente da #37) + `resolveS3StorageEnv` — sem twin. Runbook do OPS58 atualizado com o comando real; nota no AGENTS.md.
|
||||
|
|
@ -61,7 +61,7 @@ Appetite restante: herdado (~0,5 dia — execução no homeserver + runbook)
|
|||
```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]
|
||||
B -->|ok| C[SEED_MEDIA_CONFIRM=1 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]
|
||||
```
|
||||
|
|
@ -157,7 +157,9 @@ em gate de conteúdo — a intenção quer o sync desbloqueado).
|
|||
```
|
||||
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
|
||||
só acontece no sync), slugs existentes a pular, sem escrita. (O dry-run não
|
||||
escreve — segue sem a flag `SEED_MEDIA_CONFIRM` do sync, mesmo com as `S3_*`
|
||||
exportadas.) 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
|
||||
|
|
@ -165,11 +167,14 @@ teqo-1313-build-proxy` — o deploy script o cria idempotentemente; se
|
|||
(primeira execução do runbook).
|
||||
3. **Sincronizar:**
|
||||
```bash
|
||||
ALLOW_REMOTE_DB=true pnpm db:seed:posts
|
||||
SEED_MEDIA_CONFIRM=1 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
|
||||
(`S3_ENDPOINT` já reescrito no passo 2). **Nota (OPS60):** o sync escreve
|
||||
media no bucket das envs `S3_*` — o guard `SEED_MEDIA_CONFIRM=1` é
|
||||
obrigatório (fail-closed; sem ele o seed recusa com exit 1 antes de
|
||||
qualquer escrita). 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:**
|
||||
|
|
|
|||
156
docs/plans/ops60-seed-posts-guard-escrita-s3-impl.md
Normal file
156
docs/plans/ops60-seed-posts-guard-escrita-s3-impl.md
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
# Impl: OPS60 — Guard de intenção explícita para escrita no bucket via `db:seed:posts`
|
||||
|
||||
Status: aprovado (gate humano 2026-08-18)
|
||||
Atualizado em: 2026-08-18
|
||||
Issue: #49 (OPS60)
|
||||
Intenção: docs/plans/ops60-seed-posts-guard-escrita-s3.md
|
||||
Appetite restante: herdado (~0,5 dia — um outcome verificável; sem migration/UI)
|
||||
|
||||
## Leitura da intenção
|
||||
|
||||
- **Outcome:** o modo sync (padrão) do `pnpm db:seed:posts` só escreve media no
|
||||
bucket S3 das envs `S3_*` com **intenção explícita** — `SEED_MEDIA_CONFIRM=1`
|
||||
(mesmo padrão `isTruthyEnv` da #37) — e recusa com mensagem clara sem ela;
|
||||
`--dry-run` segue desprotegido (não escreve). O risco fechado: um worktree
|
||||
que receba credenciais `S3_*` de prod (cópia all-or-nothing do
|
||||
`worktree-env.mjs`) rodando o seed contra DB local sobrescreveria covers no
|
||||
bucket de **prod** — a mesma classe de risco que a #37 fechou para
|
||||
`media:recover`, ainda aberta no seed.
|
||||
- **O que NÃO negociar:** fail-closed (sem flag → sem escrita no bucket, em
|
||||
qualquer cenário de DB/alvo); comportamento local sem `S3_*` (→ disk) e o
|
||||
upload via admin/CI de prod intactos; `--dry-run` sem fricção nova; guard no
|
||||
dono (`isTruthyEnv` em `cli.mjs`) — sem twin.
|
||||
- **O que reavaliar:** a alternativa do aceite "**ou recusa apontar para bucket
|
||||
não-local**" — ver Opções (B).
|
||||
|
||||
## Abordagem recomendada
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[pnpm db:seed:posts] -->|parse args --dry-run| B{mode == sync?}
|
||||
B -->|não| C[segue — dry-run não escreve, sem guard]
|
||||
B -->|sim| D{S3_* completo?<br/>resolveS3StorageEnv.enabled}
|
||||
D -->|não| E[segue sem guard — storage local (disk)]
|
||||
D -->|sim| F{SEED_MEDIA_CONFIRM true/1?}
|
||||
F -->|não| G[die com o comando correto — antes de DB/rede]
|
||||
F -->|sim| H[guards existentes + echo do alvo + sync]
|
||||
```
|
||||
|
||||
**Opções consideradas (forma do guard):**
|
||||
|
||||
- **A — Env flag `SEED_MEDIA_CONFIRM` (truthy `true|1` via `isTruthyEnv`)
|
||||
obrigatória no sync quando o storage S3 está ativo**
|
||||
(`resolveS3StorageEnv(process.env).enabled`). **Recomendada.**
|
||||
- B — Recusar bucket não-local: allowlist de endpoints/buckets "locais" e die
|
||||
para qualquer outro alvo.
|
||||
- C — Guard incondicional no sync (mesmo com storage local).
|
||||
|
||||
**Recomendação: A** — paridade total com o guard da #37 (`MEDIA_RECOVER_CONFIRM`)
|
||||
e com o guard de DB (`ALLOW_REMOTE_DB`): funciona em runbook não-interativo
|
||||
(SSH, `set -a; source teqo-1313.env`), a condição usa o dono já existente
|
||||
(`resolveS3StorageEnv` — sem nova lógica de leitura de env), e a escrita que o
|
||||
guard protege é exatamente a escrita no bucket (media via adapter S3), deixando
|
||||
o fluxo dev local intocado.
|
||||
|
||||
**Rejeitadas:** B (o "local" do Garage é ambíguo por construção — o endpoint
|
||||
varia por contexto: `host.docker.internal:3900` dentro do compose,
|
||||
`100.119.220.31:3900` na workstation/tailnet, `127.0.0.1:3900` no host do
|
||||
homeserver; uma allowlist de hosts teria que errar em algum lugar ou bloquear o
|
||||
sync de prod legítimo — o guard da #37 já provou que a env flag é o padrão do
|
||||
repo); C (quebraria o fluxo dev local: todo `db:seed:posts` exigiria a flag
|
||||
mesmo sem bucket; o aceite pede explicitamente o comportamento local intacto).
|
||||
|
||||
### Componentes / mudanças
|
||||
|
||||
- **`scripts/seed-posts.mjs`** (extensão do dono — comportamento dev local
|
||||
inalterado; paridade estrutural com `scripts/recover-media.mjs:79-85`):
|
||||
- Constante `SEED_MEDIA_CONFIRM_FLAG = 'SEED_MEDIA_CONFIRM'`; import de
|
||||
`isTruthyEnv` (de `./lib/cli.mjs`) e `resolveS3StorageEnv` (de
|
||||
`../src/utilities/mediaStorage.ts`).
|
||||
- Imediatamente após o parsing de args/modo e **antes** de
|
||||
`assertLocalDatabase` / `getPayload` (fail fast — zero DB, zero rede):
|
||||
```js
|
||||
const storage = resolveS3StorageEnv(process.env) // throws em S3_* parcial (fail-closed, mesmo contrato do boot)
|
||||
if (mode === 'sync' && storage.enabled && !isTruthyEnv(process.env[SEED_MEDIA_CONFIRM_FLAG])) {
|
||||
die(
|
||||
'o sync ESCREVE media no bucket S3 das envs S3_* — exige confirmação explícita de intenção.\n' +
|
||||
` Re-rodar com: ${SEED_MEDIA_CONFIRM_FLAG}=1 pnpm db:seed:posts\n` +
|
||||
' (ou use --dry-run para planejar sem escrever).',
|
||||
)
|
||||
}
|
||||
```
|
||||
- Echo do alvo ganha `Bucket`/`Endpoint` quando `storage.enabled` (paridade
|
||||
com o `targetSummary` do `recover-media.mjs`).
|
||||
- Bloco de comentário do header (safety model + usage) atualizado para
|
||||
documentar o guard.
|
||||
- O gate vale para sync com QUALQUER alvo de DB (local ou remoto) — o risco
|
||||
é a escrita no bucket, não a origem dos dados (mesma decisão da #37).
|
||||
- **`package.json`:** nada — a flag é env, não arg do script.
|
||||
- **Migration:** nenhuma. **Access / Consent:** intocados. **UI:** N/A
|
||||
(Impeccable A — conteúdo/operação).
|
||||
- **Docs:** runbook do `docs/plans/ops58-sincronizar-posts-prod-impl.md`
|
||||
(passo 3 do runbook ganha `SEED_MEDIA_CONFIRM=1` + nota OPS60; passo 2
|
||||
pre-flight ganha nota de que o dry-run segue sem a flag — não escreve);
|
||||
nota curta no AGENTS.md (§ "Seeding news content" — sync com
|
||||
`S3_*` exige `SEED_MEDIA_CONFIRM=1`; `--dry-run` segue sem flag); entrada
|
||||
`docs/changelog/2026-08-18-ops60.md` + `pnpm changelog:build`.
|
||||
|
||||
## Fases verificáveis
|
||||
|
||||
1. **Guard no script** — `seed-posts.mjs` (gate + echo + header doc).
|
||||
Validação de comportamento contra o DB local do worktree:
|
||||
- Sem `S3_*`: `pnpm db:seed:posts` e `pnpm db:seed:posts --dry-run` seguem
|
||||
exatamente como hoje (sem flag exigida).
|
||||
- Com `S3_*` fake (ex.: `S3_BUCKET=teqo-media-test
|
||||
S3_ENDPOINT=http://127.0.0.1:9 S3_ACCESS_KEY_ID=x S3_SECRET_ACCESS_KEY=x`):
|
||||
sync → exit 1 com a mensagem do guard, **antes** de qualquer escrita
|
||||
(conferir via contagem de posts/tags antes/depois — zero delta);
|
||||
`--dry-run` com as mesmas envs → segue plan-only sem exigir flag.
|
||||
2. **Docs + changelog** — runbook OPS58 (passo 3 + nota), AGENTS.md,
|
||||
`docs/changelog/2026-08-18-ops60.md`, `pnpm changelog:build` +
|
||||
`pnpm changelog:check`.
|
||||
3. **Gates finais** — `pnpm gate:fast`, `pnpm format:check`, `pnpm exec knip`,
|
||||
`pnpm check:cycles`, `pnpm test` (unit+int), `pnpm build` local; PR → CI
|
||||
verde → merge em `main` (deploy automático OPS53).
|
||||
|
||||
## Rabbit holes / Não escopo (engenharia)
|
||||
|
||||
- **Testar o sync com S3 ponta a ponta** — impossível por contrato (sem
|
||||
`S3_*` de prod nesta máquina; e o guard bloqueia o caminho por design); a
|
||||
prova do caminho liberado é o runbook do homeserver (padrão OPS52 fase 3).
|
||||
- **Teste unitário do gate inline** — mesmo padrão do `recover-media.mjs`
|
||||
(parse inline, sem teste dedicado; decisões OPS52-media-guard e OPS58); a
|
||||
semântica do flag já é unit-testada em `cliEnvFlags.unit.spec.ts`.
|
||||
- **Guard para `--dry-run`** — não: não escreve; o aceite pede explicitamente
|
||||
preservar a semântica atual do dry-run.
|
||||
- **Banir re-escritas `=== '1'`/`=== 'true'` de env-read** no
|
||||
`codebaseConventions` — defer registrado na intenção (gatilho: 4º site ou
|
||||
refactor dos 3 existentes); não reabrir.
|
||||
|
||||
## Riscos e mitigação
|
||||
|
||||
- **R1 — S3\_\* parcial no env local derruba o seed.** Já acontece hoje: o
|
||||
`payload.config.ts` resolve `mediaStorage` no import e aborta em config
|
||||
parcial (fail-closed de boot, OPS52); a chamada de `resolveS3StorageEnv` no
|
||||
guard tem o mesmo contrato e ocorre depois desse import — comportamento
|
||||
inalterado, agora com mensagem explícita do dono.
|
||||
- **R2 — Runbook de prod desatualizado manda rodar o sync sem a flag e o
|
||||
guard recusa (fricção na próxima execução).** Mitigação: runbook OPS58
|
||||
editado no mesmo PR; mensagem do die exibe o comando correto.
|
||||
- **R3 — A flag vaza para outros modos (dry-run).** Mitigação: condição
|
||||
restrita a `mode === 'sync' && storage.enabled`; o dry-run é validado com
|
||||
`S3_*` fake na fase 1.
|
||||
|
||||
## Aceite de engenharia
|
||||
|
||||
- [ ] Aceite de produto da intenção coberto: sync com `S3_*` recusa sem
|
||||
`SEED_MEDIA_CONFIRM=1` (fail-closed); dry-run e storage local
|
||||
desprotegidos; admin/CI de prod intocados
|
||||
- [ ] Invariantes AGENTS/engineering-standards: identificadores em inglês;
|
||||
guard reusado do dono (`isTruthyEnv`/`resolveS3StorageEnv`) sem twin;
|
||||
zero migration/access/Consent
|
||||
- [ ] Testes/validação de domínio: die do guard com `S3_*` fake (zero delta
|
||||
de rows no DB local); dry-run com `S3_*` fake segue plan-only; gates da
|
||||
checklist do AGENTS
|
||||
- [ ] Docs: runbook OPS58 atualizado (comando real com a flag) + nota
|
||||
AGENTS.md + changelog
|
||||
|
|
@ -10,6 +10,13 @@
|
|||
* Safety model (mirrors scripts/guard-dev-db.mjs and scripts/db-pull.mjs):
|
||||
* - This script WRITES (creates posts/tags/media). It refuses to run against
|
||||
* a non-local DATABASE_URL unless ALLOW_REMOTE_DB=true is set explicitly.
|
||||
* - When the S3_* envs are set (Garage bucket), the sync writes the cover
|
||||
* media objects to that bucket — so sync additionally requires the
|
||||
* explicit intent flag SEED_MEDIA_CONFIRM=1 (OPS60, OPS52-media-guard
|
||||
* parity): the S3_* envs alone must never arm a write, since a worktree
|
||||
* with a prod-credentials copy running sync against a local DB would
|
||||
* write covers to the prod bucket. --dry-run stays guard-free: it never
|
||||
* writes.
|
||||
* - Idempotent: re-running looks up posts/tags/media by slug/filename and
|
||||
* skips (or reuses) what already exists, so it never duplicates.
|
||||
*
|
||||
|
|
@ -24,11 +31,13 @@
|
|||
* 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
|
||||
* (against the S3 bucket of the S3_* envs, on purpose:)
|
||||
* SEED_MEDIA_CONFIRM=1 [ALLOW_REMOTE_DB=true] pnpm db:seed:posts
|
||||
*/
|
||||
import { convertHTMLToLexical, editorConfigFactory } from '@payloadcms/richtext-lexical'
|
||||
import { JSDOM } from 'jsdom'
|
||||
import { getPayload } from 'payload'
|
||||
import { dieWithLabel, loadCliEnv } from './lib/cli.mjs'
|
||||
import { dieWithLabel, isTruthyEnv, loadCliEnv } from './lib/cli.mjs'
|
||||
|
||||
import { assertLocalDatabase } from './assert-local-database.mjs'
|
||||
|
||||
|
|
@ -40,6 +49,8 @@ import {
|
|||
stripHtml,
|
||||
} from './lib/wpArticles.mjs'
|
||||
|
||||
import { resolveS3StorageEnv } from '../src/utilities/mediaStorage.ts'
|
||||
|
||||
/** @import { Article } from './lib/wpArticles.mjs' */
|
||||
|
||||
// Mirror Next.js precedence (.env.local wins over .env) without clobbering a
|
||||
|
|
@ -55,6 +66,27 @@ const unknown = [...args].filter((a) => a !== '--dry-run')
|
|||
if (unknown.length > 0) die(`argumento desconhecido: ${unknown.join(', ')}`)
|
||||
const dryRun = args.has('--dry-run')
|
||||
|
||||
/**
|
||||
* Write-intent guard (OPS60, OPS52-media-guard parity): the sync writes media
|
||||
* objects to the S3 bucket configured by the S3_* envs (when set) — the S3_*
|
||||
* envs alone must never arm that write, because a worktree with a
|
||||
* prod-credentials copy (worktree-env.mjs copies S3_* all-or-nothing) running
|
||||
* sync against a local DB would write covers to the prod bucket. Fires before
|
||||
* assertLocalDatabase / getPayload (zero DB, zero network). --dry-run stays
|
||||
* guard-free: it never writes.
|
||||
*/
|
||||
const SEED_MEDIA_CONFIRM_FLAG = 'SEED_MEDIA_CONFIRM'
|
||||
|
||||
const storage = resolveS3StorageEnv(process.env)
|
||||
|
||||
if (!dryRun && storage.enabled && !isTruthyEnv(process.env[SEED_MEDIA_CONFIRM_FLAG])) {
|
||||
die(
|
||||
'o sync ESCREVE media no bucket S3 das envs S3_* — exige confirmação explícita de intenção.\n' +
|
||||
` Re-rodar com: ${SEED_MEDIA_CONFIRM_FLAG}=1 pnpm db:seed:posts\n` +
|
||||
' (ou use --dry-run para planejar sem escrever).',
|
||||
)
|
||||
}
|
||||
|
||||
/** Host do DATABASE_URL para o echo do alvo — die limpo se a URL for inválida. */
|
||||
const echoTargetHost = () => {
|
||||
try {
|
||||
|
|
@ -404,11 +436,13 @@ 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 targetSummary = [
|
||||
`Alvo da execução:`,
|
||||
` DB : ${echoTargetHost()}`,
|
||||
...(storage.enabled ? [` Bucket : ${storage.bucket}`, ` Endpoint: ${storage.endpoint}`] : []),
|
||||
` Modo : ${dryRun ? 'dry-run (plan-only — nenhuma escrita)' : 'sync (create-only)'}`,
|
||||
].join('\n')
|
||||
console.log(`\n[seed:posts] ${targetSummary}`)
|
||||
|
||||
const payload = await getPayload({ config })
|
||||
const editorConfig = await editorConfigFactory.default({ config: payload.config })
|
||||
|
|
|
|||
Loading…
Reference in a new issue