There's a couple of steps needed for this: firstly you need to get some information from GitHub by hand, and then there is a little dance that your app needs to do to swap its authentication secrets for a temporary code that can be used to authenticate a git clone.
Gather information

Before you can write a function to do this, you need three pieces of information, all of which are available from the App's settings. To get there
- Go to the organization you have created the App for
- Go to
Settings > Developer Settings > GitHub Apps
- Click
Edit next to the name of the App you're using, and authenticate with 2FA
The three pieces of information you need are:
The App ID

This is in the General page, in the About section at the top.
The Installation ID

If you haven't already, you also need to install the App into the Organization. Once this is done, go back to the Install App page in the App settings, and copy the link for the installation settings. Paste it into your editor and get the number from the end. The link should have the form https://github.com/apps/{app_name}/installations/{installation_id}; the part after the last / is the installation ID.
(If you have multiple installations of your app, there may be a way to get this programmatically; I haven't looked into this as I didn't need it for my use case.)
PEM file

This is how you prove to GitHub that you are in control of the App. Go back to the General page in the App settings, and scroll down to the Private keys section. Click the Generate a private key button; this will immediately generate a .pem file and download it to your machine.
Do not commit this to your repository unless you want everyone who can see the repository to be able to authenticate to GitHub as you.
The code
Once you have these three things, the steps you need in code are:
- Load your PEM
- Use the PEM to create a JSON Web Token that will authenticate your API call
- Call the GitHub API to get an installation token
- (Use the installation token to clone the repository of interest.)
Get the installation token
Code to do the first three steps could look like this:
from datetime import datetime
import jwt
import requests
def get_installation_access_token(
pem_filename: str, app_id: str, installation_id: str
) -> str:
"""
Obtain and return a GitHub installation access token.
Arguments:
pem_filename: Filename of a PEM file generated by GitHub to
authenticate as the installed app.
app_id: The application ID
installation_id: The ID of the app installation.
Returns:
The installation access token obtained from GitHub.
"""
# With thanks to https://github.com/orgs/community/discussions/48186
now = int(datetime.now().timestamp())
with open(pem_filename, "rb") as pem_file:
signing_key = jwt.jwk_from_pem(pem_file.read())
payload = {"iat": now, "exp": now + 600, "iss": app_id}
jwt_instance = jwt.JWT()
encoded_jwt = jwt_instance.encode(payload, signing_key, alg="RS256")
response = requests.post(
"https://api.github.com/app/installations/" f"{installation_id}/access_tokens",
headers={
"Authorization": f"Bearer {encoded_jwt}",
"Accept": "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
},
)
if not 200 <= response.status_code < 300:
raise RuntimeError(
"Unable to get token. Status code was "
f"{response.status_code}, body was {response.text}."
)
return response.json()["token"]
Pass in the information collected above as the three parameters to the function. Note that this depends on the jwt and requests packages, both available under those names from pip.
This will give an installation token that is valid for an hour. (This is much less time than the PEM file is valid, because it has a lot less security. That's the reason this dance is needed—you're trading something pretty secure for something that is less secure but easier to use with git clone; because it's less secure, it has to be time limited instead to reduce the chance of it getting stolen.)
Clone the repository
Assuming that you have a repository URL in the form
repo_url = https://github.com/organization/repository_name
then you can clone the repository as:
import git
if not original_url.startswith("https://"):
raise ValueError("Need an HTTPS URL")
auth_url = f"https://x-access-token:{token}@{original_url[8:]}"
git.Repo.clone_from(
auth_url,
deployment["tempdir_path"] / "repo",
branch="deployment",
)
Here I've used the GitPython library for Python. Equivalently, you could use the shell command
$ git clone https://x-access-token:${TOKEN}@github.com/organization/repository_name
where ${TOKEN} contains the result of calling the above Python function.
Credits
Many thanks to loujr on the GitHub Community for the guide that eventually clued me into how to do this. I've stripped out the need to use command-line arguments and to manually pass the JWT into curl, instead keeping everything in Python.