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.
Files changed (52) hide show
  1. package/.github/workflows/umino-project.yml +5 -4
  2. package/CHANGELOG.md +13 -0
  3. package/README.md +21 -9
  4. package/bin/adapter/entry-points/cli/index.js +45 -10
  5. package/bin/adapter/entry-points/cli/index.js.map +1 -1
  6. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +32 -8
  7. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
  8. package/bin/adapter/repositories/NodeLocalCommandRunner.js +3 -3
  9. package/bin/adapter/repositories/NodeLocalCommandRunner.js.map +1 -1
  10. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +412 -177
  11. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
  12. package/bin/domain/usecases/HandleScheduledEventUseCase.js +5 -2
  13. package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
  14. package/bin/domain/usecases/RevertOrphanedPreparationUseCase.js +7 -2
  15. package/bin/domain/usecases/RevertOrphanedPreparationUseCase.js.map +1 -1
  16. package/bin/domain/usecases/StartPreparationUseCase.js +107 -72
  17. package/bin/domain/usecases/StartPreparationUseCase.js.map +1 -1
  18. package/package.json +1 -1
  19. package/src/adapter/entry-points/cli/index.test.ts +26 -13
  20. package/src/adapter/entry-points/cli/index.ts +74 -13
  21. package/src/adapter/repositories/NodeLocalCommandRunner.test.ts +12 -12
  22. package/src/adapter/repositories/NodeLocalCommandRunner.ts +7 -4
  23. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.test.ts +3 -0
  24. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +626 -265
  25. package/src/adapter/repositories/issue/RestIssueRepository.test.ts +3 -0
  26. package/src/domain/entities/Issue.ts +1 -0
  27. package/src/domain/usecases/GetStoryObjectMapUseCase.test.ts +1 -0
  28. package/src/domain/usecases/HandleScheduledEventUseCase.ts +11 -3
  29. package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +1 -0
  30. package/src/domain/usecases/RevertOrphanedPreparationUseCase.test.ts +22 -9
  31. package/src/domain/usecases/RevertOrphanedPreparationUseCase.ts +8 -3
  32. package/src/domain/usecases/StartPreparationUseCase.test.ts +1696 -290
  33. package/src/domain/usecases/StartPreparationUseCase.ts +171 -126
  34. package/src/domain/usecases/adapter-interfaces/IssueRepository.ts +2 -1
  35. package/src/domain/usecases/adapter-interfaces/LocalCommandRunner.ts +4 -1
  36. package/types/adapter/entry-points/cli/index.d.ts +4 -1
  37. package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
  38. package/types/adapter/repositories/NodeLocalCommandRunner.d.ts +1 -1
  39. package/types/adapter/repositories/NodeLocalCommandRunner.d.ts.map +1 -1
  40. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +5 -3
  41. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
  42. package/types/domain/entities/Issue.d.ts +1 -0
  43. package/types/domain/entities/Issue.d.ts.map +1 -1
  44. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +5 -1
  45. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
  46. package/types/domain/usecases/RevertOrphanedPreparationUseCase.d.ts.map +1 -1
  47. package/types/domain/usecases/StartPreparationUseCase.d.ts +10 -18
  48. package/types/domain/usecases/StartPreparationUseCase.d.ts.map +1 -1
  49. package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts +2 -1
  50. package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts.map +1 -1
  51. package/types/domain/usecases/adapter-interfaces/LocalCommandRunner.d.ts +1 -1
  52. 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 isGetPullRequestResponse(value) {
11
- return (() => { const _io0 = input => (undefined === input.data || "object" === typeof input.data && null !== input.data && false === Array.isArray(input.data) && _io1(input.data)) && (undefined === input.errors || Array.isArray(input.errors) && input.errors.every(elem => "object" === typeof elem && null !== elem && _io22(elem))); const _io1 = input => undefined === input.repository || "object" === typeof input.repository && null !== input.repository && false === Array.isArray(input.repository) && _io2(input.repository); const _io2 = input => undefined === input.pullRequest || "object" === typeof input.pullRequest && null !== input.pullRequest && _io3(input.pullRequest); const _io3 = input => "string" === typeof input.state && "string" === typeof input.mergeable && ("object" === typeof input.commits && null !== input.commits && _io4(input.commits)) && ("object" === typeof input.reviewThreads && null !== input.reviewThreads && _io11(input.reviewThreads)) && ("object" === typeof input.baseRepository && null !== input.baseRepository && _io13(input.baseRepository)); const _io4 = input => Array.isArray(input.nodes) && input.nodes.every(elem => "object" === typeof elem && null !== elem && _io5(elem)); const _io5 = input => "object" === typeof input.commit && null !== input.commit && _io6(input.commit); const _io6 = input => null === input.statusCheckRollup || "object" === typeof input.statusCheckRollup && null !== input.statusCheckRollup && _io7(input.statusCheckRollup); const _io7 = input => "string" === typeof input.state && ("object" === typeof input.contexts && null !== input.contexts && _io8(input.contexts)); const _io8 = input => Array.isArray(input.nodes) && input.nodes.every(elem => "object" === typeof elem && null !== elem && false === Array.isArray(elem) && _iu0(elem)); const _io9 = input => (undefined === input.name || "string" === typeof input.name) && (undefined === input.status || "string" === typeof input.status) && (null === input.conclusion || undefined === input.conclusion || "string" === typeof input.conclusion); const _io10 = input => (undefined === input.context || "string" === typeof input.context) && (undefined === input.state || "string" === typeof input.state); const _io11 = input => Array.isArray(input.nodes) && input.nodes.every(elem => "object" === typeof elem && null !== elem && _io12(elem)); const _io12 = input => "boolean" === typeof input.isResolved; const _io13 = input => "object" === typeof input.branchProtectionRules && null !== input.branchProtectionRules && _io14(input.branchProtectionRules) && ("object" === typeof input.rulesets && null !== input.rulesets && _io16(input.rulesets)); const _io14 = input => Array.isArray(input.nodes) && input.nodes.every(elem => "object" === typeof elem && null !== elem && _io15(elem)); const _io15 = input => Array.isArray(input.requiredStatusCheckContexts) && input.requiredStatusCheckContexts.every(elem => "string" === typeof elem); const _io16 = input => Array.isArray(input.nodes) && input.nodes.every(elem => "object" === typeof elem && null !== elem && _io17(elem)); const _io17 = input => "object" === typeof input.rules && null !== input.rules && _io18(input.rules); const _io18 = input => Array.isArray(input.nodes) && input.nodes.every(elem => "object" === typeof elem && null !== elem && _io19(elem)); const _io19 = input => "string" === typeof input.type && (undefined === input.parameters || "object" === typeof input.parameters && null !== input.parameters && false === Array.isArray(input.parameters) && _io20(input.parameters)); const _io20 = input => undefined === input.requiredStatusChecks || Array.isArray(input.requiredStatusChecks) && input.requiredStatusChecks.every(elem => "object" === typeof elem && null !== elem && _io21(elem)); const _io21 = input => "string" === typeof input.context; const _io22 = input => "string" === typeof input.message; const _iu0 = input => (() => {
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
- })(); return input => "object" === typeof input && null !== input && false === Array.isArray(input) && _io0(input); })()(value);
13
+ return true;
18
14
  }
19
- function isFindRelatedPRsResponse(value) {
20
- return (() => { const _io0 = input => (undefined === input.data || "object" === typeof input.data && null !== input.data && false === Array.isArray(input.data) && _io1(input.data)) && (undefined === input.errors || Array.isArray(input.errors) && input.errors.every(elem => "object" === typeof elem && null !== elem && _io7(elem))); const _io1 = input => undefined === input.repository || "object" === typeof input.repository && null !== input.repository && false === Array.isArray(input.repository) && _io2(input.repository); const _io2 = input => undefined === input.issue || "object" === typeof input.issue && null !== input.issue && _io3(input.issue); const _io3 = input => "object" === typeof input.timelineItems && null !== input.timelineItems && _io4(input.timelineItems); const _io4 = input => Array.isArray(input.nodes) && input.nodes.every(elem => "object" === typeof elem && null !== elem && false === Array.isArray(elem) && _io5(elem)); const _io5 = input => undefined === input.source || "object" === typeof input.source && null !== input.source && false === Array.isArray(input.source) && _io6(input.source); const _io6 = input => (undefined === input.url || "string" === typeof input.url) && (undefined === input.state || "string" === typeof input.state); const _io7 = input => "string" === typeof input.message; return input => "object" === typeof input && null !== input && false === Array.isArray(input) && _io0(input); })()(value);
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 match = issueUrl.match(/https:\/\/github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)/);
316
- if (!match) {
317
- return [];
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 [, owner, repo, issueNumberStr] = match;
320
- const issueNumber = parseInt(issueNumberStr, 10);
321
- const query = `query FindRelatedPRs($owner: String!, $repo: String!, $number: Int!) {
322
- repository(owner: $owner, name: $repo) {
323
- issue(number: $number) {
324
- timelineItems(itemTypes: [CROSS_REFERENCED_EVENT], first: 50) {
325
- nodes {
326
- ... on CrossReferencedEvent {
327
- source {
328
- ... on PullRequest {
329
- url
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, number: issueNumber },
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 (!isFindRelatedPRsResponse(responseData)) {
352
- throw new Error('Unexpected response shape when fetching related PRs from GitHub GraphQL API');
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(responseData.errors.map((e) => e.message).join('\n'));
606
+ throw new Error(`GraphQL errors: ${JSON.stringify(responseData.errors)}`);
356
607
  }
357
- const nodes = responseData.data?.repository?.issue?.timelineItems?.nodes ?? [];
358
- const openPrUrls = nodes
359
- .filter((node) => node.source?.url &&
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 results;
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
  }