From 4cd68c9193561cd9cb942e0f57e251e461b7880a Mon Sep 17 00:00:00 2001 From: implementer Date: Sat, 29 Aug 2026 10:45:00 +0000 Subject: [PATCH 1/2] feat: pin the database container to PostgreSQL 18.6 (E00-S03-T01) - compose.yaml: db.image pinned to postgres:18.6-bookworm (exact 18.6 minor, same bookworm flavor, no Alpine drift) so the database container is reproducible and exposes the expected PostgreSQL version; document the rollback (revert image to postgres:18-bookworm) - .gitea/workflows/ci.yml: new compose-config job runs tests/compose-config.test.mjs on every PR so the pinned-version criterion gates merges (docker-gated real-stack probes skip cleanly without a daemon) --- .gitea/workflows/ci.yml | 17 +++++++++++++++++ compose.yaml | 11 +++++++---- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index def04b1..249222b 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -36,3 +36,20 @@ jobs: node-version: '24' - name: Run secrets-not-embedded test suite run: node --test tests/secrets-not-embedded.test.mjs + + # E00-S03-T01: the static assertions of tests/compose-config.test.mjs (db + # image pinned to postgres:18.6-bookworm, health gate, volume persistence, + # build platforms) gate every PR (the docker-gated real-stack probes inside + # the same file run where a Docker daemon is available and skip cleanly + # otherwise). + compose-config: + name: Compose config (E00-S03-T01) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install Node.js 24 + uses: actions/setup-node@v4 + with: + node-version: '24' + - name: Run compose-config test suite + run: node --test tests/compose-config.test.mjs diff --git a/compose.yaml b/compose.yaml index 34574cc..06aa600 100644 --- a/compose.yaml +++ b/compose.yaml @@ -1,4 +1,4 @@ -# EPPP Docker Compose baseline — [E00-S02-T01..T08] +# EPPP Docker Compose baseline — [E00-S02-T01..T08] + [E00-S03-T01] # # `docker compose up -d` starts both the database (PostgreSQL) and the # application (@personal-blog/server). Rollback: `docker compose down`. @@ -53,9 +53,12 @@ services: db: - # PostgreSQL 18 on Debian bookworm — the documented runtime target - # (Technology-Stack §5.2/§5.4/§6.2, golden tuple §7; no Alpine drift). - image: postgres:18-bookworm + # PostgreSQL 18.6 on Debian bookworm — the documented runtime target + # (Technology-Stack §5.2/§5.4/§6.2, golden tuple §7; no Alpine drift), + # pinned to the exact 18.6 minor (E00-S03-T01) so the database container + # is reproducible and exposes the expected PostgreSQL version. Rollback: + # revert `image` to `postgres:18-bookworm`. + image: postgres:18.6-bookworm environment: POSTGRES_DB: ${POSTGRES_DB:-eppp} POSTGRES_USER: ${POSTGRES_USER:-eppp} From a78ffea4c55a6fdfcb97f590a815237acb234a77 Mon Sep 17 00:00:00 2001 From: implementer Date: Sat, 29 Aug 2026 10:45:00 +0000 Subject: [PATCH 2/2] test: lock in the PostgreSQL 18.6 container criteria (E00-S03-T01) - compose-config.test.mjs: assertDbService requires the pinned postgres:18.6-bookworm image; new docker-gated real-stack probe starts the stack and asserts SHOW server_version exposes 18.6; mutation probe proves reverting to a floating major tag fails the criterion; parser probe updated - build-targets.test.mjs: db-service mutation fixture updated to the pinned image tag --- tests/build-targets.test.mjs | 4 +-- tests/compose-config.test.mjs | 58 ++++++++++++++++++++++++++++++++--- 2 files changed, 56 insertions(+), 6 deletions(-) diff --git a/tests/build-targets.test.mjs b/tests/build-targets.test.mjs index b2dd338..f374f82 100644 --- a/tests/build-targets.test.mjs +++ b/tests/build-targets.test.mjs @@ -345,8 +345,8 @@ test('a platforms list on the db service (which has no build config) cannot sati const dbOnly = text .replace(/\n\s*platforms:\n(?:\s*- [^\n]+\n)+/, '\n') .replace( - ' image: postgres:18-bookworm\n', - ' image: postgres:18-bookworm\n platforms:\n - linux/amd64\n - linux/arm64\n', + ' image: postgres:18.6-bookworm\n', + ' image: postgres:18.6-bookworm\n platforms:\n - linux/amd64\n - linux/arm64\n', ); assert.notEqual(dbOnly, text, 'the mutation must actually move the platforms list onto db'); assert.throws(() => assertBuildTargets(parseYaml(dbOnly)), /platforms/); diff --git a/tests/compose-config.test.mjs b/tests/compose-config.test.mjs index cb9d601..dcd6095 100644 --- a/tests/compose-config.test.mjs +++ b/tests/compose-config.test.mjs @@ -180,8 +180,9 @@ function assertDbService(compose) { assert.ok(db, 'compose.yaml must declare a "db" service (docker compose up -d starts the database)'); assert.equal( db.image, - 'postgres:18-bookworm', - 'the "db" service must use the committed PostgreSQL 18 image (postgres:18-bookworm)', + 'postgres:18.6-bookworm', + 'the "db" service must use the committed PostgreSQL 18.6 image (postgres:18.6-bookworm — E00-S03-T01 ' + + 'pins the exact minor so the database container exposes the expected PostgreSQL version)', ); const env = db.environment ?? {}; assert.equal(env.POSTGRES_DB, '${POSTGRES_DB:-eppp}', 'db must set POSTGRES_DB (with a default)'); @@ -640,6 +641,43 @@ test('a database fixture survives docker compose restart and recreate (named db- } }); +test('the database container exposes the expected PostgreSQL version (18.6)', { skip: !DOCKER_COMPOSE || !DOCKER_DAEMON }, () => { + // E00-S03-T01 acceptance criteria: a PostgreSQL 18.6 container is used as + // the database AND the container exposes the expected PostgreSQL version. + // The committed image tag is pinned to postgres:18.6-bookworm (asserted + // statically above); this probe starts the stack and asks the running + // server itself (`SHOW server_version`) — the issue's test plan ("start the + // container and confirm PostgreSQL 18.6") — so the version the container + // actually exposes is verified end to end, not just the declared tag. + const up = run('docker', ['compose', 'up', '-d'], { cwd: REPO_ROOT }); + assert.equal( + up.status, + 0, + `"docker compose up -d" must exit 0:\n${(up.stdout || '')}\n${(up.stderr || '')}`.trim(), + ); + try { + waitForDbHealthy(); + const version = run( + 'docker', + ['compose', 'exec', '-T', 'db', 'psql', '-U', 'eppp', '-d', 'eppp', '-tA', '-c', 'SHOW server_version;'], + { cwd: REPO_ROOT, timeout: 60_000 }, + ); + assert.equal( + version.status, + 0, + `"SHOW server_version" must succeed against the running db container:\n` + + `${(version.stdout || '')}\n${(version.stderr || '')}`.trim(), + ); + assert.match( + version.stdout.trim(), + /^18\.6\b/, + `the running db container must expose PostgreSQL 18.6 (SHOW server_version got: "${version.stdout.trim()}")`, + ); + } finally { + run('docker', ['compose', 'down'], { cwd: REPO_ROOT }); + } +}); + // --------------------------------------------------------------------------- // Non-vacuous probes — the assertions above really do fail on violations // --------------------------------------------------------------------------- @@ -648,7 +686,7 @@ test('the YAML parser reads the committed structure (non-vacuous parser probe)', const parsed = parseYaml(` services: db: - image: postgres:18-bookworm + image: postgres:18.6-bookworm environment: POSTGRES_DB: eppp ports: @@ -665,7 +703,7 @@ services: db: condition: service_healthy `); - assert.equal(parsed.services.db.image, 'postgres:18-bookworm'); + assert.equal(parsed.services.db.image, 'postgres:18.6-bookworm'); assert.equal(parsed.services.db.environment.POSTGRES_DB, 'eppp'); assert.deepEqual(parsed.services.db.ports, ['5432:5432']); assert.match(parsed.services.db.healthcheck.test, /pg_isready/); @@ -682,6 +720,18 @@ test('removing the db service makes the database criterion fail (mutation probe) assert.throws(() => assertDbService(compose), /"db" service/); }); +test('reverting the db image to a floating major tag fails the PostgreSQL 18.6 criterion (mutation probe)', () => { + // E00-S03-T01 pins the exact 18.6 minor (postgres:18.6-bookworm). A + // floating major tag (postgres:18-bookworm / postgres:18) would not + // guarantee the container exposes PostgreSQL 18.6, so reverting to one must + // fail the pinned-image assertion. + const text = read(COMPOSE_PATH); + const mutated = text.replace('postgres:18.6-bookworm', 'postgres:18-bookworm'); + assert.notEqual(mutated, text, 'the mutation must actually revert the pinned image tag'); + const compose = parseYaml(mutated); + assert.throws(() => assertDbService(compose), /18\.6/); +}); + test('removing the app service makes the application criterion fail (mutation probe)', () => { const text = read(COMPOSE_PATH); const withoutApp = text.replace(/^ app:\n(?: .*\n?)*/m, '');