@shipfox/api-integration-github 2.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 +2 -0
- package/.turbo/turbo-type$colon$emit.log +1 -0
- package/.turbo/turbo-type.log +1 -0
- package/CHANGELOG.md +138 -0
- package/LICENSE +21 -0
- package/dist/api/client.d.ts +90 -0
- package/dist/api/client.d.ts.map +1 -0
- package/dist/api/client.js +306 -0
- package/dist/api/client.js.map +1 -0
- package/dist/api/installation-token-envelope.d.ts +36 -0
- package/dist/api/installation-token-envelope.d.ts.map +1 -0
- package/dist/api/installation-token-envelope.js +123 -0
- package/dist/api/installation-token-envelope.js.map +1 -0
- package/dist/api/installation-token-provider.d.ts +23 -0
- package/dist/api/installation-token-provider.d.ts.map +1 -0
- package/dist/api/installation-token-provider.js +130 -0
- package/dist/api/installation-token-provider.js.map +1 -0
- package/dist/api/shared-installation-token-cache.d.ts +43 -0
- package/dist/api/shared-installation-token-cache.d.ts.map +1 -0
- package/dist/api/shared-installation-token-cache.js +224 -0
- package/dist/api/shared-installation-token-cache.js.map +1 -0
- package/dist/config.d.ts +15 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +44 -0
- package/dist/config.js.map +1 -0
- package/dist/core/actions-logs.d.ts +47 -0
- package/dist/core/actions-logs.d.ts.map +1 -0
- package/dist/core/actions-logs.js +134 -0
- package/dist/core/actions-logs.js.map +1 -0
- package/dist/core/agent-tool-selection.d.ts +4 -0
- package/dist/core/agent-tool-selection.d.ts.map +1 -0
- package/dist/core/agent-tool-selection.js +38 -0
- package/dist/core/agent-tool-selection.js.map +1 -0
- package/dist/core/agent-tools.d.ts +50 -0
- package/dist/core/agent-tools.d.ts.map +1 -0
- package/dist/core/agent-tools.js +1122 -0
- package/dist/core/agent-tools.js.map +1 -0
- package/dist/core/errors.d.ts +15 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +22 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/install.d.ts +38 -0
- package/dist/core/install.d.ts.map +1 -0
- package/dist/core/install.js +64 -0
- package/dist/core/install.js.map +1 -0
- package/dist/core/source-control.d.ts +15 -0
- package/dist/core/source-control.d.ts.map +1 -0
- package/dist/core/source-control.js +175 -0
- package/dist/core/source-control.js.map +1 -0
- package/dist/core/state.d.ts +12 -0
- package/dist/core/state.d.ts.map +1 -0
- package/dist/core/state.js +54 -0
- package/dist/core/state.js.map +1 -0
- package/dist/core/webhook.d.ts +20 -0
- package/dist/core/webhook.d.ts.map +1 -0
- package/dist/core/webhook.js +216 -0
- package/dist/core/webhook.js.map +1 -0
- package/dist/db/db.d.ts +433 -0
- package/dist/db/db.d.ts.map +1 -0
- package/dist/db/db.js +18 -0
- package/dist/db/db.js.map +1 -0
- package/dist/db/installation-token-lock.d.ts +8 -0
- package/dist/db/installation-token-lock.d.ts.map +1 -0
- package/dist/db/installation-token-lock.js +37 -0
- package/dist/db/installation-token-lock.js.map +1 -0
- package/dist/db/installations.d.ts +33 -0
- package/dist/db/installations.d.ts.map +1 -0
- package/dist/db/installations.js +56 -0
- package/dist/db/installations.js.map +1 -0
- package/dist/db/migrations.d.ts +2 -0
- package/dist/db/migrations.d.ts.map +1 -0
- package/dist/db/migrations.js +5 -0
- package/dist/db/migrations.js.map +1 -0
- package/dist/db/schema/common.d.ts +2 -0
- package/dist/db/schema/common.d.ts.map +1 -0
- package/dist/db/schema/common.js +4 -0
- package/dist/db/schema/common.js.map +1 -0
- package/dist/db/schema/installations.d.ts +218 -0
- package/dist/db/schema/installations.d.ts.map +1 -0
- package/dist/db/schema/installations.js +46 -0
- package/dist/db/schema/installations.js.map +1 -0
- package/dist/index.d.ts +53 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +66 -0
- package/dist/index.js.map +1 -0
- package/dist/metrics/index.d.ts +2 -0
- package/dist/metrics/index.d.ts.map +1 -0
- package/dist/metrics/index.js +3 -0
- package/dist/metrics/index.js.map +1 -0
- package/dist/metrics/instance.d.ts +14 -0
- package/dist/metrics/instance.d.ts.map +1 -0
- package/dist/metrics/instance.js +75 -0
- package/dist/metrics/instance.js.map +1 -0
- package/dist/presentation/dto/integrations.d.ts +15 -0
- package/dist/presentation/dto/integrations.d.ts.map +1 -0
- package/dist/presentation/dto/integrations.js +10 -0
- package/dist/presentation/dto/integrations.js.map +1 -0
- package/dist/presentation/e2eRoutes/create-connection.d.ts +11 -0
- package/dist/presentation/e2eRoutes/create-connection.d.ts.map +1 -0
- package/dist/presentation/e2eRoutes/create-connection.js +57 -0
- package/dist/presentation/e2eRoutes/create-connection.js.map +1 -0
- package/dist/presentation/e2eRoutes/index.d.ts +5 -0
- package/dist/presentation/e2eRoutes/index.d.ts.map +1 -0
- package/dist/presentation/e2eRoutes/index.js +11 -0
- package/dist/presentation/e2eRoutes/index.js.map +1 -0
- package/dist/presentation/routes/errors.d.ts +2 -0
- package/dist/presentation/routes/errors.d.ts.map +1 -0
- package/dist/presentation/routes/errors.js +46 -0
- package/dist/presentation/routes/errors.js.map +1 -0
- package/dist/presentation/routes/install.d.ts +13 -0
- package/dist/presentation/routes/install.d.ts.map +1 -0
- package/dist/presentation/routes/install.js +77 -0
- package/dist/presentation/routes/install.js.map +1 -0
- package/dist/presentation/routes/webhooks.d.ts +16 -0
- package/dist/presentation/routes/webhooks.d.ts.map +1 -0
- package/dist/presentation/routes/webhooks.js +110 -0
- package/dist/presentation/routes/webhooks.js.map +1 -0
- package/dist/tsconfig.test.tsbuildinfo +1 -0
- package/drizzle/0000_initial.sql +17 -0
- package/drizzle/meta/_journal.json +13 -0
- package/drizzle.config.ts +7 -0
- package/package.json +71 -0
- package/src/api/client.test.ts +94 -0
- package/src/api/client.ts +525 -0
- package/src/api/installation-token-envelope.ts +171 -0
- package/src/api/installation-token-provider.test.ts +241 -0
- package/src/api/installation-token-provider.ts +224 -0
- package/src/api/shared-installation-token-cache.test.ts +294 -0
- package/src/api/shared-installation-token-cache.ts +346 -0
- package/src/config.test.ts +14 -0
- package/src/config.ts +47 -0
- package/src/connection-external-url.test.ts +78 -0
- package/src/core/actions-logs.test.ts +156 -0
- package/src/core/actions-logs.ts +186 -0
- package/src/core/agent-tool-selection.test.ts +41 -0
- package/src/core/agent-tool-selection.ts +42 -0
- package/src/core/agent-tools.test.ts +539 -0
- package/src/core/agent-tools.ts +1269 -0
- package/src/core/errors.ts +23 -0
- package/src/core/install.test.ts +238 -0
- package/src/core/install.ts +119 -0
- package/src/core/source-control.test.ts +327 -0
- package/src/core/source-control.ts +223 -0
- package/src/core/state.test.ts +43 -0
- package/src/core/state.ts +85 -0
- package/src/core/webhook.test.ts +522 -0
- package/src/core/webhook.ts +296 -0
- package/src/db/db.ts +18 -0
- package/src/db/installation-token-lock.test.ts +68 -0
- package/src/db/installation-token-lock.ts +39 -0
- package/src/db/installations.test.ts +67 -0
- package/src/db/installations.ts +108 -0
- package/src/db/migrations.ts +4 -0
- package/src/db/schema/common.ts +3 -0
- package/src/db/schema/installations.ts +46 -0
- package/src/index.ts +124 -0
- package/src/metrics/index.ts +1 -0
- package/src/metrics/instance.ts +82 -0
- package/src/presentation/dto/integrations.ts +8 -0
- package/src/presentation/e2eRoutes/create-connection.ts +70 -0
- package/src/presentation/e2eRoutes/index.test.ts +126 -0
- package/src/presentation/e2eRoutes/index.ts +14 -0
- package/src/presentation/routes/errors.ts +43 -0
- package/src/presentation/routes/install.test.ts +279 -0
- package/src/presentation/routes/install.ts +93 -0
- package/src/presentation/routes/webhooks.test.ts +560 -0
- package/src/presentation/routes/webhooks.ts +115 -0
- package/test/env.ts +16 -0
- package/test/factories/github-installation.ts +35 -0
- package/test/factories/index.ts +1 -0
- package/test/fixtures/github-webhook.ts +20 -0
- package/test/globalSetup.ts +14 -0
- package/test/index.ts +2 -0
- package/test/setup.ts +17 -0
- package/tsconfig.build.json +9 -0
- package/tsconfig.build.tsbuildinfo +1 -0
- package/tsconfig.json +3 -0
- package/tsconfig.test.json +9 -0
- package/vitest.config.ts +12 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type {GithubApiClient} from '#api/client.js';
|
|
2
|
+
import type {GithubInstallation} from '#db/installations.js';
|
|
3
|
+
import {createGithubIntegrationProvider} from '#index.js';
|
|
4
|
+
|
|
5
|
+
function githubClient(): GithubApiClient {
|
|
6
|
+
return {
|
|
7
|
+
exchangeOAuthCode: vi.fn(() => Promise.resolve('user-token')),
|
|
8
|
+
listUserInstallations: vi.fn(() => Promise.resolve({installationIds: [], nextCursor: null})),
|
|
9
|
+
getInstallation: vi.fn(() => Promise.reject(new Error('not used'))),
|
|
10
|
+
listInstallationRepositories: vi.fn(() => Promise.reject(new Error('not used'))),
|
|
11
|
+
getRepository: vi.fn(() => Promise.reject(new Error('not used'))),
|
|
12
|
+
listRepositoryFiles: vi.fn(() => Promise.reject(new Error('not used'))),
|
|
13
|
+
fetchRepositoryFile: vi.fn(() => Promise.reject(new Error('not used'))),
|
|
14
|
+
createInstallationToken: vi.fn(() => Promise.reject(new Error('not used'))),
|
|
15
|
+
} as unknown as GithubApiClient;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function installation(overrides: Partial<GithubInstallation> = {}): GithubInstallation {
|
|
19
|
+
return {
|
|
20
|
+
id: crypto.randomUUID(),
|
|
21
|
+
connectionId: crypto.randomUUID(),
|
|
22
|
+
installationId: '123',
|
|
23
|
+
accountLogin: 'shipfox',
|
|
24
|
+
accountType: 'Organization',
|
|
25
|
+
repositorySelection: 'all',
|
|
26
|
+
suspendedAt: null,
|
|
27
|
+
deletedAt: null,
|
|
28
|
+
latestEvent: {},
|
|
29
|
+
installerUserId: null,
|
|
30
|
+
createdAt: new Date(),
|
|
31
|
+
updatedAt: new Date(),
|
|
32
|
+
...overrides,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function createProvider(lookup: (connectionId: string) => Promise<GithubInstallation | undefined>) {
|
|
37
|
+
return createGithubIntegrationProvider({
|
|
38
|
+
github: githubClient(),
|
|
39
|
+
getExistingGithubConnection: vi.fn(() => Promise.resolve(undefined)),
|
|
40
|
+
connectGithubInstallation: vi.fn() as never,
|
|
41
|
+
coreDb: vi.fn() as never,
|
|
42
|
+
publishIntegrationEventReceived: vi.fn(() => Promise.resolve({published: false})),
|
|
43
|
+
publishSourcePush: vi.fn(() => Promise.resolve({published: false})),
|
|
44
|
+
recordDeliveryOnly: vi.fn(() => Promise.resolve()),
|
|
45
|
+
getIntegrationConnectionById: vi.fn(() => Promise.resolve(undefined)),
|
|
46
|
+
getGithubInstallationByConnectionId: lookup,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe('github connectionExternalUrl', () => {
|
|
51
|
+
it('resolves the organization installation settings URL for org accounts', async () => {
|
|
52
|
+
const provider = createProvider(() =>
|
|
53
|
+
Promise.resolve(installation({accountType: 'Organization', accountLogin: 'acme'})),
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const url = await provider.connectionExternalUrl({id: crypto.randomUUID()});
|
|
57
|
+
|
|
58
|
+
expect(url).toBe('https://github.com/organizations/acme/settings/installations/123');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('resolves the user installation settings URL for user accounts', async () => {
|
|
62
|
+
const provider = createProvider(() =>
|
|
63
|
+
Promise.resolve(installation({accountType: 'User', accountLogin: 'octocat'})),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const url = await provider.connectionExternalUrl({id: crypto.randomUUID()});
|
|
67
|
+
|
|
68
|
+
expect(url).toBe('https://github.com/settings/installations/123');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('returns undefined when the installation row is missing', async () => {
|
|
72
|
+
const provider = createProvider(() => Promise.resolve(undefined));
|
|
73
|
+
|
|
74
|
+
const url = await provider.connectionExternalUrl({id: crypto.randomUUID()});
|
|
75
|
+
|
|
76
|
+
expect(url).toBeUndefined();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildGithubFailedJobLogsResult,
|
|
3
|
+
buildGithubSingleJobLogResult,
|
|
4
|
+
DEFAULT_JOB_LOG_TAIL_LINES,
|
|
5
|
+
MAX_JOB_LOG_LINE_BYTES,
|
|
6
|
+
MAX_TRUNCATED_JOB_LOG_LINE_CHARS,
|
|
7
|
+
processGithubJobLogContent,
|
|
8
|
+
} from './actions-logs.js';
|
|
9
|
+
|
|
10
|
+
describe('buildGithubSingleJobLogResult', () => {
|
|
11
|
+
it('returns a logs URL without fetching content by default', async () => {
|
|
12
|
+
const fetchContent = vi.fn(() => Promise.resolve('full log'));
|
|
13
|
+
|
|
14
|
+
const result = await buildGithubSingleJobLogResult({
|
|
15
|
+
job: job({fetchContent}),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
expect(fetchContent).not.toHaveBeenCalled();
|
|
19
|
+
expect(result).toEqual({
|
|
20
|
+
job_id: 1,
|
|
21
|
+
job_name: 'Unit tests',
|
|
22
|
+
logs_url: 'https://github.example/logs/1',
|
|
23
|
+
message: 'Job logs are available for download',
|
|
24
|
+
note: 'The logs_url provides a download link for the individual job logs in plain text format.\nUse return_content=true to get the actual log content.',
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('returns default tail content when content is requested', async () => {
|
|
29
|
+
const lines = Array.from(
|
|
30
|
+
{length: DEFAULT_JOB_LOG_TAIL_LINES + 50},
|
|
31
|
+
(_value, index) => `line ${index + 1}`,
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const result = await buildGithubSingleJobLogResult({
|
|
35
|
+
job: job({fetchContent: async () => lines.join('\n')}),
|
|
36
|
+
returnContent: true,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
expect(result).toEqual({
|
|
40
|
+
job_id: 1,
|
|
41
|
+
job_name: 'Unit tests',
|
|
42
|
+
logs_content: lines.slice(-DEFAULT_JOB_LOG_TAIL_LINES).join('\n'),
|
|
43
|
+
message: 'Job logs content retrieved successfully',
|
|
44
|
+
original_length: DEFAULT_JOB_LOG_TAIL_LINES + 50,
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('buildGithubFailedJobLogsResult', () => {
|
|
50
|
+
it('reports when a workflow run has no failed jobs', async () => {
|
|
51
|
+
const result = await buildGithubFailedJobLogsResult({
|
|
52
|
+
runId: 123,
|
|
53
|
+
jobs: [job({conclusion: 'success'})],
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
expect(result).toEqual({
|
|
57
|
+
message: 'No failed jobs found in this workflow run',
|
|
58
|
+
run_id: 123,
|
|
59
|
+
total_jobs: 1,
|
|
60
|
+
failed_jobs: 0,
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('returns logs for every failed job without a failed-job cap', async () => {
|
|
65
|
+
const jobs = Array.from({length: 25}, (_value, index) =>
|
|
66
|
+
job({jobId: index + 1, conclusion: index % 2 === 0 ? 'failure' : 'success'}),
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const result = await buildGithubFailedJobLogsResult({runId: 123, jobs});
|
|
70
|
+
|
|
71
|
+
expect(result).toMatchObject({
|
|
72
|
+
message: 'Retrieved logs for 13 failed jobs',
|
|
73
|
+
run_id: 123,
|
|
74
|
+
total_jobs: 25,
|
|
75
|
+
failed_jobs: 13,
|
|
76
|
+
return_format: {content: false, urls: true},
|
|
77
|
+
});
|
|
78
|
+
expect(result.logs).toHaveLength(13);
|
|
79
|
+
expect(result.logs?.[0]).toMatchObject({
|
|
80
|
+
job_id: 1,
|
|
81
|
+
job_name: 'Unit tests',
|
|
82
|
+
logs_url: 'https://github.example/logs/1',
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('keeps failed-job fetch errors local to the affected job', async () => {
|
|
87
|
+
const result = await buildGithubFailedJobLogsResult({
|
|
88
|
+
runId: 123,
|
|
89
|
+
returnContent: true,
|
|
90
|
+
jobs: [
|
|
91
|
+
job({
|
|
92
|
+
conclusion: 'failure',
|
|
93
|
+
fetchContent: () => Promise.reject(new Error('download failed')),
|
|
94
|
+
}),
|
|
95
|
+
],
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
expect(result.logs).toEqual([
|
|
99
|
+
{
|
|
100
|
+
job_id: 1,
|
|
101
|
+
job_name: 'Unit tests',
|
|
102
|
+
error: 'download failed',
|
|
103
|
+
},
|
|
104
|
+
]);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('processGithubJobLogContent', () => {
|
|
109
|
+
it('uses the smaller of tail_lines and content window size', () => {
|
|
110
|
+
const lines = Array.from({length: 10}, (_value, index) => `line ${index + 1}`);
|
|
111
|
+
|
|
112
|
+
const result = processGithubJobLogContent(lines.join('\n'), {
|
|
113
|
+
tailLines: 8,
|
|
114
|
+
contentWindowSize: 3,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
expect(result).toEqual({
|
|
118
|
+
content: ['line 8', 'line 9', 'line 10'].join('\n'),
|
|
119
|
+
totalLines: 10,
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('defaults invalid tail_lines to 500', () => {
|
|
124
|
+
const lines = Array.from(
|
|
125
|
+
{length: DEFAULT_JOB_LOG_TAIL_LINES + 1},
|
|
126
|
+
(_value, index) => `line ${index + 1}`,
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
const result = processGithubJobLogContent(lines.join('\n'), {tailLines: 0});
|
|
130
|
+
|
|
131
|
+
expect(result.content).toBe(lines.slice(-DEFAULT_JOB_LOG_TAIL_LINES).join('\n'));
|
|
132
|
+
expect(result.totalLines).toBe(DEFAULT_JOB_LOG_TAIL_LINES + 1);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('truncates extremely long single lines', () => {
|
|
136
|
+
const line = 'a'.repeat(MAX_JOB_LOG_LINE_BYTES + 1);
|
|
137
|
+
|
|
138
|
+
const result = processGithubJobLogContent(line);
|
|
139
|
+
|
|
140
|
+
expect(result.content).toBe(`${'a'.repeat(MAX_TRUNCATED_JOB_LOG_LINE_CHARS)}...\n[TRUNCATED]`);
|
|
141
|
+
expect(result.totalLines).toBe(1);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
function job(
|
|
146
|
+
overrides: Partial<Parameters<typeof buildGithubSingleJobLogResult>[0]['job']> = {},
|
|
147
|
+
): Parameters<typeof buildGithubSingleJobLogResult>[0]['job'] {
|
|
148
|
+
const jobId = overrides.jobId ?? 1;
|
|
149
|
+
return {
|
|
150
|
+
jobId,
|
|
151
|
+
name: 'Unit tests',
|
|
152
|
+
conclusion: 'failure',
|
|
153
|
+
logsUrl: `https://github.example/logs/${jobId}`,
|
|
154
|
+
...overrides,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import {Buffer} from 'node:buffer';
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_JOB_LOG_TAIL_LINES = 500;
|
|
4
|
+
export const DEFAULT_JOB_LOG_CONTENT_WINDOW_LINES = 5000;
|
|
5
|
+
export const MAX_JOB_LOG_RING_BUFFER_LINES = 100000;
|
|
6
|
+
export const MAX_JOB_LOG_LINE_BYTES = 10 * 1024 * 1024;
|
|
7
|
+
export const MAX_TRUNCATED_JOB_LOG_LINE_CHARS = 1000;
|
|
8
|
+
|
|
9
|
+
export interface GithubJobLogSource {
|
|
10
|
+
jobId: number;
|
|
11
|
+
name?: string | undefined;
|
|
12
|
+
conclusion?: string | undefined;
|
|
13
|
+
logsUrl: string;
|
|
14
|
+
fetchContent?: (() => Promise<string>) | undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface BuildGithubSingleJobLogResultInput {
|
|
18
|
+
job: GithubJobLogSource;
|
|
19
|
+
returnContent?: boolean | undefined;
|
|
20
|
+
tailLines?: number | undefined;
|
|
21
|
+
contentWindowSize?: number | undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface BuildGithubFailedJobLogsResultInput {
|
|
25
|
+
jobs: readonly GithubJobLogSource[];
|
|
26
|
+
runId: number;
|
|
27
|
+
returnContent?: boolean | undefined;
|
|
28
|
+
tailLines?: number | undefined;
|
|
29
|
+
contentWindowSize?: number | undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type GithubJobLogResult = Record<string, unknown>;
|
|
33
|
+
|
|
34
|
+
export interface GithubFailedJobLogsResult {
|
|
35
|
+
message: string;
|
|
36
|
+
run_id: number;
|
|
37
|
+
total_jobs: number;
|
|
38
|
+
failed_jobs: number;
|
|
39
|
+
logs?: GithubJobLogResult[] | undefined;
|
|
40
|
+
return_format?: {content: boolean; urls: boolean} | undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function buildGithubSingleJobLogResult(
|
|
44
|
+
input: BuildGithubSingleJobLogResultInput,
|
|
45
|
+
): Promise<GithubJobLogResult> {
|
|
46
|
+
return buildGithubJobLogData(input);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function buildGithubFailedJobLogsResult(
|
|
50
|
+
input: BuildGithubFailedJobLogsResultInput,
|
|
51
|
+
): Promise<GithubFailedJobLogsResult> {
|
|
52
|
+
const failedJobs = input.jobs.filter((job) => job.conclusion === 'failure');
|
|
53
|
+
|
|
54
|
+
if (failedJobs.length === 0) {
|
|
55
|
+
return {
|
|
56
|
+
message: 'No failed jobs found in this workflow run',
|
|
57
|
+
run_id: input.runId,
|
|
58
|
+
total_jobs: input.jobs.length,
|
|
59
|
+
failed_jobs: 0,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const logs: GithubJobLogResult[] = [];
|
|
64
|
+
for (const job of failedJobs) {
|
|
65
|
+
try {
|
|
66
|
+
logs.push(await buildGithubJobLogData({job, ...input}));
|
|
67
|
+
} catch (error) {
|
|
68
|
+
logs.push({
|
|
69
|
+
job_id: job.jobId,
|
|
70
|
+
...(job.name ? {job_name: job.name} : {}),
|
|
71
|
+
error: error instanceof Error ? error.message : String(error),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
message: `Retrieved logs for ${failedJobs.length} failed jobs`,
|
|
78
|
+
run_id: input.runId,
|
|
79
|
+
total_jobs: input.jobs.length,
|
|
80
|
+
failed_jobs: failedJobs.length,
|
|
81
|
+
logs,
|
|
82
|
+
return_format: {content: input.returnContent === true, urls: input.returnContent !== true},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function buildGithubJobLogData(
|
|
87
|
+
input: BuildGithubSingleJobLogResultInput,
|
|
88
|
+
): Promise<GithubJobLogResult> {
|
|
89
|
+
const result: GithubJobLogResult = {
|
|
90
|
+
job_id: input.job.jobId,
|
|
91
|
+
...(input.job.name ? {job_name: input.job.name} : {}),
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
if (input.returnContent === true) {
|
|
95
|
+
const content = input.job.fetchContent ? input.job.fetchContent() : Promise.resolve('');
|
|
96
|
+
return content.then((logsContent) => {
|
|
97
|
+
const processed = processGithubJobLogContent(logsContent, {
|
|
98
|
+
tailLines: input.tailLines,
|
|
99
|
+
contentWindowSize: input.contentWindowSize,
|
|
100
|
+
});
|
|
101
|
+
return {
|
|
102
|
+
...result,
|
|
103
|
+
logs_content: processed.content,
|
|
104
|
+
message: 'Job logs content retrieved successfully',
|
|
105
|
+
original_length: processed.totalLines,
|
|
106
|
+
};
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return Promise.resolve({
|
|
111
|
+
...result,
|
|
112
|
+
logs_url: input.job.logsUrl,
|
|
113
|
+
message: 'Job logs are available for download',
|
|
114
|
+
note: 'The logs_url provides a download link for the individual job logs in plain text format.\nUse return_content=true to get the actual log content.',
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function processGithubJobLogContent(
|
|
119
|
+
content: string,
|
|
120
|
+
options: {tailLines?: number | undefined; contentWindowSize?: number | undefined} = {},
|
|
121
|
+
): {content: string; totalLines: number} {
|
|
122
|
+
const tailLines = normalizeTailLines(options.tailLines);
|
|
123
|
+
const maxLines = Math.min(
|
|
124
|
+
tailLines,
|
|
125
|
+
options.contentWindowSize ?? DEFAULT_JOB_LOG_CONTENT_WINDOW_LINES,
|
|
126
|
+
);
|
|
127
|
+
const processed = processResponseAsRingBufferToEnd(content, maxLines);
|
|
128
|
+
const lines = processed.content.split('\n');
|
|
129
|
+
const finalLines = lines.length > tailLines ? lines.slice(-tailLines) : lines;
|
|
130
|
+
|
|
131
|
+
return {content: finalLines.join('\n'), totalLines: processed.totalLines};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function normalizeTailLines(tailLines: number | undefined): number {
|
|
135
|
+
if (tailLines === undefined) return DEFAULT_JOB_LOG_TAIL_LINES;
|
|
136
|
+
if (!Number.isFinite(tailLines) || tailLines <= 0) return DEFAULT_JOB_LOG_TAIL_LINES;
|
|
137
|
+
return Math.trunc(tailLines);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function processResponseAsRingBufferToEnd(
|
|
141
|
+
content: string,
|
|
142
|
+
maxJobLogLines: number,
|
|
143
|
+
): {content: string; totalLines: number} {
|
|
144
|
+
let maxLines = Math.trunc(maxJobLogLines);
|
|
145
|
+
if (maxLines <= 0) maxLines = DEFAULT_JOB_LOG_TAIL_LINES;
|
|
146
|
+
if (maxLines > MAX_JOB_LOG_RING_BUFFER_LINES) maxLines = MAX_JOB_LOG_RING_BUFFER_LINES;
|
|
147
|
+
|
|
148
|
+
const lines = new Array<string>(maxLines);
|
|
149
|
+
const validLines = new Array<boolean>(maxLines).fill(false);
|
|
150
|
+
let totalLines = 0;
|
|
151
|
+
let writeIndex = 0;
|
|
152
|
+
|
|
153
|
+
const storeLine = (line: string) => {
|
|
154
|
+
lines[writeIndex] = truncateLongLine(line);
|
|
155
|
+
validLines[writeIndex] = true;
|
|
156
|
+
totalLines += 1;
|
|
157
|
+
writeIndex = (writeIndex + 1) % maxLines;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
for (const line of splitLogLines(content)) {
|
|
161
|
+
storeLine(line);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const result: string[] = [];
|
|
165
|
+
const linesInBuffer = Math.min(totalLines, maxLines);
|
|
166
|
+
const startIndex = totalLines > maxLines ? writeIndex : 0;
|
|
167
|
+
|
|
168
|
+
for (let offset = 0; offset < linesInBuffer; offset += 1) {
|
|
169
|
+
const index = (startIndex + offset) % maxLines;
|
|
170
|
+
if (validLines[index]) result.push(lines[index] ?? '');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return {content: result.join('\n'), totalLines};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function splitLogLines(content: string): string[] {
|
|
177
|
+
if (content.length === 0) return [];
|
|
178
|
+
const lines = content.split('\n');
|
|
179
|
+
if (content.endsWith('\n')) lines.pop();
|
|
180
|
+
return lines;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function truncateLongLine(line: string): string {
|
|
184
|
+
if (Buffer.byteLength(line, 'utf8') < MAX_JOB_LOG_LINE_BYTES) return line;
|
|
185
|
+
return `${Array.from(line).slice(0, MAX_TRUNCATED_JOB_LOG_LINE_CHARS).join('')}...\n[TRUNCATED]`;
|
|
186
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import {githubAgentToolSelectionCatalog} from './agent-tools.js';
|
|
2
|
+
|
|
3
|
+
describe('githubAgentToolSelectionCatalog', () => {
|
|
4
|
+
const selectors = new Map(
|
|
5
|
+
githubAgentToolSelectionCatalog.selectors.map((selector) => [selector.token, selector]),
|
|
6
|
+
);
|
|
7
|
+
|
|
8
|
+
it('expands family tools into bare, wildcard, and method selectors', () => {
|
|
9
|
+
expect(selectors.get('issue_read')).toMatchObject({
|
|
10
|
+
kind: 'family',
|
|
11
|
+
sensitivity: 'read',
|
|
12
|
+
sensitive: false,
|
|
13
|
+
});
|
|
14
|
+
expect(selectors.get('issue_read.*')).toMatchObject({
|
|
15
|
+
kind: 'family_wildcard',
|
|
16
|
+
sensitivity: 'read',
|
|
17
|
+
sensitive: false,
|
|
18
|
+
});
|
|
19
|
+
expect(selectors.get('issue_read.get')).toMatchObject({
|
|
20
|
+
kind: 'method',
|
|
21
|
+
sensitivity: 'read',
|
|
22
|
+
sensitive: false,
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('uses method sensitivity for method selectors', () => {
|
|
27
|
+
expect(selectors.get('actions_run_trigger.run_workflow')).toMatchObject({
|
|
28
|
+
kind: 'method',
|
|
29
|
+
sensitivity: 'write',
|
|
30
|
+
sensitive: true,
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('keeps standalone tools as standalone selectors', () => {
|
|
35
|
+
expect(selectors.get('merge_pull_request')).toMatchObject({
|
|
36
|
+
kind: 'standalone',
|
|
37
|
+
sensitivity: 'write',
|
|
38
|
+
sensitive: true,
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type {AgentToolSelectionCatalog, AgentToolSelector} from '@shipfox/api-integration-core-dto';
|
|
2
|
+
import type {GithubAgentToolCatalogEntry} from './agent-tools.js';
|
|
3
|
+
|
|
4
|
+
export function buildGithubAgentToolSelectionCatalog(
|
|
5
|
+
catalog: readonly GithubAgentToolCatalogEntry[],
|
|
6
|
+
): AgentToolSelectionCatalog {
|
|
7
|
+
return {
|
|
8
|
+
selectors: catalog.flatMap((entry): AgentToolSelector[] => {
|
|
9
|
+
if (!entry.methods) {
|
|
10
|
+
return [
|
|
11
|
+
{
|
|
12
|
+
token: entry.id,
|
|
13
|
+
kind: 'standalone',
|
|
14
|
+
sensitivity: entry.sensitivity,
|
|
15
|
+
sensitive: entry.sensitive,
|
|
16
|
+
},
|
|
17
|
+
];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return [
|
|
21
|
+
{
|
|
22
|
+
token: entry.id,
|
|
23
|
+
kind: 'family',
|
|
24
|
+
sensitivity: entry.sensitivity,
|
|
25
|
+
sensitive: entry.sensitive,
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
token: `${entry.id}.*`,
|
|
29
|
+
kind: 'family_wildcard',
|
|
30
|
+
sensitivity: entry.sensitivity,
|
|
31
|
+
sensitive: entry.sensitive,
|
|
32
|
+
},
|
|
33
|
+
...entry.methods.map((method) => ({
|
|
34
|
+
token: `${entry.id}.${method.id}`,
|
|
35
|
+
kind: 'method' as const,
|
|
36
|
+
sensitivity: method.sensitivity,
|
|
37
|
+
sensitive: method.sensitive,
|
|
38
|
+
})),
|
|
39
|
+
];
|
|
40
|
+
}),
|
|
41
|
+
};
|
|
42
|
+
}
|