@shipfox/api-integration-github 12.6.0 → 13.1.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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +12 -0
- package/dist/api/client.d.ts +13 -3
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +83 -4
- package/dist/api/client.js.map +1 -1
- package/dist/core/agent-tools.d.ts.map +1 -1
- package/dist/core/agent-tools.js +93 -8
- package/dist/core/agent-tools.js.map +1 -1
- package/dist/core/bot-identity.d.ts +4 -0
- package/dist/core/bot-identity.d.ts.map +1 -0
- package/dist/core/bot-identity.js +15 -0
- package/dist/core/bot-identity.js.map +1 -0
- package/dist/core/github-agent-tool-catalog.d.ts +1 -1
- package/dist/core/github-agent-tool-catalog.d.ts.map +1 -1
- package/dist/core/github-agent-tool-catalog.js +19 -0
- package/dist/core/github-agent-tool-catalog.js.map +1 -1
- package/dist/core/source-control.d.ts +2 -1
- package/dist/core/source-control.d.ts.map +1 -1
- package/dist/core/source-control.js +15 -10
- package/dist/core/source-control.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/api/client.test.ts +206 -18
- package/src/api/client.ts +130 -8
- package/src/core/agent-tools.test.ts +134 -0
- package/src/core/agent-tools.ts +110 -13
- package/src/core/bot-identity.test.ts +19 -0
- package/src/core/bot-identity.ts +19 -0
- package/src/core/github-agent-tool-catalog.ts +35 -0
- package/src/core/source-control.test.ts +87 -1
- package/src/core/source-control.ts +27 -11
- package/tsconfig.build.tsbuildinfo +1 -1
package/src/core/agent-tools.ts
CHANGED
|
@@ -13,8 +13,9 @@ import {
|
|
|
13
13
|
createGithubInstallationTokenProvider,
|
|
14
14
|
type GithubInstallationTokenProvider,
|
|
15
15
|
} from '#api/installation-token-provider.js';
|
|
16
|
-
import {
|
|
16
|
+
import {normalizedGithubApiBaseUrl} from '#config.js';
|
|
17
17
|
import type {GithubInstallation} from '#db/installations.js';
|
|
18
|
+
import {githubAppBotLogin} from './bot-identity.js';
|
|
18
19
|
import {GithubIntegrationProviderError} from './errors.js';
|
|
19
20
|
import {
|
|
20
21
|
type GithubAgentToolCatalogEntry,
|
|
@@ -59,7 +60,6 @@ const GITHUB_GRAPHQL_ROUTE = 'POST /graphql';
|
|
|
59
60
|
const GITHUB_ARTIFACT_ARCHIVE_FORMAT = 'zip';
|
|
60
61
|
const GITHUB_ARTIFACT_DOWNLOAD_ROUTE = `GET /repos/{owner}/{repo}/actions/artifacts/{resource_id}/${GITHUB_ARTIFACT_ARCHIVE_FORMAT}`;
|
|
61
62
|
const GITHUB_ARTIFACT_DOWNLOAD_TIMEOUT_MS = 30_000;
|
|
62
|
-
const GITHUB_APP_BOT_SUFFIX = '[bot]';
|
|
63
63
|
const PENDING_REVIEW_PAGE_SIZE = 100;
|
|
64
64
|
const PENDING_REVIEW_MAX_PAGE_REQUESTS = 5;
|
|
65
65
|
const PENDING_REVIEW_LOOKUP_TIMEOUT_MS = 15_000;
|
|
@@ -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
|
-
|
|
205
|
+
executeGithubGraphqlOperation(
|
|
206
|
+
client,
|
|
207
|
+
tool.id as GithubAgentToolId,
|
|
208
|
+
method,
|
|
209
|
+
operation.parameters,
|
|
210
|
+
),
|
|
149
211
|
);
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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,
|
|
@@ -621,13 +725,6 @@ function latestPendingReviewOnPage(
|
|
|
621
725
|
return {malformed};
|
|
622
726
|
}
|
|
623
727
|
|
|
624
|
-
function githubAppBotLogin(): string {
|
|
625
|
-
const configuredUsername = config.GITHUB_APP_USERNAME?.trim() || config.GITHUB_APP_SLUG.trim();
|
|
626
|
-
return configuredUsername.toLowerCase().endsWith(GITHUB_APP_BOT_SUFFIX)
|
|
627
|
-
? configuredUsername
|
|
628
|
-
: `${configuredUsername}${GITHUB_APP_BOT_SUFFIX}`;
|
|
629
|
-
}
|
|
630
|
-
|
|
631
728
|
function githubToolResult(
|
|
632
729
|
toolId: GithubAgentToolId,
|
|
633
730
|
data: unknown,
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import {configuredGithubAppBotLogin, githubAppBotLogin, githubBotLogin} from './bot-identity.js';
|
|
2
|
+
|
|
3
|
+
describe('GitHub App bot identity', () => {
|
|
4
|
+
it.each([
|
|
5
|
+
['shipfox-ai', 'shipfox-ai[bot]'],
|
|
6
|
+
['shipfox-ai[bot]', 'shipfox-ai[bot]'],
|
|
7
|
+
['Shipfox-AI[Bot]', 'Shipfox-AI[Bot]'],
|
|
8
|
+
])('normalizes %s to %s', (username, expected) => {
|
|
9
|
+
expect(githubBotLogin(username)).toBe(expected);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('uses the configured username for commit attribution', () => {
|
|
13
|
+
expect(configuredGithubAppBotLogin()).toBe('shipfox-test[bot]');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('uses the configured username for App review identity', () => {
|
|
17
|
+
expect(githubAppBotLogin()).toBe('shipfox-test[bot]');
|
|
18
|
+
});
|
|
19
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import {config} from '#config.js';
|
|
2
|
+
|
|
3
|
+
const GITHUB_APP_BOT_SUFFIX = '[bot]';
|
|
4
|
+
|
|
5
|
+
export function githubBotLogin(username: string): string {
|
|
6
|
+
const normalized = username.trim();
|
|
7
|
+
return normalized.toLowerCase().endsWith(GITHUB_APP_BOT_SUFFIX)
|
|
8
|
+
? normalized
|
|
9
|
+
: `${normalized}${GITHUB_APP_BOT_SUFFIX}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function configuredGithubAppBotLogin(): string | undefined {
|
|
13
|
+
const configuredUsername = config.GITHUB_APP_USERNAME?.trim();
|
|
14
|
+
return configuredUsername ? githubBotLogin(configuredUsername) : undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function githubAppBotLogin(): string {
|
|
18
|
+
return configuredGithubAppBotLogin() ?? githubBotLogin(config.GITHUB_APP_SLUG);
|
|
19
|
+
}
|
|
@@ -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',
|
|
@@ -8,6 +8,7 @@ const VALID_COMMIT = 'a'.repeat(40);
|
|
|
8
8
|
function githubClient(overrides: Partial<GithubApiClient> = {}): GithubApiClient {
|
|
9
9
|
return {
|
|
10
10
|
exchangeOAuthCode: vi.fn(() => Promise.resolve('token')),
|
|
11
|
+
getBotUser: vi.fn(() => Promise.resolve({id: 12_345, login: 'shipfox-test[bot]'})),
|
|
11
12
|
listUserInstallations: vi.fn(() => Promise.resolve({installationIds: [], nextCursor: null})),
|
|
12
13
|
getInstallation: vi.fn(() => {
|
|
13
14
|
throw new Error('not used');
|
|
@@ -349,7 +350,7 @@ describe('GithubSourceControlProvider', () => {
|
|
|
349
350
|
},
|
|
350
351
|
gitAuthor: {
|
|
351
352
|
name: 'shipfox-test[bot]',
|
|
352
|
-
email: '
|
|
353
|
+
email: '12345+shipfox-test[bot]@users.noreply.github.com',
|
|
353
354
|
},
|
|
354
355
|
});
|
|
355
356
|
expect(result.repositoryUrl).not.toContain('ghs_installationtoken');
|
|
@@ -358,6 +359,91 @@ describe('GithubSourceControlProvider', () => {
|
|
|
358
359
|
repositoryId: 42,
|
|
359
360
|
permissions: {contents: 'write'},
|
|
360
361
|
});
|
|
362
|
+
expect(github.getBotUser).toHaveBeenCalledWith({
|
|
363
|
+
username: 'shipfox-test[bot]',
|
|
364
|
+
installationAccessToken: 'ghs_installationtoken',
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it('propagates bot identity resolution failures for write checkouts', async () => {
|
|
369
|
+
await createInstallation();
|
|
370
|
+
const github = githubClient({
|
|
371
|
+
getBotUser: vi.fn(() =>
|
|
372
|
+
Promise.reject(
|
|
373
|
+
new GithubIntegrationProviderError('provider-rejected', 'bot user not found'),
|
|
374
|
+
),
|
|
375
|
+
),
|
|
376
|
+
});
|
|
377
|
+
const provider = new GithubSourceControlProvider(github);
|
|
378
|
+
|
|
379
|
+
const result = provider.createCheckoutSpec({
|
|
380
|
+
connection: connection(),
|
|
381
|
+
externalRepositoryId: 'github:42',
|
|
382
|
+
permissions: {contents: 'write'},
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
await expect(result).rejects.toMatchObject({reason: 'provider-rejected'});
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it('propagates unexpected bot lookup errors', async () => {
|
|
389
|
+
await createInstallation();
|
|
390
|
+
const github = githubClient({
|
|
391
|
+
getBotUser: vi.fn(() => Promise.reject(new Error('unexpected lookup failure'))),
|
|
392
|
+
});
|
|
393
|
+
const provider = new GithubSourceControlProvider(github);
|
|
394
|
+
|
|
395
|
+
const result = provider.createCheckoutSpec({
|
|
396
|
+
connection: connection(),
|
|
397
|
+
externalRepositoryId: 'github:42',
|
|
398
|
+
permissions: {contents: 'write'},
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
await expect(result).rejects.toThrow('unexpected lookup failure');
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it('rejects write checkout when the bot identity resolver is unavailable', async () => {
|
|
405
|
+
await createInstallation();
|
|
406
|
+
const github = githubClient();
|
|
407
|
+
delete github.getBotUser;
|
|
408
|
+
const provider = new GithubSourceControlProvider(github);
|
|
409
|
+
|
|
410
|
+
const result = provider.createCheckoutSpec({
|
|
411
|
+
connection: connection(),
|
|
412
|
+
externalRepositoryId: 'github:42',
|
|
413
|
+
permissions: {contents: 'write'},
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
await expect(result).rejects.toMatchObject({reason: 'provider-unavailable'});
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it('omits the author and bot lookup for read-only checkouts', async () => {
|
|
420
|
+
await createInstallation();
|
|
421
|
+
const github = githubClient();
|
|
422
|
+
const provider = new GithubSourceControlProvider(github);
|
|
423
|
+
|
|
424
|
+
const result = await provider.createCheckoutSpec({
|
|
425
|
+
connection: connection(),
|
|
426
|
+
externalRepositoryId: 'github:42',
|
|
427
|
+
permissions: {contents: 'read'},
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
expect(result.gitAuthor).toBeUndefined();
|
|
431
|
+
expect(github.getBotUser).not.toHaveBeenCalled();
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it('omits the author and bot lookup when the App username is unset', async () => {
|
|
435
|
+
await createInstallation();
|
|
436
|
+
const github = githubClient();
|
|
437
|
+
const provider = new GithubSourceControlProvider(github, () => undefined);
|
|
438
|
+
|
|
439
|
+
const result = await provider.createCheckoutSpec({
|
|
440
|
+
connection: connection(),
|
|
441
|
+
externalRepositoryId: 'github:42',
|
|
442
|
+
permissions: {contents: 'write'},
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
expect(result.gitAuthor).toBeUndefined();
|
|
446
|
+
expect(github.getBotUser).not.toHaveBeenCalled();
|
|
361
447
|
});
|
|
362
448
|
|
|
363
449
|
it('defaults the checkout ref to the repository default branch', async () => {
|
|
@@ -25,21 +25,23 @@ import {
|
|
|
25
25
|
type TriggerReference,
|
|
26
26
|
} from '@shipfox/api-integration-spi';
|
|
27
27
|
import type {GithubApiClient, GithubRepository} from '#api/client.js';
|
|
28
|
-
import {config} from '#config.js';
|
|
29
28
|
import {getGithubInstallationByConnectionId} from '#db/installations.js';
|
|
29
|
+
import {configuredGithubAppBotLogin} from './bot-identity.js';
|
|
30
30
|
import {GithubIntegrationProviderError} from './errors.js';
|
|
31
31
|
|
|
32
32
|
type GithubIntegrationConnection = IntegrationConnection<'github'>;
|
|
33
33
|
|
|
34
34
|
const GITHUB_PROVIDER = 'github';
|
|
35
|
-
const GITHUB_APP_BOT_SUFFIX = '[bot]';
|
|
36
35
|
const SEARCH_PAGE_SIZE = 100;
|
|
37
36
|
const SEARCH_MAX_PAGES_PER_REQUEST = 5;
|
|
38
37
|
|
|
39
38
|
export class GithubSourceControlProvider
|
|
40
39
|
implements SourceControlProvider<GithubIntegrationConnection>
|
|
41
40
|
{
|
|
42
|
-
constructor(
|
|
41
|
+
constructor(
|
|
42
|
+
private readonly github: GithubApiClient,
|
|
43
|
+
private readonly appBotLogin: () => string | undefined = configuredGithubAppBotLogin,
|
|
44
|
+
) {}
|
|
43
45
|
|
|
44
46
|
async listRepositories(
|
|
45
47
|
input: ListRepositoriesInput<GithubIntegrationConnection>,
|
|
@@ -204,7 +206,11 @@ export class GithubSourceControlProvider
|
|
|
204
206
|
repositoryId,
|
|
205
207
|
permissions: input.permissions,
|
|
206
208
|
});
|
|
207
|
-
const
|
|
209
|
+
const botLogin = this.appBotLogin();
|
|
210
|
+
const gitAuthor =
|
|
211
|
+
botLogin && input.permissions?.contents === 'write'
|
|
212
|
+
? await githubAppGitAuthor(this.github, token, botLogin)
|
|
213
|
+
: undefined;
|
|
208
214
|
|
|
209
215
|
return {
|
|
210
216
|
repositoryUrl: repository.cloneUrl,
|
|
@@ -250,13 +256,23 @@ function sameGithubRepository(
|
|
|
250
256
|
return Boolean(firstName && secondName && firstName.toLowerCase() === secondName.toLowerCase());
|
|
251
257
|
}
|
|
252
258
|
|
|
253
|
-
function githubAppGitAuthor(
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
259
|
+
async function githubAppGitAuthor(
|
|
260
|
+
github: GithubApiClient,
|
|
261
|
+
installationAccessToken: string,
|
|
262
|
+
name: string,
|
|
263
|
+
): Promise<CheckoutSpec['gitAuthor']> {
|
|
264
|
+
if (!github.getBotUser) {
|
|
265
|
+
throw new GithubIntegrationProviderError(
|
|
266
|
+
'provider-unavailable',
|
|
267
|
+
'GitHub bot identity resolution is unavailable',
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const botUser = await github.getBotUser({username: name, installationAccessToken});
|
|
272
|
+
return {
|
|
273
|
+
name: botUser.login,
|
|
274
|
+
email: `${botUser.id}+${botUser.login}@users.noreply.github.com`,
|
|
275
|
+
};
|
|
260
276
|
}
|
|
261
277
|
|
|
262
278
|
function toRepositorySnapshot(repository: GithubRepository): RepositorySnapshot {
|