44# An object_reference install can be arrived at more than one way, each of
55# which can break differently, so each is exercised by its own job below:
66#
7- # - `lint`: the cheapest possible check (no database, no container beyond
8- # a plain checkout, seconds to run) -- gates everything else so a broken
9- # style baseline never ties up runner slots on the heavier jobs below.
10- #
117# - `changes`: cheap docs-only gate, PLUS the single source of truth for
128# the supported-PostgreSQL-major list every other job's matrix derives
139# from (see its own "Derive ..." step).
3430# crossing this axis against every major would just multiply job count
3531# for no added coverage.
3632#
37- # - No binary pg_upgrade job (the cat_tools reference this effort is
38- # modeled on has `pg-upgrade-test` / `pg-upgrade-stepwise`) exists yet.
39- # Deliberate, not an oversight: that job exists to catch a view/function
40- # that breaks across a PostgreSQL major specifically because it touches
41- # catalog internals (SELECT * over a system catalog whose columns get
42- # added/exposed/removed between majors). object_reference has no such
43- # construct in either its current or 0.1.0 install script (checked
44- # directly -- no view or function selects * from a system catalog; every
45- # object_reference table/view is an ordinary user object), so the risk
46- # that job protects against is correspondingly low here. Left as
47- # explicitly-noted future work rather than built preemptively; revisit
48- # if/when object_reference grows a catalog-touching view or function.
33+ # - `pg-upgrade-stepwise`: BINARY pg_upgrade coverage. ONE cluster starts
34+ # at the oldest supported PostgreSQL major with the one real historical
35+ # PGXN release (0.1.0) installed, updates straight to the current
36+ # version, then climbs every later supported major in sequence
37+ # (e.g. 12→13→...→18) via a REAL binary pg_upgrade per step, running the
38+ # full suite (existing mode) and re-proving the dependency guard after
39+ # EVERY step. This catches a regression specific to one particular
40+ # major-to-major boundary that a single before/after snapshot would
41+ # never exercise -- a view/function that breaks because it touches
42+ # catalog internals (columns added/exposed/removed between majors) is
43+ # the concrete risk. No such construct has been found in object_reference
44+ # today (checked directly -- no view or function selects * from a system
45+ # catalog; every object_reference table/view is an ordinary user
46+ # object), but that is a fact about the code today, not a permanent
47+ # property of it, and this job is cheap (the same install+pg_upgrade
48+ # shape as any other pg_upgrade leg, just run once per step) -- so
49+ # "unlikely to catch anything today" is not treated as a reason to skip
50+ # it; only genuine cost would be. There is no separate "single big jump"
51+ # pg_upgrade job (cat_tools's `pg-upgrade-test`) -- with only one
52+ # historical extension version and no identified PostgreSQL-version
53+ # floor for it, a big-jump leg would add a second job with no coverage
54+ # the stepwise climb doesn't already provide.
4955#
5056# - `all-checks-passed`: single stable required-status-check name; see its
5157# own comment below.
@@ -66,15 +72,17 @@ jobs:
6672 # all-checks-passed check would then never report and get stuck Pending in
6773 # branch protection.
6874 #
69- # Also derives the supported-PostgreSQL-major list the test job's matrix
70- # consumes, from a single pair of constants below, so adding or dropping a
71- # major is a one-line edit here instead of touching the matrix directly.
75+ # Also derives the supported-PostgreSQL-major lists the test job's matrix
76+ # and the pg-upgrade-stepwise job's climb consume, from a single pair of
77+ # constants below, so adding or dropping a major is a one-line edit here
78+ # instead of touching either job directly.
7279 changes :
7380 name : 🔍 Detect changes & derive PG matrix
7481 runs-on : ubuntu-latest
7582 outputs :
7683 docs_only : ${{ steps.diff.outputs.docs_only }}
7784 supported_pg : ${{ steps.pg.outputs.supported_pg }}
85+ climb_pg : ${{ steps.pg.outputs.climb_pg }}
7886 steps :
7987 - name : Check out the repo
8088 uses : actions/checkout@v4
@@ -123,30 +131,46 @@ jobs:
123131 echo "changed files:"
124132 echo "$CHANGED"
125133 echo "docs_only=$DOCS_ONLY" >> "$GITHUB_OUTPUT"
126- - name : Derive the supported-PostgreSQL-major list
134+ - name : Derive the supported-PostgreSQL-major lists
127135 id : pg
128136 run : |
129137 # SINGLE SOURCE OF TRUTH for the supported PostgreSQL majors. To
130- # add or drop a major, edit only the two constants below; the test
131- # job's matrix derives its version list from them. Do NOT hardcode
132- # a supported major directly in a job matrix.
138+ # add or drop a major, edit only the two constants below; every
139+ # job's matrix/climb derives its version list from them. Do NOT
140+ # hardcode a supported major directly in a job matrix or loop .
133141 #
134142 # NEWEST -- highest PostgreSQL major tested.
135143 # CURRENT_FLOOR -- oldest major supported. object_reference
136144 # requires cat_tools at both build and runtime,
137145 # and cat_tools's own current release declares
138146 # PostgreSQL 12 as its build floor, so
139147 # object_reference can't usefully claim support
140- # for anything older either.
148+ # for anything older either. 0.1.0 (the one
149+ # real historical PGXN release) has no
150+ # PostgreSQL-version floor of its own (no
151+ # SELECT * over a system catalog, no ALTER
152+ # TYPE ... ADD VALUE in its update script), so
153+ # unlike cat_tools's reference implementation
154+ # there is no separate, older LEGACY_FLOOR --
155+ # 0.1.0 installs cleanly across the whole
156+ # CURRENT_FLOOR..NEWEST range, and the stepwise
157+ # climb (pg-upgrade-stepwise) starts right at
158+ # CURRENT_FLOOR too.
141159 NEWEST=18
142160 CURRENT_FLOOR=12
143161
144162 supported=$(seq "$NEWEST" -1 "$CURRENT_FLOOR")
163+ # Ascending (ties floor-to-newest, opposite order from $supported
164+ # above): pg-upgrade-stepwise reads the first element as its
165+ # starting major and binary-pg_upgrades through the rest in turn.
166+ climb=$(seq "$CURRENT_FLOOR" "$NEWEST")
145167
146168 # Emit a JSON array for the test job's matrix to consume via
147169 # fromJSON.
148170 json=$(printf '%s\n' $supported | paste -sd, - | sed 's/^/[/; s/$/]/')
149171 echo "supported_pg=$json" >> "$GITHUB_OUTPUT"
172+ # Space-separated for direct iteration in the stepwise bash loop.
173+ echo "climb_pg=$(echo $climb)" >> "$GITHUB_OUTPUT"
150174
151175 # Style linter (https://github.com/Postgres-Extensions/linter, vendored at
152176 # .vendor/linter -- lint.mk is the thin local hand-off, see its comment).
@@ -231,14 +255,126 @@ jobs:
231255 - name : Update 0.1.0 -> stable, structurally compare, run the suite (existing mode)
232256 run : bin/test_existing update-scenario object_reference_update 0.1.0
233257
258+ # Proves object_reference survives EVERY individual major-to-major binary
259+ # pg_upgrade transition, not just the single newest-major snapshot the
260+ # `test`/`extension-update-test` jobs above cover: ONE cluster that starts
261+ # on the floor PostgreSQL major and climbs through every later supported
262+ # major in sequence via a REAL binary pg_upgrade. See the "Test strategy"
263+ # comment at the top of this file for why this is included even though no
264+ # catalog-touching view/function has been found in object_reference today.
265+ #
266+ # Installs 0.1.0 on the floor major, updates it straight to the current
267+ # version (0.1.0's update script has no PostgreSQL-version floor of its
268+ # own -- no ALTER TYPE ... ADD VALUE -- so, unlike cat_tools's reference
269+ # implementation, there is no reason to hold the extension at an old
270+ # version through any step of the climb), then binary-pg_upgrades one
271+ # major at a time through the rest of the range, running the full suite
272+ # (existing mode) and re-proving the dependency guard after EVERY step.
273+ pg-upgrade-stepwise :
274+ # Gated behind test, not just changes -- see extension-update-test's
275+ # needs comment above: every leg here would fail anyway against an
276+ # already-broken baseline. success() must be written explicitly --
277+ # GitHub only assumes success() as a job's default when it has no if: at
278+ # all.
279+ needs : [changes, test]
280+ if : success() && needs.changes.outputs.docs_only != 'true'
281+ name : 🪜 Stepwise pg_upgrade (0.1.0 → stable)
282+ runs-on : ubuntu-latest
283+ container : pgxn/pgxn-tools
284+ env :
285+ # Every pg_upgrade step pairs two clusters that must share initdb
286+ # options (checksums, auth) or pg_upgrade refuses to run.
287+ INITDB_OPTS : --data-checksums --auth trust
288+ DB : object_reference_stepwise
289+ # Ascending list of every supported PostgreSQL major, from the single
290+ # source in the changes job -- so a new major joins the climb with no
291+ # edit here. The first element is the climb's starting (floor) major.
292+ CLIMB_PG : ${{ needs.changes.outputs.climb_pg }}
293+ steps :
294+ - name : Read the climb's starting (floor) PostgreSQL major
295+ id : floor
296+ run : echo "pg=$(set -- $CLIMB_PG; echo "$1")" >> "$GITHUB_OUTPUT"
297+ - name : Start PostgreSQL ${{ steps.floor.outputs.pg }}
298+ run : pg-start ${{ steps.floor.outputs.pg }}
299+ - name : Recreate the floor cluster with data checksums enabled
300+ # pg-start's default "test" cluster doesn't enable data checksums,
301+ # but binary pg_upgrade requires the old and new clusters to have
302+ # MATCHING checksum/auth settings.
303+ run : |
304+ pg_ctlcluster ${{ steps.floor.outputs.pg }} test stop
305+ pg_dropcluster ${{ steps.floor.outputs.pg }} test
306+ # -p 5432: pg_createcluster assigns the next available port, which
307+ # may not be 5432 after pg-start has claimed and released it.
308+ # Force 5432 so later psql/createdb calls connect without -p.
309+ pg_createcluster -p 5432 ${{ steps.floor.outputs.pg }} test -- $INITDB_OPTS
310+ pg_ctlcluster ${{ steps.floor.outputs.pg }} test start
311+ pg_isready -t 30
312+ - name : Check out the repo
313+ uses : actions/checkout@v4
314+ - name : Install object_reference + its 0.1.0-only test dependency (count_nulls) into the floor cluster
315+ # TEST_LOAD_SOURCE=update activates the Makefile's conditional
316+ # `install: count_nulls` prerequisite -- 0.1.0's install script
317+ # needs count_nulls even though current object_reference.control no
318+ # longer declares it. count_nulls stays installed as its own
319+ # extension in the database even after updating object_reference
320+ # past 0.1.0 (nothing drops it), so every later `make install` in
321+ # this job's climb loop keeps passing TEST_LOAD_SOURCE=update too --
322+ # pg_upgrade needs count_nulls's files present in each new cluster
323+ # for as long as it remains a real extension in the test database.
324+ run : make install TEST_LOAD_SOURCE=update
325+ - name : Prepare the floor cluster (install 0.1.0, plant guard, update to current, run suite)
326+ run : |
327+ bin/test_existing prepare-old "$DB" 0.1.0
328+ bin/test_existing update "$DB"
329+ bin/test_existing run-suite "$DB"
330+ - name : Climb every later major via binary pg_upgrade
331+ # One sequential loop; each iteration binary-pg_upgrades the cluster
332+ # from $old to $new (a single major step), re-installs
333+ # object_reference + its dependencies into the new cluster first
334+ # (pg_upgrade needs their files present in the NEW cluster's
335+ # sharedir -- default pg_config on PATH may not be $new's once
336+ # several majors are installed, hence PG_CONFIG explicit), then
337+ # re-runs the full suite in existing mode -- re-proving the
338+ # dependency guard survived, and that the same suite/expected-output
339+ # still passes against the objects that just crossed a pg_upgrade.
340+ run : |
341+ set -- $CLIMB_PG
342+ old=$1
343+ shift
344+ for new in "$@"; do
345+ echo "=== binary pg_upgrade PostgreSQL $old -> $new ==="
346+ apt-get install -y postgresql-$new postgresql-server-dev-$new
347+ make install PG_CONFIG=/usr/lib/postgresql/$new/bin/pg_config TEST_LOAD_SOURCE=update
348+ pg_ctlcluster $old test stop
349+ pg_createcluster -p 5432 $new test -- $INITDB_OPTS
350+ # PG17+ writes pg_upgrade logs under the new datadir; older
351+ # versions write to CWD. Dump both on failure.
352+ mkdir -p /tmp/pg_upgrade_logs
353+ chown postgres:postgres /tmp/pg_upgrade_logs
354+ su -c "cd /tmp/pg_upgrade_logs && /usr/lib/postgresql/$new/bin/pg_upgrade \
355+ -b /usr/lib/postgresql/$old/bin \
356+ -B /usr/lib/postgresql/$new/bin \
357+ -d /var/lib/postgresql/$old/test \
358+ -D /var/lib/postgresql/$new/test \
359+ -o '-c config_file=/etc/postgresql/$old/test/postgresql.conf' \
360+ -O '-c config_file=/etc/postgresql/$new/test/postgresql.conf'" postgres \
361+ || { find /tmp/pg_upgrade_logs \
362+ /var/lib/postgresql/$new/test/pg_upgrade_output.d \
363+ -name '*.log' 2>/dev/null | sort | xargs -r tail -n +1; exit 1; }
364+ pg_ctlcluster $new test start
365+ pg_isready -t 30
366+ bin/test_existing run-suite "$DB"
367+ old=$new
368+ done
369+
234370 # A single stable check name for use as a required status check in branch
235371 # protection rules. Matrix jobs produce check names like "🐘 PostgreSQL 14"
236372 # which would all need to be listed individually and updated whenever the
237373 # matrix changes. This job passes if all others passed or were skipped
238374 # (e.g. test, on a docs-only push), and fails if any failed or were
239375 # cancelled.
240376 all-checks-passed :
241- needs : [changes, lint, test, extension-update-test]
377+ needs : [changes, lint, test, extension-update-test, pg-upgrade-stepwise ]
242378 if : always()
243379 runs-on : ubuntu-latest
244380 steps :
0 commit comments