diff --git a/NuGet.config b/NuGet.config index e3d6ef36f8..d2c712ad5d 100644 --- a/NuGet.config +++ b/NuGet.config @@ -2,6 +2,10 @@ + + + + @@ -9,6 +13,10 @@ + + + + diff --git a/eng/Version.Details.props b/eng/Version.Details.props index e316dc7d1b..5cc8102901 100644 --- a/eng/Version.Details.props +++ b/eng/Version.Details.props @@ -6,11 +6,11 @@ This file should be imported by eng/Versions.props - 10.0.0-beta.26378.110 - 2.0.10 - 10.0.10 - 10.0.10 - 10.0.10 + 10.0.0-beta.26412.118 + 2.0.11 + 10.0.11 + 10.0.11 + 10.0.11 diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 62f0f8b1d5..f36b58f19d 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -1,28 +1,28 @@ - + - + https://dev.azure.com/dnceng/internal/_git/dotnet-dotnet - f7d90799ce4ef09a0bb257852a57248d2a8fb8dd + e2f47b0110ed922f21a1522da67279133ce28f32 - + https://dev.azure.com/dnceng/internal/_git/dotnet-dotnet - f7d90799ce4ef09a0bb257852a57248d2a8fb8dd + e2f47b0110ed922f21a1522da67279133ce28f32 - + https://dev.azure.com/dnceng/internal/_git/dotnet-dotnet - f7d90799ce4ef09a0bb257852a57248d2a8fb8dd + e2f47b0110ed922f21a1522da67279133ce28f32 - + https://dev.azure.com/dnceng/internal/_git/dotnet-dotnet - f7d90799ce4ef09a0bb257852a57248d2a8fb8dd + e2f47b0110ed922f21a1522da67279133ce28f32 - + https://github.com/dotnet/dotnet - c9eaf6d2f7b11c935ce6025093278843c89873d9 + 88c5780996625fa108589acd22adaf8246bc78e8 diff --git a/eng/common/Get-GitHubAppToken.ps1 b/eng/common/Get-GitHubAppToken.ps1 new file mode 100644 index 0000000000..9c7e3dcd6a --- /dev/null +++ b/eng/common/Get-GitHubAppToken.ps1 @@ -0,0 +1,164 @@ +# Mints a short-lived GitHub App installation access token by signing a JWT +# with a private key stored in Azure Key Vault (RSA, RS256). The signed JWT is +# exchanged with the GitHub API for a token scoped to a single installation. +# +# Requirements: +# - A GitHub App whose private key has been uploaded into Key Vault as an RSA +# key (the PEM converted to a Key Vault *key*, NOT stored as a secret). +# - The caller (the federated Azure service connection used to run this script) +# must have the `Key Vault Crypto User` role (or at minimum the `Sign` +# action) on that key. +# - The App must be installed on the target organization/account +# (`InstallationOwner`) with the permissions/repositories it needs. +# +# Installation tokens (ghs_*) are exempt from the enterprise classic-PAT +# lifetime policy, which is why this replaces the long-lived PAT. + +[CmdletBinding()] +param( + # Name of the Key Vault that holds the GitHub App's RSA signing key. + [Parameter(Mandatory = $true)] + [string] $KeyVaultName, + + # Name of the RSA key inside the Key Vault (the App's private key). + [Parameter(Mandatory = $true)] + [string] $KeyName, + + # The GitHub App's Client ID (the value to put in the `iss` JWT claim). + [Parameter(Mandatory = $true)] + [string] $AppClientId, + + # Login of the organization or user account whose installation we should + # mint the token for (e.g. `dotnet`, `microsoft`). + [Parameter(Mandatory = $true)] + [string] $InstallationOwner, + + # Optional Azure DevOps pipeline variable name to set with the installation + # token (marked as a secret). When not specified, the token is written to + # stdout instead. + [Parameter(Mandatory = $false)] + [string] $OutputVariableName +) + +$ErrorActionPreference = 'Stop' +$PSNativeCommandUseErrorActionPreference = $true + +. $PSScriptRoot\pipeline-logging-functions.ps1 + +function ConvertTo-Base64Url([byte[]] $bytes) { + return [Convert]::ToBase64String($bytes).TrimEnd('=').Replace('+', '-').Replace('/', '_') +} + +# Build JWT header and payload. Use [ordered] hashtables so JSON +# serialization is deterministic. +$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" + +# Key Vault `sign` expects the *digest* (base64), not the raw bytes. +$sha256 = [System.Security.Cryptography.SHA256]::Create() +$digestBytes = $sha256.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($signingInput)) +$digestBase64 = [Convert]::ToBase64String($digestBytes) + +Write-Host "Signing JWT with key '$KeyName' in vault '$KeyVaultName'..." +$previousNativeCommandErrorPreference = $PSNativeCommandUseErrorActionPreference +try { + # Azure CLI can emit non-fatal Python warnings to stderr even when signing succeeds. + # Use the exit code to determine success for this invocation. + $PSNativeCommandUseErrorActionPreference = $false + $signatureBase64 = az keyvault key sign ` + --vault-name $KeyVaultName ` + --name $KeyName ` + --algorithm RS256 ` + --digest $digestBase64 ` + --query signature ` + --output tsv ` + --only-show-errors + $signExitCode = $LASTEXITCODE +} +catch { + Write-PipelineTelemetryError -Category 'Build' -Message "Failed to sign the JWT via Key Vault (key '$KeyName', vault '$KeyVaultName'): $_. Verify the service connection identity has the 'Key Vault Crypto User' role (Sign action) on the key." + exit 1 +} +finally { + $PSNativeCommandUseErrorActionPreference = $previousNativeCommandErrorPreference +} +if ($signExitCode -ne 0 -or [string]::IsNullOrWhiteSpace($signatureBase64)) { + Write-PipelineTelemetryError -Category 'Build' -Message "'az keyvault key sign' exited with code $signExitCode for key '$KeyName' in vault '$KeyVaultName'. Verify the service connection identity has the 'Key Vault Crypto User' role (Sign action) on the key." + exit 1 +} +$signatureUrl = $signatureBase64.Trim().TrimEnd('=').Replace('+', '-').Replace('/', '_') +$jwt = "$signingInput.$signatureUrl" + +$headers = @{ + Authorization = "Bearer $jwt" + 'X-GitHub-Api-Version' = '2022-11-28' + Accept = 'application/vnd.github+json' + 'User-Agent' = 'dotnet-arcade-onelocbuild' +} + +Write-Host "Looking up installation for '$InstallationOwner'..." +try { + $installations = @() + $page = 1 + do { + # Assign the response before wrapping it in @(). PowerShell otherwise + # preserves a top-level JSON array as one nested pipeline object. + $pageResponse = Invoke-RestMethod ` + -Uri "https://api.github.com/app/installations?per_page=100&page=$page" ` + -Headers $headers ` + -Method Get + $pageInstallations = @($pageResponse) + $installations += $pageInstallations + $page++ + } while ($pageInstallations.Count -eq 100) +} +catch { + Write-PipelineTelemetryError -Category 'Build' -Message "Failed to list GitHub App installations: $_. The signed JWT may be invalid or the App's Client ID ('$AppClientId') may be incorrect." + exit 1 +} +$matchingInstallations = @($installations | Where-Object { $_.account.login -ieq $InstallationOwner }) +if ($matchingInstallations.Count -eq 0) { + $found = ($installations | ForEach-Object { $_.account.login }) -join ', ' + Write-PipelineTelemetryError -Category 'Build' -Message "No installation found for '$InstallationOwner'. App is installed on: $found" + exit 1 +} +if ($matchingInstallations.Count -ne 1) { + $matchingIds = ($matchingInstallations | ForEach-Object { $_.id }) -join ', ' + Write-PipelineTelemetryError -Category 'Build' -Message "Found multiple installations for '$InstallationOwner': $matchingIds" + exit 1 +} +$installation = $matchingInstallations[0] +Write-Host "Using installation $($installation.id) for '$($installation.account.login)'." + +try { + $tokenResponse = Invoke-RestMethod ` + -Uri "https://api.github.com/app/installations/$($installation.id)/access_tokens" ` + -Headers $headers ` + -Method Post ` + -ContentType 'application/json' +} +catch { + Write-PipelineTelemetryError -Category 'Build' -Message "Failed to mint an installation access token for '$InstallationOwner' (installation $($installation.id)): $_" + exit 1 +} + +Write-Host "Got installation token for '$InstallationOwner' (expires $($tokenResponse.expires_at))." +if ($OutputVariableName) { + Write-Host "Setting pipeline variable '$OutputVariableName'." + Write-Host "##vso[task.setvariable variable=$OutputVariableName;issecret=true]$($tokenResponse.token)" +} +else { + Write-Host $tokenResponse.token -ForegroundColor Green +} diff --git a/eng/common/core-templates/job/onelocbuild.yml b/eng/common/core-templates/job/onelocbuild.yml index 12d7e55a94..b28af6613c 100644 --- a/eng/common/core-templates/job/onelocbuild.yml +++ b/eng/common/core-templates/job/onelocbuild.yml @@ -14,6 +14,15 @@ parameters: # exist, and any pipeline that sets this to '' fall back to PAT-based auth via the CeapexPat parameter. CeapexServiceConnection: 'dnceng-onelocbuild-ceapex' + # GitHub App authentication for the OneLoc check-in PR (dnceng/internal only). + # The infrastructure identifiers are centralized here and the App path is enabled by default. + # DevDiv requires its own project-scoped service connection before this path can be enabled there. + UseGitHubAppAuthentication: true + GitHubAppServiceConnection: 'dnceng-oneloc-githubapp' + GitHubAppClientId: 'Iv23lijBU8x3gc9lDOc9' + GitHubAppKeyVaultName: 'EngKeyVault' + GitHubAppKeyName: 'oneloc-localization-app-key' + SourcesDirectory: $(System.DefaultWorkingDirectory) CreatePr: true AutoCompletePr: false @@ -88,6 +97,20 @@ jobs: outputVariableName: 'CeapexEntraToken' condition: ${{ parameters.condition }} + # Mint a short-lived GitHub App installation token for the loc check-in PR (dnceng/internal only). + # All other projects fall back to PAT-based auth, since the app service connection is scoped to dnceng/internal. + - ${{ if and(eq(parameters.RepoType, 'gitHub'), eq(parameters.UseGitHubAppAuthentication, true), eq(variables['System.TeamProject'], 'internal')) }}: + - template: /eng/common/core-templates/steps/get-github-app-token.yml + parameters: + is1ESPipeline: ${{ parameters.is1ESPipeline }} + azureSubscription: ${{ parameters.GitHubAppServiceConnection }} + keyVaultName: ${{ parameters.GitHubAppKeyVaultName }} + keyName: ${{ parameters.GitHubAppKeyName }} + appClientId: ${{ parameters.GitHubAppClientId }} + installationOwner: ${{ parameters.GitHubOrg }} + outputVariableName: 'GitHubAppInstallationToken' + condition: ${{ parameters.condition }} + - task: OneLocBuild@2 displayName: OneLocBuild env: @@ -109,7 +132,10 @@ jobs: patVariable: ${{ parameters.CeapexPat }} ${{ if eq(parameters.RepoType, 'gitHub') }}: repoType: ${{ parameters.RepoType }} - gitHubPatVariable: "${{ parameters.GithubPat }}" + ${{ if and(eq(parameters.UseGitHubAppAuthentication, true), eq(variables['System.TeamProject'], 'internal')) }}: + gitHubPatVariable: "$(GitHubAppInstallationToken)" + ${{ if or(eq(parameters.UseGitHubAppAuthentication, false), ne(variables['System.TeamProject'], 'internal')) }}: + gitHubPatVariable: "${{ parameters.GithubPat }}" ${{ if ne(parameters.MirrorRepo, '') }}: isMirrorRepoSelected: true gitHubOrganization: ${{ parameters.GitHubOrg }} diff --git a/eng/common/core-templates/job/publish-build-assets.yml b/eng/common/core-templates/job/publish-build-assets.yml index 53af522d6d..718b7f0a0a 100644 --- a/eng/common/core-templates/job/publish-build-assets.yml +++ b/eng/common/core-templates/job/publish-build-assets.yml @@ -58,8 +58,6 @@ jobs: parameters: is1ESPipeline: ${{ parameters.is1ESPipeline }} - ${{ if and(eq(parameters.runAsPublic, 'false'), ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: - - group: Publish-Build-Assets - - group: AzureDevOps-Artifact-Feeds-Pats - name: runCodesignValidationInjection value: false # unconditional - needed for logs publishing (redactor tool version) diff --git a/eng/common/core-templates/jobs/codeql-build.yml b/eng/common/core-templates/jobs/codeql-build.yml index dbc14ac580..7d5e0473a1 100644 --- a/eng/common/core-templates/jobs/codeql-build.yml +++ b/eng/common/core-templates/jobs/codeql-build.yml @@ -18,7 +18,6 @@ jobs: enableTelemetry: true variables: - - group: Publish-Build-Assets # The Guardian version specified in 'eng/common/sdl/packages.config'. This value must be kept in # sync with the packages.config file. - name: DefaultGuardianVersion diff --git a/eng/common/core-templates/post-build/common-variables.yml b/eng/common/core-templates/post-build/common-variables.yml index d5627a994a..3413a9a573 100644 --- a/eng/common/core-templates/post-build/common-variables.yml +++ b/eng/common/core-templates/post-build/common-variables.yml @@ -1,6 +1,4 @@ variables: - - group: Publish-Build-Assets - # Whether the build is internal or not - name: IsInternalBuild value: ${{ and(ne(variables['System.TeamProject'], 'public'), contains(variables['Build.SourceBranch'], 'internal')) }} diff --git a/eng/common/core-templates/steps/get-github-app-token.yml b/eng/common/core-templates/steps/get-github-app-token.yml new file mode 100644 index 0000000000..6d42a48d3c --- /dev/null +++ b/eng/common/core-templates/steps/get-github-app-token.yml @@ -0,0 +1,79 @@ +# Mints a short-lived GitHub App installation access token by signing a JWT +# with a private key stored in Azure Key Vault (RSA, RS256). The JWT is +# exchanged with the GitHub API for a token scoped to a single installation. +# +# Requirements (per GitHub App you want to authenticate as): +# - A GitHub App with its private key uploaded into Key Vault as an RSA key +# (PEM converted to a key, NOT stored as a secret). +# - The Azure service connection passed via `azureSubscription` must be +# granted the `Key Vault Crypto User` role (or at minimum `Sign` action) +# on that key. +# - The App must be installed on the target organization/account +# (`installationOwner`) with the permissions/repositories you need. +# +# Output: a secret pipeline variable named ${{ parameters.outputVariableName }} +# containing the installation access token. Token lifetime is ~1 hour and is +# automatically scrubbed from logs. Installation tokens are exempt from the +# enterprise classic-PAT lifetime policy. + +parameters: +# Azure DevOps service connection (federated) that can call +# `az keyvault key sign` on the App's signing key. +- name: azureSubscription + type: string + +# Name of the Key Vault that holds the GitHub App's RSA signing key. +- name: keyVaultName + type: string + +# Name of the RSA key inside the Key Vault (the App's private key). +- name: keyName + type: string + +# The GitHub App's Client ID (the value to put in the `iss` JWT claim). +# Prefer this over the numeric App ID; GitHub accepts either, but Client ID +# is the documented form going forward. +- name: appClientId + type: string + +# Login of the organization or user account whose installation we should +# mint the token for (e.g. `dotnet`, `microsoft`). +- name: installationOwner + type: string + +# Name of the pipeline variable that will receive the installation token. +- name: outputVariableName + type: string + +- name: is1ESPipeline + type: boolean + +- name: stepName + type: string + default: getGitHubAppInstallationToken + +- name: condition + type: string + default: '' + +- name: displayName + type: string + default: Get GitHub App installation token + +steps: +- task: AzureCLI@2 + displayName: ${{ parameters.displayName }} + name: ${{ parameters.stepName }} + ${{ if ne(parameters.condition, '') }}: + condition: ${{ parameters.condition }} + inputs: + azureSubscription: ${{ parameters.azureSubscription }} + scriptType: pscore + scriptLocation: inlineScript + inlineScript: | + & "$(System.DefaultWorkingDirectory)/eng/common/Get-GitHubAppToken.ps1" ` + -KeyVaultName '${{ parameters.keyVaultName }}' ` + -KeyName '${{ parameters.keyName }}' ` + -AppClientId '${{ parameters.appClientId }}' ` + -InstallationOwner '${{ parameters.installationOwner }}' ` + -OutputVariableName '${{ parameters.outputVariableName }}' diff --git a/eng/common/core-templates/steps/publish-logs.yml b/eng/common/core-templates/steps/publish-logs.yml index 694f55a926..faf703157c 100644 --- a/eng/common/core-templates/steps/publish-logs.yml +++ b/eng/common/core-templates/steps/publish-logs.yml @@ -30,9 +30,6 @@ steps: -TokensFilePath '$(System.DefaultWorkingDirectory)/eng/BinlogSecretsRedactionFile.txt' -runtimeSourceFeed https://ci.dot.net/internal -runtimeSourceFeedKey '$(dotnetbuilds-internal-container-read-token-base64)' - '$(publishing-dnceng-devdiv-code-r-build-re)' - '$(dn-bot-all-orgs-artifact-feeds-rw)' - '$(akams-client-id)' '$(dn-bot-all-orgs-build-rw-code-rw)' '$(System.AccessToken)' ${{parameters.CustomSensitiveDataList}} diff --git a/eng/common/templates-official/steps/get-github-app-token.yml b/eng/common/templates-official/steps/get-github-app-token.yml new file mode 100644 index 0000000000..c89f3641a4 --- /dev/null +++ b/eng/common/templates-official/steps/get-github-app-token.yml @@ -0,0 +1,7 @@ +steps: +- template: /eng/common/core-templates/steps/get-github-app-token.yml + parameters: + is1ESPipeline: true + + ${{ each parameter in parameters }}: + ${{ parameter.key }}: ${{ parameter.value }} diff --git a/eng/common/templates/steps/get-github-app-token.yml b/eng/common/templates/steps/get-github-app-token.yml new file mode 100644 index 0000000000..79e182c641 --- /dev/null +++ b/eng/common/templates/steps/get-github-app-token.yml @@ -0,0 +1,7 @@ +steps: +- template: /eng/common/core-templates/steps/get-github-app-token.yml + parameters: + is1ESPipeline: false + + ${{ each parameter in parameters }}: + ${{ parameter.key }}: ${{ parameter.value }} diff --git a/global.json b/global.json index 2a2265e9f7..7bf052ff4a 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "10.0.302", + "version": "10.0.400", "allowPrerelease": true, "rollForward": "latestFeature", "paths": [ @@ -10,10 +10,10 @@ "errorMessage": "The required .NET SDK wasn't found. Please run ./eng/common/dotnet.cmd/sh to install it." }, "tools": { - "dotnet": "10.0.302" + "dotnet": "10.0.400" }, "msbuild-sdks": { - "Microsoft.DotNet.Arcade.Sdk": "10.0.0-beta.26378.110", + "Microsoft.DotNet.Arcade.Sdk": "10.0.0-beta.26412.118", "Microsoft.Build.NoTargets": "3.7.0" } } diff --git a/src/Common/AzureDevOps/AzureDevOpsUrlParser.cs b/src/Common/AzureDevOps/AzureDevOpsUrlParser.cs index 018242fc61..cd4778bb3f 100644 --- a/src/Common/AzureDevOps/AzureDevOpsUrlParser.cs +++ b/src/Common/AzureDevOps/AzureDevOpsUrlParser.cs @@ -110,11 +110,17 @@ public static bool TryParseOnPremHttp(string relativeUrl, string virtualDirector return true; } - public static bool TryParseHostedSsh(Uri uri, [NotNullWhen(true)]out string? account, [NotNullWhen(true)]out string? repositoryPath, [NotNullWhen(true)]out string? repositoryName) + public static bool TryParseHostedSsh( + Uri uri, + [NotNullWhen(true)] out string? account, + [NotNullWhen(true)] out string? repositoryPath, + [NotNullWhen(true)] out string? repositoryName, + out bool isUnsupportedFormat) { NullableDebug.Assert(uri != null); account = repositoryPath = repositoryName = null; + isUnsupportedFormat = false; // {"DefaultCollection"|""}/{repositoryPath}/"_ssh"/{"_full"|"_optimized"}/{repositoryName} if (!UriUtilities.TrySplitRelativeUrl(uri.GetPath(), out var parts) || parts.Length == 0) @@ -125,7 +131,7 @@ public static bool TryParseHostedSsh(Uri uri, [NotNullWhen(true)]out string? acc // Check for v3 url format if (parts[0] == "v3" && parts.Length >= 3 && - TryParsePath(parts, 2, type: null, out repositoryPath, out repositoryName) && + TryParsePath(parts, startIndex: 2, type: null, out repositoryPath, out repositoryName) && repositoryPath != "") { // ssh://{user}@{domain}:{port}/v3/{account}/{repositoryPath}/{'_full'|'_optimized'|''}/{repositoryName} @@ -134,11 +140,9 @@ public static bool TryParseHostedSsh(Uri uri, [NotNullWhen(true)]out string? acc else { // ssh v1/v2 url formats - // ssh://{account}@vs-ssh.visualstudio.com/ + // ssh://{account}@vs-ssh.visualstudio.com - account = uri.UserInfo; - - var index = 0; + int index = 0; if (StringComparer.OrdinalIgnoreCase.Equals(parts[0], "DefaultCollection")) { index++; @@ -149,14 +153,15 @@ public static bool TryParseHostedSsh(Uri uri, [NotNullWhen(true)]out string? acc // Failed to parse path return false; } - } - if (account.Length == 0) - { + // The format uses SSH connection user name as an account name. + // It is no longer supported since GitOperations.GetRepositoryUrl strips the user info + // to prevent leaking credentials. + isUnsupportedFormat = true; return false; } - return true; + return account.Length > 0; } public static bool TryParseOnPremSsh(Uri uri, [NotNullWhen(true)]out string? repositoryPath, [NotNullWhen(true)]out string? repositoryName) diff --git a/src/Microsoft.Build.Tasks.Git.UnitTests/GitOperationsTests.cs b/src/Microsoft.Build.Tasks.Git.UnitTests/GitOperationsTests.cs index 0832293988..ccb95432cc 100644 --- a/src/Microsoft.Build.Tasks.Git.UnitTests/GitOperationsTests.cs +++ b/src/Microsoft.Build.Tasks.Git.UnitTests/GitOperationsTests.cs @@ -224,23 +224,26 @@ public void GetRepositoryUrl_UnsupportedUrl(string kind) } [Theory] - [InlineData("https://github.com/org/repo")] - [InlineData("http://github.com/org/repo")] - [InlineData("http://github.com:102/org/repo")] - [InlineData("ssh://user@github.com/org/repo")] - [InlineData("abc://user@github.com/org/repo")] - public void NormalizeUrl_PlatformAgnostic1(string url) + [InlineData("https://github.com/org/repo", "https://github.com/org/repo")] + [InlineData("http://github.com/org/repo", "http://github.com/org/repo")] + [InlineData("http://github.com:102/org/repo", "http://github.com:102/org/repo")] + [InlineData("ssh://user@github.com/org/repo", "ssh://git@github.com/org/repo")] // "user" replaced with "git" in SSH URL + [InlineData("abc://user@github.com/org/repo", "abc://github.com/org/repo")] + public void NormalizeUrl_PlatformAgnostic1(string url, string expected) { - AssertEx.AreEqual(url, GitOperations.NormalizeUrl(url, s_root)?.AbsoluteUri); + AssertEx.AreEqual(expected, GitOperations.NormalizeUrl(url, s_root)?.AbsoluteUri); } [Theory] [InlineData("http://?", null)] [InlineData("https://github.com/org/repo/./.", "https://github.com/org/repo/")] [InlineData("http://github.com/org/" + TestStrings.RepoName, "http://github.com/org/" + TestStrings.RepoNameFullyEscaped)] - [InlineData("ssh://github.com/org/../repo", "ssh://github.com/repo")] - [InlineData("ssh://github.com/%32/repo", "ssh://github.com/2/repo")] - [InlineData("ssh://github.com/%3F/repo", "ssh://github.com/%3F/repo")] + [InlineData("ssh://github.com/org/../repo", "ssh://git@github.com/repo")] + [InlineData("ssh://github.com/%32/repo", "ssh://git@github.com/2/repo")] + [InlineData("ssh://github.com/%3F/repo", "ssh://git@github.com/%3F/repo")] + [InlineData(@"../.:./../../relative/path", null)] + [InlineData(@".:/../../relative/path", null)] + [InlineData(@"..:/../../relative/path", null)] public void NormalizeUrl_PlatformAgnostic2(string url, string? expectedUrl) { AssertEx.AreEqual(expectedUrl, GitOperations.NormalizeUrl(url, s_root)?.AbsoluteUri); @@ -269,14 +272,14 @@ public void NormalizeUrl_Unix(string url, string expectedUrl) } [Theory] - [InlineData("abc:org/repo", "ssh://abc/org/repo")] - [InlineData("abc:org/x%20y", "ssh://abc/org/x%20y")] - [InlineData("ABC:ORG/REPO/X/Y", "ssh://abc/ORG/REPO/X/Y")] - [InlineData("github.com:org/repo", "ssh://github.com/org/repo")] - [InlineData("git@github.com:org/repo", "ssh://git@github.com/org/repo")] - [InlineData("@github.com:org/repo", "ssh://@github.com/org/repo")] - [InlineData("http:x//y", "ssh://http/x//y")] - public void GetRepositoryUrl_ScpSyntax(string url, string expectedUrl) + [InlineData("abc:org/repo", "ssh://git@abc/org/repo")] + [InlineData("abc:org/x%20y", "ssh://git@abc/org/x%20y")] + [InlineData("ABC:ORG/REPO/X/Y", "ssh://git@abc/ORG/REPO/X/Y")] + [InlineData("github.com:org/repo", "ssh://git@github.com/org/repo")] + [InlineData("user@github.com:org/repo", "ssh://git@github.com/org/repo")] // "user" replaced with "git" in SSH URL + [InlineData("@github.com:org/repo", "ssh://git@github.com/org/repo")] + [InlineData("http:x//y", "ssh://git@http/x//y")] + public void NormalizeUrl_ScpSyntax(string url, string expectedUrl) { Assert.Equal(expectedUrl, GitOperations.NormalizeUrl(url, s_root)?.AbsoluteUri); } @@ -398,7 +401,7 @@ public void GetSourceRoots_RepoWithoutCommitsWithSubmodules() // URLs listed in .submodules are ignored (they are used by git submodule initialize to generate URLs stored in config). AssertEx.Equal(new[] { - $@"'{_workingDir}{s}sub{s}1{s}' SourceControl='git' RevisionId='1111111111111111111111111111111111111111' NestedRoot='sub/1/' ContainingRoot='{_workingDir}{s}' ScmRepositoryUrl='ssh://github.com/sub-1'", + $@"'{_workingDir}{s}sub{s}1{s}' SourceControl='git' RevisionId='1111111111111111111111111111111111111111' NestedRoot='sub/1/' ContainingRoot='{_workingDir}{s}' ScmRepositoryUrl='ssh://git@github.com/sub-1'", $@"'{_workingDir}{s}sub{s}3{s}' SourceControl='git' RevisionId='3333333333333333333333333333333333333333' NestedRoot='sub/3/' ContainingRoot='{_workingDir}{s}' ScmRepositoryUrl='https://github.com/sub-3'", $@"'{_workingDir}{s}sub{s}6{s}' SourceControl='git' RevisionId='6666666666666666666666666666666666666666' NestedRoot='sub/6/' ContainingRoot='{_workingDir}{s}' ScmRepositoryUrl='https://github.com/sub-6'", }, items.Select(TestUtilities.InspectSourceRoot)); diff --git a/src/Microsoft.Build.Tasks.Git/GitOperations.cs b/src/Microsoft.Build.Tasks.Git/GitOperations.cs index fc411c6ba3..12000fdaa0 100644 --- a/src/Microsoft.Build.Tasks.Git/GitOperations.cs +++ b/src/Microsoft.Build.Tasks.Git/GitOperations.cs @@ -164,7 +164,35 @@ internal static string ApplyInsteadOfUrlMapping(GitConfig config, string url) private static bool IsSupportedScheme(string scheme) => scheme is "http" or "https" or "ssh" or "git"; + // internal for testing internal static Uri? NormalizeUrl(string url, string root) + { + var normalizedUrl = NormalizeUrlImpl(url, root); + if (normalizedUrl == null) + { + return null; + } + + // remove user info to avoid embedding access tokens to build artifacts: + var builder = new UriBuilder(normalizedUrl) + { + // If user name is not specified in SSH URL, the local user name is used for connecting to the repo. + // Use "git" placeholder instead of a specific user name. + UserName = normalizedUrl.Scheme is "ssh" ? "git" : null, + Password = null, + }; + + try + { + return builder.Uri; + } + catch + { + return null; + } + } + + private static Uri? NormalizeUrlImpl(string url, string root) { // Since git supports scp-like syntax for SSH URLs we convert it here, // so that RepositoryUrl is actually a valid URL in that case. diff --git a/src/SourceLink.AzureRepos.Git.UnitTests/AzureDevOpsUrlParserHostedTests.cs b/src/SourceLink.AzureRepos.Git.UnitTests/AzureDevOpsUrlParserHostedTests.cs index 30f9184686..c3158e4d2c 100644 --- a/src/SourceLink.AzureRepos.Git.UnitTests/AzureDevOpsUrlParserHostedTests.cs +++ b/src/SourceLink.AzureRepos.Git.UnitTests/AzureDevOpsUrlParserHostedTests.cs @@ -99,37 +99,42 @@ public void TryParseHostedSsh_Error(string url) } [Theory] - [InlineData("ssh://account@vs-ssh.visualstudio.com/project/_ssh/repo", "account", "project", "repo")] - [InlineData("ssh://account@vs-ssh.visualstudio.com/project/team/_ssh/repo", "account", "project/team", "repo")] - [InlineData("ssh://account@vs-ssh.visualstudio.com/DefaultCollection/project/_ssh/repo", "account", "project", "repo")] - [InlineData("ssh://account@vs-ssh.visualstudio.com/DefaultCollection/project/team/_ssh/repo", "account", "project/team", "repo")] - [InlineData("ssh://account@vs-ssh.visualstudio.com/DefaultCollection/project/team/_ssh/_full/repo", "account", "project/team", "repo")] - [InlineData("ssh://account@vs-ssh.visualstudio.com/DefaultCollection/project/team/_ssh/_optimized/repo", "account", "project/team", "repo")] - [InlineData("ssh://account@vs-ssh.visualstudio.com/DefaultCollection/_ssh/repo", "account", "", "repo")] - [InlineData("ssh://account@vs-ssh.visualstudio.com/_ssh/repo", "account", "", "repo")] - - [InlineData("ssh://account@vs-ssh.vsts.me/project/_ssh/repo", "account", "project", "repo")] - [InlineData("ssh://account@vs-ssh.vsts.me/project/team/_ssh/repo", "account", "project/team", "repo")] - [InlineData("ssh://account@vs-ssh.vsts.me/DefaultCollection/project/_ssh/repo", "account", "project", "repo")] - [InlineData("ssh://account@vs-ssh.vsts.me/DefaultCollection/project/team/_ssh/repo", "account", "project/team", "repo")] - [InlineData("ssh://account@vs-ssh.vsts.me/DefaultCollection/project/team/_ssh/_full/repo", "account", "project/team", "repo")] - [InlineData("ssh://account@vs-ssh.vsts.me/DefaultCollection/project/team/_ssh/_optimized/repo", "account", "project/team", "repo")] - [InlineData("ssh://account@vs-ssh.vsts.me/DefaultCollection/_ssh/repo", "account", "", "repo")] - [InlineData("ssh://account@vs-ssh.vsts.me/_ssh/repo", "account", "", "repo")] - - [InlineData("ssh://account@ssh.contoso.com/project/_ssh/repo", "account", "project", "repo")] - [InlineData("ssh://account@ssh.contoso.com/project/team/_ssh/repo", "account", "project/team", "repo")] - [InlineData("ssh://account@ssh.contoso.com/project/team/_ssh/_full/repo", "account", "project/team", "repo")] - [InlineData("ssh://account@ssh.contoso.com/project/team/_ssh/_optimized/repo", "account", "project/team", "repo")] + [InlineData("ssh://account@vs-ssh.visualstudio.com/project/_ssh/repo")] + [InlineData("ssh://account@vs-ssh.visualstudio.com/project/team/_ssh/repo")] + [InlineData("ssh://account@vs-ssh.visualstudio.com/DefaultCollection/project/_ssh/repo")] + [InlineData("ssh://account@vs-ssh.visualstudio.com/DefaultCollection/project/team/_ssh/repo")] + [InlineData("ssh://account@vs-ssh.visualstudio.com/DefaultCollection/project/team/_ssh/_full/repo")] + [InlineData("ssh://account@vs-ssh.visualstudio.com/DefaultCollection/project/team/_ssh/_optimized/repo")] + [InlineData("ssh://account@vs-ssh.visualstudio.com/DefaultCollection/_ssh/repo")] + [InlineData("ssh://account@vs-ssh.visualstudio.com/_ssh/repo")] + [InlineData("ssh://account@vs-ssh.vsts.me/project/_ssh/repo")] + [InlineData("ssh://account@vs-ssh.vsts.me/project/team/_ssh/repo")] + [InlineData("ssh://account@vs-ssh.vsts.me/DefaultCollection/project/_ssh/repo")] + [InlineData("ssh://account@vs-ssh.vsts.me/DefaultCollection/project/team/_ssh/repo")] + [InlineData("ssh://account@vs-ssh.vsts.me/DefaultCollection/project/team/_ssh/_full/repo")] + [InlineData("ssh://account@vs-ssh.vsts.me/DefaultCollection/project/team/_ssh/_optimized/repo")] + [InlineData("ssh://account@vs-ssh.vsts.me/DefaultCollection/_ssh/repo")] + [InlineData("ssh://account@vs-ssh.vsts.me/_ssh/repo")] + [InlineData("ssh://account@ssh.contoso.com/project/_ssh/repo")] + [InlineData("ssh://account@ssh.contoso.com/project/team/_ssh/repo")] + [InlineData("ssh://account@ssh.contoso.com/project/team/_ssh/_full/repo")] + [InlineData("ssh://account@ssh.contoso.com/project/team/_ssh/_optimized/repo")] + [InlineData("ssh://account@vs-ssh.visualstudio.com/v3/_ssh/repo")] + [InlineData("ssh://account@vs-ssh.visualstudio.com/v3/team/_ssh/repo")] + public void TryParseHostedSshV1V2_Success(string url) + { + Assert.False(AzureDevOpsUrlParser.TryParseHostedSsh( + new Uri(url, UriKind.Absolute), out _, out _, out _, out var isUnsupportedFormat)); + Assert.True(isUnsupportedFormat); + } - [InlineData("ssh://account@vs-ssh.visualstudio.com/v3/_ssh/repo", "account", "v3", "repo")] - [InlineData("ssh://account@vs-ssh.visualstudio.com/v3/team/_ssh/repo", "account", "v3/team", "repo")] - public void TryParseHostedSshV1V2_Success(string url, string account, string repositoryPath, string repositoryName) + [Theory] + [InlineData("ssh://account@vs-ssh.visualstudio.com/v4/team/_ssh")] + public void TryParseHostedSshV1V2_Error(string url) { - Assert.True(AzureDevOpsUrlParser.TryParseHostedSsh(new Uri(url, UriKind.Absolute), out var actualAccount, out var actualRepositoryPath, out var actualRepositoryName)); - Assert.Equal(account, actualAccount); - Assert.Equal(repositoryPath, actualRepositoryPath); - Assert.Equal(repositoryName, actualRepositoryName); + Assert.False(AzureDevOpsUrlParser.TryParseHostedSsh( + new Uri(url, UriKind.Absolute), out _, out _, out _, out var isUnsupportedFormat)); + Assert.False(isUnsupportedFormat); } [Theory] @@ -149,7 +154,9 @@ public void TryParseHostedSshV1V2_Success(string url, string account, string rep [InlineData("ssh://account1@ssh.contoso.com/v3/account2/project/team/_optimized/repo", "account2", "project/team", "repo")] public void TryParseHostedSshV3_Success(string url, string account, string repositoryPath, string repositoryName) { - Assert.True(AzureDevOpsUrlParser.TryParseHostedSsh(new Uri(url, UriKind.Absolute), out var actualAccount, out var actualRepositoryPath, out var actualRepositoryName)); + Assert.True(AzureDevOpsUrlParser.TryParseHostedSsh( + new Uri(url, UriKind.Absolute), out var actualAccount, out var actualRepositoryPath, out var actualRepositoryName, out var isUnsupportedFormat)); + Assert.False(isUnsupportedFormat); Assert.Equal(account, actualAccount); Assert.Equal(repositoryPath, actualRepositoryPath); Assert.Equal(repositoryName, actualRepositoryName); diff --git a/src/SourceLink.AzureRepos.Git.UnitTests/TranslateRepositoryUrlsTests.cs b/src/SourceLink.AzureRepos.Git.UnitTests/TranslateRepositoryUrlsTests.cs index e2761321da..1fd26a6726 100644 --- a/src/SourceLink.AzureRepos.Git.UnitTests/TranslateRepositoryUrlsTests.cs +++ b/src/SourceLink.AzureRepos.Git.UnitTests/TranslateRepositoryUrlsTests.cs @@ -18,21 +18,21 @@ public void Translate() var task = new TranslateRepositoryUrls() { BuildEngine = engine, - RepositoryUrl = "ssh://account@vs-ssh.visualstudio.com/project/team/_ssh/repo", + RepositoryUrl = "ssh://vs-ssh.visualstudio.com/v3/account/project/team/repo", IsSingleProvider = true, SourceRoots = new[] { - new MockItem("/1/", KVP("SourceControl", "git"), KVP("ScmRepositoryUrl", "ssh://account@vs-ssh.visualstudio.com:22/project/team/_ssh/repo")), // ok - new MockItem("/2/", KVP("SourceControl", "git"), KVP("ScmRepositoryUrl", "ssh://test@vs-ssh.visualstudio.com:22/project/_ssh/repo")), // ok + new MockItem("/1/", KVP("SourceControl", "git"), KVP("ScmRepositoryUrl", "ssh://vs-ssh.visualstudio.com:22/v3/account/project/team/repo")), // ok + new MockItem("/2/", KVP("SourceControl", "git"), KVP("ScmRepositoryUrl", "ssh://user@vs-ssh.visualstudio.com:22/v3/test/project/repo")), // ok new MockItem("/3/", KVP("SourceControl", "git"), KVP("ScmRepositoryUrl", "ssh://user@vs-ssh.visualstudio.com:22/v3/account/project/team/repo")), // ok - new MockItem("/4/", KVP("SourceControl", "git"), KVP("ScmRepositoryUrl", "ssh://account@vs-ssh.visualstudio.com/_ssh/repo")), // ok - new MockItem("/5/", KVP("SourceControl", "git"), KVP("ScmRepositoryUrl", "ssh://account@ssh.contoso.com:22/project/team/_ssh/repo")), // ok + new MockItem("/4/", KVP("SourceControl", "git"), KVP("ScmRepositoryUrl", "ssh://vs-ssh.visualstudio.com/v3/account/project/repo")), // ok + new MockItem("/5/", KVP("SourceControl", "git"), KVP("ScmRepositoryUrl", "ssh://ssh.contoso.com:22/v3/account/project/team/repo")), // ok new MockItem("/6/", KVP("SourceControl", "git"), KVP("ScmRepositoryUrl", "ssh://user@ssh.contoso.com/v3/account/project/team/repo")), // ok - new MockItem("/7/", KVP("SourceControl", "tfvc"), KVP("ScmRepositoryUrl", "ssh://account@vs-ssh.visualstudio.com:22/project/team/_ssh/repo")), // different source control - new MockItem("/8/", KVP("SourceControl", "git"), KVP("ScmRepositoryUrl", "ssh://account@contoso.com:22/project/team/_ssh/repo")), // no "vs-ssh." prefix - new MockItem("/9/", KVP("SourceControl", "git"), KVP("ScmRepositoryUrl", "ssh://account@vs-ssh.contoso.com:22/project/team/_ssh/repo")), // known host, but not visualstudio.com - new MockItem("/A/", KVP("SourceControl", "git"), KVP("ScmRepositoryUrl", "ssh://account@vs-ssh.contoso2.com:22/project/team/_ssh/repo")), // unknown host - new MockItem("/B/", KVP("SourceControl", "git"), KVP("ScmRepositoryUrl", "ssh://account@vs-ssh.contoso.com:22/project/team/ZZZ/repo")), // bad format + new MockItem("/7/", KVP("SourceControl", "tfvc"), KVP("ScmRepositoryUrl", "ssh://vs-ssh.visualstudio.com:22/v3/account/project/team/repo")), // different source control + new MockItem("/8/", KVP("SourceControl", "git"), KVP("ScmRepositoryUrl", "ssh://contoso.com:22/v3/account/project/team/repo")), // no "vs-ssh." prefix + new MockItem("/9/", KVP("SourceControl", "git"), KVP("ScmRepositoryUrl", "ssh://vs-ssh.contoso.com:22/v3/account/project/team/repo")), // known host, but not visualstudio.com + new MockItem("/A/", KVP("SourceControl", "git"), KVP("ScmRepositoryUrl", "ssh://vs-ssh.contoso2.com:22/v3/account/project/team/repo")), // unknown host + new MockItem("/B/", KVP("SourceControl", "git"), KVP("ScmRepositoryUrl", "ssh://vs-ssh.contoso.com:22/v3/account/project/team/ZZZ/repo")), // bad format }, Hosts = new[] { @@ -50,14 +50,14 @@ public void Translate() "https://account.visualstudio.com/project/team/_git/repo", "https://test.visualstudio.com/project/_git/repo", "https://account.visualstudio.com/project/team/_git/repo", - "https://account.visualstudio.com/_git/repo", + "https://account.visualstudio.com/project/_git/repo", "https://contoso.com/account/project/team/_git/repo", "https://contoso.com/account/project/team/_git/repo", - "ssh://account@vs-ssh.visualstudio.com:22/project/team/_ssh/repo", - "ssh://account@contoso.com:22/project/team/_ssh/repo", - "ssh://account@vs-ssh.contoso.com:22/project/team/_ssh/repo", - "ssh://account@vs-ssh.contoso2.com:22/project/team/_ssh/repo", - "ssh://account@vs-ssh.contoso.com:22/project/team/ZZZ/repo" + "ssh://vs-ssh.visualstudio.com:22/v3/account/project/team/repo", + "ssh://contoso.com:22/v3/account/project/team/repo", + "ssh://vs-ssh.contoso.com:22/v3/account/project/team/repo", + "ssh://vs-ssh.contoso2.com:22/v3/account/project/team/repo", + "ssh://vs-ssh.contoso.com:22/v3/account/project/team/ZZZ/repo" }, task.TranslatedSourceRoots?.Select(r => r.GetMetadata("ScmRepositoryUrl"))); Assert.True(result); diff --git a/src/SourceLink.AzureRepos.Git/Microsoft.SourceLink.AzureRepos.Git.csproj b/src/SourceLink.AzureRepos.Git/Microsoft.SourceLink.AzureRepos.Git.csproj index aee8ef8979..534b0564b0 100644 --- a/src/SourceLink.AzureRepos.Git/Microsoft.SourceLink.AzureRepos.Git.csproj +++ b/src/SourceLink.AzureRepos.Git/Microsoft.SourceLink.AzureRepos.Git.csproj @@ -16,6 +16,7 @@ + diff --git a/src/SourceLink.AzureRepos.Git/Resources.resx b/src/SourceLink.AzureRepos.Git/Resources.resx index aae4915e43..1c42f26e5d 100644 --- a/src/SourceLink.AzureRepos.Git/Resources.resx +++ b/src/SourceLink.AzureRepos.Git/Resources.resx @@ -129,4 +129,7 @@ The value passed to task parameter {0} is not a valid domain name: '{1}' + + Remote URL '{0}' appears to be in a format that is no longer supported. + \ No newline at end of file diff --git a/src/SourceLink.AzureRepos.Git/TranslateRepositoryUrls.cs b/src/SourceLink.AzureRepos.Git/TranslateRepositoryUrls.cs index 9a345fbaa1..543a72a14d 100644 --- a/src/SourceLink.AzureRepos.Git/TranslateRepositoryUrls.cs +++ b/src/SourceLink.AzureRepos.Git/TranslateRepositoryUrls.cs @@ -10,9 +10,9 @@ namespace Microsoft.SourceLink.AzureRepos.Git public sealed class TranslateRepositoryUrls : TranslateRepositoryUrlsGitTask { // Translates - // ssh://{account}@{ssh-subdomain}.{domain}:{port}/{repositoryPath}/_ssh/{"_full"|"_optimized"}/{repositoryName} + // ssh://{user}@{domain}:{port}/v3/{account}/{repositoryPath}/{'_full'|'_optimized'|''}/{repositoryName} // to - // https://{http-domain}/{account}/{repositoryPath}/_git/{repositoryName} + // https://.../{repositoryPath}/_git/{repositoryName} // // Dommain mapping: // ssh://vs-ssh.*.com -> https://{account}.*.com @@ -27,8 +27,13 @@ public sealed class TranslateRepositoryUrls : TranslateRepositoryUrlsGitTask return null; } - if (!AzureDevOpsUrlParser.TryParseHostedSsh(uri, out var account, out var repositoryPath, out var repositoryName)) + if (!AzureDevOpsUrlParser.TryParseHostedSsh(uri, out var account, out var repositoryPath, out var repositoryName, out var isUnsupportedFormat)) { + if (isUnsupportedFormat) + { + throw new NotSupportedException(string.Format(Resources.RemoteUrlFormatNoLongerSupported, uri.AbsoluteUri)); + } + return null; } diff --git a/src/SourceLink.AzureRepos.Git/xlf/Resources.cs.xlf b/src/SourceLink.AzureRepos.Git/xlf/Resources.cs.xlf index aef5fb2ead..aba88fb561 100644 --- a/src/SourceLink.AzureRepos.Git/xlf/Resources.cs.xlf +++ b/src/SourceLink.AzureRepos.Git/xlf/Resources.cs.xlf @@ -7,6 +7,11 @@ Hodnota proměnné prostředí {0} není správně formátovaný seznam párů adres URL: {1}" + + Remote URL '{0}' appears to be in a format that is no longer supported. + Remote URL '{0}' appears to be in a format that is no longer supported. + + The value of {0} with identity '{1}' is invalid: '{2}'" Hodnota {0} s identitou {1} je neplatná: {2}" diff --git a/src/SourceLink.AzureRepos.Git/xlf/Resources.de.xlf b/src/SourceLink.AzureRepos.Git/xlf/Resources.de.xlf index f9d6cd8ca1..2165061490 100644 --- a/src/SourceLink.AzureRepos.Git/xlf/Resources.de.xlf +++ b/src/SourceLink.AzureRepos.Git/xlf/Resources.de.xlf @@ -7,6 +7,11 @@ Der Wert der Umgebungsvariablen "{0}" ist keine wohlgeformte Liste mit URL-Paaren: "{1}" + + Remote URL '{0}' appears to be in a format that is no longer supported. + Remote URL '{0}' appears to be in a format that is no longer supported. + + The value of {0} with identity '{1}' is invalid: '{2}'" Der Wert von "{0}" mit der Identität "{1}" ist ungültig: "{2}" diff --git a/src/SourceLink.AzureRepos.Git/xlf/Resources.es.xlf b/src/SourceLink.AzureRepos.Git/xlf/Resources.es.xlf index cf3efa11f2..4d672c6b74 100644 --- a/src/SourceLink.AzureRepos.Git/xlf/Resources.es.xlf +++ b/src/SourceLink.AzureRepos.Git/xlf/Resources.es.xlf @@ -7,6 +7,11 @@ El valor de la variable de entorno {0} no es una lista bien formada de pares de direcciones URL: '{1}'" + + Remote URL '{0}' appears to be in a format that is no longer supported. + Remote URL '{0}' appears to be in a format that is no longer supported. + + The value of {0} with identity '{1}' is invalid: '{2}'" El valor de {0} con identidad '{1}' no es válido '{2}'" diff --git a/src/SourceLink.AzureRepos.Git/xlf/Resources.fr.xlf b/src/SourceLink.AzureRepos.Git/xlf/Resources.fr.xlf index 3cfb005474..681e6ee0ef 100644 --- a/src/SourceLink.AzureRepos.Git/xlf/Resources.fr.xlf +++ b/src/SourceLink.AzureRepos.Git/xlf/Resources.fr.xlf @@ -7,6 +7,11 @@ La valeur de la variable d'environnement {0} n'est pas une liste correctement formée de paires d'URL : '{1}'" + + Remote URL '{0}' appears to be in a format that is no longer supported. + Remote URL '{0}' appears to be in a format that is no longer supported. + + The value of {0} with identity '{1}' is invalid: '{2}'" La valeur de {0} avec l'identité '{1}' n'est pas valide : '{2}'" diff --git a/src/SourceLink.AzureRepos.Git/xlf/Resources.it.xlf b/src/SourceLink.AzureRepos.Git/xlf/Resources.it.xlf index d63957052b..899c383c81 100644 --- a/src/SourceLink.AzureRepos.Git/xlf/Resources.it.xlf +++ b/src/SourceLink.AzureRepos.Git/xlf/Resources.it.xlf @@ -7,6 +7,11 @@ Il valore della variabile di ambiente {0} non è un elenco ben formato di coppie di URL: '{1}'" + + Remote URL '{0}' appears to be in a format that is no longer supported. + Remote URL '{0}' appears to be in a format that is no longer supported. + + The value of {0} with identity '{1}' is invalid: '{2}'" Il valore di {0} con identità '{1}' non è valido: '{2}'" diff --git a/src/SourceLink.AzureRepos.Git/xlf/Resources.ja.xlf b/src/SourceLink.AzureRepos.Git/xlf/Resources.ja.xlf index a46569e801..ec7c89992d 100644 --- a/src/SourceLink.AzureRepos.Git/xlf/Resources.ja.xlf +++ b/src/SourceLink.AzureRepos.Git/xlf/Resources.ja.xlf @@ -7,6 +7,11 @@ 環境変数 {0} の値は正しい形式の URL のペアの一覧ではありません: '{1}'" + + Remote URL '{0}' appears to be in a format that is no longer supported. + Remote URL '{0}' appears to be in a format that is no longer supported. + + The value of {0} with identity '{1}' is invalid: '{2}'" ID '{1}' の {0} の値は無効です: '{2}'" diff --git a/src/SourceLink.AzureRepos.Git/xlf/Resources.ko.xlf b/src/SourceLink.AzureRepos.Git/xlf/Resources.ko.xlf index 8e025896ad..0b3184aff1 100644 --- a/src/SourceLink.AzureRepos.Git/xlf/Resources.ko.xlf +++ b/src/SourceLink.AzureRepos.Git/xlf/Resources.ko.xlf @@ -7,6 +7,11 @@ 환경 변수 {0}의 값이 잘 구성된(Well-Formed) URL 쌍 목록이 아닙니다. '{1}'" + + Remote URL '{0}' appears to be in a format that is no longer supported. + Remote URL '{0}' appears to be in a format that is no longer supported. + + The value of {0} with identity '{1}' is invalid: '{2}'" ID가 '{1}'인 {0}의 값이 잘못되었습니다. '{2}'" diff --git a/src/SourceLink.AzureRepos.Git/xlf/Resources.pl.xlf b/src/SourceLink.AzureRepos.Git/xlf/Resources.pl.xlf index 16889347ff..a8d98b0fde 100644 --- a/src/SourceLink.AzureRepos.Git/xlf/Resources.pl.xlf +++ b/src/SourceLink.AzureRepos.Git/xlf/Resources.pl.xlf @@ -7,6 +7,11 @@ Wartość zmiennej środowiskowej {0} nie jest prawidłowo sformułowaną listą par adresów URL: „{1}”" + + Remote URL '{0}' appears to be in a format that is no longer supported. + Remote URL '{0}' appears to be in a format that is no longer supported. + + The value of {0} with identity '{1}' is invalid: '{2}'" Wartość elementu {0} z tożsamością „{1}” jest nieprawidłowa: „{2}”" diff --git a/src/SourceLink.AzureRepos.Git/xlf/Resources.pt-BR.xlf b/src/SourceLink.AzureRepos.Git/xlf/Resources.pt-BR.xlf index 38d4241ec7..78a60304fd 100644 --- a/src/SourceLink.AzureRepos.Git/xlf/Resources.pt-BR.xlf +++ b/src/SourceLink.AzureRepos.Git/xlf/Resources.pt-BR.xlf @@ -7,6 +7,11 @@ O valor da variável de ambiente {0} não é uma lista bem-formada de pares de URLs: '{1}'" + + Remote URL '{0}' appears to be in a format that is no longer supported. + Remote URL '{0}' appears to be in a format that is no longer supported. + + The value of {0} with identity '{1}' is invalid: '{2}'" O valor de {0} com a identidade '{1}' é inválido: '{2}'" diff --git a/src/SourceLink.AzureRepos.Git/xlf/Resources.ru.xlf b/src/SourceLink.AzureRepos.Git/xlf/Resources.ru.xlf index 4010c44a3d..b85b11c707 100644 --- a/src/SourceLink.AzureRepos.Git/xlf/Resources.ru.xlf +++ b/src/SourceLink.AzureRepos.Git/xlf/Resources.ru.xlf @@ -7,6 +7,11 @@ Значение переменной среды {0} не является списком пар URL в корректном формате: "{1}" + + Remote URL '{0}' appears to be in a format that is no longer supported. + Remote URL '{0}' appears to be in a format that is no longer supported. + + The value of {0} with identity '{1}' is invalid: '{2}'" Значение {0} с идентификатором "{1}" недопустимо: "{2}" diff --git a/src/SourceLink.AzureRepos.Git/xlf/Resources.tr.xlf b/src/SourceLink.AzureRepos.Git/xlf/Resources.tr.xlf index bab3b68924..a4b6ff2d24 100644 --- a/src/SourceLink.AzureRepos.Git/xlf/Resources.tr.xlf +++ b/src/SourceLink.AzureRepos.Git/xlf/Resources.tr.xlf @@ -7,6 +7,11 @@ {0} ortam değişkeninin değeri doğru biçimlendirilmiş bir URL çiftleri listesi değil: '{1}'" + + Remote URL '{0}' appears to be in a format that is no longer supported. + Remote URL '{0}' appears to be in a format that is no longer supported. + + The value of {0} with identity '{1}' is invalid: '{2}'" '{1}' kimliğine sahip {0} değeri geçersiz: '{2}'" diff --git a/src/SourceLink.AzureRepos.Git/xlf/Resources.zh-Hans.xlf b/src/SourceLink.AzureRepos.Git/xlf/Resources.zh-Hans.xlf index bc963ae656..2180e87150 100644 --- a/src/SourceLink.AzureRepos.Git/xlf/Resources.zh-Hans.xlf +++ b/src/SourceLink.AzureRepos.Git/xlf/Resources.zh-Hans.xlf @@ -7,6 +7,11 @@ 环境变量 {0} 的值不是形式正确的 URL 对列表: '{1}'" + + Remote URL '{0}' appears to be in a format that is no longer supported. + Remote URL '{0}' appears to be in a format that is no longer supported. + + The value of {0} with identity '{1}' is invalid: '{2}'" 带有 '{1}' 标识的 {0} 的值无效: '{2}'" diff --git a/src/SourceLink.AzureRepos.Git/xlf/Resources.zh-Hant.xlf b/src/SourceLink.AzureRepos.Git/xlf/Resources.zh-Hant.xlf index 177fe5765a..fdcd3c45d2 100644 --- a/src/SourceLink.AzureRepos.Git/xlf/Resources.zh-Hant.xlf +++ b/src/SourceLink.AzureRepos.Git/xlf/Resources.zh-Hant.xlf @@ -7,6 +7,11 @@ 環境變數 {0} 的值不是正確格式的 URL 配對清單: '{1}'" + + Remote URL '{0}' appears to be in a format that is no longer supported. + Remote URL '{0}' appears to be in a format that is no longer supported. + + The value of {0} with identity '{1}' is invalid: '{2}'" 識別為 {1} 之 {0} 的值無效: '{2}'" diff --git a/src/SourceLink.Git.IntegrationTests/AzureReposTests.cs b/src/SourceLink.Git.IntegrationTests/AzureReposTests.cs index 4755f68001..812d0701bf 100644 --- a/src/SourceLink.Git.IntegrationTests/AzureReposTests.cs +++ b/src/SourceLink.Git.IntegrationTests/AzureReposTests.cs @@ -3,6 +3,7 @@ // See the License.txt file in the project root for more information. using System.IO; +using Microsoft.SourceLink.AzureRepos.Git; using TestUtilities; using Xunit; @@ -79,58 +80,28 @@ public void FullValidation_Https(string host) [InlineData("vsts.me")] public void FullValidation_Ssh(string host) { - // Test non - ascii characters and escapes in the URL. - // Escaped URI reserved characters should remain escaped, non-reserved characters unescaped in the results. - var repoUrl = $"ssh://test@vs-ssh.{host}:22/test-org/_ssh/test-%72epo{TestStrings.RepoName}"; - var repoName = $"test-repo{TestStrings.RepoNameEscaped}"; + var repoUrl = $"ssh://user@vs-ssh.{host}:22/test-org/_ssh/test-repo"; var repo = GitUtilities.CreateGitRepository(ProjectDir.Path, new[] { ProjectFileName }, repoUrl); var commitSha = repo.Head.Tip.Sha; VerifyValues( customProps: @" - - true - ", customTargets: "", targets: new[] { - "Build", "Pack" + "Build", }, expressions: new[] { "@(SourceRoot)", - "@(SourceRoot->'%(SourceLinkUrl)')", - "@(SourceRoot->'%(BranchName)')", - "$(SourceLink)", - "$(PrivateRepositoryUrl)", - "$(RepositoryUrl)" }, - expectedResults: new[] + expectedErrors: new[] { - NuGetPackageFolders, - ProjectSourceRoot, - $"https://test.{host}/test-org/_apis/git/repositories/{repoName}/items?api-version=1.0&versionType=commit&version={commitSha}&path=/*", - "refs/heads/main", - s_relativeSourceLinkJsonPath, - $"https://test.{host}/test-org/_git/{repoName}", - $"https://test.{host}/test-org/_git/{repoName}", + string.Format(Resources.RemoteUrlFormatNoLongerSupported, + $"ssh://git@vs-ssh.{host}:22/test-org/_ssh/test-repo") }); - - AssertEx.AreEqual( - $@"{{""documents"":{{""{ProjectSourceRoot.Replace(@"\", @"\\")}*"":""https://test.{host}/test-org/_apis/git/repositories/{repoName}/items?api-version=1.0&versionType=commit&version={commitSha}&path=/*""}}}}", - File.ReadAllText(Path.Combine(ProjectDir.Path, s_relativeSourceLinkJsonPath))); - - TestUtilities.ValidateAssemblyInformationalVersion( - Path.Combine(ProjectDir.Path, s_relativeOutputFilePath), - "1.0.0+" + commitSha); - - TestUtilities.ValidateNuSpecRepository( - Path.Combine(ProjectDir.Path, s_relativePackagePath), - type: "git", - commit: commitSha, - url: $"https://test.{host}/test-org/_git/{repoName}"); } } } diff --git a/src/SourceLink.Git.IntegrationTests/CloudHostedProvidersTests.cs b/src/SourceLink.Git.IntegrationTests/CloudHostedProvidersTests.cs index 56cac30370..94aaab06de 100644 --- a/src/SourceLink.Git.IntegrationTests/CloudHostedProvidersTests.cs +++ b/src/SourceLink.Git.IntegrationTests/CloudHostedProvidersTests.cs @@ -242,7 +242,7 @@ public void CustomTranslation() { // Test non-ascii characters and escapes in the URL. // Escaped URI reserved characters should remain escaped, non-reserved characters unescaped in the results. - var repoUrl = $"ssh://test@vs-ssh.visualstudio.com:22/test-org/_ssh/test-%72epo{TestStrings.RepoName}"; + var repoUrl = $"ssh://test@vs-ssh.visualstudio.com/v3/account/test-org/test-%72epo{TestStrings.RepoName}"; var repoName = $"test-repo{TestStrings.RepoNameEscaped}"; var repo = GitUtilities.CreateGitRepository(ProjectDir.Path, new[] { ProjectFileName }, repoUrl); diff --git a/src/SourceLink.Git.IntegrationTests/GitWebTests.cs b/src/SourceLink.Git.IntegrationTests/GitWebTests.cs index 2a5a8aa7ff..3e71cd76a7 100644 --- a/src/SourceLink.Git.IntegrationTests/GitWebTests.cs +++ b/src/SourceLink.Git.IntegrationTests/GitWebTests.cs @@ -21,7 +21,7 @@ public void FullValidation_Ssh() { // Test non-ascii characters and escapes in the URL. Escaped URI reserved characters // should remain escaped, non-reserved characters unescaped in the results. - var repoUrl = $"ssh://git@{TestStrings.DomainName}.com/test-%72epo{TestStrings.RepoName}.git"; + var repoUrl = $"ssh://user@{TestStrings.DomainName}.com/test-%72epo{TestStrings.RepoName}.git"; var repoName = $"test-repo{TestStrings.RepoNameEscaped}.git"; var repoNameFullyEscaped = $"test-repo{TestStrings.RepoNameFullyEscaped}.git"; @@ -58,6 +58,7 @@ public void FullValidation_Ssh() $"https://{TestStrings.DomainName}.com/gitweb/?p={repoName};a=blob_plain;hb={commitSha};f=*", "refs/heads/main", s_relativeSourceLinkJsonPath, + // note that "user" was replaced with "git" to avoid leaking user info $"ssh://git@{TestStrings.DomainName}.com/{repoNameFullyEscaped}", $"ssh://git@{TestStrings.DomainName}.com/{repoNameFullyEscaped}" }); @@ -70,6 +71,7 @@ public void FullValidation_Ssh() Path.Combine(ProjectDir.Path, s_relativeOutputFilePath), "1.0.0+" + commitSha); + // note that "user" was replaced with "git" to avoid leaking user info TestUtilities.ValidateNuSpecRepository( Path.Combine(ProjectDir.Path, s_relativePackagePath), type: "git", diff --git a/src/SourceLink.Git.IntegrationTests/Microsoft.SourceLink.Git.IntegrationTests.csproj b/src/SourceLink.Git.IntegrationTests/Microsoft.SourceLink.Git.IntegrationTests.csproj index 8a4acbdb24..66045a53d4 100644 --- a/src/SourceLink.Git.IntegrationTests/Microsoft.SourceLink.Git.IntegrationTests.csproj +++ b/src/SourceLink.Git.IntegrationTests/Microsoft.SourceLink.Git.IntegrationTests.csproj @@ -5,6 +5,7 @@ + diff --git a/src/TestUtilities/DotNetSdk/DotNetSdkTestBase.cs b/src/TestUtilities/DotNetSdk/DotNetSdkTestBase.cs index 914f1431c2..f53d763421 100644 --- a/src/TestUtilities/DotNetSdk/DotNetSdkTestBase.cs +++ b/src/TestUtilities/DotNetSdk/DotNetSdkTestBase.cs @@ -52,7 +52,7 @@ private static string GetLocalNuGetConfigContent(string packagesDir) => - +