Skip to content

refactor: migrate from github3.py to PyGithub#587

Merged
jmeridth merged 5 commits into
mainfrom
migrate-github3-to-pygithub
Jul 3, 2026
Merged

refactor: migrate from github3.py to PyGithub#587
jmeridth merged 5 commits into
mainfrom
migrate-github3-to-pygithub

Conversation

@jmeridth

@jmeridth jmeridth commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

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 requests library remains as a dependency because it is used for two categories of calls that PyGithub does not support:

  • Dependabot security updates -- uses a preview API (application/vnd.github.london-preview+json) not exposed by PyGithub
  • GraphQL ProjectV2 mutations -- PyGithub is REST-only and has no GraphQL support (get_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

  1. Auth flow in auth.py -- AppAuth/get_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

## 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>
@jmeridth jmeridth requested a review from zkoppert as a code owner July 2, 2026 03:16
Copilot AI review requested due to automatic review settings July 2, 2026 03:16
Signed-off-by: jmeridth <jmeridth@gmail.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.py client 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 github module.

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.

Comment thread auth.py
Comment thread dependabot_file.py
Signed-off-by: jmeridth <jmeridth@gmail.com>

@zkoppert zkoppert left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread auth.py Outdated
jmeridth added 2 commits July 2, 2026 19:56
## 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>
@jmeridth

jmeridth commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator Author

@zkoppert Both findings from your review are now addressed:

1. repo.owner breaking security-updates URLs (High)
Changed repo.owner to repo.owner.login at both call sites (lines 216 and 219). Addressed in 086af44.

2. is_repo_created_date_before receiving a datetime (pre-existing)
Updated the function to accept both str and datetime via an isinstance check, so PyGithub's repo.created_at (a datetime) no longer raises TypeError. Added two tests covering the datetime path. Also addressed in 086af44.

3. except Exception too broad (inline thread)
Already narrowed to GithubException in 6e1b200.

@jmeridth jmeridth requested a review from zkoppert July 3, 2026 01:12

@zkoppert zkoppert left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀

@jmeridth jmeridth merged commit a7cb2af into main Jul 3, 2026
35 checks passed
@jmeridth jmeridth deleted the migrate-github3-to-pygithub branch July 3, 2026 03:12
@jmeridth jmeridth mentioned this pull request Jul 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants