diff --git a/.env.example b/.env.example index f2b0d4c0..48a18e53 100644 --- a/.env.example +++ b/.env.example @@ -88,3 +88,10 @@ GOOGLE_CALENDAR_OAUTH_CLIENT_SECRET= # point it at a fake binary, and a host without ffmpeg can install it or set # this to a wrapper. FFMPEG_PATH= + +# C163/C190 — Portal da Transparência API key used by the city/dossiê report +# scripts to fetch official emendas (`JORGE SOLLA`). Optional: without it the +# official emendas block degrades to an explicit gap (never zero silently). +# `pnpm worktree next` mirrors this key into the worktree `.env.local` when it +# is set here, so report builds run with the same source as the main checkout. +PORTAL_TRANSPARENCIA_API_KEY= diff --git a/docs/changelog/2026-09-21-worktree-portal-transparencia-key.md b/docs/changelog/2026-09-21-worktree-portal-transparencia-key.md new file mode 100644 index 00000000..db713786 --- /dev/null +++ b/docs/changelog/2026-09-21-worktree-portal-transparencia-key.md @@ -0,0 +1 @@ +**Worktree espelha `PORTAL_TRANSPARENCIA_API_KEY` no `.env.local` (2026-09-21):** o provisionamento por worktree copiava `PAYLOAD_SECRET`/`S3_*`/VAPID/Resend, mas não a chave do Portal da Transparência — então `node scripts/build-city-report.mjs` num worktree degradava as emendas oficiais para lacuna (`Chave da API do Portal da Transparência ausente.`) mesmo com a chave presente no checkout principal (visto no relatório de Miguel Calmon, C163). Correção no dono: a lista de envs espelhadas saiu do inline de `scripts/worktree.mjs` para `MIRRORED_WORKTREE_ENV_KEYS` + `mirroredEnvCopiedLines` em `scripts/lib/worktree-env.mjs` (função pura, unit-testada) e agora inclui `PORTAL_TRANSPARENCIA_API_KEY`; `.env.example` documenta a chave e o espelhamento. Sem schema, sem migration e sem mudança de URL pública — scripts + teste + doc. diff --git a/scripts/lib/worktree-env.mjs b/scripts/lib/worktree-env.mjs index 36af5b98..6dbe7610 100644 --- a/scripts/lib/worktree-env.mjs +++ b/scripts/lib/worktree-env.mjs @@ -77,6 +77,36 @@ export const s3EnvCopiedLines = (env) => ? [...S3_WORKTREE_REQUIRED_KEYS, 'S3_REGION'].map((key) => `${key}=${env[key]}`) : [] +/** + * Optional env keys mirrored from the main checkout into a worktree's + * `.env.local` when present there (PAYLOAD_SECRET, DB and port come from the + * caller). This list is the single owner of "what `pnpm worktree next` + * copies": a key that exists in the main env but is missing here silently + * degrades every worktree — `PORTAL_TRANSPARENCIA_API_KEY` is the report + * scripts' case (C163/C190: without the mirror, official emendas became a + * gap in the worktree even though the main env had the key). + */ +const MIRRORED_WORKTREE_ENV_KEYS = [ + 'NEXT_PUBLIC_VAPID_PUBLIC_KEY', + 'VAPID_PUBLIC_KEY', + 'VAPID_PRIVATE_KEY', + 'VAPID_SUBJECT', + 'RESEND_API_KEY', + 'CAMPAIGN_EMAIL_FROM', + 'CAMPAIGN_EMAIL_FROM_NAME', + 'PORTAL_TRANSPARENCIA_API_KEY', +] + +/** + * `KEY=value` lines for the mirrored keys present in `env`, in list order. + * An absent/empty key copies nothing (same fail-open semantics as before). + * + * @param {Record} env + * @returns {string[]} + */ +export const mirroredEnvCopiedLines = (env) => + MIRRORED_WORKTREE_ENV_KEYS.filter((key) => env[key]).map((key) => `${key}=${env[key]}`) + /** * Final environment for a branch. `takenSlots` = slots already claimed by * OTHER live worktrees; bumps +1 until free (bounded — live slot sets are diff --git a/scripts/worktree.mjs b/scripts/worktree.mjs index e415f24d..063855da 100644 --- a/scripts/worktree.mjs +++ b/scripts/worktree.mjs @@ -182,6 +182,7 @@ import { DEV_PORT_BASE, GENERATED_ENV_MARKER, isGeneratedDatabaseName, + mirroredEnvCopiedLines, s3EnvCopiedLines, worktreeEnvFileContents, worktreeEnvironment, @@ -503,8 +504,6 @@ const provision = async ({ dir, branch, issue, env, skipMigrate, mainRoot, purpo const devUrl = `postgresql://teqo:teqo@localhost:5432/${devDatabase}` const testUrl = `postgresql://teqo:teqo@localhost:5432/${testDatabase}` - const copy = (key) => (mainEnv[key] ? [`${key}=${mainEnv[key]}`] : []) - const issueLabel = issue ? ` · issue #${issue.number}` : '' const generatedBy = `gerado por pnpm worktree ${purpose}` @@ -517,13 +516,9 @@ const provision = async ({ dir, branch, issue, env, skipMigrate, mainRoot, purpo copiedLines: [ // Media storage (OPS52): all-or-nothing (see s3EnvCopiedLines). ...s3EnvCopiedLines(mainEnv), - ...copy('NEXT_PUBLIC_VAPID_PUBLIC_KEY'), - ...copy('VAPID_PUBLIC_KEY'), - ...copy('VAPID_PRIVATE_KEY'), - ...copy('VAPID_SUBJECT'), - ...copy('RESEND_API_KEY'), - ...copy('CAMPAIGN_EMAIL_FROM'), - ...copy('CAMPAIGN_EMAIL_FROM_NAME'), + // Optional secrets mirrored from the main env (single owner: + // MIRRORED_WORKTREE_ENV_KEYS in scripts/lib/worktree-env.mjs). + ...mirroredEnvCopiedLines(mainEnv), ], }) writeFileSync(join(dir, '.env.local'), devLines.join('\n')) diff --git a/tests/unit/worktree.unit.spec.ts b/tests/unit/worktree.unit.spec.ts index bba35bcf..f0c6d77f 100644 --- a/tests/unit/worktree.unit.spec.ts +++ b/tests/unit/worktree.unit.spec.ts @@ -8,6 +8,7 @@ import { GENERATED_ENV_MARKER, hashSlotOf, isGeneratedDatabaseName, + mirroredEnvCopiedLines, numericSlotOfCode, s3EnvCopiedLines, testDatabaseForSlot, @@ -886,6 +887,33 @@ describe('s3EnvCopiedLines (OPS52 media storage, all-or-nothing)', () => { }) }) +describe('mirroredEnvCopiedLines (optional envs mirrored into the worktree)', () => { + it('copies PORTAL_TRANSPARENCIA_API_KEY from the main env (C163/C190 reports)', () => { + expect(mirroredEnvCopiedLines({ PORTAL_TRANSPARENCIA_API_KEY: 'eb32-key' })).toEqual([ + 'PORTAL_TRANSPARENCIA_API_KEY=eb32-key', + ]) + }) + + it('copies every mirrored key present, in a stable order, skipping absent ones', () => { + expect( + mirroredEnvCopiedLines({ + RESEND_API_KEY: 're_1', + VAPID_PRIVATE_KEY: 'priv', + PORTAL_TRANSPARENCIA_API_KEY: 'eb32-key', + }), + ).toEqual([ + 'VAPID_PRIVATE_KEY=priv', + 'RESEND_API_KEY=re_1', + 'PORTAL_TRANSPARENCIA_API_KEY=eb32-key', + ]) + }) + + it('copies nothing when no mirrored key is set', () => { + expect(mirroredEnvCopiedLines({ DATABASE_URL: 'x' })).toEqual([]) + expect(mirroredEnvCopiedLines({ PORTAL_TRANSPARENCIA_API_KEY: '' })).toEqual([]) + }) +}) + describe('worktreeEnvFileContents (dev/test env parity)', () => { const env = worktreeEnvironment({ branch: 'C15-fullcalendar', code: 'C15' }) const lines = worktreeEnvFileContents({