Repository navigation
Conversation
github_api() returns None for every failure mode — a rate-limit 403, a
permissions 403, a 404, a network error — so once collapsed into a list those
are indistinguishable from "there is nothing here". Four places turned that
emptiness into a contributor-visible decision, and each got it wrong:
* post_pr_comment's dedup scan was an unpaginated fetch, which returns only
GitHub's default first 30 comments (verified against a 2495-comment issue).
On a thread with 30+ comments older than ours the scan never saw our own
comment and posted another. Each post bumps the PR's updated_at, keeping it
inside the sweeper's lookback window, so it posted again every sweep — and
each new comment lands later in the listing, so a 30-item window can never
catch up. Unbounded, self-sustaining, on a public PR.
* check_comments_for_signature read an unreadable thread as "no sign-off",
painting failure on a contributor who had in fact signed.
* check_dco_commits read an unreadable list as "not signed off".
* the signature-registry read treated an unfetchable registry as "this user
has not signed" — the primary compliance path, so a single failed fetch
failed everyone who had signed.
Adds github_api_paginated_checked() returning (items, ok) and threads that
signal through each caller. The plain wrapper stays, so the remaining call
sites are untouched. process_single_pr now writes no status and posts no
comment when any input is unreadable: whatever status a PR already has is a
better answer than one derived from a failed read.
Also fixes, found while auditing rather than from a failing test:
* get_existing_status_state had the same conflation, which quietly disabled
the already-resolved short-circuit (#67) during API trouble and let the
sweeper repaint — and so bump updated_at on — PRs it should have left alone.
* commit.get('sha')[:7] raised TypeError when a commit payload lacked 'sha'.
The sweeper's per-PR except swallowed it, silently skipping the PR.
* fetch_shared_config now reports completeness and cla_sweeper aborts the
sweep when the licence catalogues cannot be loaded. An empty catalogue does
not fail one PR, it decides CLA-vs-DCO for every PR in the sweep from no
data at all.
Completeness deliberately covers only the two licence catalogues, NOT the
allowlist. An empty allowlist is a valid configuration meaning "no overrides"
and fetch_mothership_file cannot tell empty from failed, so gating on it would
mean that emptying cla/allowlist.yml silently aborts every sweep org-wide — far
worse than the skewed decision it would prevent.
New error_log() emits ::error:: for genuine failures; debug_log() emits
::warning:: for everything including success messages, so a real problem was
indistinguishable from noise. Note the sweeper still exits 0 on abort — making
a red exit the alerting channel is deliberately left to a follow-up, since
turning a 100%-green workflow into a notifier carries its own risk.
93 tests, covering 100% of the executable statements this change adds (measured
with an AST-filtered line tracer, not estimated). Every guard is mutation-
proven: removing it makes a specific test fail. That standard caught four of
our own tests passing vacuously — asserting an outcome that held even with the
bug present — and two guards that were not pinned at all despite being claimed
as covered.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
TEMPORARY, to live-test the fetch-failure fix before merging. Revert before merging this branch. `schedule:` only fires from the default branch, so the production cron keeps running main's scripts; a `workflow_dispatch` from this branch checks out this branch's scripts instead. Full-fidelity production run, no effect on the cron. Note what this can and cannot prove: every failure path in this change is unreachable on demand (you cannot ask GitHub to 403 you), so the run only confirms the happy path is unbroken across all 84 repos. That is the failure mode worth checking here, since the change altered the return arity of four functions and added several early returns — a missed call site would crash.
… branch" This reverts commit 3739b63.
|
Closing — not worth merging on current risk. Every defect here needs a fetch failure to trigger, and the measured rate is zero across 214 PR evaluations in 7 consecutive sweeps, with full rate-limit headroom. The longest bot-touched thread has 12 comments against the 30 needed for the pagination bug. Nothing here is firing. The work stands verified if it's ever wanted: 93 tests, 100% coverage of the executable statements it adds, every guard mutation-proven, and a same-moment A/B against One thing worth recording, since it was found here and is independent of this PR. The gate's required context Today that is near-harmless — it needs the status POST to fail silently while the rest of the run succeeds, since Not doing either for now. Recorded so the coupling is not rediscovered from scratch. |
github_api()returnsNonefor every failure mode — a rate-limit 403, a permissions 403, a 404, a network error — so once collapsed into a list those are indistinguishable from "there is nothing here". Four places turned that emptiness into a contributor-visible decision.The four
post_pr_commentdedupcheck_comments_for_signaturefailureon someone who signedcheck_dco_commitsfailureon signed-off commitsThe registry one is the worst: it's the primary compliance path, so one failed fetch fails every compliant contributor at once.
post_pr_commenthad a second, independent bug: the dedup scan was an unpaginated fetch, returning only GitHub's default first 30 comments (verified independently against a 2495-comment issue). On a thread with 30+ comments older than ours the scan never saw our own comment and posted another. Each post bumpsupdated_at, keeping the PR inside the sweeper's lookback window, so it posted again every sweep — and each new comment lands later in the listing, so a 30-item window can never catch up. Unbounded, self-sustaining, on a public PR.Worth stating plainly: naively fixing that pagination alone would have caused the very spam it fixes. A transient error would return
[], read as "haven't commented", and post a duplicate. The readability signal isn't optional polish — it's a prerequisite.The fix
github_api_paginated_checked()returns(items, ok); that signal is threaded through each caller. The plain wrapper stays, so remaining call sites are untouched.process_single_prwrites no status and posts no comment when any input is unreadable — whatever status a PR already has is a better answer than one derived from a failed read.Also fixed (found by auditing, not by a failing test)
get_existing_status_statehad the same conflation, which quietly disabled the already-resolved short-circuit from fix(compliance): skip re-checking PRs that already have a successful status #67 during API trouble and let the sweeper repaint — and so bumpupdated_aton — PRs it should have left alone.commit.get('sha')[:7]raisedTypeErrorwhen a commit payload lackedsha. The sweeper's per-PRexceptswallowed it, silently skipping the PR with no status.fetch_shared_confignow reports completeness andcla_sweeperaborts the sweep when the licence catalogues can't be loaded. An empty catalogue doesn't fail one PR — it decides CLA-vs-DCO for every PR in the sweep from no data.Completeness deliberately covers only the two licence catalogues, not the allowlist. An empty allowlist is a valid configuration meaning "no overrides", and
fetch_mothership_filecan't tell empty from failed — so gating on it would mean emptyingcla/allowlist.ymlsilently aborts every sweep org-wide. Far worse than the skewed decision it would prevent.New
error_log()emits::error::for genuine failures.debug_log()emits::warning::for everything including success messages, so a real problem was indistinguishable from noise.Test plan
discoverand direct runpolicy_selector68/68,cla_sweeper3/3)policy_selector.py,cla_sweeper.py,tests/. Both workflows, both signature registries andcla/allowlist.ymlbyte-identical to mainworkflow_dispatchA/B against main's baseline (running now)Honest notes
Frequency. None of this is currently firing: zero fetch errors across 214 PR evaluations in 7 sweeps, full rate-limit headroom, and the longest bot-touched thread has 12 comments against the 30 needed. This is prophylactic. The case for it is blast radius per occurrence and the fact that these failures are silent — the sweeper has no alerting that would tell you they happened.
What a live test cannot cover. Every failure path here is stub-only by nature; you can't ask GitHub to 403 you on schedule. The canary can only confirm the happy path is unbroken across 84 repos — which matters, since this change altered the return arity of four functions and added many early returns.
Known gap, deliberately deferred. The sweeper still exits 0 when it aborts, so an org-wide compliance stop looks like a green run. Making a red exit the alerting channel is a follow-up, since turning a 100%-green workflow into a notifier carries its own risk (~79 runs/day).
Pre-existing, not addressed. GitHub caps
/pulls/{n}/commitsat 250 commits. Withper_page=100the pagination sees 100/100/50, breaks onlen(items) < 100, and reportsok=True— so a PR with >250 commits can be paintedsuccesson a truncated listing. Its own PR.Process note. An adversarial audit (7 independent lenses, 51 findings, 79 verdicts) refuted 73 and upheld 6, all at severity
low. Its real value was catching two overstatements in an earlier revision of this PR: two guards claimed as mutation-proven weren't, and the test count was inflated by two classes subclassing aTestCaseand re-running its tests. Both corrected here.🤖 Generated with Claude Code