@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/constants/config.js +34 -5
- package/constants/issue-types.js +2 -2
- package/index.js +1 -1
- package/jira-client.js +79 -44
- package/notifier.js +3 -2
- package/package.json +9 -2
- package/platform-config.js +2 -1
- package/poller.js +4 -2
- package/tools/branch-prs.js +164 -0
- package/tools/build.js +117 -0
- package/tools/cd.js +506 -119
- package/tools/ci.js +497 -29
- package/tools/deployment-helpers.js +103 -0
- package/tools/deployment.js +269 -0
- package/tools/grayrelease.js +244 -231
- package/tools/helpers.js +29 -7
- package/tools/index.js +131 -566
- package/tools/jabber.js +13 -5
- package/tools/library.js +221 -25
- package/tools/release.js +137 -38
- package/tools/transition-helpers.js +22 -0
- package/tools/workflows.js +388 -110
package/tools/grayrelease.js
CHANGED
|
@@ -2,33 +2,49 @@
|
|
|
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 {
|
|
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 {getRuntimeConfigValue} from '../constants/config.js';
|
|
44
|
+
import {
|
|
45
|
+
needSwitchExecutionNode as shouldSwitchExecutionNode,
|
|
46
|
+
waitForSwitchExecutionNode as waitForSharedSwitchExecutionNode,
|
|
47
|
+
} from './deployment-helpers.js';
|
|
32
48
|
|
|
33
49
|
// ── Flow Definition ──────────────────────────────────────────────
|
|
34
50
|
|
|
@@ -40,7 +56,7 @@ import { handleSendJabberMessage } from './jabber.js';
|
|
|
40
56
|
* PLANNING → (Accept) → WAIT FOR BUILD → (GrayRelease Build)
|
|
41
57
|
* → WAIT FOR BUILD → (Apply to approval) → WAIT APPROVAL
|
|
42
58
|
* → (Approve,依環境處理簽核) → WAIT DEPLOY
|
|
43
|
-
* → (Switch Execution Node,視上次 CD 部署環境群組 prd/nonPrd 決定是否需要)
|
|
59
|
+
* → (Switch Execution Node,視上次 CD or GrayRelease部署環境群組 prd/nonPrd 決定是否需要)
|
|
44
60
|
* → WAIT DEPLOY → (GrayRelease Deploy) → WAIT DEPLOY → (To Verify)
|
|
45
61
|
* → VERIFY → (Verify Success) → MERGE CODE AND TAG → (To Done) → DONE
|
|
46
62
|
*
|
|
@@ -49,9 +65,8 @@ import { handleSendJabberMessage } from './jabber.js';
|
|
|
49
65
|
* - STG:指派給當日 Release Manager,等待簽核
|
|
50
66
|
* - UAT:依 env/config 指派第一階段與最終簽核人
|
|
51
67
|
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* 執行 Verify fail 回到 PLANNING 後重新 GrayRelease Build。
|
|
68
|
+
* Build 規則:
|
|
69
|
+
* DONE 以外的狀態都會先回到 PLANNING,再重新觸發 GrayRelease Build。
|
|
55
70
|
*/
|
|
56
71
|
const GRAYRELEASE_FLOW_MAP = {
|
|
57
72
|
'PLANNING': {
|
|
@@ -101,14 +116,6 @@ function normalizeStatusName(statusName) {
|
|
|
101
116
|
.toUpperCase();
|
|
102
117
|
}
|
|
103
118
|
|
|
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
119
|
function progress(ctx, event) {
|
|
113
120
|
if (typeof ctx.progress === 'function') {
|
|
114
121
|
ctx.progress(event);
|
|
@@ -116,10 +123,14 @@ function progress(ctx, event) {
|
|
|
116
123
|
}
|
|
117
124
|
|
|
118
125
|
function getJabberJid(accountId) {
|
|
119
|
-
const domain =
|
|
126
|
+
const domain = getRuntimeConfigValue('JABBER_DOMAIN') ?? getDeployConfig().jabber?.domain;
|
|
120
127
|
return domain ? `${accountId}@${domain}` : accountId;
|
|
121
128
|
}
|
|
122
129
|
|
|
130
|
+
function getJiraIssueUrl(issueKey) {
|
|
131
|
+
return `${getRuntimeConfigValue('JIRA_BASE_URL')}/browse/${issueKey}`;
|
|
132
|
+
}
|
|
133
|
+
|
|
123
134
|
function getGrayReleaseUatApprovers() {
|
|
124
135
|
const config = getDeployConfig().release.grayReleaseUatApprovers ?? {};
|
|
125
136
|
return {
|
|
@@ -130,18 +141,13 @@ function getGrayReleaseUatApprovers() {
|
|
|
130
141
|
};
|
|
131
142
|
}
|
|
132
143
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
return ['fail', 'failed', 'failure', 'error'].includes(
|
|
141
|
-
String(value ?? '').trim().toLowerCase(),
|
|
142
|
-
);
|
|
143
|
-
}
|
|
144
|
-
|
|
144
|
+
/**
|
|
145
|
+
* 根據 transition name 搜尋 issue 可用的 transition,回傳該 transition 物件(包含 id)以便後續呼叫 transition API
|
|
146
|
+
* @param issueKey
|
|
147
|
+
* @param transitionName
|
|
148
|
+
* @param jira
|
|
149
|
+
* @returns {Object|undefined}
|
|
150
|
+
*/
|
|
145
151
|
async function findTransitionByName(issueKey, transitionName, jira) {
|
|
146
152
|
const transitions = await jira.getTransitions(issueKey);
|
|
147
153
|
return transitions.find((transition) => (
|
|
@@ -149,7 +155,8 @@ async function findTransitionByName(issueKey, transitionName, jira) {
|
|
|
149
155
|
));
|
|
150
156
|
}
|
|
151
157
|
|
|
152
|
-
async function waitForGrayReleaseResult(issueKey, fieldId, label, jira, onProgress = () => {
|
|
158
|
+
async function waitForGrayReleaseResult(issueKey, fieldId, label, jira, onProgress = () => {
|
|
159
|
+
}) {
|
|
153
160
|
const intervalMs = getPollIntervalMs();
|
|
154
161
|
const timeoutMs = getPollTimeoutMs();
|
|
155
162
|
const startedAt = Date.now();
|
|
@@ -174,7 +181,7 @@ async function waitForGrayReleaseResult(issueKey, fieldId, label, jira, onProgre
|
|
|
174
181
|
});
|
|
175
182
|
|
|
176
183
|
if (isPassingResult(lastValue)) {
|
|
177
|
-
return
|
|
184
|
+
return;
|
|
178
185
|
}
|
|
179
186
|
|
|
180
187
|
if (isFailingResult(lastValue)) {
|
|
@@ -277,6 +284,21 @@ export function getGrayReleaseToolDefinitions() {
|
|
|
277
284
|
},
|
|
278
285
|
},
|
|
279
286
|
},
|
|
287
|
+
{
|
|
288
|
+
name: 'build_grayrelease',
|
|
289
|
+
description:
|
|
290
|
+
'觸發 GrayRelease 上版單的 Jenkins Build。DONE 以外的狀態會先切回 PLANNING,再走到 WAIT FOR BUILD 後觸發 GrayRelease Build;若使用者要求 GrayRelease deploy、dev deploy 或部署灰度單,必須改用 deploy_grayrelease。',
|
|
291
|
+
inputSchema: {
|
|
292
|
+
type: 'object',
|
|
293
|
+
required: ['issueKey'],
|
|
294
|
+
properties: {
|
|
295
|
+
issueKey: {
|
|
296
|
+
type: 'string',
|
|
297
|
+
description: 'GrayRelease 單 issue key,例如 CID-822',
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
},
|
|
280
302
|
{
|
|
281
303
|
name: 'auto_grayrelease',
|
|
282
304
|
description:
|
|
@@ -333,34 +355,6 @@ export function getGrayReleaseToolDefinitions() {
|
|
|
333
355
|
},
|
|
334
356
|
},
|
|
335
357
|
},
|
|
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
358
|
{
|
|
365
359
|
name: 'continue_grayrelease',
|
|
366
360
|
description:
|
|
@@ -397,14 +391,13 @@ export function getGrayReleaseToolDefinitions() {
|
|
|
397
391
|
* @returns {Promise<{key:string, summary:string}[]>}
|
|
398
392
|
*/
|
|
399
393
|
async function findUnlinkedStgGrayReleases(gitBranch, moduleId, jira) {
|
|
400
|
-
const
|
|
401
|
-
const GR_ISSUE_TYPE = '12601';
|
|
394
|
+
const GR_ISSUE_TYPE = ISSUE_TYPE_IDS.GrayRelease;
|
|
402
395
|
const jql = [
|
|
403
396
|
'project = CID',
|
|
404
397
|
`issuetype = ${GR_ISSUE_TYPE}`,
|
|
405
|
-
`
|
|
406
|
-
`
|
|
407
|
-
`
|
|
398
|
+
`CID_env = STG`,
|
|
399
|
+
`CID_branch ~ "${gitBranch.replace(/"/g, '\\"').replace(/\//g, '\\u002f')}"`,
|
|
400
|
+
`CID_system_module = ${moduleId}`,
|
|
408
401
|
'ORDER BY created DESC',
|
|
409
402
|
].join(' AND ');
|
|
410
403
|
|
|
@@ -435,7 +428,7 @@ export async function handleCreateGrayReleaseTicket(args, { jira, notifier }) {
|
|
|
435
428
|
const normalizedArgs = {
|
|
436
429
|
...args,
|
|
437
430
|
module: args.module ?? args.systemCode?.toLowerCase(),
|
|
438
|
-
gitBranch: args.gitBranch
|
|
431
|
+
gitBranch: args.gitBranch,
|
|
439
432
|
environment: args.environment?.toLowerCase(),
|
|
440
433
|
};
|
|
441
434
|
|
|
@@ -519,6 +512,13 @@ export async function handleCreateGrayReleaseTicket(args, { jira, notifier }) {
|
|
|
519
512
|
// gray release notes
|
|
520
513
|
fields[GRAY_RELEASE_FIELD_IDS.grayReleaseNotes] = NOTES_TEMPLATES.grayRelease;
|
|
521
514
|
|
|
515
|
+
await assertNoOpenPRBeforeCreate({
|
|
516
|
+
ticketType: 'grayrelease',
|
|
517
|
+
systemCode: normalizedArgs.systemCode,
|
|
518
|
+
module: normalizedArgs.module,
|
|
519
|
+
branch: normalizedArgs.gitBranch,
|
|
520
|
+
}, { jira });
|
|
521
|
+
|
|
522
522
|
if (normalizedArgs.dryRun) {
|
|
523
523
|
return ok({ dryRun: true, summary: fields.summary, grayReleaseVersion, fields });
|
|
524
524
|
}
|
|
@@ -532,7 +532,7 @@ export async function handleCreateGrayReleaseTicket(args, { jira, notifier }) {
|
|
|
532
532
|
const result = {
|
|
533
533
|
issueKey: issue.key,
|
|
534
534
|
issueId: issue.id,
|
|
535
|
-
url:
|
|
535
|
+
url: getJiraIssueUrl(issue.key),
|
|
536
536
|
type: 'GrayRelease',
|
|
537
537
|
system: normalizedArgs.systemCode,
|
|
538
538
|
module: normalizedArgs.module,
|
|
@@ -546,6 +546,11 @@ export async function handleCreateGrayReleaseTicket(args, { jira, notifier }) {
|
|
|
546
546
|
if (stgTickets.length > 0) {
|
|
547
547
|
result.suggest_link = {
|
|
548
548
|
message: `找到以下 STG 灰度單(branch: ${args.gitBranch})尚未關聯到任何 UAT 灰度單,是否要建立 relates to 關聯到 ${issue.key}?`,
|
|
549
|
+
toolName: 'link_stg_grayrelease',
|
|
550
|
+
arguments: {
|
|
551
|
+
uatKey: issue.key,
|
|
552
|
+
stgKeys: stgTickets.map((ticket) => ticket.key),
|
|
553
|
+
},
|
|
549
554
|
stgTickets,
|
|
550
555
|
hint: '回覆「是」或 yes 即可自動關聯,或直接呼叫 link_stg_grayrelease',
|
|
551
556
|
};
|
|
@@ -561,22 +566,50 @@ export async function handleCreateGrayReleaseTicket(args, { jira, notifier }) {
|
|
|
561
566
|
/**
|
|
562
567
|
* 將 STG 灰度單關聯(relates to)到 UAT 灰度單
|
|
563
568
|
*/
|
|
564
|
-
export async function handleLinkStgGrayRelease(args, {
|
|
569
|
+
export async function handleLinkStgGrayRelease(args, {
|
|
570
|
+
jira, notifier, progress = () => {
|
|
571
|
+
}
|
|
572
|
+
}) {
|
|
565
573
|
try {
|
|
566
574
|
const { uatKey, stgKeys } = args;
|
|
567
575
|
if (!Array.isArray(stgKeys) || stgKeys.length === 0) {
|
|
568
576
|
return error('stgKeys 不可為空');
|
|
569
577
|
}
|
|
570
578
|
|
|
579
|
+
progress({
|
|
580
|
+
phase: 'action',
|
|
581
|
+
title: '開始關聯 STG 灰度單',
|
|
582
|
+
detail: `${stgKeys.join(', ')} -> ${uatKey}`,
|
|
583
|
+
issueKey: uatKey,
|
|
584
|
+
total: stgKeys.length,
|
|
585
|
+
});
|
|
586
|
+
|
|
571
587
|
const linked = [];
|
|
572
588
|
const failed = [];
|
|
573
589
|
|
|
574
|
-
for (const stgKey of stgKeys) {
|
|
590
|
+
for (const [index, stgKey] of stgKeys.entries()) {
|
|
575
591
|
try {
|
|
576
592
|
await jira.linkIssue(stgKey, uatKey, 'Relates');
|
|
577
593
|
linked.push(stgKey);
|
|
594
|
+
progress({
|
|
595
|
+
phase: 'done',
|
|
596
|
+
title: 'STG 灰度單關聯成功',
|
|
597
|
+
detail: `${stgKey} -> ${uatKey}`,
|
|
598
|
+
issueKey: uatKey,
|
|
599
|
+
current: index + 1,
|
|
600
|
+
total: stgKeys.length,
|
|
601
|
+
});
|
|
602
|
+
|
|
578
603
|
} catch (e) {
|
|
579
604
|
failed.push({ key: stgKey, reason: e.message });
|
|
605
|
+
progress({
|
|
606
|
+
phase: 'error',
|
|
607
|
+
title: 'STG 灰度單關聯失敗',
|
|
608
|
+
detail: `${stgKey}: ${e.message}`,
|
|
609
|
+
issueKey: uatKey,
|
|
610
|
+
current: index + 1,
|
|
611
|
+
total: stgKeys.length,
|
|
612
|
+
});
|
|
580
613
|
}
|
|
581
614
|
}
|
|
582
615
|
|
|
@@ -584,6 +617,15 @@ export async function handleLinkStgGrayRelease(args, { jira, notifier }) {
|
|
|
584
617
|
await notifier.notify(uatKey, `已將 STG 灰度單 ${linked.join(', ')} relates to ${uatKey}`);
|
|
585
618
|
}
|
|
586
619
|
|
|
620
|
+
progress({
|
|
621
|
+
phase: failed.length > 0 ? 'done' : 'done',
|
|
622
|
+
title: 'STG 灰度單關聯完成',
|
|
623
|
+
detail: `成功 ${linked.length} 張,失敗 ${failed.length} 張`,
|
|
624
|
+
issueKey: uatKey,
|
|
625
|
+
linked,
|
|
626
|
+
failed,
|
|
627
|
+
});
|
|
628
|
+
|
|
587
629
|
return ok({
|
|
588
630
|
uatKey,
|
|
589
631
|
linked,
|
|
@@ -600,6 +642,20 @@ export async function handleLinkStgGrayRelease(args, { jira, notifier }) {
|
|
|
600
642
|
|
|
601
643
|
// ── Handlers (NEW) ───────────────────────────────────────────────
|
|
602
644
|
|
|
645
|
+
/**
|
|
646
|
+
* 觸發 GrayRelease Build。
|
|
647
|
+
*/
|
|
648
|
+
export async function handleBuildGrayRelease(args, ctx) {
|
|
649
|
+
const { issueKey } = args;
|
|
650
|
+
|
|
651
|
+
try {
|
|
652
|
+
const result = await executeGrayReleaseBuildFlow(issueKey, ctx);
|
|
653
|
+
return ok(result);
|
|
654
|
+
} catch (err) {
|
|
655
|
+
return error(`build_grayrelease 失敗: ${err.message}`);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
603
659
|
/**
|
|
604
660
|
* 自動執行 GrayRelease 完整流程
|
|
605
661
|
*/
|
|
@@ -643,71 +699,12 @@ export async function handleGetGrayReleaseStatus(args, { jira }) {
|
|
|
643
699
|
}
|
|
644
700
|
}
|
|
645
701
|
|
|
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
702
|
/**
|
|
706
|
-
* 執行 GrayRelease 部署流程,不觸發
|
|
703
|
+
* 執行 GrayRelease 部署流程,不觸發 build。
|
|
707
704
|
*/
|
|
708
705
|
export async function handleDeployGrayRelease(args, ctx) {
|
|
709
706
|
const { issueKey } = args;
|
|
710
|
-
const {
|
|
707
|
+
const { notifier } = ctx;
|
|
711
708
|
|
|
712
709
|
try {
|
|
713
710
|
progress(ctx, {
|
|
@@ -777,6 +774,70 @@ export async function handleContinueGrayRelease(args, ctx) {
|
|
|
777
774
|
|
|
778
775
|
// ── Internal Helpers ─────────────────────────────────────────────
|
|
779
776
|
|
|
777
|
+
const GRAYRELEASE_RESET_TO_PLANNING_TRANSITIONS = [
|
|
778
|
+
'Verify fail',
|
|
779
|
+
'Back to Planning',
|
|
780
|
+
'To Planning',
|
|
781
|
+
'Planning',
|
|
782
|
+
];
|
|
783
|
+
|
|
784
|
+
async function executeGrayReleaseBuildFlow(issueKey, ctx) {
|
|
785
|
+
const { jira, notifier } = ctx;
|
|
786
|
+
const log = [];
|
|
787
|
+
|
|
788
|
+
const initIssue = await jira.getIssue(issueKey);
|
|
789
|
+
const initStatus = initIssue.fields.status.name;
|
|
790
|
+
const normalizedInitStatus = normalizeStatusName(initStatus);
|
|
791
|
+
|
|
792
|
+
if (normalizedInitStatus === 'DONE') {
|
|
793
|
+
throw new Error('GrayRelease 已是 DONE,不會自動重新 build。若確定要重跑,請先手動退回流程狀態。');
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
await resetGrayReleaseToPlanningForBuild(issueKey, normalizedInitStatus, jira, log);
|
|
797
|
+
|
|
798
|
+
log.push('執行 Accept,進入 WAIT FOR BUILD...');
|
|
799
|
+
await jira.transitionByName(issueKey, 'Accept');
|
|
800
|
+
await sleep(10 * 1000);
|
|
801
|
+
|
|
802
|
+
log.push('執行 GrayRelease Build...');
|
|
803
|
+
await jira.transitionByName(issueKey, 'GrayRelease Build');
|
|
804
|
+
|
|
805
|
+
const issue = await jira.getIssue(issueKey);
|
|
806
|
+
const newStatus = issue.fields.status.name;
|
|
807
|
+
log.push(`✅ GrayRelease Build 已觸發,目前狀態:${newStatus}`);
|
|
808
|
+
await notifier.notify(issueKey, `Jenkins GrayRelease Build 已觸發(${newStatus})`);
|
|
809
|
+
|
|
810
|
+
return { issueKey, status: newStatus, steps: log };
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
async function resetGrayReleaseToPlanningForBuild(issueKey, normalizedStatus, jira, log) {
|
|
814
|
+
if (normalizedStatus === 'PLANNING') {
|
|
815
|
+
log.push('目前已是 PLANNING,直接進入 build 前置流程');
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
const resetTrans = await findAnyGrayReleaseTransition(
|
|
820
|
+
issueKey,
|
|
821
|
+
GRAYRELEASE_RESET_TO_PLANNING_TRANSITIONS,
|
|
822
|
+
jira,
|
|
823
|
+
);
|
|
824
|
+
|
|
825
|
+
if (!resetTrans) {
|
|
826
|
+
throw new Error('找不到回到 PLANNING 的 transition,無法先 reset 後 build');
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
log.push(`先觸發「${resetTrans.name}」回到 PLANNING,再重新 build...`);
|
|
830
|
+
await jira.transitionById(issueKey, resetTrans.id);
|
|
831
|
+
await sleep(10 * 1000);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
async function findAnyGrayReleaseTransition(issueKey, names, jira) {
|
|
835
|
+
const list = await jira.getTransitions(issueKey);
|
|
836
|
+
return list.find((transition) => (
|
|
837
|
+
names.some((name) => transition.name.toLowerCase() === name.toLowerCase())
|
|
838
|
+
));
|
|
839
|
+
}
|
|
840
|
+
|
|
780
841
|
/**
|
|
781
842
|
* 查詢 GrayRelease 單狀態並分析下一步
|
|
782
843
|
*/
|
|
@@ -844,10 +905,7 @@ async function executeGrayReleaseDeployFlow(issueKey, ctx) {
|
|
|
844
905
|
}
|
|
845
906
|
|
|
846
907
|
log.push(`🚀 開始執行 GrayRelease Deploy - 環境: ${environment.toUpperCase()}`);
|
|
847
|
-
|
|
848
|
-
await jira.transitionByName(issueKey, 'Planning');
|
|
849
|
-
await notifier.notify(issueKey, '開始執行 GrayRelease deploy 流程,先回到 Planning');
|
|
850
|
-
|
|
908
|
+
|
|
851
909
|
while (true) {
|
|
852
910
|
const issue = await jira.getIssue(issueKey);
|
|
853
911
|
const currentStatus = issue.fields.status.name;
|
|
@@ -880,11 +938,36 @@ async function executeGrayReleaseDeployFlow(issueKey, ctx) {
|
|
|
880
938
|
}
|
|
881
939
|
|
|
882
940
|
if (normalizedStatus === 'WAIT FOR BUILD') {
|
|
941
|
+
const buildFields = await jira.getIssueFields(issueKey, [SHARED_FIELD_IDS.buildResult]);
|
|
942
|
+
const rawBuildResult = buildFields[SHARED_FIELD_IDS.buildResult];
|
|
943
|
+
const buildResult = rawBuildResult?.value ?? rawBuildResult;
|
|
944
|
+
if (!isPassingResult(buildResult)) {
|
|
945
|
+
const message = `GrayRelease Build 尚未通過,${SHARED_FIELD_IDS.buildResult}: ${buildResult ?? 'empty'}。是否要先執行 build_grayrelease?`;
|
|
946
|
+
log.push(` ⏸️ ${message}`);
|
|
947
|
+
return {
|
|
948
|
+
issueKey,
|
|
949
|
+
finalStatus: currentStatus,
|
|
950
|
+
needsBuild: true,
|
|
951
|
+
confirm_build: true,
|
|
952
|
+
buildResult,
|
|
953
|
+
buildResultFieldId: SHARED_FIELD_IDS.buildResult,
|
|
954
|
+
message,
|
|
955
|
+
suggest_build: {
|
|
956
|
+
message,
|
|
957
|
+
toolName: 'build_grayrelease',
|
|
958
|
+
arguments: { issueKey },
|
|
959
|
+
hint: '回覆「是」或 yes 即可先執行 build_grayrelease;build pass 後再執行 deploy_grayrelease。',
|
|
960
|
+
},
|
|
961
|
+
log,
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
|
|
883
965
|
const applyTransition = await findTransitionByName(issueKey, 'Apply to approval', jira);
|
|
884
966
|
if (!applyTransition) {
|
|
885
|
-
log.push(' ⏸️ 目前仍需 build,deploy_grayrelease 不會觸發
|
|
967
|
+
log.push(' ⏸️ 目前仍需 build,deploy_grayrelease 不會觸發 build');
|
|
886
968
|
break;
|
|
887
969
|
}
|
|
970
|
+
|
|
888
971
|
log.push(' 執行: Apply to approval');
|
|
889
972
|
await jira.transitionByName(issueKey, 'Apply to approval');
|
|
890
973
|
await notifier.notify(issueKey, '進入 GrayRelease 部署簽核流程');
|
|
@@ -913,7 +996,7 @@ async function executeGrayReleaseDeployFlow(issueKey, ctx) {
|
|
|
913
996
|
}
|
|
914
997
|
|
|
915
998
|
if (normalizedStatus === 'VERIFY') {
|
|
916
|
-
log.push(' ⏸️ 已進入 VERIFY
|
|
999
|
+
log.push(' ⏸️ 已進入 VERIFY,請人工驗證;若需再次 build 請明確執行 build');
|
|
917
1000
|
break;
|
|
918
1001
|
}
|
|
919
1002
|
|
|
@@ -1009,7 +1092,7 @@ async function executeAutoGrayReleaseFlow(issueKey, options, ctx) {
|
|
|
1009
1092
|
log.push(' ⏳ 等待 Jenkins Build 完成...');
|
|
1010
1093
|
await waitForGrayReleaseResult(
|
|
1011
1094
|
issueKey,
|
|
1012
|
-
|
|
1095
|
+
SHARED_FIELD_IDS.buildResult,
|
|
1013
1096
|
'GrayRelease Build',
|
|
1014
1097
|
jira,
|
|
1015
1098
|
ctx.progress,
|
|
@@ -1093,7 +1176,13 @@ async function executeAutoGrayReleaseFlow(issueKey, options, ctx) {
|
|
|
1093
1176
|
|
|
1094
1177
|
async function runGrayReleaseDeployStep(issueKey, systemCode, ctx, log) {
|
|
1095
1178
|
const { jira, notifier } = ctx;
|
|
1096
|
-
const needSwitch = await
|
|
1179
|
+
const needSwitch = await shouldSwitchExecutionNode({
|
|
1180
|
+
issueKey,
|
|
1181
|
+
systemCode,
|
|
1182
|
+
envGroup: 'nonPrd',
|
|
1183
|
+
jira,
|
|
1184
|
+
log,
|
|
1185
|
+
});
|
|
1097
1186
|
|
|
1098
1187
|
if (needSwitch) {
|
|
1099
1188
|
log.push(' 執行: Switch Execution Node');
|
|
@@ -1104,7 +1193,7 @@ async function runGrayReleaseDeployStep(issueKey, systemCode, ctx, log) {
|
|
|
1104
1193
|
});
|
|
1105
1194
|
await jira.transitionByName(issueKey, 'Switch Execution Node');
|
|
1106
1195
|
await notifier.notify(issueKey, '切換 Ansible instance');
|
|
1107
|
-
await
|
|
1196
|
+
await waitForSharedSwitchExecutionNode({ issueKey, systemCode, envGroup: 'nonPrd', jira });
|
|
1108
1197
|
}
|
|
1109
1198
|
|
|
1110
1199
|
log.push(' 執行: GrayRelease Deploy');
|
|
@@ -1192,7 +1281,7 @@ async function handleGrayReleaseApproval(issueKey, environment, systemCode, ctx)
|
|
|
1192
1281
|
|
|
1193
1282
|
// STG: 查 wiki 值班組長 → assign → 發 jabber → 等待 approve
|
|
1194
1283
|
if (env === 'stg') {
|
|
1195
|
-
const managerResult = await handleGetReleaseManager(
|
|
1284
|
+
const managerResult = await handleGetReleaseManager();
|
|
1196
1285
|
const managerData = parseToolResult(managerResult);
|
|
1197
1286
|
|
|
1198
1287
|
if (!managerData?.found) {
|
|
@@ -1227,7 +1316,7 @@ async function handleGrayReleaseApproval(issueKey, environment, systemCode, ctx)
|
|
|
1227
1316
|
await handleSendJabberMessage(
|
|
1228
1317
|
{
|
|
1229
1318
|
to: jabberTo,
|
|
1230
|
-
message: `[GrayRelease 簽核通知] ${issueKey} 需要您的簽核。環境: STG,系統: ${systemCode}\n${
|
|
1319
|
+
message: `[GrayRelease 簽核通知] ${issueKey} 需要您的簽核。環境: STG,系統: ${systemCode}\n${getJiraIssueUrl(issueKey)}`,
|
|
1231
1320
|
},
|
|
1232
1321
|
{},
|
|
1233
1322
|
);
|
|
@@ -1235,8 +1324,8 @@ async function handleGrayReleaseApproval(issueKey, environment, systemCode, ctx)
|
|
|
1235
1324
|
// 輪詢等待 Approve(狀態變為 WAIT DEPLOY)
|
|
1236
1325
|
const poller = new Poller(jira);
|
|
1237
1326
|
await poller.waitForStatus(issueKey, 'WAIT DEPLOY', {
|
|
1238
|
-
intervalMs:
|
|
1239
|
-
timeoutMs:
|
|
1327
|
+
intervalMs: getPollIntervalMs(),
|
|
1328
|
+
timeoutMs: getPollTimeoutMs(),
|
|
1240
1329
|
onProgress: ctx.progress,
|
|
1241
1330
|
});
|
|
1242
1331
|
|
|
@@ -1270,7 +1359,7 @@ async function handleGrayReleaseApproval(issueKey, environment, systemCode, ctx)
|
|
|
1270
1359
|
await handleSendJabberMessage(
|
|
1271
1360
|
{
|
|
1272
1361
|
to: commentReviewerJabber,
|
|
1273
|
-
message: `[GrayRelease 簽核通知] ${issueKey} 需要您的簽核並留言確認。環境: UAT,系統: ${systemCode}\n${
|
|
1362
|
+
message: `[GrayRelease 簽核通知] ${issueKey} 需要您的簽核並留言確認。環境: UAT,系統: ${systemCode}\n${getJiraIssueUrl(issueKey)}`,
|
|
1274
1363
|
},
|
|
1275
1364
|
{},
|
|
1276
1365
|
);
|
|
@@ -1280,8 +1369,8 @@ async function handleGrayReleaseApproval(issueKey, environment, systemCode, ctx)
|
|
|
1280
1369
|
issueKey,
|
|
1281
1370
|
keyword: 'approved',
|
|
1282
1371
|
authorAccountId: commentReviewerAccountId,
|
|
1283
|
-
|
|
1284
|
-
|
|
1372
|
+
intervalMs: getPollIntervalMs(),
|
|
1373
|
+
timeoutMs: getPollTimeoutMs(),
|
|
1285
1374
|
},
|
|
1286
1375
|
{ jira, progress: ctx.progress },
|
|
1287
1376
|
);
|
|
@@ -1317,15 +1406,15 @@ async function handleGrayReleaseApproval(issueKey, environment, systemCode, ctx)
|
|
|
1317
1406
|
await handleSendJabberMessage(
|
|
1318
1407
|
{
|
|
1319
1408
|
to: finalApproverJabber,
|
|
1320
|
-
message: `[GrayRelease 簽核通知] ${issueKey} 已由 ${commentReviewerAlias} 確認,需要您的最終簽核。環境: UAT,系統: ${systemCode}\n${
|
|
1409
|
+
message: `[GrayRelease 簽核通知] ${issueKey} 已由 ${commentReviewerAlias} 確認,需要您的最終簽核。環境: UAT,系統: ${systemCode}\n${getJiraIssueUrl(issueKey)}`,
|
|
1321
1410
|
},
|
|
1322
1411
|
{},
|
|
1323
1412
|
);
|
|
1324
1413
|
|
|
1325
1414
|
const poller = new Poller(jira);
|
|
1326
1415
|
await poller.waitForStatus(issueKey, 'WAIT DEPLOY', {
|
|
1327
|
-
intervalMs:
|
|
1328
|
-
timeoutMs:
|
|
1416
|
+
intervalMs: getPollIntervalMs(),
|
|
1417
|
+
timeoutMs: getPollTimeoutMs(),
|
|
1329
1418
|
onProgress: ctx.progress,
|
|
1330
1419
|
});
|
|
1331
1420
|
|
|
@@ -1335,82 +1424,6 @@ async function handleGrayReleaseApproval(issueKey, environment, systemCode, ctx)
|
|
|
1335
1424
|
throw new Error(`不支援的環境: ${environment}`);
|
|
1336
1425
|
}
|
|
1337
1426
|
|
|
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
1427
|
function sleep(ms) {
|
|
1415
1428
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1416
1429
|
}
|