refactor: migrate from github3.py to PyGithub#587
Conversation
## What/Why Replace github3.py (pinned at 4.0.1) with PyGithub (>=2.6.0) for GitHub API interactions. PyGithub is more actively maintained and provides first-class support for base_url, app auth, and installation tokens. ## Proof it works 176 tests passed, 32 subtests passed, 100% code coverage via `make test`. ## Risk + AI role Medium -- touches all GitHub API interaction code (auth, repo operations, exception handling). All changes AI-generated using Claude Opus 4.6 with human review. ## Review focus 1. Auth flow in auth.py -- AppAuth/installation auth two-step pattern replaces login_as_app_installation 2. dependabot_file.py -- directory_contents (tuples) merged into get_contents (objects with .name), verify the or [] pattern in test mocks handles the combined code path correctly Signed-off-by: jmeridth <jmeridth@gmail.com>
Signed-off-by: jmeridth <jmeridth@gmail.com>
There was a problem hiding this comment.
Pull request overview
This PR migrates the action’s GitHub API integration from github3.py to PyGithub, updating authentication, repository operations, and exception handling while keeping requests for preview REST + GraphQL use-cases.
Changes:
- Replace
github3.pyclient usage with PyGithub (Github,Auth,GithubIntegration) and update auth flows/tests accordingly. - Update repository operations and file interactions (
get_contents,get_pulls,get_issues,get_git_ref,create_git_ref,update_file, etc.) across core logic and tests. - Refresh dependency declarations/lockfile and mypy ignore configuration for the new
githubmodule.
Reviewed changes
Copilot reviewed 11 out of 12 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
auth.py |
Switches auth implementation to PyGithub and refactors installation token retrieval. |
dependabot_file.py |
Migrates directory/file probing to repo.get_contents() and updates exception handling to UnknownObjectException. |
evergreen.py |
Updates repo iteration and PR/file operations to PyGithub equivalents; adjusts error handling. |
exceptions.py |
Replaces github3 NotFoundError dependency with PyGithub UnknownObjectException for optional-file semantics. |
pyproject.toml |
Replaces github3-py dependency with PyGithub>=2.6.0. |
uv.lock |
Updates locked dependencies to remove github3-py and add pygithub (+ transitive deps). |
.github/linters/.mypy.ini |
Updates mypy ignore section from github3.* to github.*. |
test_auth.py |
Rewrites auth tests to mock PyGithub objects/classes. |
test_dependabot_file.py |
Updates mocks for get_contents() and PyGithub exceptions. |
test_evergreen.py |
Updates repo mock expectations to PyGithub methods and call signatures. |
test_exceptions.py |
Updates tests to assert PyGithub exception inheritance/behavior. |
test_env.py |
Makes env patching more robust with clear=True. |
Signed-off-by: jmeridth <jmeridth@gmail.com>
zkoppert
left a comment
There was a problem hiding this comment.
Multi-model review of the github3 to PyGithub migration (Opus, GPT-5.3-Codex, GPT-5.4, all findings verified in the repo venv). The migration itself reads cleanly. I have one verified High that the swap introduces even though it does not touch the line, plus a smaller inline note.
High: repo.owner breaks the security-updates URLs (evergreen.py lines 216 and 219). Now that get_repos_iterator returns PyGithub Repository objects, repo.owner is a NamedUser rather than github3's ShortUser. NamedUser has no __str__, so when is_dependabot_security_updates_enabled and enable_dependabot_security_updates interpolate it into f".../repos/{owner}/{repo}/automated-security-fixes" it renders as NamedUser(login="..."). I reproduced the resulting URL in the venv: https://api.github.com/repos/NamedUser(login="octocat")/hello-world/automated-security-fixes, which 404s, so with ENABLE_SECURITY_UPDATES on the enable step silently never runs, and the summary still shows a green check because it keys off the config flag. GitHub will not let me anchor an inline comment on those two lines because they are unchanged in the diff, so I am flagging it here. The fix is to pass repo.owner.login at both call sites:
if config.enable_security_updates:
if not is_dependabot_security_updates_enabled(
config.ghe, config.ghe_api_url, repo.owner.login, repo.name, token
):
enable_dependabot_security_updates(
config.ghe, config.ghe_api_url, repo.owner.login, repo.name, token
)A test that drives these helpers with a real NamedUser (or a stub exposing .login) would guard it, since main() is # pragma: no cover and the helper tests pass a plain string owner.
Separate, pre-existing (not caused by this migration): is_repo_created_date_before(repo.created_at, ...) at evergreen.py line 132. repo.created_at is a datetime in both github3 and PyGithub, but the helper calls datetime.fromisoformat(), which only accepts a str, so any run with CREATED_AFTER_DATE set raises TypeError. It sits outside this migration and the line is unchanged, so I would not block on it. I am happy to file a separate issue if you want it tracked.
One inline note below.
## What/Why Narrow except clause from bare Exception to GithubException so that misconfiguration errors (bad int() cast, JWT construction failures) propagate instead of silently returning None and causing confusing 401s downstream. Addresses PR review feedback from @zkoppert. ## Proof it works All 14 tests in test_auth.py pass. Updated the request_failure test to raise GithubException instead of bare Exception to match the narrowed catch. Pylint clean (no new warnings). ## Risk + AI role Low. AI-generated (Claude Opus 4.6) with human review. ## Review focus Whether GithubException covers all the API/network error cases we want to catch, or if additional specific exceptions should be included. Signed-off-by: jmeridth <jmeridth@gmail.com>
…me in created_after filter ## What/Why Fix two bugs exposed by the PyGithub migration: (1) repo.owner is now a NamedUser object, not a string, so security-updates URLs rendered as "repos/NamedUser(login=...)/..." and 404'd silently; (2) repo.created_at is a datetime object but is_repo_created_date_before called fromisoformat() on it, which raises TypeError. ## Proof it works All 180 tests pass including 2 new tests for datetime input to is_repo_created_date_before (both before and after filter date). ## Risk + AI role Medium -- the repo.owner.login fix touches the security-updates code path which is pragma: no cover. AI-generated (Claude Opus 4.6) with human review. ## Review focus Whether repo.owner.login is the correct attribute for all PyGithub Repository objects (org-owned vs user-owned repos). Signed-off-by: jmeridth <jmeridth@gmail.com>
|
@zkoppert Both findings from your review are now addressed: 1. 2. 3. |
What/Why
Replace github3.py (pinned at 4.0.1) with PyGithub (>=2.6.0) for GitHub API interactions. PyGithub is more actively maintained and provides first-class support for base_url, app auth, and installation tokens.
The
requestslibrary remains as a dependency because it is used for two categories of calls that PyGithub does not support:application/vnd.github.london-preview+json) not exposed by PyGithubget_global_project_id,get_global_issue_id,get_global_pull_request_id,link_item_to_project)Proof it works
176 tests passed, 32 subtests passed, 100% code coverage via
make test.Risk + AI role
Medium -- touches all GitHub API interaction code (auth, repo operations, exception handling). All changes AI-generated using Claude Opus 4.6 with human review.
Review focus
AppAuth/get_installation_authtwo-step pattern replaceslogin_as_app_installationdirectory_contents(tuples) merged intoget_contents(objects with.name), verify theor []pattern in test mocks handles the combined code path correctly