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

This commit is contained in:
Francisco Solla 2026-08-17 03:46:16 -03:00
parent 0de9e9f57d
commit 02f68894c3
3 changed files with 74 additions and 0 deletions

View file

@ -7,6 +7,7 @@
* 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'
@ -31,6 +32,44 @@ export const loadCliEnv = () => {
/** 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 = () =>

View file

@ -12,6 +12,11 @@
/** `teqo_test`, `teqo_wt15_test`, … — the _test suffix is the hard contract. */
const TEST_DATABASE_NAME = /^teqo(_[a-z0-9]+)?_test$/
// Self-hosted Forgejo runner: the job's postgres service is published on the
// bridge gateway IP (RFC1918), not localhost. Same source as the CLI guard so
// both guards admit exactly the same hosts (OPS50).
import { defaultGatewayHost } from '../../scripts/lib/cli.mjs'
export function assertTestDatabase(databaseUrl: string | undefined): void {
if (!databaseUrl) {
throw new Error(
@ -29,6 +34,8 @@ export function assertTestDatabase(databaseUrl: string | undefined): void {
const databaseName = decodeURIComponent(parsedUrl.pathname.replace(/^\//, ''))
const localHosts = new Set(['localhost', '127.0.0.1', '::1', '[::1]'])
const gateway = defaultGatewayHost()
if (gateway) localHosts.add(gateway)
if (
parsedUrl.protocol !== 'postgresql:' ||

View file

@ -0,0 +1,28 @@
// @vitest-environment node
import { describe, expect, it } from 'vitest'
import { defaultGatewayHost, LOCAL_HOSTS } from '../../scripts/lib/cli.mjs'
describe('LOCAL_HOSTS (OPS50 — self-hosted runner reaches services via the bridge gateway)', () => {
it('still contains the canonical static hosts', () => {
for (const host of ['localhost', '127.0.0.1', '0.0.0.0', '::1', 'postgres']) {
expect(LOCAL_HOSTS.has(host)).toBe(true)
}
})
it('admits the machine default gateway when it is RFC1918 (Linux CI bridge)', () => {
const gateway = defaultGatewayHost()
if (gateway) {
// The Forgejo runner publishes the job's postgres service on the bridge
// gateway IP; assertLocalDatabase must accept it so seeds run in CI.
expect(LOCAL_HOSTS.has(gateway)).toBe(true)
}
})
it('never admits public or loopback hosts', () => {
expect(LOCAL_HOSTS.has('8.8.8.8')).toBe(false)
expect(LOCAL_HOSTS.has('127.0.0.2')).toBe(false)
expect(LOCAL_HOSTS.has('203.0.113.1')).toBe(false)
})
})