Skip to content

test/README.md: lead with the shared-test-function-library structure #40

test/README.md: lead with the shared-test-function-library structure

test/README.md: lead with the shared-test-function-library structure #40

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 x schema leg (empty/none and Quoted -
# see TEST_SCHEMA in the Makefile).
# extension-update-test -- IN-PLACE update: CREATE EXTENSION at 0.9.6
# (the oldest version we still ship a full
# install script for) then ALTER EXTENSION
# UPDATE (same PostgreSQL, no pg_upgrade), same
# PG x schema matrix as test.
# pg-upgrade-test -- BINARY pg_upgrade: install 0.9.6 on an OLD
# PostgreSQL major, binary-upgrade the cluster
# to a NEWER major, then update the extension to
# current - proves objects created on an old
# server still work when read on a new one. A
# smaller old_pg/new_pg x schema matrix (not the
# full PG matrix - this job is by far the most
# expensive, installing two full PostgreSQL
# majors and running the real pg_upgrade binary
# per leg).
#
# All three matrices cross PostgreSQL major with the TEST_SCHEMA axis:
# without a schema specified at all (empty - CREATE EXTENSION lands wherever
# the session's own default search_path resolves) and with one explicitly
# targeted, using a name that requires SQL identifier quoting. Neither leg is
# redundant with the other - see the Makefile's TEST_SCHEMA comment.
#
# `changes` is a cheap gate that lets all three 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.
# ===========================================================================
name: CI
on: [push, 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 / extension-update-test jobs consume (see the
# "Derive ..." step): 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.
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 }}
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: both the fresh-install
# `test` matrix and the `extension-update-test` matrix derive their
# PostgreSQL set from this ONE source, so they cannot silently
# drift onto different lists. 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"
# Baseline: fresh CREATE EXTENSION, across the PG matrix AND a schema
# matrix (TEST_SCHEMA, picked up from the environment by test/deps.sql via
# the count_nulls.test_schema GUC - see the Makefile). Empty ('') runs
# WITHOUT specifying a schema at all - CREATE EXTENSION with no targeting,
# landing wherever the session's own default search_path resolves.
# 'Quoted' runs WITH one explicitly specified, using a name that requires
# SQL identifier quoting (mixed case - unquoted would fold to lowercase),
# exercising the suite's %I schema-qualification rather than just its
# literal test data. Both legs matter; neither is redundant with the
# other (see the Makefile's TEST_SCHEMA comment).
test:
needs: [changes]
if: needs.changes.outputs.docs_only != 'true'
strategy:
matrix:
# From the single source in the changes job.
pg: ${{ fromJSON(needs.changes.outputs.supported_pg) }}
schema: ["", Quoted]
name: 🐘 PostgreSQL ${{ matrix.pg }} (schema ${{ matrix.schema == '' && 'none' || matrix.schema }})
runs-on: ubuntu-latest
container: pgxn/pgxn-tools
env:
TEST_SCHEMA: ${{ matrix.schema }}
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 }}
run: pg-build-test
# Proves the in-place extension update path: CREATE EXTENSION at the
# oldest version we still ship a full install script for (0.9.6), then
# ALTER EXTENSION UPDATE (no pg_upgrade, same PostgreSQL), then run the
# FULL suite against the updated database (same expected output as a
# fresh install of the same schema, so an updated DB must behave
# identically). 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:
# From the single source in the changes job.
pg: ${{ fromJSON(needs.changes.outputs.supported_pg) }}
schema: ["", Quoted]
name: ⬆️ Extension update test on PostgreSQL ${{ matrix.pg }} (schema ${{ matrix.schema == '' && 'none' || matrix.schema }})
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: Install count_nulls
run: make install
- name: Update 0.9.6 -> current and run the suite (existing mode)
# update-scenario creates a real database at 0.9.6, 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
# blocked a drop, before dropping it itself - see bin/test_existing).
run: bin/test_existing update-scenario count_nulls_update "${{ matrix.schema }}" 0.9.6
# Proves count_nulls survives a BINARY pg_upgrade (in-place catalog
# migration to a newer PostgreSQL major), not just an in-place extension
# update. Installs 0.9.6 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.
# 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.
# The two big-jump legs below already cover the axis that matters here.
# Revisit if count_nulls ever grows something catalog-touching.
pg-upgrade-test:
needs: [changes]
if: needs.changes.outputs.docs_only != 'true'
strategy:
matrix:
include:
- old_pg: "10"
new_pg: "18"
schema: ""
- old_pg: "10"
new_pg: "18"
schema: Quoted
- old_pg: "12"
new_pg: "18"
schema: ""
- old_pg: "12"
new_pg: "18"
schema: Quoted
name: 🔄 Binary pg_upgrade ${{ matrix.old_pg }} → ${{ matrix.new_pg }} (schema ${{ matrix.schema == '' && 'none' || matrix.schema }})
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)
# prepare-old installs count_nulls at 0.9.6 in the leg's schema,
# 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.
run: bin/test_existing prepare-old count_nulls_upgrade "${{ matrix.schema }}" 0.9.6
- 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: 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
# 0.9.6->1.0.0 update script.
run: bin/test_existing update count_nulls_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),
# drops the guard, 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 count_nulls_upgrade "${{ matrix.schema }}"
# 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, test, extension-update-test, pg-upgrade-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