[SCM] PostGIS branch master updated. 3.7.0alpha1-595-g9d142afa7

git at osgeo.org git at osgeo.org
Sat Jul 18 11:56:22 PDT 2026


This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "PostGIS".

The branch, master has been updated
       via  9d142afa7d7b6bc11e73059f2ce71e5252be7395 (commit)
      from  76b5538cca7434f6ec94075e3fe99ac99cd86884 (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit 9d142afa7d7b6bc11e73059f2ce71e5252be7395
Author: Darafei Praliaskouski <komzpa at gmail.com>
Date:   Sat Jul 18 11:56:21 2026 -0700

    ci: split long regress checks (!362)
    
    This refactors the Woodpecker regress pipeline so the current long poles report independently and pull requests finish a bounded upgrade sweep.
    
    Observed failure:
    
    - regress pipeline 5097 reached `upgradecheck-pg16-postgis`, then Woodie killed the container wait after about 66 minutes with `context deadline exceeded`
    - the step reported exit code 0, so this was an infrastructure timeout rather than a PostGIS assertion failure
    
    Changes:
    
    - keep the split PostgreSQL 14/16/18 installcheck jobs and serialize jobs sharing one build tree
    - build a separate PostgreSQL 16 upgrade tree immediately after `autogen`
    - prepare that isolated tree with `make staged-install`, which script upgrades require
    - split the all-upgrades sweep by extension with `--extension`
    - add `--oldest`, which semantically sorts sources and runs the oldest compatible, non-skipped source with an update path
    - use `--oldest` only for pull-request pipelines; master and tag pipelines keep the exhaustive sweep
    - serialize the four extension sweeps because they share the installed PostgreSQL instance
    - give each serialized extension sweep its own FIFO and summary log
    
    Checks:
    
    - `python3 utils/test_check_all_upgrades.py` (7 tests)
    - `sh -n utils/check_all_upgrades.sh`
    - `sh -n ci/dronie/postgis_tools.sh`
    - `shellcheck -S error utils/check_all_upgrades.sh ci/dronie/postgis_tools.sh`
    - `woodpecker-cli:v3 lint .woodpecker/regress.yml`
    - `git diff --check`
    
    ---------
    
    Co-authored-by: Darafei Praliaskouski <me at komzpa.net>
    Reviewed-on: https://gitea.osgeo.org/postgis/postgis/pulls/362

diff --git a/.woodpecker/regress.yml b/.woodpecker/regress.yml
index 2cdaa2cc4..666b16fea 100644
--- a/.woodpecker/regress.yml
+++ b/.woodpecker/regress.yml
@@ -6,6 +6,7 @@ variables:
   test-image: &test-image repo.osgeo.org/postgis/build-test:debian13
   steps-env: &steps-env
     - export PATH=/usr/lib/postgresql/$${PGVER}/bin:$${PATH}
+    - export PGHOST=/tmp
     - export PGPORT=$$(grep ^port /etc/postgresql/$${PGVER}/main/postgresql.conf | awk '{print $$3}')
     - export POSTGIS_REGRESS_DB_OWNER=postgis_reg_unprivileged_user
     - export RUNTESTFLAGS="-v
@@ -16,7 +17,14 @@ variables:
     - export CURRENTVERSION=$$(grep '^POSTGIS_' $${SRCDIR}/Version.config | cut -d= -f2 | paste -sd '.')
     - pg_config --version
   steps-start-postgresql: &steps-start-postgresql
-    - service postgresql start $${PGVER}
+    # The test image can need crash recovery after clone-local startup. Under
+    # multi-workflow I/O load, Debian's default 60-second pg_ctl wait is short.
+    # Use /tmp for the socket: runner-specific /run mounts are not portable.
+    - runuser -u postgres -- pg_ctl
+      -D /var/lib/postgresql/$${PGVER}/main
+      -l /tmp/postgresql-$${PGVER}.log
+      -o "-c config_file=/etc/postgresql/$${PGVER}/main/postgresql.conf -c unix_socket_directories=/tmp"
+      -t 300 start
     - psql -c 'select version()' template1
   steps-pg-build: &steps-pg-build
     - mkdir -p "$${BUILDDIR}"
@@ -26,27 +34,24 @@ variables:
       --disable-spellcheck-tests
       --enable-lto
       --without-interrupt-tests
-      --disable-spellcheck-tests
       CFLAGS="-O2 -Wall -fno-omit-frame-pointer -Werror"
-    - make -j
-  steps-pg-test-preinstall: &steps-pg-test-preinstall
-    - cd build/pg$${PGVER}
-    - make check
-    - make check-double-upgrade
-    - make check-locked-upgrade
-  steps-pg-install: &steps-pg-install
-    - make -C build/pg$${PGVER} install
-  steps-pg-test-install: &steps-pg-test-install
-    - service postgresql start $${PGVER}
-    - make -C build/pg$${PGVER} installcheck
+    # Matrix jobs provide the parallelism. Keep each build serial: the PostGIS
+    # make graph can otherwise race generation of files such as postgis.control.
+    - make -j1
   steps-pg-test-all-upgrades: &steps-pg-test-all-upgrades
-    - cd build/pg$${PGVER}
-    - mkfifo check.fifo
-    - tee check.log < check.fifo &
-    - $${SRCDIR}/utils/check_all_upgrades.sh -s $${CURRENTVERSION}! > check.fifo
+    - cd "$${BUILDDIR}"
+    - export CHECK_FIFO=check-$${UPGRADE_EXTENSION}.fifo
+    - export CHECK_LOG=check-$${UPGRADE_EXTENSION}.log
+    - rm -f "$${CHECK_FIFO}"
+    - mkfifo "$${CHECK_FIFO}"
+    - tee "$${CHECK_LOG}" < "$${CHECK_FIFO}" &
+    - UPGRADECHECK_ARGS=
+    - if test "$${CI_PIPELINE_EVENT}" = pull_request; then UPGRADECHECK_ARGS=--oldest; fi
+    - $${SRCDIR}/utils/check_all_upgrades.sh -s $${UPGRADECHECK_ARGS} --extension $${UPGRADE_EXTENSION} $${CURRENTVERSION}! > "$${CHECK_FIFO}"
     - wait # for tee process to flush its buffers
+    - rm -f "$${CHECK_FIFO}"
     - echo "-- Summary of upgrade tests --"
-    - egrep '(PASS|FAIL|SKIP|INFO|ERROR)' check.log
+    - egrep '(PASS|FAIL|SKIP|INFO|ERROR)' "$${CHECK_LOG}"
 
 clone:
   git:
@@ -56,108 +61,194 @@ clone:
       partial: false
 
 steps:
-
   autogen:
     image: *test-image
     pull: true
     commands:
       - ./autogen.sh
 
-  build-pg14:
+  build:
     image: *test-image
     depends_on: autogen
-    environment:
-      PGVER: 14
     commands:
       - <<: *steps-env
       - <<: *steps-pg-build
 
-  build-pg16:
+  build-second-postgresql:
     image: *test-image
     depends_on: autogen
-    environment:
-      PGVER: 16
+    when:
+      - matrix:
+          REGRESS_SUITE: cluster-upgrade
     commands:
+      - export PGVER=$${SECOND_PGVER}
       - <<: *steps-env
       - <<: *steps-pg-build
 
-  build-pg18:
+  test-preinstall:
     image: *test-image
-    depends_on: autogen
-    environment:
-       PGVER: 18
-    commands:
-      - <<: *steps-env
-      - <<: *steps-pg-build
-
-  check-pg14:
-    image: *test-image
-    depends_on: build-pg14
-    environment:
-      PGVER: 14
+    depends_on: build
+    when:
+      - matrix:
+          REGRESS_SUITE: preinstall
     commands:
       - <<: *steps-env
       - <<: *steps-start-postgresql
-      - <<: *steps-pg-test-preinstall
+      - make -C "$${BUILDDIR}" $${REGRESS_TARGET}
 
-  check-pg18:
+  test-install:
     image: *test-image
-    depends_on: build-pg18
-    environment:
-       PGVER: 18
+    depends_on: build
+    when:
+      - matrix:
+          REGRESS_SUITE: install
     commands:
       - <<: *steps-env
       - <<: *steps-start-postgresql
-      - <<: *steps-pg-test-preinstall
+      - make -C "$${BUILDDIR}" install
+      - make -C "$${BUILDDIR}" $${REGRESS_TARGET}
 
-  installcheck-pg14:
+  test-upgrades:
     image: *test-image
-    depends_on: build-pg14
-    environment:
-      PGVER: 14
+    depends_on: build
+    when:
+      - matrix:
+          REGRESS_SUITE: extension-upgrade
     commands:
       - <<: *steps-env
       - <<: *steps-start-postgresql
-      - <<: *steps-pg-install
-      - <<: *steps-pg-test-install
-
-  installcheck-pg16:
-    image: *test-image
-    depends_on: build-pg16
-    environment:
-      PGVER: 16
-    commands:
-      - <<: *steps-env
-      - <<: *steps-start-postgresql
-      - <<: *steps-pg-install
-      - <<: *steps-pg-test-install
-
-  installcheck-pg18:
-    image: *test-image
-    depends_on: build-pg18
-    environment:
-      PGVER: 18
-    commands:
-      - <<: *steps-env
-      - <<: *steps-start-postgresql
-      - <<: *steps-pg-install
-      - <<: *steps-pg-test-install
-
-  upgradecheck-pg16:
-    image: *test-image
-    depends_on: installcheck-pg16
-    environment:
-      PGVER: 16
-    commands:
-      - <<: *steps-env
-      - <<: *steps-start-postgresql
-      - <<: *steps-pg-install
+      - make -C "$${BUILDDIR}" staged-install
+      - make -C "$${BUILDDIR}" install
       - <<: *steps-pg-test-all-upgrades
 
-  cluster-upgradecheck-pg14-to-pg18:
+  test-cluster-upgrade:
     image: *test-image
-    depends_on: [ installcheck-pg14, installcheck-pg18 ]
+    depends_on: [ build, build-second-postgresql ]
+    when:
+      - matrix:
+          REGRESS_SUITE: cluster-upgrade
     commands:
-      - make -C build/pg14 install
-      - make -C build/pg18 install
-      - su postgres -c 'utils/check_cluster_upgrade.sh -i regress/hooks/cluster-upgrade-geography.sql /usr/lib/postgresql/14/bin/pg_config /usr/lib/postgresql/18/bin/pg_config'
+      - make -C build/pg$${PGVER} install
+      - make -C build/pg$${SECOND_PGVER} install
+      - su postgres -c 'utils/check_cluster_upgrade.sh -i regress/hooks/cluster-upgrade-geography.sql /usr/lib/postgresql/'$${PGVER}'/bin/pg_config /usr/lib/postgresql/'$${SECOND_PGVER}'/bin/pg_config'
+
+# Every entry is an independent workflow with its own clone, workspace,
+# containers, PostgreSQL instance, and regression database. Splitting at this
+# boundary lets Woodpecker schedule the long suites across agents safely.
+matrix:
+  include:
+    - PGVER: "14"
+      SECOND_PGVER: "0"
+      REGRESS_SUITE: preinstall
+      REGRESS_TARGET: check
+      UPGRADE_EXTENSION: none
+    - PGVER: "14"
+      SECOND_PGVER: "0"
+      REGRESS_SUITE: preinstall
+      REGRESS_TARGET: check-double-upgrade
+      UPGRADE_EXTENSION: none
+    - PGVER: "14"
+      SECOND_PGVER: "0"
+      REGRESS_SUITE: preinstall
+      REGRESS_TARGET: check-locked-upgrade
+      UPGRADE_EXTENSION: none
+    - PGVER: "18"
+      SECOND_PGVER: "0"
+      REGRESS_SUITE: preinstall
+      REGRESS_TARGET: check
+      UPGRADE_EXTENSION: none
+    - PGVER: "18"
+      SECOND_PGVER: "0"
+      REGRESS_SUITE: preinstall
+      REGRESS_TARGET: check-double-upgrade
+      UPGRADE_EXTENSION: none
+    - PGVER: "18"
+      SECOND_PGVER: "0"
+      REGRESS_SUITE: preinstall
+      REGRESS_TARGET: check-locked-upgrade
+      UPGRADE_EXTENSION: none
+    - PGVER: "14"
+      SECOND_PGVER: "0"
+      REGRESS_SUITE: install
+      REGRESS_TARGET: installcheck-base
+      UPGRADE_EXTENSION: none
+    - PGVER: "14"
+      SECOND_PGVER: "0"
+      REGRESS_SUITE: install
+      REGRESS_TARGET: installcheck-upgrade-from-unpackaged
+      UPGRADE_EXTENSION: none
+    - PGVER: "14"
+      SECOND_PGVER: "0"
+      REGRESS_SUITE: install
+      REGRESS_TARGET: installcheck-upgrade-from-unpackaged-by-func
+      UPGRADE_EXTENSION: none
+    - PGVER: "14"
+      SECOND_PGVER: "0"
+      REGRESS_SUITE: install
+      REGRESS_TARGET: installcheck-upgrade-from-packaged-by-func
+      UPGRADE_EXTENSION: none
+    - PGVER: "16"
+      SECOND_PGVER: "0"
+      REGRESS_SUITE: install
+      REGRESS_TARGET: installcheck-base
+      UPGRADE_EXTENSION: none
+    - PGVER: "16"
+      SECOND_PGVER: "0"
+      REGRESS_SUITE: install
+      REGRESS_TARGET: installcheck-upgrade-from-unpackaged
+      UPGRADE_EXTENSION: none
+    - PGVER: "16"
+      SECOND_PGVER: "0"
+      REGRESS_SUITE: install
+      REGRESS_TARGET: installcheck-upgrade-from-unpackaged-by-func
+      UPGRADE_EXTENSION: none
+    - PGVER: "16"
+      SECOND_PGVER: "0"
+      REGRESS_SUITE: install
+      REGRESS_TARGET: installcheck-upgrade-from-packaged-by-func
+      UPGRADE_EXTENSION: none
+    - PGVER: "18"
+      SECOND_PGVER: "0"
+      REGRESS_SUITE: install
+      REGRESS_TARGET: installcheck-base
+      UPGRADE_EXTENSION: none
+    - PGVER: "18"
+      SECOND_PGVER: "0"
+      REGRESS_SUITE: install
+      REGRESS_TARGET: installcheck-upgrade-from-unpackaged
+      UPGRADE_EXTENSION: none
+    - PGVER: "18"
+      SECOND_PGVER: "0"
+      REGRESS_SUITE: install
+      REGRESS_TARGET: installcheck-upgrade-from-unpackaged-by-func
+      UPGRADE_EXTENSION: none
+    - PGVER: "18"
+      SECOND_PGVER: "0"
+      REGRESS_SUITE: install
+      REGRESS_TARGET: installcheck-upgrade-from-packaged-by-func
+      UPGRADE_EXTENSION: none
+    - PGVER: "16"
+      SECOND_PGVER: "0"
+      REGRESS_SUITE: extension-upgrade
+      REGRESS_TARGET: none
+      UPGRADE_EXTENSION: postgis
+    - PGVER: "16"
+      SECOND_PGVER: "0"
+      REGRESS_SUITE: extension-upgrade
+      REGRESS_TARGET: none
+      UPGRADE_EXTENSION: postgis_topology
+    - PGVER: "16"
+      SECOND_PGVER: "0"
+      REGRESS_SUITE: extension-upgrade
+      REGRESS_TARGET: none
+      UPGRADE_EXTENSION: postgis_raster
+    - PGVER: "16"
+      SECOND_PGVER: "0"
+      REGRESS_SUITE: extension-upgrade
+      REGRESS_TARGET: none
+      UPGRADE_EXTENSION: postgis_sfcgal
+    - PGVER: "14"
+      SECOND_PGVER: "18"
+      REGRESS_SUITE: cluster-upgrade
+      REGRESS_TARGET: none
+      UPGRADE_EXTENSION: none
diff --git a/ci/dronie/postgis_tools.sh b/ci/dronie/postgis_tools.sh
index b8f01a188..ae3e36ae4 100644
--- a/ci/dronie/postgis_tools.sh
+++ b/ci/dronie/postgis_tools.sh
@@ -1,4 +1,4 @@
- #!/usr/bin/env bash
+#!/usr/bin/env bash
 
 # Exit on first error
 set -e
@@ -11,3 +11,4 @@ make install
 /tmp/pgx/bin/postgis help
 /tmp/pgx/bin/shp2pgsql
 /tmp/pgx/bin/raster2pgsql
+python3 utils/test_check_all_upgrades.py
diff --git a/regress/core/regress_index.sql b/regress/core/regress_index.sql
index 09c71254d..4b0ea9676 100644
--- a/regress/core/regress_index.sql
+++ b/regress/core/regress_index.sql
@@ -132,14 +132,13 @@ SELECT 'expr &&', id, estimate_error(
 SELECT 'st_orderingequals_idx',
   qnodes('select * from test where ST_OrderingEquals(the_geom, ST_MakePoint(0,0))');
 
-CREATE TABLE st_orderingequals_volatile_calls (num integer);
-INSERT INTO st_orderingequals_volatile_calls VALUES (0);
+CREATE SEQUENCE st_orderingequals_volatile_calls;
 
 CREATE OR REPLACE FUNCTION st_orderingequals_volatile_geom(g geometry)
 RETURNS geometry
 LANGUAGE 'plpgsql' VOLATILE AS $$
 BEGIN
-  UPDATE st_orderingequals_volatile_calls SET num = num + 1;
+  PERFORM nextval('st_orderingequals_volatile_calls');
   RETURN g;
 END;
 $$;
@@ -148,10 +147,10 @@ SELECT 'st_orderingequals_volatile_count', count(*)
 FROM (VALUES (ST_MakePoint(0,0))) AS q(g)
 JOIN test t ON ST_OrderingEquals(st_orderingequals_volatile_geom(t.the_geom), q.g);
 
-SELECT 'st_orderingequals_volatile', num FROM st_orderingequals_volatile_calls;
+SELECT 'st_orderingequals_volatile', last_value FROM st_orderingequals_volatile_calls;
 
 DROP FUNCTION st_orderingequals_volatile_geom(geometry);
-DROP TABLE st_orderingequals_volatile_calls;
+DROP SEQUENCE st_orderingequals_volatile_calls;
 
 CREATE SCHEMA st_orderingequals_search_path;
 CREATE FUNCTION st_orderingequals_search_path.geometry_bbox_false(geometry, geometry)
diff --git a/utils/check_all_upgrades.sh b/utils/check_all_upgrades.sh
index 8e02e21a1..85edc853a 100755
--- a/utils/check_all_upgrades.sh
+++ b/utils/check_all_upgrades.sh
@@ -7,6 +7,8 @@ TMPDIR=$(mktemp -d "${TMPDIR:-/tmp}/check_all_upgrades.XXXXXX") || exit 1
 PGVER=`pg_config --version | awk '{print $2}'`
 PGVER_MAJOR=$(echo "${PGVER}" | sed 's/\.[^\.]*//' | sed 's/\(alpha\|beta\|rc\).*//' )
 SKIP_LABEL_REGEXP=
+ONLY_EXTENSION=
+ONLY_OLDEST=0
 echo "INFO: PostgreSQL version: ${PGVER} [${PGVER_MAJOR}]"
 MAKE=$(which gmake make | head -1)
 BUILDDIR=$PWD # TODO: allow override ?
@@ -24,6 +26,8 @@ usage() {
   echo "\t--help|-h        Print this usage text"
   echo "\t-s               Stop on first failure"
   echo "\t--skip <regexp>  Do not run tests with label matching given extended regexp"
+  echo "\t--extension <name>  Only run upgrade tests for the named extension"
+  echo "\t--oldest        Only run the oldest compatible upgrade source"
   echo "Positional parameters:"
   echo "\t<to_version>     Target upgrade version. See regress/run_test.pl help on --upgrade-path <to> parameter."
 }
@@ -34,6 +38,11 @@ while test -n "$1"; do
   elif test "$1" = "--skip"; then
     shift
     SKIP_LABEL_REGEXP=$1
+  elif test "$1" = "--extension"; then
+    shift
+    ONLY_EXTENSION=$1
+  elif test "$1" = "--oldest"; then
+    ONLY_OLDEST=1
   elif test "$1" = "-h" -o "$1" = "--help"; then
     usage
     exit 0
@@ -89,6 +98,28 @@ semver_compare()
   echo 0; return;
 }
 
+# Emit versions in ascending semantic-version order without relying on GNU sort.
+sort_versions()
+{
+  unsorted="$*"
+  while test -n "${unsorted}"; do
+    oldest=
+    remaining=
+    for version in ${unsorted}; do
+      if test -z "${oldest}"; then
+        oldest=${version}
+      elif test "`semver_compare "${version}" "${oldest}"`" -lt 0; then
+        remaining="${remaining} ${oldest}"
+        oldest=${version}
+      else
+        remaining="${remaining} ${version}"
+      fi
+    done
+    echo "${oldest}"
+    unsorted=${remaining}
+  done
+}
+
 failed()
 {
   failures=$((failures+1))
@@ -248,6 +279,20 @@ if test -f postgis_sfcgal--${to_version}.sql; then
   INSTALLED_EXTENSIONS="$INSTALLED_EXTENSIONS postgis_sfcgal"
 fi
 
+if test -n "${ONLY_EXTENSION}"; then
+  selected_extensions=
+  for EXT in ${INSTALLED_EXTENSIONS}; do
+    if test "${EXT}" = "${ONLY_EXTENSION}"; then
+      selected_extensions="${EXT}"
+    fi
+  done
+  if test -z "${selected_extensions}"; then
+    echo "SKIP: extension ${ONLY_EXTENSION} is not installed for target ${to_version}"
+    exit 0
+  fi
+  INSTALLED_EXTENSIONS=${selected_extensions}
+fi
+
 echo "INFO: installed extensions: $INSTALLED_EXTENSIONS"
 
 USERTESTFLAGS=${RUNTESTFLAGS}
@@ -275,6 +320,9 @@ for EXT in ${INSTALLED_EXTENSIONS}; do #{
 
   # Check extension->extension upgrades
   files=`'ls' ${EXT}--* | grep -v -- '--.*--' | sed "s/^${EXT}--\(.*\)\.sql/\1/"`
+  if test ${ONLY_OLDEST} != 0; then
+    files=`sort_versions ${files}`
+  fi
   for fname in $files; do
     from_version="$fname"
     if test "$from_version" = "unpackaged"; then
@@ -290,6 +338,11 @@ for EXT in ${INSTALLED_EXTENSIONS}; do #{
       continue;
     fi
 
+    if test ${ONLY_OLDEST} != 0 -a $cmp -gt 0; then
+      echo "SKIP: ${from_version} -> ${to_version} (--oldest only tests upgrades)"
+      continue;
+    fi
+
     if test $cmp -lt 0; then
       test_label="${EXT} extension upgrade ${UPGRADE_PATH}"
     else
@@ -337,6 +390,10 @@ for EXT in ${INSTALLED_EXTENSIONS}; do #{
       " check_downgrade
     fi
 
+    if test ${ONLY_OLDEST} != 0; then
+      break
+    fi
+
   done
 
   if ! kept_label "unpackaged"; then
@@ -345,7 +402,11 @@ for EXT in ${INSTALLED_EXTENSIONS}; do #{
   fi
 
   # Check unpackaged->extension upgrades
-  for majmin in `'ls' -d ${CTBDIR}/postgis-* | sed 's/.*postgis-//'`; do
+  unpackaged_versions=`'ls' -d ${CTBDIR}/postgis-* | sed 's/.*postgis-//'`
+  if test ${ONLY_OLDEST} != 0; then
+    unpackaged_versions=`sort_versions ${unpackaged_versions}`
+  fi
+  for majmin in ${unpackaged_versions}; do
     UPGRADE_PATH="unpackaged${majmin}--${to_version_param}"
     test_label="${EXT} extension upgrade ${UPGRADE_PATH}"
     if expr $to_version_param : ':auto' >/dev/null; then
@@ -371,6 +432,9 @@ for EXT in ${INSTALLED_EXTENSIONS}; do #{
       echo "FAIL: ${test_label}"
       failed
     }
+    if test ${ONLY_OLDEST} != 0; then
+      break
+    fi
   done
 
   # Check unpackaged->unpackaged upgrades (if target version == current version)
@@ -382,7 +446,7 @@ for EXT in ${INSTALLED_EXTENSIONS}; do #{
     continue
   fi #}
 
-  for majmin in `'ls' -d ${CTBDIR}/postgis-* | sed 's/.*postgis-//'`
+  for majmin in ${unpackaged_versions}
   do #{
     UPGRADE_PATH="unpackaged${majmin}--:auto"
     test_label="${EXT} script soft upgrade ${UPGRADE_PATH}"
@@ -392,7 +456,9 @@ for EXT in ${INSTALLED_EXTENSIONS}; do #{
 
     compatible_upgrade "${test_label}" ${majmin} ${to_version} || continue
 
-    if kept_label "${test_label}"; then #{
+    run_soft_upgrade=1
+    kept_label "${test_label}" || run_soft_upgrade=0
+    if test ${run_soft_upgrade} != 0; then #{
       echo "Testing ${test_label}"
       RUNTESTFLAGS="-v --upgrade-path=${UPGRADE_PATH} ${USERTESTFLAGS}" \
       ${MAKE} -C ${REGDIR} check ${MAKE_ARGS} && {
@@ -404,7 +470,9 @@ for EXT in ${INSTALLED_EXTENSIONS}; do #{
     fi #}
 
     test_label="${EXT} script hard upgrade ${UPGRADE_PATH}"
-    if kept_label "${test_label}"; then #{
+    run_hard_upgrade=1
+    kept_label "${test_label}" || run_hard_upgrade=0
+    if test ${run_hard_upgrade} != 0; then #{
       echo "Testing ${test_label}"
       RUNTESTFLAGS="-v --dumprestore --upgrade-path=${UPGRADE_PATH} ${USERTESTFLAGS}" \
       ${MAKE} -C ${REGDIR} check ${MAKE_ARGS} && {
@@ -415,6 +483,12 @@ for EXT in ${INSTALLED_EXTENSIONS}; do #{
       }
     fi #}
 
+    if test ${ONLY_OLDEST} != 0; then
+      if test ${run_soft_upgrade} != 0 || test ${run_hard_upgrade} != 0; then
+        break
+      fi
+    fi
+
   done #}
 
 done #}
diff --git a/utils/test_check_all_upgrades.py b/utils/test_check_all_upgrades.py
new file mode 100644
index 000000000..3b8633d56
--- /dev/null
+++ b/utils/test_check_all_upgrades.py
@@ -0,0 +1,345 @@
+#!/usr/bin/env python3
+
+import json
+import os
+import re
+import subprocess
+import tempfile
+from pathlib import Path
+import unittest
+
+
+class FakePostgresTools:
+    def __init__(self, repo_root):
+        self.repo_root = repo_root
+        self.temp_dir = tempfile.TemporaryDirectory()
+        self.root = Path(self.temp_dir.name)
+        self.sharedir = self.root / "pg_sharedir"
+        self.extension_dir = self.sharedir / "extension"
+        self.contrib_dir = self.sharedir / "contrib"
+        self.bin_dir = self.root / "bin"
+        self.gmake_log = self.root / "gmake.log"
+        self.psql_log = self.root / "psql.log"
+        self.extension_dir.mkdir(parents=True)
+        self.contrib_dir.mkdir(parents=True)
+        self.bin_dir.mkdir(parents=True)
+        self.path_map = {}
+        self.fail_patterns = {}
+        self._write_fake_commands()
+
+    def _write_fake_commands(self):
+        pg_config = """#!/usr/bin/env bash
+set -eu
+
+case "$1" in
+  --version)
+    echo "$CHECK_ALL_UPGRADES_PG_VERSION"
+    ;;
+  --sharedir)
+    echo "$CHECK_ALL_UPGRADES_SHAREDIR"
+    ;;
+  *)
+    echo "$CHECK_ALL_UPGRADES_SHAREDIR"
+    ;;
+esac
+"""
+
+        psql = """#!/usr/bin/env python3
+import json
+import os
+import re
+import sys
+
+args = sys.argv[1:]
+query = ""
+
+for i, arg in enumerate(args):
+    if arg in ("-c", "--command") and i + 1 < len(args):
+        query = args[i + 1]
+        break
+if not query:
+    query = " ".join(args).strip()
+query = " ".join(query.split())
+
+log = os.environ.get("CHECK_ALL_UPGRADES_PSQL_LOG")
+if log:
+    with open(log, "a", encoding="utf-8") as fp:
+        fp.write(query + "\\n")
+
+if re.search("select default_version", query, flags=re.I):
+    print(os.environ["CHECK_ALL_UPGRADES_AUTO_VERSION"])
+    sys.exit(0)
+
+m = re.search(
+    r"pg_extension_update_paths\\(\\s*'([^']+)'\\s*\\)\\s*.*\\s*source\\s*=\\s*'([^']+)'\\s*.*\\s*target\\s*=\\s*'([^']+)'",
+    query,
+    flags=re.I | re.S,
+)
+if not m:
+    print(query)
+    sys.exit(1)
+
+extension, source, target = m.groups()
+path_map = json.loads(os.environ.get("CHECK_ALL_UPGRADES_PSQL_PATH_MAP", "{}"))
+path = path_map.get(f"{extension}|{source}|{target}")
+if path is not None:
+    print(path)
+    sys.exit(0)
+
+sys.exit(0)
+"""
+
+        gmake = """#!/usr/bin/env python3
+import json
+import os
+import sys
+
+argv = sys.argv[1:]
+cmd = " ".join(argv)
+runflags = os.environ.get("RUNTESTFLAGS", "")
+full = " ".join([cmd, runflags]).strip()
+
+log = os.environ.get("CHECK_ALL_UPGRADES_GMAKE_LOG")
+if log:
+    with open(log, "a", encoding="utf-8") as fp:
+        json.dump({"argv": argv, "cmd": cmd}, fp)
+        fp.write("\\n")
+
+fail_patterns = os.environ.get("CHECK_ALL_UPGRADES_GMAKE_FAIL", "{}")
+for fragment, code in json.loads(fail_patterns).items():
+    if fragment in full:
+        sys.exit(int(code))
+
+sys.exit(0)
+"""
+
+        for name, body in {
+            "pg_config": pg_config,
+            "psql": psql,
+            "gmake": gmake,
+            "make": gmake,
+        }.items():
+            script = self.bin_dir / name
+            script.write_text(body + "\n", encoding="utf-8")
+            script.chmod(0o755)
+
+    def create_extension_files(self, extension, versions):
+        for version in versions:
+            (self.extension_dir / f"{extension}--{version}.sql").write_text("--", encoding="utf-8")
+
+    def create_contrib_files(self, versions):
+        for version in versions:
+            (self.contrib_dir / f"postgis-{version}").write_text("--", encoding="utf-8")
+
+    def set_path_map(self, mapping):
+        self.path_map = mapping
+
+    def set_fail_patterns(self, mapping):
+        self.fail_patterns = mapping
+
+    def env(self):
+        env = os.environ.copy()
+        env["PATH"] = f"{self.bin_dir}:{env['PATH']}"
+        env["CHECK_ALL_UPGRADES_SHAREDIR"] = str(self.sharedir)
+        env["CHECK_ALL_UPGRADES_PG_VERSION"] = "PostgreSQL 16.2 (Debian)"
+        env["CHECK_ALL_UPGRADES_AUTO_VERSION"] = "3.7.0dev"
+        env["CHECK_ALL_UPGRADES_PSQL_PATH_MAP"] = json.dumps(self.path_map)
+        env["CHECK_ALL_UPGRADES_GMAKE_FAIL"] = json.dumps(self.fail_patterns)
+        env["CHECK_ALL_UPGRADES_PSQL_LOG"] = str(self.psql_log)
+        env["CHECK_ALL_UPGRADES_GMAKE_LOG"] = str(self.gmake_log)
+        return env
+
+    def run(self, to_version, args=(), extra_env=None):
+        if self.gmake_log.exists():
+            self.gmake_log.unlink()
+        if self.psql_log.exists():
+            self.psql_log.unlink()
+
+        env = self.env()
+        if extra_env:
+            env.update(extra_env)
+
+        return subprocess.run(
+            ["bash", str(self.repo_root / "utils/check_all_upgrades.sh"), *args, to_version],
+            cwd=self.repo_root,
+            env=env,
+            text=True,
+            capture_output=True,
+        )
+
+    def make_calls(self):
+        if not self.gmake_log.exists():
+            return []
+        return [json.loads(line) for line in self.gmake_log.read_text(encoding="utf-8").splitlines() if line.strip()]
+
+    def close(self):
+        self.temp_dir.cleanup()
+
+
+class CheckAllUpgradesHarnessTest(unittest.TestCase):
+    def setUp(self):
+        self.repo_root = Path(__file__).resolve().parents[1]
+        self.harness = FakePostgresTools(self.repo_root)
+
+    def tearDown(self):
+        self.harness.close()
+
+    def test_default_exhaustive_runs_packaged_and_unpacked_paths(self):
+        self.harness.create_extension_files("postgis", ["3.5.0", "3.6.0", "3.7.0dev"])
+        self.harness.create_extension_files("postgis_topology", ["3.7.0dev"])
+        self.harness.create_contrib_files(["3.6.0"])
+        self.harness.set_path_map(
+            {
+                "postgis|3.5.0|3.7.0dev": "up",
+                "postgis|3.6.0|3.7.0dev": "up",
+                "postgis|unpackaged|3.7.0dev": "up",
+                "postgis_topology|3.7.0dev|3.7.0dev": "up",
+                "postgis_topology|unpackaged|3.7.0dev": "up",
+            }
+        )
+
+        result = self.harness.run("3.7.0dev")
+        output = result.stdout + result.stderr
+
+        self.assertEqual(0, result.returncode)
+        self.assertIn("Testing postgis extension upgrade 3.5.0--3.7.0dev", output)
+        self.assertIn("Testing postgis extension upgrade 3.6.0--3.7.0dev", output)
+        self.assertIn("Testing postgis script soft upgrade", output)
+        self.assertIn("Testing postgis script hard upgrade", output)
+
+        calls = self.harness.make_calls()
+        self.assertTrue(calls)
+
+    def test_oldest_uses_semantic_ordering_for_3_9_vs_3_10(self):
+        self.harness.create_extension_files("postgis", ["3.10.0", "3.9.0", "3.11.0"])
+        self.harness.create_contrib_files(["3.10.0"])
+        self.harness.set_path_map(
+            {
+                "postgis|3.10.0|3.11.0": "up",
+                "postgis|3.9.0|3.11.0": "up",
+                "postgis|unpackaged|3.11.0": "up",
+            }
+        )
+
+        result = self.harness.run("3.11.0", args=("--oldest",))
+        output = result.stdout + result.stderr
+
+        self.assertEqual(0, result.returncode)
+        self.assertIn("Testing postgis extension upgrade 3.9.0--3.11.0", output)
+        self.assertNotIn("extension upgrade 3.10.0--3.11.0", output)
+
+    def test_oldest_falls_back_when_first_source_has_no_update_path(self):
+        self.harness.create_extension_files("postgis", ["3.10.0", "3.9.0", "3.11.0"])
+        self.harness.create_contrib_files(["3.10.0"])
+        self.harness.set_path_map(
+            {
+                "postgis|3.9.0|3.11.0": "",
+                "postgis|3.10.0|3.11.0": "up",
+                "postgis|unpackaged|3.11.0": "up",
+            }
+        )
+
+        result = self.harness.run("3.11.0", args=("--oldest",))
+        output = result.stdout + result.stderr
+
+        self.assertEqual(0, result.returncode)
+        self.assertIn("SKIP: postgis extension upgrade 3.9.0--3.11.0 (no upgrade path", output)
+        self.assertIn("Testing postgis extension upgrade 3.10.0--3.11.0", output)
+
+    def test_oldest_skipped_when_oldest_label_is_excluded_by_regex(self):
+        self.harness.create_extension_files("postgis", ["3.10.0", "3.9.0", "3.11.0"])
+        self.harness.create_contrib_files(["3.10.0"])
+        self.harness.set_path_map(
+            {
+                "postgis|3.9.0|3.11.0": "up",
+                "postgis|3.10.0|3.11.0": "up",
+                "postgis|unpackaged|3.11.0": "up",
+            }
+        )
+
+        result = self.harness.run(
+            "3.11.0",
+            args=("--oldest", "--skip", r"3\.9\.0--3\.11\.0"),
+        )
+        output = result.stdout + result.stderr
+
+        self.assertEqual(0, result.returncode)
+        self.assertIn("SKIP: postgis extension upgrade 3.9.0--3.11.0", output)
+        self.assertIn("Testing postgis extension upgrade 3.10.0--3.11.0", output)
+
+    def test_extension_filter_runs_only_requested_extension(self):
+        self.harness.create_extension_files("postgis", ["3.8.0", "3.7.0"])
+        self.harness.create_extension_files("postgis_topology", ["3.6.0", "3.7.0"])
+        self.harness.create_contrib_files(["3.6.0"])
+        self.harness.set_path_map(
+            {
+                "postgis_topology|3.6.0|3.7.0": "up",
+                "postgis|3.8.0|3.7.0": "up",
+                "postgis_topology|3.7.0|3.7.0": "up",
+                "postgis|3.7.0|3.7.0": "up",
+                "postgis_topology|unpackaged|3.7.0": "up",
+                "postgis|unpackaged|3.7.0": "up",
+            }
+        )
+
+        result = self.harness.run("3.7.0", args=("--extension", "postgis_topology"))
+        output = result.stdout + result.stderr
+        calls = self.harness.make_calls()
+
+        self.assertEqual(0, result.returncode)
+        self.assertNotIn("Testing postgis extension upgrade", output)
+        self.assertIn("Testing postgis_topology extension upgrade 3.6.0--3.7.0", output)
+        for call in calls:
+            cmd = " ".join(call["argv"])
+            self.assertIn("-C", cmd)
+            self.assertIn("topology/test", cmd)
+
+    def test_unpackaged_soft_and_hard_share_oldest_upgrade_source(self):
+        self.harness.create_extension_files("postgis", ["3.6.0", "3.7.0dev"])
+        self.harness.create_contrib_files(["3.5.0", "3.6.0"])
+        self.harness.set_path_map(
+            {
+                "postgis|3.6.0|3.7.0dev": "up",
+                "postgis|unpackaged|3.7.0dev": "up",
+            }
+        )
+
+        result = self.harness.run("3.7.0dev", args=("--oldest",))
+        output = result.stdout + result.stderr
+        calls = self.harness.make_calls()
+
+        self.assertEqual(0, result.returncode)
+        self.assertIn("Testing postgis script soft upgrade", output)
+        self.assertIn("Testing postgis script hard upgrade", output)
+
+        self.assertTrue(calls)
+        soft = re.search(r"Testing postgis script soft upgrade unpackaged(\S+)--:auto", output)
+        hard = re.search(r"Testing postgis script hard upgrade unpackaged(\S+)--:auto", output)
+        self.assertIsNotNone(soft)
+        self.assertIsNotNone(hard)
+        self.assertEqual(soft.group(1), hard.group(1))
+
+    def test_make_child_failure_keeps_nonzero_and_stops_with_dash_s(self):
+        self.harness.create_extension_files("postgis", ["3.5.0", "3.6.0", "3.7.0"])
+        self.harness.create_contrib_files(["3.5.0"])
+        self.harness.set_path_map(
+            {
+                "postgis|3.5.0|3.7.0": "up",
+                "postgis|3.6.0|3.7.0": "up",
+                "postgis|unpackaged|3.7.0": "up",
+            }
+        )
+        self.harness.set_fail_patterns({"3.5.0--3.7.0": 1})
+
+        result = self.harness.run("3.7.0", args=("-s",))
+        output = result.stdout + result.stderr
+        calls = self.harness.make_calls()
+
+        self.assertNotEqual(0, result.returncode)
+        self.assertIn("FAIL: postgis extension upgrade 3.5.0--3.7.0", output)
+        self.assertNotIn("Testing postgis extension upgrade 3.6.0--3.7.0", output)
+        self.assertLessEqual(len(calls), 1)
+
+
+if __name__ == "__main__":
+    unittest.main()

-----------------------------------------------------------------------

Summary of changes:
 .woodpecker/regress.yml          | 265 ++++++++++++++++++++----------
 ci/dronie/postgis_tools.sh       |   3 +-
 regress/core/regress_index.sql   |   9 +-
 utils/check_all_upgrades.sh      |  82 +++++++++-
 utils/test_check_all_upgrades.py | 345 +++++++++++++++++++++++++++++++++++++++
 5 files changed, 607 insertions(+), 97 deletions(-)
 create mode 100644 utils/test_check_all_upgrades.py


hooks/post-receive
-- 
PostGIS


More information about the postgis-tickets mailing list