@shipfox/api-integration-github 13.1.0 → 14.0.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 +17 -0
- package/dist/api/client.d.ts +9 -1
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +32 -2
- package/dist/api/client.js.map +1 -1
- package/dist/api/installation-token-envelope.d.ts.map +1 -1
- package/dist/api/installation-token-envelope.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 +27 -1
- package/dist/core/source-control.js.map +1 -1
- package/dist/index.d.ts +78 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/src/api/client.test.ts +89 -19
- package/src/api/client.ts +54 -1
- package/src/api/installation-token-envelope.ts +3 -1
- package/src/core/agent-tools.test.ts +1 -0
- package/src/core/install.test.ts +3 -0
- package/src/core/source-control.test.ts +93 -0
- package/src/core/source-control.ts +38 -0
- package/src/index.test.ts +3 -1
- package/src/index.ts +2 -0
- package/src/presentation/routes/install.test.ts +3 -0
- package/tsconfig.build.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shipfox/api-integration-github",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "14.0.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/ShipfoxHQ/shipfox.git",
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
"octokit": "^5.0.3",
|
|
29
29
|
"zod": "^4.4.3",
|
|
30
30
|
"@shipfox/api-auth-context": "12.2.0",
|
|
31
|
-
"@shipfox/api-integration-spi": "
|
|
32
|
-
"@shipfox/api-integration-github-dto": "
|
|
31
|
+
"@shipfox/api-integration-spi": "2.0.0",
|
|
32
|
+
"@shipfox/api-integration-github-dto": "14.0.0",
|
|
33
33
|
"@shipfox/config": "1.2.4",
|
|
34
34
|
"@shipfox/node-drizzle": "0.3.5",
|
|
35
35
|
"@shipfox/node-fastify": "0.4.2",
|
package/src/api/client.test.ts
CHANGED
|
@@ -7,29 +7,40 @@ import {createGithubApiClient, mapGithubError} from './client.js';
|
|
|
7
7
|
|
|
8
8
|
const GITHUB_INSTALLATION_TOKEN_PATTERN = /^ghs_[A-Za-z0-9._-]{36,}$/u;
|
|
9
9
|
|
|
10
|
-
const {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
10
|
+
const {
|
|
11
|
+
authMock,
|
|
12
|
+
createInstallationAccessTokenMock,
|
|
13
|
+
getByUsernameMock,
|
|
14
|
+
listRepositoryCommitsMock,
|
|
15
|
+
octokitOptionsMock,
|
|
16
|
+
requestMock,
|
|
17
|
+
RequestErrorMock,
|
|
18
|
+
} = vi.hoisted(() => {
|
|
19
|
+
class RequestErrorMock extends Error {
|
|
20
|
+
constructor(
|
|
21
|
+
message: string,
|
|
22
|
+
public readonly status: number,
|
|
23
|
+
) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.name = 'HttpError';
|
|
20
26
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
authMock: vi.fn(),
|
|
31
|
+
createInstallationAccessTokenMock: vi.fn(),
|
|
32
|
+
getByUsernameMock: vi.fn(),
|
|
33
|
+
listRepositoryCommitsMock: vi.fn(),
|
|
34
|
+
octokitOptionsMock: vi.fn(),
|
|
35
|
+
requestMock: vi.fn(),
|
|
36
|
+
RequestErrorMock,
|
|
37
|
+
};
|
|
38
|
+
});
|
|
29
39
|
|
|
30
40
|
vi.mock('octokit', () => ({
|
|
31
41
|
App: class App {
|
|
32
42
|
octokit = {
|
|
43
|
+
auth: authMock,
|
|
33
44
|
rest: {
|
|
34
45
|
apps: {createInstallationAccessToken: createInstallationAccessTokenMock},
|
|
35
46
|
users: {getByUsername: getByUsernameMock},
|
|
@@ -37,7 +48,11 @@ vi.mock('octokit', () => ({
|
|
|
37
48
|
};
|
|
38
49
|
},
|
|
39
50
|
Octokit: class Octokit {
|
|
40
|
-
rest = {
|
|
51
|
+
rest = {
|
|
52
|
+
repos: {listCommits: listRepositoryCommitsMock},
|
|
53
|
+
users: {getByUsername: getByUsernameMock},
|
|
54
|
+
};
|
|
55
|
+
request = requestMock;
|
|
41
56
|
|
|
42
57
|
constructor(options: unknown) {
|
|
43
58
|
octokitOptionsMock(options);
|
|
@@ -252,6 +267,61 @@ describe('mapGithubError', () => {
|
|
|
252
267
|
});
|
|
253
268
|
});
|
|
254
269
|
|
|
270
|
+
describe('OctokitGithubApiClient.listRepositoryCommits', () => {
|
|
271
|
+
beforeEach(() => {
|
|
272
|
+
authMock.mockReset();
|
|
273
|
+
listRepositoryCommitsMock.mockReset();
|
|
274
|
+
requestMock.mockReset();
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it.each([
|
|
278
|
+
[404, 'repository-not-found'],
|
|
279
|
+
[409, 'ref-not-found'],
|
|
280
|
+
[422, 'ref-not-found'],
|
|
281
|
+
] as const)('maps HTTP %i commit lookup failures to %s', async (status, reason) => {
|
|
282
|
+
authMock.mockResolvedValue({token: 'ghs_installationtoken'});
|
|
283
|
+
requestMock.mockResolvedValue({
|
|
284
|
+
data: {
|
|
285
|
+
id: 42,
|
|
286
|
+
owner: {login: 'shipfox'},
|
|
287
|
+
name: 'platform',
|
|
288
|
+
full_name: 'shipfox/platform',
|
|
289
|
+
default_branch: 'main',
|
|
290
|
+
private: true,
|
|
291
|
+
clone_url: 'https://github.com/shipfox/platform.git',
|
|
292
|
+
html_url: 'https://github.com/shipfox/platform',
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
listRepositoryCommitsMock.mockRejectedValue(
|
|
296
|
+
new RequestErrorMock('No commit found for SHA', status),
|
|
297
|
+
);
|
|
298
|
+
const client = createGithubApiClient();
|
|
299
|
+
|
|
300
|
+
const result = client.listRepositoryCommits({
|
|
301
|
+
installationId: 1,
|
|
302
|
+
repositoryId: 42,
|
|
303
|
+
ref: 'refs/heads/missing',
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
await expect(result).rejects.toMatchObject({
|
|
307
|
+
reason,
|
|
308
|
+
status,
|
|
309
|
+
message: 'No commit found for SHA',
|
|
310
|
+
});
|
|
311
|
+
expect(requestMock).toHaveBeenCalledWith('GET /repositories/{repository_id}', {
|
|
312
|
+
repository_id: 42,
|
|
313
|
+
request: {signal: expect.any(AbortSignal)},
|
|
314
|
+
});
|
|
315
|
+
expect(listRepositoryCommitsMock).toHaveBeenCalledWith({
|
|
316
|
+
owner: 'shipfox',
|
|
317
|
+
repo: 'platform',
|
|
318
|
+
sha: 'refs/heads/missing',
|
|
319
|
+
per_page: 1,
|
|
320
|
+
request: {signal: expect.any(AbortSignal)},
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
|
|
255
325
|
describe('OctokitGithubApiClient.createInstallationAccessToken', () => {
|
|
256
326
|
beforeEach(() => {
|
|
257
327
|
createInstallationAccessTokenMock.mockReset();
|
package/src/api/client.ts
CHANGED
|
@@ -63,6 +63,10 @@ export interface GithubFileContent {
|
|
|
63
63
|
size: number;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
export interface GithubCommit {
|
|
67
|
+
sha: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
66
70
|
export interface GithubUserInstallationPage {
|
|
67
71
|
installationIds: number[];
|
|
68
72
|
nextCursor: string | null;
|
|
@@ -104,6 +108,11 @@ export interface GithubApiClient extends Partial<GithubBotUserClient> {
|
|
|
104
108
|
ref: string;
|
|
105
109
|
path: string;
|
|
106
110
|
}): Promise<GithubFileContent>;
|
|
111
|
+
listRepositoryCommits(input: {
|
|
112
|
+
installationId: number;
|
|
113
|
+
repositoryId: number;
|
|
114
|
+
ref: string;
|
|
115
|
+
}): Promise<GithubCommit[]>;
|
|
107
116
|
createInstallationAccessToken(input: {
|
|
108
117
|
installationId: number;
|
|
109
118
|
repositoryId: number;
|
|
@@ -267,6 +276,7 @@ class OctokitGithubApiClient implements GithubApiClient, GithubBotUserClient {
|
|
|
267
276
|
const response = await mapGithubError(() =>
|
|
268
277
|
octokit.request('GET /repositories/{repository_id}', {
|
|
269
278
|
repository_id: input.repositoryId,
|
|
279
|
+
request: {signal: AbortSignal.timeout(GITHUB_API_TIMEOUT_MS)},
|
|
270
280
|
}),
|
|
271
281
|
);
|
|
272
282
|
|
|
@@ -405,6 +415,40 @@ class OctokitGithubApiClient implements GithubApiClient, GithubBotUserClient {
|
|
|
405
415
|
};
|
|
406
416
|
}
|
|
407
417
|
|
|
418
|
+
async listRepositoryCommits(input: {
|
|
419
|
+
installationId: number;
|
|
420
|
+
repositoryId: number;
|
|
421
|
+
ref: string;
|
|
422
|
+
}): Promise<GithubCommit[]> {
|
|
423
|
+
const octokit = await mapGithubError(() =>
|
|
424
|
+
getGithubInstallationOctokit(this.getApp(), input.installationId),
|
|
425
|
+
);
|
|
426
|
+
const repository = await this.getRepository({
|
|
427
|
+
installationId: input.installationId,
|
|
428
|
+
repositoryId: input.repositoryId,
|
|
429
|
+
});
|
|
430
|
+
const response = await mapGithubError(
|
|
431
|
+
() =>
|
|
432
|
+
octokit.rest.repos.listCommits({
|
|
433
|
+
owner: repository.ownerLogin,
|
|
434
|
+
repo: repository.name,
|
|
435
|
+
sha: input.ref,
|
|
436
|
+
per_page: 1,
|
|
437
|
+
request: {signal: AbortSignal.timeout(GITHUB_API_TIMEOUT_MS)},
|
|
438
|
+
}),
|
|
439
|
+
'ref-not-found',
|
|
440
|
+
);
|
|
441
|
+
return response.data.map((commit) => {
|
|
442
|
+
if (typeof commit.sha !== 'string') {
|
|
443
|
+
throw new GithubIntegrationProviderError(
|
|
444
|
+
'malformed-provider-response',
|
|
445
|
+
'GitHub commit response is missing the commit sha',
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
return {sha: commit.sha};
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
|
|
408
452
|
async createInstallationAccessToken(input: {
|
|
409
453
|
installationId: number;
|
|
410
454
|
repositoryId: number;
|
|
@@ -558,6 +602,7 @@ export async function mapGithubError<T>(
|
|
|
558
602
|
| 'repository-not-found'
|
|
559
603
|
| 'installation-not-found'
|
|
560
604
|
| 'file-not-found'
|
|
605
|
+
| 'ref-not-found'
|
|
561
606
|
| 'provider-rejected' = 'repository-not-found',
|
|
562
607
|
): Promise<T> {
|
|
563
608
|
try {
|
|
@@ -570,7 +615,15 @@ export async function mapGithubError<T>(
|
|
|
570
615
|
if (error instanceof RequestError) {
|
|
571
616
|
if (error.status === 404) {
|
|
572
617
|
throw new GithubIntegrationProviderError(
|
|
573
|
-
notFoundReason,
|
|
618
|
+
notFoundReason === 'ref-not-found' ? 'repository-not-found' : notFoundReason,
|
|
619
|
+
error.message,
|
|
620
|
+
undefined,
|
|
621
|
+
error.status,
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
if (notFoundReason === 'ref-not-found' && (error.status === 409 || error.status === 422)) {
|
|
625
|
+
throw new GithubIntegrationProviderError(
|
|
626
|
+
'ref-not-found',
|
|
574
627
|
error.message,
|
|
575
628
|
undefined,
|
|
576
629
|
error.status,
|
|
@@ -37,9 +37,11 @@ const terminalMintErrorReasons = new Set<IntegrationProviderErrorReason>([
|
|
|
37
37
|
'malformed-provider-response',
|
|
38
38
|
]);
|
|
39
39
|
|
|
40
|
+
// Ref reasons describe ref-resolution failures, which a token mint never
|
|
41
|
+
// observes; keep them out of the envelope schema and acknowledge them here.
|
|
40
42
|
type MissingProviderErrorReason = Exclude<
|
|
41
43
|
IntegrationProviderErrorReason,
|
|
42
|
-
(typeof providerErrorReasons)[number]
|
|
44
|
+
(typeof providerErrorReasons)[number] | 'ref-not-found' | 'ref-invalid'
|
|
43
45
|
>;
|
|
44
46
|
const providerErrorReasonSchemaCoversUnion: Record<MissingProviderErrorReason, never> = {};
|
|
45
47
|
void providerErrorReasonSchemaCoversUnion;
|
|
@@ -1734,6 +1734,7 @@ function githubClient(): GithubApiClient {
|
|
|
1734
1734
|
getRepository: vi.fn(() => Promise.reject(new Error('not used'))),
|
|
1735
1735
|
listRepositoryFiles: vi.fn(() => Promise.reject(new Error('not used'))),
|
|
1736
1736
|
fetchRepositoryFile: vi.fn(() => Promise.reject(new Error('not used'))),
|
|
1737
|
+
listRepositoryCommits: vi.fn(() => Promise.reject(new Error('not used'))),
|
|
1737
1738
|
createInstallationAccessToken: vi.fn(() => Promise.reject(new Error('not used'))),
|
|
1738
1739
|
};
|
|
1739
1740
|
}
|
package/src/core/install.test.ts
CHANGED
|
@@ -37,6 +37,9 @@ function githubClient(overrides: Partial<GithubApiClient> = {}): GithubApiClient
|
|
|
37
37
|
fetchRepositoryFile: vi.fn(() => {
|
|
38
38
|
throw new Error('not used');
|
|
39
39
|
}),
|
|
40
|
+
listRepositoryCommits: vi.fn(() => {
|
|
41
|
+
throw new Error('not used');
|
|
42
|
+
}),
|
|
40
43
|
createInstallationAccessToken: vi.fn(() =>
|
|
41
44
|
Promise.resolve({
|
|
42
45
|
token: 'ghs_installationtoken',
|
|
@@ -57,6 +57,9 @@ function githubClient(overrides: Partial<GithubApiClient> = {}): GithubApiClient
|
|
|
57
57
|
size: 58,
|
|
58
58
|
}),
|
|
59
59
|
),
|
|
60
|
+
listRepositoryCommits: vi.fn(() =>
|
|
61
|
+
Promise.resolve([{sha: 'a'.repeat(40)}, {sha: 'b'.repeat(40)}]),
|
|
62
|
+
),
|
|
60
63
|
createInstallationAccessToken: vi.fn(() =>
|
|
61
64
|
Promise.resolve({
|
|
62
65
|
token: 'ghs_installationtoken',
|
|
@@ -259,6 +262,96 @@ describe('GithubSourceControlProvider', () => {
|
|
|
259
262
|
});
|
|
260
263
|
});
|
|
261
264
|
|
|
265
|
+
it('resolves a branch ref to the commit it points at', async () => {
|
|
266
|
+
await createInstallation();
|
|
267
|
+
const github = githubClient();
|
|
268
|
+
const provider = new GithubSourceControlProvider(github);
|
|
269
|
+
|
|
270
|
+
const result = await provider.resolveRef({
|
|
271
|
+
connection: connection(),
|
|
272
|
+
externalRepositoryId: 'github:42',
|
|
273
|
+
ref: 'refs/heads/main',
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
expect(result).toEqual({ref: 'refs/heads/main', commit: VALID_COMMIT});
|
|
277
|
+
expect(github.listRepositoryCommits).toHaveBeenCalledWith({
|
|
278
|
+
installationId,
|
|
279
|
+
repositoryId: 42,
|
|
280
|
+
ref: 'refs/heads/main',
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('resolves a tag ref to the commit it points at', async () => {
|
|
285
|
+
await createInstallation();
|
|
286
|
+
const github = githubClient({
|
|
287
|
+
listRepositoryCommits: vi.fn(() => Promise.resolve([{sha: 'c'.repeat(40)}])),
|
|
288
|
+
});
|
|
289
|
+
const provider = new GithubSourceControlProvider(github);
|
|
290
|
+
|
|
291
|
+
const result = await provider.resolveRef({
|
|
292
|
+
connection: connection(),
|
|
293
|
+
externalRepositoryId: 'github:42',
|
|
294
|
+
ref: 'refs/tags/v1.0.0',
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
expect(result).toEqual({ref: 'refs/tags/v1.0.0', commit: 'c'.repeat(40)});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('maps a ref with no commits to ref-not-found', async () => {
|
|
301
|
+
await createInstallation();
|
|
302
|
+
const github = githubClient({
|
|
303
|
+
listRepositoryCommits: vi.fn(() => Promise.resolve([])),
|
|
304
|
+
});
|
|
305
|
+
const provider = new GithubSourceControlProvider(github);
|
|
306
|
+
|
|
307
|
+
const result = provider.resolveRef({
|
|
308
|
+
connection: connection(),
|
|
309
|
+
externalRepositoryId: 'github:42',
|
|
310
|
+
ref: 'refs/heads/missing',
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
await expect(result).rejects.toMatchObject({reason: 'ref-not-found'});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it('rejects a commit response with an invalid object id', async () => {
|
|
317
|
+
await createInstallation();
|
|
318
|
+
const github = githubClient({
|
|
319
|
+
listRepositoryCommits: vi.fn(() => Promise.resolve([{sha: 'not-a-commit'}])),
|
|
320
|
+
});
|
|
321
|
+
const provider = new GithubSourceControlProvider(github);
|
|
322
|
+
|
|
323
|
+
const result = provider.resolveRef({
|
|
324
|
+
connection: connection(),
|
|
325
|
+
externalRepositoryId: 'github:42',
|
|
326
|
+
ref: 'refs/heads/main',
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
await expect(result).rejects.toMatchObject({
|
|
330
|
+
reason: 'malformed-provider-response',
|
|
331
|
+
message: 'GitHub ref "refs/heads/main" resolved to an invalid commit',
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it.each([
|
|
336
|
+
'a'.repeat(40),
|
|
337
|
+
'refs/pull/17/head',
|
|
338
|
+
'main',
|
|
339
|
+
'-evil',
|
|
340
|
+
])('rejects ref %s as ref-invalid', async (ref) => {
|
|
341
|
+
await createInstallation();
|
|
342
|
+
const github = githubClient();
|
|
343
|
+
const provider = new GithubSourceControlProvider(github);
|
|
344
|
+
|
|
345
|
+
const result = provider.resolveRef({
|
|
346
|
+
connection: connection(),
|
|
347
|
+
externalRepositoryId: 'github:42',
|
|
348
|
+
ref,
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
await expect(result).rejects.toMatchObject({reason: 'ref-invalid'});
|
|
352
|
+
expect(github.listRepositoryCommits).not.toHaveBeenCalled();
|
|
353
|
+
});
|
|
354
|
+
|
|
262
355
|
it('fetches repository file contents using the provider-owned repository id', async () => {
|
|
263
356
|
await createInstallation();
|
|
264
357
|
const github = githubClient();
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
type IntegrationConnection,
|
|
11
11
|
isRecord,
|
|
12
12
|
isValidGitObjectId,
|
|
13
|
+
isValidResolvableRef,
|
|
13
14
|
isValidTriggerRef,
|
|
14
15
|
type ListFilesInput,
|
|
15
16
|
type ListRepositoriesInput,
|
|
@@ -20,6 +21,8 @@ import {
|
|
|
20
21
|
type RepositoryPage,
|
|
21
22
|
type RepositorySnapshot,
|
|
22
23
|
type RepositoryVisibility,
|
|
24
|
+
type ResolvedRef,
|
|
25
|
+
type ResolveRefInput,
|
|
23
26
|
type ResolveRepositoryInput,
|
|
24
27
|
type SourceControlProvider,
|
|
25
28
|
type TriggerReference,
|
|
@@ -194,6 +197,37 @@ export class GithubSourceControlProvider
|
|
|
194
197
|
};
|
|
195
198
|
}
|
|
196
199
|
|
|
200
|
+
async resolveRef(input: ResolveRefInput<GithubIntegrationConnection>): Promise<ResolvedRef> {
|
|
201
|
+
if (!isValidResolvableRef(input.ref)) {
|
|
202
|
+
throw new GithubIntegrationProviderError(
|
|
203
|
+
'ref-invalid',
|
|
204
|
+
`GitHub ref ${formatRefForMessage(input.ref)} is not a resolvable branch or tag name`,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
const installationId = await this.installationId(input.connection.id);
|
|
208
|
+
const {repositoryId} = parseGithubRepositoryLocator(input.externalRepositoryId);
|
|
209
|
+
const commits = await this.github.listRepositoryCommits({
|
|
210
|
+
installationId,
|
|
211
|
+
repositoryId,
|
|
212
|
+
ref: input.ref,
|
|
213
|
+
});
|
|
214
|
+
const commit = commits[0]?.sha;
|
|
215
|
+
if (!commit) {
|
|
216
|
+
throw new GithubIntegrationProviderError(
|
|
217
|
+
'ref-not-found',
|
|
218
|
+
`GitHub ref ${formatRefForMessage(input.ref)} does not resolve to a commit`,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
if (!isValidGitObjectId(commit)) {
|
|
222
|
+
throw new GithubIntegrationProviderError(
|
|
223
|
+
'malformed-provider-response',
|
|
224
|
+
`GitHub ref ${formatRefForMessage(input.ref)} resolved to an invalid commit`,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return {ref: input.ref, commit};
|
|
229
|
+
}
|
|
230
|
+
|
|
197
231
|
async createCheckoutSpec(
|
|
198
232
|
input: CreateCheckoutSpecInput<GithubIntegrationConnection>,
|
|
199
233
|
): Promise<CheckoutSpec> {
|
|
@@ -256,6 +290,10 @@ function sameGithubRepository(
|
|
|
256
290
|
return Boolean(firstName && secondName && firstName.toLowerCase() === secondName.toLowerCase());
|
|
257
291
|
}
|
|
258
292
|
|
|
293
|
+
function formatRefForMessage(ref: string): string {
|
|
294
|
+
return JSON.stringify(ref);
|
|
295
|
+
}
|
|
296
|
+
|
|
259
297
|
async function githubAppGitAuthor(
|
|
260
298
|
github: GithubApiClient,
|
|
261
299
|
installationAccessToken: string,
|
package/src/index.test.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {githubEventCatalog} from '@shipfox/api-integration-github-dto';
|
|
1
2
|
import {githubInstallationTokenNamespace} from '#api/installation-token-envelope.js';
|
|
2
3
|
import {createGithubIntegrationProvider} from '#index.js';
|
|
3
4
|
|
|
@@ -19,7 +20,7 @@ vi.mock('#core/webhook-processor.js', () => ({
|
|
|
19
20
|
describe('createGithubIntegrationProvider', () => {
|
|
20
21
|
it('shares installation-token cleanup with the direct and composed processors', async () => {
|
|
21
22
|
const deleteSecrets = vi.fn(() => Promise.resolve(1));
|
|
22
|
-
createGithubIntegrationProvider({
|
|
23
|
+
const provider = createGithubIntegrationProvider({
|
|
23
24
|
github: {} as never,
|
|
24
25
|
getExistingGithubConnection: vi.fn(() => Promise.resolve(undefined)),
|
|
25
26
|
connectGithubInstallation: vi.fn() as never,
|
|
@@ -31,6 +32,7 @@ describe('createGithubIntegrationProvider', () => {
|
|
|
31
32
|
getIntegrationConnectionById: vi.fn(() => Promise.resolve(undefined)),
|
|
32
33
|
deleteSecrets,
|
|
33
34
|
});
|
|
35
|
+
expect(provider.eventCatalog).toBe(githubEventCatalog);
|
|
34
36
|
const processorOptions = state.processorOptions as {
|
|
35
37
|
deleteInstallationTokenSecret: (params: {
|
|
36
38
|
workspaceId: string;
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {githubEventCatalog} from '@shipfox/api-integration-github-dto';
|
|
1
2
|
import type {
|
|
2
3
|
GetIntegrationConnectionByIdFn,
|
|
3
4
|
PublishIntegrationEventReceivedFn,
|
|
@@ -102,6 +103,7 @@ export function createGithubIntegrationProvider(options: CreateGithubIntegration
|
|
|
102
103
|
return {
|
|
103
104
|
provider: 'github' as const,
|
|
104
105
|
displayName: 'GitHub',
|
|
106
|
+
eventCatalog: githubEventCatalog,
|
|
105
107
|
adapters: {
|
|
106
108
|
source_control: new GithubSourceControlProvider(github),
|
|
107
109
|
agent_tools: new GithubAgentToolsProvider({
|
|
@@ -61,6 +61,9 @@ function githubClient(overrides: Partial<GithubApiClient> = {}): GithubApiClient
|
|
|
61
61
|
fetchRepositoryFile: vi.fn(() => {
|
|
62
62
|
throw new Error('not used');
|
|
63
63
|
}),
|
|
64
|
+
listRepositoryCommits: vi.fn(() => {
|
|
65
|
+
throw new Error('not used');
|
|
66
|
+
}),
|
|
64
67
|
createInstallationAccessToken: vi.fn(() =>
|
|
65
68
|
Promise.resolve({
|
|
66
69
|
token: 'ghs_installationtoken',
|