@jira-deploy/core 1.0.15 → 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.
@@ -1,53 +1,73 @@
1
- import {getClusterList, ok, error} from './helpers.js';
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: 'run_stg_full_release',
7
+ name: 'run_release_to_stg',
7
8
  description:
8
- '執行標準 STG 全流程:建立 CI 單、Build、Wait To STG、建立 CD 單、Prepare CD Deployment,最後 trigger deployment Apply for close。',
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: ['systemCode'],
12
+ required: [],
12
13
  properties: {
13
14
  systemCode: {
14
15
  type: 'string',
15
- description: '系統代碼,例如 IBK、CWA、EIB、EVT、NPM、BOF',
16
+ description: '系統代碼,例如 IBK、CWA。上這次 release 或重上 release 時建議提供;未提供時會要求使用者確認 systemCode。',
16
17
  },
17
- environment: {
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
- enum: ['stg'],
20
- default: 'stg',
21
- description: '固定為 stg;保留此參數讓自然語言 planner 可以映射使用者提到的 STG。',
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: 'run_lib_to_stg_release',
48
+ name: 'continue_release_to_cd_ready',
28
49
  description:
29
- '執行 LibrarySTG 的完整流程:建立 Library 單、Build library、等待 library build 狀態、建立關聯 CI、Build CI、Wait To STG、建立 CD、Prepare CD Deployment,最後 trigger deployment Apply for close。',
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: ['systemCode', 'gitBranch'],
53
+ required: ['ciIssueKey', 'environment'],
33
54
  properties: {
34
- systemCode: {
55
+ ciIssueKey: {
35
56
  type: 'string',
36
- description: '系統代碼,例如 IBK、CWA、EIB、EVT、NPM、BOF',
57
+ description: '作為 continuation 起點的 CI 單 issue key,例如 CID-1234。找不到 CI 單時 planner 必須先詢問使用者,不可猜測。',
37
58
  },
38
- module: {
59
+ environment: {
39
60
  type: 'string',
40
- description: 'Library 模組;未提供時預設使用 systemCode 小寫,例如 IBK -> ibk。',
61
+ enum: ['uat', 'prd/dr', 'prd', 'dr'],
62
+ description: '要繼續開立 CD/Deployment 的目標環境。prd 或 dr 會正規化為 prd/dr。',
41
63
  },
42
- gitBranch: {
64
+ systemCode: {
43
65
  type: 'string',
44
- description: 'Library release/hotfix/feature branch,例如 release/v1.5.2.0。',
66
+ description: '系統代碼,例如 IBK、CWA。未提供時會嘗試從 CI 單 CID_system_code 推導;推不到會要求使用者補充。',
45
67
  },
46
- environment: {
47
- type: 'string',
48
- enum: ['stg'],
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 handleRunStgFullReleaseWorkflow(args, deps) {
143
+ export async function handleRunReleaseToStgWorkflow(args, deps) {
126
144
  try {
127
145
  const workflowLog = [];
128
- const systemCode = String(args.systemCode ?? '').trim().toUpperCase();
129
- const ciArgs = {systemCode, environment: 'stg'};
130
- const ci = await runToolOrThrow('create_ci_ticket', ciArgs, deps, workflowLog);
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 build = await runToolOrThrow('build_ticket', {issueKey: ci.issueKey}, deps, workflowLog);
133
- const toStg = await runToolOrThrow('wait_to_stg', {issueKey: ci.issueKey}, deps, workflowLog);
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 clusters = getClusterList(systemCode, 'stg');
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
- const cdArgs = {
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: systemCode.toLowerCase(),
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', applyForClose: true},
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: 'STG Full Release',
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
- assumptions: [`CD cluster 自動帶入 ${systemCode} STG 全部 cluster`],
226
+ deploymentResult: deployed.deployResult,
227
+ stoppedAt: 'Wait To UAT',
228
+ assumptions,
170
229
  workflowLog,
171
230
  });
172
231
  } catch (err) {
173
- return error(`run_stg_full_release 失敗: ${err.message}`);
232
+ return error(`run_release_to_stg 失敗: ${err.message}`);
174
233
  }
175
234
  }
176
235
 
177
- export async function handleRunLibToStgReleaseWorkflow(args, deps) {
236
+ export async function handleContinueReleaseToCdReadyWorkflow(args, deps) {
178
237
  try {
179
238
  const workflowLog = [];
180
- const assumptions = [];
181
- const systemCode = String(args.systemCode ?? '').trim().toUpperCase();
182
- const module = args.module || systemCode.toLowerCase();
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
- if (!args.module) {
185
- assumptions.push(`未指定 module,已依 systemCode 帶入 ${module}`);
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 libArgs = {
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
- module,
191
- gitBranch: args.gitBranch,
192
- environment: 'stg',
193
- jenkinsBranch: 'master',
194
- };
195
- const lib = await runToolOrThrow('create_library_ticket', libArgs, deps, workflowLog);
196
- await runToolOrThrow('build_ticket', {issueKey: lib.issueKey}, deps, workflowLog);
197
- const libFinalStatus = await waitForIssueStatus(
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
- const ciArgs = {
291
+ return ok(buildContinuationResult({
292
+ ciIssueKey,
293
+ environment,
206
294
  systemCode,
207
- environment: 'stg',
208
- relatesTo: lib.issueKey,
209
- };
210
- const ci = await runToolOrThrow('create_ci_ticket', ciArgs, deps, workflowLog);
211
- const build = await runToolOrThrow('build_ticket', {issueKey: ci.issueKey}, deps, workflowLog);
212
- const toStg = await runToolOrThrow('wait_to_stg', {issueKey: ci.issueKey}, deps, workflowLog);
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
- const clusters = getClusterList(systemCode, 'stg');
215
- if (!clusters.length) {
216
- throw new Error(`找不到 ${systemCode} STG 的 cluster 設定`);
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
- assumptions.push(`CD cluster 自動帶入 ${systemCode} STG 全部 cluster`);
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
- const cdArgs = {
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
- linkedCiKey: ci.issueKey,
224
- clusterDeploy: clusters.join(','),
225
- moduleChild: systemCode.toLowerCase(),
226
- };
227
- const cd = await runToolOrThrow('create_cd_ticket', cdArgs, deps, workflowLog);
228
- const prepared = await runToolOrThrow(
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
- workflowLog,
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
- return ok({
242
- type: 'Library To STG Full Release',
243
- systemCode,
244
- module,
245
- gitBranch: args.gitBranch,
246
- libraryIssueKey: lib.issueKey,
247
- libraryStatus: libFinalStatus,
248
- ciIssueKey: ci.issueKey,
249
- ciStatus: toStg.status ?? build.status,
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
  }