/** * 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 ?? '') /** * 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}`, '', ], } }