github-issue-tower-defence-management 1.40.0 → 1.42.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 +20 -0
- package/README.md +27 -9
- package/bin/adapter/entry-points/cli/index.js +68 -10
- package/bin/adapter/entry-points/cli/index.js.map +1 -1
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +44 -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 +6 -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 +115 -72
- package/bin/domain/usecases/StartPreparationUseCase.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter/entry-points/cli/index.test.ts +184 -13
- package/src/adapter/entry-points/cli/index.ts +105 -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 +13 -3
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +1 -0
- package/src/domain/usecases/RevertOrphanedPreparationUseCase.test.ts +64 -9
- package/src/domain/usecases/RevertOrphanedPreparationUseCase.ts +8 -3
- package/src/domain/usecases/StartPreparationUseCase.test.ts +1978 -295
- package/src/domain/usecases/StartPreparationUseCase.ts +185 -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 +5 -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 +6 -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 +11 -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
|
@@ -7,18 +7,73 @@ exports.ApiV3CheerioRestIssueRepository = void 0;
|
|
|
7
7
|
const typia_1 = __importDefault(require("typia"));
|
|
8
8
|
const BaseGitHubRepository_1 = require("../BaseGitHubRepository");
|
|
9
9
|
const utils_1 = require("../utils");
|
|
10
|
-
function
|
|
11
|
-
|
|
12
|
-
if (_io9(input))
|
|
13
|
-
return _io9(input);
|
|
14
|
-
if (_io10(input))
|
|
15
|
-
return _io10(input);
|
|
10
|
+
function isIssueTimelineResponse(value) {
|
|
11
|
+
if (typeof value !== 'object' || value === null)
|
|
16
12
|
return false;
|
|
17
|
-
|
|
13
|
+
return true;
|
|
18
14
|
}
|
|
19
|
-
function
|
|
20
|
-
|
|
15
|
+
function isDirectPullRequestResponse(value) {
|
|
16
|
+
if (typeof value !== 'object' || value === null)
|
|
17
|
+
return false;
|
|
18
|
+
return true;
|
|
21
19
|
}
|
|
20
|
+
const fnmatch = (pattern, str) => {
|
|
21
|
+
let regexStr = '^';
|
|
22
|
+
let i = 0;
|
|
23
|
+
while (i < pattern.length) {
|
|
24
|
+
const c = pattern[i];
|
|
25
|
+
if (c === '*') {
|
|
26
|
+
if (pattern[i + 1] === '*') {
|
|
27
|
+
regexStr += '.*';
|
|
28
|
+
i += 2;
|
|
29
|
+
if (pattern[i] === '/') {
|
|
30
|
+
i++;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
regexStr += '[^/]*';
|
|
35
|
+
i++;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
else if (c === '?') {
|
|
39
|
+
regexStr += '[^/]';
|
|
40
|
+
i++;
|
|
41
|
+
}
|
|
42
|
+
else if (c === '[') {
|
|
43
|
+
let j = i + 1;
|
|
44
|
+
while (j < pattern.length && pattern[j] !== ']') {
|
|
45
|
+
j++;
|
|
46
|
+
}
|
|
47
|
+
if (j >= pattern.length) {
|
|
48
|
+
regexStr += '\\[';
|
|
49
|
+
i++;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const content = pattern.slice(i + 1, j);
|
|
53
|
+
if (content.length > 0 && (content[0] === '!' || content[0] === '^')) {
|
|
54
|
+
const body = content.slice(1).replace(/\\/g, '\\\\');
|
|
55
|
+
regexStr += '[^' + body + ']';
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
const escapedContent = content.replace(/\\/g, '\\\\');
|
|
59
|
+
regexStr += '[' + escapedContent + ']';
|
|
60
|
+
}
|
|
61
|
+
i = j + 1;
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
regexStr += c.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
65
|
+
i++;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
regexStr += '$';
|
|
69
|
+
try {
|
|
70
|
+
const regex = new RegExp(regexStr);
|
|
71
|
+
return regex.test(str);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return pattern === str;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
22
77
|
class ApiV3CheerioRestIssueRepository extends BaseGitHubRepository_1.BaseGitHubRepository {
|
|
23
78
|
constructor(apiV3IssueRepository, restIssueRepository, graphqlProjectItemRepository, localStorageCacheRepository, localStorageRepository, jsonFilePath = './tmp/github.com.cookies.json', ghToken = process.env.GH_TOKEN || 'dummy', ghUserName = process.env.GH_USER_NAME, ghUserPassword = process.env.GH_USER_PASSWORD, ghAuthenticatorKey = process.env
|
|
24
79
|
.GH_AUTHENTICATOR_KEY) {
|
|
@@ -74,6 +129,7 @@ class ApiV3CheerioRestIssueRepository extends BaseGitHubRepository_1.BaseGitHubR
|
|
|
74
129
|
isInProgress: (0, utils_1.normalizeFieldName)(status || '').includes('progress'),
|
|
75
130
|
isClosed: item.state !== 'OPEN',
|
|
76
131
|
createdAt: new Date(item.createdAt || '2000-01-01'),
|
|
132
|
+
author: '',
|
|
77
133
|
};
|
|
78
134
|
};
|
|
79
135
|
this.getAllIssuesFromCache = async (cacheKey, allowCacheMinutes) => {
|
|
@@ -108,7 +164,7 @@ class ApiV3CheerioRestIssueRepository extends BaseGitHubRepository_1.BaseGitHubR
|
|
|
108
164
|
createdAt: createdAt,
|
|
109
165
|
};
|
|
110
166
|
});
|
|
111
|
-
if ((() => { const _io0 = input => "string" === typeof input.nameWithOwner && "number" === typeof input.number && "string" === typeof input.title && ("OPEN" === input.state || "CLOSED" === input.state || "MERGED" === input.state) && (null === input.status || "string" === typeof input.status) && (null === input.story || "string" === typeof input.story) && (null === input.nextActionDate || input.nextActionDate instanceof Date) && (null === input.nextActionHour || "number" === typeof input.nextActionHour) && (null === input.estimationMinutes || "number" === typeof input.estimationMinutes) && (Array.isArray(input.dependedIssueUrls) && input.dependedIssueUrls.every(elem => "string" === typeof elem)) && (null === input.completionDate50PercentConfidence || input.completionDate50PercentConfidence instanceof Date) && "string" === typeof input.url && (Array.isArray(input.assignees) && input.assignees.every(elem => "string" === typeof elem)) && (Array.isArray(input.labels) && input.labels.every(elem => "string" === typeof elem)) && "string" === typeof input.org && "string" === typeof input.repo && "string" === typeof input.body && "string" === typeof input.itemId && "boolean" === typeof input.isPr && "boolean" === typeof input.isInProgress && "boolean" === typeof input.isClosed && input.createdAt instanceof Date; return input => Array.isArray(input) && input.every(elem => "object" === typeof elem && null !== elem && _io0(elem)); })()(issues)) {
|
|
167
|
+
if ((() => { const _io0 = input => "string" === typeof input.nameWithOwner && "number" === typeof input.number && "string" === typeof input.title && ("OPEN" === input.state || "CLOSED" === input.state || "MERGED" === input.state) && (null === input.status || "string" === typeof input.status) && (null === input.story || "string" === typeof input.story) && (null === input.nextActionDate || input.nextActionDate instanceof Date) && (null === input.nextActionHour || "number" === typeof input.nextActionHour) && (null === input.estimationMinutes || "number" === typeof input.estimationMinutes) && (Array.isArray(input.dependedIssueUrls) && input.dependedIssueUrls.every(elem => "string" === typeof elem)) && (null === input.completionDate50PercentConfidence || input.completionDate50PercentConfidence instanceof Date) && "string" === typeof input.url && (Array.isArray(input.assignees) && input.assignees.every(elem => "string" === typeof elem)) && (Array.isArray(input.labels) && input.labels.every(elem => "string" === typeof elem)) && "string" === typeof input.org && "string" === typeof input.repo && "string" === typeof input.body && "string" === typeof input.itemId && "boolean" === typeof input.isPr && "boolean" === typeof input.isInProgress && "boolean" === typeof input.isClosed && input.createdAt instanceof Date && "string" === typeof input.author; return input => Array.isArray(input) && input.every(elem => "object" === typeof elem && null !== elem && _io0(elem)); })()(issues)) {
|
|
112
168
|
return issues;
|
|
113
169
|
}
|
|
114
170
|
}
|
|
@@ -152,134 +208,6 @@ class ApiV3CheerioRestIssueRepository extends BaseGitHubRepository_1.BaseGitHubR
|
|
|
152
208
|
}
|
|
153
209
|
return this.graphqlProjectItemRepository.updateProjectField(project.id, project.nextActionDate.fieldId, projectItem.id, { date: date.toISOString().split('T')[0] });
|
|
154
210
|
};
|
|
155
|
-
this.getOpenPullRequest = async (prUrl) => {
|
|
156
|
-
const match = prUrl.match(/https:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)/);
|
|
157
|
-
if (!match) {
|
|
158
|
-
return null;
|
|
159
|
-
}
|
|
160
|
-
const [, owner, repo, prNumberStr] = match;
|
|
161
|
-
const prNumber = parseInt(prNumberStr, 10);
|
|
162
|
-
const query = `query GetPullRequest($owner: String!, $repo: String!, $number: Int!) {
|
|
163
|
-
repository(owner: $owner, name: $repo) {
|
|
164
|
-
pullRequest(number: $number) {
|
|
165
|
-
state
|
|
166
|
-
mergeable
|
|
167
|
-
commits(last: 1) {
|
|
168
|
-
nodes {
|
|
169
|
-
commit {
|
|
170
|
-
statusCheckRollup {
|
|
171
|
-
state
|
|
172
|
-
contexts(first: 100) {
|
|
173
|
-
nodes {
|
|
174
|
-
... on CheckRun {
|
|
175
|
-
name
|
|
176
|
-
status
|
|
177
|
-
conclusion
|
|
178
|
-
}
|
|
179
|
-
... on StatusContext {
|
|
180
|
-
context
|
|
181
|
-
state
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
reviewThreads(first: 100) {
|
|
190
|
-
nodes {
|
|
191
|
-
isResolved
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
baseRepository {
|
|
195
|
-
branchProtectionRules(first: 10) {
|
|
196
|
-
nodes {
|
|
197
|
-
requiredStatusCheckContexts
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
rulesets(first: 10) {
|
|
201
|
-
nodes {
|
|
202
|
-
rules(first: 50) {
|
|
203
|
-
nodes {
|
|
204
|
-
type
|
|
205
|
-
parameters {
|
|
206
|
-
... on RequiredStatusChecksParameters {
|
|
207
|
-
requiredStatusChecks {
|
|
208
|
-
context
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
}`;
|
|
220
|
-
const response = await fetch('https://api.github.com/graphql', {
|
|
221
|
-
method: 'POST',
|
|
222
|
-
headers: {
|
|
223
|
-
Authorization: `Bearer ${this.ghToken}`,
|
|
224
|
-
'Content-Type': 'application/json',
|
|
225
|
-
},
|
|
226
|
-
body: JSON.stringify({
|
|
227
|
-
query,
|
|
228
|
-
variables: { owner, repo, number: prNumber },
|
|
229
|
-
}),
|
|
230
|
-
});
|
|
231
|
-
const responseData = await response.json();
|
|
232
|
-
if (!isGetPullRequestResponse(responseData)) {
|
|
233
|
-
throw new Error('Unexpected response shape when fetching pull request from GitHub GraphQL API');
|
|
234
|
-
}
|
|
235
|
-
if (responseData.errors && responseData.errors.length > 0) {
|
|
236
|
-
throw new Error(responseData.errors.map((e) => e.message).join('\n'));
|
|
237
|
-
}
|
|
238
|
-
const pr = responseData.data?.repository?.pullRequest;
|
|
239
|
-
if (!pr || pr.state !== 'OPEN') {
|
|
240
|
-
return null;
|
|
241
|
-
}
|
|
242
|
-
const isConflicted = pr.mergeable === 'CONFLICTING';
|
|
243
|
-
const lastCommit = pr.commits.nodes[pr.commits.nodes.length - 1];
|
|
244
|
-
const rollup = lastCommit?.commit?.statusCheckRollup;
|
|
245
|
-
const isCiStateSuccess = rollup?.state === 'SUCCESS';
|
|
246
|
-
const requiredCheckNames = [];
|
|
247
|
-
for (const rule of pr.baseRepository.branchProtectionRules.nodes) {
|
|
248
|
-
requiredCheckNames.push(...rule.requiredStatusCheckContexts);
|
|
249
|
-
}
|
|
250
|
-
for (const ruleset of pr.baseRepository.rulesets.nodes) {
|
|
251
|
-
for (const rule of ruleset.rules.nodes) {
|
|
252
|
-
if (rule.type === 'REQUIRED_STATUS_CHECKS' &&
|
|
253
|
-
rule.parameters?.requiredStatusChecks) {
|
|
254
|
-
requiredCheckNames.push(...rule.parameters.requiredStatusChecks.map((c) => c.context));
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
const contextNodes = rollup?.contexts?.nodes ?? [];
|
|
259
|
-
const completedCheckNames = contextNodes
|
|
260
|
-
.map((node) => {
|
|
261
|
-
if ('name' in node && node.name) {
|
|
262
|
-
return node.name;
|
|
263
|
-
}
|
|
264
|
-
if ('context' in node && node.context) {
|
|
265
|
-
return node.context;
|
|
266
|
-
}
|
|
267
|
-
return null;
|
|
268
|
-
})
|
|
269
|
-
.filter((name) => name !== null);
|
|
270
|
-
const missingRequiredCheckNames = requiredCheckNames.filter((required) => !completedCheckNames.includes(required));
|
|
271
|
-
const isPassedAllCiJob = isCiStateSuccess && missingRequiredCheckNames.length === 0;
|
|
272
|
-
const isResolvedAllReviewComments = pr.reviewThreads.nodes.every((thread) => thread.isResolved);
|
|
273
|
-
return {
|
|
274
|
-
url: prUrl,
|
|
275
|
-
isConflicted,
|
|
276
|
-
isPassedAllCiJob,
|
|
277
|
-
isCiStateSuccess,
|
|
278
|
-
isResolvedAllReviewComments,
|
|
279
|
-
isBranchOutOfDate: false,
|
|
280
|
-
missingRequiredCheckNames,
|
|
281
|
-
};
|
|
282
|
-
};
|
|
283
211
|
this.updateNextActionHour = async (project, issue, hour) => {
|
|
284
212
|
return this.graphqlProjectItemRepository.updateProjectField(project.id, project.nextActionHour.fieldId, issue.itemId, { number: hour });
|
|
285
213
|
};
|
|
@@ -311,31 +239,351 @@ class ApiV3CheerioRestIssueRepository extends BaseGitHubRepository_1.BaseGitHubR
|
|
|
311
239
|
this.update = async (issue, _project) => {
|
|
312
240
|
await this.updateIssue(issue);
|
|
313
241
|
};
|
|
242
|
+
this.parseIssueUrl = (issueUrl) => {
|
|
243
|
+
const urlMatch = issueUrl.match(/github\.com\/([^/]+)\/([^/]+)\/(issues|pull)\/(\d+)/);
|
|
244
|
+
if (!urlMatch) {
|
|
245
|
+
throw new Error(`Invalid GitHub issue URL: ${issueUrl}`);
|
|
246
|
+
}
|
|
247
|
+
return {
|
|
248
|
+
owner: urlMatch[1],
|
|
249
|
+
repo: urlMatch[2],
|
|
250
|
+
issueNumber: parseInt(urlMatch[4], 10),
|
|
251
|
+
isPr: urlMatch[3] === 'pull',
|
|
252
|
+
};
|
|
253
|
+
};
|
|
254
|
+
this.computePrStatus = (prUrl, headRefName, baseRefName, data) => {
|
|
255
|
+
const isConflicted = data.mergeable === 'CONFLICTING';
|
|
256
|
+
const lastCommit = data.commits?.nodes[0]?.commit;
|
|
257
|
+
const ciState = lastCommit?.statusCheckRollup?.state;
|
|
258
|
+
const contexts = lastCommit?.statusCheckRollup?.contexts?.nodes || [];
|
|
259
|
+
const branchProtectionRules = data.baseRepository?.branchProtectionRules?.nodes || [];
|
|
260
|
+
const matchingRules = baseRefName
|
|
261
|
+
? branchProtectionRules.filter((rule) => rule.pattern === baseRefName || fnmatch(rule.pattern, baseRefName))
|
|
262
|
+
: [];
|
|
263
|
+
const requiredCheckNamesSet = new Set();
|
|
264
|
+
for (const rule of matchingRules) {
|
|
265
|
+
for (const name of rule.requiredStatusCheckContexts) {
|
|
266
|
+
requiredCheckNamesSet.add(name);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
const rulesets = data.baseRepository?.rulesets?.nodes || [];
|
|
270
|
+
const defaultBranchName = data.baseRepository?.defaultBranchRef?.name || '';
|
|
271
|
+
for (const ruleset of rulesets) {
|
|
272
|
+
if (ruleset.enforcement !== 'ACTIVE')
|
|
273
|
+
continue;
|
|
274
|
+
const refIncludes = ruleset.conditions.refName.include;
|
|
275
|
+
const refExcludes = ruleset.conditions.refName.exclude;
|
|
276
|
+
const matchesInclude = baseRefName !== undefined &&
|
|
277
|
+
refIncludes.some((pattern) => {
|
|
278
|
+
if (pattern === '~DEFAULT_BRANCH') {
|
|
279
|
+
return baseRefName === defaultBranchName;
|
|
280
|
+
}
|
|
281
|
+
if (pattern === '~ALL') {
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
const branchPattern = pattern.replace(/^refs\/heads\//, '');
|
|
285
|
+
return (branchPattern === baseRefName || fnmatch(branchPattern, baseRefName));
|
|
286
|
+
});
|
|
287
|
+
if (!matchesInclude)
|
|
288
|
+
continue;
|
|
289
|
+
const matchesExclude = baseRefName !== undefined &&
|
|
290
|
+
refExcludes.some((pattern) => {
|
|
291
|
+
if (pattern === '~DEFAULT_BRANCH') {
|
|
292
|
+
return baseRefName === defaultBranchName;
|
|
293
|
+
}
|
|
294
|
+
const branchPattern = pattern.replace(/^refs\/heads\//, '');
|
|
295
|
+
return (branchPattern === baseRefName || fnmatch(branchPattern, baseRefName));
|
|
296
|
+
});
|
|
297
|
+
if (matchesExclude)
|
|
298
|
+
continue;
|
|
299
|
+
for (const rule of ruleset.rules.nodes) {
|
|
300
|
+
if (rule.type !== 'REQUIRED_STATUS_CHECKS')
|
|
301
|
+
continue;
|
|
302
|
+
if ('requiredStatusChecks' in rule.parameters) {
|
|
303
|
+
for (const check of rule.parameters.requiredStatusChecks) {
|
|
304
|
+
requiredCheckNamesSet.add(check.context);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
const requiredCheckNames = Array.from(requiredCheckNamesSet);
|
|
310
|
+
const seenContextNames = new Set();
|
|
311
|
+
for (const ctx of contexts) {
|
|
312
|
+
if ('name' in ctx) {
|
|
313
|
+
seenContextNames.add(ctx.name);
|
|
314
|
+
}
|
|
315
|
+
if ('context' in ctx) {
|
|
316
|
+
seenContextNames.add(ctx.context);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
const missingRequiredCheckNames = requiredCheckNames.filter((name) => !seenContextNames.has(name));
|
|
320
|
+
const allRequiredChecksPassed = missingRequiredCheckNames.length === 0;
|
|
321
|
+
const isCiStateSuccess = ciState === 'SUCCESS';
|
|
322
|
+
const isPassedAllCiJob = isCiStateSuccess && allRequiredChecksPassed;
|
|
323
|
+
const reviewThreads = data.reviewThreads?.nodes || [];
|
|
324
|
+
const isResolvedAllReviewComments = reviewThreads.length === 0 ||
|
|
325
|
+
reviewThreads.every((thread) => thread.isResolved);
|
|
326
|
+
return {
|
|
327
|
+
url: prUrl,
|
|
328
|
+
branchName: headRefName ?? null,
|
|
329
|
+
isConflicted,
|
|
330
|
+
isPassedAllCiJob,
|
|
331
|
+
isCiStateSuccess,
|
|
332
|
+
isResolvedAllReviewComments,
|
|
333
|
+
isBranchOutOfDate: false,
|
|
334
|
+
missingRequiredCheckNames,
|
|
335
|
+
};
|
|
336
|
+
};
|
|
314
337
|
this.findRelatedOpenPRs = async (issueUrl) => {
|
|
315
|
-
const
|
|
316
|
-
if (
|
|
317
|
-
|
|
338
|
+
const { owner, repo, issueNumber, isPr } = this.parseIssueUrl(issueUrl);
|
|
339
|
+
if (isPr) {
|
|
340
|
+
throw new Error('findRelatedOpenPRs only supports issue URLs, not pull request URLs');
|
|
341
|
+
}
|
|
342
|
+
const query = `
|
|
343
|
+
query($owner: String!, $repo: String!, $issueNumber: Int!, $after: String) {
|
|
344
|
+
repository(owner: $owner, name: $repo) {
|
|
345
|
+
issue(number: $issueNumber) {
|
|
346
|
+
timelineItems(first: 100, after: $after, itemTypes: [CROSS_REFERENCED_EVENT]) {
|
|
347
|
+
pageInfo {
|
|
348
|
+
endCursor
|
|
349
|
+
hasNextPage
|
|
350
|
+
}
|
|
351
|
+
nodes {
|
|
352
|
+
__typename
|
|
353
|
+
... on CrossReferencedEvent {
|
|
354
|
+
willCloseTarget
|
|
355
|
+
source {
|
|
356
|
+
__typename
|
|
357
|
+
... on PullRequest {
|
|
358
|
+
url
|
|
359
|
+
number
|
|
360
|
+
state
|
|
361
|
+
mergeable
|
|
362
|
+
headRefName
|
|
363
|
+
baseRefName
|
|
364
|
+
baseRepository {
|
|
365
|
+
branchProtectionRules(first: 100) {
|
|
366
|
+
nodes {
|
|
367
|
+
pattern
|
|
368
|
+
requiredStatusCheckContexts
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
defaultBranchRef {
|
|
372
|
+
name
|
|
373
|
+
}
|
|
374
|
+
rulesets(first: 100) {
|
|
375
|
+
nodes {
|
|
376
|
+
name
|
|
377
|
+
enforcement
|
|
378
|
+
conditions {
|
|
379
|
+
refName {
|
|
380
|
+
include
|
|
381
|
+
exclude
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
rules(first: 100) {
|
|
385
|
+
nodes {
|
|
386
|
+
type
|
|
387
|
+
parameters {
|
|
388
|
+
... on RequiredStatusChecksParameters {
|
|
389
|
+
requiredStatusChecks {
|
|
390
|
+
context
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
commits(last: 1) {
|
|
400
|
+
nodes {
|
|
401
|
+
commit {
|
|
402
|
+
statusCheckRollup {
|
|
403
|
+
state
|
|
404
|
+
contexts(first: 100) {
|
|
405
|
+
nodes {
|
|
406
|
+
__typename
|
|
407
|
+
... on CheckRun {
|
|
408
|
+
name
|
|
409
|
+
conclusion
|
|
410
|
+
}
|
|
411
|
+
... on StatusContext {
|
|
412
|
+
context
|
|
413
|
+
state
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
reviewThreads(first: 100) {
|
|
422
|
+
nodes {
|
|
423
|
+
isResolved
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
baseRef {
|
|
427
|
+
name
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
`;
|
|
438
|
+
const relatedPRsMap = new Map();
|
|
439
|
+
let after = null;
|
|
440
|
+
let hasNextPage = true;
|
|
441
|
+
while (hasNextPage) {
|
|
442
|
+
const response = await fetch('https://api.github.com/graphql', {
|
|
443
|
+
method: 'POST',
|
|
444
|
+
headers: {
|
|
445
|
+
Authorization: `Bearer ${this.ghToken}`,
|
|
446
|
+
'Content-Type': 'application/json',
|
|
447
|
+
},
|
|
448
|
+
body: JSON.stringify({
|
|
449
|
+
query,
|
|
450
|
+
variables: { owner, repo, issueNumber, after },
|
|
451
|
+
}),
|
|
452
|
+
});
|
|
453
|
+
if (!response.ok) {
|
|
454
|
+
throw new Error(`Failed to fetch issue timeline from GitHub GraphQL API: HTTP ${response.status}`);
|
|
455
|
+
}
|
|
456
|
+
const responseData = await response.json();
|
|
457
|
+
if (!isIssueTimelineResponse(responseData)) {
|
|
458
|
+
throw new Error('Unexpected response shape when fetching issue timeline');
|
|
459
|
+
}
|
|
460
|
+
const issueData = responseData.data?.repository?.issue;
|
|
461
|
+
if (!issueData) {
|
|
462
|
+
throw new Error('Issue not found when fetching timeline from GitHub GraphQL API');
|
|
463
|
+
}
|
|
464
|
+
for (const item of issueData.timelineItems.nodes) {
|
|
465
|
+
if (item.__typename !== 'CrossReferencedEvent')
|
|
466
|
+
continue;
|
|
467
|
+
if (!item.source || item.source.__typename !== 'PullRequest')
|
|
468
|
+
continue;
|
|
469
|
+
if (item.source.state !== 'OPEN')
|
|
470
|
+
continue;
|
|
471
|
+
if (!item.willCloseTarget)
|
|
472
|
+
continue;
|
|
473
|
+
const pr = item.source;
|
|
474
|
+
const prUrl = pr.url || '';
|
|
475
|
+
const baseRefName = pr.baseRefName ?? pr.baseRef?.name;
|
|
476
|
+
relatedPRsMap.set(prUrl, this.computePrStatus(prUrl, pr.headRefName, baseRefName, pr));
|
|
477
|
+
}
|
|
478
|
+
hasNextPage = issueData.timelineItems.pageInfo.hasNextPage;
|
|
479
|
+
after = issueData.timelineItems.pageInfo.endCursor;
|
|
480
|
+
}
|
|
481
|
+
return Array.from(relatedPRsMap.values());
|
|
482
|
+
};
|
|
483
|
+
this.getAllOpened = async (project) => {
|
|
484
|
+
const { issues } = await this.getAllIssues(project.id, 0);
|
|
485
|
+
return issues.filter((issue) => !issue.isClosed);
|
|
486
|
+
};
|
|
487
|
+
this.getStoryObjectMap = async (project) => {
|
|
488
|
+
const { issues } = await this.getAllIssues(project.id, 0);
|
|
489
|
+
const storyObjectMap = new Map();
|
|
490
|
+
const targetStories = project.story?.stories || [];
|
|
491
|
+
for (const story of targetStories) {
|
|
492
|
+
const storyIssue = issues.find((issue) => story.name.startsWith(issue.title));
|
|
493
|
+
storyObjectMap.set(story.name, {
|
|
494
|
+
story,
|
|
495
|
+
storyIssue: storyIssue || null,
|
|
496
|
+
issues: [],
|
|
497
|
+
});
|
|
498
|
+
for (const issue of issues) {
|
|
499
|
+
if (issue.story !== story.name)
|
|
500
|
+
continue;
|
|
501
|
+
storyObjectMap.get(story.name)?.issues.push(issue);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
return storyObjectMap;
|
|
505
|
+
};
|
|
506
|
+
this.getOpenPullRequest = async (prUrl) => {
|
|
507
|
+
const parsedUrl = this.parseIssueUrl(prUrl);
|
|
508
|
+
if (!parsedUrl.isPr) {
|
|
509
|
+
return null;
|
|
318
510
|
}
|
|
319
|
-
const
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
511
|
+
const { owner, repo, issueNumber: prNumber } = parsedUrl;
|
|
512
|
+
const query = `
|
|
513
|
+
query($owner: String!, $repo: String!, $prNumber: Int!) {
|
|
514
|
+
repository(owner: $owner, name: $repo) {
|
|
515
|
+
pullRequest(number: $prNumber) {
|
|
516
|
+
url
|
|
517
|
+
state
|
|
518
|
+
headRefName
|
|
519
|
+
baseRefName
|
|
520
|
+
mergeable
|
|
521
|
+
baseRepository {
|
|
522
|
+
branchProtectionRules(first: 100) {
|
|
523
|
+
nodes {
|
|
524
|
+
pattern
|
|
525
|
+
requiredStatusCheckContexts
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
defaultBranchRef {
|
|
529
|
+
name
|
|
530
|
+
}
|
|
531
|
+
rulesets(first: 100) {
|
|
532
|
+
nodes {
|
|
533
|
+
name
|
|
534
|
+
enforcement
|
|
535
|
+
conditions {
|
|
536
|
+
refName {
|
|
537
|
+
include
|
|
538
|
+
exclude
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
rules(first: 100) {
|
|
542
|
+
nodes {
|
|
543
|
+
type
|
|
544
|
+
parameters {
|
|
545
|
+
... on RequiredStatusChecksParameters {
|
|
546
|
+
requiredStatusChecks {
|
|
547
|
+
context
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
commits(last: 1) {
|
|
557
|
+
nodes {
|
|
558
|
+
commit {
|
|
559
|
+
statusCheckRollup {
|
|
330
560
|
state
|
|
561
|
+
contexts(first: 100) {
|
|
562
|
+
nodes {
|
|
563
|
+
__typename
|
|
564
|
+
... on CheckRun {
|
|
565
|
+
name
|
|
566
|
+
conclusion
|
|
567
|
+
}
|
|
568
|
+
... on StatusContext {
|
|
569
|
+
context
|
|
570
|
+
state
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
331
574
|
}
|
|
332
575
|
}
|
|
333
576
|
}
|
|
334
577
|
}
|
|
578
|
+
reviewThreads(first: 100) {
|
|
579
|
+
nodes {
|
|
580
|
+
isResolved
|
|
581
|
+
}
|
|
582
|
+
}
|
|
335
583
|
}
|
|
336
584
|
}
|
|
337
585
|
}
|
|
338
|
-
|
|
586
|
+
`;
|
|
339
587
|
const response = await fetch('https://api.github.com/graphql', {
|
|
340
588
|
method: 'POST',
|
|
341
589
|
headers: {
|
|
@@ -344,37 +592,24 @@ class ApiV3CheerioRestIssueRepository extends BaseGitHubRepository_1.BaseGitHubR
|
|
|
344
592
|
},
|
|
345
593
|
body: JSON.stringify({
|
|
346
594
|
query,
|
|
347
|
-
variables: { owner, repo,
|
|
595
|
+
variables: { owner, repo, prNumber },
|
|
348
596
|
}),
|
|
349
597
|
});
|
|
598
|
+
if (!response.ok) {
|
|
599
|
+
throw new Error(`Failed to fetch pull request from GitHub GraphQL API: HTTP ${response.status}`);
|
|
600
|
+
}
|
|
350
601
|
const responseData = await response.json();
|
|
351
|
-
if (!
|
|
352
|
-
throw new Error('Unexpected response shape when fetching
|
|
602
|
+
if (!isDirectPullRequestResponse(responseData)) {
|
|
603
|
+
throw new Error('Unexpected response shape when fetching pull request');
|
|
353
604
|
}
|
|
354
605
|
if (responseData.errors && responseData.errors.length > 0) {
|
|
355
|
-
throw new Error(
|
|
606
|
+
throw new Error(`GraphQL errors: ${JSON.stringify(responseData.errors)}`);
|
|
356
607
|
}
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
node.source?.state === 'OPEN' &&
|
|
361
|
-
node.source.url.includes('/pull/'))
|
|
362
|
-
.map((node) => node.source?.url)
|
|
363
|
-
.filter((url) => url !== undefined);
|
|
364
|
-
const results = [];
|
|
365
|
-
for (const prUrl of openPrUrls) {
|
|
366
|
-
const pr = await this.getOpenPullRequest(prUrl);
|
|
367
|
-
if (pr) {
|
|
368
|
-
results.push(pr);
|
|
369
|
-
}
|
|
608
|
+
const pr = responseData.data?.repository?.pullRequest;
|
|
609
|
+
if (!pr || pr.state !== 'OPEN') {
|
|
610
|
+
return null;
|
|
370
611
|
}
|
|
371
|
-
return
|
|
372
|
-
};
|
|
373
|
-
this.getAllOpened = async (_project) => {
|
|
374
|
-
throw new Error('getAllOpened is not implemented');
|
|
375
|
-
};
|
|
376
|
-
this.getStoryObjectMap = async (_project) => {
|
|
377
|
-
throw new Error('getStoryObjectMap is not implemented');
|
|
612
|
+
return this.computePrStatus(pr.url, pr.headRefName, pr.baseRefName, pr);
|
|
378
613
|
};
|
|
379
614
|
}
|
|
380
615
|
}
|