@jira-deploy/core 1.0.15 → 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.
@@ -2,33 +2,48 @@
2
2
  * Gray Release 相關 tools
3
3
  * - create_grayrelease_ticket
4
4
  * - link_stg_grayrelease
5
+ * - build_grayrelease
5
6
  * - auto_grayrelease
6
7
  * - deploy_grayrelease
7
8
  * - get_grayrelease_status
8
- * - wait_build_result
9
9
  * - continue_grayrelease
10
10
  */
11
11
  import {
12
- SYSTEM_CODES,
13
- getDeployConfig,
14
- ENV_CODES,
15
- SUPPORTED_ENVS,
16
12
  DEPT_CODES,
13
+ ENV_CODES,
14
+ FIELD_OPTIONS,
15
+ getDeployConfig,
16
+ GRAY_RELEASE_FIELD_IDS,
17
17
  GRAY_RELEASE_MODULE_IDS,
18
18
  ISSUE_TYPE_IDS,
19
+ JIRA_DEFAULTS,
19
20
  JIRA_PROJECT_ID,
20
- GRAY_RELEASE_FIELD_IDS,
21
- SYSTEM_TO_DEPT_MAP,
22
- FIELD_OPTIONS,
23
- SIGN_VALUES,
24
21
  NOTES_TEMPLATES,
25
- JIRA_DEFAULTS,
26
22
  resolveAccountId,
23
+ SHARED_FIELD_IDS,
24
+ SIGN_VALUES,
25
+ SUPPORTED_ENVS,
26
+ SYSTEM_CODES,
27
+ SYSTEM_TO_DEPT_MAP,
27
28
  } from '../constants/index.js';
28
- import { error, ok, today, getServerList } from './helpers.js';
29
+ import {
30
+ error,
31
+ getPollIntervalMs,
32
+ getPollTimeoutMs,
33
+ getServerList,
34
+ isFailingResult,
35
+ isPassingResult,
36
+ ok,
37
+ today,
38
+ } from './helpers.js';
39
+ import { assertNoOpenPRBeforeCreate } from './branch-prs.js';
29
40
  import { Poller } from '../poller.js';
30
41
  import { handleGetReleaseManager, handleWaitForComment } from './release.js';
31
42
  import { handleSendJabberMessage } from './jabber.js';
43
+ import {
44
+ needSwitchExecutionNode as shouldSwitchExecutionNode,
45
+ waitForSwitchExecutionNode as waitForSharedSwitchExecutionNode,
46
+ } from './deployment-helpers.js';
32
47
 
33
48
  // ── Flow Definition ──────────────────────────────────────────────
34
49
 
@@ -40,7 +55,7 @@ import { handleSendJabberMessage } from './jabber.js';
40
55
  * PLANNING → (Accept) → WAIT FOR BUILD → (GrayRelease Build)
41
56
  * → WAIT FOR BUILD → (Apply to approval) → WAIT APPROVAL
42
57
  * → (Approve,依環境處理簽核) → WAIT DEPLOY
43
- * → (Switch Execution Node,視上次 CD 部署環境群組 prd/nonPrd 決定是否需要)
58
+ * → (Switch Execution Node,視上次 CD or GrayRelease部署環境群組 prd/nonPrd 決定是否需要)
44
59
  * → WAIT DEPLOY → (GrayRelease Deploy) → WAIT DEPLOY → (To Verify)
45
60
  * → VERIFY → (Verify Success) → MERGE CODE AND TAG → (To Done) → DONE
46
61
  *
@@ -49,9 +64,8 @@ import { handleSendJabberMessage } from './jabber.js';
49
64
  * - STG:指派給當日 Release Manager,等待簽核
50
65
  * - UAT:依 env/config 指派第一階段與最終簽核人
51
66
  *
52
- * Rebuild 規則:
53
- * VERIFY 狀態只有在使用者明確要求 build/rebuild 時,才可透過 build_ticket({rebuild: true})
54
- * 執行 Verify fail 回到 PLANNING 後重新 GrayRelease Build。
67
+ * Build 規則:
68
+ * DONE 以外的狀態都會先回到 PLANNING,再重新觸發 GrayRelease Build。
55
69
  */
56
70
  const GRAYRELEASE_FLOW_MAP = {
57
71
  'PLANNING': {
@@ -101,14 +115,6 @@ function normalizeStatusName(statusName) {
101
115
  .toUpperCase();
102
116
  }
103
117
 
104
- function getPollIntervalMs() {
105
- return parseInt(process.env.POLL_INTERVAL_MS ?? '30000');
106
- }
107
-
108
- function getPollTimeoutMs() {
109
- return parseInt(process.env.POLL_TIMEOUT_MS ?? '3600000');
110
- }
111
-
112
118
  function progress(ctx, event) {
113
119
  if (typeof ctx.progress === 'function') {
114
120
  ctx.progress(event);
@@ -130,18 +136,13 @@ function getGrayReleaseUatApprovers() {
130
136
  };
131
137
  }
132
138
 
133
- function isPassingResult(value) {
134
- return ['pass', 'passed', 'success', 'succeeded', 'done'].includes(
135
- String(value ?? '').trim().toLowerCase(),
136
- );
137
- }
138
-
139
- function isFailingResult(value) {
140
- return ['fail', 'failed', 'failure', 'error'].includes(
141
- String(value ?? '').trim().toLowerCase(),
142
- );
143
- }
144
-
139
+ /**
140
+ * 根據 transition name 搜尋 issue 可用的 transition,回傳該 transition 物件(包含 id)以便後續呼叫 transition API
141
+ * @param issueKey
142
+ * @param transitionName
143
+ * @param jira
144
+ * @returns {Object|undefined}
145
+ */
145
146
  async function findTransitionByName(issueKey, transitionName, jira) {
146
147
  const transitions = await jira.getTransitions(issueKey);
147
148
  return transitions.find((transition) => (
@@ -149,7 +150,8 @@ async function findTransitionByName(issueKey, transitionName, jira) {
149
150
  ));
150
151
  }
151
152
 
152
- async function waitForGrayReleaseResult(issueKey, fieldId, label, jira, onProgress = () => { }) {
153
+ async function waitForGrayReleaseResult(issueKey, fieldId, label, jira, onProgress = () => {
154
+ }) {
153
155
  const intervalMs = getPollIntervalMs();
154
156
  const timeoutMs = getPollTimeoutMs();
155
157
  const startedAt = Date.now();
@@ -174,7 +176,7 @@ async function waitForGrayReleaseResult(issueKey, fieldId, label, jira, onProgre
174
176
  });
175
177
 
176
178
  if (isPassingResult(lastValue)) {
177
- return { issueKey, fieldId, result: lastValue, attempts };
179
+ return;
178
180
  }
179
181
 
180
182
  if (isFailingResult(lastValue)) {
@@ -277,6 +279,21 @@ export function getGrayReleaseToolDefinitions() {
277
279
  },
278
280
  },
279
281
  },
282
+ {
283
+ name: 'build_grayrelease',
284
+ description:
285
+ '觸發 GrayRelease 上版單的 Jenkins Build。DONE 以外的狀態會先切回 PLANNING,再走到 WAIT FOR BUILD 後觸發 GrayRelease Build;若使用者要求 GrayRelease deploy、dev deploy 或部署灰度單,必須改用 deploy_grayrelease。',
286
+ inputSchema: {
287
+ type: 'object',
288
+ required: ['issueKey'],
289
+ properties: {
290
+ issueKey: {
291
+ type: 'string',
292
+ description: 'GrayRelease 單 issue key,例如 CID-822',
293
+ },
294
+ },
295
+ },
296
+ },
280
297
  {
281
298
  name: 'auto_grayrelease',
282
299
  description:
@@ -333,34 +350,6 @@ export function getGrayReleaseToolDefinitions() {
333
350
  },
334
351
  },
335
352
  },
336
- {
337
- name: 'wait_build_result',
338
- description:
339
- '輪詢等待 Jira issue 的 Build 結果欄位變成 pass 或 fail。通常接在 build_ticket 成功觸發 Jenkins build 後使用,確認 pass 後再呼叫 deploy_grayrelease;不可用 deploy_grayrelease 取代等待 build pass。',
340
- inputSchema: {
341
- type: 'object',
342
- required: ['issueKey'],
343
- properties: {
344
- issueKey: {
345
- type: 'string',
346
- description: '要等待 build 結果的 issue key,例如 GrayRelease 單 CID-822',
347
- },
348
- resultFieldId: {
349
- type: 'string',
350
- description: '(選填) Build 結果欄位 ID,預設 customfield_13432',
351
- default: GRAY_RELEASE_FIELD_IDS.buildResult,
352
- },
353
- pollIntervalMs: {
354
- type: 'number',
355
- description: '輪詢間隔毫秒,預設讀 POLL_INTERVAL_MS',
356
- },
357
- timeoutMs: {
358
- type: 'number',
359
- description: '最長等待毫秒,預設讀 POLL_TIMEOUT_MS',
360
- },
361
- },
362
- },
363
- },
364
353
  {
365
354
  name: 'continue_grayrelease',
366
355
  description:
@@ -397,14 +386,13 @@ export function getGrayReleaseToolDefinitions() {
397
386
  * @returns {Promise<{key:string, summary:string}[]>}
398
387
  */
399
388
  async function findUnlinkedStgGrayReleases(gitBranch, moduleId, jira) {
400
- const STG_ENV_ID = '14356';
401
- const GR_ISSUE_TYPE = '12601';
389
+ const GR_ISSUE_TYPE = ISSUE_TYPE_IDS.GrayRelease;
402
390
  const jql = [
403
391
  'project = CID',
404
392
  `issuetype = ${GR_ISSUE_TYPE}`,
405
- `cf[13436] = ${STG_ENV_ID}`,
406
- `cf[13431] = "${gitBranch.replace(/"/g, '\\"')}"`,
407
- `cf[13444] = ${moduleId}`,
393
+ `CID_env = STG`,
394
+ `CID_branch ~ "${gitBranch.replace(/"/g, '\\"').replace(/\//g, '\\u002f')}"`,
395
+ `CID_system_module = ${moduleId}`,
408
396
  'ORDER BY created DESC',
409
397
  ].join(' AND ');
410
398
 
@@ -435,7 +423,7 @@ export async function handleCreateGrayReleaseTicket(args, { jira, notifier }) {
435
423
  const normalizedArgs = {
436
424
  ...args,
437
425
  module: args.module ?? args.systemCode?.toLowerCase(),
438
- gitBranch: args.gitBranch ?? args.grayVersion,
426
+ gitBranch: args.gitBranch,
439
427
  environment: args.environment?.toLowerCase(),
440
428
  };
441
429
 
@@ -519,6 +507,13 @@ export async function handleCreateGrayReleaseTicket(args, { jira, notifier }) {
519
507
  // gray release notes
520
508
  fields[GRAY_RELEASE_FIELD_IDS.grayReleaseNotes] = NOTES_TEMPLATES.grayRelease;
521
509
 
510
+ await assertNoOpenPRBeforeCreate({
511
+ ticketType: 'grayrelease',
512
+ systemCode: normalizedArgs.systemCode,
513
+ module: normalizedArgs.module,
514
+ branch: normalizedArgs.gitBranch,
515
+ }, { jira });
516
+
522
517
  if (normalizedArgs.dryRun) {
523
518
  return ok({ dryRun: true, summary: fields.summary, grayReleaseVersion, fields });
524
519
  }
@@ -546,6 +541,11 @@ export async function handleCreateGrayReleaseTicket(args, { jira, notifier }) {
546
541
  if (stgTickets.length > 0) {
547
542
  result.suggest_link = {
548
543
  message: `找到以下 STG 灰度單(branch: ${args.gitBranch})尚未關聯到任何 UAT 灰度單,是否要建立 relates to 關聯到 ${issue.key}?`,
544
+ toolName: 'link_stg_grayrelease',
545
+ arguments: {
546
+ uatKey: issue.key,
547
+ stgKeys: stgTickets.map((ticket) => ticket.key),
548
+ },
549
549
  stgTickets,
550
550
  hint: '回覆「是」或 yes 即可自動關聯,或直接呼叫 link_stg_grayrelease',
551
551
  };
@@ -561,22 +561,50 @@ export async function handleCreateGrayReleaseTicket(args, { jira, notifier }) {
561
561
  /**
562
562
  * 將 STG 灰度單關聯(relates to)到 UAT 灰度單
563
563
  */
564
- export async function handleLinkStgGrayRelease(args, { jira, notifier }) {
564
+ export async function handleLinkStgGrayRelease(args, {
565
+ jira, notifier, progress = () => {
566
+ }
567
+ }) {
565
568
  try {
566
569
  const { uatKey, stgKeys } = args;
567
570
  if (!Array.isArray(stgKeys) || stgKeys.length === 0) {
568
571
  return error('stgKeys 不可為空');
569
572
  }
570
573
 
574
+ progress({
575
+ phase: 'action',
576
+ title: '開始關聯 STG 灰度單',
577
+ detail: `${stgKeys.join(', ')} -> ${uatKey}`,
578
+ issueKey: uatKey,
579
+ total: stgKeys.length,
580
+ });
581
+
571
582
  const linked = [];
572
583
  const failed = [];
573
584
 
574
- for (const stgKey of stgKeys) {
585
+ for (const [index, stgKey] of stgKeys.entries()) {
575
586
  try {
576
587
  await jira.linkIssue(stgKey, uatKey, 'Relates');
577
588
  linked.push(stgKey);
589
+ progress({
590
+ phase: 'done',
591
+ title: 'STG 灰度單關聯成功',
592
+ detail: `${stgKey} -> ${uatKey}`,
593
+ issueKey: uatKey,
594
+ current: index + 1,
595
+ total: stgKeys.length,
596
+ });
597
+
578
598
  } catch (e) {
579
599
  failed.push({ key: stgKey, reason: e.message });
600
+ progress({
601
+ phase: 'error',
602
+ title: 'STG 灰度單關聯失敗',
603
+ detail: `${stgKey}: ${e.message}`,
604
+ issueKey: uatKey,
605
+ current: index + 1,
606
+ total: stgKeys.length,
607
+ });
580
608
  }
581
609
  }
582
610
 
@@ -584,6 +612,15 @@ export async function handleLinkStgGrayRelease(args, { jira, notifier }) {
584
612
  await notifier.notify(uatKey, `已將 STG 灰度單 ${linked.join(', ')} relates to ${uatKey}`);
585
613
  }
586
614
 
615
+ progress({
616
+ phase: failed.length > 0 ? 'done' : 'done',
617
+ title: 'STG 灰度單關聯完成',
618
+ detail: `成功 ${linked.length} 張,失敗 ${failed.length} 張`,
619
+ issueKey: uatKey,
620
+ linked,
621
+ failed,
622
+ });
623
+
587
624
  return ok({
588
625
  uatKey,
589
626
  linked,
@@ -600,6 +637,20 @@ export async function handleLinkStgGrayRelease(args, { jira, notifier }) {
600
637
 
601
638
  // ── Handlers (NEW) ───────────────────────────────────────────────
602
639
 
640
+ /**
641
+ * 觸發 GrayRelease Build。
642
+ */
643
+ export async function handleBuildGrayRelease(args, ctx) {
644
+ const { issueKey } = args;
645
+
646
+ try {
647
+ const result = await executeGrayReleaseBuildFlow(issueKey, ctx);
648
+ return ok(result);
649
+ } catch (err) {
650
+ return error(`build_grayrelease 失敗: ${err.message}`);
651
+ }
652
+ }
653
+
603
654
  /**
604
655
  * 自動執行 GrayRelease 完整流程
605
656
  */
@@ -643,71 +694,12 @@ export async function handleGetGrayReleaseStatus(args, { jira }) {
643
694
  }
644
695
  }
645
696
 
646
- export async function handleWaitBuildResult(args, ctx) {
647
- const { issueKey } = args;
648
- const { jira } = ctx;
649
- const fieldId = args.resultFieldId ?? GRAY_RELEASE_FIELD_IDS.buildResult;
650
- const intervalMs = args.pollIntervalMs ?? getPollIntervalMs();
651
- const timeoutMs = args.timeoutMs ?? getPollTimeoutMs();
652
- const startedAt = Date.now();
653
- const deadline = Date.now() + timeoutMs;
654
- let attempts = 0;
655
- let lastValue;
656
- let currentStatus;
657
-
658
- try {
659
- while (true) {
660
- attempts++;
661
- const issue = await jira.getIssue(issueKey);
662
- currentStatus = issue.fields.status?.name;
663
- const fields = await jira.getIssueFields(issueKey, [fieldId]);
664
- const raw = fields[fieldId];
665
- lastValue = raw?.value ?? raw;
666
-
667
- progress(ctx, {
668
- phase: 'polling',
669
- title: '等待 Build 結果',
670
- detail: `${fieldId}: ${lastValue ?? 'empty'}`,
671
- issueKey,
672
- currentStatus,
673
- attempts,
674
- elapsedMs: Date.now() - startedAt,
675
- timeoutMs,
676
- nextPollMs: intervalMs,
677
- });
678
-
679
- if (isPassingResult(lastValue)) {
680
- return ok({
681
- issueKey,
682
- fieldId,
683
- result: lastValue,
684
- buildResult: lastValue,
685
- currentStatus,
686
- attempts,
687
- });
688
- }
689
-
690
- if (isFailingResult(lastValue)) {
691
- return error(`Build 失敗,${fieldId}: ${lastValue}`);
692
- }
693
-
694
- if (Date.now() >= deadline) {
695
- return error(`Build 結果等待逾時,${fieldId}: ${lastValue ?? 'empty'}`);
696
- }
697
-
698
- await sleep(intervalMs);
699
- }
700
- } catch (err) {
701
- return error(`wait_build_result 失敗: ${err.message}`);
702
- }
703
- }
704
-
705
697
  /**
706
- * 執行 GrayRelease 部署流程,不觸發 rebuild
698
+ * 執行 GrayRelease 部署流程,不觸發 build
707
699
  */
708
700
  export async function handleDeployGrayRelease(args, ctx) {
709
701
  const { issueKey } = args;
710
- const { jira, notifier } = ctx;
702
+ const { notifier } = ctx;
711
703
 
712
704
  try {
713
705
  progress(ctx, {
@@ -777,6 +769,70 @@ export async function handleContinueGrayRelease(args, ctx) {
777
769
 
778
770
  // ── Internal Helpers ─────────────────────────────────────────────
779
771
 
772
+ const GRAYRELEASE_RESET_TO_PLANNING_TRANSITIONS = [
773
+ 'Verify fail',
774
+ 'Back to Planning',
775
+ 'To Planning',
776
+ 'Planning',
777
+ ];
778
+
779
+ async function executeGrayReleaseBuildFlow(issueKey, ctx) {
780
+ const { jira, notifier } = ctx;
781
+ const log = [];
782
+
783
+ const initIssue = await jira.getIssue(issueKey);
784
+ const initStatus = initIssue.fields.status.name;
785
+ const normalizedInitStatus = normalizeStatusName(initStatus);
786
+
787
+ if (normalizedInitStatus === 'DONE') {
788
+ throw new Error('GrayRelease 已是 DONE,不會自動重新 build。若確定要重跑,請先手動退回流程狀態。');
789
+ }
790
+
791
+ await resetGrayReleaseToPlanningForBuild(issueKey, normalizedInitStatus, jira, log);
792
+
793
+ log.push('執行 Accept,進入 WAIT FOR BUILD...');
794
+ await jira.transitionByName(issueKey, 'Accept');
795
+ await sleep(10 * 1000);
796
+
797
+ log.push('執行 GrayRelease Build...');
798
+ await jira.transitionByName(issueKey, 'GrayRelease Build');
799
+
800
+ const issue = await jira.getIssue(issueKey);
801
+ const newStatus = issue.fields.status.name;
802
+ log.push(`✅ GrayRelease Build 已觸發,目前狀態:${newStatus}`);
803
+ await notifier.notify(issueKey, `Jenkins GrayRelease Build 已觸發(${newStatus})`);
804
+
805
+ return { issueKey, status: newStatus, steps: log };
806
+ }
807
+
808
+ async function resetGrayReleaseToPlanningForBuild(issueKey, normalizedStatus, jira, log) {
809
+ if (normalizedStatus === 'PLANNING') {
810
+ log.push('目前已是 PLANNING,直接進入 build 前置流程');
811
+ return;
812
+ }
813
+
814
+ const resetTrans = await findAnyGrayReleaseTransition(
815
+ issueKey,
816
+ GRAYRELEASE_RESET_TO_PLANNING_TRANSITIONS,
817
+ jira,
818
+ );
819
+
820
+ if (!resetTrans) {
821
+ throw new Error('找不到回到 PLANNING 的 transition,無法先 reset 後 build');
822
+ }
823
+
824
+ log.push(`先觸發「${resetTrans.name}」回到 PLANNING,再重新 build...`);
825
+ await jira.transitionById(issueKey, resetTrans.id);
826
+ await sleep(10 * 1000);
827
+ }
828
+
829
+ async function findAnyGrayReleaseTransition(issueKey, names, jira) {
830
+ const list = await jira.getTransitions(issueKey);
831
+ return list.find((transition) => (
832
+ names.some((name) => transition.name.toLowerCase() === name.toLowerCase())
833
+ ));
834
+ }
835
+
780
836
  /**
781
837
  * 查詢 GrayRelease 單狀態並分析下一步
782
838
  */
@@ -844,10 +900,7 @@ async function executeGrayReleaseDeployFlow(issueKey, ctx) {
844
900
  }
845
901
 
846
902
  log.push(`🚀 開始執行 GrayRelease Deploy - 環境: ${environment.toUpperCase()}`);
847
- log.push(' 先執行 Planning,確保部署流程從可簽核狀態重新開始');
848
- await jira.transitionByName(issueKey, 'Planning');
849
- await notifier.notify(issueKey, '開始執行 GrayRelease deploy 流程,先回到 Planning');
850
-
903
+
851
904
  while (true) {
852
905
  const issue = await jira.getIssue(issueKey);
853
906
  const currentStatus = issue.fields.status.name;
@@ -880,11 +933,36 @@ async function executeGrayReleaseDeployFlow(issueKey, ctx) {
880
933
  }
881
934
 
882
935
  if (normalizedStatus === 'WAIT FOR BUILD') {
936
+ const buildFields = await jira.getIssueFields(issueKey, [SHARED_FIELD_IDS.buildResult]);
937
+ const rawBuildResult = buildFields[SHARED_FIELD_IDS.buildResult];
938
+ const buildResult = rawBuildResult?.value ?? rawBuildResult;
939
+ if (!isPassingResult(buildResult)) {
940
+ const message = `GrayRelease Build 尚未通過,${SHARED_FIELD_IDS.buildResult}: ${buildResult ?? 'empty'}。是否要先執行 build_grayrelease?`;
941
+ log.push(` ⏸️ ${message}`);
942
+ return {
943
+ issueKey,
944
+ finalStatus: currentStatus,
945
+ needsBuild: true,
946
+ confirm_build: true,
947
+ buildResult,
948
+ buildResultFieldId: SHARED_FIELD_IDS.buildResult,
949
+ message,
950
+ suggest_build: {
951
+ message,
952
+ toolName: 'build_grayrelease',
953
+ arguments: { issueKey },
954
+ hint: '回覆「是」或 yes 即可先執行 build_grayrelease;build pass 後再執行 deploy_grayrelease。',
955
+ },
956
+ log,
957
+ };
958
+ }
959
+
883
960
  const applyTransition = await findTransitionByName(issueKey, 'Apply to approval', jira);
884
961
  if (!applyTransition) {
885
- log.push(' ⏸️ 目前仍需 build,deploy_grayrelease 不會觸發 rebuild');
962
+ log.push(' ⏸️ 目前仍需 build,deploy_grayrelease 不會觸發 build');
886
963
  break;
887
964
  }
965
+
888
966
  log.push(' 執行: Apply to approval');
889
967
  await jira.transitionByName(issueKey, 'Apply to approval');
890
968
  await notifier.notify(issueKey, '進入 GrayRelease 部署簽核流程');
@@ -913,7 +991,7 @@ async function executeGrayReleaseDeployFlow(issueKey, ctx) {
913
991
  }
914
992
 
915
993
  if (normalizedStatus === 'VERIFY') {
916
- log.push(' ⏸️ 已進入 VERIFY,請人工驗證;若需重 build 請明確執行 build/rebuild');
994
+ log.push(' ⏸️ 已進入 VERIFY,請人工驗證;若需再次 build 請明確執行 build');
917
995
  break;
918
996
  }
919
997
 
@@ -1009,7 +1087,7 @@ async function executeAutoGrayReleaseFlow(issueKey, options, ctx) {
1009
1087
  log.push(' ⏳ 等待 Jenkins Build 完成...');
1010
1088
  await waitForGrayReleaseResult(
1011
1089
  issueKey,
1012
- GRAY_RELEASE_FIELD_IDS.buildResult,
1090
+ SHARED_FIELD_IDS.buildResult,
1013
1091
  'GrayRelease Build',
1014
1092
  jira,
1015
1093
  ctx.progress,
@@ -1093,7 +1171,13 @@ async function executeAutoGrayReleaseFlow(issueKey, options, ctx) {
1093
1171
 
1094
1172
  async function runGrayReleaseDeployStep(issueKey, systemCode, ctx, log) {
1095
1173
  const { jira, notifier } = ctx;
1096
- const needSwitch = await needSwitchExecutionNode(issueKey, systemCode, jira, log);
1174
+ const needSwitch = await shouldSwitchExecutionNode({
1175
+ issueKey,
1176
+ systemCode,
1177
+ envGroup: 'nonPrd',
1178
+ jira,
1179
+ log,
1180
+ });
1097
1181
 
1098
1182
  if (needSwitch) {
1099
1183
  log.push(' 執行: Switch Execution Node');
@@ -1104,7 +1188,7 @@ async function runGrayReleaseDeployStep(issueKey, systemCode, ctx, log) {
1104
1188
  });
1105
1189
  await jira.transitionByName(issueKey, 'Switch Execution Node');
1106
1190
  await notifier.notify(issueKey, '切換 Ansible instance');
1107
- await waitForSwitchExecutionNode(issueKey, systemCode, jira);
1191
+ await waitForSharedSwitchExecutionNode({ issueKey, systemCode, envGroup: 'nonPrd', jira });
1108
1192
  }
1109
1193
 
1110
1194
  log.push(' 執行: GrayRelease Deploy');
@@ -1192,7 +1276,7 @@ async function handleGrayReleaseApproval(issueKey, environment, systemCode, ctx)
1192
1276
 
1193
1277
  // STG: 查 wiki 值班組長 → assign → 發 jabber → 等待 approve
1194
1278
  if (env === 'stg') {
1195
- const managerResult = await handleGetReleaseManager({}, {});
1279
+ const managerResult = await handleGetReleaseManager();
1196
1280
  const managerData = parseToolResult(managerResult);
1197
1281
 
1198
1282
  if (!managerData?.found) {
@@ -1235,8 +1319,8 @@ async function handleGrayReleaseApproval(issueKey, environment, systemCode, ctx)
1235
1319
  // 輪詢等待 Approve(狀態變為 WAIT DEPLOY)
1236
1320
  const poller = new Poller(jira);
1237
1321
  await poller.waitForStatus(issueKey, 'WAIT DEPLOY', {
1238
- intervalMs: parseInt(process.env.POLL_INTERVAL_MS ?? '30000'),
1239
- timeoutMs: parseInt(process.env.POLL_TIMEOUT_MS ?? '3600000'),
1322
+ intervalMs: getPollIntervalMs(),
1323
+ timeoutMs: getPollTimeoutMs(),
1240
1324
  onProgress: ctx.progress,
1241
1325
  });
1242
1326
 
@@ -1280,8 +1364,8 @@ async function handleGrayReleaseApproval(issueKey, environment, systemCode, ctx)
1280
1364
  issueKey,
1281
1365
  keyword: 'approved',
1282
1366
  authorAccountId: commentReviewerAccountId,
1283
- timeoutMs: parseInt(process.env.POLL_TIMEOUT_MS ?? '3600000'),
1284
- intervalMs: parseInt(process.env.POLL_INTERVAL_MS ?? '30000'),
1367
+ intervalMs: getPollIntervalMs(),
1368
+ timeoutMs: getPollTimeoutMs(),
1285
1369
  },
1286
1370
  { jira, progress: ctx.progress },
1287
1371
  );
@@ -1324,8 +1408,8 @@ async function handleGrayReleaseApproval(issueKey, environment, systemCode, ctx)
1324
1408
 
1325
1409
  const poller = new Poller(jira);
1326
1410
  await poller.waitForStatus(issueKey, 'WAIT DEPLOY', {
1327
- intervalMs: parseInt(process.env.POLL_INTERVAL_MS ?? '30000'),
1328
- timeoutMs: parseInt(process.env.POLL_TIMEOUT_MS ?? '3600000'),
1411
+ intervalMs: getPollIntervalMs(),
1412
+ timeoutMs: getPollTimeoutMs(),
1329
1413
  onProgress: ctx.progress,
1330
1414
  });
1331
1415
 
@@ -1335,82 +1419,6 @@ async function handleGrayReleaseApproval(issueKey, environment, systemCode, ctx)
1335
1419
  throw new Error(`不支援的環境: ${environment}`);
1336
1420
  }
1337
1421
 
1338
- /**
1339
- * 判斷是否需要執行 Switch Execution Node
1340
- * 規則:查詢同系統上次 CD or GrayRelease 部署的環境群組,若與本次不同則需切換
1341
- */
1342
- async function needSwitchExecutionNode(issueKey, systemCode, jira, log) {
1343
- if (await hasRecentSwitchExecutionNodeComment(issueKey, systemCode, jira)) {
1344
- return false;
1345
- }
1346
-
1347
- // GrayRelease 永遠是 nonPrd 群組
1348
- const thisGroup = 'nonPrd';
1349
-
1350
- // 只查詢 CD 單的部署歷史(不包含 GrayRelease)
1351
- const isPrdEnv = (env) => ['prd', 'dr', 'prd/dr', 'prd&dr'].includes(env?.toLowerCase()?.trim());
1352
-
1353
- const jql_cd = `project = CID AND (issuetype = CD OR issuetype = GrayRelease) AND text ~ "${systemCode}" AND status = Done AND issueKey != "${issueKey}" ORDER BY updated DESC`;
1354
-
1355
- try {
1356
- log.push(` before 查詢歷史 CD/GrayRelease 單,判斷是否需要切換 Execution Node`);
1357
- const cdResults = await jira.searchIssues(jql_cd, ['customfield_13436', 'updated'], 1);
1358
- log.push(` after 查詢歷史 CD/GrayRelease 單,判斷是否需要切換 Execution Node`, cdResults);
1359
- const cdIssue = cdResults[0] ?? null;
1360
-
1361
- // 查不到歷史 → 保守執行 Switch
1362
- if (!cdIssue) {
1363
- return true;
1364
- }
1365
-
1366
- // CD:讀 customfield_13436.value 判斷群組
1367
- const envVal = cdIssue.fields?.customfield_13436?.value ?? '';
1368
- const lastGroup = isPrdEnv(envVal) ? 'prd' : 'nonPrd';
1369
-
1370
- // 群組不同 → 需要 Switch
1371
- return lastGroup !== thisGroup;
1372
- } catch {
1373
- // 查詢失敗 → 保守執行 Switch
1374
- return true;
1375
- }
1376
- }
1377
-
1378
- async function waitForSwitchExecutionNode(issueKey, systemCode, jira) {
1379
- const waitMs = parseInt(process.env.SWITCH_EXECUTION_NODE_WAIT_MS ?? '180000');
1380
- const intervalMs = getPollIntervalMs();
1381
- const deadline = Date.now() + waitMs;
1382
-
1383
- while (true) {
1384
-
1385
- if (Date.now() >= deadline) {
1386
- return;
1387
- }
1388
-
1389
- await sleep(intervalMs);
1390
- }
1391
- }
1392
-
1393
- async function hasRecentSwitchExecutionNodeComment(issueKey, systemCode, jira) {
1394
- if (!jira.getComments) {
1395
- return false;
1396
- }
1397
-
1398
- try {
1399
- const comments = await jira.getComments(issueKey);
1400
- const lowerSystemCode = String(systemCode ?? '').toLowerCase();
1401
- return comments.some((comment) => {
1402
- const body = String(comment.body ?? '').toLowerCase();
1403
- const author = String(comment.author?.displayName ?? '').toLowerCase();
1404
- return author === 'cid jira worker'
1405
- && body.includes('instance_group')
1406
- && body.includes('nonprd_executionnode')
1407
- && (!lowerSystemCode || body.includes(lowerSystemCode));
1408
- });
1409
- } catch {
1410
- return false;
1411
- }
1412
- }
1413
-
1414
1422
  function sleep(ms) {
1415
1423
  return new Promise((resolve) => setTimeout(resolve, ms));
1416
1424
  }