Skip to content
Merged
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
41 changes: 31 additions & 10 deletions scripts/pr_gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -799,18 +799,39 @@ def own_job_id(
`run_attempt` filters out the superseded attempts a re-run leaves in the
same listing; without it the first name match could be a previous
attempt's check-run, whose output nothing will ever read again.

The listing is paged through (per_page=100): our job may sit past the
first hundred (#15749). Exhausting the pages without a match still
yields None -- the degradation is unchanged, never a verdict flip.
"""
api = fetch if fetch is not None else _gh_api
payload = api(f"repos/{repo}/actions/runs/{run_id}/jobs?per_page=100")
if not isinstance(payload, dict):
raise GateError(f"unexpected jobs payload for run {run_id}")
for job in payload.get("jobs") or []:
if not isinstance(job, dict):
continue
if run_attempt is not None and str(job.get("run_attempt")) != str(run_attempt):
continue
if job.get("name") == job_name:
return job.get("id")
# Paginate the jobs listing (per_page=100 cap) with the same motif as
# fetch_checks below: a run carrying more than 100 jobs would leave our
# job off page 1 and the verdict motif would silently go unpublished
# again (#15749). total_count is the authoritative stop condition; a
# partial page (< 100 jobs) is the defensive fallback that also keeps
# total_count-less payloads terminating.
page = 1
while True:
payload = api(
f"repos/{repo}/actions/runs/{run_id}/jobs?per_page=100&page={page}"
)
if not isinstance(payload, dict):
raise GateError(f"unexpected jobs payload for run {run_id}")
jobs = payload.get("jobs") or []
for job in jobs:
if not isinstance(job, dict):
continue
if run_attempt is not None and str(job.get("run_attempt")) != str(run_attempt):
continue
if job.get("name") == job_name:
return job.get("id")
if len(jobs) < 100:
break
total = payload.get("total_count")
if total is not None and page * 100 >= total:
break
page += 1
return None


Expand Down
29 changes: 29 additions & 0 deletions scripts/tests/test_pr_gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -1711,6 +1711,35 @@ def test_own_job_id_is_none_when_the_name_is_absent():
) is None


def test_own_job_id_paginates_past_the_first_hundred_jobs():
"""#15749: the jobs listing caps at per_page=100 and own_job_id did NOT
page through. A run carrying more than 100 jobs leaves OUR job off page
1, the lookup degrades to None, and the verdict motif silently goes
unpublished again -- the exact repair #15693 made, disarmed. fetch_checks
paginates correctly in this same file; own_job_id now reuses the motif."""
from urllib.parse import parse_qs, urlparse

target = {"id": 777, "name": pr_gate.DEFAULT_SELF_NAME, "run_attempt": "3"}
filler = [{"id": i, "name": f"matrix {i}", "run_attempt": "3"}
for i in range(100)]
fetched = []

def paged(path):
q = parse_qs(urlparse(path).query)
page = int(q.get("page", ["1"])[0])
assert q.get("per_page") == ["100"], path
fetched.append(page)
if page == 1:
return {"jobs": list(filler), "total_count": 101}
assert page == 2, f"unexpected page {page}"
return {"jobs": [target], "total_count": 101}

got = pr_gate.own_job_id("o/r", "99", pr_gate.DEFAULT_SELF_NAME, "3",
fetch=paged)
assert got == 777
assert fetched == [1, 2], fetched


def test_check_run_output_is_patched_with_the_verdict(monkeypatch):
"""Acceptance 1 on the surface it names: the REQUIRED check carries the
motive, as the exact verdict string (log == summary). PATCH, not POST --
Expand Down
Loading