github-issue-tower-defence-management 1.67.1 → 1.67.3
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/CHANGELOG.md +15 -0
- package/README.md +1 -1
- package/bin/adapter/entry-points/cli/index.js +2 -2
- package/bin/adapter/entry-points/cli/index.js.map +1 -1
- package/bin/adapter/entry-points/cli/projectConfig.js +24 -2
- package/bin/adapter/entry-points/cli/projectConfig.js.map +1 -1
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +16 -1
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
- package/bin/adapter/repositories/issue/GraphqlProjectItemRepository.js +38 -7
- package/bin/adapter/repositories/issue/GraphqlProjectItemRepository.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter/entry-points/cli/index.test.ts +117 -0
- package/src/adapter/entry-points/cli/index.ts +2 -2
- package/src/adapter/entry-points/cli/projectConfig.ts +31 -1
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.test.ts +94 -0
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +47 -1
- package/src/adapter/repositories/issue/GraphqlProjectItemRepository.test.ts +136 -7
- package/src/adapter/repositories/issue/GraphqlProjectItemRepository.ts +81 -5
- package/src/adapter/repositories/issue/RestIssueRepository.test.ts +195 -77
- package/types/adapter/entry-points/cli/projectConfig.d.ts +2 -1
- package/types/adapter/entry-points/cli/projectConfig.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
- package/types/adapter/repositories/issue/GraphqlProjectItemRepository.d.ts +2 -0
- package/types/adapter/repositories/issue/GraphqlProjectItemRepository.d.ts.map +1 -1
|
@@ -457,4 +457,98 @@ claudeCodeOauthTokenListJsonPath: /readme/tokens.json
|
|
|
457
457
|
);
|
|
458
458
|
});
|
|
459
459
|
});
|
|
460
|
+
|
|
461
|
+
describe('effective config logging', () => {
|
|
462
|
+
let consoleLogSpy: jest.SpyInstance;
|
|
463
|
+
|
|
464
|
+
beforeEach(() => {
|
|
465
|
+
consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
afterEach(() => {
|
|
469
|
+
consoleLogSpy.mockRestore();
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it('should log the effective values with configFile source when only the YAML config sets them', async () => {
|
|
473
|
+
const configWithPreparation = {
|
|
474
|
+
...validConfig,
|
|
475
|
+
startPreparation: {
|
|
476
|
+
defaultAgentName: 'yaml-agent',
|
|
477
|
+
defaultLlmModelName: 'yaml-model',
|
|
478
|
+
configFilePath: './config.yml',
|
|
479
|
+
maximumPreparingIssuesCount: 10,
|
|
480
|
+
},
|
|
481
|
+
};
|
|
482
|
+
mockFetchReturningReadme(null);
|
|
483
|
+
jest
|
|
484
|
+
.mocked(fs.readFileSync)
|
|
485
|
+
.mockReturnValue(YAML.stringify(configWithPreparation));
|
|
486
|
+
|
|
487
|
+
const handler = new HandleScheduledEventUseCaseHandler();
|
|
488
|
+
await handler.handle('config.yml', false);
|
|
489
|
+
|
|
490
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
491
|
+
'Effective maximumPreparingIssuesCount: 10 (source: configFile)',
|
|
492
|
+
);
|
|
493
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
494
|
+
'Effective defaultLlmModelName: yaml-model (source: configFile)',
|
|
495
|
+
);
|
|
496
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
497
|
+
'Effective defaultAgentName: yaml-agent (source: configFile)',
|
|
498
|
+
);
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
it('should log the effective values with readmeOverride source when the README config overrides them', async () => {
|
|
502
|
+
const readmeContent = `<details>
|
|
503
|
+
<summary>config</summary>
|
|
504
|
+
maximumPreparingIssuesCount: 3
|
|
505
|
+
defaultLlmModelName: readme-model
|
|
506
|
+
defaultAgentName: readme-agent
|
|
507
|
+
</details>`;
|
|
508
|
+
mockFetchReturningReadme(readmeContent);
|
|
509
|
+
const configWithPreparation = {
|
|
510
|
+
...validConfig,
|
|
511
|
+
startPreparation: {
|
|
512
|
+
defaultAgentName: 'yaml-agent',
|
|
513
|
+
defaultLlmModelName: 'yaml-model',
|
|
514
|
+
configFilePath: './config.yml',
|
|
515
|
+
maximumPreparingIssuesCount: 10,
|
|
516
|
+
},
|
|
517
|
+
};
|
|
518
|
+
jest
|
|
519
|
+
.mocked(fs.readFileSync)
|
|
520
|
+
.mockReturnValue(YAML.stringify(configWithPreparation));
|
|
521
|
+
|
|
522
|
+
const handler = new HandleScheduledEventUseCaseHandler();
|
|
523
|
+
await handler.handle('config.yml', false);
|
|
524
|
+
|
|
525
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
526
|
+
'Effective maximumPreparingIssuesCount: 3 (source: readmeOverride)',
|
|
527
|
+
);
|
|
528
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
529
|
+
'Effective defaultLlmModelName: readme-model (source: readmeOverride)',
|
|
530
|
+
);
|
|
531
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
532
|
+
'Effective defaultAgentName: readme-agent (source: readmeOverride)',
|
|
533
|
+
);
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
it('should log null with unset (default) source when neither README nor config provides the value', async () => {
|
|
537
|
+
mockFetchReturningReadme(null);
|
|
538
|
+
jest.mocked(fs.readFileSync).mockReturnValue(YAML.stringify(validConfig));
|
|
539
|
+
|
|
540
|
+
const handler = new HandleScheduledEventUseCaseHandler();
|
|
541
|
+
await handler.handle('config.yml', false);
|
|
542
|
+
|
|
543
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
544
|
+
'Effective maximumPreparingIssuesCount: null (source: unset (default))',
|
|
545
|
+
);
|
|
546
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
547
|
+
'Effective defaultLlmModelName: null (source: unset (default))',
|
|
548
|
+
);
|
|
549
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
550
|
+
'Effective defaultAgentName: null (source: unset (default))',
|
|
551
|
+
);
|
|
552
|
+
});
|
|
553
|
+
});
|
|
460
554
|
});
|
|
@@ -94,7 +94,9 @@ export class HandleScheduledEventUseCaseHandler {
|
|
|
94
94
|
|
|
95
95
|
const managerToken = input.credentials.manager.github.token;
|
|
96
96
|
const readme = await fetchProjectReadme(input.projectUrl, managerToken);
|
|
97
|
-
const readmeConfig = readme
|
|
97
|
+
const readmeConfig = readme
|
|
98
|
+
? parseProjectReadmeConfig(readme, input.projectUrl)
|
|
99
|
+
: {};
|
|
98
100
|
|
|
99
101
|
const mergedInput = {
|
|
100
102
|
...input,
|
|
@@ -137,6 +139,50 @@ export class HandleScheduledEventUseCaseHandler {
|
|
|
137
139
|
: input.startPreparation,
|
|
138
140
|
};
|
|
139
141
|
|
|
142
|
+
type EffectiveConfigValue = string | number | null | undefined;
|
|
143
|
+
|
|
144
|
+
const resolveConfigSource = (
|
|
145
|
+
readmeValue: EffectiveConfigValue,
|
|
146
|
+
configFileValue: EffectiveConfigValue,
|
|
147
|
+
): 'readmeOverride' | 'configFile' | 'unset (default)' => {
|
|
148
|
+
if (readmeValue !== undefined && readmeValue !== null) {
|
|
149
|
+
return 'readmeOverride';
|
|
150
|
+
}
|
|
151
|
+
if (configFileValue !== undefined && configFileValue !== null) {
|
|
152
|
+
return 'configFile';
|
|
153
|
+
}
|
|
154
|
+
return 'unset (default)';
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const formatEffectiveConfig = (
|
|
158
|
+
value: EffectiveConfigValue,
|
|
159
|
+
readmeValue: EffectiveConfigValue,
|
|
160
|
+
configFileValue: EffectiveConfigValue,
|
|
161
|
+
): string =>
|
|
162
|
+
`${value ?? 'null'} (source: ${resolveConfigSource(readmeValue, configFileValue)})`;
|
|
163
|
+
|
|
164
|
+
console.log(
|
|
165
|
+
`Effective maximumPreparingIssuesCount: ${formatEffectiveConfig(
|
|
166
|
+
mergedInput.startPreparation?.maximumPreparingIssuesCount,
|
|
167
|
+
readmeConfig.maximumPreparingIssuesCount,
|
|
168
|
+
input.startPreparation?.maximumPreparingIssuesCount,
|
|
169
|
+
)}`,
|
|
170
|
+
);
|
|
171
|
+
console.log(
|
|
172
|
+
`Effective defaultLlmModelName: ${formatEffectiveConfig(
|
|
173
|
+
mergedInput.startPreparation?.defaultLlmModelName,
|
|
174
|
+
readmeConfig.defaultLlmModelName,
|
|
175
|
+
input.startPreparation?.defaultLlmModelName,
|
|
176
|
+
)}`,
|
|
177
|
+
);
|
|
178
|
+
console.log(
|
|
179
|
+
`Effective defaultAgentName: ${formatEffectiveConfig(
|
|
180
|
+
mergedInput.startPreparation?.defaultAgentName,
|
|
181
|
+
readmeConfig.defaultAgentName,
|
|
182
|
+
input.startPreparation?.defaultAgentName,
|
|
183
|
+
)}`,
|
|
184
|
+
);
|
|
185
|
+
|
|
140
186
|
const systemDateRepository = new SystemDateRepository();
|
|
141
187
|
const localStorageRepository = new LocalStorageRepository();
|
|
142
188
|
const googleSpreadsheetRepository = new GoogleSpreadsheetRepository(
|
|
@@ -24,6 +24,38 @@ const mockJsonResponse = <T>(data: T) => ({
|
|
|
24
24
|
json: jest.fn().mockResolvedValue(data),
|
|
25
25
|
});
|
|
26
26
|
|
|
27
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
28
|
+
typeof value === 'object' && value !== null;
|
|
29
|
+
|
|
30
|
+
const extractRequestedFirstFromMockCall = (
|
|
31
|
+
call: unknown,
|
|
32
|
+
): number | undefined => {
|
|
33
|
+
if (!Array.isArray(call)) {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
const second: unknown = call[1];
|
|
37
|
+
if (!isRecord(second)) {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
const json: unknown = second.json;
|
|
41
|
+
if (!isRecord(json)) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
const variables: unknown = json.variables;
|
|
45
|
+
if (!isRecord(variables)) {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
const first: unknown = variables.first;
|
|
49
|
+
return typeof first === 'number' ? first : undefined;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const extractErrorMessage = (value: unknown): string => {
|
|
53
|
+
if (value instanceof Error) {
|
|
54
|
+
return value.message;
|
|
55
|
+
}
|
|
56
|
+
return '';
|
|
57
|
+
};
|
|
58
|
+
|
|
27
59
|
describe('GraphqlProjectItemRepository', () => {
|
|
28
60
|
const makePageResponse = (
|
|
29
61
|
hasNextPage: boolean,
|
|
@@ -76,7 +108,7 @@ describe('GraphqlProjectItemRepository', () => {
|
|
|
76
108
|
|
|
77
109
|
afterEach(() => {
|
|
78
110
|
jest.useRealTimers();
|
|
79
|
-
mockPost.
|
|
111
|
+
mockPost.mockReset();
|
|
80
112
|
});
|
|
81
113
|
|
|
82
114
|
it('should sleep between paginated requests to avoid 403', async () => {
|
|
@@ -111,7 +143,7 @@ describe('GraphqlProjectItemRepository', () => {
|
|
|
111
143
|
'dummy-token',
|
|
112
144
|
);
|
|
113
145
|
|
|
114
|
-
mockPost.
|
|
146
|
+
mockPost.mockImplementation(() =>
|
|
115
147
|
mockJsonResponse({
|
|
116
148
|
data: {
|
|
117
149
|
node: {
|
|
@@ -148,7 +180,7 @@ describe('GraphqlProjectItemRepository', () => {
|
|
|
148
180
|
|
|
149
181
|
await expect(
|
|
150
182
|
repository.fetchProjectItems('test-project-id'),
|
|
151
|
-
).rejects.toThrow('GitHub GraphQL errors: RATE_LIMITED');
|
|
183
|
+
).rejects.toThrow('GitHub GraphQL errors: [{"message":"RATE_LIMITED"}]');
|
|
152
184
|
});
|
|
153
185
|
|
|
154
186
|
it('should throw when data is null in response', async () => {
|
|
@@ -158,7 +190,7 @@ describe('GraphqlProjectItemRepository', () => {
|
|
|
158
190
|
'dummy-token',
|
|
159
191
|
);
|
|
160
192
|
|
|
161
|
-
mockPost.
|
|
193
|
+
mockPost.mockImplementation(() =>
|
|
162
194
|
mockJsonResponse({
|
|
163
195
|
data: null,
|
|
164
196
|
}),
|
|
@@ -176,7 +208,7 @@ describe('GraphqlProjectItemRepository', () => {
|
|
|
176
208
|
'dummy-token',
|
|
177
209
|
);
|
|
178
210
|
|
|
179
|
-
mockPost.
|
|
211
|
+
mockPost.mockImplementation(() =>
|
|
180
212
|
mockJsonResponse({
|
|
181
213
|
data: { node: null },
|
|
182
214
|
}),
|
|
@@ -187,7 +219,7 @@ describe('GraphqlProjectItemRepository', () => {
|
|
|
187
219
|
).rejects.toThrow('No data returned from GitHub API');
|
|
188
220
|
});
|
|
189
221
|
|
|
190
|
-
it('should throw when
|
|
222
|
+
it('should throw when a returned page contains nodes, declares hasNextPage=false, yet totalCount still indicates more items remain', async () => {
|
|
191
223
|
const localStorageRepository = new LocalStorageRepository();
|
|
192
224
|
const repository = new GraphqlProjectItemRepository(
|
|
193
225
|
localStorageRepository,
|
|
@@ -231,7 +263,7 @@ describe('GraphqlProjectItemRepository', () => {
|
|
|
231
263
|
await expect(
|
|
232
264
|
repository.fetchProjectItems('test-project-id'),
|
|
233
265
|
).rejects.toThrow(
|
|
234
|
-
'fetchProjectItems:
|
|
266
|
+
'fetchProjectItems: page 1 has 1 nodes with hasNextPage=false but only 1/5 items accumulated',
|
|
235
267
|
);
|
|
236
268
|
});
|
|
237
269
|
|
|
@@ -287,6 +319,103 @@ describe('GraphqlProjectItemRepository', () => {
|
|
|
287
319
|
);
|
|
288
320
|
setTimeoutSpy.mockRestore();
|
|
289
321
|
});
|
|
322
|
+
|
|
323
|
+
it('should stringify full response errors payload including extensions when callGraphql throws', async () => {
|
|
324
|
+
const localStorageRepository = new LocalStorageRepository();
|
|
325
|
+
const repository = new GraphqlProjectItemRepository(
|
|
326
|
+
localStorageRepository,
|
|
327
|
+
'dummy-token',
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
mockPost.mockImplementation(() =>
|
|
331
|
+
mockJsonResponse({
|
|
332
|
+
errors: [
|
|
333
|
+
{
|
|
334
|
+
message: 'Something went wrong while executing your query.',
|
|
335
|
+
path: ['node', 'items', 'nodes', 7, 'id'],
|
|
336
|
+
locations: [{ line: 11, column: 11 }],
|
|
337
|
+
extensions: { code: 'INTERNAL' },
|
|
338
|
+
},
|
|
339
|
+
],
|
|
340
|
+
}),
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
let capturedError: unknown = null;
|
|
344
|
+
await expect(
|
|
345
|
+
repository
|
|
346
|
+
.fetchProjectItems('test-project-id')
|
|
347
|
+
.catch((error: unknown) => {
|
|
348
|
+
capturedError = error;
|
|
349
|
+
throw error;
|
|
350
|
+
}),
|
|
351
|
+
).rejects.toThrow(/^GitHub GraphQL errors: /);
|
|
352
|
+
expect(capturedError).toBeInstanceOf(Error);
|
|
353
|
+
const message = extractErrorMessage(capturedError);
|
|
354
|
+
expect(message).toContain('"extensions":{"code":"INTERNAL"}');
|
|
355
|
+
expect(message).toContain('"path":["node","items","nodes",7,"id"]');
|
|
356
|
+
expect(message).toContain('"locations":[{"line":11,"column":11}]');
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it('should fall back to half the page size when first=50 fails and first=25 succeeds', async () => {
|
|
360
|
+
const localStorageRepository = new LocalStorageRepository();
|
|
361
|
+
const repository = new GraphqlProjectItemRepository(
|
|
362
|
+
localStorageRepository,
|
|
363
|
+
'dummy-token',
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
const failingResponse = mockJsonResponse({
|
|
367
|
+
errors: [
|
|
368
|
+
{ message: 'Something went wrong while executing your query.' },
|
|
369
|
+
],
|
|
370
|
+
});
|
|
371
|
+
const successPageResponse = makePageResponse(false, 'cursor-1', 1);
|
|
372
|
+
|
|
373
|
+
mockPost
|
|
374
|
+
.mockReturnValueOnce(failingResponse)
|
|
375
|
+
.mockReturnValueOnce(successPageResponse);
|
|
376
|
+
|
|
377
|
+
const result = await repository.fetchProjectItems('test-project-id');
|
|
378
|
+
|
|
379
|
+
expect(mockPost).toHaveBeenCalledTimes(2);
|
|
380
|
+
expect(result).toHaveLength(1);
|
|
381
|
+
expect(extractRequestedFirstFromMockCall(mockPost.mock.calls[0])).toBe(
|
|
382
|
+
50,
|
|
383
|
+
);
|
|
384
|
+
expect(extractRequestedFirstFromMockCall(mockPost.mock.calls[1])).toBe(
|
|
385
|
+
25,
|
|
386
|
+
);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it('should exhaust halving down to first=1 and rethrow the stringified errors payload', async () => {
|
|
390
|
+
const localStorageRepository = new LocalStorageRepository();
|
|
391
|
+
const repository = new GraphqlProjectItemRepository(
|
|
392
|
+
localStorageRepository,
|
|
393
|
+
'dummy-token',
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
mockPost.mockImplementation(() =>
|
|
397
|
+
mockJsonResponse({
|
|
398
|
+
errors: [
|
|
399
|
+
{
|
|
400
|
+
message: 'Something went wrong while executing your query.',
|
|
401
|
+
extensions: { code: 'INTERNAL' },
|
|
402
|
+
},
|
|
403
|
+
],
|
|
404
|
+
}),
|
|
405
|
+
);
|
|
406
|
+
|
|
407
|
+
await expect(
|
|
408
|
+
repository.fetchProjectItems('test-project-id'),
|
|
409
|
+
).rejects.toThrow(
|
|
410
|
+
/GitHub GraphQL errors: .*"extensions":\{"code":"INTERNAL"\}.*/,
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
expect(mockPost).toHaveBeenCalledTimes(6);
|
|
414
|
+
const requestedFirstSeries = mockPost.mock.calls.map((call) =>
|
|
415
|
+
extractRequestedFirstFromMockCall(call),
|
|
416
|
+
);
|
|
417
|
+
expect(requestedFirstSeries).toEqual([50, 25, 12, 6, 3, 1]);
|
|
418
|
+
});
|
|
290
419
|
});
|
|
291
420
|
|
|
292
421
|
describe('getProjectItemFields', () => {
|
|
@@ -20,6 +20,21 @@ export type ProjectItem = {
|
|
|
20
20
|
}[];
|
|
21
21
|
};
|
|
22
22
|
export const PAGINATION_DELAY_MS = 5000;
|
|
23
|
+
export const FETCH_PROJECT_ITEMS_INITIAL_PAGE_SIZE = 50;
|
|
24
|
+
export const FETCH_PROJECT_ITEMS_GRAPHQL_ERROR_PAYLOAD_MAX_LENGTH = 4000;
|
|
25
|
+
|
|
26
|
+
const stringifyGraphqlErrorsForLog = (
|
|
27
|
+
errors: { message: string }[],
|
|
28
|
+
): string => {
|
|
29
|
+
const serialized = JSON.stringify(errors);
|
|
30
|
+
if (
|
|
31
|
+
serialized.length <= FETCH_PROJECT_ITEMS_GRAPHQL_ERROR_PAYLOAD_MAX_LENGTH
|
|
32
|
+
) {
|
|
33
|
+
return serialized;
|
|
34
|
+
}
|
|
35
|
+
return `${serialized.slice(0, FETCH_PROJECT_ITEMS_GRAPHQL_ERROR_PAYLOAD_MAX_LENGTH)}...[truncated]`;
|
|
36
|
+
};
|
|
37
|
+
|
|
23
38
|
export class GraphqlProjectItemRepository extends BaseGitHubRepository {
|
|
24
39
|
fetchItemId = async (
|
|
25
40
|
projectId: string,
|
|
@@ -97,10 +112,10 @@ export class GraphqlProjectItemRepository extends BaseGitHubRepository {
|
|
|
97
112
|
};
|
|
98
113
|
fetchProjectItems = async (projectId: string): Promise<ProjectItem[]> => {
|
|
99
114
|
const graphqlQueryString = `
|
|
100
|
-
query GetProjectItems($projectId: ID!, $after: String) {
|
|
115
|
+
query GetProjectItems($projectId: ID!, $after: String, $first: Int!) {
|
|
101
116
|
node(id: $projectId) {
|
|
102
117
|
... on ProjectV2 {
|
|
103
|
-
items(first:
|
|
118
|
+
items(first: $first, after: $after) {
|
|
104
119
|
totalCount
|
|
105
120
|
pageInfo {
|
|
106
121
|
endCursor
|
|
@@ -210,6 +225,7 @@ query GetProjectItems($projectId: ID!, $after: String) {
|
|
|
210
225
|
const callGraphql = async (
|
|
211
226
|
projectId: string,
|
|
212
227
|
after: string | null,
|
|
228
|
+
first: number,
|
|
213
229
|
): Promise<{
|
|
214
230
|
node: {
|
|
215
231
|
items: {
|
|
@@ -251,6 +267,7 @@ query GetProjectItems($projectId: ID!, $after: String) {
|
|
|
251
267
|
variables: {
|
|
252
268
|
projectId: projectId,
|
|
253
269
|
after: after,
|
|
270
|
+
first: first,
|
|
254
271
|
},
|
|
255
272
|
};
|
|
256
273
|
const response = await ky
|
|
@@ -301,7 +318,7 @@ query GetProjectItems($projectId: ID!, $after: String) {
|
|
|
301
318
|
}>();
|
|
302
319
|
if (response.errors && response.errors.length > 0) {
|
|
303
320
|
throw new Error(
|
|
304
|
-
`GitHub GraphQL errors: ${response.errors
|
|
321
|
+
`GitHub GraphQL errors: ${stringifyGraphqlErrorsForLog(response.errors)}`,
|
|
305
322
|
);
|
|
306
323
|
}
|
|
307
324
|
const rawData = response.data;
|
|
@@ -314,6 +331,65 @@ query GetProjectItems($projectId: ID!, $after: String) {
|
|
|
314
331
|
}
|
|
315
332
|
return { node: rawNode };
|
|
316
333
|
};
|
|
334
|
+
const callGraphqlWithHalvingFallback = async (
|
|
335
|
+
after: string | null,
|
|
336
|
+
): Promise<{
|
|
337
|
+
node: {
|
|
338
|
+
items: {
|
|
339
|
+
totalCount: number;
|
|
340
|
+
pageInfo: {
|
|
341
|
+
endCursor: string;
|
|
342
|
+
startCursor: string;
|
|
343
|
+
hasNextPage: boolean;
|
|
344
|
+
};
|
|
345
|
+
nodes: {
|
|
346
|
+
id: string;
|
|
347
|
+
fieldValues: {
|
|
348
|
+
nodes: {
|
|
349
|
+
text: string;
|
|
350
|
+
number: number;
|
|
351
|
+
date: string;
|
|
352
|
+
field: {
|
|
353
|
+
name: string;
|
|
354
|
+
};
|
|
355
|
+
}[];
|
|
356
|
+
};
|
|
357
|
+
content: {
|
|
358
|
+
repository: { nameWithOwner: string };
|
|
359
|
+
number: number;
|
|
360
|
+
title: string;
|
|
361
|
+
state: string;
|
|
362
|
+
url: string;
|
|
363
|
+
createdAt: string;
|
|
364
|
+
author: { login: string } | null;
|
|
365
|
+
labels: { nodes: { name: string }[] };
|
|
366
|
+
assignees: { nodes: { login: string }[] };
|
|
367
|
+
};
|
|
368
|
+
}[];
|
|
369
|
+
};
|
|
370
|
+
};
|
|
371
|
+
}> => {
|
|
372
|
+
let attemptFirst = FETCH_PROJECT_ITEMS_INITIAL_PAGE_SIZE;
|
|
373
|
+
let lastError: unknown = null;
|
|
374
|
+
while (attemptFirst >= 1) {
|
|
375
|
+
try {
|
|
376
|
+
return await callGraphql(projectId, after, attemptFirst);
|
|
377
|
+
} catch (error) {
|
|
378
|
+
lastError = error;
|
|
379
|
+
if (attemptFirst === 1) {
|
|
380
|
+
throw error;
|
|
381
|
+
}
|
|
382
|
+
const nextFirst = Math.max(1, Math.floor(attemptFirst / 2));
|
|
383
|
+
console.log(
|
|
384
|
+
`fetchProjectItems: page request with first=${attemptFirst} failed, halving to first=${nextFirst}: ${error instanceof Error ? error.message : String(error)}`,
|
|
385
|
+
);
|
|
386
|
+
attemptFirst = nextFirst;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
throw lastError instanceof Error
|
|
390
|
+
? lastError
|
|
391
|
+
: new Error(String(lastError));
|
|
392
|
+
};
|
|
317
393
|
const issues: ProjectItem[] = [];
|
|
318
394
|
let after: string | null = null;
|
|
319
395
|
let hasNextPage = true;
|
|
@@ -327,7 +403,7 @@ query GetProjectItems($projectId: ID!, $after: String) {
|
|
|
327
403
|
setTimeout(resolve, PAGINATION_DELAY_MS),
|
|
328
404
|
);
|
|
329
405
|
}
|
|
330
|
-
const data = await
|
|
406
|
+
const data = await callGraphqlWithHalvingFallback(after);
|
|
331
407
|
const pageNodes = data.node.items.nodes;
|
|
332
408
|
const pageInfo = data.node.items.pageInfo;
|
|
333
409
|
totalCount = data.node.items.totalCount;
|
|
@@ -393,7 +469,7 @@ query GetProjectItems($projectId: ID!, $after: String) {
|
|
|
393
469
|
});
|
|
394
470
|
});
|
|
395
471
|
if (
|
|
396
|
-
pageNodes.length
|
|
472
|
+
pageNodes.length > 0 &&
|
|
397
473
|
!pageInfo.hasNextPage &&
|
|
398
474
|
cumulativeRawNodes < totalCount
|
|
399
475
|
) {
|