diff --git a/src/constants.integrations.ts b/src/constants.integrations.ts index 89770d71ba757..afc5b4fc98352 100644 --- a/src/constants.integrations.ts +++ b/src/constants.integrations.ts @@ -29,6 +29,7 @@ export const supportedOrderedCloudIntegrationIds = [ SelfHostedIntegrationId.CloudGitHubEnterprise, HostingIntegrationId.GitLab, SelfHostedIntegrationId.CloudGitLabSelfHosted, + HostingIntegrationId.AzureDevOps, IssueIntegrationId.Jira, ]; @@ -71,6 +72,12 @@ export const supportedCloudIntegrationDescriptors: IntegrationDescriptor[] = [ icon: 'gl-provider-gitlab', supports: ['prs', 'issues'], }, + { + id: HostingIntegrationId.AzureDevOps, + name: 'Azure DevOps', + icon: 'gl-provider-azdo', + supports: ['prs', 'issues'], + }, { id: IssueIntegrationId.Jira, name: 'Jira', diff --git a/src/plus/integrations/authentication/azureDevOps.ts b/src/plus/integrations/authentication/azureDevOps.ts index 0bfd1794c28a5..64d48e8e3fda4 100644 --- a/src/plus/integrations/authentication/azureDevOps.ts +++ b/src/plus/integrations/authentication/azureDevOps.ts @@ -1,112 +1,8 @@ -import type { Disposable, QuickInputButton } from 'vscode'; -import { env, ThemeIcon, Uri, window } from 'vscode'; import { HostingIntegrationId } from '../../../constants.integrations'; -import { base64 } from '../../../system/string'; -import type { IntegrationAuthenticationSessionDescriptor } from './integrationAuthenticationProvider'; -import { LocalIntegrationAuthenticationProvider } from './integrationAuthenticationProvider'; -import type { ProviderAuthenticationSession } from './models'; +import { CloudIntegrationAuthenticationProvider } from './integrationAuthenticationProvider'; -export class AzureDevOpsAuthenticationProvider extends LocalIntegrationAuthenticationProvider { +export class AzureDevOpsAuthenticationProvider extends CloudIntegrationAuthenticationProvider { protected override get authProviderId(): HostingIntegrationId.AzureDevOps { return HostingIntegrationId.AzureDevOps; } - - override async createSession( - descriptor: IntegrationAuthenticationSessionDescriptor, - ): Promise { - let azureOrganization: string | undefined = descriptor.organization as string | undefined; - if (!azureOrganization) { - const orgInput = window.createInputBox(); - orgInput.ignoreFocusOut = true; - const orgInputDisposables: Disposable[] = []; - try { - azureOrganization = await new Promise(resolve => { - orgInputDisposables.push( - orgInput.onDidHide(() => resolve(undefined)), - orgInput.onDidChangeValue(() => (orgInput.validationMessage = undefined)), - orgInput.onDidAccept(() => { - const value = orgInput.value.trim(); - if (!value) { - orgInput.validationMessage = 'An organization is required'; - return; - } - - resolve(value); - }), - ); - - orgInput.title = `Azure DevOps Authentication \u2022 ${descriptor.domain}`; - orgInput.placeholder = 'Organization'; - orgInput.prompt = 'Enter your Azure DevOps organization'; - orgInput.show(); - }); - } finally { - orgInput.dispose(); - orgInputDisposables.forEach(d => void d.dispose()); - } - } - - if (!azureOrganization) return undefined; - - const tokenInput = window.createInputBox(); - tokenInput.ignoreFocusOut = true; - - const disposables: Disposable[] = []; - - let token; - try { - const infoButton: QuickInputButton = { - iconPath: new ThemeIcon(`link-external`), - tooltip: 'Open the Azure DevOps Access Tokens Page', - }; - - token = await new Promise(resolve => { - disposables.push( - tokenInput.onDidHide(() => resolve(undefined)), - tokenInput.onDidChangeValue(() => (tokenInput.validationMessage = undefined)), - tokenInput.onDidAccept(() => { - const value = tokenInput.value.trim(); - if (!value) { - tokenInput.validationMessage = 'A personal access token is required'; - return; - } - - resolve(value); - }), - tokenInput.onDidTriggerButton(e => { - if (e === infoButton) { - void env.openExternal( - Uri.parse(`https://${descriptor.domain}/${azureOrganization}/_usersSettings/tokens`), - ); - } - }), - ); - - tokenInput.password = true; - tokenInput.title = `Azure DevOps Authentication \u2022 ${descriptor.domain}`; - tokenInput.placeholder = `Requires ${descriptor.scopes.join(', ') ?? 'all'} scopes`; - tokenInput.prompt = `Paste your [Azure DevOps Personal Access Token](https://${descriptor.domain}/${azureOrganization}/_usersSettings/tokens "Get your Azure DevOps Access Token")`; - tokenInput.buttons = [infoButton]; - - tokenInput.show(); - }); - } finally { - tokenInput.dispose(); - disposables.forEach(d => void d.dispose()); - } - - if (!token) return undefined; - - return { - id: this.configuredIntegrationService.getSessionId(descriptor), - accessToken: base64(`:${token}`), - scopes: descriptor.scopes, - account: { - id: '', - label: '', - }, - cloud: false, - domain: descriptor.domain, - }; - } } diff --git a/src/plus/integrations/integrationService.ts b/src/plus/integrations/integrationService.ts index e7cefb0bec8be..b92784a1d5cd6 100644 --- a/src/plus/integrations/integrationService.ts +++ b/src/plus/integrations/integrationService.ts @@ -48,6 +48,7 @@ import type { SupportedIssueIntegrationIds, SupportedSelfHostedIntegrationIds, } from './integration'; +import { isAzureCloudDomain } from './providers/azureDevOps'; import { isCloudSelfHostedIntegrationId, isHostingIntegrationId, isSelfHostedIntegrationId } from './providers/models'; import type { ProvidersApi } from './providers/providersApi'; import { isGitHubDotCom, isGitLabDotCom } from './providers/utils'; @@ -668,10 +669,13 @@ export class IntegrationService implements Disposable { switch (remote.provider.id) { // TODO: Uncomment when we support these integrations - // case 'azure-devops': - // return get(HostingIntegrationId.AzureDevOps) as RT; // case 'bitbucket': // return get(HostingIntegrationId.Bitbucket) as RT; + case 'azure-devops': + if (isAzureCloudDomain(remote.provider.domain)) { + return get(HostingIntegrationId.AzureDevOps) as RT; + } + return (getOrGetCached === this.get ? Promise.resolve(undefined) : undefined) as RT; case 'github': if (remote.provider.domain != null && !isGitHubDotCom(remote.provider.domain)) { return get( @@ -1030,10 +1034,10 @@ export function remoteProviderIdToIntegrationId( ): SupportedCloudIntegrationIds | undefined { switch (remoteProviderId) { // TODO: Uncomment when we support these integrations - // case 'azure-devops': - // return HostingIntegrationId.AzureDevOps; // case 'bitbucket': // return HostingIntegrationId.Bitbucket; + case 'azure-devops': + return HostingIntegrationId.AzureDevOps; case 'github': return HostingIntegrationId.GitHub; case 'gitlab': diff --git a/src/plus/integrations/providers/azureDevOps.ts b/src/plus/integrations/providers/azureDevOps.ts index 60d9f8fb7ed9d..a0413b1f000ce 100644 --- a/src/plus/integrations/providers/azureDevOps.ts +++ b/src/plus/integrations/providers/azureDevOps.ts @@ -158,3 +158,8 @@ export class AzureDevOpsIntegration extends HostingIntegration< return Promise.resolve(undefined); } } + +const azureCloudDomainRegex = /^dev\.azure\.com$|\bvisualstudio\.com$/i; +export function isAzureCloudDomain(domain: string | undefined): boolean { + return domain != null && azureCloudDomainRegex.test(domain); +}