mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 23:25:18 +00:00
O .env.test.local (que o playwright.config.ts lê) passa a carregar PLAYWRIGHT_BASE_URL na porta do slot (3100+slot); antes só o .env.local (que o config não lê) o tinha, então dois test:e2e em worktrees paralelos subiam dev servers na porta 3000. Causa-raiz tratada: os dois blocos env inline do provision viraram um builder puro único (worktreeEnvFileContents em scripts/lib/worktree-env.mjs) que deriva NEXT_PUBLIC_SITE_URL + PLAYWRIGHT_BASE_URL do mesmo devPort para ambos os arquivos — paridade estrutural, drift impossível + unit tests de paridade. playwright.config.ts intocado (a variável certa já existia).
117 lines
4.5 KiB
JavaScript
117 lines
4.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 ?? '')
|
|
|
|
/**
|
|
* 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}`,
|
|
'',
|
|
],
|
|
}
|
|
}
|