-
Notifications
You must be signed in to change notification settings - Fork 2
331 lines (320 loc) · 16.6 KB
/
Copy pathci.yml
File metadata and controls
331 lines (320 loc) · 16.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
# Test strategy
#
# A test_factory install can be arrived at two ways, each of which can break
# differently, so each gets its own job below:
#
# test -- FRESH install: CREATE EXTENSION at the current
# version, on every supported PostgreSQL major. The
# install itself runs as a disposable non-superuser
# role (test/install/load.sql's test_factory_installer
# -- see its own comment), not as whatever superuser
# CI happens to connect as: pg_has_role(current_user,
# ...) is unconditionally true for a superuser, so
# only a real non-superuser installer actually
# exercises https://github.com/Postgres-Extensions/test_factory/issues/14's
# fix. A separate superuser-install leg would add
# nothing real -- superuser bypasses every check that
# role is subject to, so if the non-superuser installer
# can do it, a superuser certainly can too.
# pg-upgrade-test -- BINARY pg_upgrade: install the current version on an
# OLD major, binary-upgrade the cluster to a NEWER
# major, then run the suite against the migrated
# objects in "existing" mode (test/install/load.sql).
# Proves objects created on an old server still work
# read back on a new one -- fresh-install testing
# never exercises this at all. Runs as the ambient
# superuser throughout (binary pg_upgrade itself needs
# OS/cluster-level superuser access anyway), not the
# non-superuser installer -- test/install/load.sql only
# switches to it in the fresh/update branch.
#
# test_factory has shipped only one version (0.5.0), so there is no
# in-place `ALTER EXTENSION UPDATE` path to test yet (no extension-update-test
# job) and the pg_upgrade job needs no bridge step (it always installs the
# CURRENT version on the old cluster -- there's no older, pg_upgrade-unsafe
# version in the wild to carry forward). The `test` job derives its
# PostgreSQL major list from the single source of truth computed in the
# `changes` job below, so adding/dropping a supported major is a one-line
# edit there for that job. `pg-upgrade-test`'s matrix is NOT derived from
# that same source -- its old_pg/new_pg pairs are hardcoded literals below,
# deliberately chosen to straddle the PostgreSQL 16 boundary without ever
# crossing it (see that job's own comment for why), so bumping NEWEST/FLOOR
# does not automatically update it.
name: CI
on:
# Post-merge CI only; pull_request already covers every PR commit. Without
# this, a commit on a branch with an open PR fires the workflow TWICE for
# the identical SHA (once for push, once for pull_request/synchronize) --
# double the compute, and a flake on one duplicate run shows a confusing
# red check right next to an identical green one for the same commit.
push:
branches:
- master
pull_request:
# A superseded push (e.g. a quick follow-up commit on the same PR) cancels
# any in-flight run for the same branch instead of letting the full,
# expensive matrix -- including pg-upgrade-test's binary pg_upgrade legs --
# run to completion for a commit nobody cares about anymore.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
PGUSER: postgres
jobs:
# 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:
name: 🧹 SQL Lint
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v7
- name: Lint SQL
run: make lint
# Cheap gate that lets the heavy jobs below skip themselves on commits that
# touch only docs, without a workflow-level `paths-ignore` (which would skip
# the *whole* workflow, including all-checks-passed, on a docs-only push --
# leaving a required check stuck Pending forever). Also derives, from a
# single set of constants, the supported-PostgreSQL-major list every heavy
# job below consumes, so adding a major is a one-line edit here instead of
# N separate matrix edits.
changes:
name: 🔍 Detect 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@v7
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 is the literal first line: any early exit or error
# further down (a bad BASE/HEAD, a failed git diff) leaves this in
# place, so a broken check never silently skips real testing.
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 -- the fail-safe default above stands.
if [ -z "$HEAD" ] || [ -z "$BASE" ] || [[ "$BASE" =~ ^0+$ ]]; then
exit 0
fi
CHANGED=$(git diff --name-only "$BASE" "$HEAD" || echo __DIFF_FAILED__)
echo "changed files:"
echo "$CHANGED"
if [ "$CHANGED" = "__DIFF_FAILED__" ] || [ -z "$CHANGED" ]; then
exit 0
fi
DOCS_ONLY=true
while IFS= read -r f; do
if ! [[ "$f" =~ \.(md|asc)$ ]]; then
DOCS_ONLY=false
break
fi
done <<< "$CHANGED"
echo "docs_only=$DOCS_ONLY" >> "$GITHUB_OUTPUT"
- name: Derive the supported-PostgreSQL-major list
id: pg
run: |
# Single source of truth for the supported PostgreSQL majors: the
# `test` and `pg-upgrade-test` matrices below both derive their
# version lists from here, so they cannot silently drift onto
# different lists. To add or drop a major, edit only NEWEST/FLOOR.
NEWEST=18
FLOOR=10
supported=$(seq "$NEWEST" -1 "$FLOOR")
# Emit a JSON array from a list of ints, for matrix: to consume
# with fromJSON.
json() { printf '%s\n' "$@" | paste -sd, - | sed 's/^/[/; s/$/]/'; }
echo "supported_pg=$(json $supported)" >> "$GITHUB_OUTPUT"
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@v7
# rsync first: pgxntool/run-test-build.sh needs it to sync
# test/build/*.sql into test/build/sql/, and the CONTAINER IMAGE
# doesn't ship rsync.
- name: Install rsync
run: apt-get install -y rsync
# We deliberately don't use `pg-build-test` (this job's CONTAINER
# IMAGE's own helper) for a few reasons -- notably, pgxntool marks
# `installcheck` `.IGNORE`, so pg-build-test's `make installcheck` can
# never actually detect a real regression failure (confirmed
# empirically with a deliberately broken test). `make verify-results`
# (pgxntool's own target) inspects the actual TAP output instead of
# trusting an exit code, and its dependency chain installs the
# extension itself, so no separate `make install` step is needed here.
- name: Test on PostgreSQL ${{ matrix.pg }}
run: make verify-results
# Proves test_factory survives a BINARY pg_upgrade (in-place catalog
# migration to a newer PostgreSQL major), not just a fresh CREATE EXTENSION.
# Each leg: install the CURRENT version on an old cluster, binary-pg_upgrade
# to a newer major, then run the suite against the REAL migrated objects in
# "existing" mode (test/install/load.sql asserts the version, plants, and
# proves the dependency guard -- see bin/test_existing and
# test/install/load.sql). No bridge step: unlike an extension with multiple
# shipped versions, test_factory has only ever shipped 0.5.0, so there is no
# older, pg_upgrade-unsafe install to carry forward -- every leg installs
# the current version directly on the old cluster.
#
# Two legs, deliberately chosen to each stay entirely on one side of the
# PostgreSQL 16 boundary (10->15 and 16->18), never crossing PG16 itself:
# binary pg_upgrade doesn't re-run the install script, so
# https://github.com/Postgres-Extensions/test_factory/issues/14's
# `GRANT ... WITH SET TRUE` fix can't reach a role grant that predates PG16
# (see README's "Known limitation" section).
pg-upgrade-test:
# Also needs `test`, not just `changes`: without this, a trivially-broken
# PR (fails the cheap fresh-install matrix) still burns the full,
# expensive binary pg_upgrade matrix for nothing.
needs: [changes, test]
if: needs.changes.outputs.docs_only != 'true'
strategy:
matrix:
include:
- old_pg: "10"
new_pg: "15"
- old_pg: "16"
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 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@v7
- name: Recreate old cluster with data checksums enabled
run: .github/scripts/pg_upgrade_cluster recreate-old ${{ matrix.old_pg }}
- name: Install pgtap into the old cluster
# test_factory_pgtap requires pgtap (see prepare-old below). Unlike
# the fresh-install `test` job, which gets this for free because
# pg-start's default cluster already has it, this job recreates the
# cluster from scratch (previous step) with none of the fresh-install
# job's setup -- pgtap must be installed explicitly here too, for
# every PostgreSQL major this job touches, old and new alike.
# --pg_config is explicit (not relying on pg-start having already
# pointed the bare `pg_config` on PATH at this major) so this step's
# correctness doesn't depend on that PATH-switching behavior.
run: pgxn install pgtap --sudo --pg_config /usr/lib/postgresql/${{ matrix.old_pg }}/bin/pg_config
- name: Install test_factory into old cluster
run: make install
- name: Prepare the old cluster (install current version + tap schema)
# test_factory_pgtap needs pgtap installed into a dedicated "tap"
# schema BEFORE it installs (see bin/test_existing's prepare-old
# comment) -- a bare CREATE EXTENSION test_factory_pgtap CASCADE with
# no schema prep leaves pgtap wherever the ambient search_path
# resolves, and the suite's SET search_path = tap then can't find
# pgtap's functions.
run: bin/test_existing prepare-old test_factory_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 into the new cluster
# pg_upgrade validates that every extension installed in a database
# being upgraded is also available (control file + library) on the
# NEW cluster's PostgreSQL install -- without this, the upgrade
# itself fails, not just the post-upgrade suite run. --pg_config is
# required here (unlike the previous pgtap install step): this step
# runs before pg-start ever points at the new major, so the plain
# `pg_config` on PATH would still resolve to the OLD version's.
run: pgxn install pgtap --sudo --pg_config /usr/lib/postgresql/${{ matrix.new_pg }}/bin/pg_config
- name: Install test_factory 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: Run the suite against the pg_upgraded database (existing mode)
# run-suite dynamically asserts the installed version (never
# hardcoded), then runs the suite via --use-existing (so pg_regress
# does not drop/recreate the database) and gates on verify-results --
# test/install/load.sql's existing-mode branch plants and proves the
# dependency guard as part of this same invocation.
run: bin/test_existing run-suite test_factory_upgrade
# A single stable check name for use as a required status check in branch
# protection rules (not configured by this PR -- that needs a repo admin).
# 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 every other job passed or was 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]
if: always()
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v7
- 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