[SCM] PostGIS branch master updated. 3.7.0alpha1-640-ge2794c236

git at osgeo.org git at osgeo.org
Sun Jul 19 08:37:49 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  e2794c236e2811023c3423f90099ec9f9e59f50b (commit)
       via  262efd9656603b12c20a24635504efe89db3a25d (commit)
       via  63950b0740870b892fd8b45d79c7af90b2a089ef (commit)
      from  da401396910c2e24e403eb2c97a2cfac2c6bfbd0 (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 e2794c236e2811023c3423f90099ec9f9e59f50b
Merge: da4013969 262efd965
Author: Darafei Praliaskouski <komzpa at gmail.com>
Date:   Sun Jul 19 08:37:47 2026 -0700

    Merge pull request 'ci: show required failures in status banner' (!442) from Komzpa/postgis:ci/rollup-failure-attribution into master
    
    Reviewed-on: https://gitea.osgeo.org/postgis/postgis/pulls/442


commit 262efd9656603b12c20a24635504efe89db3a25d
Author: Darafei Praliaskouski <me at komzpa.net>
Date:   Sun Jul 19 18:41:00 2026 +0400

    ci: simplify failure attribution banner

diff --git a/utils/ci-status.py b/utils/ci-status.py
index 3ce1d74ae..7c036ba63 100755
--- a/utils/ci-status.py
+++ b/utils/ci-status.py
@@ -1353,10 +1353,7 @@ def html_required_failures(branches):
     if not failures:
         return ""
 
-    count = sum(len(checks) for _, checks in failures)
-    branch_count = len(failures)
-    branch_word = "branch" if branch_count == 1 else "branches"
-    rows = []
+    groups = []
     for branch_label, checks in failures:
         check_links = []
         for check in checks:
@@ -1365,15 +1362,13 @@ def html_required_failures(branches):
             check_links.append(
                 f"<a href='{html.escape(link)}'>{check_name}</a>" if link else check_name
             )
-        rows.append(
-            f"<li><strong>{html.escape(branch_label)}</strong> — {', '.join(check_links)}</li>"
+        groups.append(
+            f"<strong>{html.escape(branch_label)}</strong> — {', '.join(check_links)}"
         )
 
     return (
-        "<div class='failure-attribution' aria-label='Required CI failures'>"
-        f"<div class='failure-attribution-title'>Required failures "
-        f"({count} across {branch_count} {branch_word})</div>"
-        f"<ul>{''.join(rows)}</ul></div>"
+        " <span aria-label='Required CI failures'>· <strong>Required:</strong> "
+        f"{' · '.join(groups)}</span>"
     )
 
 
@@ -1383,11 +1378,6 @@ def render_html(data):
     page_summary_text = summary_text({"status": page_status, "checks": [
         check for branch in data["branches"] for check in branch["checks"]
     ]})
-    if page_status == FAILURE:
-        page_summary_text = (
-            "Failing means one or more required checks failed on a supported branch. "
-            + page_summary_text
-        )
     page_summary = html.escape(page_summary_text)
     failure_attribution = html_required_failures(data["branches"])
     rows = []
@@ -1503,22 +1493,6 @@ h1 {{
   color: var(--muted);
   font-size: 14px;
 }}
-.failure-attribution {{
-  border-top: 1px solid #ffc9cf;
-  margin-top: 10px;
-  padding-top: 10px;
-}}
-.failure-attribution-title {{
-  color: var(--failure);
-  font-weight: 650;
-}}
-.failure-attribution ul {{
-  margin: 8px 0 0;
-  padding-left: 22px;
-}}
-.failure-attribution li + li {{
-  margin-top: 4px;
-}}
 .panel {{
   background: var(--panel);
   border: 1px solid var(--line);
@@ -1770,8 +1744,7 @@ a:hover {{ color: var(--brand-strong); }}
 <span class="status-dot" aria-hidden="true"></span>
 <div>
 <div class="banner-title"><span aria-hidden="true">{html.escape(status_mark(page_status))}</span> {html.escape(html_status_label(page_status))}</div>
-<div class="banner-summary">{page_summary}</div>
-{failure_attribution}
+<div class="banner-summary">{page_summary}{failure_attribution}</div>
 </div>
 </section>
 <section class="panel" aria-label="Supported branch status">
diff --git a/utils/test_ci_status.py b/utils/test_ci_status.py
index a947887bf..8241ac5ba 100644
--- a/utils/test_ci_status.py
+++ b/utils/test_ci_status.py
@@ -50,7 +50,7 @@ class RequiredFailureHtmlTest(unittest.TestCase):
 
         summary = CI_STATUS.html_required_failures(branches)
 
-        self.assertIn("Required failures (2 across 1 branch)", summary)
+        self.assertIn("<strong>Required:</strong>", summary)
         self.assertIn("Jenkins / Alpha", summary)
         self.assertIn("https://ci.example.test/a", summary)
         self.assertIn("Woodie / Beta", summary)
@@ -76,18 +76,17 @@ class RequiredFailureHtmlTest(unittest.TestCase):
             "branches": [branch],
         })
 
-        self.assertIn(
-            "Failing means one or more required checks failed on a supported branch. "
-            "1 OK; 1 failure; 1 running",
-            rendered,
-        )
-        self.assertIn("Required failures (1 across 1 branch)", rendered)
+        self.assertIn("1 OK; 1 failure; 1 running", rendered)
+        self.assertIn("<strong>Required:</strong>", rendered)
         banner_start = rendered.index('<section class="status-banner')
         banner_end = rendered.index("</section>", banner_start)
-        failure_start = rendered.index("<div class='failure-attribution'", banner_start)
+        failure_start = rendered.index("aria-label='Required CI failures'", banner_start)
         self.assertLess(failure_start, banner_end)
+        banner = rendered[banner_start:banner_end]
+        self.assertNotIn("Failing means", banner)
+        self.assertNotIn("<ul>", banner)
 
-    def test_failure_summary_pluralizes_branches(self):
+    def test_failure_summary_keeps_multiple_branches_inline(self):
         branches = [
             {
                 "name": f"stable-synthetic-{index}",
@@ -101,7 +100,8 @@ class RequiredFailureHtmlTest(unittest.TestCase):
 
         summary = CI_STATUS.html_required_failures(branches)
 
-        self.assertIn("Required failures (2 across 2 branches)", summary)
+        self.assertIn("Synthetic 1", summary)
+        self.assertIn("· <strong>Synthetic 2</strong>", summary)
 
     def test_failure_block_is_absent_without_required_failures(self):
         branch = {
@@ -117,7 +117,7 @@ class RequiredFailureHtmlTest(unittest.TestCase):
             "generated_at": "2026-07-19T00:00:00+00:00",
             "branches": [branch],
         })
-        self.assertNotIn("<div class='failure-attribution'", rendered)
+        self.assertNotIn("aria-label='Required CI failures'", rendered)
 
 
 if __name__ == "__main__":

commit 63950b0740870b892fd8b45d79c7af90b2a089ef
Author: Darafei Praliaskouski <me at komzpa.net>
Date:   Sun Jul 19 16:06:28 2026 +0400

    ci: attribute required failures in status banner

diff --git a/utils/ci-status.py b/utils/ci-status.py
index 543d60a89..3ce1d74ae 100755
--- a/utils/ci-status.py
+++ b/utils/ci-status.py
@@ -1173,6 +1173,18 @@ def overall_status(branches):
     return NOT_APPLICABLE
 
 
+def required_failures(branches):
+    failures = []
+    for branch in branches:
+        checks = [
+            check for check in branch["checks"]
+            if check.get("required") and check["status"] == FAILURE
+        ]
+        if checks:
+            failures.append((branch["label"], checks))
+    return failures
+
+
 def html_status_label(status):
     return {
         SUCCESS: "Passing",
@@ -1336,12 +1348,48 @@ def html_check_table(problem_checks, passing_checks=None):
     )
 
 
+def html_required_failures(branches):
+    failures = required_failures(branches)
+    if not failures:
+        return ""
+
+    count = sum(len(checks) for _, checks in failures)
+    branch_count = len(failures)
+    branch_word = "branch" if branch_count == 1 else "branches"
+    rows = []
+    for branch_label, checks in failures:
+        check_links = []
+        for check in checks:
+            check_name = html.escape(check["check"])
+            link = result_url(check)
+            check_links.append(
+                f"<a href='{html.escape(link)}'>{check_name}</a>" if link else check_name
+            )
+        rows.append(
+            f"<li><strong>{html.escape(branch_label)}</strong> — {', '.join(check_links)}</li>"
+        )
+
+    return (
+        "<div class='failure-attribution' aria-label='Required CI failures'>"
+        f"<div class='failure-attribution-title'>Required failures "
+        f"({count} across {branch_count} {branch_word})</div>"
+        f"<ul>{''.join(rows)}</ul></div>"
+    )
+
+
 def render_html(data):
     generated = html.escape(data["generated_at"])
     page_status = overall_status(data["branches"])
-    page_summary = html.escape(summary_text({"status": page_status, "checks": [
+    page_summary_text = summary_text({"status": page_status, "checks": [
         check for branch in data["branches"] for check in branch["checks"]
-    ]}))
+    ]})
+    if page_status == FAILURE:
+        page_summary_text = (
+            "Failing means one or more required checks failed on a supported branch. "
+            + page_summary_text
+        )
+    page_summary = html.escape(page_summary_text)
+    failure_attribution = html_required_failures(data["branches"])
     rows = []
     details = []
     for branch in data["branches"]:
@@ -1455,6 +1503,22 @@ h1 {{
   color: var(--muted);
   font-size: 14px;
 }}
+.failure-attribution {{
+  border-top: 1px solid #ffc9cf;
+  margin-top: 10px;
+  padding-top: 10px;
+}}
+.failure-attribution-title {{
+  color: var(--failure);
+  font-weight: 650;
+}}
+.failure-attribution ul {{
+  margin: 8px 0 0;
+  padding-left: 22px;
+}}
+.failure-attribution li + li {{
+  margin-top: 4px;
+}}
 .panel {{
   background: var(--panel);
   border: 1px solid var(--line);
@@ -1707,6 +1771,7 @@ a:hover {{ color: var(--brand-strong); }}
 <div>
 <div class="banner-title"><span aria-hidden="true">{html.escape(status_mark(page_status))}</span> {html.escape(html_status_label(page_status))}</div>
 <div class="banner-summary">{page_summary}</div>
+{failure_attribution}
 </div>
 </section>
 <section class="panel" aria-label="Supported branch status">
diff --git a/utils/test_ci_status.py b/utils/test_ci_status.py
new file mode 100644
index 000000000..a947887bf
--- /dev/null
+++ b/utils/test_ci_status.py
@@ -0,0 +1,124 @@
+import importlib.util
+import pathlib
+import unittest
+
+
+MODULE_PATH = pathlib.Path(__file__).with_name("ci-status.py")
+SPEC = importlib.util.spec_from_file_location("ci_status", MODULE_PATH)
+CI_STATUS = importlib.util.module_from_spec(SPEC)
+SPEC.loader.exec_module(CI_STATUS)
+
+
+def check(name, status, *, required=True, url=None):
+    result = {
+        "branch": "stable-synthetic",
+        "branch_label": "Synthetic",
+        "check": name,
+        "provider": "synthetic",
+        "required": required,
+        "status": status,
+    }
+    if url:
+        result["url"] = url
+    return result
+
+
+class RequiredFailureHtmlTest(unittest.TestCase):
+    def test_failure_summary_names_only_required_failed_checks(self):
+        branches = [
+            {
+                "name": "stable-synthetic",
+                "label": "Synthetic",
+                "status": CI_STATUS.FAILURE,
+                "failures": 2,
+                "checks": [
+                    check("Jenkins / Alpha", CI_STATUS.FAILURE, url="https://ci.example.test/a"),
+                    check("Woodie / Beta", CI_STATUS.FAILURE, url="https://ci.example.test/b"),
+                    check("Optional / Gamma", CI_STATUS.FAILURE, required=False),
+                    check("Required / Waiting", CI_STATUS.UNKNOWN),
+                    check("Required / Passing", CI_STATUS.SUCCESS),
+                ],
+            },
+            {
+                "name": "stable-disabled",
+                "label": "Disabled",
+                "status": CI_STATUS.NOT_APPLICABLE,
+                "failures": 0,
+                "checks": [check("Retired / Delta", CI_STATUS.DISABLED)],
+            },
+        ]
+
+        summary = CI_STATUS.html_required_failures(branches)
+
+        self.assertIn("Required failures (2 across 1 branch)", summary)
+        self.assertIn("Jenkins / Alpha", summary)
+        self.assertIn("https://ci.example.test/a", summary)
+        self.assertIn("Woodie / Beta", summary)
+        self.assertNotIn("Optional / Gamma", summary)
+        self.assertNotIn("Required / Waiting", summary)
+        self.assertNotIn("Retired / Delta", summary)
+
+    def test_page_explains_rollup_scope(self):
+        branch = {
+            "name": "stable-synthetic",
+            "label": "Synthetic",
+            "status": CI_STATUS.FAILURE,
+            "failures": 1,
+            "checks": [
+                check("Jenkins / Alpha", CI_STATUS.FAILURE),
+                check("Required / Passing", CI_STATUS.SUCCESS),
+                check("Required / Waiting", CI_STATUS.IN_PROGRESS),
+            ],
+        }
+
+        rendered = CI_STATUS.render_html({
+            "generated_at": "2026-07-19T00:00:00+00:00",
+            "branches": [branch],
+        })
+
+        self.assertIn(
+            "Failing means one or more required checks failed on a supported branch. "
+            "1 OK; 1 failure; 1 running",
+            rendered,
+        )
+        self.assertIn("Required failures (1 across 1 branch)", rendered)
+        banner_start = rendered.index('<section class="status-banner')
+        banner_end = rendered.index("</section>", banner_start)
+        failure_start = rendered.index("<div class='failure-attribution'", banner_start)
+        self.assertLess(failure_start, banner_end)
+
+    def test_failure_summary_pluralizes_branches(self):
+        branches = [
+            {
+                "name": f"stable-synthetic-{index}",
+                "label": f"Synthetic {index}",
+                "status": CI_STATUS.FAILURE,
+                "failures": 1,
+                "checks": [check(f"Required / Failed {index}", CI_STATUS.FAILURE)],
+            }
+            for index in (1, 2)
+        ]
+
+        summary = CI_STATUS.html_required_failures(branches)
+
+        self.assertIn("Required failures (2 across 2 branches)", summary)
+
+    def test_failure_block_is_absent_without_required_failures(self):
+        branch = {
+            "name": "stable-synthetic",
+            "label": "Synthetic",
+            "status": CI_STATUS.UNKNOWN,
+            "failures": 0,
+            "checks": [check("Required / Waiting", CI_STATUS.UNKNOWN)],
+        }
+
+        self.assertEqual("", CI_STATUS.html_required_failures([branch]))
+        rendered = CI_STATUS.render_html({
+            "generated_at": "2026-07-19T00:00:00+00:00",
+            "branches": [branch],
+        })
+        self.assertNotIn("<div class='failure-attribution'", rendered)
+
+
+if __name__ == "__main__":
+    unittest.main()

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

Summary of changes:
 utils/ci-status.py      |  44 +++++++++++++++--
 utils/test_ci_status.py | 124 ++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 165 insertions(+), 3 deletions(-)
 create mode 100644 utils/test_ci_status.py


hooks/post-receive
-- 
PostGIS


More information about the postgis-tickets mailing list