@jira-deploy/core 1.0.14 → 1.0.16

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,39 +1,46 @@
1
1
  import { Poller } from '../poller.js';
2
+ import { getBuildToolDefinitions, handleWaitBuildResult } from './build.js';
3
+ import { getBranchPRToolDefinitions, handleValidateBranchNoOpenPR } from './branch-prs.js';
2
4
  import {
3
5
  getLibraryToolDefinitions,
6
+ handleBuildLibrary,
4
7
  handleCreateLibraryTicket,
5
- handleGetNextLibVersion,
6
8
  } from './library.js';
7
- import { getCIToolDefinitions, handleCreateCITicket, handleGetNextCIVersion } from './ci.js';
8
9
  import {
9
- SYSTEM_CODES,
10
- ENV_CODES,
11
- CD_FIELD_IDS,
12
- SUPPORTED_ENVS,
13
- resolveAccountId,
14
- } from '../constants/index.js';
15
- import { getCDToolDefinitions, handleCreateCDTicket } from './cd.js';
10
+ getCIToolDefinitions,
11
+ handleBuildCI,
12
+ handleCreateCITicket,
13
+ handleGetNextCIVersion,
14
+ handleWaitToDev,
15
+ handleWaitToPrdDr,
16
+ handleWaitToStg,
17
+ handleWaitToUat,
18
+ } from './ci.js';
19
+ import { CD_FIELD_IDS, ENV_CODES, resolveAccountId, SUPPORTED_ENVS, } from '../constants/index.js';
20
+ import { getCDToolDefinitions, handleCDApproval, handleCreateCDTicket } from './cd.js';
21
+ import { getDeploymentToolDefinitions, handleTriggerDeployment } from './deployment.js';
16
22
  import {
17
23
  getGrayReleaseToolDefinitions,
18
- handleCreateGrayReleaseTicket,
19
- handleLinkStgGrayRelease,
20
24
  handleAutoGrayRelease,
25
+ handleBuildGrayRelease,
26
+ handleContinueGrayRelease,
27
+ handleCreateGrayReleaseTicket,
21
28
  handleDeployGrayRelease,
22
29
  handleGetGrayReleaseStatus,
23
- handleContinueGrayRelease,
30
+ handleLinkStgGrayRelease,
24
31
  } from './grayrelease.js';
25
32
  import {
26
33
  getReleaseToolDefinitions,
34
+ handleGetReleaseManager,
27
35
  handleGetUnreleasedVersions,
28
36
  handleTransitionToWaitApproval,
29
- handleGetReleaseManager,
30
37
  handleWaitForComment,
31
38
  } from './release.js';
32
39
  import { getJabberToolDefinitions, handleSendJabberMessage } from './jabber.js';
33
40
  import {
34
41
  getWorkflowToolDefinitions,
35
- handleRunLibToStgReleaseWorkflow,
36
- handleRunStgFullReleaseWorkflow,
42
+ handleContinueReleaseToCdReadyWorkflow,
43
+ handleRunReleaseToStgWorkflow,
37
44
  } from './workflows.js';
38
45
 
39
46
  const READ_ONLY_TOOL_NAMES = new Set([
@@ -44,6 +51,8 @@ const READ_ONLY_TOOL_NAMES = new Set([
44
51
  'get_release_manager',
45
52
  'wait_for_comment',
46
53
  'get_grayrelease_status',
54
+ 'wait_build_result',
55
+ 'validate_branch_no_open_pr',
47
56
  ]);
48
57
 
49
58
  function withToolAnnotations(tools) {
@@ -75,9 +84,12 @@ function withToolAnnotations(tools) {
75
84
  */
76
85
  export function getToolDefinitions() {
77
86
  return withToolAnnotations([
87
+ ...getBuildToolDefinitions(),
88
+ ...getBranchPRToolDefinitions(),
78
89
  ...getLibraryToolDefinitions(),
79
90
  ...getCIToolDefinitions(),
80
91
  ...getCDToolDefinitions(),
92
+ ...getDeploymentToolDefinitions(),
81
93
  ...getGrayReleaseToolDefinitions(),
82
94
  ...getReleaseToolDefinitions(),
83
95
  ...getJabberToolDefinitions(),
@@ -175,83 +187,10 @@ export function getToolDefinitions() {
175
187
  },
176
188
  },
177
189
  },
178
- {
179
- name: 'build_ticket',
180
- description:
181
- '觸發 Library、CI 或 GrayRelease 上版單的 Jenkins Build。自動處理前置狀態切換與 Jira Automation 等待,不需要手動切換狀態。GrayRelease 僅在使用者明確要求 build/rebuild 時使用此 tool;若使用者要求 GrayRelease deploy、dev deploy 或部署灰度單,必須改用 deploy_grayrelease,不可用此 tool 觸發 rebuild。',
182
- inputSchema: {
183
- type: 'object',
184
- required: ['issueKey'],
185
- properties: {
186
- issueKey: {
187
- type: 'string',
188
- description: '要 build 的 issue key,例如 CI、Library 或 GrayRelease 單',
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',
208
- },
209
- },
210
- },
211
- },
212
- {
213
- name: 'wait_to_stg',
214
- description:
215
- 'CI 單 build 完成後,自動走完掃描流程切到 Wait To STG 狀態。流程:Compliance Scan → Upload Scan Report → Accept → Dev Done → Wait To STG',
216
- inputSchema: {
217
- type: 'object',
218
- required: ['issueKey'],
219
- properties: {
220
- issueKey: {
221
- type: 'string',
222
- description: 'CI issue key',
223
- },
224
- },
225
- },
226
- },
227
- {
228
- name: 'trigger_deployment',
229
- description:
230
- '在 CD 單的 Deployment sub-task 上依序觸發部署 transitions(To Pretask → To AutoDeploy → Trigger AutoDeploy)。接在 prepare_cd_deployment 之後使用。當使用者說「幫我 Deploy」、「上版」、「deploy」時優先使用這個 tool',
231
- inputSchema: {
232
- type: 'object',
233
- required: ['cdIssueKey', 'environment'],
234
- properties: {
235
- cdIssueKey: {
236
- type: 'string',
237
- description: 'CD 單 issue key',
238
- },
239
- environment: {
240
- type: 'string',
241
- enum: SUPPORTED_ENVS.cd,
242
- description: `部署目標環境:${SUPPORTED_ENVS.cd.join(' / ')}`,
243
- },
244
- applyForClose: {
245
- type: 'boolean',
246
- description: '部署觸發後是否同步對 CD 單執行 Apply for close(預設 false)',
247
- },
248
- },
249
- },
250
- },
251
190
  {
252
191
  name: 'prepare_cd_deployment',
253
192
  description:
254
- '在 CD 單點擊「Prepare to Create Deployment」觸發部署。自動更新環境欄位並執行 transition,啟動 Jenkins 部署流程。',
193
+ '在 CD 單點擊「Prepare to Create Deployment」觸發部署前置流程;非 DEV 會依環境處理 CD 簽核,核准後自動推進到 Wait Deploy。',
255
194
  inputSchema: {
256
195
  type: 'object',
257
196
  required: ['issueKey', 'environment'],
@@ -333,7 +272,8 @@ export function getToolDefinitions() {
333
272
  */
334
273
  export async function executeTool(name, args, deps) {
335
274
  const { jira, notifier } = deps;
336
- const progress = typeof deps.progress === 'function' ? deps.progress : () => {};
275
+ const progress = typeof deps.progress === 'function' ? deps.progress : () => {
276
+ };
337
277
  const poller = new Poller(jira);
338
278
 
339
279
  switch (name) {
@@ -347,6 +287,9 @@ export async function executeTool(name, args, deps) {
347
287
  }
348
288
  }
349
289
 
290
+ case 'validate_branch_no_open_pr':
291
+ return handleValidateBranchNoOpenPR(args, { jira });
292
+
350
293
  case 'wait_for_approval': {
351
294
  const targetStatus = args.targetStatus ?? 'Approved';
352
295
 
@@ -388,12 +331,18 @@ export async function executeTool(name, args, deps) {
388
331
  case 'create_library_ticket':
389
332
  return handleCreateLibraryTicket(args, { jira, notifier });
390
333
 
334
+ case 'build_library':
335
+ return handleBuildLibrary(args, { jira, notifier, progress });
336
+
391
337
  case 'create_ci_ticket': {
392
338
  return handleCreateCITicket(args, { jira, notifier });
393
339
  }
394
340
 
341
+ case 'build_ci':
342
+ return handleBuildCI(args, { jira, notifier, progress });
343
+
395
344
  case 'create_cd_ticket': {
396
- return handleCreateCDTicket(args, { jira, notifier });
345
+ return handleCreateCDTicket(args, { jira, notifier, progress });
397
346
  }
398
347
 
399
348
  case 'create_grayrelease_ticket': {
@@ -409,9 +358,15 @@ export async function executeTool(name, args, deps) {
409
358
  case 'deploy_grayrelease':
410
359
  return handleDeployGrayRelease(args, { jira, notifier, progress });
411
360
 
361
+ case 'build_grayrelease':
362
+ return handleBuildGrayRelease(args, { jira, notifier, progress });
363
+
412
364
  case 'get_grayrelease_status':
413
365
  return handleGetGrayReleaseStatus(args, { jira });
414
366
 
367
+ case 'wait_build_result':
368
+ return handleWaitBuildResult(args, { jira, progress });
369
+
415
370
  case 'continue_grayrelease':
416
371
  return handleContinueGrayRelease(args, { jira, notifier, progress });
417
372
 
@@ -422,7 +377,7 @@ export async function executeTool(name, args, deps) {
422
377
  return handleTransitionToWaitApproval(args, { jira, notifier });
423
378
 
424
379
  case 'get_release_manager':
425
- return handleGetReleaseManager(args, {});
380
+ return handleGetReleaseManager(args);
426
381
 
427
382
  case 'wait_for_comment':
428
383
  return handleWaitForComment(args, { jira, progress });
@@ -430,8 +385,8 @@ export async function executeTool(name, args, deps) {
430
385
  case 'send_jabber_message':
431
386
  return handleSendJabberMessage(args, {});
432
387
 
433
- case 'run_stg_full_release':
434
- return handleRunStgFullReleaseWorkflow(args, {
388
+ case 'run_release_to_stg':
389
+ return handleRunReleaseToStgWorkflow(args, {
435
390
  jira,
436
391
  notifier,
437
392
  executeToolImpl: deps.executeToolImpl ?? executeTool,
@@ -439,8 +394,8 @@ export async function executeTool(name, args, deps) {
439
394
  progress,
440
395
  });
441
396
 
442
- case 'run_lib_to_stg_release':
443
- return handleRunLibToStgReleaseWorkflow(args, {
397
+ case 'continue_release_to_cd_ready':
398
+ return handleContinueReleaseToCdReadyWorkflow(args, {
444
399
  jira,
445
400
  notifier,
446
401
  executeToolImpl: deps.executeToolImpl ?? executeTool,
@@ -459,467 +414,20 @@ export async function executeTool(name, args, deps) {
459
414
  }
460
415
  }
461
416
 
462
- case 'build_ticket': {
463
- const { issueKey, rebuild = false } = args;
464
- const log = [];
465
-
466
- /**
467
- * CI Release 完整狀態流程:
468
- * TO DO → (Accept) → Wait for Build → (Build) → Compliance Scan
469
- * → (Upload Scan Report) → Upload Report → (Accept) → Wait To DEV
470
- * → (Dev Done) → Wait To STG → (STG Done) → Wait To UAT
471
- * → (UAT Done) → Wait For Upload → (Upload To Pre-Release)
472
- * → Wait For Upload → (Upload Done) → Wait To PRD/DR
473
- * → (PRD/DR Done)
474
- *
475
- * Library Release 完整狀態流程:
476
- * TO DO → (Upload Lib Report) → UPLOAD LIB REPORT → (Apply for approval) → Wait Approval → (Approved) → WAIT FOR LIB BUILD → (Build) → Released
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,這裡不可處理部署流程
483
- */
484
-
485
- const BUILD_TRANSITIONS = ['Build', 'GrayRelease Build'];
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
- ];
493
- const POLL_INTERVAL = 3000; // 3s
494
- const MAX_WAIT_MS = 30000; // 最多等 30s(避免 MCP client timeout)
495
-
496
- const findTransition = async (name) => {
497
- const list = await jira.getTransitions(issueKey);
498
- return list.find((t) => t.name.toLowerCase() === name.toLowerCase());
499
- };
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
-
506
- const waitForTransition = async (name) => {
507
- const deadline = Date.now() + MAX_WAIT_MS;
508
- while (Date.now() < deadline) {
509
- const t = await findTransition(name);
510
- if (t) return t;
511
- await new Promise((r) => setTimeout(r, POLL_INTERVAL));
512
- }
513
- return null;
514
- };
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
-
526
- try {
527
- // 記錄初始狀態(用於判斷是否有狀態推進)
528
- const initIssue = await jira.getIssue(issueKey);
529
- const initStatus = initIssue.fields.status.name;
530
-
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
- }
564
-
565
- // Step 2: 若沒有,逐步觸發前置 transitions
566
- if (!buildTrans) {
567
- log.push('未找到 Build/GrayRelease Build transition,逐步觸發前置狀態...');
568
- let preTriggered = false;
569
-
570
- // 最多嘗試 PRE_TRANSITIONS.length 輪,每輪觸發一個可用的前置 transition
571
- for (let step = 0; step < PRE_TRANSITIONS.length; step++) {
572
- const transitions = await jira.getTransitions(issueKey);
573
- const pre = transitions.find((t) =>
574
- PRE_TRANSITIONS.some((name) => t.name.toLowerCase() === name.toLowerCase()),
575
- );
576
- if (!pre) break;
577
-
578
- log.push(` [step ${step + 1}] 觸發「${pre.name}」...`);
579
- await jira.transitionById(issueKey, pre.id).catch(() => {
580
- });
581
- preTriggered = true;
582
- // 等 Jira 更新狀態
583
- await new Promise((r) => setTimeout(r, 2000));
584
-
585
- buildTrans = await findAnyTransition(BUILD_TRANSITIONS);
586
- if (buildTrans) {
587
- log.push(` 已找到 ${buildTrans.name} transition`);
588
- break;
589
- }
590
- }
591
-
592
- // 若還沒出現,再給 Jira Automation 最多 30s
593
- if (!buildTrans) {
594
- log.push(' 等待 Jira Automation 推進(最多 30s)...');
595
- buildTrans = await waitForAnyTransition(BUILD_TRANSITIONS);
596
- }
597
-
598
- // 若仍未找到 Build transition,但有觸發前置 transition 且狀態已推進,
599
- // 代表此 Library workflow 由 Jira Automation 自動觸發 Jenkins(無需手動 Build)
600
- if (!buildTrans && preTriggered) {
601
- const currentIssue = await jira.getIssue(issueKey);
602
- const currentStatus = currentIssue.fields.status.name;
603
- if (currentStatus !== initStatus) {
604
- log.push(
605
- `⚠️ 無手動 Build transition,但狀態已推進:${initStatus} → ${currentStatus}`,
606
- );
607
- log.push(' Jenkins Build 可能已由 Jira Automation 自動觸發');
608
- await notifier.notify(
609
- issueKey,
610
- `Library Build 已觸發(Jira Auto)狀態:${initStatus} → ${currentStatus}`,
611
- );
612
- return ok({ issueKey, status: currentStatus, steps: log, autoTriggered: true });
613
- }
614
- }
615
- }
616
-
617
- if (!buildTrans) {
618
- const issue = await jira.getIssue(issueKey);
619
- return error(`找不到 Build/GrayRelease Build transition,目前狀態:${issue.fields.status.name}`);
620
- }
621
-
622
- // Step 4: 執行 Build
623
- log.push(`執行 ${buildTrans.name} transition (id: ${buildTrans.id})...`);
624
- await jira.transitionById(issueKey, buildTrans.id);
625
-
626
- const issue = await jira.getIssue(issueKey);
627
- const newStatus = issue.fields.status.name;
628
- log.push(`✅ ${buildTrans.name} 已觸發,目前狀態:${newStatus}`);
629
- await notifier.notify(issueKey, `Jenkins ${buildTrans.name} 已觸發(${newStatus})`);
630
-
631
- return ok({ issueKey, status: newStatus, steps: log });
632
- } catch (err) {
633
- return error(`build_ticket 失敗: ${err.message}`);
634
- }
635
- }
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
-
679
- case 'wait_to_stg': {
680
- const { issueKey } = args;
681
- const log = [];
682
-
683
- // CI 掃描 → STG 的標準流程步驟
684
- // 每個元素:{ transition: '名稱', targetStatus: '到達後的狀態名稱' }
685
- const STEPS = [
686
- { transition: 'Upload Scan Report', targetStatus: 'Upload Report' },
687
- { transition: 'Accept', targetStatus: 'Wait To DEV' },
688
- { transition: 'Dev Done', targetStatus: 'Wait To STG' },
689
- ];
690
-
691
- try {
692
- for (const step of STEPS) {
693
- const transitions = await jira.getTransitions(issueKey);
694
- const t = transitions.find((t) => t.name.toLowerCase() === step.transition.toLowerCase());
695
- if (!t) {
696
- // 確認目前狀態是否已達目標,若是則跳過此步
697
- const issue = await jira.getIssue(issueKey);
698
- const current = issue.fields.status.name;
699
- if (current.toLowerCase() === step.targetStatus.toLowerCase()) {
700
- log.push(` 已是 ${current},跳過「${step.transition}」`);
701
- continue;
702
- }
703
- return error(`找不到 transition「${step.transition}」,目前狀態:${current}`);
704
- }
705
- log.push(`執行「${t.name}」→ ${step.targetStatus}`);
706
- await jira.transitionById(issueKey, t.id);
707
- }
708
-
709
- const issue = await jira.getIssue(issueKey);
710
- const finalStatus = issue.fields.status.name;
711
- log.push(`✅ 完成,目前狀態:${finalStatus}`);
712
- await notifier.notify(issueKey, `已切換至 ${finalStatus},可進行 STG 部署`);
713
-
714
- return ok({ issueKey, status: finalStatus, steps: log });
715
- } catch (err) {
716
- return error(`wait_to_stg 失敗: ${err.message}`);
717
- }
718
- }
719
-
720
- case 'trigger_deployment': {
721
- const { cdIssueKey, environment } = args;
722
- const log = [];
723
-
724
- // 判斷環境是否屬於 PRD 群組(prd / dr / prd/dr / prd&dr)
725
- const isPrdEnv = (env) =>
726
- ['prd', 'dr', 'prd/dr', 'prd&dr'].includes(env.toLowerCase().trim());
727
-
728
- // 查詢同系統最近一筆已完成的部署(CD 或 GrayRelease),回傳 'prd' | 'nonPrd' | null
729
- const getLastDeployEnvGroup = async (systemCode, excludeKey) => {
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`;
417
+ case 'wait_to_dev':
418
+ return handleWaitToDev(args, { jira, notifier, progress });
732
419
 
733
- const [cdResults, grResults] = await Promise.all([
734
- jira.searchIssues(jql_cd, ['customfield_13436', 'updated'], 1).catch(() => []),
735
- jira.searchIssues(jql_gr, ['updated'], 1).catch(() => []),
736
- ]);
420
+ case 'wait_to_stg':
421
+ return handleWaitToStg(args, { jira, notifier, progress });
737
422
 
738
- const cdIssue = cdResults[0] ?? null;
739
- const grIssue = grResults[0] ?? null;
423
+ case 'wait_to_uat':
424
+ return handleWaitToUat(args, { jira, notifier, progress });
740
425
 
741
- // 沒有任何記錄 → 保守執行
742
- if (!cdIssue && !grIssue) return null;
426
+ case 'wait_to_prd_dr':
427
+ return handleWaitToPrdDr(args, { jira, notifier, progress });
743
428
 
744
- // 若只有其中一筆,直接用那筆
745
- // 若兩筆都有,取 updated 較新者
746
- let lastIssue = null;
747
- let lastIsGray = false;
748
-
749
- if (cdIssue && grIssue) {
750
- const cdUpdated = new Date(cdIssue.fields?.updated ?? 0);
751
- const grUpdated = new Date(grIssue.fields?.updated ?? 0);
752
- if (grUpdated > cdUpdated) {
753
- lastIssue = grIssue;
754
- lastIsGray = true;
755
- } else {
756
- lastIssue = cdIssue;
757
- }
758
- } else if (grIssue) {
759
- lastIssue = grIssue;
760
- lastIsGray = true;
761
- } else {
762
- lastIssue = cdIssue;
763
- }
764
-
765
- // GrayRelease 永遠是 nonPrd
766
- if (lastIsGray) return 'nonPrd';
767
-
768
- // CD:讀 customfield_13436.value 判斷群組
769
- const envVal = lastIssue.fields?.customfield_13436?.value ?? '';
770
- return isPrdEnv(envVal) ? 'prd' : 'nonPrd';
771
- };
772
-
773
- // 依序觸發的 Deployment transitions
774
- // conditionalWait: true → 依 needSwitch 決定是否執行並等待 180s
775
- const DEPLOY_TRANSITIONS = [
776
- { name: 'To Pretask' },
777
- { name: 'Swtich Execution Node', conditionalWait: true },
778
- { name: 'To AutoDeploy' },
779
- { name: 'Trigger AutoDeploy' },
780
- ];
781
- // 環境字串對照(用於 sub-task summary 比對)
782
- const envUpper = environment.toUpperCase().replace('/', '').replace('&', '');
783
-
784
- try {
785
- // Step 1: 取得 CD 單的 sub-tasks
786
- const subTasks = await jira.getSubTasks(cdIssueKey);
787
- if (!subTasks.length) {
788
- return error(
789
- `CD 單 ${cdIssueKey} 目前沒有 Deployment sub-task,請先執行 prepare_cd_deployment`,
790
- );
791
- }
792
-
793
- log.push(`找到 ${subTasks.length} 個 sub-task:${subTasks.map((t) => t.key).join(', ')}`);
794
-
795
- // Step 2: 依環境篩選 Deployment sub-task(比對 summary 中的環境標籤)
796
- // 支援格式:[STG]、[UAT]、[PRD]、[DR]、[PRD/DR]、PRDDR 等
797
- const envVariants = [envUpper, `[${envUpper}]`];
798
- let deploymentKey = null;
799
-
800
- for (const st of subTasks) {
801
- const summaryUpper = (st.fields?.summary ?? '').toUpperCase().replace(/[\s/]/g, '');
802
- if (envVariants.some((v) => summaryUpper.includes(v.replace(/[\s/]/g, '')))) {
803
- deploymentKey = st.key;
804
- break;
805
- }
806
- }
807
-
808
- // fallback:若沒有符合環境的,取第一個 sub-task
809
- if (!deploymentKey) {
810
- deploymentKey = subTasks[0].key;
811
- log.push(
812
- ` ⚠️ 未找到明確符合環境 ${environment} 的 sub-task,使用第一個:${deploymentKey}`,
813
- );
814
- } else {
815
- log.push(` 對應環境 ${environment} 的 Deployment 單:${deploymentKey}`);
816
- }
817
-
818
- // Step 2.5: 判斷是否需要執行 Switch Execution Node
819
- // 規則:上次部署環境群組(prd/nonPrd)與本次不同時才需要切換 Ansible instance
820
- const thisGroup = isPrdEnv(environment) ? 'prd' : 'nonPrd';
821
- let needSwitch = true; // 預設保守執行
822
-
823
- try {
824
- const cdFields = await jira.getIssueFields(cdIssueKey, ['customfield_13443']);
825
- const systemCode = cdFields?.customfield_13443?.value;
826
- if (systemCode) {
827
- const lastGroup = await getLastDeployEnvGroup(systemCode, cdIssueKey);
828
- if (lastGroup === null) {
829
- log.push(` ⚠️ 找不到 ${systemCode} 的歷史部署記錄,保守執行 Switch Execution Node`);
830
- } else if (lastGroup === thisGroup) {
831
- needSwitch = false;
832
- log.push(` ✅ 上次部署同為 ${lastGroup} 群組,跳過 Switch Execution Node`);
833
- } else {
834
- log.push(` 🔄 環境群組變更(${lastGroup} → ${thisGroup}),需執行 Switch Execution Node`);
835
- }
836
- } else {
837
- log.push(` ⚠️ 無法取得 systemCode,保守執行 Switch Execution Node`);
838
- }
839
- } catch (e) {
840
- log.push(` ⚠️ 判斷 Switch 條件失敗(${e.message}),保守執行`);
841
- }
842
-
843
- // Step 3: 依序執行 transitions
844
- for (const step of DEPLOY_TRANSITIONS) {
845
- // conditionalWait: 依 needSwitch 決定是否執行
846
- if (step.conditionalWait && !needSwitch) {
847
- log.push(` ⏭️ 跳過「${step.name}」(同環境群組,無需切換 Ansible instance)`);
848
- continue;
849
- }
850
-
851
- const transitions = await jira.getTransitions(deploymentKey);
852
- const t = transitions.find((t) => t.name.toLowerCase() === step.name.toLowerCase());
853
-
854
- if (!t) {
855
- const issue = await jira.getIssue(deploymentKey);
856
- const available = transitions.map((t) => t.name).join(', ');
857
- if (['To AutoDeploy', 'Trigger AutoDeploy'].includes(step.name)) {
858
- return error(
859
- `找不到必要部署 transition「${step.name}」,目前狀態:${issue.fields.status.name},可用:${available || '無'}`,
860
- );
861
- }
862
- log.push(
863
- ` ⚠️ 找不到「${step.name}」(目前狀態:${issue.fields.status.name},可用:${available || '無'}),跳過`,
864
- );
865
- continue;
866
- }
867
-
868
- log.push(` 執行「${t.name}」...`);
869
- await jira.transitionById(deploymentKey, t.id);
870
- await new Promise((r) => setTimeout(r, 2000));
871
-
872
- if (step.conditionalWait) {
873
- const SWITCH_WAIT_MS = 180_000;
874
- const secs = SWITCH_WAIT_MS / 1000;
875
- log.push(` ⏳ 等待 ${secs}s(等待 Ansible instance 切換完成)...`);
876
- await notifier.notify(deploymentKey, `⏳ 等待 ${secs}s 後繼續部署...`);
877
- await new Promise((r) => setTimeout(r, SWITCH_WAIT_MS));
878
- log.push(` ✅ 等待完成,繼續執行`);
879
- }
880
- }
881
-
882
- const finalIssue = await jira.getIssue(deploymentKey);
883
- const finalStatus = finalIssue.fields.status.name;
884
- log.push(`✅ Deployment 已觸发,目前狀態:${finalStatus}`);
885
- await notifier.notify(
886
- deploymentKey,
887
- `Deployment 部署已觸發(環境:${environment.toUpperCase()},狀態:${finalStatus})`,
888
- );
889
-
890
- // 若 applyForClose=true,同步對 CD 單觸發 Apply for close
891
- if (args.applyForClose) {
892
- try {
893
- const cdTransitions = await jira.getTransitions(cdIssueKey);
894
- const closeT = cdTransitions.find((t) => t.name.toLowerCase() === 'apply for close');
895
- if (closeT) {
896
- log.push(` 對 CD 單 ${cdIssueKey} 執行「${closeT.name}」...`);
897
- await jira.transitionById(cdIssueKey, closeT.id);
898
- const cdIssue = await jira.getIssue(cdIssueKey);
899
- log.push(` CD 單狀態:${cdIssue.fields.status.name}`);
900
- await notifier.notify(cdIssueKey, `CD 單已提交關閉申請(Apply for close)`);
901
- } else {
902
- const available = cdTransitions.map((t) => t.name).join(', ');
903
- log.push(
904
- ` ⚠️ CD 單找不到「Apply for close」transition(可用:${available || '無'})`,
905
- );
906
- }
907
- } catch (e) {
908
- log.push(` ⚠️ Apply for close 失敗:${e.message}`);
909
- }
910
- }
911
-
912
- return ok({
913
- cdIssueKey,
914
- deploymentKey,
915
- environment,
916
- status: finalStatus,
917
- steps: log,
918
- });
919
- } catch (err) {
920
- return error(`trigger_deployment 失敗: ${err.message}`);
921
- }
922
- }
429
+ case 'trigger_deployment':
430
+ return handleTriggerDeployment(args, { jira, notifier, progress });
923
431
 
924
432
  case 'prepare_cd_deployment': {
925
433
  const { issueKey } = args;
@@ -941,7 +449,6 @@ export async function executeTool(name, args, deps) {
941
449
  'Accept',
942
450
  'Prepare to create deployment ticket',
943
451
  'Apply for approval',
944
- 'To Wait Deploy',
945
452
  ];
946
453
 
947
454
  const DEV_SELF_SERVICE_TRANSITIONS = [
@@ -991,6 +498,11 @@ export async function executeTool(name, args, deps) {
991
498
  return issue.fields.status.name;
992
499
  };
993
500
 
501
+ const getSystemCode = async () => {
502
+ const fields = await jira.getIssueFields(issueKey, [CD_FIELD_IDS.systemCode]);
503
+ return fields?.[CD_FIELD_IDS.systemCode]?.value ?? fields?.[CD_FIELD_IDS.systemCode] ?? '';
504
+ };
505
+
994
506
  const runDevSelfServiceTransitions = async () => {
995
507
  if (envCode !== 'dev') return;
996
508
  for (const transitionName of DEV_SELF_SERVICE_TRANSITIONS) {
@@ -1044,6 +556,52 @@ export async function executeTool(name, args, deps) {
1044
556
  }
1045
557
  }
1046
558
 
559
+ const runNonDevApprovalFlow = async () => {
560
+ if (envCode === 'dev') return false;
561
+
562
+ for (let attempts = 0; attempts < 12; attempts++) {
563
+ const currentStatus = await getCurrentStatus();
564
+ const normalizedStatus = currentStatus.trim().replace(/\s+/g, ' ').toLowerCase();
565
+
566
+ if (normalizedStatus === 'wait deploy') {
567
+ log.push('✅ CD 單已在 Wait Deploy,簽核前置流程完成');
568
+ return true;
569
+ }
570
+
571
+ if (normalizedStatus === 'wait approval') {
572
+ const systemCode = await getSystemCode();
573
+ log.push(` 處理 CD 簽核流程 (環境: ${envCode})`);
574
+ const approvalResult = await handleCDApproval(issueKey, envCode, systemCode, {
575
+ jira,
576
+ notifier,
577
+ progress,
578
+ });
579
+ log.push(` ✅ CD 簽核完成 by ${approvalResult.by}`);
580
+ continue;
581
+ }
582
+
583
+ const transitions = await jira.getTransitions(issueKey);
584
+ const nextTransitionNames = normalizedStatus === 'wait for send notice email'
585
+ ? ['To Wait Deploy']
586
+ : ['Apply for approval', 'To Wait Deploy'];
587
+ const next = nextTransitionNames
588
+ .map((transitionName) => transitions.find(
589
+ (transition) => transition.name.toLowerCase() === transitionName.toLowerCase(),
590
+ ))
591
+ .find(Boolean);
592
+
593
+ if (!next) {
594
+ return false;
595
+ }
596
+
597
+ log.push(` 觸發「${next.name}」...`);
598
+ await jira.transitionById(issueKey, next.id);
599
+ await new Promise((r) => setTimeout(r, 2000));
600
+ }
601
+
602
+ throw new Error('CD 簽核前置流程超過最大嘗試次數');
603
+ };
604
+
1047
605
  if (!deployTrans && envCode === 'dev') {
1048
606
  await runDevSelfServiceTransitions();
1049
607
  const currentStatus = await getCurrentStatus();
@@ -1091,6 +649,7 @@ export async function executeTool(name, args, deps) {
1091
649
  log.push(`✅ 部署已觸发,目前狀態:${newStatus}`);
1092
650
 
1093
651
  await runDevSelfServiceTransitions();
652
+ await runNonDevApprovalFlow();
1094
653
 
1095
654
  const finalIssue = await jira.getIssue(issueKey);
1096
655
  const finalStatus = finalIssue.fields.status.name;
@@ -1244,21 +803,25 @@ export async function executeTool(name, args, deps) {
1244
803
  const describeStatus = (issueType, status) => {
1245
804
  const s = status.toLowerCase();
1246
805
  if (issueType === 'Library') {
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` };
806
+ if (s === 'to do') return { emoji: '⏳', desc: '尚未開始', next: `執行 build_library` };
807
+ if (s === 'upload lib report') return { emoji: '⏳', desc: '等待上傳 Lib Report', next: `執行 build_library` };
1249
808
  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` };
809
+ if (s === 'wait for lib build') return { emoji: '🔨', desc: '等待 Jenkins Build', next: `執行 build_library` };
1251
810
  if (s === 'released') return { emoji: '✅', desc: '已完成', next: '可開 CI' };
1252
811
  if (s === 'cancelled') return { emoji: '❌', desc: '已取消', next: null };
1253
812
  return { emoji: '🔄', desc: status, next: null };
1254
813
  }
1255
814
  if (issueType === 'CI') {
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' };
815
+ if (s === 'to do') return { emoji: '⏳', desc: '尚未開始', next: '執行 build_ci' };
816
+ if (s === 'wait for build') return { emoji: '⏳', desc: '等待 Jenkins Build', next: '執行 build_ci' };
1258
817
  if (s === 'compliance scan') return { emoji: '🔍', desc: '掃描中', next: '執行 wait_to_stg' };
1259
818
  if (s === 'upload report') return { emoji: '📋', desc: '上傳掃描報告', next: '執行 wait_to_stg' };
1260
819
  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' };
820
+ if (s === 'wait to stg') return {
821
+ emoji: '✅',
822
+ desc: 'Build 完成,等待 STG 部署',
823
+ next: '可建 CD(STG) 並執行 prepare_cd_deployment'
824
+ };
1262
825
  if (s === 'wait to uat') return { emoji: '✅', desc: 'STG 完成,等待 UAT', next: '可建 CD(UAT)' };
1263
826
  if (s === 'wait for upload') return { emoji: '📦', desc: '等待上傳 Pre-Release', next: null };
1264
827
  if (s === 'wait to prd/dr') return { emoji: '✅', desc: 'UAT 完成,等待 PRD/DR', next: '可建 CD(PRD/DR)' };
@@ -1268,7 +831,11 @@ export async function executeTool(name, args, deps) {
1268
831
  }
1269
832
  if (issueType === 'CD') {
1270
833
  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' };
834
+ if (s === 'wait for send notice email') return {
835
+ emoji: '⏳',
836
+ desc: '等待通知信',
837
+ next: '執行 prepare_cd_deployment'
838
+ };
1272
839
  if (s === 'prepare for deploy') return { emoji: '⏳', desc: '部署單建立中', next: null };
1273
840
  if (s === 'wait approval') return { emoji: '⏳', desc: '等待主管核准', next: 'wait_for_approval' };
1274
841
  if (s === 'wait deploy') return { emoji: '✅', desc: '已核准,等待部署', next: '執行 trigger_deployment' };
@@ -1295,7 +862,13 @@ export async function executeTool(name, args, deps) {
1295
862
  const ciIssue = ciIssues[0];
1296
863
  const ciStatus = describeStatus('CI', ciIssue.fields.status.name);
1297
864
  const ciVersion = ciIssue.fields.customfield_13438
1298
- ? (() => { try { return JSON.stringify(JSON.parse(ciIssue.fields.customfield_13438)); } catch { return ciIssue.fields.customfield_13438; } })()
865
+ ? (() => {
866
+ try {
867
+ return JSON.stringify(JSON.parse(ciIssue.fields.customfield_13438));
868
+ } catch {
869
+ return ciIssue.fields.customfield_13438;
870
+ }
871
+ })()
1299
872
  : '(尚無版本)';
1300
873
 
1301
874
  // Step 2: 從 CI issuelinks 找 Library 單(Relates),並平行查 gitBranch
@@ -1364,9 +937,6 @@ export async function executeTool(name, args, deps) {
1364
937
  }
1365
938
  }
1366
939
 
1367
- case 'get_next_lib_version':
1368
- return handleGetNextLibVersion(args, { jira });
1369
-
1370
940
  case 'get_next_ci_version':
1371
941
  return handleGetNextCIVersion(args, { jira });
1372
942