teqo/scripts/lib/cli.mjs
Francisco Solla 02f68894c3
Some checks failed
Agent PR Ready + auto-merge / ready-automerge (pull_request) Has been skipped
CI (PR) / docs-guards (pull_request) Successful in 6s
CI (PR) / plans-only-closes (pull_request) Successful in 6s
CI (PR) / scope (pull_request) Successful in 31s
CI (PR) / lint (pull_request) Successful in 40s
CI (PR) / format (pull_request) Successful in 48s
CI (PR) / typecheck (pull_request) Successful in 31s
CI (PR) / knip (pull_request) Successful in 32s
CI (PR) / cycles (pull_request) Successful in 33s
CI (PR) / unit (pull_request) Successful in 46s
CI (PR) / build (pull_request) Has been skipped
CI (PR) / int (pull_request) Failing after 1m5s
CI (PR) / e2e (pull_request) Failing after 2m47s
CI (PR) / checks (pull_request) Failing after 1s
fix(ci OPS50): guards de banco aceitam o gateway da bridge do runner (RFC1918) — postgres do services: fica no IP do gateway, não em localhost
2026-08-17 03:46:16 -03:00

161 lines
6.1 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Shared CLI skeleton for scripts/*.mjs (Pass 3 P3-H; the `scripts/lib/` home
* exists since B5 F2). `die`, `sha256`, the download→cache→sha256 prologue,
* `writeText`, the dotenv preamble and the local-host list were each spelled
* 5–7 times — the newest script copied the OLD shape, i.e. live drift. The
* convention guard in `codebaseConventions.unit.spec.ts` fails the build on
* any of these re-spelled outside this module.
*/
import { createHash } from 'node:crypto'
import { readFileSync } from 'node:fs'
import { access, mkdir, readFile, writeFile } from 'node:fs/promises'
import { createRequire } from 'node:module'
import { dirname, join } from 'node:path'
const require = createRequire(import.meta.url)
/** Labelled `die` factory: `dieWithLabel('seed:tse')` → the script's `die`. */
export const dieWithLabel = (label) => (message) => {
console.error(`\n[${label}] ${message}\n`)
process.exit(1)
}
export const sha256Hex = (buffer) => createHash('sha256').update(buffer).digest('hex')
/** The dotenv preamble every DB-touching script needs (.env.local wins over .env). */
export const loadCliEnv = () => {
const { config: loadEnv } = require('dotenv')
loadEnv({ path: '.env.local' })
loadEnv({ path: '.env' })
}
/** Local database hosts — including the docker-compose service name. THE one list. */
export const LOCAL_HOSTS = new Set(['localhost', '127.0.0.1', '0.0.0.0', '::1', 'postgres'])
/**
* Gateway of the default route, read from /proc/net/route (Linux). The
* self-hosted Forgejo runner publishes each job's `services:` Postgres on the
* bridge gateway IP (192.168.x.1 — differs per job), so a job reaches it via
* that IP instead of localhost. The same octet order the ci-pr.yml awk
* produces (the /proc fields are little-endian). Returns null when there is
* no default route or the file is unreadable (macOS/Windows dev boxes).
*/
export const defaultGatewayHost = () => {
try {
const routes = readFileSync('/proc/net/route', 'utf8').split('\n')
for (const line of routes) {
const fields = line.trim().split(/\s+/)
if (fields[1] !== '00000000') continue
const hex = fields[2] ?? ''
if (hex === '00000000' || hex.length < 8) return null
const octets = [
parseInt(hex.slice(6, 8), 16),
parseInt(hex.slice(4, 6), 16),
parseInt(hex.slice(2, 4), 16),
parseInt(hex.slice(0, 2), 16),
]
const isRfc1918 =
octets[0] === 10 ||
(octets[0] === 172 && octets[1] >= 16 && octets[1] <= 31) ||
(octets[0] === 192 && octets[1] === 168)
if (!isRfc1918) return null
return octets.join('.')
}
} catch {
// /proc/net/route is Linux-only; fall through to the static list.
}
return null
}
const gatewayHost = defaultGatewayHost()
if (gatewayHost) LOCAL_HOSTS.add(gatewayHost)
export const ALLOW_REMOTE_DB_FLAG = 'ALLOW_REMOTE_DB'
export const isRemoteDbOverrideSet = () =>
process.env[ALLOW_REMOTE_DB_FLAG] === 'true' || process.env[ALLOW_REMOTE_DB_FLAG] === '1'
/**
* Port for `next dev` (OPS40). Next's CLI resolves its port via commander's
* `.env('PORT')` BEFORE `@next/env` loads `.env.local`, so a `PORT` written by
* the worktree provisioner was silently ignored and every `pnpm dev` bound
* 3000. `loadCliEnv()` has already merged real env > `.env.local` > `.env`
* (override:false) into `process.env`, so reading `env.PORT` gives exactly the
* precedence the repo wants: real env (Playwright's webServer) wins, then the
* worktree's file, then no port at all (Next's 3000 default + allowRetry).
* Returns null when absent (caller omits `-p`); throws fail-closed on a
* non-empty invalid value (PORT=abc must not silently fall back to 3000).
*
* @param {Record<string, string | undefined>} [env]
*/
export const resolveDevPort = (env = process.env) => {
const raw = (env.PORT ?? '').trim()
if (raw === '') return null
const port = Number(raw)
if (!Number.isInteger(port) || port < 1 || port > 65535) {
throw new Error(`PORT inválida: ${JSON.stringify(raw)} (esperado 1..65535)`)
}
return port
}
/**
* `next dev` argv for a resolved port. The port MUST go as a CLI flag — the
* env-file value never reaches commander's `env('PORT')`. A null port omits
* `-p`, keeping Next's default 3000 and its "retry a free port" behavior.
*/
export const nextDevArgs = (port) => (port === null ? ['dev'] : ['dev', '-p', String(port)])
/**
* download → cache → sha256 with provenance logging. `ext` picks the cache
* filename; when it is 'json' the parsed body rides along. `expectedSha256`
* fails hard on mismatch (provenance-pinned sources, e.g. TSE zips).
* `download` is injected (`downloadToBuffer` from src/lib/electionResultsZip.ts
* needs the script's tsx loader, so it cannot be imported here).
*/
export const ensureCachedDownload = async ({
label,
key,
url,
ext,
cacheDir,
expectedSha256,
download,
}) => {
await mkdir(cacheDir, { recursive: true })
const path = join(cacheDir, `${key}.${ext}`)
let buffer
try {
await access(path)
console.log(`[${label}] cache hit ${path}`)
buffer = await readFile(path)
} catch (error) {
if (error?.code !== 'ENOENT') throw error
console.log(`[${label}] downloading ${url}`)
buffer = await download(url)
await writeFile(path, buffer)
console.log(`[${label}] saved ${path} (${buffer.length} bytes)`)
}
const hash = sha256Hex(buffer)
if (expectedSha256 && hash !== expectedSha256) {
dieWithLabel(label)(
`SHA-256 mismatch for ${key}\n expected=${expectedSha256}\n actual=${hash}\n Delete ${path} and retry.`,
)
}
return {
url,
hash,
buffer,
// 'json' and 'geojson' both carry a JSON body.
...(ext.endsWith('json') ? { json: JSON.parse(buffer.toString('utf8')) } : {}),
}
}
/** Write a repo file with the directory walk and the byte-count log line. */
export const writeRepoFile = async ({ label, root, relativePath, body }) => {
const path = join(root, relativePath)
await mkdir(dirname(path), { recursive: true })
await writeFile(path, body)
console.log(`[${label}] wrote ${relativePath} (${Buffer.byteLength(body)} bytes)`)
}