github-issue-tower-defence-management 1.153.5 → 1.153.6
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 +7 -0
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +161 -0
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
- package/bin/domain/usecases/IssueRejectionEvaluator.js +28 -11
- package/bin/domain/usecases/IssueRejectionEvaluator.js.map +1 -1
- package/bin/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.js +15 -3
- package/bin/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter/entry-points/console/ui/e2e/consoleTestHarness.ts +11 -0
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.test.ts +194 -0
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +252 -0
- package/src/domain/usecases/IssueRejectionEvaluator.ts +49 -15
- package/src/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.test.ts +154 -0
- package/src/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.ts +33 -5
- package/src/domain/usecases/adapter-interfaces/IssueRepository.ts +3 -0
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +3 -0
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
- package/types/domain/usecases/IssueRejectionEvaluator.d.ts +6 -1
- package/types/domain/usecases/IssueRejectionEvaluator.d.ts.map +1 -1
- package/types/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.d.ts +1 -1
- package/types/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts +1 -0
- package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RevertNotReadyReviewQueueIssueUseCase.js","sourceRoot":"","sources":["../../../src/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.ts"],"names":[],"mappings":";;;AAIA,uEAAoE;AACpE,uFAAoF;AACpF,iGAA8F;AAC9F,yFAAsF;AACtF,iGAA8F;AAC9F,+DAIoC;AAEpC,qEAAqE;AACrE,8EAA8E;AAC9E,0EAA0E;AAC1E,6EAA6E;AAC7E,kDAAkD;AAClD,MAAM,0BAA0B,GAAG,CAAC,KAAc,EAAW,EAAE;IAC7D,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACvE,OAAO,OAAO,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;AACpD,CAAC,CAAC;AAEF,MAAM,cAAc,GAAG,CAAC,KAAc,EAAW,EAAE,CACjD,KAAK,YAAY,KAAK,IAAI,KAAK,CAAC,IAAI,KAAK,cAAc,CAAC;AAE1D,MAAa,qCAAqC;IAIhD,YACmB,iBAGhB,EACgB,
|
|
1
|
+
{"version":3,"file":"RevertNotReadyReviewQueueIssueUseCase.js","sourceRoot":"","sources":["../../../src/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -426,6 +426,17 @@ const createStubIssueRepository = (
|
|
|
426
426
|
url === CONSOLE_E2E_AWAITING_QUALITY_CHECK_PR_URL
|
|
427
427
|
? awaitingQualityCheckPullRequest
|
|
428
428
|
: null,
|
|
429
|
+
getOpenPullRequests: async (
|
|
430
|
+
urls: string[],
|
|
431
|
+
): Promise<Map<string, RelatedPullRequest | null>> =>
|
|
432
|
+
new Map(
|
|
433
|
+
urls.map((url) => [
|
|
434
|
+
url,
|
|
435
|
+
url === CONSOLE_E2E_AWAITING_QUALITY_CHECK_PR_URL
|
|
436
|
+
? awaitingQualityCheckPullRequest
|
|
437
|
+
: null,
|
|
438
|
+
]),
|
|
439
|
+
),
|
|
429
440
|
getPullRequestChangedFilePaths: async (): Promise<string[]> => [],
|
|
430
441
|
approvePullRequest: async (): Promise<void> => undefined,
|
|
431
442
|
requestChangesWithInlineComment: async (
|
|
@@ -2339,6 +2339,9 @@ describe('ApiV3CheerioRestIssueRepository', () => {
|
|
|
2339
2339
|
prNumber: number;
|
|
2340
2340
|
reviewThreadsAfter: string | null;
|
|
2341
2341
|
}) => Response | object;
|
|
2342
|
+
slimPullRequestBatch?: (
|
|
2343
|
+
variables: Record<string, unknown>,
|
|
2344
|
+
) => Response | object;
|
|
2342
2345
|
branchRules?: (url: string) => Response | object;
|
|
2343
2346
|
branchDetail?: (url: string) => Response | object;
|
|
2344
2347
|
checkRuns?: (url: string) => Response | object;
|
|
@@ -2363,6 +2366,7 @@ describe('ApiV3CheerioRestIssueRepository', () => {
|
|
|
2363
2366
|
prNumber: number;
|
|
2364
2367
|
reviewThreadsAfter: string | null;
|
|
2365
2368
|
};
|
|
2369
|
+
rawVariables: Record<string, unknown>;
|
|
2366
2370
|
};
|
|
2367
2371
|
|
|
2368
2372
|
const parseGraphqlRequestBody = (init?: RequestInit): GraphqlRequestBody => {
|
|
@@ -2401,6 +2405,7 @@ describe('ApiV3CheerioRestIssueRepository', () => {
|
|
|
2401
2405
|
return {
|
|
2402
2406
|
query: parsed.query,
|
|
2403
2407
|
variables: { owner, repo, prNumber, reviewThreadsAfter },
|
|
2408
|
+
rawVariables: Object.fromEntries(Object.entries(variables)),
|
|
2404
2409
|
};
|
|
2405
2410
|
};
|
|
2406
2411
|
|
|
@@ -2421,6 +2426,12 @@ describe('ApiV3CheerioRestIssueRepository', () => {
|
|
|
2421
2426
|
) {
|
|
2422
2427
|
return toResponse(routes.mergeability());
|
|
2423
2428
|
}
|
|
2429
|
+
if (
|
|
2430
|
+
body.query.includes('PullRequestSlimStatusBatch') &&
|
|
2431
|
+
routes.slimPullRequestBatch
|
|
2432
|
+
) {
|
|
2433
|
+
return toResponse(routes.slimPullRequestBatch(body.rawVariables));
|
|
2434
|
+
}
|
|
2424
2435
|
if (body.query.includes('headRefOid') && routes.slimPullRequest) {
|
|
2425
2436
|
return toResponse(routes.slimPullRequest(body.variables));
|
|
2426
2437
|
}
|
|
@@ -2472,6 +2483,14 @@ describe('ApiV3CheerioRestIssueRepository', () => {
|
|
|
2472
2483
|
body.includes('mergeStateStatus'),
|
|
2473
2484
|
);
|
|
2474
2485
|
|
|
2486
|
+
const countSlimPullRequestBatchQueries = (fetchSpy: FetchSpy): number =>
|
|
2487
|
+
countCallsMatching(
|
|
2488
|
+
fetchSpy,
|
|
2489
|
+
(url, body) =>
|
|
2490
|
+
url === 'https://api.github.com/graphql' &&
|
|
2491
|
+
body.includes('PullRequestSlimStatusBatch'),
|
|
2492
|
+
);
|
|
2493
|
+
|
|
2475
2494
|
const buildSlimPullRequestResponse = (
|
|
2476
2495
|
overrides: {
|
|
2477
2496
|
url?: string;
|
|
@@ -2508,6 +2527,181 @@ describe('ApiV3CheerioRestIssueRepository', () => {
|
|
|
2508
2527
|
},
|
|
2509
2528
|
});
|
|
2510
2529
|
|
|
2530
|
+
const buildSlimPullRequestNode = (
|
|
2531
|
+
overrides: Parameters<typeof buildSlimPullRequestResponse>[0] = {},
|
|
2532
|
+
) => buildSlimPullRequestResponse(overrides).data.repository.pullRequest;
|
|
2533
|
+
|
|
2534
|
+
describe('getOpenPullRequests batched resolution', () => {
|
|
2535
|
+
afterEach(() => {
|
|
2536
|
+
jest.restoreAllMocks();
|
|
2537
|
+
});
|
|
2538
|
+
|
|
2539
|
+
const prUrlOf = (prNumber: number): string =>
|
|
2540
|
+
`https://github.com/HiromiShikata/test-repository/pull/${prNumber}`;
|
|
2541
|
+
|
|
2542
|
+
it('resolves every pull request state in one GraphQL request', async () => {
|
|
2543
|
+
const prNumbers = [31, 32, 33];
|
|
2544
|
+
const capturedVariables: Record<string, unknown>[] = [];
|
|
2545
|
+
const fetchSpy = mockFetchRoutes({
|
|
2546
|
+
slimPullRequestBatch: (variables) => {
|
|
2547
|
+
capturedVariables.push(variables);
|
|
2548
|
+
return {
|
|
2549
|
+
data: Object.fromEntries(
|
|
2550
|
+
prNumbers.map((prNumber, index) => [
|
|
2551
|
+
`pullRequest${index}`,
|
|
2552
|
+
{
|
|
2553
|
+
pullRequest: buildSlimPullRequestNode({
|
|
2554
|
+
url: prUrlOf(prNumber),
|
|
2555
|
+
headRefName: `feature-${prNumber}`,
|
|
2556
|
+
}),
|
|
2557
|
+
},
|
|
2558
|
+
]),
|
|
2559
|
+
),
|
|
2560
|
+
};
|
|
2561
|
+
},
|
|
2562
|
+
checkRuns: () => ({
|
|
2563
|
+
total_count: 1,
|
|
2564
|
+
check_runs: [{ id: 1, name: 'ci', conclusion: 'success' }],
|
|
2565
|
+
}),
|
|
2566
|
+
});
|
|
2567
|
+
|
|
2568
|
+
const { repository } = createApiV3CheerioRestIssueRepository();
|
|
2569
|
+
const resolved = await repository.getOpenPullRequests(
|
|
2570
|
+
prNumbers.map(prUrlOf),
|
|
2571
|
+
);
|
|
2572
|
+
|
|
2573
|
+
expect(countSlimPullRequestBatchQueries(fetchSpy)).toBe(1);
|
|
2574
|
+
expect(capturedVariables).toHaveLength(1);
|
|
2575
|
+
expect(capturedVariables[0].prNumber0).toBe(31);
|
|
2576
|
+
expect(capturedVariables[0].prNumber2).toBe(33);
|
|
2577
|
+
expect(Array.from(resolved.keys())).toEqual(prNumbers.map(prUrlOf));
|
|
2578
|
+
for (const prNumber of prNumbers) {
|
|
2579
|
+
expect(resolved.get(prUrlOf(prNumber))?.branchName).toBe(
|
|
2580
|
+
`feature-${prNumber}`,
|
|
2581
|
+
);
|
|
2582
|
+
}
|
|
2583
|
+
});
|
|
2584
|
+
|
|
2585
|
+
it('resolves a pull request the batch reports as not found to an absent pull request', async () => {
|
|
2586
|
+
mockFetchRoutes({
|
|
2587
|
+
slimPullRequestBatch: () => ({
|
|
2588
|
+
data: { pullRequest0: null },
|
|
2589
|
+
errors: [
|
|
2590
|
+
{
|
|
2591
|
+
message: 'Could not resolve to a PullRequest',
|
|
2592
|
+
type: 'NOT_FOUND',
|
|
2593
|
+
path: ['pullRequest0', 'pullRequest'],
|
|
2594
|
+
},
|
|
2595
|
+
],
|
|
2596
|
+
}),
|
|
2597
|
+
});
|
|
2598
|
+
|
|
2599
|
+
const { repository } = createApiV3CheerioRestIssueRepository();
|
|
2600
|
+
const resolved = await repository.getOpenPullRequests([prUrlOf(31)]);
|
|
2601
|
+
|
|
2602
|
+
expect(resolved.has(prUrlOf(31))).toBe(true);
|
|
2603
|
+
expect(resolved.get(prUrlOf(31))).toBeNull();
|
|
2604
|
+
});
|
|
2605
|
+
|
|
2606
|
+
it('omits a pull request whose alias failed for a reason other than not found so an unknown state is not read as absent', async () => {
|
|
2607
|
+
mockFetchRoutes({
|
|
2608
|
+
slimPullRequestBatch: () => ({
|
|
2609
|
+
data: { pullRequest0: null },
|
|
2610
|
+
errors: [
|
|
2611
|
+
{
|
|
2612
|
+
message: 'Something went wrong while executing your query',
|
|
2613
|
+
type: 'SERVICE_UNAVAILABLE',
|
|
2614
|
+
path: ['pullRequest0', 'pullRequest'],
|
|
2615
|
+
},
|
|
2616
|
+
],
|
|
2617
|
+
}),
|
|
2618
|
+
});
|
|
2619
|
+
|
|
2620
|
+
const { repository } = createApiV3CheerioRestIssueRepository();
|
|
2621
|
+
const resolved = await repository.getOpenPullRequests([prUrlOf(31)]);
|
|
2622
|
+
|
|
2623
|
+
expect(resolved.has(prUrlOf(31))).toBe(false);
|
|
2624
|
+
});
|
|
2625
|
+
|
|
2626
|
+
it('omits a pull request whose review threads do not fit one page so it is resolved by the paginating path', async () => {
|
|
2627
|
+
mockFetchRoutes({
|
|
2628
|
+
slimPullRequestBatch: () => ({
|
|
2629
|
+
data: {
|
|
2630
|
+
pullRequest0: {
|
|
2631
|
+
pullRequest: buildSlimPullRequestNode({
|
|
2632
|
+
reviewThreads: {
|
|
2633
|
+
pageInfo: { endCursor: 'cursor-1', hasNextPage: true },
|
|
2634
|
+
nodes: [{ isResolved: true }],
|
|
2635
|
+
},
|
|
2636
|
+
}),
|
|
2637
|
+
},
|
|
2638
|
+
},
|
|
2639
|
+
}),
|
|
2640
|
+
});
|
|
2641
|
+
|
|
2642
|
+
const { repository } = createApiV3CheerioRestIssueRepository();
|
|
2643
|
+
const resolved = await repository.getOpenPullRequests([prUrlOf(31)]);
|
|
2644
|
+
|
|
2645
|
+
expect(resolved.has(prUrlOf(31))).toBe(false);
|
|
2646
|
+
});
|
|
2647
|
+
|
|
2648
|
+
it('leaves every url of a failed batch unresolved so the caller falls back to the single query', async () => {
|
|
2649
|
+
mockFetchRoutes({
|
|
2650
|
+
slimPullRequestBatch: () => ({
|
|
2651
|
+
data: null,
|
|
2652
|
+
errors: [
|
|
2653
|
+
{ message: 'Something went wrong while executing your query' },
|
|
2654
|
+
],
|
|
2655
|
+
}),
|
|
2656
|
+
});
|
|
2657
|
+
|
|
2658
|
+
const { repository } = createApiV3CheerioRestIssueRepository();
|
|
2659
|
+
const resolved = await repository.getOpenPullRequests([
|
|
2660
|
+
prUrlOf(31),
|
|
2661
|
+
prUrlOf(32),
|
|
2662
|
+
]);
|
|
2663
|
+
|
|
2664
|
+
expect(resolved.size).toBe(0);
|
|
2665
|
+
});
|
|
2666
|
+
|
|
2667
|
+
it('resolves an issue url to an absent pull request without issuing any request', async () => {
|
|
2668
|
+
const fetchSpy = mockFetchRoutes({});
|
|
2669
|
+
const issueUrl =
|
|
2670
|
+
'https://github.com/HiromiShikata/test-repository/issues/31';
|
|
2671
|
+
|
|
2672
|
+
const { repository } = createApiV3CheerioRestIssueRepository();
|
|
2673
|
+
const resolved = await repository.getOpenPullRequests([issueUrl]);
|
|
2674
|
+
|
|
2675
|
+
expect(resolved.get(issueUrl)).toBeNull();
|
|
2676
|
+
expect(fetchSpy).not.toHaveBeenCalled();
|
|
2677
|
+
});
|
|
2678
|
+
|
|
2679
|
+
it('splits more pull requests than one batch holds into separate GraphQL requests', async () => {
|
|
2680
|
+
const prNumbers = Array.from({ length: 101 }, (_, index) => 1000 + index);
|
|
2681
|
+
const fetchSpy = mockFetchRoutes({
|
|
2682
|
+
slimPullRequestBatch: (variables) => ({
|
|
2683
|
+
data: Object.fromEntries(
|
|
2684
|
+
Object.keys(variables)
|
|
2685
|
+
.filter((name) => name.startsWith('prNumber'))
|
|
2686
|
+
.map((name) => [
|
|
2687
|
+
`pullRequest${name.slice('prNumber'.length)}`,
|
|
2688
|
+
{ pullRequest: buildSlimPullRequestNode() },
|
|
2689
|
+
]),
|
|
2690
|
+
),
|
|
2691
|
+
}),
|
|
2692
|
+
checkRuns: () => ({ total_count: 0, check_runs: [] }),
|
|
2693
|
+
});
|
|
2694
|
+
|
|
2695
|
+
const { repository } = createApiV3CheerioRestIssueRepository();
|
|
2696
|
+
const resolved = await repository.getOpenPullRequests(
|
|
2697
|
+
prNumbers.map(prUrlOf),
|
|
2698
|
+
);
|
|
2699
|
+
|
|
2700
|
+
expect(countSlimPullRequestBatchQueries(fetchSpy)).toBe(2);
|
|
2701
|
+
expect(resolved.size).toBe(prNumbers.length);
|
|
2702
|
+
});
|
|
2703
|
+
});
|
|
2704
|
+
|
|
2511
2705
|
describe('getOpenPullRequest CI state computation', () => {
|
|
2512
2706
|
afterEach(() => {
|
|
2513
2707
|
jest.restoreAllMocks();
|
|
@@ -108,6 +108,43 @@ type PrStatusComputationData = {
|
|
|
108
108
|
}>;
|
|
109
109
|
};
|
|
110
110
|
|
|
111
|
+
// One GraphQL query carrying this many aliased pull requests costs the same
|
|
112
|
+
// single rate-limit point as a query carrying one, measured against the live
|
|
113
|
+
// API by reading the rateLimit cost field it returns.
|
|
114
|
+
const SLIM_PULL_REQUEST_BATCH_SIZE = 100;
|
|
115
|
+
const SLIM_PULL_REQUEST_REVIEW_THREADS_PAGE_SIZE = 100;
|
|
116
|
+
|
|
117
|
+
type SlimPullRequestNodeResponse = {
|
|
118
|
+
url: string;
|
|
119
|
+
state: string;
|
|
120
|
+
isDraft?: boolean;
|
|
121
|
+
headRefName?: string;
|
|
122
|
+
baseRefName?: string;
|
|
123
|
+
mergeable?: string;
|
|
124
|
+
headRefOid?: string;
|
|
125
|
+
reviewThreads?: {
|
|
126
|
+
pageInfo: {
|
|
127
|
+
endCursor: string | null;
|
|
128
|
+
hasNextPage: boolean;
|
|
129
|
+
};
|
|
130
|
+
nodes: Array<{
|
|
131
|
+
isResolved: boolean;
|
|
132
|
+
}>;
|
|
133
|
+
};
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
type SlimPullRequestBatchResponse = {
|
|
137
|
+
data?: Record<
|
|
138
|
+
string,
|
|
139
|
+
{ pullRequest?: SlimPullRequestNodeResponse | null } | null
|
|
140
|
+
> | null;
|
|
141
|
+
errors?: Array<{
|
|
142
|
+
message: string;
|
|
143
|
+
type?: string;
|
|
144
|
+
path?: Array<string | number>;
|
|
145
|
+
}>;
|
|
146
|
+
};
|
|
147
|
+
|
|
111
148
|
type SlimPullRequestResponse = {
|
|
112
149
|
data?: {
|
|
113
150
|
repository?: {
|
|
@@ -206,6 +243,13 @@ function isSlimPullRequestResponse(
|
|
|
206
243
|
return true;
|
|
207
244
|
}
|
|
208
245
|
|
|
246
|
+
function isSlimPullRequestBatchResponse(
|
|
247
|
+
value: unknown,
|
|
248
|
+
): value is SlimPullRequestBatchResponse {
|
|
249
|
+
if (typeof value !== 'object' || value === null) return false;
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
|
|
209
253
|
function isBranchRulesResponse(
|
|
210
254
|
value: unknown,
|
|
211
255
|
): value is BranchRulesResponseItem[] {
|
|
@@ -1691,6 +1735,214 @@ export class ApiV3CheerioRestIssueRepository
|
|
|
1691
1735
|
return this.buildRelatedPullRequestFromSlim(owner, repo, slimPullRequest);
|
|
1692
1736
|
};
|
|
1693
1737
|
|
|
1738
|
+
// Resolves many pull requests with one GraphQL query per hundred instead of
|
|
1739
|
+
// one query per pull request. A URL this cannot settle is left out of the
|
|
1740
|
+
// returned map rather than mapped to null, so the caller falls back to
|
|
1741
|
+
// getOpenPullRequest for it and an unknown state is never mistaken for an
|
|
1742
|
+
// absent pull request.
|
|
1743
|
+
getOpenPullRequests = async (
|
|
1744
|
+
prUrls: string[],
|
|
1745
|
+
): Promise<Map<string, RelatedPullRequest | null>> => {
|
|
1746
|
+
const resolved = new Map<string, RelatedPullRequest | null>();
|
|
1747
|
+
const parsedByUrl = new Map<
|
|
1748
|
+
string,
|
|
1749
|
+
{ owner: string; repo: string; prNumber: number }
|
|
1750
|
+
>();
|
|
1751
|
+
for (const prUrl of Array.from(new Set(prUrls))) {
|
|
1752
|
+
const parsedUrl = this.parseIssueUrl(prUrl);
|
|
1753
|
+
if (!parsedUrl.isPr) {
|
|
1754
|
+
resolved.set(prUrl, null);
|
|
1755
|
+
continue;
|
|
1756
|
+
}
|
|
1757
|
+
parsedByUrl.set(prUrl, {
|
|
1758
|
+
owner: parsedUrl.owner,
|
|
1759
|
+
repo: parsedUrl.repo,
|
|
1760
|
+
prNumber: parsedUrl.issueNumber,
|
|
1761
|
+
});
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
const urlsToFetch = Array.from(parsedByUrl.keys());
|
|
1765
|
+
for (
|
|
1766
|
+
let start = 0;
|
|
1767
|
+
start < urlsToFetch.length;
|
|
1768
|
+
start += SLIM_PULL_REQUEST_BATCH_SIZE
|
|
1769
|
+
) {
|
|
1770
|
+
const batchUrls = urlsToFetch.slice(
|
|
1771
|
+
start,
|
|
1772
|
+
start + SLIM_PULL_REQUEST_BATCH_SIZE,
|
|
1773
|
+
);
|
|
1774
|
+
let slimByUrl: Map<string, SlimPullRequest | null>;
|
|
1775
|
+
try {
|
|
1776
|
+
slimByUrl = await this.fetchSlimPullRequestsInOneQuery(
|
|
1777
|
+
batchUrls.map((prUrl) => ({
|
|
1778
|
+
prUrl,
|
|
1779
|
+
...this.requireParsedPullRequest(parsedByUrl, prUrl),
|
|
1780
|
+
})),
|
|
1781
|
+
);
|
|
1782
|
+
} catch (error) {
|
|
1783
|
+
console.warn(
|
|
1784
|
+
`ApiV3CheerioRestIssueRepository: batched pull request status query failed, leaving ${batchUrls.length} pull request(s) to per-request resolution. error: ${error instanceof Error ? error.message : String(error)}`,
|
|
1785
|
+
);
|
|
1786
|
+
continue;
|
|
1787
|
+
}
|
|
1788
|
+
for (const prUrl of batchUrls) {
|
|
1789
|
+
if (!slimByUrl.has(prUrl)) {
|
|
1790
|
+
continue;
|
|
1791
|
+
}
|
|
1792
|
+
const slimPullRequest = slimByUrl.get(prUrl) ?? null;
|
|
1793
|
+
if (!slimPullRequest || slimPullRequest.state !== 'OPEN') {
|
|
1794
|
+
resolved.set(prUrl, null);
|
|
1795
|
+
continue;
|
|
1796
|
+
}
|
|
1797
|
+
const parsed = this.requireParsedPullRequest(parsedByUrl, prUrl);
|
|
1798
|
+
try {
|
|
1799
|
+
resolved.set(
|
|
1800
|
+
prUrl,
|
|
1801
|
+
await this.buildRelatedPullRequestFromSlim(
|
|
1802
|
+
parsed.owner,
|
|
1803
|
+
parsed.repo,
|
|
1804
|
+
slimPullRequest,
|
|
1805
|
+
),
|
|
1806
|
+
);
|
|
1807
|
+
} catch (error) {
|
|
1808
|
+
console.warn(
|
|
1809
|
+
`ApiV3CheerioRestIssueRepository: building the pull request status failed, leaving it to per-request resolution. prUrl: ${prUrl} error: ${error instanceof Error ? error.message : String(error)}`,
|
|
1810
|
+
);
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
return resolved;
|
|
1815
|
+
};
|
|
1816
|
+
|
|
1817
|
+
private requireParsedPullRequest = (
|
|
1818
|
+
parsedByUrl: Map<string, { owner: string; repo: string; prNumber: number }>,
|
|
1819
|
+
prUrl: string,
|
|
1820
|
+
): { owner: string; repo: string; prNumber: number } => {
|
|
1821
|
+
const parsed = parsedByUrl.get(prUrl);
|
|
1822
|
+
if (!parsed) {
|
|
1823
|
+
throw new Error(`Pull request URL was not parsed: ${prUrl}`);
|
|
1824
|
+
}
|
|
1825
|
+
return parsed;
|
|
1826
|
+
};
|
|
1827
|
+
|
|
1828
|
+
// A pull request whose review threads do not fit the first page is omitted
|
|
1829
|
+
// from the returned map, so the caller resolves it through the paginating
|
|
1830
|
+
// single-pull-request path and the resolved-thread state stays complete.
|
|
1831
|
+
private fetchSlimPullRequestsInOneQuery = async (
|
|
1832
|
+
references: {
|
|
1833
|
+
prUrl: string;
|
|
1834
|
+
owner: string;
|
|
1835
|
+
repo: string;
|
|
1836
|
+
prNumber: number;
|
|
1837
|
+
}[],
|
|
1838
|
+
): Promise<Map<string, SlimPullRequest | null>> => {
|
|
1839
|
+
const aliasOf = (index: number): string => `pullRequest${index}`;
|
|
1840
|
+
const variableDeclarations = references
|
|
1841
|
+
.map(
|
|
1842
|
+
(_, index) =>
|
|
1843
|
+
`$owner${index}: String!, $repo${index}: String!, $prNumber${index}: Int!`,
|
|
1844
|
+
)
|
|
1845
|
+
.join(', ');
|
|
1846
|
+
const selections = references
|
|
1847
|
+
.map(
|
|
1848
|
+
(
|
|
1849
|
+
_,
|
|
1850
|
+
index,
|
|
1851
|
+
) => ` ${aliasOf(index)}: repository(owner: $owner${index}, name: $repo${index}) {
|
|
1852
|
+
pullRequest(number: $prNumber${index}) {
|
|
1853
|
+
url
|
|
1854
|
+
state
|
|
1855
|
+
isDraft
|
|
1856
|
+
headRefName
|
|
1857
|
+
baseRefName
|
|
1858
|
+
mergeable
|
|
1859
|
+
headRefOid
|
|
1860
|
+
reviewThreads(first: ${SLIM_PULL_REQUEST_REVIEW_THREADS_PAGE_SIZE}) {
|
|
1861
|
+
pageInfo {
|
|
1862
|
+
endCursor
|
|
1863
|
+
hasNextPage
|
|
1864
|
+
}
|
|
1865
|
+
nodes {
|
|
1866
|
+
isResolved
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
}`,
|
|
1871
|
+
)
|
|
1872
|
+
.join('\n');
|
|
1873
|
+
const query = `query PullRequestSlimStatusBatch(${variableDeclarations}) {\n${selections}\n}`;
|
|
1874
|
+
const variables: Record<string, string | number> = {};
|
|
1875
|
+
references.forEach((reference, index) => {
|
|
1876
|
+
variables[`owner${index}`] = reference.owner;
|
|
1877
|
+
variables[`repo${index}`] = reference.repo;
|
|
1878
|
+
variables[`prNumber${index}`] = reference.prNumber;
|
|
1879
|
+
});
|
|
1880
|
+
|
|
1881
|
+
const response = await this.fetchWithRateLimitRetry(() =>
|
|
1882
|
+
fetchGithubGraphql({ ghToken: this.ghToken, query, variables }),
|
|
1883
|
+
);
|
|
1884
|
+
if (!response.ok) {
|
|
1885
|
+
throw new Error(
|
|
1886
|
+
`Failed to fetch pull requests from GitHub GraphQL API: HTTP ${response.status}`,
|
|
1887
|
+
);
|
|
1888
|
+
}
|
|
1889
|
+
const responseData: unknown = await response.json();
|
|
1890
|
+
if (!isSlimPullRequestBatchResponse(responseData)) {
|
|
1891
|
+
throw new Error('Unexpected response shape when fetching pull requests');
|
|
1892
|
+
}
|
|
1893
|
+
const errors = responseData.errors || [];
|
|
1894
|
+
const notFoundAliases = new Set(
|
|
1895
|
+
errors
|
|
1896
|
+
.filter((error) => error.type === 'NOT_FOUND')
|
|
1897
|
+
.map((error) => String(error.path?.[0] ?? '')),
|
|
1898
|
+
);
|
|
1899
|
+
const otherErrorAliases = new Set(
|
|
1900
|
+
errors
|
|
1901
|
+
.filter((error) => error.type !== 'NOT_FOUND')
|
|
1902
|
+
.map((error) => String(error.path?.[0] ?? '')),
|
|
1903
|
+
);
|
|
1904
|
+
if (!responseData.data) {
|
|
1905
|
+
throw new Error(`GraphQL errors: ${JSON.stringify(errors)}`);
|
|
1906
|
+
}
|
|
1907
|
+
const unattributedError = errors.some(
|
|
1908
|
+
(error) => error.type !== 'NOT_FOUND' && !error.path,
|
|
1909
|
+
);
|
|
1910
|
+
if (unattributedError) {
|
|
1911
|
+
throw new Error(`GraphQL errors: ${JSON.stringify(errors)}`);
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
const slimByUrl = new Map<string, SlimPullRequest | null>();
|
|
1915
|
+
references.forEach((reference, index) => {
|
|
1916
|
+
const alias = aliasOf(index);
|
|
1917
|
+
if (otherErrorAliases.has(alias)) {
|
|
1918
|
+
return;
|
|
1919
|
+
}
|
|
1920
|
+
const node = responseData.data?.[alias]?.pullRequest ?? null;
|
|
1921
|
+
if (!node) {
|
|
1922
|
+
if (notFoundAliases.has(alias) || !errors.length) {
|
|
1923
|
+
slimByUrl.set(reference.prUrl, null);
|
|
1924
|
+
}
|
|
1925
|
+
return;
|
|
1926
|
+
}
|
|
1927
|
+
if (node.reviewThreads?.pageInfo.hasNextPage === true) {
|
|
1928
|
+
return;
|
|
1929
|
+
}
|
|
1930
|
+
slimByUrl.set(reference.prUrl, {
|
|
1931
|
+
url: node.url,
|
|
1932
|
+
state: node.state,
|
|
1933
|
+
isDraft: node.isDraft,
|
|
1934
|
+
headRefName: node.headRefName,
|
|
1935
|
+
baseRefName: node.baseRefName,
|
|
1936
|
+
mergeable: node.mergeable,
|
|
1937
|
+
headRefOid: node.headRefOid,
|
|
1938
|
+
reviewThreads: (node.reviewThreads?.nodes || []).map((thread) => ({
|
|
1939
|
+
isResolved: thread.isResolved,
|
|
1940
|
+
})),
|
|
1941
|
+
});
|
|
1942
|
+
});
|
|
1943
|
+
return slimByUrl;
|
|
1944
|
+
};
|
|
1945
|
+
|
|
1694
1946
|
closePullRequest = async (prUrl: string): Promise<void> => {
|
|
1695
1947
|
const { owner, repo, issueNumber: prNumber } = this.parseIssueUrl(prUrl);
|
|
1696
1948
|
const response = await this.fetchWithRateLimitRetry(() =>
|
|
@@ -27,6 +27,12 @@ export type EvaluateOptions = {
|
|
|
27
27
|
// closingIssueReferenceUrls, eliminating the per-issue timeline fan-out.
|
|
28
28
|
// When null or undefined, behaviour is unchanged: findRelatedOpenPRs runs.
|
|
29
29
|
relatedOpenPrUrls?: string[] | null;
|
|
30
|
+
// Pull request states the caller already resolved in one batched query for
|
|
31
|
+
// the whole cycle. A URL present as a key is taken from here and costs no
|
|
32
|
+
// further request; a URL absent from the map falls back to the per-URL
|
|
33
|
+
// getOpenPullRequest call, which keeps the transient-failure handling that
|
|
34
|
+
// distinguishes an unknown state from an absent pull request.
|
|
35
|
+
resolvedOpenPrByUrl?: ReadonlyMap<string, RelatedPullRequest | null> | null;
|
|
30
36
|
};
|
|
31
37
|
|
|
32
38
|
export class IssueRejectionEvaluator {
|
|
@@ -53,26 +59,16 @@ export class IssueRejectionEvaluator {
|
|
|
53
59
|
const rejections: { type: PrRejectedReasonType; detail: string }[] = [];
|
|
54
60
|
let approvedPrUrl: string | null = null;
|
|
55
61
|
|
|
56
|
-
const categoryLabels = issue.labels.filter((label) =>
|
|
57
|
-
label.startsWith('category:'),
|
|
58
|
-
);
|
|
59
|
-
const hasLlmAgentLabel = issue.labels.some(
|
|
60
|
-
(l) => l === 'llm-agent' || l.startsWith('llm-agent:'),
|
|
61
|
-
);
|
|
62
|
-
const hasLabelNotRequiringPullRequest = issue.labels.some((label) =>
|
|
63
|
-
labelsNotRequiringPullRequest.includes(label),
|
|
64
|
-
);
|
|
65
|
-
|
|
66
62
|
if (
|
|
67
|
-
|
|
68
|
-
!hasLabelNotRequiringPullRequest &&
|
|
69
|
-
this.isPullRequestRequiredByBody(issue.body) &&
|
|
70
|
-
(categoryLabels.length <= 0 || categoryLabels.includes('category:e2e'))
|
|
63
|
+
this.requiresPullRequestEvaluation(issue, labelsNotRequiringPullRequest)
|
|
71
64
|
) {
|
|
72
65
|
let prsToCheck: RelatedPullRequest[];
|
|
73
66
|
let anyPrResolutionFailed = false;
|
|
74
67
|
if (issue.isPr) {
|
|
75
|
-
const resolved = await this.resolveOpenPrsForPrItem(
|
|
68
|
+
const resolved = await this.resolveOpenPrsForPrItem(
|
|
69
|
+
issue.url,
|
|
70
|
+
options.resolvedOpenPrByUrl ?? null,
|
|
71
|
+
);
|
|
76
72
|
if (resolved === null) {
|
|
77
73
|
// getOpenPullRequest failed transiently: the PR's state is unknown
|
|
78
74
|
// for this cycle, so skip it without emitting any rejection.
|
|
@@ -82,6 +78,7 @@ export class IssueRejectionEvaluator {
|
|
|
82
78
|
} else if (options.relatedOpenPrUrls != null) {
|
|
83
79
|
const resolved = await this.resolveOpenPrsFromUrls(
|
|
84
80
|
options.relatedOpenPrUrls,
|
|
81
|
+
options.resolvedOpenPrByUrl ?? null,
|
|
85
82
|
);
|
|
86
83
|
prsToCheck = resolved.prs;
|
|
87
84
|
anyPrResolutionFailed = resolved.anyResolutionFailed;
|
|
@@ -185,12 +182,41 @@ export class IssueRejectionEvaluator {
|
|
|
185
182
|
return { rejections, approvedPrUrl };
|
|
186
183
|
};
|
|
187
184
|
|
|
185
|
+
// Whether this item's pull requests are looked at at all. A caller that
|
|
186
|
+
// resolves pull request state in one batch for a whole cycle asks this first,
|
|
187
|
+
// so the batch covers exactly the items evaluate would have resolved.
|
|
188
|
+
requiresPullRequestEvaluation = (
|
|
189
|
+
issue: { labels: string[]; body?: string | null },
|
|
190
|
+
labelsNotRequiringPullRequest: string[] = [],
|
|
191
|
+
): boolean => {
|
|
192
|
+
const categoryLabels = issue.labels.filter((label) =>
|
|
193
|
+
label.startsWith('category:'),
|
|
194
|
+
);
|
|
195
|
+
const hasLlmAgentLabel = issue.labels.some(
|
|
196
|
+
(l) => l === 'llm-agent' || l.startsWith('llm-agent:'),
|
|
197
|
+
);
|
|
198
|
+
const hasLabelNotRequiringPullRequest = issue.labels.some((label) =>
|
|
199
|
+
labelsNotRequiringPullRequest.includes(label),
|
|
200
|
+
);
|
|
201
|
+
return (
|
|
202
|
+
!hasLlmAgentLabel &&
|
|
203
|
+
!hasLabelNotRequiringPullRequest &&
|
|
204
|
+
this.isPullRequestRequiredByBody(issue.body) &&
|
|
205
|
+
(categoryLabels.length <= 0 || categoryLabels.includes('category:e2e'))
|
|
206
|
+
);
|
|
207
|
+
};
|
|
208
|
+
|
|
188
209
|
// Returns null when getOpenPullRequest throws (e.g. a transient GitHub
|
|
189
210
|
// GraphQL server error): the PR's state is unknown for this cycle and the
|
|
190
211
|
// caller skips it instead of letting the error abort the schedule cycle.
|
|
191
212
|
private resolveOpenPrsForPrItem = async (
|
|
192
213
|
prUrl: string,
|
|
214
|
+
resolvedOpenPrByUrl: ReadonlyMap<string, RelatedPullRequest | null> | null,
|
|
193
215
|
): Promise<RelatedPullRequest[] | null> => {
|
|
216
|
+
if (resolvedOpenPrByUrl?.has(prUrl)) {
|
|
217
|
+
const alreadyResolved = resolvedOpenPrByUrl.get(prUrl) ?? null;
|
|
218
|
+
return alreadyResolved === null ? [] : [alreadyResolved];
|
|
219
|
+
}
|
|
194
220
|
let pr: RelatedPullRequest | null;
|
|
195
221
|
try {
|
|
196
222
|
pr = await this.issueRepository.getOpenPullRequest(prUrl);
|
|
@@ -220,11 +246,19 @@ export class IssueRejectionEvaluator {
|
|
|
220
246
|
// PULL_REQUEST_NOT_FOUND.
|
|
221
247
|
private resolveOpenPrsFromUrls = async (
|
|
222
248
|
prUrls: string[],
|
|
249
|
+
resolvedOpenPrByUrl: ReadonlyMap<string, RelatedPullRequest | null> | null,
|
|
223
250
|
): Promise<{ prs: RelatedPullRequest[]; anyResolutionFailed: boolean }> => {
|
|
224
251
|
const uniquePrUrls = Array.from(new Set(prUrls));
|
|
225
252
|
const resolvedPrs: RelatedPullRequest[] = [];
|
|
226
253
|
let anyResolutionFailed = false;
|
|
227
254
|
for (const prUrl of uniquePrUrls) {
|
|
255
|
+
if (resolvedOpenPrByUrl?.has(prUrl)) {
|
|
256
|
+
const alreadyResolved = resolvedOpenPrByUrl.get(prUrl) ?? null;
|
|
257
|
+
if (alreadyResolved !== null) {
|
|
258
|
+
resolvedPrs.push(alreadyResolved);
|
|
259
|
+
}
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
228
262
|
let pr: RelatedPullRequest | null;
|
|
229
263
|
try {
|
|
230
264
|
pr = await this.issueRepository.getOpenPullRequest(prUrl);
|