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
22 changes: 19 additions & 3 deletions dependabot/merge.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,21 +192,34 @@ def checks_readable(gh, owner, kind):
(ok, detail): True verified, False the permission is genuinely missing, and None when the
account simply has no repository to probe -- which is not a permission failure and must not
be reported as one.

The probe must prefer a PRIVATE repository, because `checks: read` is only enforced on one.
The same 2026-11-03 run proves it: `simtabi` examined one pull request and it passed, on the
public `simtabi/cli-kit-js`, with the very token that answered 403 for every private repo in
the other four orgs. A control that happens to pick a public repo therefore reports
"readable" while the gate is blind to every repository it actually works on -- the estate's
Dependabot pull requests sit overwhelmingly in private ones. Sorting private-first over a
wide-enough page closes that; where an account has only public repositories there is no
private blindness to detect and a public probe is complete for it.
"""
listing = f"{'orgs' if kind == 'org' else 'users'}/{owner}/repos?per_page=5&sort=pushed"
listing = f"{'orgs' if kind == 'org' else 'users'}/{owner}/repos?per_page=30&sort=pushed"
try:
repos = gh.api(listing) or []
except Exception as exc:
return False, f"cannot list repositories: {str(exc)[:200]}"

# False sorts before True, so private repositories come first.
repos = sorted(repos, key=lambda r: not r.get("private"))

last = None
for repo in repos:
full, branch = repo.get("full_name"), repo.get("default_branch")
if not full or not branch:
continue
try:
check_runs(gh, full, branch)
return True, f"verified on {full}@{branch}"
scope = "private" if repo.get("private") else "public"
return True, f"verified on {full}@{branch} ({scope})"
except Exception as exc:
detail = str(exc)[:200]
# An empty repository answers 409 and proves nothing either way; keep looking.
Expand Down Expand Up @@ -284,7 +297,10 @@ def main(argv=None, gh=None):
elif ok is None:
text += f"\nNothing examined; no positive control available ({detail}).\n"
else:
text += f"\nNothing examined; checks API confirmed readable ({detail}).\n"
caveat = "" if "(private)" in detail else (
" Only a public repository was available to probe, which does not exercise "
"`checks: read` — that is enforced on private repositories only.")
text += f"\nNothing examined; checks API confirmed readable ({detail}).{caveat}\n"
elif errors and errors == len(examined):
text += (f"\n**Every one of the {len(examined)} pull requests examined errored.** Treat this "
"as one gate failure rather than N unrelated ones — a systemic cause (a token "
Expand Down
34 changes: 34 additions & 0 deletions dependabot/tests/test_merge.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,40 @@ def api(self, path, paginate=False):

self.assertEqual(merge.main(["--owner", "laranail"], gh=EmptyRepoGh(self.repos_route())), 0)

def test_control_prefers_a_private_repository(self):
"""`checks: read` is enforced on private repos only, so probing a public one can
report 'readable' while the gate is blind to every repo it actually works on. That is
what happened on 2026-11-03: simtabi's single pull request passed on the public
cli-kit-js with the same token that 403'd on four orgs' private repos."""
routes = dict(self.EMPTY_SEARCH)
routes["orgs/laranail/repos"] = [
{"full_name": "laranail/pub", "default_branch": "main", "private": False},
{"full_name": "laranail/priv", "default_branch": "main", "private": True},
]
# Only the PUBLIC repo is readable -- the private one 403s, as in the real run.
routes["repos/laranail/pub/commits/main/check-runs"] = [{"check_runs": []}]
gh = FakeGh(routes)
ok, detail = merge.checks_readable(gh, "laranail", "org")
self.assertIs(ok, False, "a public-only read must not pass as verification")
self.assertIn("laranail/priv", detail)

def test_control_names_the_visibility_it_verified(self):
routes = dict(self.EMPTY_SEARCH)
routes["orgs/laranail/repos"] = [{"full_name": "laranail/priv", "default_branch": "main", "private": True}]
routes["repos/laranail/priv/commits/main/check-runs"] = [{"check_runs": []}]
ok, detail = merge.checks_readable(FakeGh(routes), "laranail", "org")
self.assertIs(ok, True)
self.assertIn("(private)", detail)

def test_a_public_only_account_still_verifies(self):
# No private repository means no private blindness to detect; a public probe is complete.
routes = dict(self.EMPTY_SEARCH)
routes["orgs/laranail/repos"] = [{"full_name": "laranail/pub", "default_branch": "main", "private": False}]
routes["repos/laranail/pub/commits/main/check-runs"] = [{"check_runs": []}]
ok, detail = merge.checks_readable(FakeGh(routes), "laranail", "org")
self.assertIs(ok, True)
self.assertIn("(public)", detail)

def test_control_reports_absence_not_just_presence(self):
# The negative half: a control that can only ever say "readable" proves nothing.
ok, detail = merge.checks_readable(FakeGh(self.repos_route()), "laranail", "org")
Expand Down
Loading