diff --git a/.github/workflows/ql3-ci.yml b/.github/workflows/ql3-ci.yml index 1c90fc98..cedc966c 100644 --- a/.github/workflows/ql3-ci.yml +++ b/.github/workflows/ql3-ci.yml @@ -45,7 +45,9 @@ jobs: - name: Install dependencies without lifecycle scripts run: pnpm install --frozen-lockfile --ignore-scripts - name: Initialize the public test environment - run: cp .env.example .env + run: | + cp .env.example .env + mkdir -p data/db - name: Rebuild the reviewed native SQLite binding run: pnpm rebuild @whyour/sqlite3 - name: Build backend and QL3 workspace from source @@ -951,6 +953,8 @@ jobs: docker exec ${{ job.services.postgres.id }} psql -U postgres -d ql3_contract -v ON_ERROR_STOP=1 -c "CREATE ROLE ql3_runtime LOGIN PASSWORD 'ql3_runtime_test'" docker exec ${{ job.services.postgres.id }} psql -U postgres -d ql3_contract -v ON_ERROR_STOP=1 -c "CREATE ROLE ql3_admin LOGIN PASSWORD 'ql3_admin_test'" docker exec ${{ job.services.postgres.id }} psql -U postgres -d ql3_contract -v ON_ERROR_STOP=1 -c "CREATE ROLE ql3_automation_manager LOGIN PASSWORD 'ql3_automation_manager_test'" + docker exec ${{ job.services.postgres.id }} psql -U postgres -d ql3_contract -v ON_ERROR_STOP=1 -c "CREATE ROLE ql3_approval_manager LOGIN PASSWORD 'ql3_approval_manager_test'" + docker exec ${{ job.services.postgres.id }} psql -U postgres -d ql3_contract -v ON_ERROR_STOP=1 -c "CREATE ROLE ql3_run_manager LOGIN PASSWORD 'ql3_run_manager_test'" docker exec ${{ job.services.postgres.id }} psql -U postgres -d ql3_contract -v ON_ERROR_STOP=1 -c "CREATE ROLE ql3_package_manager LOGIN PASSWORD 'ql3_package_manager_test'" docker exec ${{ job.services.postgres.id }} psql -U postgres -d ql3_contract -v ON_ERROR_STOP=1 -c "CREATE ROLE ql3_package_executor LOGIN PASSWORD 'ql3_package_executor_test'" docker exec ${{ job.services.postgres.id }} psql -U postgres -d ql3_contract -v ON_ERROR_STOP=1 -c "CREATE ROLE ql3_worker_credential_manager LOGIN PASSWORD 'ql3_worker_credential_manager_test'" diff --git a/scripts/ql3-postgres-tls-rotation-contract.cjs b/scripts/ql3-postgres-tls-rotation-contract.cjs index 518307af..4b75945f 100644 --- a/scripts/ql3-postgres-tls-rotation-contract.cjs +++ b/scripts/ql3-postgres-tls-rotation-contract.cjs @@ -54,11 +54,21 @@ function docker(args, options = {}) { } function runCertificateTool(directory, args) { + const uid = process.getuid?.(); + const gid = process.getgid?.(); + if ( + !Number.isSafeInteger(uid) || + uid < 0 || + !Number.isSafeInteger(gid) || + gid < 0 + ) { + throw new Error('cannot resolve the host certificate owner'); + } docker([ 'run', '--rm', '--user', - '0:0', + `${uid}:${gid}`, '--volume', `${directory}:/work`, '--workdir', @@ -187,6 +197,7 @@ function preparePostgresFileOwnership(directory) { 'chown', `${identity[0]}:${identity[1]}`, '/work/server.key', + '/work/active.crt', ]); } @@ -221,24 +232,34 @@ async function waitFor(operation, description, timeoutMs = WAIT_TIMEOUT_MS) { } async function waitForPostgres(containerName) { - await waitFor( - () => - docker( - [ - 'exec', - containerName, - 'pg_isready', - '-h', - '127.0.0.1', - '-U', - USER, - '-d', - DATABASE, - ], - { allowFailure: true }, - ).status === 0, - 'TLS PostgreSQL readiness', - ); + try { + await waitFor( + () => + docker( + [ + 'exec', + containerName, + 'pg_isready', + '-h', + '127.0.0.1', + '-U', + USER, + '-d', + DATABASE, + ], + { allowFailure: true }, + ).status === 0, + 'TLS PostgreSQL readiness', + ); + } catch (error) { + const logs = docker(['logs', containerName], { allowFailure: true }); + const detail = [logs.stderr, logs.stdout].filter(Boolean).join('\n'); + throw new Error( + `${error instanceof Error ? error.message : String(error)}${ + detail ? `\nPostgreSQL container logs:\n${detail}` : '' + }`, + ); + } } function databaseUrl(port) { @@ -340,6 +361,7 @@ async function expectTlsRejection( async function reloadCertificate(containerName, directory, generation) { activateServerCertificate(directory, generation); + preparePostgresFileOwnership(directory); docker(['kill', '--signal', 'HUP', containerName]); await delay(500); } diff --git a/test/back/ql3CiCleanCheckoutContract.test.cjs b/test/back/ql3CiCleanCheckoutContract.test.cjs index 53f5a9ac..679c1cee 100644 --- a/test/back/ql3CiCleanCheckoutContract.test.cjs +++ b/test/back/ql3CiCleanCheckoutContract.test.cjs @@ -39,6 +39,7 @@ test('pins backend CI to the released Node line and bootstraps a clean checkout' assertOrdered(backend, [ 'pnpm install --frozen-lockfile --ignore-scripts', 'cp .env.example .env', + 'mkdir -p data/db', 'pnpm rebuild @whyour/sqlite3', 'pnpm build:back', 'pnpm run build:packages:ql3', @@ -47,6 +48,18 @@ test('pins backend CI to the released Node line and bootstraps a clean checkout' assert.doesNotMatch(backend, /if: matrix\.node == '24'/); }); +test('provisions every role required by the PostgreSQL migration stream', () => { + const postgres = jobSource('cluster-postgres', 'cluster-postgres-ha'); + for (const role of [ + 'ql3_automation_manager', + 'ql3_approval_manager', + 'ql3_run_manager', + 'ql3_package_manager', + ]) { + assert.match(postgres, new RegExp(`CREATE ROLE ${role} LOGIN PASSWORD`)); + } +}); + test('bootstraps jobs that previously depended on local modules or artifacts', () => { const serviceManager = jobSource( 'service-manager-bridge', @@ -99,9 +112,7 @@ test('rebuilds the only native legacy binding used by resource evidence', () => }); test('does not forward a literal separator into recovery evidence CLIs', () => { - const recovery = jobSource( - 'cluster-plugin-package-recovery-e2e', - ); + const recovery = jobSource('cluster-plugin-package-recovery-e2e'); assert.doesNotMatch( recovery, /pnpm (?:test|audit):plugin-package-recovery-e2e:ql3 -- \\/, diff --git a/test/back/ql3PostgresTlsRotationWorkflow.test.cjs b/test/back/ql3PostgresTlsRotationWorkflow.test.cjs index 03b78281..40bb4c70 100644 --- a/test/back/ql3PostgresTlsRotationWorkflow.test.cjs +++ b/test/back/ql3PostgresTlsRotationWorkflow.test.cjs @@ -60,7 +60,21 @@ test('builds cluster-postgres before the standalone TLS rotation contract', () = 'pg_stat_ssl', 'pg_is_in_recovery()', "current_setting('transaction_read_only')", + 'PostgreSQL container logs:', ]) { assert.ok(source.includes(required), `missing TLS evidence: ${required}`); } + assert.match(source, /const uid = process\.getuid\?\.\(\)/); + assert.match(source, /const gid = process\.getgid\?\.\(\)/); + const certificateTool = source.match( + /function runCertificateTool[\s\S]*?\n}\n/, + )?.[0]; + assert.ok(certificateTool); + assert.match(certificateTool, /'--user',\s*`\$\{uid}:\$\{gid}`/); + assert.doesNotMatch(certificateTool, /'0:0'/); + assert.match(source, /'\/work\/server\.key',\s*'\/work\/active\.crt'/); + assert.match( + source, + /activateServerCertificate\(directory, generation\);\s*preparePostgresFileOwnership\(directory\);\s*docker\(\['kill'/, + ); });