From 9760a13047ecbad59c001949a26122c503aa21d9 Mon Sep 17 00:00:00 2001 From: Kemal Buyukkaya Date: Thu, 18 Jun 2026 14:09:12 +0000 Subject: [PATCH 01/14] Add signed PostgreSQL 16 core package build for Ubuntu focal PGDG dropped PostgreSQL 16 binaries for Ubuntu 20.04 (focal) after focal reached EOL standard support (last official focal build: 16.9-1.pgdg20.04+1). This adds a standalone, signed pipeline that rebuilds newer 16.x core packages for focal. Approach (validated locally; produces the full 13-package set that installs and runs on stock focal with working JIT): - Combine the newer upstream orig.tar.bz2 with the focal-era debian/ packaging (16.9-1.pgdg20.04+1), whose default toolchain (clang/llvm-dev = LLVM 10) yields focal-native dependencies (libicu66, libssl1.1, libldap-2.4-2, libllvm10) instead of the clang-19/llvm-19 required by newer packaging. - Restore the removed focal-pgdg build tooling (debhelper 13, dh-exec, postgresql-common-dev) from apt-archive.postgresql.org. - Drop the obsolete hurd-iovec patch (merged upstream as of 16.14) and gate on the full quilt series applying cleanly so future drift fails loudly. - Sign with the existing debsigner image (debsigs --sign=maint), using the pg-azure-storage signing secrets (PGAZ_PACKAGE_SECRET_KEY / PGAZ_PACKAGE_PASSPHRASE), matching build-pgazure-nightlies.yml. The minor version is parameterized: set PG_UPSTREAM_VERSION (workflow input pg_upstream_version) to build e.g. 16.15; the orig/debian checksums are auto-resolved from the official .dsc unless pinned. Files: - dockerfiles/pg16-focal-builder/Dockerfile: focal builder image - scripts/build_pg16_focal: fetch/verify/assemble/build entrypoint - .github/workflows/build-pg16-focal.yml: build -> sign -> verify -> upload --- .github/workflows/build-pg16-focal.yml | 105 ++++++++++++++ dockerfiles/pg16-focal-builder/Dockerfile | 62 +++++++++ scripts/build_pg16_focal | 158 ++++++++++++++++++++++ 3 files changed, 325 insertions(+) create mode 100644 .github/workflows/build-pg16-focal.yml create mode 100644 dockerfiles/pg16-focal-builder/Dockerfile create mode 100755 scripts/build_pg16_focal diff --git a/.github/workflows/build-pg16-focal.yml b/.github/workflows/build-pg16-focal.yml new file mode 100644 index 00000000..921e260e --- /dev/null +++ b/.github/workflows/build-pg16-focal.yml @@ -0,0 +1,105 @@ +name: Build PostgreSQL 16 (focal) + +# Builds PostgreSQL 16 *core* .deb packages for Ubuntu 20.04 (focal) by rebuilding +# the official Debian source package (PGDG dropped focal binaries upstream), then +# signs them with debsigs (--sign=maint) using the existing packaging key. +# +# This is intentionally a *standalone* pipeline: it does not use the extension +# build flow (citus_package / pg_buildext / the build-package.yml matrix), which +# assumes PostgreSQL itself comes from PGDG. + +on: + workflow_dispatch: + inputs: + pg_upstream_version: + description: "PostgreSQL 16 minor version to build (e.g. 16.14, 16.15)" + required: true + default: "16.14" + pg_orig_sha256: + description: "Optional: pin sha256 of postgresql-16_.orig.tar.bz2 (leave empty to auto-resolve from the official .dsc)" + required: false + default: "" + run_tests: + description: "Run upstream regression suite (1=yes, slower)" + required: false + default: "0" + push: + branches: + - pg16-focal + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-and-sign: + name: Build & sign PostgreSQL ${{ github.event.inputs.pg_upstream_version || '16.14' }} (focal) + runs-on: ubuntu-latest + env: + # Same signing key the pg-azure-storage pipeline uses + # (see .github/workflows/build-pgazure-nightlies.yml on all-pg-azure-storage). + PACKAGING_SECRET_KEY: ${{ secrets.PGAZ_PACKAGE_SECRET_KEY }} + PACKAGING_PASSPHRASE: ${{ secrets.PGAZ_PACKAGE_PASSPHRASE }} + PG_UPSTREAM_VERSION: ${{ github.event.inputs.pg_upstream_version || '16.14' }} + PG_ORIG_SHA256: ${{ github.event.inputs.pg_orig_sha256 || '' }} + RUN_TESTS: ${{ github.event.inputs.run_tests || '0' }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Build focal builder image + run: | + docker build -t pg16-focal-builder \ + -f dockerfiles/pg16-focal-builder/Dockerfile . + + - name: Build PostgreSQL 16 packages + run: | + mkdir -p packages + docker run --rm \ + -e PG_UPSTREAM_VERSION="${PG_UPSTREAM_VERSION}" \ + -e PG_ORIG_SHA256="${PG_ORIG_SHA256}" \ + -e RUN_TESTS="${RUN_TESTS}" \ + -v "${PWD}/packages:/packages" \ + pg16-focal-builder + echo "Built packages:" + ls -1 packages/focal/*.deb + + - name: Build debsigner image + run: | + docker build -t debsigner \ + -f dockerfiles/debsigner/Dockerfile dockerfiles/debsigner + + - name: Sign packages (debsigs --sign=maint) + run: | + if [ -z "${PACKAGING_SECRET_KEY}" ] || [ -z "${PACKAGING_PASSPHRASE}" ]; then + echo "::error::PACKAGING_SECRET_KEY / PACKAGING_PASSPHRASE secrets are not set" >&2 + exit 1 + fi + docker run --rm \ + -e PACKAGING_SECRET_KEY \ + -e PACKAGING_PASSPHRASE \ + -v "${PWD}/packages:/packages" \ + debsigner + + - name: Verify signatures are embedded + run: | + rc=0 + for deb in packages/focal/*.deb; do + if ar t "$deb" | grep -q '^_gpgmaint$'; then + echo "signed: $deb" + else + echo "::error::missing _gpgmaint signature in $deb" >&2 + rc=1 + fi + done + exit $rc + + - name: Upload signed packages + uses: actions/upload-artifact@v4 + with: + name: postgresql-16-focal-deb + path: | + packages/focal/*.deb + packages/focal/*.changes + packages/focal/*.buildinfo + if-no-files-found: error diff --git a/dockerfiles/pg16-focal-builder/Dockerfile b/dockerfiles/pg16-focal-builder/Dockerfile new file mode 100644 index 00000000..f14258d4 --- /dev/null +++ b/dockerfiles/pg16-focal-builder/Dockerfile @@ -0,0 +1,62 @@ +# vim:set ft=dockerfile: +# +# Builder image for PostgreSQL 16 *core* packages targeting Ubuntu 20.04 (focal). +# +# Why this exists: +# apt.postgresql.org (PGDG) no longer ships PostgreSQL 16 binaries for focal +# (focal reached EOL standard support 2025-04; the last official focal build +# was 16.9-1.pgdg20.04+1). To get a newer 16.x on focal we rebuild the official +# Debian source package ourselves. +# +# Strategy (validated): +# - Upstream tarball: postgresql-16_.orig.tar.bz2 (e.g. 16.14) +# - Debian packaging: the focal-era debian/ from 16.9-1.pgdg20.04+1, because its +# build profile uses focal's *default* toolchain (clang/llvm-dev = LLVM 10), +# so the resulting JIT depends on focal's libllvm10 (installable on focal), +# unlike newer packaging which requires clang-19/llvm-19. +# - Build tooling (debhelper 13, dh-exec, postgresql-common-dev) is restored from +# the PGDG *archive* (apt-archive.postgresql.org), which keeps the removed +# focal-pgdg suite. +# +# The heavy lifting lives in scripts/build_pg16_focal (the entrypoint). +FROM ubuntu:20.04 +ARG DEBIAN_FRONTEND=noninteractive + +# PGDG repository signing key fingerprint: +# B97B 0AFC AA1A 47F0 44F2 44A0 7FCC 7D46 ACCC 4CF8 +RUN set -ex; \ + apt-get update; \ + apt-get install -y --no-install-recommends ca-certificates curl gnupg; \ + install -d /usr/share/keyrings; \ + curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc \ + | gpg --dearmor -o /usr/share/keyrings/pgdg-archive.gpg; \ + echo "deb [signed-by=/usr/share/keyrings/pgdg-archive.gpg] https://apt-archive.postgresql.org/pub/repos/apt focal-pgdg main 16" \ + > /etc/apt/sources.list.d/pgdg-archive.list; \ + # make sure 'universe' is enabled (clang / llvm-dev live there on focal) + sed -i 's/^# deb \(.*universe\)/deb \1/' /etc/apt/sources.list; \ + apt-get update; \ + # base build tooling; the per-build Build-Depends are resolved at run time + # by scripts/build_pg16_focal via mk-build-deps against debian/control. + apt-get install -y --no-install-recommends \ + build-essential \ + devscripts \ + equivs \ + fakeroot \ + quilt \ + dpkg-dev \ + debhelper \ + dh-exec \ + postgresql-common-dev \ + xz-utils \ + bzip2; \ + rm -rf /var/lib/apt/lists/* + +# Fail the image build early if the archived focal-pgdg debhelper (>= 13) is not +# what we picked up (debhelper-compat (= 13) is required by the packaging). +RUN dpkg-query -W -f='${Package} ${Version}\n' debhelper postgresql-common-dev dh-exec + +COPY scripts/build_pg16_focal /usr/local/bin/build_pg16_focal +RUN chmod +x /usr/local/bin/build_pg16_focal + +VOLUME /packages +ENTRYPOINT ["/usr/local/bin/build_pg16_focal"] diff --git a/scripts/build_pg16_focal b/scripts/build_pg16_focal new file mode 100755 index 00000000..e7ff19ea --- /dev/null +++ b/scripts/build_pg16_focal @@ -0,0 +1,158 @@ +#!/bin/bash +# +# build_pg16_focal -- rebuild PostgreSQL 16 *core* Debian packages for Ubuntu 20.04 (focal). +# +# Approach (see dockerfiles/pg16-focal-builder/Dockerfile for the rationale): +# newer upstream orig.tar.bz2 + focal-era debian/ packaging (16.9-1.pgdg20.04+1) +# -> dpkg-buildpackage inside a focal environment that has the archived +# focal-pgdg build tooling available. +# +# Output: unsigned *.deb in ${OUTPUT_DIR} (default /packages/focal). Signing is a +# separate step performed by the debsigner image (debsigs --sign=maint), so that +# the build and the signing key never live in the same container. +# +# All inputs are overridable via environment variables. +# +# To build a different 16.x minor version (e.g. 16.15) you only need to set +# PG_UPSTREAM_VERSION=16.15 +# The orig-tarball checksum is resolved automatically from PostgreSQL's official +# .dsc unless you pin it explicitly via PG_ORIG_SHA256 (recommended for releases). + +set -euo pipefail + +# ---------------------------------------------------------------------------- +# Inputs (override via env) +# ---------------------------------------------------------------------------- +UPSTREAM="${PG_UPSTREAM_VERSION:-16.14}" # upstream PostgreSQL version to build +DEBIAN_BASE="${PG_DEBIAN_BASE:-16.9-1.pgdg20.04+1}" # focal-era packaging to reuse (kept constant across 16.x) +TARGET_VERSION="${PG_TARGET_VERSION:-${UPSTREAM}-1.pgdg20.04+1}" # produced package version + +# sha256 of the artifacts we download. Leave empty to auto-resolve from the +# official .dsc; set to pin a specific value (integrity / reproducibility). +ORIG_SHA256="${PG_ORIG_SHA256:-}" +DEBIAN_SHA256="${PG_DEBIAN_SHA256:-}" + +# Set RUN_TESTS=1 to run the upstream regression suite (slower, needs more deps). +RUN_TESTS="${RUN_TESTS:-0}" + +OUTPUT_DIR="${OUTPUT_DIR:-/packages/focal}" + +# Sources: orig tarball from the live pool (shared across distros), focal-era +# debian/ packaging from the archive (removed from the live pool with focal). +LIVE_POOL="https://apt.postgresql.org/pub/repos/apt/pool/main/p/postgresql-16" +ARCHIVE_POOL="https://apt-archive.postgresql.org/pub/repos/apt/pool/main/p/postgresql-16" + +ORIG_TARBALL="postgresql-16_${UPSTREAM}.orig.tar.bz2" +DEBIAN_TARBALL="postgresql-16_${DEBIAN_BASE}.debian.tar.xz" + +WORK="${WORK_DIR:-/build}" +SRCDIR="${WORK}/postgresql-16-${UPSTREAM}" + +# changelog identity (only cosmetic for a binary build) +export DEBEMAIL="${DEBEMAIL:-pgsql-pkg-debian@lists.postgresql.org}" +export DEBFULLNAME="${DEBFULLNAME:-PostgreSQL focal rebuild}" + +case "${UPSTREAM}" in + 16.*) : ;; + *) echo "ERROR: this pipeline builds PostgreSQL 16.x only (got '${UPSTREAM}')." >&2; exit 64 ;; +esac + +# dsc_sha256 -> prints the sha256 recorded for +# in the Checksums-Sha256 stanza of a Debian .dsc (served over HTTPS by PGDG). +dsc_sha256() { + curl -fsSL "$1" | awk -v f="$2" ' + /^Checksums-Sha256:/ { insec=1; next } + /^[A-Za-z]/ { insec=0 } + insec && $3==f { print $1; exit }' +} + +# Find any released .dsc for this upstream version (the orig tarball is shared +# across every per-distro revision, so any of them carries its checksum). +find_orig_dsc_url() { + local v="${UPSTREAM//./\\.}" + curl -fsSL "${LIVE_POOL}/" \ + | grep -oE "postgresql-16_${v}-[0-9]+\.pgdg[A-Za-z0-9.+~]*\.dsc" \ + | sort -u | tail -1 +} + +echo "==> Building PostgreSQL ${UPSTREAM} as ${TARGET_VERSION} (focal)" +mkdir -p "${WORK}" "${OUTPUT_DIR}" +cd "${WORK}" + +echo "==> [1/6] Download source artifacts" +curl -fSL "${LIVE_POOL}/${ORIG_TARBALL}" -o "${ORIG_TARBALL}" +curl -fSL "${ARCHIVE_POOL}/${DEBIAN_TARBALL}" -o "${DEBIAN_TARBALL}" + +echo "==> [2/6] Resolve & verify checksums" +if [ -z "${ORIG_SHA256}" ]; then + orig_dsc="$(find_orig_dsc_url || true)" + [ -n "${orig_dsc}" ] || { echo "ERROR: could not find a .dsc for ${ORIG_TARBALL} (is ${UPSTREAM} released?)." >&2; exit 1; } + ORIG_SHA256="$(dsc_sha256 "${LIVE_POOL}/${orig_dsc}" "${ORIG_TARBALL}")" + [ -n "${ORIG_SHA256}" ] || { echo "ERROR: ${ORIG_TARBALL} not listed in ${orig_dsc}." >&2; exit 1; } + echo " orig sha256 resolved from ${orig_dsc}: ${ORIG_SHA256}" +else + echo " orig sha256 pinned: ${ORIG_SHA256}" +fi +if [ -z "${DEBIAN_SHA256}" ]; then + DEBIAN_SHA256="$(dsc_sha256 "${ARCHIVE_POOL}/postgresql-16_${DEBIAN_BASE}.dsc" "${DEBIAN_TARBALL}")" + [ -n "${DEBIAN_SHA256}" ] || { echo "ERROR: ${DEBIAN_TARBALL} not listed in postgresql-16_${DEBIAN_BASE}.dsc." >&2; exit 1; } + echo " debian sha256 resolved from postgresql-16_${DEBIAN_BASE}.dsc: ${DEBIAN_SHA256}" +else + echo " debian sha256 pinned: ${DEBIAN_SHA256}" +fi +echo "${ORIG_SHA256} ${ORIG_TARBALL}" | sha256sum -c - +echo "${DEBIAN_SHA256} ${DEBIAN_TARBALL}" | sha256sum -c - + +echo "==> [3/6] Assemble 3.0 (quilt) source tree" +rm -rf "${SRCDIR}" +mkdir -p "${SRCDIR}" +tar -xf "${ORIG_TARBALL}" -C "${SRCDIR}" --strip-components=1 +tar -xf "${DEBIAN_TARBALL}" -C "${SRCDIR}" # unpacks debian/ +# NB: ${ORIG_TARBALL} is already named postgresql-16_${UPSTREAM}.orig.tar.bz2, +# exactly what dpkg-buildpackage expects one level above ${SRCDIR}. + +cd "${SRCDIR}" + +# 'hurd-iovec' (GNU/Hurd-only) was merged upstream as of 16.14: pg_iovec.h now +# guards IOV_MAX with '#ifndef IOV_MAX', so the 16.9-era patch no longer applies. +# Irrelevant to linux/amd64; drop it so the quilt series stays consistent. +if grep -qx 'hurd-iovec' debian/patches/series 2>/dev/null; then + sed -i '/^hurd-iovec$/d' debian/patches/series + rm -f debian/patches/hurd-iovec + echo " dropped obsolete patch: hurd-iovec" +fi + +echo "==> [4/6] Verify the quilt patch series applies cleanly to ${UPSTREAM}" +export QUILT_PATCHES=debian/patches +if [ -s debian/patches/series ]; then + quilt push -a + quilt pop -a +fi + +echo "==> [5/6] Set version to ${TARGET_VERSION} and install Build-Depends" +dch --newversion "${TARGET_VERSION}" --distribution focal --force-distribution \ + "Rebuild of PostgreSQL ${UPSTREAM} for focal (upstream PGDG focal-pgdg discontinued)." + +BUILD_PROFILES="" +BUILD_OPTIONS="parallel=$(nproc)" +if [ "${RUN_TESTS}" != "1" ]; then + BUILD_PROFILES="nocheck" + BUILD_OPTIONS="${BUILD_OPTIONS} nocheck" +fi +export DEB_BUILD_PROFILES="${BUILD_PROFILES}" + +apt-get update +mk-build-deps --install --remove \ + --tool 'apt-get -o Debug::pkgProblemResolver=yes --yes --no-install-recommends' \ + debian/control + +echo "==> [6/6] Build binary packages (DEB_BUILD_OPTIONS='${BUILD_OPTIONS}')" +export DEB_BUILD_OPTIONS="${BUILD_OPTIONS}" +dpkg-buildpackage -b -uc -us + +echo "==> Collect artifacts into ${OUTPUT_DIR}" +cp -v "${WORK}"/*.deb "${OUTPUT_DIR}/" +cp -v "${WORK}"/*.buildinfo "${WORK}"/*.changes "${OUTPUT_DIR}/" 2>/dev/null || true + +echo "==> DONE. Packages:" +ls -1 "${OUTPUT_DIR}"/*.deb From 461ac49478392453de6954a9f1206f548848d4c2 Mon Sep 17 00:00:00 2001 From: Kemal Buyukkaya Date: Thu, 18 Jun 2026 14:56:04 +0000 Subject: [PATCH 02/14] debsigner: accept ASCII-armored secret key, fail fast if none imported The packaging signing secrets in this repo (incl. PGAZ_PACKAGE_SECRET_KEY) are stored as raw ASCII-armored keys, not base64. import_and_sign assumed base64 and ran `base64 -d` first, which fails on armored input ("base64: invalid input" -> "no valid OpenPGP data found" -> "secret key not available"). Detect the format: import ASCII-armored keys directly, otherwise base64-decode as before (backward compatible). Also verify a PRIVATE key was actually imported and exit non-zero with an actionable message if only a public key is present, so debsigs never silently emits unsigned packages. Validated in the xenial debsigner image: armored private key -> signs (_gpgmaint added); base64 key -> signs; armored public-only key -> exits 78. --- dockerfiles/debsigner/scripts/import_and_sign | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/dockerfiles/debsigner/scripts/import_and_sign b/dockerfiles/debsigner/scripts/import_and_sign index 22dfb509..56761620 100755 --- a/dockerfiles/debsigner/scripts/import_and_sign +++ b/dockerfiles/debsigner/scripts/import_and_sign @@ -15,8 +15,29 @@ elif [ -z "${PACKAGING_SECRET_KEY+x}" ]; then exit $badconfig fi -gpg --batch --no-tty --trust-model always \ - --import <(echo "${PACKAGING_SECRET_KEY}" | base64 -d) +# PACKAGING_SECRET_KEY may be provided either as an ASCII-armored private key +# block (-----BEGIN PGP PRIVATE KEY BLOCK-----) or as base64-encoded key +# material (the historical format). Detect and import accordingly. +if printf '%s' "${PACKAGING_SECRET_KEY}" | grep -qa 'BEGIN PGP'; then + printf '%s\n' "${PACKAGING_SECRET_KEY}" \ + | gpg --batch --no-tty --trust-model always --import +else + printf '%s' "${PACKAGING_SECRET_KEY}" | base64 -d \ + | gpg --batch --no-tty --trust-model always --import +fi + +# Fail fast if no secret (private) key is available; otherwise debsigs would +# silently emit unsigned packages. +if ! gpg --list-secret-keys --with-colons 2>/dev/null | grep -q '^sec'; then + echo "$0: ERROR: no PRIVATE (secret) key was imported from PACKAGING_SECRET_KEY." >&2 + if gpg --list-keys --with-colons 2>/dev/null | grep -q '^pub'; then + echo "$0: a PUBLIC key was found instead:" >&2 + gpg --list-keys 2>/dev/null | sed 's/^/ /' >&2 + fi + echo "$0: PACKAGING_SECRET_KEY must contain the PRIVATE signing key (ASCII-armored," >&2 + echo "$0: or base64 of the armored key), e.g.: gpg --armor --export-secret-keys " >&2 + exit $badconfig +fi for deb in /packages/*/*.deb do From e71107b1a15192b9e4bc4d59ad463524330f3fb9 Mon Sep 17 00:00:00 2001 From: Kemal Buyukkaya Date: Thu, 18 Jun 2026 14:56:04 +0000 Subject: [PATCH 03/14] ci: don't run Citus extension build/test on the pg16-focal branch build-package.yml and build-package-test.yml trigger on every branch (branches: "**") and run the Citus extension build plus test_build_packages, which is unrelated to the PostgreSQL-core focal pipeline and fails here on a pre-existing PACKAGING_PASSPHRASE mismatch. Exclude pg16-focal via branches-ignore so it no longer blocks this work; workflow_dispatch stays. --- .github/workflows/build-package-test.yml | 5 ++++- .github/workflows/build-package.yml | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-package-test.yml b/.github/workflows/build-package-test.yml index dd8393b8..f9b109c6 100644 --- a/.github/workflows/build-package-test.yml +++ b/.github/workflows/build-package-test.yml @@ -13,7 +13,10 @@ env: TEST: true on: push: - branches: "**" + branches-ignore: + # pg16-focal builds PostgreSQL core (see build-pg16-focal.yml); the Citus + # extension build/test here is unrelated and should not run on that branch. + - pg16-focal workflow_dispatch: concurrency: diff --git a/.github/workflows/build-package.yml b/.github/workflows/build-package.yml index d4c67b2c..2ee324ae 100644 --- a/.github/workflows/build-package.yml +++ b/.github/workflows/build-package.yml @@ -13,7 +13,10 @@ env: TEST: false on: push: - branches: "**" + branches-ignore: + # pg16-focal builds PostgreSQL core (see build-pg16-focal.yml); the Citus + # extension build/test here is unrelated and should not run on that branch. + - pg16-focal workflow_dispatch: concurrency: From 4f51e0ba75e686413bc7c6e137626559c267940a Mon Sep 17 00:00:00 2001 From: Kemal Buyukkaya Date: Thu, 18 Jun 2026 17:11:21 +0000 Subject: [PATCH 04/14] Revert debsigner script and extension-workflow changes Roll back the earlier workarounds now that signing uses the prebuilt citusdata/packaging:debsigner image: - dockerfiles/debsigner/scripts/import_and_sign: back to upstream (we no longer build our own signer, so the armored-key handling is unnecessary). - build-package.yml / build-package-test.yml: restore branches: "**" (drop the pg16-focal branches-ignore guard) to avoid touching shared extension CI. These three files now match develop; only the PostgreSQL-core focal pipeline remains in this branch. --- .github/workflows/build-package-test.yml | 5 +--- .github/workflows/build-package.yml | 5 +--- dockerfiles/debsigner/scripts/import_and_sign | 25 ++----------------- 3 files changed, 4 insertions(+), 31 deletions(-) diff --git a/.github/workflows/build-package-test.yml b/.github/workflows/build-package-test.yml index f9b109c6..dd8393b8 100644 --- a/.github/workflows/build-package-test.yml +++ b/.github/workflows/build-package-test.yml @@ -13,10 +13,7 @@ env: TEST: true on: push: - branches-ignore: - # pg16-focal builds PostgreSQL core (see build-pg16-focal.yml); the Citus - # extension build/test here is unrelated and should not run on that branch. - - pg16-focal + branches: "**" workflow_dispatch: concurrency: diff --git a/.github/workflows/build-package.yml b/.github/workflows/build-package.yml index 2ee324ae..d4c67b2c 100644 --- a/.github/workflows/build-package.yml +++ b/.github/workflows/build-package.yml @@ -13,10 +13,7 @@ env: TEST: false on: push: - branches-ignore: - # pg16-focal builds PostgreSQL core (see build-pg16-focal.yml); the Citus - # extension build/test here is unrelated and should not run on that branch. - - pg16-focal + branches: "**" workflow_dispatch: concurrency: diff --git a/dockerfiles/debsigner/scripts/import_and_sign b/dockerfiles/debsigner/scripts/import_and_sign index 56761620..22dfb509 100755 --- a/dockerfiles/debsigner/scripts/import_and_sign +++ b/dockerfiles/debsigner/scripts/import_and_sign @@ -15,29 +15,8 @@ elif [ -z "${PACKAGING_SECRET_KEY+x}" ]; then exit $badconfig fi -# PACKAGING_SECRET_KEY may be provided either as an ASCII-armored private key -# block (-----BEGIN PGP PRIVATE KEY BLOCK-----) or as base64-encoded key -# material (the historical format). Detect and import accordingly. -if printf '%s' "${PACKAGING_SECRET_KEY}" | grep -qa 'BEGIN PGP'; then - printf '%s\n' "${PACKAGING_SECRET_KEY}" \ - | gpg --batch --no-tty --trust-model always --import -else - printf '%s' "${PACKAGING_SECRET_KEY}" | base64 -d \ - | gpg --batch --no-tty --trust-model always --import -fi - -# Fail fast if no secret (private) key is available; otherwise debsigs would -# silently emit unsigned packages. -if ! gpg --list-secret-keys --with-colons 2>/dev/null | grep -q '^sec'; then - echo "$0: ERROR: no PRIVATE (secret) key was imported from PACKAGING_SECRET_KEY." >&2 - if gpg --list-keys --with-colons 2>/dev/null | grep -q '^pub'; then - echo "$0: a PUBLIC key was found instead:" >&2 - gpg --list-keys 2>/dev/null | sed 's/^/ /' >&2 - fi - echo "$0: PACKAGING_SECRET_KEY must contain the PRIVATE signing key (ASCII-armored," >&2 - echo "$0: or base64 of the armored key), e.g.: gpg --armor --export-secret-keys " >&2 - exit $badconfig -fi +gpg --batch --no-tty --trust-model always \ + --import <(echo "${PACKAGING_SECRET_KEY}" | base64 -d) for deb in /packages/*/*.deb do From 7573c8864916961cf4c4687f2ed9c7cfb12ab594 Mon Sep 17 00:00:00 2001 From: Kemal Buyukkaya Date: Thu, 18 Jun 2026 17:11:21 +0000 Subject: [PATCH 05/14] build-pg16-focal: sign with prebuilt debsigner image, pin ubuntu-20.04 The signer images are maintained out-of-band (not built by this repo's image pipeline), so the deployed citusdata/packaging:debsigner has drifted from dockerfiles/debsigner. Building our own signer from that source could not import the same signing key that signs every other Citus package, while the deployed image does (pg-azure-storage nightlies are green with PGAZ_PACKAGE_SECRET_KEY). Use the prebuilt citusdata/packaging:debsigner with a Docker Hub login and pipe the passphrase via stdin + env, mirroring citus_package.sign_packages. Pin the job to ubuntu-20.04 to match the green pg-azure-storage signing pipeline. --- .github/workflows/build-pg16-focal.yml | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build-pg16-focal.yml b/.github/workflows/build-pg16-focal.yml index 921e260e..3ef23f32 100644 --- a/.github/workflows/build-pg16-focal.yml +++ b/.github/workflows/build-pg16-focal.yml @@ -34,7 +34,8 @@ concurrency: jobs: build-and-sign: name: Build & sign PostgreSQL ${{ github.event.inputs.pg_upstream_version || '16.14' }} (focal) - runs-on: ubuntu-latest + # Pinned to focal to match the (green) pg-azure-storage signing pipeline. + runs-on: ubuntu-20.04 env: # Same signing key the pg-azure-storage pipeline uses # (see .github/workflows/build-pgazure-nightlies.yml on all-pg-azure-storage). @@ -47,6 +48,12 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 + - name: Login to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USER_NAME }} + password: ${{ secrets.DOCKERHUB_PASSWORD }} + - name: Build focal builder image run: | docker build -t pg16-focal-builder \ @@ -64,22 +71,20 @@ jobs: echo "Built packages:" ls -1 packages/focal/*.deb - - name: Build debsigner image - run: | - docker build -t debsigner \ - -f dockerfiles/debsigner/Dockerfile dockerfiles/debsigner - - name: Sign packages (debsigs --sign=maint) + # Use the prebuilt, deployed debsigner image (the one all Citus signing + # uses), not a locally built copy of dockerfiles/debsigner, which has + # drifted from it. Mirrors tools.packaging_automation.citus_package. run: | if [ -z "${PACKAGING_SECRET_KEY}" ] || [ -z "${PACKAGING_PASSPHRASE}" ]; then echo "::error::PACKAGING_SECRET_KEY / PACKAGING_PASSPHRASE secrets are not set" >&2 exit 1 fi - docker run --rm \ + printf '%s' "${PACKAGING_PASSPHRASE}" | docker run --rm -i \ -e PACKAGING_SECRET_KEY \ -e PACKAGING_PASSPHRASE \ -v "${PWD}/packages:/packages" \ - debsigner + citusdata/packaging:debsigner - name: Verify signatures are embedded run: | From e2e96f6d445420afdd4c1e33863bad44b6cec69a Mon Sep 17 00:00:00 2001 From: Kemal Buyukkaya Date: Thu, 18 Jun 2026 17:12:22 +0000 Subject: [PATCH 06/14] build-pg16-focal: run on ubuntu-latest Revert the ubuntu-20.04 pin to ubuntu-latest to stay future-proof as GitHub retires the hosted ubuntu-20.04 image. Signing uses the prebuilt citusdata/packaging:debsigner image (same as all-citus, which signs fine on ubuntu-latest), so the runner version is not the relevant factor. --- .github/workflows/build-pg16-focal.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/build-pg16-focal.yml b/.github/workflows/build-pg16-focal.yml index 3ef23f32..0ba69c8c 100644 --- a/.github/workflows/build-pg16-focal.yml +++ b/.github/workflows/build-pg16-focal.yml @@ -34,8 +34,7 @@ concurrency: jobs: build-and-sign: name: Build & sign PostgreSQL ${{ github.event.inputs.pg_upstream_version || '16.14' }} (focal) - # Pinned to focal to match the (green) pg-azure-storage signing pipeline. - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest env: # Same signing key the pg-azure-storage pipeline uses # (see .github/workflows/build-pgazure-nightlies.yml on all-pg-azure-storage). From 4a773db098fa47dbd3989693c2fc97dd3606e9fd Mon Sep 17 00:00:00 2001 From: Kemal Buyukkaya Date: Thu, 18 Jun 2026 18:02:23 +0000 Subject: [PATCH 07/14] build-pg16-focal: sign with the common PACKAGING_SECRET_KEY/PASSPHRASE PGAZ_PACKAGE_SECRET_KEY is only referenced on the pg-azure-storage branches and did not import in our run. Switch to the common signing secrets used across the other pipelines (PACKAGING_SECRET_KEY / PACKAGING_PASSPHRASE) to match the standard convention. --- .github/workflows/build-pg16-focal.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-pg16-focal.yml b/.github/workflows/build-pg16-focal.yml index 0ba69c8c..84eadb4e 100644 --- a/.github/workflows/build-pg16-focal.yml +++ b/.github/workflows/build-pg16-focal.yml @@ -36,10 +36,8 @@ jobs: name: Build & sign PostgreSQL ${{ github.event.inputs.pg_upstream_version || '16.14' }} (focal) runs-on: ubuntu-latest env: - # Same signing key the pg-azure-storage pipeline uses - # (see .github/workflows/build-pgazure-nightlies.yml on all-pg-azure-storage). - PACKAGING_SECRET_KEY: ${{ secrets.PGAZ_PACKAGE_SECRET_KEY }} - PACKAGING_PASSPHRASE: ${{ secrets.PGAZ_PACKAGE_PASSPHRASE }} + PACKAGING_SECRET_KEY: ${{ secrets.PACKAGING_SECRET_KEY }} + PACKAGING_PASSPHRASE: ${{ secrets.PACKAGING_PASSPHRASE }} PG_UPSTREAM_VERSION: ${{ github.event.inputs.pg_upstream_version || '16.14' }} PG_ORIG_SHA256: ${{ github.event.inputs.pg_orig_sha256 || '' }} RUN_TESTS: ${{ github.event.inputs.run_tests || '0' }} From d40afe8bb466b6de7973ccf36c0b2e21da820a98 Mon Sep 17 00:00:00 2001 From: Kemal Buyukkaya Date: Fri, 26 Jun 2026 21:36:27 +0000 Subject: [PATCH 08/14] Generalize focal PG core build to a parameterized PG13-17 pipeline Replace the PG16-only focal pipeline with a single parameterized one keyed on PG_MAJOR, covering every focal-buildable major (PG 13, 14, 15, 16, 17). PGDG dropped focal binaries once focal reached EOL; these are the majors that still receive new upstream minors beyond their last focal build. - scripts/build_pg16_focal -> scripts/build_pg_focal: add PG_MAJOR; auto- resolve the latest minor per major from the live pool (PG_UPSTREAM_VERSION override stays); static frozen DEBIAN_BASE map per major (focal is EOL, so these never change) with a PG_DEBIAN_BASE override; replace the hardcoded hurd-iovec drop with a declarative PG_DROP_PATCHES list (default hurd-iovec, a no-op where absent) so future drift is a one-line, no-code change. The quilt-series gate stays as the fail-loud safety net. - dockerfiles/pg16-focal-builder -> dockerfiles/focal-pg-builder: one generic builder image (focal-pgdg main 13 14 15 16 17); built once, reused per major. - build-pg16-focal.yml -> build-pg-focal.yml: a setup job computes the matrix (single major via workflow_dispatch, or all 13-17), build-and-sign matrixes over majors with fail-fast: false; sign/verify/upload are unchanged but namespaced per major. Trigger on the pg-focal branch. Validated end-to-end for all five majors (full 13-package set each, depending on focal-native libllvm10/libssl1.1, hurd-iovec dropped only on 16/17): 13.23, 14.23, 15.18, 16.14, 17.10. --- ...uild-pg16-focal.yml => build-pg-focal.yml} | 80 +++++-- .../Dockerfile | 43 ++-- scripts/build_pg16_focal | 158 ------------- scripts/build_pg_focal | 219 ++++++++++++++++++ 4 files changed, 303 insertions(+), 197 deletions(-) rename .github/workflows/{build-pg16-focal.yml => build-pg-focal.yml} (52%) rename dockerfiles/{pg16-focal-builder => focal-pg-builder}/Dockerfile (50%) delete mode 100755 scripts/build_pg16_focal create mode 100755 scripts/build_pg_focal diff --git a/.github/workflows/build-pg16-focal.yml b/.github/workflows/build-pg-focal.yml similarity index 52% rename from .github/workflows/build-pg16-focal.yml rename to .github/workflows/build-pg-focal.yml index 84eadb4e..e434c7ac 100644 --- a/.github/workflows/build-pg16-focal.yml +++ b/.github/workflows/build-pg-focal.yml @@ -1,9 +1,13 @@ -name: Build PostgreSQL 16 (focal) +name: Build PostgreSQL core (focal) -# Builds PostgreSQL 16 *core* .deb packages for Ubuntu 20.04 (focal) by rebuilding +# Builds PostgreSQL *core* .deb packages for Ubuntu 20.04 (focal) by rebuilding # the official Debian source package (PGDG dropped focal binaries upstream), then # signs them with debsigs (--sign=maint) using the existing packaging key. # +# One parameterized pipeline covers every focal-buildable major (PG 13..17): a +# matrix builds them all by default; workflow_dispatch can target a single major +# and pin its minor / orig sha256. +# # This is intentionally a *standalone* pipeline: it does not use the extension # build flow (citus_package / pg_buildext / the build-package.yml matrix), which # assumes PostgreSQL itself comes from PGDG. @@ -11,12 +15,24 @@ name: Build PostgreSQL 16 (focal) on: workflow_dispatch: inputs: - pg_upstream_version: - description: "PostgreSQL 16 minor version to build (e.g. 16.14, 16.15)" + pg_major: + description: "PostgreSQL major to build" required: true - default: "16.14" + type: choice + default: "all" + options: + - "all" + - "13" + - "14" + - "15" + - "16" + - "17" + pg_upstream_version: + description: "Optional: pin the minor (e.g. 16.15). Blank = latest. Only used when a single major is selected." + required: false + default: "" pg_orig_sha256: - description: "Optional: pin sha256 of postgresql-16_.orig.tar.bz2 (leave empty to auto-resolve from the official .dsc)" + description: "Optional: pin sha256 of the orig.tar.bz2 (blank = auto-resolve from the official .dsc). Only used when a single major is selected." required: false default: "" run_tests: @@ -25,21 +41,44 @@ on: default: "0" push: branches: - - pg16-focal + - pg-focal concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: + setup: + name: Resolve build matrix + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.matrix.outputs.matrix }} + steps: + - name: Compute pg_major matrix + id: matrix + run: | + sel="${{ github.event.inputs.pg_major }}" + if [ -z "${sel}" ] || [ "${sel}" = "all" ]; then + echo 'matrix={"pg_major":["13","14","15","16","17"]}' >> "$GITHUB_OUTPUT" + else + echo "matrix={\"pg_major\":[\"${sel}\"]}" >> "$GITHUB_OUTPUT" + fi + build-and-sign: - name: Build & sign PostgreSQL ${{ github.event.inputs.pg_upstream_version || '16.14' }} (focal) + needs: setup + name: Build & sign PG${{ matrix.pg_major }} (focal) runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.setup.outputs.matrix) }} env: PACKAGING_SECRET_KEY: ${{ secrets.PACKAGING_SECRET_KEY }} PACKAGING_PASSPHRASE: ${{ secrets.PACKAGING_PASSPHRASE }} - PG_UPSTREAM_VERSION: ${{ github.event.inputs.pg_upstream_version || '16.14' }} - PG_ORIG_SHA256: ${{ github.event.inputs.pg_orig_sha256 || '' }} + PG_MAJOR: ${{ matrix.pg_major }} + # Minor / sha pins only make sense for a single explicitly-selected major; + # ignore them on the "all" matrix so each major still auto-resolves latest. + PG_UPSTREAM_VERSION: ${{ github.event.inputs.pg_major != 'all' && github.event.inputs.pg_upstream_version || '' }} + PG_ORIG_SHA256: ${{ github.event.inputs.pg_major != 'all' && github.event.inputs.pg_orig_sha256 || '' }} RUN_TESTS: ${{ github.event.inputs.run_tests || '0' }} steps: - name: Checkout repository @@ -53,20 +92,21 @@ jobs: - name: Build focal builder image run: | - docker build -t pg16-focal-builder \ - -f dockerfiles/pg16-focal-builder/Dockerfile . + docker build -t focal-pg-builder \ + -f dockerfiles/focal-pg-builder/Dockerfile . - - name: Build PostgreSQL 16 packages + - name: Build PostgreSQL ${{ matrix.pg_major }} packages run: | mkdir -p packages docker run --rm \ + -e PG_MAJOR="${PG_MAJOR}" \ -e PG_UPSTREAM_VERSION="${PG_UPSTREAM_VERSION}" \ -e PG_ORIG_SHA256="${PG_ORIG_SHA256}" \ -e RUN_TESTS="${RUN_TESTS}" \ -v "${PWD}/packages:/packages" \ - pg16-focal-builder + focal-pg-builder echo "Built packages:" - ls -1 packages/focal/*.deb + ls -1 "packages/focal/pg${PG_MAJOR}"/*.deb - name: Sign packages (debsigs --sign=maint) # Use the prebuilt, deployed debsigner image (the one all Citus signing @@ -86,7 +126,7 @@ jobs: - name: Verify signatures are embedded run: | rc=0 - for deb in packages/focal/*.deb; do + for deb in "packages/focal/pg${PG_MAJOR}"/*.deb; do if ar t "$deb" | grep -q '^_gpgmaint$'; then echo "signed: $deb" else @@ -99,9 +139,9 @@ jobs: - name: Upload signed packages uses: actions/upload-artifact@v4 with: - name: postgresql-16-focal-deb + name: postgresql-${{ matrix.pg_major }}-focal-deb path: | - packages/focal/*.deb - packages/focal/*.changes - packages/focal/*.buildinfo + packages/focal/pg${{ matrix.pg_major }}/*.deb + packages/focal/pg${{ matrix.pg_major }}/*.changes + packages/focal/pg${{ matrix.pg_major }}/*.buildinfo if-no-files-found: error diff --git a/dockerfiles/pg16-focal-builder/Dockerfile b/dockerfiles/focal-pg-builder/Dockerfile similarity index 50% rename from dockerfiles/pg16-focal-builder/Dockerfile rename to dockerfiles/focal-pg-builder/Dockerfile index f14258d4..1008474e 100644 --- a/dockerfiles/pg16-focal-builder/Dockerfile +++ b/dockerfiles/focal-pg-builder/Dockerfile @@ -1,24 +1,26 @@ # vim:set ft=dockerfile: # -# Builder image for PostgreSQL 16 *core* packages targeting Ubuntu 20.04 (focal). +# Generic builder image for PostgreSQL *core* packages targeting Ubuntu 20.04 +# (focal). One image serves every focal-buildable major (PG 13, 14, 15, 16, 17); +# the major is selected at run time via PG_MAJOR (see scripts/build_pg_focal). # # Why this exists: -# apt.postgresql.org (PGDG) no longer ships PostgreSQL 16 binaries for focal -# (focal reached EOL standard support 2025-04; the last official focal build -# was 16.9-1.pgdg20.04+1). To get a newer 16.x on focal we rebuild the official -# Debian source package ourselves. +# apt.postgresql.org (PGDG) no longer ships PostgreSQL binaries for focal +# (focal reached EOL standard support 2025-04). To get a newer minor on focal +# we rebuild the official Debian source package ourselves. # # Strategy (validated): -# - Upstream tarball: postgresql-16_.orig.tar.bz2 (e.g. 16.14) -# - Debian packaging: the focal-era debian/ from 16.9-1.pgdg20.04+1, because its -# build profile uses focal's *default* toolchain (clang/llvm-dev = LLVM 10), -# so the resulting JIT depends on focal's libllvm10 (installable on focal), -# unlike newer packaging which requires clang-19/llvm-19. -# - Build tooling (debhelper 13, dh-exec, postgresql-common-dev) is restored from -# the PGDG *archive* (apt-archive.postgresql.org), which keeps the removed -# focal-pgdg suite. +# - Upstream tarball: postgresql-_.orig.tar.bz2 (from the live pool) +# - Debian packaging: the frozen focal-era debian/ (the last +# ".-N.pgdg20.04+1" PGDG built for focal), because its build profile +# uses focal's *default* toolchain (clang/llvm-dev = LLVM 10), so the +# resulting JIT depends on focal's libllvm10 (installable on focal), unlike +# newer packaging which requires clang-19/llvm-19. +# - Build tooling (debhelper 13, dh-exec, postgresql-common-dev) is restored +# from the PGDG *archive* (apt-archive.postgresql.org), which keeps the +# removed focal-pgdg suite. # -# The heavy lifting lives in scripts/build_pg16_focal (the entrypoint). +# The heavy lifting lives in scripts/build_pg_focal (the entrypoint). FROM ubuntu:20.04 ARG DEBIAN_FRONTEND=noninteractive @@ -30,13 +32,16 @@ RUN set -ex; \ install -d /usr/share/keyrings; \ curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc \ | gpg --dearmor -o /usr/share/keyrings/pgdg-archive.gpg; \ - echo "deb [signed-by=/usr/share/keyrings/pgdg-archive.gpg] https://apt-archive.postgresql.org/pub/repos/apt focal-pgdg main 16" \ + # The focal-pgdg suite exposes a 'main' component (build tooling) plus a + # per-major component for each PostgreSQL major. List every focal-buildable + # major so the same image works for PG 13..17. + echo "deb [signed-by=/usr/share/keyrings/pgdg-archive.gpg] https://apt-archive.postgresql.org/pub/repos/apt focal-pgdg main 13 14 15 16 17" \ > /etc/apt/sources.list.d/pgdg-archive.list; \ # make sure 'universe' is enabled (clang / llvm-dev live there on focal) sed -i 's/^# deb \(.*universe\)/deb \1/' /etc/apt/sources.list; \ apt-get update; \ # base build tooling; the per-build Build-Depends are resolved at run time - # by scripts/build_pg16_focal via mk-build-deps against debian/control. + # by scripts/build_pg_focal via mk-build-deps against debian/control. apt-get install -y --no-install-recommends \ build-essential \ devscripts \ @@ -55,8 +60,8 @@ RUN set -ex; \ # what we picked up (debhelper-compat (= 13) is required by the packaging). RUN dpkg-query -W -f='${Package} ${Version}\n' debhelper postgresql-common-dev dh-exec -COPY scripts/build_pg16_focal /usr/local/bin/build_pg16_focal -RUN chmod +x /usr/local/bin/build_pg16_focal +COPY scripts/build_pg_focal /usr/local/bin/build_pg_focal +RUN chmod +x /usr/local/bin/build_pg_focal VOLUME /packages -ENTRYPOINT ["/usr/local/bin/build_pg16_focal"] +ENTRYPOINT ["/usr/local/bin/build_pg_focal"] diff --git a/scripts/build_pg16_focal b/scripts/build_pg16_focal deleted file mode 100755 index e7ff19ea..00000000 --- a/scripts/build_pg16_focal +++ /dev/null @@ -1,158 +0,0 @@ -#!/bin/bash -# -# build_pg16_focal -- rebuild PostgreSQL 16 *core* Debian packages for Ubuntu 20.04 (focal). -# -# Approach (see dockerfiles/pg16-focal-builder/Dockerfile for the rationale): -# newer upstream orig.tar.bz2 + focal-era debian/ packaging (16.9-1.pgdg20.04+1) -# -> dpkg-buildpackage inside a focal environment that has the archived -# focal-pgdg build tooling available. -# -# Output: unsigned *.deb in ${OUTPUT_DIR} (default /packages/focal). Signing is a -# separate step performed by the debsigner image (debsigs --sign=maint), so that -# the build and the signing key never live in the same container. -# -# All inputs are overridable via environment variables. -# -# To build a different 16.x minor version (e.g. 16.15) you only need to set -# PG_UPSTREAM_VERSION=16.15 -# The orig-tarball checksum is resolved automatically from PostgreSQL's official -# .dsc unless you pin it explicitly via PG_ORIG_SHA256 (recommended for releases). - -set -euo pipefail - -# ---------------------------------------------------------------------------- -# Inputs (override via env) -# ---------------------------------------------------------------------------- -UPSTREAM="${PG_UPSTREAM_VERSION:-16.14}" # upstream PostgreSQL version to build -DEBIAN_BASE="${PG_DEBIAN_BASE:-16.9-1.pgdg20.04+1}" # focal-era packaging to reuse (kept constant across 16.x) -TARGET_VERSION="${PG_TARGET_VERSION:-${UPSTREAM}-1.pgdg20.04+1}" # produced package version - -# sha256 of the artifacts we download. Leave empty to auto-resolve from the -# official .dsc; set to pin a specific value (integrity / reproducibility). -ORIG_SHA256="${PG_ORIG_SHA256:-}" -DEBIAN_SHA256="${PG_DEBIAN_SHA256:-}" - -# Set RUN_TESTS=1 to run the upstream regression suite (slower, needs more deps). -RUN_TESTS="${RUN_TESTS:-0}" - -OUTPUT_DIR="${OUTPUT_DIR:-/packages/focal}" - -# Sources: orig tarball from the live pool (shared across distros), focal-era -# debian/ packaging from the archive (removed from the live pool with focal). -LIVE_POOL="https://apt.postgresql.org/pub/repos/apt/pool/main/p/postgresql-16" -ARCHIVE_POOL="https://apt-archive.postgresql.org/pub/repos/apt/pool/main/p/postgresql-16" - -ORIG_TARBALL="postgresql-16_${UPSTREAM}.orig.tar.bz2" -DEBIAN_TARBALL="postgresql-16_${DEBIAN_BASE}.debian.tar.xz" - -WORK="${WORK_DIR:-/build}" -SRCDIR="${WORK}/postgresql-16-${UPSTREAM}" - -# changelog identity (only cosmetic for a binary build) -export DEBEMAIL="${DEBEMAIL:-pgsql-pkg-debian@lists.postgresql.org}" -export DEBFULLNAME="${DEBFULLNAME:-PostgreSQL focal rebuild}" - -case "${UPSTREAM}" in - 16.*) : ;; - *) echo "ERROR: this pipeline builds PostgreSQL 16.x only (got '${UPSTREAM}')." >&2; exit 64 ;; -esac - -# dsc_sha256 -> prints the sha256 recorded for -# in the Checksums-Sha256 stanza of a Debian .dsc (served over HTTPS by PGDG). -dsc_sha256() { - curl -fsSL "$1" | awk -v f="$2" ' - /^Checksums-Sha256:/ { insec=1; next } - /^[A-Za-z]/ { insec=0 } - insec && $3==f { print $1; exit }' -} - -# Find any released .dsc for this upstream version (the orig tarball is shared -# across every per-distro revision, so any of them carries its checksum). -find_orig_dsc_url() { - local v="${UPSTREAM//./\\.}" - curl -fsSL "${LIVE_POOL}/" \ - | grep -oE "postgresql-16_${v}-[0-9]+\.pgdg[A-Za-z0-9.+~]*\.dsc" \ - | sort -u | tail -1 -} - -echo "==> Building PostgreSQL ${UPSTREAM} as ${TARGET_VERSION} (focal)" -mkdir -p "${WORK}" "${OUTPUT_DIR}" -cd "${WORK}" - -echo "==> [1/6] Download source artifacts" -curl -fSL "${LIVE_POOL}/${ORIG_TARBALL}" -o "${ORIG_TARBALL}" -curl -fSL "${ARCHIVE_POOL}/${DEBIAN_TARBALL}" -o "${DEBIAN_TARBALL}" - -echo "==> [2/6] Resolve & verify checksums" -if [ -z "${ORIG_SHA256}" ]; then - orig_dsc="$(find_orig_dsc_url || true)" - [ -n "${orig_dsc}" ] || { echo "ERROR: could not find a .dsc for ${ORIG_TARBALL} (is ${UPSTREAM} released?)." >&2; exit 1; } - ORIG_SHA256="$(dsc_sha256 "${LIVE_POOL}/${orig_dsc}" "${ORIG_TARBALL}")" - [ -n "${ORIG_SHA256}" ] || { echo "ERROR: ${ORIG_TARBALL} not listed in ${orig_dsc}." >&2; exit 1; } - echo " orig sha256 resolved from ${orig_dsc}: ${ORIG_SHA256}" -else - echo " orig sha256 pinned: ${ORIG_SHA256}" -fi -if [ -z "${DEBIAN_SHA256}" ]; then - DEBIAN_SHA256="$(dsc_sha256 "${ARCHIVE_POOL}/postgresql-16_${DEBIAN_BASE}.dsc" "${DEBIAN_TARBALL}")" - [ -n "${DEBIAN_SHA256}" ] || { echo "ERROR: ${DEBIAN_TARBALL} not listed in postgresql-16_${DEBIAN_BASE}.dsc." >&2; exit 1; } - echo " debian sha256 resolved from postgresql-16_${DEBIAN_BASE}.dsc: ${DEBIAN_SHA256}" -else - echo " debian sha256 pinned: ${DEBIAN_SHA256}" -fi -echo "${ORIG_SHA256} ${ORIG_TARBALL}" | sha256sum -c - -echo "${DEBIAN_SHA256} ${DEBIAN_TARBALL}" | sha256sum -c - - -echo "==> [3/6] Assemble 3.0 (quilt) source tree" -rm -rf "${SRCDIR}" -mkdir -p "${SRCDIR}" -tar -xf "${ORIG_TARBALL}" -C "${SRCDIR}" --strip-components=1 -tar -xf "${DEBIAN_TARBALL}" -C "${SRCDIR}" # unpacks debian/ -# NB: ${ORIG_TARBALL} is already named postgresql-16_${UPSTREAM}.orig.tar.bz2, -# exactly what dpkg-buildpackage expects one level above ${SRCDIR}. - -cd "${SRCDIR}" - -# 'hurd-iovec' (GNU/Hurd-only) was merged upstream as of 16.14: pg_iovec.h now -# guards IOV_MAX with '#ifndef IOV_MAX', so the 16.9-era patch no longer applies. -# Irrelevant to linux/amd64; drop it so the quilt series stays consistent. -if grep -qx 'hurd-iovec' debian/patches/series 2>/dev/null; then - sed -i '/^hurd-iovec$/d' debian/patches/series - rm -f debian/patches/hurd-iovec - echo " dropped obsolete patch: hurd-iovec" -fi - -echo "==> [4/6] Verify the quilt patch series applies cleanly to ${UPSTREAM}" -export QUILT_PATCHES=debian/patches -if [ -s debian/patches/series ]; then - quilt push -a - quilt pop -a -fi - -echo "==> [5/6] Set version to ${TARGET_VERSION} and install Build-Depends" -dch --newversion "${TARGET_VERSION}" --distribution focal --force-distribution \ - "Rebuild of PostgreSQL ${UPSTREAM} for focal (upstream PGDG focal-pgdg discontinued)." - -BUILD_PROFILES="" -BUILD_OPTIONS="parallel=$(nproc)" -if [ "${RUN_TESTS}" != "1" ]; then - BUILD_PROFILES="nocheck" - BUILD_OPTIONS="${BUILD_OPTIONS} nocheck" -fi -export DEB_BUILD_PROFILES="${BUILD_PROFILES}" - -apt-get update -mk-build-deps --install --remove \ - --tool 'apt-get -o Debug::pkgProblemResolver=yes --yes --no-install-recommends' \ - debian/control - -echo "==> [6/6] Build binary packages (DEB_BUILD_OPTIONS='${BUILD_OPTIONS}')" -export DEB_BUILD_OPTIONS="${BUILD_OPTIONS}" -dpkg-buildpackage -b -uc -us - -echo "==> Collect artifacts into ${OUTPUT_DIR}" -cp -v "${WORK}"/*.deb "${OUTPUT_DIR}/" -cp -v "${WORK}"/*.buildinfo "${WORK}"/*.changes "${OUTPUT_DIR}/" 2>/dev/null || true - -echo "==> DONE. Packages:" -ls -1 "${OUTPUT_DIR}"/*.deb diff --git a/scripts/build_pg_focal b/scripts/build_pg_focal new file mode 100755 index 00000000..ddead094 --- /dev/null +++ b/scripts/build_pg_focal @@ -0,0 +1,219 @@ +#!/bin/bash +# +# build_pg_focal -- rebuild PostgreSQL *core* Debian packages for Ubuntu 20.04 (focal). +# +# PGDG stopped shipping focal binaries once focal reached EOL standard support, so +# the only way to get newer minors on focal is to rebuild the official Debian +# source package ourselves. This works for every major that ever had a focal +# build *and* still receives new upstream minors: PG 13, 14, 15, 16, 17. +# +# Approach (see dockerfiles/focal-pg-builder/Dockerfile for the rationale): +# newer upstream orig.tar.bz2 + the frozen focal-era debian/ packaging +# (the last ".-N.pgdg20.04+1" PGDG published for focal) +# -> dpkg-buildpackage inside a focal environment that has the archived +# focal-pgdg build tooling available. +# +# The focal-era debian/ packaging matters: its *default* build profile uses +# focal's default toolchain (clang/llvm-dev = LLVM 10), so the resulting JIT +# depends on focal's libllvm10 (installable on focal) instead of the +# clang-19/llvm-19 that newer packaging requires. +# +# Output: unsigned *.deb in ${OUTPUT_DIR} (default /packages/focal/pg). +# Signing is a separate step performed by the debsigner image (debsigs +# --sign=maint), so the build and the signing key never live in the same +# container. +# +# All inputs are overridable via environment variables. The common cases need +# nothing but PG_MAJOR: +# * PG_UPSTREAM_VERSION defaults to the latest minor PGDG publishes for the +# major, so a new minor release needs no edit here. +# * PG_DEBIAN_BASE defaults to the known last focal packaging for the major +# (focal is EOL, so these are frozen); override only if PGDG ever re-touches +# the archive. +# +# Examples: +# PG_MAJOR=15 build_pg_focal # latest 15.x +# PG_MAJOR=16 PG_UPSTREAM_VERSION=16.15 build_pg_focal +# PG_MAJOR=17 PG_DROP_PATCHES="hurd-iovec foo" build_pg_focal + +set -euo pipefail + +# ---------------------------------------------------------------------------- +# Inputs (override via env) +# ---------------------------------------------------------------------------- +PG_MAJOR="${PG_MAJOR:?ERROR: set PG_MAJOR (one of: 13 14 15 16 17)}" + +case "${PG_MAJOR}" in + 13|14|15|16|17) : ;; + *) echo "ERROR: PG_MAJOR='${PG_MAJOR}' is not a focal-buildable major (expected 13|14|15|16|17)." >&2 + echo " (11/12 are EOL with no minor newer than their last focal build; 18 never shipped on focal.)" >&2 + exit 64 ;; +esac + +# Sources: orig tarball from the live pool (shared across distros), focal-era +# debian/ packaging from the archive (removed from the live pool with focal). +LIVE_POOL="https://apt.postgresql.org/pub/repos/apt/pool/main/p/postgresql-${PG_MAJOR}" +ARCHIVE_POOL="https://apt-archive.postgresql.org/pub/repos/apt/pool/main/p/postgresql-${PG_MAJOR}" + +# default_debian_base -- the last focal packaging PGDG published per major. +# These are frozen (focal is EOL); override with PG_DEBIAN_BASE if ever needed. +default_debian_base() { + case "${PG_MAJOR}" in + 13) echo "13.21-1.pgdg20.04+1" ;; + 14) echo "14.18-1.pgdg20.04+1" ;; + 15) echo "15.13-1.pgdg20.04+1" ;; + 16) echo "16.9-1.pgdg20.04+1" ;; + 17) echo "17.5-1.pgdg20.04+1" ;; + esac +} + +# find_latest_upstream_minor -- newest . orig tarball in the live pool. +# The orig tarball is shared across every per-distro revision, so the live pool +# still carries it long after focal binaries were dropped. +find_latest_upstream_minor() { + curl -fsSL "${LIVE_POOL}/" \ + | grep -oE "postgresql-${PG_MAJOR}_${PG_MAJOR}\.[0-9]+\.orig\.tar\.bz2" \ + | sed -E "s/.*_(${PG_MAJOR}\.[0-9]+)\.orig.*/\1/" \ + | sort -V | tail -1 +} + +UPSTREAM="${PG_UPSTREAM_VERSION:-}" # upstream PostgreSQL minor to build +if [ -z "${UPSTREAM}" ]; then + UPSTREAM="$(find_latest_upstream_minor || true)" + [ -n "${UPSTREAM}" ] || { echo "ERROR: could not auto-resolve the latest ${PG_MAJOR}.x minor from ${LIVE_POOL}." >&2; exit 1; } + echo "==> PG_UPSTREAM_VERSION not set; auto-resolved latest minor: ${UPSTREAM}" +fi + +DEBIAN_BASE="${PG_DEBIAN_BASE:-$(default_debian_base)}" # focal-era packaging to reuse +TARGET_VERSION="${PG_TARGET_VERSION:-${UPSTREAM}-1.pgdg20.04+1}" # produced package version + +# sha256 of the artifacts we download. Leave empty to auto-resolve from the +# official .dsc; set to pin a specific value (integrity / reproducibility). +ORIG_SHA256="${PG_ORIG_SHA256:-}" +DEBIAN_SHA256="${PG_DEBIAN_SHA256:-}" + +# Patches in the frozen focal debian/ packaging that no longer apply to newer +# upstream (e.g. merged upstream). Space-separated; each is dropped from the +# quilt series only if present, so the default is safe for every major: +# * hurd-iovec was merged upstream as of 16.14 (pg_iovec.h guards IOV_MAX), +# so it fails to apply on 16.x/17.x but is absent from 13/14/15 packaging. +# If a future minor introduces new drift, append the offending patch name here +# (or via the PG_DROP_PATCHES env) -- no other code change required. +PG_DROP_PATCHES="${PG_DROP_PATCHES:-hurd-iovec}" + +# Set RUN_TESTS=1 to run the upstream regression suite (slower, needs more deps). +RUN_TESTS="${RUN_TESTS:-0}" + +OUTPUT_DIR="${OUTPUT_DIR:-/packages/focal/pg${PG_MAJOR}}" + +ORIG_TARBALL="postgresql-${PG_MAJOR}_${UPSTREAM}.orig.tar.bz2" +DEBIAN_TARBALL="postgresql-${PG_MAJOR}_${DEBIAN_BASE}.debian.tar.xz" + +WORK="${WORK_DIR:-/build}" +SRCDIR="${WORK}/postgresql-${PG_MAJOR}-${UPSTREAM}" + +# changelog identity (only cosmetic for a binary build) +export DEBEMAIL="${DEBEMAIL:-pgsql-pkg-debian@lists.postgresql.org}" +export DEBFULLNAME="${DEBFULLNAME:-PostgreSQL focal rebuild}" + +# dsc_sha256 -> prints the sha256 recorded for +# in the Checksums-Sha256 stanza of a Debian .dsc (served over HTTPS by PGDG). +dsc_sha256() { + curl -fsSL "$1" | awk -v f="$2" ' + /^Checksums-Sha256:/ { insec=1; next } + /^[A-Za-z]/ { insec=0 } + insec && $3==f { print $1; exit }' +} + +# Find any released .dsc for this upstream version (the orig tarball is shared +# across every per-distro revision, so any of them carries its checksum). +find_orig_dsc_url() { + local v="${UPSTREAM//./\\.}" + curl -fsSL "${LIVE_POOL}/" \ + | grep -oE "postgresql-${PG_MAJOR}_${v}-[0-9]+\.pgdg[A-Za-z0-9.+~]*\.dsc" \ + | sort -u | tail -1 +} + +echo "==> Building PostgreSQL ${UPSTREAM} (PG${PG_MAJOR}) as ${TARGET_VERSION} (focal)" +echo " debian/ packaging base: ${DEBIAN_BASE}" +mkdir -p "${WORK}" "${OUTPUT_DIR}" +cd "${WORK}" + +echo "==> [1/6] Download source artifacts" +curl -fSL "${LIVE_POOL}/${ORIG_TARBALL}" -o "${ORIG_TARBALL}" +curl -fSL "${ARCHIVE_POOL}/${DEBIAN_TARBALL}" -o "${DEBIAN_TARBALL}" + +echo "==> [2/6] Resolve & verify checksums" +if [ -z "${ORIG_SHA256}" ]; then + orig_dsc="$(find_orig_dsc_url || true)" + [ -n "${orig_dsc}" ] || { echo "ERROR: could not find a .dsc for ${ORIG_TARBALL} (is ${UPSTREAM} released?)." >&2; exit 1; } + ORIG_SHA256="$(dsc_sha256 "${LIVE_POOL}/${orig_dsc}" "${ORIG_TARBALL}")" + [ -n "${ORIG_SHA256}" ] || { echo "ERROR: ${ORIG_TARBALL} not listed in ${orig_dsc}." >&2; exit 1; } + echo " orig sha256 resolved from ${orig_dsc}: ${ORIG_SHA256}" +else + echo " orig sha256 pinned: ${ORIG_SHA256}" +fi +if [ -z "${DEBIAN_SHA256}" ]; then + DEBIAN_SHA256="$(dsc_sha256 "${ARCHIVE_POOL}/postgresql-${PG_MAJOR}_${DEBIAN_BASE}.dsc" "${DEBIAN_TARBALL}")" + [ -n "${DEBIAN_SHA256}" ] || { echo "ERROR: ${DEBIAN_TARBALL} not listed in postgresql-${PG_MAJOR}_${DEBIAN_BASE}.dsc." >&2; exit 1; } + echo " debian sha256 resolved from postgresql-${PG_MAJOR}_${DEBIAN_BASE}.dsc: ${DEBIAN_SHA256}" +else + echo " debian sha256 pinned: ${DEBIAN_SHA256}" +fi +echo "${ORIG_SHA256} ${ORIG_TARBALL}" | sha256sum -c - +echo "${DEBIAN_SHA256} ${DEBIAN_TARBALL}" | sha256sum -c - + +echo "==> [3/6] Assemble 3.0 (quilt) source tree" +rm -rf "${SRCDIR}" +mkdir -p "${SRCDIR}" +tar -xf "${ORIG_TARBALL}" -C "${SRCDIR}" --strip-components=1 +tar -xf "${DEBIAN_TARBALL}" -C "${SRCDIR}" # unpacks debian/ +# NB: ${ORIG_TARBALL} is already named postgresql-${PG_MAJOR}_${UPSTREAM}.orig.tar.bz2, +# exactly what dpkg-buildpackage expects one level above ${SRCDIR}. + +cd "${SRCDIR}" + +# Drop patches from the frozen focal packaging that no longer apply to the newer +# upstream (each removed only if present, so this is a no-op where unneeded). +for p in ${PG_DROP_PATCHES}; do + if grep -qx "${p}" debian/patches/series 2>/dev/null; then + sed -i "/^${p}$/d" debian/patches/series + rm -f "debian/patches/${p}" + echo " dropped patch (PG_DROP_PATCHES): ${p}" + fi +done + +echo "==> [4/6] Verify the quilt patch series applies cleanly to ${UPSTREAM}" +export QUILT_PATCHES=debian/patches +if [ -s debian/patches/series ]; then + quilt push -a + quilt pop -a +fi + +echo "==> [5/6] Set version to ${TARGET_VERSION} and install Build-Depends" +dch --newversion "${TARGET_VERSION}" --distribution focal --force-distribution \ + "Rebuild of PostgreSQL ${UPSTREAM} for focal (upstream PGDG focal-pgdg discontinued)." + +BUILD_PROFILES="" +BUILD_OPTIONS="parallel=$(nproc)" +if [ "${RUN_TESTS}" != "1" ]; then + BUILD_PROFILES="nocheck" + BUILD_OPTIONS="${BUILD_OPTIONS} nocheck" +fi +export DEB_BUILD_PROFILES="${BUILD_PROFILES}" + +apt-get update +mk-build-deps --install --remove \ + --tool 'apt-get -o Debug::pkgProblemResolver=yes --yes --no-install-recommends' \ + debian/control + +echo "==> [6/6] Build binary packages (DEB_BUILD_OPTIONS='${BUILD_OPTIONS}')" +export DEB_BUILD_OPTIONS="${BUILD_OPTIONS}" +dpkg-buildpackage -b -uc -us + +echo "==> Collect artifacts into ${OUTPUT_DIR}" +cp -v "${WORK}"/*.deb "${OUTPUT_DIR}/" +cp -v "${WORK}"/*.buildinfo "${WORK}"/*.changes "${OUTPUT_DIR}/" 2>/dev/null || true + +echo "==> DONE. Packages:" +ls -1 "${OUTPUT_DIR}"/*.deb From d7228a889f3384d8be936e874047152a1704c156 Mon Sep 17 00:00:00 2001 From: Kemal Buyukkaya Date: Sat, 27 Jun 2026 05:05:19 +0000 Subject: [PATCH 09/14] build-pg-focal: fix signing for the per-major output layout The debsigner entrypoint signs exactly "/packages/*/*.deb" (one directory level deep) and, with no nullglob, passes the unexpanded literal to debsigs when nothing matches -> "File /packages/*/*.deb does not exist". The build writes to packages/focal/pg/, so mounting ${PWD}/packages left the debs one level too deep (/packages/focal/pg/*.deb) and the glob matched nothing. Mount ${PWD}/packages/focal instead, so the signer sees /packages/pg/*.deb. Verified against citusdata/packaging:debsigner. --- .github/workflows/build-pg-focal.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-pg-focal.yml b/.github/workflows/build-pg-focal.yml index e434c7ac..86c5e082 100644 --- a/.github/workflows/build-pg-focal.yml +++ b/.github/workflows/build-pg-focal.yml @@ -112,6 +112,11 @@ jobs: # Use the prebuilt, deployed debsigner image (the one all Citus signing # uses), not a locally built copy of dockerfiles/debsigner, which has # drifted from it. Mirrors tools.packaging_automation.citus_package. + # + # The signer's entrypoint signs exactly "/packages/*/*.deb" (one dir + # level deep), so mount the parent of the per-major output dir: with + # "${PWD}/packages/focal:/packages" the debs land at /packages/pg/*.deb, + # which is what that glob expects. run: | if [ -z "${PACKAGING_SECRET_KEY}" ] || [ -z "${PACKAGING_PASSPHRASE}" ]; then echo "::error::PACKAGING_SECRET_KEY / PACKAGING_PASSPHRASE secrets are not set" >&2 @@ -120,7 +125,7 @@ jobs: printf '%s' "${PACKAGING_PASSPHRASE}" | docker run --rm -i \ -e PACKAGING_SECRET_KEY \ -e PACKAGING_PASSPHRASE \ - -v "${PWD}/packages:/packages" \ + -v "${PWD}/packages/focal:/packages" \ citusdata/packaging:debsigner - name: Verify signatures are embedded From 3d42522262805b1842e21a412a22fcf713f36414 Mon Sep 17 00:00:00 2001 From: Kemal Buyukkaya Date: Mon, 29 Jun 2026 20:28:07 +0000 Subject: [PATCH 10/14] build-pg-focal: ship dbgsym packages and a conflict-free combined set Two issues with the multi-major focal output: 1. Debug-symbol packages were missing. dpkg-buildpackage emits them as .ddeb on Ubuntu, but the collection step copied only *.deb, so every dbgsym was silently dropped. Collect them too, renamed .ddeb -> .deb (Debian's own convention; identical on-disk format), so they flow through the existing debsigs signing, _gpgmaint verification and artifact upload unchanged. 2. Installing PG 13-17 on one machine conflicted on the shared libraries. Each major's source builds six single-instance system libraries with the same package name but a per-major version -- libpq5, libpq-dev, libpgtypes3, libecpg6, libecpg-dev, libecpg-compat3 (plus their -dbgsym) -- so they cannot be co-installed. The newest copy satisfies every major's ">=" dependency. Add an "assemble" job that, after the per-major matrix, keeps all per-major packages from every major but the shared libraries only from the highest major present, guards against duplicate package names, and uploads a flat, de-duplicated postgresql-all-focal artifact. Per-major artifacts are retained for traceability. Packages are copied byte-for-byte, preserving their signatures. Validated end-to-end (PG 13-17): dbgsym now collected (~11 per major); the combined set is 72 packages with no duplicate names and a single 17.10 copy of each shared lib; PG 13-17 co-install on a clean focal container with no conflicts and all server binaries reporting the expected versions. --- .github/workflows/build-pg-focal.yml | 71 ++++++++++++++++++++++++++++ scripts/build_pg_focal | 8 ++++ 2 files changed, 79 insertions(+) diff --git a/.github/workflows/build-pg-focal.yml b/.github/workflows/build-pg-focal.yml index 86c5e082..745dc14b 100644 --- a/.github/workflows/build-pg-focal.yml +++ b/.github/workflows/build-pg-focal.yml @@ -150,3 +150,74 @@ jobs: packages/focal/pg${{ matrix.pg_major }}/*.changes packages/focal/pg${{ matrix.pg_major }}/*.buildinfo if-no-files-found: error + + assemble: + needs: build-and-sign + name: Assemble combined co-installable set + runs-on: ubuntu-latest + steps: + - name: Download all per-major package sets + uses: actions/download-artifact@v4 + with: + pattern: postgresql-*-focal-deb + path: per-major # -> per-major/postgresql--focal-deb/*.deb + + - name: Assemble de-duplicated set + # PostgreSQL builds six shared, single-instance system libraries from + # *every* major's source -- libpq5, libpq-dev, libpgtypes3, libecpg6, + # libecpg-dev, libecpg-compat3 (plus their -dbgsym). They share one + # package name but carry a per-major version, so they cannot be + # co-installed; the newest copy satisfies every major's ">=" dependency. + # Keep all per-major packages from every major, but keep the shared + # libraries only from the highest major present. The result installs + # 13..17 side by side with no conflicts. Packages stay byte-identical + # (already signed) -- we only copy, never repackage. + run: | + set -euo pipefail + shared_re='^(libpq5|libpq-dev|libpgtypes3|libecpg6|libecpg-dev|libecpg-compat3)(-dbgsym)?$' + + # Discover the majors we actually received and pick the highest. + majors="$(find per-major -maxdepth 1 -type d -name 'postgresql-*-focal-deb' \ + | sed -E 's#.*/postgresql-([0-9]+)-focal-deb#\1#' | sort -n)" + [ -n "${majors}" ] || { echo "::error::no per-major artifacts found" >&2; exit 1; } + newest="$(echo "${majors}" | tail -1)" + echo "majors present: $(echo ${majors} | tr '\n' ' '); shared libs taken from PG${newest}" + + mkdir -p postgresql-all-focal + for m in ${majors}; do + for deb in per-major/postgresql-${m}-focal-deb/*.deb; do + [ -e "${deb}" ] || continue + pkg="$(dpkg-deb -f "${deb}" Package)" + if [[ "${pkg}" =~ ${shared_re} ]] && [ "${m}" != "${newest}" ]; then + echo " skip shared ${pkg} from PG${m} (kept from PG${newest})" + continue + fi + cp -n "${deb}" postgresql-all-focal/ + done + done + + echo "==> Assembled $(ls postgresql-all-focal/*.deb | wc -l) packages" + echo "==> Shared libraries in the combined set (must be exactly one version each):" + for deb in postgresql-all-focal/*.deb; do + pkg="$(dpkg-deb -f "${deb}" Package)" + [[ "${pkg}" =~ ${shared_re} ]] && echo " $(dpkg-deb -f "${deb}" Package Version | tr '\n' ' ')" + done | sort -u + + - name: Verify the combined set has no duplicate package names + run: | + set -euo pipefail + dupes="$(for deb in postgresql-all-focal/*.deb; do dpkg-deb -f "${deb}" Package; done \ + | sort | uniq -d)" + if [ -n "${dupes}" ]; then + echo "::error::duplicate package names in combined set (would conflict on install):" >&2 + echo "${dupes}" >&2 + exit 1 + fi + echo "OK: every package name appears exactly once" + + - name: Upload combined co-installable set + uses: actions/upload-artifact@v4 + with: + name: postgresql-all-focal + path: postgresql-all-focal/*.deb + if-no-files-found: error diff --git a/scripts/build_pg_focal b/scripts/build_pg_focal index ddead094..ae29208d 100755 --- a/scripts/build_pg_focal +++ b/scripts/build_pg_focal @@ -213,6 +213,14 @@ dpkg-buildpackage -b -uc -us echo "==> Collect artifacts into ${OUTPUT_DIR}" cp -v "${WORK}"/*.deb "${OUTPUT_DIR}/" +# Debug-symbol packages are emitted as .ddeb on Ubuntu (e.g. postgresql-16-dbgsym +# _..._amd64.ddeb). Ship them as .deb (the Debian convention; the on-disk format +# is identical) so they flow through the same signing/verification/publishing as +# every other package instead of being silently dropped by the *.deb glob above. +for ddeb in "${WORK}"/*.ddeb; do + [ -e "${ddeb}" ] || continue + cp -v "${ddeb}" "${OUTPUT_DIR}/$(basename "${ddeb}" .ddeb).deb" +done cp -v "${WORK}"/*.buildinfo "${WORK}"/*.changes "${OUTPUT_DIR}/" 2>/dev/null || true echo "==> DONE. Packages:" From 2f213888904c832512daa016cb8bb648fb11478b Mon Sep 17 00:00:00 2001 From: Kemal Buyukkaya Date: Mon, 29 Jun 2026 20:52:28 +0000 Subject: [PATCH 11/14] build-pg-focal: fix assemble job exit 1 and cp -n warnings The assemble step assembled correctly but failed under set -euo pipefail: the diagnostic loop used `[[ ... ]] && echo ...`, which returns non-zero on the last package when it isn't a shared lib; piped into sort that made the whole step exit 1. Use an explicit `if` so the loop ends with status 0. Also drop `cp -n` (newer coreutils warns it's non-portable, and the flag is unnecessary -- after de-dup no destination filename collides) for plain `cp`. --- .github/workflows/build-pg-focal.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-pg-focal.yml b/.github/workflows/build-pg-focal.yml index 745dc14b..f531451e 100644 --- a/.github/workflows/build-pg-focal.yml +++ b/.github/workflows/build-pg-focal.yml @@ -192,7 +192,7 @@ jobs: echo " skip shared ${pkg} from PG${m} (kept from PG${newest})" continue fi - cp -n "${deb}" postgresql-all-focal/ + cp "${deb}" postgresql-all-focal/ done done @@ -200,7 +200,9 @@ jobs: echo "==> Shared libraries in the combined set (must be exactly one version each):" for deb in postgresql-all-focal/*.deb; do pkg="$(dpkg-deb -f "${deb}" Package)" - [[ "${pkg}" =~ ${shared_re} ]] && echo " $(dpkg-deb -f "${deb}" Package Version | tr '\n' ' ')" + if [[ "${pkg}" =~ ${shared_re} ]]; then + dpkg-deb -f "${deb}" Package Version | tr '\n' ' '; echo + fi done | sort -u - name: Verify the combined set has no duplicate package names From d1fd3baf75c90007be0d8b77023c3015b464207d Mon Sep 17 00:00:00 2001 From: Kemal Buyukkaya Date: Tue, 28 Jul 2026 13:04:57 +0000 Subject: [PATCH 12/14] build-pg-focal: drop PG17 from "all", bump checkout/login actions PG17 is not needed up front, so the default "all" matrix now builds 13..16 only. PG17 stays fully supported and selectable -- dispatch with pg_major=17 to build it; scripts/build_pg_focal and the builder image still carry the 13..17 support they always had. The assemble job derives the newest major from the artifacts it actually receives, so the combined set now takes the six single-instance shared libraries (libpq5, libpq-dev, libpgtypes3, libecpg6, libecpg-dev, libecpg-compat3) from PG16 instead of PG17 with no code change. Also align action versions with develop: actions/checkout v4 -> v6 and docker/login-action v2 -> v4 (v2 runs on the deprecated Node 16 runtime). --- .github/workflows/build-pg-focal.yml | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build-pg-focal.yml b/.github/workflows/build-pg-focal.yml index f531451e..5ac501e1 100644 --- a/.github/workflows/build-pg-focal.yml +++ b/.github/workflows/build-pg-focal.yml @@ -4,9 +4,10 @@ name: Build PostgreSQL core (focal) # the official Debian source package (PGDG dropped focal binaries upstream), then # signs them with debsigs (--sign=maint) using the existing packaging key. # -# One parameterized pipeline covers every focal-buildable major (PG 13..17): a -# matrix builds them all by default; workflow_dispatch can target a single major -# and pin its minor / orig sha256. +# One parameterized pipeline covers every focal-buildable major (PG 13..17). The +# default "all" matrix builds 13..16; PG17 is excluded up front and is built on +# demand by dispatching pg_major=17. workflow_dispatch can also target a single +# major and pin its minor / orig sha256. # # This is intentionally a *standalone* pipeline: it does not use the extension # build flow (citus_package / pg_buildext / the build-package.yml matrix), which @@ -16,7 +17,7 @@ on: workflow_dispatch: inputs: pg_major: - description: "PostgreSQL major to build" + description: "PostgreSQL major to build (\"all\" builds 13-16; pick 17 explicitly if needed)" required: true type: choice default: "all" @@ -59,7 +60,9 @@ jobs: run: | sel="${{ github.event.inputs.pg_major }}" if [ -z "${sel}" ] || [ "${sel}" = "all" ]; then - echo 'matrix={"pg_major":["13","14","15","16","17"]}' >> "$GITHUB_OUTPUT" + # PG17 is intentionally excluded from "all" -- not needed up front. + # It remains fully supported: dispatch with pg_major=17 to build it. + echo 'matrix={"pg_major":["13","14","15","16"]}' >> "$GITHUB_OUTPUT" else echo "matrix={\"pg_major\":[\"${sel}\"]}" >> "$GITHUB_OUTPUT" fi @@ -82,10 +85,10 @@ jobs: RUN_TESTS: ${{ github.event.inputs.run_tests || '0' }} steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Login to Docker Hub - uses: docker/login-action@v2 + uses: docker/login-action@v4 with: username: ${{ secrets.DOCKERHUB_USER_NAME }} password: ${{ secrets.DOCKERHUB_PASSWORD }} @@ -170,8 +173,8 @@ jobs: # co-installed; the newest copy satisfies every major's ">=" dependency. # Keep all per-major packages from every major, but keep the shared # libraries only from the highest major present. The result installs - # 13..17 side by side with no conflicts. Packages stay byte-identical - # (already signed) -- we only copy, never repackage. + # every built major side by side with no conflicts. Packages stay + # byte-identical (already signed) -- we only copy, never repackage. run: | set -euo pipefail shared_re='^(libpq5|libpq-dev|libpgtypes3|libecpg6|libecpg-dev|libecpg-compat3)(-dbgsym)?$' From 485ffe5a60aa312731184731110a88f326ee450e Mon Sep 17 00:00:00 2001 From: Kemal Buyukkaya Date: Wed, 29 Jul 2026 13:40:55 +0000 Subject: [PATCH 13/14] build-pg-focal: add an install smoke test for the built package set The pipeline verified that packages exist, are signed, and carry unique names, but never that they can actually be installed. In particular the combined set ships the six single-instance shared libraries from only the highest major, assuming e.g. libpq5 16.x satisfies PG13's "libpq5 (>= 13~~)". That assumption was untested. New install-smoke-test job installs the whole shipped set (including -dbgsym) into a stock ubuntu:20.04, then asserts: * every shipped package installed at its exact version, proving apt used our binaries rather than substituting same-named ones, * postgresql-common resolved to 278.pgdg20.04+1, * a cluster per installed major starts and answers queries, * pg_jit_available() is true on every cluster, * clusters coexist on distinct ports. The JIT assertion matters because the failure is silent: if WITH_LLVM resolves empty in postgresql.mk, configure just omits --with-llvm and the build still produces a complete, signed, installable set with no llvmjit.so. Nothing else in the pipeline would notice. This is not a self-containment test. Focal ships postgresql-common 214ubuntu0.1 while the server packages need >= 252~, so the PGDG archive's "main" component is enabled -- consumers need it too, which is why it is encoded here rather than left as tribal knowledge. The logic lives in scripts/smoke_test_focal_debs, mirroring how scripts/build_pg_focal is run in a container, so it can be executed locally against a downloaded artifact. --- .github/workflows/build-pg-focal.yml | 31 +++++ scripts/smoke_test_focal_debs | 182 +++++++++++++++++++++++++++ 2 files changed, 213 insertions(+) create mode 100755 scripts/smoke_test_focal_debs diff --git a/.github/workflows/build-pg-focal.yml b/.github/workflows/build-pg-focal.yml index 5ac501e1..95ef8af2 100644 --- a/.github/workflows/build-pg-focal.yml +++ b/.github/workflows/build-pg-focal.yml @@ -226,3 +226,34 @@ jobs: name: postgresql-all-focal path: postgresql-all-focal/*.deb if-no-files-found: error + + install-smoke-test: + needs: assemble + name: Install smoke test (focal) + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Download combined co-installable set + uses: actions/download-artifact@v4 + with: + name: postgresql-all-focal + path: debs + + - name: Install and verify in a clean focal container + # The jobs above only prove the packages exist, are signed, and have + # unique names -- not that they can actually be installed. This installs + # the whole shipped set (including -dbgsym) into a stock ubuntu:20.04, + # starts every cluster, and checks JIT is live. + # + # See scripts/smoke_test_focal_debs for why the PGDG archive's "main" + # component has to be enabled: focal ships postgresql-common + # 214ubuntu0.1, but the server packages need >= 252~. + run: | + docker run --rm \ + -v "${PWD}/debs:/debs:ro" \ + -v "${PWD}/scripts/smoke_test_focal_debs:/usr/local/bin/smoke_test_focal_debs:ro" \ + -e DEBS_DIR=/debs \ + ubuntu:20.04 \ + /usr/local/bin/smoke_test_focal_debs diff --git a/scripts/smoke_test_focal_debs b/scripts/smoke_test_focal_debs new file mode 100755 index 00000000..d9d2d4ab --- /dev/null +++ b/scripts/smoke_test_focal_debs @@ -0,0 +1,182 @@ +#!/bin/bash +# +# smoke_test_focal_debs -- install the built PostgreSQL core .deb set inside a +# clean Ubuntu 20.04 (focal) container and prove it actually works. +# +# The build pipeline already checks that the packages *exist*, are signed, and +# carry unique package names. None of that proves they are installable: the +# combined set deliberately ships the six single-instance shared libraries +# (libpq5, libpq-dev, libpgtypes3, libecpg6, libecpg-dev, libecpg-compat3) from +# only the highest major, on the assumption that e.g. libpq5 16.x satisfies +# PG13's "libpq5 (>= 13~~)". This script tests that assumption for real. +# +# It also asserts JIT is live (SELECT pg_jit_available()). PostgreSQL here links +# libllvm10 directly -- there is no separate postgresql-NN-jit-llvm package -- +# so a regression in the frozen-focal-packaging / LLVM-10 strategy that silently +# disables JIT would otherwise ship unnoticed. +# +# NOTE ON CLUSTER CREATION +# ------------------------ +# Installing several majors at once produces exactly *one* cluster, not one per +# major: postgresql-common's create_main_cluster() returns early if any cluster +# already exists, and again if the package being configured is not the newest +# installed major. That is intended upstream behaviour, so this script creates +# the remaining clusters explicitly rather than expecting them to appear. +# +# NOTE ON THE PGDG ARCHIVE REPOSITORY +# ----------------------------------- +# This is *not* a self-containment test. The produced server packages depend on +# postgresql-common (>= 229~ / 241~ / 252~ depending on major), but Ubuntu focal +# only ships 214ubuntu0.1. The only focal build that satisfies them is +# 278.pgdg20.04+1 from apt-archive.postgresql.org, so the archive's "main" +# component is enabled below. Consumers of these packages need it too -- that is +# the point of encoding it here rather than leaving it as tribal knowledge. +# +# Only "main" is enabled, never the per-major components: those still carry +# PGDG's own libpq5/libecpg6/... for focal and would shadow the packages under +# test. +# +# Usage (inside a stock ubuntu:20.04 container, as root): +# DEBS_DIR=/debs smoke_test_focal_debs +# +# Everything is overridable via environment variables: +# DEBS_DIR directory holding the *.deb set (default /debs) +# EXPECTED_PGCOMMON postgresql-common version to expect (default 278.pgdg20.04+1) +# PGDG_ARCHIVE_SUITE archive suite to enable (default focal-pgdg) + +set -euo pipefail + +DEBS_DIR="${DEBS_DIR:-/debs}" +EXPECTED_PGCOMMON="${EXPECTED_PGCOMMON:-278.pgdg20.04+1}" +PGDG_ARCHIVE_SUITE="${PGDG_ARCHIVE_SUITE:-focal-pgdg}" +PGDG_ARCHIVE_URL="${PGDG_ARCHIVE_URL:-https://apt-archive.postgresql.org/pub/repos/apt}" + +export DEBIAN_FRONTEND=noninteractive +# Pin the locale so initdb is deterministic. C.UTF-8 always exists on focal and +# needs no locale-gen. +export LANG=C.UTF-8 + +fail=0 +note() { echo "==> $*"; } +err() { echo "::error::$*" >&2; fail=1; } + +shopt -s nullglob +debs=("${DEBS_DIR}"/*.deb) +shopt -u nullglob +[ ${#debs[@]} -gt 0 ] || { echo "ERROR: no .deb files found in ${DEBS_DIR}" >&2; exit 1; } + +note "[1/8] Preparing container environment (${#debs[@]} packages to install)" +# postgresql-NN.postinst ends in `invoke-rc.d postgresql start $VERSION` under +# `set -e`, and there is no systemd in a container. A policy-rc.d denying the +# action makes invoke-rc.d return 0, so the postinst still creates the cluster; +# we start the clusters explicitly further down instead. +printf '#!/bin/sh\nexit 101\n' > /usr/sbin/policy-rc.d +chmod +x /usr/sbin/policy-rc.d + +apt-get update -qq +apt-get install -y -qq --no-install-recommends ca-certificates curl gnupg >/dev/null + +note "[2/8] Enabling the PGDG archive (main only) for postgresql-common" +# PGDG repository signing key fingerprint: +# B97B 0AFC AA1A 47F0 44F2 44A0 7FCC 7D46 ACCC 4CF8 +install -d /usr/share/keyrings +curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc \ + | gpg --dearmor -o /usr/share/keyrings/pgdg-archive.gpg +echo "deb [signed-by=/usr/share/keyrings/pgdg-archive.gpg] ${PGDG_ARCHIVE_URL} ${PGDG_ARCHIVE_SUITE} main" \ + > /etc/apt/sources.list.d/pgdg-archive.list +apt-get update -qq + +note "[3/8] Installing the full shipped set (including -dbgsym)" +apt-get install -y "${debs[@]}" + +note "[4/8] Verifying every shipped package installed at its exact version" +# Proves apt used *our* binaries and did not silently substitute a package of +# the same name from focal or from the PGDG archive. +for deb in "${debs[@]}"; do + pkg="$(dpkg-deb -f "${deb}" Package)" + want="$(dpkg-deb -f "${deb}" Version)" + got="$(dpkg-query -W -f='${Version}' "${pkg}" 2>/dev/null || echo MISSING)" + if [ "${got}" != "${want}" ]; then + err "${pkg}: installed ${got}, expected ${want}" + fi +done +if [ "${fail}" -eq 0 ]; then + note " all ${#debs[@]} packages installed at the expected versions" +fi + +note "[5/8] Verifying postgresql-common came from the archive at ${EXPECTED_PGCOMMON}" +pgc="$(dpkg-query -W -f='${Version}' postgresql-common 2>/dev/null || echo MISSING)" +if [ "${pgc}" != "${EXPECTED_PGCOMMON}" ]; then + err "postgresql-common is ${pgc}, expected ${EXPECTED_PGCOMMON}" +else + note " postgresql-common ${pgc}" +fi + +note "[6/8] Creating a cluster for every installed major" +# Derive the majors from the installed server binaries rather than hardcoding, +# so this works unchanged for a single-major dispatch as well as the full matrix. +mapfile -t majors < <(ls -v /usr/lib/postgresql/*/bin/postgres \ + | sed -E 's#.*/postgresql/([0-9]+)/bin/postgres#\1#') +[ ${#majors[@]} -gt 0 ] || { echo "ERROR: no PostgreSQL server binaries installed" >&2; exit 1; } +echo " installed majors: ${majors[*]}" + +# postgresql-common's create_main_cluster() deliberately creates at most ONE +# cluster when several majors are installed at once -- it returns early if any +# cluster already exists, and again if the package being configured is not the +# newest major. So exactly one "main" cluster (on the newest major) exists at +# this point. Create the rest explicitly: running every major side by side is +# precisely the property the assemble job's shared-library de-duplication +# assumes, and nothing else verifies it. +for m in "${majors[@]}"; do + if [ -e "/etc/postgresql/${m}/main/postgresql.conf" ]; then + echo " PG${m}: cluster already present (created by postinst)" + else + pg_createcluster -u postgres "${m}" main + fi +done + +note "[7/8] Starting every cluster" +for m in "${majors[@]}"; do + status="$(pg_lsclusters -h | awk -v v="${m}" '$1 == v && $2 == "main" { print $4 }')" + [ "${status}" = "online" ] || pg_ctlcluster "${m}" main start +done +pg_lsclusters + +note "[8/8] Querying each cluster (version + JIT availability)" +for m in "${majors[@]}"; do + port="$(pg_lsclusters -h | awk -v v="${m}" '$1 == v && $2 == "main" { print $3 }')" + status="$(pg_lsclusters -h | awk -v v="${m}" '$1 == v && $2 == "main" { print $4 }')" + if [ "${status}" != "online" ]; then + err "PG${m}: cluster is '${status}', expected 'online'" + continue + fi + ver="$(su postgres -c "psql -p ${port} -tAc 'SELECT version()'" | head -1)" + jit="$(su postgres -c "psql -p ${port} -tAc 'SELECT pg_jit_available()'" | tr -d '[:space:]')" + echo " PG${m} (port ${port}): ${ver}" + if [ "${jit}" != "t" ]; then + err "PG${m}: pg_jit_available() returned '${jit}', expected 't' (JIT is not available)" + else + echo " PG${m}: JIT available" + fi +done + +# Co-installability: every major that shipped a server package must be installed +# and running, each on its own port. +note "Verifying co-existence" +for m in $(printf '%s\n' "${debs[@]}" | sed -nE 's#.*/postgresql-([0-9]+)_.*#\1#p' | sort -un); do + if ! printf '%s\n' "${majors[@]}" | grep -qx "${m}"; then + err "PG${m} shipped a server package but is not installed" + fi +done +nports="$(pg_lsclusters -h | awk '{print $3}' | sort -u | wc -l)" +nclusters="$(pg_lsclusters -h | wc -l)" +if [ "${nports}" -ne "${nclusters}" ]; then + err "expected ${nclusters} distinct ports, found ${nports} (clusters are colliding)" +fi +echo " ${nclusters} clusters running on ${nports} distinct ports" + +if [ "${fail}" -ne 0 ]; then + echo "SMOKE TEST FAILED" >&2 + exit 1 +fi +echo "SMOKE TEST PASSED: ${#debs[@]} packages installed, ${nclusters} clusters running (PG ${majors[*]}), JIT available on all" From a3acf1a3739ad0138578e8bd52e60144b394d978 Mon Sep 17 00:00:00 2001 From: Kemal Buyukkaya Date: Tue, 4 Aug 2026 09:27:26 +0000 Subject: [PATCH 14/14] build-pg-focal: restrict GITHUB_TOKEN to contents: read CodeQL flagged every job in the workflow for not limiting the permissions of GITHUB_TOKEN (alerts 13-17, rule actions/missing-workflow-permissions). No job writes back to the repository: they check out sources, build and sign inside containers, and exchange artifacts. actions/upload-artifact and actions/download-artifact v4 use the Actions runtime token rather than GITHUB_TOKEN, and every download here is same-run, so read-only contents covers all four jobs. A single top-level block clears all five alerts. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 38a0aa7c-f6f0-4d5f-b1a9-568d4c7e6729 --- .github/workflows/build-pg-focal.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/build-pg-focal.yml b/.github/workflows/build-pg-focal.yml index 95ef8af2..10676a2f 100644 --- a/.github/workflows/build-pg-focal.yml +++ b/.github/workflows/build-pg-focal.yml @@ -44,6 +44,9 @@ on: branches: - pg-focal +permissions: + contents: read + concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true