[SCM] PostGIS branch master updated. 3.7.0beta2-119-g8077e8218
git at osgeo.org
git at osgeo.org
Sat Aug 22 15:23:14 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 8077e8218ce113774d33926b01ab5daa55664f05 (commit)
via 4ac786abbfa78b9e284b71c1a6beaf54d44e5142 (commit)
from 99f9ff5396e9ac19e5f66c7638f42668ca001d0e (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 8077e8218ce113774d33926b01ab5daa55664f05
Merge: 99f9ff539 4ac786abb
Author: Darafei Praliaskouski <komzpa at gmail.com>
Date: Sat Aug 22 15:23:13 2026 -0700
Merge pull request 'ci: cache GitHub rate-limit results with revisions' (!767) from Komzpa/postgis:ci/github-rate-cache-20260823 into master
## Summary
- reuse an exact-branch-head cached GitHub Actions result when the GitHub API returns a rate-limit `403`
- preserve the cached workflow status and commit revision instead of falling back to a revisionless badge
- keep ordinary `403` responses on the existing badge fallback
This prevents anonymous API rate exhaustion from turning a current CI row into a revisionless or misleading badge result. Cache reuse still requires the same branch, check, provider, required flag, and freshly resolved canonical branch head.
## Validation
- `python3 utils/docs/tests/test_ci_status.py` (41 tests)
- `git diff --check upstream/master...HEAD`
Reviewed-on: https://gitea.osgeo.org/postgis/postgis/pulls/767
commit 4ac786abbfa78b9e284b71c1a6beaf54d44e5142
Author: Darafei Praliaskouski <me at komzpa.net>
Date: Sun Aug 23 01:43:17 2026 +0400
ci: cache GitHub rate-limit results with revisions
diff --git a/utils/docs/ci_status/report.py b/utils/docs/ci_status/report.py
index 8f322c069..a5aa0cc35 100644
--- a/utils/docs/ci_status/report.py
+++ b/utils/docs/ci_status/report.py
@@ -297,13 +297,38 @@ def github_runs_for_workflow(repo, branch, workflow, token, timeout):
return data.get("workflow_runs", []), url
-def github_actions_check(check, branch, timeout):
+def github_rate_limit_error(exc):
+ if not isinstance(exc, urllib.error.HTTPError) or exc.code != 403:
+ return False
+ remaining = exc.headers.get("X-RateLimit-Remaining") if exc.headers else None
+ if remaining == "0":
+ exc.close()
+ return True
+ try:
+ body = json.loads(exc.read().decode("utf-8"))
+ except (AttributeError, UnicodeDecodeError, json.JSONDecodeError, OSError):
+ return False
+ finally:
+ exc.close()
+ message = body.get("message") if isinstance(body, dict) else None
+ return isinstance(message, str) and message.startswith("API rate limit exceeded")
+
+
+def github_actions_check(check, branch, timeout, cached_result=None):
workflow = check["workflow"]
repo = check.get("repo", "postgis/postgis")
token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN")
try:
runs, debug_url = github_runs_for_workflow(repo, branch, workflow, token, timeout)
except RECOVERABLE_PROVIDER_ERRORS as exc:
+ if github_rate_limit_error(exc) and cached_result:
+ result = dict(cached_result)
+ result["cached"] = True
+ result["message"] = (
+ f"{result.get('message') or 'cached GitHub Actions result'} "
+ "(cached; GitHub API rate limit exceeded)"
+ )
+ return result
return github_badge_check(check, branch, repo, workflow, timeout, api_error=exc)
if not runs:
try:
@@ -1439,6 +1464,35 @@ def cached_success_result(branch, check, cache, cache_heads):
return result
+def cached_current_result(branch, check, cache, cache_heads):
+ """Return an exact-head cached provider result for rate-limit fallback."""
+ if not cache:
+ return None
+ cached = cache["checks"].get((branch["name"], check["name"]))
+ if not cached:
+ return None
+ if cached.get("branch") != branch["name"] or cached.get("check") != check["name"]:
+ return None
+ if cached.get("provider") != check.get("provider"):
+ return None
+ if cached.get("required") != bool(check.get("required", True)):
+ return None
+ if cached.get("status") not in STATUS_DISPLAY_ORDER:
+ return None
+ revision = cached.get("revision")
+ if not revision or revision != cache_heads.get(branch["name"]):
+ return None
+ result = dict(cached)
+ result.update({
+ "branch": branch["name"],
+ "branch_label": branch["label"],
+ "check": check["name"],
+ "provider": check.get("provider"),
+ "required": bool(check.get("required", True)),
+ })
+ return result
+
+
def stale_after_hours(config, check):
value = check.get("stale_after_hours", config.get("stale_after_hours"))
if value is None:
@@ -1566,7 +1620,12 @@ async def collect_status_async(config, selected_branch=None, include_eol=False,
if provider is None:
raise ConfigError(f"unsupported provider for {check['name']}: {check.get('provider')}")
try:
- return apply_staleness(provider(check, branch, timeout), config, check, cache_heads)
+ cached_fallback = cached_current_result(branch, check, cache, cache_heads)
+ if check.get("provider") == "github_actions":
+ result = provider(check, branch, timeout, cached_fallback)
+ else:
+ result = provider(check, branch, timeout)
+ return apply_staleness(result, config, check, cache_heads)
except RECOVERABLE_PROVIDER_ERRORS as exc:
return result_from_exception(check, branch, exc)
diff --git a/utils/docs/tests/test_ci_status.py b/utils/docs/tests/test_ci_status.py
index bb8d013d7..4469dbb6f 100644
--- a/utils/docs/tests/test_ci_status.py
+++ b/utils/docs/tests/test_ci_status.py
@@ -5,6 +5,7 @@ import subprocess
import sys
import tempfile
import unittest
+import urllib.error
from unittest import mock
@@ -667,6 +668,56 @@ class CIStatusTest(unittest.TestCase):
http_json.call_args.args[0],
)
+ def test_github_actions_rate_limit_uses_exact_head_cached_result(self):
+ check_config = {
+ "name": "GitHub Actions / Linux",
+ "provider": "github_actions",
+ "required": True,
+ "workflow": "ci.yml",
+ }
+ branch = {"name": "master", "label": "master"}
+ revision = "a" * 40
+ cached = {
+ "branch": "master",
+ "branch_label": "master",
+ "check": check_config["name"],
+ "provider": "github_actions",
+ "required": True,
+ "status": CI_STATUS.SUCCESS,
+ "revision": revision,
+ "message": "current Linux run",
+ }
+ headers = {"X-RateLimit-Remaining": "0"}
+ error = urllib.error.HTTPError("https://api.github.com", 403, "forbidden", headers, io.BytesIO(b"{}"))
+ with mock.patch.object(CI_STATUS, "github_runs_for_workflow", side_effect=error):
+ result = CI_STATUS.github_actions_check(check_config, branch, timeout=5, cached_result=cached)
+
+ self.assertEqual(CI_STATUS.SUCCESS, result["status"])
+ self.assertEqual(revision, result["revision"])
+ self.assertTrue(result["cached"])
+ self.assertIn("rate limit exceeded", result["message"])
+
+ def test_github_actions_ordinary_forbidden_does_not_use_cache(self):
+ check_config = {
+ "name": "GitHub Actions / Linux",
+ "provider": "github_actions",
+ "required": True,
+ "workflow": "ci.yml",
+ }
+ branch = {"name": "master", "label": "master"}
+ error = urllib.error.HTTPError(
+ "https://api.github.com", 403, "forbidden", {"X-RateLimit-Remaining": "1"}, io.BytesIO(b"{}")
+ )
+ badge = {"status": CI_STATUS.FAILURE, "message": "badge: failing"}
+ with (
+ mock.patch.object(CI_STATUS, "github_runs_for_workflow", side_effect=error),
+ mock.patch.object(CI_STATUS, "github_badge_check", return_value=badge) as fallback,
+ ):
+ result = CI_STATUS.github_actions_check(check_config, branch, timeout=5, cached_result={"revision": "a" * 40})
+
+ fallback.assert_called_once()
+ self.assertEqual(badge, result)
+
def test_github_actions_does_not_share_one_hundred_runs_between_workflows(self):
branch = {"name": "master", "label": "master"}
linux = {"name": "GitHub Actions / Linux", "provider": "github_actions", "required": True, "workflow": "ci.yml"}
-----------------------------------------------------------------------
Summary of changes:
utils/docs/ci_status/report.py | 63 ++++++++++++++++++++++++++++++++++++--
utils/docs/tests/test_ci_status.py | 51 ++++++++++++++++++++++++++++++
2 files changed, 112 insertions(+), 2 deletions(-)
hooks/post-receive
--
PostGIS
More information about the postgis-tickets
mailing list