pg-tle-test: extend to the 0.9.6 -> current update path via DESTDIR sandboxing #267
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # =========================================================================== | |
| # 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 |