@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/workflows.js
CHANGED
|
@@ -1,53 +1,73 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { error, getServerList, ok } from './helpers.js';
|
|
2
|
+
import { CI_FIELD_IDS, LIBRARY_MODULE_IDS, SYSTEM_MODULES } from '../constants/index.js';
|
|
2
3
|
|
|
3
4
|
export function getWorkflowToolDefinitions() {
|
|
4
5
|
return [
|
|
5
6
|
{
|
|
6
|
-
name: '
|
|
7
|
+
name: 'run_release_to_stg',
|
|
7
8
|
description:
|
|
8
|
-
'
|
|
9
|
+
'執行正式 release branch 到 STG 的完整流程。支援:① 上這次 release 到 STG(用 get_unreleased_versions 找 module/branch)② 指定多個 module/branch 上到 STG ③ 重上 release 部分 module。完成後 STG CD/Deployment deploy pass,並將 CI 停在 Wait To UAT。',
|
|
9
10
|
inputSchema: {
|
|
10
11
|
type: 'object',
|
|
11
|
-
required: [
|
|
12
|
+
required: [],
|
|
12
13
|
properties: {
|
|
13
14
|
systemCode: {
|
|
14
15
|
type: 'string',
|
|
15
|
-
description: '系統代碼,例如 IBK、CWA
|
|
16
|
+
description: '系統代碼,例如 IBK、CWA。上這次 release 或重上 release 時建議提供;未提供時會要求使用者確認 systemCode。',
|
|
16
17
|
},
|
|
17
|
-
|
|
18
|
+
mode: {
|
|
19
|
+
type: 'string',
|
|
20
|
+
enum: ['current_release', 'explicit_modules', 'rerun_release'],
|
|
21
|
+
description: '流程模式:current_release=上這次 release;explicit_modules=使用指定 module/branch;rerun_release=重上部分 module。未提供時依 modules/oldCiIssueKey 推論。',
|
|
22
|
+
},
|
|
23
|
+
modules: {
|
|
24
|
+
type: 'array',
|
|
25
|
+
description: '指定 module/branch 或 rerun module 清單。explicit_modules 需提供 gitBranch;rerun_release 可只提供 module。',
|
|
26
|
+
items: {
|
|
27
|
+
type: 'object',
|
|
28
|
+
required: ['module'],
|
|
29
|
+
properties: {
|
|
30
|
+
module: { type: 'string', description: '模組名稱,例如 ssr、wealth、a11y、ibk、cwa' },
|
|
31
|
+
gitBranch: { type: 'string', description: '要上版的 branch,例如 release/v1.5.2.0 或 release/abc-123' },
|
|
32
|
+
rerun: { type: 'boolean', description: '是否重開此 module 的 Library' },
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
oldCiIssueKey: {
|
|
18
37
|
type: 'string',
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
38
|
+
description: '重上 release 時的舊 CI key;不提供時會查該 systemCode 最新進行中的 CI。',
|
|
39
|
+
},
|
|
40
|
+
dryRun: {
|
|
41
|
+
type: 'boolean',
|
|
42
|
+
description: '只回傳計畫,不實際建立或切換 Jira 單。',
|
|
22
43
|
},
|
|
23
44
|
},
|
|
24
45
|
},
|
|
25
46
|
},
|
|
26
47
|
{
|
|
27
|
-
name: '
|
|
48
|
+
name: 'continue_release_to_cd_ready',
|
|
28
49
|
description:
|
|
29
|
-
'
|
|
50
|
+
'從既有 CI 單繼續正式 release 到 UAT 或 PRD/DR 的 CD-ready 流程。UAT/PRD/DR 只會建立 CD 單並執行 prepare_cd_deployment 建立/準備 Deployment;不會呼叫 trigger_deployment。PRD/DR 會先執行 wait_to_prd_dr,完成 Upload To Pre-Release 並等待 CID_upload_result pass 後才開 CD。後續 Switch Execution Node / To AutoDeploy / Trigger AutoDeploy 需由資管人員執行。',
|
|
30
51
|
inputSchema: {
|
|
31
52
|
type: 'object',
|
|
32
|
-
required: ['
|
|
53
|
+
required: ['ciIssueKey', 'environment'],
|
|
33
54
|
properties: {
|
|
34
|
-
|
|
55
|
+
ciIssueKey: {
|
|
35
56
|
type: 'string',
|
|
36
|
-
description: '
|
|
57
|
+
description: '作為 continuation 起點的 CI 單 issue key,例如 CID-1234。找不到 CI 單時 planner 必須先詢問使用者,不可猜測。',
|
|
37
58
|
},
|
|
38
|
-
|
|
59
|
+
environment: {
|
|
39
60
|
type: 'string',
|
|
40
|
-
|
|
61
|
+
enum: ['uat', 'prd/dr', 'prd', 'dr'],
|
|
62
|
+
description: '要繼續開立 CD/Deployment 的目標環境。prd 或 dr 會正規化為 prd/dr。',
|
|
41
63
|
},
|
|
42
|
-
|
|
64
|
+
systemCode: {
|
|
43
65
|
type: 'string',
|
|
44
|
-
description: '
|
|
66
|
+
description: '系統代碼,例如 IBK、CWA。未提供時會嘗試從 CI 單 CID_system_code 推導;推不到會要求使用者補充。',
|
|
45
67
|
},
|
|
46
|
-
|
|
47
|
-
type: '
|
|
48
|
-
|
|
49
|
-
default: 'stg',
|
|
50
|
-
description: '固定為 stg;保留此參數讓自然語言 planner 可以映射使用者提到的 STG。',
|
|
68
|
+
dryRun: {
|
|
69
|
+
type: 'boolean',
|
|
70
|
+
description: '只回傳計畫,不實際推進 CI、建立 CD 或準備 Deployment。',
|
|
51
71
|
},
|
|
52
72
|
},
|
|
53
73
|
},
|
|
@@ -56,9 +76,7 @@ export function getWorkflowToolDefinitions() {
|
|
|
56
76
|
}
|
|
57
77
|
|
|
58
78
|
function extractText(result) {
|
|
59
|
-
if (!result?.content?.length)
|
|
60
|
-
return '';
|
|
61
|
-
}
|
|
79
|
+
if (!result?.content?.length) return '';
|
|
62
80
|
return result.content
|
|
63
81
|
.filter((item) => item.type === 'text')
|
|
64
82
|
.map((item) => item.text)
|
|
@@ -73,7 +91,7 @@ function formatJson(value) {
|
|
|
73
91
|
function parseToolJson(result) {
|
|
74
92
|
const text = extractText(result);
|
|
75
93
|
if (result?.isError || text.startsWith('❌')) {
|
|
76
|
-
throw new Error(text);
|
|
94
|
+
throw new Error(text.replace(/^❌ 錯誤: /, ''));
|
|
77
95
|
}
|
|
78
96
|
return JSON.parse(text);
|
|
79
97
|
}
|
|
@@ -84,7 +102,7 @@ async function runToolOrThrow(name, args, deps, workflowLog) {
|
|
|
84
102
|
phase: 'action',
|
|
85
103
|
title: `執行 workflow step: ${name}`,
|
|
86
104
|
toolName: name,
|
|
87
|
-
issueKey: args.issueKey ?? args.cdIssueKey ?? args.linkedCiKey,
|
|
105
|
+
issueKey: args.issueKey ?? args.cdIssueKey ?? args.linkedCiKey ?? args.oldCiIssueKey,
|
|
88
106
|
});
|
|
89
107
|
const result = await deps.executeToolImpl(name, args, deps);
|
|
90
108
|
return parseToolJson(result);
|
|
@@ -122,138 +140,398 @@ async function waitForIssueStatus(issueKey, targetStatuses, deps, options = {})
|
|
|
122
140
|
throw new Error(`等待 ${issueKey} 狀態 ${targetStatuses.join(' / ')} 超時`);
|
|
123
141
|
}
|
|
124
142
|
|
|
125
|
-
export async function
|
|
143
|
+
export async function handleRunReleaseToStgWorkflow(args, deps) {
|
|
126
144
|
try {
|
|
127
145
|
const workflowLog = [];
|
|
128
|
-
const
|
|
129
|
-
const
|
|
130
|
-
const
|
|
146
|
+
const assumptions = [];
|
|
147
|
+
const systemCode = normalizeSystemCode(args.systemCode);
|
|
148
|
+
const mode = resolveMode(args);
|
|
149
|
+
|
|
150
|
+
if (!systemCode) {
|
|
151
|
+
return ok({
|
|
152
|
+
needsSystemSelection: true,
|
|
153
|
+
message: '請指定要上版的 systemCode,例如 IBK 或 CWA。',
|
|
154
|
+
mode,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
131
157
|
|
|
132
|
-
const
|
|
133
|
-
|
|
158
|
+
const releasePlan = await buildReleasePlan({ systemCode, mode, args, deps, workflowLog, assumptions });
|
|
159
|
+
if (args.dryRun) {
|
|
160
|
+
return ok({ dryRun: true, type: 'Release To STG', systemCode, mode, plan: releasePlan, assumptions });
|
|
161
|
+
}
|
|
134
162
|
|
|
135
|
-
const
|
|
163
|
+
const libraryRefs = await prepareLibraries({ systemCode, releasePlan, mode, deps, workflowLog });
|
|
164
|
+
const ciRelatesTo = [...libraryRefs.map((library) => library.issueKey)];
|
|
165
|
+
if (releasePlan.oldCiIssueKey) {
|
|
166
|
+
ciRelatesTo.unshift(releasePlan.oldCiIssueKey);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const ci = await runToolOrThrow('create_ci_ticket', {
|
|
170
|
+
systemCode,
|
|
171
|
+
environment: 'stg',
|
|
172
|
+
modules: releasePlan.modules.map((item) => item.module),
|
|
173
|
+
relatesTo: ciRelatesTo,
|
|
174
|
+
}, deps, workflowLog);
|
|
175
|
+
|
|
176
|
+
if (releasePlan.oldCiIssueKey) {
|
|
177
|
+
await runToolOrThrow('transition_issue', {
|
|
178
|
+
issueKey: releasePlan.oldCiIssueKey,
|
|
179
|
+
transitionName: 'Cancelled',
|
|
180
|
+
}, deps, workflowLog);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const build = await runToolOrThrow('build_ci', { issueKey: ci.issueKey }, deps, workflowLog);
|
|
184
|
+
const toStg = await runToolOrThrow('wait_to_stg', { issueKey: ci.issueKey }, deps, workflowLog);
|
|
185
|
+
const clusters = getServerList(systemCode, 'stg');
|
|
136
186
|
if (!clusters.length) {
|
|
137
187
|
throw new Error(`找不到 ${systemCode} STG 的 cluster 設定`);
|
|
138
188
|
}
|
|
139
189
|
|
|
140
|
-
|
|
190
|
+
assumptions.push(`CD cluster 自動帶入 ${systemCode} STG 全部 cluster`);
|
|
191
|
+
const cd = await runToolOrThrow('create_cd_ticket', {
|
|
141
192
|
systemCode,
|
|
142
193
|
environment: 'stg',
|
|
143
194
|
linkedCiKey: ci.issueKey,
|
|
144
195
|
clusterDeploy: clusters.join(','),
|
|
145
|
-
moduleChild:
|
|
146
|
-
};
|
|
147
|
-
const cd = await runToolOrThrow('create_cd_ticket', cdArgs, deps, workflowLog);
|
|
196
|
+
moduleChild: releasePlan.modules.map((item) => item.module).join(','),
|
|
197
|
+
}, deps, workflowLog);
|
|
148
198
|
const prepared = await runToolOrThrow(
|
|
149
199
|
'prepare_cd_deployment',
|
|
150
|
-
{issueKey: cd.issueKey, environment: 'stg'},
|
|
200
|
+
{ issueKey: cd.issueKey, environment: 'stg' },
|
|
151
201
|
deps,
|
|
152
202
|
workflowLog,
|
|
153
203
|
);
|
|
154
204
|
const deployed = await runToolOrThrow(
|
|
155
205
|
'trigger_deployment',
|
|
156
|
-
{cdIssueKey: cd.issueKey, environment: 'stg'
|
|
206
|
+
{ cdIssueKey: cd.issueKey, environment: 'stg' },
|
|
157
207
|
deps,
|
|
158
208
|
workflowLog,
|
|
159
209
|
);
|
|
210
|
+
const toUat = await runToolOrThrow('wait_to_uat', { issueKey: ci.issueKey }, deps, workflowLog);
|
|
160
211
|
|
|
161
212
|
return ok({
|
|
162
|
-
type: '
|
|
213
|
+
type: 'Release To STG',
|
|
163
214
|
systemCode,
|
|
215
|
+
mode,
|
|
216
|
+
modules: releasePlan.modules,
|
|
217
|
+
libraryIssueKeys: libraryRefs.map((library) => library.issueKey),
|
|
218
|
+
reusedLibraryIssueKeys: libraryRefs.filter((library) => library.reused).map((library) => library.issueKey),
|
|
219
|
+
oldCiIssueKey: releasePlan.oldCiIssueKey,
|
|
220
|
+
oldCiCancelled: Boolean(releasePlan.oldCiIssueKey),
|
|
164
221
|
ciIssueKey: ci.issueKey,
|
|
165
|
-
ciStatus: toStg.status ?? build.status,
|
|
222
|
+
ciStatus: toUat.status ?? toStg.status ?? build.status,
|
|
166
223
|
cdIssueKey: cd.issueKey,
|
|
167
224
|
cdStatus: prepared.status,
|
|
168
225
|
deploymentStatus: deployed.status,
|
|
169
|
-
|
|
226
|
+
deploymentResult: deployed.deployResult,
|
|
227
|
+
stoppedAt: 'Wait To UAT',
|
|
228
|
+
assumptions,
|
|
170
229
|
workflowLog,
|
|
171
230
|
});
|
|
172
231
|
} catch (err) {
|
|
173
|
-
return error(`
|
|
232
|
+
return error(`run_release_to_stg 失敗: ${err.message}`);
|
|
174
233
|
}
|
|
175
234
|
}
|
|
176
235
|
|
|
177
|
-
export async function
|
|
236
|
+
export async function handleContinueReleaseToCdReadyWorkflow(args, deps) {
|
|
178
237
|
try {
|
|
179
238
|
const workflowLog = [];
|
|
180
|
-
const
|
|
181
|
-
const
|
|
182
|
-
|
|
239
|
+
const ciIssueKey = normalizeIssueKey(args.ciIssueKey);
|
|
240
|
+
const environment = normalizeContinuationEnvironment(args.environment);
|
|
241
|
+
|
|
242
|
+
if (!ciIssueKey) {
|
|
243
|
+
return ok({
|
|
244
|
+
needsCiIssueKey: true,
|
|
245
|
+
message: '請提供要繼續開立 UAT/PRD CD 的 CI 單號,例如 CID-1234。',
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (!environment) {
|
|
250
|
+
return ok({
|
|
251
|
+
needsEnvironmentSelection: true,
|
|
252
|
+
ciIssueKey,
|
|
253
|
+
message: '請指定要繼續開立的環境:UAT 或 PRD/DR。',
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const systemCode = await resolveContinuationSystemCode(args.systemCode, ciIssueKey, deps);
|
|
258
|
+
if (!systemCode) {
|
|
259
|
+
return ok({
|
|
260
|
+
needsSystemSelection: true,
|
|
261
|
+
ciIssueKey,
|
|
262
|
+
environment,
|
|
263
|
+
message: `無法從 ${ciIssueKey} 推導 systemCode,請指定系統代碼,例如 IBK 或 CWA。`,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
183
266
|
|
|
184
|
-
|
|
185
|
-
|
|
267
|
+
const plan = buildContinuationPlan({ ciIssueKey, environment, systemCode });
|
|
268
|
+
if (args.dryRun) {
|
|
269
|
+
return ok({
|
|
270
|
+
dryRun: true,
|
|
271
|
+
type: 'Release Continuation To CD Ready',
|
|
272
|
+
...plan,
|
|
273
|
+
});
|
|
186
274
|
}
|
|
187
275
|
|
|
188
|
-
const
|
|
276
|
+
const ciReady = isPrdDrEnvironment(environment)
|
|
277
|
+
? await runToolOrThrow('wait_to_prd_dr', { issueKey: ciIssueKey }, deps, workflowLog)
|
|
278
|
+
: await runToolOrThrow('wait_to_uat', { issueKey: ciIssueKey }, deps, workflowLog);
|
|
279
|
+
|
|
280
|
+
const cd = await runToolOrThrow('create_cd_ticket', {
|
|
189
281
|
systemCode,
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
lib.issueKey,
|
|
199
|
-
['Released'],
|
|
200
|
-
deps,
|
|
201
|
-
{timeoutMs: 10 * 60 * 1000, ...deps.workflowWaitOptions},
|
|
202
|
-
);
|
|
203
|
-
workflowLog.push(`- wait_library_status: ${lib.issueKey} -> ${libFinalStatus}`);
|
|
282
|
+
environment,
|
|
283
|
+
linkedCiKey: ciIssueKey,
|
|
284
|
+
}, deps, workflowLog);
|
|
285
|
+
|
|
286
|
+
const prepared = await runToolOrThrow('prepare_cd_deployment', {
|
|
287
|
+
issueKey: cd.issueKey,
|
|
288
|
+
environment,
|
|
289
|
+
}, deps, workflowLog);
|
|
204
290
|
|
|
205
|
-
|
|
291
|
+
return ok(buildContinuationResult({
|
|
292
|
+
ciIssueKey,
|
|
293
|
+
environment,
|
|
206
294
|
systemCode,
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
295
|
+
ciReady,
|
|
296
|
+
cd,
|
|
297
|
+
prepared,
|
|
298
|
+
workflowLog,
|
|
299
|
+
}));
|
|
300
|
+
} catch (err) {
|
|
301
|
+
return error(`continue_release_to_cd_ready 失敗: ${err.message}`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
213
304
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
305
|
+
function buildContinuationPlan({ ciIssueKey, environment, systemCode }) {
|
|
306
|
+
const prdDr = isPrdDrEnvironment(environment);
|
|
307
|
+
return {
|
|
308
|
+
ciIssueKey,
|
|
309
|
+
environment,
|
|
310
|
+
systemCode,
|
|
311
|
+
type: 'Release Continuation To CD Ready',
|
|
312
|
+
steps: [
|
|
313
|
+
prdDr ? 'wait_to_prd_dr' : 'wait_to_uat',
|
|
314
|
+
'create_cd_ticket',
|
|
315
|
+
'prepare_cd_deployment',
|
|
316
|
+
],
|
|
317
|
+
triggerDeploymentSkipped: true,
|
|
318
|
+
triggerDeploymentReason: 'UAT/PRD/DR deployment transitions require privileged operator execution',
|
|
319
|
+
handoffRequired: true,
|
|
320
|
+
handoffTo: '資管人員',
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function buildContinuationResult({ ciIssueKey, environment, systemCode, ciReady, cd, prepared, workflowLog }) {
|
|
325
|
+
const prdDr = isPrdDrEnvironment(environment);
|
|
326
|
+
return {
|
|
327
|
+
type: 'Release Continuation To CD Ready',
|
|
328
|
+
environment,
|
|
329
|
+
systemCode,
|
|
330
|
+
ciIssueKey,
|
|
331
|
+
ciStatus: ciReady.status,
|
|
332
|
+
uploadResult: prdDr ? ciReady.uploadResult : undefined,
|
|
333
|
+
cdIssueKey: cd.issueKey,
|
|
334
|
+
cdStatus: prepared.status,
|
|
335
|
+
deploymentPrepared: true,
|
|
336
|
+
triggerDeploymentSkipped: true,
|
|
337
|
+
triggerDeploymentReason: 'UAT/PRD/DR deployment transitions require privileged operator execution',
|
|
338
|
+
handoffRequired: true,
|
|
339
|
+
handoffTo: '資管人員',
|
|
340
|
+
manualNextSteps: [
|
|
341
|
+
'請資管人員在 Deployment sub-task 執行 Switch Execution Node / To AutoDeploy / Trigger AutoDeploy。',
|
|
342
|
+
'Ares 不會自動執行 trigger_deployment,避免 UAT/PRD/DR 權限被擋。',
|
|
343
|
+
],
|
|
344
|
+
message: prdDr
|
|
345
|
+
? '已完成 PRD/DR upload pre-release、CD 開單與 Deployment 建立;後續部署 transitions 需由資管人員執行。'
|
|
346
|
+
: '已完成 UAT CD 開單與 Deployment 建立;後續部署 transitions 需由資管人員執行。',
|
|
347
|
+
workflowLog,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async function resolveContinuationSystemCode(inputSystemCode, ciIssueKey, deps) {
|
|
352
|
+
const explicit = normalizeSystemCode(inputSystemCode);
|
|
353
|
+
if (explicit) {
|
|
354
|
+
return explicit;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
try {
|
|
358
|
+
const fields = await deps.jira.getIssueFields(ciIssueKey, [CI_FIELD_IDS.systemCode]);
|
|
359
|
+
return normalizeSystemCode(
|
|
360
|
+
fields?.[CI_FIELD_IDS.systemCode]?.value
|
|
361
|
+
?? fields?.[CI_FIELD_IDS.systemCode],
|
|
362
|
+
);
|
|
363
|
+
} catch {
|
|
364
|
+
return '';
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function normalizeContinuationEnvironment(environment) {
|
|
369
|
+
const env = String(environment ?? '').trim().toLowerCase().replace('&', '/');
|
|
370
|
+
if (env === 'prd' || env === 'dr' || env === 'prd/dr') {
|
|
371
|
+
return 'prd/dr';
|
|
372
|
+
}
|
|
373
|
+
if (env === 'uat') {
|
|
374
|
+
return 'uat';
|
|
375
|
+
}
|
|
376
|
+
return '';
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function isPrdDrEnvironment(environment) {
|
|
380
|
+
return normalizeContinuationEnvironment(environment) === 'prd/dr';
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function normalizeIssueKey(issueKey) {
|
|
384
|
+
return String(issueKey ?? '').trim().toUpperCase();
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
async function buildReleasePlan({ systemCode, mode, args, deps, workflowLog, assumptions }) {
|
|
388
|
+
if (mode === 'explicit_modules') {
|
|
389
|
+
const modules = normalizeModuleSpecs(args.modules);
|
|
390
|
+
const missingBranch = modules.find((item) => !item.gitBranch);
|
|
391
|
+
if (missingBranch) {
|
|
392
|
+
throw new Error(`指定 module/branch 上版時,${missingBranch.module} 缺少 gitBranch`);
|
|
217
393
|
}
|
|
218
|
-
|
|
394
|
+
return { modules };
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const releaseModules = await getCurrentReleaseModules(systemCode, deps, workflowLog);
|
|
398
|
+
if (mode === 'current_release') {
|
|
399
|
+
return { modules: releaseModules };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const rerunModules = new Set(normalizeModuleSpecs(args.modules).map((item) => item.module));
|
|
403
|
+
if (rerunModules.size === 0) {
|
|
404
|
+
throw new Error('重上 release 需指定至少一個 module,例如 ssr');
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const oldCiIssueKey = args.oldCiIssueKey ?? await findLatestActiveCi(systemCode, deps);
|
|
408
|
+
const oldLibraries = await getOldCiLibrariesByModule(systemCode, oldCiIssueKey, deps);
|
|
409
|
+
const modules = releaseModules.map((item) => ({
|
|
410
|
+
...item,
|
|
411
|
+
rerun: rerunModules.has(item.module),
|
|
412
|
+
reusedLibraryKey: rerunModules.has(item.module) ? null : oldLibraries.get(item.module)?.issueKey,
|
|
413
|
+
}));
|
|
414
|
+
|
|
415
|
+
const missingReuse = modules.find((item) => !item.rerun && !item.reusedLibraryKey);
|
|
416
|
+
if (missingReuse) {
|
|
417
|
+
assumptions.push(`找不到 ${missingReuse.module} 可沿用的舊 Library,將改為重開該 Library`);
|
|
418
|
+
missingReuse.rerun = true;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return { modules, oldCiIssueKey };
|
|
422
|
+
}
|
|
219
423
|
|
|
220
|
-
|
|
424
|
+
async function getCurrentReleaseModules(systemCode, deps, workflowLog) {
|
|
425
|
+
const data = await runToolOrThrow('get_unreleased_versions', { systemCode }, deps, workflowLog);
|
|
426
|
+
const versions = data.versions ?? [];
|
|
427
|
+
if (versions.length === 0) {
|
|
428
|
+
throw new Error(`get_unreleased_versions 找不到 ${systemCode} 這次 release modules`);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return versions.map((version) => ({
|
|
432
|
+
module: String(version.module ?? '').trim(),
|
|
433
|
+
gitBranch: version.branch,
|
|
434
|
+
releaseVersionId: version.id,
|
|
435
|
+
releaseVersionName: version.name,
|
|
436
|
+
})).filter((item) => item.module && item.gitBranch);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
async function prepareLibraries({ systemCode, releasePlan, mode, deps, workflowLog }) {
|
|
440
|
+
const refs = [];
|
|
441
|
+
for (const moduleSpec of releasePlan.modules) {
|
|
442
|
+
if (mode === 'rerun_release' && !moduleSpec.rerun && moduleSpec.reusedLibraryKey) {
|
|
443
|
+
refs.push({ module: moduleSpec.module, issueKey: moduleSpec.reusedLibraryKey, reused: true });
|
|
444
|
+
workflowLog.push(`- reuse_library: ${moduleSpec.module} -> ${moduleSpec.reusedLibraryKey}`);
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const library = await runToolOrThrow('create_library_ticket', {
|
|
221
449
|
systemCode,
|
|
450
|
+
module: moduleSpec.module,
|
|
451
|
+
gitBranch: moduleSpec.gitBranch,
|
|
222
452
|
environment: 'stg',
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
'prepare_cd_deployment',
|
|
230
|
-
{issueKey: cd.issueKey, environment: 'stg'},
|
|
231
|
-
deps,
|
|
232
|
-
workflowLog,
|
|
233
|
-
);
|
|
234
|
-
const deployed = await runToolOrThrow(
|
|
235
|
-
'trigger_deployment',
|
|
236
|
-
{cdIssueKey: cd.issueKey, environment: 'stg', applyForClose: true},
|
|
453
|
+
jenkinsBranch: 'master',
|
|
454
|
+
}, deps, workflowLog);
|
|
455
|
+
await runToolOrThrow('build_library', { issueKey: library.issueKey }, deps, workflowLog);
|
|
456
|
+
const libraryStatus = await waitForIssueStatus(
|
|
457
|
+
library.issueKey,
|
|
458
|
+
['Released'],
|
|
237
459
|
deps,
|
|
238
|
-
|
|
460
|
+
{ timeoutMs: 10 * 60 * 1000, ...deps.workflowWaitOptions },
|
|
239
461
|
);
|
|
462
|
+
workflowLog.push(`- wait_library_status: ${library.issueKey} -> ${libraryStatus}`);
|
|
463
|
+
refs.push({ module: moduleSpec.module, issueKey: library.issueKey, reused: false });
|
|
464
|
+
}
|
|
465
|
+
return refs;
|
|
466
|
+
}
|
|
240
467
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
cdIssueKey: cd.issueKey,
|
|
251
|
-
cdStatus: prepared.status,
|
|
252
|
-
deploymentStatus: deployed.status,
|
|
253
|
-
assumptions,
|
|
254
|
-
workflowLog,
|
|
255
|
-
});
|
|
256
|
-
} catch (err) {
|
|
257
|
-
return error(`run_lib_to_stg_release 失敗: ${err.message}`);
|
|
468
|
+
async function findLatestActiveCi(systemCode, deps) {
|
|
469
|
+
const issues = await deps.jira.searchIssues(
|
|
470
|
+
`project = CID AND issuetype = CI AND cf[13443] = "${systemCode}" AND status NOT IN (Done, Cancelled) ORDER BY updated DESC`,
|
|
471
|
+
['summary', 'status'],
|
|
472
|
+
1,
|
|
473
|
+
);
|
|
474
|
+
const issue = issues[0];
|
|
475
|
+
if (!issue?.key) {
|
|
476
|
+
throw new Error(`找不到 ${systemCode} 進行中的舊 CI 單,請指定 oldCiIssueKey`);
|
|
258
477
|
}
|
|
478
|
+
return issue.key;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
async function getOldCiLibrariesByModule(systemCode, ciIssueKey, deps) {
|
|
482
|
+
const fields = await deps.jira.getIssueFields(ciIssueKey, ['issuelinks']);
|
|
483
|
+
const links = fields.issuelinks ?? [];
|
|
484
|
+
const childIdToName = Object.fromEntries(
|
|
485
|
+
Object.entries(LIBRARY_MODULE_IDS[systemCode] ?? {}).map(([name, id]) => [id, name]),
|
|
486
|
+
);
|
|
487
|
+
const libraries = new Map();
|
|
488
|
+
|
|
489
|
+
for (const link of links) {
|
|
490
|
+
const linked = link.inwardIssue ?? link.outwardIssue;
|
|
491
|
+
if (linked?.fields?.issuetype?.name !== 'Library') continue;
|
|
492
|
+
|
|
493
|
+
const libFields = await deps.jira.getIssueFields(linked.key, ['customfield_13702', 'customfield_13431']).catch(() => ({}));
|
|
494
|
+
const childId = libFields?.customfield_13702?.child?.id;
|
|
495
|
+
const module = childIdToName[childId] ?? inferModuleFromSummary(systemCode, linked.fields?.summary);
|
|
496
|
+
if (module) {
|
|
497
|
+
libraries.set(module, {
|
|
498
|
+
issueKey: linked.key,
|
|
499
|
+
module,
|
|
500
|
+
gitBranch: libFields?.customfield_13431,
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
return libraries;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function inferModuleFromSummary(systemCode, summary = '') {
|
|
509
|
+
const modules = SYSTEM_MODULES[systemCode] ?? [];
|
|
510
|
+
const lower = String(summary).toLowerCase();
|
|
511
|
+
return modules.find((module) => lower.includes(module.toLowerCase())) ?? null;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function resolveMode(args) {
|
|
515
|
+
if (args.mode) return args.mode;
|
|
516
|
+
const modules = normalizeModuleSpecs(args.modules);
|
|
517
|
+
if (args.oldCiIssueKey || modules.some((item) => item.rerun || !item.gitBranch)) return 'rerun_release';
|
|
518
|
+
if (modules.length > 0) return 'explicit_modules';
|
|
519
|
+
return 'current_release';
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function normalizeModuleSpecs(modules) {
|
|
523
|
+
if (!Array.isArray(modules)) return [];
|
|
524
|
+
return modules
|
|
525
|
+
.map((item) => typeof item === 'string' ? { module: item } : item)
|
|
526
|
+
.filter(Boolean)
|
|
527
|
+
.map((item) => ({
|
|
528
|
+
module: String(item.module ?? '').trim().toLowerCase(),
|
|
529
|
+
gitBranch: item.gitBranch,
|
|
530
|
+
rerun: item.rerun === true,
|
|
531
|
+
}))
|
|
532
|
+
.filter((item) => item.module);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function normalizeSystemCode(systemCode) {
|
|
536
|
+
return String(systemCode ?? '').trim().toUpperCase();
|
|
259
537
|
}
|