@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/ci.js
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* CI Golden Image 相關 tools
|
|
3
3
|
* - create_ci_ticket
|
|
4
|
+
* - build_ci
|
|
4
5
|
*/
|
|
5
|
-
import {error, ok, today} from './helpers.js';
|
|
6
|
+
import { error, getPollIntervalMs, getPollTimeoutMs, isFailingResult, isPassingResult, ok, today, } from './helpers.js';
|
|
7
|
+
import {
|
|
8
|
+
findAnyTransition as findAnyTransitionForIssue,
|
|
9
|
+
sleep,
|
|
10
|
+
waitForAnyTransition as waitForAnyTransitionForIssue,
|
|
11
|
+
} from './transition-helpers.js';
|
|
6
12
|
import {
|
|
7
13
|
CI_FIELD_IDS,
|
|
8
14
|
DEPT_CODES,
|
|
@@ -16,6 +22,32 @@ import {
|
|
|
16
22
|
SYSTEM_TO_CI_REPO_MAP,
|
|
17
23
|
SYSTEM_TO_DEPT_MAP,
|
|
18
24
|
} from '../constants/index.js';
|
|
25
|
+
import { assertNoOpenPRBeforeCreate } from './branch-prs.js';
|
|
26
|
+
|
|
27
|
+
// ── Flow Definition ──────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* CI Release 狀態流程定義
|
|
31
|
+
*
|
|
32
|
+
* 完整流程:
|
|
33
|
+
* TO DO → (Accept) → Wait for Build → (Build) → Compliance Scan
|
|
34
|
+
* → (Upload Scan Report) → Upload Report → (Accept) → Wait To DEV
|
|
35
|
+
* → (Dev Done) → Wait To STG → (STG Done) → Wait To UAT
|
|
36
|
+
* → (UAT Done) → Wait For Upload → (Upload To Pre-Release)
|
|
37
|
+
* → Wait For Upload → (Upload Done) → Wait To PRD/DR
|
|
38
|
+
* → (PRD/DR Done)
|
|
39
|
+
*
|
|
40
|
+
* build_ci 負責觸發前段 Jenkins Build:
|
|
41
|
+
* TO DO → (Accept) → Wait for Build → (Build) → Compliance Scan
|
|
42
|
+
*
|
|
43
|
+
* Build 後續的 Wait To DEV/STG/UAT/PRD/DR 等狀態,
|
|
44
|
+
* 由 wait_to_dev、wait_to_stg、wait_to_uat、wait_to_prd_dr 接續處理。
|
|
45
|
+
*
|
|
46
|
+
* Jira Automation 可能在前置狀態推進後自動觸發 Jenkins,
|
|
47
|
+
* 此時不一定會出現手動 Build transition。
|
|
48
|
+
*/
|
|
49
|
+
const CI_BUILD_TRANSITIONS = ['Build'];
|
|
50
|
+
const CI_PRE_BUILD_TRANSITIONS = ['Accept'];
|
|
19
51
|
|
|
20
52
|
// ── Schema definitions ───────────────────────────────────────────
|
|
21
53
|
export function getCIToolDefinitions() {
|
|
@@ -46,23 +78,13 @@ export function getCIToolDefinitions() {
|
|
|
46
78
|
enum: Object.keys(ENV_CODES),
|
|
47
79
|
description: '(選填) 部署環境,預設 stg',
|
|
48
80
|
},
|
|
49
|
-
summary: {
|
|
50
|
-
type: 'string',
|
|
51
|
-
description:
|
|
52
|
-
'(選填)自訂 Ticket 標題,不填則自動生成,格式:[IBK][CI] IBK & WEALTH & SSR & A11Y for 0.0.1',
|
|
53
|
-
},
|
|
54
|
-
goldenImageVersion: {
|
|
55
|
-
type: 'string',
|
|
56
|
-
description:
|
|
57
|
-
'Golden Image 版本號,例如 0.0.1(用於 summary:[IBK][CI] IBK & WEALTH for 0.0.1)',
|
|
58
|
-
},
|
|
59
81
|
antiScanRequired: {
|
|
60
82
|
type: 'boolean',
|
|
61
83
|
description: '是否需要反掃描檢測(選填,預設 true)',
|
|
62
84
|
},
|
|
63
85
|
relatesTo: {
|
|
64
86
|
type: 'array',
|
|
65
|
-
items: {type: 'string'},
|
|
87
|
+
items: { type: 'string' },
|
|
66
88
|
description:
|
|
67
89
|
'關聯的 Library 單 issue key,有可能多個,例如 CID-1178 CID-1182(建立後自動加上 relates to link)',
|
|
68
90
|
},
|
|
@@ -73,50 +95,358 @@ export function getCIToolDefinitions() {
|
|
|
73
95
|
},
|
|
74
96
|
},
|
|
75
97
|
},
|
|
98
|
+
{
|
|
99
|
+
name: 'build_ci',
|
|
100
|
+
description:
|
|
101
|
+
'觸發 CI 上版單的 Jenkins Build。自動處理 CI 前置狀態切換與 Jira Automation 等待;Library 單請使用 build_library,GrayRelease 單請使用 build_grayrelease。',
|
|
102
|
+
inputSchema: {
|
|
103
|
+
type: 'object',
|
|
104
|
+
required: ['issueKey'],
|
|
105
|
+
properties: {
|
|
106
|
+
issueKey: {
|
|
107
|
+
type: 'string',
|
|
108
|
+
description: '要 build 的 CI issue key',
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
name: 'wait_to_dev',
|
|
115
|
+
description:
|
|
116
|
+
'CI 單 build 完成後,自動走完掃描流程切到 Wait To DEV 狀態。流程:Compliance Scan → Upload Scan Report → Accept → Wait To DEV(不執行 Dev Done)',
|
|
117
|
+
inputSchema: {
|
|
118
|
+
type: 'object',
|
|
119
|
+
required: ['issueKey'],
|
|
120
|
+
properties: {
|
|
121
|
+
issueKey: {
|
|
122
|
+
type: 'string',
|
|
123
|
+
description: 'CI issue key',
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
name: 'wait_to_stg',
|
|
130
|
+
description:
|
|
131
|
+
'CI 單 build 完成後,自動走完掃描流程切到 Wait To STG 狀態。流程:Compliance Scan → Upload Scan Report → Accept → Dev Done → Wait To STG',
|
|
132
|
+
inputSchema: {
|
|
133
|
+
type: 'object',
|
|
134
|
+
required: ['issueKey'],
|
|
135
|
+
properties: {
|
|
136
|
+
issueKey: {
|
|
137
|
+
type: 'string',
|
|
138
|
+
description: 'CI issue key',
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
name: 'wait_to_uat',
|
|
145
|
+
description:
|
|
146
|
+
'CI 單 STG 部署完成後切到 Wait To UAT 狀態。流程:必要時補到 Wait To STG → STG Done → Wait To UAT。',
|
|
147
|
+
inputSchema: {
|
|
148
|
+
type: 'object',
|
|
149
|
+
required: ['issueKey'],
|
|
150
|
+
properties: {
|
|
151
|
+
issueKey: {
|
|
152
|
+
type: 'string',
|
|
153
|
+
description: 'CI issue key',
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
name: 'wait_to_prd_dr',
|
|
160
|
+
description:
|
|
161
|
+
'CI 單 UAT 完成後進入 PRD/DR 前置狀態。流程:必要時補到 Wait To UAT → UAT Done → Wait For Upload → Upload To Pre-Release → 等 CID_upload_result(customfield_13452) pass → Upload Done → Wait To PRD/DR。',
|
|
162
|
+
inputSchema: {
|
|
163
|
+
type: 'object',
|
|
164
|
+
required: ['issueKey'],
|
|
165
|
+
properties: {
|
|
166
|
+
issueKey: {
|
|
167
|
+
type: 'string',
|
|
168
|
+
description: 'CI issue key',
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
},
|
|
76
173
|
];
|
|
77
174
|
}
|
|
78
175
|
|
|
176
|
+
export async function handleBuildCI(args, ctx) {
|
|
177
|
+
const { issueKey } = args;
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
const result = await executeCIBuildFlow(issueKey, ctx);
|
|
181
|
+
return ok(result);
|
|
182
|
+
} catch (err) {
|
|
183
|
+
return error(`build_ci 失敗: ${err.message}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export async function handleWaitToDev(args, ctx) {
|
|
188
|
+
try {
|
|
189
|
+
const result = await runCITransitionSteps({
|
|
190
|
+
issueKey: args.issueKey,
|
|
191
|
+
ctx,
|
|
192
|
+
finalTargetStatus: 'Wait To DEV',
|
|
193
|
+
steps: [
|
|
194
|
+
{ transition: 'Upload Scan Report', targetStatus: 'Upload Report' },
|
|
195
|
+
{ transition: 'Accept', targetStatus: 'Wait To DEV' },
|
|
196
|
+
],
|
|
197
|
+
notifyMessage: '已切換至 Wait To DEV,可進行 DEV 部署',
|
|
198
|
+
});
|
|
199
|
+
return ok(result);
|
|
200
|
+
} catch (err) {
|
|
201
|
+
return error(`wait_to_dev 失敗: ${err.message}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export async function handleWaitToStg(args, ctx) {
|
|
206
|
+
try {
|
|
207
|
+
const result = await runCITransitionSteps({
|
|
208
|
+
issueKey: args.issueKey,
|
|
209
|
+
ctx,
|
|
210
|
+
finalTargetStatus: 'Wait To STG',
|
|
211
|
+
steps: CI_STEPS_TO_STG,
|
|
212
|
+
notifyMessage: '已切換至 Wait To STG,可進行 STG 部署',
|
|
213
|
+
});
|
|
214
|
+
return ok(result);
|
|
215
|
+
} catch (err) {
|
|
216
|
+
return error(`wait_to_stg 失敗: ${err.message}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export async function handleWaitToUat(args, ctx) {
|
|
221
|
+
try {
|
|
222
|
+
const result = await runCITransitionSteps({
|
|
223
|
+
issueKey: args.issueKey,
|
|
224
|
+
ctx,
|
|
225
|
+
finalTargetStatus: 'Wait To UAT',
|
|
226
|
+
steps: CI_STEPS_TO_UAT,
|
|
227
|
+
notifyMessage: '已切換至 Wait To UAT,可進行 UAT 部署',
|
|
228
|
+
});
|
|
229
|
+
return ok(result);
|
|
230
|
+
} catch (err) {
|
|
231
|
+
return error(`wait_to_uat 失敗: ${err.message}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export async function handleWaitToPrdDr(args, ctx) {
|
|
236
|
+
const { issueKey } = args;
|
|
237
|
+
const { jira, notifier } = ctx;
|
|
238
|
+
const log = [];
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
const initialIssue = await jira.getIssue(issueKey);
|
|
242
|
+
const initialStatus = initialIssue.fields.status.name;
|
|
243
|
+
if (sameStatus(initialStatus, 'Wait To PRD/DR')) {
|
|
244
|
+
log.push(` 已是 ${initialStatus},流程已完成`);
|
|
245
|
+
return ok({ issueKey, status: initialStatus, steps: log });
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const toUpload = await runCITransitionSteps({
|
|
249
|
+
issueKey,
|
|
250
|
+
ctx,
|
|
251
|
+
finalTargetStatus: 'Wait For Upload',
|
|
252
|
+
steps: CI_STEPS_TO_UPLOAD,
|
|
253
|
+
log,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
const currentIssue = await jira.getIssue(issueKey);
|
|
257
|
+
const currentStatus = currentIssue.fields.status.name;
|
|
258
|
+
if (sameStatus(currentStatus, 'Wait To PRD/DR')) {
|
|
259
|
+
log.push(` 已是 ${currentStatus},流程已完成`);
|
|
260
|
+
return ok({ issueKey, status: currentStatus, steps: log });
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const uploadTransition = await findTransitionByName(issueKey, 'Upload To Pre-Release', jira);
|
|
264
|
+
if (uploadTransition) {
|
|
265
|
+
log.push(`執行「${uploadTransition.name}」→ Wait For Upload`);
|
|
266
|
+
await jira.transitionById(issueKey, uploadTransition.id);
|
|
267
|
+
} else if (!sameStatus(toUpload.status, 'Wait For Upload')) {
|
|
268
|
+
throw new Error(`找不到 transition「Upload To Pre-Release」,目前狀態:${toUpload.status}`);
|
|
269
|
+
} else {
|
|
270
|
+
log.push(' 未找到「Upload To Pre-Release」,視為已觸發或正在等待 upload 結果');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const uploadResult = await waitForCIUploadResult(issueKey, ctx, log);
|
|
274
|
+
|
|
275
|
+
const uploadDoneTransition = await findTransitionByName(issueKey, 'Upload Done', jira);
|
|
276
|
+
if (!uploadDoneTransition) {
|
|
277
|
+
const issue = await jira.getIssue(issueKey);
|
|
278
|
+
const finalStatus = issue.fields.status.name;
|
|
279
|
+
if (sameStatus(finalStatus, 'Wait To PRD/DR')) {
|
|
280
|
+
log.push(` 已是 ${finalStatus},流程已完成`);
|
|
281
|
+
return ok({ issueKey, status: finalStatus, uploadResult, steps: log });
|
|
282
|
+
}
|
|
283
|
+
throw new Error(`找不到 transition「Upload Done」,目前狀態:${finalStatus}`);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
log.push(`執行「${uploadDoneTransition.name}」→ Wait To PRD/DR`);
|
|
287
|
+
await jira.transitionById(issueKey, uploadDoneTransition.id);
|
|
288
|
+
|
|
289
|
+
const finalIssue = await jira.getIssue(issueKey);
|
|
290
|
+
const finalStatus = finalIssue.fields.status.name;
|
|
291
|
+
log.push(`✅ 完成,目前狀態:${finalStatus}`);
|
|
292
|
+
await notifier.notify(issueKey, `已切換至 ${finalStatus},可進行 PRD/DR 部署`);
|
|
293
|
+
return ok({ issueKey, status: finalStatus, uploadResult, steps: log });
|
|
294
|
+
} catch (err) {
|
|
295
|
+
return error(`wait_to_prd_dr 失敗: ${err.message}`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export async function ensureCIReadyForCD({ issueKey, environment }, ctx) {
|
|
300
|
+
const env = normalizeEnvironment(environment);
|
|
301
|
+
if (!issueKey) {
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (env === 'dev') {
|
|
306
|
+
return assertToolOk(await handleWaitToDev({ issueKey }, ctx), 'wait_to_dev');
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (env === 'stg') {
|
|
310
|
+
return assertToolOk(await handleWaitToStg({ issueKey }, ctx), 'wait_to_stg');
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (env === 'uat') {
|
|
314
|
+
return assertToolOk(await handleWaitToUat({ issueKey }, ctx), 'wait_to_uat');
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (['prd', 'dr', 'prd/dr'].includes(env)) {
|
|
318
|
+
return assertToolOk(await handleWaitToPrdDr({ issueKey }, ctx), 'wait_to_prd_dr');
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
throw new Error(`不支援的 CD 部署環境:${environment}`);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async function executeCIBuildFlow(issueKey, ctx) {
|
|
325
|
+
const { jira, notifier } = ctx;
|
|
326
|
+
const log = [];
|
|
327
|
+
|
|
328
|
+
const findAnyTransition = (names) => findAnyTransitionForIssue({ jira, issueKey, names });
|
|
329
|
+
|
|
330
|
+
const waitForAnyTransition = (names) => waitForAnyTransitionForIssue({
|
|
331
|
+
jira,
|
|
332
|
+
issueKey,
|
|
333
|
+
names,
|
|
334
|
+
intervalMs: getPollIntervalMs(),
|
|
335
|
+
timeoutMs: getPollTimeoutMs(),
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
let buildTransition = await findAnyTransition(CI_BUILD_TRANSITIONS);
|
|
339
|
+
|
|
340
|
+
if (!buildTransition) {
|
|
341
|
+
log.push('未找到 Build transition,逐步觸發 CI 前置狀態...');
|
|
342
|
+
|
|
343
|
+
const preBuildResult = await runCIPreBuildTransitions({
|
|
344
|
+
issueKey,
|
|
345
|
+
jira,
|
|
346
|
+
log,
|
|
347
|
+
findAnyTransition,
|
|
348
|
+
});
|
|
349
|
+
buildTransition = preBuildResult.buildTransition;
|
|
350
|
+
|
|
351
|
+
if (!buildTransition) {
|
|
352
|
+
log.push(` 等待 Jira Automation 推進(最多 ${getPollTimeoutMs() / 1000}s)...`);
|
|
353
|
+
buildTransition = await waitForAnyTransition(CI_BUILD_TRANSITIONS);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (!buildTransition) {
|
|
358
|
+
const issue = await jira.getIssue(issueKey);
|
|
359
|
+
throw new Error(`找不到 Build transition,目前狀態:${issue.fields.status.name}`);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
log.push(`執行 ${buildTransition.name} transition (id: ${buildTransition.id})...`);
|
|
363
|
+
await jira.transitionById(issueKey, buildTransition.id);
|
|
364
|
+
|
|
365
|
+
const issue = await jira.getIssue(issueKey);
|
|
366
|
+
const newStatus = issue.fields.status.name;
|
|
367
|
+
log.push(`✅ ${buildTransition.name} 已觸發,目前狀態:${newStatus}`);
|
|
368
|
+
await notifier.notify(issueKey, `Jenkins ${buildTransition.name} 已觸發(${newStatus})`);
|
|
369
|
+
|
|
370
|
+
return { issueKey, status: newStatus, steps: log };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
async function runCIPreBuildTransitions({ issueKey, jira, log, findAnyTransition }) {
|
|
374
|
+
let preTriggered = false;
|
|
375
|
+
|
|
376
|
+
for (let step = 0; step < CI_PRE_BUILD_TRANSITIONS.length; step++) {
|
|
377
|
+
const preTransition = await findAnyTransition(CI_PRE_BUILD_TRANSITIONS);
|
|
378
|
+
if (!preTransition) {
|
|
379
|
+
break;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
log.push(` [step ${step + 1}] 觸發「${preTransition.name}」...`);
|
|
383
|
+
try {
|
|
384
|
+
await jira.transitionById(issueKey, preTransition.id);
|
|
385
|
+
} catch (err) {
|
|
386
|
+
log.push(` ⚠️ ${preTransition.name} transition 失敗: ${err.message}`);
|
|
387
|
+
}
|
|
388
|
+
preTriggered = true;
|
|
389
|
+
await sleep(2000);
|
|
390
|
+
|
|
391
|
+
const buildTransition = await findAnyTransition(CI_BUILD_TRANSITIONS);
|
|
392
|
+
if (buildTransition) {
|
|
393
|
+
log.push(` 已找到 ${buildTransition.name} transition`);
|
|
394
|
+
return { buildTransition, preTriggered };
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return { buildTransition: null, preTriggered };
|
|
399
|
+
}
|
|
400
|
+
|
|
79
401
|
/**
|
|
80
402
|
* 建立 CI Golden Image 上版單
|
|
81
403
|
*/
|
|
82
|
-
export async function handleCreateCITicket(args, {jira, notifier}) {
|
|
404
|
+
export async function handleCreateCITicket(args, { jira, notifier }) {
|
|
83
405
|
try {
|
|
406
|
+
const systemCode = String(args.systemCode ?? '').trim().toUpperCase();
|
|
84
407
|
const branch = args.branch || 'master';
|
|
85
408
|
const envCode = (args.environment ?? 'stg').toLowerCase();
|
|
409
|
+
await assertNoOpenPRBeforeCreate({
|
|
410
|
+
ticketType: 'ci',
|
|
411
|
+
systemCode,
|
|
412
|
+
branch: 'master',
|
|
413
|
+
}, { jira });
|
|
414
|
+
|
|
415
|
+
const goldenImageVersion = await resolveGoldenImageVersion({ systemCode, branch }, { jira });
|
|
86
416
|
// Summary 格式對齊 Confluence:[IBK][CI] Wealth & SSR & IBK & A11y for 0.0.1
|
|
87
417
|
const modules = Array.isArray(args.modules)
|
|
88
418
|
? args.modules
|
|
89
419
|
: typeof args.modules === 'string'
|
|
90
420
|
? args.modules.split(',').map((module) => module.trim()).filter(Boolean)
|
|
91
|
-
: SYSTEM_MODULES[
|
|
421
|
+
: SYSTEM_MODULES[systemCode];
|
|
92
422
|
const modulesStr = modules.map((m) => m.toUpperCase()).join(' & ');
|
|
93
423
|
|
|
94
|
-
const versionSuffix =
|
|
95
|
-
const autoSummary =
|
|
424
|
+
const versionSuffix = goldenImageVersion ? ` for ${goldenImageVersion}` : '';
|
|
425
|
+
const autoSummary = `[${systemCode}][CI] ${modulesStr}${versionSuffix}`;
|
|
96
426
|
|
|
97
427
|
const fields = {
|
|
98
|
-
project: {key: JIRA_PROJECT_ID},
|
|
99
|
-
issuetype: {id: ISSUE_TYPE_IDS.CI},
|
|
428
|
+
project: { key: JIRA_PROJECT_ID },
|
|
429
|
+
issuetype: { id: ISSUE_TYPE_IDS.CI },
|
|
100
430
|
summary: autoSummary,
|
|
101
431
|
duedate: today(),
|
|
102
432
|
};
|
|
103
433
|
|
|
104
434
|
// systemCode
|
|
105
435
|
if (CI_FIELD_IDS.systemCode) {
|
|
106
|
-
fields[CI_FIELD_IDS.systemCode] = {value:
|
|
436
|
+
fields[CI_FIELD_IDS.systemCode] = { value: systemCode };
|
|
107
437
|
}
|
|
108
438
|
// env (必填)
|
|
109
439
|
if (CI_FIELD_IDS.env && ENV_CODES[envCode]) {
|
|
110
|
-
fields[CI_FIELD_IDS.env] = {id: ENV_CODES[envCode]};
|
|
440
|
+
fields[CI_FIELD_IDS.env] = { id: ENV_CODES[envCode] };
|
|
111
441
|
}
|
|
112
442
|
// dept_code (必填,由 systemCode 推導)
|
|
113
|
-
const deptStr = SYSTEM_TO_DEPT_MAP[
|
|
443
|
+
const deptStr = SYSTEM_TO_DEPT_MAP[systemCode];
|
|
114
444
|
if (CI_FIELD_IDS.deptCode && deptStr && DEPT_CODES[deptStr]) {
|
|
115
|
-
fields[CI_FIELD_IDS.deptCode] = {id: DEPT_CODES[deptStr]};
|
|
445
|
+
fields[CI_FIELD_IDS.deptCode] = { id: DEPT_CODES[deptStr] };
|
|
116
446
|
}
|
|
117
447
|
// system_module (必填,預設 assembly)
|
|
118
448
|
if (CI_FIELD_IDS.systemModule) {
|
|
119
|
-
fields[CI_FIELD_IDS.systemModule] = {id: FIELD_OPTIONS.systemModule.assembly};
|
|
449
|
+
fields[CI_FIELD_IDS.systemModule] = { id: FIELD_OPTIONS.systemModule.assembly };
|
|
120
450
|
}
|
|
121
451
|
// git branch → customfield_13431 (14702 不在 CI screen)
|
|
122
452
|
if (CI_FIELD_IDS.gitBranch && branch) {
|
|
@@ -131,7 +461,7 @@ export async function handleCreateCITicket(args, {jira, notifier}) {
|
|
|
131
461
|
};
|
|
132
462
|
|
|
133
463
|
if (args.dryRun)
|
|
134
|
-
return ok({dryRun: true, summary: autoSummary, fields, relatesTo: args.relatesTo ?? []});
|
|
464
|
+
return ok({ dryRun: true, summary: autoSummary, fields, relatesTo: args.relatesTo ?? [] });
|
|
135
465
|
|
|
136
466
|
const issue = await jira.createIssue(fields);
|
|
137
467
|
|
|
@@ -153,19 +483,156 @@ export async function handleCreateCITicket(args, {jira, notifier}) {
|
|
|
153
483
|
issueId: issue.id,
|
|
154
484
|
url: `${process.env.JIRA_BASE_URL}/browse/${issue.key}`,
|
|
155
485
|
type: 'CI Release',
|
|
156
|
-
system:
|
|
157
|
-
|
|
486
|
+
system: systemCode,
|
|
487
|
+
goldenImageVersion,
|
|
488
|
+
...(relatesTo.length > 0 && { relatesTo }),
|
|
158
489
|
});
|
|
159
490
|
} catch (err) {
|
|
160
491
|
return error(`無法建立 CI 單: ${err.message}`);
|
|
161
492
|
}
|
|
162
493
|
}
|
|
163
494
|
|
|
495
|
+
const CI_STEPS_TO_STG = [
|
|
496
|
+
{ transition: 'Upload Scan Report', targetStatus: 'Upload Report' },
|
|
497
|
+
{ transition: 'Accept', targetStatus: 'Wait To DEV' },
|
|
498
|
+
{ transition: 'Dev Done', targetStatus: 'Wait To STG' },
|
|
499
|
+
];
|
|
500
|
+
|
|
501
|
+
const CI_STEPS_TO_UAT = [
|
|
502
|
+
...CI_STEPS_TO_STG,
|
|
503
|
+
{ transition: 'STG Done', targetStatus: 'Wait To UAT' },
|
|
504
|
+
];
|
|
505
|
+
|
|
506
|
+
const CI_STEPS_TO_UPLOAD = [
|
|
507
|
+
...CI_STEPS_TO_UAT,
|
|
508
|
+
{ transition: 'UAT Done', targetStatus: 'Wait For Upload' },
|
|
509
|
+
];
|
|
510
|
+
|
|
511
|
+
async function runCITransitionSteps({ issueKey, ctx, steps, finalTargetStatus, notifyMessage, log = [] }) {
|
|
512
|
+
const { jira, notifier } = ctx;
|
|
513
|
+
|
|
514
|
+
for (const [index, step] of steps.entries()) {
|
|
515
|
+
const issue = await jira.getIssue(issueKey);
|
|
516
|
+
const current = issue.fields.status.name;
|
|
517
|
+
|
|
518
|
+
if (sameStatus(current, finalTargetStatus)) {
|
|
519
|
+
log.push(` 已是 ${current},流程已完成`);
|
|
520
|
+
break;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const reachedStepIndex = steps.findIndex((candidate) => sameStatus(current, candidate.targetStatus));
|
|
524
|
+
if (reachedStepIndex >= index) {
|
|
525
|
+
log.push(` 目前是 ${current},跳過「${step.transition}」`);
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const transition = await findTransitionByName(issueKey, step.transition, jira);
|
|
530
|
+
if (!transition) {
|
|
531
|
+
throw new Error(`找不到 transition「${step.transition}」,目前狀態:${current}`);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
log.push(`執行「${transition.name}」→ ${step.targetStatus}`);
|
|
535
|
+
await jira.transitionById(issueKey, transition.id);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const finalIssue = await jira.getIssue(issueKey);
|
|
539
|
+
const finalStatus = finalIssue.fields.status.name;
|
|
540
|
+
log.push(`✅ 完成,目前狀態:${finalStatus}`);
|
|
541
|
+
|
|
542
|
+
if (notifyMessage) {
|
|
543
|
+
await notifier.notify(issueKey, notifyMessage.replace(finalTargetStatus, finalStatus));
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
return { issueKey, status: finalStatus, steps: log };
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
async function findTransitionByName(issueKey, transitionName, jira) {
|
|
550
|
+
const transitions = await jira.getTransitions(issueKey);
|
|
551
|
+
return transitions.find((transition) => transition.name.toLowerCase() === transitionName.toLowerCase());
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
async function waitForCIUploadResult(issueKey, ctx, log) {
|
|
555
|
+
const { jira } = ctx;
|
|
556
|
+
const intervalMs = getPollIntervalMs();
|
|
557
|
+
const timeoutMs = getPollTimeoutMs();
|
|
558
|
+
const startedAt = Date.now();
|
|
559
|
+
const deadline = Date.now() + timeoutMs;
|
|
560
|
+
let attempts = 0;
|
|
561
|
+
let lastValue;
|
|
562
|
+
|
|
563
|
+
while (true) {
|
|
564
|
+
attempts++;
|
|
565
|
+
const fields = await jira.getIssueFields(issueKey, [CI_FIELD_IDS.uploadResult]);
|
|
566
|
+
const raw = fields[CI_FIELD_IDS.uploadResult];
|
|
567
|
+
lastValue = raw?.value ?? raw;
|
|
568
|
+
|
|
569
|
+
if (typeof ctx.progress === 'function') {
|
|
570
|
+
ctx.progress({
|
|
571
|
+
phase: 'polling',
|
|
572
|
+
title: '等待 CI Upload To Pre-Release 結果',
|
|
573
|
+
detail: `${CI_FIELD_IDS.uploadResult}: ${lastValue ?? 'empty'}`,
|
|
574
|
+
issueKey,
|
|
575
|
+
attempts,
|
|
576
|
+
elapsedMs: Date.now() - startedAt,
|
|
577
|
+
timeoutMs,
|
|
578
|
+
nextPollMs: intervalMs,
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
if (isPassingResult(lastValue)) {
|
|
583
|
+
log.push(` ✅ Upload To Pre-Release 完成,${CI_FIELD_IDS.uploadResult}: ${lastValue}`);
|
|
584
|
+
return lastValue;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if (isFailingResult(lastValue)) {
|
|
588
|
+
throw new Error(`Upload To Pre-Release 失敗,${CI_FIELD_IDS.uploadResult}: ${lastValue}`);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
if (Date.now() >= deadline) {
|
|
592
|
+
throw new Error(`Upload To Pre-Release 等待逾時,${CI_FIELD_IDS.uploadResult}: ${lastValue ?? 'empty'}`);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
await sleep(intervalMs);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
async function resolveGoldenImageVersion({ systemCode, branch }, { jira }) {
|
|
600
|
+
const result = await handleGetNextCIVersion({ systemCode, branch }, { jira });
|
|
601
|
+
const data = assertToolOk(result, 'get_next_ci_version');
|
|
602
|
+
return data.summaryVersion;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function assertToolOk(result, toolName) {
|
|
606
|
+
const text = result?.content?.[0]?.text ?? '';
|
|
607
|
+
if (result?.isError || text.startsWith('❌')) {
|
|
608
|
+
throw new Error(text.replace(/^❌ 錯誤: /, '') || `${toolName} failed`);
|
|
609
|
+
}
|
|
610
|
+
return JSON.parse(text);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function sameStatus(left, right) {
|
|
614
|
+
return normalizeStatusName(left) === normalizeStatusName(right);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function normalizeStatusName(statusName) {
|
|
618
|
+
return String(statusName ?? '')
|
|
619
|
+
.trim()
|
|
620
|
+
.replace(/\s+/g, ' ')
|
|
621
|
+
.toLowerCase();
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function normalizeEnvironment(environment) {
|
|
625
|
+
return String(environment ?? '')
|
|
626
|
+
.trim()
|
|
627
|
+
.toLowerCase()
|
|
628
|
+
.replace('&', '/');
|
|
629
|
+
}
|
|
630
|
+
|
|
164
631
|
/**
|
|
165
632
|
* 計算下一個 Golden Image Release 版號
|
|
166
633
|
*/
|
|
167
|
-
export async function handleGetNextCIVersion(args, {jira}) {
|
|
168
|
-
const {systemCode, branch} = args;
|
|
634
|
+
export async function handleGetNextCIVersion(args, { jira }) {
|
|
635
|
+
const { systemCode, branch } = args;
|
|
169
636
|
|
|
170
637
|
const repoName = SYSTEM_TO_CI_REPO_MAP[systemCode];
|
|
171
638
|
if (!repoName) {
|