@iki-inc/test-registry 1.0.10 → 1.0.18

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 (93) hide show
  1. package/.editorconfig +15 -0
  2. package/.env.sample +12 -0
  3. package/.npm/_cacache/content-v2/sha512/09/60/735d0adcfea3a4862770d4ecc14d0b9845b7f2c435c93152755cdc141c644e566b9a7a2126dceb25a59d5157de9ded228a8d92ab489bb7fadff1970dd1f4 +0 -0
  4. package/.npm/_cacache/content-v2/sha512/0b/a0/8b0190f2ea6340287aaab7e4f6b3444d5dfeed9a34fd3cb0314d909db52d36efa7808f5c66aa63cfcbd44328a3f38466a2522727f6ada90372321033358d +0 -0
  5. package/.npm/_cacache/content-v2/sha512/14/22/c7b510ff827a428821c48892cec1d9853fec330a60c491cf72ecdb18c5e178bbb06db27d59bb0830246c4898898789c240acb3f8474c97e1cd8a0ab32b4c +0 -0
  6. package/.npm/_cacache/content-v2/sha512/14/68/07da1f3328d8a6f658e3edd6a79053dc20220af42a796e6f9cda041261e3e1a5a1b9f9eb2b2ce0e2848a2b9fe3dee85189cd6857428b4fbfbde34da95d5c +0 -0
  7. package/.npm/_cacache/content-v2/sha512/15/8c/64d48ef03efdcad9705aa321f67139e871012ef00399eb484f6eccae90c3cbbf9ff9aabed5d62f0f2c7765e997fcec8fb165716f3c25d88e24c5abe6120d +0 -0
  8. package/.npm/_cacache/content-v2/sha512/1c/98/3329206d747adfa5b1f7bc78fed6a4b21fd451d0bff33030b0be8e0d136160f4fda791631f8f6033872b44347193b4bea071f48f392d7a1cb2d3438b957a +0 -0
  9. package/.npm/_cacache/content-v2/sha512/20/9a/2978f18ee879cc59575e2c28a19f15d9d16096d26c57894e4b1376d9d34187112b02680be00fc84dfc04c825e6619eb55d53971f4724b5b95eb7566738ce +0 -0
  10. package/.npm/_cacache/content-v2/sha512/28/83/7f9c3241411717c3430b561644f62407986ebca80548060f42aa65188e64088608a3f54e4c16faea9142f915bb72cb366e39e3add3375e45ee1463b72df8 +0 -0
  11. package/.npm/_cacache/content-v2/sha512/2d/fb/25e8313fd91905be948484c12737ab0db98d08791ebc50442adaacd1d0c91417544c0a6385943dcec9d9d3f7263e7e45a307814f486e1203fa0155961315 +0 -0
  12. package/.npm/_cacache/content-v2/sha512/41/25/fcd4a845a191b07a75723e86deae081d5b59eb4194af7d50c02145ebdd6a8b459199a9693f3f84fca7648ff9d7b64abe473dcbe385c273122ba52a09f1b4 +0 -0
  13. package/.npm/_cacache/content-v2/sha512/4a/9d/5a6e52748af0e44b38dc68977112e9cde7f5ef92c149dac30115fabac74af285057fd9bfcac057b6d5c329987b4f3928a3f0af7dff049fa04b9339b9ae31 +0 -0
  14. package/.npm/_cacache/content-v2/sha512/52/61/5720f53ef4483713d9bfef6f018a2d25008ba873eea76eb35429a44f24e169f758a1e38aaa1a55adc4c07a91716b443ea9a3ef3a92c1cce571c27e4ea23f +0 -0
  15. package/.npm/_cacache/content-v2/sha512/54/28/c235f80cb1bcb7b53768d369db8ed33f7b0adaea33c79a94e17a7913621f291bdb9c67fd4ff12a38bb814605e93f063a4e56c0c23282c0fe2b8128815744 +0 -0
  16. package/.npm/_cacache/content-v2/sha512/58/f4/bf1ef1d04d89c78ac2e8f4c72a0473899361641cefed969be5772ae77a6e1a790a7885a8b7832b61b3083aa74d684a84e5e7cadca621408c5d9baf6024d8 +0 -0
  17. package/.npm/_cacache/content-v2/sha512/5b/ae/e22e5e09d845c41936df78709f7eb8c37e2b6f2c0360d14957df01545124f1f762974457a0307515812a84fb0be101b8b85aa8c683d733cac4d5d84a5b7b +0 -0
  18. package/.npm/_cacache/content-v2/sha512/61/65/938e000148a72fb3f9d6062f4fc9c63f2623c9a8e0f8240ea8f527a3d80b6f4866ab5f1282dce412938a406ab6dd4252808790f814242424d916f86027df +0 -0
  19. package/.npm/_cacache/content-v2/sha512/65/42/9187afe4505a0089302d4d83d9277870f70371c7e04804e8a39e51bd3e7ac9b027128ecd70cb20fabc9a5a62d827cc3aca6114aa7f738ee917daf77c6c46 +0 -0
  20. package/.npm/_cacache/content-v2/sha512/65/7f/7d7bab51c1ea145ea47e541aec96175ae75361e4c4d0c28bb9b6750381bb723347418268440ed5863ffc5b2a7ea1a9f3d11ee8d4370cf97f2ff06db867a7 +0 -0
  21. package/.npm/_cacache/content-v2/sha512/65/fe/47d8ac6ddb18d3bdb26f3f66562c4202c40ea3fa1026333225ca9cb8c5c060d6f2959f1f3d5b2d066d2fa47f9730095145cdd0858765d20853542d2e9cb3 +0 -0
  22. package/.npm/_cacache/content-v2/sha512/6a/e1/f2278020333eef812f07a7737a1d74a694c754ca1494adf045d7ac35e77af1b4b204384f871aa681a6973366f9c72ea4097e21e8b4262936197495444e15 +0 -0
  23. package/.npm/_cacache/content-v2/sha512/72/46/b8edf552a3a95f4032004d8a9bfef3b59ef15f6cfc3bb962dac885c88461138f4c1c38e96b964a1b38c0cd40e16bf5039a7de8143ead77576fbabed0fbca +0 -0
  24. package/.npm/_cacache/content-v2/sha512/7b/79/d17e07d4678acd18bdb7da05205f4e90372c9ecf4e0a76316b17e2d34683979ab3a014a0e0e0109db235bc1274faf5ea9d606991a49c223d560dac2696de +0 -0
  25. package/.npm/_cacache/content-v2/sha512/82/83/9a72a304d8663239965f6f92e0b6efb520398094012ee719c3dfec53cd2d74853d2fce97f448f8e45f0030f499bee27fe64060cfdcb3bc08f6d844e7817d +0 -0
  26. package/.npm/_cacache/content-v2/sha512/b1/34/9f063a17069f3d26f20a21e7eac3b53608279bb1cef892263a6b0886a202ada1219b823604fc6ffe97db05dcc5853cd73d21ca0e0b83837ca1dfc459a9d2 +0 -0
  27. package/.npm/_cacache/content-v2/sha512/b1/e4/b64e3dba4c154e0b6348736ace7b6cb664eede7f1213b4b65c1923a71c734e43b0a489405fc34230d9c93ac642213f02e128d2d2f013be844a6781096acf +0 -0
  28. package/.npm/_cacache/content-v2/sha512/d2/12/54f5208fbe633320175916a34f5d66ba76a87b59d1f470823dcbe0b24bcac6de72f8f01725adaf4798a8555541f23d6347e58ef10f0001edb7e04a391431 +0 -0
  29. package/.npm/_cacache/content-v2/sha512/d3/2a/6c390b7d0f87b7873895f0ee1e943f6e374cff8ed5047f1551b4d68002d5c8c94c7d9bf4b394ed36d22f1834bd9a2ff13a6e53afbc741c8a7e87665172ff +0 -0
  30. package/.npm/_cacache/content-v2/sha512/d5/c0/cd77027625aa2199bdec8383a629a301c2e0b8f2c6278b91d4c360efb02f0b8c64cb2bd87e79bd57e91cae3877b8853d142c25baf22a26863528294aa53d +0 -0
  31. package/.npm/_cacache/content-v2/sha512/ed/71/cdc47eea5fdc46e66230c6486e993a31fcc21135c3a00ebc56b0cb76a40af6dd61e9e8cad194dec50521690a9afea153b417be38894811f369c931f1b648 +0 -0
  32. package/.npm/_cacache/content-v2/sha512/f5/18/862af0b06aac4eda8c0feb5b90e0180d6e8ac042c90c47a42eb1f5b342abfb0193a0e6819e85d636a5124c4cabb0c5dd480c13f8ad26c8e651f353b48cb1 +0 -0
  33. package/.npm/_cacache/content-v2/sha512/f5/f4/a349aa2cfdf448548a7ec5226513a95fc21112ecb36d29a08121a987b23af69dad418800493e8d263a38f3f062435116ab9823c6a9a89583999f8dbf7c09 +0 -0
  34. package/.npm/_cacache/content-v2/sha512/fb/02/a330d53dc3f11a41ac875dbed603b65a1d8ea137c5a07ef1cd437b753e753dd7fecef137139c88420d84d9c27b7c26c3bd201a99df42e7d6bee93c5e7603 +0 -0
  35. package/.npm/_cacache/content-v2/sha512/fb/2b/3df7b53dea9a382b1fc0069042aa103d12ec49690583420ef6f791f8841a61bf72198346e804abb0629b78617a7a319e4099942753fb72313951a5a49e8e +0 -0
  36. package/.npm/_cacache/content-v2/sha512/fc/85/ed6f0124e474cfc84c32297ea11a4617c4cf676e3eb807e8a55499c2fd1e81d291f91b85776f4a556cbec3063e2d921040a696d05257fa17a5e5f4b1eed6 +0 -0
  37. package/.npm/_cacache/index-v5/00/60/6a2298196c9cd65881a7267db2824adff4d9c24f0ed6d3a2ca4c81d3e235 +2 -0
  38. package/.npm/_cacache/index-v5/18/55/9c21c8d8605592da1a2d8ad777878425c65933e52599a58e642960dce2e5 +2 -0
  39. package/.npm/_cacache/index-v5/1d/67/152bf23a6b2f18b828f3bba90e56430367ec7471521e92ab71ec92263d0f +2 -0
  40. package/.npm/_cacache/index-v5/1d/f8/e0e47cb46e4fa089b0801bd0a1ae48912c4e323215f569ad1c3832070f9f +2 -0
  41. package/.npm/_cacache/index-v5/20/af/2322ca225285ead2f23e5dbe0416b3ae6e18d91ed2d463d97780850fc40f +2 -0
  42. package/.npm/_cacache/index-v5/22/50/6e01e0a33342f552257e2351f2cd7d26148bd9d2c73fc3797a4c4ddb0e39 +2 -0
  43. package/.npm/_cacache/index-v5/24/e3/6a5b57ca4d63f37ccae38ea6b710ed659eeb763c63181c02e9470f8b29b9 +2 -0
  44. package/.npm/_cacache/index-v5/25/7f/0b973fb10d0d62b5962532a6e5691f81dd1c20b63fcce513967941450a82 +2 -0
  45. package/.npm/_cacache/index-v5/25/98/f306f68eac58ac50c3a0a0453cb8f29d78b9505c01947c965b4768a4e60d +2 -0
  46. package/.npm/_cacache/index-v5/32/e2/5912d88421e6b76e5e0365ae8015aac3093d6e53b5b60fdfa91116720417 +2 -0
  47. package/.npm/_cacache/index-v5/34/cb/dd5e6f02ebea3574006624e5dd81e72b124d66a1bd1598ab078fda7118f7 +2 -0
  48. package/.npm/_cacache/index-v5/36/e7/0b6493f625a039ca4cb935e7526c5a564658f0781f039bc1e79ea8aa7a81 +2 -0
  49. package/.npm/_cacache/index-v5/3d/d0/639107e3f13a812eb6cb868ab09827c1e4a913b741a0db28c7b348e62204 +2 -0
  50. package/.npm/_cacache/index-v5/4d/de/54bca7c04d5de2f23c10f555ac8bcc9c61e9c96e203394bb7afa2cbbc867 +2 -0
  51. package/.npm/_cacache/index-v5/54/c8/008397b4ada8d972166fe22e555240d3dd695fd6526b397c6e271de4f66e +2 -0
  52. package/.npm/_cacache/index-v5/56/9a/f71b742ff3706d89fbad8eea6d75d14f5fb0aed00409ee07d85ccf03a4ec +2 -0
  53. package/.npm/_cacache/index-v5/56/bc/31c9c48635fb47969a293eba4f565d970b8b2dd86e021ae1713753ddbaec +2 -0
  54. package/.npm/_cacache/index-v5/6b/33/9648614194cfe05bcf87e96e6d1a9d596eff2ccd2be75d5a63840ff5c5fa +2 -0
  55. package/.npm/_cacache/index-v5/7d/65/c2c4e4cf88f6f5a26a474eec1eb28a1a0c087f156fa82df974013a50081f +2 -0
  56. package/.npm/_cacache/index-v5/84/3c/a34aeaf29195f5f9401c15f6857af1bd76c7606e0b093aab991c9bb6dc49 +2 -0
  57. package/.npm/_cacache/index-v5/85/10/8e7b4fa59d85fc845f40d555750cde254b8f420226a92e17aa2aa651402e +2 -0
  58. package/.npm/_cacache/index-v5/87/da/af28f1133468f98432a0caf94d8a9e1a4f1bcd830398a06a4e51d9d17bcd +2 -0
  59. package/.npm/_cacache/index-v5/8d/ef/9d2076851d6fe0fa0268eaff636ef6e5460e964a4ef2dd328962a5bcaf60 +2 -0
  60. package/.npm/_cacache/index-v5/97/34/dbe58f025608dd514d67beb18cc0629d4de7ddaf177a9db1e4055f87ea95 +2 -0
  61. package/.npm/_cacache/index-v5/9a/85/0413827ea05586904775c2a90fc65067ec0bb008d3fdf73a12fa898b7a04 +2 -0
  62. package/.npm/_cacache/index-v5/9b/97/3672a25ed2ffa8387e2aef3d8efe3ab063f3468ecc8f8fd04357c75571ef +2 -0
  63. package/.npm/_cacache/index-v5/af/a8/e6580916548ffab34b60d8768e6aee8675ef59ad518a9c183058685c39c3 +2 -0
  64. package/.npm/_cacache/index-v5/be/fa/ec64da8a1ca11da9a4e002cd9e3e5c9a9bac8f1f61de82ff2495e7cadb90 +2 -0
  65. package/.npm/_cacache/index-v5/c9/10/e51aeab9f2ed39d1416132dca81cf2f580dc5de2fd02b9a85d1df3b191f3 +2 -0
  66. package/.npm/_cacache/index-v5/e6/ee/a7f609296af013f459dea95f5b86155b1d4d3fa7dc9ec423562d0c55294f +2 -0
  67. package/.npm/_cacache/index-v5/eb/bf/3d55cbdfa15db3275139376e851469f22db49100d7fe1c648784b7c07c58 +2 -0
  68. package/.npm/_cacache/index-v5/f0/3b/58c14b3461e0a75ddd4a0be80707cae7f82bf1ef1ae03a0dfc8054aa2570 +2 -0
  69. package/.npm/_cacache/index-v5/f5/91/b4bddca1df9d1a5db8fc8bd09d22078228fd3c1f50ad12a3c9886499c03f +2 -0
  70. package/.npm/_cacache/index-v5/fd/fb/c68ba9a1320725050b57ca9c7fbc237e694f1f4a6cb94104f7e3e654f7f4 +2 -0
  71. package/.npm/_logs/2025-10-11T01_26_43_124Z-debug-0.log +283 -0
  72. package/.npm/_logs/2025-10-11T23_03_53_873Z-debug-0.log +283 -0
  73. package/.npm/_logs/2025-10-11T23_04_45_411Z-debug-0.log +283 -0
  74. package/.npm/_logs/2025-10-11T23_12_37_676Z-debug-0.log +283 -0
  75. package/.npm/_logs/2025-10-11T23_13_52_627Z-debug-0.log +283 -0
  76. package/.npm/_logs/2025-10-11T23_29_34_855Z-debug-0.log +283 -0
  77. package/.npm/_logs/2025-10-11T23_35_33_841Z-debug-0.log +283 -0
  78. package/.npm/_logs/2025-10-12T00_02_59_292Z-debug-0.log +284 -0
  79. package/.npm/_logs/2025-10-12T00_04_16_309Z-debug-0.log +284 -0
  80. package/.npm/_logs/2025-10-17T01_57_18_990Z-debug-0.log +156 -0
  81. package/.npm/_logs/2025-10-17T02_27_57_689Z-debug-0.log +156 -0
  82. package/mise.toml +5 -0
  83. package/package.json +12 -3
  84. package/scripts/closeIssues/index.ts +23 -0
  85. package/scripts/closeIssues/modules/closeIssuesOperation.ts +167 -0
  86. package/scripts/modules/gitlabApi.ts +49 -0
  87. package/scripts/modules/utils.ts +5 -0
  88. package/scripts/releaseNote/index.ts +19 -0
  89. package/scripts/releaseNote/modules/releaseNoteGenerator.ts +231 -0
  90. package/tsconfig.json +4 -0
  91. package/tsconfig.node.json +29 -0
  92. package/types/env.d.ts +10 -0
  93. package/types/reset.d.ts +1 -0
@@ -0,0 +1,23 @@
1
+ import CloseIssuesOperation from "./modules/closeIssuesOperation";
2
+
3
+ const operation = new CloseIssuesOperation()
4
+
5
+ if (!operation.isMergeRequest) {
6
+ console.log('⚠️ Not a merge request. Skip closing issues.')
7
+ process.exit(0)
8
+ }
9
+
10
+ operation.execute()
11
+ .then((result) => {
12
+ const error = result.find(r => r.status === 'error')
13
+ if (error) {
14
+ console.error('😭 Failed to close issues.')
15
+ process.exit(1)
16
+ }
17
+
18
+ console.log('👏 Issues closed successfully.')
19
+ })
20
+ .catch((error) => {
21
+ console.error('💥 Fatal error', error)
22
+ process.exit(1)
23
+ })
@@ -0,0 +1,167 @@
1
+ import type { Camelize, ExpandedMergeRequestSchema } from '@gitbeaker/rest'
2
+ import GitLabApi from '../../modules/gitlabApi'
3
+
4
+ type TargetIssue = {
5
+ keyword: string
6
+ id: number
7
+ }
8
+
9
+ type OperationResultError = {id: string; status: 'error'; message: string}
10
+ type OperationResult = {id: string; status: 'closed' | 'skipped'} | OperationResultError
11
+
12
+ export default class CloseIssuesOperation extends GitLabApi {
13
+ // マージリクエストのID
14
+ readonly #mergeRequestIid = NaN
15
+
16
+
17
+ constructor() {
18
+ super()
19
+ this.#mergeRequestIid = this.getMergeRequestIidFromCommitMessage(process.env.CI_COMMIT_MESSAGE ?? '')
20
+ }
21
+
22
+ /** マージリクエストが存在するか */
23
+ get isMergeRequest(): boolean {
24
+ return !isNaN(this.#mergeRequestIid)
25
+ }
26
+
27
+ /**
28
+ * マージリクエストに紐づくイシューをクローズする
29
+ */
30
+ public async execute() {
31
+ const mr = await this.api.MergeRequests.show(this.projectId, this.#mergeRequestIid)
32
+
33
+ const issues = this.#getIssues(mr)
34
+ const result = await this.#closeIssues(issues)
35
+
36
+ this.#printSummary(issues, result)
37
+
38
+ return result
39
+ }
40
+
41
+ /**
42
+ * マージリクエストの説明文からクローズ対象のイシューIDリストを取得する
43
+ * @param mr
44
+ * @private
45
+ */
46
+ #getIssues(mr: ExpandedMergeRequestSchema | Camelize<ExpandedMergeRequestSchema>) {
47
+ const issues = Array.from([...mr.description?.matchAll(this.closeIssuesRegex) ?? '']
48
+ .map((match) => {
49
+ if (typeof match === 'string') {
50
+ return {
51
+ keyword: 'fixes',
52
+ id: NaN
53
+ }
54
+ }
55
+
56
+ return {
57
+ keyword: match.groups?.keyword ?? 'fixes',
58
+ id: Number(match.groups?.number)
59
+ }
60
+ })
61
+ .filter(({ id }) => !isNaN(id))
62
+ .reduce((map, issue) => {
63
+ // イシューIDの重複排除
64
+ map.set(issue.id, issue)
65
+ return map
66
+ }, new Map<number, TargetIssue>())
67
+ .values())
68
+
69
+ return issues
70
+ }
71
+
72
+ async #closeIssues(issues: TargetIssue[]) {
73
+ if (Array.from(issues).length === 0) return []
74
+
75
+ const result: OperationResult[] = []
76
+
77
+ for (const issue of issues) {
78
+ try {
79
+ const targetIssue = await this.api.Issues.show(issue.id, {
80
+ projectId: this.projectId
81
+ })
82
+
83
+ if (targetIssue.state === 'closed') {
84
+ result.push({
85
+ id: `#${issue.id}`,
86
+ status: 'skipped'
87
+ })
88
+ continue
89
+ }
90
+
91
+ if (targetIssue.state === 'opened') {
92
+ await this.api.Issues.edit(this.projectId, issue.id, {
93
+ stateEvent: 'close',
94
+ })
95
+
96
+ result.push({
97
+ id: `#${issue.id}`,
98
+ status: 'closed'
99
+ })
100
+ }
101
+ } catch (error) {
102
+ result.push({
103
+ id: `#${issue.id}`,
104
+ status: 'error',
105
+ message: (error as Error).message
106
+ })
107
+ }
108
+ }
109
+
110
+ return result
111
+ }
112
+
113
+ /**
114
+ * 処理結果のログ出力
115
+ * @param issues
116
+ * @param result
117
+ * @private
118
+ */
119
+ #printSummary(issues: TargetIssue[], result: OperationResult[]) {
120
+ console.log('📊 Summary')
121
+
122
+ console.log(`\nMerge Request IID: !${this.#mergeRequestIid}`)
123
+
124
+ // ターゲットリスト
125
+ const targetIssues = Array.from(issues, (issue) => `#${issue.id}`)
126
+ const targetLength = targetIssues.length
127
+
128
+ console.log(`\nTarget issues: ${targetLength}件`)
129
+ if (targetIssues.length > 0) {
130
+ console.log(`${targetIssues.join(', ')}`)
131
+ } else {
132
+ // 0件なら終了
133
+ return
134
+ }
135
+
136
+ console.log('\n')
137
+
138
+ const closed = result.filter(({ status }) => status === 'closed')
139
+ this.#printResultData('✅ Closed issues:', closed)
140
+
141
+ const skipped = result.filter(({ status }) => status === 'skipped')
142
+ this.#printResultData('⏭️ Skipped issues:', skipped)
143
+
144
+ const errors = result.filter(({ status }) => status === 'error')
145
+ this.#printResultData('❌ Error issues:', errors, true)
146
+ }
147
+
148
+ /**
149
+ * 処理結果の詳細ログ
150
+ * @param label
151
+ * @param result
152
+ * @param isError
153
+ * @private
154
+ */
155
+ #printResultData(label: string, result: OperationResult[], isError = false) {
156
+ console.log(`${label} ${result.length}件`)
157
+
158
+ for (const res of result) {
159
+ console.log(` - ${res.id}`)
160
+
161
+ if (isError) {
162
+ console.log(` Error: ${(res as OperationResultError).message}`)
163
+ }
164
+ }
165
+ }
166
+
167
+ }
@@ -0,0 +1,49 @@
1
+ import { Gitlab } from '@gitbeaker/rest'
2
+
3
+ export default class GitLabApi {
4
+ protected readonly api: InstanceType<typeof Gitlab>
5
+ protected readonly projectId: number
6
+
7
+ constructor() {
8
+ this.api = new Gitlab({
9
+ host: `https://${process.env.CI_SERVER_HOST ?? 'gitlab.com'}`,
10
+ token: process.env.CI_API_TOKEN,
11
+ })
12
+
13
+ this.projectId = Number(process.env.CI_PROJECT_ID)
14
+ }
15
+
16
+ /** MRからクローズ対象のイシューIDを抽出するための正規表現 */
17
+ protected get closeIssuesRegex() {
18
+ return /(?<keyword>fix(?:es)?|closes?|resolves?)\s*#(?<number>\d+)/gi
19
+ }
20
+
21
+ /**
22
+ * コミットメッセージからマージリクエストのIIDを抽出する
23
+ * @param commitMessage
24
+ * @protected
25
+ */
26
+ protected getMergeRequestIidFromCommitMessage(commitMessage: string): number {
27
+ // 検索したいコミットメッセージの正規表現パターン
28
+ const searchRegExpPattern = [
29
+ // パターン1:"See merge request project/repo!123"
30
+ /See merge request [^\s!]+!(?<iid>\d+)/i,
31
+ // パターン2:"See merge request !123"
32
+ /See merge request !(?<iid>\d+)/i,
33
+ // パターン3:"Closes !123" または "Merge !123"
34
+ /(?:closes?|merges?|see)\s+!(?<iid>\d+)/i,
35
+ // パターン4:"https://gitlab.com/project/repo/-/merge_requests/123"
36
+ /https?:\/\/[^\s\/]+\/[^\s\/]+\/[^\s\/]+\/-\/merge_requests\/(?<iid>\d+)/i,
37
+ ]
38
+
39
+ for (const pattern of searchRegExpPattern) {
40
+ const match = commitMessage.match(pattern)
41
+ if (match && match.groups?.iid) {
42
+ return Number(match.groups.iid)
43
+ }
44
+ }
45
+
46
+ // マッチしなかった場合
47
+ return NaN
48
+ }
49
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * 型ガード関数でstring型かどうかをチェック
3
+ * @param value
4
+ */
5
+ export const isString = (value: unknown): value is string => typeof value === 'string'
@@ -0,0 +1,19 @@
1
+ import releaseNoteGenerator from './modules/releaseNoteGenerator'
2
+
3
+ const generator = new releaseNoteGenerator()
4
+
5
+ if (!generator.isReleaseTag) {
6
+ console.log('⚠️ Not a release tag. Skip generating release notes.')
7
+ process.exit(0)
8
+ }
9
+
10
+ generator.execute()
11
+ .then((body: string) => {
12
+ console.log(body)
13
+ console.log('👏 Release notes generated successfully.')
14
+ })
15
+ .catch((error) => {
16
+ console.error('💥 Fatal error', error)
17
+ process.exit(1)
18
+ })
19
+
@@ -0,0 +1,231 @@
1
+ import GitLabApi from '../../modules/gitlabApi'
2
+ import type { ExpandedMergeRequestSchema, SimpleLabelSchema } from '@gitbeaker/rest'
3
+ import { writeFile } from 'node:fs/promises'
4
+ import { isString } from '../../modules/utils'
5
+
6
+ interface ScopedMergeRequest {
7
+ scope: string
8
+ title: string
9
+ webUrl: string
10
+ iid: number
11
+ }
12
+
13
+ // 最古のコミットを指す定数
14
+ const OLDEST_COMMIT_REF = 'HEAD~9999'
15
+
16
+ export default class ReleaseNoteGenerator extends GitLabApi {
17
+ readonly #currentTag: string;
18
+
19
+ constructor() {
20
+ super()
21
+
22
+ this.#currentTag = process.env.CI_COMMIT_TAG ?? ''
23
+ }
24
+
25
+ /** リリースタグが存在している */
26
+ get isReleaseTag() {
27
+ return this.#currentTag !== ''
28
+ }
29
+
30
+ /** リリースノート作成! */
31
+ public async execute() {
32
+ const previousTag = await this.#getPreviousTag()
33
+ console.log({ previousTag, currentTag: this.#currentTag })
34
+
35
+ const mergeRequest = await this.#getMergeRequestsBetweenTags(previousTag, this.#currentTag)
36
+ const scopedMergeRequests = this.#categorizeMergeRequestsByScope(mergeRequest)
37
+
38
+ const releaseNoteBody = this.#generateReleaseNoteBody(scopedMergeRequests)
39
+
40
+ // flag: 'a' だと末尾に追記する。先頭に追記したい場合は既存のドキュメントを読み込んで結合する必要がある
41
+ await writeFile('release_notes.md', releaseNoteBody, { encoding: 'utf8', flag: 'w' })
42
+
43
+ return releaseNoteBody
44
+ }
45
+
46
+ /**
47
+ * 前のリリースタグを取得
48
+ * @private
49
+ */
50
+ async #getPreviousTag(): Promise<string> {
51
+ const tags = await this.api.Tags.all(this.projectId, {
52
+ orderBy: 'updated',
53
+ sort: 'desc',
54
+ perPage: 100
55
+ })
56
+
57
+ const currentTagIndex = tags.findIndex((tag) => tag.name === this.#currentTag)
58
+
59
+ if (currentTagIndex === -1) {
60
+ throw new Error(`Current tag ${this.#currentTag} not found`)
61
+ }
62
+
63
+ if (currentTagIndex === tags.length - 1) {
64
+ // 最初のタグの場合は、リポジトリの最初のコミットから
65
+ return OLDEST_COMMIT_REF
66
+ }
67
+
68
+ return tags[currentTagIndex + 1].name
69
+ }
70
+
71
+ /**
72
+ * 指定された2つのタグ間のマージリクエストを取得
73
+ * @param fromTag
74
+ * @param toTag
75
+ */
76
+ async #getMergeRequestsBetweenTags(fromTag: string, toTag: string): Promise<ExpandedMergeRequestSchema[]> {
77
+ const fromTagInfo = await this.#getTagInfo(fromTag)
78
+
79
+ const createdAt = fromTagInfo?.commit.created_at
80
+ const since = createdAt && isString(createdAt) ? createdAt : undefined
81
+
82
+ const commits = await this.api.Commits.all(this.projectId, {
83
+ refName: toTag,
84
+ since
85
+ })
86
+
87
+ // マージコミットからコミットリクエストを特定
88
+ const mergeRequestIids = new Set<number>()
89
+
90
+ // デフォルトブランチへのマージコミットを判定する正規表現
91
+ const defaultBranchMergeRegExp = new RegExp(`^Merge branch.*into\\s+'?${process.env.CI_DEFAULT_BRANCH}'?`, 'i')
92
+
93
+ for (const commit of commits) {
94
+ // デフォルトブランチへのマージコミットは対象外
95
+ if (commit.title.match(defaultBranchMergeRegExp)) continue
96
+
97
+ const iid = this.getMergeRequestIidFromCommitMessage(commit.message)
98
+ if (!isNaN(iid)) {
99
+ mergeRequestIids.add(iid)
100
+ }
101
+ }
102
+
103
+ const promises = Array.from(mergeRequestIids).map((iid) => this.api.MergeRequests.show(this.projectId, iid))
104
+ const results = await Promise.allSettled(promises)
105
+
106
+ return results
107
+ .filter((result): result is PromiseFulfilledResult<ExpandedMergeRequestSchema> => {
108
+ if (result.status === 'rejected') {
109
+ // マージリクエストIIDを取得するために、元の配列のインデックスを使用
110
+ const iid = Array.from(mergeRequestIids)[results.indexOf(result)]
111
+ console.warn(`⚠️ Could not fetch MR !${iid}:`, result.reason?.message || result.reason)
112
+ return false
113
+ }
114
+
115
+ return true
116
+ })
117
+ .map((result) => result.value)
118
+ }
119
+
120
+ /**
121
+ * タグの情報を取得
122
+ * @param tag
123
+ * @private
124
+ */
125
+ async #getTagInfo(tag: string) {
126
+ if (tag === OLDEST_COMMIT_REF) return undefined
127
+
128
+ return await this.api.Tags.show(this.projectId, tag)
129
+ }
130
+
131
+ /**
132
+ * マージリクエストをスコープごとに分類
133
+ * @param mergeRequests
134
+ * @private
135
+ */
136
+ #categorizeMergeRequestsByScope(mergeRequests: ExpandedMergeRequestSchema[]): Map<string, ScopedMergeRequest[]> {
137
+ const scopedMrs = new Map<string, ScopedMergeRequest[]>()
138
+
139
+ for (const mr of mergeRequests) {
140
+ const scope = this.#extractScopesFromLabels(this.#getLabelNames(mr.labels ?? []))
141
+ if (scope == null) continue
142
+
143
+ const scopedMr: ScopedMergeRequest = {
144
+ scope,
145
+ title: mr.title,
146
+ webUrl: mr.web_url,
147
+ iid: mr.iid
148
+ }
149
+
150
+ if (!scopedMrs.has(scope)) {
151
+ scopedMrs.set(scope, [])
152
+ }
153
+ scopedMrs.get(scope)!.push(scopedMr)
154
+ }
155
+
156
+ return scopedMrs
157
+ }
158
+
159
+ /**
160
+ * ラベル名を配列で取得
161
+ * @param labels
162
+ * @private
163
+ */
164
+ #getLabelNames(labels: string[] | SimpleLabelSchema[]): string[] {
165
+ if (labels.length === 0) return []
166
+
167
+ // GitLab APIのレスポンスは一貫しているので方が混在することはないため最初の型をチェックしてstring[]になるようにする
168
+ return typeof labels.at(0) === 'string'
169
+ ? labels as string[]
170
+ : (labels as SimpleLabelSchema[]).map(label => label.name)
171
+ }
172
+
173
+ /**
174
+ * ラベルからリリースノート用のラベルを抽出
175
+ * リリース用のラベルはスコープドなので必ず一つだけしか存在しない
176
+ * @param labels
177
+ * @private
178
+ */
179
+ #extractScopesFromLabels(labels: string[]): string | null {
180
+ const scopes = labels
181
+ .filter((label) => label.startsWith('log::'))
182
+ .map((label) => label.replace('log::', ''))
183
+
184
+ return scopes.at(0) ?? null
185
+ }
186
+
187
+ /**
188
+ * リリースノートの本文を生成
189
+ * @param scopedMRs
190
+ * @private
191
+ */
192
+ #generateReleaseNoteBody(scopedMRs: Map<string, ScopedMergeRequest[]>): string {
193
+ const header = `# ${this.#currentTag}`
194
+
195
+ if (scopedMRs.size === 0) {
196
+ return [
197
+ header,
198
+ '',
199
+ 'No changes.'
200
+ ].join('\n')
201
+ }
202
+
203
+ const sortedScopes = Array.from(scopedMRs.keys()).sort()
204
+
205
+ const scopeSections: string[] = []
206
+
207
+ for (const scope of sortedScopes) {
208
+ const mrs = scopedMRs.get(scope)!
209
+ const sectionHeader = `## ${scope.charAt(0).toUpperCase() + scope.slice(1)}`
210
+
211
+ const mrList: string[] = []
212
+ for (const mr of mrs) {
213
+ mrList.push(`- ${mr.title} ([!${mr.iid}](${mr.webUrl}))`)
214
+ }
215
+
216
+ scopeSections.push([
217
+ '',
218
+ sectionHeader,
219
+ ...mrList,
220
+ ]
221
+ .join('\n'))
222
+ }
223
+
224
+ return [
225
+ header,
226
+ ...scopeSections,
227
+ '',
228
+ ''
229
+ ].join('\n')
230
+ }
231
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,4 @@
1
+ {
2
+ "files": [],
3
+ "references": [{ "path": "./tsconfig.node.json" }]
4
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["ES2022"],
5
+ "module": "ES2022",
6
+ "skipLibCheck": true,
7
+
8
+ /* Bundler mode */
9
+ "moduleResolution": "bundler",
10
+ "allowImportingTsExtensions": false,
11
+ "isolatedModules": true,
12
+ "esModuleInterop": true,
13
+ "moduleDetection": "force",
14
+ "noEmit": true,
15
+ "types": ["node"],
16
+
17
+ /* Linting */
18
+ "strict": true,
19
+ "noUnusedLocals": true,
20
+ "noUnusedParameters": true,
21
+ "noFallthroughCasesInSwitch": true,
22
+ "noUncheckedSideEffectImports": true,
23
+ "verbatimModuleSyntax": true,
24
+ "stripInternal": true,
25
+ "erasableSyntaxOnly": true
26
+ },
27
+ "include": ["types/**/*", "scripts/**/*"],
28
+ "exclude": ["node_modules"]
29
+ }
package/types/env.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ interface ProcessEnv {
2
+ CI_API_TOKEN: string;
3
+ CI_SERVER_HOST: string;
4
+ CI_PROJECT_ID: string;
5
+ CI_COMMIT_MESSAGE?: string;
6
+
7
+ // リリースノート
8
+ CI_COMMIT_TAG?: string;
9
+ CI_DEFAULT_BRANCH: string
10
+ }
@@ -0,0 +1 @@
1
+ import '@total-typescript/ts-reset'