Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,11 @@ jobs:

- name: Collect timings and generate report
env:
GITHUB_TOKEN: ${{ secrets.AUTOFIX_BOT_PAT }}
# Forks get no repo secrets (AUTOFIX_BOT_PAT is empty); fall back to
# the run's read-only github.token so this observability job doesn't
# crash on a missing GITHUB_TOKEN and redden every fork PR. Mirrors
# the detect-changes fallback above.
GITHUB_TOKEN: ${{ secrets.AUTOFIX_BOT_PAT || github.token }}
run: |
python3 scripts/ci/timings_report.py \
--baseline ci-timings-baseline.json \
Expand Down
11 changes: 10 additions & 1 deletion scripts/ci/timings_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -923,11 +923,20 @@ def main():
with open(args.from_json, encoding="utf-8") as f:
timings = json.load(f)
else:
token = expect_env("GITHUB_TOKEN")
# Fork PRs get no repo secrets, so the workflow's ``AUTOFIX_BOT_PAT``
# is empty and ``GITHUB_TOKEN`` can arrive unset. This is an
# observability job — degrade gracefully (the handler below) rather
# than reddening the PR with a hard "missing env var" crash before the
# collect even runs. (The workflow also falls back to ``github.token``;
# this keeps the invariant "a missing report never reddens the PR" true
# regardless of how the token is wired.)
token = os.environ.get("GITHUB_TOKEN") or ""
repo = expect_env("GITHUB_REPOSITORY")
run_id = expect_env("GITHUB_RUN_ID")
head_sha = expect_env("GITHUB_SHA")
try:
if not token:
raise TimingsUnavailable("GITHUB_TOKEN not available (fork PR?)")
timings = collect_timings(token, repo, run_id, head_sha)
except TimingsUnavailable as e:
# Observability job: a missing report must never redden the PR.
Expand Down
76 changes: 76 additions & 0 deletions tests/ci/test_timings_report.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
"""Tests for scripts/ci/timings_report.py.

The CI timing report is an *observability* job: its own contract is that "a
missing report must never redden the PR". A fork PR gets no repo secrets, so
the workflow's ``AUTOFIX_BOT_PAT`` is empty and the job can receive an unset
``GITHUB_TOKEN``. The script must then degrade gracefully (exit 0 with a
placeholder report/summary), not crash with "missing environment variable
GITHUB_TOKEN" and fail every fork PR.
"""

from __future__ import annotations

import importlib.util
import sys
from pathlib import Path

import pytest

_PATH = Path(__file__).resolve().parents[2] / "scripts" / "ci" / "timings_report.py"
_spec = importlib.util.spec_from_file_location("timings_report", _PATH)
if _spec is None or _spec.loader is None:
raise ImportError("Failed to load timings_report.py")
_mod = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(_mod)


def _run_main(monkeypatch, tmp_path, *, token):
summary = tmp_path / "summary.md"
output = tmp_path / "report.html"
json_out = tmp_path / "timings.json"
if token is None:
monkeypatch.delenv("GITHUB_TOKEN", raising=False)
else:
monkeypatch.setenv("GITHUB_TOKEN", token)
monkeypatch.setenv("GITHUB_REPOSITORY", "org/repo")
monkeypatch.setenv("GITHUB_RUN_ID", "123")
monkeypatch.setenv("GITHUB_SHA", "deadbeef")
monkeypatch.setattr(
sys,
"argv",
[
"timings_report.py",
"--summary-out", str(summary),
"--output", str(output),
"--json-out", str(json_out),
],
)
return summary, output, json_out


def test_missing_github_token_degrades_instead_of_crashing(monkeypatch, tmp_path):
"""Empty GITHUB_TOKEN (fork PR) must exit 0 with a placeholder report, not
raise ``ValueError: missing environment variable GITHUB_TOKEN``."""
summary, output, json_out = _run_main(monkeypatch, tmp_path, token=None)

with pytest.raises(SystemExit) as exc_info:
_mod.main()

assert exc_info.value.code == 0
# A placeholder HTML report + a degraded summary are emitted...
assert output.exists()
assert "unavailable" in summary.read_text(encoding="utf-8").lower()
# ...but NO JSON, so an empty run can never be cached as the main baseline.
assert not json_out.exists()


def test_empty_string_github_token_also_degrades(monkeypatch, tmp_path):
"""A present-but-empty GITHUB_TOKEN (``AUTOFIX_BOT_PAT`` resolving to '')
is treated the same as unset."""
summary, output, _ = _run_main(monkeypatch, tmp_path, token="")

with pytest.raises(SystemExit) as exc_info:
_mod.main()

assert exc_info.value.code == 0
assert output.exists()
Loading