[SCM] PostGIS branch master updated. 3.7.0alpha1-649-g5f4a33662

git at osgeo.org git at osgeo.org
Sun Jul 19 13:15:17 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  5f4a33662ea87b0bca09f116655d15ab6ce358be (commit)
       via  a142f8e7e4cffc62ba8e261795a46aecab9aeb7e (commit)
      from  1a0b660346d2e80bdbd4b6201e8e47b4194e23a2 (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 5f4a33662ea87b0bca09f116655d15ab6ce358be
Merge: 1a0b66034 a142f8e7e
Author: Darafei Praliaskouski <komzpa at gmail.com>
Date:   Sun Jul 19 13:15:14 2026 -0700

    Merge pull request 'ci: distinguish stale passed and failed checks' (!449) from Komzpa/postgis:ci/stale-pass-fail-dashboard into master
    
    Reviewed-on: https://gitea.osgeo.org/postgis/postgis/pulls/449


commit a142f8e7e4cffc62ba8e261795a46aecab9aeb7e
Author: Darafei Praliaskouski <me at komzpa.net>
Date:   Sun Jul 19 21:32:27 2026 +0400

    ci: distinguish stale passed and failed checks

diff --git a/utils/ci-status.py b/utils/ci-status.py
index 7c036ba63..e9c4b0a9b 100755
--- a/utils/ci-status.py
+++ b/utils/ci-status.py
@@ -817,7 +817,9 @@ def apply_staleness(result, config, check):
         stale["revision_commits_behind"] = distance_count
         stale["revision_compare_ref"] = distance_ref
         stale["revision_distance"] = revision_distance_text(distance_count, distance_ref)
+        stale["stale_base_status"] = result["status"]
         stale["status"] = STALE
+        stale["status_label"] = stale_status_label(result["status"])
         stale["message"] = f"{result.get('message', 'CI run')} ({stale['revision_distance']})"
         return stale
     if result["status"] != SUCCESS:
@@ -836,7 +838,9 @@ def apply_staleness(result, config, check):
         stale["revision_commits_behind"] = distance_count
         stale["revision_compare_ref"] = distance_ref
         stale["revision_distance"] = distance_text
+    stale["stale_base_status"] = result["status"]
     stale["status"] = STALE
+    stale["status_label"] = stale_status_label(result["status"])
     stale["message"] = f"{result.get('message', 'successful run')} (older than {threshold:g}h)"
     return stale
 
@@ -948,6 +952,47 @@ def check_counts(branch):
     return counts
 
 
+def stale_status_label(base_status):
+    return {
+        SUCCESS: "Stale passed",
+        FAILURE: "Stale failed",
+        UNKNOWN: "Stale unknown",
+    }.get(base_status, "Stale")
+
+
+def stale_count_bucket(check):
+    if check["status"] != STALE:
+        return None
+    base_status = check.get("stale_base_status")
+    if base_status in (SUCCESS, FAILURE):
+        return base_status
+    return UNKNOWN
+
+
+def stale_check_counts(branch):
+    counts = {
+        SUCCESS: 0,
+        FAILURE: 0,
+        UNKNOWN: 0,
+    }
+    for check in branch["checks"]:
+        if not check.get("required"):
+            continue
+        bucket = stale_count_bucket(check)
+        if bucket:
+            counts[bucket] += 1
+    return counts
+
+
+def stale_summary_parts(branch):
+    counts = stale_check_counts(branch)
+    return nonzero_parts(
+        (counts[SUCCESS], f"{counts[SUCCESS]} stale-passed"),
+        (counts[FAILURE], f"{counts[FAILURE]} stale-failed"),
+        (counts[UNKNOWN], f"{counts[UNKNOWN]} stale-unknown"),
+    )
+
+
 def plural(count, word):
     return f"{count} {word}" + ("" if count == 1 else "s")
 
@@ -959,7 +1004,7 @@ def nonzero_parts(*items):
 def summary_text(branch):
     status = branch["status"]
     counts = check_counts(branch)
-    unknown = counts[UNKNOWN] + counts[STALE]
+    stale_parts = stale_summary_parts(branch)
     if status == SUCCESS:
         return f"all {counts['required']} required CI checks OK"
     if status == FAILURE:
@@ -967,24 +1012,27 @@ def summary_text(branch):
             (counts[SUCCESS], f"{counts[SUCCESS]} OK"),
             (counts[FAILURE], plural(counts[FAILURE], "failure")),
             (counts[IN_PROGRESS], f"{counts[IN_PROGRESS]} running"),
-            (unknown, f"{unknown} unknown/stale"),
+            (counts[UNKNOWN], f"{counts[UNKNOWN]} unknown"),
         )
+        parts.extend(stale_parts)
         return "; ".join(parts)
     if status == IN_PROGRESS:
         previous = previous_summary(branch["checks"])
         parts = nonzero_parts(
             (counts[SUCCESS], f"{counts[SUCCESS]} OK"),
             (counts[IN_PROGRESS], f"{counts[IN_PROGRESS]} running"),
-            (unknown, f"{unknown} unknown/stale"),
+            (counts[UNKNOWN], f"{counts[UNKNOWN]} unknown"),
         )
+        parts.extend(stale_parts)
         prefix = "no known failures; " + ", ".join(parts)
         return f"{prefix}; {previous}" if previous else prefix
     if status == UNKNOWN:
         parts = nonzero_parts(
             (counts[SUCCESS], f"{counts[SUCCESS]} OK"),
             (counts[IN_PROGRESS], f"{counts[IN_PROGRESS]} running"),
-            (unknown, f"{unknown} unknown/stale"),
+            (counts[UNKNOWN], f"{counts[UNKNOWN]} unknown"),
         )
+        parts.extend(stale_parts)
         return "no known failures; " + ", ".join(parts)
     return "no required CI configured"
 
@@ -1114,7 +1162,8 @@ def print_terminal(data, use_color=True, verbose=False):
         heading_text = f"{glyph} {branch['label']} / {check['check']}"
         heading = terminal_link(heading_text, url, use_color)
         print(heading)
-        print(terminal_field("status", check["status"], use_color))
+        status_value = check.get("status_label") or check["status"]
+        print(terminal_field("status", status_value, use_color))
         if url:
             print(terminal_field("url", url, use_color))
         if check["status"] == IN_PROGRESS and check.get("previous_completed_status"):
@@ -1217,6 +1266,7 @@ def html_status_pill(status, label=None):
 
 def html_branch_progress(branch):
     counts = check_counts(branch)
+    stale_counts = stale_check_counts(branch)
     total = counts["required"]
     if not total:
         return ""
@@ -1224,7 +1274,10 @@ def html_branch_progress(branch):
         (SUCCESS, counts[SUCCESS], "OK"),
         (FAILURE, counts[FAILURE], "failing"),
         (IN_PROGRESS, counts[IN_PROGRESS], "running"),
-        (UNKNOWN, counts[UNKNOWN] + counts[STALE], "unknown or stale"),
+        (UNKNOWN, counts[UNKNOWN], "unknown"),
+        ("stale-passed", stale_counts[SUCCESS], "stale-passed"),
+        ("stale-failed", stale_counts[FAILURE], "stale-failed"),
+        ("stale-unknown", stale_counts[UNKNOWN], "stale-unknown"),
     ]
     labels = [f"{count} {label}" for status, count, label in segments if count]
     pieces = []
@@ -1242,7 +1295,7 @@ def html_branch_progress(branch):
 
 def html_branch_summary(branch):
     counts = check_counts(branch)
-    unknown = counts[UNKNOWN] + counts[STALE]
+    stale_counts = stale_check_counts(branch)
     parts = []
     if counts[SUCCESS]:
         parts.append((SUCCESS, f"{counts[SUCCESS]} OK"))
@@ -1250,8 +1303,14 @@ def html_branch_summary(branch):
         parts.append((FAILURE, plural(counts[FAILURE], "failure")))
     if counts[IN_PROGRESS]:
         parts.append((IN_PROGRESS, f"{counts[IN_PROGRESS]} running"))
-    if unknown:
-        parts.append((UNKNOWN, f"{unknown} unknown/stale"))
+    if counts[UNKNOWN]:
+        parts.append((UNKNOWN, f"{counts[UNKNOWN]} unknown"))
+    if stale_counts[SUCCESS]:
+        parts.append(("stale-passed", f"{stale_counts[SUCCESS]} stale-passed"))
+    if stale_counts[FAILURE]:
+        parts.append(("stale-failed", f"{stale_counts[FAILURE]} stale-failed"))
+    if stale_counts[UNKNOWN]:
+        parts.append(("stale-unknown", f"{stale_counts[UNKNOWN]} stale-unknown"))
     if not parts:
         return html.escape(summary_text(branch))
     return " ".join(
@@ -1313,7 +1372,10 @@ def html_check_rows(checks):
             status_text = f"{status_text} previous: {previous}"
             status = f"{status} <span class='previous-note'>previous: {html.escape(previous)}</span>"
         message = " ".join(str(check.get("message") or "").split())
-        check_status = html.escape(check["status"])
+        check_classes = [f"status-{check['status']}"]
+        if check["status"] == STALE:
+            check_classes.append(f"stale-{stale_count_bucket(check)}")
+        check_status = html.escape(" ".join(check_classes))
         revision = html_revision(check)
         revision_text = check.get("revision") or ""
         if revision_text:
@@ -1323,7 +1385,7 @@ def html_check_rows(checks):
         age = html_time(check.get("completed_at"))
         age_text_value = age_text(check.get("completed_at")) if check.get("completed_at") else ""
         detail_rows.append(
-            f"<tr class='status-{check_status}'>"
+            f"<tr class='{check_status}'>"
             f"<td class='check-name'>{check_html}{terminal_pad(check['check'], 27)}</td>"
             f"<td>{terminal_sep}{status}{terminal_pad(status_text, 30)}</td>"
             f"<td class='revision-cell'>{terminal_sep}{revision}{terminal_pad(revision_text, 44)}</td>"
@@ -1567,6 +1629,18 @@ h1 {{
   color: var(--unknown);
   background: #fff8e5;
 }}
+.chip-stale-passed {{
+  color: #5b6519;
+  background: #f5f8e8;
+}}
+.chip-stale-failed {{
+  color: #9a4f00;
+  background: #fff3e0;
+}}
+.chip-stale-unknown {{
+  color: var(--unknown);
+  background: #fff8e5;
+}}
 .branch-progress {{
   display: flex;
   height: 5px;
@@ -1589,9 +1663,15 @@ h1 {{
 .segment-in_progress {{
   background: #70a7db;
 }}
-.segment-unknown, .segment-stale {{
+.segment-unknown, .segment-stale, .segment-stale-unknown {{
   background: #c8a24b;
 }}
+.segment-stale-passed {{
+  background: #a4b85d;
+}}
+.segment-stale-failed {{
+  background: #d48a3a;
+}}
 .status-dot {{
   width: 10px;
   height: 10px;
@@ -1622,6 +1702,8 @@ h1 {{
 .status-failure .status-pill {{ color: var(--failure); border-color: #ffc9cf; background: #fff5f6; }}
 .status-in_progress .status-pill {{ color: var(--running); border-color: #bfdbfe; background: #f0f7ff; }}
 .status-unknown .status-pill, .status-stale .status-pill {{ color: var(--unknown); border-color: #f1d08a; background: #fff8e5; }}
+.status-stale.stale-success .status-pill {{ color: #5b6519; border-color: #d8df9f; background: #f5f8e8; }}
+.status-stale.stale-failure .status-pill {{ color: #9a4f00; border-color: #efc07a; background: #fff3e0; }}
 tr.status-success .status-pill {{ color: var(--success); border-color: #b7dfc1; background: #f0fff4; }}
 tr.status-failure .status-pill {{ color: var(--failure); border-color: #ffc9cf; background: #fff5f6; }}
 tr.status-in_progress .status-pill {{ color: var(--running); border-color: #bfdbfe; background: #f0f7ff; }}
diff --git a/utils/test_ci_status.py b/utils/test_ci_status.py
index 8241ac5ba..5e9891758 100644
--- a/utils/test_ci_status.py
+++ b/utils/test_ci_status.py
@@ -1,6 +1,7 @@
 import importlib.util
 import pathlib
 import unittest
+from unittest import mock
 
 
 MODULE_PATH = pathlib.Path(__file__).with_name("ci-status.py")
@@ -24,6 +25,77 @@ def check(name, status, *, required=True, url=None):
 
 
 class RequiredFailureHtmlTest(unittest.TestCase):
+    def test_apply_staleness_labels_passed_and_failed_results(self):
+        config = {
+            "stale_after_hours": 168,
+            "branches": [{"name": "stable-synthetic", "label": "Synthetic"}],
+        }
+        stale_check = {"name": "Synthetic CI"}
+        base = {
+            "branch": "stable-synthetic",
+            "branch_label": "Synthetic",
+            "check": "Synthetic CI",
+            "provider": "synthetic",
+            "required": True,
+            "revision": "0" * 40,
+        }
+
+        with mock.patch.object(CI_STATUS, "result_revision_distance", return_value=(3, "stable-synthetic")):
+            failed = CI_STATUS.apply_staleness({
+                **base,
+                "status": CI_STATUS.FAILURE,
+                "message": "build 1",
+            }, config, stale_check)
+            passed = CI_STATUS.apply_staleness({
+                **base,
+                "status": CI_STATUS.SUCCESS,
+                "completed_at": "2026-07-01T00:00:00Z",
+                "message": "build 2",
+            }, config, stale_check)
+
+        self.assertEqual(CI_STATUS.STALE, failed["status"])
+        self.assertEqual(CI_STATUS.FAILURE, failed["stale_base_status"])
+        self.assertEqual("Stale failed", failed["status_label"])
+        self.assertIn("3 commits behind stable-synthetic", failed["message"])
+
+        self.assertEqual(CI_STATUS.STALE, passed["status"])
+        self.assertEqual(CI_STATUS.SUCCESS, passed["stale_base_status"])
+        self.assertEqual("Stale passed", passed["status_label"])
+
+    def test_stale_summary_distinguishes_passed_and_failed(self):
+        branch = {
+            "name": "stable-synthetic",
+            "label": "Synthetic",
+            "status": CI_STATUS.UNKNOWN,
+            "failures": 0,
+            "checks": [
+                check("Required / Passed", CI_STATUS.STALE),
+                check("Required / Failed", CI_STATUS.STALE),
+                check("Required / Unknown", CI_STATUS.UNKNOWN),
+            ],
+        }
+        branch["checks"][0]["stale_base_status"] = CI_STATUS.SUCCESS
+        branch["checks"][0]["status_label"] = "Stale passed"
+        branch["checks"][1]["stale_base_status"] = CI_STATUS.FAILURE
+        branch["checks"][1]["status_label"] = "Stale failed"
+
+        self.assertEqual(
+            "no known failures; 1 unknown, 1 stale-passed, 1 stale-failed",
+            CI_STATUS.summary_text(branch),
+        )
+
+        rendered = CI_STATUS.render_html({
+            "generated_at": "2026-07-19T00:00:00+00:00",
+            "branches": [branch],
+        })
+
+        self.assertIn("1 stale-passed", rendered)
+        self.assertIn("1 stale-failed", rendered)
+        self.assertIn("Stale passed", rendered)
+        self.assertIn("Stale failed", rendered)
+        self.assertIn("status-stale stale-success", rendered)
+        self.assertIn("status-stale stale-failure", rendered)
+
     def test_failure_summary_names_only_required_failed_checks(self):
         branches = [
             {

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

Summary of changes:
 utils/ci-status.py      | 106 ++++++++++++++++++++++++++++++++++++++++++------
 utils/test_ci_status.py |  72 ++++++++++++++++++++++++++++++++
 2 files changed, 166 insertions(+), 12 deletions(-)


hooks/post-receive
-- 
PostGIS


More information about the postgis-tickets mailing list