This is an automated email from the ASF dual-hosted git repository.
potiuk pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow-steward.git
The following commit(s) were added to refs/heads/main by this push:
new ff99a32 Fix stale snapshots in security-tracker-stats-dashboard (now,
is_open, event cache) (#448)
ff99a32 is described below
commit ff99a32b085b696a600aba28617dc02560bddb23
Author: Jarek Potiuk <[email protected]>
AuthorDate: Thu Jun 4 04:39:47 2026 +0200
Fix stale snapshots in security-tracker-stats-dashboard (now, is_open,
event cache) (#448)
Three independent causes made the dashboard diverge from the tracker's
current state:
1. render.py hard-coded NOW to 2026-05-21, freezing every snapshot at that
date. Use datetime.now(timezone.utc).
2. labels_open_at derived is_open only from replayed closed/reopened events;
GitHub's issue-events API omits the closed event for some (often older)
issues, leaving them stuck open. Backstop is_open with the authoritative
closedAt from the issue payload (the cum_closed series already trusts
it).
3. fetch_events.py never refreshed cached event files, so issues relabeled
or
closed after their first fetch kept stale history. Re-fetch when the
cached
file is older than the issue's updatedAt.
Generated-by: Claude Opus 4.8 (1M context)
---
tools/security-tracker-stats-dashboard/fetch_events.py | 17 ++++++++++++++++-
tools/security-tracker-stats-dashboard/render.py | 9 ++++++++-
2 files changed, 24 insertions(+), 2 deletions(-)
diff --git a/tools/security-tracker-stats-dashboard/fetch_events.py
b/tools/security-tracker-stats-dashboard/fetch_events.py
index a496b67..6488572 100644
--- a/tools/security-tracker-stats-dashboard/fetch_events.py
+++ b/tools/security-tracker-stats-dashboard/fetch_events.py
@@ -4,6 +4,7 @@
import json
import subprocess
import concurrent.futures
+import datetime as dt
import os
ROOT = os.environ.get('TRACKER_STATS_CACHE', '/tmp/tracker-stats-cache')
@@ -14,6 +15,16 @@ with open(f'{ROOT}/issues.json') as f:
issues = json.load(f)
numbers = [i['number'] for i in issues]
+# Map issue number -> last-updated epoch so the cache can be refreshed when an
+# issue was relabeled / closed after its events were last fetched.
+def _iso_to_epoch(s):
+ if not s:
+ return None
+ try:
+ return dt.datetime.fromisoformat(s.replace('Z', '+00:00')).timestamp()
+ except ValueError:
+ return None
+UPDATED_AT = {i['number']: _iso_to_epoch(i.get('updatedAt')) for i in issues}
print(f"Fetching events for {len(numbers)} issues...")
os.makedirs(EVENTS_DIR, exist_ok=True)
@@ -21,7 +32,11 @@ os.makedirs(EVENTS_DIR, exist_ok=True)
def fetch_one(n):
out_path = f'{EVENTS_DIR}/{n}.json'
if os.path.exists(out_path) and os.path.getsize(out_path) > 0:
- return (n, True, 'cached')
+ # Trust the cache only if it was written after the issue was last
+ # updated; otherwise the issue changed since and the events are stale.
+ upd = UPDATED_AT.get(n)
+ if upd is None or os.path.getmtime(out_path) >= upd:
+ return (n, True, 'cached')
try:
r = subprocess.run(
['gh', 'api', f'repos/{REPO}/issues/{n}/events',
diff --git a/tools/security-tracker-stats-dashboard/render.py
b/tools/security-tracker-stats-dashboard/render.py
index ccf971f..42c3cff 100644
--- a/tools/security-tracker-stats-dashboard/render.py
+++ b/tools/security-tracker-stats-dashboard/render.py
@@ -382,7 +382,7 @@ if UPSTREAM_REPO:
with open(prs_path) as f:
prs_cache = json.load(f)
-NOW = dt.datetime(2026, 5, 21, 0, 0, 0, tzinfo=dt.timezone.utc)
+NOW = dt.datetime.now(dt.timezone.utc)
if UPSTREAM_REPO:
# Match the original literal in the body-parse regex so an upstream
@@ -586,6 +586,13 @@ def labels_open_at(issue, ts):
is_open = False
elif e['event'] == 'reopened':
is_open = True
+ # The issue-events API can omit the 'closed' event for some (often
+ # older) issues, which would leave is_open stuck True and miscount a
+ # closed issue as open. Trust the authoritative closedAt from the
+ # issue payload as a backstop (the cum_closed series already does).
+ closed_at = parse_dt(issue.get('closedAt'))
+ if issue.get('state') == 'CLOSED' and closed_at and closed_at <= ts:
+ is_open = False
return labels, is_open