Repository navigation
Conversation
--as-app only accepts an installation token created elsewhere. Those expire after an hour, so long backups fail part way through. With --app-id and --app-private-key, github-backup creates the token itself and replaces it ten minutes before it expires. The installation is looked up from the user or organization being backed up, unless --app-installation-id names it. The JWT is signed by the openssl command line tool so the project keeps zero runtime dependencies; the Docker image now installs openssl. A missing key file or openssl is reported by parse_args before any work starts. API requests resolve auth per request, and wiki clones rebuild their URL, so both pick up a refreshed token mid-run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
|
Nice. Reviewed and tested exiting paths. Looks good to merge. Could not test the GitHub App path myself because I don't have an app set up, so relying on the live test done by @capehart. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follows up on #533: as discussed there, this is the stdlib-only version of the GitHub App support @durist started, opened separately as @Iamrodos suggested. Thanks to @durist for getting it going.
--as-apponly accepts an installation token created elsewhere, and those expire after an hour, so the README currently steers long-running backups back to a PAT. With this, github-backup creates the installation token itself from the app's private key and replaces it before it expires.How it works
--app-idand--app-private-key(a path, orfile://...) are required together. They imply--as-appand are rejected alongside-t,-f,--token-from-ghor the keychain options.parse_args()checks that the key file exists andopensslis onPATH, so setup problems are reported withparser.errorbefore any work starts.build_app_jwt()builds the JWT (iat= now - 60,exp= now + 540,iss=--app-id, so a client ID also works) and signs it withopenssl dgst -sha256 -sign.GET /users/{user}/installation, or/orgs/{org}/installationwith--organization) once per run.--app-installation-idskips the lookup.get_app_installation_token()POSTs to/app/installations/{id}/access_tokensthroughmake_request_with_retry(GHES viaget_github_api_host). It caches the token and itsexpires_atonargsand creates a new one within 10 minutes of expiry.get_auth()puts the current token intoargs.token_classic, so every existing--as-appcode path (REST, GraphQL, downloads, git URLs) works unchanged.retrieve_data()resolves auth per request instead of once per call, so a long paginated listing survives a refresh. Wiki clones rebuild their URL rather than reusing the one built before the repository clone.fetch_repository()alreadyset-urlsoriginon every run, so existing clones get the fresh URL.Trade-offs
--app-id, and nothing changes for anyone else. It's present on Linux and macOS, and Git for Windows ships it. The Docker image adds it. PyJWT plus cryptography would add the project's first runtime dependencies.-t, which works as before.get_auth(): for keychain users this runssecurityonce per page instead of once perretrieve_data()call. File-based tokens are read once and cached as before.make_request_with_retry()reuses a request's headers across its own retry sleeps. If a rate-limit sleep crosses token expiry, that one retry gets a 401. Installation tokens have higher rate limits, and fixing this would mean restructuring the retry helper, so I left it out.Testing
pytest tests/: 210 passed on Python 3.10 and 3.14.flake8with CI's flags,rst-lint README.rstand black on the changed lines are clean.tests/test_app_auth.py(24 tests): flag validation and the early checks, a real RS256 signature verified withopenssl, JWT claims, the installation lookup for users and orgs (once per run), the token request (github.com and GHES), caching, refresh near expiry, git URL format, error messages, and a paginated listing that switches tokens between pages.--cap-drop ALL --read-only. It exits cleanly with every repository backed up. With this commit's image and no--app-installation-id, a one-repository run withAPP_TOKEN_REFRESH_MARGINforced to an hour looked up the installation once, created a new token five times during the run, and GitHub accepted every one. A missing key file stops with a one-line usage error.🤖 Generated with Claude Code