@jira-deploy/core 1.0.0
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/defaults.js +111 -0
- package/constants/environments.js +37 -0
- package/constants/field-ids.js +63 -0
- package/constants/index.js +53 -0
- package/constants/issue-types.js +23 -0
- package/constants/modules.js +55 -0
- package/constants/repos.js +55 -0
- package/constants/server.js +100 -0
- package/constants/system-codes.js +79 -0
- package/constants/users.js +47 -0
- package/dry-run.js +691 -0
- package/index.js +6 -0
- package/jira-client.js +313 -0
- package/notifier.js +56 -0
- package/package.json +34 -0
- package/platform-config.js +64 -0
- package/poller.js +64 -0
- package/tools/cd.js +666 -0
- package/tools/ci.js +204 -0
- package/tools/ci.test.js +154 -0
- package/tools/grayrelease.js +296 -0
- package/tools/helpers.js +78 -0
- package/tools/index.js +1119 -0
- package/tools/jabber.js +97 -0
- package/tools/library.js +225 -0
- package/tools/release.js +320 -0
- package/tools/release.test.js +137 -0
- package/tools.test.js +1711 -0
package/tools/cd.js
ADDED
|
@@ -0,0 +1,666 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CI Golden Image 相關 tools
|
|
3
|
+
* - create_cd_ticket
|
|
4
|
+
*/
|
|
5
|
+
import {error, getModuleName, getServerList, ok, today} from './helpers.js';
|
|
6
|
+
import {
|
|
7
|
+
CD_FIELD_IDS,
|
|
8
|
+
CI_FIELD_IDS,
|
|
9
|
+
SERVERS,
|
|
10
|
+
DEPT_CODES,
|
|
11
|
+
ENV_CODES,
|
|
12
|
+
FIELD_OPTIONS,
|
|
13
|
+
ISSUE_TYPE_IDS,
|
|
14
|
+
JIRA_PROJECT_ID,
|
|
15
|
+
LIBRARY_MODULE_IDS,
|
|
16
|
+
SYSTEM_CODES,
|
|
17
|
+
SYSTEM_MODULES,
|
|
18
|
+
SYSTEM_TO_DEPT_MAP,
|
|
19
|
+
MODULE_MAP,
|
|
20
|
+
REPO_MAPS,
|
|
21
|
+
REPO_LABEL_MAP,
|
|
22
|
+
} from '../constants/index.js';
|
|
23
|
+
import {SERVER_MODULE_MAP} from '../constants/server.js';
|
|
24
|
+
|
|
25
|
+
// ── Schema definitions ───────────────────────────────────────────
|
|
26
|
+
export function getCDToolDefinitions() {
|
|
27
|
+
return [
|
|
28
|
+
{
|
|
29
|
+
name: 'create_cd_ticket',
|
|
30
|
+
description: '建立 CD Deploy 上版單。專用工具,提前驗證必填欄位和叢集配置',
|
|
31
|
+
inputSchema: {
|
|
32
|
+
type: 'object',
|
|
33
|
+
required: ['systemCode', 'ciTicket'],
|
|
34
|
+
properties: {
|
|
35
|
+
systemCode: {
|
|
36
|
+
type: 'string',
|
|
37
|
+
enum: Object.values(SYSTEM_CODES),
|
|
38
|
+
description: '系統代碼',
|
|
39
|
+
},
|
|
40
|
+
environment: {
|
|
41
|
+
type: 'string',
|
|
42
|
+
enum: Object.keys(ENV_CODES),
|
|
43
|
+
description: '部署環境,必填,預設為 stg',
|
|
44
|
+
},
|
|
45
|
+
isClusterDeploy: {
|
|
46
|
+
type: 'boolean',
|
|
47
|
+
description: '(選填) 是否要集群部署,預設為 true',
|
|
48
|
+
},
|
|
49
|
+
summary: {
|
|
50
|
+
type: 'string',
|
|
51
|
+
description:
|
|
52
|
+
'(選填)自訂標題,不填則自動生成:[IBK][STG] 程式上版作業申請單_YYYYMMDD (CD deployment with {release_version})',
|
|
53
|
+
},
|
|
54
|
+
ciTicket: {
|
|
55
|
+
type: 'string',
|
|
56
|
+
description:
|
|
57
|
+
'關聯的 CI 單 issue key,例如 CID-1677。自動:① 取 release_version 填入 summary/CID_deploy_version ② 查 CI 所有 relates Library 單,各自取 CID_branch 找 LBPRJ 版本頁 → 加 Web Link',
|
|
58
|
+
},
|
|
59
|
+
metaTest: {
|
|
60
|
+
type: 'boolean',
|
|
61
|
+
description:
|
|
62
|
+
'(選填) 是否為封測用,true 代表是封測用單,只有 cwa or ibk 有 prd 封測機 web05,extra_cluster_deploy 只有有 tvprd-{ibk/cwa}-web05,預設為 false',
|
|
63
|
+
},
|
|
64
|
+
dryRun: {
|
|
65
|
+
type: 'boolean',
|
|
66
|
+
description:
|
|
67
|
+
'(選填) 預覽模式,不實際建立 Jira 單,回傳會送出的 payload(reads 操作如取 CI 版本仍會執行)',
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 建立 CD 上版單
|
|
77
|
+
*/
|
|
78
|
+
export async function handleCreateCDTicket(args, {jira, notifier}) {
|
|
79
|
+
try {
|
|
80
|
+
const envCode = (args.environment ?? '').toLowerCase();
|
|
81
|
+
const ciTicket = args.ciTicket ?? args.linkedCiKey;
|
|
82
|
+
const clusterDeploy = typeof args.clusterDeploy === 'string'
|
|
83
|
+
? args.clusterDeploy.split(',').map((cluster) => cluster.trim()).filter(Boolean)
|
|
84
|
+
: null;
|
|
85
|
+
const isClusterDeploy = args.isClusterDeploy ?? (envCode !== 'dev');
|
|
86
|
+
const normalizedArgs = {
|
|
87
|
+
...args,
|
|
88
|
+
ciTicket,
|
|
89
|
+
isClusterDeploy,
|
|
90
|
+
environment: envCode,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// 未指定環境時,詢問使用者是否使用 STG
|
|
94
|
+
if (!normalizedArgs.environment) {
|
|
95
|
+
return ok({
|
|
96
|
+
confirm_environment: true,
|
|
97
|
+
message: '請問要部署到 STG 環境嗎?(STG / UAT / PRD)',
|
|
98
|
+
hint: '若要使用 STG,請回覆「是」或直接告訴我環境名稱',
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// extra_vars 參數邏輯:只對 ibk/cwa 的 prd 環境生效
|
|
103
|
+
const serverList = clusterDeploy ?? getServerList(
|
|
104
|
+
normalizedArgs.systemCode,
|
|
105
|
+
normalizedArgs.environment,
|
|
106
|
+
normalizedArgs.metaTest,
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
let goldenImageVersion = '';
|
|
110
|
+
|
|
111
|
+
// 先取 CI 單的 deploy_version JSON(customfield_13438,如 {"cwa_ap_version":"0.0.12"})
|
|
112
|
+
let ciReleaseVersion = '';
|
|
113
|
+
if (normalizedArgs.ciTicket) {
|
|
114
|
+
try {
|
|
115
|
+
const ciFields = await jira.getIssueFields(normalizedArgs.ciTicket, [CI_FIELD_IDS.releaseVersion]);
|
|
116
|
+
ciReleaseVersion = ciFields?.[CI_FIELD_IDS.releaseVersion] ?? '';
|
|
117
|
+
if (!ciReleaseVersion) {
|
|
118
|
+
const legacyFields = await jira.getIssueFields(normalizedArgs.ciTicket, ['customfield_14705']);
|
|
119
|
+
ciReleaseVersion = legacyFields?.customfield_14705 ?? '';
|
|
120
|
+
}
|
|
121
|
+
if (!ciReleaseVersion) {
|
|
122
|
+
const summaryFields = await jira.getIssueFields(normalizedArgs.ciTicket, ['summary']);
|
|
123
|
+
const summaryMatch = summaryFields?.summary?.match(/\bfor\s+([0-9]+(?:\.[0-9]+){2,})\b/i);
|
|
124
|
+
ciReleaseVersion = summaryMatch?.[1] ?? '';
|
|
125
|
+
}
|
|
126
|
+
if (ciReleaseVersion) {
|
|
127
|
+
try {
|
|
128
|
+
const parsed = JSON.parse(ciReleaseVersion);
|
|
129
|
+
goldenImageVersion = Object.values(parsed)[0] ?? '';
|
|
130
|
+
} catch {
|
|
131
|
+
goldenImageVersion = ciReleaseVersion;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
} catch (e) {
|
|
135
|
+
console.error(
|
|
136
|
+
`[create_cd_ticket] 無法讀取 ${normalizedArgs.ciTicket} 的 CID_release_version: ${e.message}`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const dateStr = today().replace(/-/g, '');
|
|
142
|
+
const displayEnv =
|
|
143
|
+
normalizedArgs.metaTest ? 'prd' : (envCode === 'prd' || envCode === 'prd/dr') ? 'prd/dr' : envCode;
|
|
144
|
+
const summaryVersion = goldenImageVersion || normalizedArgs.ciTicket || '';
|
|
145
|
+
const autoSummary = `[${normalizedArgs.systemCode}][${displayEnv.toUpperCase()}] 程式上版作業申請單_${dateStr} (CD deployment with ${summaryVersion})`;
|
|
146
|
+
|
|
147
|
+
const fields = {
|
|
148
|
+
project: {key: JIRA_PROJECT_ID},
|
|
149
|
+
issuetype: {id: ISSUE_TYPE_IDS.CD},
|
|
150
|
+
duedate: today(),
|
|
151
|
+
summary: normalizedArgs.summary ?? autoSummary,
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// CID_deploy_version:deploy JSON 優先,否則 release_version
|
|
155
|
+
if (normalizedArgs.ciTicket && ciReleaseVersion) {
|
|
156
|
+
fields[CD_FIELD_IDS.deployVersion] = ciReleaseVersion;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// CID_env(必填,用 id 格式)
|
|
160
|
+
if (ENV_CODES[envCode]) {
|
|
161
|
+
fields[CD_FIELD_IDS.env] = {id: ENV_CODES[envCode]};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// dept_code(由 systemCode 推導)
|
|
165
|
+
const deptStr = SYSTEM_TO_DEPT_MAP[normalizedArgs.systemCode];
|
|
166
|
+
if (deptStr && DEPT_CODES[deptStr]) {
|
|
167
|
+
fields[CD_FIELD_IDS.deptCode] = {id: DEPT_CODES[deptStr]};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
fields[CD_FIELD_IDS.systemCode] = {value: normalizedArgs.systemCode};
|
|
171
|
+
|
|
172
|
+
// CID_cluster_deploy:DEV → false,STG/UAT/PRD/DR → true(Confluence Step 1)
|
|
173
|
+
fields[CD_FIELD_IDS.clusterDeploy] = {
|
|
174
|
+
id: normalizedArgs.isClusterDeploy
|
|
175
|
+
? FIELD_OPTIONS.clusterDeploy.true
|
|
176
|
+
: FIELD_OPTIONS.clusterDeploy.false,
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// CID_server_list(換行分隔)
|
|
180
|
+
fields[CD_FIELD_IDS.serverList] = serverList.map((c) => c.trim()).join('\n');
|
|
181
|
+
|
|
182
|
+
// CID_restart_only(option id 格式)
|
|
183
|
+
if (normalizedArgs.restartOnly !== undefined) {
|
|
184
|
+
fields[CD_FIELD_IDS.restartOnly] = {
|
|
185
|
+
id: normalizedArgs.restartOnly
|
|
186
|
+
? FIELD_OPTIONS.restartOnly.true
|
|
187
|
+
: FIELD_OPTIONS.restartOnly.false,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
fields[CD_FIELD_IDS.extraVars] = normalizedArgs.extraVars
|
|
192
|
+
?? await getExtraVarsJson({serverList, ...normalizedArgs}, {jira});
|
|
193
|
+
|
|
194
|
+
// 預先生成 description(dryRun 預覽用,正式開單後也會再呼叫 updateIssue 寫入)
|
|
195
|
+
let previewDescription = generateCDDescription(ciReleaseVersion || '');
|
|
196
|
+
if (normalizedArgs.ciTicket) {
|
|
197
|
+
const releaseNotesStr = await generateReleaseNotes(
|
|
198
|
+
jira,
|
|
199
|
+
normalizedArgs.ciTicket,
|
|
200
|
+
envCode,
|
|
201
|
+
normalizedArgs.systemCode,
|
|
202
|
+
);
|
|
203
|
+
if (releaseNotesStr) previewDescription += `\n\n${releaseNotesStr}`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (args.dryRun)
|
|
207
|
+
return ok({
|
|
208
|
+
dryRun: true,
|
|
209
|
+
summary: fields.summary,
|
|
210
|
+
fields,
|
|
211
|
+
description: previewDescription,
|
|
212
|
+
ciTicket: normalizedArgs.ciTicket,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const issue = await jira.createIssue(fields);
|
|
216
|
+
|
|
217
|
+
// CI contains CD 關聯(Hierarchy link (WBSGantt):outward=contains / inward=is contained in)
|
|
218
|
+
if (normalizedArgs.ciTicket) {
|
|
219
|
+
await jira.linkIssue(normalizedArgs.ciTicket, issue.key, 'Hierarchy link (WBSGantt)');
|
|
220
|
+
await notifier.notify(
|
|
221
|
+
issue.key,
|
|
222
|
+
`已建立 contains 關聯:${normalizedArgs.ciTicket} contains ${issue.key}`,
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Confluence Step 3:Web Link(對應 LBPRJ 版本頁)
|
|
227
|
+
await handleWeblink(jira, issue, notifier, normalizedArgs);
|
|
228
|
+
|
|
229
|
+
// 更新 description 表格,因為 cid jira worker 會將 init 的表格回朔
|
|
230
|
+
if (issue.key) {
|
|
231
|
+
// version = ciReleaseVersion(CI golden image version,例如 0.0.11)
|
|
232
|
+
let cdDescription = generateCDDescription(ciReleaseVersion || '');
|
|
233
|
+
|
|
234
|
+
// 生成 release notes Bitbucket compare URL
|
|
235
|
+
if (normalizedArgs.ciTicket) {
|
|
236
|
+
const releaseNotesStr = await generateReleaseNotes(
|
|
237
|
+
jira,
|
|
238
|
+
normalizedArgs.ciTicket,
|
|
239
|
+
envCode,
|
|
240
|
+
normalizedArgs.systemCode,
|
|
241
|
+
);
|
|
242
|
+
if (releaseNotesStr) {
|
|
243
|
+
cdDescription += `\n\n${releaseNotesStr}`;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (normalizedArgs.description) {
|
|
247
|
+
cdDescription += `\n\n${normalizedArgs.description}`;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
await jira.updateIssue(issue.key, {
|
|
251
|
+
[CD_FIELD_IDS.description]: cdDescription,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
await notifier.notify(
|
|
256
|
+
issue.key,
|
|
257
|
+
`CD Deploy 單已建立。系統: ${args.systemCode}, 環境: ${args.environment}, Cluster: ${args.clusterDeploy}`,
|
|
258
|
+
);
|
|
259
|
+
return ok({
|
|
260
|
+
issueKey: issue.key,
|
|
261
|
+
issueId: issue.id,
|
|
262
|
+
url: `${process.env.JIRA_BASE_URL}/browse/${issue.key}`,
|
|
263
|
+
type: 'CD Deploy',
|
|
264
|
+
system: normalizedArgs.systemCode,
|
|
265
|
+
environment: normalizedArgs.environment,
|
|
266
|
+
});
|
|
267
|
+
} catch (err) {
|
|
268
|
+
return error(`無法建立 CD 單: ${err.message}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* 計算下一個 Golden Image Release 版號
|
|
274
|
+
*/
|
|
275
|
+
export async function getExtraVarsJson(args, {jira}) {
|
|
276
|
+
try {
|
|
277
|
+
const {serverList, module} = args;
|
|
278
|
+
|
|
279
|
+
let extraVarsJson = null;
|
|
280
|
+
// ① 從 CI 關聯的 Library 票推導模組(只含本次實際部署的模組)
|
|
281
|
+
let modules = null;
|
|
282
|
+
|
|
283
|
+
if (args.ciTicket) {
|
|
284
|
+
try {
|
|
285
|
+
const ciFields = await jira.getIssueFields(args.ciTicket, ['issuelinks']);
|
|
286
|
+
const issueLinks = ciFields?.issuelinks ?? [];
|
|
287
|
+
// 建立反查表:child module ID → module name(僅限本系統)
|
|
288
|
+
const childIdToName = Object.fromEntries(
|
|
289
|
+
Object.entries(LIBRARY_MODULE_IDS[args.systemCode] ?? {}).map(([name, id]) => [id, name]),
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
const detectedModules = [];
|
|
293
|
+
for (const link of issueLinks) {
|
|
294
|
+
const linked = link.inwardIssue ?? link.outwardIssue;
|
|
295
|
+
if (linked?.fields?.issuetype?.name !== 'Library') continue;
|
|
296
|
+
try {
|
|
297
|
+
const libFields = await jira.getIssueFields(linked.key, ['customfield_13702']);
|
|
298
|
+
const childId = libFields?.['customfield_13702']?.child?.id;
|
|
299
|
+
if (childId && childIdToName[childId]) {
|
|
300
|
+
detectedModules.push(childIdToName[childId]);
|
|
301
|
+
}
|
|
302
|
+
} catch (_) {
|
|
303
|
+
/* skip this library */
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
if (detectedModules.length > 0) modules = [...new Set(detectedModules)];
|
|
307
|
+
} catch (_) {
|
|
308
|
+
/* fall through */
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ② fallback:CI 無關聯 Library 時使用全系統模組
|
|
313
|
+
if (!modules) modules = SYSTEM_MODULES[args.systemCode] ?? [];
|
|
314
|
+
|
|
315
|
+
const sysLower = args.systemCode.toLowerCase();
|
|
316
|
+
|
|
317
|
+
const envServerList = SERVERS[args.systemCode]?.[args.environment];
|
|
318
|
+
|
|
319
|
+
// 只對 cluster 是 flat array 的系統自動生成(EIB/EVT 結構較複雜,另外處理)
|
|
320
|
+
const flatClusters = Array.isArray(envServerList) ? serverList : null;
|
|
321
|
+
|
|
322
|
+
if (flatClusters) {
|
|
323
|
+
const serverMap = Object.fromEntries(flatClusters.map((s) => [s, true]));
|
|
324
|
+
const vars = Object.fromEntries(
|
|
325
|
+
modules.map((mod) => [`${sysLower}_${mod}_installation`, serverMap]),
|
|
326
|
+
);
|
|
327
|
+
extraVarsJson = JSON.stringify(vars);
|
|
328
|
+
} else {
|
|
329
|
+
// EVT & EIB 有分 web was 機器
|
|
330
|
+
const serverType = modules.map((module) => SERVER_MODULE_MAP[module]);
|
|
331
|
+
const vars = Object.fromEntries(
|
|
332
|
+
modules.map((mod, i) => {
|
|
333
|
+
const servers = envServerList[serverType[i]] ?? [];
|
|
334
|
+
return [
|
|
335
|
+
`${sysLower}_${mod}_installation`,
|
|
336
|
+
Object.fromEntries(servers.map((s) => [s, true])),
|
|
337
|
+
];
|
|
338
|
+
}),
|
|
339
|
+
);
|
|
340
|
+
extraVarsJson = JSON.stringify(vars);
|
|
341
|
+
}
|
|
342
|
+
return extraVarsJson;
|
|
343
|
+
} catch (e) {
|
|
344
|
+
}
|
|
345
|
+
} // end getExtraVarsJson
|
|
346
|
+
|
|
347
|
+
// ── Private helpers ───────────────────────────────────────────────
|
|
348
|
+
|
|
349
|
+
function generateCDDescription(version) {
|
|
350
|
+
return `||一般資訊作業申請單||參考符號 ◼️◻️||
|
|
351
|
+
|*上版版本號*|${version}|
|
|
352
|
+
|*相關電子單號*|參照連結|
|
|
353
|
+
|*Purpose*|參照連結|
|
|
354
|
+
|*防毒掃描 (需檢附 CI 單連結)*|不適用|
|
|
355
|
+
|*測試報告 (檢附前一環境 CD 單驗證結果並附上連結)*|參照連結|
|
|
356
|
+
|*SIT 測試涵蓋檢核表 (UAT 申請時需確認)*|參照連結|
|
|
357
|
+
|*UAT 黑箱掃描報告 (PRD 申請時需檢附,需白箱則請附上報告連結)*|不適用|
|
|
358
|
+
|*操作步驟說明*|Ansible 程式上版。|
|
|
359
|
+
|*簽核流程*|STG : 單位 Reviewer\\\\UAT : 單位部長 > INFRA_資管部長\\\\PRD/DR : 單位部長 > 單位處長 > INFRA_資管部長|`;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* 從 Library Jenkins build comment 提取 CID_version 與 moduleKey
|
|
364
|
+
* comment format: "Release version [ch014.ibk.ibk-release-v1.5.2.0-0.0.1|url] pass in ..."
|
|
365
|
+
* @returns {{ CID_version: string|null, moduleKey: string|null }}
|
|
366
|
+
*/
|
|
367
|
+
function parseLibraryInfo(comments = []) {
|
|
368
|
+
for (const comment of comments) {
|
|
369
|
+
const body = comment.body ?? String(comment);
|
|
370
|
+
const bracketMatch = body.match(/Release version \[([^\]]+)/);
|
|
371
|
+
if (!bracketMatch) continue;
|
|
372
|
+
const text = bracketMatch[1].split('|')[0];
|
|
373
|
+
const releaseIdx = text.indexOf('-release-');
|
|
374
|
+
if (releaseIdx === -1) continue;
|
|
375
|
+
const prefix = text.slice(0, releaseIdx);
|
|
376
|
+
const suffix = text.slice(releaseIdx + '-release-'.length);
|
|
377
|
+
const moduleKey = prefix.split('.').pop();
|
|
378
|
+
const versionMatch = suffix.match(/.*-(\d+\.\d+\.\d+)$/);
|
|
379
|
+
const CID_version = versionMatch ? versionMatch[1] : null;
|
|
380
|
+
if (!moduleKey || !CID_version) continue;
|
|
381
|
+
return {CID_version, moduleKey};
|
|
382
|
+
}
|
|
383
|
+
return {CID_version: null, moduleKey: null};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function normalizeLibraryCidVersion(branch, parsedComment) {
|
|
387
|
+
if (!branch || !parsedComment.CID_version) return '';
|
|
388
|
+
const normalizedBranch = branch.replace(/\//g, '-');
|
|
389
|
+
return normalizedBranch.endsWith(`-${parsedComment.CID_version}`)
|
|
390
|
+
? normalizedBranch
|
|
391
|
+
: `${normalizedBranch}-${parsedComment.CID_version}`;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function inferModuleFromBranch(branch, systemCode) {
|
|
395
|
+
const normalized = branch.replace(/^release[\/\-]/, '');
|
|
396
|
+
const prefix = `${systemCode}_`;
|
|
397
|
+
if (!normalized.startsWith(prefix)) return '';
|
|
398
|
+
const rest = normalized.slice(prefix.length);
|
|
399
|
+
return rest.split('_')[0] ?? '';
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function getLibraryVersionName(branch, systemCode, module) {
|
|
403
|
+
const normalized = branch.startsWith('release-')
|
|
404
|
+
? branch.replace(/^release-/, '').replace(/-\d+\.\d+\.\d+$/, '')
|
|
405
|
+
: branch.replace(/^release\//, '');
|
|
406
|
+
const prefix = `${systemCode}_`;
|
|
407
|
+
if (systemCode === SYSTEM_CODES.CWA && branch.startsWith('release-')) return normalized;
|
|
408
|
+
if (!normalized.startsWith(prefix)) return normalized.replace(/^v/, '');
|
|
409
|
+
|
|
410
|
+
const rest = normalized.slice(prefix.length);
|
|
411
|
+
const modulePrefix = `${module}_`;
|
|
412
|
+
if (!rest.startsWith(modulePrefix)) return rest;
|
|
413
|
+
const version = rest.slice(modulePrefix.length);
|
|
414
|
+
return module === systemCode.toLowerCase() ? rest : version;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
async function findPrevLibraryDeployedToEnv(jira, branchBase, cidVersion, env, systemCode) {
|
|
418
|
+
try {
|
|
419
|
+
// ex: cwa CID-1714 library release-v1.5.2.0-0.0.3
|
|
420
|
+
// STG: sourceBranch=release-v1.5.2.0-0.0.3, targetBranch=release-v1.5.2.0-0.0.2
|
|
421
|
+
// UAT: sourceBranch=release-v1.5.2.0-0.0.3, targetBranch=release-v1.5.1.1
|
|
422
|
+
|
|
423
|
+
const envLower = env.toLowerCase();
|
|
424
|
+
const parts = cidVersion.split('.').map(Number);
|
|
425
|
+
if (parts.length < 3) return null;
|
|
426
|
+
|
|
427
|
+
// PRD 永遠找最後一個 release/ 分支 → 回傳 null,由上層 fallback 到 getBitbucketTags
|
|
428
|
+
if (envLower === 'prd' || envLower === 'dr' || envLower === 'prd/dr') {
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// STG: 只有第一次(serial = 0.0.1)才找前一個 release/;其他找上一個 Library CID_version
|
|
433
|
+
if (envLower === 'stg' || envLower === 'dev') {
|
|
434
|
+
if (parts[2] <= 1) return null; // 第一次 → fallback to release/ tag
|
|
435
|
+
const prevSerial = `${parts[0]}.${parts[1]}.${parts[2] - 1}`;
|
|
436
|
+
// 驗證上一個 serial 的 Library 是否已經有同環境 CD 完成。
|
|
437
|
+
const searchResults = await jira.searchIssues(
|
|
438
|
+
`project = CID AND issuetype = Library AND "CID_branch" ~ "${branchBase}" ORDER BY created DESC`,
|
|
439
|
+
['comment', 'issuelinks'],
|
|
440
|
+
);
|
|
441
|
+
for (const lib of searchResults) {
|
|
442
|
+
const {CID_version: libVersion} = parseLibraryInfo(lib.fields?.comment?.comments ?? []);
|
|
443
|
+
if (libVersion !== prevSerial) continue;
|
|
444
|
+
for (const link of lib.fields?.issuelinks ?? []) {
|
|
445
|
+
const ci = link.inwardIssue ?? link.outwardIssue;
|
|
446
|
+
if (ci?.fields?.issuetype?.name !== 'CI') continue;
|
|
447
|
+
try {
|
|
448
|
+
const ciFields = await jira.getIssueFields(ci.key, ['issuelinks']);
|
|
449
|
+
for (const cdLink of ciFields?.issuelinks ?? []) {
|
|
450
|
+
const cd = cdLink.inwardIssue ?? cdLink.outwardIssue;
|
|
451
|
+
if (
|
|
452
|
+
cd?.fields?.issuetype?.name === 'CD' &&
|
|
453
|
+
(cd.fields?.summary ?? '').includes(`[${systemCode}][${envLower.toUpperCase()}]`) &&
|
|
454
|
+
cd.fields?.status?.name?.toLowerCase() === 'done'
|
|
455
|
+
) {
|
|
456
|
+
return prevSerial;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
} catch (_) {
|
|
460
|
+
/* skip */
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// UAT: 往前找,直到找到有 UAT CD status=Done 的版本
|
|
468
|
+
if (envLower === 'uat') {
|
|
469
|
+
if (parts[2] <= 1) return null; // 第一版 → fallback to release/ tag
|
|
470
|
+
// 一次撈同 branchBase 下的所有 Library
|
|
471
|
+
const searchResults = await jira.searchIssues(
|
|
472
|
+
`project = CID AND issuetype = Library AND "CID_branch" ~ "${branchBase}" ORDER BY created DESC`,
|
|
473
|
+
['comment', 'issuelinks'],
|
|
474
|
+
);
|
|
475
|
+
// 從 cidVersion - 1 逐步往前找
|
|
476
|
+
for (let i = parts[2] - 1; i >= 1; i--) {
|
|
477
|
+
const prevSerial = `${parts[0]}.${parts[1]}.${i}`;
|
|
478
|
+
for (const lib of searchResults) {
|
|
479
|
+
const {CID_version: libVersion} = parseLibraryInfo(lib.fields?.comment?.comments ?? []);
|
|
480
|
+
if (libVersion !== prevSerial) continue;
|
|
481
|
+
// 找 Library → CI → CD(UAT, status = Done)
|
|
482
|
+
for (const link of lib.fields?.issuelinks ?? []) {
|
|
483
|
+
const ci = link.inwardIssue ?? link.outwardIssue;
|
|
484
|
+
if (ci?.fields?.issuetype?.name !== 'CI') continue;
|
|
485
|
+
try {
|
|
486
|
+
const ciFields = await jira.getIssueFields(ci.key, ['issuelinks']);
|
|
487
|
+
for (const cdLink of ciFields?.issuelinks ?? []) {
|
|
488
|
+
const cd = cdLink.inwardIssue ?? cdLink.outwardIssue;
|
|
489
|
+
if (
|
|
490
|
+
cd?.fields?.issuetype?.name === 'CD' &&
|
|
491
|
+
(cd.fields?.summary ?? '').includes(`[${systemCode}][UAT]`) &&
|
|
492
|
+
cd.fields?.status?.name?.toLowerCase() === 'done'
|
|
493
|
+
) {
|
|
494
|
+
return prevSerial;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
} catch (_) {
|
|
498
|
+
/* skip */
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return null; // 都找不到 → fallback to release/ tag
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
return null;
|
|
507
|
+
} catch (_) {
|
|
508
|
+
return null;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* 根據 CI 關聯的 Library 單生成 release notes,格式為 "模組: Bitbucket compare URL"
|
|
514
|
+
* @param jira
|
|
515
|
+
* @param ciKey
|
|
516
|
+
* @param env
|
|
517
|
+
* @param systemCode
|
|
518
|
+
* @returns {Promise<string|string>}
|
|
519
|
+
*/
|
|
520
|
+
async function generateReleaseNotes(jira, ciKey, env, systemCode) {
|
|
521
|
+
try {
|
|
522
|
+
const ciFields = await jira.getIssueFields(ciKey, ['issuelinks']);
|
|
523
|
+
const notes = [];
|
|
524
|
+
|
|
525
|
+
for (const link of ciFields?.issuelinks ?? []) {
|
|
526
|
+
const linked = link.inwardIssue ?? link.outwardIssue;
|
|
527
|
+
if (linked?.fields?.issuetype?.name !== 'Library') continue;
|
|
528
|
+
try {
|
|
529
|
+
const libFields = await jira.getIssueFields(linked.key, [
|
|
530
|
+
'customfield_13431',
|
|
531
|
+
'customfield_13702',
|
|
532
|
+
'customfield_14705',
|
|
533
|
+
'comment',
|
|
534
|
+
]);
|
|
535
|
+
const branch = libFields?.['customfield_13431'] ?? '';
|
|
536
|
+
const child = libFields?.['customfield_13702']?.child;
|
|
537
|
+
const childIdToName = Object.fromEntries(
|
|
538
|
+
Object.entries(LIBRARY_MODULE_IDS[systemCode] ?? {}).map(([name, id]) => [id, name]),
|
|
539
|
+
);
|
|
540
|
+
const parsedComment = parseLibraryInfo(libFields?.comment?.comments ?? []);
|
|
541
|
+
const module =
|
|
542
|
+
child?.value ??
|
|
543
|
+
childIdToName[child?.id] ??
|
|
544
|
+
parsedComment.moduleKey ??
|
|
545
|
+
inferModuleFromBranch(branch, systemCode);
|
|
546
|
+
const cidVersion = libFields?.['customfield_14705'] ?? normalizeLibraryCidVersion(
|
|
547
|
+
branch,
|
|
548
|
+
parsedComment,
|
|
549
|
+
);
|
|
550
|
+
|
|
551
|
+
// library build 流水號 0.0.1
|
|
552
|
+
const cidVersionSerialNum = cidVersion.split('-').pop();
|
|
553
|
+
|
|
554
|
+
if (!cidVersionSerialNum || !module) continue;
|
|
555
|
+
const repoName = MODULE_MAP[module];
|
|
556
|
+
if (!repoName) continue;
|
|
557
|
+
const repoMeta = REPO_MAPS[repoName];
|
|
558
|
+
if (!repoMeta) continue;
|
|
559
|
+
const label = REPO_LABEL_MAP[repoName];
|
|
560
|
+
const branchWithDashes = branch.replace(/\//g, '-');
|
|
561
|
+
|
|
562
|
+
// source branch 直接抓 lib CID_version customfield_14705
|
|
563
|
+
const sourceBranch = `ci/${cidVersion}`;
|
|
564
|
+
|
|
565
|
+
const prevVersion = await findPrevLibraryDeployedToEnv(
|
|
566
|
+
jira,
|
|
567
|
+
branch,
|
|
568
|
+
cidVersionSerialNum,
|
|
569
|
+
env,
|
|
570
|
+
systemCode,
|
|
571
|
+
);
|
|
572
|
+
|
|
573
|
+
// let targetBranch = await findPrevLibraryDeployedToEnv(jira, branch, cidVersionSerialNum, env, systemCode)
|
|
574
|
+
let targetBranch;
|
|
575
|
+
|
|
576
|
+
if (prevVersion) {
|
|
577
|
+
targetBranch = `ci/${branchWithDashes}-${prevVersion}`;
|
|
578
|
+
} else {
|
|
579
|
+
// 找最新的 release/ 開頭 branch(非 tag)
|
|
580
|
+
const branches = await jira.getBitbucketBranches(repoMeta.project, repoName, {
|
|
581
|
+
filterValue: 'release/',
|
|
582
|
+
});
|
|
583
|
+
if (!branches?.length) continue;
|
|
584
|
+
targetBranch = branches[0].displayId;
|
|
585
|
+
}
|
|
586
|
+
const baseUrl = process.env.BITBUCKET_URL || 'https://bitbucket.example.com';
|
|
587
|
+
const compareUrl = `${baseUrl}/projects/${repoMeta.project}/repos/${repoName}/compare/diff?sourceBranch=${sourceBranch.replace(/\//g, '%2F')}&targetBranch=${targetBranch.replace(/\//g, '%2F')}&targetRepoId=${repoMeta.repoId}`;
|
|
588
|
+
notes.push(`${label}: ${compareUrl}`);
|
|
589
|
+
} catch (_) {
|
|
590
|
+
/* skip */
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
return notes.length ? `release notes:\n${notes.join('\n')}` : '';
|
|
594
|
+
} catch (_) {
|
|
595
|
+
return '';
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* 自動從 CI 單的 relates Library 單取 CID_branch,比對 LBPRJ 版本頁,每個 Library 單加一筆 Web Link
|
|
601
|
+
*/
|
|
602
|
+
async function handleWeblink(jira, issue, notifier, args) {
|
|
603
|
+
if (args.ciTicket) {
|
|
604
|
+
try {
|
|
605
|
+
const ciFields = await jira.getIssueFields(args.ciTicket, ['issuelinks']);
|
|
606
|
+
const issueLinks = ciFields?.issuelinks ?? [];
|
|
607
|
+
// 找出所有 Library 類型的關聯單(inward 或 outward 都算)
|
|
608
|
+
const libraryKeys = [];
|
|
609
|
+
for (const link of issueLinks) {
|
|
610
|
+
const linked = link.outwardIssue ?? link.inwardIssue;
|
|
611
|
+
if (linked?.fields?.issuetype?.name === 'Library') {
|
|
612
|
+
libraryKeys.push(linked.key);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// CI 單有找到 library relates
|
|
617
|
+
if (libraryKeys.length > 0) {
|
|
618
|
+
for (const libKey of libraryKeys) {
|
|
619
|
+
try {
|
|
620
|
+
// 同時取 branch 與 comment(comment 用於解析 CID_version 去除後綴)
|
|
621
|
+
const libFields = await jira.getIssueFields(libKey, [
|
|
622
|
+
'customfield_13431',
|
|
623
|
+
'customfield_13702',
|
|
624
|
+
'comment',
|
|
625
|
+
]);
|
|
626
|
+
const branch = libFields?.['customfield_13431'] ?? '';
|
|
627
|
+
const child = libFields?.['customfield_13702']?.child;
|
|
628
|
+
const childIdToName = Object.fromEntries(
|
|
629
|
+
Object.entries(LIBRARY_MODULE_IDS[args.systemCode] ?? {}).map(([name, id]) => [id, name]),
|
|
630
|
+
);
|
|
631
|
+
const parsedComment = parseLibraryInfo(libFields?.comment?.comments ?? []);
|
|
632
|
+
const module =
|
|
633
|
+
child?.value ??
|
|
634
|
+
childIdToName[child?.id] ??
|
|
635
|
+
parsedComment.moduleKey ??
|
|
636
|
+
inferModuleFromBranch(branch, args.systemCode);
|
|
637
|
+
|
|
638
|
+
// 去掉 'release/' 或 'release-' 前綴取得版本名稱
|
|
639
|
+
const versionName = getLibraryVersionName(branch, args.systemCode, module);
|
|
640
|
+
const moduleName = args.systemCode === SYSTEM_CODES.CWA
|
|
641
|
+
? versionName
|
|
642
|
+
: getModuleName(args.systemCode, module, versionName);
|
|
643
|
+
// 只取 unreleased、名稱含 systemCode 的版本(對應 UI: status=unreleased&contains=systemCode)
|
|
644
|
+
const versionId = await jira.getProjectVersions('LBPRJ', {
|
|
645
|
+
name: moduleName,
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
if (versionId) {
|
|
649
|
+
const versionUrl = `${process.env.JIRA_BASE_URL}/projects/LBPRJ/versions/${versionId}`;
|
|
650
|
+
await jira.addRemoteLink(issue.key, versionUrl, moduleName);
|
|
651
|
+
await notifier.notify(issue.key, `已附上版本 Web Link:${moduleName}(${libKey})`);
|
|
652
|
+
} else {
|
|
653
|
+
console.error(
|
|
654
|
+
`[create_cd_ticket] 找不到 Release Version unreleased 的版本 "${moduleName}"(來自 ${libKey}",有可能已經 release 了)`,
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
} catch (e) {
|
|
658
|
+
console.error(`[create_cd_ticket] 處理 ${libKey} remote link 失敗: ${e.message}`);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
} catch (e) {
|
|
663
|
+
console.error(`[create_cd_ticket] 取得 CI 關聯 Library 單失敗: ${e.message}`);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|