Repository navigation
fix(security-tracker-stats): retry 5xx and default cache to temp root - #1752
Conversation
Why: `fetch_issues.py` ran `gh` once and turned any non-zero exit into a `SystemExit`, so a single transient `HTTP 502` aborted the whole dashboard run. Because this fetch is the first of six steps, the operator had to restart the pipeline by hand. `run_gh` now retries the failures that can clear on their own (5xx, gateway timeouts, connection resets) with exponential backoff, and still fails fast on the ones that cannot: an auth error, a bad repo slug, or an unknown flag never fixes itself. The default cache also moves off a hardcoded `/tmp`. A sandboxed run is granted its own scratch directory and cannot write `/tmp` at all, so the first fetch step failed before it ever reached the API. The default is now `<tempfile.gettempdir()>/tracker-stats-cache`, which honours `$TMPDIR` on POSIX and `%TEMP%` on Windows; an explicit `TRACKER_STATS_CACHE` still wins. The default output path moves with it, for the same reason. Closes apache#1744. Generated-by: WorkBuddy AI (DeepSeek-V4.1-Flash)
Kaap10
left a comment
There was a problem hiding this comment.
Thanks for addressing #1744, @sharpca! Moving the cache defaults out of hardcoded /tmp to tempfile.gettempdir() solves the sandbox write restriction nicely, and introducing exponential backoff on 5xx transient errors makes the pipeline much more resilient. The test suite in test_fetch_retry.py is clean and covers the retry states well.
A few findings from review to address before this can land:
1. SKILL.md frontmatter drift (surface_hash & measured_tokens)
File: plugins/magpie-security/skills/tracker-stats-dashboard/SKILL.md (lines 19–21)
Because prose in Step 2 was modified (lines 210–212), the surface_hash and measured_tokens in the frontmatter are out of date:
- In this repository,
prekrunsskill-surface-hashandcheck-measured-tokens, which will fail CI once the workflows run on this PR. - Fix: Running
prek run --all-files(or updating the hash with the helper scripts) will automatically recalculate and update these frontmatter stamps.
2. Unhandled subprocess.TimeoutExpired in run_gh
File: tools/security-tracker-stats-dashboard/fetch_issues.py (lines 70–75)
In fetch_issues.py:
for attempt in range(1, attempts + 1):
r = subprocess.run(args, capture_output=True, text=True, timeout=timeout)
if r.returncode == 0:
return r_TRANSIENT_FAILURE regex includes "timed out", which matches if gh itself outputs a timeout to stderr. However, if the process hits the 300-second subprocess limit, subprocess.run raises subprocess.TimeoutExpired rather than returning a CompletedProcess with non-zero exit code. Because this exception is unhandled, it will terminate execution immediately without retrying.
Suggested fix: Catch subprocess.TimeoutExpired so it participates in the retry loop:
for attempt in range(1, attempts + 1):
try:
r = subprocess.run(args, capture_output=True, text=True, timeout=timeout)
except subprocess.TimeoutExpired as exc:
if attempt == attempts:
raise SystemExit(f"gh timed out after {timeout}s: {exc}")
delay = base_delay * 2 ** (attempt - 1)
print(
f"gh timed out (attempt {attempt}/{attempts}), retrying in {delay:g}s",
file=sys.stderr,
)
time.sleep(delay)
continue
if r.returncode == 0:
return r3. Path consistency in run.sh
File: tools/security-tracker-stats-dashboard/run.sh (line 58)
echo "done: ${TRACKER_STATS_OUT:-${TMPDIR:-/tmp}/airflow_s_monthly.html}"render.py resolves the path via Python's tempfile.gettempdir(), which resolves to %TEMP% on Windows, whereas bash's ${TMPDIR:-/tmp} defaults to /tmp. On non-POSIX environments, the completion message will point to /tmp while the file actually lives under %TEMP%. Not a blocker for POSIX hosts, but worth keeping aligned.
Generated-by: Claude Code (Opus 5.5)
`run_gh` moves to `gh_retry.py`, which `fetch_issues.py` and `fetch_roster.py` both use, so a single 502 on the roster fetch no longer aborts the run. A call that hits its timeout is now retried like a 504 instead of escaping as `TimeoutExpired`. `run.sh` stops guessing the output path in its last line, since `render.py` already prints the path it wrote, and the skill's `measured_tokens` stamp is refreshed. Generated-by: Claude Code (Opus 5.5)
potiuk
left a comment
There was a problem hiding this comment.
Retrying the transient gh failures and moving the defaults off a hard-coded /tmp are both right, and the new tests cover the retry paths well.
I pushed one fixup on top (4c2667f) for the remaining points, including the three raised inline earlier: run_gh moves to gh_retry.py and fetch_roster.py uses it too, so a single 502 on the roster fetch no longer aborts the run; a call that hits its timeout is retried like a 504 instead of escaping as TimeoutExpired; run.sh no longer guesses the output path, since render.py already prints the one it wrote; and measured_tokens is restamped. There are three new tests in test_fetch_retry.py. I also merged main in. Please pull before pushing anything else to the branch.
Thank you for your first contribution!
This review was drafted by an AI-assisted tool and
confirmed by an Apache Magpie maintainer. The findings
below are observations, not blockers; an Apache Magpie
maintainer — a real person — will take the next look at the
PR. If you think a finding is mis-applied, please reply on
the PR and a maintainer will weigh in.More on how Apache Magpie handles maintainer review:
Contributing guide.
Summary
fetch_issues.pyretried nothing: a single transientHTTP 502from the APIbecame a
SystemExit, and since this fetch is the first of six steps theoperator had to restart the whole dashboard run by hand.
run_ghnow retriesthe failures that can clear on their own (5xx, gateway timeouts, connection
resets) with exponential backoff, and still fails fast on an auth error, a bad
repo slug, or an unknown flag.
/tmp. A sandboxed runis granted its own scratch directory and cannot write
/tmpat all, so thefirst fetch step failed before it ever reached the API. Both defaults now
derive from
tempfile.gettempdir()($TMPDIRon POSIX,%TEMP%on Windows);TRACKER_STATS_CACHEandTRACKER_STATS_OUTstill win when set.run.sh, the skill prose, the adopter template, and the step-2 evalsystem prompt follow the new default.
Type of change
tools/*/withpyproject.toml)docs/,README.md,CONTRIBUTING.md)tracker-stats-dashboardSKILL.mdrestates the cache default; no behaviour change, so no new fixture.
Test plan
uv run pytest/ruff check/mypypassespytest: 149 passed (142 existing + 7 new intests/test_fetch_retry.py)ruff check .andruff format --check .: cleanmypy: no issues in 6 source filestests/test_fetch_retry.pycover transient-status detection,retry-then-succeed with a doubling backoff, give-up after the attempt
limit, no retry on a 401, and both branches of the cache default.
RFC-AI-0004 compliance
paths (
/tmp/tracker-stats-cache,/tmp/airflow_s_monthly.html); no newhost access or network reach is introduced.
Linked issues
Closes #1744
Notes for reviewers
TRACKER_STATS_OUTwas not named in the issue, but it hardcoded/tmpfor thesame reason and would have kept the run failing after the cache was fixed.
Happy to split it out if you would rather keep this PR to the cache alone.
report.mdfiles keepCache directory: /tmp/...: on aPOSIX host without
TMPDIR,tempfile.gettempdir()is/tmp, so that outputis still exactly what the new default produces.
secondary-rate-limit is deliberately not retried — it needs the operator to
read the message, not a shorter backoff.
Generative AI disclosure
This change was authored with an AI coding agent (WorkBuddy AI, DeepSeek-V4.1-Flash)
under human review. The commit carries the
Generated-by:trailer; thecontributor has reviewed the diff and owns the change.