Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions azure-pipelines/Get-GitHubAppToken.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string] $KeyVaultName,

[Parameter(Mandatory = $true)]
[string] $KeyName,

[Parameter(Mandatory = $true)]
[string] $AppClientId,

[Parameter(Mandatory = $true)]
[string] $InstallationOwner,

[Parameter(Mandatory = $true)]
[string] $OutputVariableName
)

$ErrorActionPreference = 'Stop'
$PSNativeCommandUseErrorActionPreference = $true

function ConvertTo-Base64Url([byte[]] $bytes) {
return [Convert]::ToBase64String($bytes).TrimEnd('=').Replace('+', '-').Replace('/', '_')
}

$jwtHeader = [ordered]@{
alg = 'RS256'
typ = 'JWT'
}
$now = [System.DateTimeOffset]::UtcNow
$jwtPayload = [ordered]@{
iat = $now.AddMinutes(-1).ToUnixTimeSeconds()
exp = $now.AddMinutes(5).ToUnixTimeSeconds()
iss = $AppClientId
}

$headerEncoded = ConvertTo-Base64Url ([System.Text.Encoding]::UTF8.GetBytes(($jwtHeader | ConvertTo-Json -Compress)))
$payloadEncoded = ConvertTo-Base64Url ([System.Text.Encoding]::UTF8.GetBytes(($jwtPayload | ConvertTo-Json -Compress)))
$signingInput = "$headerEncoded.$payloadEncoded"

$sha256 = [System.Security.Cryptography.SHA256]::Create()
$digestBytes = $sha256.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($signingInput))
$digestBase64 = [Convert]::ToBase64String($digestBytes)

Write-Host "Signing GitHub App JWT with key '$KeyName' in vault '$KeyVaultName'..."
$signResponseJson = az keyvault key sign `
--vault-name $KeyVaultName `
--name $KeyName `
--algorithm RS256 `
--digest $digestBase64 `
--output json
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($signResponseJson)) {
throw "'az keyvault key sign' failed with exit code $LASTEXITCODE for key '$KeyName' in vault '$KeyVaultName'."
}

$signResponse = $signResponseJson | ConvertFrom-Json
if ([string]::IsNullOrWhiteSpace($signResponse.signature)) {
throw "Key Vault returned an empty signature for key '$KeyName' in vault '$KeyVaultName'."
}

$signatureEncoded = $signResponse.signature.TrimEnd('=').Replace('+', '-').Replace('/', '_')
$jwt = "$signingInput.$signatureEncoded"
$headers = @{
Authorization = "Bearer $jwt"
'X-GitHub-Api-Version' = '2022-11-28'
Accept = 'application/vnd.github+json'
'User-Agent' = 'vscode-csharp-onelocbuild'
}

Write-Host "Looking up the GitHub App installation for '$InstallationOwner'..."
$installations = Invoke-RestMethod -Uri 'https://api.github.com/app/installations' -Headers $headers -Method Get
$installation = $installations | Where-Object { $_.account.login -eq $InstallationOwner } | Select-Object -First 1
if (-not $installation) {
throw "No GitHub App installation found for '$InstallationOwner'."
}

$tokenResponse = Invoke-RestMethod `
-Uri "https://api.github.com/app/installations/$($installation.id)/access_tokens" `
-Headers $headers `
-Method Post `
-ContentType 'application/json'
if ([string]::IsNullOrWhiteSpace($tokenResponse.token)) {
throw "GitHub returned an empty installation token for '$InstallationOwner'."
}

Write-Host "Got an installation token for '$InstallationOwner' that expires at $($tokenResponse.expires_at)."
Write-Host "##vso[task.setvariable variable=$OutputVariableName;issecret=true]$($tokenResponse.token)"
17 changes: 15 additions & 2 deletions azure-pipelines/loc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ resources:
ref: refs/tags/release

variables:
# Variable group contains the PAT to LOC
# Variable group contains the credential for the localization package source.
- group: OneLocBuildVariables

extends:
Expand Down Expand Up @@ -86,7 +86,20 @@ extends:
LclPackageId: 'LCL-JUNO-PROD-VSCODECS'
- pwsh: npm run l10nDevImportXlf
displayName: 'Import xlf to json.'
- task: AzureCLI@2
displayName: 'Get GitHub App installation token'
inputs:
azureSubscription: 'dnceng-oneloc-githubapp'
scriptType: pscore
scriptLocation: inlineScript
inlineScript: |
& "$(Build.SourcesDirectory)/azure-pipelines/Get-GitHubAppToken.ps1" `
-KeyVaultName 'EngKeyVault' `
-KeyName 'oneloc-localization-app-key' `
-AppClientId 'Iv23lijBU8x3gc9lDOc9' `
-InstallationOwner 'dotnet' `
-OutputVariableName 'GitHubAppInstallationToken'
- pwsh: npm run publishLocalizationContent -- --userName dotnet-bot --email dotnet-bot@dotnetfoundation.org --commitSha $(Build.SourceVersion) --targetRemoteRepo vscode-csharp --baseBranch 'main'
displayName: 'Create PR in GitHub.'
env:
GitHubPAT: $(BotAccount-dotnet-bot-repo-PAT)
GitHubToken: $(GitHubAppInstallationToken)
2 changes: 1 addition & 1 deletion package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@
"configuration.omnisharp.projectFilesExcludePattern": "The exclude pattern used by OmniSharp to find all project files.",
"configuration.omnisharp.projectLoadTimeout": "The time Visual Studio Code will wait for the OmniSharp server to start. Time is expressed in seconds.",
"configuration.omnisharp.maxProjectResults": "The maximum number of projects to be shown in the 'Select Project' dropdown (maximum 250).",
"configuration.omnisharp.useEditorFormattingSettings": "Specifes whether OmniSharp should use VS Code editor settings for C# code formatting (use of tabs, indentation size).",
"configuration.omnisharp.useEditorFormattingSettings": "Specifies whether OmniSharp should use VS Code editor settings for C# code formatting (use of tabs, indentation size).",
"configuration.omnisharp.minFindSymbolsFilterLength": "The minimum number of characters to enter before 'Go to Symbol in Workspace' operation shows any results.",
"configuration.omnisharp.maxFindSymbolsItems": "The maximum number of items that 'Go to Symbol in Workspace' operation can show. The limit is applied only when a positive number is specified here.",
"configuration.omnisharp.disableMSBuildDiagnosticWarning": "Specifies whether notifications should be shown if OmniSharp encounters warnings or errors loading a project. Note that these warnings/errors are always emitted to the OmniSharp log",
Expand Down
34 changes: 21 additions & 13 deletions tasks/localization/publishLocalizationContent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ async function publishLocalizationContent() {
const localizationChanges = getAllPossibleLocalizationFiles();
await git(['add'].concat(localizationChanges));

const diff = await git_diff(['--name-only', 'HEAD']);
// Check only staged localization files; other build steps may leave unrelated tracked files modified.
const diff = await git_diff(['--cached', '--name-only']);
if (diff.length == 0) {
console.log('No localization file changed');
return;
Expand All @@ -55,9 +56,9 @@ async function publishLocalizationContent() {
await git(['checkout', '-b', newBranchName]);
await git(['commit', '-m', `Localization result of ${parsedArgs.commitSha}.`]);

const pat = process.env['GitHubPAT'];
if (!pat) {
throw 'No GitHub Pat found.';
const token = process.env['GitHubToken'];
if (!token) {
throw 'No GitHub token found.';
}
Comment on lines +64 to 67

const remoteRepoAlias = 'targetRepo';
Expand All @@ -66,9 +67,9 @@ async function publishLocalizationContent() {
'remote',
'add',
remoteRepoAlias,
`https://${parsedArgs.userName}:${pat}@github.com/dotnet/${parsedArgs.targetRemoteRepo}.git`,
`https://x-access-token:${token}@github.com/dotnet/${parsedArgs.targetRemoteRepo}.git`,
],
// Note: don't print PAT to console
// Do not print the token to the console.
false
);
await git(['fetch', remoteRepoAlias]);
Expand All @@ -83,7 +84,7 @@ async function publishLocalizationContent() {
await git(['push', '-u', remoteRepoAlias]);
}

const octokit = new Octokit({ auth: pat });
const octokit = new Octokit({ auth: token });
const listPullRequest = await octokit.rest.pulls.list({
owner: 'dotnet',
repo: parsedArgs.targetRemoteRepo,
Expand Down Expand Up @@ -144,16 +145,23 @@ async function git(args: string[], printCommand = true): Promise<string> {
console.log(`git ${args.join(' ')}`);
}

const git = spawnSync('git', args);
if (git.status != 0) {
const err = git.stderr.toString();
const result = spawnSync('git', args, { encoding: 'utf8' });
const command = printCommand ? `git ${args.join(' ')}` : 'git command';
if (result.error) {
throw new Error(`Failed to start ${command}: ${result.error.message}`);
}
if (result.status != 0) {
const output = [result.stderr, result.stdout]
.map((value) => value.trim())
.filter((value) => value.length > 0)
.join(EOL);
if (printCommand) {
console.log(`Failed to execute git ${args.join(' ')}.`);
console.error(`Failed to execute ${command}.`);
}
throw err;
throw new Error(output || `${command} failed with code ${result.status}.`);
Comment on lines +159 to +174

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we sanitize the output for known PAT formats?

}

const stdout = git.stdout.toString();
const stdout = result.stdout;
if (printCommand) {
console.log(stdout);
}
Expand Down