Skip to main content
Glama
runs-client.ts17.4 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 { DeepSourceRun, RunFilterParams, AnalysisRunStatus } from '../models/runs.js'; import { DeepSourceIssue } from '../models/issues.js'; import { PaginatedResponse } from '../utils/pagination/types.js'; import { isErrorWithMessage } from '../utils/errors/handlers.js'; import { BranchName, asGraphQLNodeId, asRunId, asCommitOid, asBranchName, } from '../types/branded.js'; /** * Response type for recent run issues query * @public */ export interface RecentRunIssuesResponse { run: DeepSourceRun | null; items: DeepSourceIssue[]; pageInfo: { hasNextPage: boolean; hasPreviousPage: boolean; startCursor?: string | null; endCursor?: string | null; }; totalCount: number; } /** * 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: string, params: RunFilterParams = {} ): Promise<PaginatedResponse<DeepSourceRun>> { 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<DeepSourceRun>(); } 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: string): Promise<DeepSourceRun | null> { 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: string, branchName: string ): Promise<DeepSourceRun | null> { try { this.logger.info('Finding most recent run for branch', { projectKey, branchName }); let mostRecentRun: DeepSourceRun | null = null; let hasNextPage = true; let after: string | undefined; // Paginate through runs to find the most recent for the specific branch while (hasNextPage) { const params: { first: number; after?: string } = { 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: string, branchName: BranchName, paginationParams?: { first?: number; after?: string; last?: number; before?: string } ): Promise<RecentRunIssuesResponse> { 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 */ private static buildRunsQuery(): string { 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 */ private static buildRunByUidQuery(): string { 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 */ private static buildRunByCommitQuery(): string { 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 */ private extractRunsFromResponse(responseData: unknown): DeepSourceRun[] { const runs: DeepSourceRun[] = []; try { const repository = (responseData as Record<string, unknown>)?.repository as Record< string, unknown >; const repositoryRuns = repository?.runs as Record<string, unknown>; const repoRuns = (repositoryRuns?.edges ?? []) as Array<Record<string, unknown>>; for (const { node: run } of repoRuns) { const runNode = run as Record<string, unknown>; 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 */ private extractSingleRunFromResponse(responseData: unknown): DeepSourceRun | null { try { const data = responseData as Record<string, unknown>; const runNode = data?.run || data?.runByCommit; if (!runNode || typeof runNode !== 'object') { return null; } return RunsClient.mapRunNode(runNode as Record<string, unknown>); } catch (error) { this.logger.error('Error extracting single run from response', { error }); return null; } } /** * Maps a run node from GraphQL to DeepSourceRun * @private */ private static mapRunNode(runNode: Record<string, unknown>): DeepSourceRun { const summary = (runNode.summary as Record<string, unknown>) || {}; const repository = (runNode.repository as Record<string, unknown>) || {}; 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') as AnalysisRunStatus, 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 */ private static buildRunOccurrencesQuery(): string { 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 */ private extractIssuesFromRunResponse(responseData: unknown, limit?: number): DeepSourceIssue[] { const issues: DeepSourceIssue[] = []; const maxIssues = limit ?? Number.MAX_SAFE_INTEGER; try { const run = (responseData as Record<string, unknown>)?.run as Record<string, unknown>; const checks = ((run?.checks as Record<string, unknown>)?.edges as Array<Record<string, unknown>>) || []; for (const checkEdge of checks) { if (issues.length >= maxIssues) break; const checkNode = checkEdge?.node as Record<string, unknown>; const occurrences = ((checkNode?.occurrences as Record<string, unknown>)?.edges as Array< Record<string, unknown> >) || []; for (const occurrenceEdge of occurrences) { if (issues.length >= maxIssues) break; const occurrence = occurrenceEdge?.node as Record<string, unknown>; const issue = occurrence?.issue as Record<string, unknown>; 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 */ private handleRunsError(error: unknown): PaginatedResponse<DeepSourceRun> { 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; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/sapientpants/deepsource-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server