[SCM] PostGIS branch master updated. 3.7.0beta1-26-g17935ee71
git at osgeo.org
git at osgeo.org
Sat Jul 25 10:02:53 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 17935ee71e12df9b315b171d2d080c7a16aa7193 (commit)
from 6111c98a95a0799cf0e318247d8630c2acb00863 (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 17935ee71e12df9b315b171d2d080c7a16aa7193
Author: Darafei Praliaskouski <komzpa at gmail.com>
Date: Sat Jul 25 10:02:52 2026 -0700
ci: reject NEWS entries in closed releases (!508)
Pull-request branches can currently add release notes to already closed release
sections because `check_news.sh` validates heading order but does not compare
new entries with the target branch.
Fetch the pull request's target branch into a dedicated ref and compare its
`NEWS` file with the proposed tree. Reject new entries in closed sections while
allowing entries in the current unreleased section, a new unreleased section
created after a release cut, and unchanged historical entries.
Run the comparison from the Woodpecker tools workflow and add 12 focused unit
tests covering release cuts, stale branches, shallow checkouts, duplicate
entries, and historical indentation.
---------
Co-authored-by: Darafei Praliaskouski <me at komzpa.net>
Reviewed-on: https://gitea.osgeo.org/postgis/postgis/pulls/508
diff --git a/GNUmakefile.in b/GNUmakefile.in
index 89b1675a0..cbd0fd83b 100644
--- a/GNUmakefile.in
+++ b/GNUmakefile.in
@@ -100,6 +100,11 @@ visual-examples: staged-install
$(MAKE) -C regress visual-examples-run
check-news:
+ @if test -z "$(PYTHON3)"; then \
+ echo "SKIP: NEWS unit tests require python3"; \
+ else \
+ $(PYTHON3) -B $(top_srcdir)/utils/test_check_news.py; \
+ fi
$(top_srcdir)/utils/check_news.sh $(top_srcdir)
.PHONY: check-contributor-credits
diff --git a/ci/dronie/postgis_tools.sh b/ci/dronie/postgis_tools.sh
index ae3e36ae4..920392862 100644
--- a/ci/dronie/postgis_tools.sh
+++ b/ci/dronie/postgis_tools.sh
@@ -3,6 +3,17 @@
# Exit on first error
set -e
+if test "${CI_PIPELINE_EVENT:-}" = pull_request; then
+ if test -z "${CI_COMMIT_TARGET_BRANCH:-}"; then
+ echo "CI_COMMIT_TARGET_BRANCH is required for pull-request NEWS checks" >&2
+ exit 1
+ fi
+ git fetch --no-tags origin \
+ "+refs/heads/${CI_COMMIT_TARGET_BRANCH}:refs/news-check/target"
+ NEWS_CHECK_BASE_REF="refs/news-check/target"
+ export NEWS_CHECK_BASE_REF
+fi
+
sh autogen.sh
./configure --without-pgconfig --prefix=/tmp/pgx
make
diff --git a/utils/check_news.sh b/utils/check_news.sh
index ed7f85096..8379ada08 100755
--- a/utils/check_news.sh
+++ b/utils/check_news.sh
@@ -1,7 +1,7 @@
#!/bin/sh
usage() {
- echo "Usage: $0 [-v] [--ticket-refs] [--ticket-refs-skip-commits=<file>] [<sourcedir>]"
+ echo "Usage: $0 [-v] [--base-ref=<ref>] [--ticket-refs] [--ticket-refs-skip-commits=<file>] [<sourcedir>]"
echo "Sourcedir defaults to one directory above this script"
}
@@ -10,6 +10,7 @@ usage() {
VERBOSE=no
TICKET_REFS=no
RD= # Root source dir
+NEWS_BASE_REF=${NEWS_CHECK_BASE_REF:-}
TICKET_REFS_SKIP_COMMITS=/dev/null
while [ $# -gt 0 ]; do
if [ "$1" = "--help" ]; then
@@ -19,6 +20,16 @@ while [ $# -gt 0 ]; do
VERBOSE=yes
elif [ "$1" = "--ticket-refs" ]; then
TICKET_REFS="yes"
+ elif [ "$1" = "--base-ref" ]; then
+ shift
+ if [ $# -eq 0 ]; then
+ echo "ERROR: --base-ref requires a Git ref" >&2
+ usage >&2
+ exit 1
+ fi
+ NEWS_BASE_REF=$1
+ elif [ "${1#--base-ref=}" != "$1" ]; then
+ NEWS_BASE_REF=${1#--base-ref=}
elif [ "$1" = "--ticket-refs-skip-commits" ]; then
shift
TICKET_REFS_SKIP_COMMITS=$( cd $( dirname $1 ) && pwd )
@@ -70,6 +81,84 @@ grep -B1 '^[0-9]\{4\}/[0-9]\{2\}/[0-9]\{2\}' NEWS |
test $? = 0 || exit 1
echo "PASS: NEWS file entries are in good order"
+if test -n "${NEWS_BASE_REF}"; then
+ if ! git rev-parse --verify "${NEWS_BASE_REF}^{commit}" >/dev/null 2>&1; then
+ echo "FAIL: NEWS base ref does not resolve to a commit: ${NEWS_BASE_REF}"
+ exit 1
+ fi
+
+ news_target_file=$(mktemp "${TMPDIR:-/tmp}/check-news-target.XXXXXX") || {
+ exit 1
+ }
+ trap 'rm -f "${news_target_file}"' EXIT HUP INT TERM
+ if ! git show "${NEWS_BASE_REF}:NEWS" > "${news_target_file}"; then
+ echo "FAIL: NEWS is unavailable at target ref ${NEWS_BASE_REF}"
+ exit 1
+ fi
+
+ if ! awk '
+ function is_unreleased(date) {
+ return date ~ /^[0-9][0-9][0-9][0-9]\/xx\/xx$/
+ }
+ function section_accepts_new_entries() {
+ target_section_is_open = release == target_first &&
+ is_unreleased(target_dates[target_first])
+ current_section_is_open = release == current_first &&
+ is_unreleased(release_date) &&
+ !(release in target_dates)
+ return target_section_is_open || current_section_is_open
+ }
+ function emit_release() {
+ if (release == "")
+ return
+
+ if (file_number == 1) {
+ target_dates[release] = release_date
+ if (target_first == "")
+ target_first = release
+ }
+ }
+ FNR == 1 {
+ if (file_number != 0)
+ emit_release()
+ file_number++
+ release = ""
+ }
+ /^PostGIS [0-9]/ {
+ emit_release()
+ release = $0
+ release_date = ""
+ if (file_number == 2 && current_first == "")
+ current_first = release
+ next
+ }
+ release != "" && release_date == "" &&
+ /^[0-9][0-9][0-9][0-9]\/([0-9][0-9]|xx)\/([0-9][0-9]|xx)$/ {
+ release_date = $0
+ next
+ }
+ release != "" && /^[[:space:]]+-[[:space:]]/ {
+ entry_key = release SUBSEP $0
+ if (file_number == 1) {
+ target_entries[entry_key]++
+ } else if (file_number == 2 &&
+ !section_accepts_new_entries() &&
+ ++current_entries[entry_key] > target_entries[entry_key]) {
+ print "FAIL: " release \
+ " has a new NEWS entry outside the current unreleased section: " $0
+ failures++
+ }
+ }
+ END {
+ emit_release()
+ exit failures != 0
+ }
+ ' "${news_target_file}" NEWS; then
+ exit 1
+ fi
+ echo "PASS: New NEWS entries are in the current unreleased section"
+fi
+
if test "${TICKET_REFS}" = "yes"; then
last_news_release=$( grep \
diff --git a/utils/test_check_news.py b/utils/test_check_news.py
new file mode 100644
index 000000000..093126115
--- /dev/null
+++ b/utils/test_check_news.py
@@ -0,0 +1,312 @@
+#!/usr/bin/env python3
+
+import os
+import subprocess
+import tempfile
+import textwrap
+import unittest
+from pathlib import Path
+
+
+CHECK_NEWS = Path(__file__).with_name("check_news.sh")
+
+BASE_NEWS = """\
+PostGIS 4.0.0rc1
+2026/xx/xx
+
+* Bug Fixes *
+
+ - Existing unreleased fix
+
+PostGIS 3.9.0
+2020/01/01
+
+* Bug Fixes *
+
+ - Existing released fix
+ with historical detail
+ - Another released fix
+"""
+
+
+class NewsFixture:
+ def __init__(self):
+ self.temporary_directory = tempfile.TemporaryDirectory()
+ self.repo = Path(self.temporary_directory.name)
+ self.git("init", "--initial-branch=main")
+ self.git("config", "user.name", "Fixture Committer")
+ self.git("config", "user.email", "committer at example.com")
+ self.write_news(BASE_NEWS)
+ self.git("add", "NEWS")
+ self.git("commit", "-m", "base NEWS")
+ self.base_commit = self.git("rev-parse", "HEAD").stdout.strip()
+
+ def close(self):
+ self.temporary_directory.cleanup()
+
+ def git(self, *args):
+ return subprocess.run(
+ ["git", *args],
+ cwd=self.repo,
+ check=True,
+ text=True,
+ capture_output=True,
+ )
+
+ def write_news(self, news):
+ (self.repo / "NEWS").write_text(
+ textwrap.dedent(news),
+ encoding="utf-8",
+ )
+
+ def check_news(self, base_ref=None, environment_base_ref=None):
+ command = [str(CHECK_NEWS)]
+ if base_ref is not None:
+ command.append(f"--base-ref={base_ref}")
+ command.append(str(self.repo))
+ environment = os.environ.copy()
+ environment.pop("NEWS_CHECK_BASE_REF", None)
+ if environment_base_ref is not None:
+ environment["NEWS_CHECK_BASE_REF"] = environment_base_ref
+ return subprocess.run(
+ command,
+ env=environment,
+ text=True,
+ capture_output=True,
+ )
+
+
+class NewsValidationTest(unittest.TestCase):
+ def setUp(self):
+ self.fixture = NewsFixture()
+
+ def tearDown(self):
+ self.fixture.close()
+
+ def test_new_entry_in_current_unreleased_section_passes(self):
+ self.fixture.write_news(
+ BASE_NEWS.replace(
+ " - Existing unreleased fix",
+ " - New unreleased fix\n - Existing unreleased fix",
+ )
+ )
+ result = self.fixture.check_news(self.fixture.base_commit)
+ self.assertEqual(0, result.returncode, result.stdout + result.stderr)
+
+ def test_new_entry_in_released_section_fails(self):
+ self.fixture.write_news(
+ BASE_NEWS.replace(
+ " - Existing released fix",
+ " - New misplaced fix\n - Existing released fix",
+ )
+ )
+ result = self.fixture.check_news(
+ environment_base_ref=self.fixture.base_commit
+ )
+ self.assertNotEqual(0, result.returncode)
+ self.assertIn(
+ "PostGIS 3.9.0 has a new NEWS entry",
+ result.stdout,
+ )
+
+ def test_new_two_space_entry_in_released_section_fails(self):
+ self.fixture.write_news(
+ BASE_NEWS.replace(
+ " - Existing released fix",
+ " - New misplaced legacy-format fix\n"
+ " - Existing released fix",
+ )
+ )
+ result = self.fixture.check_news(self.fixture.base_commit)
+ self.assertNotEqual(0, result.returncode)
+ self.assertIn("New misplaced legacy-format fix", result.stdout)
+
+ def test_rewording_released_entry_heading_fails(self):
+ self.fixture.write_news(
+ BASE_NEWS.replace(
+ " - Existing released fix",
+ " - Corrected wording for released fix",
+ )
+ )
+ result = self.fixture.check_news(self.fixture.base_commit)
+ self.assertNotEqual(0, result.returncode)
+ self.assertIn("has a new NEWS entry", result.stdout)
+
+ def test_rewording_released_entry_detail_passes(self):
+ self.fixture.write_news(
+ BASE_NEWS.replace(
+ " with historical detail",
+ " with corrected historical detail",
+ )
+ )
+ result = self.fixture.check_news(self.fixture.base_commit)
+ self.assertEqual(0, result.returncode, result.stdout + result.stderr)
+
+ def test_new_released_entry_cannot_hide_behind_deleted_entry(self):
+ news = BASE_NEWS.replace(
+ " - Existing released fix",
+ " - New misplaced fix\n - Existing released fix",
+ ).replace("\n - Another released fix", "")
+ self.fixture.write_news(news)
+ result = self.fixture.check_news(self.fixture.base_commit)
+ self.assertNotEqual(0, result.returncode)
+ self.assertIn("New misplaced fix", result.stdout)
+
+ def test_duplicate_released_entry_fails(self):
+ self.fixture.write_news(
+ BASE_NEWS.replace(
+ " - Existing released fix",
+ " - Existing released fix\n - Existing released fix",
+ )
+ )
+ result = self.fixture.check_news(self.fixture.base_commit)
+ self.assertNotEqual(0, result.returncode)
+ self.assertIn("Existing released fix", result.stdout)
+
+ def test_release_promotion_can_finish_previous_unreleased_section(self):
+ self.fixture.write_news(
+ """\
+ PostGIS 4.1.0dev
+ 2026/xx/xx
+
+ * Bug Fixes *
+
+ PostGIS 4.0.0rc1
+ 2020/02/01
+
+ * Bug Fixes *
+
+ - Final fix added while preparing the release
+ - Existing unreleased fix
+
+ PostGIS 3.9.0
+ 2020/01/01
+
+ * Bug Fixes *
+
+ - Existing released fix
+ """
+ )
+ result = self.fixture.check_news(self.fixture.base_commit)
+ self.assertEqual(0, result.returncode, result.stdout + result.stderr)
+
+ def test_new_unreleased_section_can_follow_released_target(self):
+ self.fixture.write_news(
+ BASE_NEWS.replace(
+ "PostGIS 4.0.0rc1\n2026/xx/xx",
+ "PostGIS 4.0.0rc1\n2020/02/01",
+ )
+ )
+ self.fixture.git("add", "NEWS")
+ self.fixture.git("commit", "-m", "release current section")
+ target_commit = self.fixture.git("rev-parse", "HEAD").stdout.strip()
+ self.fixture.write_news(
+ """\
+ PostGIS 4.1.0dev
+ 2026/xx/xx
+
+ * Bug Fixes *
+
+ - First fix after the release
+
+ """
+ + (self.fixture.repo / "NEWS").read_text(encoding="utf-8")
+ )
+
+ result = self.fixture.check_news(target_commit)
+ self.assertEqual(0, result.returncode, result.stdout + result.stderr)
+
+ def test_stale_branch_cannot_add_to_section_closed_on_target(self):
+ self.fixture.git("checkout", "-b", "feature")
+ self.fixture.git("checkout", "-b", "target", self.fixture.base_commit)
+ self.fixture.write_news(
+ BASE_NEWS.replace(
+ "PostGIS 4.0.0rc1\n2026/xx/xx",
+ "PostGIS 4.1.0dev\n2026/xx/xx\n\n"
+ "* Bug Fixes *\n\n"
+ "PostGIS 4.0.0rc1\n2026/07/25",
+ )
+ )
+ self.fixture.git("add", "NEWS")
+ self.fixture.git("commit", "-m", "close release")
+ target_commit = self.fixture.git("rev-parse", "HEAD").stdout.strip()
+ self.fixture.git("checkout", "feature")
+ self.fixture.write_news(
+ BASE_NEWS.replace(
+ " - Existing unreleased fix",
+ " - Late stale-branch fix\n - Existing unreleased fix",
+ )
+ )
+
+ result = self.fixture.check_news(target_commit)
+ self.assertNotEqual(0, result.returncode)
+ self.assertIn("Late stale-branch fix", result.stdout)
+
+ def test_shallow_feature_checkout_uses_target_tip_directly(self):
+ self.fixture.git("checkout", "-b", "feature")
+ self.fixture.write_news(
+ BASE_NEWS.replace(
+ " - Existing unreleased fix",
+ " - New feature fix\n - Existing unreleased fix",
+ )
+ )
+ self.fixture.git("add", "NEWS")
+ self.fixture.git("commit", "-m", "feature NEWS")
+
+ with tempfile.TemporaryDirectory() as clone_directory:
+ clone = Path(clone_directory)
+ subprocess.run(
+ [
+ "git",
+ "clone",
+ "--depth=1",
+ "--branch=feature",
+ f"file://{self.fixture.repo}",
+ str(clone),
+ ],
+ check=True,
+ text=True,
+ capture_output=True,
+ )
+ subprocess.run(
+ [
+ "git",
+ "fetch",
+ "origin",
+ "refs/heads/main:refs/news-check/target",
+ ],
+ cwd=clone,
+ check=True,
+ text=True,
+ capture_output=True,
+ )
+ self.assertEqual(
+ "true",
+ subprocess.run(
+ ["git", "rev-parse", "--is-shallow-repository"],
+ cwd=clone,
+ check=True,
+ text=True,
+ capture_output=True,
+ ).stdout.strip(),
+ )
+ result = subprocess.run(
+ [
+ str(CHECK_NEWS),
+ "--base-ref=refs/news-check/target",
+ str(clone),
+ ],
+ text=True,
+ capture_output=True,
+ )
+
+ self.assertEqual(0, result.returncode, result.stdout + result.stderr)
+
+ def test_missing_explicit_base_ref_fails(self):
+ result = self.fixture.check_news("refs/heads/missing")
+ self.assertNotEqual(0, result.returncode)
+ self.assertIn("does not resolve to a commit", result.stdout)
+
+
+if __name__ == "__main__":
+ unittest.main()
-----------------------------------------------------------------------
Summary of changes:
GNUmakefile.in | 5 +
ci/dronie/postgis_tools.sh | 11 ++
utils/check_news.sh | 91 ++++++++++++-
utils/test_check_news.py | 312 +++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 418 insertions(+), 1 deletion(-)
create mode 100644 utils/test_check_news.py
hooks/post-receive
--
PostGIS
More information about the postgis-tickets
mailing list