@shipfox/api-integration-github 12.6.0 → 12.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@shipfox/api-integration-github",
3
3
  "license": "MIT",
4
- "version": "12.6.0",
4
+ "version": "12.7.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/ShipfoxHQ/shipfox.git",
@@ -83,6 +83,7 @@ const expectedCatalogRows = [
83
83
  'get_files',
84
84
  'get_commits',
85
85
  'get_review_comments',
86
+ 'get_review_threads',
86
87
  'get_reviews',
87
88
  'get_comments',
88
89
  'get_check_runs',
@@ -148,6 +149,14 @@ const expectedCatalogRows = [
148
149
  requiredScope: [{permission: 'pull_requests', access: 'write'}],
149
150
  methods: ['create', 'submit_pending', 'delete_pending'],
150
151
  },
152
+ {
153
+ id: 'pull_request_review_thread_write',
154
+ category: 'pull_requests',
155
+ sensitivity: 'write',
156
+ sensitive: false,
157
+ requiredScope: [{permission: 'pull_requests', access: 'write'}],
158
+ methods: ['resolve'],
159
+ },
151
160
  {
152
161
  id: 'add_comment_to_pending_review',
153
162
  category: 'pull_requests',
@@ -351,6 +360,12 @@ const githubOperationRouteCases = [
351
360
  args: {pull_number: 1},
352
361
  expectedRoute: 'GET /repos/{owner}/{repo}/pulls/{pull_number}/comments',
353
362
  },
363
+ {
364
+ toolId: 'pull_request_read',
365
+ method: 'get_review_threads',
366
+ args: {pull_number: 1},
367
+ expectedRoute: 'POST /graphql',
368
+ },
354
369
  {
355
370
  toolId: 'pull_request_read',
356
371
  method: 'get_reviews',
@@ -429,6 +444,12 @@ const githubOperationRouteCases = [
429
444
  runtimeInjectedProperties: ['review_id'],
430
445
  expectedRoute: 'DELETE /repos/{owner}/{repo}/pulls/{pull_number}/reviews/{review_id}',
431
446
  },
447
+ {
448
+ toolId: 'pull_request_review_thread_write',
449
+ method: 'resolve',
450
+ args: {thread_id: 'PRRT_kwDOExample'},
451
+ expectedRoute: 'POST /graphql',
452
+ },
432
453
  {
433
454
  toolId: 'add_comment_to_pending_review',
434
455
  args: {pull_number: 1, path: 'src/index.ts', body: 'Comment'},
@@ -663,6 +684,7 @@ describe('github agent tool catalog', () => {
663
684
  {properties: {method: {const: 'get_files'}}, required: []},
664
685
  {properties: {method: {const: 'get_commits'}}, required: []},
665
686
  {properties: {method: {const: 'get_review_comments'}}, required: []},
687
+ {properties: {method: {const: 'get_review_threads'}}, required: []},
666
688
  {properties: {method: {const: 'get_reviews'}}, required: []},
667
689
  {properties: {method: {const: 'get_comments'}}, required: []},
668
690
  {properties: {method: {const: 'get_check_runs'}}, required: ['ref']},
@@ -841,6 +863,104 @@ describe('github agent tool catalog', () => {
841
863
  });
842
864
  });
843
865
 
866
+ it('reads pull request review threads through GraphQL', async () => {
867
+ const request = vi.fn();
868
+ const data = {
869
+ repository: {
870
+ pullRequest: {
871
+ reviewThreads: {
872
+ nodes: [
873
+ {
874
+ id: 'PRRT_kwDOExample',
875
+ isResolved: false,
876
+ comments: {
877
+ nodes: [
878
+ {
879
+ id: 'PRRC_kwDOExample',
880
+ databaseId: 7,
881
+ body: 'Please handle this.',
882
+ author: {login: 'reviewer'},
883
+ path: 'src/index.ts',
884
+ line: 42,
885
+ },
886
+ ],
887
+ },
888
+ },
889
+ ],
890
+ },
891
+ },
892
+ },
893
+ };
894
+ const graphql = vi.fn().mockResolvedValueOnce(data);
895
+ const provider = createAgentToolsProvider({request, graphql});
896
+ const session = await provider.openSession({
897
+ connection: connection(),
898
+ tools: [pullRequestReadTool()],
899
+ scope: undefined,
900
+ });
901
+
902
+ const result = await session.call({
903
+ toolId: 'pull_request_read',
904
+ arguments: {
905
+ method: 'get_review_threads',
906
+ owner: 'shipfox',
907
+ repo: 'platform',
908
+ pull_number: 2,
909
+ cursor: 'cursor-1',
910
+ },
911
+ });
912
+
913
+ expect(request).not.toHaveBeenCalled();
914
+ expect(graphql).toHaveBeenCalledWith(
915
+ expect.stringContaining('reviewThreads(first: 100, after: $after)'),
916
+ {owner: 'shipfox', repo: 'platform', pullNumber: 2, after: 'cursor-1'},
917
+ );
918
+ const query = graphql.mock.calls[0]?.[0];
919
+ expect(query).toContain('isResolved');
920
+ expect(query).toContain('author');
921
+ expect(query).toContain('path');
922
+ expect(query).toContain('line');
923
+ expect(result).toEqual({
924
+ content: [{type: 'text', text: JSON.stringify(data)}],
925
+ structuredContent: data,
926
+ });
927
+ });
928
+
929
+ it('resolves a pull request review thread through GraphQL', async () => {
930
+ const request = vi.fn();
931
+ const data = {
932
+ resolveReviewThread: {
933
+ thread: {id: 'PRRT_kwDOExample', isResolved: true},
934
+ },
935
+ };
936
+ const graphql = vi.fn().mockResolvedValueOnce(data);
937
+ const provider = createAgentToolsProvider({request, graphql});
938
+ const session = await provider.openSession({
939
+ connection: connection(),
940
+ tools: [pullRequestReviewThreadWriteTool()],
941
+ scope: undefined,
942
+ });
943
+
944
+ const result = await session.call({
945
+ toolId: 'pull_request_review_thread_write',
946
+ arguments: {
947
+ method: 'resolve',
948
+ owner: 'shipfox',
949
+ repo: 'platform',
950
+ thread_id: 'PRRT_kwDOExample',
951
+ },
952
+ });
953
+
954
+ expect(request).not.toHaveBeenCalled();
955
+ expect(graphql).toHaveBeenCalledWith(expect.stringContaining('resolveReviewThread'), {
956
+ input: {threadId: 'PRRT_kwDOExample'},
957
+ });
958
+ expect(result).toEqual({
959
+ content: [{type: 'text', text: JSON.stringify(data)}],
960
+ structuredContent: data,
961
+ });
962
+ });
963
+
844
964
  it('projects issue comment reactions through the provider session', async () => {
845
965
  const request = vi.fn(() => Promise.resolve({data: {id: 7}}));
846
966
  const result = await callGithubToolWithRequest(
@@ -1546,6 +1666,20 @@ function pendingReviewTool() {
1546
1666
  return tool;
1547
1667
  }
1548
1668
 
1669
+ function pullRequestReadTool() {
1670
+ const tool = githubAgentToolCatalog.find((entry) => entry.id === 'pull_request_read');
1671
+ if (!tool) throw new Error('Missing pull_request_read tool');
1672
+ return tool;
1673
+ }
1674
+
1675
+ function pullRequestReviewThreadWriteTool() {
1676
+ const tool = githubAgentToolCatalog.find(
1677
+ (entry) => entry.id === 'pull_request_review_thread_write',
1678
+ );
1679
+ if (!tool) throw new Error('Missing pull_request_review_thread_write tool');
1680
+ return tool;
1681
+ }
1682
+
1549
1683
  function connection() {
1550
1684
  return {
1551
1685
  id: 'connection-1',
@@ -78,6 +78,63 @@ const ADD_PENDING_REVIEW_COMMENT_MUTATION = `
78
78
  }
79
79
  `;
80
80
 
81
+ const GET_PULL_REQUEST_REVIEW_THREADS_QUERY = `
82
+ query GetPullRequestReviewThreads(
83
+ $owner: String!
84
+ $repo: String!
85
+ $pullNumber: Int!
86
+ $after: String
87
+ ) {
88
+ repository(owner: $owner, name: $repo) {
89
+ pullRequest(number: $pullNumber) {
90
+ reviewThreads(first: 100, after: $after) {
91
+ nodes {
92
+ id
93
+ isResolved
94
+ comments(first: 100) {
95
+ nodes {
96
+ id
97
+ databaseId
98
+ body
99
+ author {
100
+ login
101
+ }
102
+ path
103
+ line
104
+ side
105
+ startLine
106
+ startSide
107
+ createdAt
108
+ updatedAt
109
+ url
110
+ }
111
+ pageInfo {
112
+ hasNextPage
113
+ endCursor
114
+ }
115
+ }
116
+ }
117
+ pageInfo {
118
+ hasNextPage
119
+ endCursor
120
+ }
121
+ }
122
+ }
123
+ }
124
+ }
125
+ `;
126
+
127
+ const RESOLVE_PULL_REQUEST_REVIEW_THREAD_MUTATION = `
128
+ mutation ResolvePullRequestReviewThread($input: ResolveReviewThreadInput!) {
129
+ resolveReviewThread(input: $input) {
130
+ thread {
131
+ id
132
+ isResolved
133
+ }
134
+ }
135
+ }
136
+ `;
137
+
81
138
  export class GithubAgentToolsProvider
82
139
  implements
83
140
  AgentToolsProvider<
@@ -145,11 +202,17 @@ export class GithubAgentToolsProvider
145
202
 
146
203
  if (operation.kind === 'graphql') {
147
204
  const data = await mapGithubError(() =>
148
- addCommentToPendingReview(client, operation.parameters),
205
+ executeGithubGraphqlOperation(
206
+ client,
207
+ tool.id as GithubAgentToolId,
208
+ method,
209
+ operation.parameters,
210
+ ),
149
211
  );
150
- return data === undefined
151
- ? githubToolError(NO_PENDING_REVIEW_MESSAGE, 'provider-rejected')
152
- : githubToolResult(tool.id as GithubAgentToolId, data);
212
+ if (data === undefined && tool.id === 'add_comment_to_pending_review') {
213
+ return githubToolError(NO_PENDING_REVIEW_MESSAGE, 'provider-rejected');
214
+ }
215
+ return githubToolResult(tool.id as GithubAgentToolId, data);
153
216
  }
154
217
 
155
218
  const operationParameters = await mapGithubError(() =>
@@ -322,6 +385,8 @@ export function githubOperationRoute(
322
385
  return `GET ${repoPath}/pulls/${pull}/commits`;
323
386
  case 'pull_request_read.get_review_comments':
324
387
  return `GET ${repoPath}/pulls/${pull}/comments`;
388
+ case 'pull_request_read.get_review_threads':
389
+ return GITHUB_GRAPHQL_ROUTE;
325
390
  case 'pull_request_read.get_reviews':
326
391
  return `GET ${repoPath}/pulls/${pull}/reviews`;
327
392
  case 'pull_request_read.get_comments':
@@ -350,6 +415,8 @@ export function githubOperationRoute(
350
415
  return `POST ${repoPath}/pulls/${pull}/reviews/{review_id}/events`;
351
416
  case 'pull_request_review_write.delete_pending':
352
417
  return `DELETE ${repoPath}/pulls/${pull}/reviews/{review_id}`;
418
+ case 'pull_request_review_thread_write.resolve':
419
+ return GITHUB_GRAPHQL_ROUTE;
353
420
  case 'add_comment_to_pending_review.':
354
421
  return GITHUB_GRAPHQL_ROUTE;
355
422
  case 'actions_list.list_workflows':
@@ -423,6 +490,43 @@ async function addCommentToPendingReview(
423
490
  return await client.graphql(ADD_PENDING_REVIEW_COMMENT_MUTATION, {input});
424
491
  }
425
492
 
493
+ async function executeGithubGraphqlOperation(
494
+ client: GithubToolClient,
495
+ toolId: GithubAgentToolId,
496
+ method: string | undefined,
497
+ parameters: Record<string, unknown>,
498
+ ): Promise<unknown | undefined> {
499
+ if (client.graphql === undefined) {
500
+ throw new GithubIntegrationProviderError(
501
+ 'malformed-provider-response',
502
+ 'GitHub client does not support GraphQL operations',
503
+ );
504
+ }
505
+
506
+ switch (`${toolId}.${method ?? ''}`) {
507
+ case 'pull_request_read.get_review_threads': {
508
+ const variables: Record<string, unknown> = {
509
+ owner: parameters.owner,
510
+ repo: parameters.repo,
511
+ pullNumber: parameters.pull_number,
512
+ };
513
+ if (typeof parameters.cursor === 'string') variables.after = parameters.cursor;
514
+ return await client.graphql(GET_PULL_REQUEST_REVIEW_THREADS_QUERY, variables);
515
+ }
516
+ case 'pull_request_review_thread_write.resolve':
517
+ return await client.graphql(RESOLVE_PULL_REQUEST_REVIEW_THREAD_MUTATION, {
518
+ input: {threadId: parameters.thread_id},
519
+ });
520
+ case 'add_comment_to_pending_review.':
521
+ return await addCommentToPendingReview(client, parameters);
522
+ default:
523
+ throw new GithubIntegrationProviderError(
524
+ 'malformed-provider-response',
525
+ 'GitHub operation does not support GraphQL operations',
526
+ );
527
+ }
528
+ }
529
+
426
530
  export function projectGithubOperationParameters(
427
531
  toolId: GithubAgentToolId,
428
532
  method: string | undefined,
@@ -174,6 +174,13 @@ const pullRequestReadMethods = [
174
174
  false,
175
175
  scopes.pullRequestsRead,
176
176
  ),
177
+ method(
178
+ 'get_review_threads',
179
+ 'Get review threads, their resolution state, and comments for a specific pull request.',
180
+ 'read',
181
+ false,
182
+ scopes.pullRequestsRead,
183
+ ),
177
184
  method(
178
185
  'get_reviews',
179
186
  'Get reviews for a specific pull request.',
@@ -221,6 +228,16 @@ const pullRequestReviewWriteMethods = [
221
228
  ),
222
229
  ] as const satisfies readonly GithubAgentToolCatalogMethod[];
223
230
 
231
+ const pullRequestReviewThreadWriteMethods = [
232
+ method(
233
+ 'resolve',
234
+ 'Resolve a pull request review thread.',
235
+ 'write',
236
+ false,
237
+ scopes.pullRequestsWrite,
238
+ ),
239
+ ] as const satisfies readonly GithubAgentToolCatalogMethod[];
240
+
224
241
  const actionsListMethods = [
225
242
  method('list_workflows', 'List workflows in a repository.', 'read', false, scopes.actionsRead),
226
243
  method(
@@ -472,6 +489,7 @@ export const githubAgentToolCatalog = [
472
489
  methodRequiredSchema('get_files', []),
473
490
  methodRequiredSchema('get_commits', []),
474
491
  methodRequiredSchema('get_review_comments', []),
492
+ methodRequiredSchema('get_review_threads', []),
475
493
  methodRequiredSchema('get_reviews', []),
476
494
  methodRequiredSchema('get_comments', []),
477
495
  methodRequiredSchema('get_check_runs', ['ref']),
@@ -666,6 +684,23 @@ export const githubAgentToolCatalog = [
666
684
  ),
667
685
  outputSchema: openObjectSchema('Pull request review write result'),
668
686
  }),
687
+ tool({
688
+ id: 'pull_request_review_thread_write',
689
+ category: 'pull_requests',
690
+ description: 'Resolve review threads on a pull request in a GitHub repository.',
691
+ methods: pullRequestReviewThreadWriteMethods,
692
+ inputSchema: repositoryInputSchema(
693
+ {
694
+ method: methodSchema(
695
+ pullRequestReviewThreadWriteMethods,
696
+ 'The write operation to perform on a pull request review thread',
697
+ ),
698
+ thread_id: stringSchema('The node ID of the review thread'),
699
+ },
700
+ ['method', 'thread_id'],
701
+ ),
702
+ outputSchema: openObjectSchema('Pull request review thread write result'),
703
+ }),
669
704
  tool({
670
705
  id: 'add_comment_to_pending_review',
671
706
  category: 'pull_requests',