@jira-deploy/core 1.0.15 → 1.0.17

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