From f0d76e6d189f1c965d450272ba9a4ecd3e97aca7 Mon Sep 17 00:00:00 2001 From: Denis_Drobyshev Date: Fri, 18 Sep 2026 23:15:32 +0300 Subject: [PATCH] Report a workflow it could not read as unknown, not as pinned The check merged an hour ago returned an empty list when a request failed, so a single dropped call reported a repository with twenty-one loose references as fully pinned. That happened on its first real use: a sweep across the nine repositories skipped decisionrl silently, and three immediate retries all returned twenty-one, so the failure was one blip rather than anything persistent. This file's docstring already states the rule I broke -- a repository that could not be read is listed as unread, never silently counted as quiet -- and I broke it in the check added to enforce a different one. unpinned_actions now returns None for unreadable and never an empty list in that case, and the report says the pinning is unknown rather than fine. Three attempts before giving up, because a truthful check that cries wolf on every dropped packet becomes weekly noise, and noise is how a report stops being read. --- scripts/org_triage.py | 62 ++++++++++++++++++++++++++++++++----------- 1 file changed, 46 insertions(+), 16 deletions(-) diff --git a/scripts/org_triage.py b/scripts/org_triage.py index dd6313b..39ca072 100644 --- a/scripts/org_triage.py +++ b/scripts/org_triage.py @@ -181,7 +181,22 @@ def collect(repo: str, branch: str) -> dict: return report -def unpinned_actions(full: str) -> list[str]: +def _retry(call, expected: type, attempts: int = 3): + """Call something up to `attempts` times, returning the first result of `expected`. + + The API drops a request now and then. Reporting that as "could not read" + every time would make a truthful check into weekly noise, and silently + treating it as "nothing found" is what this function exists to prevent, so + the middle answer is to ask again before giving up. + """ + for _ in range(attempts): + result = call() + if isinstance(result, expected): + return result + return None + + +def unpinned_actions(full: str) -> list[str] | None: """Actions still referenced by a tag or a branch rather than by a commit. A tag is a label its owner can move, and these workflows run with a token @@ -189,25 +204,33 @@ def unpinned_actions(full: str) -> list[str]: what notices the next workflow added with `@v4` on it. Local actions (`./.github/...`) and reusable workflows in this organisation are skipped -- those move only when somebody here moves them. + + Returns None when the workflows could not be read, and never an empty list + in that case. The distinction is the whole point: the first version returned + [] on a failed request, so a single timed-out call reported a repository with + twenty-one loose references as fully pinned. That is this file's own stated + rule -- a repository that could not be read is unread, never quiet -- broken + by the check added to enforce another one. """ - # No --jq here: it prints one name per line, which is not JSON, so gh() - # discards it and the check silently sees an empty directory. - listing = gh("api", f"repos/{full}/contents/.github/workflows") + listing = _retry(lambda: gh("api", f"repos/{full}/contents/.github/workflows"), list) + if listing is None: + return None names = [ entry.get("name") - for entry in (listing if isinstance(listing, list) else []) - if isinstance(entry, dict) + for entry in listing + if isinstance(entry, dict) and str(entry.get("name", "")).endswith((".yml", ".yaml")) ] loose: list[str] = [] for name in names: - if not str(name).endswith((".yml", ".yaml")): - continue - body = gh_text( - "api", f"repos/{full}/contents/.github/workflows/{name}", - "-H", "Accept: application/vnd.github.raw", + body = _retry( + lambda name=name: gh_text( + "api", f"repos/{full}/contents/.github/workflows/{name}", + "-H", "Accept: application/vnd.github.raw", + ), + str, ) - if not body: - continue + if body is None: + return None for match in re.finditer(r"uses:\s*([^\s#]+)", body): ref = match.group(1) if ref.startswith(("./", f"{ORG}/")) or "@" not in ref: @@ -256,11 +279,18 @@ def render(reports: list[dict]) -> tuple[str, bool]: ) interesting = True - if report["unpinned"]: - count = len(report["unpinned"]) + unpinned = report["unpinned"] + if unpinned is None: + drifting.append( + f"{repo} — could not read the workflows, so whether its actions are " + "pinned is unknown rather than fine." + ) + interesting = True + elif unpinned: + count = len(unpinned) drifting.append( f"{repo} — {count} action{'s' if count != 1 else ''} still on a moving tag: " - + ", ".join(sorted({line.split(': ', 1)[1] for line in report["unpinned"]})) + + ", ".join(sorted({line.split(': ', 1)[1] for line in unpinned})) ) interesting = True