@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/constants/config.js +13 -1
- package/index.js +1 -1
- package/jira-client.js +67 -36
- package/package.json +8 -2
- package/tools/branch-prs.js +164 -0
- package/tools/build.js +117 -0
- package/tools/cd.js +502 -116
- package/tools/ci.js +495 -28
- package/tools/deployment-helpers.js +103 -0
- package/tools/deployment.js +269 -0
- package/tools/grayrelease.js +234 -138
- package/tools/helpers.js +28 -7
- package/tools/index.js +135 -565
- package/tools/library.js +219 -24
- package/tools/release.js +131 -33
- package/tools/transition-helpers.js +22 -0
- package/tools/workflows.js +388 -110
package/tools/grayrelease.js
CHANGED
|
@@ -2,32 +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
9
|
* - continue_grayrelease
|
|
9
10
|
*/
|
|
10
11
|
import {
|
|
11
|
-
SYSTEM_CODES,
|
|
12
|
-
getDeployConfig,
|
|
13
|
-
ENV_CODES,
|
|
14
|
-
SUPPORTED_ENVS,
|
|
15
12
|
DEPT_CODES,
|
|
13
|
+
ENV_CODES,
|
|
14
|
+
FIELD_OPTIONS,
|
|
15
|
+
getDeployConfig,
|
|
16
|
+
GRAY_RELEASE_FIELD_IDS,
|
|
16
17
|
GRAY_RELEASE_MODULE_IDS,
|
|
17
18
|
ISSUE_TYPE_IDS,
|
|
19
|
+
JIRA_DEFAULTS,
|
|
18
20
|
JIRA_PROJECT_ID,
|
|
19
|
-
GRAY_RELEASE_FIELD_IDS,
|
|
20
|
-
SYSTEM_TO_DEPT_MAP,
|
|
21
|
-
FIELD_OPTIONS,
|
|
22
|
-
SIGN_VALUES,
|
|
23
21
|
NOTES_TEMPLATES,
|
|
24
|
-
JIRA_DEFAULTS,
|
|
25
22
|
resolveAccountId,
|
|
23
|
+
SHARED_FIELD_IDS,
|
|
24
|
+
SIGN_VALUES,
|
|
25
|
+
SUPPORTED_ENVS,
|
|
26
|
+
SYSTEM_CODES,
|
|
27
|
+
SYSTEM_TO_DEPT_MAP,
|
|
26
28
|
} from '../constants/index.js';
|
|
27
|
-
import {
|
|
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';
|
|
28
40
|
import { Poller } from '../poller.js';
|
|
29
41
|
import { handleGetReleaseManager, handleWaitForComment } from './release.js';
|
|
30
42
|
import { handleSendJabberMessage } from './jabber.js';
|
|
43
|
+
import {
|
|
44
|
+
needSwitchExecutionNode as shouldSwitchExecutionNode,
|
|
45
|
+
waitForSwitchExecutionNode as waitForSharedSwitchExecutionNode,
|
|
46
|
+
} from './deployment-helpers.js';
|
|
31
47
|
|
|
32
48
|
// ── Flow Definition ──────────────────────────────────────────────
|
|
33
49
|
|
|
@@ -39,7 +55,7 @@ import { handleSendJabberMessage } from './jabber.js';
|
|
|
39
55
|
* PLANNING → (Accept) → WAIT FOR BUILD → (GrayRelease Build)
|
|
40
56
|
* → WAIT FOR BUILD → (Apply to approval) → WAIT APPROVAL
|
|
41
57
|
* → (Approve,依環境處理簽核) → WAIT DEPLOY
|
|
42
|
-
* → (Switch Execution Node,視上次 CD 部署環境群組 prd/nonPrd 決定是否需要)
|
|
58
|
+
* → (Switch Execution Node,視上次 CD or GrayRelease部署環境群組 prd/nonPrd 決定是否需要)
|
|
43
59
|
* → WAIT DEPLOY → (GrayRelease Deploy) → WAIT DEPLOY → (To Verify)
|
|
44
60
|
* → VERIFY → (Verify Success) → MERGE CODE AND TAG → (To Done) → DONE
|
|
45
61
|
*
|
|
@@ -48,9 +64,8 @@ import { handleSendJabberMessage } from './jabber.js';
|
|
|
48
64
|
* - STG:指派給當日 Release Manager,等待簽核
|
|
49
65
|
* - UAT:依 env/config 指派第一階段與最終簽核人
|
|
50
66
|
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* 執行 Verify fail 回到 PLANNING 後重新 GrayRelease Build。
|
|
67
|
+
* Build 規則:
|
|
68
|
+
* DONE 以外的狀態都會先回到 PLANNING,再重新觸發 GrayRelease Build。
|
|
54
69
|
*/
|
|
55
70
|
const GRAYRELEASE_FLOW_MAP = {
|
|
56
71
|
'PLANNING': {
|
|
@@ -100,14 +115,6 @@ function normalizeStatusName(statusName) {
|
|
|
100
115
|
.toUpperCase();
|
|
101
116
|
}
|
|
102
117
|
|
|
103
|
-
function getPollIntervalMs() {
|
|
104
|
-
return parseInt(process.env.POLL_INTERVAL_MS ?? '30000');
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function getPollTimeoutMs() {
|
|
108
|
-
return parseInt(process.env.POLL_TIMEOUT_MS ?? '3600000');
|
|
109
|
-
}
|
|
110
|
-
|
|
111
118
|
function progress(ctx, event) {
|
|
112
119
|
if (typeof ctx.progress === 'function') {
|
|
113
120
|
ctx.progress(event);
|
|
@@ -129,18 +136,13 @@ function getGrayReleaseUatApprovers() {
|
|
|
129
136
|
};
|
|
130
137
|
}
|
|
131
138
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
return ['fail', 'failed', 'failure', 'error'].includes(
|
|
140
|
-
String(value ?? '').trim().toLowerCase(),
|
|
141
|
-
);
|
|
142
|
-
}
|
|
143
|
-
|
|
139
|
+
/**
|
|
140
|
+
* 根據 transition name 搜尋 issue 可用的 transition,回傳該 transition 物件(包含 id)以便後續呼叫 transition API
|
|
141
|
+
* @param issueKey
|
|
142
|
+
* @param transitionName
|
|
143
|
+
* @param jira
|
|
144
|
+
* @returns {Object|undefined}
|
|
145
|
+
*/
|
|
144
146
|
async function findTransitionByName(issueKey, transitionName, jira) {
|
|
145
147
|
const transitions = await jira.getTransitions(issueKey);
|
|
146
148
|
return transitions.find((transition) => (
|
|
@@ -148,7 +150,8 @@ async function findTransitionByName(issueKey, transitionName, jira) {
|
|
|
148
150
|
));
|
|
149
151
|
}
|
|
150
152
|
|
|
151
|
-
async function waitForGrayReleaseResult(issueKey, fieldId, label, jira, onProgress = () => {
|
|
153
|
+
async function waitForGrayReleaseResult(issueKey, fieldId, label, jira, onProgress = () => {
|
|
154
|
+
}) {
|
|
152
155
|
const intervalMs = getPollIntervalMs();
|
|
153
156
|
const timeoutMs = getPollTimeoutMs();
|
|
154
157
|
const startedAt = Date.now();
|
|
@@ -173,7 +176,7 @@ async function waitForGrayReleaseResult(issueKey, fieldId, label, jira, onProgre
|
|
|
173
176
|
});
|
|
174
177
|
|
|
175
178
|
if (isPassingResult(lastValue)) {
|
|
176
|
-
return
|
|
179
|
+
return;
|
|
177
180
|
}
|
|
178
181
|
|
|
179
182
|
if (isFailingResult(lastValue)) {
|
|
@@ -276,6 +279,21 @@ export function getGrayReleaseToolDefinitions() {
|
|
|
276
279
|
},
|
|
277
280
|
},
|
|
278
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
|
+
},
|
|
279
297
|
{
|
|
280
298
|
name: 'auto_grayrelease',
|
|
281
299
|
description:
|
|
@@ -368,14 +386,13 @@ export function getGrayReleaseToolDefinitions() {
|
|
|
368
386
|
* @returns {Promise<{key:string, summary:string}[]>}
|
|
369
387
|
*/
|
|
370
388
|
async function findUnlinkedStgGrayReleases(gitBranch, moduleId, jira) {
|
|
371
|
-
const
|
|
372
|
-
const GR_ISSUE_TYPE = '12601';
|
|
389
|
+
const GR_ISSUE_TYPE = ISSUE_TYPE_IDS.GrayRelease;
|
|
373
390
|
const jql = [
|
|
374
391
|
'project = CID',
|
|
375
392
|
`issuetype = ${GR_ISSUE_TYPE}`,
|
|
376
|
-
`
|
|
377
|
-
`
|
|
378
|
-
`
|
|
393
|
+
`CID_env = STG`,
|
|
394
|
+
`CID_branch ~ "${gitBranch.replace(/"/g, '\\"').replace(/\//g, '\\u002f')}"`,
|
|
395
|
+
`CID_system_module = ${moduleId}`,
|
|
379
396
|
'ORDER BY created DESC',
|
|
380
397
|
].join(' AND ');
|
|
381
398
|
|
|
@@ -406,7 +423,7 @@ export async function handleCreateGrayReleaseTicket(args, { jira, notifier }) {
|
|
|
406
423
|
const normalizedArgs = {
|
|
407
424
|
...args,
|
|
408
425
|
module: args.module ?? args.systemCode?.toLowerCase(),
|
|
409
|
-
gitBranch: args.gitBranch
|
|
426
|
+
gitBranch: args.gitBranch,
|
|
410
427
|
environment: args.environment?.toLowerCase(),
|
|
411
428
|
};
|
|
412
429
|
|
|
@@ -490,6 +507,13 @@ export async function handleCreateGrayReleaseTicket(args, { jira, notifier }) {
|
|
|
490
507
|
// gray release notes
|
|
491
508
|
fields[GRAY_RELEASE_FIELD_IDS.grayReleaseNotes] = NOTES_TEMPLATES.grayRelease;
|
|
492
509
|
|
|
510
|
+
await assertNoOpenPRBeforeCreate({
|
|
511
|
+
ticketType: 'grayrelease',
|
|
512
|
+
systemCode: normalizedArgs.systemCode,
|
|
513
|
+
module: normalizedArgs.module,
|
|
514
|
+
branch: normalizedArgs.gitBranch,
|
|
515
|
+
}, { jira });
|
|
516
|
+
|
|
493
517
|
if (normalizedArgs.dryRun) {
|
|
494
518
|
return ok({ dryRun: true, summary: fields.summary, grayReleaseVersion, fields });
|
|
495
519
|
}
|
|
@@ -517,6 +541,11 @@ export async function handleCreateGrayReleaseTicket(args, { jira, notifier }) {
|
|
|
517
541
|
if (stgTickets.length > 0) {
|
|
518
542
|
result.suggest_link = {
|
|
519
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
|
+
},
|
|
520
549
|
stgTickets,
|
|
521
550
|
hint: '回覆「是」或 yes 即可自動關聯,或直接呼叫 link_stg_grayrelease',
|
|
522
551
|
};
|
|
@@ -532,22 +561,50 @@ export async function handleCreateGrayReleaseTicket(args, { jira, notifier }) {
|
|
|
532
561
|
/**
|
|
533
562
|
* 將 STG 灰度單關聯(relates to)到 UAT 灰度單
|
|
534
563
|
*/
|
|
535
|
-
export async function handleLinkStgGrayRelease(args, {
|
|
564
|
+
export async function handleLinkStgGrayRelease(args, {
|
|
565
|
+
jira, notifier, progress = () => {
|
|
566
|
+
}
|
|
567
|
+
}) {
|
|
536
568
|
try {
|
|
537
569
|
const { uatKey, stgKeys } = args;
|
|
538
570
|
if (!Array.isArray(stgKeys) || stgKeys.length === 0) {
|
|
539
571
|
return error('stgKeys 不可為空');
|
|
540
572
|
}
|
|
541
573
|
|
|
574
|
+
progress({
|
|
575
|
+
phase: 'action',
|
|
576
|
+
title: '開始關聯 STG 灰度單',
|
|
577
|
+
detail: `${stgKeys.join(', ')} -> ${uatKey}`,
|
|
578
|
+
issueKey: uatKey,
|
|
579
|
+
total: stgKeys.length,
|
|
580
|
+
});
|
|
581
|
+
|
|
542
582
|
const linked = [];
|
|
543
583
|
const failed = [];
|
|
544
584
|
|
|
545
|
-
for (const stgKey of stgKeys) {
|
|
585
|
+
for (const [index, stgKey] of stgKeys.entries()) {
|
|
546
586
|
try {
|
|
547
587
|
await jira.linkIssue(stgKey, uatKey, 'Relates');
|
|
548
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
|
+
|
|
549
598
|
} catch (e) {
|
|
550
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
|
+
});
|
|
551
608
|
}
|
|
552
609
|
}
|
|
553
610
|
|
|
@@ -555,6 +612,15 @@ export async function handleLinkStgGrayRelease(args, { jira, notifier }) {
|
|
|
555
612
|
await notifier.notify(uatKey, `已將 STG 灰度單 ${linked.join(', ')} relates to ${uatKey}`);
|
|
556
613
|
}
|
|
557
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
|
+
|
|
558
624
|
return ok({
|
|
559
625
|
uatKey,
|
|
560
626
|
linked,
|
|
@@ -571,6 +637,20 @@ export async function handleLinkStgGrayRelease(args, { jira, notifier }) {
|
|
|
571
637
|
|
|
572
638
|
// ── Handlers (NEW) ───────────────────────────────────────────────
|
|
573
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
|
+
|
|
574
654
|
/**
|
|
575
655
|
* 自動執行 GrayRelease 完整流程
|
|
576
656
|
*/
|
|
@@ -615,11 +695,11 @@ export async function handleGetGrayReleaseStatus(args, { jira }) {
|
|
|
615
695
|
}
|
|
616
696
|
|
|
617
697
|
/**
|
|
618
|
-
* 執行 GrayRelease 部署流程,不觸發
|
|
698
|
+
* 執行 GrayRelease 部署流程,不觸發 build。
|
|
619
699
|
*/
|
|
620
700
|
export async function handleDeployGrayRelease(args, ctx) {
|
|
621
701
|
const { issueKey } = args;
|
|
622
|
-
const {
|
|
702
|
+
const { notifier } = ctx;
|
|
623
703
|
|
|
624
704
|
try {
|
|
625
705
|
progress(ctx, {
|
|
@@ -689,6 +769,70 @@ export async function handleContinueGrayRelease(args, ctx) {
|
|
|
689
769
|
|
|
690
770
|
// ── Internal Helpers ─────────────────────────────────────────────
|
|
691
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
|
+
|
|
692
836
|
/**
|
|
693
837
|
* 查詢 GrayRelease 單狀態並分析下一步
|
|
694
838
|
*/
|
|
@@ -756,10 +900,7 @@ async function executeGrayReleaseDeployFlow(issueKey, ctx) {
|
|
|
756
900
|
}
|
|
757
901
|
|
|
758
902
|
log.push(`🚀 開始執行 GrayRelease Deploy - 環境: ${environment.toUpperCase()}`);
|
|
759
|
-
|
|
760
|
-
await jira.transitionByName(issueKey, 'Planning');
|
|
761
|
-
await notifier.notify(issueKey, '開始執行 GrayRelease deploy 流程,先回到 Planning');
|
|
762
|
-
|
|
903
|
+
|
|
763
904
|
while (true) {
|
|
764
905
|
const issue = await jira.getIssue(issueKey);
|
|
765
906
|
const currentStatus = issue.fields.status.name;
|
|
@@ -792,11 +933,36 @@ async function executeGrayReleaseDeployFlow(issueKey, ctx) {
|
|
|
792
933
|
}
|
|
793
934
|
|
|
794
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
|
+
|
|
795
960
|
const applyTransition = await findTransitionByName(issueKey, 'Apply to approval', jira);
|
|
796
961
|
if (!applyTransition) {
|
|
797
|
-
log.push(' ⏸️ 目前仍需 build,deploy_grayrelease 不會觸發
|
|
962
|
+
log.push(' ⏸️ 目前仍需 build,deploy_grayrelease 不會觸發 build');
|
|
798
963
|
break;
|
|
799
964
|
}
|
|
965
|
+
|
|
800
966
|
log.push(' 執行: Apply to approval');
|
|
801
967
|
await jira.transitionByName(issueKey, 'Apply to approval');
|
|
802
968
|
await notifier.notify(issueKey, '進入 GrayRelease 部署簽核流程');
|
|
@@ -825,7 +991,7 @@ async function executeGrayReleaseDeployFlow(issueKey, ctx) {
|
|
|
825
991
|
}
|
|
826
992
|
|
|
827
993
|
if (normalizedStatus === 'VERIFY') {
|
|
828
|
-
log.push(' ⏸️ 已進入 VERIFY
|
|
994
|
+
log.push(' ⏸️ 已進入 VERIFY,請人工驗證;若需再次 build 請明確執行 build');
|
|
829
995
|
break;
|
|
830
996
|
}
|
|
831
997
|
|
|
@@ -921,7 +1087,7 @@ async function executeAutoGrayReleaseFlow(issueKey, options, ctx) {
|
|
|
921
1087
|
log.push(' ⏳ 等待 Jenkins Build 完成...');
|
|
922
1088
|
await waitForGrayReleaseResult(
|
|
923
1089
|
issueKey,
|
|
924
|
-
|
|
1090
|
+
SHARED_FIELD_IDS.buildResult,
|
|
925
1091
|
'GrayRelease Build',
|
|
926
1092
|
jira,
|
|
927
1093
|
ctx.progress,
|
|
@@ -1005,7 +1171,13 @@ async function executeAutoGrayReleaseFlow(issueKey, options, ctx) {
|
|
|
1005
1171
|
|
|
1006
1172
|
async function runGrayReleaseDeployStep(issueKey, systemCode, ctx, log) {
|
|
1007
1173
|
const { jira, notifier } = ctx;
|
|
1008
|
-
const needSwitch = await
|
|
1174
|
+
const needSwitch = await shouldSwitchExecutionNode({
|
|
1175
|
+
issueKey,
|
|
1176
|
+
systemCode,
|
|
1177
|
+
envGroup: 'nonPrd',
|
|
1178
|
+
jira,
|
|
1179
|
+
log,
|
|
1180
|
+
});
|
|
1009
1181
|
|
|
1010
1182
|
if (needSwitch) {
|
|
1011
1183
|
log.push(' 執行: Switch Execution Node');
|
|
@@ -1016,7 +1188,7 @@ async function runGrayReleaseDeployStep(issueKey, systemCode, ctx, log) {
|
|
|
1016
1188
|
});
|
|
1017
1189
|
await jira.transitionByName(issueKey, 'Switch Execution Node');
|
|
1018
1190
|
await notifier.notify(issueKey, '切換 Ansible instance');
|
|
1019
|
-
await
|
|
1191
|
+
await waitForSharedSwitchExecutionNode({ issueKey, systemCode, envGroup: 'nonPrd', jira });
|
|
1020
1192
|
}
|
|
1021
1193
|
|
|
1022
1194
|
log.push(' 執行: GrayRelease Deploy');
|
|
@@ -1104,7 +1276,7 @@ async function handleGrayReleaseApproval(issueKey, environment, systemCode, ctx)
|
|
|
1104
1276
|
|
|
1105
1277
|
// STG: 查 wiki 值班組長 → assign → 發 jabber → 等待 approve
|
|
1106
1278
|
if (env === 'stg') {
|
|
1107
|
-
const managerResult = await handleGetReleaseManager(
|
|
1279
|
+
const managerResult = await handleGetReleaseManager();
|
|
1108
1280
|
const managerData = parseToolResult(managerResult);
|
|
1109
1281
|
|
|
1110
1282
|
if (!managerData?.found) {
|
|
@@ -1147,8 +1319,8 @@ async function handleGrayReleaseApproval(issueKey, environment, systemCode, ctx)
|
|
|
1147
1319
|
// 輪詢等待 Approve(狀態變為 WAIT DEPLOY)
|
|
1148
1320
|
const poller = new Poller(jira);
|
|
1149
1321
|
await poller.waitForStatus(issueKey, 'WAIT DEPLOY', {
|
|
1150
|
-
intervalMs:
|
|
1151
|
-
timeoutMs:
|
|
1322
|
+
intervalMs: getPollIntervalMs(),
|
|
1323
|
+
timeoutMs: getPollTimeoutMs(),
|
|
1152
1324
|
onProgress: ctx.progress,
|
|
1153
1325
|
});
|
|
1154
1326
|
|
|
@@ -1192,8 +1364,8 @@ async function handleGrayReleaseApproval(issueKey, environment, systemCode, ctx)
|
|
|
1192
1364
|
issueKey,
|
|
1193
1365
|
keyword: 'approved',
|
|
1194
1366
|
authorAccountId: commentReviewerAccountId,
|
|
1195
|
-
|
|
1196
|
-
|
|
1367
|
+
intervalMs: getPollIntervalMs(),
|
|
1368
|
+
timeoutMs: getPollTimeoutMs(),
|
|
1197
1369
|
},
|
|
1198
1370
|
{ jira, progress: ctx.progress },
|
|
1199
1371
|
);
|
|
@@ -1236,8 +1408,8 @@ async function handleGrayReleaseApproval(issueKey, environment, systemCode, ctx)
|
|
|
1236
1408
|
|
|
1237
1409
|
const poller = new Poller(jira);
|
|
1238
1410
|
await poller.waitForStatus(issueKey, 'WAIT DEPLOY', {
|
|
1239
|
-
intervalMs:
|
|
1240
|
-
timeoutMs:
|
|
1411
|
+
intervalMs: getPollIntervalMs(),
|
|
1412
|
+
timeoutMs: getPollTimeoutMs(),
|
|
1241
1413
|
onProgress: ctx.progress,
|
|
1242
1414
|
});
|
|
1243
1415
|
|
|
@@ -1247,82 +1419,6 @@ async function handleGrayReleaseApproval(issueKey, environment, systemCode, ctx)
|
|
|
1247
1419
|
throw new Error(`不支援的環境: ${environment}`);
|
|
1248
1420
|
}
|
|
1249
1421
|
|
|
1250
|
-
/**
|
|
1251
|
-
* 判斷是否需要執行 Switch Execution Node
|
|
1252
|
-
* 規則:查詢同系統上次 CD or GrayRelease 部署的環境群組,若與本次不同則需切換
|
|
1253
|
-
*/
|
|
1254
|
-
async function needSwitchExecutionNode(issueKey, systemCode, jira, log) {
|
|
1255
|
-
if (await hasRecentSwitchExecutionNodeComment(issueKey, systemCode, jira)) {
|
|
1256
|
-
return false;
|
|
1257
|
-
}
|
|
1258
|
-
|
|
1259
|
-
// GrayRelease 永遠是 nonPrd 群組
|
|
1260
|
-
const thisGroup = 'nonPrd';
|
|
1261
|
-
|
|
1262
|
-
// 只查詢 CD 單的部署歷史(不包含 GrayRelease)
|
|
1263
|
-
const isPrdEnv = (env) => ['prd', 'dr', 'prd/dr', 'prd&dr'].includes(env?.toLowerCase()?.trim());
|
|
1264
|
-
|
|
1265
|
-
const jql_cd = `project = CID AND (issuetype = CD OR issuetype = GrayRelease) AND text ~ "${systemCode}" AND status = Done AND issueKey != "${issueKey}" ORDER BY updated DESC`;
|
|
1266
|
-
|
|
1267
|
-
try {
|
|
1268
|
-
log.push(` before 查詢歷史 CD/GrayRelease 單,判斷是否需要切換 Execution Node`);
|
|
1269
|
-
const cdResults = await jira.searchIssues(jql_cd, ['customfield_13436', 'updated'], 1);
|
|
1270
|
-
log.push(` after 查詢歷史 CD/GrayRelease 單,判斷是否需要切換 Execution Node`, cdResults);
|
|
1271
|
-
const cdIssue = cdResults[0] ?? null;
|
|
1272
|
-
|
|
1273
|
-
// 查不到歷史 → 保守執行 Switch
|
|
1274
|
-
if (!cdIssue) {
|
|
1275
|
-
return true;
|
|
1276
|
-
}
|
|
1277
|
-
|
|
1278
|
-
// CD:讀 customfield_13436.value 判斷群組
|
|
1279
|
-
const envVal = cdIssue.fields?.customfield_13436?.value ?? '';
|
|
1280
|
-
const lastGroup = isPrdEnv(envVal) ? 'prd' : 'nonPrd';
|
|
1281
|
-
|
|
1282
|
-
// 群組不同 → 需要 Switch
|
|
1283
|
-
return lastGroup !== thisGroup;
|
|
1284
|
-
} catch {
|
|
1285
|
-
// 查詢失敗 → 保守執行 Switch
|
|
1286
|
-
return true;
|
|
1287
|
-
}
|
|
1288
|
-
}
|
|
1289
|
-
|
|
1290
|
-
async function waitForSwitchExecutionNode(issueKey, systemCode, jira) {
|
|
1291
|
-
const waitMs = parseInt(process.env.SWITCH_EXECUTION_NODE_WAIT_MS ?? '180000');
|
|
1292
|
-
const intervalMs = getPollIntervalMs();
|
|
1293
|
-
const deadline = Date.now() + waitMs;
|
|
1294
|
-
|
|
1295
|
-
while (true) {
|
|
1296
|
-
|
|
1297
|
-
if (Date.now() >= deadline) {
|
|
1298
|
-
return;
|
|
1299
|
-
}
|
|
1300
|
-
|
|
1301
|
-
await sleep(intervalMs);
|
|
1302
|
-
}
|
|
1303
|
-
}
|
|
1304
|
-
|
|
1305
|
-
async function hasRecentSwitchExecutionNodeComment(issueKey, systemCode, jira) {
|
|
1306
|
-
if (!jira.getComments) {
|
|
1307
|
-
return false;
|
|
1308
|
-
}
|
|
1309
|
-
|
|
1310
|
-
try {
|
|
1311
|
-
const comments = await jira.getComments(issueKey);
|
|
1312
|
-
const lowerSystemCode = String(systemCode ?? '').toLowerCase();
|
|
1313
|
-
return comments.some((comment) => {
|
|
1314
|
-
const body = String(comment.body ?? '').toLowerCase();
|
|
1315
|
-
const author = String(comment.author?.displayName ?? '').toLowerCase();
|
|
1316
|
-
return author === 'cid jira worker'
|
|
1317
|
-
&& body.includes('instance_group')
|
|
1318
|
-
&& body.includes('nonprd_executionnode')
|
|
1319
|
-
&& (!lowerSystemCode || body.includes(lowerSystemCode));
|
|
1320
|
-
});
|
|
1321
|
-
} catch {
|
|
1322
|
-
return false;
|
|
1323
|
-
}
|
|
1324
|
-
}
|
|
1325
|
-
|
|
1326
1422
|
function sleep(ms) {
|
|
1327
1423
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1328
1424
|
}
|
package/tools/helpers.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {getDeployConfig, META_TEST_NODES,
|
|
1
|
+
import {getDeployConfig, META_TEST_NODES, SERVER_MODULE_MAP, SERVERS} from '../constants/index.js';
|
|
2
2
|
|
|
3
3
|
export function ok(data) {
|
|
4
4
|
return {content: [{type: 'text', text: JSON.stringify(data, null, 2)}]};
|
|
@@ -22,7 +22,7 @@ export function today() {
|
|
|
22
22
|
* @param {boolean} [metaTest] - 是否為 meta test 模式(ibk/cwa 只回傳單一節點)
|
|
23
23
|
* @returns {string[]} Server 列表
|
|
24
24
|
*/
|
|
25
|
-
export function getServerList(systemCode, env, metaTest, module) {
|
|
25
|
+
export function getServerList(systemCode, env, metaTest = false, module) {
|
|
26
26
|
const systemConfig = SERVERS[systemCode];
|
|
27
27
|
if (!systemConfig) return [];
|
|
28
28
|
|
|
@@ -36,7 +36,12 @@ export function getServerList(systemCode, env, metaTest, module) {
|
|
|
36
36
|
|
|
37
37
|
// 如果是嵌套結構 (EIB, EVT),依 SERVER_MODULE_MAP 選擇 was 或 web
|
|
38
38
|
if (typeof envConfig === 'object' && !Array.isArray(envConfig)) {
|
|
39
|
-
const
|
|
39
|
+
const moduleNameMap = {
|
|
40
|
+
cust: 'web_cust',
|
|
41
|
+
agent: 'web_agnt',
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const serverKey = systemCode === getDeployConfig().systemCodes.EIB ? SERVER_MODULE_MAP[moduleNameMap[module]] : SERVER_MODULE_MAP[module];
|
|
40
45
|
if (serverKey && envConfig[serverKey]) return envConfig[serverKey];
|
|
41
46
|
// fallback:無法判斷時回傳所有(避免空陣列)
|
|
42
47
|
return Object.values(envConfig).flat();
|
|
@@ -45,10 +50,6 @@ export function getServerList(systemCode, env, metaTest, module) {
|
|
|
45
50
|
return Array.isArray(envConfig) ? envConfig : [];
|
|
46
51
|
}
|
|
47
52
|
|
|
48
|
-
export function getClusterList(systemCode, env, module) {
|
|
49
|
-
return getServerList(systemCode, env, false, module);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
53
|
/**
|
|
53
54
|
* 依 systemCode + module + versionName 組出 release 版本名稱。
|
|
54
55
|
*/
|
|
@@ -63,3 +64,23 @@ export function getModuleName(systemCode, module, versionName) {
|
|
|
63
64
|
versionName
|
|
64
65
|
);
|
|
65
66
|
}
|
|
67
|
+
|
|
68
|
+
export function getPollIntervalMs() {
|
|
69
|
+
return parseInt(process.env.POLL_INTERVAL_MS ?? '30000');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function getPollTimeoutMs() {
|
|
73
|
+
return parseInt(process.env.POLL_TIMEOUT_MS ?? '3600000');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function isPassingResult(value) {
|
|
77
|
+
return ['pass', 'passed', 'success', 'succeeded', 'done'].includes(
|
|
78
|
+
String(value ?? '').trim().toLowerCase(),
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function isFailingResult(value) {
|
|
83
|
+
return ['fail', 'failed', 'failure', 'error'].includes(
|
|
84
|
+
String(value ?? '').trim().toLowerCase(),
|
|
85
|
+
);
|
|
86
|
+
}
|