Skip to content

Commit b8dd1b0

Browse files
committed
Adds support for GKDev Cloud GitHub Enterprise integration
(#3901, #3922)
1 parent 3ebae2f commit b8dd1b0

9 files changed

+100
-10
lines changed

src/constants.integrations.ts

+2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export enum HostingIntegrationId {
77

88
export enum SelfHostedIntegrationId {
99
GitHubEnterprise = 'github-enterprise',
10+
CloudGitHubEnterprise = 'cloud-github-enterprise',
1011
GitLabSelfHosted = 'gitlab-self-hosted',
1112
}
1213

@@ -19,6 +20,7 @@ export type IntegrationId = HostingIntegrationId | IssueIntegrationId | SelfHost
1920

2021
export const supportedOrderedCloudIssueIntegrationIds = [IssueIntegrationId.Jira];
2122
export const supportedOrderedCloudIntegrationIds = [
23+
SelfHostedIntegrationId.CloudGitHubEnterprise,
2224
HostingIntegrationId.GitHub,
2325
HostingIntegrationId.GitLab,
2426
IssueIntegrationId.Jira,

src/plus/integrations/authentication/github.ts

+10
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,16 @@ export class GitHubAuthenticationProvider extends CloudIntegrationAuthentication
6969
}
7070
}
7171

72+
export class GitHubEnterpriseCloudAuthenticationProvider extends CloudIntegrationAuthenticationProvider<SelfHostedIntegrationId.CloudGitHubEnterprise> {
73+
protected override getCompletionInputTitle(): string {
74+
throw new Error('Connect to GitHub Enterprise');
75+
}
76+
77+
protected override get authProviderId(): SelfHostedIntegrationId.CloudGitHubEnterprise {
78+
return SelfHostedIntegrationId.CloudGitHubEnterprise;
79+
}
80+
}
81+
7282
export class GitHubEnterpriseAuthenticationProvider extends LocalIntegrationAuthenticationProvider<SelfHostedIntegrationId.GitHubEnterprise> {
7383
protected override get authProviderId(): SelfHostedIntegrationId.GitHubEnterprise {
7484
return SelfHostedIntegrationId.GitHubEnterprise;

src/plus/integrations/authentication/integrationAuthentication.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,11 @@ export abstract class CloudIntegrationAuthenticationProvider<
338338
let session = await cloudIntegrations.getConnectionSession(this.authProviderId);
339339

340340
// Make an exception for GitHub because they always return 0
341-
if (session?.expiresIn === 0 && this.authProviderId === HostingIntegrationId.GitHub) {
341+
if (
342+
session?.expiresIn === 0 &&
343+
(this.authProviderId === HostingIntegrationId.GitHub ||
344+
this.authProviderId === SelfHostedIntegrationId.CloudGitHubEnterprise)
345+
) {
342346
// It never expires so don't refresh it frequently:
343347
session.expiresIn = maxSmallIntegerV8; // maximum expiration length
344348
}
@@ -522,6 +526,11 @@ export class IntegrationAuthenticationService implements Disposable {
522526
).GitHubAuthenticationProvider(this.container)
523527
: new BuiltInAuthenticationProvider(this.container, providerId);
524528

529+
break;
530+
case SelfHostedIntegrationId.CloudGitHubEnterprise:
531+
provider = new (
532+
await import(/* webpackChunkName: "integrations" */ './github')
533+
).GitHubEnterpriseCloudAuthenticationProvider(this.container);
525534
break;
526535
case SelfHostedIntegrationId.GitHubEnterprise:
527536
provider = new (

src/plus/integrations/authentication/models.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export interface CloudIntegrationConnection {
3232
domain: string;
3333
}
3434

35-
export type CloudIntegrationType = 'jira' | 'trello' | 'gitlab' | 'github' | 'bitbucket' | 'azure';
35+
export type CloudIntegrationType = 'jira' | 'trello' | 'gitlab' | 'github' | 'bitbucket' | 'azure' | 'githubEnterprise';
3636

3737
export type CloudIntegrationAuthType = 'oauth' | 'pat';
3838

@@ -53,6 +53,7 @@ export const toIntegrationId: { [key in CloudIntegrationType]: IntegrationId } =
5353
trello: IssueIntegrationId.Trello,
5454
gitlab: HostingIntegrationId.GitLab,
5555
github: HostingIntegrationId.GitHub,
56+
githubEnterprise: SelfHostedIntegrationId.CloudGitHubEnterprise,
5657
bitbucket: HostingIntegrationId.Bitbucket,
5758
azure: HostingIntegrationId.AzureDevOps,
5859
};
@@ -64,6 +65,7 @@ export const toCloudIntegrationType: { [key in IntegrationId]: CloudIntegrationT
6465
[HostingIntegrationId.GitHub]: 'github',
6566
[HostingIntegrationId.Bitbucket]: 'bitbucket',
6667
[HostingIntegrationId.AzureDevOps]: 'azure',
68+
[SelfHostedIntegrationId.CloudGitHubEnterprise]: 'githubEnterprise',
6769
[SelfHostedIntegrationId.GitHubEnterprise]: undefined,
6870
[SelfHostedIntegrationId.GitLabSelfHosted]: undefined,
6971
};

src/plus/integrations/integrationService.ts

+29-3
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import type {
4444
} from './integration';
4545
import { isHostingIntegrationId, isSelfHostedIntegrationId } from './providers/models';
4646
import type { ProvidersApi } from './providers/providersApi';
47+
import { isGitHubDotCom } from './providers/utils';
4748

4849
export interface ConnectionStateChangeEvent {
4950
key: string;
@@ -88,8 +89,10 @@ export class IntegrationService implements Disposable {
8889
@gate()
8990
@debug()
9091
private async syncCloudIntegrations(forceConnect: boolean) {
92+
const scope = getLogScope();
9193
const connectedIntegrations = new Set<IntegrationId>();
9294
const loggedIn = await this.container.subscription.getAuthenticationSession();
95+
const domains = new Map<string, string>();
9396
if (loggedIn) {
9497
const cloudIntegrations = await this.container.cloudIntegrations;
9598
const connections = await cloudIntegrations?.getConnections();
@@ -100,10 +103,18 @@ export class IntegrationService implements Disposable {
100103
// GKDev includes some integrations like "google" that we don't support
101104
if (integrationId == null) return;
102105
connectedIntegrations.add(toIntegrationId[p.provider]);
106+
if (p.domain?.length > 0) {
107+
try {
108+
const host = new URL(p.domain).host;
109+
domains.set(integrationId, host);
110+
} catch {
111+
Logger.warn(`Invalid domain for ${integrationId} integration: ${p.domain}. Ignoring.`, scope);
112+
}
113+
}
103114
});
104115
}
105116

106-
for await (const integration of this.getSupportedCloudIntegrations()) {
117+
for await (const integration of this.getSupportedCloudIntegrations(domains)) {
107118
await integration.syncCloudConnection(
108119
connectedIntegrations.has(integration.id) ? 'connected' : 'disconnected',
109120
forceConnect,
@@ -121,9 +132,19 @@ export class IntegrationService implements Disposable {
121132
return connectedIntegrations;
122133
}
123134

124-
private async *getSupportedCloudIntegrations() {
135+
private async *getSupportedCloudIntegrations(domains: Map<string, string>): AsyncIterable<Integration> {
125136
for (const id of getSupportedCloudIntegrationIds()) {
126-
yield this.get(id);
137+
if (id === SelfHostedIntegrationId.CloudGitHubEnterprise && !domains.has(id)) {
138+
try {
139+
// Try getting whatever we have now because we will need to disconnect
140+
yield this.get(id);
141+
} catch {
142+
// Ignore this exception and continue,
143+
// because we probably haven't ever had an instance of this integration
144+
}
145+
} else {
146+
yield this.get(id, domains?.get(id));
147+
}
127148
}
128149
}
129150

@@ -437,6 +458,7 @@ export class IntegrationService implements Disposable {
437458
await import(/* webpackChunkName: "integrations" */ './providers/github')
438459
).GitHubIntegration(this.container, this.authenticationService, this.getProvidersApi.bind(this));
439460
break;
461+
case SelfHostedIntegrationId.CloudGitHubEnterprise:
440462
case SelfHostedIntegrationId.GitHubEnterprise:
441463
if (domain == null) throw new Error(`Domain is required for '${id}' integration`);
442464
integration = new (
@@ -446,6 +468,7 @@ export class IntegrationService implements Disposable {
446468
this.authenticationService,
447469
this.getProvidersApi.bind(this),
448470
domain,
471+
id,
449472
);
450473
break;
451474
case HostingIntegrationId.GitLab:
@@ -549,6 +572,9 @@ export class IntegrationService implements Disposable {
549572
if (remote.provider.custom && remote.provider.domain != null) {
550573
return get(SelfHostedIntegrationId.GitHubEnterprise, remote.provider.domain) as RT;
551574
}
575+
if (remote.provider.domain != null && !isGitHubDotCom(remote.provider.domain)) {
576+
return get(SelfHostedIntegrationId.CloudGitHubEnterprise, remote.provider.domain) as RT;
577+
}
552578
return get(HostingIntegrationId.GitHub) as RT;
553579
case 'gitlab':
554580
if (remote.provider.custom && remote.provider.domain != null) {

src/plus/integrations/providers/github.ts

+14-4
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ const enterpriseAuthProvider: IntegrationAuthenticationProviderDescriptor = Obje
3636
id: enterpriseMetadata.id,
3737
scopes: enterpriseMetadata.scopes,
3838
});
39+
const cloudEnterpriseMetadata = providersMetadata[SelfHostedIntegrationId.CloudGitHubEnterprise];
40+
const cloudEnterpriseAuthProvider: IntegrationAuthenticationProviderDescriptor = Object.freeze({
41+
id: cloudEnterpriseMetadata.id,
42+
scopes: cloudEnterpriseMetadata.scopes,
43+
});
3944

4045
export type GitHubRepositoryDescriptor = RepositoryDescriptor;
4146

@@ -300,10 +305,11 @@ export class GitHubIntegration extends GitHubIntegrationBase<HostingIntegrationI
300305
}
301306
}
302307

303-
export class GitHubEnterpriseIntegration extends GitHubIntegrationBase<SelfHostedIntegrationId.GitHubEnterprise> {
304-
readonly authProvider = enterpriseAuthProvider;
305-
readonly id = SelfHostedIntegrationId.GitHubEnterprise;
306-
protected readonly key = `${this.id}:${this.domain}` as const;
308+
export class GitHubEnterpriseIntegration extends GitHubIntegrationBase<
309+
SelfHostedIntegrationId.GitHubEnterprise | SelfHostedIntegrationId.CloudGitHubEnterprise
310+
> {
311+
readonly authProvider;
312+
protected readonly key;
307313
readonly name = 'GitHub Enterprise';
308314
get domain(): string {
309315
return this._domain;
@@ -318,8 +324,12 @@ export class GitHubEnterpriseIntegration extends GitHubIntegrationBase<SelfHoste
318324
authenticationService: IntegrationAuthenticationService,
319325
getProvidersApi: () => Promise<ProvidersApi>,
320326
private readonly _domain: string,
327+
readonly id: SelfHostedIntegrationId.GitHubEnterprise | SelfHostedIntegrationId.CloudGitHubEnterprise,
321328
) {
322329
super(container, authenticationService, getProvidersApi);
330+
this.key = `${this.id}:${this.domain}` as const;
331+
this.authProvider =
332+
this.id === SelfHostedIntegrationId.GitHubEnterprise ? enterpriseAuthProvider : cloudEnterpriseAuthProvider;
323333
}
324334

325335
@log()

src/plus/integrations/providers/models.ts

+17
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ export type ProviderRequestResponse<T> = Response<T>;
7373
export type ProviderRequestOptions = RequestOptions;
7474

7575
const selfHostedIntegrationIds: SelfHostedIntegrationId[] = [
76+
SelfHostedIntegrationId.CloudGitHubEnterprise,
7677
SelfHostedIntegrationId.GitHubEnterprise,
7778
SelfHostedIntegrationId.GitLabSelfHosted,
7879
] as const;
@@ -349,6 +350,22 @@ export const providersMetadata: ProvidersMetadata = {
349350
supportedIssueFilters: [IssueFilter.Author, IssueFilter.Assignee, IssueFilter.Mention],
350351
scopes: ['repo', 'read:user', 'user:email'],
351352
},
353+
[SelfHostedIntegrationId.CloudGitHubEnterprise]: {
354+
domain: '',
355+
id: SelfHostedIntegrationId.CloudGitHubEnterprise,
356+
issuesPagingMode: PagingMode.Repos,
357+
pullRequestsPagingMode: PagingMode.Repos,
358+
// Use 'username' property on account for PR filters
359+
supportedPullRequestFilters: [
360+
PullRequestFilter.Author,
361+
PullRequestFilter.Assignee,
362+
PullRequestFilter.ReviewRequested,
363+
PullRequestFilter.Mention,
364+
],
365+
// Use 'username' property on account for issue filters
366+
supportedIssueFilters: [IssueFilter.Author, IssueFilter.Assignee, IssueFilter.Mention],
367+
scopes: ['repo', 'read:user', 'user:email'],
368+
},
352369
[SelfHostedIntegrationId.GitHubEnterprise]: {
353370
domain: '',
354371
id: SelfHostedIntegrationId.GitHubEnterprise,

src/plus/integrations/providers/providersApi.ts

+14
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,20 @@ export class ProvidersApi {
9898
providerApis.github,
9999
) as GetIssuesForReposFn,
100100
},
101+
[SelfHostedIntegrationId.CloudGitHubEnterprise]: {
102+
...providersMetadata[SelfHostedIntegrationId.GitHubEnterprise],
103+
provider: providerApis.github,
104+
getCurrentUserFn: providerApis.github.getCurrentUser.bind(providerApis.github) as GetCurrentUserFn,
105+
getPullRequestsForReposFn: providerApis.github.getPullRequestsForRepos.bind(
106+
providerApis.github,
107+
) as GetPullRequestsForReposFn,
108+
getPullRequestsForUserFn: providerApis.github.getPullRequestsAssociatedWithUser.bind(
109+
providerApis.github,
110+
) as GetPullRequestsForUserFn,
111+
getIssuesForReposFn: providerApis.github.getIssuesForRepos.bind(
112+
providerApis.github,
113+
) as GetIssuesForReposFn,
114+
},
101115
[SelfHostedIntegrationId.GitHubEnterprise]: {
102116
...providersMetadata[SelfHostedIntegrationId.GitHubEnterprise],
103117
provider: providerApis.github,

src/plus/integrations/providers/utils.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import type { IssueResourceDescriptor, RepositoryDescriptor } from '../integrati
1212
import { isIssueResourceDescriptor, isRepositoryDescriptor } from '../integration';
1313
import type { GitConfigEntityIdentifier } from './models';
1414

15-
function isGitHubDotCom(domain: string): boolean {
15+
export function isGitHubDotCom(domain: string): boolean {
1616
return equalsIgnoreCase(domain, 'github.com');
1717
}
1818

0 commit comments

Comments
 (0)