fix(worktree): espelha PORTAL_TRANSPARENCIA_API_KEY no env provisionado

A lista de envs espelhadas sai do inline do worktree.mjs para o helper puro
MIRRORED_WORKTREE_ENV_KEYS/mirroredEnvCopiedLines (worktree-env.mjs, unit-testado)
e passa a incluir a chave do Portal da Transparência: sem ela, build-city-report
num worktree degradava as emendas oficiais para lacuna mesmo com a chave no
checkout principal. .env.example documenta a chave e o espelhamento.
This commit is contained in:
Francisco Solla 2026-09-21 13:00:33 -03:00
parent 162dc8d017
commit f29e8357e0
5 changed files with 70 additions and 9 deletions

View file

@ -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=

View file

@ -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.

View file

@ -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<string, string | undefined>} 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

View file

@ -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'))

View file

@ -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({