[SCM] PostGIS branch master updated. 3.7.0beta1-77-g023ac85a98
git at osgeo.org
git at osgeo.org
Mon Jul 27 11:15:41 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 023ac85a987b77c2045c165c7ff539d94ba067ff (commit)
via de37c70c27d72cbb8e88b82bcfd42f4b42fe9c8f (commit)
from d3bda3e955fd941fae9b043c67ca311e4240b938 (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 023ac85a987b77c2045c165c7ff539d94ba067ff
Merge: d3bda3e955 de37c70c27
Author: Darafei Praliaskouski <komzpa at gmail.com>
Date: Mon Jul 27 11:15:40 2026 -0700
Merge pull request 'ci: show actionable dashboard failure reasons' (!557) from Komzpa/postgis:ci/dashboard-failure-reasons-20260727 into master
Reviewed-on: https://gitea.osgeo.org/postgis/postgis/pulls/557
commit de37c70c27d72cbb8e88b82bcfd42f4b42fe9c8f
Author: Darafei Praliaskouski <me at komzpa.net>
Date: Mon Jul 27 14:20:06 2026 +0400
ci: show actionable dashboard failure reasons
diff --git a/utils/ci-status.py b/utils/ci-status.py
index 7d8f3f3cd2..866a00a3d4 100755
--- a/utils/ci-status.py
+++ b/utils/ci-status.py
@@ -31,6 +31,7 @@ STALE_PASSED = "stale-passed"
STALE_FAILED = "stale-fail"
DISABLED = "disabled"
NOT_APPLICABLE = "not_applicable"
+JENKINS_STALE_QUEUE_HOURS = 4
SYMBOLS = {
SUCCESS: ("✅", "OK"),
@@ -465,6 +466,57 @@ def woodpecker_workflow_details(pipeline, web_url):
return details
+def woodpecker_error_details(pipeline):
+ errors = [
+ error.get("message")
+ for error in pipeline.get("errors") or []
+ if isinstance(error, dict) and error.get("message") and not error.get("is_warning")
+ ]
+ if not errors:
+ return None
+ return {
+ "message": "; ".join(errors),
+ "status_label": "Config error",
+ }
+
+
+def woodpecker_leaf_steps(workflow):
+ children = workflow.get("children") or []
+ if children:
+ return children
+ return [workflow]
+
+
+def woodpecker_killed_details(pipeline):
+ workflows = pipeline.get("workflows") or []
+ if not workflows:
+ return None
+
+ non_success = []
+ killed_zero = []
+ for workflow in workflows:
+ for step in woodpecker_leaf_steps(workflow):
+ status = normalize_woodpecker_status(step.get("state") or step.get("status"))
+ if status == SUCCESS:
+ continue
+ non_success.append(step)
+ if str(step.get("state") or step.get("status")).lower() == "killed" and step.get("exit_code") == 0:
+ killed_zero.append(step)
+
+ if not non_success or len(non_success) != len(killed_zero):
+ return None
+
+ labels = [
+ str(step.get("name") or f"step {step.get('pid') or step.get('id')}")
+ for step in killed_zero[:3]
+ ]
+ suffix = f" ({', '.join(labels)}" + (", ..." if len(killed_zero) > len(labels) else "") + ")"
+ return {
+ "message": f"agent lost: {plural(len(killed_zero), 'step')} killed at exit 0{suffix}",
+ "status_label": "Agent lost",
+ }
+
+
def woodpecker_check(check, branch, timeout):
query = urllib.parse.urlencode({
"branch": branch["name"],
@@ -490,17 +542,32 @@ def woodpecker_check(check, branch, timeout):
web_url = check.get("web_url")
run_url = woodpecker_pipeline_url(web_url, current)
detail_url = woodpecker_pipeline_detail_url(api_url, current)
- if detail_url and "workflows" not in current:
+ needs_detail = "workflows" not in current
+ if (
+ normalize_woodpecker_status(current.get("status")) == FAILURE
+ and str(current.get("status")).lower() == "error"
+ and current.get("errors")
+ ):
+ needs_detail = False
+ if detail_url and needs_detail:
try:
current = {**current, **http_json(detail_url, timeout=timeout)}
except RECOVERABLE_PROVIDER_ERRORS:
pass
message = current.get("message")
+ extra = {}
if normalize_woodpecker_status(current.get("status")) != SUCCESS:
- details = woodpecker_workflow_details(current, web_url)
+ details = None
+ if str(current.get("status")).lower() == "error" and not (current.get("workflows") or []):
+ details = woodpecker_error_details(current)
+ if not details and str(current.get("status")).lower() == "failure":
+ details = woodpecker_killed_details(current)
+ if not details:
+ details = woodpecker_workflow_details(current, web_url)
if details:
message = details["message"]
run_url = details.get("url") or run_url
+ extra.update({key: details[key] for key in ("status_label",) if key in details})
result = make_result(
check,
branch,
@@ -510,6 +577,7 @@ def woodpecker_check(check, branch, timeout):
revision=current.get("commit"),
completed_at=current.get("finished") or current.get("updated") or current.get("created"),
message=message,
+ **extra,
)
if previous:
previous_url = woodpecker_pipeline_url(web_url, previous)
@@ -795,6 +863,18 @@ def queued_jenkins_revision(item):
return None
+def jenkins_queued_status_label(item):
+ queued_at = parse_time(item.get("inQueueSince"))
+ if not queued_at:
+ return "Queued"
+ age = utc_now() - queued_at
+ if age.total_seconds() > JENKINS_STALE_QUEUE_HOURS * 3600:
+ return "Queued stale"
+ if item.get("stuck"):
+ return "Queued stuck"
+ return "Queued"
+
+
def jenkins_queue_item_rank(item, branch):
revision = queued_jenkins_revision(item)
is_current = False
@@ -837,6 +917,7 @@ def jenkins_queued_check(check, branch, job_url, timeout):
revision=params.get("after") or params.get("BRANCH"),
completed_at=item.get("inQueueSince"),
message=message,
+ status_label=jenkins_queued_status_label(item),
)
diff --git a/utils/test_ci_status.py b/utils/test_ci_status.py
index c22a04fa7c..9ba6674795 100644
--- a/utils/test_ci_status.py
+++ b/utils/test_ci_status.py
@@ -536,6 +536,16 @@ class RequiredFailureHtmlTest(unittest.TestCase):
self.assertEqual(current_revision, result["revision"])
self.assertEqual("queued item 108746: Build #8,030 is already in progress", result["message"])
+ def test_jenkins_old_queue_item_is_labeled_stale(self):
+ queued = {
+ "id": 108746,
+ "inQueueSince": 1784821000000,
+ }
+
+ now = CI_STATUS.dt.datetime.fromtimestamp(1784840000, CI_STATUS.dt.timezone.utc)
+ with mock.patch.object(CI_STATUS, "utc_now", return_value=now):
+ self.assertEqual("Queued stale", CI_STATUS.jenkins_queued_status_label(queued))
+
def test_woodpecker_failure_names_single_failed_workflow(self):
check_config = {
"name": "Woodpecker",
@@ -575,6 +585,79 @@ class RequiredFailureHtmlTest(unittest.TestCase):
http_json.call_args_list[1].args[0],
)
+ def test_woodpecker_error_without_workflows_shows_error_message(self):
+ check_config = {
+ "name": "Woodpecker",
+ "provider": "woodpecker",
+ "required": True,
+ "api_url": "https://woodie.example.test/api/repos/30/pipelines",
+ "web_url": "https://woodie.example.test/repos/30",
+ }
+ branch = {"name": "master", "label": "master"}
+ pipeline = {
+ "number": 5733,
+ "event": "push",
+ "branch": "master",
+ "ref": "refs/heads/master",
+ "status": "error",
+ "commit": "c" * 40,
+ "message": "opaque commit message",
+ "errors": [{"message": "step 'html-ja' depends on unknown step 'html-de'"}],
+ }
+
+ with mock.patch.object(CI_STATUS, "http_json", return_value=[pipeline]) as http_json:
+ result = CI_STATUS.woodpecker_check(check_config, branch, timeout=5)
+
+ self.assertEqual(CI_STATUS.FAILURE, result["status"])
+ self.assertEqual("Config error", result["status_label"])
+ self.assertEqual("step 'html-ja' depends on unknown step 'html-de'", result["message"])
+ http_json.assert_called_once()
+
+ def test_woodpecker_killed_zero_exit_steps_are_agent_loss(self):
+ check_config = {
+ "name": "Woodpecker",
+ "provider": "woodpecker",
+ "required": True,
+ "api_url": "https://woodie.example.test/api/repos/30/pipelines",
+ "web_url": "https://woodie.example.test/repos/30",
+ }
+ branch = {"name": "stable-3.6", "label": "3.6"}
+ pipeline = {
+ "number": 5696,
+ "event": "pull_request",
+ "branch": "stable-3.6",
+ "ref": "refs/heads/stable-3.6",
+ "status": "failure",
+ "commit": "d" * 40,
+ "message": "opaque commit message",
+ }
+ pipeline_detail = {
+ **pipeline,
+ "workflows": [
+ {
+ "pid": 1,
+ "name": "docs",
+ "state": "failure",
+ "children": [
+ {"pid": 4, "name": "clone", "state": "killed", "exit_code": 0},
+ {"pid": 5, "name": "prepare", "state": "killed", "exit_code": 0},
+ {"pid": 6, "name": "check-xml", "state": "killed", "exit_code": 0},
+ ],
+ },
+ ],
+ }
+
+ with mock.patch.object(CI_STATUS, "http_json", side_effect=([pipeline], pipeline_detail)):
+ result = CI_STATUS.woodpecker_check(
+ {**check_config, "event": "pull_request"},
+ branch,
+ timeout=5,
+ )
+
+ self.assertEqual(CI_STATUS.FAILURE, result["status"])
+ self.assertEqual("Agent lost", result["status_label"])
+ self.assertEqual("agent lost: 3 steps killed at exit 0 (clone, prepare, check-xml)", result["message"])
+
def test_woodpecker_running_workflows_are_summarized(self):
check_config = {
"name": "Woodpecker",
-----------------------------------------------------------------------
Summary of changes:
utils/ci-status.py | 85 +++++++++++++++++++++++++++++++++++++++++++++++--
utils/test_ci_status.py | 83 +++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 166 insertions(+), 2 deletions(-)
hooks/post-receive
--
PostGIS
More information about the postgis-tickets
mailing list