From af6312dc8626616f0849dff2e4f78df83da00255 Mon Sep 17 00:00:00 2001 From: ramsessanchez <63934382+ramsessanchez@users.noreply.github.com> Date: Fri, 10 Jul 2026 15:16:05 -0700 Subject: [PATCH 1/2] ci: authenticate weekly generation pipelines with GitHub App instead of PAT Mint a short-lived GitHub App installation token from Key Vault (akv-prod-eastus secrets microsoft-graph-devx-bot-appid / -privatekey via the "Federated AKV Managed Identity Connection") and use it for git push and gh pr create, replacing the PAT GITHUB_TOKEN in the weekly-generation, weekly-examples-update, and command-metadata-refresh pipelines. Adds scripts/Generate-Github-Token.ps1 and the reusable common-templates/get-github-app-token.yml, inserted before every push/PR step. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b1e4dbfd-ffcd-4a3e-bd35-1632b2912252 --- .azure-pipelines/command-metadata-refresh.yml | 1 + .../common-templates/create-pr.yml | 2 + .../download-openapi-docs.yml | 2 + .../common-templates/get-github-app-token.yml | 45 ++++ .../generate-command-metadata.yml | 2 + .azure-pipelines/weekly-examples-update.yml | 1 + scripts/Generate-Github-Token.ps1 | 213 ++++++++++++++++++ 7 files changed, 266 insertions(+) create mode 100644 .azure-pipelines/common-templates/get-github-app-token.yml create mode 100644 scripts/Generate-Github-Token.ps1 diff --git a/.azure-pipelines/command-metadata-refresh.yml b/.azure-pipelines/command-metadata-refresh.yml index 647b4a9e3db..22931e33da6 100644 --- a/.azure-pipelines/command-metadata-refresh.yml +++ b/.azure-pipelines/command-metadata-refresh.yml @@ -137,6 +137,7 @@ extends: targetType: inline script: | . "$(System.DefaultWorkingDirectory)\tools\Versions\BumpModuleVersion.ps1" -BumpV1Module -BumpBetaModule -BumpAuthModule -Debug + - template: .azure-pipelines/common-templates/get-github-app-token.yml@self - task: Bash@3 displayName: Push version bump changes env: diff --git a/.azure-pipelines/common-templates/create-pr.yml b/.azure-pipelines/common-templates/create-pr.yml index 7c572b93c23..99b02c20f78 100644 --- a/.azure-pipelines/common-templates/create-pr.yml +++ b/.azure-pipelines/common-templates/create-pr.yml @@ -16,6 +16,8 @@ parameters: default: "" steps: + - template: ./get-github-app-token.yml + - task: PowerShell@2 displayName: Create Pull Request for generated build env: diff --git a/.azure-pipelines/common-templates/download-openapi-docs.yml b/.azure-pipelines/common-templates/download-openapi-docs.yml index 15d7475e125..c814b5c29ea 100644 --- a/.azure-pipelines/common-templates/download-openapi-docs.yml +++ b/.azure-pipelines/common-templates/download-openapi-docs.yml @@ -78,6 +78,8 @@ steps: script: dotnet run workingDirectory: "$(System.DefaultWorkingDirectory)/tools/OpenApiInfoGenerator/OpenApiInfoGenerator" + - template: ./get-github-app-token.yml + - task: Bash@3 displayName: Commit downloaded files condition: and(succeeded(), ne(variables['ModuleGenerationList'], '')) diff --git a/.azure-pipelines/common-templates/get-github-app-token.yml b/.azure-pipelines/common-templates/get-github-app-token.yml new file mode 100644 index 00000000000..8edd64f2c50 --- /dev/null +++ b/.azure-pipelines/common-templates/get-github-app-token.yml @@ -0,0 +1,45 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +# Fetches the Microsoft Graph DevX GitHub App credentials from Key Vault and mints a short-lived +# installation access token, exposing it to subsequent steps as the secret variable GITHUB_TOKEN. +# +# Include this template immediately before any step that pushes to GitHub or creates a pull +# request. Downstream steps continue to reference $(GITHUB_TOKEN) exactly as before, but the value +# is now a GitHub App installation token instead of a long-lived PAT. Because installation tokens +# expire after ~1 hour, generate the token right before it is used rather than once per job. + +parameters: + - name: RepoName + type: string + default: microsoftgraph/msgraph-sdk-powershell + +steps: + - task: AzureKeyVault@2 + displayName: "Azure Key Vault: Get GitHub App secrets" + inputs: + azureSubscription: "Federated AKV Managed Identity Connection" + KeyVaultName: akv-prod-eastus + SecretsFilter: "microsoft-graph-devx-bot-appid,microsoft-graph-devx-bot-privatekey" + + - task: PowerShell@2 + displayName: "Generate GitHub App installation token" + env: + GhAppId: $(microsoft-graph-devx-bot-appid) + GhAppKey: $(microsoft-graph-devx-bot-privatekey) + inputs: + pwsh: true + targetType: inline + errorActionPreference: stop + script: | + $token = & "$(System.DefaultWorkingDirectory)/scripts/Generate-Github-Token.ps1" ` + -AppClientId $env:GhAppId ` + -AppPrivateKeyContents $env:GhAppKey ` + -Repository "${{ parameters.RepoName }}" + if ([string]::IsNullOrWhiteSpace($token)) { + throw "Failed to generate GitHub App installation token (empty result)." + } + # Mask the token so it can never surface in pipeline logs, then expose it to later steps. + Write-Host "##vso[task.setsecret]$token" + Write-Host "##vso[task.setvariable variable=GITHUB_TOKEN;issecret=true]$token" + Write-Host "Generated GitHub App installation token and set GITHUB_TOKEN." diff --git a/.azure-pipelines/generation-templates/generate-command-metadata.yml b/.azure-pipelines/generation-templates/generate-command-metadata.yml index 2de3074be8d..27911f97f0a 100644 --- a/.azure-pipelines/generation-templates/generate-command-metadata.yml +++ b/.azure-pipelines/generation-templates/generate-command-metadata.yml @@ -17,6 +17,8 @@ steps: script: | . $(System.DefaultWorkingDirectory)/tools/PostGeneration/AuthModuleMetadata.ps1 + - template: ../common-templates/get-github-app-token.yml + - task: Bash@3 displayName: Push command metadata enabled: true diff --git a/.azure-pipelines/weekly-examples-update.yml b/.azure-pipelines/weekly-examples-update.yml index 1aa080b88d6..674b08bb651 100644 --- a/.azure-pipelines/weekly-examples-update.yml +++ b/.azure-pipelines/weekly-examples-update.yml @@ -82,6 +82,7 @@ extends: targetType: 'filePath' pwsh: true filePath: tools\ImportExamples.ps1 + - template: .azure-pipelines/common-templates/get-github-app-token.yml@self - task: PowerShell@2 displayName: Pushing to github env: diff --git a/scripts/Generate-Github-Token.ps1 b/scripts/Generate-Github-Token.ps1 new file mode 100644 index 00000000000..192feea76f1 --- /dev/null +++ b/scripts/Generate-Github-Token.ps1 @@ -0,0 +1,213 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +# Generates a short-lived GitHub App installation access token for the specified repository. +# +# The token is minted by: +# 1. Building an RS256-signed JWT from the App's client id and private key. +# 2. Resolving the App installation id for the owner (org, then repo, then user scope). +# 3. Exchanging the JWT for a repository-scoped installation access token. +# +# The resulting token is valid for ~1 hour and should be generated immediately before it is +# used (e.g. right before a git push or PR creation). Emitted on the pipeline via Write-Output. + +[CmdletBinding()] +param ( + [Parameter(Mandatory = $true)] + [string] + $AppClientId, + [Parameter(Mandatory = $true)] + [string] + $AppPrivateKeyContents, + [Parameter(Mandatory = $true)] + [ValidatePattern('^[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+$', ErrorMessage = "Repository must be in the format 'owner/repo' (e.g. 'octocat/hello-world')")] + [string] + $Repository +) + +$ErrorActionPreference = "Stop" + +function Generate-AppToken { + param ( + [string] + $ClientId, + [string] + $PrivateKeyContents + ) + + $header = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes((ConvertTo-Json -InputObject @{ + alg = "RS256" + typ = "JWT" + }))).TrimEnd('=').Replace('+', '-').Replace('/', '_'); + + $payload = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes((ConvertTo-Json -InputObject @{ + iat = [System.DateTimeOffset]::UtcNow.AddSeconds(-10).ToUnixTimeSeconds() + exp = [System.DateTimeOffset]::UtcNow.AddMinutes(1).ToUnixTimeSeconds() + iss = $ClientId + }))).TrimEnd('=').Replace('+', '-').Replace('/', '_'); + + $rsa = [System.Security.Cryptography.RSA]::Create() + $rsa.ImportFromPem($PrivateKeyContents) + + $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('/', '_') + $jwt = "$header.$payload.$signature" + + return $jwt +} + +function Generate-InstallationToken { + param ( + [string] + $AppToken, + [string] + $InstallationId, + [string] + $Repository + ) + + $uri = "https://api.github.com/app/installations/$InstallationId/access_tokens" + $headers = @{ + Authorization = "Bearer $AppToken" + Accept = "application/vnd.github+json" + "X-GitHub-Api-Version" = "2022-11-28" + } + + $body = @{ + repositories = @($Repository) + } + + $response = Invoke-RestMethod -Uri $uri -Method Post -Headers $headers -Body (ConvertTo-Json -InputObject $body -Compress -Depth 10) + + return $response.token +} + +function Get-OrganizationInstallationId { + param ( + [string] + $AppToken, + [string] + $Organization + ) + + $uri = "https://api.github.com/orgs/$Organization/installation" + $headers = @{ + Authorization = "Bearer $AppToken" + Accept = "application/vnd.github+json" + "X-GitHub-Api-Version" = "2022-11-28" + } + + try { + $response = Invoke-RestMethod -Uri $uri -Method Get -Headers $headers + + return $response.id + } + catch [Microsoft.PowerShell.Commands.HttpResponseException] { + if ($_.Exception.Response.StatusCode -eq [System.Net.HttpStatusCode]::UnprocessableContent -or $_.Exception.Response.StatusCode -eq [System.Net.HttpStatusCode]::NotFound) { + return $null + } + + throw + } +} + +function Get-RepositoryInstallationId { + param ( + [string] + $AppToken, + [string] + $Repository + ) + + $uri = "https://api.github.com/repos/$Repository/installation" + $headers = @{ + Authorization = "Bearer $AppToken" + Accept = "application/vnd.github+json" + "X-GitHub-Api-Version" = "2022-11-28" + } + + try { + $response = Invoke-RestMethod -Uri $uri -Method Get -Headers $headers + + return $response.id + } + catch [Microsoft.PowerShell.Commands.HttpResponseException] { + if ($_.Exception.Response.StatusCode -eq [System.Net.HttpStatusCode]::UnprocessableContent -or $_.Exception.Response.StatusCode -eq [System.Net.HttpStatusCode]::NotFound) { + return $null + } + + throw + } +} + +function Get-UserInstallationId { + param ( + [string] + $AppToken, + [string] + $Username + ) + + $uri = "https://api.github.com/users/$Username/installation" + $headers = @{ + Authorization = "Bearer $AppToken" + Accept = "application/vnd.github+json" + "X-GitHub-Api-Version" = "2022-11-28" + } + + try { + $response = Invoke-RestMethod -Uri $uri -Method Get -Headers $headers + + return $response.id + } + catch [Microsoft.PowerShell.Commands.HttpResponseException] { + if ($_.Exception.Response.StatusCode -eq [System.Net.HttpStatusCode]::UnprocessableContent -or $_.Exception.Response.StatusCode -eq [System.Net.HttpStatusCode]::NotFound) { + return $null + } + + throw + } +} + +function Get-InstallationId { + param ( + [string] + $AppToken, + [string] + $Owner, + [string] + $Repo + ) + + $orgInstallationId = Get-OrganizationInstallationId -AppToken $AppToken -Organization $Owner + + if ($null -eq $orgInstallationId) { + $repoInstallationId = Get-RepositoryInstallationId -AppToken $AppToken -Repository "$Owner/$Repo" + } + else { + return $orgInstallationId + } + + if ($null -eq $repoInstallationId) { + $userInstallationId = Get-UserInstallationId -AppToken $AppToken -Username $Owner + } + else { + return $repoInstallationId + } + + if ($null -eq $userInstallationId) { + throw "Installation not found for repository '$Repo'" + } + else { + return $userInstallationId + } +} + +$owner, $repo = $Repository -split '/' + +$AppToken = Generate-AppToken -ClientId $AppClientId -PrivateKeyContents $AppPrivateKeyContents + +$InstallationId = Get-InstallationId -AppToken $AppToken -Owner $owner -Repo $repo + +$InstallationToken = Generate-InstallationToken -AppToken $AppToken -InstallationId $InstallationId -Repository $repo + +Write-Output $InstallationToken From 312d0d114cff901b21ca6c55ff64b33b91e2d587 Mon Sep 17 00:00:00 2001 From: ramsessanchez <63934382+ramsessanchez@users.noreply.github.com> Date: Fri, 10 Jul 2026 16:13:38 -0700 Subject: [PATCH 2/2] ci: remove orphaned generate-helpdocs-template.yml This template is not referenced by any pipeline in this repo or in MicrosoftDocs/microsoftgraph-docs-powershell, so it never runs. Removing it as dead code (it also carried the last remaining PAT-based git push). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b1e4dbfd-ffcd-4a3e-bd35-1632b2912252 --- .../generate-helpdocs-template.yml | 57 ------------------- 1 file changed, 57 deletions(-) delete mode 100644 .azure-pipelines/generation-templates/generate-helpdocs-template.yml diff --git a/.azure-pipelines/generation-templates/generate-helpdocs-template.yml b/.azure-pipelines/generation-templates/generate-helpdocs-template.yml deleted file mode 100644 index a36d1742209..00000000000 --- a/.azure-pipelines/generation-templates/generate-helpdocs-template.yml +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -parameters: - - name: DOCSBRANCH - type: string - default: "DocsGeneration" - - name: ModulesToGenerate - type: string - default: "" - - name: BUILDAGENT - default: 1es-windows-ps-compute-m - displayName: Build Agent - -steps: - - task: PowerShell@2 - name: "ComputeBranch" - displayName: "Compute Docs Branch Name" - inputs: - targetType: inline - script: | - $branch = "{0}/{1}" -f "${{ parameters.DOCSBRANCH }}", (Get-Date -Format yyyyMMddHHmm) - Write-Host "Docs Branch $branch" - Write-Host "##vso[task.setvariable variable=DocsBranch;isOutput=true]$branch" - - - task: Bash@3 - displayName: "Create Docs branch" - inputs: - targetType: inline - script: | - git status - git branch $(ComputeBranch.DocsBranch) - git checkout $(ComputeBranch.DocsBranch) - git status - - - task: PowerShell@2 - displayName: Generate Help Docs - continueOnError: false - enabled: true - inputs: - targetType: "inline" - pwsh: true - script: | - $ModulesToGenerate = "${{ parameters.ModulesToGenerate }}" - $Modules = $ModulesToGenerate.Split(" ", [System.StringSplitOptions]::RemoveEmptyEntries) - . $(System.DefaultWorkingDirectory)/tools/GenerateHelp.ps1 -ModulesToGenerate $Modules - - - task: Bash@3 - displayName: "Push Generated Help Docs" - env: - GITHUB_TOKEN: $(GITHUB_TOKEN) - inputs: - targetType: inline - script: | - git status - git push --set-upstream "https://$(GITHUB_TOKEN)@github.com/microsoftgraph/msgraph-sdk-powershell.git" $(ComputeBranch.DocsBranch) - git status