@jira-deploy/core 1.0.15 → 1.0.17

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