@jira-deploy/core 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dry-run.js ADDED
@@ -0,0 +1,691 @@
1
+ /**
2
+ * Dry-run / Debug 腳本
3
+ *
4
+ * 模式 1 — Mock(預設):全部 API 呼叫改用假資料
5
+ * node src/dry-run.js <tool> '<args_json>'
6
+ *
7
+ * 模式 2 — Live(真實 Jira,讀取不寫入):
8
+ * node src/dry-run.js <tool> '<args_json>' --live
9
+ * 需要 .env 有 JIRA_BASE_URL / JIRA_API_TOKEN
10
+ *
11
+ * 附加 Node.js debugger:
12
+ * node --inspect-brk src/dry-run.js <tool> '<args_json>' [--live]
13
+ * → 開 Chrome chrome://inspect 連上後下斷點
14
+ *
15
+ * ── 常用指令範例 ──────────────────────────────────────────────────────
16
+ *
17
+ * # Library 票
18
+ * node src/dry-run.js create_library_ticket '{"systemCode":"CWA","moduleChild":"cwa","gitBranch":"release/v1.5.2.0"}'
19
+ * node src/dry-run.js build_ticket '{"issueKey":"CID-1708"}' --live
20
+ *
21
+ * # CI 票
22
+ * node src/dry-run.js get_next_ci_version '{"systemCode":"IBK","branch":"master"}'
23
+ * node src/dry-run.js get_next_ci_version '{"systemCode":"CWA","branch":"master"}' --live
24
+ * node src/dry-run.js create_ci_ticket '{"systemCode":"IBK","relatesTo":["CID-1708"]}'
25
+ * node src/dry-run.js create_ci_ticket '{"systemCode":"IBK","relatesTo":["CID-1178","CID-1182"]}'
26
+ * node src/dry-run.js build_ticket '{"issueKey":"CID-1709"}' --live
27
+ * node src/dry-run.js wait_to_stg '{"issueKey":"CID-1709"}' --live
28
+ *
29
+ * # CD 票
30
+ * node src/dry-run.js create_cd_ticket '{"systemCode":"IBK","environment":"stg","ciTicket":"CID-1709"}'
31
+ * node src/dry-run.js create_cd_ticket '{"systemCode":"CWA","environment":"prd","ciTicket":"CID-1709","metaTest":true}'
32
+ * node src/dry-run.js create_cd_ticket '{"systemCode":"IBK","environment":"prd","ciTicket":"CID-1709","metaTest":true}' --live
33
+ * node src/dry-run.js prepare_cd_deployment '{"issueKey":"CID-1710","environment":"stg"}' --live
34
+ *
35
+ * # Deployment sub-task 觸發(新)
36
+ * node src/dry-run.js trigger_deployment '{"cdIssueKey":"CID-1710","environment":"stg"}' --live
37
+ * node src/dry-run.js trigger_deployment '{"cdIssueKey":"CID-1713","environment":"uat"}' --live
38
+ * node src/dry-run.js trigger_deployment '{"cdIssueKey":"CID-1716","environment":"stg","applyForClose":true}' --live
39
+ *
40
+ * # CD 開單 → 建立 Deployment → 部署 全流程 mock 測試
41
+ * node src/dry-run.js prepare_cd_deployment '{"issueKey":"CID-1716","environment":"stg"}'
42
+ * node src/dry-run.js trigger_deployment '{"cdIssueKey":"CID-1716","environment":"stg","applyForClose":true}'
43
+ *
44
+ * # Assignee 切換
45
+ * node src/dry-run.js update_assignee '{"issueKey":"CID-1718","displayName":"Solar"}'
46
+ * node src/dry-run.js update_assignee '{"issueKey":"CID-1718","accountId":"BK00129"}' --live
47
+ *
48
+ * # 計算下一個 Library Release 版號流水號
49
+ * node src/dry-run.js get_next_lib_version '{"moduleChild":"ibk","branch":"release/v1.5.2.0","fixVersion":"v1.5.2.0"}'
50
+ * node src/dry-run.js get_next_lib_version '{"moduleChild":"cwa","branch":"release/v1.5.2.0","fixVersion":"v1.5.2.0"}' --live
51
+ *
52
+ * # 狀態查詢 / transitions
53
+ * node src/dry-run.js list_transitions '{"issueKey":"CID-1710"}' --live
54
+ * node src/dry-run.js get_issue_status '{"issueKey":"CID-1709"}' --live
55
+ *
56
+ * # Release 現況查詢
57
+ * node src/dry-run.js get_release_status '{"systemCode":"IBK"}' --live
58
+ * node src/dry-run.js get_release_status '{"systemCode":"IBK"}'
59
+ *
60
+ * # Release Reroll — cancel CI 並取得 Library 模組清單
61
+ * node src/dry-run.js cancel_release '{"systemCode":"IBK"}' --live
62
+ * node src/dry-run.js cancel_release '{"systemCode":"IBK","ciIssueKey":"CID-1799"}' --live
63
+ * node src/dry-run.js cancel_release '{"systemCode":"IBK"}'
64
+ * ──────────────────────────────────────────────────────────────────────
65
+ */
66
+ import 'dotenv/config';
67
+ import { executeTool } from './tools/index.js';
68
+
69
+ const TOOL = process.argv[2] || 'create_library_ticket';
70
+ const ARGS = JSON.parse(process.argv[3] || '{}');
71
+ const LIVE = process.argv.includes('--live');
72
+
73
+ process.env.JIRA_BASE_URL = process.env.JIRA_BASE_URL || 'https://jira.example.com';
74
+ const BASE_URL = `${process.env.JIRA_BASE_URL}/rest/api/2`;
75
+
76
+ // ── 彩色 log helpers ──────────────────────────────────────────────
77
+ const c = {
78
+ cyan: (s) => `\x1b[36m${s}\x1b[0m`,
79
+ yellow: (s) => `\x1b[33m${s}\x1b[0m`,
80
+ green: (s) => `\x1b[32m${s}\x1b[0m`,
81
+ red: (s) => `\x1b[31m${s}\x1b[0m`,
82
+ gray: (s) => `\x1b[90m${s}\x1b[0m`,
83
+ };
84
+
85
+ function printCall(method, path, body) {
86
+ console.log('\n' + '═'.repeat(70));
87
+ console.log(c.cyan(`${method.toUpperCase()} ${BASE_URL}${path}`));
88
+ if (body !== undefined) {
89
+ console.log(c.gray('Body:'));
90
+ console.log(JSON.stringify(body, null, 2));
91
+ }
92
+ console.log('═'.repeat(70));
93
+ }
94
+
95
+ function printRead(key, fieldNames, result) {
96
+ console.log(c.gray(` ← GET /issue/${key}?fields=${fieldNames.join(',')}`));
97
+ console.log(c.gray(' Response: ' + JSON.stringify(result)));
98
+ }
99
+
100
+ // ─────────────────────────────────────────────────────────────────
101
+ // Mock Jira(模式 1)
102
+ // ─────────────────────────────────────────────────────────────────
103
+ const mockJira = {
104
+ createIssue: async (fields) => {
105
+ printCall('POST', '/issue', { fields });
106
+ return { id: 'DRY-RUN', key: 'CID-DRYRUN', self: `${BASE_URL}/issue/CID-DRYRUN` };
107
+ },
108
+ getIssueFields: async (key, fieldNames) => {
109
+ console.log(c.gray(` [MOCK] GET /issue/${key}?fields=${fieldNames.join(',')}`));
110
+ // CI 單:issuelinks(關聯 Library + 關聯 CI)
111
+ if (fieldNames.includes('issuelinks')) {
112
+ return {
113
+ summary: '[MOCK] IBK CI',
114
+ status: { name: 'Wait To STG' },
115
+ customfield_13438: '{"ibk_ap_version":"0.0.18"}',
116
+ issuelinks: [
117
+ {
118
+ type: {name: 'Relates', inward: 'relates to', outward: 'relates to'},
119
+ outwardIssue: {
120
+ key: 'CID-LIB-IBK-MOCK',
121
+ fields: {
122
+ summary: '[STG][Lib] IBK_IBK_MOCK',
123
+ status: {name: 'Released'},
124
+ issuetype: {name: 'Library'},
125
+ },
126
+ },
127
+ },
128
+ {
129
+ type: {name: 'Relates', inward: 'relates to', outward: 'relates to'},
130
+ outwardIssue: {
131
+ key: 'CID-LIB-SSR-MOCK',
132
+ fields: {
133
+ summary: '[STG][Lib] IBK_SSR_MOCK',
134
+ status: {name: 'Released'},
135
+ issuetype: {name: 'Library'},
136
+ },
137
+ },
138
+ },
139
+ {
140
+ type: {name: 'Relates', inward: 'relates to', outward: 'relates to'},
141
+ outwardIssue: {
142
+ key: 'CID-CI-OLD-MOCK',
143
+ fields: {
144
+ summary: '[MOCK] IBK Old CI',
145
+ status: {name: 'Done'},
146
+ issuetype: {name: 'CI'},
147
+ },
148
+ },
149
+ },
150
+ ],
151
+ };
152
+ }
153
+ // CI 單 release_version
154
+ if (fieldNames.includes('customfield_13438')) {
155
+ return {customfield_13438: '{"ibk_ap_version":"0.0.18"}'};
156
+ }
157
+ // Library 單:gitBranch + systemModule
158
+ if (fieldNames.includes('customfield_13431')) {
159
+ const moduleMap = {
160
+ 'CID-LIB-IBK-MOCK': {customfield_13431: 'release/v1.5.2.0', customfield_13444: {value: 'ibk'}},
161
+ 'CID-LIB-SSR-MOCK': {customfield_13431: 'release/v1.3.1.0', customfield_13444: {value: 'ssr'}},
162
+ };
163
+ return moduleMap[key] ?? {customfield_13431: 'release/v1.0.0.0', customfield_13444: {value: 'unknown'}};
164
+ }
165
+ // systemCode (customfield_13443)
166
+ if (fieldNames.includes('customfield_13443')) {
167
+ return {customfield_13443: {value: 'IBK'}};
168
+ }
169
+ return {};
170
+ },
171
+ updateIssue: async (key, fields) => {
172
+ printCall('PUT', `/issue/${key}`, { fields });
173
+ return {};
174
+ },
175
+ linkIssue: async (inward, outward, type) => {
176
+ printCall('POST', '/issueLink', {
177
+ type: { name: type },
178
+ inwardIssue: { key: inward },
179
+ outwardIssue: { key: outward },
180
+ });
181
+ return {};
182
+ },
183
+ addRemoteLink: async (issueKey, url, title) => {
184
+ printCall('POST', `/issue/${issueKey}/remotelink`, { object: { url, title } });
185
+ return {};
186
+ },
187
+ getIssue: async (key) => ({ fields: { status: { name: 'To Do' }, summary: `[MOCK] ${key}` } }),
188
+ // stateful mock:模擬 CD 單逐步推進的 transition 清單
189
+ _cdTransitionStep: 0,
190
+ getTransitions: async function (key) {
191
+ // Deployment sub-task mock
192
+ if (key === 'CID-DEPLOY-MOCK') {
193
+ const mockTrans = [
194
+ {id: '11', name: 'To AutoDeploy', to: {name: 'Pre Auto Deploy'}},
195
+ {id: '12', name: 'Trigger AutoDeploy', to: {name: 'Auto Deploy'}},
196
+ ];
197
+ console.log(c.gray(` [MOCK] getTransitions(${key}) → [${mockTrans.map((t) => t.name).join(', ')}]`));
198
+ return mockTrans;
199
+ }
200
+ // CI 單 mock:包含 Cancelled transition(供 cancel_release 使用)
201
+ if (key === 'CID-CI-MOCK' || key.startsWith('CID-CI')) {
202
+ const trans = [{id: '71', name: 'Cancelled', to: {name: 'Cancelled'}}];
203
+ console.log(c.gray(` [MOCK] getTransitions(${key}) → [${trans.map((t) => t.name).join(', ')}]`));
204
+ return trans;
205
+ }
206
+ // CD 單 mock:依呼叫次數模擬流程推進
207
+ // step 0-1: TO DO → Accept 可用
208
+ // step 2-3: Wait For Send Notice Email → Prepare to create deployment ticket 可用
209
+ // step 4-5: Prepare For Deploy → Apply for approval 可用
210
+ // step 6+: Wait Approval → Apply for close 可用
211
+ const CD_STEP_TRANSITIONS = [
212
+ [{ id: '10', name: 'Accept', to: { name: 'Wait For Send Notice Email' } }],
213
+ [{ id: '10', name: 'Accept', to: { name: 'Wait For Send Notice Email' } }],
214
+ [
215
+ {
216
+ id: '71',
217
+ name: 'Prepare to create deployment ticket',
218
+ to: { name: 'Prepare For Deploy' },
219
+ },
220
+ ],
221
+ [
222
+ {
223
+ id: '71',
224
+ name: 'Prepare to create deployment ticket',
225
+ to: { name: 'Prepare For Deploy' },
226
+ },
227
+ ],
228
+ [{ id: '21', name: 'Apply for approval', to: { name: 'Wait Approval' } }],
229
+ [{ id: '21', name: 'Apply for approval', to: { name: 'Wait Approval' } }],
230
+ [{ id: '99', name: 'Apply for close', to: { name: 'Wait For Close Approval' } }],
231
+ ];
232
+ const step = Math.min(this._cdTransitionStep++, CD_STEP_TRANSITIONS.length - 1);
233
+ const trans = CD_STEP_TRANSITIONS[step];
234
+ console.log(
235
+ c.gray(
236
+ ` [MOCK] getTransitions(${key}) step=${step} → [${trans.map((t) => t.name).join(', ')}]`,
237
+ ),
238
+ );
239
+ return trans;
240
+ },
241
+ transitionById: async (key, id) => {
242
+ console.log(c.gray(` [MOCK] transitionById(${key}, ${id})`));
243
+ },
244
+ transitionByName: async () => ({ transitioned: 'Done', toStatus: 'Done' }),
245
+ addComment: async (key, body) => {
246
+ printCall('POST', `/issue/${key}/comment`, { body });
247
+ return {};
248
+ },
249
+ searchIssues: async (jql, fields, max) => {
250
+ console.log(c.gray(` [MOCK] searchIssues(${jql.slice(0, 80)}...)`));
251
+ // cancel_release / get_release_status 查 CI 單
252
+ if (jql.includes('issuetype = CI')) {
253
+ return [{
254
+ key: 'CID-CI-MOCK',
255
+ fields: {
256
+ summary: '[IBK][CI] Mock CI 單',
257
+ status: {name: 'Wait To STG'},
258
+ customfield_13438: '{"ibk_ap_version":"0.0.18"}',
259
+ updated: new Date().toISOString(),
260
+ issuelinks: [
261
+ {
262
+ type: {name: 'Relates', inward: 'relates to', outward: 'relates to'},
263
+ outwardIssue: {
264
+ key: 'CID-LIB-IBK-MOCK',
265
+ fields: {
266
+ summary: '[STG][Lib] IBK_IBK_MOCK',
267
+ status: {name: 'Released'},
268
+ issuetype: {name: 'Library'},
269
+ },
270
+ },
271
+ },
272
+ {
273
+ type: {name: 'Relates', inward: 'relates to', outward: 'relates to'},
274
+ outwardIssue: {
275
+ key: 'CID-LIB-SSR-MOCK',
276
+ fields: {
277
+ summary: '[STG][Lib] IBK_SSR_MOCK',
278
+ status: {name: 'Released'},
279
+ issuetype: {name: 'Library'},
280
+ },
281
+ },
282
+ },
283
+ ],
284
+ },
285
+ }];
286
+ }
287
+ // get_release_status 查 CD 單
288
+ if (jql.includes('issuetype = CD')) {
289
+ return [{
290
+ key: 'CID-CD-STG-MOCK',
291
+ fields: {
292
+ summary: '[IBK][STG] CD MOCK',
293
+ status: {name: 'Wait Deploy'},
294
+ customfield_13436: {value: 'stg'},
295
+ customfield_14101: '{"ibk_ap_version":"0.0.18"}',
296
+ updated: new Date().toISOString(),
297
+ },
298
+ }];
299
+ }
300
+ return [];
301
+ },
302
+ getBitbucketTags: async () => [],
303
+ getBitbucketBranches: async () => [],
304
+ getBitbucketFileContent: async (project, repo, filePath, branch) => {
305
+ console.log(
306
+ c.gray(` [MOCK] getBitbucketFileContent(${project}/${repo}/${filePath}@${branch})`),
307
+ );
308
+ // pom.xml → CI Golden Image 格式
309
+ if (filePath === 'pom.xml') {
310
+ return `<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
311
+ <modelVersion>4.0.0</modelVersion>
312
+
313
+ <parent>
314
+ <groupId>com.line.bank</groupId>
315
+ <artifactId>assembly.parent</artifactId>
316
+ <version>0.0.1</version>
317
+ </parent>
318
+
319
+ <groupId>com.line.bank</groupId>
320
+ <artifactId>ch014.ibk.assembly</artifactId>
321
+ <version>0.0.12-SNAPSHOT</version>
322
+ <packaging>pom</packaging>
323
+
324
+ <name>ch014.ibk.assembly</name>
325
+ <description>IBK Golden Image</description>
326
+
327
+ <scm>
328
+ <connection>scm:git:https://bitbucket.linebank.com.tw/scm/lbtwgol/ch014.ibk.assembly.git</connection>
329
+ <developerConnection>scm:git:https://bitbucket.linebank.com.tw/scm/lbtwgol/ch014.ibk.assembly.git</developerConnection>
330
+ <url>https://bitbucket.linebank.com.tw/scm/lbtwgol/ch014.ibk.assembly</url>
331
+ <tag>HEAD</tag>
332
+ </scm>
333
+
334
+ <properties>
335
+ <ssr.version>release-v1.3.1.0-0.0.1</ssr.version>
336
+ <wealth.version>release-v1.1.0.0-0.0.1</wealth.version>
337
+ <ibk.version>release-v1.5.2.0-0.0.1</ibk.version>
338
+ <a11y.version>release-v1.5.1.0-0.0.4</a11y.version>
339
+ </properties>
340
+
341
+ <build>
342
+ <plugins>
343
+ <plugin>
344
+ <groupId>org.apache.maven.plugins</groupId>
345
+ <artifactId>maven-resources-plugin</artifactId>
346
+ <version>3.3.1</version>
347
+ <executions>
348
+ <execution>
349
+ <id>copy-dependency-libs</id>
350
+ <phase>process-resources</phase>
351
+ <goals>
352
+ <goal>copy-resources</goal>
353
+ </goals>
354
+ <configuration>
355
+ <overwrite>true</overwrite>
356
+ <includeEmptyDirs>true</includeEmptyDirs>
357
+ <outputDirectory>../../target/checkout/libs</outputDirectory>
358
+ <resources>
359
+ <resource>
360
+ <directory>../../libs</directory>
361
+ </resource>
362
+ </resources>
363
+ </configuration>
364
+ </execution>
365
+ <execution>
366
+ <id>copy-wealth</id>
367
+ <phase>process-resources</phase>
368
+ <goals>
369
+ <goal>copy-resources</goal>
370
+ </goals>
371
+ <configuration>
372
+ <overwrite>true</overwrite>
373
+ <includeEmptyDirs>true</includeEmptyDirs>
374
+ <outputDirectory>./target/wealth</outputDirectory>
375
+ <resources>
376
+ <resource>
377
+ <directory>libs/wealth</directory>
378
+ </resource>
379
+ </resources>
380
+ </configuration>
381
+ </execution>
382
+ <execution>
383
+ <id>copy-ssr</id>
384
+ <phase>process-resources</phase>
385
+ <goals>
386
+ <goal>copy-resources</goal>
387
+ </goals>
388
+ <configuration>
389
+ <overwrite>true</overwrite>
390
+ <includeEmptyDirs>true</includeEmptyDirs>
391
+ <outputDirectory>./target/ssr</outputDirectory>
392
+ <resources>
393
+ <resource>
394
+ <directory>libs/ssr</directory>
395
+ </resource>
396
+ </resources>
397
+ </configuration>
398
+ </execution>
399
+ <execution>
400
+ <id>copy-ibk</id>
401
+ <phase>process-resources</phase>
402
+ <goals>
403
+ <goal>copy-resources</goal>
404
+ </goals>
405
+ <configuration>
406
+ <overwrite>true</overwrite>
407
+ <includeEmptyDirs>true</includeEmptyDirs>
408
+ <outputDirectory>./target/ibk</outputDirectory>
409
+ <resources>
410
+ <resource>
411
+ <directory>libs/ibk</directory>
412
+ </resource>
413
+ </resources>
414
+ </configuration>
415
+ </execution>
416
+ <execution>
417
+ <id>copy-a11y</id>
418
+ <phase>process-resources</phase>
419
+ <goals>
420
+ <goal>copy-resources</goal>
421
+ </goals>
422
+ <configuration>
423
+ <overwrite>true</overwrite>
424
+ <includeEmptyDirs>true</includeEmptyDirs>
425
+ <outputDirectory>./target/a11y</outputDirectory>
426
+ <resources>
427
+ <resource>
428
+ <directory>libs/a11y</directory>
429
+ </resource>
430
+ </resources>
431
+ </configuration>
432
+ </execution>
433
+ </executions>
434
+ </plugin>
435
+ <plugin>
436
+ <groupId>org.apache.maven.plugins</groupId>
437
+ <artifactId>maven-assembly-plugin</artifactId>
438
+ <version>3.6.0</version>
439
+ <executions>
440
+ <execution>
441
+ <id>create-wealth</id>
442
+ <phase>package</phase>
443
+ <goals>
444
+ <goal>single</goal>
445
+ </goals>
446
+ <configuration>
447
+ <appendAssemblyId>true</appendAssemblyId>
448
+ <descriptors>
449
+ <descriptor>src/main/resources/build-wealth.xml</descriptor>
450
+ </descriptors>
451
+ </configuration>
452
+ </execution>
453
+ <execution>
454
+ <id>create-ssr</id>
455
+ <phase>package</phase>
456
+ <goals>
457
+ <goal>single</goal>
458
+ </goals>
459
+ <configuration>
460
+ <appendAssemblyId>true</appendAssemblyId>
461
+ <descriptors>
462
+ <descriptor>src/main/resources/build-ssr.xml</descriptor>
463
+ </descriptors>
464
+ </configuration>
465
+ </execution>
466
+ <execution>
467
+ <id>create-ibk</id>
468
+ <phase>package</phase>
469
+ <goals>
470
+ <goal>single</goal>
471
+ </goals>
472
+ <configuration>
473
+ <appendAssemblyId>true</appendAssemblyId>
474
+ <descriptors>
475
+ <descriptor>src/main/resources/build-ibk.xml</descriptor>
476
+ </descriptors>
477
+ </configuration>
478
+ </execution>
479
+ <execution>
480
+ <id>create-a11y</id>
481
+ <phase>package</phase>
482
+ <goals>
483
+ <goal>single</goal>
484
+ </goals>
485
+ <configuration>
486
+ <appendAssemblyId>true</appendAssemblyId>
487
+ <descriptors>
488
+ <descriptor>src/main/resources/build-a11y.xml</descriptor>
489
+ </descriptors>
490
+ </configuration>
491
+ </execution>
492
+ </executions>
493
+ </plugin>
494
+ </plugins>
495
+ </build>
496
+
497
+
498
+ </project>`;
499
+ }
500
+ // *.xml → Library Release 格式(假設現版 = release-v1.5.1.1-0.0.2)
501
+ return `<?xml version="1.0"?>\n<root>\n <version>release-v1.5.1.1-0.0.2-SNAPSHOT</version>\n</root>`;
502
+ },
503
+ getProjectVersions: async (projectKey, { name } = {}) => {
504
+ console.log(
505
+ c.gray(
506
+ ` [MOCK] getProjectVersions(${projectKey}, {name=${name}}) → null (mock:模擬找不到版本)`,
507
+ ),
508
+ );
509
+ return null;
510
+ },
511
+ // trigger_deployment 用:回傳 mock sub-task
512
+ getSubTasks: async (key) => {
513
+ const mockSubTasks = [
514
+ {
515
+ id: 'MOCK-1',
516
+ key: 'CID-DEPLOY-MOCK',
517
+ fields: {
518
+ summary: `[STG] Deployment for ${key}`,
519
+ status: { name: 'Open' },
520
+ issuetype: { name: 'Deployment' },
521
+ },
522
+ },
523
+ ];
524
+ console.log(
525
+ c.gray(` [MOCK] getSubTasks(${key}) → ${mockSubTasks.map((t) => t.key).join(', ')}`),
526
+ );
527
+ return mockSubTasks;
528
+ },
529
+ updateAssignee: async (issueKey, accountId) => {
530
+ printCall('PUT', `/issue/${issueKey}/assignee`, { name: accountId });
531
+ return { issueKey, accountId };
532
+ },
533
+ getUnreleasedVersionsList: async (projectKey) => {
534
+ console.log(c.gray(` [MOCK] getUnreleasedVersionsList(${projectKey})`));
535
+ return [
536
+ { id: '10001', name: 'IBK_1.5.2.0', released: false, archived: false },
537
+ { id: '10002', name: 'IBK_ssr_1.3.1.0', released: false, archived: false },
538
+ { id: '10003', name: 'IBK_wealth_1.1.0.0', released: false, archived: false },
539
+ { id: '10004', name: 'IBK_accessibility_1.5.1.0', released: false, archived: false },
540
+ { id: '10005', name: 'CWA_1.5.4.0', released: false, archived: false },
541
+ ];
542
+ },
543
+ getComments: async (issueKey) => {
544
+ console.log(c.gray(` [MOCK] getComments(${issueKey})`));
545
+ return [
546
+ {
547
+ author: { name: 'BK00178', displayName: 'James Yu' },
548
+ body: 'Approved',
549
+ created: new Date().toISOString(),
550
+ },
551
+ ];
552
+ },
553
+ };
554
+
555
+ // ─────────────────────────────────────────────────────────────────
556
+ // Live Jira(模式 2):讀取用真實 API,寫入全攔截
557
+ // ─────────────────────────────────────────────────────────────────
558
+ async function buildLiveJira() {
559
+ const { JiraClient } = await import('./jira-client.js');
560
+ const real = new JiraClient();
561
+
562
+ return {
563
+ // ── 讀取:直接呼叫真實 API,印出 response ──
564
+ getIssueFields: async (key, fieldNames) => {
565
+ const result = await real.getIssueFields(key, fieldNames);
566
+ printRead(key, fieldNames, result);
567
+ return result;
568
+ },
569
+ getIssue: async (key) => {
570
+ const result = await real.getIssue(key);
571
+ console.log(c.gray(` ← getIssue(${key}) status=${result.fields?.status?.name}`));
572
+ return result;
573
+ },
574
+ getTransitions: async (key) => {
575
+ const result = await real.getTransitions(key);
576
+ console.log(c.gray(` ← getTransitions(${key}) [${result.map((t) => t.name).join(', ')}]`));
577
+ return result;
578
+ },
579
+ getSubTasks: async (key) => {
580
+ const result = await real.getSubTasks(key);
581
+ console.log(
582
+ c.gray(
583
+ ` ← getSubTasks(${key}) → [${result.map((t) => `${t.key}(${t.fields?.summary?.slice(0, 30)})`).join(', ')}]`,
584
+ ),
585
+ );
586
+ return result;
587
+ },
588
+ searchIssues: async (jql, fields, max) => {
589
+ const result = await real.searchIssues(jql, fields, max);
590
+ console.log(c.gray(` ← searchIssues(${jql.slice(0, 60)}...) → ${result.length} results`));
591
+ return result;
592
+ },
593
+ getBitbucketTags: async (project, repo, opts) => {
594
+ const result = await real.getBitbucketTags(project, repo, opts);
595
+ console.log(
596
+ c.gray(` ← getBitbucketTags(${repo}) → [${result.map((t) => t.displayId).join(', ')}]`),
597
+ );
598
+ return result;
599
+ },
600
+ getBitbucketBranches: async (project, repo, opts) => {
601
+ const result = await real.getBitbucketBranches(project, repo, opts);
602
+ console.log(
603
+ c.gray(
604
+ ` ← getBitbucketBranches(${repo}) → [${result.map((b) => b.displayId).join(', ')}]`,
605
+ ),
606
+ );
607
+ return result;
608
+ },
609
+ getBitbucketFileContent: async (project, repo, filePath, branch) => {
610
+ const result = await real.getBitbucketFileContent(project, repo, filePath, branch);
611
+ console.log(
612
+ c.gray(
613
+ ` ← getBitbucketFileContent(${repo}/${filePath}@${branch}) → ${result.slice(0, 80)}...`,
614
+ ),
615
+ );
616
+ return result;
617
+ },
618
+ getProjectVersions: async (projectKey, opts = {}) => {
619
+ const result = await real.getProjectVersions(projectKey, opts);
620
+ console.log(
621
+ c.gray(` ← getProjectVersions(${projectKey}, {name=${opts.name}}) → ${result ?? 'null'}`),
622
+ );
623
+ return result;
624
+ },
625
+ getUnreleasedVersionsList: async (projectKey) => {
626
+ const result = await real.getUnreleasedVersionsList(projectKey);
627
+ console.log(
628
+ c.gray(` ← getUnreleasedVersionsList(${projectKey}) → ${result.length} versions`),
629
+ );
630
+ return result;
631
+ },
632
+ getComments: async (issueKey) => {
633
+ const result = await real.getComments(issueKey);
634
+ console.log(c.gray(` ← getComments(${issueKey}) → ${result.length} comments`));
635
+ return result;
636
+ },
637
+
638
+ // ── 寫入:全部攔截,只印出不執行 ──
639
+ createIssue: async (fields) => {
640
+ printCall('POST [INTERCEPTED]', '/issue', { fields });
641
+ return { id: 'DRY-RUN', key: 'CID-DRYRUN', self: '' };
642
+ },
643
+ updateIssue: async (key, fields) => {
644
+ printCall('PUT [INTERCEPTED]', `/issue/${key}`, { fields });
645
+ return {};
646
+ },
647
+ linkIssue: async (inward, outward, type) => {
648
+ printCall('POST [INTERCEPTED]', '/issueLink', {
649
+ type: { name: type },
650
+ inwardIssue: { key: inward },
651
+ outwardIssue: { key: outward },
652
+ });
653
+ return {};
654
+ },
655
+ addRemoteLink: async (issueKey, url, title) => {
656
+ printCall('POST [INTERCEPTED]', `/issue/${issueKey}/remotelink`, { object: { url, title } });
657
+ return {};
658
+ },
659
+ transitionById: async (key, id) => {
660
+ printCall('POST [INTERCEPTED]', `/issue/${key}/transitions`, { transition: { id } });
661
+ return {};
662
+ },
663
+ transitionByName: async (key, name) => {
664
+ printCall('POST [INTERCEPTED]', `/issue/${key}/transitions`, { transition: { name } });
665
+ return { transitioned: name, toStatus: name };
666
+ },
667
+ addComment: async (key, body) => {
668
+ printCall('POST [INTERCEPTED]', `/issue/${key}/comment`, { body });
669
+ return {};
670
+ },
671
+ updateAssignee: async (issueKey, accountId) => {
672
+ printCall('PUT [INTERCEPTED]', `/issue/${issueKey}/assignee`, { name: accountId });
673
+ return { issueKey, accountId };
674
+ },
675
+ };
676
+ }
677
+
678
+ // ─────────────────────────────────────────────────────────────────
679
+ // Main
680
+ // ─────────────────────────────────────────────────────────────────
681
+ const mockNotifier = { notify: async () => [] };
682
+
683
+ console.log(c.yellow(`\n🔍 Dry-run [${LIVE ? 'LIVE' : 'MOCK'}]: ${TOOL}`));
684
+ console.log('Args:', JSON.stringify(ARGS, null, 2));
685
+ if (LIVE) console.log(c.yellow('⚠️ Live mode: 讀取用真實 Jira,寫入全部攔截不執行'));
686
+
687
+ const jira = LIVE ? await buildLiveJira() : mockJira;
688
+
689
+ const result = await executeTool(TOOL, ARGS, { jira, notifier: mockNotifier });
690
+ console.log(c.green('\n✅ Tool result:'));
691
+ console.log(result.content[0].text);