github-issue-tower-defence-management 1.39.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/create-pr.yml +12 -5
- package/.github/workflows/umino-project.yml +5 -4
- package/CHANGELOG.md +26 -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 +446 -11
- 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/NotifyFinishedIssuePreparationUseCase.js +111 -16
- package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.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 +689 -12
- 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 +983 -167
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.ts +177 -26
- 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 +4 -4
- 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 +7 -6
- 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/NotifyFinishedIssuePreparationUseCase.d.ts +5 -1
- package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.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 +3 -3
- 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,6 +18,250 @@ import { normalizeFieldName } from '../utils';
|
|
|
18
18
|
import { LocalStorageRepository } from '../LocalStorageRepository';
|
|
19
19
|
import { Member } from '../../../domain/entities/Member';
|
|
20
20
|
|
|
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[];
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
rules: {
|
|
53
|
+
nodes: Array<{
|
|
54
|
+
type: string;
|
|
55
|
+
parameters:
|
|
56
|
+
| {
|
|
57
|
+
requiredStatusChecks: Array<{
|
|
58
|
+
context: string;
|
|
59
|
+
}>;
|
|
60
|
+
}
|
|
61
|
+
| Record<string, never>;
|
|
62
|
+
}>;
|
|
63
|
+
};
|
|
64
|
+
}>;
|
|
65
|
+
};
|
|
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;
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
type IssueTimelineResponse = {
|
|
102
|
+
data?: {
|
|
103
|
+
repository?: {
|
|
104
|
+
issue?: {
|
|
105
|
+
timelineItems: {
|
|
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
|
+
}>;
|
|
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;
|
|
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;
|
|
194
|
+
};
|
|
195
|
+
};
|
|
196
|
+
errors?: Array<{ message: string }>;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
function isIssueTimelineResponse(
|
|
200
|
+
value: unknown,
|
|
201
|
+
): value is IssueTimelineResponse {
|
|
202
|
+
if (typeof value !== 'object' || value === null) return false;
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function isDirectPullRequestResponse(
|
|
207
|
+
value: unknown,
|
|
208
|
+
): value is DirectPullRequestResponse {
|
|
209
|
+
if (typeof value !== 'object' || value === null) return false;
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
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
|
+
|
|
21
265
|
export class ApiV3CheerioRestIssueRepository
|
|
22
266
|
extends BaseGitHubRepository
|
|
23
267
|
implements IssueRepository
|
|
@@ -131,6 +375,7 @@ export class ApiV3CheerioRestIssueRepository
|
|
|
131
375
|
isInProgress: normalizeFieldName(status || '').includes('progress'),
|
|
132
376
|
isClosed: item.state !== 'OPEN',
|
|
133
377
|
createdAt: new Date(item.createdAt || '2000-01-01'),
|
|
378
|
+
author: '',
|
|
134
379
|
};
|
|
135
380
|
};
|
|
136
381
|
getAllIssuesFromCache = async (
|
|
@@ -239,18 +484,23 @@ export class ApiV3CheerioRestIssueRepository
|
|
|
239
484
|
return this.convertProjectItemToIssue(projectItem);
|
|
240
485
|
};
|
|
241
486
|
updateNextActionDate = async (
|
|
242
|
-
|
|
243
|
-
|
|
487
|
+
issueUrl: string,
|
|
488
|
+
project: Project,
|
|
244
489
|
date: Date,
|
|
245
490
|
): Promise<void> => {
|
|
246
|
-
if (project.nextActionDate
|
|
247
|
-
|
|
491
|
+
if (!project.nextActionDate) {
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
const projectItem =
|
|
495
|
+
await this.graphqlProjectItemRepository.fetchProjectItemByUrl(issueUrl);
|
|
496
|
+
if (!projectItem) {
|
|
497
|
+
return;
|
|
248
498
|
}
|
|
249
499
|
return this.graphqlProjectItemRepository.updateProjectField(
|
|
250
500
|
project.id,
|
|
251
501
|
project.nextActionDate.fieldId,
|
|
252
|
-
|
|
253
|
-
{ date: date.toISOString() },
|
|
502
|
+
projectItem.id,
|
|
503
|
+
{ date: date.toISOString().split('T')[0] },
|
|
254
504
|
);
|
|
255
505
|
};
|
|
256
506
|
updateNextActionHour = async (
|
|
@@ -326,15 +576,442 @@ export class ApiV3CheerioRestIssueRepository
|
|
|
326
576
|
update = async (issue: Issue, _project: Project): Promise<void> => {
|
|
327
577
|
await this.updateIssue(issue);
|
|
328
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
|
+
|
|
329
705
|
findRelatedOpenPRs = async (
|
|
330
|
-
|
|
706
|
+
issueUrl: string,
|
|
331
707
|
): Promise<RelatedPullRequest[]> => {
|
|
332
|
-
|
|
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());
|
|
333
870
|
};
|
|
334
|
-
|
|
335
|
-
|
|
871
|
+
|
|
872
|
+
getAllOpened = async (project: Project): Promise<Issue[]> => {
|
|
873
|
+
const { issues } = await this.getAllIssues(project.id, 0);
|
|
874
|
+
return issues.filter((issue) => !issue.isClosed);
|
|
336
875
|
};
|
|
337
|
-
|
|
338
|
-
|
|
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;
|
|
904
|
+
}
|
|
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 {
|
|
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
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
reviewThreads(first: 100) {
|
|
974
|
+
nodes {
|
|
975
|
+
isResolved
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
`;
|
|
982
|
+
|
|
983
|
+
const response = await fetch('https://api.github.com/graphql', {
|
|
984
|
+
method: 'POST',
|
|
985
|
+
headers: {
|
|
986
|
+
Authorization: `Bearer ${this.ghToken}`,
|
|
987
|
+
'Content-Type': 'application/json',
|
|
988
|
+
},
|
|
989
|
+
body: JSON.stringify({
|
|
990
|
+
query,
|
|
991
|
+
variables: { owner, repo, prNumber },
|
|
992
|
+
}),
|
|
993
|
+
});
|
|
994
|
+
|
|
995
|
+
if (!response.ok) {
|
|
996
|
+
throw new Error(
|
|
997
|
+
`Failed to fetch pull request from GitHub GraphQL API: HTTP ${response.status}`,
|
|
998
|
+
);
|
|
999
|
+
}
|
|
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
|
+
|
|
1006
|
+
if (responseData.errors && responseData.errors.length > 0) {
|
|
1007
|
+
throw new Error(`GraphQL errors: ${JSON.stringify(responseData.errors)}`);
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
const pr = responseData.data?.repository?.pullRequest;
|
|
1011
|
+
if (!pr || pr.state !== 'OPEN') {
|
|
1012
|
+
return null;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
return this.computePrStatus(pr.url, pr.headRefName, pr.baseRefName, pr);
|
|
339
1016
|
};
|
|
340
1017
|
}
|