From 185ffdda50f8e18bad2201f6ae0d97394493f762 Mon Sep 17 00:00:00 2001 From: Ramin Tadayon Date: Fri, 13 Dec 2024 11:24:55 -0700 Subject: [PATCH] Refines issue model and adds getIssue to supported integrations --- src/cache.ts | 37 +++++++++- src/git/models/issue.ts | 18 ++++- src/plus/integrations/integration.ts | 71 ++++++++++++++++++- .../integrations/providers/azureDevOps.ts | 10 ++- src/plus/integrations/providers/bitbucket.ts | 10 ++- src/plus/integrations/providers/github.ts | 21 ++++-- .../integrations/providers/github/github.ts | 69 +++++++++++++++++- .../integrations/providers/github/models.ts | 12 ++-- src/plus/integrations/providers/gitlab.ts | 35 +++++++-- src/plus/integrations/providers/jira.ts | 70 +++++++++++------- src/plus/integrations/providers/models.ts | 9 ++- .../integrations/providers/providersApi.ts | 9 +-- 12 files changed, 315 insertions(+), 56 deletions(-) diff --git a/src/cache.ts b/src/cache.ts index 29ec62d3e40e7..102df4bc84501 100644 --- a/src/cache.ts +++ b/src/cache.ts @@ -3,7 +3,7 @@ import type { Disposable } from './api/gitlens'; import type { Container } from './container'; import type { Account } from './git/models/author'; import type { DefaultBranch } from './git/models/defaultBranch'; -import type { IssueOrPullRequest } from './git/models/issue'; +import type { Issue, IssueOrPullRequest } from './git/models/issue'; import type { PullRequest } from './git/models/pullRequest'; import type { RepositoryMetadata } from './git/models/repositoryMetadata'; import type { HostingIntegration, IntegrationBase, ResourceDescriptor } from './plus/integrations/integration'; @@ -12,6 +12,8 @@ import { isPromise } from './system/promise'; type Caches = { defaultBranch: { key: `repo:${string}`; value: DefaultBranch }; // enrichedAutolinksBySha: { key: `sha:${string}:${string}`; value: Map }; + issuesById: { key: `id:${string}:${string}`; value: Issue }; + issuesByIdAndResource: { key: `id:${string}:${string}:${string}`; value: Issue }; issuesOrPrsById: { key: `id:${string}:${string}`; value: IssueOrPullRequest }; issuesOrPrsByIdAndRepo: { key: `id:${string}:${string}:${string}`; value: IssueOrPullRequest }; prByBranch: { key: `branch:${string}:${string}`; value: PullRequest }; @@ -127,6 +129,27 @@ export class CacheProvider implements Disposable { ); } + getIssue( + id: string, + resource: ResourceDescriptor, + integration: IntegrationBase | undefined, + cacheable: Cacheable, + options?: { expiryOverride?: boolean | number }, + ): CacheResult { + const { key, etag } = getResourceKeyAndEtag(resource, integration); + + if (resource == null) { + return this.get('issuesById', `id:${id}:${key}`, etag, cacheable, options); + } + return this.get( + 'issuesByIdAndResource', + `id:${id}:${key}:${JSON.stringify(resource)}}`, + etag, + cacheable, + options, + ); + } + getPullRequest( id: string, resource: ResourceDescriptor, @@ -259,6 +282,18 @@ function getExpiresAt(cache: T, value: CacheValue | undefine case 'repoMetadata': case 'currentAccount': return 0; // Never expires + case 'issuesById': + case 'issuesByIdAndResource': { + if (value == null) return 0; // Never expires + + // Open issues expire after 1 hour, but closed issues expire after 12 hours unless recently updated and then expire in 1 hour + + const issue = value as CacheValue<'issuesById'>; + if (!issue.closed) return defaultExpiresAt; + + const updatedAgo = now - (issue.closedDate ?? issue.updatedDate).getTime(); + return now + (updatedAgo > 14 * 24 * 60 * 60 * 1000 ? 12 : 1) * 60 * 60 * 1000; + } case 'issuesOrPrsById': case 'issuesOrPrsByIdAndRepo': { if (value == null) return 0; // Never expires diff --git a/src/git/models/issue.ts b/src/git/models/issue.ts index 9359d98268187..b4da9f4c36843 100644 --- a/src/git/models/issue.ts +++ b/src/git/models/issue.ts @@ -52,12 +52,19 @@ export interface IssueRepository { url?: string; } +export interface IssueProject { + id: string; + name: string; + resourceId: string; +} + export interface IssueShape extends IssueOrPullRequest { author: IssueMember; assignees: IssueMember[]; repository?: IssueRepository; labels?: IssueLabel[]; body?: string; + project?: IssueProject; } export interface SearchedIssue { @@ -224,6 +231,14 @@ export function serializeIssue(value: IssueShape): IssueShape { repo: value.repository.repo, url: value.repository.url, }, + project: + value.project == null + ? undefined + : { + id: value.project.id, + name: value.project.name, + resourceId: value.project.resourceId, + }, assignees: value.assignees.map(assignee => ({ id: assignee.id, name: assignee.name, @@ -258,13 +273,14 @@ export class Issue implements IssueShape { public readonly closed: boolean, public readonly state: IssueOrPullRequestState, public readonly author: IssueMember, - public readonly repository: IssueRepository, public readonly assignees: IssueMember[], + public readonly repository?: IssueRepository, public readonly closedDate?: Date, public readonly labels?: IssueLabel[], public readonly commentsCount?: number, public readonly thumbsUpCount?: number, public readonly body?: string, + public readonly project?: IssueProject, ) {} } diff --git a/src/plus/integrations/integration.ts b/src/plus/integrations/integration.ts index 9634b44b5222f..c7aabf3a2c54a 100644 --- a/src/plus/integrations/integration.ts +++ b/src/plus/integrations/integration.ts @@ -9,7 +9,7 @@ import { AuthenticationError, CancellationError, RequestClientError } from '../. import type { PagedResult } from '../../git/gitProvider'; import type { Account, UnidentifiedAuthor } from '../../git/models/author'; import type { DefaultBranch } from '../../git/models/defaultBranch'; -import type { IssueOrPullRequest, SearchedIssue } from '../../git/models/issue'; +import type { Issue, IssueOrPullRequest, SearchedIssue } from '../../git/models/issue'; import type { PullRequest, PullRequestMergeMethod, @@ -70,6 +70,38 @@ export type IntegrationType = 'issues' | 'hosting'; export type ResourceDescriptor = { key: string } & Record; +export type IssueResourceDescriptor = ResourceDescriptor & { + id: string; + name: string; +}; + +export type RepositoryDescriptor = ResourceDescriptor & { + owner: string; + name: string; +}; + +export function isIssueResourceDescriptor(resource: ResourceDescriptor): resource is IssueResourceDescriptor { + return ( + 'key' in resource && + resource.key != null && + 'id' in resource && + resource.id != null && + 'name' in resource && + resource.name != null + ); +} + +export function isRepositoryDescriptor(resource: ResourceDescriptor): resource is RepositoryDescriptor { + return ( + 'key' in resource && + resource.key != null && + 'owner' in resource && + resource.owner != null && + 'name' in resource && + resource.name != null + ); +} + export function isHostingIntegration(integration: Integration): integration is HostingIntegration { return integration.type === 'hosting'; } @@ -446,6 +478,43 @@ export abstract class IntegrationBase< id: string, ): Promise; + @debug() + async getIssue( + resource: T, + id: string, + options?: { expiryOverride?: boolean | number }, + ): Promise { + const scope = getLogScope(); + + const connected = this.maybeConnected ?? (await this.isConnected()); + if (!connected) return undefined; + + const issue = this.container.cache.getIssue( + id, + resource, + this, + () => ({ + value: (async () => { + try { + const result = await this.getProviderIssue(this._session!, resource, id); + this.resetRequestExceptionCount(); + return result; + } catch (ex) { + return this.handleProviderException(ex, scope, undefined); + } + })(), + }), + options, + ); + return issue; + } + + protected abstract getProviderIssue( + session: ProviderAuthenticationSession, + resource: T, + id: string, + ): Promise; + async getCurrentAccount(options?: { avatarSize?: number; expiryOverride?: boolean | number; diff --git a/src/plus/integrations/providers/azureDevOps.ts b/src/plus/integrations/providers/azureDevOps.ts index 8f743bccd31c8..30f860058df37 100644 --- a/src/plus/integrations/providers/azureDevOps.ts +++ b/src/plus/integrations/providers/azureDevOps.ts @@ -3,7 +3,7 @@ import { HostingIntegrationId } from '../../../constants.integrations'; import type { PagedResult } from '../../../git/gitProvider'; import type { Account } from '../../../git/models/author'; import type { DefaultBranch } from '../../../git/models/defaultBranch'; -import type { IssueOrPullRequest, SearchedIssue } from '../../../git/models/issue'; +import type { Issue, IssueOrPullRequest, SearchedIssue } from '../../../git/models/issue'; import type { PullRequest, PullRequestMergeMethod, @@ -107,6 +107,14 @@ export class AzureDevOpsIntegration extends HostingIntegration< return Promise.resolve(undefined); } + protected override async getProviderIssue( + _session: AuthenticationSession, + _repo: AzureRepositoryDescriptor, + _id: string, + ): Promise { + return Promise.resolve(undefined); + } + protected override async getProviderPullRequestForBranch( _session: AuthenticationSession, _repo: AzureRepositoryDescriptor, diff --git a/src/plus/integrations/providers/bitbucket.ts b/src/plus/integrations/providers/bitbucket.ts index 2d7f653cbcb1a..7903857fa47e6 100644 --- a/src/plus/integrations/providers/bitbucket.ts +++ b/src/plus/integrations/providers/bitbucket.ts @@ -2,7 +2,7 @@ import type { AuthenticationSession, CancellationToken } from 'vscode'; import { HostingIntegrationId } from '../../../constants.integrations'; import type { Account } from '../../../git/models/author'; import type { DefaultBranch } from '../../../git/models/defaultBranch'; -import type { IssueOrPullRequest, SearchedIssue } from '../../../git/models/issue'; +import type { Issue, IssueOrPullRequest, SearchedIssue } from '../../../git/models/issue'; import type { PullRequest, PullRequestMergeMethod, @@ -86,6 +86,14 @@ export class BitbucketIntegration extends HostingIntegration< return Promise.resolve(undefined); } + protected override async getProviderIssue( + _session: AuthenticationSession, + _repo: BitbucketRepositoryDescriptor, + _id: string, + ): Promise { + return Promise.resolve(undefined); + } + protected override async getProviderPullRequestForBranch( _session: AuthenticationSession, _repo: BitbucketRepositoryDescriptor, diff --git a/src/plus/integrations/providers/github.ts b/src/plus/integrations/providers/github.ts index 4145d3617b4c2..a5d73cd3402b8 100644 --- a/src/plus/integrations/providers/github.ts +++ b/src/plus/integrations/providers/github.ts @@ -4,7 +4,7 @@ import type { Sources } from '../../../constants.telemetry'; import type { Container } from '../../../container'; import type { Account, UnidentifiedAuthor } from '../../../git/models/author'; import type { DefaultBranch } from '../../../git/models/defaultBranch'; -import type { IssueOrPullRequest, SearchedIssue } from '../../../git/models/issue'; +import type { Issue, IssueOrPullRequest, SearchedIssue } from '../../../git/models/issue'; import type { PullRequest, PullRequestMergeMethod, @@ -18,7 +18,7 @@ import type { IntegrationAuthenticationProviderDescriptor, IntegrationAuthenticationService, } from '../authentication/integrationAuthentication'; -import type { SupportedIntegrationIds } from '../integration'; +import type { RepositoryDescriptor, SupportedIntegrationIds } from '../integration'; import { HostingIntegration } from '../integration'; import { providersMetadata } from './models'; import type { ProvidersApi } from './providersApi'; @@ -35,11 +35,7 @@ const enterpriseAuthProvider: IntegrationAuthenticationProviderDescriptor = Obje scopes: enterpriseMetadata.scopes, }); -export type GitHubRepositoryDescriptor = { - key: string; - owner: string; - name: string; -}; +export type GitHubRepositoryDescriptor = RepositoryDescriptor; abstract class GitHubIntegrationBase extends HostingIntegration< ID, @@ -101,6 +97,17 @@ abstract class GitHubIntegrationBase extends ); } + protected override async getProviderIssue( + { accessToken }: AuthenticationSession, + repo: GitHubRepositoryDescriptor, + id: string, + ): Promise { + return (await this.container.github)?.getIssue(this, accessToken, repo.owner, repo.name, Number(id), { + baseUrl: this.apiBaseUrl, + includeBody: true, + }); + } + protected override async getProviderPullRequest( { accessToken }: AuthenticationSession, repo: GitHubRepositoryDescriptor, diff --git a/src/plus/integrations/providers/github/github.ts b/src/plus/integrations/providers/github/github.ts index 7e1d867a8bab4..441b32f7d5bf1 100644 --- a/src/plus/integrations/providers/github/github.ts +++ b/src/plus/integrations/providers/github/github.ts @@ -19,7 +19,7 @@ import { import type { PagedResult, RepositoryVisibility } from '../../../../git/gitProvider'; import type { Account, UnidentifiedAuthor } from '../../../../git/models/author'; import type { DefaultBranch } from '../../../../git/models/defaultBranch'; -import type { IssueOrPullRequest, SearchedIssue } from '../../../../git/models/issue'; +import type { Issue, IssueOrPullRequest, SearchedIssue } from '../../../../git/models/issue'; import type { PullRequest, SearchedPullRequest } from '../../../../git/models/pullRequest'; import { PullRequestMergeMethod } from '../../../../git/models/pullRequest'; import type { GitRevisionRange } from '../../../../git/models/reference'; @@ -622,6 +622,73 @@ export class GitHubApi implements Disposable { } } + @debug({ args: { 0: p => p.name, 1: '' } }) + async getIssue( + provider: Provider, + token: string, + owner: string, + repo: string, + number: number, + options?: { + baseUrl?: string; + avatarSize?: number; + includeBody?: boolean; + }, + ): Promise { + const scope = getLogScope(); + + interface QueryResult { + repository: + | { + issue: GitHubIssue | null | undefined; + } + | null + | undefined; + } + + try { + const query = `query getIssue( + $owner: String! + $repo: String! + $number: Int! + $avatarSize: Int + ) { + repository(name: $repo, owner: $owner) { + issue(number: $number) { + ${gqIssueFragment}${ + options?.includeBody + ? ` + body + ` + : '' + } + } + } + }`; + + const rsp = await this.graphql( + provider, + token, + query, + { + ...options, + owner: owner, + repo: repo, + number: number, + }, + scope, + ); + + if (rsp?.repository?.issue == null) return undefined; + + return fromGitHubIssue(rsp.repository.issue, provider); + } catch (ex) { + if (ex instanceof RequestNotFoundError) return undefined; + + throw this.handleException(ex, provider, scope); + } + } + @debug({ args: { 0: p => p.name, 1: '' } }) async getPullRequest( provider: Provider, diff --git a/src/plus/integrations/providers/github/models.ts b/src/plus/integrations/providers/github/models.ts index 47017a55c1f5f..eec1bdd8cecae 100644 --- a/src/plus/integrations/providers/github/models.ts +++ b/src/plus/integrations/providers/github/models.ts @@ -418,18 +418,18 @@ export function fromGitHubIssue(value: GitHubIssue, provider: Provider): Issue { avatarUrl: value.author.avatarUrl, url: value.author.url, }, - { - owner: value.repository.owner.login, - repo: value.repository.name, - accessLevel: fromGitHubViewerPermissionToAccessLevel(value.repository.viewerPermission), - url: value.repository.url, - }, value.assignees.nodes.map(assignee => ({ id: assignee.login, name: assignee.login, avatarUrl: assignee.avatarUrl, url: assignee.url, })), + { + owner: value.repository.owner.login, + repo: value.repository.name, + accessLevel: fromGitHubViewerPermissionToAccessLevel(value.repository.viewerPermission), + url: value.repository.url, + }, value.closedAt == null ? undefined : new Date(value.closedAt), value.labels?.nodes == null ? undefined diff --git a/src/plus/integrations/providers/gitlab.ts b/src/plus/integrations/providers/gitlab.ts index eaff45e70f2a3..3c44a6eec4ce3 100644 --- a/src/plus/integrations/providers/gitlab.ts +++ b/src/plus/integrations/providers/gitlab.ts @@ -5,7 +5,7 @@ import type { Sources } from '../../../constants.telemetry'; import type { Container } from '../../../container'; import type { Account } from '../../../git/models/author'; import type { DefaultBranch } from '../../../git/models/defaultBranch'; -import type { IssueOrPullRequest, SearchedIssue } from '../../../git/models/issue'; +import type { Issue, IssueOrPullRequest, SearchedIssue } from '../../../git/models/issue'; import type { PullRequest, PullRequestMergeMethod, @@ -20,6 +20,7 @@ import type { IntegrationAuthenticationProviderDescriptor, IntegrationAuthenticationService, } from '../authentication/integrationAuthentication'; +import type { RepositoryDescriptor } from '../integration'; import { HostingIntegration } from '../integration'; import { fromGitLabMergeRequestProvidersApi } from './gitlab/models'; import type { ProviderPullRequest } from './models'; @@ -38,11 +39,7 @@ const enterpriseAuthProvider: IntegrationAuthenticationProviderDescriptor = Obje scopes: enterpriseMetadata.scopes, }); -export type GitLabRepositoryDescriptor = { - key: string; - owner: string; - name: string; -}; +export type GitLabRepositoryDescriptor = RepositoryDescriptor; abstract class GitLabIntegrationBase< ID extends HostingIntegrationId.GitLab | SelfHostedIntegrationId.GitLabSelfHosted, @@ -103,6 +100,32 @@ abstract class GitLabIntegrationBase< ); } + protected override async getProviderIssue( + { accessToken }: AuthenticationSession, + repo: GitLabRepositoryDescriptor, + id: string, + ): Promise { + const api = await this.container.gitlab; + const providerApi = await this.getProvidersApi(); + + if (!api || !repo || !id) { + return undefined; + } + + const repoId = await api.getProjectId(this, accessToken, repo.owner, repo.name, this.apiBaseUrl, undefined); + if (!repoId) { + return undefined; + } + + const apiResult = await providerApi.getIssue( + this.id, + { namespace: repo.owner, name: repo.name, number: id }, + { accessToken: accessToken }, + ); + const issue = apiResult != null ? toSearchedIssue(apiResult, this)?.issue : undefined; + return issue != null ? { ...issue, type: 'issue' } : undefined; + } + protected override async getProviderPullRequestForBranch( { accessToken }: AuthenticationSession, repo: GitLabRepositoryDescriptor, diff --git a/src/plus/integrations/providers/jira.ts b/src/plus/integrations/providers/jira.ts index 96f541c6cfd83..ba16f793e55bf 100644 --- a/src/plus/integrations/providers/jira.ts +++ b/src/plus/integrations/providers/jira.ts @@ -2,11 +2,11 @@ import type { AuthenticationSession, CancellationToken } from 'vscode'; import type { AutolinkReference, DynamicAutolinkReference } from '../../../autolinks'; import { IssueIntegrationId } from '../../../constants.integrations'; import type { Account } from '../../../git/models/author'; -import type { IssueOrPullRequest, SearchedIssue } from '../../../git/models/issue'; +import type { Issue, IssueOrPullRequest, SearchedIssue } from '../../../git/models/issue'; import { filterMap, flatten } from '../../../system/iterable'; import { Logger } from '../../../system/logger'; import type { IntegrationAuthenticationProviderDescriptor } from '../authentication/integrationAuthentication'; -import type { ResourceDescriptor } from '../integration'; +import type { IssueResourceDescriptor } from '../integration'; import { IssueIntegration } from '../integration'; import { IssueFilter, providersMetadata, toAccount, toSearchedIssue } from './models'; @@ -14,10 +14,8 @@ const metadata = providersMetadata[IssueIntegrationId.Jira]; const authProvider = Object.freeze({ id: metadata.id, scopes: metadata.scopes }); const maxPagesPerRequest = 10; -export interface JiraBaseDescriptor extends ResourceDescriptor { - id: string; - name: string; -} +export type JiraBaseDescriptor = IssueResourceDescriptor; + export interface JiraOrganizationDescriptor extends JiraBaseDescriptor { url: string; avatarUrl: string; @@ -235,25 +233,25 @@ export class JiraIntegration extends IssueIntegration { const results: SearchedIssue[] = []; for (const resource of myResources) { try { - const userLogin = (await this.getProviderAccountForResource(session, resource))?.username; - let cursor = undefined; - let hasMore = false; - let requestCount = 0; - do { - const resourceIssues = await api.getIssuesForResourceForCurrentUser(this.id, resource.id, { - accessToken: session.accessToken, - cursor: cursor, - }); - requestCount += 1; - hasMore = resourceIssues.paging?.more ?? false; - cursor = resourceIssues.paging?.cursor; - const formattedIssues = resourceIssues.values - .map(issue => toSearchedIssue(issue, this, undefined, userLogin)) - .filter((result): result is SearchedIssue => result != null); - if (formattedIssues.length > 0) { - results.push(...formattedIssues); - } - } while (requestCount < maxPagesPerRequest && hasMore); + const userLogin = (await this.getProviderAccountForResource(session, resource))?.username; + let cursor = undefined; + let hasMore = false; + let requestCount = 0; + do { + const resourceIssues = await api.getIssuesForResourceForCurrentUser(this.id, resource.id, { + accessToken: session.accessToken, + cursor: cursor, + }); + requestCount += 1; + hasMore = resourceIssues.paging?.more ?? false; + cursor = resourceIssues.paging?.cursor; + const formattedIssues = resourceIssues.values + .map(issue => toSearchedIssue(issue, this, undefined, userLogin)) + .filter((result): result is SearchedIssue => result != null); + if (formattedIssues.length > 0) { + results.push(...formattedIssues); + } + } while (requestCount < maxPagesPerRequest && hasMore); } catch (ex) { // TODO: We need a better way to message the failure to the user here. // This is a stopgap to prevent one bag org from throwing and preventing any issues from being returned. @@ -271,10 +269,30 @@ export class JiraIntegration extends IssueIntegration { ): Promise { const api = await this.getProvidersApi(); const userLogin = (await this.getProviderAccountForResource(session, resource))?.username; - const issue = await api.getIssue(this.id, resource.id, id, { accessToken: session.accessToken }); + const issue = await api.getIssue( + this.id, + { resourceId: resource.id, number: id }, + { accessToken: session.accessToken }, + ); return issue != null ? toSearchedIssue(issue, this, undefined, userLogin)?.issue : undefined; } + protected override async getProviderIssue( + session: AuthenticationSession, + resource: JiraOrganizationDescriptor, + id: string, + ): Promise { + const api = await this.getProvidersApi(); + const userLogin = (await this.getProviderAccountForResource(session, resource))?.username; + const apiResult = await api.getIssue( + this.id, + { resourceId: resource.id, number: id }, + { accessToken: session.accessToken }, + ); + const issue = apiResult != null ? toSearchedIssue(apiResult, this, undefined, userLogin)?.issue : undefined; + return issue != null ? { ...issue, type: 'issue' } : undefined; + } + protected override async providerOnConnect(): Promise { this._autolinks = undefined; if (this._session == null) return; diff --git a/src/plus/integrations/providers/models.ts b/src/plus/integrations/providers/models.ts index 3a88ca79bf184..3f913c66b0bb7 100644 --- a/src/plus/integrations/providers/models.ts +++ b/src/plus/integrations/providers/models.ts @@ -237,7 +237,9 @@ export type GetPullRequestsForAzureProjectsFn = ( export type MergePullRequestFn = GitProvider['mergePullRequest']; export type GetIssueFn = ( - input: { resourceId: string; number: string }, + input: + | { resourceId: string; number: string } // jira + | { namespace: string; name: string; number: string }, // gitlab options?: EnterpriseOptions, ) => Promise<{ data: ProviderIssue }>; @@ -522,6 +524,11 @@ export function toSearchedIssue( avatarUrl: assignee.avatarUrl ?? undefined, url: assignee.url ?? undefined, })) ?? [], + project: { + id: issue.project?.id ?? '', + name: issue.project?.name ?? '', + resourceId: issue.project?.resourceId ?? '', + }, repository: issue.repository?.owner?.login != null ? { diff --git a/src/plus/integrations/providers/providersApi.ts b/src/plus/integrations/providers/providersApi.ts index 486944b0c0c1b..cc36150538be2 100644 --- a/src/plus/integrations/providers/providersApi.ts +++ b/src/plus/integrations/providers/providersApi.ts @@ -21,6 +21,7 @@ import type { GetAzureResourcesForUserFn, GetCurrentUserFn, GetCurrentUserForInstanceFn, + GetIssueFn, GetIssuesForAzureProjectFn, GetIssuesForRepoFn, GetIssuesForReposFn, @@ -124,6 +125,7 @@ export class ProvidersApi { getPullRequestsForUserFn: providerApis.gitlab.getPullRequestsAssociatedWithUser.bind( providerApis.gitlab, ) as GetPullRequestsForUserFn, + getIssueFn: providerApis.gitlab.getIssue.bind(providerApis.gitlab) as GetIssueFn, getIssuesForReposFn: providerApis.gitlab.getIssuesForRepos.bind( providerApis.gitlab, ) as GetIssuesForReposFn, @@ -207,7 +209,7 @@ export class ProvidersApi { providerApis.jira, ), getJiraProjectsForResourcesFn: providerApis.jira.getJiraProjectsForResources.bind(providerApis.jira), - getIssueFn: providerApis.jira.getIssue.bind(providerApis.jira), + getIssueFn: providerApis.jira.getIssue.bind(providerApis.jira) as GetIssueFn, getIssuesForProjectFn: providerApis.jira.getIssuesForProject.bind(providerApis.jira), getIssuesForResourceForCurrentUserFn: providerApis.jira.getIssuesForResourceForCurrentUser.bind( providerApis.jira, @@ -807,8 +809,7 @@ export class ProvidersApi { async getIssue( providerId: IntegrationId, - resourceId: string, - issueId: string, + input: { resourceId: string; number: string } | { namespace: string; name: string; number: string }, options?: { accessToken?: string }, ): Promise { const { provider, token } = await this.ensureProviderTokenAndFunction( @@ -818,7 +819,7 @@ export class ProvidersApi { ); try { - const result = await provider.getIssueFn?.({ resourceId: resourceId, number: issueId }, { token: token }); + const result = await provider.getIssueFn?.(input, { token: token }); return result?.data; } catch (e) {