Skip to content

pg-tle-test: extend to the 0.9.6 -> current update path via DESTDIR sandboxing #267

pg-tle-test: extend to the 0.9.6 -> current update path via DESTDIR sandboxing

pg-tle-test: extend to the 0.9.6 -> current update path via DESTDIR sandboxing #267

Workflow file for this run

# ===========================================================================
# Test strategy
#
# count_nulls can be arrived at several ways, each of which can break
# differently, so each is exercised by its own job below:
#
# test -- FRESH install: CREATE EXTENSION at the current
# version, across every supported PostgreSQL
# major. Also proves the IN-PLACE extension
# update path (CREATE EXTENSION at 0.9.6, then
# ALTER EXTENSION UPDATE - same PostgreSQL, no
# pg_upgrade) in the same job/matrix, rather
# than a dedicated job: a load mode is just an
# input the same assertions run against, not a
# real environment difference, so giving it its
# own job would only duplicate this job's own
# per-PG-version container/checkout setup for
# no added confidence.
# pg-upgrade-test -- BINARY pg_upgrade: install 0.9.6 on an OLD
# PostgreSQL major, update the extension to
# current (still on the old major), THEN
# binary-upgrade the cluster to a NEWER major -
# proves pg_upgrade correctly migrates the
# objects the extension actually creates
# TODAY, not objects frozen at some past
# version (which would be untestable anyway -
# that old version already shipped). A smaller
# old_pg/new_pg matrix (not the full PG matrix
# - by far the most expensive job here,
# installing two full PostgreSQL majors and
# running the real pg_upgrade binary per leg).
# pg-tle-test -- pg_tle DEPLOYMENT: fresh install AND the
# 0.9.6 -> current update path, both registered
# through AWS pg_tle's database-backed catalog
# instead of a filesystem .control file.
#
# Every TEST_SCHEMA value (empty - no schema targeting at all - and
# 'Quoted', a name requiring SQL identifier quoting) is exercised in every
# job above too, but never as a CI matrix dimension - a schema name is just
# an input the same assertions run against, not a real environment
# difference, so crossing it into the matrix would only multiply job count
# for no added confidence (see the Makefile's TEST_SCHEMA_VALUES comment).
# `test` loops it (both its fresh and update legs) via `make
# test-schema-all` / `make test-update-schema-all`; `pg-upgrade-test`
# (shell, not `make test`, for the parts that matter here) prepares two
# databases - one per schema -
# ahead of a single pg_upgrade call that migrates both at once, which is
# strictly better than a doubled matrix would have been: it also halves the
# number of actual pg_upgrade binary invocations, not just container/
# checkout overhead. Every leg passes against the SAME
# test/expected/extension_tests.out (see test/README.md for how the suite
# keeps its output schema-invariant).
#
# `changes` is a cheap gate that lets the heavy jobs above skip themselves on
# doc-only pushes, and also derives the shared PostgreSQL-major list those
# jobs consume from a single set of constants. `all-checks-passed` is the
# single stable required-status-check name.
#
# Draft PRs get a further reduction, independent of `changes`/docs_only,
# aimed at cutting shared-runner load while a PR is still being iterated on
# (this repo's org-wide Actions queue backs up easily): `lint` always runs
# in full; `test`'s matrix drops to just the newest supported PostgreSQL
# major (see its own comment) instead of running full or being skipped
# outright, since it's cheap per-leg and a draft author still wants signal
# on every push; every other heavy job (`pg-tle-test`, and `pg-upgrade-test`
# etc. from later phases) is skipped entirely via an added
# `&& github.event.pull_request.draft != true` on its existing `if:`. None
# of this applies to a `push` event (e.g. the post-merge run on master) or
# a non-draft PR, both of which always run the full suite exactly as
# before. `github.event.pull_request.draft` reflects the PR's CURRENT
# draft status at the time each event fires, so once a PR is marked
# ready-for-review, its next actual trigger (a `synchronize` push - this
# workflow's `pull_request:` has no `types:` override, so it only runs on
# the GitHub default of opened/synchronize/reopened, NOT the
# `ready_for_review` action by itself) correctly sees draft=false and runs
# the full suite; the reduced draft-time result on prior commits is not
# retroactively re-run.
# ===========================================================================
name: CI
on:
push:
branches:
- master
pull_request:
jobs:
# Cheap gate that lets the heavy jobs below skip themselves on commits that
# touch only docs. 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.
#
# Also derives, from a SINGLE set of constants, the supported-PostgreSQL-
# major list the test job consumes: every job that cares which majors are
# supported reads the SAME list, so they can't silently drift onto
# different sets, and adding a new major is a one-line change here
# instead of an edit in several jobs. `newest_pg` is the same NEWEST
# constant emitted again as a bare scalar (not wrapped in the JSON-array
# `supported_pg`), consumed only by the `test` job's draft-PR matrix
# reduction (see the top-of-file comment and that job's own comment) - so
# NEWEST still only needs to change in one place.
changes:
name: 🔍 Detect docs-only changes & derive PG matrix
runs-on: ubuntu-latest
outputs:
docs_only: ${{ steps.diff.outputs.docs_only }}
supported_pg: ${{ steps.pg.outputs.supported_pg }}
newest_pg: ${{ steps.pg.outputs.newest_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 to running the full matrix: default docs_only to false
# immediately, before anything below has a chance to compute or
# fail. Writing the same GITHUB_OUTPUT key twice is fine (the last
# write wins), so the only way this step ends with docs_only=true
# is by genuinely proving it further down - never by skipping past
# an edge case with a default.
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. docs_only is already false from above;
# just stop here rather than risk skipping tests.
if [ -z "$HEAD" ] || [ -z "$BASE" ] || [[ "$BASE" =~ ^0+$ ]]; then
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"
- name: Derive the supported-PostgreSQL-major list
id: pg
run: |
# A dozen-odd lines to replace what looks like a handful of version
# references, but it buys CONSISTENCY: the fresh-install/update
# `test` matrix derives its PostgreSQL set from this ONE source, so
# it cannot silently drift onto a different list. Adding a new
# major is a one-line NEWEST bump here, not an edit in N places.
#
# Only one floor is needed here: 0.9.6 (the oldest version
# count_nulls still ships a full install script for) is pure SQL
# over anyarray/json/jsonb with no catalog-version sensitivity, so
# it installs on every PostgreSQL major count_nulls supports -
# there's no separate legacy-only floor to carve out.
NEWEST=18
FLOOR=10
supported=$(seq "$NEWEST" -1 "$FLOOR")
# Emit a JSON array from a list of ints, for the job matrices to
# consume with fromJSON (GitHub evaluates a literal dollar-brace
# expression even inside a run block, so none is written here).
json() { printf '%s\n' "$@" | paste -sd, - | sed 's/^/[/; s/$/]/'; }
echo "supported_pg=$(json $supported)" >> "$GITHUB_OUTPUT"
# Also emitted as a bare scalar (not a JSON array) so the `test`
# job's draft-PR matrix reduction (see its own comment) can build a
# single-element list from it via fromJSON(format(...)) without a
# second hardcoded "18" anywhere in this file.
echo "newest_pg=$NEWEST" >> "$GITHUB_OUTPUT"
lint:
name: 🧹 SQL lint
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v4
- name: make lint
# Deliberately not pre-initializing the .vendor/linter submodule via
# `submodules:` above -- letting `make lint` self-init it (lint.mk)
# is what actually proves that works from a plain clone.
run: make lint
# Fresh install, then the in-place extension update path, both across the
# PG matrix. The update leg CREATE EXTENSIONs at the oldest version we
# still ship a full install script for (0.9.6), then ALTER EXTENSION
# UPDATEs to current (no pg_upgrade, same PostgreSQL) and reruns the
# suite - a single job rather than a dedicated one, since a load mode is
# just an input the same assertions run against, not a real environment
# difference (same reasoning as TEST_SCHEMA below), and the per-version
# container/checkout setup would otherwise be duplicated across two jobs
# with the same PG matrix. Every TEST_SCHEMA value (empty - no schema
# targeting at all - and 'Quoted', a name requiring SQL identifier
# quoting) is exercised too, via `make test-schema-all`'s in-Makefile
# loop rather than a CI matrix dimension - a schema name is just an
# input the same assertions run against, not a real environment
# difference, so crossing it into the matrix would only multiply job
# count for no added confidence (see the Makefile's TEST_SCHEMA_VALUES
# comment). Every leg passes against the SAME
# test/expected/extension_tests.out (see test/README.md for how the
# suite keeps its output schema-invariant).
test:
needs: [changes]
if: needs.changes.outputs.docs_only != 'true'
strategy:
matrix:
# From the single source in the changes job. On a draft PR, reduced
# to just the newest supported major (never skipped outright, unlike
# the other heavy jobs below - this is the one signal a draft author
# still wants on every push): `github.event.pull_request.draft` is
# null/falsy for a push event (e.g. the post-merge run on master), so
# this expression falls through to the full list there with no extra
# guard needed.
pg: ${{ github.event.pull_request.draft && fromJSON(format('[{0}]', needs.changes.outputs.newest_pg)) || fromJSON(needs.changes.outputs.supported_pg) }}
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@v4
- name: Test on PostgreSQL ${{ matrix.pg }}, across every TEST_SCHEMA value
run: make test-schema-all
- name: Install count_nulls
run: make install
- name: Update 0.9.6 -> current and run the suite, across every TEST_SCHEMA value
run: make test-update-schema-all
- name: Structurally compare the updated objects against a fresh install, across every TEST_SCHEMA value
# A fixed pgTAP suite only proves the specific behaviors it asserts
# still hold; it can't catch an update script that leaves some
# definition/comment/ACL subtly different from what a fresh install
# of the same version produces. bin/compare_fresh_vs_update installs
# both ways itself (in its own scratch databases) and diffs every
# object the extension owns - any nonempty diff fails the step. Not
# a make target (it's a standalone script, not `make test`), so
# looped directly here rather than via test-schema-all.
run: |
for schema in "" Quoted; do
echo "=== schema=$schema ==="
bin/compare_fresh_vs_update "$schema" 0.9.6 || exit 1
done
# Proves count_nulls survives a BINARY pg_upgrade (in-place catalog
# migration to a newer PostgreSQL major). Installs 0.9.6 on an old
# cluster, plants a dependency guard, updates the extension to CURRENT
# (still on the old major), THEN binary-pg_upgrades to a newer cluster,
# then runs the suite against the REAL migrated objects in existing mode.
# Updating before the binary upgrade (not after) is deliberate: the whole
# point of this job is proving pg_upgrade correctly migrates the objects
# count_nulls' CURRENT code actually creates - migrating 0.9.6's objects
# and updating afterward would instead test whether pg_upgrade can
# migrate a legacy structure frozen in the past, which isn't actionable
# (that version already shipped; nothing to fix if it turned out
# fragile). No bridge-update step first: count_nulls has always been
# pure SQL functions with no SELECT-*-over-catalog views, so it has no
# known pg_upgrade-unsafe old version to bridge past.
#
# Deliberately not doing a stepwise every-major-in-sequence climb (one
# cluster walking 10->11->12->...->newest, vs. the single big jumps here):
# that would catch a regression specific to one particular major-to-major
# boundary, which would matter if count_nulls had views/functions touching
# catalog internals, but it doesn't - pure SQL functions over anyarray/
# json/jsonb, nothing version-sensitive to break at a specific boundary.
# Revisit if count_nulls ever grows something catalog-touching.
#
# Every TEST_SCHEMA value is exercised here too, but NOT via a matrix
# dimension (would double this job's already-expensive count) and not
# via a make-level loop either (bin/test_existing's steps below are
# shell, not `make test`) - instead, TWO databases (one per schema) are
# prepared before the SINGLE pg_upgrade call, which migrates the WHOLE
# cluster (every database in it) in one pass. This is strictly better
# than a doubled matrix would have been, not just cheaper: it also
# halves the number of actual pg_upgrade binary invocations (the single
# most expensive operation in this job) instead of just avoiding
# redundant container/checkout overhead.
pg-upgrade-test:
needs: [changes]
# Skipped outright (not just matrix-reduced like `test`) on a draft PR:
# this is a heavy job, and a draft author doesn't need a real binary
# pg_upgrade re-proven on every push while still iterating.
if: needs.changes.outputs.docs_only != 'true' && github.event.pull_request.draft != true
strategy:
matrix:
old_pg: ["10", "12"]
new_pg: ["18"]
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 or pg_upgrade
# refuses to run.
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@v4
- name: Install count_nulls into old cluster
run: make install
- name: Prepare the old cluster (install + dependency guard), across every TEST_SCHEMA value
# prepare-old installs count_nulls at 0.9.6, then plants + proves
# the dependency guard, so a later accidental CASCADE drop anywhere
# in this job cannot silently make the eventual existing-mode run
# test a fresh install instead. Two separate databases (distinct
# names, one per TEST_SCHEMA value) so both exist in the SAME
# cluster ahead of the single pg_upgrade call below - that one
# binary upgrade migrates both at once.
run: |
bin/test_existing prepare-old count_nulls_upgrade_none "" 0.9.6
bin/test_existing prepare-old count_nulls_upgrade_quoted Quoted 0.9.6
- name: Update the extension to the current version (still on the old cluster), across every TEST_SCHEMA value
# Exercises ALTER EXTENSION UPDATE on the OLD cluster, BEFORE the
# binary pg_upgrade below, running the 0.9.6->stable update script,
# once per database prepared above - deliberately in this order
# (not update-after-upgrade): this job exists to prove pg_upgrade
# correctly migrates the objects count_nulls' CURRENT code creates,
# so pg_upgrade must run against already-current objects, not 0.9.6
# ones. `make install` above already installed the current
# version's update scripts/control file into this (old) cluster's
# sharedir, so they're in place for this ALTER EXTENSION UPDATE to
# use.
run: |
bin/test_existing update count_nulls_upgrade_none
bin/test_existing update count_nulls_upgrade_quoted
- name: Install PostgreSQL ${{ matrix.new_pg }}
run: apt-get install -y postgresql-${{ matrix.new_pg }} postgresql-server-dev-${{ matrix.new_pg }}
- name: Install count_nulls 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: Run the suite against the pg_upgraded database (existing mode), across every TEST_SCHEMA value
# run-suite asserts the version, re-proves the dependency guard
# still blocks a non-CASCADE drop (i.e. it survived both the update
# and pg_upgrade), drops the guard, then runs the suite against the
# REAL pg_upgraded 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 count_nulls_upgrade_none ""
bin/test_existing run-suite count_nulls_upgrade_quoted Quoted
- name: Structurally compare the pg_upgraded database against a fresh install, across every TEST_SCHEMA value
# Same rationale as the test job's own update leg's use of this tool
# (see above), but here the "other side" is the REAL database a binary
# pg_upgrade + ALTER EXTENSION UPDATE just produced, not a scratch
# database this tool created itself - passed as EXISTING_DB so the
# script queries it in place instead of re-deriving it. Catches a
# divergence class the fixed pgTAP suite above doesn't: an object
# left subtly different (body, comment, ACL) by surviving a real
# catalog migration, as opposed to only an in-place update. Each
# pg_upgraded database is compared against a fresh install in ITS
# OWN schema, matching prepare-old above.
run: |
bin/compare_fresh_vs_update "" 0.9.6 count_nulls_upgrade_none
bin/compare_fresh_vs_update Quoted 0.9.6 count_nulls_upgrade_quoted
# Covers both a fresh install AND the 0.9.6 -> current update path, both
# purely via pg_tle. pgxntool 2.3.0's fix for installcheck's ordering bug
# (Postgres-Extensions/pgxntool#83) made `installcheck` (and so `make
# test`) unconditionally depend on `install`, which writes a real
# .control file to disk - defeating the entire point of proving a pg_tle
# deployment never touches the filesystem. There's still no upstream fix
# for that (Postgres-Extensions/pgxntool#90, open) that would let
# bin/test_existing's real pgTAP suite run without it, so the update-path
# steps below use TEST_EXISTING_DEPLOY=pgtle (see bin/test_existing),
# which instead sandboxes `make test`'s install step behind a scratch
# DESTDIR - harmless here since a pg_tle-deployed database never needs
# those files.
pg-tle-test:
needs: [changes]
# Skipped outright (not just matrix-reduced like `test` above) on a
# draft PR: this is a heavy job, and a draft author doesn't need the
# pg_tle deployment path re-proven on every push while still iterating.
if: needs.changes.outputs.docs_only != 'true' && github.event.pull_request.draft != true
strategy:
matrix:
# Intersection of count_nulls' own supported range (10-18, see the
# `test` job above / #21) with pg_tle 1.5.2's supported range (12-18,
# see pgxntool/pgtle_versions.md): drop 10 and 11 since pg_tle doesn't
# support them.
pg: [18, 17, 16, 15, 14, 13, 12]
name: 🧩 pg_tle ${{ matrix.pg }}
runs-on: ubuntu-latest
container: pgxn/pgxn-tools
env:
# NOT named PGTLE_VERSION: on the pgxntool version this job was
# written against, that collided with pgxntool's own Makefile variable
# of the same name (`pgtle:`'s
# `$(if $(PGTLE_VERSION),--pgtle-version $(PGTLE_VERSION))`), which Make
# auto-imports from the environment - `make run-pgtle` would silently
# generate into pg_tle/1.5.2/ (the literal version) instead of the
# correct range directory pg_tle/1.5.0+/, then fail when --run looked
# for the range directory and didn't find it. Filed as
# Postgres-Extensions/pgxntool#78, fixed upstream in pgxntool 2.3.0
# (renamed to PGXNTOOL_PGTLE_VERSION - see this repo's pgxntool bump).
# Keeping our own name distinct from EITHER of those regardless -
# still the simplest way to guarantee no future collision.
PG_TLE_BUILD_VERSION: "1.5.2"
steps:
# A dedicated cluster, never shared with the other jobs in this
# workflow: pg_tle requires shared_preload_libraries and mixing
# pg_tle/non-pg_tle extension installs on one cluster can misbehave.
- name: Start PostgreSQL ${{ matrix.pg }}
run: pg-start ${{ matrix.pg }}
- name: Check out the repo
uses: actions/checkout@v4
- name: Install rsync
run: apt-get install -y rsync
- name: Snapshot filesystem extension control files (pre-pgtap baseline)
# Whatever ships on disk by default (e.g. contrib), before installing
# pgTAP. Lets the next step prove pgTAP is the ONLY thing `make pgtap`
# puts on disk, instead of trusting that and folding whatever it did
# into the pre-pg_tle baseline unexamined -- a future pgxntool change
# to `make pgtap` that also happened to touch count_nulls' own files
# would otherwise be silently absorbed into that baseline and never
# get flagged by any later check.
run: bin/assert_fs_clean snapshot ${{ matrix.pg }} /tmp/pre_pgtap_baseline.txt
- name: Install pgtap (test harness dependency)
# pgTAP is a filesystem-installed dependency of the TEST HARNESS, not
# part of what this job proves is pg_tle-only -- it's not being
# deployed via pg_tle here, and never will be.
run: make pgtap
- name: Verify make pgtap installed exactly pgtap.control, nothing else
run: bin/assert_fs_clean verify ${{ matrix.pg }} /tmp/pre_pgtap_baseline.txt pgtap.control
- name: Snapshot filesystem extension control files (pre-pg_tle baseline)
# Everything on disk now that pgtap is confirmed the only addition
# (contrib, pgtap). bin/assert_fs_clean's later checks diff against
# this, so they flag ANY extension that lands on disk instead of
# being registered via pg_tle -- not just count_nulls -- without
# hardcoding contrib/pgtap names.
run: bin/assert_fs_clean snapshot ${{ matrix.pg }} /tmp/control_baseline.txt
- name: Build and install pg_tle ${{ env.PG_TLE_BUILD_VERSION }}
# flex/bison/libkrb5-dev aren't in the pgxn-tools image; pg_tle's build
# needs them (guc-file.l, and clientauth.c includes gssapi.h).
run: |
apt-get install -y flex bison libkrb5-dev
git clone --branch v${{ env.PG_TLE_BUILD_VERSION }} --depth 1 https://github.com/aws/pg_tle.git /tmp/pg_tle
make -C /tmp/pg_tle install
- name: Enable pg_tle and restart PostgreSQL ${{ matrix.pg }}
run: |
echo "shared_preload_libraries = 'pg_tle'" >> /etc/postgresql/${{ matrix.pg }}/test/postgresql.conf
pg_ctlcluster ${{ matrix.pg }} test restart
pg_isready -t 30
- name: Register pg_tle + count_nulls against template1
# template1, not the ambient default db: pg_tle's registration catalog
# is per-database, and `createdb` only inherits it because it copies
# template1 by default. Every count_nulls database used below (the
# smoke-test db) is created AFTER this step specifically so it
# inherits both registrations.
run: |
psql -d template1 -c "CREATE EXTENSION pg_tle"
PGDATABASE=template1 make run-pgtle
- name: Verify no stray extension control files landed on the filesystem
# CRITICAL, and intentionally redundant with the count_nulls-specific
# check in the next step: a filesystem control file silently wins
# over a pg_tle-registered extension of the same name, which would
# make this whole job a false pass without ever raising an error. Run
# again after every step below that could plausibly write extension
# files to disk -- never trust a single check to catch everything.
run: bin/assert_fs_clean verify ${{ matrix.pg }} /tmp/control_baseline.txt pg_tle.control
- name: Install count_nulls purely via pg_tle (fresh install, no filesystem trace)
# count_nulls is never `make install`ed in this job, so a successful
# CREATE EXTENSION here can only be resolving through pg_tle's
# registration, not a control file on disk. Checked explicitly here
# too (not just via the comprehensive check above) as a guard
# specifically for the extension under test, in case that check's
# exclude-list logic has a bug.
run: |
test ! -e /usr/share/postgresql/${{ matrix.pg }}/extension/count_nulls.control
createdb count_nulls_smoke
psql -d count_nulls_smoke -c "CREATE EXTENSION count_nulls"
- name: Verify count_nulls works when deployed via pg_tle
run: |
INSTALLED=$(psql -d count_nulls_smoke -tAc "SELECT extversion FROM pg_extension WHERE extname = 'count_nulls'")
# EXTENSION_count_nulls_VERSION (the .control file's default_version),
# NOT PGXNVERSION (the PGXN distribution version, from META.in.json)
# -- a version-less CREATE EXTENSION installs whatever the control
# file's default_version says, and count_nulls' is currently the
# 'stable' pseudo-version, not the last real release. See
# RELEASE.md's note on distribution vs. extension versions.
EXPECTED=$(make -s print-EXTENSION_count_nulls_VERSION 2>/dev/null | sed -n 's/.*set to "\(.*\)"$/\1/p')
echo "installed=$INSTALLED expected=$EXPECTED"
if [ -z "$INSTALLED" ] || [ -z "$EXPECTED" ] || [ "$INSTALLED" != "$EXPECTED" ]; then
echo "FAIL: installed='$INSTALLED' expected='$EXPECTED'"; exit 1
fi
RESULT=$(psql -d count_nulls_smoke -v ON_ERROR_STOP=1 -tAc "SELECT null_count(1, NULL, 2)")
echo "null_count(1, NULL, 2)=$RESULT"
if [ "$RESULT" != "1" ]; then
echo "FAIL: expected null_count(1, NULL, 2) = 1, got '$RESULT'"; exit 1
fi
- name: Verify no stray extension control files after the fresh-install smoke test
run: bin/assert_fs_clean verify ${{ matrix.pg }} /tmp/control_baseline.txt pg_tle.control
- name: Install count_nulls at 0.9.6, purely via pg_tle (update-path prep)
# A SECOND, separate scratch database, created after the template1
# registration above so it inherits both registrations too (same
# reasoning as count_nulls_smoke). prepare-old creates the database,
# CREATE EXTENSIONs at 0.9.6 (pure SQL - resolves through pg_tle's
# catalog, no `make install` call, which would defeat the whole
# point), then plants + proves the dependency guard so a stray
# CASCADE drop anywhere below can't silently turn the eventual
# existing-mode run into a fresh install instead.
run: |
test ! -e /usr/share/postgresql/${{ matrix.pg }}/extension/count_nulls.control
bin/test_existing prepare-old count_nulls_pgtle_update "" 0.9.6
- name: Verify no stray extension control files after installing 0.9.6 via pg_tle
run: bin/assert_fs_clean verify ${{ matrix.pg }} /tmp/control_baseline.txt pg_tle.control
- name: Update 0.9.6 -> current, purely via pg_tle
# Pure SQL (ALTER EXTENSION ... UPDATE), no filesystem write either.
run: bin/test_existing update count_nulls_pgtle_update
- name: Verify no stray extension control files after the pg_tle update
run: bin/assert_fs_clean verify ${{ matrix.pg }} /tmp/control_baseline.txt pg_tle.control
- name: Run the real pgTAP suite against the pg_tle-updated database (existing mode)
# run-suite re-proves the dependency guard, drops it, then runs the
# FULL suite via --use-existing against the real pg_tle-deployed +
# updated database - the same suite/expected-output as every other
# leg (see test/README.md). TEST_EXISTING_DEPLOY=pgtle makes
# run-suite sandbox `make test`'s otherwise-unavoidable `install`
# step behind a scratch DESTDIR instead of writing to the real
# extension directory (see bin/test_existing's TEST_EXISTING_DEPLOY
# comment), and makes test/install/load.sql's existing-mode
# assertion cross-check pgtle.available_extensions() instead of
# pg_available_extensions (which never sees pg_tle registrations -
# see the Makefile's TEST_EXISTING_DEPLOY comment).
run: TEST_EXISTING_DEPLOY=pgtle bin/test_existing run-suite count_nulls_pgtle_update ""
- name: Verify no stray extension control files after the pgTAP suite
# THE step that actually proves the DESTDIR sandboxing worked: the
# real extension directory must still be clean after `make test`
# ran (with its otherwise-unavoidable `install` step) sandboxed
# behind a scratch DESTDIR.
run: bin/assert_fs_clean verify ${{ matrix.pg }} /tmp/control_baseline.txt pg_tle.control
# A single stable check name for use as a required status check in branch
# protection rules. Matrix jobs produce check names like
# "🐘 PostgreSQL 14 (schema none)" 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, lint, test, pg-upgrade-test, pg-tle-test]
if: always()
runs-on: ubuntu-latest
steps:
- 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