@jira-deploy/core 1.0.2 → 1.0.4

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,11 +12,15 @@ 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
+ handleAutoGrayRelease,
21
+ handleDeployGrayRelease,
22
+ handleGetGrayReleaseStatus,
23
+ handleContinueGrayRelease,
20
24
  } from './grayrelease.js';
21
25
  import {
22
26
  getReleaseToolDefinitions,
@@ -25,7 +29,7 @@ import {
25
29
  handleGetReleaseManager,
26
30
  handleWaitForComment,
27
31
  } from './release.js';
28
- import {getJabberToolDefinitions, handleSendJabberMessage} from './jabber.js';
32
+ import { getJabberToolDefinitions, handleSendJabberMessage } from './jabber.js';
29
33
  import {
30
34
  getWorkflowToolDefinitions,
31
35
  handleRunLibToStgReleaseWorkflow,
@@ -39,6 +43,7 @@ const READ_ONLY_TOOL_NAMES = new Set([
39
43
  'get_unreleased_versions',
40
44
  'get_release_manager',
41
45
  'wait_for_comment',
46
+ 'get_grayrelease_status',
42
47
  ]);
43
48
 
44
49
  function withToolAnnotations(tools) {
@@ -84,7 +89,7 @@ export function getToolDefinitions() {
84
89
  type: 'object',
85
90
  required: ['issueKey', 'transitionName'],
86
91
  properties: {
87
- issueKey: {type: 'string', description: '例如 OPS-123'},
92
+ issueKey: { type: 'string', description: '例如 OPS-123' },
88
93
  transitionName: {
89
94
  type: 'string',
90
95
  description: '狀態名稱,例如 "Pending Approval"、"Approved"、"In Progress"、"Done"',
@@ -100,7 +105,7 @@ export function getToolDefinitions() {
100
105
  type: 'object',
101
106
  required: ['issueKey'],
102
107
  properties: {
103
- issueKey: {type: 'string'},
108
+ issueKey: { type: 'string' },
104
109
  targetStatus: {
105
110
  type: 'string',
106
111
  default: 'Approved',
@@ -124,7 +129,7 @@ export function getToolDefinitions() {
124
129
  type: 'object',
125
130
  required: ['issueKey'],
126
131
  properties: {
127
- issueKey: {type: 'string'},
132
+ issueKey: { type: 'string' },
128
133
  },
129
134
  },
130
135
  },
@@ -135,8 +140,8 @@ export function getToolDefinitions() {
135
140
  type: 'object',
136
141
  required: ['issueKey', 'message'],
137
142
  properties: {
138
- issueKey: {type: 'string'},
139
- message: {type: 'string', description: '留言內容'},
143
+ issueKey: { type: 'string' },
144
+ message: { type: 'string', description: '留言內容' },
140
145
  },
141
146
  },
142
147
  },
@@ -147,7 +152,7 @@ export function getToolDefinitions() {
147
152
  type: 'object',
148
153
  required: ['issueKey'],
149
154
  properties: {
150
- issueKey: {type: 'string'},
155
+ issueKey: { type: 'string' },
151
156
  },
152
157
  },
153
158
  },
@@ -159,8 +164,8 @@ export function getToolDefinitions() {
159
164
  type: 'object',
160
165
  required: ['inwardKey', 'outwardKey'],
161
166
  properties: {
162
- inwardKey: {type: 'string', description: '被包含方 issue key,例如 CD 單 CID-1669'},
163
- 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' },
164
169
  linkType: {
165
170
  type: 'string',
166
171
  description:
@@ -173,14 +178,33 @@ export function getToolDefinitions() {
173
178
  {
174
179
  name: 'build_ticket',
175
180
  description:
176
- '觸發 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。',
177
182
  inputSchema: {
178
183
  type: 'object',
179
184
  required: ['issueKey'],
180
185
  properties: {
181
186
  issueKey: {
182
187
  type: 'string',
183
- 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。',
193
+ },
194
+ },
195
+ },
196
+ },
197
+ {
198
+ name: 'wait_to_dev',
199
+ description:
200
+ 'CI 單 build 完成後,自動走完掃描流程切到 Wait To DEV 狀態。流程:Compliance Scan → Upload Scan Report → Accept → Wait To DEV(不執行 Dev Done)',
201
+ inputSchema: {
202
+ type: 'object',
203
+ required: ['issueKey'],
204
+ properties: {
205
+ issueKey: {
206
+ type: 'string',
207
+ description: 'CI issue key,例如 CID-1668',
184
208
  },
185
209
  },
186
210
  },
@@ -308,7 +332,8 @@ export function getToolDefinitions() {
308
332
  * 執行 tool,回傳 { content: [{ type: 'text', text }] }
309
333
  */
310
334
  export async function executeTool(name, args, deps) {
311
- const {jira, notifier} = deps;
335
+ const { jira, notifier } = deps;
336
+ const progress = typeof deps.progress === 'function' ? deps.progress : () => {};
312
337
  const poller = new Poller(jira);
313
338
 
314
339
  switch (name) {
@@ -330,6 +355,7 @@ export async function executeTool(name, args, deps) {
330
355
  const result = await poller.waitForStatus(args.issueKey, targetStatus, {
331
356
  intervalMs: args.pollIntervalMs,
332
357
  timeoutMs: args.timeoutMs,
358
+ onProgress: progress,
333
359
  });
334
360
 
335
361
  await notifier.notify(
@@ -351,43 +377,55 @@ export async function executeTool(name, args, deps) {
351
377
 
352
378
  case 'add_deploy_comment': {
353
379
  await notifier.notify(args.issueKey, args.message);
354
- return ok({issueKey: args.issueKey, commented: true});
380
+ return ok({ issueKey: args.issueKey, commented: true });
355
381
  }
356
382
 
357
383
  case 'list_transitions': {
358
384
  const transitions = await jira.getTransitions(args.issueKey);
359
- 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 })));
360
386
  }
361
387
 
362
388
  case 'create_library_ticket':
363
- return handleCreateLibraryTicket(args, {jira, notifier});
389
+ return handleCreateLibraryTicket(args, { jira, notifier });
364
390
 
365
391
  case 'create_ci_ticket': {
366
- return handleCreateCITicket(args, {jira, notifier});
392
+ return handleCreateCITicket(args, { jira, notifier });
367
393
  }
368
394
 
369
395
  case 'create_cd_ticket': {
370
- return handleCreateCDTicket(args, {jira, notifier});
396
+ return handleCreateCDTicket(args, { jira, notifier });
371
397
  }
372
398
 
373
399
  case 'create_grayrelease_ticket': {
374
- return handleCreateGrayReleaseTicket(args, {jira, notifier});
400
+ return handleCreateGrayReleaseTicket(args, { jira, notifier, progress });
375
401
  }
376
402
 
377
403
  case 'link_stg_grayrelease':
378
- return handleLinkStgGrayRelease(args, {jira, notifier});
404
+ return handleLinkStgGrayRelease(args, { jira, notifier, progress });
405
+
406
+ case 'auto_grayrelease':
407
+ return handleAutoGrayRelease(args, { jira, notifier, progress });
408
+
409
+ case 'deploy_grayrelease':
410
+ return handleDeployGrayRelease(args, { jira, notifier, progress });
411
+
412
+ case 'get_grayrelease_status':
413
+ return handleGetGrayReleaseStatus(args, { jira });
414
+
415
+ case 'continue_grayrelease':
416
+ return handleContinueGrayRelease(args, { jira, notifier, progress });
379
417
 
380
418
  case 'get_unreleased_versions':
381
- return handleGetUnreleasedVersions(args, {jira});
419
+ return handleGetUnreleasedVersions(args, { jira });
382
420
 
383
421
  case 'transition_to_wait_approval':
384
- return handleTransitionToWaitApproval(args, {jira, notifier});
422
+ return handleTransitionToWaitApproval(args, { jira, notifier });
385
423
 
386
424
  case 'get_release_manager':
387
425
  return handleGetReleaseManager(args, {});
388
426
 
389
427
  case 'wait_for_comment':
390
- return handleWaitForComment(args, {jira});
428
+ return handleWaitForComment(args, { jira, progress });
391
429
 
392
430
  case 'send_jabber_message':
393
431
  return handleSendJabberMessage(args, {});
@@ -398,6 +436,7 @@ export async function executeTool(name, args, deps) {
398
436
  notifier,
399
437
  executeToolImpl: deps.executeToolImpl ?? executeTool,
400
438
  workflowWaitOptions: deps.workflowWaitOptions,
439
+ progress,
401
440
  });
402
441
 
403
442
  case 'run_lib_to_stg_release':
@@ -406,6 +445,7 @@ export async function executeTool(name, args, deps) {
406
445
  notifier,
407
446
  executeToolImpl: deps.executeToolImpl ?? executeTool,
408
447
  workflowWaitOptions: deps.workflowWaitOptions,
448
+ progress,
409
449
  });
410
450
 
411
451
  case 'link_issues': {
@@ -420,7 +460,7 @@ export async function executeTool(name, args, deps) {
420
460
  }
421
461
 
422
462
  case 'build_ticket': {
423
- const {issueKey} = args;
463
+ const { issueKey, rebuild = false } = args;
424
464
  const log = [];
425
465
 
426
466
  /**
@@ -435,11 +475,21 @@ export async function executeTool(name, args, deps) {
435
475
  * Library Release 完整狀態流程:
436
476
  * TO DO → (Upload Lib Report) → UPLOAD LIB REPORT → (Apply for approval) → Wait Approval → (Approved) → WAIT FOR LIB BUILD → (Build) → Released
437
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,這裡不可處理部署流程
438
483
  */
439
484
 
440
- // 前置 transitions:若 "Build" 尚未出現,先觸發這些讓 Jira Automation 推進
441
- // 'Upload Lib Report':Library 票從 TO DO 的第一步(對應 UI 的 Upload Library Report)
485
+ const BUILD_TRANSITIONS = ['Build', 'GrayRelease Build'];
442
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
+ ];
443
493
  const POLL_INTERVAL = 3000; // 3s
444
494
  const MAX_WAIT_MS = 30000; // 最多等 30s(避免 MCP client timeout)
445
495
 
@@ -448,6 +498,11 @@ export async function executeTool(name, args, deps) {
448
498
  return list.find((t) => t.name.toLowerCase() === name.toLowerCase());
449
499
  };
450
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
+
451
506
  const waitForTransition = async (name) => {
452
507
  const deadline = Date.now() + MAX_WAIT_MS;
453
508
  while (Date.now() < deadline) {
@@ -458,17 +513,58 @@ export async function executeTool(name, args, deps) {
458
513
  return null;
459
514
  };
460
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
+
461
526
  try {
462
527
  // 記錄初始狀態(用於判斷是否有狀態推進)
463
528
  const initIssue = await jira.getIssue(issueKey);
464
529
  const initStatus = initIssue.fields.status.name;
465
530
 
466
- // Step 1: 確認目前是否已有 Build transition
467
- 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
+ }
468
564
 
469
565
  // Step 2: 若沒有,逐步觸發前置 transitions
470
566
  if (!buildTrans) {
471
- log.push('未找到 Build transition,逐步觸發前置狀態...');
567
+ log.push('未找到 Build/GrayRelease Build transition,逐步觸發前置狀態...');
472
568
  let preTriggered = false;
473
569
 
474
570
  // 最多嘗試 PRE_TRANSITIONS.length 輪,每輪觸發一個可用的前置 transition
@@ -486,9 +582,9 @@ export async function executeTool(name, args, deps) {
486
582
  // 等 Jira 更新狀態
487
583
  await new Promise((r) => setTimeout(r, 2000));
488
584
 
489
- buildTrans = await findTransition('Build');
585
+ buildTrans = await findAnyTransition(BUILD_TRANSITIONS);
490
586
  if (buildTrans) {
491
- log.push(` 已找到 Build transition`);
587
+ log.push(` 已找到 ${buildTrans.name} transition`);
492
588
  break;
493
589
  }
494
590
  }
@@ -496,7 +592,7 @@ export async function executeTool(name, args, deps) {
496
592
  // 若還沒出現,再給 Jira Automation 最多 30s
497
593
  if (!buildTrans) {
498
594
  log.push(' 等待 Jira Automation 推進(最多 30s)...');
499
- buildTrans = await waitForTransition('Build');
595
+ buildTrans = await waitForAnyTransition(BUILD_TRANSITIONS);
500
596
  }
501
597
 
502
598
  // 若仍未找到 Build transition,但有觸發前置 transition 且狀態已推進,
@@ -513,41 +609,83 @@ export async function executeTool(name, args, deps) {
513
609
  issueKey,
514
610
  `Library Build 已觸發(Jira Auto)狀態:${initStatus} → ${currentStatus}`,
515
611
  );
516
- return ok({issueKey, status: currentStatus, steps: log, autoTriggered: true});
612
+ return ok({ issueKey, status: currentStatus, steps: log, autoTriggered: true });
517
613
  }
518
614
  }
519
615
  }
520
616
 
521
617
  if (!buildTrans) {
522
618
  const issue = await jira.getIssue(issueKey);
523
- return error(`找不到 Build transition,目前狀態:${issue.fields.status.name}`);
619
+ return error(`找不到 Build/GrayRelease Build transition,目前狀態:${issue.fields.status.name}`);
524
620
  }
525
621
 
526
622
  // Step 4: 執行 Build
527
- log.push(`執行 Build transition (id: ${buildTrans.id})...`);
623
+ log.push(`執行 ${buildTrans.name} transition (id: ${buildTrans.id})...`);
528
624
  await jira.transitionById(issueKey, buildTrans.id);
529
625
 
530
626
  const issue = await jira.getIssue(issueKey);
531
627
  const newStatus = issue.fields.status.name;
532
- log.push(`✅ Build 已觸發,目前狀態:${newStatus}`);
533
- await notifier.notify(issueKey, `Jenkins Build 已觸發(${newStatus})`);
628
+ log.push(`✅ ${buildTrans.name} 已觸發,目前狀態:${newStatus}`);
629
+ await notifier.notify(issueKey, `Jenkins ${buildTrans.name} 已觸發(${newStatus})`);
534
630
 
535
- return ok({issueKey, status: newStatus, steps: log});
631
+ return ok({ issueKey, status: newStatus, steps: log });
536
632
  } catch (err) {
537
633
  return error(`build_ticket 失敗: ${err.message}`);
538
634
  }
539
635
  }
540
636
 
637
+ case 'wait_to_dev': {
638
+ const { issueKey } = args;
639
+ const log = [];
640
+
641
+ const STEPS = [
642
+ { transition: 'Upload Scan Report', targetStatus: 'Upload Report' },
643
+ { transition: 'Accept', targetStatus: 'Wait To DEV' },
644
+ ];
645
+ const finalTargetStatus = STEPS.at(-1).targetStatus;
646
+
647
+ try {
648
+ for (const step of STEPS) {
649
+ const transitions = await jira.getTransitions(issueKey);
650
+ const t = transitions.find((t) => t.name.toLowerCase() === step.transition.toLowerCase());
651
+ if (!t) {
652
+ const issue = await jira.getIssue(issueKey);
653
+ const current = issue.fields.status.name;
654
+ if (current.toLowerCase() === step.targetStatus.toLowerCase()) {
655
+ log.push(` 已是 ${current},跳過「${step.transition}」`);
656
+ continue;
657
+ }
658
+ if (current.toLowerCase() === finalTargetStatus.toLowerCase()) {
659
+ log.push(` 已是 ${current},流程已完成`);
660
+ break;
661
+ }
662
+ return error(`找不到 transition「${step.transition}」,目前狀態:${current}`);
663
+ }
664
+ log.push(`執行「${t.name}」→ ${step.targetStatus}`);
665
+ await jira.transitionById(issueKey, t.id);
666
+ }
667
+
668
+ const issue = await jira.getIssue(issueKey);
669
+ const finalStatus = issue.fields.status.name;
670
+ log.push(`✅ 完成,目前狀態:${finalStatus}`);
671
+ await notifier.notify(issueKey, `已切換至 ${finalStatus},可進行 DEV 部署`);
672
+
673
+ return ok({ issueKey, status: finalStatus, steps: log });
674
+ } catch (err) {
675
+ return error(`wait_to_dev 失敗: ${err.message}`);
676
+ }
677
+ }
678
+
541
679
  case 'wait_to_stg': {
542
- const {issueKey} = args;
680
+ const { issueKey } = args;
543
681
  const log = [];
544
682
 
545
683
  // CI 掃描 → STG 的標準流程步驟
546
684
  // 每個元素:{ transition: '名稱', targetStatus: '到達後的狀態名稱' }
547
685
  const STEPS = [
548
- {transition: 'Upload Scan Report', targetStatus: 'Upload Report'},
549
- {transition: 'Accept', targetStatus: 'Wait To DEV'},
550
- {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' },
551
689
  ];
552
690
 
553
691
  try {
@@ -573,14 +711,14 @@ export async function executeTool(name, args, deps) {
573
711
  log.push(`✅ 完成,目前狀態:${finalStatus}`);
574
712
  await notifier.notify(issueKey, `已切換至 ${finalStatus},可進行 STG 部署`);
575
713
 
576
- return ok({issueKey, status: finalStatus, steps: log});
714
+ return ok({ issueKey, status: finalStatus, steps: log });
577
715
  } catch (err) {
578
716
  return error(`wait_to_stg 失敗: ${err.message}`);
579
717
  }
580
718
  }
581
719
 
582
720
  case 'trigger_deployment': {
583
- const {cdIssueKey, environment} = args;
721
+ const { cdIssueKey, environment } = args;
584
722
  const log = [];
585
723
 
586
724
  // 判斷環境是否屬於 PRD 群組(prd / dr / prd/dr / prd&dr)
@@ -635,10 +773,10 @@ export async function executeTool(name, args, deps) {
635
773
  // 依序觸發的 Deployment transitions
636
774
  // conditionalWait: true → 依 needSwitch 決定是否執行並等待 180s
637
775
  const DEPLOY_TRANSITIONS = [
638
- {name: 'To Pretask'},
639
- {name: 'Swtich Execution Node', conditionalWait: true},
640
- {name: 'To AutoDeploy'},
641
- {name: 'Trigger AutoDeploy'},
776
+ { name: 'To Pretask' },
777
+ { name: 'Swtich Execution Node', conditionalWait: true },
778
+ { name: 'To AutoDeploy' },
779
+ { name: 'Trigger AutoDeploy' },
642
780
  ];
643
781
  // 環境字串對照(用於 sub-task summary 比對)
644
782
  const envUpper = environment.toUpperCase().replace('/', '').replace('&', '');
@@ -784,25 +922,34 @@ export async function executeTool(name, args, deps) {
784
922
  }
785
923
 
786
924
  case 'prepare_cd_deployment': {
787
- const {issueKey} = args;
925
+ const { issueKey } = args;
788
926
  // 正規化環境名稱(prd&dr → prd/dr)
789
927
  const envCode = args.environment.toLowerCase().replace('&', '/');
790
928
  const log = [];
791
929
 
792
930
  // CD 部署前置 transitions(依序嘗試,直到找到部署 transition 或完成申請流程)
793
- // STG 完整流程:
931
+ // DEV 流程會跳過通知主管簽核,建立 deployment 後直接嘗試自助 Approved / To Wait Deploy。
932
+ // STG/UAT/PRD 完整流程:
794
933
  // TO DO → (Accept) → Wait For Send Notice Email
795
934
  // → (Prepare to create deployment ticket) → Prepare For Deploy ← 建立 Deployment sub-task
796
935
  // → (Apply for approval) → Wait Approval ← 等 Reviewer 核准
797
936
  // → Wait Deploy ← 核准後自動切換,再呼叫 trigger_deployment
798
937
  // ⚠️ 不加 'Approved':Approved 需要其他主管才能執行
799
- const CD_PRE_TRANSITIONS = [
938
+ const CD_PRE_TRANSITIONS = envCode === 'dev' ? [
939
+ 'Accept',
940
+ ] : [
800
941
  'Accept',
801
942
  'Prepare to create deployment ticket',
802
943
  'Apply for approval',
803
944
  'To Wait Deploy',
804
945
  ];
805
946
 
947
+ const DEV_SELF_SERVICE_TRANSITIONS = [
948
+ 'Apply for approval',
949
+ 'Approved',
950
+ 'To Wait Deploy',
951
+ ];
952
+
806
953
  // 部署 transition 名稱(支援多種命名)
807
954
  const DEPLOY_TRANSITION_NAMES = [
808
955
  'Prepare to create deployment ticket', // 實際 Jira transition 名稱
@@ -822,7 +969,7 @@ export async function executeTool(name, args, deps) {
822
969
  // Step 2: 更新 CD 單的環境欄位(customfield_13436)
823
970
  if (CD_FIELD_IDS.env && ENV_CODES[envCode]) {
824
971
  await jira.updateIssue(issueKey, {
825
- [CD_FIELD_IDS.env]: {id: ENV_CODES[envCode]},
972
+ [CD_FIELD_IDS.env]: { id: ENV_CODES[envCode] },
826
973
  });
827
974
  log.push(`✅ 環境欄位更新為 ${envCode.toUpperCase()}`);
828
975
  }
@@ -839,6 +986,45 @@ export async function executeTool(name, args, deps) {
839
986
 
840
987
  let deployTrans = await findDeployTrans();
841
988
 
989
+ const getCurrentStatus = async () => {
990
+ const issue = await jira.getIssue(issueKey);
991
+ return issue.fields.status.name;
992
+ };
993
+
994
+ const runDevSelfServiceTransitions = async () => {
995
+ if (envCode !== 'dev') return;
996
+ for (const transitionName of DEV_SELF_SERVICE_TRANSITIONS) {
997
+ const currentStatus = await getCurrentStatus();
998
+ if (currentStatus.toLowerCase() === 'wait deploy') {
999
+ log.push(' DEV 自助流程已到 Wait Deploy,停止續跑');
1000
+ break;
1001
+ }
1002
+
1003
+ const transitions = await jira.getTransitions(issueKey);
1004
+ const next = transitions.find(
1005
+ (t) => t.name.toLowerCase() === transitionName.toLowerCase(),
1006
+ );
1007
+ if (!next) continue;
1008
+
1009
+ log.push(` DEV 自助流程觸發「${next.name}」...`);
1010
+ await jira.transitionById(issueKey, next.id);
1011
+ await new Promise((r) => setTimeout(r, 2000));
1012
+ }
1013
+ };
1014
+
1015
+ if (envCode === 'dev') {
1016
+ const currentStatus = await getCurrentStatus();
1017
+ if (currentStatus.toLowerCase() === 'wait deploy') {
1018
+ log.push('✅ CD 單已在 Wait Deploy,無需重複 prepare');
1019
+ return ok({
1020
+ issueKey,
1021
+ environment: envCode,
1022
+ status: currentStatus,
1023
+ steps: log,
1024
+ });
1025
+ }
1026
+ }
1027
+
842
1028
  if (!deployTrans) {
843
1029
  log.push('未找到部署 transition,逐步觸發前置狀態...');
844
1030
  for (const preName of CD_PRE_TRANSITIONS) {
@@ -858,6 +1044,25 @@ export async function executeTool(name, args, deps) {
858
1044
  }
859
1045
  }
860
1046
 
1047
+ if (!deployTrans && envCode === 'dev') {
1048
+ await runDevSelfServiceTransitions();
1049
+ const currentStatus = await getCurrentStatus();
1050
+ if (currentStatus.toLowerCase() === 'wait deploy') {
1051
+ log.push(`✅ DEV 自助流程完成,目前狀態:${currentStatus}`);
1052
+ await notifier.notify(
1053
+ issueKey,
1054
+ `CD 部署已觸發(環境: ${envCode.toUpperCase()},狀態: ${currentStatus})`,
1055
+ );
1056
+ return ok({
1057
+ issueKey,
1058
+ environment: envCode,
1059
+ status: currentStatus,
1060
+ steps: log,
1061
+ });
1062
+ }
1063
+ deployTrans = await findDeployTrans();
1064
+ }
1065
+
861
1066
  if (!deployTrans) {
862
1067
  const issue = await jira.getIssue(issueKey);
863
1068
  const transitions = await jira.getTransitions(issueKey);
@@ -871,21 +1076,37 @@ export async function executeTool(name, args, deps) {
871
1076
 
872
1077
  // Step 4: 觸发部署 transition
873
1078
  log.push(`執行「${deployTrans.name}」transition(id: ${deployTrans.id})...`);
874
- await jira.transitionById(issueKey, deployTrans.id);
1079
+ try {
1080
+ await jira.transitionById(issueKey, deployTrans.id);
1081
+ } catch (err) {
1082
+ if (envCode === 'dev' && err.message.includes('Already create deployment ticket')) {
1083
+ log.push(' Deployment ticket 已存在,繼續 DEV approval 續跑流程');
1084
+ } else {
1085
+ throw err;
1086
+ }
1087
+ }
875
1088
 
876
1089
  const issue = await jira.getIssue(issueKey);
877
1090
  const newStatus = issue.fields.status.name;
878
1091
  log.push(`✅ 部署已觸发,目前狀態:${newStatus}`);
879
1092
 
1093
+ await runDevSelfServiceTransitions();
1094
+
1095
+ const finalIssue = await jira.getIssue(issueKey);
1096
+ const finalStatus = finalIssue.fields.status.name;
1097
+ if (finalStatus !== newStatus) {
1098
+ log.push(`✅ DEV 自助流程完成,目前狀態:${finalStatus}`);
1099
+ }
1100
+
880
1101
  await notifier.notify(
881
1102
  issueKey,
882
- `CD 部署已觸發(環境: ${envCode.toUpperCase()},狀態: ${newStatus})`,
1103
+ `CD 部署已觸發(環境: ${envCode.toUpperCase()},狀態: ${finalStatus})`,
883
1104
  );
884
1105
 
885
1106
  return ok({
886
1107
  issueKey,
887
1108
  environment: envCode,
888
- status: newStatus,
1109
+ status: finalStatus,
889
1110
  steps: log,
890
1111
  });
891
1112
  } catch (err) {
@@ -894,8 +1115,8 @@ export async function executeTool(name, args, deps) {
894
1115
  }
895
1116
 
896
1117
  case 'update_assignee': {
897
- const {issueKey} = args;
898
- let {accountId, displayName} = args;
1118
+ const { issueKey } = args;
1119
+ let { accountId, displayName } = args;
899
1120
 
900
1121
  // 若只提供 displayName(或名稱),嘗試從 USER_MAP 查找 accountId
901
1122
  if (!accountId && displayName) {
@@ -916,7 +1137,7 @@ export async function executeTool(name, args, deps) {
916
1137
  await jira.updateAssignee(issueKey, accountId);
917
1138
  return ok({
918
1139
  issueKey,
919
- assignee: {accountId, displayName: displayName ?? accountId},
1140
+ assignee: { accountId, displayName: displayName ?? accountId },
920
1141
  message: `✅ ${issueKey} Assignee 已更新為 ${displayName ? `${displayName}(${accountId})` : accountId}`,
921
1142
  });
922
1143
  } catch (err) {
@@ -925,7 +1146,7 @@ export async function executeTool(name, args, deps) {
925
1146
  }
926
1147
 
927
1148
  case 'cancel_release': {
928
- const {systemCode} = args;
1149
+ const { systemCode } = args;
929
1150
  let ciIssueKey = args.ciIssueKey ?? null;
930
1151
 
931
1152
  try {
@@ -1017,46 +1238,46 @@ export async function executeTool(name, args, deps) {
1017
1238
  }
1018
1239
 
1019
1240
  case 'get_release_status': {
1020
- const {systemCode} = args;
1241
+ const { systemCode } = args;
1021
1242
 
1022
1243
  // 狀態 → 說明 + 建議下一步
1023
1244
  const describeStatus = (issueType, status) => {
1024
1245
  const s = status.toLowerCase();
1025
1246
  if (issueType === 'Library') {
1026
- if (s === 'to do') return {emoji: '⏳', desc: '尚未開始', next: `執行 build_ticket`};
1027
- if (s === 'upload lib report') return {emoji: '⏳', desc: '等待上傳 Lib Report', next: `執行 build_ticket`};
1028
- if (s === 'wait approval') return {emoji: '⏳', desc: '等待主管核准', next: `wait_for_approval`};
1029
- if (s === 'wait for lib build') return {emoji: '🔨', desc: '等待 Jenkins Build', next: `執行 build_ticket`};
1030
- if (s === 'released') return {emoji: '✅', desc: '已完成', next: '可開 CI'};
1031
- if (s === 'cancelled') return {emoji: '❌', desc: '已取消', next: null};
1032
- 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 };
1033
1254
  }
1034
1255
  if (issueType === 'CI') {
1035
- if (s === 'to do') return {emoji: '⏳', desc: '尚未開始', next: '執行 build_ticket'};
1036
- if (s === 'wait for build') return {emoji: '⏳', desc: '等待 Jenkins Build', next: '執行 build_ticket'};
1037
- if (s === 'compliance scan') return {emoji: '🔍', desc: '掃描中', next: '執行 wait_to_stg'};
1038
- if (s === 'upload report') return {emoji: '📋', desc: '上傳掃描報告', next: '執行 wait_to_stg'};
1039
- if (s === 'wait to dev') return {emoji: '🔄', desc: 'Build 完成,等待 DEV', next: '執行 wait_to_stg'};
1040
- if (s === 'wait to stg') return {emoji: '✅', desc: 'Build 完成,等待 STG 部署', next: '可建 CD(STG) 並執行 prepare_cd_deployment'};
1041
- if (s === 'wait to uat') return {emoji: '✅', desc: 'STG 完成,等待 UAT', next: '可建 CD(UAT)'};
1042
- if (s === 'wait for upload') return {emoji: '📦', desc: '等待上傳 Pre-Release', next: null};
1043
- if (s === 'wait to prd/dr') return {emoji: '✅', desc: 'UAT 完成,等待 PRD/DR', next: '可建 CD(PRD/DR)'};
1044
- if (s === 'done') return {emoji: '✅', desc: '已完成', next: null};
1045
- if (s === 'cancelled') return {emoji: '❌', desc: '已取消', next: null};
1046
- 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 };
1047
1268
  }
1048
1269
  if (issueType === 'CD') {
1049
- if (s === 'to do') return {emoji: '⏳', desc: '尚未開始', next: '執行 prepare_cd_deployment'};
1050
- if (s === 'wait for send notice email') return {emoji: '⏳', desc: '等待通知信', next: '執行 prepare_cd_deployment'};
1051
- if (s === 'prepare for deploy') return {emoji: '⏳', desc: '部署單建立中', next: null};
1052
- if (s === 'wait approval') return {emoji: '⏳', desc: '等待主管核准', next: 'wait_for_approval'};
1053
- if (s === 'wait deploy') return {emoji: '✅', desc: '已核准,等待部署', next: '執行 trigger_deployment'};
1054
- if (s === 'deploying') return {emoji: '🚀', desc: '部署中', next: null};
1055
- if (s === 'done') return {emoji: '✅', desc: '部署完成', next: null};
1056
- if (s === 'cancelled') return {emoji: '❌', desc: '已取消', next: null};
1057
- 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 };
1058
1279
  }
1059
- return {emoji: '🔄', desc: status, next: null};
1280
+ return { emoji: '🔄', desc: status, next: null };
1060
1281
  };
1061
1282
 
1062
1283
  try {
@@ -1096,9 +1317,8 @@ export async function executeTool(name, args, deps) {
1096
1317
  const linked = l.inwardIssue ?? l.outwardIssue;
1097
1318
  const d = describeStatus('Library', linked.fields.status?.name ?? '');
1098
1319
  const gitBranch = libraryExtraFields[i]?.customfield_13431 ?? '(無)';
1099
- return ` ${linked.key} [${linked.fields.status?.name}] ${d.emoji} ${d.desc}${
1100
- d.next ? ` → ${d.next}(${linked.key})` : ''
1101
- }\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 ?? ''}`;
1102
1322
  });
1103
1323
 
1104
1324
  // Step 3: 查近期 CD 單(取 5 筆)
@@ -1117,9 +1337,8 @@ export async function executeTool(name, args, deps) {
1117
1337
 
1118
1338
  const cdLines = Object.entries(cdByEnv).map(([env, cd]) => {
1119
1339
  const d = describeStatus('CD', cd.fields.status.name);
1120
- return ` ${env.toUpperCase()}:${cd.key} [${cd.fields.status.name}] ${d.emoji} ${d.desc}${
1121
- d.next ? ` → ${d.next}` : ''
1122
- }`;
1340
+ return ` ${env.toUpperCase()}:${cd.key} [${cd.fields.status.name}] ${d.emoji} ${d.desc}${d.next ? ` → ${d.next}` : ''
1341
+ }`;
1123
1342
  });
1124
1343
 
1125
1344
  // Step 4: 組合輸出
@@ -1128,8 +1347,7 @@ export async function executeTool(name, args, deps) {
1128
1347
  `📦 ${systemCode} Release 現況(${today})`,
1129
1348
  '',
1130
1349
  '── CI ──────────────────────────────',
1131
- ` ${ciIssue.key} [${ciIssue.fields.status.name}] ${ciStatus.emoji} ${ciStatus.desc}${
1132
- ciStatus.next ? ` → ${ciStatus.next}` : ''
1350
+ ` ${ciIssue.key} [${ciIssue.fields.status.name}] ${ciStatus.emoji} ${ciStatus.desc}${ciStatus.next ? ` → ${ciStatus.next}` : ''
1133
1351
  }`,
1134
1352
  ` 版本:${ciVersion}`,
1135
1353
  '',
@@ -1140,17 +1358,17 @@ export async function executeTool(name, args, deps) {
1140
1358
  ...(cdLines.length ? cdLines : [' (尚無 CD 單)']),
1141
1359
  ];
1142
1360
 
1143
- return ok({summary: lines.join('\n'), ci: ciIssue.key, version: ciVersion});
1361
+ return ok({ summary: lines.join('\n'), ci: ciIssue.key, version: ciVersion });
1144
1362
  } catch (err) {
1145
1363
  return error(`get_release_status 失敗: ${err.message}`);
1146
1364
  }
1147
1365
  }
1148
1366
 
1149
1367
  case 'get_next_lib_version':
1150
- return handleGetNextLibVersion(args, {jira});
1368
+ return handleGetNextLibVersion(args, { jira });
1151
1369
 
1152
1370
  case 'get_next_ci_version':
1153
- return handleGetNextCIVersion(args, {jira});
1371
+ return handleGetNextCIVersion(args, { jira });
1154
1372
 
1155
1373
  default:
1156
1374
  throw new Error(`Unknown tool: ${name}`);
@@ -1159,13 +1377,13 @@ export async function executeTool(name, args, deps) {
1159
1377
 
1160
1378
  function ok(data) {
1161
1379
  return {
1162
- content: [{type: 'text', text: JSON.stringify(data, null, 2)}],
1380
+ content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
1163
1381
  };
1164
1382
  }
1165
1383
 
1166
1384
  function error(msg) {
1167
1385
  return {
1168
- content: [{type: 'text', text: `❌ 錯誤: ${msg}`}],
1386
+ content: [{ type: 'text', text: `❌ 錯誤: ${msg}` }],
1169
1387
  isError: true,
1170
1388
  };
1171
1389
  }