From 9760a13047ecbad59c001949a26122c503aa21d9 Mon Sep 17 00:00:00 2001 From: Kemal Buyukkaya Date: Thu, 18 Jun 2026 14:09:12 +0000 Subject: [PATCH 01/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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