Automating GitHub operations using GitHub Apps
GitHub Apps
At the company I work at, InSpark, we had a use case for automating our operations on the GitHub platform. We needed to perform automated actions in a headless fashion, so an external process on Azure should be able to call the GitHub Platform and perform some actions. To do so, I learned about GitHub Apps, which enable me to perform actions from a script. In this blog, I will share what I’ve learned and how you can utilize a daemon application to automate actions on the GitHub platform.
Why
In some cases, you need to be able to initiate a commit of specific files, address GitHub issues, or trigger a workflow dispatch event.
We were researching how to automate the enrollment process for a new customer of one of our company’s services. Enrollment meant that, through a web portal, a customer would enter information, and, with the press of a button, the portal would initiate a single request to my tool, triggering a chain of operations.
I had a wishlist of operations on the GitHub platform for my tool:
- Create a repository secret on the repository.
- Copy template folder structure
- Create new files in the new folder.
- Create a new workflow.
- Start a workflow with information from the customer’s web request.
Platform
With my PowerShell background, I began exploring GitHub scripts and soon discovered that they did not precisely meet my use case. I then read the REST API reference (GitHub REST API documentation - GitHub Docs), which showed that the REST API was a better fit for the use case. I would like to know what platform to use to interact with the REST API from a PowerShell script. The host running my PowerShell must be fast, available 24/7, and easily extendable, which ruled out Azure Automation. I had yet to gain experience using Azure Functions, though.
I created an Azure Function App and was puzzled by the authentication. Will I use a Personal Access Token (PAT) and attribute every action to my account, or do it in a more sophisticated way? In Azure DevOps, service accounts (users) with PATs were once a workaround for the lack of SPN support.
PATs are always user-account bound, sometimes scoped, and expire when you don’t want them to. If I win the lottery someday, I hope my colleagues don’t pull too many hairs out on the search for which PAT expired. So, authentication-wise, I wanted something else for my new shiny project.
Entra ID SPNs can now integrate with Azure DevOps, and it works excellently Managed identity and service principal support for Azure DevOps now in general availability (GA). This video by John Savill explains it in detail: Azure DevOps Workload Identity Federation with Azure Overview. NO MORE SECRETS!
GitHub App
I looked to see if GitHub supported an extension from Microsoft Entra ID, meaning I hoped to invite or register an Entra ID SPN to perform these actions instead of everything attributed to my GitHub user with a PAT. Unfortunately, the GitHub platform doesn’t support Entra ID SPNs as an entity; however, GitHub has its own SPN implementation on its platform, known as a GitHub App.
I found documentation and started reading the GitHub documentation. GitHub Apps are identities or apps that can act independently, rather than on behalf of a user (this is also possible, but it is not my use case). In my use case, a PowerShell script can authenticate as the GitHub App and call the GitHub REST API with that identity.
First, we needed to register a new GitHub App, which you can read about here: Registering a GitHub App - GitHub Docs.
- I gave my GitHub app a name.
- I gave the GitHub app a homepage URL (a reference to our company).
- I didn’t need anything related to users. Therefore, I removed all content related to user flows and users.
- There was no need for feedback on the PoC, so the webhook feedback was out, too.
Based on the REST API documentation, I had a good idea of what repository permissions we required, and the role permissions are excellent. Therefore, I selected the required permissions, adhering to the principle of least privilege, while also considering the REST API’s documented requirements. From there, we have a GitHub App ready to be installed on our repositories. After registering, GitHub automatically forwards you to the overview of your GitHub App. Lastly, I chose to install the GitHub App only on a demo organization. If you’d like to use this in your organization, you will need to delegate the GitHub App to a set of repositories. It will then have the permissions you configured earlier on those repositories.
Authentication
The overview shows you details like “Owned by” (your organization or user, depending on where you created it), your “App-ID”, and a “Client ID”. This App ID is essential because you’ll need it later.
GitHub has a 2 step authentication. Authentication to the GitHub Platform as the GitHub App, as documented by GitHub:
To authenticate as itself, the app will use a JSON Web Token (JWT). Your app should authenticate as itself when it needs to generate an installation access token. An installation access token is required to authenticate as an app installation. Your app should also authenticate as itself when it needs to make API requests to manage resources related to the app.
First, we need to write some code to generate a JWT. With that JWT, we can authenticate to GitHub, request an installation token, and then make API requests. However, I had yet to learn what an installation token is.
I had to look up the meaning of ‘installation’ in this context; the installation is the registration of the app in a GitHub organization. The installation ID is the identifier of that installation. GitHub gives the following guidance to find the Installation ID: You can also use the REST API to find the installation ID for an installation of your app. For example, you can get an installation ID with the GET /users/{username}/installation, GET /repos/{owner}/{repo}/installation, GET /orgs/{org}/installation, or GET /app/installations endpoints.
Secondly, you’ll need to create an installation token to authenticate with that organization and retrieve the access_tokens_url token.

In short:
- Create a JWT and send a request to
https://api.github.com/app/installations
,/orgs/{org}/installation
or/repos/{owner}/{repo}/installation
(make sure to use least privilege) - Find the correct installation in the response and its access_tokens_url
- Call the access_tokens_url and receive an installation token
- Call all the repo APIs
Creating a JWT
Creating JWTs may sound more intimidating than it is. It’s just a base64 URL-encoded representation of a JSON string, accompanied by a signature. To sign your JWT, you can download a private key to sign your JWT. Fortunately, a PowerShell module author has already taken into account all of these considerations. I tried using powershell-jwt, which worked great. Note: The module’s implementation requires having the private key file of my GitHub App locally on the host, where I am running the PowerShell code.
1install-PSResource 'powershell-jwt' -Repository PSGallery | Import-Module
2
3$newjwtSplat = @{
4 Algorithm = 'RS256'
5 SecretKey = (Get-Content /Users/christianpiet/Downloads/manbearpiet-app.2025-06-25.private-key.pem -AsByteStream) # This accepts a byte-array
6 ExpiryTimestamp = ([math]::Round((Get-Date -UFormat %s)) + (8 * 60))
7 Issuer = 1459729 # Trimmed
8 PayloadClaims = @{
9 iat = ([math]::Round((Get-Date -UFormat %s)) - 10)
10 }
11}
12
13$JWT = New-JWT @newjwtSplat
With a locally stored private key, you can create your JWTs. Very cool, right?
With this JWT, we could request an installation token:
1function New-GHRepoToken {
2 [CmdletBinding(SupportsShouldProcess)]
3 param (
4 [Parameter()]
5 [String]
6 $JWT
7 )
8
9 if ($PSCmdlet.ShouldProcess('Requesting GitHub App Installations using Signed JWT Token')) {
10
11 $invokeRestMethodSplat = @{
12 Uri = 'https://api.github.com/app/installations' # This gets all installations and should just be used cross organizations.
13 Headers = @{
14 Accept = 'application/vnd.github+json'
15 Authorization = "Bearer $JWT"
16 'X-GitHub-Api-Version' = '2022-11-28'
17 }
18 }
19 try {
20 $Installations = Invoke-RestMethod @invokeRestMethodSplat
21 if (!$Installations) {
22 throw 'Could not get installations with JWT'
23 }
24 } catch {
25 $PSCmdlet.ThrowTerminatingError($_)
26 }
27 }
28
29 $AccessTokensUrl = ($Installations).access_tokens_url # This assumes there is only 1 installation
30
31 if ($PSCmdlet.ShouldProcess('Requesting GitHub Repo token using JWT')) {
32
33 $irmSplat = @{
34 Uri = $AccessTokensUrl
35 Method = 'Post'
36 Headers = @{
37 Accept = 'application/vnd.github+json'
38 Authorization = "Bearer $JWT"
39 'X-GitHub-Api-Version' = '2022-11-28'
40 }
41 }
42 try {
43 $outputtoken = Invoke-RestMethod @irmSplat
44 if (!$outputtoken) {
45 throw 'Could not get GitHub Repo token'
46 }
47 } catch {
48 $PSCmdlet.ThrowTerminatingError($_)
49 }
50 }
51 $outputtoken # for reflection, this has the token as plaintext as a property
52}
53
54$installationToken = New-GHRepoToken -JWT $JWT
This $installationToken
-variable has our wanted installation token, which works similarly to a short-lived PAT.
Finally, we can call the REST API as we’re used to:
1$Organization = 'Manbearpiet'
2$RepositoryName = 'Manbearpiet' # Public repo
3$Path = 'README.md'
4
5$file = @{
6 Uri = "https://api.github.com/repos/$Organization/$RepositoryName/contents/$Path"
7 Method = 'Get'
8 Headers = @{
9 Accept = 'application/vnd.github+json'
10 Authorization = "Bearer $($installationToken.token)"
11 'X-GitHub-Api-Version' = '2022-11-28'
12 }
13}
14Invoke-RestMethod @file
15
16$Organization = 'ManbearpietApp'
17$RepositoryName = 'thisisprivate' # Private repo
18$file = @{
19 Uri = "https://api.github.com/repos/$Organization/$RepositoryName/contents/$Path"
20 Method = 'Get'
21 Headers = @{
22 Accept = 'application/vnd.github+json'
23 Authorization = "Bearer $($installationToken.token)"
24 'X-GitHub-Api-Version' = '2022-11-28'
25 }
26}
27
28Invoke-RestMethod @file


In the content key of the response body we can find our private repositories it’s README.md contents base64 encoded:
IyB0aGlzaXNwcml2YXRlCuKAnE9uZSBkb2VzIG5vdCBzaW1wbHkgYWNjZXNzIGEgcHJpdmF0ZSByZXBvIHdpdGhvdXQgYSB0b2tlbi7igJ0g4oCTIEJvcm9taXIK
With this setup I can call my installations and retrieve a short-lived installation token. Using that I can access our repositories, start workflows et cetera. AWESOME! For convenience I added my code to a GitHub Gist.
Conclusion
Using GitHub Apps, we have an identity that allows us to automate actions on the GitHub platform. After registering and installing a GitHub App, you can assign it access to repositories or the organization and access the REST API. By fetching and accessing the installation token, you can access your public and private repositories through GitHub REST APIs. So we can continue to automate all the things. That’s my adventure with GitHub Apps. I hope someone can utilize this to do cool stuff with it. I certainly enjoyed tinkering with it.