github-issue-tower-defence-management 1.40.0 → 1.41.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/.github/workflows/umino-project.yml +5 -4
- package/CHANGELOG.md +13 -0
- package/README.md +21 -9
- package/bin/adapter/entry-points/cli/index.js +45 -10
- package/bin/adapter/entry-points/cli/index.js.map +1 -1
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +32 -8
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
- package/bin/adapter/repositories/NodeLocalCommandRunner.js +3 -3
- package/bin/adapter/repositories/NodeLocalCommandRunner.js.map +1 -1
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +412 -177
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
- package/bin/domain/usecases/HandleScheduledEventUseCase.js +5 -2
- package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
- package/bin/domain/usecases/RevertOrphanedPreparationUseCase.js +7 -2
- package/bin/domain/usecases/RevertOrphanedPreparationUseCase.js.map +1 -1
- package/bin/domain/usecases/StartPreparationUseCase.js +107 -72
- package/bin/domain/usecases/StartPreparationUseCase.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter/entry-points/cli/index.test.ts +26 -13
- package/src/adapter/entry-points/cli/index.ts +74 -13
- package/src/adapter/repositories/NodeLocalCommandRunner.test.ts +12 -12
- package/src/adapter/repositories/NodeLocalCommandRunner.ts +7 -4
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.test.ts +3 -0
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +626 -265
- package/src/adapter/repositories/issue/RestIssueRepository.test.ts +3 -0
- package/src/domain/entities/Issue.ts +1 -0
- package/src/domain/usecases/GetStoryObjectMapUseCase.test.ts +1 -0
- package/src/domain/usecases/HandleScheduledEventUseCase.ts +11 -3
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +1 -0
- package/src/domain/usecases/RevertOrphanedPreparationUseCase.test.ts +22 -9
- package/src/domain/usecases/RevertOrphanedPreparationUseCase.ts +8 -3
- package/src/domain/usecases/StartPreparationUseCase.test.ts +1696 -290
- package/src/domain/usecases/StartPreparationUseCase.ts +171 -126
- package/src/domain/usecases/adapter-interfaces/IssueRepository.ts +2 -1
- package/src/domain/usecases/adapter-interfaces/LocalCommandRunner.ts +4 -1
- package/types/adapter/entry-points/cli/index.d.ts +4 -1
- package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
- package/types/adapter/repositories/NodeLocalCommandRunner.d.ts +1 -1
- package/types/adapter/repositories/NodeLocalCommandRunner.d.ts.map +1 -1
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +5 -3
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
- package/types/domain/entities/Issue.d.ts +1 -0
- package/types/domain/entities/Issue.d.ts.map +1 -1
- package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +5 -1
- package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
- package/types/domain/usecases/RevertOrphanedPreparationUseCase.d.ts.map +1 -1
- package/types/domain/usecases/StartPreparationUseCase.d.ts +10 -18
- package/types/domain/usecases/StartPreparationUseCase.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts +2 -1
- package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/LocalCommandRunner.d.ts +1 -1
- package/types/domain/usecases/adapter-interfaces/LocalCommandRunner.d.ts.map +1 -1
|
@@ -18,90 +18,250 @@ import { normalizeFieldName } from '../utils';
|
|
|
18
18
|
import { LocalStorageRepository } from '../LocalStorageRepository';
|
|
19
19
|
import { Member } from '../../../domain/entities/Member';
|
|
20
20
|
|
|
21
|
-
type
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
21
|
+
type TimelineItem = {
|
|
22
|
+
__typename: string;
|
|
23
|
+
willCloseTarget?: boolean;
|
|
24
|
+
source?: {
|
|
25
|
+
__typename: string;
|
|
26
|
+
url?: string;
|
|
27
|
+
number?: number;
|
|
28
|
+
state?: string;
|
|
29
|
+
mergeable?: string;
|
|
30
|
+
headRefName?: string;
|
|
31
|
+
baseRefName?: string;
|
|
32
|
+
baseRepository?: {
|
|
33
|
+
branchProtectionRules?: {
|
|
34
|
+
nodes: Array<{
|
|
35
|
+
pattern: string;
|
|
36
|
+
requiredStatusCheckContexts: string[];
|
|
37
|
+
}>;
|
|
38
|
+
};
|
|
39
|
+
defaultBranchRef?: {
|
|
40
|
+
name: string;
|
|
41
|
+
} | null;
|
|
42
|
+
rulesets?: {
|
|
43
|
+
nodes: Array<{
|
|
44
|
+
name: string;
|
|
45
|
+
enforcement: string;
|
|
46
|
+
conditions: {
|
|
47
|
+
refName: {
|
|
48
|
+
include: string[];
|
|
49
|
+
exclude: string[];
|
|
46
50
|
};
|
|
47
|
-
}[];
|
|
48
|
-
};
|
|
49
|
-
reviewThreads: {
|
|
50
|
-
nodes: { isResolved: boolean }[];
|
|
51
|
-
};
|
|
52
|
-
baseRepository: {
|
|
53
|
-
branchProtectionRules: {
|
|
54
|
-
nodes: { requiredStatusCheckContexts: string[] }[];
|
|
55
51
|
};
|
|
56
|
-
|
|
57
|
-
nodes: {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
52
|
+
rules: {
|
|
53
|
+
nodes: Array<{
|
|
54
|
+
type: string;
|
|
55
|
+
parameters:
|
|
56
|
+
| {
|
|
57
|
+
requiredStatusChecks: Array<{
|
|
58
|
+
context: string;
|
|
59
|
+
}>;
|
|
60
|
+
}
|
|
61
|
+
| Record<string, never>;
|
|
62
|
+
}>;
|
|
67
63
|
};
|
|
68
|
-
}
|
|
64
|
+
}>;
|
|
69
65
|
};
|
|
70
66
|
};
|
|
67
|
+
commits?: {
|
|
68
|
+
nodes: Array<{
|
|
69
|
+
commit: {
|
|
70
|
+
statusCheckRollup?: {
|
|
71
|
+
state: string;
|
|
72
|
+
contexts?: {
|
|
73
|
+
nodes: Array<
|
|
74
|
+
| {
|
|
75
|
+
__typename: 'CheckRun';
|
|
76
|
+
name: string;
|
|
77
|
+
conclusion: string | null;
|
|
78
|
+
}
|
|
79
|
+
| {
|
|
80
|
+
__typename: 'StatusContext';
|
|
81
|
+
context: string;
|
|
82
|
+
state: string;
|
|
83
|
+
}
|
|
84
|
+
>;
|
|
85
|
+
};
|
|
86
|
+
} | null;
|
|
87
|
+
};
|
|
88
|
+
}>;
|
|
89
|
+
};
|
|
90
|
+
reviewThreads?: {
|
|
91
|
+
nodes: Array<{
|
|
92
|
+
isResolved: boolean;
|
|
93
|
+
}>;
|
|
94
|
+
};
|
|
95
|
+
baseRef?: {
|
|
96
|
+
name: string;
|
|
97
|
+
} | null;
|
|
71
98
|
};
|
|
72
|
-
errors?: { message: string }[];
|
|
73
99
|
};
|
|
74
100
|
|
|
75
|
-
type
|
|
101
|
+
type IssueTimelineResponse = {
|
|
76
102
|
data?: {
|
|
77
103
|
repository?: {
|
|
78
104
|
issue?: {
|
|
79
105
|
timelineItems: {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
106
|
+
pageInfo: {
|
|
107
|
+
endCursor: string;
|
|
108
|
+
hasNextPage: boolean;
|
|
109
|
+
};
|
|
110
|
+
nodes: TimelineItem[];
|
|
111
|
+
};
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
};
|
|
115
|
+
errors?: Array<{ message: string }>;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
type PrStatusComputationData = {
|
|
119
|
+
mergeable?: string;
|
|
120
|
+
baseRepository?: {
|
|
121
|
+
branchProtectionRules?: {
|
|
122
|
+
nodes: Array<{
|
|
123
|
+
pattern: string;
|
|
124
|
+
requiredStatusCheckContexts: string[];
|
|
125
|
+
}>;
|
|
126
|
+
};
|
|
127
|
+
defaultBranchRef?: {
|
|
128
|
+
name: string;
|
|
129
|
+
} | null;
|
|
130
|
+
rulesets?: {
|
|
131
|
+
nodes: Array<{
|
|
132
|
+
name: string;
|
|
133
|
+
enforcement: string;
|
|
134
|
+
conditions: {
|
|
135
|
+
refName: {
|
|
136
|
+
include: string[];
|
|
137
|
+
exclude: string[];
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
rules: {
|
|
141
|
+
nodes: Array<{
|
|
142
|
+
type: string;
|
|
143
|
+
parameters:
|
|
144
|
+
| {
|
|
145
|
+
requiredStatusChecks: Array<{
|
|
146
|
+
context: string;
|
|
147
|
+
}>;
|
|
148
|
+
}
|
|
149
|
+
| Record<string, never>;
|
|
150
|
+
}>;
|
|
86
151
|
};
|
|
152
|
+
}>;
|
|
153
|
+
};
|
|
154
|
+
};
|
|
155
|
+
commits?: {
|
|
156
|
+
nodes: Array<{
|
|
157
|
+
commit: {
|
|
158
|
+
statusCheckRollup?: {
|
|
159
|
+
state: string;
|
|
160
|
+
contexts?: {
|
|
161
|
+
nodes: Array<
|
|
162
|
+
| {
|
|
163
|
+
__typename: 'CheckRun';
|
|
164
|
+
name: string;
|
|
165
|
+
conclusion: string | null;
|
|
166
|
+
}
|
|
167
|
+
| {
|
|
168
|
+
__typename: 'StatusContext';
|
|
169
|
+
context: string;
|
|
170
|
+
state: string;
|
|
171
|
+
}
|
|
172
|
+
>;
|
|
173
|
+
};
|
|
174
|
+
} | null;
|
|
87
175
|
};
|
|
176
|
+
}>;
|
|
177
|
+
};
|
|
178
|
+
reviewThreads?: {
|
|
179
|
+
nodes: Array<{
|
|
180
|
+
isResolved: boolean;
|
|
181
|
+
}>;
|
|
182
|
+
};
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
type DirectPullRequestResponse = {
|
|
186
|
+
data?: {
|
|
187
|
+
repository?: {
|
|
188
|
+
pullRequest?: {
|
|
189
|
+
url: string;
|
|
190
|
+
state: string;
|
|
191
|
+
headRefName?: string;
|
|
192
|
+
baseRefName?: string;
|
|
193
|
+
} & PrStatusComputationData;
|
|
88
194
|
};
|
|
89
195
|
};
|
|
90
|
-
errors?: { message: string }
|
|
196
|
+
errors?: Array<{ message: string }>;
|
|
91
197
|
};
|
|
92
198
|
|
|
93
|
-
function
|
|
199
|
+
function isIssueTimelineResponse(
|
|
94
200
|
value: unknown,
|
|
95
|
-
): value is
|
|
96
|
-
|
|
201
|
+
): value is IssueTimelineResponse {
|
|
202
|
+
if (typeof value !== 'object' || value === null) return false;
|
|
203
|
+
return true;
|
|
97
204
|
}
|
|
98
205
|
|
|
99
|
-
function
|
|
206
|
+
function isDirectPullRequestResponse(
|
|
100
207
|
value: unknown,
|
|
101
|
-
): value is
|
|
102
|
-
|
|
208
|
+
): value is DirectPullRequestResponse {
|
|
209
|
+
if (typeof value !== 'object' || value === null) return false;
|
|
210
|
+
return true;
|
|
103
211
|
}
|
|
104
212
|
|
|
213
|
+
const fnmatch = (pattern: string, str: string): boolean => {
|
|
214
|
+
let regexStr = '^';
|
|
215
|
+
let i = 0;
|
|
216
|
+
while (i < pattern.length) {
|
|
217
|
+
const c = pattern[i];
|
|
218
|
+
if (c === '*') {
|
|
219
|
+
if (pattern[i + 1] === '*') {
|
|
220
|
+
regexStr += '.*';
|
|
221
|
+
i += 2;
|
|
222
|
+
if (pattern[i] === '/') {
|
|
223
|
+
i++;
|
|
224
|
+
}
|
|
225
|
+
} else {
|
|
226
|
+
regexStr += '[^/]*';
|
|
227
|
+
i++;
|
|
228
|
+
}
|
|
229
|
+
} else if (c === '?') {
|
|
230
|
+
regexStr += '[^/]';
|
|
231
|
+
i++;
|
|
232
|
+
} else if (c === '[') {
|
|
233
|
+
let j = i + 1;
|
|
234
|
+
while (j < pattern.length && pattern[j] !== ']') {
|
|
235
|
+
j++;
|
|
236
|
+
}
|
|
237
|
+
if (j >= pattern.length) {
|
|
238
|
+
regexStr += '\\[';
|
|
239
|
+
i++;
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
const content = pattern.slice(i + 1, j);
|
|
243
|
+
if (content.length > 0 && (content[0] === '!' || content[0] === '^')) {
|
|
244
|
+
const body = content.slice(1).replace(/\\/g, '\\\\');
|
|
245
|
+
regexStr += '[^' + body + ']';
|
|
246
|
+
} else {
|
|
247
|
+
const escapedContent = content.replace(/\\/g, '\\\\');
|
|
248
|
+
regexStr += '[' + escapedContent + ']';
|
|
249
|
+
}
|
|
250
|
+
i = j + 1;
|
|
251
|
+
} else {
|
|
252
|
+
regexStr += c.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
253
|
+
i++;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
regexStr += '$';
|
|
257
|
+
try {
|
|
258
|
+
const regex = new RegExp(regexStr);
|
|
259
|
+
return regex.test(str);
|
|
260
|
+
} catch {
|
|
261
|
+
return pattern === str;
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
|
|
105
265
|
export class ApiV3CheerioRestIssueRepository
|
|
106
266
|
extends BaseGitHubRepository
|
|
107
267
|
implements IssueRepository
|
|
@@ -215,6 +375,7 @@ export class ApiV3CheerioRestIssueRepository
|
|
|
215
375
|
isInProgress: normalizeFieldName(status || '').includes('progress'),
|
|
216
376
|
isClosed: item.state !== 'OPEN',
|
|
217
377
|
createdAt: new Date(item.createdAt || '2000-01-01'),
|
|
378
|
+
author: '',
|
|
218
379
|
};
|
|
219
380
|
};
|
|
220
381
|
getAllIssuesFromCache = async (
|
|
@@ -342,163 +503,6 @@ export class ApiV3CheerioRestIssueRepository
|
|
|
342
503
|
{ date: date.toISOString().split('T')[0] },
|
|
343
504
|
);
|
|
344
505
|
};
|
|
345
|
-
|
|
346
|
-
getOpenPullRequest = async (
|
|
347
|
-
prUrl: string,
|
|
348
|
-
): Promise<RelatedPullRequest | null> => {
|
|
349
|
-
const match = prUrl.match(
|
|
350
|
-
/https:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)/,
|
|
351
|
-
);
|
|
352
|
-
if (!match) {
|
|
353
|
-
return null;
|
|
354
|
-
}
|
|
355
|
-
const [, owner, repo, prNumberStr] = match;
|
|
356
|
-
const prNumber = parseInt(prNumberStr, 10);
|
|
357
|
-
|
|
358
|
-
const query = `query GetPullRequest($owner: String!, $repo: String!, $number: Int!) {
|
|
359
|
-
repository(owner: $owner, name: $repo) {
|
|
360
|
-
pullRequest(number: $number) {
|
|
361
|
-
state
|
|
362
|
-
mergeable
|
|
363
|
-
commits(last: 1) {
|
|
364
|
-
nodes {
|
|
365
|
-
commit {
|
|
366
|
-
statusCheckRollup {
|
|
367
|
-
state
|
|
368
|
-
contexts(first: 100) {
|
|
369
|
-
nodes {
|
|
370
|
-
... on CheckRun {
|
|
371
|
-
name
|
|
372
|
-
status
|
|
373
|
-
conclusion
|
|
374
|
-
}
|
|
375
|
-
... on StatusContext {
|
|
376
|
-
context
|
|
377
|
-
state
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
reviewThreads(first: 100) {
|
|
386
|
-
nodes {
|
|
387
|
-
isResolved
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
baseRepository {
|
|
391
|
-
branchProtectionRules(first: 10) {
|
|
392
|
-
nodes {
|
|
393
|
-
requiredStatusCheckContexts
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
rulesets(first: 10) {
|
|
397
|
-
nodes {
|
|
398
|
-
rules(first: 50) {
|
|
399
|
-
nodes {
|
|
400
|
-
type
|
|
401
|
-
parameters {
|
|
402
|
-
... on RequiredStatusChecksParameters {
|
|
403
|
-
requiredStatusChecks {
|
|
404
|
-
context
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
}`;
|
|
416
|
-
|
|
417
|
-
const response = await fetch('https://api.github.com/graphql', {
|
|
418
|
-
method: 'POST',
|
|
419
|
-
headers: {
|
|
420
|
-
Authorization: `Bearer ${this.ghToken}`,
|
|
421
|
-
'Content-Type': 'application/json',
|
|
422
|
-
},
|
|
423
|
-
body: JSON.stringify({
|
|
424
|
-
query,
|
|
425
|
-
variables: { owner, repo, number: prNumber },
|
|
426
|
-
}),
|
|
427
|
-
});
|
|
428
|
-
|
|
429
|
-
const responseData: unknown = await response.json();
|
|
430
|
-
if (!isGetPullRequestResponse(responseData)) {
|
|
431
|
-
throw new Error(
|
|
432
|
-
'Unexpected response shape when fetching pull request from GitHub GraphQL API',
|
|
433
|
-
);
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
if (responseData.errors && responseData.errors.length > 0) {
|
|
437
|
-
throw new Error(responseData.errors.map((e) => e.message).join('\n'));
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
const pr = responseData.data?.repository?.pullRequest;
|
|
441
|
-
if (!pr || pr.state !== 'OPEN') {
|
|
442
|
-
return null;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
const isConflicted = pr.mergeable === 'CONFLICTING';
|
|
446
|
-
|
|
447
|
-
const lastCommit = pr.commits.nodes[pr.commits.nodes.length - 1];
|
|
448
|
-
const rollup = lastCommit?.commit?.statusCheckRollup;
|
|
449
|
-
const isCiStateSuccess = rollup?.state === 'SUCCESS';
|
|
450
|
-
|
|
451
|
-
const requiredCheckNames: string[] = [];
|
|
452
|
-
for (const rule of pr.baseRepository.branchProtectionRules.nodes) {
|
|
453
|
-
requiredCheckNames.push(...rule.requiredStatusCheckContexts);
|
|
454
|
-
}
|
|
455
|
-
for (const ruleset of pr.baseRepository.rulesets.nodes) {
|
|
456
|
-
for (const rule of ruleset.rules.nodes) {
|
|
457
|
-
if (
|
|
458
|
-
rule.type === 'REQUIRED_STATUS_CHECKS' &&
|
|
459
|
-
rule.parameters?.requiredStatusChecks
|
|
460
|
-
) {
|
|
461
|
-
requiredCheckNames.push(
|
|
462
|
-
...rule.parameters.requiredStatusChecks.map((c) => c.context),
|
|
463
|
-
);
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
const contextNodes = rollup?.contexts?.nodes ?? [];
|
|
469
|
-
const completedCheckNames = contextNodes
|
|
470
|
-
.map((node) => {
|
|
471
|
-
if ('name' in node && node.name) {
|
|
472
|
-
return node.name;
|
|
473
|
-
}
|
|
474
|
-
if ('context' in node && node.context) {
|
|
475
|
-
return node.context;
|
|
476
|
-
}
|
|
477
|
-
return null;
|
|
478
|
-
})
|
|
479
|
-
.filter((name): name is string => name !== null);
|
|
480
|
-
|
|
481
|
-
const missingRequiredCheckNames = requiredCheckNames.filter(
|
|
482
|
-
(required) => !completedCheckNames.includes(required),
|
|
483
|
-
);
|
|
484
|
-
|
|
485
|
-
const isPassedAllCiJob =
|
|
486
|
-
isCiStateSuccess && missingRequiredCheckNames.length === 0;
|
|
487
|
-
|
|
488
|
-
const isResolvedAllReviewComments = pr.reviewThreads.nodes.every(
|
|
489
|
-
(thread) => thread.isResolved,
|
|
490
|
-
);
|
|
491
|
-
|
|
492
|
-
return {
|
|
493
|
-
url: prUrl,
|
|
494
|
-
isConflicted,
|
|
495
|
-
isPassedAllCiJob,
|
|
496
|
-
isCiStateSuccess,
|
|
497
|
-
isResolvedAllReviewComments,
|
|
498
|
-
isBranchOutOfDate: false,
|
|
499
|
-
missingRequiredCheckNames,
|
|
500
|
-
};
|
|
501
|
-
};
|
|
502
506
|
updateNextActionHour = async (
|
|
503
507
|
project: Project & {
|
|
504
508
|
nextActionHour: NonNullable<Project['nextActionHour']>;
|
|
@@ -572,36 +576,409 @@ export class ApiV3CheerioRestIssueRepository
|
|
|
572
576
|
update = async (issue: Issue, _project: Project): Promise<void> => {
|
|
573
577
|
await this.updateIssue(issue);
|
|
574
578
|
};
|
|
579
|
+
private parseIssueUrl = (
|
|
580
|
+
issueUrl: string,
|
|
581
|
+
): {
|
|
582
|
+
owner: string;
|
|
583
|
+
repo: string;
|
|
584
|
+
issueNumber: number;
|
|
585
|
+
isPr: boolean;
|
|
586
|
+
} => {
|
|
587
|
+
const urlMatch = issueUrl.match(
|
|
588
|
+
/github\.com\/([^/]+)\/([^/]+)\/(issues|pull)\/(\d+)/,
|
|
589
|
+
);
|
|
590
|
+
if (!urlMatch) {
|
|
591
|
+
throw new Error(`Invalid GitHub issue URL: ${issueUrl}`);
|
|
592
|
+
}
|
|
593
|
+
return {
|
|
594
|
+
owner: urlMatch[1],
|
|
595
|
+
repo: urlMatch[2],
|
|
596
|
+
issueNumber: parseInt(urlMatch[4], 10),
|
|
597
|
+
isPr: urlMatch[3] === 'pull',
|
|
598
|
+
};
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
private computePrStatus = (
|
|
602
|
+
prUrl: string,
|
|
603
|
+
headRefName: string | undefined,
|
|
604
|
+
baseRefName: string | undefined,
|
|
605
|
+
data: PrStatusComputationData,
|
|
606
|
+
): RelatedPullRequest => {
|
|
607
|
+
const isConflicted = data.mergeable === 'CONFLICTING';
|
|
608
|
+
const lastCommit = data.commits?.nodes[0]?.commit;
|
|
609
|
+
const ciState = lastCommit?.statusCheckRollup?.state;
|
|
610
|
+
const contexts = lastCommit?.statusCheckRollup?.contexts?.nodes || [];
|
|
611
|
+
|
|
612
|
+
const branchProtectionRules =
|
|
613
|
+
data.baseRepository?.branchProtectionRules?.nodes || [];
|
|
614
|
+
const matchingRules = baseRefName
|
|
615
|
+
? branchProtectionRules.filter(
|
|
616
|
+
(rule) =>
|
|
617
|
+
rule.pattern === baseRefName || fnmatch(rule.pattern, baseRefName),
|
|
618
|
+
)
|
|
619
|
+
: [];
|
|
620
|
+
const requiredCheckNamesSet = new Set<string>();
|
|
621
|
+
for (const rule of matchingRules) {
|
|
622
|
+
for (const name of rule.requiredStatusCheckContexts) {
|
|
623
|
+
requiredCheckNamesSet.add(name);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const rulesets = data.baseRepository?.rulesets?.nodes || [];
|
|
628
|
+
const defaultBranchName = data.baseRepository?.defaultBranchRef?.name || '';
|
|
629
|
+
for (const ruleset of rulesets) {
|
|
630
|
+
if (ruleset.enforcement !== 'ACTIVE') continue;
|
|
631
|
+
const refIncludes = ruleset.conditions.refName.include;
|
|
632
|
+
const refExcludes = ruleset.conditions.refName.exclude;
|
|
633
|
+
const matchesInclude =
|
|
634
|
+
baseRefName !== undefined &&
|
|
635
|
+
refIncludes.some((pattern) => {
|
|
636
|
+
if (pattern === '~DEFAULT_BRANCH') {
|
|
637
|
+
return baseRefName === defaultBranchName;
|
|
638
|
+
}
|
|
639
|
+
if (pattern === '~ALL') {
|
|
640
|
+
return true;
|
|
641
|
+
}
|
|
642
|
+
const branchPattern = pattern.replace(/^refs\/heads\//, '');
|
|
643
|
+
return (
|
|
644
|
+
branchPattern === baseRefName || fnmatch(branchPattern, baseRefName)
|
|
645
|
+
);
|
|
646
|
+
});
|
|
647
|
+
if (!matchesInclude) continue;
|
|
648
|
+
const matchesExclude =
|
|
649
|
+
baseRefName !== undefined &&
|
|
650
|
+
refExcludes.some((pattern) => {
|
|
651
|
+
if (pattern === '~DEFAULT_BRANCH') {
|
|
652
|
+
return baseRefName === defaultBranchName;
|
|
653
|
+
}
|
|
654
|
+
const branchPattern = pattern.replace(/^refs\/heads\//, '');
|
|
655
|
+
return (
|
|
656
|
+
branchPattern === baseRefName || fnmatch(branchPattern, baseRefName)
|
|
657
|
+
);
|
|
658
|
+
});
|
|
659
|
+
if (matchesExclude) continue;
|
|
660
|
+
for (const rule of ruleset.rules.nodes) {
|
|
661
|
+
if (rule.type !== 'REQUIRED_STATUS_CHECKS') continue;
|
|
662
|
+
if ('requiredStatusChecks' in rule.parameters) {
|
|
663
|
+
for (const check of rule.parameters.requiredStatusChecks) {
|
|
664
|
+
requiredCheckNamesSet.add(check.context);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
const requiredCheckNames = Array.from(requiredCheckNamesSet);
|
|
671
|
+
const seenContextNames = new Set<string>();
|
|
672
|
+
for (const ctx of contexts) {
|
|
673
|
+
if ('name' in ctx) {
|
|
674
|
+
seenContextNames.add(ctx.name);
|
|
675
|
+
}
|
|
676
|
+
if ('context' in ctx) {
|
|
677
|
+
seenContextNames.add(ctx.context);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
const missingRequiredCheckNames = requiredCheckNames.filter(
|
|
682
|
+
(name) => !seenContextNames.has(name),
|
|
683
|
+
);
|
|
684
|
+
const allRequiredChecksPassed = missingRequiredCheckNames.length === 0;
|
|
685
|
+
const isCiStateSuccess = ciState === 'SUCCESS';
|
|
686
|
+
const isPassedAllCiJob = isCiStateSuccess && allRequiredChecksPassed;
|
|
687
|
+
|
|
688
|
+
const reviewThreads = data.reviewThreads?.nodes || [];
|
|
689
|
+
const isResolvedAllReviewComments =
|
|
690
|
+
reviewThreads.length === 0 ||
|
|
691
|
+
reviewThreads.every((thread) => thread.isResolved);
|
|
692
|
+
|
|
693
|
+
return {
|
|
694
|
+
url: prUrl,
|
|
695
|
+
branchName: headRefName ?? null,
|
|
696
|
+
isConflicted,
|
|
697
|
+
isPassedAllCiJob,
|
|
698
|
+
isCiStateSuccess,
|
|
699
|
+
isResolvedAllReviewComments,
|
|
700
|
+
isBranchOutOfDate: false,
|
|
701
|
+
missingRequiredCheckNames,
|
|
702
|
+
};
|
|
703
|
+
};
|
|
704
|
+
|
|
575
705
|
findRelatedOpenPRs = async (
|
|
576
706
|
issueUrl: string,
|
|
577
707
|
): Promise<RelatedPullRequest[]> => {
|
|
578
|
-
const
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
708
|
+
const { owner, repo, issueNumber, isPr } = this.parseIssueUrl(issueUrl);
|
|
709
|
+
if (isPr) {
|
|
710
|
+
throw new Error(
|
|
711
|
+
'findRelatedOpenPRs only supports issue URLs, not pull request URLs',
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
const query = `
|
|
716
|
+
query($owner: String!, $repo: String!, $issueNumber: Int!, $after: String) {
|
|
717
|
+
repository(owner: $owner, name: $repo) {
|
|
718
|
+
issue(number: $issueNumber) {
|
|
719
|
+
timelineItems(first: 100, after: $after, itemTypes: [CROSS_REFERENCED_EVENT]) {
|
|
720
|
+
pageInfo {
|
|
721
|
+
endCursor
|
|
722
|
+
hasNextPage
|
|
723
|
+
}
|
|
724
|
+
nodes {
|
|
725
|
+
__typename
|
|
726
|
+
... on CrossReferencedEvent {
|
|
727
|
+
willCloseTarget
|
|
728
|
+
source {
|
|
729
|
+
__typename
|
|
730
|
+
... on PullRequest {
|
|
731
|
+
url
|
|
732
|
+
number
|
|
733
|
+
state
|
|
734
|
+
mergeable
|
|
735
|
+
headRefName
|
|
736
|
+
baseRefName
|
|
737
|
+
baseRepository {
|
|
738
|
+
branchProtectionRules(first: 100) {
|
|
739
|
+
nodes {
|
|
740
|
+
pattern
|
|
741
|
+
requiredStatusCheckContexts
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
defaultBranchRef {
|
|
745
|
+
name
|
|
746
|
+
}
|
|
747
|
+
rulesets(first: 100) {
|
|
748
|
+
nodes {
|
|
749
|
+
name
|
|
750
|
+
enforcement
|
|
751
|
+
conditions {
|
|
752
|
+
refName {
|
|
753
|
+
include
|
|
754
|
+
exclude
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
rules(first: 100) {
|
|
758
|
+
nodes {
|
|
759
|
+
type
|
|
760
|
+
parameters {
|
|
761
|
+
... on RequiredStatusChecksParameters {
|
|
762
|
+
requiredStatusChecks {
|
|
763
|
+
context
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
commits(last: 1) {
|
|
773
|
+
nodes {
|
|
774
|
+
commit {
|
|
775
|
+
statusCheckRollup {
|
|
776
|
+
state
|
|
777
|
+
contexts(first: 100) {
|
|
778
|
+
nodes {
|
|
779
|
+
__typename
|
|
780
|
+
... on CheckRun {
|
|
781
|
+
name
|
|
782
|
+
conclusion
|
|
783
|
+
}
|
|
784
|
+
... on StatusContext {
|
|
785
|
+
context
|
|
786
|
+
state
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
reviewThreads(first: 100) {
|
|
795
|
+
nodes {
|
|
796
|
+
isResolved
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
baseRef {
|
|
800
|
+
name
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
`;
|
|
811
|
+
|
|
812
|
+
const relatedPRsMap: Map<string, RelatedPullRequest> = new Map();
|
|
813
|
+
let after: string | null = null;
|
|
814
|
+
let hasNextPage = true;
|
|
815
|
+
|
|
816
|
+
while (hasNextPage) {
|
|
817
|
+
const response = await fetch('https://api.github.com/graphql', {
|
|
818
|
+
method: 'POST',
|
|
819
|
+
headers: {
|
|
820
|
+
Authorization: `Bearer ${this.ghToken}`,
|
|
821
|
+
'Content-Type': 'application/json',
|
|
822
|
+
},
|
|
823
|
+
body: JSON.stringify({
|
|
824
|
+
query,
|
|
825
|
+
variables: { owner, repo, issueNumber, after },
|
|
826
|
+
}),
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
if (!response.ok) {
|
|
830
|
+
throw new Error(
|
|
831
|
+
`Failed to fetch issue timeline from GitHub GraphQL API: HTTP ${response.status}`,
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
const responseData: unknown = await response.json();
|
|
836
|
+
if (!isIssueTimelineResponse(responseData)) {
|
|
837
|
+
throw new Error(
|
|
838
|
+
'Unexpected response shape when fetching issue timeline',
|
|
839
|
+
);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
const issueData = responseData.data?.repository?.issue;
|
|
843
|
+
if (!issueData) {
|
|
844
|
+
throw new Error(
|
|
845
|
+
'Issue not found when fetching timeline from GitHub GraphQL API',
|
|
846
|
+
);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
for (const item of issueData.timelineItems.nodes) {
|
|
850
|
+
if (item.__typename !== 'CrossReferencedEvent') continue;
|
|
851
|
+
if (!item.source || item.source.__typename !== 'PullRequest') continue;
|
|
852
|
+
if (item.source.state !== 'OPEN') continue;
|
|
853
|
+
if (!item.willCloseTarget) continue;
|
|
854
|
+
|
|
855
|
+
const pr = item.source;
|
|
856
|
+
const prUrl = pr.url || '';
|
|
857
|
+
const baseRefName = pr.baseRefName ?? pr.baseRef?.name;
|
|
858
|
+
|
|
859
|
+
relatedPRsMap.set(
|
|
860
|
+
prUrl,
|
|
861
|
+
this.computePrStatus(prUrl, pr.headRefName, baseRefName, pr),
|
|
862
|
+
);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
hasNextPage = issueData.timelineItems.pageInfo.hasNextPage;
|
|
866
|
+
after = issueData.timelineItems.pageInfo.endCursor;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
return Array.from(relatedPRsMap.values());
|
|
870
|
+
};
|
|
871
|
+
|
|
872
|
+
getAllOpened = async (project: Project): Promise<Issue[]> => {
|
|
873
|
+
const { issues } = await this.getAllIssues(project.id, 0);
|
|
874
|
+
return issues.filter((issue) => !issue.isClosed);
|
|
875
|
+
};
|
|
876
|
+
|
|
877
|
+
getStoryObjectMap = async (project: Project): Promise<StoryObjectMap> => {
|
|
878
|
+
const { issues } = await this.getAllIssues(project.id, 0);
|
|
879
|
+
const storyObjectMap: StoryObjectMap = new Map();
|
|
880
|
+
const targetStories = project.story?.stories || [];
|
|
881
|
+
for (const story of targetStories) {
|
|
882
|
+
const storyIssue = issues.find((issue) =>
|
|
883
|
+
story.name.startsWith(issue.title),
|
|
884
|
+
);
|
|
885
|
+
storyObjectMap.set(story.name, {
|
|
886
|
+
story,
|
|
887
|
+
storyIssue: storyIssue || null,
|
|
888
|
+
issues: [],
|
|
889
|
+
});
|
|
890
|
+
for (const issue of issues) {
|
|
891
|
+
if (issue.story !== story.name) continue;
|
|
892
|
+
storyObjectMap.get(story.name)?.issues.push(issue);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
return storyObjectMap;
|
|
896
|
+
};
|
|
897
|
+
|
|
898
|
+
getOpenPullRequest = async (
|
|
899
|
+
prUrl: string,
|
|
900
|
+
): Promise<RelatedPullRequest | null> => {
|
|
901
|
+
const parsedUrl = this.parseIssueUrl(prUrl);
|
|
902
|
+
if (!parsedUrl.isPr) {
|
|
903
|
+
return null;
|
|
583
904
|
}
|
|
584
|
-
const
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
905
|
+
const { owner, repo, issueNumber: prNumber } = parsedUrl;
|
|
906
|
+
|
|
907
|
+
const query = `
|
|
908
|
+
query($owner: String!, $repo: String!, $prNumber: Int!) {
|
|
909
|
+
repository(owner: $owner, name: $repo) {
|
|
910
|
+
pullRequest(number: $prNumber) {
|
|
911
|
+
url
|
|
912
|
+
state
|
|
913
|
+
headRefName
|
|
914
|
+
baseRefName
|
|
915
|
+
mergeable
|
|
916
|
+
baseRepository {
|
|
917
|
+
branchProtectionRules(first: 100) {
|
|
918
|
+
nodes {
|
|
919
|
+
pattern
|
|
920
|
+
requiredStatusCheckContexts
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
defaultBranchRef {
|
|
924
|
+
name
|
|
925
|
+
}
|
|
926
|
+
rulesets(first: 100) {
|
|
927
|
+
nodes {
|
|
928
|
+
name
|
|
929
|
+
enforcement
|
|
930
|
+
conditions {
|
|
931
|
+
refName {
|
|
932
|
+
include
|
|
933
|
+
exclude
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
rules(first: 100) {
|
|
937
|
+
nodes {
|
|
938
|
+
type
|
|
939
|
+
parameters {
|
|
940
|
+
... on RequiredStatusChecksParameters {
|
|
941
|
+
requiredStatusChecks {
|
|
942
|
+
context
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
commits(last: 1) {
|
|
952
|
+
nodes {
|
|
953
|
+
commit {
|
|
954
|
+
statusCheckRollup {
|
|
596
955
|
state
|
|
956
|
+
contexts(first: 100) {
|
|
957
|
+
nodes {
|
|
958
|
+
__typename
|
|
959
|
+
... on CheckRun {
|
|
960
|
+
name
|
|
961
|
+
conclusion
|
|
962
|
+
}
|
|
963
|
+
... on StatusContext {
|
|
964
|
+
context
|
|
965
|
+
state
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
}
|
|
597
969
|
}
|
|
598
970
|
}
|
|
599
971
|
}
|
|
600
972
|
}
|
|
973
|
+
reviewThreads(first: 100) {
|
|
974
|
+
nodes {
|
|
975
|
+
isResolved
|
|
976
|
+
}
|
|
977
|
+
}
|
|
601
978
|
}
|
|
602
979
|
}
|
|
603
980
|
}
|
|
604
|
-
|
|
981
|
+
`;
|
|
605
982
|
|
|
606
983
|
const response = await fetch('https://api.github.com/graphql', {
|
|
607
984
|
method: 'POST',
|
|
@@ -611,46 +988,30 @@ export class ApiV3CheerioRestIssueRepository
|
|
|
611
988
|
},
|
|
612
989
|
body: JSON.stringify({
|
|
613
990
|
query,
|
|
614
|
-
variables: { owner, repo,
|
|
991
|
+
variables: { owner, repo, prNumber },
|
|
615
992
|
}),
|
|
616
993
|
});
|
|
617
994
|
|
|
618
|
-
|
|
619
|
-
if (!isFindRelatedPRsResponse(responseData)) {
|
|
995
|
+
if (!response.ok) {
|
|
620
996
|
throw new Error(
|
|
621
|
-
|
|
997
|
+
`Failed to fetch pull request from GitHub GraphQL API: HTTP ${response.status}`,
|
|
622
998
|
);
|
|
623
999
|
}
|
|
624
1000
|
|
|
1001
|
+
const responseData: unknown = await response.json();
|
|
1002
|
+
if (!isDirectPullRequestResponse(responseData)) {
|
|
1003
|
+
throw new Error('Unexpected response shape when fetching pull request');
|
|
1004
|
+
}
|
|
1005
|
+
|
|
625
1006
|
if (responseData.errors && responseData.errors.length > 0) {
|
|
626
|
-
throw new Error(
|
|
1007
|
+
throw new Error(`GraphQL errors: ${JSON.stringify(responseData.errors)}`);
|
|
627
1008
|
}
|
|
628
1009
|
|
|
629
|
-
const
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
.filter(
|
|
633
|
-
(node) =>
|
|
634
|
-
node.source?.url &&
|
|
635
|
-
node.source?.state === 'OPEN' &&
|
|
636
|
-
node.source.url.includes('/pull/'),
|
|
637
|
-
)
|
|
638
|
-
.map((node) => node.source?.url)
|
|
639
|
-
.filter((url): url is string => url !== undefined);
|
|
640
|
-
|
|
641
|
-
const results: RelatedPullRequest[] = [];
|
|
642
|
-
for (const prUrl of openPrUrls) {
|
|
643
|
-
const pr = await this.getOpenPullRequest(prUrl);
|
|
644
|
-
if (pr) {
|
|
645
|
-
results.push(pr);
|
|
646
|
-
}
|
|
1010
|
+
const pr = responseData.data?.repository?.pullRequest;
|
|
1011
|
+
if (!pr || pr.state !== 'OPEN') {
|
|
1012
|
+
return null;
|
|
647
1013
|
}
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
getAllOpened = async (_project: Project): Promise<Issue[]> => {
|
|
651
|
-
throw new Error('getAllOpened is not implemented');
|
|
652
|
-
};
|
|
653
|
-
getStoryObjectMap = async (_project: Project): Promise<StoryObjectMap> => {
|
|
654
|
-
throw new Error('getStoryObjectMap is not implemented');
|
|
1014
|
+
|
|
1015
|
+
return this.computePrStatus(pr.url, pr.headRefName, pr.baseRefName, pr);
|
|
655
1016
|
};
|
|
656
1017
|
}
|