Skip to content

Overhaul the test harness: committed-once install, load modes, existing-mode CI #234

Overhaul the test harness: committed-once install, load modes, existing-mode CI

Overhaul the test harness: committed-once install, load modes, existing-mode CI #234

Workflow file for this run

name: CI
on:
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. This must run on every push/pull_request (no
# paths-ignore on the workflow itself), otherwise the required
# all-checks-passed check would never report on doc-only pushes and get
# stuck Pending in branch protection.
changes:
name: 🔍 Detect docs-only changes
runs-on: ubuntu-latest
outputs:
docs_only: ${{ steps.diff.outputs.docs_only }}
steps:
- name: Check out the repo
uses: actions/checkout@v6
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: |
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"
# Fail-safe: 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. Run the full matrix rather
# than risk skipping tests.
if [ -z "$HEAD" ] || [ -z "$BASE" ] || [[ "$BASE" =~ ^0+$ ]]; then
echo "docs_only=false" >> "$GITHUB_OUTPUT"
exit 0
fi
CHANGED=$(git diff --name-only "$BASE" "$HEAD" || echo __DIFF_FAILED__)
DOCS_ONLY=true
if [ "$CHANGED" = "__DIFF_FAILED__" ] || [ -z "$CHANGED" ]; then
DOCS_ONLY=false
else
while IFS= read -r f; do
if ! [[ "$f" =~ \.(md|asc)$ ]]; then
DOCS_ONLY=false
break
fi
done <<< "$CHANGED"
fi
echo "changed files:"
echo "$CHANGED"
echo "docs_only=$DOCS_ONLY" >> "$GITHUB_OUTPUT"
test:
needs: [changes]
if: needs.changes.outputs.docs_only != 'true'
strategy:
matrix:
pg: [18, 17, 16, 15, 14, 13, 12, 11, 10]
name: 🐘 PostgreSQL ${{ matrix.pg }}
runs-on: ubuntu-latest
container: pgxn/pgxn-tools
steps:
- name: Start PostgreSQL ${{ matrix.pg }}
run: pg-start ${{ matrix.pg }}
- name: Check out the repo
uses: actions/checkout@v6
- name: Install rsync
run: apt-get install -y rsync
- name: Test on PostgreSQL ${{ matrix.pg }}
run: make test
# Proves cat_tools survives a BINARY pg_upgrade (in-place catalog migration to a
# newer PostgreSQL major), not just an in-place extension update. Installs an old
# cat_tools on an old cluster, plants a dependency guard, binary-pg_upgrades to a
# newer cluster, updates the extension to current, then runs the suite against
# the REAL migrated objects in "existing" mode. Catches view/catalog breakage
# that only surfaces when objects created on an old server are read on a new one.
pg-upgrade-test:
needs: [changes]
if: needs.changes.outputs.docs_only != 'true'
strategy:
matrix:
include:
# old_pg=10: install an old cat_tools on the oldest PostgreSQL, then
# BRIDGE-update to 0.2.3 (the current version) on PG10 BEFORE pg_upgrade
# -- modelling a user who updates the extension, then binary-upgrades to
# a newer major.
#
# old_install SHOULD be 0.2.0: proving that a database installed at the
# OLDEST version can update to 0.2.3 and then survive binary pg_upgrade
# is the whole point of this leg. It is TEMPORARILY pinned to a FRESH
# 0.2.2 because a 0.2.0-origin database cannot yet survive the journey --
# the shipped 0.2.x update scripts have fresh-vs-update divergences that
# break on newer PostgreSQL. Specifically cat_tools.trigger__parse as
# produced by the update scripts hardcodes `EXECUTE PROCEDURE` (PG11+
# needs an `EXECUTE (?:PROCEDURE|FUNCTION)` match) and omits the
# empty-args guard, so it errors with "cannot determine type of empty
# array" when the suite parses a trigger (surfaces on PG18). (The
# pg_class_v view divergence is already repaired by 0.2.2->0.2.3.) A
# FRESH 0.2.2 has the corrected trigger__parse and already strips
# relhasoids/relhaspkey (fixed `!= ALL` omit_column), so it is both
# pg_upgrade-safe and free of that divergence.
#
# Restore old_install=0.2.0 once the 0.2.2->0.2.3 repair also fixes
# trigger__parse (and any remaining divergence) -- that work is in
# progress. Meanwhile the 0.2.0-only install and its 0.2.2->0.2.3 view
# rebuild stay covered by extension-update-test on PG10.
- old_pg: "10"
new_pg: "11"
old_install: "0.2.2"
bridge_to: "0.2.3"
- old_pg: "10"
new_pg: "18"
old_install: "0.2.2"
bridge_to: "0.2.3"
# old_pg>=11: such users are already on 0.2.2+. A FRESH 0.2.2 install
# already strips relhasoids/relhaspkey (fixed `!= ALL` omit_column), so
# it is pg_upgrade-safe with no bridge. pg_upgrade, then update to current.
- old_pg: "11"
new_pg: "12"
old_install: "0.2.2"
bridge_to: ""
- old_pg: "11"
new_pg: "18"
old_install: "0.2.2"
bridge_to: ""
- old_pg: "12"
new_pg: "13"
old_install: "0.2.2"
bridge_to: ""
- old_pg: "12"
new_pg: "18"
old_install: "0.2.2"
bridge_to: ""
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: Recreate old cluster with data checksums enabled
run: |
pg_ctlcluster ${{ matrix.old_pg }} test stop
pg_dropcluster ${{ matrix.old_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 ${{ matrix.old_pg }} test -- $INITDB_OPTS
pg_ctlcluster ${{ matrix.old_pg }} test start
pg_isready -t 30
- name: Check out the repo
uses: actions/checkout@v6
- name: Install rsync
run: apt-get install -y rsync
- name: Install cat_tools into old cluster
run: make install
- name: Prepare the old cluster (install + bridge + dependency guard)
# prepare-old installs cat_tools at old_install and, when bridge_to is set
# (old_pg=10), ALTER EXTENSION UPDATEs to it BEFORE pg_upgrade -- the
# bridge that makes the views pg_upgrade-safe (see prepare-old in the
# script). It then plants + proves the dependency guard so the later
# existing-mode run cannot silently drop+reinstall and test a fresh
# install instead. The extension is always older than or equal to
# current, so the post-upgrade ALTER EXTENSION UPDATE is exercised too.
run: >-
bin/test_existing prepare-old cat_tools_upgrade
"${{ matrix.old_install }}" "${{ matrix.bridge_to }}"
- name: Install PostgreSQL ${{ matrix.new_pg }}
run: apt-get install -y postgresql-${{ matrix.new_pg }} postgresql-server-dev-${{ matrix.new_pg }}
- name: Install cat_tools 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: |
pg_ctlcluster ${{ matrix.old_pg }} test stop
pg_createcluster -p 5432 ${{ matrix.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/${{ matrix.new_pg }}/bin/pg_upgrade \
-b /usr/lib/postgresql/${{ matrix.old_pg }}/bin \
-B /usr/lib/postgresql/${{ matrix.new_pg }}/bin \
-d /var/lib/postgresql/${{ matrix.old_pg }}/test \
-D /var/lib/postgresql/${{ matrix.new_pg }}/test \
-o '-c config_file=/etc/postgresql/${{ matrix.old_pg }}/test/postgresql.conf' \
-O '-c config_file=/etc/postgresql/${{ matrix.new_pg }}/test/postgresql.conf'" postgres \
|| { find /tmp/pg_upgrade_logs \
/var/lib/postgresql/${{ matrix.new_pg }}/test/pg_upgrade_output.d \
-name '*.log' 2>/dev/null | sort | xargs -r tail -n +1; exit 1; }
pg_ctlcluster ${{ matrix.new_pg }} test start
- name: Update the pg_upgraded extension to the current version
# Exercises ALTER EXTENSION UPDATE on genuinely pg_upgraded objects (the
# extension binary pg_upgrade just migrated), running the
# version-to-version scripts up to the current version. A no-op when the
# bridge already reached current (old_pg=10), which still verifies UPDATE
# does not error on pg_upgraded objects.
run: bin/test_existing update cat_tools_upgrade
- name: Run the suite against the pg_upgraded database (existing mode)
# run-suite asserts the version, re-proves the dependency guard still
# blocks a non-CASCADE drop (i.e. it survived pg_upgrade), then runs the
# suite against the REAL pg_upgraded + updated database via --use-existing
# (so pg_regress does not drop/recreate it) -- a plain fresh `make test`
# would silently test a fresh install instead of the migrated objects.
run: bin/test_existing run-suite cat_tools_upgrade
# Proves the in-place extension update path: CREATE EXTENSION at an OLD cat_tools
# version then ALTER EXTENSION UPDATE (no pg_upgrade, same PostgreSQL). On PG12+
# it updates 0.2.2 -> current and runs the FULL suite against the updated
# database (same expected output as a fresh install, so an updated DB must behave
# identically). The PG10 leg only exercises the pre-0.2.2 update scripts, the
# sole version where they still load. Complements pg-upgrade-test, which covers
# the cross-major-version binary upgrade instead.
extension-update-test:
needs: [changes]
if: needs.changes.outputs.docs_only != 'true'
strategy:
matrix:
# The update-scenario legs (0.2.2 -> current + full suite, existing mode)
# run on PG12-18. 0.2.2 is the update-from floor: the 0.2.0/0.2.1 install
# scripts fail on PG11+/PG12+.
# PG10 is the ONLY version where the pre-0.2.2 install scripts still load,
# so it is the only place the 0.2.0->0.2.2 / 0.2.1->0.2.2 update scripts
# and the 0.2.2->0.2.3 view rebuild on the broken path can be exercised.
# It runs only those legacy checks (no current-version suite; that is
# covered on PG10 by the `test` job). See the per-step `if` guards.
pg: [18, 17, 16, 15, 14, 13, 12, 10]
name: ⬆️ Extension update test on PostgreSQL ${{ matrix.pg }}
runs-on: ubuntu-latest
container: pgxn/pgxn-tools
steps:
- name: Start PostgreSQL ${{ matrix.pg }}
run: pg-start ${{ matrix.pg }}
- name: Check out the repo
uses: actions/checkout@v6
- name: Install rsync
run: apt-get install -y rsync
- name: Install cat_tools (all versions)
run: make install
- name: Test pre-0.2.2 update scripts + 0.2.2→0.2.3 rebuild (PG10 only)
# 0.2.0/0.2.1 install only on PG10; their update scripts target 0.2.2 (not
# the current version) and are otherwise never exercised. PG takes the
# shortest path, so 0.2.0 → 0.2.2 uses the direct 0.2.0--0.2.2 script;
# starting at 0.2.1 exercises the 0.2.1--0.2.2 script directly. update-check
# asserts each lands on 0.2.2.
#
# The third check exercises the REAL broken path end to end: a 0.2.0
# install on PG10 leaves relhasoids in _cat_tools.pg_class_v (the buggy
# 0.2.0--0.2.2 omit_column no-op), and updating to 0.2.3 routes through
# 0.2.2--0.2.3 (shortest path 0.2.0--0.2.2 then 0.2.2--0.2.3), firing the
# conditional rebuild that strips relhasoids. On PG12+ relhasoids never
# existed, so only PG10 exercises the rebuild. The psql assertion fails
# loudly if the rebuild did not fire (relhasoids still present) --
# complementing the stronger 10→18 pg_upgrade bridge leg. `$$` is escaped
# as `\$\$` so the shell passes literal dollar quotes through to psql.
if: matrix.pg == '10'
run: |
bin/test_existing update-check cat_tools_from_020 0.2.0 0.2.2
bin/test_existing update-check cat_tools_from_021 0.2.1 0.2.2
bin/test_existing update-check cat_tools_rebuild_020 0.2.0 0.2.3
psql -d cat_tools_rebuild_020 -v ON_ERROR_STOP=1 -c "DO \$\$ BEGIN IF EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid='_cat_tools.pg_class_v'::regclass AND attname='relhasoids' AND NOT attisdropped AND attnum>0) THEN RAISE EXCEPTION 'pg_class_v still exposes relhasoids after update through 0.2.2->0.2.3 -- rebuild did not fire'; END IF; END \$\$"
- name: Update 0.2.2 → current and run the suite (existing mode, PG12+)
# update-scenario creates a real database at 0.2.2, plants + proves the
# dependency guard, ALTER EXTENSION UPDATEs to the current version, and
# runs the suite against that updated database in existing mode (asserting
# the version and that the guard still blocks a drop). Reusing the SAME
# suite and expected output asserts the updated database behaves
# identically to a fresh install.
if: matrix.pg != '10'
run: bin/test_existing update-scenario cat_tools_update 0.2.2
# A single stable check name for use as a required status check in branch
# protection rules. 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 all others passed or were 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, extension-update-test]
if: always()
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- 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