ops(OPS52-media-guard): reconcile do media:recover exige MEDIA_RECOVER_CONFIRM=1 (guard de intenção explícita)
Some checks failed
CI (PR) / scope (pull_request) Successful in 32s
CI (PR) / lint (pull_request) Successful in 46s
CI (PR) / docs-guards (pull_request) Successful in 9s
CI (PR) / plans-only-closes (pull_request) Successful in 7s
CI (PR) / format (pull_request) Successful in 47s
CI (PR) / typecheck (pull_request) Successful in 28s
CI (PR) / knip (pull_request) Successful in 32s
CI (PR) / cycles (pull_request) Successful in 34s
CI (PR) / unit (pull_request) Successful in 45s
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) / int (pull_request) Successful in 1m34s
CI (PR) / checks (pull_request) Successful in 1s
PR Ready + auto-merge / ready-automerge (pull_request) Failing after 19m21s
CI / checks (push) Blocked by required conditions
CI / deploy homeserver (teqo-1313) (push) Blocked by required conditions
Main merge flips claimed issues to done + in-prod / issue-done (pull_request) Successful in 13s
Promote plan-issue Related on main merge / plan-issue-ready (pull_request) Has been skipped
Agent pool supervisor / tick (pull_request) Successful in 29s
CI / static (push) Successful in 2m15s
CI / int (push) Successful in 2m48s
CI / build (push) Successful in 2m37s
CI / e2e (2) (push) Successful in 5m56s
CI / e2e (1) (push) Failing after 17m21s
Some checks failed
CI (PR) / scope (pull_request) Successful in 32s
CI (PR) / lint (pull_request) Successful in 46s
CI (PR) / docs-guards (pull_request) Successful in 9s
CI (PR) / plans-only-closes (pull_request) Successful in 7s
CI (PR) / format (pull_request) Successful in 47s
CI (PR) / typecheck (pull_request) Successful in 28s
CI (PR) / knip (pull_request) Successful in 32s
CI (PR) / cycles (pull_request) Successful in 34s
CI (PR) / unit (pull_request) Successful in 45s
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) / int (pull_request) Successful in 1m34s
CI (PR) / checks (pull_request) Successful in 1s
PR Ready + auto-merge / ready-automerge (pull_request) Failing after 19m21s
CI / checks (push) Blocked by required conditions
CI / deploy homeserver (teqo-1313) (push) Blocked by required conditions
Main merge flips claimed issues to done + in-prod / issue-done (pull_request) Successful in 13s
Promote plan-issue Related on main merge / plan-issue-ready (pull_request) Has been skipped
Agent pool supervisor / tick (pull_request) Successful in 29s
CI / static (push) Successful in 2m15s
CI / int (push) Successful in 2m48s
CI / build (push) Successful in 2m37s
CI / e2e (2) (push) Successful in 5m56s
CI / e2e (1) (push) Failing after 17m21s
This commit is contained in:
parent
1dfad8ac99
commit
228d5da9c8
9 changed files with 284 additions and 9 deletions
|
|
@ -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)**: the `deploy` job (runs-on `host` on the workstation runner) streams `scripts/deploy-homeserver.sh` to the homeserver over SSH; the remote script 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` verde == site atualizado. Runbook (rollback, falhas conhecidas): `docs/ops/teqo-1313-deploy.md`. **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`); 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)**: the `deploy` job (runs-on `host` on the workstation runner) streams `scripts/deploy-homeserver.sh` to the homeserver over SSH; the remote script 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` verde == site atualizado. Runbook (rollback, falhas conhecidas): `docs/ops/teqo-1313-deploy.md`. **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.
|
||||
|
|
|
|||
|
|
@ -20,10 +20,12 @@ 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 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-18):** **OPS52-media-guard** — o modo reconcile (escrita) do `pnpm media:recover` ganhou guard de intenção explícita (achado do /simplify da #10): exige `MEDIA_RECOVER_CONFIRM=1` para armar a escrita no bucket — antes, bastava o conjunto completo de `S3_*` (cópia all-or-nothing de credenciais de prod num worktree + rows locais via db:pull → escrevia covers no bucket de prod; o echo do alvo mostrava, não impedia). `--dry-run`/`--verify` seguem sem flag (não escrevem). Sem a flag, reconcile recusa com exit 1 e a mensagem exibe o comando correto. Implementação no dono dos guards: `isTruthyEnv` novo em `scripts/lib/cli.mjs` (semântica exata `true|1` do `ALLOW_REMOTE_DB` — sem trim/case; `isRemoteDbOverrideSet` passou a delegar, sem twin), unit-testado com pin do comportamento preservado (`TRUE`/`yes` continuam recusados). Runbook do homeserver atualizado com a flag; nota no AGENTS.md.
|
||||
|
||||
**Recently resolved (2026-08-18):** **OPS57** — o safety net do `forgejo-pr-automerge` agora cobre **todas as PRs same-repo para `main`**, não só `cursor/*`. Motivação (débito deferido na OPS56): dos 25 PRs mergeados no repo, **zero** eram `cursor/*` — 9 de `plans/*`, 11 de worktrees humanos, 5 de `refs/pull/N/head` (era da migração) — o filtro antigo nunca cobriu nenhum PR real, e o incidente OPS52 (PR #27, humano, merge falhou silenciosamente) era exatamente a classe excluída. Mudanças: `if:` do job em `agent-pr-ready-automerge.yml` → `state == 'open' && head.repo.full_name == github.repository` (guarda de fork — o repo é público e uma PR de fork mergeada dispararia o deploy de prod; campos já usados no payload, sem dependência de `is_draft` do webhook); política de draft movida para o CLI `forgejo-pr-automerge.mjs` com o head ref do **re-read da API** — draft `cursor/*` → Ready (pool inalterado); draft não-`cursor/*` → skip exit 0 (**draft = veto do ator, PR humana nunca é forçada**); PRs ready não-`cursor/*` (humanas/plans) passam a mergear com CI verde — redundante no caminho feliz (o fluxo humano já arma `--auto --rebase`) e é o que pega a classe OPS52; `ready_for_review` em PR draft re-dispara o job e mergea. Docs sincronizadas (`docs/AGENT-OPS.md` tabela CI por alvo, `.agents/rules/agent-pr-workflow.mdc` seção Safety net). Rejeitado: `!is_draft` no YAML (campo do payload não verificado, falha fail-open) e allowlist de prefixos (códigos humanos sem prefixo comum — OPS/S1/PUB5). A própria PR desta entrega ainda usa o workflow de `main` (antigo); a próxima PR exercita o gatilho alargado. Sem migration/access/Consent/UI. Testes: nenhum novo (lógica de veredito já coberta pelos specs da OPS56; branch de draft é 3 linhas no CLI); gates unit/int verdes.
|
||||
|
||||
**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-18):** **OPS57** — o safety net do `forgejo-pr-automerge` agora cobre **todas as PRs same-repo para `main`**, não só `cursor/*`. Motivação (débito deferido na OPS56): dos 25 PRs mergeados no repo, **zero** eram `cursor/*` — 9 de `plans/*`, 11 de worktrees humanos, 5 de `refs/pull/N/head` (era da migração) — o filtro antigo nunca cobriu nenhum PR real, e o incidente OPS52 (PR #27, humano, merge falhou silenciosamente) era exatamente a classe excluída. Mudanças: `if:` do job em `agent-pr-ready-automerge.yml` → `state == 'open' && head.repo.full_name == github.repository` (guarda de fork — o repo é público e uma PR de fork mergeada dispararia o deploy de prod; campos já usados no payload, sem dependência de `is_draft` do webhook); política de draft movida para o CLI `forgejo-pr-automerge.mjs` com o head ref do **re-read da API** — draft `cursor/*` → Ready (pool inalterado); draft não-`cursor/*` → skip exit 0 (**draft = veto do ator, PR humana nunca é forçada**); PRs ready não-`cursor/*` (humanas/plans) passam a mergear com CI verde — redundante no caminho feliz (o fluxo humano já arma `--auto --rebase`) e é o que pega a classe OPS52; `ready_for_review` em PR draft re-dispara o job e mergea. Docs sincronizadas (`docs/AGENT-OPS.md` tabela CI por alvo, `.agents/rules/agent-pr-workflow.mdc` seção Safety net). Rejeitado: `!is_draft` no YAML (campo do payload não verificado, falha fail-open) e allowlist de prefixos (códigos humanos sem prefixo comum — OPS/S1/PUB5). A própria PR desta entrega ainda usa o workflow de `main` (antigo); a próxima PR exercita o gatilho alargado. Sem migration/access/Consent/UI. Testes: spec nova em `forgejoApi.unit.spec.ts` (waitForChecks para quando o PR vira draft no meio do wait — veto garantido mesmo em conversão mid-run; autoMerge idempotente); gates unit/int verdes. **Validação ao vivo na própria entrega:** o Forgejo roda o workflow do head da PR (não o de main), então a PR #48 foi mergeada pelo safety net alargado no primeiro uso real — o `if:` com `head.repo.full_name` avaliou true em PR real e o script mergeou por rebase. Fixes pós-review do /simplify: `waitForChecks`/`autoMerge` param em PR draft (antes: 405 ruidoso na conversão mid-run, contradizendo o "veto nunca tocado"); CLI valida `base.ref === 'main'` (invocação manual); comentário do fork guard documenta por que ele não pode descer pro CLI (checkout de fork roda o script do fork = RCE com o token do Actions); docs sem o mecanismo fantasma `gh pr` (era GitHub, OPS50 trocou por CLI plain-Node).
|
||||
|
||||
**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.
|
||||
|
|
|
|||
1
docs/changelog/2026-08-18-ops52-media-guard.md
Normal file
1
docs/changelog/2026-08-18-ops52-media-guard.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
**Recently resolved (2026-08-18):** **OPS52-media-guard** — o modo reconcile (escrita) do `pnpm media:recover` ganhou guard de intenção explícita (achado do /simplify da #10): exige `MEDIA_RECOVER_CONFIRM=1` para armar a escrita no bucket — antes, bastava o conjunto completo de `S3_*` (cópia all-or-nothing de credenciais de prod num worktree + rows locais via db:pull → escrevia covers no bucket de prod; o echo do alvo mostrava, não impedia). `--dry-run`/`--verify` seguem sem flag (não escrevem). Sem a flag, reconcile recusa com exit 1 e a mensagem exibe o comando correto. Implementação no dono dos guards: `isTruthyEnv` novo em `scripts/lib/cli.mjs` (semântica exata `true|1` do `ALLOW_REMOTE_DB` — sem trim/case; `isRemoteDbOverrideSet` passou a delegar, sem twin), unit-testado com pin do comportamento preservado (`TRUE`/`yes` continuam recusados). Runbook do homeserver atualizado com a flag; nota no AGENTS.md.
|
||||
142
docs/plans/ops52-media-guard-impl.md
Normal file
142
docs/plans/ops52-media-guard-impl.md
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
# Impl: OPS52-media-guard — Guard de intenção explícita para escrita em bucket de produção
|
||||
|
||||
Status: aprovado (gate humano 2026-08-18)
|
||||
Atualizado em: 2026-08-18
|
||||
Issue: #37 (OPS52-media-guard)
|
||||
Intenção: body da Issue #37 (spec — sem plano de intenção separado; achado do /simplify da #10)
|
||||
Appetite restante: pequeno (chore P3 — guard fail-closed de ~10 linhas + docs)
|
||||
|
||||
## Leitura da intenção
|
||||
|
||||
- **Outcome:** o modo reconcile (padrão) do `pnpm media:recover` só escreve no
|
||||
bucket com **intenção explícita** — `MEDIA_RECOVER_CONFIRM=1` — e recusa com
|
||||
mensagem clara sem ela; `--dry-run` e `--verify` seguem desprotegidos (não
|
||||
escrevem). O risco fechado: um worktree que receba credenciais S3\_\* de prod
|
||||
(cópia all-or-nothing do main) rodando reconcile contra rows locais
|
||||
(`db:pull` inclui media) escreveria covers no bucket de prod — hoje só o
|
||||
echo do alvo torna visível, não impede.
|
||||
- **O que NÃO negociar:** fail-closed (sem flag → sem escrita, em qualquer
|
||||
cenário de DB/alvo); dry-run/verify sem fricção nova; o runbook do
|
||||
homeserver continua sendo o contrato de execução; zero mudança de
|
||||
comportamento do reconcile além do novo gate.
|
||||
- **O que reavaliar:** a forma da confirmação — a Issue propõe
|
||||
"ex.: `MEDIA_RECOVER_CONFIRM=1` **ou** prompt interativo". Prompt interativo
|
||||
é pior para este repo: o runbook roda via SSH no homeserver (potencialmente
|
||||
sem TTY), e o padrão de guard do repo é env flag (`ALLOW_REMOTE_DB`,
|
||||
`isRemoteDbOverrideSet`). Env flag vence (ver Opções).
|
||||
|
||||
## Abordagem recomendada
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[pnpm media:recover] -->|parse args| B{mode == reconcile?}
|
||||
B -->|não dry-run/verify| C[segue desprotegido — zero escrita]
|
||||
B -->|sim| D{MEDIA_RECOVER_CONFIRM true/1?}
|
||||
D -->|não| E[die com o comando correto — sem S3 client, sem DB]
|
||||
D -->|sim| F[guards existentes + echo do alvo + reconcile]
|
||||
```
|
||||
|
||||
**Opções consideradas:**
|
||||
|
||||
- **A — Env flag `MEDIA_RECOVER_CONFIRM` (truthy `true|1`) obrigatória no
|
||||
reconcile**, com helper genérico `isTruthyEnv` em `scripts/lib/cli.mjs` e
|
||||
`isRemoteDbOverrideSet` delegando para ele (sem twin). **Recomendada.**
|
||||
- B — Prompt interativo (`readline`) antes do reconcile.
|
||||
- C — Flag CLI `--confirm` no parsing de args.
|
||||
|
||||
**Recomendação: A** — paridade total com o guard de DB existente
|
||||
(`ALLOW_REMOTE_DB=true|1`): funcionamento em runbook não-interativo (SSH,
|
||||
`set -a; source teqo-1313.env`), sem fricção para dry-run/verify, e o parse de
|
||||
args do script continua restrito aos modos mutuamente exclusivos
|
||||
(`--dry-run`/`--verify`).
|
||||
|
||||
**Rejeitadas:** B (trava/falha sem TTY — o runbook roda por SSH no homeserver;
|
||||
mais código; fora do padrão de guards do repo); C (mistura conceito de modo
|
||||
com flag de confirmação; env var é o padrão e já é usada no runbook — o
|
||||
comando `set -a; source` injeta as duas flags juntas).
|
||||
|
||||
### Componentes / mudanças
|
||||
|
||||
- **`isTruthyEnv`** (`scripts/lib/cli.mjs`, novo export puro): `value === 'true' || value === '1'`
|
||||
— **exatamente** a semântica atual de `isRemoteDbOverrideSet` (sem trim, sem
|
||||
lowercase: `TRUE`/`yes` continuam recusados — não relaxar guard existente na
|
||||
refatoração). `isRemoteDbOverrideSet` passa a delegar
|
||||
(`isTruthyEnv(process.env[ALLOW_REMOTE_DB_FLAG])`) — comportamento idêntico.
|
||||
- **`scripts/recover-media.mjs`**: constante `MEDIA_RECOVER_CONFIRM`;
|
||||
imediatamente após o parsing de modo (antes de `assertLocalDatabase`,
|
||||
`resolveS3StorageEnv` e de qualquer rede):
|
||||
`if (mode === 'reconcile' && !isTruthyEnv(process.env[MEDIA_RECOVER_CONFIRM])) die(...)`
|
||||
— mensagem pt-BR com o comando correto (`MEDIA_RECOVER_CONFIRM=1 pnpm media:recover`)
|
||||
e a alternativa `--dry-run`. Bloco de comentário do header (safety model +
|
||||
modes) atualizado para documentar o guard. O gate vale para reconcile com
|
||||
QUALQUER alvo de DB (local ou remoto) — o risco é a escrita no bucket, não a
|
||||
origem dos dados.
|
||||
- **`tests/unit/cliEnvFlags.unit.spec.ts`** (novo): `isTruthyEnv`
|
||||
(`true`/`1` passam; `TRUE`, `yes`, `0`, vazio, ausente recusam) e delegação
|
||||
de `isRemoteDbOverrideSet` preservando o comportamento (mesmo caso
|
||||
`ALLOW_REMOTE_DB=TRUE` recusado).
|
||||
- **Docs:** runbook do `docs/plans/ops52-media-recuperar-arquivos-impl.md`
|
||||
(passo 3 do runbook ganha `MEDIA_RECOVER_CONFIRM=1` + nota do guard no
|
||||
cabeçalho do passo 2/3); nota curta no AGENTS.md (parágrafo da media —
|
||||
`reconcile exige MEDIA_RECOVER_CONFIRM=1`); entrada
|
||||
`docs/changelog/2026-08-18-ops52-media-guard.md` + `pnpm changelog:build`.
|
||||
- **Migration:** nenhuma. **Access/Consent:** intocados. **UI:** N/A.
|
||||
- **`package.json`:** sem mudança (a flag é env, não arg do script).
|
||||
|
||||
## Fases verificáveis
|
||||
|
||||
1. **Helper + testes** — `isTruthyEnv` em `cli.mjs` + refactor de
|
||||
`isRemoteDbOverrideSet` + `tests/unit/cliEnvFlags.unit.spec.ts`; rodar
|
||||
`pnpm test:unit`.
|
||||
2. **Guard no script** — `recover-media.mjs` (gate + header doc); conferir
|
||||
comportamento: `pnpm media:recover` sem flag → exit 1 com a mensagem
|
||||
(com S3\_\* ausente local o guard do confirm dispara antes do die de S3 —
|
||||
ordem verificada); `--dry-run`/`--verify` seguem sem exigência.
|
||||
3. **Docs + changelog** — runbook (passo 3 + nota), AGENTS.md,
|
||||
`docs/changelog/2026-08-18-ops52-media-guard.md`, `pnpm changelog:build` +
|
||||
`pnpm changelog:check`.
|
||||
4. **Gates finais** — `pnpm gate:fast`, `pnpm format:check`, `pnpm exec knip`,
|
||||
`pnpm check:cycles`, `pnpm test` (unit+int), `pnpm build` local.
|
||||
|
||||
## Rabbit holes / Não escopo (engenharia)
|
||||
|
||||
- **Testar o reconcile ponta a ponta** — impossível por contrato (sem S3\_\* e
|
||||
sem DB prod nesta máquina); a prova é o runbook do homeserver (padrão da
|
||||
OPS52 fase 3). O que é testável (semântica do flag, preservação do
|
||||
`ALLOW_REMOTE_DB`) é unit-testado.
|
||||
- **Prompt interativo / TTY detection** — rejeitado (Opção B).
|
||||
- **Guard para `--dry-run`** — não: não escreve; o Issue pede explicitamente
|
||||
mantê-los desprotegidos.
|
||||
- **Renomear `ALLOW_REMOTE_DB`** ou mexer em outros scripts — fora de escopo;
|
||||
o refactor de `isRemoteDbOverrideSet` é só delegação (uma ortografia do
|
||||
truthy-env, sem mudança de aceite).
|
||||
- **Adiado com gatilho:** banir re-escritas `=== '1'`/`=== 'true'` de env-read
|
||||
no `codebaseConventions` — quando um 4º site aparecer ou no próximo refactor
|
||||
dos 3 existentes (ver `docs/plans/ops60-seed-posts-guard-escrita-s3.md`,
|
||||
seção Explicitamente fora).
|
||||
- **Registrado no simplify (OPS60, #49):** `db:seed:posts` escreve no bucket
|
||||
via adapter S3 sem flag de intenção — mesma classe de risco; Issue nova com
|
||||
`depends: [37]`.
|
||||
|
||||
## Riscos e mitigação
|
||||
|
||||
- **R1 — Refatorar `isRemoteDbOverrideSet` relaxa o guard de DB.**
|
||||
Mitigação: `isTruthyEnv` com a semântica EXATA atual (sem trim/lowercase) e
|
||||
teste de comportamento que pin o caso `TRUE` recusado.
|
||||
- **R2 — Runbook desatualizado manda rodar reconcile sem a flag e o guard
|
||||
recusa (fricção na próxima execução de prod).** Mitigação: runbook editado
|
||||
no mesmo PR; mensagem do die exibe o comando correto.
|
||||
- **R3 — Automação futura (CI/homeserver) esquece a flag.** Mitigação:
|
||||
mensagem de erro autoexplicativa + dry-run disponível como pre-flight;
|
||||
o contrato é do runbook.
|
||||
|
||||
## Aceite de engenharia
|
||||
|
||||
- [ ] Aceite de produto da intenção coberto: reconcile recusa sem
|
||||
`MEDIA_RECOVER_CONFIRM=1` (fail-closed); dry-run/verify desprotegidos
|
||||
- [ ] Invariantes AGENTS/engineering-standards: identificadores em inglês;
|
||||
guard reusado do dono (`cli.mjs`) sem twin; zero migration/access/Consent
|
||||
- [ ] Testes de domínio: `isTruthyEnv` + preservação de `isRemoteDbOverrideSet`
|
||||
(unit); gates da checklist do AGENTS rodados
|
||||
- [ ] Docs: runbook atualizado (comando real com a flag) + nota AGENTS.md +
|
||||
changelog
|
||||
|
|
@ -95,7 +95,9 @@ contratos de guard diferentes no mesmo script; o seed é local-first por design)
|
|||
`slugFromFilename(filename)` (remove a última extensão) e a resolução
|
||||
`resolveCoverSource(article)` (`article.coverUrl || inlineCoverUrl(...)`).
|
||||
Unit-testados (padrão dos módulos de `scripts/lib`).
|
||||
- **`scripts/recover-media.mjs`** (novo): fluxo —
|
||||
- **`scripts/recover-media.mjs`** (novo): fluxo — _(desde OPS52-media-guard,
|
||||
#37, o reconcile também exige `MEDIA_RECOVER_CONFIRM=1` antes de qualquer
|
||||
passo abaixo — ver `docs/plans/ops52-media-guard-impl.md`)_
|
||||
1. `loadCliEnv` + `assertLocalDatabase('media:recover', hint do runbook)` +
|
||||
`resolveS3StorageEnv(process.env)` (reuso de `src/utilities/mediaStorage.ts`;
|
||||
aborta se `enabled: false` — recuperação é operação de prod, nunca storage
|
||||
|
|
@ -199,9 +201,11 @@ Ordem das próximas execuções (pós-merge, humano):
|
|||
segundos. Se TODAS as rows derem `erro` (timeout/ECONNREFUSED), o container
|
||||
não alcança o Garage — repetir o diagnóstico do estado acima (firewall +
|
||||
extra_hosts + endpoint) antes de recuperar.
|
||||
3. **Recuperar:**
|
||||
3. **Recuperar** (o reconcile escreve no bucket — guard de intenção explícita
|
||||
OPS52-media-guard exige `MEDIA_RECOVER_CONFIRM=1`; sem ela o script recusa
|
||||
com a mensagem e o comando correto):
|
||||
```bash
|
||||
ALLOW_REMOTE_DB=true pnpm media:recover
|
||||
MEDIA_RECOVER_CONFIRM=1 ALLOW_REMOTE_DB=true pnpm media:recover
|
||||
```
|
||||
Esperado: 40 uploads (`PutObject` key = filename; idempotente por overwrite),
|
||||
exceções reais (sem cover / download 404) listadas no relatório — conferir
|
||||
|
|
|
|||
59
docs/plans/ops60-seed-posts-guard-escrita-s3.md
Normal file
59
docs/plans/ops60-seed-posts-guard-escrita-s3.md
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
# Plano: OPS60 — Guard de intenção explícita para escrita no bucket via `db:seed:posts`
|
||||
|
||||
Status: rascunho
|
||||
Atualizado em: 2026-08-18
|
||||
Issue: OPS60 (a registrar; achado do /simplify da #37)
|
||||
Depends: [37] (destrava quando o pai flipar done)
|
||||
Pai do achado: #10 (OPS52-media — quem fiou o S3 no seed)
|
||||
Priority: P2
|
||||
Kind: chore
|
||||
|
||||
## Problema
|
||||
|
||||
Achado do /simplify da OPS52-media-guard (#37, reviewer 1): o `pnpm db:seed:posts`
|
||||
(`scripts/seed-posts.mjs:347-348`) faz upload das capas através do adapter de
|
||||
media configurado — **Garage S3 quando as `S3_*` estão setadas** — sem flag de
|
||||
intenção dedicada. O provisionador de worktree copia `S3_*` all-or-nothing
|
||||
(`scripts/lib/worktree-env.mjs:58-77`), então um worktree com credenciais de
|
||||
prod rodando o seed contra um DB local escreve covers no bucket de **prod** —
|
||||
a mesma classe de risco que a #37 fechou para `media:recover`, ainda aberta no
|
||||
seed. O seed é idempotente por slug e sobrescreve o key determinístico — a
|
||||
escrita é silenciosa e "inofensiva" na aparência.
|
||||
|
||||
## Aceite
|
||||
|
||||
- Rodar `db:seed:posts` (ou o upload de media do seed) contra bucket `S3_*` de
|
||||
prod exige intenção explícita (ex.: `SEED_MEDIA_CONFIRM=1` ou equivalente do
|
||||
mesmo padrão `isTruthyEnv` da #37), fail-closed — sem a flag, o seed **não**
|
||||
escreve media no bucket (ou recusa apontar para bucket não-local).
|
||||
- O comportamento local (sem `S3_*` → disk) e o upload via admin/CI de prod
|
||||
ficam intactos; dry-run/semântica de guard do seed preservada.
|
||||
- Guard no dono: `isTruthyEnv` (cli.mjs, já existe da #37) — sem twin.
|
||||
|
||||
## Direção no codebase
|
||||
|
||||
- `scripts/seed-posts.mjs` — ponto de escrita da media; reusar
|
||||
`resolveS3StorageEnv` + `isTruthyEnv`.
|
||||
- Alternativas a decidir na execução (gate humano): flag de env exigida para
|
||||
escrita com S3 ativo (paridade #37) vs exigir bucket local/explicitado;
|
||||
`--dry-run` do seed já existente como escape de planejamento.
|
||||
- Runbook/AGENTS.md com o comando real; changelog.
|
||||
|
||||
## Já resolvido no simplify (não reabrir)
|
||||
|
||||
- Nada adicional: o achado desta Issue é o único do lote.
|
||||
|
||||
## Explicitamente fora (descartes + defers com gatilho)
|
||||
|
||||
- **Defer:** banir re-escritas `=== '1'`/`=== 'true'` de env-read no
|
||||
`codebaseConventions.unit.spec.ts` — gatilho: quando um 4º site aparecer ou
|
||||
no próximo refactor dos 3 sites existentes (`run-e2e-affected.mjs`,
|
||||
`worktree.mjs`, `agent-pool-state.mjs` — os dois primeiros são subset
|
||||
estrito e o terceiro é outro domínio).
|
||||
- **Descartados:** echo do confirm no target summary; die com
|
||||
`ALLOW_REMOTE_DB`; rename `isTruthyEnv`; grep-invariante do wiring do guard
|
||||
(contrato é o runbook; teste de linha de guard é frágil).
|
||||
|
||||
## Appetite
|
||||
|
||||
~0,5 dia eng (um outcome verificável; sem migration/UI).
|
||||
|
|
@ -72,8 +72,14 @@ if (gatewayHost) LOCAL_HOSTS.add(gatewayHost)
|
|||
|
||||
export const ALLOW_REMOTE_DB_FLAG = 'ALLOW_REMOTE_DB'
|
||||
|
||||
export const isRemoteDbOverrideSet = () =>
|
||||
process.env[ALLOW_REMOTE_DB_FLAG] === 'true' || process.env[ALLOW_REMOTE_DB_FLAG] === '1'
|
||||
/**
|
||||
* Truthy intent-flag check (the one spelling of the `true`/`1` semantics).
|
||||
* Deliberately exact: no trim, no case-folding — `TRUE` or `yes` stay refused
|
||||
* (relaxing a guard is a product decision, not a refactor byproduct).
|
||||
*/
|
||||
export const isTruthyEnv = (value) => value === 'true' || value === '1'
|
||||
|
||||
export const isRemoteDbOverrideSet = () => isTruthyEnv(process.env[ALLOW_REMOTE_DB_FLAG])
|
||||
|
||||
/**
|
||||
* Port for `next dev` (OPS40). Next's CLI resolves its port via commander's
|
||||
|
|
|
|||
|
|
@ -17,6 +17,11 @@
|
|||
* (resolveS3StorageEnv throws on a partial config) — recovery is a
|
||||
* production-bucket operation, never local storage; --verify skips storage
|
||||
* entirely (it only HEADs public URLs).
|
||||
* - Reconcile writes objects, so it additionally requires the explicit
|
||||
* intent flag MEDIA_RECOVER_CONFIRM=1 (OPS52-media-guard): the S3_* envs
|
||||
* alone must never arm a write — a worktree with a prod-credentials copy
|
||||
* running reconcile against local rows would write the prod bucket.
|
||||
* --dry-run/--verify stay guard-free: they never write.
|
||||
* - Refuses a non-local DATABASE_URL unless ALLOW_REMOTE_DB=true is set
|
||||
* explicitly, and echoes the target (DB host, endpoint, bucket, verify
|
||||
* origin) before any write.
|
||||
|
|
@ -29,7 +34,9 @@
|
|||
* Modes:
|
||||
* pnpm media:recover reconcile — plan + upload every
|
||||
* recoverable cover (PutObject overwrites,
|
||||
* idempotent by filename)
|
||||
* idempotent by filename); requires
|
||||
* MEDIA_RECOVER_CONFIRM=1 (explicit
|
||||
* write-intent guard)
|
||||
* pnpm media:recover --dry-run plan only — resolve covers + headObject
|
||||
* each key (present/missing), zero writes
|
||||
* pnpm media:recover --verify acceptance check — HEAD every filename
|
||||
|
|
@ -44,7 +51,7 @@ import { NodeHttpHandler } from '@smithy/node-http-handler'
|
|||
import { getPayload } from 'payload'
|
||||
|
||||
import { assertLocalDatabase } from './assert-local-database.mjs'
|
||||
import { dieWithLabel, loadCliEnv } from './lib/cli.mjs'
|
||||
import { dieWithLabel, isTruthyEnv, loadCliEnv } from './lib/cli.mjs'
|
||||
import {
|
||||
fetchArticlesFromWordPress,
|
||||
resolveCoverDownloadUrl,
|
||||
|
|
@ -58,6 +65,8 @@ loadCliEnv()
|
|||
|
||||
const die = dieWithLabel('media:recover')
|
||||
|
||||
const RECOVER_CONFIRM_FLAG = 'MEDIA_RECOVER_CONFIRM'
|
||||
|
||||
const PROD_PUBLIC_ORIGIN = process.env.NEXT_PUBLIC_SITE_URL || 'https://jorgesolla1313.com.br'
|
||||
|
||||
const args = new Set(process.argv.slice(2))
|
||||
|
|
@ -67,6 +76,14 @@ if (args.size > 1)
|
|||
die('modos são mutuamente exclusivos: use apenas um de --dry-run / --verify (ou nenhum).')
|
||||
const mode = args.has('--dry-run') ? 'dry-run' : args.has('--verify') ? 'verify' : 'reconcile'
|
||||
|
||||
if (mode === 'reconcile' && !isTruthyEnv(process.env[RECOVER_CONFIRM_FLAG])) {
|
||||
die(
|
||||
'modo reconcile ESCREVE objetos no bucket — exige confirmação explícita de intenção.\n' +
|
||||
` Re-rodar com: ${RECOVER_CONFIRM_FLAG}=1 pnpm media:recover\n` +
|
||||
' (ou use --dry-run para planejar sem escrever; --verify só HEADs URLs públicas).',
|
||||
)
|
||||
}
|
||||
|
||||
assertLocalDatabase(
|
||||
'media:recover',
|
||||
`Este script LÊ o banco de produção (rows de media) e ESCREVE apenas objetos no bucket S3 das envs S3_*.\n` +
|
||||
|
|
|
|||
44
tests/unit/cliEnvFlags.unit.spec.ts
Normal file
44
tests/unit/cliEnvFlags.unit.spec.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
// @vitest-environment node
|
||||
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
|
||||
import { ALLOW_REMOTE_DB_FLAG, isRemoteDbOverrideSet, isTruthyEnv } from '../../scripts/lib/cli.mjs'
|
||||
|
||||
describe('isTruthyEnv (OPS52-media-guard — one spelling of the true/1 intent-flag semantics)', () => {
|
||||
it('accepts exactly true and 1', () => {
|
||||
expect(isTruthyEnv('true')).toBe(true)
|
||||
expect(isTruthyEnv('1')).toBe(true)
|
||||
})
|
||||
|
||||
it('refuses every other value — including case variants and empty/absent', () => {
|
||||
for (const value of ['TRUE', 'True', 'yes', 'on', '0', 'false', '', ' ', '2']) {
|
||||
expect(isTruthyEnv(value), JSON.stringify(value)).toBe(false)
|
||||
}
|
||||
expect(isTruthyEnv(undefined)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isRemoteDbOverrideSet (behavior preserved through the delegation refactor)', () => {
|
||||
const original = process.env[ALLOW_REMOTE_DB_FLAG]
|
||||
|
||||
afterEach(() => {
|
||||
if (original === undefined) delete process.env[ALLOW_REMOTE_DB_FLAG]
|
||||
else process.env[ALLOW_REMOTE_DB_FLAG] = original
|
||||
})
|
||||
|
||||
it('still passes for true/1', () => {
|
||||
process.env[ALLOW_REMOTE_DB_FLAG] = 'true'
|
||||
expect(isRemoteDbOverrideSet()).toBe(true)
|
||||
process.env[ALLOW_REMOTE_DB_FLAG] = '1'
|
||||
expect(isRemoteDbOverrideSet()).toBe(true)
|
||||
})
|
||||
|
||||
it('still refuses TRUE/yes/empty/absent', () => {
|
||||
for (const value of ['TRUE', 'yes', '', '0', 'false']) {
|
||||
process.env[ALLOW_REMOTE_DB_FLAG] = value
|
||||
expect(isRemoteDbOverrideSet(), JSON.stringify(value)).toBe(false)
|
||||
}
|
||||
delete process.env[ALLOW_REMOTE_DB_FLAG]
|
||||
expect(isRemoteDbOverrideSet()).toBe(false)
|
||||
})
|
||||
})
|
||||
Loading…
Reference in a new issue