Skip to content

Commit 9c004d3

Browse files
ramsessanchezMicrosoft Graph DevX ToolingCopilot
authored
ci: use microsoft-graph-devx-bot GitHub App instead of PAT (#802)
Pivots the weekly reference docs pipeline off the PAT-backed GITHUB_TOKEN secret and onto the existing microsoft-graph-devx-bot GitHub App identity for both the branch push and PR creation. - scripts/Generate-Github-Token.ps1 (new): mints a short-lived App installation token from the App ID + private key. - powershell-docs.yml: fetch App secrets from Key Vault and push using an App installation token (masked) instead of the PAT. - create-pr.yml: mint an App token for gh pr create; drop the redundant PAT-based git push. Co-authored-by: Microsoft Graph DevX Tooling <GraphTooling@service.microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 95ecd4c commit 9c004d3

3 files changed

Lines changed: 222 additions & 4 deletions

File tree

‎azure-pipelines/common-templates/create-pr.yml‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ steps:
1919
- task: PowerShell@2
2020
displayName: Create Pull Request for generated build
2121
env:
22-
GITHUB_TOKEN: $(GITHUB_TOKEN)
22+
GhAppId: $(microsoft-graph-devx-bot-appid)
23+
GhAppKey: $(microsoft-graph-devx-bot-privatekey)
2324
inputs:
2425
pwsh: true
2526
targetType: inline
@@ -28,6 +29,10 @@ steps:
2829
$Head = "MicrosoftDocs:${{ parameters.TargetBranch }}"
2930
$Title = "${{ parameters.Title }}"
3031
$Body = "${{ parameters.Body }}"
32+
# The microsoft-graph-devx-bot GitHub App must have pull_requests:write on this repo.
33+
$env:GITHUB_TOKEN = & "$(Build.SourcesDirectory)/scripts/Generate-Github-Token.ps1" -AppClientId $env:GhAppId -AppPrivateKeyContents $env:GhAppKey -Repository "MicrosoftDocs/microsoftgraph-docs-powershell"
34+
if ([string]::IsNullOrWhiteSpace($env:GITHUB_TOKEN)) { throw "Failed to generate GitHub App installation token." }
35+
# Mask the token so it is never surfaced in pipeline logs.
36+
Write-Host "##vso[task.setsecret]$env:GITHUB_TOKEN"
3137
git status
32-
git push "https://$(GITHUB_TOKEN)@github.com/MicrosoftDocs/microsoftgraph-docs-powershell.git"
3338
gh pr create -t $Title -b $Body -B $BaseBranchName -H $Head

‎azure-pipelines/powershell-docs.yml‎

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,21 +166,33 @@ extends:
166166
pwsh: true
167167
filePath: scripts/StabilizeMsDate.ps1
168168
errorActionPreference: 'stop'
169+
- task: AzureKeyVault@2
170+
displayName: "Azure Key Vault: Get GitHub App secrets"
171+
inputs:
172+
azureSubscription: "Federated AKV Managed Identity Connection"
173+
KeyVaultName: akv-prod-eastus
174+
SecretsFilter: "microsoft-graph-devx-bot-appid,microsoft-graph-devx-bot-privatekey"
169175
- task: PowerShell@2
170176
displayName: Pushing to github
171177
env:
172-
GITHUB_TOKEN: $(GITHUB_TOKEN)
178+
GhAppId: $(microsoft-graph-devx-bot-appid)
179+
GhAppKey: $(microsoft-graph-devx-bot-privatekey)
173180
inputs:
174181
targetType: inline
175182
pwsh: true
176183
script: |
177184
git config --global user.email "GraphTooling@service.microsoft.com"
178185
git config --global user.name "Microsoft Graph DevX Tooling"
179186
git config --system core.longpaths true
187+
# The microsoft-graph-devx-bot GitHub App must have contents:write on this repo.
188+
$token = & "$(Build.SourcesDirectory)/scripts/Generate-Github-Token.ps1" -AppClientId $env:GhAppId -AppPrivateKeyContents $env:GhAppKey -Repository "MicrosoftDocs/microsoftgraph-docs-powershell"
189+
if ([string]::IsNullOrWhiteSpace($token)) { throw "Failed to generate GitHub App installation token." }
190+
# Mask the token so it is never surfaced in pipeline logs.
191+
Write-Host "##vso[task.setsecret]$token"
180192
git status
181193
git add .
182194
git commit -m "Updating reference docs"
183-
git push --set-upstream "https://$(GITHUB_TOKEN)@github.com/MicrosoftDocs/microsoftgraph-docs-powershell.git" $(ComputeBranch.WeeklyReferenceDocsBranch)
195+
git push --set-upstream "https://x-access-token:$token@github.com/MicrosoftDocs/microsoftgraph-docs-powershell.git" $(ComputeBranch.WeeklyReferenceDocsBranch)
184196
git status
185197
- template: azure-pipelines/common-templates/create-pr.yml@self
186198
parameters:

‎scripts/Generate-Github-Token.ps1‎

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
[CmdletBinding()]
2+
param (
3+
[Parameter(Mandatory = $true)]
4+
[string]
5+
$AppClientId,
6+
[Parameter(Mandatory = $true)]
7+
[string]
8+
$AppPrivateKeyContents,
9+
[Parameter(Mandatory = $true)]
10+
[ValidatePattern('^[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+$', ErrorMessage = "Repository must be in the format 'owner/repo' (e.g. 'octocat/hello-world')")]
11+
[string]
12+
$Repository
13+
)
14+
15+
$ErrorActionPreference = "Stop"
16+
17+
function Generate-AppToken {
18+
param (
19+
[string]
20+
$ClientId,
21+
[string]
22+
$PrivateKeyContents
23+
)
24+
25+
$header = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes((ConvertTo-Json -InputObject @{
26+
alg = "RS256"
27+
typ = "JWT"
28+
}))).TrimEnd('=').Replace('+', '-').Replace('/', '_');
29+
30+
$payload = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes((ConvertTo-Json -InputObject @{
31+
iat = [System.DateTimeOffset]::UtcNow.AddSeconds(-10).ToUnixTimeSeconds()
32+
exp = [System.DateTimeOffset]::UtcNow.AddMinutes(1).ToUnixTimeSeconds()
33+
iss = $ClientId
34+
}))).TrimEnd('=').Replace('+', '-').Replace('/', '_');
35+
36+
$rsa = [System.Security.Cryptography.RSA]::Create()
37+
$rsa.ImportFromPem($PrivateKeyContents)
38+
39+
$signature = [Convert]::ToBase64String($rsa.SignData([System.Text.Encoding]::UTF8.GetBytes("$header.$payload"), [System.Security.Cryptography.HashAlgorithmName]::SHA256, [System.Security.Cryptography.RSASignaturePadding]::Pkcs1)).TrimEnd('=').Replace('+', '-').Replace('/', '_')
40+
$jwt = "$header.$payload.$signature"
41+
42+
return $jwt
43+
}
44+
45+
function Generate-InstallationToken {
46+
param (
47+
[string]
48+
$AppToken,
49+
[string]
50+
$InstallationId,
51+
[string]
52+
$Repository
53+
)
54+
55+
$uri = "https://api.github.com/app/installations/$InstallationId/access_tokens"
56+
$headers = @{
57+
Authorization = "Bearer $AppToken"
58+
Accept = "application/vnd.github+json"
59+
"X-GitHub-Api-Version" = "2022-11-28"
60+
}
61+
62+
$body = @{
63+
repositories = @($Repository)
64+
}
65+
66+
$response = Invoke-RestMethod -Uri $uri -Method Post -Headers $headers -Body (ConvertTo-Json -InputObject $body -Compress -Depth 10)
67+
68+
return $response.token
69+
}
70+
71+
72+
function Get-OrganizationInstallationId {
73+
param (
74+
[string]
75+
$AppToken,
76+
[string]
77+
$Organization
78+
)
79+
80+
$uri = "https://api.github.com/orgs/$Organization/installation"
81+
$headers = @{
82+
Authorization = "Bearer $AppToken"
83+
Accept = "application/vnd.github+json"
84+
"X-GitHub-Api-Version" = "2022-11-28"
85+
}
86+
87+
try {
88+
$response = Invoke-RestMethod -Uri $uri -Method Get -Headers $headers
89+
90+
return $response.id
91+
}
92+
catch [Microsoft.PowerShell.Commands.HttpResponseException] {
93+
if ($_.Exception.Response.StatusCode -eq [System.Net.HttpStatusCode]::UnprocessableContent -or $_.Exception.Response.StatusCode -eq [System.Net.HttpStatusCode]::NotFound) {
94+
return $null
95+
}
96+
97+
throw
98+
}
99+
}
100+
101+
function Get-RepositoryInstallationId {
102+
param (
103+
[string]
104+
$AppToken,
105+
[string]
106+
$Repository
107+
)
108+
109+
$uri = "https://api.github.com/repos/$Repository/installation"
110+
$headers = @{
111+
Authorization = "Bearer $AppToken"
112+
Accept = "application/vnd.github+json"
113+
"X-GitHub-Api-Version" = "2022-11-28"
114+
}
115+
116+
try {
117+
$response = Invoke-RestMethod -Uri $uri -Method Get -Headers $headers
118+
119+
return $response.id
120+
}
121+
catch [Microsoft.PowerShell.Commands.HttpResponseException] {
122+
if ($_.Exception.Response.StatusCode -eq [System.Net.HttpStatusCode]::UnprocessableContent -or $_.Exception.Response.StatusCode -eq [System.Net.HttpStatusCode]::NotFound) {
123+
return $null
124+
}
125+
126+
throw
127+
}
128+
}
129+
130+
function Get-UserInstallationId {
131+
param (
132+
[string]
133+
$AppToken,
134+
[string]
135+
$Username
136+
)
137+
138+
$uri = "https://api.github.com/users/$Username/installation"
139+
$headers = @{
140+
Authorization = "Bearer $AppToken"
141+
Accept = "application/vnd.github+json"
142+
"X-GitHub-Api-Version" = "2022-11-28"
143+
}
144+
145+
try {
146+
$response = Invoke-RestMethod -Uri $uri -Method Get -Headers $headers
147+
148+
return $response.id
149+
}
150+
catch [Microsoft.PowerShell.Commands.HttpResponseException] {
151+
if ($_.Exception.Response.StatusCode -eq [System.Net.HttpStatusCode]::UnprocessableContent -or $_.Exception.Response.StatusCode -eq [System.Net.HttpStatusCode]::NotFound) {
152+
return $null
153+
}
154+
155+
throw
156+
}
157+
}
158+
159+
function Get-InstallationId {
160+
param (
161+
[string]
162+
$AppToken,
163+
[string]
164+
$Owner,
165+
[string]
166+
$Repo
167+
)
168+
169+
$orgInstallationId = Get-OrganizationInstallationId -AppToken $AppToken -Organization $Owner
170+
171+
if ($null -eq $orgInstallationId) {
172+
$repoInstallationId = Get-RepositoryInstallationId -AppToken $AppToken -Repository "$Owner/$Repo"
173+
}
174+
else {
175+
return $orgInstallationId
176+
}
177+
178+
if ($null -eq $repoInstallationId) {
179+
$userInstallationId = Get-UserInstallationId -AppToken $AppToken -Username $Owner
180+
}
181+
else {
182+
return $repoInstallationId
183+
}
184+
185+
if ($null -eq $userInstallationId) {
186+
throw "Installation not found for repository '$Repo'"
187+
}
188+
else {
189+
return $userInstallationId
190+
}
191+
}
192+
193+
$owner, $repo = $Repository -split '/'
194+
195+
$AppToken = Generate-AppToken -ClientId $AppClientId -PrivateKeyContents $AppPrivateKeyContents
196+
197+
$InstallationId = Get-InstallationId -AppToken $AppToken -Owner $owner -Repo $repo
198+
199+
$InstallationToken = Generate-InstallationToken -AppToken $AppToken -InstallationId $InstallationId -Repository $repo
200+
201+
Write-Output $InstallationToken

0 commit comments

Comments
 (0)