UNPKG

deepsource-mcp-server

Version:
499 lines 17.5 kB
/** * @fileoverview Runs client for the DeepSource API * This module provides functionality for working with DeepSource analysis runs. */ import { BaseDeepSourceClient } from './base-client.js'; import { isErrorWithMessage } from '../utils/errors/handlers.js'; import { asGraphQLNodeId, asRunId, asCommitOid, asBranchName, } from '../types/branded.js'; /** * Client for interacting with DeepSource runs API * @class * @extends BaseDeepSourceClient * @public */ export class RunsClient extends BaseDeepSourceClient { /** * Fetches analysis runs from a DeepSource project with optional filtering * @param projectKey The project key to fetch runs for * @param params Optional filter parameters * @returns Promise that resolves to a paginated list of runs * @throws {ClassifiedError} When the API request fails * @public */ async listRuns(projectKey, params = {}) { try { this.logger.info('Fetching runs from DeepSource API', { projectKey, hasFilters: Object.keys(params).length > 0, }); const project = await this.findProjectByKey(projectKey); if (!project) { return BaseDeepSourceClient.createEmptyPaginatedResponse(); } const normalizedParams = BaseDeepSourceClient.normalizePaginationParams(params); const query = RunsClient.buildRunsQuery(); const response = await this.executeGraphQL(query, { login: project.repository.login, name: project.repository.name, provider: project.repository.provider, ...normalizedParams, }); if (!response.data) { throw new Error('No data received from GraphQL API'); } const runs = this.extractRunsFromResponse(response.data); this.logger.info('Successfully fetched runs', { count: runs.length, totalCount: runs.length, // Note: Would need cursor-based pagination for accurate total }); return { items: runs, pageInfo: { hasNextPage: false, // Simplified for now hasPreviousPage: false, }, totalCount: runs.length, }; } catch (error) { return this.handleRunsError(error); } } /** * Fetches a specific run by identifier (runUid or commitOid) * @param runIdentifier The run UID or commit OID * @returns Promise that resolves to the run if found, null otherwise * @public */ async getRun(runIdentifier) { try { this.logger.info('Fetching specific run', { runIdentifier }); // Determine if the identifier is a UUID or a commit hash const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(runIdentifier); const query = isUuid ? RunsClient.buildRunByUidQuery() : RunsClient.buildRunByCommitQuery(); const response = await this.executeGraphQL(query, { [isUuid ? 'runUid' : 'commitOid']: runIdentifier, }); if (!response.data) { return null; } const run = this.extractSingleRunFromResponse(response.data); if (!run) { return null; } this.logger.info('Successfully fetched run', { runUid: run.runUid, status: run.status, }); return run; } catch (error) { if (isErrorWithMessage(error, 'NoneType') || isErrorWithMessage(error, 'not found')) { return null; } throw error; } } /** * Finds the most recent run for a specific branch * @param projectKey The project key * @param branchName The branch name to search for * @returns Promise that resolves to the most recent run if found, null otherwise * @public */ async findMostRecentRunForBranch(projectKey, branchName) { try { this.logger.info('Finding most recent run for branch', { projectKey, branchName }); let mostRecentRun = null; let hasNextPage = true; let after; // Paginate through runs to find the most recent for the specific branch while (hasNextPage) { const params = { first: 50, }; if (after !== undefined) { params.after = after; } const runs = await this.listRuns(projectKey, params); for (const run of runs.items) { if (run.branchName === branchName) { // If this is the first matching run or it's more recent than our current most recent if (!mostRecentRun || new Date(run.createdAt) > new Date(mostRecentRun.createdAt)) { mostRecentRun = run; } } } hasNextPage = runs.pageInfo.hasNextPage; after = runs.pageInfo.endCursor || undefined; } if (!mostRecentRun) { this.logger.error(`No runs found for branch '${branchName}' in project '${projectKey}'`); throw new Error(`No runs found for branch '${branchName}' in project '${projectKey}'`); } this.logger.info('Found most recent run for branch', { runUid: mostRecentRun.runUid, branchName: mostRecentRun.branchName, createdAt: mostRecentRun.createdAt, }); return mostRecentRun; } catch (error) { this.logger.error('Error finding most recent run for branch', { projectKey, branchName, error, }); throw error; } } /** * Fetches issues from the most recent run on a specific branch * @param projectKey The project key * @param branchName The branch name * @returns Promise that resolves to recent run issues response * @public */ async getRecentRunIssues(projectKey, branchName, paginationParams) { try { this.logger.info('Fetching recent run issues', { projectKey, branchName }); // First find the most recent run for the branch const mostRecentRun = await this.findMostRecentRunForBranch(projectKey, branchName); if (!mostRecentRun) { return { run: null, items: [], pageInfo: { hasNextPage: false, hasPreviousPage: false }, totalCount: 0, }; } // Now fetch the issues for this specific run const runIssuesQuery = RunsClient.buildRunOccurrencesQuery(); const requestedLimit = paginationParams?.first ?? 100; // To avoid fetching too much data, limit both checks and occurrences // If user wants 100 issues total, we might fetch from up to 10 checks with 10 issues each const checksLimit = Math.min(10, Math.ceil(requestedLimit / 10)); const occurrencesPerCheck = Math.ceil(requestedLimit / checksLimit); const response = await this.executeGraphQL(runIssuesQuery, { runUid: mostRecentRun.runUid, first: occurrencesPerCheck, checksFirst: checksLimit, }); if (!response.data) { this.logger.warn('No data received for run occurrences'); return { run: mostRecentRun, items: [], pageInfo: { hasNextPage: false, hasPreviousPage: false }, totalCount: 0, }; } const issues = this.extractIssuesFromRunResponse(response.data, requestedLimit); this.logger.info('Successfully fetched run issues', { runUid: mostRecentRun.runUid, issueCount: issues.length, requestedLimit, }); return { run: mostRecentRun, items: issues, pageInfo: { hasNextPage: issues.length >= requestedLimit, hasPreviousPage: false }, totalCount: issues.length, }; } catch (error) { this.logger.error('Error fetching recent run issues', { projectKey, branchName, error, }); throw error; } } /** * Builds the GraphQL query for fetching runs * @private */ static buildRunsQuery() { return ` query getRepositoryRuns( $login: String! $name: String! $provider: VCSProvider! $first: Int $after: String ) { repository(login: $login, name: $name, vcsProvider: $provider) { runs(first: $first, after: $after) { edges { node { id runUid commitOid branchName baseOid status createdAt updatedAt finishedAt summary { occurrencesIntroduced occurrencesResolved occurrencesSuppressed } repository { name id } } } pageInfo { hasNextPage hasPreviousPage startCursor endCursor } } } } `; } /** * Builds GraphQL query for fetching a run by UID * @private */ static buildRunByUidQuery() { return ` query getRunByUid($runUid: UUID!) { run(runUid: $runUid) { id runUid commitOid branchName baseOid status createdAt updatedAt finishedAt summary { occurrencesIntroduced occurrencesResolved occurrencesSuppressed } repository { name id } } } `; } /** * Builds GraphQL query for fetching a run by commit OID * @private */ static buildRunByCommitQuery() { return ` query getRunByCommit($commitOid: String!) { runByCommit(commitOid: $commitOid) { id runUid commitOid branchName baseOid status createdAt updatedAt finishedAt summary { occurrencesIntroduced occurrencesResolved occurrencesSuppressed } repository { name id } } } `; } /** * Extracts runs from GraphQL response * @private */ extractRunsFromResponse(responseData) { const runs = []; try { const repository = responseData?.repository; const repositoryRuns = repository?.runs; const repoRuns = (repositoryRuns?.edges ?? []); for (const { node: run } of repoRuns) { const runNode = run; runs.push(RunsClient.mapRunNode(runNode)); } } catch (error) { this.logger.error('Error extracting runs from response', { error }); } return runs; } /** * Extracts a single run from GraphQL response * @private */ extractSingleRunFromResponse(responseData) { try { const data = responseData; const runNode = data?.run || data?.runByCommit; if (!runNode || typeof runNode !== 'object') { return null; } return RunsClient.mapRunNode(runNode); } catch (error) { this.logger.error('Error extracting single run from response', { error }); return null; } } /** * Maps a run node from GraphQL to DeepSourceRun * @private */ static mapRunNode(runNode) { const summary = runNode.summary || {}; const repository = runNode.repository || {}; return { id: asGraphQLNodeId(String(runNode.id ?? '')), runUid: asRunId(String(runNode.runUid ?? '')), commitOid: asCommitOid(String(runNode.commitOid ?? '')), branchName: asBranchName(String(runNode.branchName ?? '')), baseOid: asCommitOid(String(runNode.baseOid ?? '')), status: String(runNode.status ?? 'UNKNOWN'), createdAt: String(runNode.createdAt ?? ''), updatedAt: String(runNode.updatedAt ?? ''), finishedAt: String(runNode.finishedAt ?? ''), summary: { occurrencesIntroduced: Number(summary.occurrencesIntroduced ?? 0), occurrencesResolved: Number(summary.occurrencesResolved ?? 0), occurrencesSuppressed: Number(summary.occurrencesSuppressed ?? 0), }, repository: { name: String(repository.name ?? ''), id: asGraphQLNodeId(String(repository.id ?? '')), }, }; } /** * Builds GraphQL query for fetching run occurrences * @private */ static buildRunOccurrencesQuery() { return ` query getRunOccurrences($runUid: UUID!, $first: Int, $checksFirst: Int) { run(runUid: $runUid) { checks(first: $checksFirst) { edges { node { analyzer { shortcode } occurrences(first: $first) { edges { node { id issue { id shortcode title category severity } path beginLine issueText } } pageInfo { hasNextPage endCursor } totalCount } } } pageInfo { hasNextPage endCursor } } } } `; } /** * Extracts issues from run occurrences response * @private */ extractIssuesFromRunResponse(responseData, limit) { const issues = []; const maxIssues = limit ?? Number.MAX_SAFE_INTEGER; try { const run = responseData?.run; const checks = run?.checks?.edges || []; for (const checkEdge of checks) { if (issues.length >= maxIssues) break; const checkNode = checkEdge?.node; const occurrences = checkNode?.occurrences?.edges || []; for (const occurrenceEdge of occurrences) { if (issues.length >= maxIssues) break; const occurrence = occurrenceEdge?.node; const issue = occurrence?.issue; if (occurrence && issue) { issues.push({ id: String(occurrence.id ?? 'unknown'), title: String(issue.title ?? 'Unknown Issue'), shortcode: String(issue.shortcode ?? 'UNKNOWN'), category: String(issue.category ?? 'UNKNOWN'), severity: String(issue.severity ?? 'UNKNOWN'), status: 'OPEN', // Occurrences in a run are typically open issues issue_text: String(occurrence.issueText ?? ''), file_path: String(occurrence.path ?? ''), line_number: Number(occurrence.beginLine ?? 0), tags: [], // Tags would need to be fetched separately if needed }); } } } } catch (error) { this.logger.error('Error extracting issues from run response', { error }); } return issues; } /** * Handles errors during runs fetching * @private */ handleRunsError(error) { this.logger.error('Error in listRuns', { errorType: typeof error, errorMessage: error instanceof Error ? error.message : String(error), }); // Handle special case where no runs exist if (isErrorWithMessage(error, 'NoneType')) { return { items: [], pageInfo: { hasNextPage: false, hasPreviousPage: false, }, totalCount: 0, }; } throw error; } } //# sourceMappingURL=runs-client.js.map