[SCM] PostGIS branch master updated. 3.7.0beta2-102-g8c2127c5f
git at osgeo.org
git at osgeo.org
Fri Aug 21 11:01:43 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 8c2127c5f2f50824fdcdba20a71edf74134290a6 (commit)
via 4fcf6f959a16242255f79ce28334787f72e36015 (commit)
from 428a7b7e6869bf7058ff1e22d848618ce7fc20d8 (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 8c2127c5f2f50824fdcdba20a71edf74134290a6
Merge: 428a7b7e6 4fcf6f959
Author: Darafei Praliaskouski <komzpa at gmail.com>
Date: Fri Aug 21 11:01:42 2026 -0700
Merge pull request 'ci: link Jenkins failures to leaf consoles' (!757) from Komzpa/postgis:fix/ci-status-github-refresh-20260821 into master
Mark a provider result stale immediately when its known revision is behind the tracked branch.
For a failed Jenkins matrix configuration, follow the bounded Parameterized Trigger chain in its console and publish the terminal failed build plus its `/console` URL. With multiple failed leaves, retain each leaf link separately instead of choosing one arbitrarily.
Reviewed-on: https://gitea.osgeo.org/postgis/postgis/pulls/757
commit 4fcf6f959a16242255f79ce28334787f72e36015
Author: Darafei Praliaskouski <me at komzpa.net>
Date: Fri Aug 21 20:34:40 2026 +0400
ci: link Jenkins failures to leaf consoles
diff --git a/utils/docs/ci_status/report.py b/utils/docs/ci_status/report.py
index c2eaab039..46774780c 100644
--- a/utils/docs/ci_status/report.py
+++ b/utils/docs/ci_status/report.py
@@ -25,6 +25,7 @@ STALE_FAILED = "stale-fail"
DISABLED = "disabled"
NOT_APPLICABLE = "not_applicable"
JENKINS_STALE_QUEUE_HOURS = 4
+JENKINS_DOWNSTREAM_DEPTH = 4
STATUS_DISPLAY_ORDER = {
FAILURE: 0,
@@ -1095,6 +1096,78 @@ def jenkins_matrix_configuration_label(configuration, selected):
return configuration.get("name") or configuration.get("url") or "configuration"
+def jenkins_console_url(build_url):
+ return build_url.rstrip("/") + "/console"
+
+
+def jenkins_console_text_url(build_url):
+ return build_url.rstrip("/") + "/consoleText"
+
+
+def jenkins_downstream_build_url(parent_url, job_name, build_number):
+ parsed = urllib.parse.urlparse(parent_url)
+ if parsed.scheme not in ("http", "https") or not parsed.netloc:
+ return None
+ job_parts = job_name.split("/")
+ if not job_parts or any(not part for part in job_parts):
+ return None
+ path = "/job/" + "/job/".join(
+ urllib.parse.quote(part, safe="") for part in job_parts
+ ) + f"/{build_number}/"
+ return urllib.parse.urlunparse((parsed.scheme, parsed.netloc, path, "", "", ""))
+
+
+def jenkins_failed_downstream_builds(console_text):
+ marker = " completed. Result was "
+ failures = []
+ for line in console_text.splitlines():
+ before, separator, result = line.partition(marker)
+ if not separator or result.strip() != "FAILURE":
+ continue
+ job_name, separator, build_number = before.rpartition(" #")
+ if not separator or not job_name or not build_number.isdecimal():
+ continue
+ failures.append((job_name, int(build_number)))
+ return failures
+
+
+def jenkins_terminal_failures(build_url, timeout, depth=JENKINS_DOWNSTREAM_DEPTH, seen=None, job=None):
+ if depth < 0 or build_url in (seen or set()):
+ return None
+ try:
+ console_text = http_text(jenkins_console_text_url(build_url), timeout=timeout)
+ except RECOVERABLE_PROVIDER_ERRORS:
+ return None
+ failed = jenkins_failed_downstream_builds(console_text)
+ if not failed:
+ if "Finished: FAILURE" not in console_text:
+ return None
+ return [{
+ "url": build_url,
+ "console_url": jenkins_console_url(build_url),
+ "job": job,
+ }]
+
+ leaves = []
+ next_seen = set(seen or ())
+ next_seen.add(build_url)
+ for job_name, build_number in failed:
+ child_url = jenkins_downstream_build_url(build_url, job_name, build_number)
+ if not child_url:
+ return None
+ child_leaves = jenkins_terminal_failures(
+ child_url,
+ timeout,
+ depth - 1,
+ next_seen,
+ f"{job_name} #{build_number}",
+ )
+ if child_leaves is None:
+ return None
+ leaves.extend(child_leaves)
+ return leaves
+
+
def jenkins_matrix_details(job_url, timeout, parent_build_number):
try:
configurations = jenkins_matrix_configurations(job_url, timeout)
@@ -1139,11 +1212,32 @@ def jenkins_matrix_details(job_url, timeout, parent_build_number):
for status in (FAILURE, IN_PROGRESS, UNKNOWN)
for item in by_status.get(status) or []
]
- url = non_success[0][1].get("url") if len(non_success) == 1 else None
- return {
+ details = {
"message": "; ".join(parts),
- "url": url,
}
+ failures = []
+ for label, build in by_status[FAILURE]:
+ build_url = build.get("url")
+ if not build_url:
+ continue
+ leaves = jenkins_terminal_failures(build_url, timeout)
+ if leaves is None:
+ continue
+ for failure in leaves:
+ failure["label"] = ": ".join(
+ part for part in (label, failure.pop("job", None)) if part
+ )
+ failures.append(failure)
+ if failures:
+ details["failures"] = failures
+ if len(non_success) == 1:
+ build_url = non_success[0][1].get("url")
+ if build_url:
+ failure = failures[0] if len(failures) == 1 else {"url": build_url}
+ details["url"] = failure["url"]
+ if failure.get("console_url"):
+ details["console_url"] = failure["console_url"]
+ return details
def jenkins_badge_url(job_url, check, branch):
@@ -1239,6 +1333,10 @@ def jenkins_check(check, branch, timeout):
result["message"] = f"{result['message']}; {details['message']}"
if details.get("url"):
result["url"] = details["url"]
+ if details.get("console_url"):
+ result["console_url"] = details["console_url"]
+ if details.get("failures"):
+ result["failure_details"] = details["failures"]
if previous:
result.update(previous_fields(normalize_jenkins_status(previous), previous))
return result
@@ -1414,7 +1512,7 @@ def apply_staleness(result, config, check):
distance_count, distance_ref = None, None
if result["status"] != IN_PROGRESS:
distance_count, distance_ref = result_revision_distance(config, result)
- if result["status"] not in (IN_PROGRESS, SUCCESS) and distance_count and distance_count > 0:
+ if result["status"] != IN_PROGRESS and distance_count and distance_count > 0:
stale = dict(result)
stale["revision_commits_behind"] = distance_count
stale["revision_compare_ref"] = distance_ref
@@ -1710,7 +1808,7 @@ def safe_http_href(value):
def result_url(check):
- return safe_http_href(check.get("url") or check.get("debug_url") or "")
+ return safe_http_href(check.get("console_url") or check.get("url") or check.get("debug_url") or "")
def terminal_link(text, url, enabled):
@@ -1797,6 +1895,10 @@ def print_terminal(data, use_color=True, verbose=False):
if check.get("message"):
message = " ".join(str(check["message"]).split())
print(terminal_field("message", message, use_color))
+ for failure in check.get("failure_details") or []:
+ label = failure.get("label") or "failed child"
+ url = safe_http_href(failure.get("console_url") or failure.get("url") or "")
+ print(terminal_field("failure", terminal_link(label, url, use_color), use_color))
print()
diff --git a/utils/docs/tests/test_ci_status.py b/utils/docs/tests/test_ci_status.py
index 2e7127a92..ea00c5b1e 100644
--- a/utils/docs/tests/test_ci_status.py
+++ b/utils/docs/tests/test_ci_status.py
@@ -348,6 +348,12 @@ class CIStatusTest(unittest.TestCase):
"completed_at": "2026-07-01T00:00:00Z",
"message": "build 2",
}, config, stale_check)
+ recent_passed = CI_STATUS.apply_staleness({
+ **base,
+ "status": CI_STATUS.SUCCESS,
+ "completed_at": CI_STATUS.utc_now().isoformat(),
+ "message": "build 3",
+ }, config, stale_check)
self.assertEqual(CI_STATUS.STALE_FAILED, failed["status"])
self.assertEqual(CI_STATUS.FAILURE, failed["stale_base_status"])
@@ -357,6 +363,8 @@ class CIStatusTest(unittest.TestCase):
self.assertEqual(CI_STATUS.STALE_PASSED, passed["status"])
self.assertEqual(CI_STATUS.SUCCESS, passed["stale_base_status"])
self.assertEqual("Stale passed", passed["status_label"])
+ self.assertEqual(CI_STATUS.STALE_PASSED, recent_passed["status"])
+ self.assertIn("3 commits behind stable-synthetic", recent_passed["message"])
def test_jenkins_matrix_failure_names_failing_axis(self):
check_config = {
@@ -398,16 +406,30 @@ class CIStatusTest(unittest.TestCase):
},
]
+ child_console = (
+ "Waiting for the completion of PostGIS_2_windows_PGVERSION_winnie\n"
+ "PostGIS_2_windows_PGVERSION_winnie #20003 completed. Result was FAILURE\n"
+ )
with (
mock.patch.object(CI_STATUS, "jenkins_queued_check", return_value=None),
mock.patch.object(CI_STATUS, "jenkins_builds", return_value=[current]),
mock.patch.object(CI_STATUS, "jenkins_matrix_configurations", return_value=matrix),
+ mock.patch.object(CI_STATUS, "http_text", side_effect=[child_console, "Finished: FAILURE\n"]),
):
result = CI_STATUS.jenkins_check(check_config, branch, timeout=5)
self.assertEqual(CI_STATUS.FAILURE, result["status"])
self.assertEqual("build 5284; failed: PG19", result["message"])
- self.assertEqual("https://ci.example.test/job/PostGIS_trunk/PG_VER=19/5284/", result["url"])
+ self.assertEqual("https://ci.example.test/job/PostGIS_2_windows_PGVERSION_winnie/20003/", result["url"])
+ self.assertEqual(
+ "https://ci.example.test/job/PostGIS_2_windows_PGVERSION_winnie/20003/console",
+ result["console_url"],
+ )
+ self.assertEqual(result["console_url"], CI_STATUS.result_url(result))
+ self.assertEqual(
+ "PG19: PostGIS_2_windows_PGVERSION_winnie #20003",
+ result["failure_details"][0]["label"],
+ )
self.assertEqual("a" * 40, result["revision"])
def test_jenkins_single_configuration_matrix_keeps_parent_message(self):
@@ -451,6 +473,35 @@ class CIStatusTest(unittest.TestCase):
self.assertEqual("build 7808", result["message"])
self.assertEqual("https://ci.example.test/job/PostGIS_Make_Dist/7808/", result["url"])
+ def test_jenkins_downstream_parse_failure_keeps_matrix_build(self):
+ build_url = "https://ci.example.test/job/PostGIS_3.5/PG_VER=17/208/"
+ with mock.patch.object(CI_STATUS, "http_text", return_value="unstructured failure"):
+ result = CI_STATUS.jenkins_terminal_failures(build_url, timeout=5)
+
+ self.assertIsNone(result)
+
+ def test_jenkins_downstream_failure_fanout_keeps_each_leaf_console(self):
+ build_url = "https://ci.example.test/job/PostGIS_3.5/PG_VER=17/208/"
+ parent_console = (
+ "PostGIS_2_windows_PGVERSION_winnie #20003 completed. Result was FAILURE\n"
+ "PostGIS_EDB_Regress_winnie #23462 completed. Result was FAILURE\n"
+ )
+ leaf_console = "Finished: FAILURE\n"
+ with mock.patch.object(
+ CI_STATUS, "http_text", side_effect=[parent_console, leaf_console, leaf_console]
+ ):
+ failures = CI_STATUS.jenkins_terminal_failures(build_url, timeout=5)
+
+ self.assertEqual(2, len(failures))
+ self.assertEqual(
+ "https://ci.example.test/job/PostGIS_2_windows_PGVERSION_winnie/20003/console",
+ failures[0]["console_url"],
+ )
+ self.assertEqual(
+ "https://ci.example.test/job/PostGIS_EDB_Regress_winnie/23462/console",
+ failures[1]["console_url"],
+ )
+
def test_jenkins_matrix_ignores_another_parent_build(self):
check_config = {
"name": "Jenkins / Debbie main",
-----------------------------------------------------------------------
Summary of changes:
utils/docs/ci_status/report.py | 112 +++++++++++++++++++++++++++++++++++--
utils/docs/tests/test_ci_status.py | 53 +++++++++++++++++-
2 files changed, 159 insertions(+), 6 deletions(-)
hooks/post-receive
--
PostGIS
More information about the postgis-tickets
mailing list