Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions .github/scripts/pg_upgrade_cluster
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#!/usr/bin/env bash
#
# pg_upgrade_cluster - Shared binary pg_upgrade mechanics for CI. Modeled on
# Postgres-Extensions/cat_tools's script of the same name/purpose -- this
# part of the flow (recreating the pgxn-tools image's "test" cluster with
# matching initdb options, then driving pg_upgrade between two majors) has
# nothing extension-specific about it, so it lives here once instead of
# being inlined in the workflow.
#
# Lives under .github/, not bin/: unlike bin/test_existing (which a developer
# can run locally against a scratch database), this is CI-mechanics-only --
# pg_ctlcluster/pg_createcluster/pg_upgrade against the pgxn-tools image's
# "test" cluster convention isn't a locally-runnable workflow.
#
# USAGE: .github/scripts/pg_upgrade_cluster <subcommand> [args]
#
# recreate-old PG_VERSION
# pg-start's default "test" cluster doesn't have data checksums
# enabled, but binary pg_upgrade requires the old and new clusters to
# have MATCHING checksum/auth settings (see $INITDB_OPTS below) --
# stop, drop, and recreate the "test" cluster for PG_VERSION with them,
# then start it and wait for readiness.
#
# upgrade OLD_PG NEW_PG
# Stop the old cluster, create the new cluster's "test" cluster (same
# $INITDB_OPTS), binary pg_upgrade OLD_PG -> NEW_PG, then start the new
# cluster and wait for readiness. On pg_upgrade failure, dumps its logs
# (PG17+ writes them to $new_datadir/pg_upgrade_output.d/; older
# versions write to CWD -- both are searched) before failing.
#
# $INITDB_OPTS (env var, required): initdb options both clusters must share so
# pg_upgrade sees consistent settings on old and new (e.g.
# "--data-checksums --auth trust"). Deliberately left unquoted at each use
# site so its (space-separated) options word-split into separate arguments.
set -euo pipefail

: "${INITDB_OPTS:?INITDB_OPTS must be set}"

recreate_old() {
local pg=$1
pg_ctlcluster "$pg" test stop
pg_dropcluster "$pg" test
# -p 5432: pg_createcluster assigns the next available port, which may not
# be 5432 after pg-start has claimed and released it. Force 5432 so
# subsequent psql/createdb calls connect without -p.
pg_createcluster -p 5432 "$pg" test -- $INITDB_OPTS
pg_ctlcluster "$pg" test start
pg_isready -t 30
}

upgrade() {
local old_pg=$1 new_pg=$2
pg_ctlcluster "$old_pg" test stop
pg_createcluster -p 5432 "$new_pg" test -- $INITDB_OPTS
# PG17+ writes logs to $new_datadir/pg_upgrade_output.d/; older versions
# write to CWD. Search both on failure.
mkdir -p /tmp/pg_upgrade_logs
chown postgres:postgres /tmp/pg_upgrade_logs
su -c "cd /tmp/pg_upgrade_logs && /usr/lib/postgresql/$new_pg/bin/pg_upgrade \
-b /usr/lib/postgresql/$old_pg/bin \
-B /usr/lib/postgresql/$new_pg/bin \
-d /var/lib/postgresql/$old_pg/test \
-D /var/lib/postgresql/$new_pg/test \
-o '-c config_file=/etc/postgresql/$old_pg/test/postgresql.conf' \
-O '-c config_file=/etc/postgresql/$new_pg/test/postgresql.conf'" postgres \
|| { find /tmp/pg_upgrade_logs \
"/var/lib/postgresql/$new_pg/test/pg_upgrade_output.d" \
-name '*.log' 2>/dev/null | sort | xargs -r tail -n +1; exit 1; }
pg_ctlcluster "$new_pg" test start
pg_isready -t 30
}

usage() {
echo "usage: .github/scripts/pg_upgrade_cluster <subcommand> [args]" >&2
echo " recreate-old PG_VERSION" >&2
echo " upgrade OLD_PG NEW_PG" >&2
exit 2
}

main() {
local cmd=${1:-}
shift || true
case "$cmd" in
recreate-old) recreate_old "$@" ;;
upgrade) upgrade "$@" ;;
*) usage ;;
esac
}

main "$@"
277 changes: 274 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,135 @@
# Test strategy
#
# A test_factory install can be arrived at two ways, each of which can break
# differently, so each gets its own job below:
#
# test -- FRESH install: CREATE EXTENSION at the current
# version, on every supported PostgreSQL major. The
# baseline a brand-new user gets.
# pg-upgrade-test -- BINARY pg_upgrade: install the current version on an
# OLD major, binary-upgrade the cluster to a NEWER
# major, then run the suite against the migrated
# objects in "existing" mode (test/install/load.sql).
# Proves objects created on an old server still work
# read back on a new one -- fresh-install testing
# never exercises this at all.
#
# test_factory has shipped only one version (0.5.0), so there is no
# in-place `ALTER EXTENSION UPDATE` path to test yet (no extension-update-test
# job) and the pg_upgrade job needs no bridge step (it always installs the
# CURRENT version on the old cluster -- there's no older, pg_upgrade-unsafe
# version in the wild to carry forward). Both jobs derive their PostgreSQL
# major list from the single source of truth computed in the `changes` job
# below, so adding/dropping a supported major is a one-line edit there.
name: CI
on: [push, pull_request]
on:
# Post-merge CI only; pull_request already covers every PR commit. Without
# this, a commit on a branch with an open PR fires the workflow TWICE for
# the identical SHA (once for push, once for pull_request/synchronize) --
# double the compute, and a flake on one duplicate run shows a confusing
# red check right next to an identical green one for the same commit.
push:
branches:
- master
pull_request:
env:
PGUSER: postgres
jobs:
# Cheap gate that lets the heavy jobs below skip themselves on commits that
# touch only docs, without a workflow-level `paths-ignore` (which would skip
# the *whole* workflow, including all-checks-passed, on a docs-only push --
# leaving a required check stuck Pending forever). Also derives, from a
# single set of constants, the supported-PostgreSQL-major list every heavy
# job below consumes, so adding a major is a one-line edit here instead of
# N separate matrix edits.
changes:
name: 🔍 Detect changes & derive PG matrix
runs-on: ubuntu-latest
outputs:
docs_only: ${{ steps.diff.outputs.docs_only }}
supported_pg: ${{ steps.pg.outputs.supported_pg }}
steps:
- name: Check out the repo
uses: actions/checkout@v4
with:
# Full history needed so BASE and HEAD below are both reachable
# for `git diff`.
fetch-depth: 0
- name: Compute per-push changed files
id: diff
run: |
# Fail-safe is the literal first line: any early exit or error
# further down (a bad BASE/HEAD, a failed git diff) leaves this in
# place, so a broken check never silently skips real testing.
echo "docs_only=false" >> "$GITHUB_OUTPUT"

if [ "${{ github.event_name }}" = "pull_request" ] && \
[ "${{ github.event.action }}" = "synchronize" ] && \
[ -n "${{ github.event.before }}" ]; then
# A push to an already-open PR: before/after give the true
# per-push diff, same as for a branch push.
BASE="${{ github.event.before }}"
HEAD="${{ github.event.after }}"
elif [ "${{ github.event_name }}" = "pull_request" ]; then
# First run for this PR (opened/reopened/etc, or synchronize
# without a usable before): fall back to the whole base...head
# diff.
BASE="${{ github.event.pull_request.base.sha }}"
HEAD="${{ github.event.pull_request.head.sha }}"
else
BASE="${{ github.event.before }}"
HEAD="${{ github.event.after }}"
fi

echo "base=$BASE"
echo "head=$HEAD"

# A missing HEAD, or an all-zeros BASE (e.g. a new branch's first
# push, where GitHub reports no prior commit), means we can't
# compute a real diff -- the fail-safe default above stands.
if [ -z "$HEAD" ] || [ -z "$BASE" ] || [[ "$BASE" =~ ^0+$ ]]; then
exit 0
fi

CHANGED=$(git diff --name-only "$BASE" "$HEAD" || echo __DIFF_FAILED__)
echo "changed files:"
echo "$CHANGED"

if [ "$CHANGED" = "__DIFF_FAILED__" ] || [ -z "$CHANGED" ]; then
exit 0
fi

DOCS_ONLY=true
while IFS= read -r f; do
if ! [[ "$f" =~ \.(md|asc)$ ]]; then
DOCS_ONLY=false
break
fi
done <<< "$CHANGED"

echo "docs_only=$DOCS_ONLY" >> "$GITHUB_OUTPUT"
- name: Derive the supported-PostgreSQL-major list
id: pg
run: |
# Single source of truth for the supported PostgreSQL majors: the
# `test` and `pg-upgrade-test` matrices below both derive their
# version lists from here, so they cannot silently drift onto
# different lists. To add or drop a major, edit only NEWEST/FLOOR.
NEWEST=17
FLOOR=10
supported=$(seq "$NEWEST" -1 "$FLOOR")
# Emit a JSON array from a list of ints, for matrix: to consume
# with fromJSON.
json() { printf '%s\n' "$@" | paste -sd, - | sed 's/^/[/; s/$/]/'; }
echo "supported_pg=$(json $supported)" >> "$GITHUB_OUTPUT"

test:
needs: [changes]
if: needs.changes.outputs.docs_only != 'true'
strategy:
matrix:
pg: [17, 16, 15, 14, 13, 12, 11, 10]
# Current-supported majors, from the single source in the changes job.
pg: ${{ fromJSON(needs.changes.outputs.supported_pg) }}
name: 🐘 PostgreSQL ${{ matrix.pg }}
runs-on: ubuntu-latest
container: pgxn/pgxn-tools
Expand All @@ -14,4 +139,150 @@ jobs:
- name: Check out the repo
uses: actions/checkout@v4
- name: Test on PostgreSQL ${{ matrix.pg }}
run: pg-build-test
# verify-results (not a bare `make test`, and not pg-build-test's own
# `make installcheck || status=$?`) is the real gate: pgxntool marks
# installcheck .IGNORE so make itself always exits 0 there, silently
# passing even when regression.diffs is nonempty. verify-results
# depends on `test` (so it builds+installs+runs the suite) and then
# actually inspects the TAP output/regression.diffs for failures.
#
# `make install` first, as its own separate invocation, is required,
# not redundant with verify-results' own `install` prerequisite: this
# container's ambient MAKEFLAGS enables parallel make, and `install`
# and `installcheck` are independent prerequisites of the same `test`
# target, so under `-j` pg_regress can start (and fail with "extension
# ... is not available") before `install`'s file copy finishes. Two
# separate `make` invocations can't race with each other.
run: |
make install
make verify-results

# Proves test_factory survives a BINARY pg_upgrade (in-place catalog
# migration to a newer PostgreSQL major), not just a fresh CREATE EXTENSION.
# Each leg: install the CURRENT version on an old cluster, binary-pg_upgrade
# to a newer major, then run the suite against the REAL migrated objects in
# "existing" mode (test/install/load.sql asserts the version, plants, and
# proves the dependency guard -- see bin/test_existing and
# test/install/load.sql). No bridge step: unlike an extension with multiple
# shipped versions, test_factory has only ever shipped 0.5.0, so there is no
# older, pg_upgrade-unsafe install to carry forward -- every leg installs
# the current version directly on the old cluster.
#
# Two legs, not an every-major stepwise climb (deliberately out of scope --
# see the PR description): the oldest-to-newest jump (widest catalog
# distance) and the newest-boundary jump (most likely to hit a *new*
# PostgreSQL major's catalog change first). test_factory has no views or
# functions touching catalog internals (no SELECT * over a system catalog),
# so the per-major-boundary risk a full stepwise climb protects against is
# low here.
pg-upgrade-test:
needs: [changes]
if: needs.changes.outputs.docs_only != 'true'
strategy:
matrix:
include:
- old_pg: "10"
new_pg: "17"
- old_pg: "16"
new_pg: "17"
name: 🔄 Binary pg_upgrade ${{ matrix.old_pg }} → ${{ matrix.new_pg }}
runs-on: ubuntu-latest
container: pgxn/pgxn-tools
env:
# Both clusters must use the same initdb options so pg_upgrade sees
# consistent settings (checksums, auth) on old and new clusters.
INITDB_OPTS: --data-checksums --auth trust
steps:
- name: Start PostgreSQL ${{ matrix.old_pg }}
run: pg-start ${{ matrix.old_pg }}
- name: Check out the repo
uses: actions/checkout@v4
- name: Recreate old cluster with data checksums enabled
run: .github/scripts/pg_upgrade_cluster recreate-old ${{ matrix.old_pg }}
- name: Install pgtap into the old cluster
# test_factory_pgtap requires pgtap (see prepare-old below). Unlike
# the fresh-install `test` job, which gets this for free because
# pg-start's default cluster already has it, this job recreates the
# cluster from scratch (previous step) with none of the fresh-install
# job's setup -- pgtap must be installed explicitly here too, for
# every PostgreSQL major this job touches, old and new alike.
# --pg_config is explicit (not relying on pg-start having already
# pointed the bare `pg_config` on PATH at this major) so this step's
# correctness doesn't depend on that PATH-switching behavior.
run: pgxn install pgtap --sudo --pg_config /usr/lib/postgresql/${{ matrix.old_pg }}/bin/pg_config
- name: Install test_factory into old cluster
run: make install
- name: Prepare the old cluster (install current version + tap schema)
# test_factory_pgtap needs pgtap installed into a dedicated "tap"
# schema BEFORE it installs (see bin/test_existing's prepare-old
# comment) -- a bare CREATE EXTENSION test_factory_pgtap CASCADE with
# no schema prep leaves pgtap wherever the ambient search_path
# resolves, and the suite's SET search_path = tap then can't find
# pgtap's functions.
run: bin/test_existing prepare-old test_factory_upgrade
- name: Install PostgreSQL ${{ matrix.new_pg }}
run: apt-get install -y postgresql-${{ matrix.new_pg }} postgresql-server-dev-${{ matrix.new_pg }}
- name: Install pgtap into the new cluster
# pg_upgrade validates that every extension installed in a database
# being upgraded is also available (control file + library) on the
# NEW cluster's PostgreSQL install -- without this, the upgrade
# itself fails, not just the post-upgrade suite run. --pg_config is
# required here (unlike the previous pgtap install step): this step
# runs before pg-start ever points at the new major, so the plain
# `pg_config` on PATH would still resolve to the OLD version's.
run: pgxn install pgtap --sudo --pg_config /usr/lib/postgresql/${{ matrix.new_pg }}/bin/pg_config
- name: Install test_factory into new cluster
# PG_CONFIG must be specified explicitly: at this point both old and
# new PostgreSQL are installed, and the default pg_config on PATH may
# not be the new version's.
run: make install PG_CONFIG=/usr/lib/postgresql/${{ matrix.new_pg }}/bin/pg_config
- name: Stop old cluster, binary pg_upgrade to PostgreSQL ${{ matrix.new_pg }}, start new cluster
run: .github/scripts/pg_upgrade_cluster upgrade ${{ matrix.old_pg }} ${{ matrix.new_pg }}
- name: Run the suite against the pg_upgraded database (existing mode)
# run-suite dynamically asserts the installed version (never
# hardcoded), then runs the suite via --use-existing (so pg_regress
# does not drop/recreate the database) and gates on verify-results --
# test/install/load.sql's existing-mode branch plants and proves the
# dependency guard as part of this same invocation.
run: bin/test_existing run-suite test_factory_upgrade

# A single stable check name for use as a required status check in branch
# protection rules (not configured by this PR -- that needs a repo admin).
# Matrix jobs produce check names like "🐘 PostgreSQL 14", which would all
# need to be listed individually and updated whenever the matrix changes.
# This job passes if every other job passed or was skipped (e.g. the heavy
# jobs gated off by the `changes` job on a docs-only push), and fails if any
# failed or were cancelled.
all-checks-passed:
needs: [changes, test, pg-upgrade-test]
if: always()
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v4
- name: Verify all jobs are listed in needs
# Ensures this job won't silently ignore a newly-added job that was
# omitted from the needs list above.
run: |
DEFINED=$(python3 -c "
import yaml
with open('.github/workflows/ci.yml') as f:
w = yaml.safe_load(f)
print('\n'.join(sorted(j for j in w['jobs'] if j != 'all-checks-passed')))
")
NEEDED=$(echo '${{ toJson(needs) }}' | python3 -c "
import json, sys
print('\n'.join(sorted(json.load(sys.stdin))))
")
if [ "$DEFINED" != "$NEEDED" ]; then
echo "Some jobs are missing from all-checks-passed needs:"
diff <(echo "$DEFINED") <(echo "$NEEDED")
exit 1
fi
- name: Check all jobs passed or were skipped
run: |
if [[ "${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}" == "true" ]]; then
echo "One or more jobs failed or were cancelled"
exit 1
fi
# vi: expandtab ts=2 sw=2
8 changes: 8 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@ include pgxntool/base.mk

PGXNTOOL_ENABLE_TEST_INSTALL = yes

# Already pgxntool's own default, but pinned explicitly (same reasoning as
# ENABLE_TEST_INSTALL above): CI's `test`/`pg-upgrade-test` jobs gate on
# `make verify-results`, not a bare `make test` -- pgxntool marks
# installcheck .IGNORE, so a plain test run exits 0 even when
# regression.diffs is nonempty. Relying on a default silently protects that
# gate only until pgxntool's own default changes.
PGXNTOOL_ENABLE_VERIFY_RESULTS = yes

# ------------------------------------------------------------------------------
# TEST_LOAD_SOURCE: how test/install/load.sql gets the extension to its
# target state (fresh/update/existing). See test/install/load.sql for what
Expand Down
Loading
Loading