From be1545564a21d9f4398d2f792c8af1540742d183 Mon Sep 17 00:00:00 2001 From: Muhammad Aqeel Date: Mon, 7 Sep 2026 17:30:32 +0500 Subject: [PATCH 01/16] Adds coldfront image flavor chained from standard --- Dockerfile | 55 ++++++++ coldfront-entrypoint.sh | 37 ++++++ .../amd64/pg16.15-spock5.0.11-coldfront.txt | 2 + .../amd64/pg17.11-spock5.0.11-coldfront.txt | 2 + .../amd64/pg18.6-spock5.0.11-coldfront.txt | 2 + .../arm64/pg16.15-spock5.0.11-coldfront.txt | 2 + .../arm64/pg17.11-spock5.0.11-coldfront.txt | 2 + .../arm64/pg18.6-spock5.0.11-coldfront.txt | 2 + pgedge.docker-bake.hcl | 15 ++- scripts/build_pgedge_images.py | 41 +++++- tests/main.go | 122 ++++++++++++++++-- 11 files changed, 260 insertions(+), 22 deletions(-) create mode 100755 coldfront-entrypoint.sh create mode 100644 packagelists/amd64/pg16.15-spock5.0.11-coldfront.txt create mode 100644 packagelists/amd64/pg17.11-spock5.0.11-coldfront.txt create mode 100644 packagelists/amd64/pg18.6-spock5.0.11-coldfront.txt create mode 100644 packagelists/arm64/pg16.15-spock5.0.11-coldfront.txt create mode 100644 packagelists/arm64/pg17.11-spock5.0.11-coldfront.txt create mode 100644 packagelists/arm64/pg18.6-spock5.0.11-coldfront.txt diff --git a/Dockerfile b/Dockerfile index e9a7568..de46056 100644 --- a/Dockerfile +++ b/Dockerfile @@ -180,3 +180,58 @@ STOPSIGNAL SIGINT EXPOSE 5432 CMD ["postgres"] + +############################ +# coldfront-flavored image # +############################ +# +# Chained FROM standard rather than FROM base: ColdFront's vector-tiering path +# needs pgvector, which only standard ships. Chaining also makes the coldfront +# layers a genuine delta over standard rather than a parallel full install, so +# the inherited layers keep byte-identical digests. + +FROM standard AS coldfront + +# A separate ARG is required. This stage cannot reuse PACKAGE_LIST_FILE, because +# that ARG is consumed by the inherited standard stage -- passing the coldfront +# list through it would make standard COPY the coldfront list in place of its own. +ARG COLDFRONT_PACKAGE_LIST_FILE +ARG TARGETARCH +ARG POSTGRES_MAJOR_VERSION + +USER root + +COPY packagelists/${TARGETARCH}/${COLDFRONT_PACKAGE_LIST_FILE} /usr/share/pgedge/coldfront-packages.txt + +RUN < and are dnf's to resolve. +# Deliberately no second "dnf update -y": the inherited standard layers already +# ran one, and repeating it here would move ColdFront past its pinned NVR. +xargs dnf install -y --setopt=install_weak_deps=False < /usr/share/pgedge/coldfront-packages.txt +dnf clean all + +CFEOF + +# ColdFront needs pg_duckdb and coldfront preloaded at postmaster start, and +# DuckDB's extensions come from the read-only RPM path. autoinstall is off: the +# RPM ships all four loadable extensions and httpfs is compiled into libduckdb, +# so nothing needs fetching -- and with allow_unsigned on, autoinstall would mean +# loading unsigned code from the network at runtime. +ENV COLDFRONT_PRELOAD="pg_duckdb,coldfront" +ENV COLDFRONT_EXTENSION_DIR="/usr/lib/pgedge/coldfront/duckdb-extensions" + +COPY coldfront-entrypoint.sh /usr/local/bin/ + +USER postgres + +ENTRYPOINT ["/usr/local/bin/coldfront-entrypoint.sh"] +STOPSIGNAL SIGINT +EXPOSE 5432 +CMD ["postgres"] diff --git a/coldfront-entrypoint.sh b/coldfront-entrypoint.sh new file mode 100755 index 0000000..d54809a --- /dev/null +++ b/coldfront-entrypoint.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# +# ColdFront wrapper around pgEdge's docker-entrypoint.sh. +# +# It does NOT replace that entrypoint: it appends ColdFront's postmaster-start +# GUCs as -c arguments and then execs it, so POSTGRES_* variables, _FILE secrets +# and /docker-entrypoint-initdb.d/ all keep working. +# +# -c arguments rather than writing postgresql.conf because: +# * appending on every start would grow the file without bound; +# * they apply to an already-initialised PGDATA, which an initdb.d script cannot; +# * pgEdge's entrypoint forwards "$@" into docker_temp_server_start, so the +# preload is live during initdb.d and an init script can CREATE EXTENSION. +# +# Kubernetes never reaches this file: CNPG runs its own instance manager and +# takes these settings from the Cluster spec instead. + +set -o errexit +set -o nounset + +# pg_duckdb refuses to install in a non-UTF8 database, and pgEdge's initdb +# defaults to SQL_ASCII. Prepend the encoding ColdFront needs while preserving +# anything the operator passed, so an explicit override still wins. +export POSTGRES_INITDB_ARGS="--encoding=UTF8 --locale=C ${POSTGRES_INITDB_ARGS:-}" + +if [ "${1:-}" = "postgres" ]; then + shift + set -- postgres \ + -c shared_preload_libraries="${COLDFRONT_PRELOAD}" \ + -c duckdb.extension_directory="${COLDFRONT_EXTENSION_DIR}" \ + -c duckdb.autoinstall_known_extensions=false \ + -c duckdb.autoload_known_extensions=true \ + -c duckdb.allow_unsigned_extensions=true \ + "$@" +fi + +exec /usr/local/bin/docker-entrypoint.sh "$@" diff --git a/packagelists/amd64/pg16.15-spock5.0.11-coldfront.txt b/packagelists/amd64/pg16.15-spock5.0.11-coldfront.txt new file mode 100644 index 0000000..85c9410 --- /dev/null +++ b/packagelists/amd64/pg16.15-spock5.0.11-coldfront.txt @@ -0,0 +1,2 @@ +pgedge-coldfront_16-1.0.0-beta2_1.el9 +pgedge-coldfront-1.0.0-beta2_1.el9 diff --git a/packagelists/amd64/pg17.11-spock5.0.11-coldfront.txt b/packagelists/amd64/pg17.11-spock5.0.11-coldfront.txt new file mode 100644 index 0000000..a5b55a8 --- /dev/null +++ b/packagelists/amd64/pg17.11-spock5.0.11-coldfront.txt @@ -0,0 +1,2 @@ +pgedge-coldfront_17-1.0.0-beta2_1.el9 +pgedge-coldfront-1.0.0-beta2_1.el9 diff --git a/packagelists/amd64/pg18.6-spock5.0.11-coldfront.txt b/packagelists/amd64/pg18.6-spock5.0.11-coldfront.txt new file mode 100644 index 0000000..f242c11 --- /dev/null +++ b/packagelists/amd64/pg18.6-spock5.0.11-coldfront.txt @@ -0,0 +1,2 @@ +pgedge-coldfront_18-1.0.0-beta2_1.el9 +pgedge-coldfront-1.0.0-beta2_1.el9 diff --git a/packagelists/arm64/pg16.15-spock5.0.11-coldfront.txt b/packagelists/arm64/pg16.15-spock5.0.11-coldfront.txt new file mode 100644 index 0000000..85c9410 --- /dev/null +++ b/packagelists/arm64/pg16.15-spock5.0.11-coldfront.txt @@ -0,0 +1,2 @@ +pgedge-coldfront_16-1.0.0-beta2_1.el9 +pgedge-coldfront-1.0.0-beta2_1.el9 diff --git a/packagelists/arm64/pg17.11-spock5.0.11-coldfront.txt b/packagelists/arm64/pg17.11-spock5.0.11-coldfront.txt new file mode 100644 index 0000000..a5b55a8 --- /dev/null +++ b/packagelists/arm64/pg17.11-spock5.0.11-coldfront.txt @@ -0,0 +1,2 @@ +pgedge-coldfront_17-1.0.0-beta2_1.el9 +pgedge-coldfront-1.0.0-beta2_1.el9 diff --git a/packagelists/arm64/pg18.6-spock5.0.11-coldfront.txt b/packagelists/arm64/pg18.6-spock5.0.11-coldfront.txt new file mode 100644 index 0000000..f242c11 --- /dev/null +++ b/packagelists/arm64/pg18.6-spock5.0.11-coldfront.txt @@ -0,0 +1,2 @@ +pgedge-coldfront_18-1.0.0-beta2_1.el9 +pgedge-coldfront-1.0.0-beta2_1.el9 diff --git a/pgedge.docker-bake.hcl b/pgedge.docker-bake.hcl index 70e1cda..4886bcb 100644 --- a/pgedge.docker-bake.hcl +++ b/pgedge.docker-bake.hcl @@ -21,6 +21,14 @@ variable "PACKAGE_LIST_FILE" { default = "" } +// Chained flavors need their own packagelist ARG. A flavor built FROM another +// flavor still triggers the parent stage, which consumes PACKAGE_LIST_FILE, so +// reusing that variable would make the parent install the child's list. +variable "COLDFRONT_PACKAGE_LIST_FILE" { + type = string + default = "" +} + variable "TAG" { type = string default = "pgedge" @@ -31,9 +39,10 @@ target "default" { target = TARGET tags = [TAG] args = { - PACKAGE_RELEASE_CHANNEL = PACKAGE_RELEASE_CHANNEL - PACKAGE_LIST_FILE = PACKAGE_LIST_FILE - POSTGRES_MAJOR_VERSION = POSTGRES_MAJOR_VERSION + PACKAGE_RELEASE_CHANNEL = PACKAGE_RELEASE_CHANNEL + PACKAGE_LIST_FILE = PACKAGE_LIST_FILE + COLDFRONT_PACKAGE_LIST_FILE = COLDFRONT_PACKAGE_LIST_FILE + POSTGRES_MAJOR_VERSION = POSTGRES_MAJOR_VERSION } platforms = [ "linux/amd64", diff --git a/scripts/build_pgedge_images.py b/scripts/build_pgedge_images.py index 161adb8..9ef88c8 100755 --- a/scripts/build_pgedge_images.py +++ b/scripts/build_pgedge_images.py @@ -34,6 +34,17 @@ def from_env() -> "Config": ) +# Flavors that are built FROM another flavor rather than FROM base. A chained +# flavor's build also runs its parent's stage, so the parent's packagelist has to +# be passed alongside the child's -- see build(). +FLAVOR_PARENTS = {"coldfront": "standard"} + +# Flavors built for every image. coldfront is deliberately absent: ColdFront's +# cold-write protocol is validated against spock 5 only, so it is opted in per +# image rather than fanned out over the whole matrix. +DEFAULT_FLAVORS = ["minimal", "standard"] + + @dataclass class Tag: postgres_version: str @@ -74,15 +85,24 @@ def postgres_major(self) -> str: def spock_major(self) -> str: return self.spock_version.split(".")[0] - @property - def package_list(self) -> str: + def _package_list_for(self, flavor: str) -> str: filename = f"pg{self.postgres_version}-spock{self.spock_version}" - if self.flavor: - filename += f"-{self.flavor}" + if flavor: + filename += f"-{flavor}" return filename + ".txt" + @property + def package_list(self) -> str: + return self._package_list_for(self.flavor) + + @property + def parent_package_list(self) -> str: + """Packagelist of the flavor this one is chained FROM, or "" if none.""" + parent = FLAVOR_PARENTS.get(self.flavor) + return self._package_list_for(parent) if parent else "" + @property def build_tag(self) -> Tag: # Immutable tag with epoch @@ -138,9 +158,10 @@ def make_all_flavor_images( is_latest_for_pg_major: bool = False, is_latest_for_spock_major: bool = False, package_release_channel: str = "", + flavors: list[str] = None, ) -> list[PgEdgeImage]: images: list[PgEdgeImage] = [] - for flavor in ["minimal", "standard"]: + for flavor in flavors if flavors is not None else DEFAULT_FLAVORS: images.append( PgEdgeImage( postgres_version=postgres_version, @@ -166,6 +187,7 @@ def make_all_flavor_images( epoch=3, is_latest_for_pg_major=True, is_latest_for_spock_major=True, + flavors=DEFAULT_FLAVORS + ["coldfront"], ), # pg17 images *make_all_flavor_images( @@ -174,6 +196,7 @@ def make_all_flavor_images( epoch=3, is_latest_for_pg_major=True, is_latest_for_spock_major=True, + flavors=DEFAULT_FLAVORS + ["coldfront"], ), # pg18 images *make_all_flavor_images( @@ -182,6 +205,7 @@ def make_all_flavor_images( epoch=3, is_latest_for_pg_major=True, is_latest_for_spock_major=True, + flavors=DEFAULT_FLAVORS + ["coldfront"], ), # pg16 spock60 images *make_all_flavor_images( @@ -284,7 +308,12 @@ def build( **os.environ.copy(), "PACKAGE_RELEASE_CHANNEL": image.package_release_channel, "POSTGRES_MAJOR_VERSION": image.postgres_major, - "PACKAGE_LIST_FILE": image.package_list, + # A chained flavor needs its parent's list for the inherited stage and + # its own for the delta stage; an unchained flavor sends only its own. + "PACKAGE_LIST_FILE": image.parent_package_list or image.package_list, + "COLDFRONT_PACKAGE_LIST_FILE": ( + image.package_list if image.parent_package_list else "" + ), "TAG": f"{repo}:{image.build_tag}", "TARGET": image.flavor, }, diff --git a/tests/main.go b/tests/main.go index e833989..b829339 100644 --- a/tests/main.go +++ b/tests/main.go @@ -27,7 +27,14 @@ type Test struct { Name string Cmd string ExpectedOutput func(exitCode int, output string) error - StandardOnly bool // Only run on standard flavor images + StandardOnly bool // Only run on standard-or-later flavors (standard, coldfront) + ColdfrontOnly bool // Only run on the coldfront flavor +} + +// includesStandard reports whether a flavor ships everything standard does. +// coldfront is chained FROM standard, so it is a superset. +func includesStandard(flavor string) bool { + return flavor == "standard" || flavor == "coldfront" } // TestRunner manages container lifecycle and test execution @@ -101,20 +108,20 @@ func spockMajorFromImage(image string) string { func parseFlags() (string, string) { image := flag.String("image", "", "Docker image to test (required)") - flavor := flag.String("flavor", "", "Image flavor: minimal or standard (required)") + flavor := flag.String("flavor", "", "Image flavor: minimal, standard or coldfront (required)") flag.Parse() if *image == "" || *flavor == "" { - fmt.Println("Usage: go run main.go -image -flavor ") + fmt.Println("Usage: go run main.go -image -flavor ") fmt.Println() fmt.Println("Arguments:") fmt.Println(" -image Docker image to test (e.g., ghcr.io/pgedge/pgedge-postgres:17-spock5-standard)") - fmt.Println(" -flavor Image flavor: 'minimal' or 'standard'") + fmt.Println(" -flavor Image flavor: 'minimal', 'standard' or 'coldfront'") os.Exit(1) } - if *flavor != "minimal" && *flavor != "standard" { - log.Fatalf("Invalid flavor '%s'. Must be 'minimal' or 'standard'", *flavor) + if *flavor != "minimal" && !includesStandard(*flavor) { + log.Fatalf("Invalid flavor '%s'. Must be 'minimal', 'standard' or 'coldfront'", *flavor) } return *image, *flavor @@ -151,8 +158,8 @@ func runEntrypointTests(runner *DefaultEntrypointRunner, flavor string) int { } fmt.Println() - // Phase 2: Test Patroni entrypoint (standard only) - if flavor == "standard" { + // Phase 2: Test Patroni entrypoint (standard and the flavors chained from it) + if includesStandard(flavor) { printPhaseHeader("Phase 2: Patroni Entrypoint Test") if err := runner.TestPatroniEntrypoint(); err != nil { errorCount++ @@ -199,13 +206,17 @@ func printSummary(errorCount int, flavor, spockMajor string) { tests := buildTestSuite(spockMajor) extensionTests := 0 for _, t := range tests { - if !t.StandardOnly || flavor == "standard" { - extensionTests++ + if t.StandardOnly && !includesStandard(flavor) { + continue } + if t.ColdfrontOnly && flavor != "coldfront" { + continue + } + extensionTests++ } testsRun := 1 + extensionTests // default entrypoint + extensions - if flavor == "standard" { + if includesStandard(flavor) { testsRun++ // patroni entrypoint } @@ -408,9 +419,15 @@ func (r *TestRunner) Start() error { // These extensions require preloading before they can be used // Note: We only include extensions that are guaranteed to be in all images sharedLibs := "spock,snowflake" - if r.flavor == "standard" { + if includesStandard(r.flavor) { sharedLibs = "spock,snowflake,pgaudit,supautils,pg_cron,pg_tokenizer" } + // pg_duckdb and coldfront install hooks at postmaster start. The image's own + // entrypoint already passes them, but the -c built below is appended after it + // and would otherwise replace the value. + if r.flavor == "coldfront" { + sharedLibs += ",pg_duckdb,coldfront" + } // Build postgres command with required configuration // Note: We pass these as postgres arguments, which the entrypoint will handle @@ -634,7 +651,10 @@ func (r *TestRunner) RunTests(tests []Test) int { for _, test := range tests { // Skip standard-only tests for minimal flavor - if test.StandardOnly && r.flavor != "standard" { + if test.StandardOnly && !includesStandard(r.flavor) { + continue + } + if test.ColdfrontOnly && r.flavor != "coldfront" { continue } @@ -669,6 +689,7 @@ func buildTestSuite(spockMajor string) []Test { // Runs after getCommonExtensionTests, which creates the spock extension. tests = append(tests, getSpockVersionTests(spockMajor)...) tests = append(tests, getStandardOnlyTests()...) + tests = append(tests, getColdfrontTests()...) return tests } @@ -792,6 +813,81 @@ func getCommonExtensionTests() []Test { } } +// rpmExtensionDir is where pgedge-coldfront-duckdb-extensions installs the +// DuckDB extension binaries. duckdb.extension_directory points here and +// duckdb.autoinstall_known_extensions is off, so a successful load proves the +// extensions are read from the read-only package path rather than fetched. +const rpmExtensionDir = "/usr/lib/pgedge/coldfront/duckdb-extensions" + +func expectTrimmed(want string) func(int, string) error { + return func(exitCode int, output string) error { + if exitCode != 0 { + return fmt.Errorf("unexpected exit code: %d", exitCode) + } + if got := strings.TrimSpace(output); got != want { + return fmt.Errorf("expected %q, got %q", want, got) + } + return nil + } +} + +func getColdfrontTests() []Test { + loadAll := `SELECT duckdb.load_extension('iceberg');` + + `SELECT duckdb.load_extension('avro');` + + `SELECT duckdb.load_extension('azure');` + + `SELECT duckdb.load_extension('postgres_scanner');` + + `SELECT * FROM duckdb.query('SELECT count(*) FROM duckdb_extensions() ` + + `WHERE loaded AND install_path LIKE ''` + rpmExtensionDir + `%''');` + + return []Test{ + { + Name: "pg_duckdb extension can be created", + ColdfrontOnly: true, + Cmd: `psql -U postgres -d testdb -t -A -c "CREATE EXTENSION IF NOT EXISTS pg_duckdb; SELECT 1;"`, + ExpectedOutput: expectSuccess, + }, + { + Name: "coldfront extension can be created", + ColdfrontOnly: true, + Cmd: `psql -U postgres -d testdb -t -A -c "CREATE EXTENSION IF NOT EXISTS coldfront CASCADE; SELECT 1;"`, + ExpectedOutput: expectSuccess, + }, + { + Name: "DuckDB executes a query", + ColdfrontOnly: true, + Cmd: `psql -U postgres -d testdb -t -A -c "SELECT * FROM duckdb.query('SELECT 42');"`, + ExpectedOutput: expectTrimmed("42"), + }, + { + Name: "duckdb.extension_directory points at the package path", + ColdfrontOnly: true, + Cmd: `psql -U postgres -d testdb -t -A -c "SHOW duckdb.extension_directory;"`, + ExpectedOutput: expectTrimmed(rpmExtensionDir), + }, + { + Name: "DuckDB extension autoinstall is disabled", + ColdfrontOnly: true, + Cmd: `psql -U postgres -d testdb -t -A -c "SHOW duckdb.autoinstall_known_extensions;"`, + ExpectedOutput: expectTrimmed("off"), + }, + { + Name: "all four DuckDB extensions load from the package path", + ColdfrontOnly: true, + Cmd: `psql -U postgres -d testdb -t -A -c "` + loadAll + `"`, + ExpectedOutput: func(exitCode int, output string) error { + if exitCode != 0 { + return fmt.Errorf("unexpected exit code: %d", exitCode) + } + fields := strings.Fields(strings.TrimSpace(output)) + if len(fields) == 0 || fields[len(fields)-1] != "4" { + return fmt.Errorf("expected 4 extensions loaded from %s, got: %s", rpmExtensionDir, output) + } + return nil + }, + }, + } +} + func getStandardOnlyTests() []Test { tests := append(getSystemStatsAndVectorTests(), getPostGISAuditBackrestTests()...) tests = append(tests, getSupautilsTests()...) From 8db0f98e0afae33d6aec3fa951876bdf4552ec75 Mon Sep 17 00:00:00 2001 From: Muhammad Aqeel Date: Tue, 8 Sep 2026 14:08:03 +0500 Subject: [PATCH 02/16] Chains standard from minimal and builds flavors in per-flavor waves --- .github/actions/build-flavor/action.yml | 117 ++++++++++ .github/actions/merge-flavor/action.yml | 109 ++++++++++ .github/workflows/build_images.yaml | 199 ++++++++++++++++-- .gitignore | 3 + Dockerfile | 43 +++- .../amd64/pg16.15-spock5.0.11-standard.txt | 8 +- .../pg16.15-spock6.0.0-beta1-standard.txt | 8 +- .../amd64/pg17.11-spock5.0.11-standard.txt | 8 +- .../pg17.11-spock6.0.0-beta1-standard.txt | 8 +- .../amd64/pg18.6-spock5.0.11-standard.txt | 8 +- .../pg18.6-spock6.0.0-beta1-standard.txt | 8 +- .../arm64/pg16.15-spock5.0.11-standard.txt | 8 +- .../pg16.15-spock6.0.0-beta1-standard.txt | 8 +- .../arm64/pg17.11-spock5.0.11-standard.txt | 8 +- .../pg17.11-spock6.0.0-beta1-standard.txt | 8 +- .../arm64/pg18.6-spock5.0.11-standard.txt | 8 +- .../pg18.6-spock6.0.0-beta1-standard.txt | 8 +- pgedge.docker-bake.hcl | 28 ++- scripts/build_pgedge_images.py | 138 ++++++++++-- 19 files changed, 642 insertions(+), 91 deletions(-) create mode 100644 .github/actions/build-flavor/action.yml create mode 100644 .github/actions/merge-flavor/action.yml diff --git a/.github/actions/build-flavor/action.yml b/.github/actions/build-flavor/action.yml new file mode 100644 index 0000000..13f69cd --- /dev/null +++ b/.github/actions/build-flavor/action.yml @@ -0,0 +1,117 @@ +name: Build one image flavor +description: > + Builds one flavor of one cell for a single architecture and pushes it by digest. + Push-by-digest leaves the manifest untagged; the merge-flavor action is what + assembles the tagged manifest list, so no consumer-facing tag ever points at a + half-published or unsigned image. + +inputs: + cell: + description: One build entry emitted by scripts/build_pgedge_images.py (PGEDGE_EMIT_MATRIX=1) + required: true + repo: + description: Image repository to push to + required: true + registry_token: + description: > + Token for the container registry. A composite action has no secrets + context, so this has to be passed in by the calling workflow. + required: true + dry_run: + description: When true, resolve and print the build without pushing + required: false + default: "false" + no_cache: + description: When true, build without cache + required: false + default: "false" + +outputs: + digest: + description: Digest of the pushed per-architecture manifest + value: ${{ steps.build.outputs.digest }} + +runs: + using: composite + steps: + # No QEMU: the calling job selects a runner native to the target + # architecture, so nothing here is emulated. + - name: Setup Docker Buildx + shell: bash + run: make buildx-init + + - name: Login to the container registry + if: ${{ inputs.dry_run != 'true' }} + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ inputs.registry_token }} + + - name: Build and push by digest + id: build + shell: bash + env: + CELL: ${{ inputs.cell }} + REPO: ${{ inputs.repo }} + DRY_RUN: ${{ inputs.dry_run }} + NO_CACHE: ${{ inputs.no_cache }} + run: | + set -o errexit -o pipefail -o nounset + + target=$(jq -r '.target' <<< "$CELL") + arch=$(jq -r '.arch' <<< "$CELL") + parent=$(jq -r '.parent_build_tag' <<< "$CELL") + build_tag=$(jq -r '.build_tag' <<< "$CELL") + + args=( + --file pgedge.docker-bake.hcl + --set "default.platform=linux/${arch}" + --set "default.tags=" + --metadata-file metadata.json + ) + if [[ "$NO_CACHE" == "true" ]]; then + args+=(--no-cache) + fi + if [[ "$DRY_RUN" == "true" ]]; then + args+=(--print) + else + args+=(--set "default.output=type=image,name=${REPO},push-by-digest=true,name-canonical=true,push=true") + fi + + # A chained flavor starts FROM the image the previous wave published, so + # its ancestor stages are not part of this build graph at all. Passing + # their packagelists anyway is harmless and keeps the call uniform. + envs=( + "PACKAGE_RELEASE_CHANNEL=$(jq -r '.package_release_channel' <<< "$CELL")" + "POSTGRES_MAJOR_VERSION=$(jq -r '.postgres_major' <<< "$CELL")" + "PACKAGE_LIST_FILE=$(jq -r '.package_list_args.PACKAGE_LIST_FILE' <<< "$CELL")" + "STANDARD_PACKAGE_LIST_FILE=$(jq -r '.package_list_args.STANDARD_PACKAGE_LIST_FILE' <<< "$CELL")" + "COLDFRONT_PACKAGE_LIST_FILE=$(jq -r '.package_list_args.COLDFRONT_PACKAGE_LIST_FILE' <<< "$CELL")" + "TARGET=${target}" + "TAG=${REPO}" + ) + case "$target" in + standard) [[ -n "$parent" ]] && envs+=("MINIMAL_IMAGE=${REPO}:${parent}") ;; + coldfront) [[ -n "$parent" ]] && envs+=("STANDARD_IMAGE=${REPO}:${parent}") ;; + esac + + env "${envs[@]}" docker buildx bake "${args[@]}" + + if [[ "$DRY_RUN" != "true" ]]; then + digest=$(jq -r '.["default"]["containerimage.digest"] // .["containerimage.digest"]' metadata.json) + test -n "$digest" && test "$digest" != "null" + echo "digest=${digest}" >> "$GITHUB_OUTPUT" + mkdir -p digests + echo -n "$digest" > "digests/${arch}" + echo "::notice::${target} ${build_tag} ${arch} -> ${digest}" + fi + + - name: Upload the digest for the merge job + if: ${{ inputs.dry_run != 'true' }} + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: digest-${{ fromJSON(inputs.cell).target }}-${{ fromJSON(inputs.cell).build_tag }}-${{ fromJSON(inputs.cell).arch }} + path: digests/* + retention-days: 1 + if-no-files-found: error diff --git a/.github/actions/merge-flavor/action.yml b/.github/actions/merge-flavor/action.yml new file mode 100644 index 0000000..ebd2a5c --- /dev/null +++ b/.github/actions/merge-flavor/action.yml @@ -0,0 +1,109 @@ +name: Merge one image flavor +description: > + Assembles one flavor's per-architecture digests into a manifest list, signs the + index, and only then applies the mutable tags. Ordering matters: the per-arch + manifests pushed by the build jobs are untagged, and the immutable epoch tag is + created here from an index that is signed before any other tag points at it, so + no consumer-facing tag is ever live without a signature. + +inputs: + cell: + description: One merge entry emitted by scripts/build_pgedge_images.py (PGEDGE_EMIT_MATRIX=1) + required: true + repo: + description: Image repository to publish to + required: true + registry_token: + description: > + Token for the container registry. A composite action has no secrets + context, so this has to be passed in by the calling workflow. + required: true + dry_run: + description: When true, print what would be published and exit + required: false + default: "false" + +runs: + using: composite + steps: + - name: Install cosign + if: ${{ inputs.dry_run != 'true' }} + uses: sigstore/cosign-installer@7e8b541eb2e61bf99390e1afd4be13a184e9ebc5 # v3.10.1 + + - name: Setup Docker Buildx + shell: bash + run: make buildx-init + + - name: Login to the container registry + if: ${{ inputs.dry_run != 'true' }} + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ inputs.registry_token }} + + - name: Collect the per-architecture digests + if: ${{ inputs.dry_run != 'true' && fromJSON(inputs.cell).needs_build }} + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + pattern: digest-*-${{ fromJSON(inputs.cell).build_tag }}-* + path: digests + merge-multiple: true + + - name: Create, sign, then tag + if: ${{ inputs.dry_run != 'true' }} + shell: bash + env: + CELL: ${{ inputs.cell }} + REPO: ${{ inputs.repo }} + run: | + set -o errexit -o pipefail -o nounset + + build_tag=$(jq -r '.build_tag' <<< "$CELL") + + # needs_build is false when the immutable tag was already published and + # republish was not requested. The index is then left exactly as it is -- + # not reassembled, not re-signed -- and only the mutable tags are + # refreshed, so a re-dispatch repairs tag drift without rebuilding. + if [[ "$(jq -r '.needs_build' <<< "$CELL")" == "true" ]]; then + # One source per architecture, addressed by digest. + sources=() + while read -r arch; do + digest=$(cat "digests/${arch}") + test -n "$digest" + sources+=("${REPO}@${digest}") + done < <(jq -r '.arches[]' <<< "$CELL") + test "${#sources[@]}" -gt 0 + + echo "Assembling ${REPO}:${build_tag} from ${#sources[@]} platform manifest(s)" + docker buildx imagetools create --tag "${REPO}:${build_tag}" "${sources[@]}" + + # cosign signs the index. Its children are content-addressed by the + # index, so verifying a tag transitively covers every platform. + index_digest=$(docker buildx imagetools inspect "${REPO}:${build_tag}" \ + --format '{{ printf "%s" .Manifest.Digest }}') + cosign sign --yes "${REPO}@${index_digest}" + else + echo "${REPO}:${build_tag} is already published; refreshing tags only" + fi + + # Mutable tags last, so each points at an already-signed index. + while read -r tag; do + echo "Tagging ${REPO}:${tag}" + docker buildx imagetools create --tag "${REPO}:${tag}" "${REPO}:${build_tag}" + done < <(jq -r '.extra_tags[]' <<< "$CELL") + + - name: Dry run summary + if: ${{ inputs.dry_run == 'true' }} + shell: bash + env: + CELL: ${{ inputs.cell }} + REPO: ${{ inputs.repo }} + run: | + if [[ "$(jq -r '.needs_build' <<< "$CELL")" == "true" ]]; then + echo "would assemble and sign ${REPO}:$(jq -r '.build_tag' <<< "$CELL")" + echo " from arches : $(jq -r '.arches | join(", ")' <<< "$CELL")" + else + echo "${REPO}:$(jq -r '.build_tag' <<< "$CELL") already published; tags only" + fi + echo " mutable tags: $(jq -r '.extra_tags | join(", ")' <<< "$CELL")" diff --git a/.github/workflows/build_images.yaml b/.github/workflows/build_images.yaml index ef9c409..7cc4664 100644 --- a/.github/workflows/build_images.yaml +++ b/.github/workflows/build_images.yaml @@ -1,3 +1,21 @@ +# Per-flavor wave build. +# +# Each flavor is a wave of native single-platform builds -- one job per +# (postgres major, spock version, architecture) -- followed by a merge round +# that assembles the manifest list, signs it, and applies the mutable tags. +# A wave builds FROM the image the previous wave published, so a chained stage +# is never rebuilt on a second runner: rebuilding it would re-run its unpinned +# "dnf update -y" and produce layers that differ from the published parent's. +# +# Why waves rather than one job per cell: +# * a flavor's failure cannot unpublish the flavors beneath it; +# * every layer is built exactly once, so the published images provably share +# them instead of relying on a warm build cache; +# * arm64 builds run on native runners instead of under QEMU. +# +# The matrix comes from scripts/build_pgedge_images.py, so the cell list cannot +# drift away from the image definitions it already owns. + name: build-images on: @@ -9,7 +27,7 @@ on: default: "ghcr.io/pgedge/pgedge-postgres-internal" required: false pgedge_image_republish: - description: "Republish images? (true/false)" + description: "Republish images that are already published? (true/false)" type: boolean default: false pgedge_image_dry_run: @@ -19,7 +37,7 @@ on: pgedge_image_no_cache: description: "Build without cache? (true/false)" type: boolean - default: true + default: false pgedge_image_only_postgres_version: description: "Build only this Postgres version (leave blank for all)" type: string @@ -50,22 +68,27 @@ env: PGEDGE_IMAGE_ONLY_ARCH: ${{ inputs.pgedge_image_only_arch }} jobs: - build-images: - runs-on: ubuntu-latest + plan: + name: Plan waves + runs-on: ubuntu-24.04 + outputs: + minimal_build: ${{ steps.matrix.outputs.minimal_build }} + minimal_merge: ${{ steps.matrix.outputs.minimal_merge }} + standard_build: ${{ steps.matrix.outputs.standard_build }} + standard_merge: ${{ steps.matrix.outputs.standard_merge }} + coldfront_build: ${{ steps.matrix.outputs.coldfront_build }} + coldfront_merge: ${{ steps.matrix.outputs.coldfront_merge }} steps: - name: Checkout repository uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 - - name: Setup QEMU - uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 - - - name: Install cosign - uses: sigstore/cosign-installer@7e8b541eb2e61bf99390e1afd4be13a184e9ebc5 # v3.10.1 - - - name: Setup Docker Buildx - run: | - make buildx-init + - name: Set up python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + with: + python-version: '3.x' + # The plan reads the registry to decide what is already published, so it + # needs credentials even though it publishes nothing itself. - name: Login to GitHub Container Registry uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 with: @@ -73,11 +96,147 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Set up python - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 - with: - python-version: '3.x' - - - name: Build images + - name: Emit the wave matrices + id: matrix run: | - make pgedge-images \ No newline at end of file + set -o errexit -o pipefail + PGEDGE_EMIT_MATRIX=1 ./scripts/build_pgedge_images.py > waves.json + for flavor in minimal standard coldfront; do + for kind in build merge; do + value=$(python3 -c "import json;print(json.dumps(json.load(open('waves.json'))['$flavor']['$kind']))") + echo "${flavor}_${kind}=${value}" >> "$GITHUB_OUTPUT" + done + done + + - name: Show the plan + run: python3 -m json.tool waves.json + + build-minimal: + name: "minimal ${{ matrix.cell.name }}" + needs: [plan] + if: ${{ fromJSON(needs.plan.outputs.minimal_build)[0] != null }} + runs-on: ${{ matrix.cell.runner }} + strategy: + fail-fast: false + matrix: + cell: ${{ fromJSON(needs.plan.outputs.minimal_build) }} + steps: + - name: Checkout repository + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + + - name: Build and push by digest + uses: ./.github/actions/build-flavor + with: + cell: ${{ toJSON(matrix.cell) }} + repo: ${{ inputs.pgedge_image_repo }} + registry_token: ${{ secrets.GITHUB_TOKEN }} + dry_run: ${{ inputs.pgedge_image_dry_run }} + no_cache: ${{ inputs.pgedge_image_no_cache }} + + merge-minimal: + name: "merge minimal ${{ matrix.cell.name }}" + needs: [plan, build-minimal] + if: ${{ fromJSON(needs.plan.outputs.minimal_merge)[0] != null }} + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + cell: ${{ fromJSON(needs.plan.outputs.minimal_merge) }} + steps: + - name: Checkout repository + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + + - name: Merge, sign and tag + uses: ./.github/actions/merge-flavor + with: + cell: ${{ toJSON(matrix.cell) }} + repo: ${{ inputs.pgedge_image_repo }} + registry_token: ${{ secrets.GITHUB_TOKEN }} + dry_run: ${{ inputs.pgedge_image_dry_run }} + + build-standard: + name: "standard ${{ matrix.cell.name }}" + needs: [plan, merge-minimal] + if: ${{ fromJSON(needs.plan.outputs.standard_build)[0] != null }} + runs-on: ${{ matrix.cell.runner }} + strategy: + fail-fast: false + matrix: + cell: ${{ fromJSON(needs.plan.outputs.standard_build) }} + steps: + - name: Checkout repository + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + + - name: Build and push by digest + uses: ./.github/actions/build-flavor + with: + cell: ${{ toJSON(matrix.cell) }} + repo: ${{ inputs.pgedge_image_repo }} + registry_token: ${{ secrets.GITHUB_TOKEN }} + dry_run: ${{ inputs.pgedge_image_dry_run }} + no_cache: ${{ inputs.pgedge_image_no_cache }} + + merge-standard: + name: "merge standard ${{ matrix.cell.name }}" + needs: [plan, build-standard] + if: ${{ fromJSON(needs.plan.outputs.standard_merge)[0] != null }} + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + cell: ${{ fromJSON(needs.plan.outputs.standard_merge) }} + steps: + - name: Checkout repository + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + + - name: Merge, sign and tag + uses: ./.github/actions/merge-flavor + with: + cell: ${{ toJSON(matrix.cell) }} + repo: ${{ inputs.pgedge_image_repo }} + registry_token: ${{ secrets.GITHUB_TOKEN }} + dry_run: ${{ inputs.pgedge_image_dry_run }} + + # A failure here leaves this run's minimal and standard images published. + build-coldfront: + name: "coldfront ${{ matrix.cell.name }}" + needs: [plan, merge-standard] + if: ${{ fromJSON(needs.plan.outputs.coldfront_build)[0] != null }} + runs-on: ${{ matrix.cell.runner }} + strategy: + fail-fast: false + matrix: + cell: ${{ fromJSON(needs.plan.outputs.coldfront_build) }} + steps: + - name: Checkout repository + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + + - name: Build and push by digest + uses: ./.github/actions/build-flavor + with: + cell: ${{ toJSON(matrix.cell) }} + repo: ${{ inputs.pgedge_image_repo }} + registry_token: ${{ secrets.GITHUB_TOKEN }} + dry_run: ${{ inputs.pgedge_image_dry_run }} + no_cache: ${{ inputs.pgedge_image_no_cache }} + + merge-coldfront: + name: "merge coldfront ${{ matrix.cell.name }}" + needs: [plan, build-coldfront] + if: ${{ fromJSON(needs.plan.outputs.coldfront_merge)[0] != null }} + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + cell: ${{ fromJSON(needs.plan.outputs.coldfront_merge) }} + steps: + - name: Checkout repository + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + + - name: Merge, sign and tag + uses: ./.github/actions/merge-flavor + with: + cell: ${{ toJSON(matrix.cell) }} + repo: ${{ inputs.pgedge_image_repo }} + registry_token: ${{ secrets.GITHUB_TOKEN }} + dry_run: ${{ inputs.pgedge_image_dry_run }} diff --git a/.gitignore b/.gitignore index 44b7acb..05a3223 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,6 @@ __pycache__/ # Go binaries tests/tests +# Scratch written by the image build (bake metadata, per-arch digests) +digests/ +metadata.json diff --git a/Dockerfile b/Dockerfile index de46056..2b94f83 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,6 +2,11 @@ # base image for all flavors # ############################## +# Global ARG: an ARG used in a FROM instruction has to be declared before the +# first FROM, outside any stage. See the coldfront stage for what it selects. +ARG MINIMAL_IMAGE=minimal +ARG STANDARD_IMAGE=standard + FROM rockylinux/rockylinux:9-ubi AS base ARG PACKAGE_RELEASE_CHANNEL="" @@ -48,7 +53,7 @@ set -o errexit set -o pipefail set -o nounset -xargs dnf install -y < /usr/share/pgedge/packages.txt +grep -vE '^[[:space:]]*(#|$)' /usr/share/pgedge/packages.txt | xargs dnf install -y # Patch any OS packages (including transitive dependencies pulled in above) # to the latest available errata so the image ships with security fixes. dnf update -y @@ -97,13 +102,22 @@ CMD ["postgres"] # standard-flavored image # ########################### -FROM base AS standard +# MINIMAL_IMAGE works exactly like STANDARD_IMAGE on the coldfront stage: the +# default resolves to the stage above for a single-graph build, and a registry +# reference makes this stage start from an already-published minimal, which is +# what a per-flavor CI wave needs. Chaining rather than a second FROM base is +# what makes standard's inherited layers byte-identical to minimal's -- as +# parallel stages they shared only 1 of 6. +FROM ${MINIMAL_IMAGE} AS standard -ARG PACKAGE_LIST_FILE +ARG STANDARD_PACKAGE_LIST_FILE ARG TARGETARCH ARG POSTGRES_MAJOR_VERSION -COPY packagelists/${TARGETARCH}/${PACKAGE_LIST_FILE} /usr/share/pgedge/packages.txt +# The inherited stage ends as USER postgres. +USER root + +COPY packagelists/${TARGETARCH}/${STANDARD_PACKAGE_LIST_FILE} /usr/share/pgedge/packages.txt RUN < and are dnf's to resolve. # Deliberately no second "dnf update -y": the inherited standard layers already # ran one, and repeating it here would move ColdFront past its pinned NVR. -xargs dnf install -y --setopt=install_weak_deps=False < /usr/share/pgedge/coldfront-packages.txt +grep -vE '^[[:space:]]*(#|$)' /usr/share/pgedge/coldfront-packages.txt \ + | xargs dnf install -y --setopt=install_weak_deps=False dnf clean all CFEOF diff --git a/packagelists/amd64/pg16.15-spock5.0.11-standard.txt b/packagelists/amd64/pg16.15-spock5.0.11-standard.txt index 210dbde..847137e 100644 --- a/packagelists/amd64/pg16.15-spock5.0.11-standard.txt +++ b/packagelists/amd64/pg16.15-spock5.0.11-standard.txt @@ -1,7 +1,7 @@ -pgedge-postgresql16-16.15-1.el9 -pgedge-spock50_16-5.0.11-1.el9 -pgedge-snowflake_16-2.6.0-1.el9 -pgedge-lolor_16-1.2.2-1.el9 +# Delta over pg16.15-spock5.0.11-minimal.txt -- standard is chained FROM minimal, so the +# packages minimal already pins are deliberately absent here. Re-pinning one +# that minimal's 'dnf update -y' has moved past its NVR is a downgrade, which +# dnf refuses. pgedge-pgaudit_16-16.1-1.el9 pgedge-postgis36_16-3.6.4-1.el9 pgedge-pgvector_16-0.8.5-1.el9 diff --git a/packagelists/amd64/pg16.15-spock6.0.0-beta1-standard.txt b/packagelists/amd64/pg16.15-spock6.0.0-beta1-standard.txt index 902bcc7..fff5c7f 100644 --- a/packagelists/amd64/pg16.15-spock6.0.0-beta1-standard.txt +++ b/packagelists/amd64/pg16.15-spock6.0.0-beta1-standard.txt @@ -1,7 +1,7 @@ -pgedge-postgresql16-16.15-1.el9 -pgedge-spock60_16-6.0.0-beta1_1.el9 -pgedge-snowflake_16-2.6.0-1.el9 -pgedge-lolor_16-1.2.2-1.el9 +# Delta over pg16.15-spock6.0.0-beta1-minimal.txt -- standard is chained FROM minimal, so the +# packages minimal already pins are deliberately absent here. Re-pinning one +# that minimal's 'dnf update -y' has moved past its NVR is a downgrade, which +# dnf refuses. pgedge-pgaudit_16-16.1-1.el9 pgedge-postgis36_16-3.6.4-1.el9 pgedge-pgvector_16-0.8.5-1.el9 diff --git a/packagelists/amd64/pg17.11-spock5.0.11-standard.txt b/packagelists/amd64/pg17.11-spock5.0.11-standard.txt index 3d3df4d..9a3420f 100644 --- a/packagelists/amd64/pg17.11-spock5.0.11-standard.txt +++ b/packagelists/amd64/pg17.11-spock5.0.11-standard.txt @@ -1,7 +1,7 @@ -pgedge-postgresql17-17.11-1.el9 -pgedge-spock50_17-5.0.11-1.el9 -pgedge-snowflake_17-2.6.0-1.el9 -pgedge-lolor_17-1.2.2-1.el9 +# Delta over pg17.11-spock5.0.11-minimal.txt -- standard is chained FROM minimal, so the +# packages minimal already pins are deliberately absent here. Re-pinning one +# that minimal's 'dnf update -y' has moved past its NVR is a downgrade, which +# dnf refuses. pgedge-pgaudit_17-17.1-1.el9 pgedge-postgis36_17-3.6.4-1.el9 pgedge-pgvector_17-0.8.5-1.el9 diff --git a/packagelists/amd64/pg17.11-spock6.0.0-beta1-standard.txt b/packagelists/amd64/pg17.11-spock6.0.0-beta1-standard.txt index 925c82f..3f36542 100644 --- a/packagelists/amd64/pg17.11-spock6.0.0-beta1-standard.txt +++ b/packagelists/amd64/pg17.11-spock6.0.0-beta1-standard.txt @@ -1,7 +1,7 @@ -pgedge-postgresql17-17.11-1.el9 -pgedge-spock60_17-6.0.0-beta1_1.el9 -pgedge-snowflake_17-2.6.0-1.el9 -pgedge-lolor_17-1.2.2-1.el9 +# Delta over pg17.11-spock6.0.0-beta1-minimal.txt -- standard is chained FROM minimal, so the +# packages minimal already pins are deliberately absent here. Re-pinning one +# that minimal's 'dnf update -y' has moved past its NVR is a downgrade, which +# dnf refuses. pgedge-pgaudit_17-17.1-1.el9 pgedge-postgis36_17-3.6.4-1.el9 pgedge-pgvector_17-0.8.5-1.el9 diff --git a/packagelists/amd64/pg18.6-spock5.0.11-standard.txt b/packagelists/amd64/pg18.6-spock5.0.11-standard.txt index dddbfaf..eefe6f3 100644 --- a/packagelists/amd64/pg18.6-spock5.0.11-standard.txt +++ b/packagelists/amd64/pg18.6-spock5.0.11-standard.txt @@ -1,7 +1,7 @@ -pgedge-postgresql18-18.6-1.el9 -pgedge-spock50_18-5.0.11-1.el9 -pgedge-snowflake_18-2.6.0-1.el9 -pgedge-lolor_18-1.2.2-1.el9 +# Delta over pg18.6-spock5.0.11-minimal.txt -- standard is chained FROM minimal, so the +# packages minimal already pins are deliberately absent here. Re-pinning one +# that minimal's 'dnf update -y' has moved past its NVR is a downgrade, which +# dnf refuses. pgedge-pgaudit_18-18.0-1.el9 pgedge-postgis36_18-3.6.4-1.el9 pgedge-pgvector_18-0.8.5-1.el9 diff --git a/packagelists/amd64/pg18.6-spock6.0.0-beta1-standard.txt b/packagelists/amd64/pg18.6-spock6.0.0-beta1-standard.txt index 444748b..fbc4e1a 100644 --- a/packagelists/amd64/pg18.6-spock6.0.0-beta1-standard.txt +++ b/packagelists/amd64/pg18.6-spock6.0.0-beta1-standard.txt @@ -1,7 +1,7 @@ -pgedge-postgresql18-18.6-1.el9 -pgedge-spock60_18-6.0.0-beta1_1.el9 -pgedge-snowflake_18-2.6.0-1.el9 -pgedge-lolor_18-1.2.2-1.el9 +# Delta over pg18.6-spock6.0.0-beta1-minimal.txt -- standard is chained FROM minimal, so the +# packages minimal already pins are deliberately absent here. Re-pinning one +# that minimal's 'dnf update -y' has moved past its NVR is a downgrade, which +# dnf refuses. pgedge-pgaudit_18-18.0-1.el9 pgedge-postgis36_18-3.6.4-1.el9 pgedge-pgvector_18-0.8.5-1.el9 diff --git a/packagelists/arm64/pg16.15-spock5.0.11-standard.txt b/packagelists/arm64/pg16.15-spock5.0.11-standard.txt index 210dbde..847137e 100644 --- a/packagelists/arm64/pg16.15-spock5.0.11-standard.txt +++ b/packagelists/arm64/pg16.15-spock5.0.11-standard.txt @@ -1,7 +1,7 @@ -pgedge-postgresql16-16.15-1.el9 -pgedge-spock50_16-5.0.11-1.el9 -pgedge-snowflake_16-2.6.0-1.el9 -pgedge-lolor_16-1.2.2-1.el9 +# Delta over pg16.15-spock5.0.11-minimal.txt -- standard is chained FROM minimal, so the +# packages minimal already pins are deliberately absent here. Re-pinning one +# that minimal's 'dnf update -y' has moved past its NVR is a downgrade, which +# dnf refuses. pgedge-pgaudit_16-16.1-1.el9 pgedge-postgis36_16-3.6.4-1.el9 pgedge-pgvector_16-0.8.5-1.el9 diff --git a/packagelists/arm64/pg16.15-spock6.0.0-beta1-standard.txt b/packagelists/arm64/pg16.15-spock6.0.0-beta1-standard.txt index 902bcc7..fff5c7f 100644 --- a/packagelists/arm64/pg16.15-spock6.0.0-beta1-standard.txt +++ b/packagelists/arm64/pg16.15-spock6.0.0-beta1-standard.txt @@ -1,7 +1,7 @@ -pgedge-postgresql16-16.15-1.el9 -pgedge-spock60_16-6.0.0-beta1_1.el9 -pgedge-snowflake_16-2.6.0-1.el9 -pgedge-lolor_16-1.2.2-1.el9 +# Delta over pg16.15-spock6.0.0-beta1-minimal.txt -- standard is chained FROM minimal, so the +# packages minimal already pins are deliberately absent here. Re-pinning one +# that minimal's 'dnf update -y' has moved past its NVR is a downgrade, which +# dnf refuses. pgedge-pgaudit_16-16.1-1.el9 pgedge-postgis36_16-3.6.4-1.el9 pgedge-pgvector_16-0.8.5-1.el9 diff --git a/packagelists/arm64/pg17.11-spock5.0.11-standard.txt b/packagelists/arm64/pg17.11-spock5.0.11-standard.txt index 3d3df4d..9a3420f 100644 --- a/packagelists/arm64/pg17.11-spock5.0.11-standard.txt +++ b/packagelists/arm64/pg17.11-spock5.0.11-standard.txt @@ -1,7 +1,7 @@ -pgedge-postgresql17-17.11-1.el9 -pgedge-spock50_17-5.0.11-1.el9 -pgedge-snowflake_17-2.6.0-1.el9 -pgedge-lolor_17-1.2.2-1.el9 +# Delta over pg17.11-spock5.0.11-minimal.txt -- standard is chained FROM minimal, so the +# packages minimal already pins are deliberately absent here. Re-pinning one +# that minimal's 'dnf update -y' has moved past its NVR is a downgrade, which +# dnf refuses. pgedge-pgaudit_17-17.1-1.el9 pgedge-postgis36_17-3.6.4-1.el9 pgedge-pgvector_17-0.8.5-1.el9 diff --git a/packagelists/arm64/pg17.11-spock6.0.0-beta1-standard.txt b/packagelists/arm64/pg17.11-spock6.0.0-beta1-standard.txt index 925c82f..3f36542 100644 --- a/packagelists/arm64/pg17.11-spock6.0.0-beta1-standard.txt +++ b/packagelists/arm64/pg17.11-spock6.0.0-beta1-standard.txt @@ -1,7 +1,7 @@ -pgedge-postgresql17-17.11-1.el9 -pgedge-spock60_17-6.0.0-beta1_1.el9 -pgedge-snowflake_17-2.6.0-1.el9 -pgedge-lolor_17-1.2.2-1.el9 +# Delta over pg17.11-spock6.0.0-beta1-minimal.txt -- standard is chained FROM minimal, so the +# packages minimal already pins are deliberately absent here. Re-pinning one +# that minimal's 'dnf update -y' has moved past its NVR is a downgrade, which +# dnf refuses. pgedge-pgaudit_17-17.1-1.el9 pgedge-postgis36_17-3.6.4-1.el9 pgedge-pgvector_17-0.8.5-1.el9 diff --git a/packagelists/arm64/pg18.6-spock5.0.11-standard.txt b/packagelists/arm64/pg18.6-spock5.0.11-standard.txt index dddbfaf..eefe6f3 100644 --- a/packagelists/arm64/pg18.6-spock5.0.11-standard.txt +++ b/packagelists/arm64/pg18.6-spock5.0.11-standard.txt @@ -1,7 +1,7 @@ -pgedge-postgresql18-18.6-1.el9 -pgedge-spock50_18-5.0.11-1.el9 -pgedge-snowflake_18-2.6.0-1.el9 -pgedge-lolor_18-1.2.2-1.el9 +# Delta over pg18.6-spock5.0.11-minimal.txt -- standard is chained FROM minimal, so the +# packages minimal already pins are deliberately absent here. Re-pinning one +# that minimal's 'dnf update -y' has moved past its NVR is a downgrade, which +# dnf refuses. pgedge-pgaudit_18-18.0-1.el9 pgedge-postgis36_18-3.6.4-1.el9 pgedge-pgvector_18-0.8.5-1.el9 diff --git a/packagelists/arm64/pg18.6-spock6.0.0-beta1-standard.txt b/packagelists/arm64/pg18.6-spock6.0.0-beta1-standard.txt index 444748b..fbc4e1a 100644 --- a/packagelists/arm64/pg18.6-spock6.0.0-beta1-standard.txt +++ b/packagelists/arm64/pg18.6-spock6.0.0-beta1-standard.txt @@ -1,7 +1,7 @@ -pgedge-postgresql18-18.6-1.el9 -pgedge-spock60_18-6.0.0-beta1_1.el9 -pgedge-snowflake_18-2.6.0-1.el9 -pgedge-lolor_18-1.2.2-1.el9 +# Delta over pg18.6-spock6.0.0-beta1-minimal.txt -- standard is chained FROM minimal, so the +# packages minimal already pins are deliberately absent here. Re-pinning one +# that minimal's 'dnf update -y' has moved past its NVR is a downgrade, which +# dnf refuses. pgedge-pgaudit_18-18.0-1.el9 pgedge-postgis36_18-3.6.4-1.el9 pgedge-pgvector_18-0.8.5-1.el9 diff --git a/pgedge.docker-bake.hcl b/pgedge.docker-bake.hcl index 4886bcb..a7831d4 100644 --- a/pgedge.docker-bake.hcl +++ b/pgedge.docker-bake.hcl @@ -21,14 +21,33 @@ variable "PACKAGE_LIST_FILE" { default = "" } -// Chained flavors need their own packagelist ARG. A flavor built FROM another -// flavor still triggers the parent stage, which consumes PACKAGE_LIST_FILE, so -// reusing that variable would make the parent install the child's list. +// Every stage in the chain needs its own packagelist variable. A chained flavor +// still triggers its ancestors' stages, and each of those consumes its own ARG, +// so one shared variable would make an ancestor install a descendant's list. +variable "STANDARD_PACKAGE_LIST_FILE" { + type = string + default = "" +} + variable "COLDFRONT_PACKAGE_LIST_FILE" { type = string default = "" } +// Select what each chained stage is built FROM. Empty keeps the in-Dockerfile +// default (the parent stage), which is the single-graph build. A registry +// reference switches that stage to start from an already-published image, which +// is what a per-flavor CI wave needs. +variable "MINIMAL_IMAGE" { + type = string + default = "minimal" +} + +variable "STANDARD_IMAGE" { + type = string + default = "standard" +} + variable "TAG" { type = string default = "pgedge" @@ -41,7 +60,10 @@ target "default" { args = { PACKAGE_RELEASE_CHANNEL = PACKAGE_RELEASE_CHANNEL PACKAGE_LIST_FILE = PACKAGE_LIST_FILE + STANDARD_PACKAGE_LIST_FILE = STANDARD_PACKAGE_LIST_FILE COLDFRONT_PACKAGE_LIST_FILE = COLDFRONT_PACKAGE_LIST_FILE + MINIMAL_IMAGE = MINIMAL_IMAGE + STANDARD_IMAGE = STANDARD_IMAGE POSTGRES_MAJOR_VERSION = POSTGRES_MAJOR_VERSION } platforms = [ diff --git a/scripts/build_pgedge_images.py b/scripts/build_pgedge_images.py index 9ef88c8..69feea9 100755 --- a/scripts/build_pgedge_images.py +++ b/scripts/build_pgedge_images.py @@ -34,10 +34,18 @@ def from_env() -> "Config": ) -# Flavors that are built FROM another flavor rather than FROM base. A chained -# flavor's build also runs its parent's stage, so the parent's packagelist has to -# be passed alongside the child's -- see build(). -FLAVOR_PARENTS = {"coldfront": "standard"} +# Flavors that are built FROM another flavor rather than FROM base. Building a +# chained flavor in one graph also runs every ancestor's stage, and each stage +# consumes its own packagelist ARG, so build() has to pass the whole ancestry -- +# see PgEdgeImage.package_list_args. +FLAVOR_PARENTS = {"standard": "minimal", "coldfront": "standard"} + +# The Dockerfile ARG each flavor's stage reads its packagelist from. +FLAVOR_LIST_ARGS = { + "minimal": "PACKAGE_LIST_FILE", + "standard": "STANDARD_PACKAGE_LIST_FILE", + "coldfront": "COLDFRONT_PACKAGE_LIST_FILE", +} # Flavors built for every image. coldfront is deliberately absent: ColdFront's # cold-write protocol is validated against spock 5 only, so it is opted in per @@ -98,10 +106,46 @@ def package_list(self) -> str: return self._package_list_for(self.flavor) @property - def parent_package_list(self) -> str: - """Packagelist of the flavor this one is chained FROM, or "" if none.""" + def parent_build_tag(self) -> str: + """Immutable tag of the flavor this one is chained FROM, or "" if none. + + The per-flavor wave model builds each flavor FROM the image the previous + wave published, so the stage is never rebuilt on a different runner. + """ parent = FLAVOR_PARENTS.get(self.flavor) - return self._package_list_for(parent) if parent else "" + if not parent: + return "" + return str( + Tag( + postgres_version=self.postgres_version, + flavor=parent, + spock_version=self.spock_version, + epoch=self.epoch, + ) + ) + + @property + def ancestry(self) -> list[str]: + """This flavor and every flavor it is chained FROM, base-most first.""" + chain = [self.flavor] + while FLAVOR_PARENTS.get(chain[0]): + chain.insert(0, FLAVOR_PARENTS[chain[0]]) + return chain + + @property + def package_list_args(self) -> dict[str, str]: + """One packagelist build-arg per stage in this image's ancestry. + + A single-graph build of a chained flavor runs its ancestors' stages too, + and each reads its own ARG, so all of them have to be supplied. Args for + flavors outside the ancestry are sent empty so bake does not carry a + stale value over from another image. + """ + chain = self.ancestry + return { + arg: (self._package_list_for(flavor) if flavor in chain else "") + for flavor, arg in FLAVOR_LIST_ARGS.items() + } @property def build_tag(self) -> Tag: @@ -234,6 +278,74 @@ def make_all_flavor_images( ] +# Runner label per architecture. arm64 builds go to a native runner rather than +# QEMU on an amd64 host: emulated dnf transactions dominate the build time. +ARCH_RUNNERS = {"amd64": "ubuntu-24.04", "arm64": "ubuntu-24.04-arm"} + +FLAVOR_WAVES = ["minimal", "standard", "coldfront"] + + +def emit_matrix(config: "Config") -> None: + """Print the per-wave build and merge matrices as JSON. + + The workflow consumes this instead of hardcoding the cell list, so the + matrix and the image definitions above cannot drift apart. + + An image whose immutable tag is already published is left out of the build + matrix unless republish is set, but stays in the merge matrix with + needs_build false, so a re-dispatch repairs its mutable tags without + rebuilding anything. + """ + arches = [config.only_arch] if config.only_arch else list(ARCH_RUNNERS) + waves: dict = {} + + for flavor in FLAVOR_WAVES: + builds: list[dict] = [] + merges: list[dict] = [] + + for image in all_images: + if image.flavor != flavor or _should_skip_image(image, config): + continue + + needs_build = config.republish or not published_digests( + config.repo, image.build_tag + ) + if not needs_build: + logging.info(f"{image.build_tag} is already published") + + merges.append( + { + "name": f"{image.postgres_major}-spock{image.spock_major}", + "build_tag": str(image.build_tag), + "extra_tags": [str(t) for t in image.extra_tags], + "arches": arches, + "needs_build": needs_build, + } + ) + + if not needs_build: + continue + + for arch in arches: + builds.append( + { + "name": f"{image.postgres_major}-spock{image.spock_major}-{arch}", + "runner": ARCH_RUNNERS[arch], + "arch": arch, + "target": flavor, + "build_tag": str(image.build_tag), + "postgres_major": image.postgres_major, + "package_release_channel": image.package_release_channel, + "parent_build_tag": image.parent_build_tag, + "package_list_args": image.package_list_args, + } + ) + + waves[flavor] = {"build": builds, "merge": merges} + + print(json.dumps(waves)) + + def validate_images(images: list[PgEdgeImage]): all_tags = set() @@ -308,12 +420,7 @@ def build( **os.environ.copy(), "PACKAGE_RELEASE_CHANNEL": image.package_release_channel, "POSTGRES_MAJOR_VERSION": image.postgres_major, - # A chained flavor needs its parent's list for the inherited stage and - # its own for the delta stage; an unchained flavor sends only its own. - "PACKAGE_LIST_FILE": image.parent_package_list or image.package_list, - "COLDFRONT_PACKAGE_LIST_FILE": ( - image.package_list if image.parent_package_list else "" - ), + **image.package_list_args, "TAG": f"{repo}:{image.build_tag}", "TARGET": image.flavor, }, @@ -409,6 +516,11 @@ def main(): print(",".join(get_latest_tags())) return + if os.getenv("PGEDGE_EMIT_MATRIX", "0") == "1": + validate_images(all_images) + emit_matrix(config) + return + _log_config(config) validate_images(all_images) From 49799c63d0ebe1cf56c660ffc75229ed88a8a46d Mon Sep 17 00:00:00 2001 From: Muhammad Aqeel Date: Tue, 8 Sep 2026 14:19:05 +0500 Subject: [PATCH 03/16] fix(ci): block wave jobs only on upstream failure, not on skips --- .github/workflows/build_images.yaml | 54 ++++++++++++++++++++++++----- 1 file changed, 45 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build_images.yaml b/.github/workflows/build_images.yaml index 7cc4664..d3ba9bb 100644 --- a/.github/workflows/build_images.yaml +++ b/.github/workflows/build_images.yaml @@ -135,8 +135,18 @@ jobs: merge-minimal: name: "merge minimal ${{ matrix.cell.name }}" + # An empty matrix skips a job, and a skipped "needs" fails the implicit + # success() of everything downstream. So each job below lists its whole + # upstream chain and blocks only on a real failure. Every upstream job is + # named rather than just the previous one, because a failure reaches the + # next job as a *skip*: checking only the immediate predecessor would let a + # later wave build on top of a broken earlier one. needs: [plan, build-minimal] - if: ${{ fromJSON(needs.plan.outputs.minimal_merge)[0] != null }} + if: >- + ${{ !cancelled() + && needs.plan.result == 'success' + && needs.build-minimal.result != 'failure' + && fromJSON(needs.plan.outputs.minimal_merge)[0] != null }} runs-on: ubuntu-24.04 strategy: fail-fast: false @@ -156,8 +166,13 @@ jobs: build-standard: name: "standard ${{ matrix.cell.name }}" - needs: [plan, merge-minimal] - if: ${{ fromJSON(needs.plan.outputs.standard_build)[0] != null }} + needs: [plan, build-minimal, merge-minimal] + if: >- + ${{ !cancelled() + && needs.plan.result == 'success' + && needs.build-minimal.result != 'failure' + && needs.merge-minimal.result != 'failure' + && fromJSON(needs.plan.outputs.standard_build)[0] != null }} runs-on: ${{ matrix.cell.runner }} strategy: fail-fast: false @@ -178,8 +193,14 @@ jobs: merge-standard: name: "merge standard ${{ matrix.cell.name }}" - needs: [plan, build-standard] - if: ${{ fromJSON(needs.plan.outputs.standard_merge)[0] != null }} + needs: [plan, build-minimal, merge-minimal, build-standard] + if: >- + ${{ !cancelled() + && needs.plan.result == 'success' + && needs.build-minimal.result != 'failure' + && needs.merge-minimal.result != 'failure' + && needs.build-standard.result != 'failure' + && fromJSON(needs.plan.outputs.standard_merge)[0] != null }} runs-on: ubuntu-24.04 strategy: fail-fast: false @@ -200,8 +221,15 @@ jobs: # A failure here leaves this run's minimal and standard images published. build-coldfront: name: "coldfront ${{ matrix.cell.name }}" - needs: [plan, merge-standard] - if: ${{ fromJSON(needs.plan.outputs.coldfront_build)[0] != null }} + needs: [plan, build-minimal, merge-minimal, build-standard, merge-standard] + if: >- + ${{ !cancelled() + && needs.plan.result == 'success' + && needs.build-minimal.result != 'failure' + && needs.merge-minimal.result != 'failure' + && needs.build-standard.result != 'failure' + && needs.merge-standard.result != 'failure' + && fromJSON(needs.plan.outputs.coldfront_build)[0] != null }} runs-on: ${{ matrix.cell.runner }} strategy: fail-fast: false @@ -222,8 +250,16 @@ jobs: merge-coldfront: name: "merge coldfront ${{ matrix.cell.name }}" - needs: [plan, build-coldfront] - if: ${{ fromJSON(needs.plan.outputs.coldfront_merge)[0] != null }} + needs: [plan, build-minimal, merge-minimal, build-standard, merge-standard, build-coldfront] + if: >- + ${{ !cancelled() + && needs.plan.result == 'success' + && needs.build-minimal.result != 'failure' + && needs.merge-minimal.result != 'failure' + && needs.build-standard.result != 'failure' + && needs.merge-standard.result != 'failure' + && needs.build-coldfront.result != 'failure' + && fromJSON(needs.plan.outputs.coldfront_merge)[0] != null }} runs-on: ubuntu-24.04 strategy: fail-fast: false From fa11fd453c9066ed506afbc5407bcdc1c8a3370c Mon Sep 17 00:00:00 2001 From: Muhammad Aqeel Date: Tue, 8 Sep 2026 14:52:42 +0500 Subject: [PATCH 04/16] fix(ci): select the buildx builder so attestations work in the wave build --- .github/actions/build-flavor/action.yml | 8 +++++++- .github/actions/merge-flavor/action.yml | 8 +++++++- Makefile | 6 ++++++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/.github/actions/build-flavor/action.yml b/.github/actions/build-flavor/action.yml index 13f69cd..6fae4eb 100644 --- a/.github/actions/build-flavor/action.yml +++ b/.github/actions/build-flavor/action.yml @@ -36,9 +36,15 @@ runs: steps: # No QEMU: the calling job selects a runner native to the target # architecture, so nothing here is emulated. + # buildx-init only registers the builder; buildx selects it via the + # BUILDX_BUILDER environment variable. Without this the build lands on the + # default docker driver, which rejects the bake file's attestations. - name: Setup Docker Buildx shell: bash - run: make buildx-init + run: | + set -o errexit -o pipefail + make buildx-init + echo "BUILDX_BUILDER=$(make -s print-buildx-builder)" >> "$GITHUB_ENV" - name: Login to the container registry if: ${{ inputs.dry_run != 'true' }} diff --git a/.github/actions/merge-flavor/action.yml b/.github/actions/merge-flavor/action.yml index ebd2a5c..2b334be 100644 --- a/.github/actions/merge-flavor/action.yml +++ b/.github/actions/merge-flavor/action.yml @@ -30,9 +30,15 @@ runs: if: ${{ inputs.dry_run != 'true' }} uses: sigstore/cosign-installer@7e8b541eb2e61bf99390e1afd4be13a184e9ebc5 # v3.10.1 + # buildx-init only registers the builder; buildx selects it via the + # BUILDX_BUILDER environment variable. Without this the build lands on the + # default docker driver, which rejects the bake file's attestations. - name: Setup Docker Buildx shell: bash - run: make buildx-init + run: | + set -o errexit -o pipefail + make buildx-init + echo "BUILDX_BUILDER=$(make -s print-buildx-builder)" >> "$GITHUB_ENV" - name: Login to the container registry if: ${{ inputs.dry_run != 'true' }} diff --git a/Makefile b/Makefile index 5555b68..0782607 100644 --- a/Makefile +++ b/Makefile @@ -36,6 +36,12 @@ buildx-init: --platform=linux/arm64,linux/amd64 \ --config=$(BUILDX_CONFIG) +# Prints the builder name so callers other than pgedge-images can select it. +# buildx-init only *creates* the builder; buildx picks it up via BUILDX_BUILDER. +.PHONY: print-buildx-builder +print-buildx-builder: + @echo $(BUILDX_BUILDER) + .PHONY: pgedge-images pgedge-images: PGEDGE_IMAGE_REPO=$(PGEDGE_IMAGE_REPO) \ From 6ccadc0d500ef1a5aa72393219fd0e13a370d473 Mon Sep 17 00:00:00 2001 From: Muhammad Aqeel Date: Tue, 8 Sep 2026 16:46:01 +0500 Subject: [PATCH 05/16] fix(ci): survive a bad or stalling package mirror --- Dockerfile | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Dockerfile b/Dockerfile index 2b94f83..fcf422a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,6 +22,14 @@ set -o nounset useradd -u ${POSTGRES_USER_ID} -m postgres -s /bin/bash +cat >> /etc/dnf/dnf.conf <<'CONF' +retries=10 +timeout=30 +minrate=100k +fastestmirror=1 +max_parallel_downloads=10 +CONF + dnf install -y epel-release dnf dnf config-manager --set-enabled crb dnf update -y --allowerasing From 08cd17373c0f4ad0c65e50b6360d5e6b6e953a08 Mon Sep 17 00:00:00 2001 From: Muhammad Aqeel Date: Wed, 9 Sep 2026 17:40:22 +0500 Subject: [PATCH 06/16] Configures coldfront from the environment and tests each pushed image --- .github/actions/build-flavor/action.yml | 26 ++++ Dockerfile | 9 ++ coldfront-entrypoint.sh | 159 ++++++++++++++++++++---- 3 files changed, 170 insertions(+), 24 deletions(-) diff --git a/.github/actions/build-flavor/action.yml b/.github/actions/build-flavor/action.yml index 6fae4eb..e141e98 100644 --- a/.github/actions/build-flavor/action.yml +++ b/.github/actions/build-flavor/action.yml @@ -54,6 +54,13 @@ runs: username: ${{ github.actor }} password: ${{ inputs.registry_token }} + - name: Set up Go + if: ${{ inputs.dry_run != 'true' }} + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 + with: + go-version: '1.25' + cache-dependency-path: tests/go.sum + - name: Build and push by digest id: build shell: bash @@ -113,6 +120,25 @@ runs: echo "::notice::${target} ${build_tag} ${arch} -> ${digest}" fi + # Runs against the bytes just pushed, on a runner native to their + # architecture, so a broken image never reaches the merge step. The digest is + # re-tagged locally first: the harness derives the expected spock major from + # the tag and silently skips that assertion for a bare digest reference. + - name: Test the pushed image + if: ${{ inputs.dry_run != 'true' }} + shell: bash + env: + CELL: ${{ inputs.cell }} + REPO: ${{ inputs.repo }} + DIGEST: ${{ steps.build.outputs.digest }} + run: | + set -o errexit -o pipefail -o nounset + target=$(jq -r '.target' <<< "$CELL") + build_tag=$(jq -r '.build_tag' <<< "$CELL") + docker pull -q "${REPO}@${DIGEST}" + docker tag "${REPO}@${DIGEST}" "${REPO}:${build_tag}" + make test-image IMAGE="${REPO}:${build_tag}" FLAVOR="${target}" + - name: Upload the digest for the merge job if: ${{ inputs.dry_run != 'true' }} uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 diff --git a/Dockerfile b/Dockerfile index fcf422a..b30294a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -261,6 +261,15 @@ CFEOF # RPM ships all four loadable extensions and httpfs is compiled into libduckdb, # so nothing needs fetching -- and with allow_unsigned on, autoinstall would mean # loading unsigned code from the network at runtime. +# A writable home for configuration the entrypoint renders from the environment. +# The packaged /etc/pgedge/coldfront/config.yaml cannot serve: it is +# 0600 coldfront:coldfront for the bare-metal service account, while this image +# runs as postgres. Declared here rather than created at runtime so the path is +# discoverable, and so it can be mounted -- which is what a --read-only root +# filesystem needs. +RUN install --verbose --directory --owner postgres --group postgres --mode 0700 \ + /var/lib/pgedge/coldfront + ENV COLDFRONT_PRELOAD="pg_duckdb,coldfront" ENV COLDFRONT_EXTENSION_DIR="/usr/lib/pgedge/coldfront/duckdb-extensions" diff --git a/coldfront-entrypoint.sh b/coldfront-entrypoint.sh index d54809a..e2d8376 100755 --- a/coldfront-entrypoint.sh +++ b/coldfront-entrypoint.sh @@ -1,37 +1,148 @@ #!/usr/bin/env bash # -# ColdFront wrapper around pgEdge's docker-entrypoint.sh. -# -# It does NOT replace that entrypoint: it appends ColdFront's postmaster-start -# GUCs as -c arguments and then execs it, so POSTGRES_* variables, _FILE secrets -# and /docker-entrypoint-initdb.d/ all keep working. -# -# -c arguments rather than writing postgresql.conf because: -# * appending on every start would grow the file without bound; -# * they apply to an already-initialised PGDATA, which an initdb.d script cannot; -# * pgEdge's entrypoint forwards "$@" into docker_temp_server_start, so the -# preload is live during initdb.d and an init script can CREATE EXTENSION. -# -# Kubernetes never reaches this file: CNPG runs its own instance manager and -# takes these settings from the Cluster spec instead. +# ColdFront wrapper around pgEdge's docker-entrypoint.sh. It appends ColdFront's +# settings as -c arguments and execs that entrypoint, so POSTGRES_* variables, +# _FILE secrets and /docker-entrypoint-initdb.d/ keep working. -c rather than +# writing postgresql.conf: appending on every start would grow the file, and +# args apply to an already-initialised PGDATA. Kubernetes never reaches here -- +# CNPG runs its own instance manager and configures from the Cluster spec. set -o errexit set -o nounset -# pg_duckdb refuses to install in a non-UTF8 database, and pgEdge's initdb -# defaults to SQL_ASCII. Prepend the encoding ColdFront needs while preserving -# anything the operator passed, so an explicit override still wins. +# pg_duckdb refuses a non-UTF8 database and pgEdge's initdb defaults to +# SQL_ASCII. Prepended, so an operator's own value still wins. export POSTGRES_INITDB_ARGS="--encoding=UTF8 --locale=C ${POSTGRES_INITDB_ARGS:-}" +# --- config for the Go tools ------------------------------------------------- +# They read YAML, not GUCs. The packaged /etc/pgedge/coldfront/config.yaml is an +# example owned by the bare-metal service account and unreadable here, so +# environment values are rendered instead. + +# section|key|variable, in emission order. An empty variable is skipped: s3 or +# azure configures that backend, and neither selects vended credentials. The +# config format allows exactly one, so setting both is passed through for the +# tools to reject by name -- dropping one here would silently write to the +# other store. +_CF_FIELDS='postgres|dsn|COLDFRONT_PG_DSN +iceberg|warehouse|COLDFRONT_WAREHOUSE +iceberg|lakekeeper_endpoint|COLDFRONT_LAKEKEEPER +s3|endpoint|COLDFRONT_S3_ENDPOINT +s3|region|COLDFRONT_S3_REGION +s3|access_key|COLDFRONT_S3_ACCESS_KEY +s3|secret_key|COLDFRONT_S3_SECRET_KEY +s3|use_ssl|COLDFRONT_S3_USE_SSL +s3|url_style|COLDFRONT_S3_URL_STYLE +azure|connection_string|COLDFRONT_AZURE_CONNECTION_STRING' + +# Writes the fields that are set to $1; succeeds only if something was, so the +# caller can tell "unconfigured" from "configured". Single-quoted YAML: the only +# escape is a doubled quote, so nothing else in a DSN or secret can change the +# meaning. +_cf_render_config() { + local out=$1 emitted='' section key var value + : > "$out" + chmod 0600 "$out" + + while IFS='|' read -r section key var; do + [ -n "$section" ] || continue + value=${!var-} + [ -n "$value" ] || continue + # Fatal, not skipped: skipping would surface as the tool's "no config + # file" instead of naming the variable at fault. + case $value in + *$'\n'*) + echo "coldfront: ${var} must not contain a newline" >&2 + exit 1 + ;; + esac + if [ "$section" != "$emitted" ]; then + printf '%s:\n' "$section" >> "$out" + emitted=$section + fi + case $value in + true | false) printf ' %s: %s\n' "$key" "$value" ;; + *) printf " %s: '%s'\n" "$key" "${value//\'/\'\'}" ;; + esac >> "$out" + done <<< "$_CF_FIELDS" + + [ -s "$out" ] +} + +case ${1:-} in +archiver | partitioner | compactor) + _cf_tool=$1 + shift + + _cf_flagged=0 + for _cf_arg; do + case $_cf_arg in -config | -config=* | --config | --config=*) _cf_flagged=1 ;; esac + done + + if [ "$_cf_flagged" = 0 ]; then + # COLDFRONT_CONFIG names a file to READ, never to write, and outranks + # rendering: COLDFRONT_WAREHOUSE and COLDFRONT_LAKEKEEPER are read by + # the postgres path too, so they are set even when a file is supplied. + _cf_rendered=${COLDFRONT_RENDER_CONFIG_TO:-/var/lib/pgedge/coldfront/config.yaml} + if [ -n "${COLDFRONT_CONFIG:-}" ]; then + set -- -config "$COLDFRONT_CONFIG" "$@" + elif _cf_render_config "$_cf_rendered"; then + echo "coldfront: rendered ${_cf_rendered} from the environment" >&2 + set -- -config "$_cf_rendered" "$@" + fi + fi + + exec "/usr/bin/${_cf_tool}" "$@" + ;; +esac + if [ "${1:-}" = "postgres" ]; then shift - set -- postgres \ - -c shared_preload_libraries="${COLDFRONT_PRELOAD}" \ - -c duckdb.extension_directory="${COLDFRONT_EXTENSION_DIR}" \ - -c duckdb.autoinstall_known_extensions=false \ - -c duckdb.autoload_known_extensions=true \ - -c duckdb.allow_unsigned_extensions=true \ - "$@" + + # coldfront.so installs DML-routing hooks and an XactCallback, pg_duckdb + # installs planner and executor hooks, so both need preloading. autoinstall + # is off because the RPM ships all four loadable extensions and httpfs is + # compiled into libduckdb -- with allow_unsigned also on, leaving it enabled + # would mean fetching unsigned code at runtime. + args=( + -c shared_preload_libraries="${COLDFRONT_PRELOAD}" + -c duckdb.extension_directory="${COLDFRONT_EXTENSION_DIR}" + -c duckdb.autoinstall_known_extensions=false + -c duckdb.autoload_known_extensions=true + -c duckdb.allow_unsigned_extensions=true + ) + + # Unset rather than defaulted when absent: coldfront reads these with + # current_setting(..., true), so the image still starts as a plain node with + # the extension present but idle. + if [ -n "${COLDFRONT_WAREHOUSE:-}" ]; then + args+=(-c coldfront.warehouse="${COLDFRONT_WAREHOUSE}") + fi + if [ -n "${COLDFRONT_LAKEKEEPER:-}" ]; then + args+=(-c coldfront.lakekeeper_endpoint="${COLDFRONT_LAKEKEEPER}") + fi + + # Loopback DSN for coldfront.ensure_pg_attached(). Derived from the same + # variables pgEdge's entrypoint uses for the role and database, following + # its defaulting (POSTGRES_DB falls back to POSTGRES_USER). Override where + # the socket lives elsewhere -- CNPG forces /controller/run. + if [ -z "${COLDFRONT_LOCAL_PG_DSN:-}" ]; then + _cf_user="${POSTGRES_USER:-postgres}" + COLDFRONT_LOCAL_PG_DSN="host=${COLDFRONT_SOCKET_DIR:-/var/run/postgresql}" + COLDFRONT_LOCAL_PG_DSN+=" dbname=${POSTGRES_DB:-${_cf_user}}" + COLDFRONT_LOCAL_PG_DSN+=" user=${_cf_user}" + COLDFRONT_LOCAL_PG_DSN+=" application_name=coldfront_pglocal" + fi + args+=(-c coldfront.local_pg_dsn="${COLDFRONT_LOCAL_PG_DSN}") + + # pg_duckdb gates DuckDB on membership of this role; unset keeps its stock + # superuser-only default. + if [ -n "${COLDFRONT_DUCKDB_ROLE:-}" ]; then + args+=(-c duckdb.postgres_role="${COLDFRONT_DUCKDB_ROLE}") + fi + + # Operator arguments last, so an explicit -c wins. + set -- postgres "${args[@]}" "$@" fi exec /usr/local/bin/docker-entrypoint.sh "$@" From 7db7e2e4bb0566100043493da673876c117f00bb Mon Sep 17 00:00:00 2001 From: Muhammad Aqeel Date: Thu, 10 Sep 2026 13:45:42 +0500 Subject: [PATCH 07/16] Adds a PostgreSQL-only base image and chains minimal from it --- .github/workflows/build_images.yaml | 90 +++++++++++-- Dockerfile | 118 ++++++++++-------- packagelists/amd64/pg16.15-postgres.txt | 2 + .../amd64/pg16.15-spock5.0.11-minimal.txt | 1 - .../amd64/pg16.15-spock5.0.11-standard.txt | 4 - .../pg16.15-spock6.0.0-beta1-minimal.txt | 1 - .../pg16.15-spock6.0.0-beta1-standard.txt | 4 - packagelists/amd64/pg17.11-postgres.txt | 2 + .../amd64/pg17.11-spock5.0.11-minimal.txt | 1 - .../amd64/pg17.11-spock5.0.11-standard.txt | 4 - .../pg17.11-spock6.0.0-beta1-minimal.txt | 1 - .../pg17.11-spock6.0.0-beta1-standard.txt | 4 - packagelists/amd64/pg18.6-postgres.txt | 2 + .../amd64/pg18.6-spock5.0.11-minimal.txt | 1 - .../amd64/pg18.6-spock5.0.11-standard.txt | 4 - .../amd64/pg18.6-spock6.0.0-beta1-minimal.txt | 1 - .../pg18.6-spock6.0.0-beta1-standard.txt | 4 - packagelists/arm64/pg16.15-postgres.txt | 2 + .../arm64/pg16.15-spock5.0.11-minimal.txt | 1 - .../arm64/pg16.15-spock5.0.11-standard.txt | 4 - .../pg16.15-spock6.0.0-beta1-minimal.txt | 1 - .../pg16.15-spock6.0.0-beta1-standard.txt | 4 - packagelists/arm64/pg17.11-postgres.txt | 2 + .../arm64/pg17.11-spock5.0.11-minimal.txt | 1 - .../arm64/pg17.11-spock5.0.11-standard.txt | 4 - .../pg17.11-spock6.0.0-beta1-minimal.txt | 1 - .../pg17.11-spock6.0.0-beta1-standard.txt | 4 - packagelists/arm64/pg18.6-postgres.txt | 2 + .../arm64/pg18.6-spock5.0.11-minimal.txt | 1 - .../arm64/pg18.6-spock5.0.11-standard.txt | 4 - .../arm64/pg18.6-spock6.0.0-beta1-minimal.txt | 1 - .../pg18.6-spock6.0.0-beta1-standard.txt | 4 - pgedge.docker-bake.hcl | 12 ++ scripts/build_pgedge_images.py | 50 ++++++-- tests/main.go | 57 ++++++--- 35 files changed, 246 insertions(+), 153 deletions(-) create mode 100644 packagelists/amd64/pg16.15-postgres.txt create mode 100644 packagelists/amd64/pg17.11-postgres.txt create mode 100644 packagelists/amd64/pg18.6-postgres.txt create mode 100644 packagelists/arm64/pg16.15-postgres.txt create mode 100644 packagelists/arm64/pg17.11-postgres.txt create mode 100644 packagelists/arm64/pg18.6-postgres.txt diff --git a/.github/workflows/build_images.yaml b/.github/workflows/build_images.yaml index d3ba9bb..eb75275 100644 --- a/.github/workflows/build_images.yaml +++ b/.github/workflows/build_images.yaml @@ -72,6 +72,8 @@ jobs: name: Plan waves runs-on: ubuntu-24.04 outputs: + postgres_build: ${{ steps.matrix.outputs.postgres_build }} + postgres_merge: ${{ steps.matrix.outputs.postgres_merge }} minimal_build: ${{ steps.matrix.outputs.minimal_build }} minimal_merge: ${{ steps.matrix.outputs.minimal_merge }} standard_build: ${{ steps.matrix.outputs.standard_build }} @@ -101,7 +103,7 @@ jobs: run: | set -o errexit -o pipefail PGEDGE_EMIT_MATRIX=1 ./scripts/build_pgedge_images.py > waves.json - for flavor in minimal standard coldfront; do + for flavor in postgres minimal standard coldfront; do for kind in build merge; do value=$(python3 -c "import json;print(json.dumps(json.load(open('waves.json'))['$flavor']['$kind']))") echo "${flavor}_${kind}=${value}" >> "$GITHUB_OUTPUT" @@ -111,15 +113,15 @@ jobs: - name: Show the plan run: python3 -m json.tool waves.json - build-minimal: - name: "minimal ${{ matrix.cell.name }}" + build-postgres: + name: "postgres ${{ matrix.cell.name }}" needs: [plan] - if: ${{ fromJSON(needs.plan.outputs.minimal_build)[0] != null }} + if: ${{ fromJSON(needs.plan.outputs.postgres_build)[0] != null }} runs-on: ${{ matrix.cell.runner }} strategy: fail-fast: false matrix: - cell: ${{ fromJSON(needs.plan.outputs.minimal_build) }} + cell: ${{ fromJSON(needs.plan.outputs.postgres_build) }} steps: - name: Checkout repository uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 @@ -133,18 +135,72 @@ jobs: dry_run: ${{ inputs.pgedge_image_dry_run }} no_cache: ${{ inputs.pgedge_image_no_cache }} - merge-minimal: - name: "merge minimal ${{ matrix.cell.name }}" + merge-postgres: + name: "merge postgres ${{ matrix.cell.name }}" # An empty matrix skips a job, and a skipped "needs" fails the implicit # success() of everything downstream. So each job below lists its whole # upstream chain and blocks only on a real failure. Every upstream job is # named rather than just the previous one, because a failure reaches the # next job as a *skip*: checking only the immediate predecessor would let a # later wave build on top of a broken earlier one. - needs: [plan, build-minimal] + needs: [plan, build-postgres] + if: >- + ${{ !cancelled() + && needs.plan.result == 'success' + && needs.build-postgres.result != 'failure' + && fromJSON(needs.plan.outputs.postgres_merge)[0] != null }} + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + cell: ${{ fromJSON(needs.plan.outputs.postgres_merge) }} + steps: + - name: Checkout repository + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + + - name: Merge, sign and tag + uses: ./.github/actions/merge-flavor + with: + cell: ${{ toJSON(matrix.cell) }} + repo: ${{ inputs.pgedge_image_repo }} + registry_token: ${{ secrets.GITHUB_TOKEN }} + dry_run: ${{ inputs.pgedge_image_dry_run }} + + build-minimal: + name: "minimal ${{ matrix.cell.name }}" + needs: [plan, build-postgres, merge-postgres] + if: >- + ${{ !cancelled() + && needs.plan.result == 'success' + && needs.build-postgres.result != 'failure' + && needs.merge-postgres.result != 'failure' + && fromJSON(needs.plan.outputs.minimal_build)[0] != null }} + runs-on: ${{ matrix.cell.runner }} + strategy: + fail-fast: false + matrix: + cell: ${{ fromJSON(needs.plan.outputs.minimal_build) }} + steps: + - name: Checkout repository + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + + - name: Build and push by digest + uses: ./.github/actions/build-flavor + with: + cell: ${{ toJSON(matrix.cell) }} + repo: ${{ inputs.pgedge_image_repo }} + registry_token: ${{ secrets.GITHUB_TOKEN }} + dry_run: ${{ inputs.pgedge_image_dry_run }} + no_cache: ${{ inputs.pgedge_image_no_cache }} + + merge-minimal: + name: "merge minimal ${{ matrix.cell.name }}" + needs: [plan, build-postgres, merge-postgres, build-minimal] if: >- ${{ !cancelled() && needs.plan.result == 'success' + && needs.build-postgres.result != 'failure' + && needs.merge-postgres.result != 'failure' && needs.build-minimal.result != 'failure' && fromJSON(needs.plan.outputs.minimal_merge)[0] != null }} runs-on: ubuntu-24.04 @@ -166,10 +222,12 @@ jobs: build-standard: name: "standard ${{ matrix.cell.name }}" - needs: [plan, build-minimal, merge-minimal] + needs: [plan, build-postgres, merge-postgres, build-minimal, merge-minimal] if: >- ${{ !cancelled() && needs.plan.result == 'success' + && needs.build-postgres.result != 'failure' + && needs.merge-postgres.result != 'failure' && needs.build-minimal.result != 'failure' && needs.merge-minimal.result != 'failure' && fromJSON(needs.plan.outputs.standard_build)[0] != null }} @@ -193,10 +251,12 @@ jobs: merge-standard: name: "merge standard ${{ matrix.cell.name }}" - needs: [plan, build-minimal, merge-minimal, build-standard] + needs: [plan, build-postgres, merge-postgres, build-minimal, merge-minimal, build-standard] if: >- ${{ !cancelled() && needs.plan.result == 'success' + && needs.build-postgres.result != 'failure' + && needs.merge-postgres.result != 'failure' && needs.build-minimal.result != 'failure' && needs.merge-minimal.result != 'failure' && needs.build-standard.result != 'failure' @@ -218,13 +278,15 @@ jobs: registry_token: ${{ secrets.GITHUB_TOKEN }} dry_run: ${{ inputs.pgedge_image_dry_run }} - # A failure here leaves this run's minimal and standard images published. + # A failure here leaves this run's earlier flavors published. build-coldfront: name: "coldfront ${{ matrix.cell.name }}" - needs: [plan, build-minimal, merge-minimal, build-standard, merge-standard] + needs: [plan, build-postgres, merge-postgres, build-minimal, merge-minimal, build-standard, merge-standard] if: >- ${{ !cancelled() && needs.plan.result == 'success' + && needs.build-postgres.result != 'failure' + && needs.merge-postgres.result != 'failure' && needs.build-minimal.result != 'failure' && needs.merge-minimal.result != 'failure' && needs.build-standard.result != 'failure' @@ -250,10 +312,12 @@ jobs: merge-coldfront: name: "merge coldfront ${{ matrix.cell.name }}" - needs: [plan, build-minimal, merge-minimal, build-standard, merge-standard, build-coldfront] + needs: [plan, build-postgres, merge-postgres, build-minimal, merge-minimal, build-standard, merge-standard, build-coldfront] if: >- ${{ !cancelled() && needs.plan.result == 'success' + && needs.build-postgres.result != 'failure' + && needs.merge-postgres.result != 'failure' && needs.build-minimal.result != 'failure' && needs.merge-minimal.result != 'failure' && needs.build-standard.result != 'failure' diff --git a/Dockerfile b/Dockerfile index b30294a..009d7ee 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,8 +2,11 @@ # base image for all flavors # ############################## -# Global ARG: an ARG used in a FROM instruction has to be declared before the -# first FROM, outside any stage. See the coldfront stage for what it selects. +# What each chained stage builds FROM. Declared here because an ARG used in a +# FROM must precede the first FROM. The default names the stage above it, giving +# one build graph; a registry reference instead starts that stage from an +# already-published image, which is what a per-flavor CI wave needs. +ARG POSTGRES_IMAGE=postgres ARG MINIMAL_IMAGE=minimal ARG STANDARD_IMAGE=standard @@ -42,17 +45,19 @@ mkdir /docker-entrypoint-initdb.d EOF -########################## -# minimal-flavored image # -########################## +############################# +# PostgreSQL-only base image # +############################# +# +# Spock-independent: built once per major, shared across spock lines. -FROM base AS minimal +FROM base AS postgres -ARG PACKAGE_LIST_FILE +ARG POSTGRES_PACKAGE_LIST_FILE ARG TARGETARCH ARG POSTGRES_MAJOR_VERSION -COPY packagelists/${TARGETARCH}/${PACKAGE_LIST_FILE} /usr/share/pgedge/packages.txt +COPY packagelists/${TARGETARCH}/${POSTGRES_PACKAGE_LIST_FILE} /usr/share/pgedge/packages.txt RUN < and are dnf's to resolve. -# Deliberately no second "dnf update -y": the inherited standard layers already -# ran one, and repeating it here would move ColdFront past its pinned NVR. -grep -vE '^[[:space:]]*(#|$)' /usr/share/pgedge/coldfront-packages.txt \ - | xargs dnf install -y --setopt=install_weak_deps=False +# Dependencies are dnf's to resolve. No second "dnf update -y": that would move +# ColdFront past its pinned NVR. +xargs dnf install -y --setopt=install_weak_deps=False < /usr/share/pgedge/coldfront-packages.txt dnf clean all CFEOF -# ColdFront needs pg_duckdb and coldfront preloaded at postmaster start, and -# DuckDB's extensions come from the read-only RPM path. autoinstall is off: the -# RPM ships all four loadable extensions and httpfs is compiled into libduckdb, -# so nothing needs fetching -- and with allow_unsigned on, autoinstall would mean -# loading unsigned code from the network at runtime. -# A writable home for configuration the entrypoint renders from the environment. -# The packaged /etc/pgedge/coldfront/config.yaml cannot serve: it is -# 0600 coldfront:coldfront for the bare-metal service account, while this image -# runs as postgres. Declared here rather than created at runtime so the path is -# discoverable, and so it can be mounted -- which is what a --read-only root -# filesystem needs. +# Read by coldfront-entrypoint.sh. autoinstall stays off there: the RPM ships +# all four extensions, and with allow_unsigned on it would fetch unsigned code. +# Writable home for the config the entrypoint renders; the packaged one is +# 0600 coldfront:coldfront and this image runs as postgres. Declared, not created +# at runtime, so it is discoverable and mountable under --read-only. RUN install --verbose --directory --owner postgres --group postgres --mode 0700 \ /var/lib/pgedge/coldfront diff --git a/packagelists/amd64/pg16.15-postgres.txt b/packagelists/amd64/pg16.15-postgres.txt new file mode 100644 index 0000000..e0b60e3 --- /dev/null +++ b/packagelists/amd64/pg16.15-postgres.txt @@ -0,0 +1,2 @@ +pgedge-postgresql16-16.15-1.el9 +pgedge-postgresql16-server-16.15-1.el9 diff --git a/packagelists/amd64/pg16.15-spock5.0.11-minimal.txt b/packagelists/amd64/pg16.15-spock5.0.11-minimal.txt index 52266aa..7bf91a4 100644 --- a/packagelists/amd64/pg16.15-spock5.0.11-minimal.txt +++ b/packagelists/amd64/pg16.15-spock5.0.11-minimal.txt @@ -1,4 +1,3 @@ -pgedge-postgresql16-16.15-1.el9 pgedge-spock50_16-5.0.11-1.el9 pgedge-snowflake_16-2.6.0-1.el9 pgedge-lolor_16-1.2.2-1.el9 diff --git a/packagelists/amd64/pg16.15-spock5.0.11-standard.txt b/packagelists/amd64/pg16.15-spock5.0.11-standard.txt index 847137e..da0b6c2 100644 --- a/packagelists/amd64/pg16.15-spock5.0.11-standard.txt +++ b/packagelists/amd64/pg16.15-spock5.0.11-standard.txt @@ -1,7 +1,3 @@ -# Delta over pg16.15-spock5.0.11-minimal.txt -- standard is chained FROM minimal, so the -# packages minimal already pins are deliberately absent here. Re-pinning one -# that minimal's 'dnf update -y' has moved past its NVR is a downgrade, which -# dnf refuses. pgedge-pgaudit_16-16.1-1.el9 pgedge-postgis36_16-3.6.4-1.el9 pgedge-pgvector_16-0.8.5-1.el9 diff --git a/packagelists/amd64/pg16.15-spock6.0.0-beta1-minimal.txt b/packagelists/amd64/pg16.15-spock6.0.0-beta1-minimal.txt index 1a4c084..87fc969 100644 --- a/packagelists/amd64/pg16.15-spock6.0.0-beta1-minimal.txt +++ b/packagelists/amd64/pg16.15-spock6.0.0-beta1-minimal.txt @@ -1,4 +1,3 @@ -pgedge-postgresql16-16.15-1.el9 pgedge-spock60_16-6.0.0-beta1_1.el9 pgedge-snowflake_16-2.6.0-1.el9 pgedge-lolor_16-1.2.2-1.el9 diff --git a/packagelists/amd64/pg16.15-spock6.0.0-beta1-standard.txt b/packagelists/amd64/pg16.15-spock6.0.0-beta1-standard.txt index fff5c7f..da0b6c2 100644 --- a/packagelists/amd64/pg16.15-spock6.0.0-beta1-standard.txt +++ b/packagelists/amd64/pg16.15-spock6.0.0-beta1-standard.txt @@ -1,7 +1,3 @@ -# Delta over pg16.15-spock6.0.0-beta1-minimal.txt -- standard is chained FROM minimal, so the -# packages minimal already pins are deliberately absent here. Re-pinning one -# that minimal's 'dnf update -y' has moved past its NVR is a downgrade, which -# dnf refuses. pgedge-pgaudit_16-16.1-1.el9 pgedge-postgis36_16-3.6.4-1.el9 pgedge-pgvector_16-0.8.5-1.el9 diff --git a/packagelists/amd64/pg17.11-postgres.txt b/packagelists/amd64/pg17.11-postgres.txt new file mode 100644 index 0000000..e6b6f11 --- /dev/null +++ b/packagelists/amd64/pg17.11-postgres.txt @@ -0,0 +1,2 @@ +pgedge-postgresql17-17.11-1.el9 +pgedge-postgresql17-server-17.11-1.el9 diff --git a/packagelists/amd64/pg17.11-spock5.0.11-minimal.txt b/packagelists/amd64/pg17.11-spock5.0.11-minimal.txt index a185cb9..920688d 100644 --- a/packagelists/amd64/pg17.11-spock5.0.11-minimal.txt +++ b/packagelists/amd64/pg17.11-spock5.0.11-minimal.txt @@ -1,4 +1,3 @@ -pgedge-postgresql17-17.11-1.el9 pgedge-spock50_17-5.0.11-1.el9 pgedge-snowflake_17-2.6.0-1.el9 pgedge-lolor_17-1.2.2-1.el9 diff --git a/packagelists/amd64/pg17.11-spock5.0.11-standard.txt b/packagelists/amd64/pg17.11-spock5.0.11-standard.txt index 9a3420f..6098198 100644 --- a/packagelists/amd64/pg17.11-spock5.0.11-standard.txt +++ b/packagelists/amd64/pg17.11-spock5.0.11-standard.txt @@ -1,7 +1,3 @@ -# Delta over pg17.11-spock5.0.11-minimal.txt -- standard is chained FROM minimal, so the -# packages minimal already pins are deliberately absent here. Re-pinning one -# that minimal's 'dnf update -y' has moved past its NVR is a downgrade, which -# dnf refuses. pgedge-pgaudit_17-17.1-1.el9 pgedge-postgis36_17-3.6.4-1.el9 pgedge-pgvector_17-0.8.5-1.el9 diff --git a/packagelists/amd64/pg17.11-spock6.0.0-beta1-minimal.txt b/packagelists/amd64/pg17.11-spock6.0.0-beta1-minimal.txt index 3ad4040..bdad999 100644 --- a/packagelists/amd64/pg17.11-spock6.0.0-beta1-minimal.txt +++ b/packagelists/amd64/pg17.11-spock6.0.0-beta1-minimal.txt @@ -1,4 +1,3 @@ -pgedge-postgresql17-17.11-1.el9 pgedge-spock60_17-6.0.0-beta1_1.el9 pgedge-snowflake_17-2.6.0-1.el9 pgedge-lolor_17-1.2.2-1.el9 diff --git a/packagelists/amd64/pg17.11-spock6.0.0-beta1-standard.txt b/packagelists/amd64/pg17.11-spock6.0.0-beta1-standard.txt index 3f36542..6098198 100644 --- a/packagelists/amd64/pg17.11-spock6.0.0-beta1-standard.txt +++ b/packagelists/amd64/pg17.11-spock6.0.0-beta1-standard.txt @@ -1,7 +1,3 @@ -# Delta over pg17.11-spock6.0.0-beta1-minimal.txt -- standard is chained FROM minimal, so the -# packages minimal already pins are deliberately absent here. Re-pinning one -# that minimal's 'dnf update -y' has moved past its NVR is a downgrade, which -# dnf refuses. pgedge-pgaudit_17-17.1-1.el9 pgedge-postgis36_17-3.6.4-1.el9 pgedge-pgvector_17-0.8.5-1.el9 diff --git a/packagelists/amd64/pg18.6-postgres.txt b/packagelists/amd64/pg18.6-postgres.txt new file mode 100644 index 0000000..c363ba2 --- /dev/null +++ b/packagelists/amd64/pg18.6-postgres.txt @@ -0,0 +1,2 @@ +pgedge-postgresql18-18.6-1.el9 +pgedge-postgresql18-server-18.6-1.el9 diff --git a/packagelists/amd64/pg18.6-spock5.0.11-minimal.txt b/packagelists/amd64/pg18.6-spock5.0.11-minimal.txt index 2f1fa16..a0a7793 100644 --- a/packagelists/amd64/pg18.6-spock5.0.11-minimal.txt +++ b/packagelists/amd64/pg18.6-spock5.0.11-minimal.txt @@ -1,4 +1,3 @@ -pgedge-postgresql18-18.6-1.el9 pgedge-spock50_18-5.0.11-1.el9 pgedge-snowflake_18-2.6.0-1.el9 pgedge-lolor_18-1.2.2-1.el9 diff --git a/packagelists/amd64/pg18.6-spock5.0.11-standard.txt b/packagelists/amd64/pg18.6-spock5.0.11-standard.txt index eefe6f3..82ece0c 100644 --- a/packagelists/amd64/pg18.6-spock5.0.11-standard.txt +++ b/packagelists/amd64/pg18.6-spock5.0.11-standard.txt @@ -1,7 +1,3 @@ -# Delta over pg18.6-spock5.0.11-minimal.txt -- standard is chained FROM minimal, so the -# packages minimal already pins are deliberately absent here. Re-pinning one -# that minimal's 'dnf update -y' has moved past its NVR is a downgrade, which -# dnf refuses. pgedge-pgaudit_18-18.0-1.el9 pgedge-postgis36_18-3.6.4-1.el9 pgedge-pgvector_18-0.8.5-1.el9 diff --git a/packagelists/amd64/pg18.6-spock6.0.0-beta1-minimal.txt b/packagelists/amd64/pg18.6-spock6.0.0-beta1-minimal.txt index 4f57c19..ab432d1 100644 --- a/packagelists/amd64/pg18.6-spock6.0.0-beta1-minimal.txt +++ b/packagelists/amd64/pg18.6-spock6.0.0-beta1-minimal.txt @@ -1,4 +1,3 @@ -pgedge-postgresql18-18.6-1.el9 pgedge-spock60_18-6.0.0-beta1_1.el9 pgedge-snowflake_18-2.6.0-1.el9 pgedge-lolor_18-1.2.2-1.el9 diff --git a/packagelists/amd64/pg18.6-spock6.0.0-beta1-standard.txt b/packagelists/amd64/pg18.6-spock6.0.0-beta1-standard.txt index fbc4e1a..82ece0c 100644 --- a/packagelists/amd64/pg18.6-spock6.0.0-beta1-standard.txt +++ b/packagelists/amd64/pg18.6-spock6.0.0-beta1-standard.txt @@ -1,7 +1,3 @@ -# Delta over pg18.6-spock6.0.0-beta1-minimal.txt -- standard is chained FROM minimal, so the -# packages minimal already pins are deliberately absent here. Re-pinning one -# that minimal's 'dnf update -y' has moved past its NVR is a downgrade, which -# dnf refuses. pgedge-pgaudit_18-18.0-1.el9 pgedge-postgis36_18-3.6.4-1.el9 pgedge-pgvector_18-0.8.5-1.el9 diff --git a/packagelists/arm64/pg16.15-postgres.txt b/packagelists/arm64/pg16.15-postgres.txt new file mode 100644 index 0000000..e0b60e3 --- /dev/null +++ b/packagelists/arm64/pg16.15-postgres.txt @@ -0,0 +1,2 @@ +pgedge-postgresql16-16.15-1.el9 +pgedge-postgresql16-server-16.15-1.el9 diff --git a/packagelists/arm64/pg16.15-spock5.0.11-minimal.txt b/packagelists/arm64/pg16.15-spock5.0.11-minimal.txt index 52266aa..7bf91a4 100644 --- a/packagelists/arm64/pg16.15-spock5.0.11-minimal.txt +++ b/packagelists/arm64/pg16.15-spock5.0.11-minimal.txt @@ -1,4 +1,3 @@ -pgedge-postgresql16-16.15-1.el9 pgedge-spock50_16-5.0.11-1.el9 pgedge-snowflake_16-2.6.0-1.el9 pgedge-lolor_16-1.2.2-1.el9 diff --git a/packagelists/arm64/pg16.15-spock5.0.11-standard.txt b/packagelists/arm64/pg16.15-spock5.0.11-standard.txt index 847137e..da0b6c2 100644 --- a/packagelists/arm64/pg16.15-spock5.0.11-standard.txt +++ b/packagelists/arm64/pg16.15-spock5.0.11-standard.txt @@ -1,7 +1,3 @@ -# Delta over pg16.15-spock5.0.11-minimal.txt -- standard is chained FROM minimal, so the -# packages minimal already pins are deliberately absent here. Re-pinning one -# that minimal's 'dnf update -y' has moved past its NVR is a downgrade, which -# dnf refuses. pgedge-pgaudit_16-16.1-1.el9 pgedge-postgis36_16-3.6.4-1.el9 pgedge-pgvector_16-0.8.5-1.el9 diff --git a/packagelists/arm64/pg16.15-spock6.0.0-beta1-minimal.txt b/packagelists/arm64/pg16.15-spock6.0.0-beta1-minimal.txt index 1a4c084..87fc969 100644 --- a/packagelists/arm64/pg16.15-spock6.0.0-beta1-minimal.txt +++ b/packagelists/arm64/pg16.15-spock6.0.0-beta1-minimal.txt @@ -1,4 +1,3 @@ -pgedge-postgresql16-16.15-1.el9 pgedge-spock60_16-6.0.0-beta1_1.el9 pgedge-snowflake_16-2.6.0-1.el9 pgedge-lolor_16-1.2.2-1.el9 diff --git a/packagelists/arm64/pg16.15-spock6.0.0-beta1-standard.txt b/packagelists/arm64/pg16.15-spock6.0.0-beta1-standard.txt index fff5c7f..da0b6c2 100644 --- a/packagelists/arm64/pg16.15-spock6.0.0-beta1-standard.txt +++ b/packagelists/arm64/pg16.15-spock6.0.0-beta1-standard.txt @@ -1,7 +1,3 @@ -# Delta over pg16.15-spock6.0.0-beta1-minimal.txt -- standard is chained FROM minimal, so the -# packages minimal already pins are deliberately absent here. Re-pinning one -# that minimal's 'dnf update -y' has moved past its NVR is a downgrade, which -# dnf refuses. pgedge-pgaudit_16-16.1-1.el9 pgedge-postgis36_16-3.6.4-1.el9 pgedge-pgvector_16-0.8.5-1.el9 diff --git a/packagelists/arm64/pg17.11-postgres.txt b/packagelists/arm64/pg17.11-postgres.txt new file mode 100644 index 0000000..e6b6f11 --- /dev/null +++ b/packagelists/arm64/pg17.11-postgres.txt @@ -0,0 +1,2 @@ +pgedge-postgresql17-17.11-1.el9 +pgedge-postgresql17-server-17.11-1.el9 diff --git a/packagelists/arm64/pg17.11-spock5.0.11-minimal.txt b/packagelists/arm64/pg17.11-spock5.0.11-minimal.txt index a185cb9..920688d 100644 --- a/packagelists/arm64/pg17.11-spock5.0.11-minimal.txt +++ b/packagelists/arm64/pg17.11-spock5.0.11-minimal.txt @@ -1,4 +1,3 @@ -pgedge-postgresql17-17.11-1.el9 pgedge-spock50_17-5.0.11-1.el9 pgedge-snowflake_17-2.6.0-1.el9 pgedge-lolor_17-1.2.2-1.el9 diff --git a/packagelists/arm64/pg17.11-spock5.0.11-standard.txt b/packagelists/arm64/pg17.11-spock5.0.11-standard.txt index 9a3420f..6098198 100644 --- a/packagelists/arm64/pg17.11-spock5.0.11-standard.txt +++ b/packagelists/arm64/pg17.11-spock5.0.11-standard.txt @@ -1,7 +1,3 @@ -# Delta over pg17.11-spock5.0.11-minimal.txt -- standard is chained FROM minimal, so the -# packages minimal already pins are deliberately absent here. Re-pinning one -# that minimal's 'dnf update -y' has moved past its NVR is a downgrade, which -# dnf refuses. pgedge-pgaudit_17-17.1-1.el9 pgedge-postgis36_17-3.6.4-1.el9 pgedge-pgvector_17-0.8.5-1.el9 diff --git a/packagelists/arm64/pg17.11-spock6.0.0-beta1-minimal.txt b/packagelists/arm64/pg17.11-spock6.0.0-beta1-minimal.txt index 3ad4040..bdad999 100644 --- a/packagelists/arm64/pg17.11-spock6.0.0-beta1-minimal.txt +++ b/packagelists/arm64/pg17.11-spock6.0.0-beta1-minimal.txt @@ -1,4 +1,3 @@ -pgedge-postgresql17-17.11-1.el9 pgedge-spock60_17-6.0.0-beta1_1.el9 pgedge-snowflake_17-2.6.0-1.el9 pgedge-lolor_17-1.2.2-1.el9 diff --git a/packagelists/arm64/pg17.11-spock6.0.0-beta1-standard.txt b/packagelists/arm64/pg17.11-spock6.0.0-beta1-standard.txt index 3f36542..6098198 100644 --- a/packagelists/arm64/pg17.11-spock6.0.0-beta1-standard.txt +++ b/packagelists/arm64/pg17.11-spock6.0.0-beta1-standard.txt @@ -1,7 +1,3 @@ -# Delta over pg17.11-spock6.0.0-beta1-minimal.txt -- standard is chained FROM minimal, so the -# packages minimal already pins are deliberately absent here. Re-pinning one -# that minimal's 'dnf update -y' has moved past its NVR is a downgrade, which -# dnf refuses. pgedge-pgaudit_17-17.1-1.el9 pgedge-postgis36_17-3.6.4-1.el9 pgedge-pgvector_17-0.8.5-1.el9 diff --git a/packagelists/arm64/pg18.6-postgres.txt b/packagelists/arm64/pg18.6-postgres.txt new file mode 100644 index 0000000..c363ba2 --- /dev/null +++ b/packagelists/arm64/pg18.6-postgres.txt @@ -0,0 +1,2 @@ +pgedge-postgresql18-18.6-1.el9 +pgedge-postgresql18-server-18.6-1.el9 diff --git a/packagelists/arm64/pg18.6-spock5.0.11-minimal.txt b/packagelists/arm64/pg18.6-spock5.0.11-minimal.txt index 2f1fa16..a0a7793 100644 --- a/packagelists/arm64/pg18.6-spock5.0.11-minimal.txt +++ b/packagelists/arm64/pg18.6-spock5.0.11-minimal.txt @@ -1,4 +1,3 @@ -pgedge-postgresql18-18.6-1.el9 pgedge-spock50_18-5.0.11-1.el9 pgedge-snowflake_18-2.6.0-1.el9 pgedge-lolor_18-1.2.2-1.el9 diff --git a/packagelists/arm64/pg18.6-spock5.0.11-standard.txt b/packagelists/arm64/pg18.6-spock5.0.11-standard.txt index eefe6f3..82ece0c 100644 --- a/packagelists/arm64/pg18.6-spock5.0.11-standard.txt +++ b/packagelists/arm64/pg18.6-spock5.0.11-standard.txt @@ -1,7 +1,3 @@ -# Delta over pg18.6-spock5.0.11-minimal.txt -- standard is chained FROM minimal, so the -# packages minimal already pins are deliberately absent here. Re-pinning one -# that minimal's 'dnf update -y' has moved past its NVR is a downgrade, which -# dnf refuses. pgedge-pgaudit_18-18.0-1.el9 pgedge-postgis36_18-3.6.4-1.el9 pgedge-pgvector_18-0.8.5-1.el9 diff --git a/packagelists/arm64/pg18.6-spock6.0.0-beta1-minimal.txt b/packagelists/arm64/pg18.6-spock6.0.0-beta1-minimal.txt index 4f57c19..ab432d1 100644 --- a/packagelists/arm64/pg18.6-spock6.0.0-beta1-minimal.txt +++ b/packagelists/arm64/pg18.6-spock6.0.0-beta1-minimal.txt @@ -1,4 +1,3 @@ -pgedge-postgresql18-18.6-1.el9 pgedge-spock60_18-6.0.0-beta1_1.el9 pgedge-snowflake_18-2.6.0-1.el9 pgedge-lolor_18-1.2.2-1.el9 diff --git a/packagelists/arm64/pg18.6-spock6.0.0-beta1-standard.txt b/packagelists/arm64/pg18.6-spock6.0.0-beta1-standard.txt index fbc4e1a..82ece0c 100644 --- a/packagelists/arm64/pg18.6-spock6.0.0-beta1-standard.txt +++ b/packagelists/arm64/pg18.6-spock6.0.0-beta1-standard.txt @@ -1,7 +1,3 @@ -# Delta over pg18.6-spock6.0.0-beta1-minimal.txt -- standard is chained FROM minimal, so the -# packages minimal already pins are deliberately absent here. Re-pinning one -# that minimal's 'dnf update -y' has moved past its NVR is a downgrade, which -# dnf refuses. pgedge-pgaudit_18-18.0-1.el9 pgedge-postgis36_18-3.6.4-1.el9 pgedge-pgvector_18-0.8.5-1.el9 diff --git a/pgedge.docker-bake.hcl b/pgedge.docker-bake.hcl index a7831d4..12c81a3 100644 --- a/pgedge.docker-bake.hcl +++ b/pgedge.docker-bake.hcl @@ -24,6 +24,11 @@ variable "PACKAGE_LIST_FILE" { // Every stage in the chain needs its own packagelist variable. A chained flavor // still triggers its ancestors' stages, and each of those consumes its own ARG, // so one shared variable would make an ancestor install a descendant's list. +variable "POSTGRES_PACKAGE_LIST_FILE" { + type = string + default = "" +} + variable "STANDARD_PACKAGE_LIST_FILE" { type = string default = "" @@ -38,6 +43,11 @@ variable "COLDFRONT_PACKAGE_LIST_FILE" { // default (the parent stage), which is the single-graph build. A registry // reference switches that stage to start from an already-published image, which // is what a per-flavor CI wave needs. +variable "POSTGRES_IMAGE" { + type = string + default = "postgres" +} + variable "MINIMAL_IMAGE" { type = string default = "minimal" @@ -59,9 +69,11 @@ target "default" { tags = [TAG] args = { PACKAGE_RELEASE_CHANNEL = PACKAGE_RELEASE_CHANNEL + POSTGRES_PACKAGE_LIST_FILE = POSTGRES_PACKAGE_LIST_FILE PACKAGE_LIST_FILE = PACKAGE_LIST_FILE STANDARD_PACKAGE_LIST_FILE = STANDARD_PACKAGE_LIST_FILE COLDFRONT_PACKAGE_LIST_FILE = COLDFRONT_PACKAGE_LIST_FILE + POSTGRES_IMAGE = POSTGRES_IMAGE MINIMAL_IMAGE = MINIMAL_IMAGE STANDARD_IMAGE = STANDARD_IMAGE POSTGRES_MAJOR_VERSION = POSTGRES_MAJOR_VERSION diff --git a/scripts/build_pgedge_images.py b/scripts/build_pgedge_images.py index 69feea9..6576f88 100755 --- a/scripts/build_pgedge_images.py +++ b/scripts/build_pgedge_images.py @@ -38,18 +38,22 @@ def from_env() -> "Config": # chained flavor in one graph also runs every ancestor's stage, and each stage # consumes its own packagelist ARG, so build() has to pass the whole ancestry -- # see PgEdgeImage.package_list_args. -FLAVOR_PARENTS = {"standard": "minimal", "coldfront": "standard"} +FLAVOR_PARENTS = {"minimal": "postgres", "standard": "minimal", "coldfront": "standard"} + +# Built once per major and shared by every spock line: a "-spock…-postgres" tag +# would claim a version the image does not contain. +SPOCK_INDEPENDENT_FLAVORS = {"postgres"} # The Dockerfile ARG each flavor's stage reads its packagelist from. FLAVOR_LIST_ARGS = { + "postgres": "POSTGRES_PACKAGE_LIST_FILE", "minimal": "PACKAGE_LIST_FILE", "standard": "STANDARD_PACKAGE_LIST_FILE", "coldfront": "COLDFRONT_PACKAGE_LIST_FILE", } -# Flavors built for every image. coldfront is deliberately absent: ColdFront's -# cold-write protocol is validated against spock 5 only, so it is opted in per -# image rather than fanned out over the whole matrix. +# Flavors built for every spock line. coldfront is opted in per image (its +# protocol is validated against spock 5 only); postgres is listed once per major. DEFAULT_FLAVORS = ["minimal", "standard"] @@ -91,10 +95,13 @@ def postgres_major(self) -> str: @property def spock_major(self) -> str: - return self.spock_version.split(".")[0] + return self.spock_version.split(".")[0] if self.spock_version else "" def _package_list_for(self, flavor: str) -> str: - filename = f"pg{self.postgres_version}-spock{self.spock_version}" + filename = f"pg{self.postgres_version}" + + if flavor not in SPOCK_INDEPENDENT_FLAVORS: + filename += f"-spock{self.spock_version}" if flavor: filename += f"-{flavor}" @@ -119,7 +126,9 @@ def parent_build_tag(self) -> str: Tag( postgres_version=self.postgres_version, flavor=parent, - spock_version=self.spock_version, + spock_version=( + "" if parent in SPOCK_INDEPENDENT_FLAVORS else self.spock_version + ), epoch=self.epoch, ) ) @@ -168,7 +177,7 @@ def extra_tags(self) -> list[Tag]: ) ] - if self.is_latest_for_spock_major: + if self.is_latest_for_spock_major and self.spock_version: # Mutable tag without spock minor/patch and epoch tags.append( Tag( @@ -187,6 +196,8 @@ def extra_tags(self) -> list[Tag]: spock_version=self.spock_major, ) ) + elif not self.spock_version and self.is_latest_for_pg_major: + tags.append(Tag(postgres_version=self.postgres_major, flavor=self.flavor)) return tags @@ -224,6 +235,19 @@ def make_all_flavor_images( # This is the list of all images that this script will build. Any new images should be # added to this list. all_images: list[PgEdgeImage] = [ + # PostgreSQL-only base, one per major; no spock segment. + PgEdgeImage( + postgres_version="16.15", spock_version="", epoch=2, flavor="postgres", + is_latest_for_pg_major=True, + ), + PgEdgeImage( + postgres_version="17.11", spock_version="", epoch=2, flavor="postgres", + is_latest_for_pg_major=True, + ), + PgEdgeImage( + postgres_version="18.6", spock_version="", epoch=2, flavor="postgres", + is_latest_for_pg_major=True, + ), # pg16 images *make_all_flavor_images( postgres_version="16.15", @@ -282,7 +306,7 @@ def make_all_flavor_images( # QEMU on an amd64 host: emulated dnf transactions dominate the build time. ARCH_RUNNERS = {"amd64": "ubuntu-24.04", "arm64": "ubuntu-24.04-arm"} -FLAVOR_WAVES = ["minimal", "standard", "coldfront"] +FLAVOR_WAVES = ["postgres", "minimal", "standard", "coldfront"] def emit_matrix(config: "Config") -> None: @@ -475,7 +499,13 @@ def _log_config(config: "Config") -> None: def _should_skip_image(image: "PgEdgeImage", config: "Config") -> bool: if config.only_postgres_version and image.postgres_version != config.only_postgres_version: return True - if config.only_spock_version and image.spock_version != config.only_spock_version: + # Every spock line depends on the spock-independent base, so a spock filter + # must not exclude it. + if ( + config.only_spock_version + and image.spock_version + and image.spock_version != config.only_spock_version + ): return True return False diff --git a/tests/main.go b/tests/main.go index b829339..e2677ce 100644 --- a/tests/main.go +++ b/tests/main.go @@ -27,10 +27,17 @@ type Test struct { Name string Cmd string ExpectedOutput func(exitCode int, output string) error + MinimalOnly bool // Only run on minimal-or-later (needs the pgEdge extensions) StandardOnly bool // Only run on standard-or-later flavors (standard, coldfront) ColdfrontOnly bool // Only run on the coldfront flavor } +// includesMinimal reports whether a flavor ships the pgEdge extensions minimal +// adds. postgres, the bare server, does not. +func includesMinimal(flavor string) bool { + return flavor == "minimal" || includesStandard(flavor) +} + // includesStandard reports whether a flavor ships everything standard does. // coldfront is chained FROM standard, so it is a superset. func includesStandard(flavor string) bool { @@ -108,20 +115,20 @@ func spockMajorFromImage(image string) string { func parseFlags() (string, string) { image := flag.String("image", "", "Docker image to test (required)") - flavor := flag.String("flavor", "", "Image flavor: minimal, standard or coldfront (required)") + flavor := flag.String("flavor", "", "Image flavor: postgres, minimal, standard or coldfront (required)") flag.Parse() if *image == "" || *flavor == "" { - fmt.Println("Usage: go run main.go -image -flavor ") + fmt.Println("Usage: go run main.go -image -flavor ") fmt.Println() fmt.Println("Arguments:") fmt.Println(" -image Docker image to test (e.g., ghcr.io/pgedge/pgedge-postgres:17-spock5-standard)") - fmt.Println(" -flavor Image flavor: 'minimal', 'standard' or 'coldfront'") + fmt.Println(" -flavor Image flavor: 'postgres', 'minimal', 'standard' or 'coldfront'") os.Exit(1) } - if *flavor != "minimal" && !includesStandard(*flavor) { - log.Fatalf("Invalid flavor '%s'. Must be 'minimal', 'standard' or 'coldfront'", *flavor) + if *flavor != "postgres" && !includesMinimal(*flavor) { + log.Fatalf("Invalid flavor '%s'. Must be 'postgres', 'minimal', 'standard' or 'coldfront'", *flavor) } return *image, *flavor @@ -206,6 +213,9 @@ func printSummary(errorCount int, flavor, spockMajor string) { tests := buildTestSuite(spockMajor) extensionTests := 0 for _, t := range tests { + if t.MinimalOnly && !includesMinimal(flavor) { + continue + } if t.StandardOnly && !includesStandard(flavor) { continue } @@ -418,7 +428,10 @@ func (r *TestRunner) Start() error { // Build shared_preload_libraries based on flavor // These extensions require preloading before they can be used // Note: We only include extensions that are guaranteed to be in all images - sharedLibs := "spock,snowflake" + sharedLibs := "" + if includesMinimal(r.flavor) { + sharedLibs = "spock,snowflake" + } if includesStandard(r.flavor) { sharedLibs = "spock,snowflake,pgaudit,supautils,pg_cron,pg_tokenizer" } @@ -438,7 +451,11 @@ func (r *TestRunner) Start() error { "-c", "track_commit_timestamp=on", "-c", "max_replication_slots=10", "-c", "max_wal_senders=10", - "-c", "snowflake.node=1", + } + // snowflake.node exists only once that extension is preloaded; passing it to + // the bare server makes the postmaster refuse to start. + if includesMinimal(r.flavor) { + cmd = append(cmd, "-c", "snowflake.node=1") } if r.flavor == "standard" { // pg_cron only ever installs into the one database this names, @@ -651,6 +668,9 @@ func (r *TestRunner) RunTests(tests []Test) int { for _, test := range tests { // Skip standard-only tests for minimal flavor + if test.MinimalOnly && !includesMinimal(r.flavor) { + continue + } if test.StandardOnly && !includesStandard(r.flavor) { continue } @@ -704,8 +724,9 @@ func getSpockVersionTests(spockMajor string) []Test { } return []Test{ { - Name: fmt.Sprintf("Spock extension major version is %s", spockMajor), - Cmd: "psql -U postgres -d testdb -t -A -c \"SELECT extversion FROM pg_extension WHERE extname = 'spock';\"", + Name: fmt.Sprintf("Spock extension major version is %s", spockMajor), + MinimalOnly: true, + Cmd: "psql -U postgres -d testdb -t -A -c \"SELECT extversion FROM pg_extension WHERE extname = 'spock';\"", ExpectedOutput: func(exitCode int, output string) error { if exitCode != 0 { return fmt.Errorf("unexpected exit code: %d", exitCode) @@ -758,12 +779,14 @@ func getCommonExtensionTests() []Test { return []Test{ { Name: "Spock extension can be created", + MinimalOnly: true, Cmd: "psql -U postgres -d testdb -t -A -c \"CREATE EXTENSION IF NOT EXISTS spock; SELECT 1;\"", ExpectedOutput: expectSuccess, }, { - Name: "Spock subscription table accessible", - Cmd: "psql -U postgres -d testdb -t -A -c \"SELECT count(*) FROM spock.subscription;\"", + Name: "Spock subscription table accessible", + MinimalOnly: true, + Cmd: "psql -U postgres -d testdb -t -A -c \"SELECT count(*) FROM spock.subscription;\"", ExpectedOutput: func(exitCode int, output string) error { if exitCode != 0 { return fmt.Errorf("unexpected exit code: %d", exitCode) @@ -776,12 +799,14 @@ func getCommonExtensionTests() []Test { }, { Name: "LOLOR extension can be created", + MinimalOnly: true, Cmd: "psql -U postgres -d testdb -t -A -c \"CREATE EXTENSION IF NOT EXISTS lolor; SELECT 1;\"", ExpectedOutput: expectSuccess, }, { - Name: "LOLOR lo_create works", - Cmd: "psql -U postgres -d testdb -t -A -c \"SELECT lo_create(200000);\"", + Name: "LOLOR lo_create works", + MinimalOnly: true, + Cmd: "psql -U postgres -d testdb -t -A -c \"SELECT lo_create(200000);\"", ExpectedOutput: func(exitCode int, output string) error { if exitCode != 0 { return fmt.Errorf("unexpected exit code: %d", exitCode) @@ -794,12 +819,14 @@ func getCommonExtensionTests() []Test { }, { Name: "Snowflake extension can be created", + MinimalOnly: true, Cmd: "psql -U postgres -d testdb -t -A -c \"CREATE EXTENSION IF NOT EXISTS snowflake; SELECT 1;\"", ExpectedOutput: expectSuccess, }, { - Name: "Snowflake ID generation works", - Cmd: "psql -U postgres -d testdb -t -A -c \"SELECT snowflake.nextval() > 0;\"", + Name: "Snowflake ID generation works", + MinimalOnly: true, + Cmd: "psql -U postgres -d testdb -t -A -c \"SELECT snowflake.nextval() > 0;\"", ExpectedOutput: func(exitCode int, output string) error { if exitCode != 0 { return fmt.Errorf("unexpected exit code: %d", exitCode) From 30dec72c6cd80d6cf674be192535c6fdde0ee87b Mon Sep 17 00:00:00 2001 From: Muhammad Aqeel Date: Thu, 10 Sep 2026 13:52:32 +0500 Subject: [PATCH 08/16] fix(ci): drop the empty spock segment from spock-independent job names --- scripts/build_pgedge_images.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/scripts/build_pgedge_images.py b/scripts/build_pgedge_images.py index 6576f88..41d5bbc 100755 --- a/scripts/build_pgedge_images.py +++ b/scripts/build_pgedge_images.py @@ -326,6 +326,8 @@ def emit_matrix(config: "Config") -> None: for flavor in FLAVOR_WAVES: builds: list[dict] = [] merges: list[dict] = [] + # Job label. A spock-independent flavor has no spock version to name. + cell = "{major}" if flavor in SPOCK_INDEPENDENT_FLAVORS else "{major}-spock{spock}" for image in all_images: if image.flavor != flavor or _should_skip_image(image, config): @@ -339,7 +341,9 @@ def emit_matrix(config: "Config") -> None: merges.append( { - "name": f"{image.postgres_major}-spock{image.spock_major}", + "name": cell.format( + major=image.postgres_major, spock=image.spock_major + ), "build_tag": str(image.build_tag), "extra_tags": [str(t) for t in image.extra_tags], "arches": arches, @@ -353,7 +357,10 @@ def emit_matrix(config: "Config") -> None: for arch in arches: builds.append( { - "name": f"{image.postgres_major}-spock{image.spock_major}-{arch}", + "name": cell.format( + major=image.postgres_major, spock=image.spock_major + ) + + f"-{arch}", "runner": ARCH_RUNNERS[arch], "arch": arch, "target": flavor, From 9081f57b8a4392dab8866546d42576b8d3cd440b Mon Sep 17 00:00:00 2001 From: Muhammad Aqeel Date: Thu, 10 Sep 2026 14:01:39 +0500 Subject: [PATCH 09/16] fix(ci): retry dnf when EPEL metadata lags its mirrors --- Dockerfile | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 009d7ee..25d3a0e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -33,10 +33,18 @@ fastestmirror=1 max_parallel_downloads=10 CONF -dnf install -y epel-release dnf +# EPEL mirrors lag behind its repomd.xml, so a repodata file can 404 on every +# mirror at once -- failover cannot help. Clearing metadata re-resolves to a +# repomd the mirrors actually have. +retry() { + for _ in 1 2 3; do "$@" && return 0; dnf clean metadata; sleep 5; done + return 1 +} + +retry dnf install -y epel-release dnf dnf config-manager --set-enabled crb -dnf update -y --allowerasing -dnf install -y https://dnf.pgedge.com/reporpm/pgedge-release-latest.noarch.rpm +retry dnf update -y --allowerasing +retry dnf install -y https://dnf.pgedge.com/reporpm/pgedge-release-latest.noarch.rpm if [[ -n "${PACKAGE_RELEASE_CHANNEL}" ]]; then sed -i "s|release|${PACKAGE_RELEASE_CHANNEL}|g" /etc/yum.repos.d/pgedge.repo fi From 2a4d14e7d1ddb2578544297182a7b2d00799715c Mon Sep 17 00:00:00 2001 From: Muhammad Aqeel Date: Thu, 10 Sep 2026 16:02:59 +0500 Subject: [PATCH 10/16] fix(ci): pass every packagelist and parent-image ARG from the cell --- .github/actions/build-flavor/action.yml | 21 +++++++++++---------- scripts/build_pgedge_images.py | 9 +++++++++ 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/.github/actions/build-flavor/action.yml b/.github/actions/build-flavor/action.yml index e141e98..da2271a 100644 --- a/.github/actions/build-flavor/action.yml +++ b/.github/actions/build-flavor/action.yml @@ -92,22 +92,23 @@ runs: args+=(--set "default.output=type=image,name=${REPO},push-by-digest=true,name-canonical=true,push=true") fi - # A chained flavor starts FROM the image the previous wave published, so - # its ancestor stages are not part of this build graph at all. Passing - # their packagelists anyway is harmless and keeps the call uniform. + # Every packagelist ARG comes from the cell, so a new flavor needs no + # change here -- only the Dockerfile and the driver's maps. envs=( "PACKAGE_RELEASE_CHANNEL=$(jq -r '.package_release_channel' <<< "$CELL")" "POSTGRES_MAJOR_VERSION=$(jq -r '.postgres_major' <<< "$CELL")" - "PACKAGE_LIST_FILE=$(jq -r '.package_list_args.PACKAGE_LIST_FILE' <<< "$CELL")" - "STANDARD_PACKAGE_LIST_FILE=$(jq -r '.package_list_args.STANDARD_PACKAGE_LIST_FILE' <<< "$CELL")" - "COLDFRONT_PACKAGE_LIST_FILE=$(jq -r '.package_list_args.COLDFRONT_PACKAGE_LIST_FILE' <<< "$CELL")" "TARGET=${target}" "TAG=${REPO}" ) - case "$target" in - standard) [[ -n "$parent" ]] && envs+=("MINIMAL_IMAGE=${REPO}:${parent}") ;; - coldfront) [[ -n "$parent" ]] && envs+=("STANDARD_IMAGE=${REPO}:${parent}") ;; - esac + while IFS= read -r kv; do + envs+=("$kv") + done < <(jq -r '.package_list_args | to_entries[] | "\(.key)=\(.value)"' <<< "$CELL") + + # A chained flavor starts FROM the image the previous wave published. + parent_arg=$(jq -r '.parent_image_arg' <<< "$CELL") + if [[ -n "$parent" && -n "$parent_arg" ]]; then + envs+=("${parent_arg}=${REPO}:${parent}") + fi env "${envs[@]}" docker buildx bake "${args[@]}" diff --git a/scripts/build_pgedge_images.py b/scripts/build_pgedge_images.py index 41d5bbc..cdbe945 100755 --- a/scripts/build_pgedge_images.py +++ b/scripts/build_pgedge_images.py @@ -44,6 +44,14 @@ def from_env() -> "Config": # would claim a version the image does not contain. SPOCK_INDEPENDENT_FLAVORS = {"postgres"} +# The Dockerfile ARG that selects what a chained flavor is built FROM. Emitted +# with each cell so the workflow never has to know these names. +FLAVOR_IMAGE_ARGS = { + "minimal": "POSTGRES_IMAGE", + "standard": "MINIMAL_IMAGE", + "coldfront": "STANDARD_IMAGE", +} + # The Dockerfile ARG each flavor's stage reads its packagelist from. FLAVOR_LIST_ARGS = { "postgres": "POSTGRES_PACKAGE_LIST_FILE", @@ -368,6 +376,7 @@ def emit_matrix(config: "Config") -> None: "postgres_major": image.postgres_major, "package_release_channel": image.package_release_channel, "parent_build_tag": image.parent_build_tag, + "parent_image_arg": FLAVOR_IMAGE_ARGS.get(flavor, ""), "package_list_args": image.package_list_args, } ) From 99cd9536ccb413bdca905fb97ea65fac60a2607f Mon Sep 17 00:00:00 2001 From: Muhammad Aqeel Date: Thu, 10 Sep 2026 16:24:52 +0500 Subject: [PATCH 11/16] fix(ci): revert dnf mirror tuning that increased metadata failures --- Dockerfile | 22 +++------------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/Dockerfile b/Dockerfile index 25d3a0e..ef4ded6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,26 +25,10 @@ set -o nounset useradd -u ${POSTGRES_USER_ID} -m postgres -s /bin/bash -cat >> /etc/dnf/dnf.conf <<'CONF' -retries=10 -timeout=30 -minrate=100k -fastestmirror=1 -max_parallel_downloads=10 -CONF - -# EPEL mirrors lag behind its repomd.xml, so a repodata file can 404 on every -# mirror at once -- failover cannot help. Clearing metadata re-resolves to a -# repomd the mirrors actually have. -retry() { - for _ in 1 2 3; do "$@" && return 0; dnf clean metadata; sleep 5; done - return 1 -} - -retry dnf install -y epel-release dnf +dnf install -y epel-release dnf dnf config-manager --set-enabled crb -retry dnf update -y --allowerasing -retry dnf install -y https://dnf.pgedge.com/reporpm/pgedge-release-latest.noarch.rpm +dnf update -y --allowerasing +dnf install -y https://dnf.pgedge.com/reporpm/pgedge-release-latest.noarch.rpm if [[ -n "${PACKAGE_RELEASE_CHANNEL}" ]]; then sed -i "s|release|${PACKAGE_RELEASE_CHANNEL}|g" /etc/yum.repos.d/pgedge.repo fi From 69dbb4f92cd43ab63af02636e883bc9f96ac99dc Mon Sep 17 00:00:00 2001 From: Muhammad Aqeel Date: Fri, 11 Sep 2026 15:02:21 +0500 Subject: [PATCH 12/16] fix: harden the coldfront entrypoint and test each image as its own flavor --- .github/workflows/pr_test_latest.yaml | 6 +- .github/workflows/test_images.yaml | 6 +- coldfront-entrypoint.sh | 20 ++++- tests/main.go | 114 ++++++++++++++++++++++++-- 4 files changed, 136 insertions(+), 10 deletions(-) diff --git a/.github/workflows/pr_test_latest.yaml b/.github/workflows/pr_test_latest.yaml index 802dd0a..1ebce6c 100644 --- a/.github/workflows/pr_test_latest.yaml +++ b/.github/workflows/pr_test_latest.yaml @@ -65,10 +65,14 @@ jobs: tag=$(echo "$tag" | xargs) # trim whitespace # Determine flavor from tag - if [[ "$tag" == *"-minimal"* ]]; then + if [[ "$tag" == *"-postgres"* ]]; then + flavor="postgres" + elif [[ "$tag" == *"-minimal"* ]]; then flavor="minimal" elif [[ "$tag" == *"-standard"* ]]; then flavor="standard" + elif [[ "$tag" == *"-coldfront"* ]]; then + flavor="coldfront" else # Default to standard if not specified flavor="standard" diff --git a/.github/workflows/test_images.yaml b/.github/workflows/test_images.yaml index ff3eab5..7ffc8f0 100644 --- a/.github/workflows/test_images.yaml +++ b/.github/workflows/test_images.yaml @@ -50,10 +50,14 @@ jobs: tag=$(echo "$tag" | xargs) # trim whitespace # Determine flavor from tag - if [[ "$tag" == *"-minimal"* ]]; then + if [[ "$tag" == *"-postgres"* ]]; then + flavor="postgres" + elif [[ "$tag" == *"-minimal"* ]]; then flavor="minimal" elif [[ "$tag" == *"-standard"* ]]; then flavor="standard" + elif [[ "$tag" == *"-coldfront"* ]]; then + flavor="coldfront" else # Default to standard if not specified flavor="standard" diff --git a/coldfront-entrypoint.sh b/coldfront-entrypoint.sh index e2d8376..240d938 100755 --- a/coldfront-entrypoint.sh +++ b/coldfront-entrypoint.sh @@ -96,6 +96,20 @@ archiver | partitioner | compactor) ;; esac +# libpq keyword/value quoting for the loopback DSN: single-quote the value and +# backslash-escape backslashes and quotes, so a space or quote in a role or +# database name cannot split the DSN or end a value early. +_cf_dsn_quote() { + printf "'%s'" "$(printf '%s' "$1" | sed "s/[\\\\']/\\\\&/g")" +} + +# docker-entrypoint.sh turns a leading option into "postgres $@", but only after +# this wrapper has run. Normalising first keeps `run -c work_mem=...` on +# the branch below instead of silently starting without the preloads. +case ${1:-} in +-*) set -- postgres "$@" ;; +esac + if [ "${1:-}" = "postgres" ]; then shift @@ -128,9 +142,9 @@ if [ "${1:-}" = "postgres" ]; then # the socket lives elsewhere -- CNPG forces /controller/run. if [ -z "${COLDFRONT_LOCAL_PG_DSN:-}" ]; then _cf_user="${POSTGRES_USER:-postgres}" - COLDFRONT_LOCAL_PG_DSN="host=${COLDFRONT_SOCKET_DIR:-/var/run/postgresql}" - COLDFRONT_LOCAL_PG_DSN+=" dbname=${POSTGRES_DB:-${_cf_user}}" - COLDFRONT_LOCAL_PG_DSN+=" user=${_cf_user}" + COLDFRONT_LOCAL_PG_DSN="host=$(_cf_dsn_quote "${COLDFRONT_SOCKET_DIR:-/var/run/postgresql}")" + COLDFRONT_LOCAL_PG_DSN+=" dbname=$(_cf_dsn_quote "${POSTGRES_DB:-${_cf_user}}")" + COLDFRONT_LOCAL_PG_DSN+=" user=$(_cf_dsn_quote "${_cf_user}")" COLDFRONT_LOCAL_PG_DSN+=" application_name=coldfront_pglocal" fi args+=(-c coldfront.local_pg_dsn="${COLDFRONT_LOCAL_PG_DSN}") diff --git a/tests/main.go b/tests/main.go index e2677ce..fbedcb6 100644 --- a/tests/main.go +++ b/tests/main.go @@ -178,6 +178,19 @@ func runEntrypointTests(runner *DefaultEntrypointRunner, flavor string) int { fmt.Println() } + // Phase 2b: ColdFront's wrapper around that entrypoint + if flavor == "coldfront" { + printPhaseHeader("Phase 2b: ColdFront Entrypoint Test") + if err := runner.TestColdfrontEntrypoint(); err != nil { + errorCount++ + fmt.Printf(" ColdFront entrypoint test ❌\n") + log.Printf(" Error: %v", err) + } else { + fmt.Printf(" ColdFront entrypoint test ✅\n") + } + fmt.Println() + } + return errorCount } @@ -362,6 +375,92 @@ patroni /tmp/patroni.yml`, patroniConfig) return resp.ID, nil } +// TestColdfrontEntrypoint starts the image the way an operator passing server +// options does -- a leading "-c" rather than an explicit "postgres" -- and with +// a space in the role and database names, so both ways the wrapper can lose +// ColdFront's settings are covered. +func (r *DefaultEntrypointRunner) TestColdfrontEntrypoint() error { + const ( + user = "cf user" + db = "cf db" + ) + + resp, err := r.cli.ContainerCreate(r.ctx, &container.Config{ + Image: r.image, + Env: []string{ + "POSTGRES_PASSWORD=testpassword", + "POSTGRES_USER=" + user, + "POSTGRES_DB=" + db, + "COLDFRONT_WAREHOUSE=wh", + }, + // docker-entrypoint.sh only turns this into "postgres -c ..." after the + // ColdFront wrapper has run, so the wrapper has to normalise it itself. + Cmd: []string{"-c", "work_mem=8MB"}, + }, &container.HostConfig{}, nil, nil, "") + if err != nil { + return fmt.Errorf("error creating container: %w", err) + } + defer r.cleanupContainer(resp.ID) + + if err := r.cli.ContainerStart(r.ctx, resp.ID, container.StartOptions{}); err != nil { + return fmt.Errorf("error starting container: %w", err) + } + + psql := func(sql string) (string, error) { + exitCode, out, err := execInContainer(r.cli, r.ctx, resp.ID, + []string{"psql", "-U", user, "-d", db, "-X", "-t", "-A", "-c", sql}) + if err != nil { + return "", err + } + if exitCode != 0 { + return "", fmt.Errorf("psql exited %d: %s", exitCode, strings.TrimSpace(out)) + } + return strings.TrimSpace(out), nil + } + + // A real query, not pg_isready: initdb's own bootstrap server answers before + // the postmaster this test is about is listening. + ready := false + for deadline := time.Now().Add(90 * time.Second); time.Now().Before(deadline); { + if _, err := psql("SELECT 1"); err == nil { + ready = true + break + } + time.Sleep(2 * time.Second) + } + if !ready { + return fmt.Errorf("timeout waiting for PostgreSQL to be ready") + } + + checks := []struct { + name, sql, want string + }{ + // Present only if the leading option did not bypass the wrapper. + {"shared_preload_libraries", "SHOW shared_preload_libraries", "pg_duckdb,coldfront"}, + // The operator's own argument is appended last and still wins. + {"work_mem", "SHOW work_mem", "8MB"}, + {"coldfront.warehouse", "SHOW coldfront.warehouse", "wh"}, + // Spaces have to be quoted, not split into further libpq keywords. + {"coldfront.local_pg_dsn", "SHOW coldfront.local_pg_dsn", + "host='/var/run/postgresql' dbname='" + db + "' user='" + user + "' application_name=coldfront_pglocal"}, + } + for _, c := range checks { + got, err := psql(c.sql) + if err != nil { + return fmt.Errorf("%s: %w", c.name, err) + } + if got != c.want { + return fmt.Errorf("%s = %q, want %q", c.name, got, c.want) + } + } + + // Connects back over that DSN, which a value split on its spaces could not do. + if _, err := psql("CREATE EXTENSION IF NOT EXISTS coldfront CASCADE; SELECT coldfront.ensure_pg_attached()"); err != nil { + return fmt.Errorf("ensure_pg_attached: %w", err) + } + return nil +} + func (r *DefaultEntrypointRunner) cleanupContainer(containerID string) { r.cli.ContainerStop(r.ctx, containerID, container.StopOptions{}) r.cli.ContainerRemove(r.ctx, containerID, container.RemoveOptions{}) @@ -634,7 +733,13 @@ func (r *TestRunner) exec(cmd string) (int, string, error) { return -1, "", fmt.Errorf("empty command") } - execID, err := r.cli.ContainerExecCreate(r.ctx, r.containerID, container.ExecOptions{ + return execInContainer(r.cli, r.ctx, r.containerID, cmdArgs) +} + +// execInContainer runs an already-parsed argv in a container and returns its +// exit code with stdout and stderr interleaved. +func execInContainer(cli *client.Client, ctx context.Context, containerID string, cmdArgs []string) (int, string, error) { + execID, err := cli.ContainerExecCreate(ctx, containerID, container.ExecOptions{ Cmd: cmdArgs, AttachStdout: true, AttachStderr: true, @@ -643,19 +748,18 @@ func (r *TestRunner) exec(cmd string) (int, string, error) { return -1, "", fmt.Errorf("error creating exec: %w", err) } - resp, err := r.cli.ContainerExecAttach(r.ctx, execID.ID, container.ExecAttachOptions{}) + resp, err := cli.ContainerExecAttach(ctx, execID.ID, container.ExecAttachOptions{}) if err != nil { return -1, "", fmt.Errorf("error attaching to exec: %w", err) } defer resp.Close() var outputBuf bytes.Buffer - _, err = stdcopy.StdCopy(&outputBuf, &outputBuf, resp.Reader) - if err != nil { + if _, err := stdcopy.StdCopy(&outputBuf, &outputBuf, resp.Reader); err != nil { return -1, "", fmt.Errorf("error copying output: %w", err) } - inspectResp, err := r.cli.ContainerExecInspect(r.ctx, execID.ID) + inspectResp, err := cli.ContainerExecInspect(ctx, execID.ID) if err != nil { return -1, "", fmt.Errorf("error inspecting exec: %w", err) } From dd9161a067b40aeffd7e583dc2d9d032f9fafdcc Mon Sep 17 00:00:00 2001 From: Muhammad Aqeel Date: Thu, 17 Sep 2026 15:27:35 +0500 Subject: [PATCH 13/16] Updates pg_cron, pgmq and pg_stat_monitor versions --- packagelists/amd64/pg16.15-spock5.0.11-standard.txt | 6 +++--- packagelists/amd64/pg16.15-spock6.0.0-beta1-standard.txt | 6 +++--- packagelists/amd64/pg17.11-spock5.0.11-standard.txt | 6 +++--- packagelists/amd64/pg17.11-spock6.0.0-beta1-standard.txt | 6 +++--- packagelists/amd64/pg18.6-spock5.0.11-standard.txt | 6 +++--- packagelists/amd64/pg18.6-spock6.0.0-beta1-standard.txt | 6 +++--- packagelists/arm64/pg16.15-spock5.0.11-standard.txt | 6 +++--- packagelists/arm64/pg16.15-spock6.0.0-beta1-standard.txt | 6 +++--- packagelists/arm64/pg17.11-spock5.0.11-standard.txt | 6 +++--- packagelists/arm64/pg17.11-spock6.0.0-beta1-standard.txt | 6 +++--- packagelists/arm64/pg18.6-spock5.0.11-standard.txt | 6 +++--- packagelists/arm64/pg18.6-spock6.0.0-beta1-standard.txt | 6 +++--- scripts/build_pgedge_images.py | 6 +++--- 13 files changed, 39 insertions(+), 39 deletions(-) diff --git a/packagelists/amd64/pg16.15-spock5.0.11-standard.txt b/packagelists/amd64/pg16.15-spock5.0.11-standard.txt index da0b6c2..25a46ff 100644 --- a/packagelists/amd64/pg16.15-spock5.0.11-standard.txt +++ b/packagelists/amd64/pg16.15-spock5.0.11-standard.txt @@ -3,11 +3,11 @@ pgedge-postgis36_16-3.6.4-1.el9 pgedge-pgvector_16-0.8.5-1.el9 pgedge-pgbackrest-2.59.0-1.el9 pgedge-python3-psycopg2-2.9.12-1.el9 -pgedge-pgmq_16-1.12.0-1.el9 -pgedge-pg_cron_16-1.6.7-1.el9 +pgedge-pgmq_16-1.13.0-1.el9 +pgedge-pg_cron_16-1.6.8-1.el9 pgedge-pg-tokenizer_16-0.1.1-1.el9 pgedge-pg-vectorize_16-0.23.0-1.el9 -pgedge-pg-stat-monitor_16-2.3.0-1.el9 +pgedge-pg-stat-monitor_16-2.4.0-1.el9 pgedge-vchord-bm25_16-0.2.2-1.el9 pgedge-vectorizer_16-1.1-1.el9 pgedge-system_stats_16-4.0-1.el9 diff --git a/packagelists/amd64/pg16.15-spock6.0.0-beta1-standard.txt b/packagelists/amd64/pg16.15-spock6.0.0-beta1-standard.txt index da0b6c2..25a46ff 100644 --- a/packagelists/amd64/pg16.15-spock6.0.0-beta1-standard.txt +++ b/packagelists/amd64/pg16.15-spock6.0.0-beta1-standard.txt @@ -3,11 +3,11 @@ pgedge-postgis36_16-3.6.4-1.el9 pgedge-pgvector_16-0.8.5-1.el9 pgedge-pgbackrest-2.59.0-1.el9 pgedge-python3-psycopg2-2.9.12-1.el9 -pgedge-pgmq_16-1.12.0-1.el9 -pgedge-pg_cron_16-1.6.7-1.el9 +pgedge-pgmq_16-1.13.0-1.el9 +pgedge-pg_cron_16-1.6.8-1.el9 pgedge-pg-tokenizer_16-0.1.1-1.el9 pgedge-pg-vectorize_16-0.23.0-1.el9 -pgedge-pg-stat-monitor_16-2.3.0-1.el9 +pgedge-pg-stat-monitor_16-2.4.0-1.el9 pgedge-vchord-bm25_16-0.2.2-1.el9 pgedge-vectorizer_16-1.1-1.el9 pgedge-system_stats_16-4.0-1.el9 diff --git a/packagelists/amd64/pg17.11-spock5.0.11-standard.txt b/packagelists/amd64/pg17.11-spock5.0.11-standard.txt index 6098198..f4090f3 100644 --- a/packagelists/amd64/pg17.11-spock5.0.11-standard.txt +++ b/packagelists/amd64/pg17.11-spock5.0.11-standard.txt @@ -3,11 +3,11 @@ pgedge-postgis36_17-3.6.4-1.el9 pgedge-pgvector_17-0.8.5-1.el9 pgedge-pgbackrest-2.59.0-1.el9 pgedge-python3-psycopg2-2.9.12-1.el9 -pgedge-pgmq_17-1.12.0-1.el9 -pgedge-pg_cron_17-1.6.7-1.el9 +pgedge-pgmq_17-1.13.0-1.el9 +pgedge-pg_cron_17-1.6.8-1.el9 pgedge-pg-tokenizer_17-0.1.1-1.el9 pgedge-pg-vectorize_17-0.23.0-1.el9 -pgedge-pg-stat-monitor_17-2.3.0-1.el9 +pgedge-pg-stat-monitor_17-2.4.0-1.el9 pgedge-vchord-bm25_17-0.2.2-1.el9 pgedge-vectorizer_17-1.1-1.el9 pgedge-system_stats_17-4.0-1.el9 diff --git a/packagelists/amd64/pg17.11-spock6.0.0-beta1-standard.txt b/packagelists/amd64/pg17.11-spock6.0.0-beta1-standard.txt index 6098198..f4090f3 100644 --- a/packagelists/amd64/pg17.11-spock6.0.0-beta1-standard.txt +++ b/packagelists/amd64/pg17.11-spock6.0.0-beta1-standard.txt @@ -3,11 +3,11 @@ pgedge-postgis36_17-3.6.4-1.el9 pgedge-pgvector_17-0.8.5-1.el9 pgedge-pgbackrest-2.59.0-1.el9 pgedge-python3-psycopg2-2.9.12-1.el9 -pgedge-pgmq_17-1.12.0-1.el9 -pgedge-pg_cron_17-1.6.7-1.el9 +pgedge-pgmq_17-1.13.0-1.el9 +pgedge-pg_cron_17-1.6.8-1.el9 pgedge-pg-tokenizer_17-0.1.1-1.el9 pgedge-pg-vectorize_17-0.23.0-1.el9 -pgedge-pg-stat-monitor_17-2.3.0-1.el9 +pgedge-pg-stat-monitor_17-2.4.0-1.el9 pgedge-vchord-bm25_17-0.2.2-1.el9 pgedge-vectorizer_17-1.1-1.el9 pgedge-system_stats_17-4.0-1.el9 diff --git a/packagelists/amd64/pg18.6-spock5.0.11-standard.txt b/packagelists/amd64/pg18.6-spock5.0.11-standard.txt index 82ece0c..06bc1e0 100644 --- a/packagelists/amd64/pg18.6-spock5.0.11-standard.txt +++ b/packagelists/amd64/pg18.6-spock5.0.11-standard.txt @@ -3,11 +3,11 @@ pgedge-postgis36_18-3.6.4-1.el9 pgedge-pgvector_18-0.8.5-1.el9 pgedge-pgbackrest-2.59.0-1.el9 pgedge-python3-psycopg2-2.9.12-1.el9 -pgedge-pgmq_18-1.12.0-1.el9 -pgedge-pg_cron_18-1.6.7-1.el9 +pgedge-pgmq_18-1.13.0-1.el9 +pgedge-pg_cron_18-1.6.8-1.el9 pgedge-pg-tokenizer_18-0.1.1-1.el9 pgedge-pg-vectorize_18-0.23.0-1.el9 -pgedge-pg-stat-monitor_18-2.3.0-1.el9 +pgedge-pg-stat-monitor_18-2.4.0-1.el9 pgedge-vchord-bm25_18-0.2.2-1.el9 pgedge-vectorizer_18-1.1-1.el9 pgedge-system_stats_18-4.0-1.el9 diff --git a/packagelists/amd64/pg18.6-spock6.0.0-beta1-standard.txt b/packagelists/amd64/pg18.6-spock6.0.0-beta1-standard.txt index 82ece0c..06bc1e0 100644 --- a/packagelists/amd64/pg18.6-spock6.0.0-beta1-standard.txt +++ b/packagelists/amd64/pg18.6-spock6.0.0-beta1-standard.txt @@ -3,11 +3,11 @@ pgedge-postgis36_18-3.6.4-1.el9 pgedge-pgvector_18-0.8.5-1.el9 pgedge-pgbackrest-2.59.0-1.el9 pgedge-python3-psycopg2-2.9.12-1.el9 -pgedge-pgmq_18-1.12.0-1.el9 -pgedge-pg_cron_18-1.6.7-1.el9 +pgedge-pgmq_18-1.13.0-1.el9 +pgedge-pg_cron_18-1.6.8-1.el9 pgedge-pg-tokenizer_18-0.1.1-1.el9 pgedge-pg-vectorize_18-0.23.0-1.el9 -pgedge-pg-stat-monitor_18-2.3.0-1.el9 +pgedge-pg-stat-monitor_18-2.4.0-1.el9 pgedge-vchord-bm25_18-0.2.2-1.el9 pgedge-vectorizer_18-1.1-1.el9 pgedge-system_stats_18-4.0-1.el9 diff --git a/packagelists/arm64/pg16.15-spock5.0.11-standard.txt b/packagelists/arm64/pg16.15-spock5.0.11-standard.txt index da0b6c2..25a46ff 100644 --- a/packagelists/arm64/pg16.15-spock5.0.11-standard.txt +++ b/packagelists/arm64/pg16.15-spock5.0.11-standard.txt @@ -3,11 +3,11 @@ pgedge-postgis36_16-3.6.4-1.el9 pgedge-pgvector_16-0.8.5-1.el9 pgedge-pgbackrest-2.59.0-1.el9 pgedge-python3-psycopg2-2.9.12-1.el9 -pgedge-pgmq_16-1.12.0-1.el9 -pgedge-pg_cron_16-1.6.7-1.el9 +pgedge-pgmq_16-1.13.0-1.el9 +pgedge-pg_cron_16-1.6.8-1.el9 pgedge-pg-tokenizer_16-0.1.1-1.el9 pgedge-pg-vectorize_16-0.23.0-1.el9 -pgedge-pg-stat-monitor_16-2.3.0-1.el9 +pgedge-pg-stat-monitor_16-2.4.0-1.el9 pgedge-vchord-bm25_16-0.2.2-1.el9 pgedge-vectorizer_16-1.1-1.el9 pgedge-system_stats_16-4.0-1.el9 diff --git a/packagelists/arm64/pg16.15-spock6.0.0-beta1-standard.txt b/packagelists/arm64/pg16.15-spock6.0.0-beta1-standard.txt index da0b6c2..25a46ff 100644 --- a/packagelists/arm64/pg16.15-spock6.0.0-beta1-standard.txt +++ b/packagelists/arm64/pg16.15-spock6.0.0-beta1-standard.txt @@ -3,11 +3,11 @@ pgedge-postgis36_16-3.6.4-1.el9 pgedge-pgvector_16-0.8.5-1.el9 pgedge-pgbackrest-2.59.0-1.el9 pgedge-python3-psycopg2-2.9.12-1.el9 -pgedge-pgmq_16-1.12.0-1.el9 -pgedge-pg_cron_16-1.6.7-1.el9 +pgedge-pgmq_16-1.13.0-1.el9 +pgedge-pg_cron_16-1.6.8-1.el9 pgedge-pg-tokenizer_16-0.1.1-1.el9 pgedge-pg-vectorize_16-0.23.0-1.el9 -pgedge-pg-stat-monitor_16-2.3.0-1.el9 +pgedge-pg-stat-monitor_16-2.4.0-1.el9 pgedge-vchord-bm25_16-0.2.2-1.el9 pgedge-vectorizer_16-1.1-1.el9 pgedge-system_stats_16-4.0-1.el9 diff --git a/packagelists/arm64/pg17.11-spock5.0.11-standard.txt b/packagelists/arm64/pg17.11-spock5.0.11-standard.txt index 6098198..f4090f3 100644 --- a/packagelists/arm64/pg17.11-spock5.0.11-standard.txt +++ b/packagelists/arm64/pg17.11-spock5.0.11-standard.txt @@ -3,11 +3,11 @@ pgedge-postgis36_17-3.6.4-1.el9 pgedge-pgvector_17-0.8.5-1.el9 pgedge-pgbackrest-2.59.0-1.el9 pgedge-python3-psycopg2-2.9.12-1.el9 -pgedge-pgmq_17-1.12.0-1.el9 -pgedge-pg_cron_17-1.6.7-1.el9 +pgedge-pgmq_17-1.13.0-1.el9 +pgedge-pg_cron_17-1.6.8-1.el9 pgedge-pg-tokenizer_17-0.1.1-1.el9 pgedge-pg-vectorize_17-0.23.0-1.el9 -pgedge-pg-stat-monitor_17-2.3.0-1.el9 +pgedge-pg-stat-monitor_17-2.4.0-1.el9 pgedge-vchord-bm25_17-0.2.2-1.el9 pgedge-vectorizer_17-1.1-1.el9 pgedge-system_stats_17-4.0-1.el9 diff --git a/packagelists/arm64/pg17.11-spock6.0.0-beta1-standard.txt b/packagelists/arm64/pg17.11-spock6.0.0-beta1-standard.txt index 6098198..f4090f3 100644 --- a/packagelists/arm64/pg17.11-spock6.0.0-beta1-standard.txt +++ b/packagelists/arm64/pg17.11-spock6.0.0-beta1-standard.txt @@ -3,11 +3,11 @@ pgedge-postgis36_17-3.6.4-1.el9 pgedge-pgvector_17-0.8.5-1.el9 pgedge-pgbackrest-2.59.0-1.el9 pgedge-python3-psycopg2-2.9.12-1.el9 -pgedge-pgmq_17-1.12.0-1.el9 -pgedge-pg_cron_17-1.6.7-1.el9 +pgedge-pgmq_17-1.13.0-1.el9 +pgedge-pg_cron_17-1.6.8-1.el9 pgedge-pg-tokenizer_17-0.1.1-1.el9 pgedge-pg-vectorize_17-0.23.0-1.el9 -pgedge-pg-stat-monitor_17-2.3.0-1.el9 +pgedge-pg-stat-monitor_17-2.4.0-1.el9 pgedge-vchord-bm25_17-0.2.2-1.el9 pgedge-vectorizer_17-1.1-1.el9 pgedge-system_stats_17-4.0-1.el9 diff --git a/packagelists/arm64/pg18.6-spock5.0.11-standard.txt b/packagelists/arm64/pg18.6-spock5.0.11-standard.txt index 82ece0c..06bc1e0 100644 --- a/packagelists/arm64/pg18.6-spock5.0.11-standard.txt +++ b/packagelists/arm64/pg18.6-spock5.0.11-standard.txt @@ -3,11 +3,11 @@ pgedge-postgis36_18-3.6.4-1.el9 pgedge-pgvector_18-0.8.5-1.el9 pgedge-pgbackrest-2.59.0-1.el9 pgedge-python3-psycopg2-2.9.12-1.el9 -pgedge-pgmq_18-1.12.0-1.el9 -pgedge-pg_cron_18-1.6.7-1.el9 +pgedge-pgmq_18-1.13.0-1.el9 +pgedge-pg_cron_18-1.6.8-1.el9 pgedge-pg-tokenizer_18-0.1.1-1.el9 pgedge-pg-vectorize_18-0.23.0-1.el9 -pgedge-pg-stat-monitor_18-2.3.0-1.el9 +pgedge-pg-stat-monitor_18-2.4.0-1.el9 pgedge-vchord-bm25_18-0.2.2-1.el9 pgedge-vectorizer_18-1.1-1.el9 pgedge-system_stats_18-4.0-1.el9 diff --git a/packagelists/arm64/pg18.6-spock6.0.0-beta1-standard.txt b/packagelists/arm64/pg18.6-spock6.0.0-beta1-standard.txt index 82ece0c..06bc1e0 100644 --- a/packagelists/arm64/pg18.6-spock6.0.0-beta1-standard.txt +++ b/packagelists/arm64/pg18.6-spock6.0.0-beta1-standard.txt @@ -3,11 +3,11 @@ pgedge-postgis36_18-3.6.4-1.el9 pgedge-pgvector_18-0.8.5-1.el9 pgedge-pgbackrest-2.59.0-1.el9 pgedge-python3-psycopg2-2.9.12-1.el9 -pgedge-pgmq_18-1.12.0-1.el9 -pgedge-pg_cron_18-1.6.7-1.el9 +pgedge-pgmq_18-1.13.0-1.el9 +pgedge-pg_cron_18-1.6.8-1.el9 pgedge-pg-tokenizer_18-0.1.1-1.el9 pgedge-pg-vectorize_18-0.23.0-1.el9 -pgedge-pg-stat-monitor_18-2.3.0-1.el9 +pgedge-pg-stat-monitor_18-2.4.0-1.el9 pgedge-vchord-bm25_18-0.2.2-1.el9 pgedge-vectorizer_18-1.1-1.el9 pgedge-system_stats_18-4.0-1.el9 diff --git a/scripts/build_pgedge_images.py b/scripts/build_pgedge_images.py index cdbe945..b195278 100755 --- a/scripts/build_pgedge_images.py +++ b/scripts/build_pgedge_images.py @@ -245,15 +245,15 @@ def make_all_flavor_images( all_images: list[PgEdgeImage] = [ # PostgreSQL-only base, one per major; no spock segment. PgEdgeImage( - postgres_version="16.15", spock_version="", epoch=2, flavor="postgres", + postgres_version="16.15", spock_version="", epoch=3, flavor="postgres", is_latest_for_pg_major=True, ), PgEdgeImage( - postgres_version="17.11", spock_version="", epoch=2, flavor="postgres", + postgres_version="17.11", spock_version="", epoch=3, flavor="postgres", is_latest_for_pg_major=True, ), PgEdgeImage( - postgres_version="18.6", spock_version="", epoch=2, flavor="postgres", + postgres_version="18.6", spock_version="", epoch=3, flavor="postgres", is_latest_for_pg_major=True, ), # pg16 images From e1a1ef33bc54d08aba9be0874b74dc3276936f89 Mon Sep 17 00:00:00 2001 From: Muhammad Aqeel Date: Thu, 17 Sep 2026 16:08:13 +0500 Subject: [PATCH 14/16] fix: rebuild an image whose published tag is missing a requested architecture --- scripts/build_pgedge_images.py | 39 ++++++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/scripts/build_pgedge_images.py b/scripts/build_pgedge_images.py index b195278..d18abfa 100755 --- a/scripts/build_pgedge_images.py +++ b/scripts/build_pgedge_images.py @@ -323,10 +323,10 @@ def emit_matrix(config: "Config") -> None: The workflow consumes this instead of hardcoding the cell list, so the matrix and the image definitions above cannot drift apart. - An image whose immutable tag is already published is left out of the build - matrix unless republish is set, but stays in the merge matrix with - needs_build false, so a re-dispatch repairs its mutable tags without - rebuilding anything. + An image whose immutable tag already carries every requested architecture is + left out of the build matrix unless republish is set, but stays in the merge + matrix with needs_build false, so a re-dispatch repairs its mutable tags + without rebuilding anything. """ arches = [config.only_arch] if config.only_arch else list(ARCH_RUNNERS) waves: dict = {} @@ -341,11 +341,13 @@ def emit_matrix(config: "Config") -> None: if image.flavor != flavor or _should_skip_image(image, config): continue - needs_build = config.republish or not published_digests( - config.repo, image.build_tag + # Every requested architecture, not merely a published tag: an index + # missing one is rebuilt and reassembled rather than carried forward. + needs_build = config.republish or not set(arches).issubset( + published_platforms(config.repo, image.build_tag) ) if not needs_build: - logging.info(f"{image.build_tag} is already published") + logging.info(f"{image.build_tag} is already published for {', '.join(arches)}") merges.append( { @@ -430,6 +432,29 @@ def published_digests(repo: str, tag: Tag) -> set[str]: return set() +def published_platforms(repo: str, tag: Tag) -> set[str]: + """Architectures that have a real image manifest under `tag`. + + Attestation manifests sit alongside them with platform unknown, so the + presence of a manifest says nothing about coverage: an only-arch run leaves + a complete-looking index that holds a single architecture. + """ + try: + out = subprocess.check_output( + imagetools_cmd("inspect", "--raw", f"{repo}:{tag}"), + stderr=subprocess.PIPE, + ) + except subprocess.CalledProcessError: + return set() + + platforms = set() + for manifest in json.loads(out).get("manifests", []): + arch = manifest.get("platform", {}).get("architecture") + if arch and arch != "unknown": + platforms.add(arch) + return platforms + + def build( repo: str, image: PgEdgeImage, From 89b94e370c50b04fcfafd4562939fc5aae978eda Mon Sep 17 00:00:00 2001 From: Muhammad Aqeel Date: Mon, 21 Sep 2026 22:27:56 +0500 Subject: [PATCH 15/16] build: bump image epoch to 4 and update pg_vectorize to 0.27.1 --- .../amd64/pg16.15-spock5.0.11-standard.txt | 2 +- .../pg16.15-spock6.0.0-beta1-standard.txt | 2 +- .../amd64/pg17.11-spock5.0.11-standard.txt | 2 +- .../pg17.11-spock6.0.0-beta1-standard.txt | 2 +- .../amd64/pg18.6-spock5.0.11-standard.txt | 2 +- .../amd64/pg18.6-spock6.0.0-beta1-standard.txt | 2 +- .../arm64/pg16.15-spock5.0.11-standard.txt | 2 +- .../pg16.15-spock6.0.0-beta1-standard.txt | 2 +- .../arm64/pg17.11-spock5.0.11-standard.txt | 2 +- .../pg17.11-spock6.0.0-beta1-standard.txt | 2 +- .../arm64/pg18.6-spock5.0.11-standard.txt | 2 +- .../arm64/pg18.6-spock6.0.0-beta1-standard.txt | 2 +- scripts/build_pgedge_images.py | 18 +++++++++--------- tests/main.go | 2 +- 14 files changed, 22 insertions(+), 22 deletions(-) diff --git a/packagelists/amd64/pg16.15-spock5.0.11-standard.txt b/packagelists/amd64/pg16.15-spock5.0.11-standard.txt index 25a46ff..d424681 100644 --- a/packagelists/amd64/pg16.15-spock5.0.11-standard.txt +++ b/packagelists/amd64/pg16.15-spock5.0.11-standard.txt @@ -6,7 +6,7 @@ pgedge-python3-psycopg2-2.9.12-1.el9 pgedge-pgmq_16-1.13.0-1.el9 pgedge-pg_cron_16-1.6.8-1.el9 pgedge-pg-tokenizer_16-0.1.1-1.el9 -pgedge-pg-vectorize_16-0.23.0-1.el9 +pgedge-pg-vectorize_16-0.27.1-1.el9 pgedge-pg-stat-monitor_16-2.4.0-1.el9 pgedge-vchord-bm25_16-0.2.2-1.el9 pgedge-vectorizer_16-1.1-1.el9 diff --git a/packagelists/amd64/pg16.15-spock6.0.0-beta1-standard.txt b/packagelists/amd64/pg16.15-spock6.0.0-beta1-standard.txt index 25a46ff..d424681 100644 --- a/packagelists/amd64/pg16.15-spock6.0.0-beta1-standard.txt +++ b/packagelists/amd64/pg16.15-spock6.0.0-beta1-standard.txt @@ -6,7 +6,7 @@ pgedge-python3-psycopg2-2.9.12-1.el9 pgedge-pgmq_16-1.13.0-1.el9 pgedge-pg_cron_16-1.6.8-1.el9 pgedge-pg-tokenizer_16-0.1.1-1.el9 -pgedge-pg-vectorize_16-0.23.0-1.el9 +pgedge-pg-vectorize_16-0.27.1-1.el9 pgedge-pg-stat-monitor_16-2.4.0-1.el9 pgedge-vchord-bm25_16-0.2.2-1.el9 pgedge-vectorizer_16-1.1-1.el9 diff --git a/packagelists/amd64/pg17.11-spock5.0.11-standard.txt b/packagelists/amd64/pg17.11-spock5.0.11-standard.txt index f4090f3..d3a9d00 100644 --- a/packagelists/amd64/pg17.11-spock5.0.11-standard.txt +++ b/packagelists/amd64/pg17.11-spock5.0.11-standard.txt @@ -6,7 +6,7 @@ pgedge-python3-psycopg2-2.9.12-1.el9 pgedge-pgmq_17-1.13.0-1.el9 pgedge-pg_cron_17-1.6.8-1.el9 pgedge-pg-tokenizer_17-0.1.1-1.el9 -pgedge-pg-vectorize_17-0.23.0-1.el9 +pgedge-pg-vectorize_17-0.27.1-1.el9 pgedge-pg-stat-monitor_17-2.4.0-1.el9 pgedge-vchord-bm25_17-0.2.2-1.el9 pgedge-vectorizer_17-1.1-1.el9 diff --git a/packagelists/amd64/pg17.11-spock6.0.0-beta1-standard.txt b/packagelists/amd64/pg17.11-spock6.0.0-beta1-standard.txt index f4090f3..d3a9d00 100644 --- a/packagelists/amd64/pg17.11-spock6.0.0-beta1-standard.txt +++ b/packagelists/amd64/pg17.11-spock6.0.0-beta1-standard.txt @@ -6,7 +6,7 @@ pgedge-python3-psycopg2-2.9.12-1.el9 pgedge-pgmq_17-1.13.0-1.el9 pgedge-pg_cron_17-1.6.8-1.el9 pgedge-pg-tokenizer_17-0.1.1-1.el9 -pgedge-pg-vectorize_17-0.23.0-1.el9 +pgedge-pg-vectorize_17-0.27.1-1.el9 pgedge-pg-stat-monitor_17-2.4.0-1.el9 pgedge-vchord-bm25_17-0.2.2-1.el9 pgedge-vectorizer_17-1.1-1.el9 diff --git a/packagelists/amd64/pg18.6-spock5.0.11-standard.txt b/packagelists/amd64/pg18.6-spock5.0.11-standard.txt index 06bc1e0..eab98a1 100644 --- a/packagelists/amd64/pg18.6-spock5.0.11-standard.txt +++ b/packagelists/amd64/pg18.6-spock5.0.11-standard.txt @@ -6,7 +6,7 @@ pgedge-python3-psycopg2-2.9.12-1.el9 pgedge-pgmq_18-1.13.0-1.el9 pgedge-pg_cron_18-1.6.8-1.el9 pgedge-pg-tokenizer_18-0.1.1-1.el9 -pgedge-pg-vectorize_18-0.23.0-1.el9 +pgedge-pg-vectorize_18-0.27.1-1.el9 pgedge-pg-stat-monitor_18-2.4.0-1.el9 pgedge-vchord-bm25_18-0.2.2-1.el9 pgedge-vectorizer_18-1.1-1.el9 diff --git a/packagelists/amd64/pg18.6-spock6.0.0-beta1-standard.txt b/packagelists/amd64/pg18.6-spock6.0.0-beta1-standard.txt index 06bc1e0..eab98a1 100644 --- a/packagelists/amd64/pg18.6-spock6.0.0-beta1-standard.txt +++ b/packagelists/amd64/pg18.6-spock6.0.0-beta1-standard.txt @@ -6,7 +6,7 @@ pgedge-python3-psycopg2-2.9.12-1.el9 pgedge-pgmq_18-1.13.0-1.el9 pgedge-pg_cron_18-1.6.8-1.el9 pgedge-pg-tokenizer_18-0.1.1-1.el9 -pgedge-pg-vectorize_18-0.23.0-1.el9 +pgedge-pg-vectorize_18-0.27.1-1.el9 pgedge-pg-stat-monitor_18-2.4.0-1.el9 pgedge-vchord-bm25_18-0.2.2-1.el9 pgedge-vectorizer_18-1.1-1.el9 diff --git a/packagelists/arm64/pg16.15-spock5.0.11-standard.txt b/packagelists/arm64/pg16.15-spock5.0.11-standard.txt index 25a46ff..d424681 100644 --- a/packagelists/arm64/pg16.15-spock5.0.11-standard.txt +++ b/packagelists/arm64/pg16.15-spock5.0.11-standard.txt @@ -6,7 +6,7 @@ pgedge-python3-psycopg2-2.9.12-1.el9 pgedge-pgmq_16-1.13.0-1.el9 pgedge-pg_cron_16-1.6.8-1.el9 pgedge-pg-tokenizer_16-0.1.1-1.el9 -pgedge-pg-vectorize_16-0.23.0-1.el9 +pgedge-pg-vectorize_16-0.27.1-1.el9 pgedge-pg-stat-monitor_16-2.4.0-1.el9 pgedge-vchord-bm25_16-0.2.2-1.el9 pgedge-vectorizer_16-1.1-1.el9 diff --git a/packagelists/arm64/pg16.15-spock6.0.0-beta1-standard.txt b/packagelists/arm64/pg16.15-spock6.0.0-beta1-standard.txt index 25a46ff..d424681 100644 --- a/packagelists/arm64/pg16.15-spock6.0.0-beta1-standard.txt +++ b/packagelists/arm64/pg16.15-spock6.0.0-beta1-standard.txt @@ -6,7 +6,7 @@ pgedge-python3-psycopg2-2.9.12-1.el9 pgedge-pgmq_16-1.13.0-1.el9 pgedge-pg_cron_16-1.6.8-1.el9 pgedge-pg-tokenizer_16-0.1.1-1.el9 -pgedge-pg-vectorize_16-0.23.0-1.el9 +pgedge-pg-vectorize_16-0.27.1-1.el9 pgedge-pg-stat-monitor_16-2.4.0-1.el9 pgedge-vchord-bm25_16-0.2.2-1.el9 pgedge-vectorizer_16-1.1-1.el9 diff --git a/packagelists/arm64/pg17.11-spock5.0.11-standard.txt b/packagelists/arm64/pg17.11-spock5.0.11-standard.txt index f4090f3..d3a9d00 100644 --- a/packagelists/arm64/pg17.11-spock5.0.11-standard.txt +++ b/packagelists/arm64/pg17.11-spock5.0.11-standard.txt @@ -6,7 +6,7 @@ pgedge-python3-psycopg2-2.9.12-1.el9 pgedge-pgmq_17-1.13.0-1.el9 pgedge-pg_cron_17-1.6.8-1.el9 pgedge-pg-tokenizer_17-0.1.1-1.el9 -pgedge-pg-vectorize_17-0.23.0-1.el9 +pgedge-pg-vectorize_17-0.27.1-1.el9 pgedge-pg-stat-monitor_17-2.4.0-1.el9 pgedge-vchord-bm25_17-0.2.2-1.el9 pgedge-vectorizer_17-1.1-1.el9 diff --git a/packagelists/arm64/pg17.11-spock6.0.0-beta1-standard.txt b/packagelists/arm64/pg17.11-spock6.0.0-beta1-standard.txt index f4090f3..d3a9d00 100644 --- a/packagelists/arm64/pg17.11-spock6.0.0-beta1-standard.txt +++ b/packagelists/arm64/pg17.11-spock6.0.0-beta1-standard.txt @@ -6,7 +6,7 @@ pgedge-python3-psycopg2-2.9.12-1.el9 pgedge-pgmq_17-1.13.0-1.el9 pgedge-pg_cron_17-1.6.8-1.el9 pgedge-pg-tokenizer_17-0.1.1-1.el9 -pgedge-pg-vectorize_17-0.23.0-1.el9 +pgedge-pg-vectorize_17-0.27.1-1.el9 pgedge-pg-stat-monitor_17-2.4.0-1.el9 pgedge-vchord-bm25_17-0.2.2-1.el9 pgedge-vectorizer_17-1.1-1.el9 diff --git a/packagelists/arm64/pg18.6-spock5.0.11-standard.txt b/packagelists/arm64/pg18.6-spock5.0.11-standard.txt index 06bc1e0..eab98a1 100644 --- a/packagelists/arm64/pg18.6-spock5.0.11-standard.txt +++ b/packagelists/arm64/pg18.6-spock5.0.11-standard.txt @@ -6,7 +6,7 @@ pgedge-python3-psycopg2-2.9.12-1.el9 pgedge-pgmq_18-1.13.0-1.el9 pgedge-pg_cron_18-1.6.8-1.el9 pgedge-pg-tokenizer_18-0.1.1-1.el9 -pgedge-pg-vectorize_18-0.23.0-1.el9 +pgedge-pg-vectorize_18-0.27.1-1.el9 pgedge-pg-stat-monitor_18-2.4.0-1.el9 pgedge-vchord-bm25_18-0.2.2-1.el9 pgedge-vectorizer_18-1.1-1.el9 diff --git a/packagelists/arm64/pg18.6-spock6.0.0-beta1-standard.txt b/packagelists/arm64/pg18.6-spock6.0.0-beta1-standard.txt index 06bc1e0..eab98a1 100644 --- a/packagelists/arm64/pg18.6-spock6.0.0-beta1-standard.txt +++ b/packagelists/arm64/pg18.6-spock6.0.0-beta1-standard.txt @@ -6,7 +6,7 @@ pgedge-python3-psycopg2-2.9.12-1.el9 pgedge-pgmq_18-1.13.0-1.el9 pgedge-pg_cron_18-1.6.8-1.el9 pgedge-pg-tokenizer_18-0.1.1-1.el9 -pgedge-pg-vectorize_18-0.23.0-1.el9 +pgedge-pg-vectorize_18-0.27.1-1.el9 pgedge-pg-stat-monitor_18-2.4.0-1.el9 pgedge-vchord-bm25_18-0.2.2-1.el9 pgedge-vectorizer_18-1.1-1.el9 diff --git a/scripts/build_pgedge_images.py b/scripts/build_pgedge_images.py index d18abfa..5228206 100755 --- a/scripts/build_pgedge_images.py +++ b/scripts/build_pgedge_images.py @@ -245,22 +245,22 @@ def make_all_flavor_images( all_images: list[PgEdgeImage] = [ # PostgreSQL-only base, one per major; no spock segment. PgEdgeImage( - postgres_version="16.15", spock_version="", epoch=3, flavor="postgres", + postgres_version="16.15", spock_version="", epoch=4, flavor="postgres", is_latest_for_pg_major=True, ), PgEdgeImage( - postgres_version="17.11", spock_version="", epoch=3, flavor="postgres", + postgres_version="17.11", spock_version="", epoch=4, flavor="postgres", is_latest_for_pg_major=True, ), PgEdgeImage( - postgres_version="18.6", spock_version="", epoch=3, flavor="postgres", + postgres_version="18.6", spock_version="", epoch=4, flavor="postgres", is_latest_for_pg_major=True, ), # pg16 images *make_all_flavor_images( postgres_version="16.15", spock_version="5.0.11", - epoch=3, + epoch=4, is_latest_for_pg_major=True, is_latest_for_spock_major=True, flavors=DEFAULT_FLAVORS + ["coldfront"], @@ -269,7 +269,7 @@ def make_all_flavor_images( *make_all_flavor_images( postgres_version="17.11", spock_version="5.0.11", - epoch=3, + epoch=4, is_latest_for_pg_major=True, is_latest_for_spock_major=True, flavors=DEFAULT_FLAVORS + ["coldfront"], @@ -278,7 +278,7 @@ def make_all_flavor_images( *make_all_flavor_images( postgres_version="18.6", spock_version="5.0.11", - epoch=3, + epoch=4, is_latest_for_pg_major=True, is_latest_for_spock_major=True, flavors=DEFAULT_FLAVORS + ["coldfront"], @@ -287,7 +287,7 @@ def make_all_flavor_images( *make_all_flavor_images( postgres_version="16.15", spock_version="6.0.0-beta1", - epoch=3, + epoch=4, is_latest_for_pg_major=True, is_latest_for_spock_major=True, ), @@ -295,7 +295,7 @@ def make_all_flavor_images( *make_all_flavor_images( postgres_version="17.11", spock_version="6.0.0-beta1", - epoch=3, + epoch=4, is_latest_for_pg_major=True, is_latest_for_spock_major=True, ), @@ -303,7 +303,7 @@ def make_all_flavor_images( *make_all_flavor_images( postgres_version="18.6", spock_version="6.0.0-beta1", - epoch=3, + epoch=4, is_latest_for_pg_major=True, is_latest_for_spock_major=True, ), diff --git a/tests/main.go b/tests/main.go index fbedcb6..a76bd53 100644 --- a/tests/main.go +++ b/tests/main.go @@ -556,7 +556,7 @@ func (r *TestRunner) Start() error { if includesMinimal(r.flavor) { cmd = append(cmd, "-c", "snowflake.node=1") } - if r.flavor == "standard" { + if includesStandard(r.flavor) { // pg_cron only ever installs into the one database this names, // and refuses CREATE EXTENSION anywhere else. cmd = append(cmd, "-c", "cron.database_name=testdb", "-c", "cron.use_background_workers=on") From f5c41242b781fee8bfdd003ab01886172e543650 Mon Sep 17 00:00:00 2001 From: Muhammad Aqeel Date: Mon, 21 Sep 2026 23:36:57 +0500 Subject: [PATCH 16/16] fix: count the coldfront entrypoint test in the summary --- tests/main.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/main.go b/tests/main.go index a76bd53..badcaf4 100644 --- a/tests/main.go +++ b/tests/main.go @@ -242,6 +242,9 @@ func printSummary(errorCount int, flavor, spockMajor string) { if includesStandard(flavor) { testsRun++ // patroni entrypoint } + if flavor == "coldfront" { + testsRun++ // coldfront entrypoint + } fmt.Println() fmt.Println("Test Summary")