Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 36 additions & 1 deletion src/api/base-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,41 @@ export class BaseAPIClient {
return this.request<TData>(url.toString());
}

/**
* Fetch all pages of a paginated list endpoint.
* Follows `links.next` until no more pages are available.
* If `maxItems` is provided, stops paginating once enough items are collected.
*/
protected async listAll<TData>(
path: string,
params?: Record<string, string>,
maxItems?: number,
): Promise<TData[]> {
const allData: TData[] = [];

let response = await this.get<TData>(path, params);
if (Array.isArray(response.data)) {
allData.push(...response.data);
} else {
return [response.data];
}

while (response.links?.next && (!maxItems || allData.length < maxItems)) {
// The next URL is a full absolute URL from Apple's API
// Use request() directly since the URL is already absolute
response = await this.request<TData>(response.links.next);
if (Array.isArray(response.data)) {
allData.push(...response.data);
}
}

if (maxItems && allData.length > maxItems) {
return allData.slice(0, maxItems);
}

return allData;
}

protected async patch<TData, TBody>(
path: string,
body: TBody,
Expand Down Expand Up @@ -107,4 +142,4 @@ export class BaseAPIClient {

return payload as APIResponse<TData, TIncluded>;
}
}
}
14 changes: 6 additions & 8 deletions src/api/resources/builds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,15 @@ export class BuildsClient extends BaseAPIClient {
}

/**
* List recent build runs for a workflow.
* List build runs for a workflow, paginating through all results.
* Optionally limit the total number of build runs returned.
*/
async listForWorkflow(workflowId: string, limit?: number): Promise<CiBuildRun[]> {
const response = await this.get<CiBuildRun[]>(
return this.listAll<CiBuildRun>(
`/v1/ciWorkflows/${workflowId}/buildRuns`,
{
...(limit ? { limit: String(limit) } : {}),
},
{ limit: '200' },
limit,
);

return response.data;
}

/**
Expand All @@ -37,4 +35,4 @@ export class BuildsClient extends BaseAPIClient {

return response.data;
}
}
}
12 changes: 4 additions & 8 deletions src/api/resources/products.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,9 @@ import type { CiProduct } from '../types.js';
*/
export class ProductsClient extends BaseAPIClient {
/**
* List Xcode Cloud products.
* List all Xcode Cloud products, paginating through all results.
*/
async list(limit?: number): Promise<CiProduct[]> {
const response = await this.get<CiProduct[]>('/v1/ciProducts', {
...(limit ? { limit: String(limit) } : {}),
});

return response.data;
async list(): Promise<CiProduct[]> {
return this.listAll<CiProduct>('/v1/ciProducts', { limit: '200' });
}
}
}
14 changes: 5 additions & 9 deletions src/api/resources/workflows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,13 @@ type WorkflowAttributeUpdate = Partial<CiWorkflow['attributes']>;
*/
export class WorkflowsClient extends BaseAPIClient {
/**
* List workflows belonging to a product.
* List all workflows belonging to a product, paginating through all results.
*/
async listForProduct(productId: string, limit?: number): Promise<CiWorkflow[]> {
const response = await this.get<CiWorkflow[]>(
async listForProduct(productId: string): Promise<CiWorkflow[]> {
return this.listAll<CiWorkflow>(
`/v1/ciProducts/${productId}/workflows`,
{
...(limit ? { limit: String(limit) } : {}),
},
{ limit: '200' },
);

return response.data;
}

/**
Expand Down Expand Up @@ -122,4 +118,4 @@ export class WorkflowsClient extends BaseAPIClient {
): Promise<CiWorkflow> {
return this.updateById(workflowId, { actions });
}
}
}
10 changes: 5 additions & 5 deletions src/tools/build-runs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,21 @@ export function registerBuildRunTools(
'list_build_runs',
{
description:
'List recent build runs for a workflow, optionally filtered by outcome.',
'List recent build runs for a workflow, optionally filtered by outcome. Automatically paginates through build runs. Use limit to cap the number of results returned.',
inputSchema: {
workflowId: z.string(),
limit: z.number().int().positive().max(200).optional(),
status: z.enum(['all', 'failed', 'pending', 'running', 'succeeded']).optional(),
limit: z.number().int().min(1).max(500).optional().describe('Maximum number of build runs to return. Defaults to 20 if not specified.'),
},
},
async ({
workflowId,
limit,
status,
limit,
}: {
workflowId: string;
limit?: number;
status?: BuildRunStatusFilter;
limit?: number;
}) => {
try {
const buildRuns = sortBuildRuns(
Expand Down Expand Up @@ -99,4 +99,4 @@ function filterBuildRuns(
return buildRuns.filter(
(buildRun) => buildRun.attributes.completionStatus === 'SUCCEEDED',
);
}
}
18 changes: 7 additions & 11 deletions src/tools/discovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,12 @@ export function registerDiscoveryTools(
'list_products',
{
description:
'List Xcode Cloud products available to the configured App Store Connect account.',
inputSchema: {
limit: z.number().int().positive().max(200).optional(),
},
'List Xcode Cloud products available to the configured App Store Connect account. Automatically paginates through all results.',
inputSchema: {},
},
async ({ limit }: { limit?: number }) => {
async () => {
try {
const products = await client.products.list(limit);
const products = await client.products.list();

return jsonResponse({
products: products.map((product) => ({
Expand All @@ -42,17 +40,15 @@ export function registerDiscoveryTools(
server.registerTool(
'list_workflows',
{
description: 'List workflows for a given Xcode Cloud product.',
description: 'List workflows for a given Xcode Cloud product. Automatically paginates through all results.',
inputSchema: {
productId: z.string(),
limit: z.number().int().positive().max(200).optional(),
},
},
async ({ productId, limit }: { productId: string; limit?: number }) => {
async ({ productId }: { productId: string }) => {
try {
const workflows = await client.workflows.listForProduct(
parseIdentifier(productId, 'product'),
limit,
);

return jsonResponse({
Expand Down Expand Up @@ -91,4 +87,4 @@ export function registerDiscoveryTools(
}
},
);
}
}
2 changes: 1 addition & 1 deletion src/utils/build-locator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export async function resolveBuildLocator(
}

const buildRuns = sortBuildRuns(
await client.builds.listForWorkflow(locator.workflowId!, 100),
await client.builds.listForWorkflow(locator.workflowId!, 50),
);

if (locator.buildNumber !== undefined) {
Expand Down
2 changes: 1 addition & 1 deletion tests/build-locator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,4 +128,4 @@ function createBuildRun(
},
},
};
}
}
Loading
Loading