diff --git a/src/constants.integrations.ts b/src/constants.integrations.ts index 6b64aea480bce..0519e6643d812 100644 --- a/src/constants.integrations.ts +++ b/src/constants.integrations.ts @@ -7,6 +7,7 @@ export enum HostingIntegrationId { export enum SelfHostedIntegrationId { GitHubEnterprise = 'github-enterprise', + CloudGitHubEnterprise = 'cloud-github-enterprise', GitLabSelfHosted = 'gitlab-self-hosted', } @@ -19,6 +20,7 @@ export type IntegrationId = HostingIntegrationId | IssueIntegrationId | SelfHost export const supportedOrderedCloudIssueIntegrationIds = [IssueIntegrationId.Jira]; export const supportedOrderedCloudIntegrationIds = [ + SelfHostedIntegrationId.CloudGitHubEnterprise, HostingIntegrationId.GitHub, HostingIntegrationId.GitLab, IssueIntegrationId.Jira, diff --git a/src/plus/integrations/authentication/github.ts b/src/plus/integrations/authentication/github.ts index 8aeabef4db25a..abce1d69c5fc6 100644 --- a/src/plus/integrations/authentication/github.ts +++ b/src/plus/integrations/authentication/github.ts @@ -69,6 +69,16 @@ export class GitHubAuthenticationProvider extends CloudIntegrationAuthentication } } +export class GitHubEnterpriseCloudAuthenticationProvider extends CloudIntegrationAuthenticationProvider { + protected override getCompletionInputTitle(): string { + throw new Error('Connect to GitHub Enterprise'); + } + + protected override get authProviderId(): SelfHostedIntegrationId.CloudGitHubEnterprise { + return SelfHostedIntegrationId.CloudGitHubEnterprise; + } +} + export class GitHubEnterpriseAuthenticationProvider extends LocalIntegrationAuthenticationProvider { protected override get authProviderId(): SelfHostedIntegrationId.GitHubEnterprise { return SelfHostedIntegrationId.GitHubEnterprise; diff --git a/src/plus/integrations/authentication/integrationAuthentication.ts b/src/plus/integrations/authentication/integrationAuthentication.ts index 215e8460b2672..5c4b386013974 100644 --- a/src/plus/integrations/authentication/integrationAuthentication.ts +++ b/src/plus/integrations/authentication/integrationAuthentication.ts @@ -338,7 +338,11 @@ export abstract class CloudIntegrationAuthenticationProvider< let session = await cloudIntegrations.getConnectionSession(this.authProviderId); // Make an exception for GitHub because they always return 0 - if (session?.expiresIn === 0 && this.authProviderId === HostingIntegrationId.GitHub) { + if ( + session?.expiresIn === 0 && + (this.authProviderId === HostingIntegrationId.GitHub || + this.authProviderId === SelfHostedIntegrationId.CloudGitHubEnterprise) + ) { // It never expires so don't refresh it frequently: session.expiresIn = maxSmallIntegerV8; // maximum expiration length } @@ -522,6 +526,11 @@ export class IntegrationAuthenticationService implements Disposable { ).GitHubAuthenticationProvider(this.container) : new BuiltInAuthenticationProvider(this.container, providerId); + break; + case SelfHostedIntegrationId.CloudGitHubEnterprise: + provider = new ( + await import(/* webpackChunkName: "integrations" */ './github') + ).GitHubEnterpriseCloudAuthenticationProvider(this.container); break; case SelfHostedIntegrationId.GitHubEnterprise: provider = new ( diff --git a/src/plus/integrations/authentication/models.ts b/src/plus/integrations/authentication/models.ts index 9c328ea0d7c99..9b340525c17a4 100644 --- a/src/plus/integrations/authentication/models.ts +++ b/src/plus/integrations/authentication/models.ts @@ -32,7 +32,7 @@ export interface CloudIntegrationConnection { domain: string; } -export type CloudIntegrationType = 'jira' | 'trello' | 'gitlab' | 'github' | 'bitbucket' | 'azure'; +export type CloudIntegrationType = 'jira' | 'trello' | 'gitlab' | 'github' | 'bitbucket' | 'azure' | 'githubEnterprise'; export type CloudIntegrationAuthType = 'oauth' | 'pat'; @@ -53,6 +53,7 @@ export const toIntegrationId: { [key in CloudIntegrationType]: IntegrationId } = trello: IssueIntegrationId.Trello, gitlab: HostingIntegrationId.GitLab, github: HostingIntegrationId.GitHub, + githubEnterprise: SelfHostedIntegrationId.CloudGitHubEnterprise, bitbucket: HostingIntegrationId.Bitbucket, azure: HostingIntegrationId.AzureDevOps, }; @@ -64,6 +65,7 @@ export const toCloudIntegrationType: { [key in IntegrationId]: CloudIntegrationT [HostingIntegrationId.GitHub]: 'github', [HostingIntegrationId.Bitbucket]: 'bitbucket', [HostingIntegrationId.AzureDevOps]: 'azure', + [SelfHostedIntegrationId.CloudGitHubEnterprise]: 'githubEnterprise', [SelfHostedIntegrationId.GitHubEnterprise]: undefined, [SelfHostedIntegrationId.GitLabSelfHosted]: undefined, }; diff --git a/src/plus/integrations/integrationService.ts b/src/plus/integrations/integrationService.ts index d1ed46b05b93a..5b449019b3bc7 100644 --- a/src/plus/integrations/integrationService.ts +++ b/src/plus/integrations/integrationService.ts @@ -90,6 +90,7 @@ export class IntegrationService implements Disposable { private async syncCloudIntegrations(forceConnect: boolean) { const connectedIntegrations = new Set(); const loggedIn = await this.container.subscription.getAuthenticationSession(); + const domains = new Map(); if (loggedIn) { const cloudIntegrations = await this.container.cloudIntegrations; const connections = await cloudIntegrations?.getConnections(); @@ -100,10 +101,13 @@ export class IntegrationService implements Disposable { // GKDev includes some integrations like "google" that we don't support if (integrationId == null) return; connectedIntegrations.add(toIntegrationId[p.provider]); + if (p.domain != null) { + domains.set(integrationId, p.domain); + } }); } - for await (const integration of this.getSupportedCloudIntegrations()) { + for await (const integration of this.getSupportedCloudIntegrations(domains)) { await integration.syncCloudConnection( connectedIntegrations.has(integration.id) ? 'connected' : 'disconnected', forceConnect, @@ -121,9 +125,9 @@ export class IntegrationService implements Disposable { return connectedIntegrations; } - private async *getSupportedCloudIntegrations() { + private async *getSupportedCloudIntegrations(domains: Map): AsyncIterable { for (const id of getSupportedCloudIntegrationIds()) { - yield this.get(id); + yield this.get(id, domains?.get(id)); } } @@ -437,6 +441,7 @@ export class IntegrationService implements Disposable { await import(/* webpackChunkName: "integrations" */ './providers/github') ).GitHubIntegration(this.container, this.authenticationService, this.getProvidersApi.bind(this)); break; + case SelfHostedIntegrationId.CloudGitHubEnterprise: case SelfHostedIntegrationId.GitHubEnterprise: if (domain == null) throw new Error(`Domain is required for '${id}' integration`); integration = new ( @@ -446,6 +451,7 @@ export class IntegrationService implements Disposable { this.authenticationService, this.getProvidersApi.bind(this), domain, + id, ); break; case HostingIntegrationId.GitLab: diff --git a/src/plus/integrations/providers/github.ts b/src/plus/integrations/providers/github.ts index a5d73cd3402b8..de99e9b003c9c 100644 --- a/src/plus/integrations/providers/github.ts +++ b/src/plus/integrations/providers/github.ts @@ -34,6 +34,11 @@ const enterpriseAuthProvider: IntegrationAuthenticationProviderDescriptor = Obje id: enterpriseMetadata.id, scopes: enterpriseMetadata.scopes, }); +const cloudEnterpriseMetadata = providersMetadata[SelfHostedIntegrationId.CloudGitHubEnterprise]; +const cloudEnterpriseAuthProvider: IntegrationAuthenticationProviderDescriptor = Object.freeze({ + id: cloudEnterpriseMetadata.id, + scopes: cloudEnterpriseMetadata.scopes, +}); export type GitHubRepositoryDescriptor = RepositoryDescriptor; @@ -294,10 +299,11 @@ export class GitHubIntegration extends GitHubIntegrationBase { - readonly authProvider = enterpriseAuthProvider; - readonly id = SelfHostedIntegrationId.GitHubEnterprise; - protected readonly key = `${this.id}:${this.domain}` as const; +export class GitHubEnterpriseIntegration extends GitHubIntegrationBase< + SelfHostedIntegrationId.GitHubEnterprise | SelfHostedIntegrationId.CloudGitHubEnterprise +> { + readonly authProvider; + protected readonly key; readonly name = 'GitHub Enterprise'; get domain(): string { return this._domain; @@ -312,8 +318,12 @@ export class GitHubEnterpriseIntegration extends GitHubIntegrationBase Promise, private readonly _domain: string, + readonly id: SelfHostedIntegrationId.GitHubEnterprise | SelfHostedIntegrationId.CloudGitHubEnterprise, ) { super(container, authenticationService, getProvidersApi); + this.key = `${this.id}:${this.domain}` as const; + this.authProvider = + this.id === SelfHostedIntegrationId.GitHubEnterprise ? enterpriseAuthProvider : cloudEnterpriseAuthProvider; } @log() diff --git a/src/plus/integrations/providers/models.ts b/src/plus/integrations/providers/models.ts index 180d0a5288504..e91415ec3ad89 100644 --- a/src/plus/integrations/providers/models.ts +++ b/src/plus/integrations/providers/models.ts @@ -349,6 +349,22 @@ export const providersMetadata: ProvidersMetadata = { supportedIssueFilters: [IssueFilter.Author, IssueFilter.Assignee, IssueFilter.Mention], scopes: ['repo', 'read:user', 'user:email'], }, + [SelfHostedIntegrationId.CloudGitHubEnterprise]: { + domain: '', + id: SelfHostedIntegrationId.CloudGitHubEnterprise, + issuesPagingMode: PagingMode.Repos, + pullRequestsPagingMode: PagingMode.Repos, + // Use 'username' property on account for PR filters + supportedPullRequestFilters: [ + PullRequestFilter.Author, + PullRequestFilter.Assignee, + PullRequestFilter.ReviewRequested, + PullRequestFilter.Mention, + ], + // Use 'username' property on account for issue filters + supportedIssueFilters: [IssueFilter.Author, IssueFilter.Assignee, IssueFilter.Mention], + scopes: ['repo', 'read:user', 'user:email'], + }, [SelfHostedIntegrationId.GitHubEnterprise]: { domain: '', id: SelfHostedIntegrationId.GitHubEnterprise, diff --git a/src/plus/integrations/providers/providersApi.ts b/src/plus/integrations/providers/providersApi.ts index cc36150538be2..ff8dfe81f7743 100644 --- a/src/plus/integrations/providers/providersApi.ts +++ b/src/plus/integrations/providers/providersApi.ts @@ -98,6 +98,20 @@ export class ProvidersApi { providerApis.github, ) as GetIssuesForReposFn, }, + [SelfHostedIntegrationId.CloudGitHubEnterprise]: { + ...providersMetadata[SelfHostedIntegrationId.GitHubEnterprise], + provider: providerApis.github, + getCurrentUserFn: providerApis.github.getCurrentUser.bind(providerApis.github) as GetCurrentUserFn, + getPullRequestsForReposFn: providerApis.github.getPullRequestsForRepos.bind( + providerApis.github, + ) as GetPullRequestsForReposFn, + getPullRequestsForUserFn: providerApis.github.getPullRequestsAssociatedWithUser.bind( + providerApis.github, + ) as GetPullRequestsForUserFn, + getIssuesForReposFn: providerApis.github.getIssuesForRepos.bind( + providerApis.github, + ) as GetIssuesForReposFn, + }, [SelfHostedIntegrationId.GitHubEnterprise]: { ...providersMetadata[SelfHostedIntegrationId.GitHubEnterprise], provider: providerApis.github,