Extract PR #46's manual structural-diff technique into reusable bin/structural_diff tooling #298
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
| name: CI | |
| on: | |
| push: | |
| branches: | |
| - master | |
| pull_request: | |
| env: | |
| PGUSER: postgres | |
| jobs: | |
| # Cheap gate that lets the heavy jobs below skip themselves on commits | |
| # that touch only docs. This must run on every push/pull_request (no | |
| # paths-ignore on the workflow itself), otherwise the required | |
| # all-checks-passed check would never report on doc-only pushes and get | |
| # stuck Pending in branch protection. | |
| # | |
| # It also derives, from a SINGLE set of constants, the supported-PostgreSQL- | |
| # major lists that the test / extension-update / stepwise jobs consume (see | |
| # the "Derive ..." step). Because those lists live here and every heavy job | |
| # already needs this job, adding a PG major is a one-line edit with no new job. | |
| changes: | |
| name: 🔍 Detect changes & derive PG matrix | |
| runs-on: ubuntu-latest | |
| permissions: | |
| # Needed only by "Find the last commit where real code changed" below, | |
| # to query the Actions API for a past run's conclusion/URL. | |
| actions: read | |
| outputs: | |
| docs_only: ${{ steps.diff.outputs.docs_only }} | |
| # Set when docs_only is true: whether there's a real "last tested" commit | |
| # to report at all (false), or this entire PR/push chain has never | |
| # touched anything but docs (true, docs_only_pr) -- see the "Find the | |
| # last commit..." step below for how these are derived. | |
| docs_only_pr: ${{ steps.last_tested.outputs.docs_only_pr }} | |
| last_tested_sha: ${{ steps.last_tested.outputs.last_tested_sha }} | |
| last_tested_conclusion: ${{ steps.last_tested.outputs.last_tested_conclusion }} | |
| last_tested_url: ${{ steps.last_tested.outputs.last_tested_url }} | |
| # Derived PG-major lists (see the "Derive ..." step for their meaning). | |
| supported_pg: ${{ steps.pg.outputs.supported_pg }} | |
| update_pg: ${{ steps.pg.outputs.update_pg }} | |
| climb_pg: ${{ steps.pg.outputs.climb_pg }} | |
| legacy_pg: ${{ steps.pg.outputs.legacy_pg }} | |
| steps: | |
| - name: Check out the repo | |
| uses: actions/checkout@v6 | |
| with: | |
| # Full history needed so BASE and HEAD below are both reachable | |
| # for `git diff`. | |
| fetch-depth: 0 | |
| - name: Compute per-push changed files | |
| id: diff | |
| run: | | |
| if [ "${{ github.event_name }}" = "pull_request" ] && \ | |
| [ "${{ github.event.action }}" = "synchronize" ] && \ | |
| [ -n "${{ github.event.before }}" ]; then | |
| # A push to an already-open PR: before/after give the true | |
| # per-push diff, same as for a branch push. | |
| BASE="${{ github.event.before }}" | |
| HEAD="${{ github.event.after }}" | |
| elif [ "${{ github.event_name }}" = "pull_request" ]; then | |
| # First run for this PR (opened/reopened/etc, or synchronize | |
| # without a usable before): fall back to the whole base...head | |
| # diff. | |
| BASE="${{ github.event.pull_request.base.sha }}" | |
| HEAD="${{ github.event.pull_request.head.sha }}" | |
| else | |
| BASE="${{ github.event.before }}" | |
| HEAD="${{ github.event.after }}" | |
| fi | |
| echo "base=$BASE" | |
| echo "head=$HEAD" | |
| # Fail-safe: a missing HEAD, or an all-zeros BASE (e.g. a new | |
| # branch's first push, where GitHub reports no prior commit), | |
| # means we can't compute a real diff. Run the full matrix rather | |
| # than risk skipping tests. | |
| if [ -z "$HEAD" ] || [ -z "$BASE" ] || [[ "$BASE" =~ ^0+$ ]]; then | |
| echo "docs_only=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| CHANGED=$(git diff --name-only "$BASE" "$HEAD" || echo __DIFF_FAILED__) | |
| DOCS_ONLY=true | |
| if [ "$CHANGED" = "__DIFF_FAILED__" ] || [ -z "$CHANGED" ]; then | |
| DOCS_ONLY=false | |
| else | |
| while IFS= read -r f; do | |
| if ! [[ "$f" =~ \.(md|asc)$ ]]; then | |
| DOCS_ONLY=false | |
| break | |
| fi | |
| done <<< "$CHANGED" | |
| fi | |
| echo "changed files:" | |
| echo "$CHANGED" | |
| echo "docs_only=$DOCS_ONLY" >> "$GITHUB_OUTPUT" | |
| - name: Find the last commit where real code changed | |
| # This push is docs-only, so the heavy jobs below are about to skip -- | |
| # meaning THIS run's checks don't cover the code currently on this | |
| # branch. Someone reviewing before merge needs to know whether that | |
| # code was actually fully tested somewhere, and whether it was green. | |
| # Walk backward from HEAD (bounded by the PR's base, or a generous | |
| # depth cap for a direct push) to the newest commit whose OWN diff | |
| # from its parent touched something other than docs -- every commit | |
| # strictly after that one only changed docs, so that commit's CI run | |
| # is still the authoritative test of the code on this branch now. | |
| if: steps.diff.outputs.docs_only == 'true' | |
| id: last_tested | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| REPO: ${{ github.repository }} | |
| PR_BASE_SHA: ${{ github.event.pull_request.base.sha }} | |
| # actions/checkout defaults to checking out an EPHEMERAL MERGE | |
| # COMMIT for pull_request events (refs/pull/N/merge), not the PR's | |
| # real head commit -- `git rev-parse HEAD` would return that merge | |
| # commit's SHA, and walking `^` from it follows the wrong (base | |
| # branch) first parent, landing on an unrelated commit entirely | |
| # (confirmed hitting exactly this in real CI). Use the actual PR | |
| # head SHA directly instead, same event-type split already used in | |
| # "Compute per-push changed files" above. | |
| PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} | |
| run: | | |
| if [ "${{ github.event_name }}" = "pull_request" ]; then | |
| HEAD_SHA="$PR_HEAD_SHA" | |
| else | |
| HEAD_SHA="${{ github.sha }}" | |
| fi | |
| if [ -n "$PR_BASE_SHA" ] && ! [[ "$PR_BASE_SHA" =~ ^0+$ ]]; then | |
| BOUNDARY=$(git merge-base "$PR_BASE_SHA" "$HEAD_SHA") | |
| else | |
| # Not a PR (e.g. a direct push to master): no PR base to bound | |
| # the walk against, so cap it at a generous depth instead of | |
| # walking arbitrarily far into history. | |
| BOUNDARY=$(git rev-list --max-count=1 --skip=100 "$HEAD_SHA" 2>/dev/null || echo "$HEAD_SHA") | |
| fi | |
| commit="$HEAD_SHA" | |
| last_code_commit="" | |
| while [ "$commit" != "$BOUNDARY" ]; do | |
| parent=$(git rev-parse "$commit^" 2>/dev/null) || break | |
| changed=$(git diff --name-only "$parent" "$commit") | |
| all_docs=true | |
| while IFS= read -r f; do | |
| [ -z "$f" ] && continue | |
| if ! [[ "$f" =~ \.(md|asc)$ ]]; then all_docs=false; break; fi | |
| done <<< "$changed" | |
| if [ "$all_docs" = true ]; then | |
| commit="$parent" | |
| else | |
| last_code_commit="$commit" | |
| break | |
| fi | |
| done | |
| if [ -z "$last_code_commit" ]; then | |
| # Reached the PR's own base (or the depth cap) without ever | |
| # finding a commit that changed code: this whole PR/push chain | |
| # is docs-only, so there is no "last tested" run to point to. | |
| echo "docs_only_pr=true" >> "$GITHUB_OUTPUT" | |
| echo "This entire PR/push contains only documentation changes -- no code testing applies." | |
| exit 0 | |
| fi | |
| echo "docs_only_pr=false" >> "$GITHUB_OUTPUT" | |
| echo "last_tested_sha=$last_code_commit" >> "$GITHUB_OUTPUT" | |
| echo "last code-changing commit: $last_code_commit" | |
| # Find that commit's most recent completed run of this same "CI" | |
| # workflow (there can be other workflows -- e.g. Claude Code Review | |
| # -- triggered on the same SHA, hence the name filter). | |
| RUN_JSON=$(gh api "repos/$REPO/actions/runs?head_sha=$last_code_commit&status=completed" --jq \ | |
| '[.workflow_runs[] | select(.name == "CI")] | sort_by(.run_started_at) | last // empty' 2>/dev/null || echo "") | |
| if [ -z "$RUN_JSON" ] || [ "$RUN_JSON" = "null" ]; then | |
| echo "last_tested_conclusion=unknown" >> "$GITHUB_OUTPUT" | |
| echo "last_tested_url=" >> "$GITHUB_OUTPUT" | |
| echo "No completed CI run found for $last_code_commit" | |
| else | |
| echo "last_tested_conclusion=$(jq -r '.conclusion' <<<"$RUN_JSON")" >> "$GITHUB_OUTPUT" | |
| echo "last_tested_url=$(jq -r '.html_url' <<<"$RUN_JSON")" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Derive the supported-PostgreSQL-major lists | |
| id: pg | |
| run: | | |
| # Spending 20+ lines to replace a handful of version references looks | |
| # silly on the surface, but the point is CONSISTENCY: every job -- the | |
| # fresh-install `test` matrix, the `extension-update-test` matrix and | |
| # the stepwise climb -- derives its PostgreSQL set from this ONE source, | |
| # so they cannot drift onto different version lists. | |
| # | |
| # SINGLE SOURCE OF TRUTH for the supported PostgreSQL majors. To add | |
| # or drop a PG major, edit ONLY the three constants below; the test, | |
| # extension-update-test and pg-upgrade-stepwise jobs all derive their | |
| # version lists from them (adding the newest major is a one-line NEWEST | |
| # bump). Do NOT hardcode a supported major in any job matrix or loop. | |
| # | |
| # NEWEST -- highest PostgreSQL major cat_tools is tested on. | |
| # CURRENT_FLOOR -- oldest major the CURRENT extension version supports: | |
| # the 0.2.3->0.3.0 update runs ALTER TYPE ... ADD | |
| # VALUE, which cannot run in a pre-PG12 transaction. | |
| # LEGACY_FLOOR -- oldest major the pre-0.2.2 install scripts still | |
| # load on (PG11 added pg_attribute.attmissingval and | |
| # PG12+ exposes the oid system column in SELECT *, both | |
| # of which those old scripts trip over). Only the | |
| # update and stepwise paths reach back this far. | |
| NEWEST=18 | |
| CURRENT_FLOOR=12 | |
| LEGACY_FLOOR=10 | |
| # supported = CURRENT_FLOOR..NEWEST (newest-first). The FRESH-install | |
| # matrix (test job) runs exactly these. | |
| supported=$(seq "$NEWEST" -1 "$CURRENT_FLOOR") | |
| # update = supported plus the legacy floor: the extension-update job | |
| # additionally exercises the PG10-only pre-0.2.2 update scripts. | |
| update="$supported $LEGACY_FLOOR" | |
| # climb = LEGACY_FLOOR+1 .. NEWEST (ascending). The stepwise job starts | |
| # one cluster on the legacy floor and binary-pg_upgrades through every | |
| # later major in turn, so its targets are every major above the floor. | |
| climb=$(seq $((LEGACY_FLOOR + 1)) "$NEWEST") | |
| # 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" | |
| echo "update_pg=$(json $update)" >> "$GITHUB_OUTPUT" | |
| echo "legacy_pg=$LEGACY_FLOOR" >> "$GITHUB_OUTPUT" | |
| # Space-separated for direct iteration in the stepwise bash loop. | |
| echo "climb_pg=$(echo $climb)" >> "$GITHUB_OUTPUT" | |
| # =========================================================================== | |
| # Test strategy | |
| # | |
| # A cat_tools install can be arrived at several ways, each of which can break | |
| # differently, so each is exercised by its own job below (the per-job comments | |
| # carry the details; this is the big picture): | |
| # | |
| # test -- FRESH install: CREATE EXTENSION at the current | |
| # version on every supported PostgreSQL. The baseline | |
| # a brand-new user gets. | |
| # extension-update-test -- IN-PLACE update: CREATE EXTENSION at an OLD version | |
| # then ALTER EXTENSION UPDATE (same PostgreSQL, no | |
| # pg_upgrade). | |
| # pg-upgrade-test -- BINARY pg_upgrade, SINGLE jump: install an OLD | |
| # version on an OLD major, binary-upgrade the cluster | |
| # straight to a NEWER major (skipping intermediate | |
| # majors), then update the extension. Proves objects | |
| # created on an old server work when read on a new one. | |
| # pg-upgrade-stepwise -- BINARY pg_upgrade, EVERY major in sequence: one | |
| # cluster climbing 10 -> 11 -> ... -> 18, exercising | |
| # each individual major-to-major transition in turn. | |
| # | |
| # Supported update origins are 0.2.0, 0.2.1 and 0.2.2 (0.1.x is unsupported). | |
| # 0.2.0 and 0.2.1 are BOTH tested as origins because ALTER EXTENSION UPDATE | |
| # takes the shortest path: a 0.2.0 origin updates straight through the | |
| # 0.2.0--0.2.2 script and never touches 0.2.1--0.2.2, so starting at 0.2.1 is | |
| # the only way to exercise the 0.2.1--0.2.2 update script (spelled out at the | |
| # pg-upgrade-test matrix). | |
| # | |
| # Two PostgreSQL-version floors shape the matrices: | |
| # - The pre-0.2.2 install scripts (0.2.0 / 0.2.1) load ONLY on PG10: PG11 | |
| # added pg_attribute.attmissingval and PG12+ exposes the oid system column | |
| # in SELECT *, both of which those old scripts trip over. So a 0.2.0 / 0.2.1 | |
| # origin can only start on PG10. | |
| # - The current version needs PG12+: the 0.2.3->0.3.0 update runs | |
| # ALTER TYPE ... ADD VALUE, which cannot run in a pre-PG12 transaction (and | |
| # an extension update script is one). | |
| # | |
| # KEY invariant: every pg_upgrade leg CLIMBS to a PostgreSQL that supports the | |
| # current version, then updates to the current version and runs the full suite | |
| # -- no leg stops short. A PG10/11 origin simply HOLDS the extension at 0.2.3 | |
| # (the highest version reachable on those majors) until the cluster reaches | |
| # PG12+, where it is updated to the current version. | |
| # =========================================================================== | |
| test: | |
| needs: [changes] | |
| if: needs.changes.outputs.docs_only != 'true' | |
| strategy: | |
| matrix: | |
| # Current-supported majors, from the single source in the changes job. | |
| 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@v6 | |
| - name: Install rsync and server headers | |
| # postgresql-server-dev-NN provides catalog/pg_class.h, which | |
| # test/gen-relkinds.sh reads for the relkind drift check in | |
| # test/sql/relation__.sql. | |
| run: apt-get install -y rsync postgresql-server-dev-${{ matrix.pg }} | |
| - name: Test on PostgreSQL ${{ matrix.pg }} | |
| run: | | |
| # Fail if the relkind drift source is empty (headers missing): the | |
| # drift check must actually run on every version, not pass silently. | |
| make check-relkind-source | |
| # verify-results is the real gate: base.mk declares `verify-results: | |
| # test`, so this runs the suite via its `test` prerequisite and then | |
| # checks the pgtap/regression.diffs. A bare `make test` is redundant here | |
| # and would not gate anyway -- it never exits non-zero on regressions | |
| # (pgxntool marks installcheck `.IGNORE`), so failures would pass silently. | |
| make verify-results | |
| # Style linter (https://github.com/Postgres-Extensions/linter, vendored at | |
| # .vendor/linter). Deliberately checked out WITHOUT submodules -- `make | |
| # lint` is the same command a developer runs locally, and lint.mk | |
| # self-initializes the submodule on first use (see its comment). Using the | |
| # exact same entry point here is what actually proves that self-init works, | |
| # rather than papering over it with a submodules: true checkout. The | |
| # linter's own test suite (fixtures + scanner edge cases) is that repo's | |
| # own CI's job, not this one's. No PostgreSQL needed -- sql-lint is a | |
| # standalone Perl script -- so this doesn't use the pgxn-tools container. | |
| lint: | |
| needs: [changes] | |
| if: needs.changes.outputs.docs_only != 'true' | |
| name: 🧹 SQL Lint | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Check out the repo | |
| uses: actions/checkout@v6 | |
| - name: Lint SQL | |
| run: make lint | |
| # Proves cat_tools survives a BINARY pg_upgrade (in-place catalog migration to a | |
| # newer PostgreSQL major), not just an in-place extension update. Each leg is a | |
| # SINGLE jump: install an old cat_tools on an old cluster, plant a dependency | |
| # guard, binary-pg_upgrade STRAIGHT to a newer major (skipping any intermediate | |
| # majors), update the extension to the current version, then run the suite | |
| # against the REAL migrated objects in "existing" mode. Catches view/catalog | |
| # breakage that only surfaces when objects created on an old server are read on a | |
| # new one. Every leg's new_pg supports the current version (PG12+); the | |
| # version-by-version climb through EVERY major is pg-upgrade-stepwise. | |
| pg-upgrade-test: | |
| needs: [changes] | |
| if: needs.changes.outputs.docs_only != 'true' | |
| strategy: | |
| matrix: | |
| include: | |
| # old_pg=10 origins: the FULL real-world journey for a user on old | |
| # PostgreSQL + old cat_tools. old_install is 0.2.0/0.2.1 -- the only | |
| # versions that install on PG10 (PG11 added pg_attribute.attmissingval, | |
| # PG12+ exposes oid in SELECT *, so 0.2.0/0.2.1 install ONLY on PG10). | |
| # The BRIDGE update to 0.2.3, run on PG10 BEFORE pg_upgrade, makes the | |
| # objects pg_upgrade-safe: the shipped 0.2.0->0.2.2 / 0.2.1->0.2.2 | |
| # scripts leave fresh-vs-update divergences (relhasoids/relhaspkey never | |
| # stripped from _cat_tools.pg_class_v via the no-op `!= ANY` omit_column), | |
| # and the 0.2.2->0.2.3 update repairs them (its pg_class_v DROP+CREATE | |
| # rebuild strips the dropped columns). 0.2.3 is the highest version a | |
| # PG10 cluster can hold (0.2.3->0.3.0 needs PG12+); the single jump lands | |
| # on a PG12+ major, where the post-upgrade step updates 0.2.3 -> current. | |
| # | |
| # BOTH 0.2.0 and 0.2.1 appear as origins because ALTER EXTENSION UPDATE | |
| # takes the SHORTEST path: a 0.2.0 origin updates straight through the | |
| # 0.2.0--0.2.2 script and never exercises 0.2.1--0.2.2, so starting at | |
| # 0.2.1 is the ONLY way to exercise the 0.2.1--0.2.2 update script. | |
| - old_pg: "10" | |
| new_pg: "18" | |
| old_install: "0.2.0" | |
| bridge_to: "0.2.3" | |
| - old_pg: "10" | |
| new_pg: "18" | |
| old_install: "0.2.1" | |
| bridge_to: "0.2.3" | |
| # old_pg>=11: 0.2.0/0.2.1 do not install on PG11+, and such users are | |
| # already on 0.2.2+. A FRESH 0.2.2 install already strips relhasoids/ | |
| # relhaspkey (fixed `!= ALL` omit_column), so it is pg_upgrade-safe with | |
| # no bridge. pg_upgrade to a PG12+ major, then update to the current | |
| # version. | |
| - old_pg: "11" | |
| new_pg: "12" | |
| old_install: "0.2.2" | |
| bridge_to: "" | |
| - old_pg: "11" | |
| new_pg: "18" | |
| old_install: "0.2.2" | |
| bridge_to: "" | |
| - old_pg: "12" | |
| new_pg: "13" | |
| old_install: "0.2.2" | |
| bridge_to: "" | |
| - old_pg: "12" | |
| new_pg: "18" | |
| old_install: "0.2.2" | |
| bridge_to: "" | |
| # Include old_install in the name so matrix legs that differ only by origin | |
| # version (e.g. the two 10 → 18 legs from 0.2.0 and 0.2.1) get distinct, | |
| # unambiguous check names. | |
| name: 🔄 Binary pg_upgrade ${{ matrix.old_pg }} → ${{ matrix.new_pg }} (from ${{ matrix.old_install }}) | |
| runs-on: ubuntu-latest | |
| container: pgxn/pgxn-tools | |
| env: | |
| # Both clusters must use the same initdb options so pg_upgrade sees | |
| # consistent settings (checksums, auth) on old and new clusters. | |
| INITDB_OPTS: --data-checksums --auth trust | |
| steps: | |
| - name: Start PostgreSQL ${{ matrix.old_pg }} | |
| run: pg-start ${{ matrix.old_pg }} | |
| - name: Check out the repo | |
| uses: actions/checkout@v6 | |
| - name: Install rsync | |
| run: apt-get install -y rsync | |
| - name: Recreate old cluster with data checksums enabled | |
| run: .github/scripts/pg_upgrade_cluster recreate-old ${{ matrix.old_pg }} | |
| - name: Install cat_tools into old cluster | |
| run: make install | |
| - name: Prepare the old cluster (install + bridge + dependency guard) | |
| # prepare-old installs cat_tools at old_install and, when bridge_to is set | |
| # (old_pg=10), ALTER EXTENSION UPDATEs to it BEFORE pg_upgrade -- the | |
| # bridge that makes the views pg_upgrade-safe (see prepare-old in the | |
| # script). It then plants + proves the dependency guard so the later | |
| # existing-mode run cannot silently drop+reinstall and test a fresh | |
| # install instead. The extension is always older than the current version, | |
| # so the post-upgrade ALTER EXTENSION UPDATE always has real work to do. | |
| run: >- | |
| bin/test_existing prepare-old cat_tools_upgrade | |
| "${{ matrix.old_install }}" "${{ matrix.bridge_to }}" | |
| - name: Install PostgreSQL ${{ matrix.new_pg }} | |
| run: apt-get install -y postgresql-${{ matrix.new_pg }} postgresql-server-dev-${{ matrix.new_pg }} | |
| - name: Install cat_tools into new cluster | |
| # PG_CONFIG must be specified explicitly: at this point both old and new | |
| # PostgreSQL are installed, and the default pg_config on PATH may not be | |
| # the new version's. | |
| run: make install PG_CONFIG=/usr/lib/postgresql/${{ matrix.new_pg }}/bin/pg_config | |
| - name: Stop old cluster, binary pg_upgrade to PostgreSQL ${{ matrix.new_pg }}, start new cluster | |
| run: .github/scripts/pg_upgrade_cluster upgrade ${{ matrix.old_pg }} ${{ matrix.new_pg }} | |
| - name: Update the pg_upgraded extension to the current version | |
| # Exercises ALTER EXTENSION UPDATE on genuinely pg_upgraded objects (the | |
| # extension binary pg_upgrade just migrated), running the version-to-version | |
| # scripts up to the current version. The 0.2.3 -> 0.3.0 script uses | |
| # ALTER TYPE ... ADD VALUE, which is why every leg's new_pg is >= 12. | |
| run: bin/test_existing update cat_tools_upgrade | |
| - name: Run the suite against the pg_upgraded database (existing mode) | |
| # run-suite asserts the version, re-proves the dependency guard still | |
| # blocks a non-CASCADE drop (i.e. it survived pg_upgrade), then runs the | |
| # suite against the REAL pg_upgraded + updated database via --use-existing | |
| # (so pg_regress does not drop/recreate it) -- a plain fresh `make test` | |
| # would silently test a fresh install instead of the migrated objects. | |
| run: bin/test_existing run-suite cat_tools_upgrade | |
| # Proves cat_tools survives EVERY individual major-to-major binary pg_upgrade | |
| # transition, not just a single big jump (that is pg-upgrade-test): ONE cluster | |
| # that starts on the oldest origin and climbs through every major in sequence. | |
| # Installs 0.2.0 on PG10, bridges to 0.2.3 (the highest version a PG10 cluster | |
| # can hold -- see the pg-upgrade-test matrix comment), plants the dependency | |
| # guard, then binary-pg_upgrades 10->11->12->...->18 ONE major at a time. The | |
| # extension is HELD at 0.2.3 for the 10->11 and 11->12 steps (PG11 cannot run | |
| # the 0.2.3->0.3.0 update, which needs PG12+); on reaching PG12 it is updated | |
| # 0.2.3 -> the current version, and from PG12 on the full suite runs in existing | |
| # mode at EVERY major (12..18) against the REAL pg_upgraded objects, re-proving | |
| # the guard survived each step. CI-heavy on purpose: it installs PostgreSQL 10 | |
| # through 18, performs 8 sequential pg_upgrades, and runs the suite 7 times. | |
| pg-upgrade-stepwise: | |
| needs: [changes] | |
| if: needs.changes.outputs.docs_only != 'true' | |
| name: 🪜 Stepwise pg_upgrade 10 → 18 (from 0.2.0) | |
| runs-on: ubuntu-latest | |
| container: pgxn/pgxn-tools | |
| env: | |
| # Every pg_upgrade in the climb pairs two clusters that must share initdb | |
| # options (checksums, auth), exactly as in pg-upgrade-test. | |
| INITDB_OPTS: --data-checksums --auth trust | |
| DB: cat_tools_stepwise | |
| # Climb targets and starting (legacy) major, from the single source in the | |
| # changes job -- so a new PG major joins the climb with no edit here. | |
| CLIMB_PG: ${{ needs.changes.outputs.climb_pg }} | |
| LEGACY_PG: ${{ needs.changes.outputs.legacy_pg }} | |
| steps: | |
| - name: Start PostgreSQL 10 | |
| run: pg-start 10 | |
| - name: Recreate the PG10 cluster with data checksums enabled | |
| run: | | |
| pg_ctlcluster 10 test stop | |
| pg_dropcluster 10 test | |
| # -p 5432: force the well-known port (see pg-upgrade-test for why). | |
| pg_createcluster -p 5432 10 test -- $INITDB_OPTS | |
| pg_ctlcluster 10 test start | |
| pg_isready -t 30 | |
| - name: Check out the repo | |
| uses: actions/checkout@v6 | |
| - name: Install rsync | |
| # Must NOT install a newer server-dev yet: `make install` just below runs | |
| # with the DEFAULT pg_config, which must still resolve to PG10. Each newer | |
| # major's server-dev is installed inside the climb loop, at which point the | |
| # default pg_config advances with it (the final-major suite reads that | |
| # version's catalog header for the relkind drift check; see the test job). | |
| run: apt-get install -y rsync | |
| - name: Install cat_tools into the PG10 cluster | |
| run: make install | |
| - name: Prepare PG10 (install 0.2.0, bridge to 0.2.3, plant guard) | |
| run: bin/test_existing prepare-old "$DB" 0.2.0 0.2.3 | |
| - name: Climb every major via binary pg_upgrade (10 → 11 → ... → 18) | |
| # One sequential loop; each iteration binary-pg_upgrades the cluster from | |
| # $old to $new (a single major step). cat_tools is a SQL-only extension, so | |
| # pg_upgrade only needs its scripts present in the NEW cluster's sharedir: | |
| # `make install PG_CONFIG=<new>` before each step (the $old cluster already | |
| # has them, from the previous iteration or the PG10 install above). The | |
| # extension is held at 0.2.3 until PG12, where it is updated to the current | |
| # version; the suite then runs at every major from PG12 on (see the suite | |
| # policy inside the loop). The per-step pg_upgrade invocation and log | |
| # handling mirror pg-upgrade-test. | |
| run: | | |
| old=$LEGACY_PG | |
| for new in $CLIMB_PG; do | |
| echo "=== binary pg_upgrade PostgreSQL $old -> $new ===" | |
| apt-get install -y postgresql-$new postgresql-server-dev-$new | |
| # PG_CONFIG explicit: several majors are installed, so the default | |
| # pg_config on PATH may not be $new's. | |
| make install PG_CONFIG=/usr/lib/postgresql/$new/bin/pg_config | |
| pg_ctlcluster $old test stop | |
| pg_createcluster -p 5432 $new test -- $INITDB_OPTS | |
| # PG17+ writes logs under the new datadir; older versions to CWD. Dump | |
| # both on failure (same handling as pg-upgrade-test). | |
| mkdir -p /tmp/pg_upgrade_logs | |
| chown postgres:postgres /tmp/pg_upgrade_logs | |
| su -c "cd /tmp/pg_upgrade_logs && /usr/lib/postgresql/$new/bin/pg_upgrade \ | |
| -b /usr/lib/postgresql/$old/bin \ | |
| -B /usr/lib/postgresql/$new/bin \ | |
| -d /var/lib/postgresql/$old/test \ | |
| -D /var/lib/postgresql/$new/test \ | |
| -o '-c config_file=/etc/postgresql/$old/test/postgresql.conf' \ | |
| -O '-c config_file=/etc/postgresql/$new/test/postgresql.conf'" postgres \ | |
| || { find /tmp/pg_upgrade_logs \ | |
| /var/lib/postgresql/$new/test/pg_upgrade_output.d \ | |
| -name '*.log' 2>/dev/null | sort | xargs -r tail -n +1; exit 1; } | |
| pg_ctlcluster $new test start | |
| pg_isready -t 30 | |
| # Suite policy across the climb. We unfortunately CANNOT run the suite | |
| # against the OLD version (0.2.3) at the pre-PG12 steps (10->11, 11->12): | |
| # the suite matches the CURRENT version's objects and we do not maintain | |
| # an old-version expected-output set. So those steps only verify that | |
| # 0.2.3 SURVIVES each pg_upgrade -- i.e. the climb completing without | |
| # error. On reaching PG12 (the floor for 0.2.3->0.3.0, which needs | |
| # ALTER TYPE ... ADD VALUE) update once to the current version; from | |
| # PG12 on the extension is at current, so run the full suite in existing | |
| # mode at EVERY major (run-suite asserts the version and re-proves the | |
| # guard survived this step's pg_upgrade). | |
| if [ "$new" = 12 ]; then | |
| bin/test_existing update "$DB" | |
| fi | |
| if [ "$new" -ge 12 ]; then | |
| bin/test_existing run-suite "$DB" | |
| fi | |
| old=$new | |
| done | |
| # Proves the in-place extension update path: CREATE EXTENSION at an OLD cat_tools | |
| # version then ALTER EXTENSION UPDATE (no pg_upgrade, same PostgreSQL). On PG12+ | |
| # it updates 0.2.2 -> current and runs the FULL suite against the updated | |
| # database (same expected output as a fresh install, so an updated DB must behave | |
| # identically). The PG10 leg only exercises the pre-0.2.2 update scripts, the | |
| # sole version where they still load. Complements pg-upgrade-test, which covers | |
| # the cross-major-version binary upgrade instead. | |
| extension-update-test: | |
| needs: [changes] | |
| if: needs.changes.outputs.docs_only != 'true' | |
| strategy: | |
| matrix: | |
| # PG12+: exercise the WIDEST update path we support — CREATE EXTENSION at | |
| # the 0.2.2 backward-compat floor, ALTER EXTENSION UPDATE to the CURRENT | |
| # version, and run the full suite against the updated database. 0.2.2 is | |
| # the floor because the 0.2.0/0.2.1 install scripts fail on PG11+/PG12+; | |
| # PG12 is the PostgreSQL floor because the update runs | |
| # `ALTER TYPE ... ADD VALUE`, which PG11 and below cannot run in an | |
| # extension update script (lifted in PG12). | |
| # PG10: the ONLY version where the pre-0.2.2 install scripts still load, | |
| # so the only place the 0.2.0->0.2.2 and 0.2.1->0.2.2 update scripts and | |
| # the 0.2.2->0.2.3 view rebuild on the broken path can be exercised. They | |
| # target 0.2.2/0.2.3 (not the current version) and use no | |
| # ALTER TYPE ... ADD VALUE, so they run on PG10. The PG10 leg runs only | |
| # those legacy checks — not the current-version suite (the current version | |
| # needs PG12+: the 0.2.3->0.3.0 update adds enum values via ALTER TYPE ... | |
| # ADD VALUE, unrunnable in a pre-PG12 transaction). See the per-step `if` | |
| # guards. | |
| # | |
| # Current-supported majors + the legacy PG10 floor, from the single | |
| # source in the changes job (update_pg = supported_pg plus legacy_pg). | |
| pg: ${{ fromJSON(needs.changes.outputs.update_pg) }} | |
| name: ⬆️ Extension update test on PostgreSQL ${{ matrix.pg }} | |
| runs-on: ubuntu-latest | |
| container: pgxn/pgxn-tools | |
| steps: | |
| - name: Start PostgreSQL ${{ matrix.pg }} | |
| run: pg-start ${{ matrix.pg }} | |
| - name: Check out the repo | |
| uses: actions/checkout@v6 | |
| - name: Install rsync and server headers | |
| # server-dev provides catalog/pg_class.h for the relkind drift check | |
| # (see the "test" job); required so check-relkind-source below passes. | |
| # PG10 runs only the legacy-script checks (no suite), so it needs no headers. | |
| if: matrix.pg != '10' | |
| run: apt-get install -y rsync postgresql-server-dev-${{ matrix.pg }} | |
| - name: Install rsync | |
| if: matrix.pg == '10' | |
| run: apt-get install -y rsync | |
| - name: Install cat_tools (all versions) | |
| run: make install | |
| - name: Test pre-0.2.2 update scripts + 0.2.2→0.2.3 rebuild (PG10 only) | |
| # 0.2.0/0.2.1 install only on PG10; their update scripts target 0.2.2 (not | |
| # the current version) and are otherwise never exercised. Both origins are | |
| # checked because ALTER EXTENSION UPDATE takes the shortest path (see the | |
| # pg-upgrade-test matrix): 0.2.0 goes via the 0.2.0--0.2.2 script, 0.2.1 via | |
| # 0.2.1--0.2.2. update-check-version asserts each lands on 0.2.2 -- NOT | |
| # plain update-check: landing on 0.2.2 from these origins is a KNOWN | |
| # divergence from a fresh 0.2.2 install (trigger__parse and the | |
| # pg_class_v omit_column bug, repaired in 0.2.2->0.2.3; a type-ACL gap, | |
| # repaired only in 0.2.3->0.3.0), and both 0.2.0->0.2.2 / 0.2.1->0.2.2 | |
| # are already-published scripts that cannot be edited to fix it | |
| # directly, so asserting fresh-parity here would fail forever by | |
| # design. No suite runs (the current version needs PG12+). | |
| # | |
| # The rebuild_020/rebuild_021 checks exercise the REAL broken path end to | |
| # end from BOTH pre-0.2.2 origins: a 0.2.0 (resp. 0.2.1) install on PG10 | |
| # leaves relhasoids in _cat_tools.pg_class_v (the buggy 0.2.0--0.2.2 / | |
| # 0.2.1--0.2.2 omit_column no-op), and updating to 0.2.3 routes through | |
| # 0.2.2--0.2.3 (shortest path <origin>--0.2.2 then 0.2.2--0.2.3), firing | |
| # the conditional rebuild that strips relhasoids and the trigger__parse | |
| # repair. It stays update-check-version, not plain update-check: 0.2.3 | |
| # is ALSO already-published (tagged), so the type-ACL gap above is not | |
| # (and cannot be) fixed until 0.2.3->0.3.0 either -- this landing point | |
| # still diverges from a fresh 0.2.3 install on that ACL alone. 0.2.3 is | |
| # the furthest PG10 can reach (0.2.3--0.3.0 needs PG12+). On PG12+ | |
| # relhasoids never existed, so only PG10 exercises the rebuild. The | |
| # psql assertion fails loudly if the rebuild did not fire (relhasoids | |
| # still present) -- complementing the stronger 10→18 pg_upgrade bridge | |
| # legs. `$$` is escaped as `\$\$` so the shell passes literal dollar | |
| # quotes through to psql. | |
| if: matrix.pg == '10' | |
| run: | | |
| bin/test_existing update-check-version cat_tools_from_020 0.2.0 0.2.2 | |
| bin/test_existing update-check-version cat_tools_from_021 0.2.1 0.2.2 | |
| for origin in 020:0.2.0 021:0.2.1; do | |
| db="cat_tools_rebuild_${origin%%:*}" | |
| from="${origin##*:}" | |
| bin/test_existing update-check-version "$db" "$from" 0.2.3 | |
| psql -d "$db" -v ON_ERROR_STOP=1 -c "DO \$\$ BEGIN IF EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid='_cat_tools.pg_class_v'::regclass AND attname='relhasoids' AND NOT attisdropped AND attnum>0) THEN RAISE EXCEPTION 'pg_class_v still exposes relhasoids after update through 0.2.2->0.2.3 -- rebuild did not fire'; END IF; END \$\$" | |
| done | |
| - name: Update 0.2.2 → current and run the suite (existing mode, PG12+) | |
| # update-scenario creates a real database at 0.2.2, plants + proves the | |
| # dependency guard, ALTER EXTENSION UPDATEs to the current version, and | |
| # runs the suite against that updated database in existing mode (asserting | |
| # the version and that the guard still blocks a drop). Reusing the SAME | |
| # suite and expected output asserts the updated database behaves | |
| # identically to a fresh install. | |
| if: matrix.pg != '10' | |
| run: bin/test_existing update-scenario cat_tools_update 0.2.2 | |
| pg-tle-test: | |
| needs: [changes] | |
| if: needs.changes.outputs.docs_only != 'true' | |
| strategy: | |
| matrix: | |
| # Current-supported majors, from the single source in the changes job | |
| # (see its "Derive ..." step) -- same list the `test` job uses. This | |
| # currently happens to line up with pg_tle 1.5.2's own supported range | |
| # (12-18, see pgxntool/pgtle_versions.md) too, but that's a coincidence: | |
| # if either range moves independently in the future, re-check they | |
| # still overlap before assuming this job covers what it claims to. | |
| # 1.5.2 itself is what AWS RDS for PostgreSQL 17 currently ships | |
| # (RDS PG17.7+, our primary deployment target -- see | |
| # https://docs.aws.amazon.com/AmazonRDS/latest/PostgreSQLReleaseNotes/postgresql-versions.html). | |
| pg: ${{ fromJSON(needs.changes.outputs.supported_pg) }} | |
| name: 🧩 pg_tle ${{ matrix.pg }} | |
| runs-on: ubuntu-latest | |
| container: pgxn/pgxn-tools | |
| env: | |
| PG_TLE_RELEASE: "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@v6 | |
| - name: Install rsync | |
| run: apt-get install -y rsync | |
| - 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. `bin/test_existing`'s | |
| # pg_tle-mode run_suite calls `make testdeps`, which would otherwise | |
| # filesystem-install pgTAP for the first time partway through the job | |
| # and trip the contamination checks below (confirmed happening in a | |
| # real CI run). Installing it explicitly here, before the baseline | |
| # snapshot, makes it part of the accepted starting state -- like any | |
| # other extension already on disk -- instead of needing a hardcoded | |
| # exclude-by-name rule that would erode the whole point of diffing | |
| # against a baseline. | |
| run: make pgtap | |
| - name: Snapshot filesystem extension control files (pre-pg_tle baseline) | |
| # Whatever ships on disk by default (e.g. contrib, and now pgtap) | |
| # before we install pg_tle. 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 cat_tools -- 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_RELEASE }} | |
| # 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_RELEASE }} --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 + cat_tools 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 cat_tools database used below (the | |
| # smoke-test db, and whatever bin/test_existing createdb's) 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 cat_tools-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. | |
| # Confirmed hitting exactly this while researching pg_tle version | |
| # compatibility (a stale filesystem cat_tools install shadowed the | |
| # pg_tle-registered one under test). 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 | |
| - name: Install cat_tools purely via pg_tle (fresh install, no filesystem trace) | |
| # cat_tools 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/cat_tools.control | |
| createdb cat_tools_smoke | |
| psql -d cat_tools_smoke -c "CREATE EXTENSION cat_tools" | |
| - name: Verify cat_tools works when deployed via pg_tle | |
| run: | | |
| INSTALLED=$(psql -d cat_tools_smoke -tAc "SELECT extversion FROM pg_extension WHERE extname = 'cat_tools'") | |
| EXPECTED=$(make -s print-PGXNVERSION 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 | |
| psql -d cat_tools_smoke -v ON_ERROR_STOP=1 -c "SELECT relname FROM cat_tools.pg_class_v LIMIT 1" > /dev/null | |
| - 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 | |
| - name: Install PostgreSQL ${{ matrix.pg }} server dev headers | |
| # Needed by `make check-relkind-source`, which bin/test_existing's | |
| # run_suite calls -- every other job that reaches that check already | |
| # installs this; pg-tle-test didn't need it until now because the | |
| # smoke test above never runs the full suite. | |
| run: apt-get install -y postgresql-server-dev-${{ matrix.pg }} | |
| - name: Test the update path via pg_tle (full pgTAP suite, --use-existing) | |
| # Exercises the SAME update path extension-update-test proves for a | |
| # filesystem install (0.2.2 -> current), but entirely through pg_tle. | |
| # bin/test_existing's update_scenario is UNMODIFIED from the | |
| # filesystem case -- only run_suite's internals differ, gated by this | |
| # one env var (see the TEST_EXISTING_DEPLOY comment in | |
| # bin/test_existing) -- so this reuses the exact same update/upgrade | |
| # code the other jobs use instead of duplicating it for pg_tle. | |
| run: TEST_EXISTING_DEPLOY=pgtle bin/test_existing update-scenario cat_tools_update 0.2.2 | |
| - name: Verify no stray extension control files after the pg_tle update test | |
| # Runs AFTER the complete update+full-suite flow, not before: the | |
| # whole point of pg_tle-mode testing is proving NOTHING touched the | |
| # filesystem THROUGHOUT that flow, not merely that the environment | |
| # started clean. A check placed before this point could not catch a | |
| # regression introduced by anything update-scenario itself does. | |
| run: bin/assert_fs_clean verify ${{ matrix.pg }} /tmp/control_baseline.txt | |
| pg-tle-upgrade-test: | |
| needs: [changes] | |
| if: needs.changes.outputs.docs_only != 'true' | |
| strategy: | |
| matrix: | |
| include: | |
| # Mirrors pg-upgrade-test's old_pg=12 legs (the only ones cleanly | |
| # within pg_tle 1.5.2's own supported range, PG12-18) so the SAME PG | |
| # version jumps are proven for both deployment methods. Unlike | |
| # pg-upgrade-test, there is no bridge/old_install complexity here -- | |
| # pg_tle has no equivalent of the pre-0.2.2 filesystem install-script | |
| # issues (those are about SELECT * over catalog columns in old SQL | |
| # files, irrelevant to how the extension gets registered). | |
| - old_pg: "12" | |
| new_pg: "13" | |
| - old_pg: "12" | |
| new_pg: "18" | |
| name: 🧩🔄 pg_tle binary pg_upgrade ${{ matrix.old_pg }} → ${{ matrix.new_pg }} | |
| runs-on: ubuntu-latest | |
| container: pgxn/pgxn-tools | |
| env: | |
| # Both clusters must use the same initdb options so pg_upgrade sees | |
| # consistent settings (checksums, auth) on old and new clusters. | |
| INITDB_OPTS: --data-checksums --auth trust | |
| PG_TLE_RELEASE: "1.5.2" | |
| steps: | |
| # A dedicated cluster pair, never shared with the other jobs in this | |
| # workflow -- same reasoning as pg-tle-test. | |
| - name: Start PostgreSQL ${{ matrix.old_pg }} | |
| run: pg-start ${{ matrix.old_pg }} | |
| - name: Check out the repo | |
| uses: actions/checkout@v6 | |
| - name: Install rsync | |
| run: apt-get install -y rsync | |
| - name: Recreate old cluster with data checksums enabled | |
| run: .github/scripts/pg_upgrade_cluster recreate-old ${{ matrix.old_pg }} | |
| - name: Snapshot filesystem extension control files (old cluster baseline) | |
| run: bin/assert_fs_clean snapshot ${{ matrix.old_pg }} /tmp/control_baseline_old.txt | |
| - name: Build and install pg_tle ${{ env.PG_TLE_RELEASE }} on the old cluster | |
| # 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_RELEASE }} --depth 1 https://github.com/aws/pg_tle.git /tmp/pg_tle_old | |
| make -C /tmp/pg_tle_old install | |
| - name: Enable pg_tle and restart PostgreSQL ${{ matrix.old_pg }} | |
| run: | | |
| echo "shared_preload_libraries = 'pg_tle'" >> /etc/postgresql/${{ matrix.old_pg }}/test/postgresql.conf | |
| pg_ctlcluster ${{ matrix.old_pg }} test restart | |
| pg_isready -t 30 | |
| - name: Register pg_tle + cat_tools against template1 (old cluster) | |
| run: | | |
| psql -d template1 -c "CREATE EXTENSION pg_tle" | |
| PGDATABASE=template1 make run-pgtle | |
| - name: Verify no stray extension control files on the old cluster | |
| run: bin/assert_fs_clean verify ${{ matrix.old_pg }} /tmp/control_baseline_old.txt | |
| - name: Prepare the old cluster (install via pg_tle + dependency guard) | |
| # A fresh 0.2.2 install (no bridge needed -- see pg-upgrade-test's own | |
| # comment on why its old_pg>=11 legs don't need one), created via | |
| # pg_tle only, then the same dependency guard bin/test_existing's | |
| # other subcommands rely on to prove existing-mode never silently | |
| # drops+reinstalls the extension. | |
| run: | | |
| test ! -e /usr/share/postgresql/${{ matrix.old_pg }}/extension/cat_tools.control | |
| createdb cat_tools_tle_upgrade | |
| psql -d cat_tools_tle_upgrade -c "CREATE EXTENSION cat_tools VERSION '0.2.2'" | |
| bin/test_existing plant-guard cat_tools_tle_upgrade | |
| - name: Install PostgreSQL ${{ matrix.new_pg }} | |
| run: apt-get install -y postgresql-${{ matrix.new_pg }} postgresql-server-dev-${{ matrix.new_pg }} | |
| - name: Install pgtap on the new cluster (test harness dependency) | |
| # Must happen BEFORE the new-cluster baseline snapshot below, same | |
| # reasoning as pg-tle-test's "Install pgtap" step: pgTAP is a | |
| # filesystem-installed dependency of the test harness, unrelated to | |
| # cat_tools's own pg_tle-only deployment story, so it needs to be part | |
| # of the accepted starting state rather than flagged as contamination. | |
| # PATH is set explicitly (not just relying on apt's update-alternatives | |
| # switch from installing postgresql-${{ matrix.new_pg }} above) since | |
| # `pgxn install` resolves pg_config via PATH, not via a passed-through | |
| # PG_CONFIG make variable. | |
| run: PATH=/usr/lib/postgresql/${{ matrix.new_pg }}/bin:$PATH make pgtap | |
| - name: Snapshot filesystem extension control files (new cluster baseline) | |
| run: bin/assert_fs_clean snapshot ${{ matrix.new_pg }} /tmp/control_baseline_new.txt | |
| - name: Build and install pg_tle ${{ env.PG_TLE_RELEASE }} on the new cluster | |
| # Only pg_tle's OWN framework is needed on the new binary tree -- NOT | |
| # cat_tools. Binary pg_upgrade's schema-only restore recreates each | |
| # object individually (binary_upgrade_create_empty_extension), and | |
| # pg_tle's own catalog tables (holding cat_tools's registered SQL) are | |
| # carried over as ordinary heap data during the physical copy phase -- | |
| # no re-registration needed on the new side. A SEPARATE clone | |
| # (pg_tle_new, not pg_tle_old rebuilt) avoids installing a .so still | |
| # compiled against the old cluster's PostgreSQL headers: pg_tle has C | |
| # code, unlike cat_tools, so switching PG_CONFIG on an already-built | |
| # tree without a fresh build would risk installing the wrong binary. | |
| run: | | |
| git clone --branch v${{ env.PG_TLE_RELEASE }} --depth 1 https://github.com/aws/pg_tle.git /tmp/pg_tle_new | |
| make -C /tmp/pg_tle_new install PG_CONFIG=/usr/lib/postgresql/${{ matrix.new_pg }}/bin/pg_config | |
| - name: Verify no stray extension control files on the new cluster (pre-upgrade) | |
| run: bin/assert_fs_clean verify ${{ matrix.new_pg }} /tmp/control_baseline_new.txt | |
| - name: Stop old cluster, create new cluster, enable pg_tle, binary pg_upgrade, start new cluster | |
| run: .github/scripts/pg_upgrade_cluster upgrade ${{ matrix.old_pg }} ${{ matrix.new_pg }} "shared_preload_libraries = 'pg_tle'" | |
| - name: Verify no stray extension control files after binary pg_upgrade | |
| run: bin/assert_fs_clean verify ${{ matrix.new_pg }} /tmp/control_baseline_new.txt | |
| - name: Update the pg_upgraded extension to the current version | |
| # Exercises ALTER EXTENSION UPDATE on genuinely pg_upgraded objects | |
| # registered via pg_tle. update_ext() is pure psql (no `make` calls), | |
| # so it's identical regardless of deployment method -- no pg_tle-mode | |
| # flag needed here, only for run-suite below. | |
| run: bin/test_existing update cat_tools_tle_upgrade | |
| - name: Run the suite against the pg_upgraded database via pg_tle (existing mode) | |
| run: TEST_EXISTING_DEPLOY=pgtle bin/test_existing run-suite cat_tools_tle_upgrade | |
| - name: Verify no stray extension control files after the pg_tle upgrade+update+suite run | |
| # CRITICAL: must run AFTER the complete flow, not before -- same | |
| # principle as pg-tle-test's final checkpoint. Proves nothing touched | |
| # the filesystem across binary pg_upgrade, the extension update, AND | |
| # the full pgTAP suite run, not merely that the environment started | |
| # clean. | |
| run: bin/assert_fs_clean verify ${{ matrix.new_pg }} /tmp/control_baseline_new.txt | |
| # A single stable check name for use as a required status check in branch | |
| # protection rules. Matrix jobs produce check names like "🐘 PostgreSQL 14" | |
| # which would all need to be listed individually and updated whenever the | |
| # matrix changes. This job passes if all others passed or were skipped (e.g. | |
| # the heavy jobs gated off by the `changes` job on a docs-only push), and | |
| # fails if any failed or were cancelled. | |
| all-checks-passed: | |
| needs: [changes, test, lint, pg-upgrade-test, pg-upgrade-stepwise, extension-update-test, pg-tle-test, pg-tle-upgrade-test] | |
| if: always() | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Verify all jobs are listed in needs | |
| # Ensures this job won't silently ignore a newly-added job that was | |
| # omitted from the needs list above. | |
| run: | | |
| DEFINED=$(python3 -c " | |
| import yaml | |
| with open('.github/workflows/ci.yml') as f: | |
| w = yaml.safe_load(f) | |
| print('\n'.join(sorted(j for j in w['jobs'] if j != 'all-checks-passed'))) | |
| ") | |
| NEEDED=$(echo '${{ toJson(needs) }}' | python3 -c " | |
| import json, sys | |
| print('\n'.join(sorted(json.load(sys.stdin)))) | |
| ") | |
| if [ "$DEFINED" != "$NEEDED" ]; then | |
| echo "Some jobs are missing from all-checks-passed needs:" | |
| diff <(echo "$DEFINED") <(echo "$NEEDED") | |
| exit 1 | |
| fi | |
| - name: Check all jobs passed or were skipped | |
| run: | | |
| if [[ "${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}" == "true" ]]; then | |
| echo "One or more jobs failed or were cancelled" | |
| exit 1 | |
| fi | |
| - name: Summarize what this push actually tested | |
| # This job's own checks report "skipped" for every heavy job on a | |
| # docs-only push -- true for THIS push, but easy to mistake for "never | |
| # tested" when just glancing at the PR's Checks tab. Make it explicit | |
| # what's actually being vouched for. Reporting only -- does not affect | |
| # pass/fail; see the "Check all jobs passed" step above for that gate. | |
| run: | | |
| if [ "${{ needs.changes.outputs.docs_only }}" != "true" ]; then | |
| echo "✅ This push's own CI run tested the code (not a docs-only push)." >> "$GITHUB_STEP_SUMMARY" | |
| elif [ "${{ needs.changes.outputs.docs_only_pr }}" = "true" ]; then | |
| echo "📄 This entire PR/push contains only documentation changes -- no code testing applies." >> "$GITHUB_STEP_SUMMARY" | |
| else | |
| SHA="${{ needs.changes.outputs.last_tested_sha }}" | |
| CONCLUSION="${{ needs.changes.outputs.last_tested_conclusion }}" | |
| URL="${{ needs.changes.outputs.last_tested_url }}" | |
| if [ "$CONCLUSION" = "success" ]; then ICON="✅"; else ICON="⚠️"; fi | |
| if [ -n "$URL" ]; then | |
| echo "$ICON This push only changed docs. The code was last fully tested at \`${SHA:0:7}\` -- [$CONCLUSION]($URL)" >> "$GITHUB_STEP_SUMMARY" | |
| else | |
| echo "⚠️ This push only changed docs, and no completed CI run was found for the last code-changing commit (\`${SHA:0:7}\`) -- verify manually." >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| fi | |
| # vi: expandtab ts=2 sw=2 |