Skip to content

Commit 388a3c4

Browse files
committed
Fix build action artifact retrieval
1 parent db83c61 commit 388a3c4

11 files changed

Lines changed: 143 additions & 20 deletions

File tree

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@thatfactory/xcode-cloud-mcp",
3-
"version": "0.1.0",
3+
"version": "0.1.1",
44
"description": "Minimal MCP server for discovering Xcode Cloud workflows and retrieving build logs, issues, and test artifacts.",
55
"license": "MIT",
66
"type": "module",

src/api/resources/artifacts.ts

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@ export interface GroupedArtifacts {
1616
*/
1717
export class ArtifactsClient extends BaseAPIClient {
1818
/**
19-
* List artifacts for a build run.
19+
* List artifacts for a build action.
2020
*/
21-
async listForBuildRun(buildRunId: string): Promise<GroupedArtifacts> {
21+
async listForBuildAction(buildActionId: string): Promise<GroupedArtifacts> {
2222
const response = await this.get<CiArtifact[]>(
23-
`/v1/ciBuildRuns/${buildRunId}/artifacts`,
23+
`/v1/ciBuildActions/${buildActionId}/artifacts`,
2424
);
2525

2626
return groupArtifactsByType(response.data);
@@ -51,6 +51,7 @@ export function groupArtifactsByType(artifacts: CiArtifact[]): GroupedArtifacts
5151
for (const artifact of artifacts) {
5252
switch (artifact.attributes.fileType) {
5353
case 'LOG':
54+
case 'LOG_BUNDLE':
5455
groupedArtifacts.logs.push(artifact);
5556
break;
5657
case 'ARCHIVE':
@@ -76,3 +77,32 @@ export function groupArtifactsByType(artifacts: CiArtifact[]): GroupedArtifacts
7677

7778
return groupedArtifacts;
7879
}
80+
81+
/**
82+
* Merge grouped artifact collections from multiple build actions.
83+
*/
84+
export function mergeGroupedArtifacts(
85+
groupedArtifactCollections: GroupedArtifacts[],
86+
): GroupedArtifacts {
87+
const mergedArtifacts: GroupedArtifacts = {
88+
logs: [],
89+
archives: [],
90+
screenshots: [],
91+
videos: [],
92+
resultBundles: [],
93+
testProducts: [],
94+
other: [],
95+
};
96+
97+
for (const groupedArtifacts of groupedArtifactCollections) {
98+
mergedArtifacts.logs.push(...groupedArtifacts.logs);
99+
mergedArtifacts.archives.push(...groupedArtifacts.archives);
100+
mergedArtifacts.screenshots.push(...groupedArtifacts.screenshots);
101+
mergedArtifacts.videos.push(...groupedArtifacts.videos);
102+
mergedArtifacts.resultBundles.push(...groupedArtifacts.resultBundles);
103+
mergedArtifacts.testProducts.push(...groupedArtifacts.testProducts);
104+
mergedArtifacts.other.push(...groupedArtifacts.other);
105+
}
106+
107+
return mergedArtifacts;
108+
}

src/api/resources/builds.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { BaseAPIClient } from '../base-client.js';
2-
import type { CiBuildRun } from '../types.js';
2+
import type { CiBuildAction, CiBuildRun } from '../types.js';
33

44
/**
55
* Build run endpoints.
@@ -26,4 +26,15 @@ export class BuildsClient extends BaseAPIClient {
2626

2727
return response.data;
2828
}
29+
30+
/**
31+
* List build actions for a build run.
32+
*/
33+
async getActions(buildRunId: string): Promise<CiBuildAction[]> {
34+
const response = await this.get<CiBuildAction[]>(
35+
`/v1/ciBuildRuns/${buildRunId}/actions`,
36+
);
37+
38+
return response.data;
39+
}
2940
}

src/api/types.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,27 @@ export interface CiBuildRun {
9393
};
9494
}
9595

96+
/**
97+
* Xcode Cloud build action.
98+
*/
99+
export interface CiBuildAction {
100+
type: 'ciBuildActions';
101+
id: string;
102+
attributes: {
103+
actionType: 'BUILD' | 'TEST' | 'ANALYZE' | 'ARCHIVE';
104+
issueCounts?: CiIssueCounts | null;
105+
executionProgress: 'PENDING' | 'RUNNING' | 'COMPLETE';
106+
name: string;
107+
startedDate?: string;
108+
completionStatus?: 'SUCCEEDED' | 'FAILED' | 'ERRORED' | 'CANCELED' | 'SKIPPED';
109+
finishedDate?: string;
110+
isRequiredToPass?: boolean;
111+
};
112+
}
113+
96114
export type CiArtifactFileType =
97115
| 'LOG'
116+
| 'LOG_BUNDLE'
98117
| 'ARCHIVE'
99118
| 'XCODEBUILD_ARCHIVE'
100119
| 'RESULT_BUNDLE'

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export function createServer(): McpServer {
2222

2323
const server = new McpServer({
2424
name: 'Xcode Cloud MCP',
25-
version: '0.1.0',
25+
version: '0.1.1',
2626
});
2727

2828
registerDiscoveryTools(server, client);

src/tools/results.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
resolveBuildLocator,
77
type BuildSelector,
88
} from '../utils/build-locator.js';
9+
import { collectBuildRunArtifacts } from '../utils/build-artifacts.js';
910
import {
1011
extractTextFromArtifact,
1112
summarizeLogTexts,
@@ -60,7 +61,7 @@ export function registerResultTools(
6061
async (input: BuildLookupInput & { maxCharacters?: number }) => {
6162
try {
6263
const buildRun = await resolveBuildLocator(client, input);
63-
const groupedArtifacts = await client.artifacts.listForBuildRun(buildRun.id);
64+
const groupedArtifacts = await collectBuildRunArtifacts(client, buildRun.id);
6465
const maxCharacters = input.maxCharacters ?? 8000;
6566
const parsedLogTexts = await downloadLogTexts(
6667
client,

src/tools/tests.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
resolveBuildLocator,
77
type BuildSelector,
88
} from '../utils/build-locator.js';
9+
import { collectBuildRunArtifacts } from '../utils/build-artifacts.js';
910
import {
1011
extractTextFromArtifact,
1112
summarizeLogTexts,
@@ -36,7 +37,7 @@ export function registerTestTools(
3637
async (input: BuildLookupInput) => {
3738
try {
3839
const buildRun = await resolveBuildLocator(client, input);
39-
const groupedArtifacts = await client.artifacts.listForBuildRun(buildRun.id);
40+
const groupedArtifacts = await collectBuildRunArtifacts(client, buildRun.id);
4041
const parsedLogTexts = await downloadLogTexts(
4142
client,
4243
groupedArtifacts.logs,
@@ -80,7 +81,7 @@ export function registerTestTools(
8081
async (input: BuildLookupInput) => {
8182
try {
8283
const buildRun = await resolveBuildLocator(client, input);
83-
const groupedArtifacts = await client.artifacts.listForBuildRun(buildRun.id);
84+
const groupedArtifacts = await collectBuildRunArtifacts(client, buildRun.id);
8485

8586
return jsonResponse({
8687
buildRun: {

src/utils/build-artifacts.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import type { AppStoreConnectClient } from '../api/client.js';
2+
import {
3+
mergeGroupedArtifacts,
4+
type GroupedArtifacts,
5+
} from '../api/resources/artifacts.js';
6+
7+
/**
8+
* Resolve all artifacts for a build run by aggregating artifacts from each build action.
9+
*/
10+
export async function collectBuildRunArtifacts(
11+
client: AppStoreConnectClient,
12+
buildRunId: string,
13+
): Promise<GroupedArtifacts> {
14+
const buildActions = await client.builds.getActions(buildRunId);
15+
const groupedArtifactCollections = await Promise.all(
16+
buildActions.map((buildAction) =>
17+
client.artifacts.listForBuildAction(buildAction.id),
18+
),
19+
);
20+
21+
return mergeGroupedArtifacts(groupedArtifactCollections);
22+
}

tests/artifacts.test.ts

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,47 @@
11
import test from 'node:test';
22
import assert from 'node:assert/strict';
3-
import { groupArtifactsByType } from '../src/api/resources/artifacts.js';
3+
import {
4+
groupArtifactsByType,
5+
mergeGroupedArtifacts,
6+
} from '../src/api/resources/artifacts.js';
47
import type { CiArtifact } from '../src/api/types.js';
58

69
test('groupArtifactsByType classifies known artifact types', () => {
710
const groupedArtifacts = groupArtifactsByType([
811
createArtifact('1', 'LOG'),
9-
createArtifact('2', 'SCREENSHOT'),
10-
createArtifact('3', 'VIDEO'),
11-
createArtifact('4', 'RESULT_BUNDLE'),
12-
createArtifact('5', 'TEST_PRODUCTS'),
13-
createArtifact('6', 'ARCHIVE'),
12+
createArtifact('2', 'LOG_BUNDLE'),
13+
createArtifact('3', 'SCREENSHOT'),
14+
createArtifact('4', 'VIDEO'),
15+
createArtifact('5', 'RESULT_BUNDLE'),
16+
createArtifact('6', 'TEST_PRODUCTS'),
17+
createArtifact('7', 'ARCHIVE'),
1418
]);
1519

16-
assert.equal(groupedArtifacts.logs.length, 1);
20+
assert.equal(groupedArtifacts.logs.length, 2);
1721
assert.equal(groupedArtifacts.screenshots.length, 1);
1822
assert.equal(groupedArtifacts.videos.length, 1);
1923
assert.equal(groupedArtifacts.resultBundles.length, 1);
2024
assert.equal(groupedArtifacts.testProducts.length, 1);
2125
assert.equal(groupedArtifacts.archives.length, 1);
2226
});
2327

28+
test('mergeGroupedArtifacts combines action artifact groups', () => {
29+
const mergedArtifacts = mergeGroupedArtifacts([
30+
groupArtifactsByType([
31+
createArtifact('1', 'LOG'),
32+
createArtifact('2', 'SCREENSHOT'),
33+
]),
34+
groupArtifactsByType([
35+
createArtifact('3', 'LOG'),
36+
createArtifact('4', 'RESULT_BUNDLE'),
37+
]),
38+
]);
39+
40+
assert.equal(mergedArtifacts.logs.length, 2);
41+
assert.equal(mergedArtifacts.screenshots.length, 1);
42+
assert.equal(mergedArtifacts.resultBundles.length, 1);
43+
});
44+
2445
function createArtifact(
2546
id: string,
2647
fileType: CiArtifact['attributes']['fileType'],

0 commit comments

Comments
 (0)