From 6c30fb1f509bd243528ebcfddaaf6a0e8c59effa Mon Sep 17 00:00:00 2001 From: "Gavin Barron (from Dev Box)" Date: Thu, 25 Jun 2026 11:34:59 -0700 Subject: [PATCH 1/2] ci: use GitHub App identity instead of PAT for Agents-M365Copilot Pivots the Agents-M365Copilot generation flow off the PAT-backed `microsoftkiota` service connection and onto the existing `microsoft-graph-devx-bot` GitHub App identity. - Add opt-in `useGitHubAppAuth` param to language-generation-kiota.yml. When true, the public repo is cloned anonymously and the push remote is switched to a GitHub App installation token before pushing. SDK repos (param defaults false) are unaffected. - Add scripts/set-app-token-push-url.ps1 to set the origin push URL to the App token without logging it (fetch stays anonymous). - Remove the Agents-M365Copilot repo resource (microsoftkiota endpoint) and its SDL-exclude entry; set useGitHubAppAuth: true on the 6 Agents jobs. PR creation already used the GitHub App, so this completes PAT elimination for Agents-M365Copilot checkout/push/PR. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .azure-pipelines/generation-pipeline.yml | 11 ++-- .../language-generation-kiota.yml | 62 ++++++++++++++++--- scripts/set-app-token-push-url.ps1 | 40 ++++++++++++ 3 files changed, 98 insertions(+), 15 deletions(-) create mode 100644 scripts/set-app-token-push-url.ps1 diff --git a/.azure-pipelines/generation-pipeline.yml b/.azure-pipelines/generation-pipeline.yml index dc6c3d55a..8e1ef579d 100644 --- a/.azure-pipelines/generation-pipeline.yml +++ b/.azure-pipelines/generation-pipeline.yml @@ -97,10 +97,6 @@ resources: type: github endpoint: microsoftgraph (22) name: microsoftgraph/microsoft-graph-docs - - repository: Agents-M365Copilot - type: github - endpoint: microsoftkiota - name: microsoft/Agents-M365Copilot - repository: 1ESPipelineTemplates type: git name: 1ESPipelineTemplates/1ESPipelineTemplates @@ -190,7 +186,6 @@ extends: - repository: msgraph-beta-sdk-python - repository: msgraph-metadata - repository: microsoft-graph-docs - - repository: Agents-M365Copilot - repository: 1ESPipelineTemplates pool: name: Azure-Pipelines-1ESPT-ExDShared @@ -963,6 +958,7 @@ extends: targetClassName: "BaseAgentsM365CopilotServiceClient" targetNamespace: "Microsoft.Agents.M365Copilot" commitMessagePrefix: "feat(generation): update request builders and models for dotnet v1" + useGitHubAppAuth: true customArguments: "-i '**/copilot/**'" # include only copilot paths cleanMetadataFolder: $(cleanOpenAPIFolderV1) pathExclusionArguments: '' @@ -1009,6 +1005,7 @@ extends: targetClassName: "BaseAgentsM365CopilotBetaServiceClient" targetNamespace: "Microsoft.Agents.M365Copilot.Beta" commitMessagePrefix: "feat(generation): update request builders and models for dotnet beta" + useGitHubAppAuth: true customArguments: "-i '**/copilot/**'" # include only copilot paths cleanMetadataFolder: $(cleanOpenAPIFolderBeta) pathExclusionArguments: '' @@ -1057,6 +1054,7 @@ extends: customArguments: "-i '**/copilot/**'" # include only copilot paths cleanMetadataFolder: $(cleanOpenAPIFolderV1) commitMessagePrefix: "feat(generation): update request builders and models for python v1" + useGitHubAppAuth: true pathExclusionArguments: '' languageSpecificSteps: - template: /.azure-pipelines/generation-templates/python.yml@self @@ -1103,6 +1101,7 @@ extends: customArguments: "-i '**/copilot/**'" # include only copilot paths cleanMetadataFolder: $(cleanOpenAPIFolderBeta) commitMessagePrefix: "feat(generation): update request builders and models for python beta" + useGitHubAppAuth: true pathExclusionArguments: '' languageSpecificSteps: - template: /.azure-pipelines/generation-templates/python.yml@self @@ -1145,6 +1144,7 @@ extends: baseBranchName: 'main' branchName: 'ccs-typescript/$(v1Branch)' commitMessagePrefix: "feat(generation): update request builders and models for typescript v1" + useGitHubAppAuth: true targetClassName: "BaseAgentsM365CopilotServiceClient" targetNamespace: "github.com/microsoft/Agents-M365Copilot/typescript/" customArguments: "-i '**/copilot/**'" @@ -1191,6 +1191,7 @@ extends: baseBranchName: 'main' branchName: 'ccs-typescript/$(betaBranch)' commitMessagePrefix: "feat(generation): update request builders and models for typescript beta" + useGitHubAppAuth: true targetClassName: "BaseAgentsM365CopilotBetaServiceClient" targetNamespace: "github.com/microsoft/Agents-M365Copilot/typescript/" customArguments: "-i '**/copilot/**'" diff --git a/.azure-pipelines/generation-templates/language-generation-kiota.yml b/.azure-pipelines/generation-templates/language-generation-kiota.yml index 847fda7b0..f62e45dc9 100644 --- a/.azure-pipelines/generation-templates/language-generation-kiota.yml +++ b/.azure-pipelines/generation-templates/language-generation-kiota.yml @@ -56,15 +56,25 @@ parameters: type: string default: "-e '/copilot' -e '/copilot/**'" +# When true, the target repository is read anonymously (public repo) and the generated +# files are pushed using a GitHub App installation token instead of a PAT-backed service +# connection. PR creation is App-based regardless. Defaults to false to preserve the +# existing PAT-based behaviour for the microsoftgraph SDK repos. +- name: useGitHubAppAuth + type: boolean + default: false + steps: - template: /.azure-pipelines/generation-templates/set-up-for-generation-kiota.yml@self parameters: cleanMetadataFolder: ${{ parameters.cleanMetadataFolder }} -- checkout: ${{ parameters.repoName }} - displayName: 'checkout ${{ parameters.repoName }}' - fetchDepth: 1 - persistCredentials: true +# Default (PAT) path: check out the target repo via its service connection. +${{ if eq(parameters.useGitHubAppAuth, false) }}: + - checkout: ${{ parameters.repoName }} + displayName: 'checkout ${{ parameters.repoName }}' + fetchDepth: 1 + persistCredentials: true # need this for the shared scripts (maybe we should move them to msgraph-metadata) # no need for recursive, just scripts from the main repo @@ -73,6 +83,16 @@ steps: fetchDepth: 1 persistCredentials: true +# App-auth path: the target repo is public, so clone it anonymously (no PAT). The push +# remote is switched to a GitHub App token in a later step before the push happens. +${{ if eq(parameters.useGitHubAppAuth, true) }}: + - pwsh: | + $repoDir = "$(Build.SourcesDirectory)/${{ parameters.repoName }}" + if (Test-Path $repoDir) { Remove-Item $repoDir -Recurse -Force } + git clone --depth 1 "https://github.com/${{ parameters.orgName }}/${{ parameters.repoName }}.git" "$repoDir" + if ($LASTEXITCODE -ne 0) { throw "Failed to clone ${{ parameters.orgName }}/${{ parameters.repoName }}" } + displayName: 'Clone ${{ parameters.repoName }} (anonymous, public repo)' + - pwsh: '$(scriptsDirectory)/checkout-custom-base-branch.ps1' displayName: 'Checking out custom base branch ${{ parameters.baseBranchName }}' env: @@ -103,6 +123,25 @@ steps: - ${{ parameters.languageSpecificSteps }} +# App-auth path: fetch the App secrets and switch the push remote to the App token +# BEFORE pushing, so the push never uses a PAT. +${{ if eq(parameters.useGitHubAppAuth, true) }}: + - task: AzureKeyVault@2 + displayName: "Azure Key Vault: Get Secrets" + inputs: + azureSubscription: "Federated AKV Managed Identity Connection" + KeyVaultName: akv-prod-eastus + SecretsFilter: "microsoft-graph-devx-bot-appid,microsoft-graph-devx-bot-privatekey" + + - pwsh: '$(scriptsDirectory)/set-app-token-push-url.ps1' + displayName: 'Git: use GitHub App token for push' + env: + GhAppId: $(microsoft-graph-devx-bot-appid) + GhAppKey: $(microsoft-graph-devx-bot-privatekey) + RepoName: '${{ parameters.orgName }}/${{ parameters.repoName }}' + ScriptsDirectory: $(scriptsDirectory) + workingDirectory: ${{ parameters.repoName }} + - pwsh: '$(scriptsDirectory)/git-push-files.ps1' displayName: 'Git: push generated files' env: @@ -112,12 +151,15 @@ steps: CommitMessagePrefix: ${{ parameters.commitMessagePrefix }} workingDirectory: ${{ parameters.repoName }} -- task: AzureKeyVault@2 - displayName: "Azure Key Vault: Get Secrets" - inputs: - azureSubscription: "Federated AKV Managed Identity Connection" - KeyVaultName: akv-prod-eastus - SecretsFilter: "microsoft-graph-devx-bot-appid,microsoft-graph-devx-bot-privatekey" +# Default path: secrets are fetched after the push, just before PR creation. On the +# App-auth path the secrets were already fetched above and persist for the PR step. +${{ if eq(parameters.useGitHubAppAuth, false) }}: + - task: AzureKeyVault@2 + displayName: "Azure Key Vault: Get Secrets" + inputs: + azureSubscription: "Federated AKV Managed Identity Connection" + KeyVaultName: akv-prod-eastus + SecretsFilter: "microsoft-graph-devx-bot-appid,microsoft-graph-devx-bot-privatekey" - pwsh: '$(scriptsDirectory)/create-pull-request.ps1' displayName: 'Create Pull Request for the generated build for ${{ parameters.repoName }}' diff --git a/scripts/set-app-token-push-url.ps1 b/scripts/set-app-token-push-url.ps1 new file mode 100644 index 000000000..a7aa22c31 --- /dev/null +++ b/scripts/set-app-token-push-url.ps1 @@ -0,0 +1,40 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +# Configure the 'origin' push URL of an already-cloned repository to authenticate with a +# GitHub App installation token instead of a PAT. The fetch URL is left untouched so reads +# stay anonymous for public repositories. +# +# Required environment variables: +# GhAppId - GitHub App client ID (from AKV) +# GhAppKey - GitHub App private key (from AKV) +# RepoName - Target repo in "owner/repo" format (e.g. "microsoft/Agents-M365Copilot") +# ScriptsDirectory - Path to the scripts folder containing Generate-Github-Token.ps1 +# Runs from the repository working directory (the cloned repo). + +[CmdletBinding()] +param () + +$ErrorActionPreference = "Stop" + +if ([string]::IsNullOrWhiteSpace($env:GhAppId)) { throw "GhAppId is required" } +if ([string]::IsNullOrWhiteSpace($env:GhAppKey)) { throw "GhAppKey is required" } +if ([string]::IsNullOrWhiteSpace($env:RepoName)) { throw "RepoName is required" } +if ([string]::IsNullOrWhiteSpace($env:ScriptsDirectory)){ throw "ScriptsDirectory is required" } + +# The installed application is required to have contents:write on the target repository. +$tokenGenerationScript = Join-Path $env:ScriptsDirectory "Generate-Github-Token.ps1" +$token = & $tokenGenerationScript -AppClientId $env:GhAppId -AppPrivateKeyContents $env:GhAppKey -Repository $env:RepoName +if ([string]::IsNullOrWhiteSpace($token)) { + throw "Failed to generate GitHub App installation token (empty result)" +} + +# Mask the token so it is never surfaced in pipeline logs. +Write-Host "##vso[task.setsecret]$token" + +# Set the push URL only; leave the (anonymous) fetch URL alone. +$pushUrl = "https://x-access-token:$token@github.com/$($env:RepoName).git" +git remote set-url --push origin $pushUrl +if ($LASTEXITCODE -ne 0) { throw "Failed to set push URL for origin" } + +Write-Host "Configured origin push URL to use the GitHub App installation token." -ForegroundColor Green From bf0dee3263d435f0b140a0f4d3e80dd8f1d57aa0 Mon Sep 17 00:00:00 2001 From: Gavin Barron Date: Thu, 25 Jun 2026 12:02:40 -0700 Subject: [PATCH 2/2] clear up comments Co-authored-by: Gavin Barron --- .../generation-templates/language-generation-kiota.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.azure-pipelines/generation-templates/language-generation-kiota.yml b/.azure-pipelines/generation-templates/language-generation-kiota.yml index f62e45dc9..5ce308584 100644 --- a/.azure-pipelines/generation-templates/language-generation-kiota.yml +++ b/.azure-pipelines/generation-templates/language-generation-kiota.yml @@ -59,7 +59,7 @@ parameters: # When true, the target repository is read anonymously (public repo) and the generated # files are pushed using a GitHub App installation token instead of a PAT-backed service # connection. PR creation is App-based regardless. Defaults to false to preserve the -# existing PAT-based behaviour for the microsoftgraph SDK repos. +# existing service connection based behaviour for the microsoftgraph SDK repos. - name: useGitHubAppAuth type: boolean default: false @@ -69,7 +69,7 @@ steps: parameters: cleanMetadataFolder: ${{ parameters.cleanMetadataFolder }} -# Default (PAT) path: check out the target repo via its service connection. +# Default path: check out the target repo via its service connection. ${{ if eq(parameters.useGitHubAppAuth, false) }}: - checkout: ${{ parameters.repoName }} displayName: 'checkout ${{ parameters.repoName }}' @@ -124,7 +124,7 @@ ${{ if eq(parameters.useGitHubAppAuth, true) }}: - ${{ parameters.languageSpecificSteps }} # App-auth path: fetch the App secrets and switch the push remote to the App token -# BEFORE pushing, so the push never uses a PAT. +# BEFORE pushing. ${{ if eq(parameters.useGitHubAppAuth, true) }}: - task: AzureKeyVault@2 displayName: "Azure Key Vault: Get Secrets"