From 4a5a7c9fd5a58a5eb190bc86c33ae92ec37ca368 Mon Sep 17 00:00:00 2001 From: jnasbyupgrade Date: Fri, 31 Jul 2026 18:43:55 -0500 Subject: [PATCH 1/2] Phase 3.5: CI hygiene - docs-only gate, all-checks-passed, single-source PG list Independent of the U&U testing work itself, but best done now that multiple CI jobs exist and before the next phase adds the most expensive one (a real pg_upgrade job): - `changes` job: computes the actual per-push diff and skips test/ extension-update-test/pg-tle-test entirely on doc-only pushes, always triggering itself (no workflow-level paths-ignore, which would leave all-checks-passed stuck Pending on doc-only pushes in branch protection). - Derives the supported-PostgreSQL-major list from ONE set of constants (NEWEST/FLOOR) in that same job, consumed by both the `test` and `extension-update-test` matrices via fromJSON - they can't silently drift onto different lists, and a new major is a one-line change. - `all-checks-passed`: single stable required-status-check name, with a self-check that its own needs list can't silently omit a newly-added job. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/ci.yml | 192 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 191 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ba9a555..1848af8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,3 +1,40 @@ +# =========================================================================== +# 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-tle-test -- pg_tle DEPLOYMENT: fresh install 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 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). +# +# `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. +# =========================================================================== name: CI on: push: @@ -5,6 +42,114 @@ on: - 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. + changes: + name: ๐Ÿ” Detect docs-only changes & derive PG matrix + runs-on: ubuntu-latest + outputs: + docs_only: ${{ steps.diff.outputs.docs_only }} + supported_pg: ${{ steps.pg.outputs.supported_pg }} + steps: + - name: Check out the repo + uses: actions/checkout@v4 + with: + # Full history needed so BASE and HEAD below are both reachable + # for `git diff`. + fetch-depth: 0 + - name: Compute per-push changed files + id: diff + run: | + # Fail safe to running the full matrix: default docs_only to false + # immediately, before anything below has a chance to compute or + # fail. Writing the same GITHUB_OUTPUT key twice is fine (the last + # write wins), so the only way this step ends with docs_only=true + # is by genuinely proving it further down - never by skipping past + # an edge case with a default. + echo "docs_only=false" >> "$GITHUB_OUTPUT" + + if [ "${{ github.event_name }}" = "pull_request" ] && \ + [ "${{ github.event.action }}" = "synchronize" ] && \ + [ -n "${{ github.event.before }}" ]; then + # A push to an already-open PR: before/after give the true + # per-push diff, same as for a branch push. + BASE="${{ github.event.before }}" + HEAD="${{ github.event.after }}" + elif [ "${{ github.event_name }}" = "pull_request" ]; then + # First run for this PR (opened/reopened/etc, or synchronize + # without a usable before): fall back to the whole base...head + # diff. + BASE="${{ github.event.pull_request.base.sha }}" + HEAD="${{ github.event.pull_request.head.sha }}" + else + BASE="${{ github.event.before }}" + HEAD="${{ github.event.after }}" + fi + + echo "base=$BASE" + echo "head=$HEAD" + + # A missing HEAD, or an all-zeros BASE (e.g. a new branch's first + # push, where GitHub reports no prior commit), means we can't + # compute a real diff. docs_only is already false from above; + # just stop here rather than risk skipping tests. + if [ -z "$HEAD" ] || [ -z "$BASE" ] || [[ "$BASE" =~ ^0+$ ]]; then + exit 0 + fi + + CHANGED=$(git diff --name-only "$BASE" "$HEAD" || echo __DIFF_FAILED__) + + DOCS_ONLY=true + if [ "$CHANGED" = "__DIFF_FAILED__" ] || [ -z "$CHANGED" ]; then + DOCS_ONLY=false + else + while IFS= read -r f; do + if ! [[ "$f" =~ \.(md|asc)$ ]]; then + DOCS_ONLY=false + break + fi + done <<< "$CHANGED" + fi + + echo "changed files:" + echo "$CHANGED" + echo "docs_only=$DOCS_ONLY" >> "$GITHUB_OUTPUT" + + - name: Derive the supported-PostgreSQL-major list + id: pg + run: | + # A dozen-odd lines to replace what looks like a handful of version + # references, but it buys CONSISTENCY: 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" + lint: name: ๐Ÿงน SQL lint runs-on: ubuntu-latest @@ -36,9 +181,12 @@ jobs: # 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: - pg: [18, 17, 16, 15, 14, 13, 12, 11, 10] + # 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 @@ -55,6 +203,8 @@ jobs: run: make verify-results TEST_LOAD_SOURCE=update pg-tle-test: + needs: [changes] + if: needs.changes.outputs.docs_only != 'true' strategy: matrix: # Intersection of count_nulls' own supported range (10-18, see the @@ -173,3 +323,43 @@ jobs: 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 + + # 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-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 From f7c8395080856b512ea29984db675f766879f25f Mon Sep 17 00:00:00 2001 From: jnasbyupgrade Date: Thu, 6 Aug 2026 17:04:18 -0500 Subject: [PATCH 2/2] CI: gate draft PRs down to lint + newest-PG test only The org-wide Actions runner queue backs up easily; a draft PR being actively iterated on doesn't need the full PG matrix or the heavy pg-tle-test job re-run on every push. Add a newest_pg scalar output (single source alongside supported_pg) and reduce the test job's matrix to just that value on a draft PR, while skipping pg-tle-test (and any later heavy job following the same needs:[changes]/if: docs_only pattern) outright. Non-draft PRs and push events (e.g. post-merge on master) are unaffected. --- .github/workflows/ci.yml | 48 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 44 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1848af8..1170b2b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,6 +34,26 @@ # 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: @@ -52,13 +72,18 @@ jobs: # 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. + # 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 @@ -150,6 +175,12 @@ jobs: 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 @@ -185,8 +216,14 @@ jobs: if: needs.changes.outputs.docs_only != 'true' strategy: matrix: - # From the single source in the changes job. - pg: ${{ fromJSON(needs.changes.outputs.supported_pg) }} + # 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 @@ -204,7 +241,10 @@ jobs: pg-tle-test: needs: [changes] - if: needs.changes.outputs.docs_only != 'true' + # 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