mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 06:35:21 +00:00
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.
174 lines
6.5 KiB
JavaScript
174 lines
6.5 KiB
JavaScript
/**
|
|
* Pure helpers for the per-worktree environment provisioning in
|
|
* `scripts/worktree.mjs` — deterministic port + database names derived from
|
|
* the branch, so parallel agents (each in their own worktree) never fight
|
|
* over port 3000, the shared `teqo_test` database, or a second Postgres
|
|
* container on 5432.
|
|
*
|
|
* Contract: `slot` is the single collision domain. Port and BOTH database
|
|
* names derive from the final slot, so a bumped slot frees all three at
|
|
* once. Deterministic for a given (branch, code, live taken-slots set) —
|
|
* the same "depends on live state" determinism the claim-queue branch picker
|
|
* already has.
|
|
*/
|
|
|
|
/** Dev-server ports start here: slot 15 → http://localhost:3115. */
|
|
export const DEV_PORT_BASE = 3100
|
|
|
|
/** Marker line identifying a generated env file (never overwrite manual ones). */
|
|
export const GENERATED_ENV_MARKER = '# teqo worktree env (generated by pnpm worktree next)'
|
|
|
|
/** Slot cap — ports above 3100 + 999 would leave the usual ephemeral range. */
|
|
const MAX_SLOT = 999
|
|
|
|
/** First run of digits in the issue code (`C15` → 15, `B164` → 164); null when none. */
|
|
export const numericSlotOfCode = (code) => {
|
|
const match = /([0-9]+)/.exec(code ?? '')
|
|
return match ? Number(match[1]) : null
|
|
}
|
|
|
|
/** Stable 32-bit FNV-1a hash → slot in [0, MAX_SLOT] for codes without digits. */
|
|
export const hashSlotOf = (value) => {
|
|
let hash = 0x811c9dc5
|
|
for (const byte of Buffer.from(String(value), 'utf8')) {
|
|
hash ^= byte
|
|
hash = Math.imul(hash, 0x01000193) >>> 0
|
|
}
|
|
return hash % (MAX_SLOT + 1)
|
|
}
|
|
|
|
/** The slot a branch prefers before collision resolution. */
|
|
const preferredSlotForBranch = (branch, code) => {
|
|
const numeric = numericSlotOfCode(code)
|
|
return numeric !== null && numeric <= MAX_SLOT ? numeric : hashSlotOf(branch)
|
|
}
|
|
|
|
export const devPortForSlot = (slot) => DEV_PORT_BASE + slot
|
|
export const devDatabaseForSlot = (slot) => `teqo_wt${slot}`
|
|
export const testDatabaseForSlot = (slot) => `teqo_wt${slot}_test`
|
|
|
|
/** DB name charset guard — derived names are fed into `CREATE DATABASE` SQL. */
|
|
export const isGeneratedDatabaseName = (name) => /^teqo_wt[0-9]+(_test)?$/.test(name ?? '')
|
|
|
|
/**
|
|
* Media storage (OPS52) — the four required `S3_*` keys, mirrored from
|
|
* `resolveS3StorageEnv` in `src/utilities/mediaStorage.ts` (scripts cannot
|
|
* import the TS module). `S3_REGION` is optional (default `garage`).
|
|
*/
|
|
const S3_WORKTREE_REQUIRED_KEYS = [
|
|
'S3_BUCKET',
|
|
'S3_ENDPOINT',
|
|
'S3_ACCESS_KEY_ID',
|
|
'S3_SECRET_ACCESS_KEY',
|
|
]
|
|
|
|
/**
|
|
* `S3_*` lines to copy into a worktree env — ALL or NOTHING: a partial set
|
|
* would abort the worktree's boot (fail-closed `resolveS3StorageEnv`), so a
|
|
* partial main-env copies nothing and the worktree falls back to local disk
|
|
* storage. The dev is expected to keep the main env's bucket out of
|
|
* production (`.env.example` contract).
|
|
*
|
|
* @param {Record<string, string | undefined>} env
|
|
* @returns {string[]}
|
|
*/
|
|
export const s3EnvCopiedLines = (env) =>
|
|
S3_WORKTREE_REQUIRED_KEYS.every((key) => env[key])
|
|
? [...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
|
|
* tiny). Every value a consumer needs derives from the one final slot.
|
|
*/
|
|
export const worktreeEnvironment = ({ branch, code, takenSlots = new Set() }) => {
|
|
let slot = preferredSlotForBranch(branch, code)
|
|
while (takenSlots.has(slot)) slot += 1
|
|
return {
|
|
slot,
|
|
devPort: devPortForSlot(slot),
|
|
devDatabase: devDatabaseForSlot(slot),
|
|
testDatabase: testDatabaseForSlot(slot),
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Line arrays for `.env.local` (dev) and `.env.test.local` (test) of a
|
|
* provisioned worktree. `NEXT_PUBLIC_SITE_URL` and `PLAYWRIGHT_BASE_URL` must
|
|
* point BOTH files at the SAME slot port — the parity is what keeps a parallel
|
|
* e2e run in another worktree from falling back to port 3000 (OPS20: they had
|
|
* drifted — the dev file carried PLAYWRIGHT_BASE_URL, the test file didn't, and
|
|
* playwright.config.ts only reads `.env.test.local`). Building both from one
|
|
* `url` makes the drift structurally impossible.
|
|
*
|
|
* @param {{ branch: string, issueLabel?: string, generatedBy: string, env: { slot: number, devPort: number, devDatabase: string, testDatabase: string }, payloadSecret: string, copiedLines?: string[] }} options
|
|
* @returns {{ dev: string[], test: string[] }}
|
|
*/
|
|
export const worktreeEnvFileContents = ({
|
|
branch,
|
|
issueLabel = '',
|
|
generatedBy,
|
|
env,
|
|
payloadSecret,
|
|
copiedLines = [],
|
|
}) => {
|
|
// Both DATABASE_URLs derive from the same slot as the port (module contract).
|
|
const devUrl = `postgresql://teqo:teqo@localhost:5432/${env.devDatabase}`
|
|
const testUrl = `postgresql://teqo:teqo@localhost:5432/${env.testDatabase}`
|
|
const url = `http://localhost:${env.devPort}`
|
|
const header = [
|
|
GENERATED_ENV_MARKER,
|
|
`# branch ${branch}${issueLabel} · slot ${env.slot} — ${generatedBy}`,
|
|
]
|
|
return {
|
|
dev: [
|
|
...header,
|
|
`DATABASE_URL=${devUrl}`,
|
|
`PORT=${env.devPort}`,
|
|
`NEXT_PUBLIC_SITE_URL=${url}`,
|
|
`PLAYWRIGHT_BASE_URL=${url}`,
|
|
`PAYLOAD_SECRET=${payloadSecret}`,
|
|
...copiedLines,
|
|
'',
|
|
],
|
|
test: [
|
|
...header,
|
|
'# Lido DEPOIS de .env.test (vitest.setup.ts / playwright.config.ts) — suites isoladas.',
|
|
`DATABASE_URL=${testUrl}`,
|
|
`NEXT_PUBLIC_SITE_URL=${url}`,
|
|
`PLAYWRIGHT_BASE_URL=${url}`,
|
|
'',
|
|
],
|
|
}
|
|
}
|