github-issue-tower-defence-management 1.1.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 (221) hide show
  1. package/.env.example +6 -0
  2. package/.eslintrc.cjs +55 -0
  3. package/.github/CODEOWNERS +2 -0
  4. package/.github/workflows/assign-all-cards-to-owner.yml +14 -0
  5. package/.github/workflows/commit-lint.yml +54 -0
  6. package/.github/workflows/configs/commitlint.config.js +27 -0
  7. package/.github/workflows/create-pr.yml +64 -0
  8. package/.github/workflows/format.yml +25 -0
  9. package/.github/workflows/publish.yml +47 -0
  10. package/.github/workflows/test.yml +45 -0
  11. package/.github/workflows/umino-project.yml +181 -0
  12. package/.prettierignore +22 -0
  13. package/.prettierrc +5 -0
  14. package/CHANGELOG.md +49 -0
  15. package/CONTRIBUTING.md +107 -0
  16. package/README.md +108 -0
  17. package/bin/adapter/entry-points/cli/index.js +26 -0
  18. package/bin/adapter/entry-points/cli/index.js.map +1 -0
  19. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +142 -0
  20. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -0
  21. package/bin/adapter/repositories/AxiosSlackRepository.js +124 -0
  22. package/bin/adapter/repositories/AxiosSlackRepository.js.map +1 -0
  23. package/bin/adapter/repositories/BaseGitHubRepository.js +136 -0
  24. package/bin/adapter/repositories/BaseGitHubRepository.js.map +1 -0
  25. package/bin/adapter/repositories/GoogleSpreadsheetRepository.js +123 -0
  26. package/bin/adapter/repositories/GoogleSpreadsheetRepository.js.map +1 -0
  27. package/bin/adapter/repositories/GraphqlProjectRepository.js +167 -0
  28. package/bin/adapter/repositories/GraphqlProjectRepository.js.map +1 -0
  29. package/bin/adapter/repositories/LocalStorageCacheRepository.js +46 -0
  30. package/bin/adapter/repositories/LocalStorageCacheRepository.js.map +1 -0
  31. package/bin/adapter/repositories/LocalStorageRepository.js +30 -0
  32. package/bin/adapter/repositories/LocalStorageRepository.js.map +1 -0
  33. package/bin/adapter/repositories/SystemDateRepository.js +23 -0
  34. package/bin/adapter/repositories/SystemDateRepository.js.map +1 -0
  35. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +148 -0
  36. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -0
  37. package/bin/adapter/repositories/issue/ApiV3IssueRepository.js +48 -0
  38. package/bin/adapter/repositories/issue/ApiV3IssueRepository.js.map +1 -0
  39. package/bin/adapter/repositories/issue/CheerioIssueRepository.js +120 -0
  40. package/bin/adapter/repositories/issue/CheerioIssueRepository.js.map +1 -0
  41. package/bin/adapter/repositories/issue/GraphqlProjectItemRepository.js +485 -0
  42. package/bin/adapter/repositories/issue/GraphqlProjectItemRepository.js.map +1 -0
  43. package/bin/adapter/repositories/issue/InternalGraphqlIssueRepository.js +114 -0
  44. package/bin/adapter/repositories/issue/InternalGraphqlIssueRepository.js.map +1 -0
  45. package/bin/adapter/repositories/issue/RestIssueRepository.js +79 -0
  46. package/bin/adapter/repositories/issue/RestIssueRepository.js.map +1 -0
  47. package/bin/adapter/repositories/issue/issueTimelineUtils.js +38 -0
  48. package/bin/adapter/repositories/issue/issueTimelineUtils.js.map +1 -0
  49. package/bin/adapter/repositories/utils.js +45 -0
  50. package/bin/adapter/repositories/utils.js.map +1 -0
  51. package/bin/domain/entities/Issue.js +3 -0
  52. package/bin/domain/entities/Issue.js.map +1 -0
  53. package/bin/domain/entities/Member.js +3 -0
  54. package/bin/domain/entities/Member.js.map +1 -0
  55. package/bin/domain/entities/Project.js +3 -0
  56. package/bin/domain/entities/Project.js.map +1 -0
  57. package/bin/domain/entities/ProjectField.js +3 -0
  58. package/bin/domain/entities/ProjectField.js.map +1 -0
  59. package/bin/domain/entities/ProjectFieldSingleSelect.js +3 -0
  60. package/bin/domain/entities/ProjectFieldSingleSelect.js.map +1 -0
  61. package/bin/domain/entities/ProjectFieldSingleSelectOption.js +3 -0
  62. package/bin/domain/entities/ProjectFieldSingleSelectOption.js.map +1 -0
  63. package/bin/domain/entities/WorkingTime.js +3 -0
  64. package/bin/domain/entities/WorkingTime.js.map +1 -0
  65. package/bin/domain/usecases/ActionAnnouncementUseCase.js +46 -0
  66. package/bin/domain/usecases/ActionAnnouncementUseCase.js.map +1 -0
  67. package/bin/domain/usecases/AnalyzeProblemByIssueUseCase.js +116 -0
  68. package/bin/domain/usecases/AnalyzeProblemByIssueUseCase.js.map +1 -0
  69. package/bin/domain/usecases/ClearNextActionHourUseCase.js +38 -0
  70. package/bin/domain/usecases/ClearNextActionHourUseCase.js.map +1 -0
  71. package/bin/domain/usecases/GenerateWorkingTimeReportUseCase.js +180 -0
  72. package/bin/domain/usecases/GenerateWorkingTimeReportUseCase.js.map +1 -0
  73. package/bin/domain/usecases/HandleScheduledEventUseCase.js +122 -0
  74. package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -0
  75. package/bin/domain/usecases/SetWorkflowManagementIssueToStoryUseCase.js +35 -0
  76. package/bin/domain/usecases/SetWorkflowManagementIssueToStoryUseCase.js.map +1 -0
  77. package/bin/domain/usecases/adapter-interfaces/DateRepository.js +3 -0
  78. package/bin/domain/usecases/adapter-interfaces/DateRepository.js.map +1 -0
  79. package/bin/domain/usecases/adapter-interfaces/IssueRepository.js +3 -0
  80. package/bin/domain/usecases/adapter-interfaces/IssueRepository.js.map +1 -0
  81. package/bin/domain/usecases/adapter-interfaces/ProjectRepository.js +3 -0
  82. package/bin/domain/usecases/adapter-interfaces/ProjectRepository.js.map +1 -0
  83. package/bin/domain/usecases/adapter-interfaces/SlackRepository.js +3 -0
  84. package/bin/domain/usecases/adapter-interfaces/SlackRepository.js.map +1 -0
  85. package/bin/domain/usecases/adapter-interfaces/SpreadsheetRepository.js +3 -0
  86. package/bin/domain/usecases/adapter-interfaces/SpreadsheetRepository.js.map +1 -0
  87. package/bin/index.js +13 -0
  88. package/bin/index.js.map +1 -0
  89. package/commitlint.config.js +6 -0
  90. package/jest.config.js +19 -0
  91. package/package.json +80 -0
  92. package/renovate.json +37 -0
  93. package/src/adapter/entry-points/cli/index.test.ts +20 -0
  94. package/src/adapter/entry-points/cli/index.ts +36 -0
  95. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +95 -0
  96. package/src/adapter/repositories/AxiosSlackRepository.test.ts +119 -0
  97. package/src/adapter/repositories/AxiosSlackRepository.ts +184 -0
  98. package/src/adapter/repositories/BaseGitHubRepository.test.ts +95 -0
  99. package/src/adapter/repositories/BaseGitHubRepository.ts +172 -0
  100. package/src/adapter/repositories/GoogleSpreadsheetRepository.test.ts +124 -0
  101. package/src/adapter/repositories/GoogleSpreadsheetRepository.ts +151 -0
  102. package/src/adapter/repositories/GraphqlProjectRepository.test.ts +46 -0
  103. package/src/adapter/repositories/GraphqlProjectRepository.ts +236 -0
  104. package/src/adapter/repositories/LocalStorageCacheRepository.test.ts +146 -0
  105. package/src/adapter/repositories/LocalStorageCacheRepository.ts +53 -0
  106. package/src/adapter/repositories/LocalStorageRepository.integration.test.ts +142 -0
  107. package/src/adapter/repositories/LocalStorageRepository.test.ts +161 -0
  108. package/src/adapter/repositories/LocalStorageRepository.ts +21 -0
  109. package/src/adapter/repositories/SystemDateRepository.ts +20 -0
  110. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.test.ts +158 -0
  111. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +274 -0
  112. package/src/adapter/repositories/issue/ApiV3IssueRepository.test.ts +26 -0
  113. package/src/adapter/repositories/issue/ApiV3IssueRepository.ts +59 -0
  114. package/src/adapter/repositories/issue/CheerioIssueRepository.test.ts +6610 -0
  115. package/src/adapter/repositories/issue/CheerioIssueRepository.ts +127 -0
  116. package/src/adapter/repositories/issue/GraphqlProjectItemRepository.test.ts +49 -0
  117. package/src/adapter/repositories/issue/GraphqlProjectItemRepository.ts +745 -0
  118. package/src/adapter/repositories/issue/InternalGraphqlIssueRepository.test.ts +71 -0
  119. package/src/adapter/repositories/issue/InternalGraphqlIssueRepository.ts +263 -0
  120. package/src/adapter/repositories/issue/RestIssueRepository.test.ts +29 -0
  121. package/src/adapter/repositories/issue/RestIssueRepository.ts +105 -0
  122. package/src/adapter/repositories/issue/issueTimelineUtils.test.ts +79 -0
  123. package/src/adapter/repositories/issue/issueTimelineUtils.ts +52 -0
  124. package/src/adapter/repositories/utils.test.ts +40 -0
  125. package/src/adapter/repositories/utils.ts +50 -0
  126. package/src/domain/entities/Issue.ts +23 -0
  127. package/src/domain/entities/Member.ts +3 -0
  128. package/src/domain/entities/Project.ts +29 -0
  129. package/src/domain/entities/ProjectField.ts +3 -0
  130. package/src/domain/entities/ProjectFieldSingleSelect.ts +8 -0
  131. package/src/domain/entities/ProjectFieldSingleSelectOption.ts +8 -0
  132. package/src/domain/entities/WorkingTime.ts +8 -0
  133. package/src/domain/usecases/ActionAnnouncementUseCase.ts +76 -0
  134. package/src/domain/usecases/AnalyzeProblemByIssueUseCase.ts +209 -0
  135. package/src/domain/usecases/ClearNextActionHourUseCase.ts +51 -0
  136. package/src/domain/usecases/GenerateWorkingTimeReportUseCase.test.ts +382 -0
  137. package/src/domain/usecases/GenerateWorkingTimeReportUseCase.ts +284 -0
  138. package/src/domain/usecases/HandleScheduledEventUseCase.test.ts +58 -0
  139. package/src/domain/usecases/HandleScheduledEventUseCase.ts +209 -0
  140. package/src/domain/usecases/SetWorkflowManagementIssueToStoryUseCase.ts +46 -0
  141. package/src/domain/usecases/adapter-interfaces/DateRepository.ts +5 -0
  142. package/src/domain/usecases/adapter-interfaces/IssueRepository.ts +44 -0
  143. package/src/domain/usecases/adapter-interfaces/ProjectRepository.ts +6 -0
  144. package/src/domain/usecases/adapter-interfaces/SlackRepository.ts +20 -0
  145. package/src/domain/usecases/adapter-interfaces/SpreadsheetRepository.ts +18 -0
  146. package/src/index.test.ts +8 -0
  147. package/src/index.ts +7 -0
  148. package/tsconfig.build.json +11 -0
  149. package/tsconfig.json +22 -0
  150. package/types/adapter/entry-points/cli/index.d.ts +3 -0
  151. package/types/adapter/entry-points/cli/index.d.ts.map +1 -0
  152. package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts +11 -0
  153. package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -0
  154. package/types/adapter/repositories/AxiosSlackRepository.d.ts +13 -0
  155. package/types/adapter/repositories/AxiosSlackRepository.d.ts.map +1 -0
  156. package/types/adapter/repositories/BaseGitHubRepository.d.ts +32 -0
  157. package/types/adapter/repositories/BaseGitHubRepository.d.ts.map +1 -0
  158. package/types/adapter/repositories/GoogleSpreadsheetRepository.d.ts +13 -0
  159. package/types/adapter/repositories/GoogleSpreadsheetRepository.d.ts.map +1 -0
  160. package/types/adapter/repositories/GraphqlProjectRepository.d.ts +13 -0
  161. package/types/adapter/repositories/GraphqlProjectRepository.d.ts.map +1 -0
  162. package/types/adapter/repositories/LocalStorageCacheRepository.d.ts +12 -0
  163. package/types/adapter/repositories/LocalStorageCacheRepository.d.ts.map +1 -0
  164. package/types/adapter/repositories/LocalStorageRepository.d.ts +7 -0
  165. package/types/adapter/repositories/LocalStorageRepository.d.ts.map +1 -0
  166. package/types/adapter/repositories/SystemDateRepository.d.ts +7 -0
  167. package/types/adapter/repositories/SystemDateRepository.d.ts.map +1 -0
  168. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +38 -0
  169. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -0
  170. package/types/adapter/repositories/issue/ApiV3IssueRepository.d.ts +17 -0
  171. package/types/adapter/repositories/issue/ApiV3IssueRepository.d.ts.map +1 -0
  172. package/types/adapter/repositories/issue/CheerioIssueRepository.d.ts +31 -0
  173. package/types/adapter/repositories/issue/CheerioIssueRepository.d.ts.map +1 -0
  174. package/types/adapter/repositories/issue/GraphqlProjectItemRepository.d.ts +39 -0
  175. package/types/adapter/repositories/issue/GraphqlProjectItemRepository.d.ts.map +1 -0
  176. package/types/adapter/repositories/issue/InternalGraphqlIssueRepository.d.ts +83 -0
  177. package/types/adapter/repositories/issue/InternalGraphqlIssueRepository.d.ts.map +1 -0
  178. package/types/adapter/repositories/issue/RestIssueRepository.d.ts +16 -0
  179. package/types/adapter/repositories/issue/RestIssueRepository.d.ts.map +1 -0
  180. package/types/adapter/repositories/issue/issueTimelineUtils.d.ts +12 -0
  181. package/types/adapter/repositories/issue/issueTimelineUtils.d.ts.map +1 -0
  182. package/types/adapter/repositories/utils.d.ts +4 -0
  183. package/types/adapter/repositories/utils.d.ts.map +1 -0
  184. package/types/domain/entities/Issue.d.ts +24 -0
  185. package/types/domain/entities/Issue.d.ts.map +1 -0
  186. package/types/domain/entities/Member.d.ts +4 -0
  187. package/types/domain/entities/Member.d.ts.map +1 -0
  188. package/types/domain/entities/Project.d.ts +29 -0
  189. package/types/domain/entities/Project.d.ts.map +1 -0
  190. package/types/domain/entities/ProjectField.d.ts +3 -0
  191. package/types/domain/entities/ProjectField.d.ts.map +1 -0
  192. package/types/domain/entities/ProjectFieldSingleSelect.d.ts +8 -0
  193. package/types/domain/entities/ProjectFieldSingleSelect.d.ts.map +1 -0
  194. package/types/domain/entities/ProjectFieldSingleSelectOption.d.ts +8 -0
  195. package/types/domain/entities/ProjectFieldSingleSelectOption.d.ts.map +1 -0
  196. package/types/domain/entities/WorkingTime.d.ts +8 -0
  197. package/types/domain/entities/WorkingTime.d.ts.map +1 -0
  198. package/types/domain/usecases/ActionAnnouncementUseCase.d.ts +17 -0
  199. package/types/domain/usecases/ActionAnnouncementUseCase.d.ts.map +1 -0
  200. package/types/domain/usecases/AnalyzeProblemByIssueUseCase.d.ts +26 -0
  201. package/types/domain/usecases/AnalyzeProblemByIssueUseCase.d.ts.map +1 -0
  202. package/types/domain/usecases/ClearNextActionHourUseCase.d.ts +14 -0
  203. package/types/domain/usecases/ClearNextActionHourUseCase.d.ts.map +1 -0
  204. package/types/domain/usecases/GenerateWorkingTimeReportUseCase.d.ts +50 -0
  205. package/types/domain/usecases/GenerateWorkingTimeReportUseCase.d.ts.map +1 -0
  206. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +63 -0
  207. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -0
  208. package/types/domain/usecases/SetWorkflowManagementIssueToStoryUseCase.d.ts +14 -0
  209. package/types/domain/usecases/SetWorkflowManagementIssueToStoryUseCase.d.ts.map +1 -0
  210. package/types/domain/usecases/adapter-interfaces/DateRepository.d.ts +6 -0
  211. package/types/domain/usecases/adapter-interfaces/DateRepository.d.ts.map +1 -0
  212. package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts +23 -0
  213. package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts.map +1 -0
  214. package/types/domain/usecases/adapter-interfaces/ProjectRepository.d.ts +6 -0
  215. package/types/domain/usecases/adapter-interfaces/ProjectRepository.d.ts.map +1 -0
  216. package/types/domain/usecases/adapter-interfaces/SlackRepository.d.ts +9 -0
  217. package/types/domain/usecases/adapter-interfaces/SlackRepository.d.ts.map +1 -0
  218. package/types/domain/usecases/adapter-interfaces/SpreadsheetRepository.d.ts +6 -0
  219. package/types/domain/usecases/adapter-interfaces/SpreadsheetRepository.d.ts.map +1 -0
  220. package/types/index.d.ts +10 -0
  221. package/types/index.d.ts.map +1 -0
@@ -0,0 +1,71 @@
1
+ import { InternalGraphqlIssueRepository } from './InternalGraphqlIssueRepository';
2
+
3
+ describe('InternalGraphqlIssueRepository', () => {
4
+ jest.setTimeout(30 * 1000);
5
+ const repository = new InternalGraphqlIssueRepository();
6
+
7
+ const testIssueUrl =
8
+ 'https://github.com/HiromiShikata/test-repository/issues/38';
9
+ const testCursor = null;
10
+ const testIssueId = 'I_kwDOCNXcUc6GaFia';
11
+ const testCount = 10;
12
+
13
+ test('getFrontTimelineItems returns timeline with proper types', async () => {
14
+ const result = await repository.getFrontTimelineItems(
15
+ testIssueUrl,
16
+ testCursor,
17
+ testIssueId,
18
+ testCount,
19
+ );
20
+ expect(result).toBeDefined();
21
+ });
22
+
23
+ test('getIssueFromBetaFeatureView returns typed Issue object', async () => {
24
+ const htmlContent = `
25
+ <script type="application/json" data-target="react-app.embeddedData">{"payload":{"preloaded_records":{},"preloadedQueries":[{"queryId":"52bc607938179bfb6b15285c7d68285c","queryName":"IssueViewerViewQuery","variables":{"id":"repository","number":38,"owner":"HiromiShikata","repo":"test-repository","useNewTimeline":true},"result":{"data":{"repository":{"isOwnerEnterpriseManaged":false,"issue":{"id":"I_kwDOCNXcUc6GaFia","updatedAt":"2024-11-23T00:01:07Z","resourcePath":"/HiromiShikata/test-repository/issues/38","canBeSummarized":false,"subIssuesConnection":{"totalCount":0},"viewerCanUpdateMetadata":true,"repository":{"isArchived":false,"id":"R_kgDOHusmCw","nameWithOwner":"HiromiShikata/test-repository","name":"test-repository","owner":{"__typename":"Organization","login":"HiromiShikata","id":"MDEyOk9yZ2FuaXphdGlvbjc1MjM2NTUy","url":"https://github.com/HiromiShikata"},"isPrivate":true,"databaseId":518727179,"slashCommandsEnabled":true,"viewerCanInteract":true,"viewerInteractionLimitReasonHTML":"","planFeatures":{"maximumAssignees":10},"pinnedIssues":{"totalCount":0},"viewerCanPinIssues":true,"issueTypes":{"edges":[{"node":{"id":"IT_kwDOBHwEyM4A0REA"}},{"node":{"id":"IT_kwDOBHwEyM4A0REC"}},{"node":{"id":"IT_kwDOBHwEyM4A0REF"}}]}},"title":"Test title","number":38,"titleHTML":"Test title","url":"https://github.com/HiromiShikata/test-repository/issues/38","viewerCanUpdateNext":true,"issueType":null,"state":"OPEN","stateReason":null,"duplicateOf":null,"linkedPullRequests":{"nodes":[]},"subIssuesSummary":{"completed":0},"databaseId":2685000461,"viewerDidAuthor":true,"locked":false,"author":{"__typename":"User","__isActor":"User","login":"HiromiShikata","id":"MDQ6VXNlcjY0NDA4MTE=","avatarUrl":"https://avatars.githubusercontent.com/u/6440811?v=4","profileUrl":"https://github.com/HiromiShikata"},"__isComment":"Issue","body":"Test Body","bodyHTML":"Test Body","bodyVersion":"804513af29eabc52a58f62e24db7fe7f8e3a8179d770138f3cba77fb3fa2b77d","createdAt":"2024-11-23T00:01:07Z","__isReactable":"Issue","reactionGroups":[{"content":"THUMBS_UP","viewerHasReacted":false,"reactors":{"totalCount":0,"nodes":[]}},{"content":"THUMBS_DOWN","viewerHasReacted":false,"reactors":{"totalCount":0,"nodes":[]}},{"content":"LAUGH","viewerHasReacted":false,"reactors":{"totalCount":0,"nodes":[]}},{"content":"HOORAY","viewerHasReacted":false,"reactors":{"totalCount":0,"nodes":[]}},{"content":"CONFUSED","viewerHasReacted":false,"reactors":{"totalCount":0,"nodes":[]}},{"content":"HEART","viewerHasReacted":false,"reactors":{"totalCount":0,"nodes":[]}},{"content":"ROCKET","viewerHasReacted":false,"reactors":{"totalCount":0,"nodes":[]}},{"content":"EYES","viewerHasReacted":false,"reactors":{"totalCount":0,"nodes":[]}}],"parent":null,"viewerCanComment":true,"assignees":{"nodes":[{"id":"MDQ6VXNlcjY0NDA4MTE=","login":"HiromiShikata","name":"Hiromi.s","avatarUrl":"https://avatars.githubusercontent.com/u/6440811?s=64\u0026v=4"}]},"viewerCanAssign":true,"__isLabelable":"Issue","labels":{"edges":[{"node":{"id":"LA_kwDOHusmC88AAAABdzKnjA","color":"0E8A16","name":"daily-routine","nameHTML":"daily-routine","description":"","url":"https://github.com/HiromiShikata/test-repository/labels/daily-routine","__typename":"Label"},"cursor":"MQ"}],"pageInfo":{"endCursor":"MQ","hasNextPage":false}},"__isNode":"Issue","viewerCanLabel":true,"__isIssueOrPullRequest":"Issue","projectItemsNext":{"edges":[{"node":{"id":"PVTI_lADOBHwEyM4AEU1gzgVEPno","isArchived":false,"project":{"id":"PVT_kwDOBHwEyM4AEU1g","title":"Test v2 project","template":false,"viewerCanUpdate":true,"url":"https://github.com/orgs/HiromiShikata/projects/3","field":{"__typename":"ProjectV2SingleSelectField","id":"PVTSSF_lADOBHwEyM4AEU1gzgCfdy0","name":"Status","options":[{"id":"b18af583","optionId":"b18af583","name":"📋 Backlog","nameHTML":"📋 Backlog","color":"GRAY","descriptionHTML":"","description":""},{"id":"8bc53e92","optionId":"8bc53e92","name":"Unread","nameHTML":"Unread","color":"GRAY","descriptionHTML":"","description":""},{"id":"e36ea728","optionId":"e36ea728","name":"To do","nameHTML":"To do","color":"GRAY","descriptionHTML":"","description":""},{"id":"7e7e1cd1","optionId":"7e7e1cd1","name":"In progress","nameHTML":"In progress","color":"GRAY","descriptionHTML":"","description":""},{"id":"2c35ad03","optionId":"2c35ad03","name":"👀 Review","nameHTML":"👀 Review","color":"GRAY","descriptionHTML":"","description":""},{"id":"b96e0f5d","optionId":"b96e0f5d","name":"✅ Done","nameHTML":"✅ Done","color":"GRAY","descriptionHTML":"","description":""}],"__isNode":"ProjectV2SingleSelectField"},"closed":false,"number":3,"__typename":"ProjectV2"},"fieldValueByName":{"__typename":"ProjectV2ItemFieldSingleSelectValue","id":"PVTFSV_lQDOBHwEyM4AEU1gzgVEPnrOD0n-xA","optionId":"8bc53e92","name":"Unread","nameHTML":"Unread","color":"GRAY","__isNode":"ProjectV2ItemFieldSingleSelectValue"},"__typename":"ProjectV2Item"},"cursor":"MQ"}],"pageInfo":{"endCursor":"MQ","hasNextPage":false}},"milestone":null,"viewerCanSetMilestone":true,"isPinned":false,"viewerCanDelete":true,"viewerCanTransfer":true,"viewerCanConvertToDiscussion":true,"viewerCanLock":true,"viewerCanType":true,"subIssues":{"nodes":[]},"frontTimelineItems":{"pageInfo":{"hasNextPage":false,"endCursor":"Y3Vyc29yOnYyOpPPAAABk1em0rgBqzE1NDA3MDEwNDk4"},"totalCount":6,"edges":[{"node":{"__typename":"LabeledEvent","__isIssueTimelineItems":"LabeledEvent","__isTimelineEvent":"LabeledEvent","databaseId":15405435198,"createdAt":"2024-11-23T00:01:07Z","actor":{"__typename":"User","login":"HiromiShikata","id":"MDQ6VXNlcjY0NDA4MTE=","__isActor":"User","avatarUrl":"https://avatars.githubusercontent.com/u/6440811?s=64\u0026v=4"},"label":{"id":"LA_kwDOHusmC88AAAABdzKnjA","nameHTML":"daily-routine","name":"daily-routine","color":"0E8A16","description":""},"__isNode":"LabeledEvent","id":"LE_lADOHusmC86gCdsNzwAAAAOWPEk-"},"cursor":"Y3Vyc29yOnYyOpPPAAABk1ZSfbgBqzE1NDA1NDM1MTk4"},{"node":{"__typename":"AssignedEvent","__isIssueTimelineItems":"AssignedEvent","__isTimelineEvent":"AssignedEvent","databaseId":15405435224,"createdAt":"2024-11-23T00:01:07Z","actor":{"__typename":"User","login":"HiromiShikata","id":"MDQ6VXNlcjY0NDA4MTE=","__isActor":"User","avatarUrl":"https://avatars.githubusercontent.com/u/6440811?s=64\u0026v=4"},"assignee":{"__typename":"User","id":"MDQ6VXNlcjY0NDA4MTE=","__isNode":"User","login":"HiromiShikata"},"__isNode":"AssignedEvent","id":"AE_lADOHusmC86gCdsNzwAAAAOWPElY"},"cursor":"Y3Vyc29yOnYyOpPPAAABk1ZSfbgBqzE1NDA1NDM1MjI0"},{"node":{"__typename":"AddedToProjectV2Event","__isIssueTimelineItems":"AddedToProjectV2Event","__isTimelineEvent":"AddedToProjectV2Event","databaseId":15405437990,"createdAt":"2024-11-23T00:01:25Z","actor":{"__typename":"User","login":"umino-bot","id":"U_kgDOCCyfsA","__isActor":"User","avatarUrl":"https://avatars.githubusercontent.com/u/137142192?s=64\u0026v=4"},"project":{"title":"Test v2 project","url":"https://github.com/orgs/HiromiShikata/projects/3","id":"PVT_kwDOBHwEyM4AEU1g"},"__isNode":"AddedToProjectV2Event","id":"ATPVTE_lADOHusmC86gCdsNzwAAAAOWPFQm"},"cursor":"Y3Vyc29yOnYyOpPPAAABk1ZSxAgBqzE1NDA1NDM3OTkw"},{"node":{"__typename":"ProjectV2ItemStatusChangedEvent","__isIssueTimelineItems":"ProjectV2ItemStatusChangedEvent","__isTimelineEvent":"ProjectV2ItemStatusChangedEvent","databaseId":15405438038,"createdAt":"2024-11-23T00:01:26Z","actor":{"__typename":"User","login":"umino-bot","id":"U_kgDOCCyfsA","__isActor":"User","avatarUrl":"https://avatars.githubusercontent.com/u/137142192?s=64\u0026v=4"},"project":{"title":"Test v2 project","url":"https://github.com/orgs/HiromiShikata/projects/3","id":"PVT_kwDOBHwEyM4AEU1g"},"previousStatus":"","status":"Unread","__isNode":"ProjectV2ItemStatusChangedEvent","id":"PVTISC_lADOHusmC86gCdsNzwAAAAOWPFRW"},"cursor":"Y3Vyc29yOnYyOpPPAAABk1ZSx_ABqzE1NDA1NDM4MDM4"},{"node":{"__typename":"ProjectV2ItemStatusChangedEvent","__isIssueTimelineItems":"ProjectV2ItemStatusChangedEvent","__isTimelineEvent":"ProjectV2ItemStatusChangedEvent","databaseId":15407010389,"createdAt":"2024-11-23T06:12:46Z","actor":{"__typename":"User","login":"HiromiShikata","id":"MDQ6VXNlcjY0NDA4MTE=","__isActor":"User","avatarUrl":"https://avatars.githubusercontent.com/u/6440811?s=64\u0026v=4"},"project":{"title":"Test v2 project","url":"https://github.com/orgs/HiromiShikata/projects/3","id":"PVT_kwDOBHwEyM4AEU1g"},"previousStatus":"Unread","status":"In progress","__isNode":"ProjectV2ItemStatusChangedEvent","id":"PVTISC_lADOHusmC86gCdsNzwAAAAOWVFJV"},"cursor":"Y3Vyc29yOnYyOpPPAAABk1emvzABqzE1NDA3MDEwMzg5"},{"node":{"__typename":"ProjectV2ItemStatusChangedEvent","__isIssueTimelineItems":"ProjectV2ItemStatusChangedEvent","__isTimelineEvent":"ProjectV2ItemStatusChangedEvent","databaseId":15407010498,"createdAt":"2024-11-23T06:12:51Z","actor":{"__typename":"User","login":"HiromiShikata","id":"MDQ6VXNlcjY0NDA4MTE=","__isActor":"User","avatarUrl":"https://avatars.githubusercontent.com/u/6440811?s=64\u0026v=4"},"project":{"title":"Test v2 project","url":"https://github.com/orgs/HiromiShikata/projects/3","id":"PVT_kwDOBHwEyM4AEU1g"},"previousStatus":"In progress","status":"Unread","__isNode":"ProjectV2ItemStatusChangedEvent","id":"PVTISC_lADOHusmC86gCdsNzwAAAAOWVFLC"},"cursor":"Y3Vyc29yOnYyOpPPAAABk1em0rgBqzE1NDA3MDEwNDk4"}]},"backTimelineItems":{"pageInfo":{"hasPreviousPage":false,"startCursor":"Y3Vyc29yOnYyOpPPAAABk1ZSfbgBqzE1NDA1NDM1MTk4"},"totalCount":6,"edges":[{"node":{"__typename":"LabeledEvent","__isIssueTimelineItems":"LabeledEvent","__isTimelineEvent":"LabeledEvent","databaseId":15405435198,"createdAt":"2024-11-23T00:01:07Z","actor":{"__typename":"User","login":"HiromiShikata","id":"MDQ6VXNlcjY0NDA4MTE=","__isActor":"User","avatarUrl":"https://avatars.githubusercontent.com/u/6440811?s=64\u0026v=4"},"label":{"id":"LA_kwDOHusmC88AAAABdzKnjA","nameHTML":"daily-routine","name":"daily-routine","color":"0E8A16","description":""},"__isNode":"LabeledEvent","id":"LE_lADOHusmC86gCdsNzwAAAAOWPEk-"},"cursor":"Y3Vyc29yOnYyOpPPAAABk1ZSfbgBqzE1NDA1NDM1MTk4"},{"node":{"__typename":"AssignedEvent","__isIssueTimelineItems":"AssignedEvent","__isTimelineEvent":"AssignedEvent","databaseId":15405435224,"createdAt":"2024-11-23T00:01:07Z","actor":{"__typename":"User","login":"HiromiShikata","id":"MDQ6VXNlcjY0NDA4MTE=","__isActor":"User","avatarUrl":"https://avatars.githubusercontent.com/u/6440811?s=64\u0026v=4"},"assignee":{"__typename":"User","id":"MDQ6VXNlcjY0NDA4MTE=","__isNode":"User","login":"HiromiShikata"},"__isNode":"AssignedEvent","id":"AE_lADOHusmC86gCdsNzwAAAAOWPElY"},"cursor":"Y3Vyc29yOnYyOpPPAAABk1ZSfbgBqzE1NDA1NDM1MjI0"},{"node":{"__typename":"AddedToProjectV2Event","__isIssueTimelineItems":"AddedToProjectV2Event","__isTimelineEvent":"AddedToProjectV2Event","databaseId":15405437990,"createdAt":"2024-11-23T00:01:25Z","actor":{"__typename":"User","login":"umino-bot","id":"U_kgDOCCyfsA","__isActor":"User","avatarUrl":"https://avatars.githubusercontent.com/u/137142192?s=64\u0026v=4"},"project":{"title":"Test v2 project","url":"https://github.com/orgs/HiromiShikata/projects/3","id":"PVT_kwDOBHwEyM4AEU1g"},"__isNode":"AddedToProjectV2Event","id":"ATPVTE_lADOHusmC86gCdsNzwAAAAOWPFQm"},"cursor":"Y3Vyc29yOnYyOpPPAAABk1ZSxAgBqzE1NDA1NDM3OTkw"},{"node":{"__typename":"ProjectV2ItemStatusChangedEvent","__isIssueTimelineItems":"ProjectV2ItemStatusChangedEvent","__isTimelineEvent":"ProjectV2ItemStatusChangedEvent","databaseId":15405438038,"createdAt":"2024-11-23T00:01:26Z","actor":{"__typename":"User","login":"umino-bot","id":"U_kgDOCCyfsA","__isActor":"User","avatarUrl":"https://avatars.githubusercontent.com/u/137142192?s=64\u0026v=4"},"project":{"title":"Test v2 project","url":"https://github.com/orgs/HiromiShikata/projects/3","id":"PVT_kwDOBHwEyM4AEU1g"},"previousStatus":"","status":"Unread","__isNode":"ProjectV2ItemStatusChangedEvent","id":"PVTISC_lADOHusmC86gCdsNzwAAAAOWPFRW"},"cursor":"Y3Vyc29yOnYyOpPPAAABk1ZSx_ABqzE1NDA1NDM4MDM4"},{"node":{"__typename":"ProjectV2ItemStatusChangedEvent","__isIssueTimelineItems":"ProjectV2ItemStatusChangedEvent","__isTimelineEvent":"ProjectV2ItemStatusChangedEvent","databaseId":15407010389,"createdAt":"2024-11-23T06:12:46Z","actor":{"__typename":"User","login":"HiromiShikata","id":"MDQ6VXNlcjY0NDA4MTE=","__isActor":"User","avatarUrl":"https://avatars.githubusercontent.com/u/6440811?s=64\u0026v=4"},"project":{"title":"Test v2 project","url":"https://github.com/orgs/HiromiShikata/projects/3","id":"PVT_kwDOBHwEyM4AEU1g"},"previousStatus":"Unread","status":"In progress","__isNode":"ProjectV2ItemStatusChangedEvent","id":"PVTISC_lADOHusmC86gCdsNzwAAAAOWVFJV"},"cursor":"Y3Vyc29yOnYyOpPPAAABk1emvzABqzE1NDA3MDEwMzg5"},{"node":{"__typename":"ProjectV2ItemStatusChangedEvent","__isIssueTimelineItems":"ProjectV2ItemStatusChangedEvent","__isTimelineEvent":"ProjectV2ItemStatusChangedEvent","databaseId":15407010498,"createdAt":"2024-11-23T06:12:51Z","actor":{"__typename":"User","login":"HiromiShikata","id":"MDQ6VXNlcjY0NDA4MTE=","__isActor":"User","avatarUrl":"https://avatars.githubusercontent.com/u/6440811?s=64\u0026v=4"},"project":{"title":"Test v2 project","url":"https://github.com/orgs/HiromiShikata/projects/3","id":"PVT_kwDOBHwEyM4AEU1g"},"previousStatus":"In progress","status":"Unread","__isNode":"ProjectV2ItemStatusChangedEvent","id":"PVTISC_lADOHusmC86gCdsNzwAAAAOWVFLC"},"cursor":"Y3Vyc29yOnYyOpPPAAABk1em0rgBqzE1NDA3MDEwNDk4"}]}},"id":"R_kgDOHusmCw"},"safeViewer":{"isEnterpriseManagedUser":false,"enterpriseManagedEnterpriseId":null,"login":"HiromiShikata","id":"MDQ6VXNlcjY0NDA4MTE=","avatarUrl":"https://avatars.githubusercontent.com/u/6440811?s=64\u0026v=4","name":"Hiromi.s","isEmployee":false}}},"timestamp":1732342388}],"preloadedSubscriptions":{"e96a2844fab333e7326a09a8e560b51c":{"{\\"issueId\\":\\"I_kwDOCNXcUc6GaFia\\"}":{"response":{"data":{"issueUpdated":{"issueTitleUpdated":null,"issueStateUpdated":null,"issueMetadataUpdated":null}}},"subscriptionId":"eyJjIjoiZzp2Mzo6aXNzdWVVcGRhdGVkOmlkOklfa3dET0h1c21DODZnQ2RzTjpnYUpwWkxKSlgydDNSRTlJZFhOdFF6ZzJaME5rYzA0PSIsInQiOjE3MzIzNDIzODh9--07fda93fa814829504a1fb3a817e9b023976fdd61988f40db526266a04cf688a"}}}},"title":null,"appPayload":{"initial_view_content":{"team_id":null,"can_edit_view":true},"current_user":{"id":"MDQ6VXNlcjY0NDA4MTE=","login":"HiromiShikata","avatarUrl":"https://avatars.githubusercontent.com/u/6440811?v=4","is_staff":false,"is_emu":false},"current_user_settings":{"use_monospace_font":false,"use_single_key_shortcut":true,"preferred_emoji_skin_tone":0},"paste_url_link_as_plain_text":false,"base_avatar_url":"https://avatars.githubusercontent.com","feedback_url":null,"help_url":"https://docs.github.com","sso_organizations":null,"tracing":false,"tracing_flamegraph":false,"catalog_service":"github/issues_experience","scoped_repository":{"id":"R_kgDOHusmCw","owner":"HiromiShikata","name":"test-repository","is_archived":false},"proxima":false,"render_opt_out":false,"enabled_features":{"use_pull_request_subscriptions_enabled":false,"pull_request_single_subscription":true,"graphql_subscriptions":true,"disable_issues_react_ssr":false,"profiles_write_to_target":true,"issues_react":false,"issues_react_prefetch":false,"issue_types":true,"issues_react_dashboard_saved_views":false,"copilot_workspace":false,"sub_issues":true,"copilot_natural_language_github_search":false,"issues_react_ui_commands_migration":true,"private_avatars":false,"reserved_domain":true,"projects_classic_sunset_ui":true,"projects_classic_sunset_override":false,"issues_react_new_timeline":true,"refresh_image_video_src":true,"issues_react_bypass_es_limits":true,"issues_react_close_as_duplicate":false,"issues_react_customise_notifications_ui":true,"notifyd_issue_watch_activity_notify":false,"notifyd_enable_issue_thread_subscriptions":false,"issues_react_new_sort_dropdown":true,"tasklist_block":false,"issues_react_perf_test":false}}}</script>
26
+ `;
27
+
28
+ const result = await repository.getIssueFromBetaFeatureView(
29
+ testIssueUrl,
30
+ htmlContent,
31
+ );
32
+ expect(result).toEqual({
33
+ assignees: ['HiromiShikata'],
34
+ inProgressTimeline: [
35
+ {
36
+ author: 'HiromiShikata',
37
+ durationMinutes: 0.08333333333333333,
38
+ endedAt: new Date('2024-11-23T06:12:51.000Z'),
39
+ issueUrl:
40
+ 'https://github.com/HiromiShikata/test-repository/issues/38',
41
+ startedAt: new Date('2024-11-23T06:12:46.000Z'),
42
+ },
43
+ ],
44
+ labels: ['daily-routine'],
45
+ project: 'Test v2 project',
46
+ status: 'Unread',
47
+ statusTimeline: [
48
+ {
49
+ author: 'umino-bot',
50
+ from: '',
51
+ time: '2024-11-23T00:01:26Z',
52
+ to: 'Unread',
53
+ },
54
+ {
55
+ author: 'HiromiShikata',
56
+ from: 'Unread',
57
+ time: '2024-11-23T06:12:46Z',
58
+ to: 'In progress',
59
+ },
60
+ {
61
+ author: 'HiromiShikata',
62
+ from: 'In progress',
63
+ time: '2024-11-23T06:12:51Z',
64
+ to: 'Unread',
65
+ },
66
+ ],
67
+ title: 'Test title',
68
+ url: 'https://github.com/HiromiShikata/test-repository/issues/38',
69
+ });
70
+ });
71
+ });
@@ -0,0 +1,263 @@
1
+ import axios from 'axios';
2
+ import { BaseGitHubRepository } from '../BaseGitHubRepository';
3
+ import typia from 'typia';
4
+ import {
5
+ getInProgressTimeline,
6
+ IssueStatusTimeline,
7
+ } from './issueTimelineUtils';
8
+ import { Issue } from './CheerioIssueRepository';
9
+
10
+ type GitHubBetaFeatureViewData = {
11
+ payload: {
12
+ preloadedQueries: [
13
+ {
14
+ variables: {
15
+ owner: string;
16
+ repo: string;
17
+ number: number;
18
+ };
19
+ result: {
20
+ data: {
21
+ repository: {
22
+ issue: {
23
+ id: string;
24
+ title: string;
25
+ state: string;
26
+ assignees: {
27
+ nodes: Array<{
28
+ login: string;
29
+ }>;
30
+ };
31
+ labels: {
32
+ edges: Array<{
33
+ node: {
34
+ name: string;
35
+ };
36
+ }>;
37
+ };
38
+ projectItemsNext: {
39
+ edges: Array<{
40
+ node: {
41
+ project: {
42
+ title: string;
43
+ };
44
+ };
45
+ }>;
46
+ };
47
+ frontTimelineItems: {
48
+ edges: Array<{
49
+ node: {
50
+ __typename: string;
51
+ id: string;
52
+ };
53
+ }>;
54
+ pageInfo: {
55
+ hasNextPage: boolean;
56
+ endCursor: string;
57
+ };
58
+ totalCount: number;
59
+ };
60
+ backTimelineItems: {
61
+ edges: Array<{
62
+ node: {
63
+ __typename: string;
64
+ id: string;
65
+ };
66
+ }>;
67
+ };
68
+ };
69
+ };
70
+ };
71
+ };
72
+ },
73
+ ];
74
+ };
75
+ };
76
+
77
+ type ProjectV2ItemStatusChangedEvent = BaseTimelineItem & {
78
+ __typename: 'ProjectV2ItemStatusChangedEvent';
79
+ id: string;
80
+ createdAt: string;
81
+ actor: {
82
+ login: string;
83
+ };
84
+ previousStatus: string;
85
+ status: string;
86
+ };
87
+ type BaseUser = {
88
+ __typename: 'User';
89
+ login: string;
90
+ id: string;
91
+ __isActor: 'User';
92
+ avatarUrl: string;
93
+ };
94
+
95
+ type BaseTimelineItem = {
96
+ __typename: string;
97
+ __isIssueTimelineItems: string;
98
+ __isTimelineEvent: string;
99
+ databaseId: number;
100
+ createdAt: string;
101
+ actor: BaseUser;
102
+ __isNode: string;
103
+ id: string;
104
+ };
105
+
106
+ type GraphqlResponse = {
107
+ data: {
108
+ node: {
109
+ __typename: 'Issue';
110
+ frontTimelineItems: GitHubBetaFeatureViewData['payload']['preloadedQueries'][0]['result']['data']['repository']['issue']['frontTimelineItems'];
111
+ id: string;
112
+ };
113
+ };
114
+ };
115
+ export class InternalGraphqlIssueRepository extends BaseGitHubRepository {
116
+ getFrontTimelineItems = async (
117
+ issueUrl: string,
118
+ cursor: string | null,
119
+ issueId: string,
120
+ maxCount: number = 9999,
121
+ ): Promise<
122
+ GraphqlResponse['data']['node']['frontTimelineItems']['edges']
123
+ > => {
124
+ const query = 'f6ff036f8e215bd07d00516664e8725c';
125
+ const callQuery = async (
126
+ query: string,
127
+ count: number | null,
128
+ cursor: string | null,
129
+ issueId: string,
130
+ ): Promise<GraphqlResponse['data']['node']['frontTimelineItems']> => {
131
+ const requestBody = {
132
+ query: query,
133
+ variables: {
134
+ cursor: cursor || '',
135
+ count: count,
136
+ id: issueId,
137
+ },
138
+ };
139
+
140
+ const bodyParam = encodeURIComponent(JSON.stringify(requestBody));
141
+ const url = `https://github.com/_graphql?body=${bodyParam}`;
142
+
143
+ const headers = {
144
+ accept: '*/*',
145
+ 'accept-language': 'en-US,en;q=0.9,ja;q=0.8',
146
+ 'cache-Control': 'no-cache',
147
+ referer: issueUrl,
148
+ 'sec-ch-ua':
149
+ '"Not/A)Brand";v="8", "Chromium";v="126", "Google Chrome";v="126"',
150
+ 'sec-ch-ua-mobile': '?0',
151
+ 'sec-ch-ua-platform': '"Linux"',
152
+ 'sec-fetch-dest': 'empty',
153
+ 'sec-fetch-mode': 'cors',
154
+ 'sec-fetch-site': 'same-origin',
155
+ 'user-agent':
156
+ 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36',
157
+ 'x-requested-with': 'XMLHttpRequest',
158
+ cookie: await this.getCookie(),
159
+ };
160
+
161
+ const response = await axios.get<GraphqlResponse>(url, {
162
+ headers: headers,
163
+ withCredentials: true,
164
+ });
165
+ return response.data.data.node.frontTimelineItems;
166
+ };
167
+
168
+ const frontTimelineItems: GraphqlResponse['data']['node']['frontTimelineItems']['edges'] =
169
+ [];
170
+ let nextCursor = cursor;
171
+ let remainingCount = maxCount;
172
+ while (frontTimelineItems.length < maxCount) {
173
+ const response = await callQuery(
174
+ query,
175
+ remainingCount,
176
+ nextCursor,
177
+ issueId,
178
+ );
179
+ frontTimelineItems.push(...response.edges);
180
+ if (response.totalCount < remainingCount) {
181
+ remainingCount = response.totalCount;
182
+ }
183
+ remainingCount -= response.edges.length;
184
+ nextCursor = response.pageInfo.endCursor;
185
+ if (!response.pageInfo.hasNextPage) {
186
+ break;
187
+ }
188
+ }
189
+ return frontTimelineItems;
190
+ };
191
+ getIssueFromBetaFeatureView = async (
192
+ issueUrl: string,
193
+ html: string,
194
+ ): Promise<Issue> => {
195
+ const pattern =
196
+ /<script type="application\/json" data-target="react-app\.embeddedData">([\s\S]*?)<\/script>/;
197
+ const match = html.match(pattern);
198
+
199
+ if (!match || !match[1]) {
200
+ throw new Error(
201
+ `No script content found. URL: ${issueUrl}, HTML: ${html}`,
202
+ );
203
+ }
204
+ const scriptContent = match[1];
205
+ if (!scriptContent) {
206
+ throw new Error('No script content found');
207
+ }
208
+ const data: unknown = JSON.parse(scriptContent);
209
+ if (!typia.is<GitHubBetaFeatureViewData>(data)) {
210
+ throw new Error(`Invalid data: ${JSON.stringify(data)}`);
211
+ }
212
+ const issueData =
213
+ data.payload.preloadedQueries[0].result.data.repository.issue;
214
+ const issueRemainingCount =
215
+ issueData.frontTimelineItems.totalCount -
216
+ issueData.frontTimelineItems.edges.length -
217
+ issueData.backTimelineItems.edges.length;
218
+ const loadedMoreIssues = issueUrl.includes('/pull/')
219
+ ? []
220
+ : await this.getFrontTimelineItems(
221
+ issueUrl,
222
+ issueData.frontTimelineItems.pageInfo.endCursor,
223
+ issueData.id,
224
+ issueRemainingCount,
225
+ );
226
+ const statusTimeline = issueData.frontTimelineItems.edges
227
+ .concat(loadedMoreIssues)
228
+ .concat(issueData.backTimelineItems.edges)
229
+ .filter(
230
+ (edge, index, self) =>
231
+ self.findIndex((t) => t.node.id === edge.node.id) === index,
232
+ )
233
+ .filter(
234
+ (edge): edge is { node: ProjectV2ItemStatusChangedEvent } =>
235
+ edge.node.__typename === 'ProjectV2ItemStatusChangedEvent',
236
+ )
237
+ .map(
238
+ (edge): IssueStatusTimeline => ({
239
+ time: edge.node.createdAt,
240
+ author: edge.node.actor?.login || '',
241
+ from: edge.node.previousStatus,
242
+ to: edge.node.status,
243
+ }),
244
+ );
245
+ const inProgressTimeline = await getInProgressTimeline(
246
+ statusTimeline,
247
+ issueUrl,
248
+ );
249
+ return {
250
+ url: issueUrl,
251
+ title: issueData.title,
252
+ status:
253
+ statusTimeline.length > 0
254
+ ? statusTimeline[statusTimeline.length - 1].to
255
+ : '',
256
+ assignees: issueData.assignees.nodes.map((node) => node.login),
257
+ labels: issueData.labels.edges.map((edge) => edge.node.name),
258
+ project: issueData.projectItemsNext.edges[0].node.project.title,
259
+ statusTimeline,
260
+ inProgressTimeline,
261
+ };
262
+ };
263
+ }
@@ -0,0 +1,29 @@
1
+ import { RestIssueRepository } from './RestIssueRepository';
2
+
3
+ describe('RestIssueRepository', () => {
4
+ const restIssueRepository: RestIssueRepository = new RestIssueRepository(
5
+ '',
6
+ process.env.GH_TOKEN || 'dummy',
7
+ );
8
+
9
+ describe('createComment', () => {
10
+ it('should create a comment', async () => {
11
+ await restIssueRepository.createComment(
12
+ 'https://github.com/HiromiShikata/test-repository/issues/40',
13
+ 'test comment',
14
+ );
15
+ });
16
+ });
17
+ describe('createNewIssue', () => {
18
+ it('should create a new issue', async () => {
19
+ await restIssueRepository.createNewIssue(
20
+ 'HiromiShikata',
21
+ 'test-repository',
22
+ 'test issue',
23
+ 'test body',
24
+ ['HiromiShikata'],
25
+ ['test'],
26
+ );
27
+ });
28
+ });
29
+ });
@@ -0,0 +1,105 @@
1
+ import axios from 'axios';
2
+ import { BaseGitHubRepository } from '../BaseGitHubRepository';
3
+ import { Issue } from '../../../domain/entities/Issue';
4
+
5
+ export class RestIssueRepository extends BaseGitHubRepository {
6
+ createComment = async (issueUrl: string, comment: string) => {
7
+ const { owner, repo, issueNumber } = this.extractIssueFromUrl(issueUrl);
8
+ const response = await axios.post(
9
+ `https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}/comments`,
10
+ {
11
+ body: comment,
12
+ },
13
+ {
14
+ headers: {
15
+ Authorization: `token ${this.ghToken}`,
16
+ 'Content-Type': 'application/json',
17
+ },
18
+ },
19
+ );
20
+ if (response.status !== 201) {
21
+ throw new Error(`Failed to create comment: ${response.status}`);
22
+ }
23
+ };
24
+ createNewIssue = async (
25
+ owner: string,
26
+ repo: string,
27
+ title: string,
28
+ body: string,
29
+ assignees: string[],
30
+ labels: string[],
31
+ ) => {
32
+ const response = await axios.post(
33
+ `https://api.github.com/repos/${owner}/${repo}/issues`,
34
+ {
35
+ title,
36
+ body,
37
+ assignees,
38
+ labels,
39
+ },
40
+ {
41
+ headers: {
42
+ Authorization: `token ${this.ghToken}`,
43
+ 'Content-Type': 'application/json',
44
+ },
45
+ },
46
+ );
47
+ if (response.status !== 201) {
48
+ throw new Error(`Failed to create issue: ${response.status}`);
49
+ }
50
+ };
51
+ getIssue = async (
52
+ issueUrl: string,
53
+ ): Promise<{
54
+ labels: string[];
55
+ assignees: string[];
56
+ title: string;
57
+ body: string;
58
+ number: number;
59
+ state: string;
60
+ }> => {
61
+ const { owner, repo, issueNumber } = this.extractIssueFromUrl(issueUrl);
62
+ const response = await axios.get<{
63
+ labels: Array<{ name: string }>;
64
+ assignees: Array<{ login: string }>;
65
+ title: string;
66
+ body: string;
67
+ number: number;
68
+ state: string;
69
+ }>(`https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}`, {
70
+ headers: {
71
+ Authorization: `token ${this.ghToken}`,
72
+ Accept: 'application/vnd.github.v3+json',
73
+ },
74
+ });
75
+ return {
76
+ labels: response.data.labels.map((label) => label.name),
77
+ assignees: response.data.assignees.map((assignee) => assignee.login),
78
+ title: response.data.title,
79
+ body: response.data.body,
80
+ number: response.data.number,
81
+ state: response.data.state,
82
+ };
83
+ };
84
+ updateIssue = async (issue: Issue) => {
85
+ const response = await axios.patch(
86
+ `https://api.github.com/repos/${issue.org}/${issue.repo}/issues/${issue.number}`,
87
+ {
88
+ title: issue.title,
89
+ body: issue.body,
90
+ assignees: issue.assignees,
91
+ labels: issue.labels,
92
+ state: issue.state,
93
+ },
94
+ {
95
+ headers: {
96
+ Authorization: `token ${this.ghToken}`,
97
+ 'Content-Type': 'application/json',
98
+ },
99
+ },
100
+ );
101
+ if (response.status !== 200) {
102
+ throw new Error(`Failed to update issue: ${response.status}`);
103
+ }
104
+ };
105
+ }
@@ -0,0 +1,79 @@
1
+ import { getInProgressTimeline } from './issueTimelineUtils';
2
+
3
+ describe('issueTimelineUtils', () => {
4
+ const issueUrl = 'https://github.com/HiromiShikata/test-repository/issues/38';
5
+ describe('getInProgressTimelineEvents', () => {
6
+ it('should return in progress timeline events', async () => {
7
+ const statusTimeline = [
8
+ {
9
+ author: 'HiromiShikata',
10
+ from: 'Todo',
11
+ time: '2024-04-21T10:13:07Z',
12
+ to: 'In Progress',
13
+ },
14
+ {
15
+ author: 'HiromiShikata',
16
+ from: 'In Progress',
17
+ time: '2024-04-21T11:13:38Z',
18
+ to: 'Todo',
19
+ },
20
+ ];
21
+ const inProgressTimeline = await getInProgressTimeline(
22
+ statusTimeline,
23
+ issueUrl,
24
+ );
25
+ expect(inProgressTimeline).toEqual([
26
+ {
27
+ author: 'HiromiShikata',
28
+ durationMinutes: 60.516666666666666,
29
+ endedAt: new Date('2024-04-21T11:13:38Z'),
30
+ issueUrl:
31
+ 'https://github.com/HiromiShikata/test-repository/issues/38',
32
+ startedAt: new Date('2024-04-21T10:13:07Z'),
33
+ },
34
+ ]);
35
+ });
36
+ describe('should count `in progress` if from is not `in progress` but input it to `in progress` before', () => {
37
+ describe(`fromToList | expectedStartEndList`, () => {
38
+ test.each`
39
+ caseName | fromToList | expectedStartEndList
40
+ ${'Normal'} | ${[['Todo', 'In Progress', 'user0'], ['In Progress', 'Todo', 'user0']]} | ${[['2000-01-01T00:00:00Z', '2000-01-01T00:01:00Z', 'user0', 1]]}
41
+ ${'Closed by other user'} | ${[['Todo', 'In Progress', 'user0'], ['In Progress', 'Todo', 'user1']]} | ${[['2000-01-01T00:00:00Z', '2000-01-01T00:01:00Z', 'user0', 1]]}
42
+ ${'No record moved from In Progress'} | ${[['Todo', 'In Progress', 'user0'], ['In Review', 'Todo', 'user0']]} | ${[['2000-01-01T00:00:00Z', '2000-01-01T00:01:00Z', 'user0', 1]]}
43
+ ${'No record moved from In Progress and other user continued'} | ${[['Todo', 'In Progress', 'user0'], ['Todo', 'In Progress', 'user1'], ['In Review', 'Todo', 'user1']]} | ${[['2000-01-01T00:00:00Z', '2000-01-01T00:01:00Z', 'user0', 1], ['2000-01-01T00:01:00Z', '2000-01-01T00:02:00Z', 'user1', 1]]}
44
+ `(
45
+ `$caseName, $fromToList, $expectedStartEndList`,
46
+ async ({
47
+ fromToList,
48
+ expectedStartEndList,
49
+ }: {
50
+ fromToList: string[][];
51
+ expectedStartEndList: string[][];
52
+ }) => {
53
+ const timeline = fromToList.map((fromTo: string[], index) => ({
54
+ time: `2000-01-01T00:0${index}:00Z`,
55
+ author: fromTo[2],
56
+ from: fromTo[0],
57
+ to: fromTo[1],
58
+ }));
59
+ const inProgressTimeline = await getInProgressTimeline(
60
+ timeline,
61
+ issueUrl,
62
+ );
63
+ expect(inProgressTimeline).toEqual(
64
+ expectedStartEndList.map((expectedStartEnd) => {
65
+ return {
66
+ author: expectedStartEnd[2],
67
+ endedAt: new Date(expectedStartEnd[1]),
68
+ issueUrl,
69
+ startedAt: new Date(expectedStartEnd[0]),
70
+ durationMinutes: expectedStartEnd[3],
71
+ };
72
+ }),
73
+ );
74
+ },
75
+ );
76
+ });
77
+ });
78
+ });
79
+ });
@@ -0,0 +1,52 @@
1
+ import { WorkingTime } from '../../../domain/entities/WorkingTime';
2
+
3
+ export type IssueStatusTimeline = {
4
+ time: string;
5
+ author: string;
6
+ from: string;
7
+ to: string;
8
+ };
9
+ export type IssueInProgressTimeline = WorkingTime & {
10
+ issueUrl: string;
11
+ };
12
+
13
+ export const getInProgressTimeline = async (
14
+ timelines: IssueStatusTimeline[],
15
+ issueUrl: string,
16
+ ): Promise<WorkingTime[]> => {
17
+ const report: IssueInProgressTimeline[] = [];
18
+ let currentInProgress:
19
+ | Pick<IssueInProgressTimeline, 'issueUrl' | 'author' | 'startedAt'>
20
+ | undefined = undefined;
21
+ for (const timeline of timelines) {
22
+ const time = new Date(timeline.time);
23
+ if (timeline.to.toLocaleLowerCase().includes('in progress')) {
24
+ if (currentInProgress !== undefined) {
25
+ report.push({
26
+ ...currentInProgress,
27
+ endedAt: time,
28
+ durationMinutes:
29
+ Math.floor(time.getTime() / 1000 / 60) -
30
+ Math.floor(currentInProgress.startedAt.getTime() / 1000 / 60),
31
+ });
32
+ currentInProgress = undefined;
33
+ }
34
+ currentInProgress = {
35
+ issueUrl: issueUrl,
36
+ author: timeline.author,
37
+ startedAt: time,
38
+ };
39
+ continue;
40
+ }
41
+ if (currentInProgress != undefined) {
42
+ report.push({
43
+ ...currentInProgress,
44
+ endedAt: time,
45
+ durationMinutes:
46
+ (time.getTime() - currentInProgress.startedAt.getTime()) / 1000 / 60,
47
+ });
48
+ currentInProgress = undefined;
49
+ }
50
+ }
51
+ return report;
52
+ };