From 80dce77e117d6a20948b083758c4221005b71fde Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 03:09:11 +0900 Subject: [PATCH 1/6] test(ci): require PostgreSQL 18.6 security baseline --- tests/test_postgresql_runtime_baseline.py | 27 +++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 tests/test_postgresql_runtime_baseline.py diff --git a/tests/test_postgresql_runtime_baseline.py b/tests/test_postgresql_runtime_baseline.py new file mode 100644 index 00000000..15fc7e4e --- /dev/null +++ b/tests/test_postgresql_runtime_baseline.py @@ -0,0 +1,27 @@ +"""Repository contract for the supported PostgreSQL security baseline.""" + +from __future__ import annotations + +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +CI_WORKFLOW = ROOT / ".github/workflows/ci.yml" +POSTGRES_IMAGE = ( + "postgres:18.6@sha256:4ef4dbc939d61acea57712655ddb4b4ab27419c913f94cca0cd57cb3ea3c2280" +) + + +class PostgreSQLRuntimeBaselineTests(unittest.TestCase): + """Keep real accounting regressions on the current supported PostgreSQL minor.""" + + def test_exact_head_ci_uses_postgresql_18_6_by_immutable_digest(self) -> None: + """The PostgreSQL service must include the August 2026 security update.""" + workflow = CI_WORKFLOW.read_text(encoding="utf-8") + self.assertIn(POSTGRES_IMAGE, workflow) + self.assertNotIn("postgres:18.4", workflow) + + +if __name__ == "__main__": + unittest.main() From 71ad985c71db745938c9e66e060a982621636236 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 03:09:57 +0900 Subject: [PATCH 2/6] fix(ci): move accounting PostgreSQL baseline to 18.6 --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cd54bdd3..4887580b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -182,7 +182,7 @@ jobs: ACCOUNTING_DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/accounting_test services: postgres: - image: postgres:18.4@sha256:a02db8cac496f15b094798a38254f14d6e00741f709360e5e00bb6668ea31636 + image: postgres:18.6@sha256:4ef4dbc939d61acea57712655ddb4b4ab27419c913f94cca0cd57cb3ea3c2280 env: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres @@ -360,4 +360,4 @@ jobs: uses: actions/attest@508db95dd578ae2727ebd6217d5ba78e4fbda05d with: subject-path: ${{ github.workspace }}/dist/*.whl - sbom-path: ${{ github.workspace }}/dist/sbom.spdx.json + sbom-path: ${{ github.workspace }}/dist/sbom.spdx.json \ No newline at end of file From b2a7196549f76eee1e3f51bf1ef196facebaab15 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 03:10:42 +0900 Subject: [PATCH 3/6] docs(ci): record PostgreSQL 18.6 runtime baseline --- docs/doctoring/POSTGRESQL_RUNTIME_BASELINE.md | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 docs/doctoring/POSTGRESQL_RUNTIME_BASELINE.md diff --git a/docs/doctoring/POSTGRESQL_RUNTIME_BASELINE.md b/docs/doctoring/POSTGRESQL_RUNTIME_BASELINE.md new file mode 100644 index 00000000..cae66b27 --- /dev/null +++ b/docs/doctoring/POSTGRESQL_RUNTIME_BASELINE.md @@ -0,0 +1,25 @@ +# PostgreSQL runtime baseline + +## Current baseline + +Accounting integration and migration regressions run on PostgreSQL **18.6**, the current supported PostgreSQL 18 minor release as of 2026-09-05. PostgreSQL 18.6 was released on 2026-08-13. The PostgreSQL project states that the release fixes 28 security vulnerabilities and more than 110 bugs across the supported branches; PostgreSQL 18.5 was not shipped because of a regression. + +GitHub Actions pins the official multi-platform `postgres:18.6` image by immutable OCI index digest: + +`sha256:4ef4dbc939d61acea57712655ddb4b4ab27419c913f94cca0cd57cb3ea3c2280` + +The tag is kept alongside the digest so reviewers can see the intended minor version; the digest, not the mutable tag, is the execution identity. Accounting tests still target PostgreSQL major-version 18 semantics. This minor update does not alter journal, posting, close, reconciliation, Billing ACL, or accounting-policy authority. + +## Upgrade acceptance + +The exact-head Accounting Foundation job must execute the complete behavior suite, real PostgreSQL regressions, 100% production statement/branch coverage, repository contracts, packaging, SBOM, and reproducibility checks against the pinned 18.6 service before the baseline can be treated as GREEN. A Docker Hub digest lookup is provenance for selecting the image, not execution evidence. + +Historical test and ADR statements that explicitly record PostgreSQL 18.4 remain historical evidence when they describe an exact past run. Statements that claim 18.4 is the *current* runtime baseline must be read as superseded by this document and should be corrected when their canonical owner lane next edits them. + +## References + +PostgreSQL Global Development Group. (2026a, August 13). *PostgreSQL 18.6, 17.11, 16.15, 15.19, 14.24 and 19 Beta 3 released*. https://www.postgresql.org/about/news/postgresql-186-1711-1615-1519-1424-and-19-beta-3-released-3365/ + +PostgreSQL Global Development Group. (2026b). *PostgreSQL release notes*. https://www.postgresql.org/docs/release/ + +Docker, Inc. (2026). *Official postgres:18.6 image*. https://hub.docker.com/_/postgres From 45c13ce5741e65d096cab4899ac3bd7c0d55c70d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 06:31:03 +0900 Subject: [PATCH 4/6] test(ci): require PostgreSQL 18.6 upgrade caveats --- tests/test_postgresql_runtime_baseline.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/test_postgresql_runtime_baseline.py b/tests/test_postgresql_runtime_baseline.py index 15fc7e4e..2e13de62 100644 --- a/tests/test_postgresql_runtime_baseline.py +++ b/tests/test_postgresql_runtime_baseline.py @@ -8,6 +8,7 @@ ROOT = Path(__file__).resolve().parents[1] CI_WORKFLOW = ROOT / ".github/workflows/ci.yml" +RUNTIME_BASELINE = ROOT / "docs/doctoring/POSTGRESQL_RUNTIME_BASELINE.md" POSTGRES_IMAGE = ( "postgres:18.6@sha256:4ef4dbc939d61acea57712655ddb4b4ab27419c913f94cca0cd57cb3ea3c2280" ) @@ -22,6 +23,23 @@ def test_exact_head_ci_uses_postgresql_18_6_by_immutable_digest(self) -> None: self.assertIn(POSTGRES_IMAGE, workflow) self.assertNotIn("postgres:18.4", workflow) + def test_runtime_baseline_records_18_6_existing_cluster_upgrade_checks(self) -> None: + """An ephemeral CI image bump must not stand in for production upgrade evidence.""" + baseline = RUNTIME_BASELINE.read_text(encoding="utf-8") + for required_evidence in ( + "output_plugin_libraries", + "pgcrypto", + "COPY ... FROM STDIN", + "GIN", + "reltuples", + "btree_gist", + "ltree", + "SHOW server_version", + ): + with self.subTest(required_evidence=required_evidence): + self.assertIn(required_evidence, baseline) + self.assertIn("does not prove an existing database upgraded safely", baseline) + if __name__ == "__main__": unittest.main() From cf3c4d8eb70bc0e36fa08c58e448cf354dde604e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 06:31:26 +0900 Subject: [PATCH 5/6] docs(ci): separate PostgreSQL 18.6 cluster upgrade evidence --- docs/doctoring/POSTGRESQL_RUNTIME_BASELINE.md | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/docs/doctoring/POSTGRESQL_RUNTIME_BASELINE.md b/docs/doctoring/POSTGRESQL_RUNTIME_BASELINE.md index cae66b27..6dc948db 100644 --- a/docs/doctoring/POSTGRESQL_RUNTIME_BASELINE.md +++ b/docs/doctoring/POSTGRESQL_RUNTIME_BASELINE.md @@ -10,16 +10,32 @@ GitHub Actions pins the official multi-platform `postgres:18.6` image by immutab The tag is kept alongside the digest so reviewers can see the intended minor version; the digest, not the mutable tag, is the execution identity. Accounting tests still target PostgreSQL major-version 18 semantics. This minor update does not alter journal, posting, close, reconciliation, Billing ACL, or accounting-policy authority. -## Upgrade acceptance +## Exact-head acceptance -The exact-head Accounting Foundation job must execute the complete behavior suite, real PostgreSQL regressions, 100% production statement/branch coverage, repository contracts, packaging, SBOM, and reproducibility checks against the pinned 18.6 service before the baseline can be treated as GREEN. A Docker Hub digest lookup is provenance for selecting the image, not execution evidence. +The exact-head Accounting Foundation job must execute the complete behavior suite, real PostgreSQL regressions, 100% production statement/branch coverage, repository contracts, packaging, SBOM, and reproducibility checks against the pinned 18.6 service before the CI baseline can be treated as GREEN. A Docker Hub digest lookup is provenance for selecting the image, not execution evidence. Historical test and ADR statements that explicitly record PostgreSQL 18.4 remain historical evidence when they describe an exact past run. Statements that claim 18.4 is the *current* runtime baseline must be read as superseded by this document and should be corrected when their canonical owner lane next edits them. +## Existing-cluster upgrade acceptance + +Changing the ephemeral GitHub Actions service image **does not prove an existing database upgraded safely**. PostgreSQL states that an 18.x-to-18.6 upgrade does not require dump/restore, but the 18.6 release notes identify configuration, data, script, and index checks that can require operator action. Release evidence for an existing accounting database therefore keeps the CI image change separate from an operator-controlled cluster upgrade. + +Before upgrading an existing cluster, inventory and retain the following evidence for the target cluster and every replica or failover member that can become authoritative: + +- logical replication slots and non-core logical-decoding plugins. PostgreSQL 18.6 introduces `output_plugin_libraries`; installations that depend on third-party output plugins must explicitly allow them, and `pg_upgrade --check` can reject an incompatible target configuration; +- use of `pgcrypto` PGP encryption with legacy algorithms that OpenSSL can reject. The 18.6 security fix makes unsupported-cipher failures visible; potentially affected ciphertext must be identified and recovered/re-encrypted according to the release notes rather than treated as valid encrypted evidence; +- operational or migration scripts containing `COPY ... FROM STDIN`. Scripts that intentionally exercise a `COPY` command which can fail before copy-in begins must retain a `\.` terminator so following data cannot be interpreted as SQL; +- GIN indexes and table statistics. The release notes describe possibly corrupt `reltuples` after affected parallel GIN index builds; the upgrade runbook must inspect the documented catalog condition and repair affected statistics with the PostgreSQL-recommended `ANALYZE` or equivalent index operation before accepting the cluster; +- `btree_gist` and `ltree` indexes. If present, follow the 18.6 release-note reindex guidance before treating the upgraded database as release-ready. + +The preflight must determine applicability from the live database catalogs and configuration; absence is evidence only when the exact target cluster was queried. Repository source search or a clean CI database is not sufficient proof that a production cluster has no affected extension, index, replication slot, historical ciphertext, or operator script. + +After upgrade, retain the exact server identity (`SHOW server_version` and `SHOW server_version_num`), extension/index/replication-slot postchecks that correspond to the preflight inventory, migration status, application readiness, and the full accounting acceptance suite. A rollback or failover decision must preserve immutable posted facts and reconciliation/close evidence; PostgreSQL runtime recovery is not permission to rewrite accounting history. + ## References PostgreSQL Global Development Group. (2026a, August 13). *PostgreSQL 18.6, 17.11, 16.15, 15.19, 14.24 and 19 Beta 3 released*. https://www.postgresql.org/about/news/postgresql-186-1711-1615-1519-1424-and-19-beta-3-released-3365/ -PostgreSQL Global Development Group. (2026b). *PostgreSQL release notes*. https://www.postgresql.org/docs/release/ +PostgreSQL Global Development Group. (2026b, August 13). *PostgreSQL 18.6 release notes*. https://www.postgresql.org/docs/18/release-18-6.html Docker, Inc. (2026). *Official postgres:18.6 image*. https://hub.docker.com/_/postgres From aaa07974595cef0e68f70cebe74d5a92d1dd1ea3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 13:29:06 +0900 Subject: [PATCH 6/6] test: bind PostgreSQL runtime evidence fields --- tests/test_postgresql_runtime_baseline.py | 40 ++++++++++++++++++++--- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/tests/test_postgresql_runtime_baseline.py b/tests/test_postgresql_runtime_baseline.py index 2e13de62..86cc5d97 100644 --- a/tests/test_postgresql_runtime_baseline.py +++ b/tests/test_postgresql_runtime_baseline.py @@ -2,6 +2,7 @@ from __future__ import annotations +import re import unittest from pathlib import Path @@ -12,6 +13,28 @@ POSTGRES_IMAGE = ( "postgres:18.6@sha256:4ef4dbc939d61acea57712655ddb4b4ab27419c913f94cca0cd57cb3ea3c2280" ) +POSTGRES_DIGEST = POSTGRES_IMAGE.partition("@")[2] + + +def _postgres_service_image(workflow: str) -> str: + """Return the direct image field from the Accounting Foundation PostgreSQL service.""" + service_match = re.search( + r"(?ms)^ postgres:\n(?P(?:(?:^ {8,}\S.*|^\s*)\n?)*)", + workflow, + ) + if service_match is None: + return "" + image_match = re.search( + r"(?m)^ image: (?P\S+)$", + service_match.group("body"), + ) + return "" if image_match is None else image_match.group("image") + + +def _markdown_section(document: str, heading: str) -> str: + """Return one level-two Markdown section without accepting another section's text.""" + section_marker = f"## {heading}\n" + return document.partition(section_marker)[2].partition("\n## ")[0] class PostgreSQLRuntimeBaselineTests(unittest.TestCase): @@ -20,12 +43,18 @@ class PostgreSQLRuntimeBaselineTests(unittest.TestCase): def test_exact_head_ci_uses_postgresql_18_6_by_immutable_digest(self) -> None: """The PostgreSQL service must include the August 2026 security update.""" workflow = CI_WORKFLOW.read_text(encoding="utf-8") - self.assertIn(POSTGRES_IMAGE, workflow) - self.assertNotIn("postgres:18.4", workflow) + self.assertEqual(_postgres_service_image(workflow), POSTGRES_IMAGE) def test_runtime_baseline_records_18_6_existing_cluster_upgrade_checks(self) -> None: """An ephemeral CI image bump must not stand in for production upgrade evidence.""" baseline = RUNTIME_BASELINE.read_text(encoding="utf-8") + current_baseline = _markdown_section(baseline, "Current baseline") + upgrade_acceptance = _markdown_section( + baseline, "Existing-cluster upgrade acceptance" + ) + self.assertIn("PostgreSQL **18.6**", current_baseline) + self.assertIn(POSTGRES_DIGEST, current_baseline) + self.assertNotIn("PostgreSQL **18.4**", current_baseline) for required_evidence in ( "output_plugin_libraries", "pgcrypto", @@ -37,8 +66,11 @@ def test_runtime_baseline_records_18_6_existing_cluster_upgrade_checks(self) -> "SHOW server_version", ): with self.subTest(required_evidence=required_evidence): - self.assertIn(required_evidence, baseline) - self.assertIn("does not prove an existing database upgraded safely", baseline) + self.assertIn(required_evidence, upgrade_acceptance) + self.assertIn( + "does not prove an existing database upgraded safely", + upgrade_acceptance, + ) if __name__ == "__main__":