Skip to content

Commit

Permalink
Adds support for GKDev Cloud GitHub Enterprise integration
Browse files Browse the repository at this point in the history
  • Loading branch information
sergeibbb committed Jan 14, 2025
1 parent 7befb4b commit a6c7542
Show file tree
Hide file tree
Showing 9 changed files with 100 additions and 10 deletions.
2 changes: 2 additions & 0 deletions src/constants.integrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export enum HostingIntegrationId {

export enum SelfHostedIntegrationId {
GitHubEnterprise = 'github-enterprise',
CloudGitHubEnterprise = 'cloud-github-enterprise',
GitLabSelfHosted = 'gitlab-self-hosted',
}

Expand All @@ -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,
Expand Down
10 changes: 10 additions & 0 deletions src/plus/integrations/authentication/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,16 @@ export class GitHubAuthenticationProvider extends CloudIntegrationAuthentication
}
}

export class GitHubEnterpriseCloudAuthenticationProvider extends CloudIntegrationAuthenticationProvider<SelfHostedIntegrationId.CloudGitHubEnterprise> {
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<SelfHostedIntegrationId.GitHubEnterprise> {
protected override get authProviderId(): SelfHostedIntegrationId.GitHubEnterprise {
return SelfHostedIntegrationId.GitHubEnterprise;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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 (
Expand Down
4 changes: 3 additions & 1 deletion src/plus/integrations/authentication/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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,
};
Expand All @@ -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,
};
32 changes: 29 additions & 3 deletions src/plus/integrations/integrationService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import type {
} from './integration';
import { isHostingIntegrationId, isSelfHostedIntegrationId } from './providers/models';
import type { ProvidersApi } from './providers/providersApi';
import { isGitHubDotCom } from './providers/utils';

export interface ConnectionStateChangeEvent {
key: string;
Expand Down Expand Up @@ -88,8 +89,10 @@ export class IntegrationService implements Disposable {
@gate()
@debug()
private async syncCloudIntegrations(forceConnect: boolean) {
const scope = getLogScope();
const connectedIntegrations = new Set<IntegrationId>();
const loggedIn = await this.container.subscription.getAuthenticationSession();
const domains = new Map<string, string>();
if (loggedIn) {
const cloudIntegrations = await this.container.cloudIntegrations;
const connections = await cloudIntegrations?.getConnections();
Expand All @@ -100,10 +103,18 @@ 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?.length > 0) {
try {
const host = new URL(p.domain).host;
domains.set(integrationId, host);
} catch {
Logger.warn(`Invalid domain for ${integrationId} integration: ${p.domain}. Ignoring.`, scope);
}
}
});
}

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,
Expand All @@ -121,9 +132,19 @@ export class IntegrationService implements Disposable {
return connectedIntegrations;
}

private async *getSupportedCloudIntegrations() {
private async *getSupportedCloudIntegrations(domains: Map<string, string>): AsyncIterable<Integration> {
for (const id of getSupportedCloudIntegrationIds()) {
yield this.get(id);
if (id === SelfHostedIntegrationId.CloudGitHubEnterprise && !domains.has(id)) {
try {
// Try getting whatever we have now because we will need to disconnect
yield this.get(id);
} catch {
// Ignore this exception and continue,
// because we probably haven't ever had an instance of this integration
}
} else {
yield this.get(id, domains?.get(id));
}
}
}

Expand Down Expand Up @@ -437,6 +458,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 (
Expand All @@ -446,6 +468,7 @@ export class IntegrationService implements Disposable {
this.authenticationService,
this.getProvidersApi.bind(this),
domain,
id,
);
break;
case HostingIntegrationId.GitLab:
Expand Down Expand Up @@ -549,6 +572,9 @@ export class IntegrationService implements Disposable {
if (remote.provider.custom && remote.provider.domain != null) {
return get(SelfHostedIntegrationId.GitHubEnterprise, remote.provider.domain) as RT;
}
if (remote.provider.domain != null && !isGitHubDotCom(remote.provider.domain)) {
return get(SelfHostedIntegrationId.CloudGitHubEnterprise, remote.provider.domain) as RT;
}
return get(HostingIntegrationId.GitHub) as RT;
case 'gitlab':
if (remote.provider.custom && remote.provider.domain != null) {
Expand Down
18 changes: 14 additions & 4 deletions src/plus/integrations/providers/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,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;

Expand Down Expand Up @@ -300,10 +305,11 @@ export class GitHubIntegration extends GitHubIntegrationBase<HostingIntegrationI
}
}

export class GitHubEnterpriseIntegration extends GitHubIntegrationBase<SelfHostedIntegrationId.GitHubEnterprise> {
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;
Expand All @@ -318,8 +324,12 @@ export class GitHubEnterpriseIntegration extends GitHubIntegrationBase<SelfHoste
authenticationService: IntegrationAuthenticationService,
getProvidersApi: () => Promise<ProvidersApi>,
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()
Expand Down
17 changes: 17 additions & 0 deletions src/plus/integrations/providers/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export type ProviderRequestResponse<T> = Response<T>;
export type ProviderRequestOptions = RequestOptions;

const selfHostedIntegrationIds: SelfHostedIntegrationId[] = [
SelfHostedIntegrationId.CloudGitHubEnterprise,
SelfHostedIntegrationId.GitHubEnterprise,
SelfHostedIntegrationId.GitLabSelfHosted,
] as const;
Expand Down Expand Up @@ -349,6 +350,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,
Expand Down
14 changes: 14 additions & 0 deletions src/plus/integrations/providers/providersApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion src/plus/integrations/providers/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import type { IssueResourceDescriptor, RepositoryDescriptor } from '../integrati
import { isIssueResourceDescriptor, isRepositoryDescriptor } from '../integration';
import type { GitConfigEntityIdentifier } from './models';

function isGitHubDotCom(domain: string): boolean {
export function isGitHubDotCom(domain: string): boolean {
return equalsIgnoreCase(domain, 'github.com');
}

Expand Down

0 comments on commit a6c7542

Please sign in to comment.