Skip to content

Add support for automatic creation and refresh of Github App Installation tokens - #533

Open
durist wants to merge 4 commits into
josegonzalez:masterfrom
durist:as-app-dynamic-token
Open

durist wants to merge 4 commits into
josegonzalez:masterfrom
durist:as-app-dynamic-token

Conversation

@durist

@durist durist commented Sep 23, 2026 •

Copy link
Copy Markdown

This PR adds support for automatically creating and refreshing Github App Installation tokens. It works by creating a separate thread for token refresh. It uses the requests, jwt and pause modules. This is useful since Github App Installation tokens have higher rate limits, but expire in an hour.

@Iamrodos

Copy link
Copy Markdown
Contributor

This introduces external libraries (jwt, requests, pause), which the project has avoided so far. requirements.txt is empty, and setup.py builds install_requires from it. Since they aren't added there, they won't be installed, and because they're imported at module level, every install that upgrades to this would fail on startup, even for users not using --as-app-dynamic-token.

I have other comments, but this is the first hurdle. It's up to @josegonzalez whether starting to take on external package requirements is OK.

@josegonzalez

Copy link
Copy Markdown
Owner

I don't want to add external dependencies - part of the charm of the codebase was that we did everything with the python stdlib.

Not sure I care as much these days, but let me mull it over for a few days.

@capehart

Copy link
Copy Markdown
Contributor

Following up on the dependency question: I needed the same thing (nightly backups through a GitHub App instead of a PAT) and wrote a version that keeps the project stdlib-only, in case that helps settle it.

  • Signing: the RS256 JWT is signed by shelling out to openssl dgst -sha256 -sign, the same way the tool already shells out to git and gh. The Docker image gets openssl (one apk line). Nothing is added to requirements.txt.
  • Refresh without a thread: the installation token is cached on args, as --token-from-gh does, and get_auth() creates a new one when the cached token is within 10 minutes of its expires_at. retrieve_data() now resolves auth per request, so long paginated listings and the git remote URLs pick up the new token.
  • Flags: --app-id, --app-private-key (a path, or file://...) and --app-installation-id. They imply --as-app and are rejected alongside -t, -f, --token-from-gh or the keychain options.
  • Tests: 17 new ones, including one that signs with a freshly generated key and verifies the signature with openssl. The README "GitHub Apps" section is updated and drops the "use a PAT for long backups" note.
  • In use: it's running my backup job on a Synology NAS, from an image built by the project's own docker.yml on my fork, as an unprivileged user with all capabilities dropped. I also ran it with the refresh margin forced to an hour, so every request created a new token, and GitHub accepted all five in one run.

One trade-off worth stating: with this, the container reads the App's private key rather than a token that expires within the hour. Anyone who wants to keep the key out of the container can still mint tokens elsewhere and pass them with -t.

Separately, I found that --as-app on a user account (rather than an org) skips private repositories. That fix is independent of this and is in #539.

Branch: master...capehart:python-github-backup:github-app-private-key

@durist, you got here first, so I'm happy for you to pull this into #533 if you'd rather keep it to one PR. Otherwise I can open it separately and link back here, whichever @josegonzalez prefers.

@josegonzalez

Copy link
Copy Markdown
Owner

I am going to defer to @Iamrodos here :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants