diff --git a/.azure-pipelines/generation-pipeline.yml b/.azure-pipelines/generation-pipeline.yml index 9857e7b82..0ec8a4b0f 100644 --- a/.azure-pipelines/generation-pipeline.yml +++ b/.azure-pipelines/generation-pipeline.yml @@ -805,26 +805,3 @@ stages: repoName: msgraph-beta-cli projectFile: src/msgraph-beta-cli.csproj -# - stage: stage_objc_v1 -# dependsOn: -# - stage_build_and_publish_typewriter -# - stage_v1_metadata -# condition: | -# and -# ( -# eq(dependencies.stage_build_and_publish_typewriter.result, 'Succeeded'), -# in(dependencies.stage_v1_metadata.result, 'Succeeded', 'Skipped') -# ) -# jobs: -# - job: objc_v1 -# steps: -# - template: generation-templates/language-generation.yml -# parameters: -# language: 'ObjC' -# version: '' -# repoName: 'msgraph-sdk-objc-models' -# branchName: $(v1Branch) -# cleanMetadataFile: $(cleanMetadataFileV1) -# cleanMetadataFolder: $(cleanMetadataFolderV1) -# languageSpecificSteps: -# - template: generation-templates/objc.yml diff --git a/.azure-pipelines/generation-templates/language-generation-kiota.yml b/.azure-pipelines/generation-templates/language-generation-kiota.yml index ed89366e5..23af8f8e9 100644 --- a/.azure-pipelines/generation-templates/language-generation-kiota.yml +++ b/.azure-pipelines/generation-templates/language-generation-kiota.yml @@ -102,12 +102,22 @@ 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" + - pwsh: '$(scriptsDirectory)/create-pull-request.ps1' - displayName: 'Create Pull Request for the generated build' + displayName: 'Create Pull Request for the generated build for ${{ parameters.repoName }}' env: - Version: ${{ parameters.version }} BaseBranch: ${{ parameters.baseBranchName}} - OverrideSkipCI: $(overrideSkipCI) - GITHUB_TOKEN: $(GithubAuthToken) GeneratePullRequest: ${{ parameters.generatePullRequest}} + GhAppId: $(microsoft-graph-devx-bot-appid) + GhAppKey: $(microsoft-graph-devx-bot-privatekey) + OverrideSkipCI: $(overrideSkipCI) + RepoName: 'microsoftgraph/${{ parameters.repoName}}' # the assumption is that repo in the microsoftgraph org + ScriptsDirectory: $(scriptsDirectory) + Version: ${{ parameters.version }} workingDirectory: ${{ parameters.repoName }} diff --git a/.azure-pipelines/generation-templates/language-generation.yml b/.azure-pipelines/generation-templates/language-generation.yml index b184420f2..0d7aa4757 100644 --- a/.azure-pipelines/generation-templates/language-generation.yml +++ b/.azure-pipelines/generation-templates/language-generation.yml @@ -80,12 +80,22 @@ steps: OverrideSkipCI: $(overrideSkipCI) 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" + - pwsh: '$(scriptsDirectory)/create-pull-request.ps1' - displayName: 'Create Pull Request for the generated build' + displayName: 'Create Pull Request for the generated build for ${{ parameters.repoName }}' env: - Version: ${{ parameters.version }} BaseBranch: ${{ parameters.baseBranchName}} - OverrideSkipCI: $(overrideSkipCI) - GITHUB_TOKEN: $(GithubAuthToken) GeneratePullRequest: ${{ parameters.generatePullRequest}} + GhAppId: $(microsoft-graph-devx-bot-appid) + GhAppKey: $(microsoft-graph-devx-bot-privatekey) + OverrideSkipCI: $(overrideSkipCI) + RepoName: 'microsoftgraph/${{ parameters.repoName}}' # the assumption is that repo in the microsoftgraph org + ScriptsDirectory: $(scriptsDirectory) + Version: ${{ parameters.version }} workingDirectory: ${{ parameters.repoName }} diff --git a/scripts/Generate-Github-Token.ps1 b/scripts/Generate-Github-Token.ps1 new file mode 100644 index 000000000..0b1c04160 --- /dev/null +++ b/scripts/Generate-Github-Token.ps1 @@ -0,0 +1,201 @@ +[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 \ No newline at end of file diff --git a/scripts/create-pull-request.ps1 b/scripts/create-pull-request.ps1 index e8af62fbc..3478a084a 100644 --- a/scripts/create-pull-request.ps1 +++ b/scripts/create-pull-request.ps1 @@ -11,7 +11,7 @@ if (($env:GeneratePullRequest -eq $False)) { # Skip CI if manually running this $version = $env:Version $title = "Generated $version models and request builders" -$body = "This pull request was automatically created by Azure Pipelines. **Important** Check for unexpected deletions or changes in this PR." +$body = ":bangbang:**_Important_**:bangbang:
Check for unexpected deletions or changes in this PR and ensure relevant CI checks are passing.

**Note:** This pull request was automatically created by Azure pipelines." $baseBranchParameter = "" if (![string]::IsNullOrEmpty($env:BaseBranch)) @@ -19,7 +19,12 @@ if (![string]::IsNullOrEmpty($env:BaseBranch)) $baseBranchParameter = "-B $env:BaseBranch" # optionally pass the base branch if provided as the PR will target the default branch otherwise } - # No need to specify reviewers as code owners should be added automatically. +# The installed application is required to have the following permissions: read/write on pull requests/ +$tokenGenerationScript = "$env:ScriptsDirectory\Generate-Github-Token.ps1" +$env:GITHUB_TOKEN = & $tokenGenerationScript -AppClientId $env:GhAppId -AppPrivateKeyContents $env:GhAppKey -Repository $env:RepoName +Write-Host "Fetched Github Token for PR generation and set as environment variable." -ForegroundColor Green + +# No need to specify reviewers as code owners should be added automatically. Invoke-Expression "gh auth login" # login to GitHub Invoke-Expression "gh pr create -t ""$title"" -b ""$body"" $baseBranchParameter | Write-Host"