By default (when using the default secrets.GITHUB_TOKEN
) GitHub Actions workflows can't trigger other workflows. So for example if a workflow sends a pull request to a repo that has a CI workflow that normally runs the tests on pull requests, the CI workflow won't run for a pull request that was sent by another workflow.
There are probably lots of other GitHub API actions that a workflow authenticating with the default secrets.GITHUB_TOKEN
can't take either.
How can I authenticate my workflow runs as a GitHub App, so that they can trigger other workfows and take any other actions that I grant the GitHub App permissions for?
The GitHub docs linked above recommend authenticating workflows using a personal access token (PAT) to allow them to trigger other workflows, but PATs have some downsides:
GitHub Apps offer the best balance of convenience and security for authenticating workflows: apps can have fine-grained permissions and they can be installed only in individual repos or in all of a user or organization's repos (including automatically installing the app in new repos when they're created). Apps also get a nice page where you can type in some docs (example), the app's avatar and username on PRs, issues, etc link to this page. Apps are also clearly labelled as "bot" on any PRs, issues, etc that they create.
This third-party documentation is a good summary of the different ways of authenticating workflows and their pros and cons.
There are guides out there on the internet that will tell you how to authenticate a workflow as an app but they all tell you to use third-party actions (from the marketplace) to do the necessary token exchange with the GitHub API. I don't want to do this because it requires sending my app's private key to a third-party action. I'd rather write (or copy-paste) my own code to do the token exchange.
Links:
ci.yml
workflow was automatically triggered on the PR)To create a GitHub App and a workflow that authenticates as that app and sends PRs that can trigger other workflows:
Create a GitHub App in your user or organization account. Take note of your app's App ID which is shown on your app's settings page, you'll need it later.
Grant your app the necessary permissions. To send pull requests an app will probably need:
Generate a private key for your app.
Store a copy of the private key in a GitHub Actions secret named MY_GITHUB_APP_PRIVATE_KEY
. This can either be a repo-level secret in the repo that will contain the workflow that you're going to write, or it can be a user- or organization-level secret in which case the repo that will contain the workflow will need access to the secret.
Install your GitHub app in your repo or user or organization account. Take note of the Installation ID. This is the 8-digit number that is at the end of the URL of the installation's page in the settings of the repo, user or organization where you installed the app. You'll need this later.
A workflow run that wants to authenticate as your app needs to get a temporary installation access token each time it runs, and use that token to authenticate itself. This is called authenticating as an installation in GitHub's docs and they give a code example in Ruby. The steps are:
iat
("issued at" time) 60s in the past, an exp
(expiration time) 10m in the future, and your App ID as the iss
(issuer), and sign the token using your private key and the RS256 algorithm.https://api.github.com/app/installations/:installation_id/access_tokens
(replacing :installation_id
with your installation ID) with the signed JWT in an Authorization: Bearer <SIGNED_JWT>
header.Here's a Python script that implements this token exchange using PyJWT and requests (you'll need to install pyjwt
with the cryptography
dependency: pip install pyjwt[crypto]
).
from argparse import ArgumentParser
from datetime import datetime, timedelta, timezone
import jwt
import requests
def get_token(app_id, private_key, installation_id):
payload = {
"iat": datetime.now(tz=timezone.utc) - timedelta(seconds=60),
"exp": datetime.now(tz=timezone.utc) + timedelta(minutes=10),
"iss": app_id,
}
encoded_jwt = jwt.encode(payload, private_key, algorithm="RS256")
response = requests.post(
f"https://api.github.com/app/installations/{installation_id}/access_tokens",
headers={
"Accept": "application/vnd.github+json",
"Authorization": f"Bearer {encoded_jwt}",
},
timeout=60,
)
return response.json()["token"]
def cli():
parser = ArgumentParser()
parser.add_argument("--app-id", required=True)
parser.add_argument("--private-key", required=True)
parser.add_argument("--installation-id", required=True)
args = parser.parse_args()
token = get_token(args.app_id, args.private_key, args.installation_id)
print(token)
if __name__ == "__main__":
cli()
https://github.com/hypothesis/gha-token is a version of the above code as an installable Python package. To install it with pipx and get a token:
$ pipx install git+https://github.com/hypothesis/gha-token.git
$ gha-token --app-id $APP_ID --installation-id $INSTALLATION_ID --private-key $PRIVATE_KEY
ghs_xyz***
You can write a workflow that uses gha-token to get a token and authenticate any API requests or GitHub CLI calls made by the workflow. The workflow below will:
gha-token
gha-token
to get an installation access token using the App ID, Installation ID, and MY_GITHUB_APP_PRIVATE_KEY
GitHub secret that you created earliergit push
and gh pr create
(GitHub CLI) commands to push a branch and open a PRCopy the workflow below to a file named .github/workflows/send-pr.yml
in your repo. Replace <APP_ID>
with your App ID and replace <INSTALLATION_ID>
with your Installation ID:
name: Send a Pull Request
on:
workflow_dispatch:
jobs:
my_job:
name: Send a Pull Request
runs-on: ubuntu-latest
steps:
- name: Install Python
uses: actions/setup-python@v4
with:
python-version: "3.11"
- name: Install gha-token
run: python3.11 -m pip install "git+https://github.com/hypothesis/gha-token.git"
- name: Get GitHub token
id: github_token
run: echo GITHUB_TOKEN=$(gha-token --app-id <APP_ID> --installation-id <INSTALLATION_ID> --private-key "$PRIVATE_KEY") >> $GITHUB_OUTPUT
env:
PRIVATE_KEY: ${{ secrets.MY_GITHUB_APP_PRIVATE_KEY }}
- name: Checkout repo
uses: actions/checkout@v3
with:
token: ${{ steps.github_token.outputs.GITHUB_TOKEN }}
- name: Make some automated changes
run: echo "Automated changes" >> README.md
- name: Configure git
run: |
git config --global user.name "send-pr.yml workflow"
git config --global user.email "<>"
- name: Switch to a branch
run: git switch --force-create my-branch main
- name: Commit the changes
run: git commit README.md -m "Automated commit"
- name: Push branch
run: git push --force origin my-branch:my-branch
- name: Open PR
run: gh pr create --fill
env:
GITHUB_TOKEN: ${{ steps.github_token.outputs.GITHUB_TOKEN }}