diff --git a/src/mcp/github-actions-pagination.ts b/src/mcp/github-actions-pagination.ts new file mode 100644 index 00000000..1bab170e --- /dev/null +++ b/src/mcp/github-actions-pagination.ts @@ -0,0 +1,19 @@ +import type { Octokit } from "@octokit/rest"; + +type ActionsClient = Octokit["actions"]; + +export type WorkflowRunsParams = Parameters< + ActionsClient["listWorkflowRunsForRepo"] +>[0]; + +export type WorkflowJobsParams = Parameters< + ActionsClient["listJobsForWorkflowRun"] +>[0]; + +export function listWorkflowRuns(client: Octokit, params: WorkflowRunsParams) { + return client.paginate(client.actions.listWorkflowRunsForRepo, params); +} + +export function listWorkflowJobs(client: Octokit, params: WorkflowJobsParams) { + return client.paginate(client.actions.listJobsForWorkflowRun, params); +} diff --git a/src/mcp/github-actions-server.ts b/src/mcp/github-actions-server.ts index e6006248..f1174e27 100644 --- a/src/mcp/github-actions-server.ts +++ b/src/mcp/github-actions-server.ts @@ -6,6 +6,10 @@ import { z } from "zod"; import { GITHUB_API_URL } from "../github/api/config"; import { mkdir, writeFile } from "fs/promises"; import { Octokit } from "@octokit/rest"; +import { + listWorkflowJobs, + listWorkflowRuns, +} from "./github-actions-pagination"; const REPO_OWNER = process.env.REPO_OWNER; const REPO_NAME = process.env.REPO_NAME; @@ -66,7 +70,7 @@ server.tool( }); const headSha = prData.head.sha; - const { data: runsData } = await client.actions.listWorkflowRunsForRepo({ + const runs = await listWorkflowRuns(client, { owner: REPO_OWNER!, repo: REPO_NAME!, head_sha: headSha, @@ -74,7 +78,6 @@ server.tool( }); // Process runs to create summary - const runs = runsData.workflow_runs || []; const summary = { total_runs: runs.length, failed: 0, @@ -148,13 +151,13 @@ server.tool( }); // Get jobs for this workflow run - const { data: jobsData } = await client.actions.listJobsForWorkflowRun({ + const jobs = await listWorkflowJobs(client, { owner: REPO_OWNER!, repo: REPO_NAME!, run_id, }); - const processedJobs = jobsData.jobs.map((job: any) => { + const processedJobs = jobs.map((job: any) => { // Extract failed steps const failedSteps = (job.steps || []) .filter((step: any) => step.conclusion === "failure") diff --git a/test/github-actions-pagination.test.ts b/test/github-actions-pagination.test.ts new file mode 100644 index 00000000..4ea77642 --- /dev/null +++ b/test/github-actions-pagination.test.ts @@ -0,0 +1,49 @@ +import { describe, expect, test } from "bun:test"; +import type { Octokit } from "@octokit/rest"; +import { + listWorkflowJobs, + listWorkflowRuns, +} from "../src/mcp/github-actions-pagination"; + +function createPaginatedClient(pages: T[][]) { + const request = async () => ({ data: pages[0] }); + const client = { + actions: { + listWorkflowRunsForRepo: request, + listJobsForWorkflowRun: request, + }, + paginate: async () => pages.flat(), + } as unknown as Octokit; + + return client; +} + +describe("GitHub Actions pagination", () => { + test("returns workflow runs from every page", async () => { + const firstPage = [{ id: 1 }, { id: 2 }]; + const secondPage = [{ id: 3 }]; + const client = createPaginatedClient([firstPage, secondPage]); + + const runs = await listWorkflowRuns(client, { + owner: "owner", + repo: "repo", + head_sha: "sha", + }); + + expect(runs.map((run) => run.id)).toEqual([1, 2, 3]); + }); + + test("returns workflow jobs from every page", async () => { + const firstPage = [{ id: 1 }, { id: 2 }]; + const secondPage = [{ id: 3 }]; + const client = createPaginatedClient([firstPage, secondPage]); + + const jobs = await listWorkflowJobs(client, { + owner: "owner", + repo: "repo", + run_id: 123, + }); + + expect(jobs.map((job) => job.id)).toEqual([1, 2, 3]); + }); +});