@jira-deploy/core 1.0.3 → 1.0.5

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/tools/index.js CHANGED
@@ -1,10 +1,10 @@
1
- import {Poller} from '../poller.js';
1
+ import { Poller } from '../poller.js';
2
2
  import {
3
3
  getLibraryToolDefinitions,
4
4
  handleCreateLibraryTicket,
5
5
  handleGetNextLibVersion,
6
6
  } from './library.js';
7
- import {getCIToolDefinitions, handleCreateCITicket, handleGetNextCIVersion} from './ci.js';
7
+ import { getCIToolDefinitions, handleCreateCITicket, handleGetNextCIVersion } from './ci.js';
8
8
  import {
9
9
  SYSTEM_CODES,
10
10
  ENV_CODES,
@@ -12,12 +12,13 @@ import {
12
12
  SUPPORTED_ENVS,
13
13
  resolveAccountId,
14
14
  } from '../constants/index.js';
15
- import {getCDToolDefinitions, handleCreateCDTicket} from './cd.js';
15
+ import { getCDToolDefinitions, handleCreateCDTicket } from './cd.js';
16
16
  import {
17
17
  getGrayReleaseToolDefinitions,
18
18
  handleCreateGrayReleaseTicket,
19
19
  handleLinkStgGrayRelease,
20
20
  handleAutoGrayRelease,
21
+ handleDeployGrayRelease,
21
22
  handleGetGrayReleaseStatus,
22
23
  handleContinueGrayRelease,
23
24
  } from './grayrelease.js';
@@ -28,7 +29,7 @@ import {
28
29
  handleGetReleaseManager,
29
30
  handleWaitForComment,
30
31
  } from './release.js';
31
- import {getJabberToolDefinitions, handleSendJabberMessage} from './jabber.js';
32
+ import { getJabberToolDefinitions, handleSendJabberMessage } from './jabber.js';
32
33
  import {
33
34
  getWorkflowToolDefinitions,
34
35
  handleRunLibToStgReleaseWorkflow,
@@ -88,7 +89,7 @@ export function getToolDefinitions() {
88
89
  type: 'object',
89
90
  required: ['issueKey', 'transitionName'],
90
91
  properties: {
91
- issueKey: {type: 'string', description: '例如 OPS-123'},
92
+ issueKey: { type: 'string', description: '例如 OPS-123' },
92
93
  transitionName: {
93
94
  type: 'string',
94
95
  description: '狀態名稱,例如 "Pending Approval"、"Approved"、"In Progress"、"Done"',
@@ -104,7 +105,7 @@ export function getToolDefinitions() {
104
105
  type: 'object',
105
106
  required: ['issueKey'],
106
107
  properties: {
107
- issueKey: {type: 'string'},
108
+ issueKey: { type: 'string' },
108
109
  targetStatus: {
109
110
  type: 'string',
110
111
  default: 'Approved',
@@ -128,7 +129,7 @@ export function getToolDefinitions() {
128
129
  type: 'object',
129
130
  required: ['issueKey'],
130
131
  properties: {
131
- issueKey: {type: 'string'},
132
+ issueKey: { type: 'string' },
132
133
  },
133
134
  },
134
135
  },
@@ -139,8 +140,8 @@ export function getToolDefinitions() {
139
140
  type: 'object',
140
141
  required: ['issueKey', 'message'],
141
142
  properties: {
142
- issueKey: {type: 'string'},
143
- message: {type: 'string', description: '留言內容'},
143
+ issueKey: { type: 'string' },
144
+ message: { type: 'string', description: '留言內容' },
144
145
  },
145
146
  },
146
147
  },
@@ -151,7 +152,7 @@ export function getToolDefinitions() {
151
152
  type: 'object',
152
153
  required: ['issueKey'],
153
154
  properties: {
154
- issueKey: {type: 'string'},
155
+ issueKey: { type: 'string' },
155
156
  },
156
157
  },
157
158
  },
@@ -163,8 +164,8 @@ export function getToolDefinitions() {
163
164
  type: 'object',
164
165
  required: ['inwardKey', 'outwardKey'],
165
166
  properties: {
166
- inwardKey: {type: 'string', description: '被包含方 issue key,例如 CD 單 CID-1669'},
167
- outwardKey: {type: 'string', description: '包含方 issue key,例如 CI 單 CID-1668'},
167
+ inwardKey: { type: 'string', description: '被包含方 issue key,例如 CD 單 CID-1669' },
168
+ outwardKey: { type: 'string', description: '包含方 issue key,例如 CI 單 CID-1668' },
168
169
  linkType: {
169
170
  type: 'string',
170
171
  description:
@@ -177,14 +178,18 @@ export function getToolDefinitions() {
177
178
  {
178
179
  name: 'build_ticket',
179
180
  description:
180
- '觸發 Library 或 CI 上版單的 Jenkins Build。自動處理前置狀態切換與 Jira Automation 等待,不需要手動切換狀態。',
181
+ '觸發 Library、CIGrayRelease 上版單的 Jenkins Build。自動處理前置狀態切換與 Jira Automation 等待,不需要手動切換狀態。GrayRelease 僅在使用者明確要求 build/rebuild 時使用此 tool;若使用者要求 GrayRelease deploy、dev deploy 或部署灰度單,必須改用 deploy_grayrelease,不可用此 tool 觸發 rebuild。',
181
182
  inputSchema: {
182
183
  type: 'object',
183
184
  required: ['issueKey'],
184
185
  properties: {
185
186
  issueKey: {
186
187
  type: 'string',
187
- description: '要 build 的 issue key,例如 CID-1668',
188
+ description: '要 build 的 issue key,例如 CI/Library 單 CID-1668,或可重複 rebuild 的 GrayRelease 單 CID-822',
189
+ },
190
+ rebuild: {
191
+ type: 'boolean',
192
+ description: '(選填) 只有明確要重 build GrayRelease VERIFY 單時才設定 rebuild=true。GrayRelease deploy 請用 deploy_grayrelease。',
188
193
  },
189
194
  },
190
195
  },
@@ -327,7 +332,8 @@ export function getToolDefinitions() {
327
332
  * 執行 tool,回傳 { content: [{ type: 'text', text }] }
328
333
  */
329
334
  export async function executeTool(name, args, deps) {
330
- const {jira, notifier} = deps;
335
+ const { jira, notifier } = deps;
336
+ const progress = typeof deps.progress === 'function' ? deps.progress : () => {};
331
337
  const poller = new Poller(jira);
332
338
 
333
339
  switch (name) {
@@ -349,6 +355,7 @@ export async function executeTool(name, args, deps) {
349
355
  const result = await poller.waitForStatus(args.issueKey, targetStatus, {
350
356
  intervalMs: args.pollIntervalMs,
351
357
  timeoutMs: args.timeoutMs,
358
+ onProgress: progress,
352
359
  });
353
360
 
354
361
  await notifier.notify(
@@ -370,52 +377,55 @@ export async function executeTool(name, args, deps) {
370
377
 
371
378
  case 'add_deploy_comment': {
372
379
  await notifier.notify(args.issueKey, args.message);
373
- return ok({issueKey: args.issueKey, commented: true});
380
+ return ok({ issueKey: args.issueKey, commented: true });
374
381
  }
375
382
 
376
383
  case 'list_transitions': {
377
384
  const transitions = await jira.getTransitions(args.issueKey);
378
- return ok(transitions.map((t) => ({id: t.id, name: t.name, to: t.to.name})));
385
+ return ok(transitions.map((t) => ({ id: t.id, name: t.name, to: t.to.name })));
379
386
  }
380
387
 
381
388
  case 'create_library_ticket':
382
- return handleCreateLibraryTicket(args, {jira, notifier});
389
+ return handleCreateLibraryTicket(args, { jira, notifier });
383
390
 
384
391
  case 'create_ci_ticket': {
385
- return handleCreateCITicket(args, {jira, notifier});
392
+ return handleCreateCITicket(args, { jira, notifier });
386
393
  }
387
394
 
388
395
  case 'create_cd_ticket': {
389
- return handleCreateCDTicket(args, {jira, notifier});
396
+ return handleCreateCDTicket(args, { jira, notifier });
390
397
  }
391
398
 
392
399
  case 'create_grayrelease_ticket': {
393
- return handleCreateGrayReleaseTicket(args, {jira, notifier});
400
+ return handleCreateGrayReleaseTicket(args, { jira, notifier, progress });
394
401
  }
395
402
 
396
403
  case 'link_stg_grayrelease':
397
- return handleLinkStgGrayRelease(args, {jira, notifier});
404
+ return handleLinkStgGrayRelease(args, { jira, notifier, progress });
398
405
 
399
406
  case 'auto_grayrelease':
400
- return handleAutoGrayRelease(args, {jira, notifier});
407
+ return handleAutoGrayRelease(args, { jira, notifier, progress });
408
+
409
+ case 'deploy_grayrelease':
410
+ return handleDeployGrayRelease(args, { jira, notifier, progress });
401
411
 
402
412
  case 'get_grayrelease_status':
403
- return handleGetGrayReleaseStatus(args, {jira});
413
+ return handleGetGrayReleaseStatus(args, { jira });
404
414
 
405
415
  case 'continue_grayrelease':
406
- return handleContinueGrayRelease(args, {jira, notifier});
416
+ return handleContinueGrayRelease(args, { jira, notifier, progress });
407
417
 
408
418
  case 'get_unreleased_versions':
409
- return handleGetUnreleasedVersions(args, {jira});
419
+ return handleGetUnreleasedVersions(args, { jira });
410
420
 
411
421
  case 'transition_to_wait_approval':
412
- return handleTransitionToWaitApproval(args, {jira, notifier});
422
+ return handleTransitionToWaitApproval(args, { jira, notifier });
413
423
 
414
424
  case 'get_release_manager':
415
425
  return handleGetReleaseManager(args, {});
416
426
 
417
427
  case 'wait_for_comment':
418
- return handleWaitForComment(args, {jira});
428
+ return handleWaitForComment(args, { jira, progress });
419
429
 
420
430
  case 'send_jabber_message':
421
431
  return handleSendJabberMessage(args, {});
@@ -426,6 +436,7 @@ export async function executeTool(name, args, deps) {
426
436
  notifier,
427
437
  executeToolImpl: deps.executeToolImpl ?? executeTool,
428
438
  workflowWaitOptions: deps.workflowWaitOptions,
439
+ progress,
429
440
  });
430
441
 
431
442
  case 'run_lib_to_stg_release':
@@ -434,6 +445,7 @@ export async function executeTool(name, args, deps) {
434
445
  notifier,
435
446
  executeToolImpl: deps.executeToolImpl ?? executeTool,
436
447
  workflowWaitOptions: deps.workflowWaitOptions,
448
+ progress,
437
449
  });
438
450
 
439
451
  case 'link_issues': {
@@ -448,7 +460,7 @@ export async function executeTool(name, args, deps) {
448
460
  }
449
461
 
450
462
  case 'build_ticket': {
451
- const {issueKey} = args;
463
+ const { issueKey, rebuild = false } = args;
452
464
  const log = [];
453
465
 
454
466
  /**
@@ -463,11 +475,21 @@ export async function executeTool(name, args, deps) {
463
475
  * Library Release 完整狀態流程:
464
476
  * TO DO → (Upload Lib Report) → UPLOAD LIB REPORT → (Apply for approval) → Wait Approval → (Approved) → WAIT FOR LIB BUILD → (Build) → Released
465
477
  * 或:TO DO → (Upload Lib Report) → UPLOAD LIB REPORT → Jira Automation 自動觸發 Jenkins(無手動 Build)
478
+ *
479
+ * GrayRelease build/rebuild:
480
+ * - 一般 build:PLANNING → Accept → WAIT FOR BUILD → GrayRelease Build
481
+ * - VERIFY rebuild:只有 args.rebuild=true 時,才允許 Verify fail 回到 PLANNING 後重 build
482
+ * - GrayRelease deploy 已拆到 deploy_grayrelease,這裡不可處理部署流程
466
483
  */
467
484
 
468
- // 前置 transitions:若 "Build" 尚未出現,先觸發這些讓 Jira Automation 推進
469
- // 'Upload Lib Report':Library 票從 TO DO 的第一步(對應 UI 的 Upload Library Report)
485
+ const BUILD_TRANSITIONS = ['Build', 'GrayRelease Build'];
470
486
  const PRE_TRANSITIONS = ['Upload Lib Report', 'Apply for approval', 'Approved', 'Accept'];
487
+ const GRAYRELEASE_REBUILD_TRANSITIONS = [
488
+ 'Verify fail',
489
+ 'Back to Planning',
490
+ 'To Planning',
491
+ 'Planning',
492
+ ];
471
493
  const POLL_INTERVAL = 3000; // 3s
472
494
  const MAX_WAIT_MS = 30000; // 最多等 30s(避免 MCP client timeout)
473
495
 
@@ -476,6 +498,11 @@ export async function executeTool(name, args, deps) {
476
498
  return list.find((t) => t.name.toLowerCase() === name.toLowerCase());
477
499
  };
478
500
 
501
+ const findAnyTransition = async (names) => {
502
+ const list = await jira.getTransitions(issueKey);
503
+ return list.find((t) => names.some((name) => t.name.toLowerCase() === name.toLowerCase()));
504
+ };
505
+
479
506
  const waitForTransition = async (name) => {
480
507
  const deadline = Date.now() + MAX_WAIT_MS;
481
508
  while (Date.now() < deadline) {
@@ -486,17 +513,58 @@ export async function executeTool(name, args, deps) {
486
513
  return null;
487
514
  };
488
515
 
516
+ const waitForAnyTransition = async (names) => {
517
+ const deadline = Date.now() + MAX_WAIT_MS;
518
+ while (Date.now() < deadline) {
519
+ const t = await findAnyTransition(names);
520
+ if (t) return t;
521
+ await new Promise((r) => setTimeout(r, POLL_INTERVAL));
522
+ }
523
+ return null;
524
+ };
525
+
489
526
  try {
490
527
  // 記錄初始狀態(用於判斷是否有狀態推進)
491
528
  const initIssue = await jira.getIssue(issueKey);
492
529
  const initStatus = initIssue.fields.status.name;
493
530
 
494
- // Step 1: 確認目前是否已有 Build transition
495
- let buildTrans = await findTransition('Build');
531
+ // Step 1: 確認目前是否已有 Build transition(GrayRelease 名稱為 GrayRelease Build)
532
+ let buildTrans = await findAnyTransition(BUILD_TRANSITIONS);
533
+
534
+ const normalizedInitStatus = initStatus.trim().toUpperCase();
535
+
536
+ // GrayRelease 特例:VERIFY 狀態只有明確 rebuild=true 才允許回到可 build 流程。
537
+ if (!buildTrans && initStatus.toLowerCase() === 'verify') {
538
+ const resetTrans = await findAnyTransition(GRAYRELEASE_REBUILD_TRANSITIONS);
539
+ if (resetTrans && !rebuild) {
540
+ return error(
541
+ '目前為 VERIFY,build_ticket 不會自動重 build GrayRelease。若要部署請使用 deploy_grayrelease;若要重 build 請呼叫 build_ticket 並設定 rebuild=true。',
542
+ );
543
+ }
544
+ if (resetTrans) {
545
+ log.push(`目前為 VERIFY,GrayRelease rebuild 先觸發「${resetTrans.name}」回到可 build 流程...`);
546
+ await jira.transitionById(issueKey, resetTrans.id);
547
+ await new Promise((r) => setTimeout(r, 2000));
548
+ buildTrans = await findAnyTransition(BUILD_TRANSITIONS);
549
+ }
550
+ }
551
+
552
+ if (!buildTrans && ['WAIT APPROVAL', 'WAIT DEPLOY'].includes(normalizedInitStatus)) {
553
+ const transitions = await jira.getTransitions(issueKey);
554
+ const grayDeployTransition = transitions.find((t) => (
555
+ ['approve', 'grayrelease deploy', 'to verify', 'switch execution node']
556
+ .includes(t.name.toLowerCase())
557
+ ));
558
+ if (grayDeployTransition) {
559
+ return error(
560
+ `目前狀態 ${initStatus} 屬於 GrayRelease deploy 流程,請使用 deploy_grayrelease,不要用 build_ticket。`,
561
+ );
562
+ }
563
+ }
496
564
 
497
565
  // Step 2: 若沒有,逐步觸發前置 transitions
498
566
  if (!buildTrans) {
499
- log.push('未找到 Build transition,逐步觸發前置狀態...');
567
+ log.push('未找到 Build/GrayRelease Build transition,逐步觸發前置狀態...');
500
568
  let preTriggered = false;
501
569
 
502
570
  // 最多嘗試 PRE_TRANSITIONS.length 輪,每輪觸發一個可用的前置 transition
@@ -514,9 +582,9 @@ export async function executeTool(name, args, deps) {
514
582
  // 等 Jira 更新狀態
515
583
  await new Promise((r) => setTimeout(r, 2000));
516
584
 
517
- buildTrans = await findTransition('Build');
585
+ buildTrans = await findAnyTransition(BUILD_TRANSITIONS);
518
586
  if (buildTrans) {
519
- log.push(` 已找到 Build transition`);
587
+ log.push(` 已找到 ${buildTrans.name} transition`);
520
588
  break;
521
589
  }
522
590
  }
@@ -524,7 +592,7 @@ export async function executeTool(name, args, deps) {
524
592
  // 若還沒出現,再給 Jira Automation 最多 30s
525
593
  if (!buildTrans) {
526
594
  log.push(' 等待 Jira Automation 推進(最多 30s)...');
527
- buildTrans = await waitForTransition('Build');
595
+ buildTrans = await waitForAnyTransition(BUILD_TRANSITIONS);
528
596
  }
529
597
 
530
598
  // 若仍未找到 Build transition,但有觸發前置 transition 且狀態已推進,
@@ -541,38 +609,38 @@ export async function executeTool(name, args, deps) {
541
609
  issueKey,
542
610
  `Library Build 已觸發(Jira Auto)狀態:${initStatus} → ${currentStatus}`,
543
611
  );
544
- return ok({issueKey, status: currentStatus, steps: log, autoTriggered: true});
612
+ return ok({ issueKey, status: currentStatus, steps: log, autoTriggered: true });
545
613
  }
546
614
  }
547
615
  }
548
616
 
549
617
  if (!buildTrans) {
550
618
  const issue = await jira.getIssue(issueKey);
551
- return error(`找不到 Build transition,目前狀態:${issue.fields.status.name}`);
619
+ return error(`找不到 Build/GrayRelease Build transition,目前狀態:${issue.fields.status.name}`);
552
620
  }
553
621
 
554
622
  // Step 4: 執行 Build
555
- log.push(`執行 Build transition (id: ${buildTrans.id})...`);
623
+ log.push(`執行 ${buildTrans.name} transition (id: ${buildTrans.id})...`);
556
624
  await jira.transitionById(issueKey, buildTrans.id);
557
625
 
558
626
  const issue = await jira.getIssue(issueKey);
559
627
  const newStatus = issue.fields.status.name;
560
- log.push(`✅ Build 已觸發,目前狀態:${newStatus}`);
561
- await notifier.notify(issueKey, `Jenkins Build 已觸發(${newStatus})`);
628
+ log.push(`✅ ${buildTrans.name} 已觸發,目前狀態:${newStatus}`);
629
+ await notifier.notify(issueKey, `Jenkins ${buildTrans.name} 已觸發(${newStatus})`);
562
630
 
563
- return ok({issueKey, status: newStatus, steps: log});
631
+ return ok({ issueKey, status: newStatus, steps: log });
564
632
  } catch (err) {
565
633
  return error(`build_ticket 失敗: ${err.message}`);
566
634
  }
567
635
  }
568
636
 
569
637
  case 'wait_to_dev': {
570
- const {issueKey} = args;
638
+ const { issueKey } = args;
571
639
  const log = [];
572
640
 
573
641
  const STEPS = [
574
- {transition: 'Upload Scan Report', targetStatus: 'Upload Report'},
575
- {transition: 'Accept', targetStatus: 'Wait To DEV'},
642
+ { transition: 'Upload Scan Report', targetStatus: 'Upload Report' },
643
+ { transition: 'Accept', targetStatus: 'Wait To DEV' },
576
644
  ];
577
645
  const finalTargetStatus = STEPS.at(-1).targetStatus;
578
646
 
@@ -602,22 +670,22 @@ export async function executeTool(name, args, deps) {
602
670
  log.push(`✅ 完成,目前狀態:${finalStatus}`);
603
671
  await notifier.notify(issueKey, `已切換至 ${finalStatus},可進行 DEV 部署`);
604
672
 
605
- return ok({issueKey, status: finalStatus, steps: log});
673
+ return ok({ issueKey, status: finalStatus, steps: log });
606
674
  } catch (err) {
607
675
  return error(`wait_to_dev 失敗: ${err.message}`);
608
676
  }
609
677
  }
610
678
 
611
679
  case 'wait_to_stg': {
612
- const {issueKey} = args;
680
+ const { issueKey } = args;
613
681
  const log = [];
614
682
 
615
683
  // CI 掃描 → STG 的標準流程步驟
616
684
  // 每個元素:{ transition: '名稱', targetStatus: '到達後的狀態名稱' }
617
685
  const STEPS = [
618
- {transition: 'Upload Scan Report', targetStatus: 'Upload Report'},
619
- {transition: 'Accept', targetStatus: 'Wait To DEV'},
620
- {transition: 'Dev Done', targetStatus: 'Wait To STG'},
686
+ { transition: 'Upload Scan Report', targetStatus: 'Upload Report' },
687
+ { transition: 'Accept', targetStatus: 'Wait To DEV' },
688
+ { transition: 'Dev Done', targetStatus: 'Wait To STG' },
621
689
  ];
622
690
 
623
691
  try {
@@ -643,14 +711,14 @@ export async function executeTool(name, args, deps) {
643
711
  log.push(`✅ 完成,目前狀態:${finalStatus}`);
644
712
  await notifier.notify(issueKey, `已切換至 ${finalStatus},可進行 STG 部署`);
645
713
 
646
- return ok({issueKey, status: finalStatus, steps: log});
714
+ return ok({ issueKey, status: finalStatus, steps: log });
647
715
  } catch (err) {
648
716
  return error(`wait_to_stg 失敗: ${err.message}`);
649
717
  }
650
718
  }
651
719
 
652
720
  case 'trigger_deployment': {
653
- const {cdIssueKey, environment} = args;
721
+ const { cdIssueKey, environment } = args;
654
722
  const log = [];
655
723
 
656
724
  // 判斷環境是否屬於 PRD 群組(prd / dr / prd/dr / prd&dr)
@@ -659,8 +727,8 @@ export async function executeTool(name, args, deps) {
659
727
 
660
728
  // 查詢同系統最近一筆已完成的部署(CD 或 GrayRelease),回傳 'prd' | 'nonPrd' | null
661
729
  const getLastDeployEnvGroup = async (systemCode, excludeKey) => {
662
- const jql_cd = `project = CID AND issuetype = CD AND "System Code[Select List (single choice)]" = "${systemCode}" AND status = Done AND issueKey != "${excludeKey}" ORDER BY updated DESC`;
663
- const jql_gr = `project = CID AND issuetype = GrayRelease AND "System Code[Select List (single choice)]" = "${systemCode}" AND status = Done ORDER BY updated DESC`;
730
+ const jql_cd = `project = CID AND issuetype = CD AND text ~ "${systemCode}" AND status = Done AND issueKey != "${excludeKey}" ORDER BY updated DESC`;
731
+ const jql_gr = `project = CID AND issuetype = GrayRelease AND text ~ "${systemCode}" AND status = Done ORDER BY updated DESC`;
664
732
 
665
733
  const [cdResults, grResults] = await Promise.all([
666
734
  jira.searchIssues(jql_cd, ['customfield_13436', 'updated'], 1).catch(() => []),
@@ -705,10 +773,10 @@ export async function executeTool(name, args, deps) {
705
773
  // 依序觸發的 Deployment transitions
706
774
  // conditionalWait: true → 依 needSwitch 決定是否執行並等待 180s
707
775
  const DEPLOY_TRANSITIONS = [
708
- {name: 'To Pretask'},
709
- {name: 'Swtich Execution Node', conditionalWait: true},
710
- {name: 'To AutoDeploy'},
711
- {name: 'Trigger AutoDeploy'},
776
+ { name: 'To Pretask' },
777
+ { name: 'Swtich Execution Node', conditionalWait: true },
778
+ { name: 'To AutoDeploy' },
779
+ { name: 'Trigger AutoDeploy' },
712
780
  ];
713
781
  // 環境字串對照(用於 sub-task summary 比對)
714
782
  const envUpper = environment.toUpperCase().replace('/', '').replace('&', '');
@@ -854,7 +922,7 @@ export async function executeTool(name, args, deps) {
854
922
  }
855
923
 
856
924
  case 'prepare_cd_deployment': {
857
- const {issueKey} = args;
925
+ const { issueKey } = args;
858
926
  // 正規化環境名稱(prd&dr → prd/dr)
859
927
  const envCode = args.environment.toLowerCase().replace('&', '/');
860
928
  const log = [];
@@ -901,7 +969,7 @@ export async function executeTool(name, args, deps) {
901
969
  // Step 2: 更新 CD 單的環境欄位(customfield_13436)
902
970
  if (CD_FIELD_IDS.env && ENV_CODES[envCode]) {
903
971
  await jira.updateIssue(issueKey, {
904
- [CD_FIELD_IDS.env]: {id: ENV_CODES[envCode]},
972
+ [CD_FIELD_IDS.env]: { id: ENV_CODES[envCode] },
905
973
  });
906
974
  log.push(`✅ 環境欄位更新為 ${envCode.toUpperCase()}`);
907
975
  }
@@ -1047,8 +1115,8 @@ export async function executeTool(name, args, deps) {
1047
1115
  }
1048
1116
 
1049
1117
  case 'update_assignee': {
1050
- const {issueKey} = args;
1051
- let {accountId, displayName} = args;
1118
+ const { issueKey } = args;
1119
+ let { accountId, displayName } = args;
1052
1120
 
1053
1121
  // 若只提供 displayName(或名稱),嘗試從 USER_MAP 查找 accountId
1054
1122
  if (!accountId && displayName) {
@@ -1069,7 +1137,7 @@ export async function executeTool(name, args, deps) {
1069
1137
  await jira.updateAssignee(issueKey, accountId);
1070
1138
  return ok({
1071
1139
  issueKey,
1072
- assignee: {accountId, displayName: displayName ?? accountId},
1140
+ assignee: { accountId, displayName: displayName ?? accountId },
1073
1141
  message: `✅ ${issueKey} Assignee 已更新為 ${displayName ? `${displayName}(${accountId})` : accountId}`,
1074
1142
  });
1075
1143
  } catch (err) {
@@ -1078,7 +1146,7 @@ export async function executeTool(name, args, deps) {
1078
1146
  }
1079
1147
 
1080
1148
  case 'cancel_release': {
1081
- const {systemCode} = args;
1149
+ const { systemCode } = args;
1082
1150
  let ciIssueKey = args.ciIssueKey ?? null;
1083
1151
 
1084
1152
  try {
@@ -1170,46 +1238,46 @@ export async function executeTool(name, args, deps) {
1170
1238
  }
1171
1239
 
1172
1240
  case 'get_release_status': {
1173
- const {systemCode} = args;
1241
+ const { systemCode } = args;
1174
1242
 
1175
1243
  // 狀態 → 說明 + 建議下一步
1176
1244
  const describeStatus = (issueType, status) => {
1177
1245
  const s = status.toLowerCase();
1178
1246
  if (issueType === 'Library') {
1179
- if (s === 'to do') return {emoji: '⏳', desc: '尚未開始', next: `執行 build_ticket`};
1180
- if (s === 'upload lib report') return {emoji: '⏳', desc: '等待上傳 Lib Report', next: `執行 build_ticket`};
1181
- if (s === 'wait approval') return {emoji: '⏳', desc: '等待主管核准', next: `wait_for_approval`};
1182
- if (s === 'wait for lib build') return {emoji: '🔨', desc: '等待 Jenkins Build', next: `執行 build_ticket`};
1183
- if (s === 'released') return {emoji: '✅', desc: '已完成', next: '可開 CI'};
1184
- if (s === 'cancelled') return {emoji: '❌', desc: '已取消', next: null};
1185
- return {emoji: '🔄', desc: status, next: null};
1247
+ if (s === 'to do') return { emoji: '⏳', desc: '尚未開始', next: `執行 build_ticket` };
1248
+ if (s === 'upload lib report') return { emoji: '⏳', desc: '等待上傳 Lib Report', next: `執行 build_ticket` };
1249
+ if (s === 'wait approval') return { emoji: '⏳', desc: '等待主管核准', next: `wait_for_approval` };
1250
+ if (s === 'wait for lib build') return { emoji: '🔨', desc: '等待 Jenkins Build', next: `執行 build_ticket` };
1251
+ if (s === 'released') return { emoji: '✅', desc: '已完成', next: '可開 CI' };
1252
+ if (s === 'cancelled') return { emoji: '❌', desc: '已取消', next: null };
1253
+ return { emoji: '🔄', desc: status, next: null };
1186
1254
  }
1187
1255
  if (issueType === 'CI') {
1188
- if (s === 'to do') return {emoji: '⏳', desc: '尚未開始', next: '執行 build_ticket'};
1189
- if (s === 'wait for build') return {emoji: '⏳', desc: '等待 Jenkins Build', next: '執行 build_ticket'};
1190
- if (s === 'compliance scan') return {emoji: '🔍', desc: '掃描中', next: '執行 wait_to_stg'};
1191
- if (s === 'upload report') return {emoji: '📋', desc: '上傳掃描報告', next: '執行 wait_to_stg'};
1192
- if (s === 'wait to dev') return {emoji: '🔄', desc: 'Build 完成,等待 DEV', next: '執行 wait_to_stg'};
1193
- if (s === 'wait to stg') return {emoji: '✅', desc: 'Build 完成,等待 STG 部署', next: '可建 CD(STG) 並執行 prepare_cd_deployment'};
1194
- if (s === 'wait to uat') return {emoji: '✅', desc: 'STG 完成,等待 UAT', next: '可建 CD(UAT)'};
1195
- if (s === 'wait for upload') return {emoji: '📦', desc: '等待上傳 Pre-Release', next: null};
1196
- if (s === 'wait to prd/dr') return {emoji: '✅', desc: 'UAT 完成,等待 PRD/DR', next: '可建 CD(PRD/DR)'};
1197
- if (s === 'done') return {emoji: '✅', desc: '已完成', next: null};
1198
- if (s === 'cancelled') return {emoji: '❌', desc: '已取消', next: null};
1199
- return {emoji: '🔄', desc: status, next: null};
1256
+ if (s === 'to do') return { emoji: '⏳', desc: '尚未開始', next: '執行 build_ticket' };
1257
+ if (s === 'wait for build') return { emoji: '⏳', desc: '等待 Jenkins Build', next: '執行 build_ticket' };
1258
+ if (s === 'compliance scan') return { emoji: '🔍', desc: '掃描中', next: '執行 wait_to_stg' };
1259
+ if (s === 'upload report') return { emoji: '📋', desc: '上傳掃描報告', next: '執行 wait_to_stg' };
1260
+ if (s === 'wait to dev') return { emoji: '🔄', desc: 'Build 完成,等待 DEV', next: '執行 wait_to_stg' };
1261
+ if (s === 'wait to stg') return { emoji: '✅', desc: 'Build 完成,等待 STG 部署', next: '可建 CD(STG) 並執行 prepare_cd_deployment' };
1262
+ if (s === 'wait to uat') return { emoji: '✅', desc: 'STG 完成,等待 UAT', next: '可建 CD(UAT)' };
1263
+ if (s === 'wait for upload') return { emoji: '📦', desc: '等待上傳 Pre-Release', next: null };
1264
+ if (s === 'wait to prd/dr') return { emoji: '✅', desc: 'UAT 完成,等待 PRD/DR', next: '可建 CD(PRD/DR)' };
1265
+ if (s === 'done') return { emoji: '✅', desc: '已完成', next: null };
1266
+ if (s === 'cancelled') return { emoji: '❌', desc: '已取消', next: null };
1267
+ return { emoji: '🔄', desc: status, next: null };
1200
1268
  }
1201
1269
  if (issueType === 'CD') {
1202
- if (s === 'to do') return {emoji: '⏳', desc: '尚未開始', next: '執行 prepare_cd_deployment'};
1203
- if (s === 'wait for send notice email') return {emoji: '⏳', desc: '等待通知信', next: '執行 prepare_cd_deployment'};
1204
- if (s === 'prepare for deploy') return {emoji: '⏳', desc: '部署單建立中', next: null};
1205
- if (s === 'wait approval') return {emoji: '⏳', desc: '等待主管核准', next: 'wait_for_approval'};
1206
- if (s === 'wait deploy') return {emoji: '✅', desc: '已核准,等待部署', next: '執行 trigger_deployment'};
1207
- if (s === 'deploying') return {emoji: '🚀', desc: '部署中', next: null};
1208
- if (s === 'done') return {emoji: '✅', desc: '部署完成', next: null};
1209
- if (s === 'cancelled') return {emoji: '❌', desc: '已取消', next: null};
1210
- return {emoji: '🔄', desc: status, next: null};
1270
+ if (s === 'to do') return { emoji: '⏳', desc: '尚未開始', next: '執行 prepare_cd_deployment' };
1271
+ if (s === 'wait for send notice email') return { emoji: '⏳', desc: '等待通知信', next: '執行 prepare_cd_deployment' };
1272
+ if (s === 'prepare for deploy') return { emoji: '⏳', desc: '部署單建立中', next: null };
1273
+ if (s === 'wait approval') return { emoji: '⏳', desc: '等待主管核准', next: 'wait_for_approval' };
1274
+ if (s === 'wait deploy') return { emoji: '✅', desc: '已核准,等待部署', next: '執行 trigger_deployment' };
1275
+ if (s === 'deploying') return { emoji: '🚀', desc: '部署中', next: null };
1276
+ if (s === 'done') return { emoji: '✅', desc: '部署完成', next: null };
1277
+ if (s === 'cancelled') return { emoji: '❌', desc: '已取消', next: null };
1278
+ return { emoji: '🔄', desc: status, next: null };
1211
1279
  }
1212
- return {emoji: '🔄', desc: status, next: null};
1280
+ return { emoji: '🔄', desc: status, next: null };
1213
1281
  };
1214
1282
 
1215
1283
  try {
@@ -1249,9 +1317,8 @@ export async function executeTool(name, args, deps) {
1249
1317
  const linked = l.inwardIssue ?? l.outwardIssue;
1250
1318
  const d = describeStatus('Library', linked.fields.status?.name ?? '');
1251
1319
  const gitBranch = libraryExtraFields[i]?.customfield_13431 ?? '(無)';
1252
- return ` ${linked.key} [${linked.fields.status?.name}] ${d.emoji} ${d.desc}${
1253
- d.next ? ` → ${d.next}(${linked.key})` : ''
1254
- }\n branch: ${gitBranch}\n ${linked.fields.summary ?? ''}`;
1320
+ return ` ${linked.key} [${linked.fields.status?.name}] ${d.emoji} ${d.desc}${d.next ? ` → ${d.next}(${linked.key})` : ''
1321
+ }\n branch: ${gitBranch}\n ${linked.fields.summary ?? ''}`;
1255
1322
  });
1256
1323
 
1257
1324
  // Step 3: 查近期 CD 單(取 5 筆)
@@ -1270,9 +1337,8 @@ export async function executeTool(name, args, deps) {
1270
1337
 
1271
1338
  const cdLines = Object.entries(cdByEnv).map(([env, cd]) => {
1272
1339
  const d = describeStatus('CD', cd.fields.status.name);
1273
- return ` ${env.toUpperCase()}:${cd.key} [${cd.fields.status.name}] ${d.emoji} ${d.desc}${
1274
- d.next ? ` → ${d.next}` : ''
1275
- }`;
1340
+ return ` ${env.toUpperCase()}:${cd.key} [${cd.fields.status.name}] ${d.emoji} ${d.desc}${d.next ? ` → ${d.next}` : ''
1341
+ }`;
1276
1342
  });
1277
1343
 
1278
1344
  // Step 4: 組合輸出
@@ -1281,8 +1347,7 @@ export async function executeTool(name, args, deps) {
1281
1347
  `📦 ${systemCode} Release 現況(${today})`,
1282
1348
  '',
1283
1349
  '── CI ──────────────────────────────',
1284
- ` ${ciIssue.key} [${ciIssue.fields.status.name}] ${ciStatus.emoji} ${ciStatus.desc}${
1285
- ciStatus.next ? ` → ${ciStatus.next}` : ''
1350
+ ` ${ciIssue.key} [${ciIssue.fields.status.name}] ${ciStatus.emoji} ${ciStatus.desc}${ciStatus.next ? ` → ${ciStatus.next}` : ''
1286
1351
  }`,
1287
1352
  ` 版本:${ciVersion}`,
1288
1353
  '',
@@ -1293,17 +1358,17 @@ export async function executeTool(name, args, deps) {
1293
1358
  ...(cdLines.length ? cdLines : [' (尚無 CD 單)']),
1294
1359
  ];
1295
1360
 
1296
- return ok({summary: lines.join('\n'), ci: ciIssue.key, version: ciVersion});
1361
+ return ok({ summary: lines.join('\n'), ci: ciIssue.key, version: ciVersion });
1297
1362
  } catch (err) {
1298
1363
  return error(`get_release_status 失敗: ${err.message}`);
1299
1364
  }
1300
1365
  }
1301
1366
 
1302
1367
  case 'get_next_lib_version':
1303
- return handleGetNextLibVersion(args, {jira});
1368
+ return handleGetNextLibVersion(args, { jira });
1304
1369
 
1305
1370
  case 'get_next_ci_version':
1306
- return handleGetNextCIVersion(args, {jira});
1371
+ return handleGetNextCIVersion(args, { jira });
1307
1372
 
1308
1373
  default:
1309
1374
  throw new Error(`Unknown tool: ${name}`);
@@ -1312,13 +1377,13 @@ export async function executeTool(name, args, deps) {
1312
1377
 
1313
1378
  function ok(data) {
1314
1379
  return {
1315
- content: [{type: 'text', text: JSON.stringify(data, null, 2)}],
1380
+ content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
1316
1381
  };
1317
1382
  }
1318
1383
 
1319
1384
  function error(msg) {
1320
1385
  return {
1321
- content: [{type: 'text', text: `❌ 錯誤: ${msg}`}],
1386
+ content: [{ type: 'text', text: `❌ 錯誤: ${msg}` }],
1322
1387
  isError: true,
1323
1388
  };
1324
1389
  }