@jira-deploy/core 1.0.3 → 1.0.5
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 +1 -1
- package/constants/field-ids.js +4 -4
- package/package.json +1 -1
- package/poller.js +16 -3
- package/tools/cd.js +6 -4
- package/tools/grayrelease.js +553 -302
- package/tools/index.js +173 -108
- package/tools/release.js +48 -21
- package/tools/workflows.js +20 -0
- package/tools.test.js +444 -97
package/tools/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {Poller} from '../poller.js';
|
|
1
|
+
import { Poller } from '../poller.js';
|
|
2
2
|
import {
|
|
3
3
|
getLibraryToolDefinitions,
|
|
4
4
|
handleCreateLibraryTicket,
|
|
5
5
|
handleGetNextLibVersion,
|
|
6
6
|
} from './library.js';
|
|
7
|
-
import {getCIToolDefinitions, handleCreateCITicket, handleGetNextCIVersion} from './ci.js';
|
|
7
|
+
import { getCIToolDefinitions, handleCreateCITicket, handleGetNextCIVersion } from './ci.js';
|
|
8
8
|
import {
|
|
9
9
|
SYSTEM_CODES,
|
|
10
10
|
ENV_CODES,
|
|
@@ -12,12 +12,13 @@ import {
|
|
|
12
12
|
SUPPORTED_ENVS,
|
|
13
13
|
resolveAccountId,
|
|
14
14
|
} from '../constants/index.js';
|
|
15
|
-
import {getCDToolDefinitions, handleCreateCDTicket} from './cd.js';
|
|
15
|
+
import { getCDToolDefinitions, handleCreateCDTicket } from './cd.js';
|
|
16
16
|
import {
|
|
17
17
|
getGrayReleaseToolDefinitions,
|
|
18
18
|
handleCreateGrayReleaseTicket,
|
|
19
19
|
handleLinkStgGrayRelease,
|
|
20
20
|
handleAutoGrayRelease,
|
|
21
|
+
handleDeployGrayRelease,
|
|
21
22
|
handleGetGrayReleaseStatus,
|
|
22
23
|
handleContinueGrayRelease,
|
|
23
24
|
} from './grayrelease.js';
|
|
@@ -28,7 +29,7 @@ import {
|
|
|
28
29
|
handleGetReleaseManager,
|
|
29
30
|
handleWaitForComment,
|
|
30
31
|
} from './release.js';
|
|
31
|
-
import {getJabberToolDefinitions, handleSendJabberMessage} from './jabber.js';
|
|
32
|
+
import { getJabberToolDefinitions, handleSendJabberMessage } from './jabber.js';
|
|
32
33
|
import {
|
|
33
34
|
getWorkflowToolDefinitions,
|
|
34
35
|
handleRunLibToStgReleaseWorkflow,
|
|
@@ -88,7 +89,7 @@ export function getToolDefinitions() {
|
|
|
88
89
|
type: 'object',
|
|
89
90
|
required: ['issueKey', 'transitionName'],
|
|
90
91
|
properties: {
|
|
91
|
-
issueKey: {type: 'string', description: '例如 OPS-123'},
|
|
92
|
+
issueKey: { type: 'string', description: '例如 OPS-123' },
|
|
92
93
|
transitionName: {
|
|
93
94
|
type: 'string',
|
|
94
95
|
description: '狀態名稱,例如 "Pending Approval"、"Approved"、"In Progress"、"Done"',
|
|
@@ -104,7 +105,7 @@ export function getToolDefinitions() {
|
|
|
104
105
|
type: 'object',
|
|
105
106
|
required: ['issueKey'],
|
|
106
107
|
properties: {
|
|
107
|
-
issueKey: {type: 'string'},
|
|
108
|
+
issueKey: { type: 'string' },
|
|
108
109
|
targetStatus: {
|
|
109
110
|
type: 'string',
|
|
110
111
|
default: 'Approved',
|
|
@@ -128,7 +129,7 @@ export function getToolDefinitions() {
|
|
|
128
129
|
type: 'object',
|
|
129
130
|
required: ['issueKey'],
|
|
130
131
|
properties: {
|
|
131
|
-
issueKey: {type: 'string'},
|
|
132
|
+
issueKey: { type: 'string' },
|
|
132
133
|
},
|
|
133
134
|
},
|
|
134
135
|
},
|
|
@@ -139,8 +140,8 @@ export function getToolDefinitions() {
|
|
|
139
140
|
type: 'object',
|
|
140
141
|
required: ['issueKey', 'message'],
|
|
141
142
|
properties: {
|
|
142
|
-
issueKey: {type: 'string'},
|
|
143
|
-
message: {type: 'string', description: '留言內容'},
|
|
143
|
+
issueKey: { type: 'string' },
|
|
144
|
+
message: { type: 'string', description: '留言內容' },
|
|
144
145
|
},
|
|
145
146
|
},
|
|
146
147
|
},
|
|
@@ -151,7 +152,7 @@ export function getToolDefinitions() {
|
|
|
151
152
|
type: 'object',
|
|
152
153
|
required: ['issueKey'],
|
|
153
154
|
properties: {
|
|
154
|
-
issueKey: {type: 'string'},
|
|
155
|
+
issueKey: { type: 'string' },
|
|
155
156
|
},
|
|
156
157
|
},
|
|
157
158
|
},
|
|
@@ -163,8 +164,8 @@ export function getToolDefinitions() {
|
|
|
163
164
|
type: 'object',
|
|
164
165
|
required: ['inwardKey', 'outwardKey'],
|
|
165
166
|
properties: {
|
|
166
|
-
inwardKey: {type: 'string', description: '被包含方 issue key,例如 CD 單 CID-1669'},
|
|
167
|
-
outwardKey: {type: 'string', description: '包含方 issue key,例如 CI 單 CID-1668'},
|
|
167
|
+
inwardKey: { type: 'string', description: '被包含方 issue key,例如 CD 單 CID-1669' },
|
|
168
|
+
outwardKey: { type: 'string', description: '包含方 issue key,例如 CI 單 CID-1668' },
|
|
168
169
|
linkType: {
|
|
169
170
|
type: 'string',
|
|
170
171
|
description:
|
|
@@ -177,14 +178,18 @@ export function getToolDefinitions() {
|
|
|
177
178
|
{
|
|
178
179
|
name: 'build_ticket',
|
|
179
180
|
description:
|
|
180
|
-
'觸發 Library 或
|
|
181
|
+
'觸發 Library、CI 或 GrayRelease 上版單的 Jenkins Build。自動處理前置狀態切換與 Jira Automation 等待,不需要手動切換狀態。GrayRelease 僅在使用者明確要求 build/rebuild 時使用此 tool;若使用者要求 GrayRelease deploy、dev deploy 或部署灰度單,必須改用 deploy_grayrelease,不可用此 tool 觸發 rebuild。',
|
|
181
182
|
inputSchema: {
|
|
182
183
|
type: 'object',
|
|
183
184
|
required: ['issueKey'],
|
|
184
185
|
properties: {
|
|
185
186
|
issueKey: {
|
|
186
187
|
type: 'string',
|
|
187
|
-
description: '要 build 的 issue key,例如 CID-1668',
|
|
188
|
+
description: '要 build 的 issue key,例如 CI/Library 單 CID-1668,或可重複 rebuild 的 GrayRelease 單 CID-822',
|
|
189
|
+
},
|
|
190
|
+
rebuild: {
|
|
191
|
+
type: 'boolean',
|
|
192
|
+
description: '(選填) 只有明確要重 build GrayRelease VERIFY 單時才設定 rebuild=true。GrayRelease deploy 請用 deploy_grayrelease。',
|
|
188
193
|
},
|
|
189
194
|
},
|
|
190
195
|
},
|
|
@@ -327,7 +332,8 @@ export function getToolDefinitions() {
|
|
|
327
332
|
* 執行 tool,回傳 { content: [{ type: 'text', text }] }
|
|
328
333
|
*/
|
|
329
334
|
export async function executeTool(name, args, deps) {
|
|
330
|
-
const {jira, notifier} = deps;
|
|
335
|
+
const { jira, notifier } = deps;
|
|
336
|
+
const progress = typeof deps.progress === 'function' ? deps.progress : () => {};
|
|
331
337
|
const poller = new Poller(jira);
|
|
332
338
|
|
|
333
339
|
switch (name) {
|
|
@@ -349,6 +355,7 @@ export async function executeTool(name, args, deps) {
|
|
|
349
355
|
const result = await poller.waitForStatus(args.issueKey, targetStatus, {
|
|
350
356
|
intervalMs: args.pollIntervalMs,
|
|
351
357
|
timeoutMs: args.timeoutMs,
|
|
358
|
+
onProgress: progress,
|
|
352
359
|
});
|
|
353
360
|
|
|
354
361
|
await notifier.notify(
|
|
@@ -370,52 +377,55 @@ export async function executeTool(name, args, deps) {
|
|
|
370
377
|
|
|
371
378
|
case 'add_deploy_comment': {
|
|
372
379
|
await notifier.notify(args.issueKey, args.message);
|
|
373
|
-
return ok({issueKey: args.issueKey, commented: true});
|
|
380
|
+
return ok({ issueKey: args.issueKey, commented: true });
|
|
374
381
|
}
|
|
375
382
|
|
|
376
383
|
case 'list_transitions': {
|
|
377
384
|
const transitions = await jira.getTransitions(args.issueKey);
|
|
378
|
-
return ok(transitions.map((t) => ({id: t.id, name: t.name, to: t.to.name})));
|
|
385
|
+
return ok(transitions.map((t) => ({ id: t.id, name: t.name, to: t.to.name })));
|
|
379
386
|
}
|
|
380
387
|
|
|
381
388
|
case 'create_library_ticket':
|
|
382
|
-
return handleCreateLibraryTicket(args, {jira, notifier});
|
|
389
|
+
return handleCreateLibraryTicket(args, { jira, notifier });
|
|
383
390
|
|
|
384
391
|
case 'create_ci_ticket': {
|
|
385
|
-
return handleCreateCITicket(args, {jira, notifier});
|
|
392
|
+
return handleCreateCITicket(args, { jira, notifier });
|
|
386
393
|
}
|
|
387
394
|
|
|
388
395
|
case 'create_cd_ticket': {
|
|
389
|
-
return handleCreateCDTicket(args, {jira, notifier});
|
|
396
|
+
return handleCreateCDTicket(args, { jira, notifier });
|
|
390
397
|
}
|
|
391
398
|
|
|
392
399
|
case 'create_grayrelease_ticket': {
|
|
393
|
-
return handleCreateGrayReleaseTicket(args, {jira, notifier});
|
|
400
|
+
return handleCreateGrayReleaseTicket(args, { jira, notifier, progress });
|
|
394
401
|
}
|
|
395
402
|
|
|
396
403
|
case 'link_stg_grayrelease':
|
|
397
|
-
return handleLinkStgGrayRelease(args, {jira, notifier});
|
|
404
|
+
return handleLinkStgGrayRelease(args, { jira, notifier, progress });
|
|
398
405
|
|
|
399
406
|
case 'auto_grayrelease':
|
|
400
|
-
return handleAutoGrayRelease(args, {jira, notifier});
|
|
407
|
+
return handleAutoGrayRelease(args, { jira, notifier, progress });
|
|
408
|
+
|
|
409
|
+
case 'deploy_grayrelease':
|
|
410
|
+
return handleDeployGrayRelease(args, { jira, notifier, progress });
|
|
401
411
|
|
|
402
412
|
case 'get_grayrelease_status':
|
|
403
|
-
return handleGetGrayReleaseStatus(args, {jira});
|
|
413
|
+
return handleGetGrayReleaseStatus(args, { jira });
|
|
404
414
|
|
|
405
415
|
case 'continue_grayrelease':
|
|
406
|
-
return handleContinueGrayRelease(args, {jira, notifier});
|
|
416
|
+
return handleContinueGrayRelease(args, { jira, notifier, progress });
|
|
407
417
|
|
|
408
418
|
case 'get_unreleased_versions':
|
|
409
|
-
return handleGetUnreleasedVersions(args, {jira});
|
|
419
|
+
return handleGetUnreleasedVersions(args, { jira });
|
|
410
420
|
|
|
411
421
|
case 'transition_to_wait_approval':
|
|
412
|
-
return handleTransitionToWaitApproval(args, {jira, notifier});
|
|
422
|
+
return handleTransitionToWaitApproval(args, { jira, notifier });
|
|
413
423
|
|
|
414
424
|
case 'get_release_manager':
|
|
415
425
|
return handleGetReleaseManager(args, {});
|
|
416
426
|
|
|
417
427
|
case 'wait_for_comment':
|
|
418
|
-
return handleWaitForComment(args, {jira});
|
|
428
|
+
return handleWaitForComment(args, { jira, progress });
|
|
419
429
|
|
|
420
430
|
case 'send_jabber_message':
|
|
421
431
|
return handleSendJabberMessage(args, {});
|
|
@@ -426,6 +436,7 @@ export async function executeTool(name, args, deps) {
|
|
|
426
436
|
notifier,
|
|
427
437
|
executeToolImpl: deps.executeToolImpl ?? executeTool,
|
|
428
438
|
workflowWaitOptions: deps.workflowWaitOptions,
|
|
439
|
+
progress,
|
|
429
440
|
});
|
|
430
441
|
|
|
431
442
|
case 'run_lib_to_stg_release':
|
|
@@ -434,6 +445,7 @@ export async function executeTool(name, args, deps) {
|
|
|
434
445
|
notifier,
|
|
435
446
|
executeToolImpl: deps.executeToolImpl ?? executeTool,
|
|
436
447
|
workflowWaitOptions: deps.workflowWaitOptions,
|
|
448
|
+
progress,
|
|
437
449
|
});
|
|
438
450
|
|
|
439
451
|
case 'link_issues': {
|
|
@@ -448,7 +460,7 @@ export async function executeTool(name, args, deps) {
|
|
|
448
460
|
}
|
|
449
461
|
|
|
450
462
|
case 'build_ticket': {
|
|
451
|
-
const {issueKey} = args;
|
|
463
|
+
const { issueKey, rebuild = false } = args;
|
|
452
464
|
const log = [];
|
|
453
465
|
|
|
454
466
|
/**
|
|
@@ -463,11 +475,21 @@ export async function executeTool(name, args, deps) {
|
|
|
463
475
|
* Library Release 完整狀態流程:
|
|
464
476
|
* TO DO → (Upload Lib Report) → UPLOAD LIB REPORT → (Apply for approval) → Wait Approval → (Approved) → WAIT FOR LIB BUILD → (Build) → Released
|
|
465
477
|
* 或:TO DO → (Upload Lib Report) → UPLOAD LIB REPORT → Jira Automation 自動觸發 Jenkins(無手動 Build)
|
|
478
|
+
*
|
|
479
|
+
* GrayRelease build/rebuild:
|
|
480
|
+
* - 一般 build:PLANNING → Accept → WAIT FOR BUILD → GrayRelease Build
|
|
481
|
+
* - VERIFY rebuild:只有 args.rebuild=true 時,才允許 Verify fail 回到 PLANNING 後重 build
|
|
482
|
+
* - GrayRelease deploy 已拆到 deploy_grayrelease,這裡不可處理部署流程
|
|
466
483
|
*/
|
|
467
484
|
|
|
468
|
-
|
|
469
|
-
// 'Upload Lib Report':Library 票從 TO DO 的第一步(對應 UI 的 Upload Library Report)
|
|
485
|
+
const BUILD_TRANSITIONS = ['Build', 'GrayRelease Build'];
|
|
470
486
|
const PRE_TRANSITIONS = ['Upload Lib Report', 'Apply for approval', 'Approved', 'Accept'];
|
|
487
|
+
const GRAYRELEASE_REBUILD_TRANSITIONS = [
|
|
488
|
+
'Verify fail',
|
|
489
|
+
'Back to Planning',
|
|
490
|
+
'To Planning',
|
|
491
|
+
'Planning',
|
|
492
|
+
];
|
|
471
493
|
const POLL_INTERVAL = 3000; // 3s
|
|
472
494
|
const MAX_WAIT_MS = 30000; // 最多等 30s(避免 MCP client timeout)
|
|
473
495
|
|
|
@@ -476,6 +498,11 @@ export async function executeTool(name, args, deps) {
|
|
|
476
498
|
return list.find((t) => t.name.toLowerCase() === name.toLowerCase());
|
|
477
499
|
};
|
|
478
500
|
|
|
501
|
+
const findAnyTransition = async (names) => {
|
|
502
|
+
const list = await jira.getTransitions(issueKey);
|
|
503
|
+
return list.find((t) => names.some((name) => t.name.toLowerCase() === name.toLowerCase()));
|
|
504
|
+
};
|
|
505
|
+
|
|
479
506
|
const waitForTransition = async (name) => {
|
|
480
507
|
const deadline = Date.now() + MAX_WAIT_MS;
|
|
481
508
|
while (Date.now() < deadline) {
|
|
@@ -486,17 +513,58 @@ export async function executeTool(name, args, deps) {
|
|
|
486
513
|
return null;
|
|
487
514
|
};
|
|
488
515
|
|
|
516
|
+
const waitForAnyTransition = async (names) => {
|
|
517
|
+
const deadline = Date.now() + MAX_WAIT_MS;
|
|
518
|
+
while (Date.now() < deadline) {
|
|
519
|
+
const t = await findAnyTransition(names);
|
|
520
|
+
if (t) return t;
|
|
521
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL));
|
|
522
|
+
}
|
|
523
|
+
return null;
|
|
524
|
+
};
|
|
525
|
+
|
|
489
526
|
try {
|
|
490
527
|
// 記錄初始狀態(用於判斷是否有狀態推進)
|
|
491
528
|
const initIssue = await jira.getIssue(issueKey);
|
|
492
529
|
const initStatus = initIssue.fields.status.name;
|
|
493
530
|
|
|
494
|
-
// Step 1: 確認目前是否已有 Build transition
|
|
495
|
-
let buildTrans = await
|
|
531
|
+
// Step 1: 確認目前是否已有 Build transition(GrayRelease 名稱為 GrayRelease Build)
|
|
532
|
+
let buildTrans = await findAnyTransition(BUILD_TRANSITIONS);
|
|
533
|
+
|
|
534
|
+
const normalizedInitStatus = initStatus.trim().toUpperCase();
|
|
535
|
+
|
|
536
|
+
// GrayRelease 特例:VERIFY 狀態只有明確 rebuild=true 才允許回到可 build 流程。
|
|
537
|
+
if (!buildTrans && initStatus.toLowerCase() === 'verify') {
|
|
538
|
+
const resetTrans = await findAnyTransition(GRAYRELEASE_REBUILD_TRANSITIONS);
|
|
539
|
+
if (resetTrans && !rebuild) {
|
|
540
|
+
return error(
|
|
541
|
+
'目前為 VERIFY,build_ticket 不會自動重 build GrayRelease。若要部署請使用 deploy_grayrelease;若要重 build 請呼叫 build_ticket 並設定 rebuild=true。',
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
if (resetTrans) {
|
|
545
|
+
log.push(`目前為 VERIFY,GrayRelease rebuild 先觸發「${resetTrans.name}」回到可 build 流程...`);
|
|
546
|
+
await jira.transitionById(issueKey, resetTrans.id);
|
|
547
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
548
|
+
buildTrans = await findAnyTransition(BUILD_TRANSITIONS);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
if (!buildTrans && ['WAIT APPROVAL', 'WAIT DEPLOY'].includes(normalizedInitStatus)) {
|
|
553
|
+
const transitions = await jira.getTransitions(issueKey);
|
|
554
|
+
const grayDeployTransition = transitions.find((t) => (
|
|
555
|
+
['approve', 'grayrelease deploy', 'to verify', 'switch execution node']
|
|
556
|
+
.includes(t.name.toLowerCase())
|
|
557
|
+
));
|
|
558
|
+
if (grayDeployTransition) {
|
|
559
|
+
return error(
|
|
560
|
+
`目前狀態 ${initStatus} 屬於 GrayRelease deploy 流程,請使用 deploy_grayrelease,不要用 build_ticket。`,
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
496
564
|
|
|
497
565
|
// Step 2: 若沒有,逐步觸發前置 transitions
|
|
498
566
|
if (!buildTrans) {
|
|
499
|
-
log.push('未找到 Build transition,逐步觸發前置狀態...');
|
|
567
|
+
log.push('未找到 Build/GrayRelease Build transition,逐步觸發前置狀態...');
|
|
500
568
|
let preTriggered = false;
|
|
501
569
|
|
|
502
570
|
// 最多嘗試 PRE_TRANSITIONS.length 輪,每輪觸發一個可用的前置 transition
|
|
@@ -514,9 +582,9 @@ export async function executeTool(name, args, deps) {
|
|
|
514
582
|
// 等 Jira 更新狀態
|
|
515
583
|
await new Promise((r) => setTimeout(r, 2000));
|
|
516
584
|
|
|
517
|
-
buildTrans = await
|
|
585
|
+
buildTrans = await findAnyTransition(BUILD_TRANSITIONS);
|
|
518
586
|
if (buildTrans) {
|
|
519
|
-
log.push(` 已找到
|
|
587
|
+
log.push(` 已找到 ${buildTrans.name} transition`);
|
|
520
588
|
break;
|
|
521
589
|
}
|
|
522
590
|
}
|
|
@@ -524,7 +592,7 @@ export async function executeTool(name, args, deps) {
|
|
|
524
592
|
// 若還沒出現,再給 Jira Automation 最多 30s
|
|
525
593
|
if (!buildTrans) {
|
|
526
594
|
log.push(' 等待 Jira Automation 推進(最多 30s)...');
|
|
527
|
-
buildTrans = await
|
|
595
|
+
buildTrans = await waitForAnyTransition(BUILD_TRANSITIONS);
|
|
528
596
|
}
|
|
529
597
|
|
|
530
598
|
// 若仍未找到 Build transition,但有觸發前置 transition 且狀態已推進,
|
|
@@ -541,38 +609,38 @@ export async function executeTool(name, args, deps) {
|
|
|
541
609
|
issueKey,
|
|
542
610
|
`Library Build 已觸發(Jira Auto)狀態:${initStatus} → ${currentStatus}`,
|
|
543
611
|
);
|
|
544
|
-
return ok({issueKey, status: currentStatus, steps: log, autoTriggered: true});
|
|
612
|
+
return ok({ issueKey, status: currentStatus, steps: log, autoTriggered: true });
|
|
545
613
|
}
|
|
546
614
|
}
|
|
547
615
|
}
|
|
548
616
|
|
|
549
617
|
if (!buildTrans) {
|
|
550
618
|
const issue = await jira.getIssue(issueKey);
|
|
551
|
-
return error(`找不到 Build transition,目前狀態:${issue.fields.status.name}`);
|
|
619
|
+
return error(`找不到 Build/GrayRelease Build transition,目前狀態:${issue.fields.status.name}`);
|
|
552
620
|
}
|
|
553
621
|
|
|
554
622
|
// Step 4: 執行 Build
|
|
555
|
-
log.push(`執行
|
|
623
|
+
log.push(`執行 ${buildTrans.name} transition (id: ${buildTrans.id})...`);
|
|
556
624
|
await jira.transitionById(issueKey, buildTrans.id);
|
|
557
625
|
|
|
558
626
|
const issue = await jira.getIssue(issueKey);
|
|
559
627
|
const newStatus = issue.fields.status.name;
|
|
560
|
-
log.push(`✅
|
|
561
|
-
await notifier.notify(issueKey, `Jenkins
|
|
628
|
+
log.push(`✅ ${buildTrans.name} 已觸發,目前狀態:${newStatus}`);
|
|
629
|
+
await notifier.notify(issueKey, `Jenkins ${buildTrans.name} 已觸發(${newStatus})`);
|
|
562
630
|
|
|
563
|
-
return ok({issueKey, status: newStatus, steps: log});
|
|
631
|
+
return ok({ issueKey, status: newStatus, steps: log });
|
|
564
632
|
} catch (err) {
|
|
565
633
|
return error(`build_ticket 失敗: ${err.message}`);
|
|
566
634
|
}
|
|
567
635
|
}
|
|
568
636
|
|
|
569
637
|
case 'wait_to_dev': {
|
|
570
|
-
const {issueKey} = args;
|
|
638
|
+
const { issueKey } = args;
|
|
571
639
|
const log = [];
|
|
572
640
|
|
|
573
641
|
const STEPS = [
|
|
574
|
-
{transition: 'Upload Scan Report', targetStatus: 'Upload Report'},
|
|
575
|
-
{transition: 'Accept', targetStatus: 'Wait To DEV'},
|
|
642
|
+
{ transition: 'Upload Scan Report', targetStatus: 'Upload Report' },
|
|
643
|
+
{ transition: 'Accept', targetStatus: 'Wait To DEV' },
|
|
576
644
|
];
|
|
577
645
|
const finalTargetStatus = STEPS.at(-1).targetStatus;
|
|
578
646
|
|
|
@@ -602,22 +670,22 @@ export async function executeTool(name, args, deps) {
|
|
|
602
670
|
log.push(`✅ 完成,目前狀態:${finalStatus}`);
|
|
603
671
|
await notifier.notify(issueKey, `已切換至 ${finalStatus},可進行 DEV 部署`);
|
|
604
672
|
|
|
605
|
-
return ok({issueKey, status: finalStatus, steps: log});
|
|
673
|
+
return ok({ issueKey, status: finalStatus, steps: log });
|
|
606
674
|
} catch (err) {
|
|
607
675
|
return error(`wait_to_dev 失敗: ${err.message}`);
|
|
608
676
|
}
|
|
609
677
|
}
|
|
610
678
|
|
|
611
679
|
case 'wait_to_stg': {
|
|
612
|
-
const {issueKey} = args;
|
|
680
|
+
const { issueKey } = args;
|
|
613
681
|
const log = [];
|
|
614
682
|
|
|
615
683
|
// CI 掃描 → STG 的標準流程步驟
|
|
616
684
|
// 每個元素:{ transition: '名稱', targetStatus: '到達後的狀態名稱' }
|
|
617
685
|
const STEPS = [
|
|
618
|
-
{transition: 'Upload Scan Report', targetStatus: 'Upload Report'},
|
|
619
|
-
{transition: 'Accept', targetStatus: 'Wait To DEV'},
|
|
620
|
-
{transition: 'Dev Done', targetStatus: 'Wait To STG'},
|
|
686
|
+
{ transition: 'Upload Scan Report', targetStatus: 'Upload Report' },
|
|
687
|
+
{ transition: 'Accept', targetStatus: 'Wait To DEV' },
|
|
688
|
+
{ transition: 'Dev Done', targetStatus: 'Wait To STG' },
|
|
621
689
|
];
|
|
622
690
|
|
|
623
691
|
try {
|
|
@@ -643,14 +711,14 @@ export async function executeTool(name, args, deps) {
|
|
|
643
711
|
log.push(`✅ 完成,目前狀態:${finalStatus}`);
|
|
644
712
|
await notifier.notify(issueKey, `已切換至 ${finalStatus},可進行 STG 部署`);
|
|
645
713
|
|
|
646
|
-
return ok({issueKey, status: finalStatus, steps: log});
|
|
714
|
+
return ok({ issueKey, status: finalStatus, steps: log });
|
|
647
715
|
} catch (err) {
|
|
648
716
|
return error(`wait_to_stg 失敗: ${err.message}`);
|
|
649
717
|
}
|
|
650
718
|
}
|
|
651
719
|
|
|
652
720
|
case 'trigger_deployment': {
|
|
653
|
-
const {cdIssueKey, environment} = args;
|
|
721
|
+
const { cdIssueKey, environment } = args;
|
|
654
722
|
const log = [];
|
|
655
723
|
|
|
656
724
|
// 判斷環境是否屬於 PRD 群組(prd / dr / prd/dr / prd&dr)
|
|
@@ -659,8 +727,8 @@ export async function executeTool(name, args, deps) {
|
|
|
659
727
|
|
|
660
728
|
// 查詢同系統最近一筆已完成的部署(CD 或 GrayRelease),回傳 'prd' | 'nonPrd' | null
|
|
661
729
|
const getLastDeployEnvGroup = async (systemCode, excludeKey) => {
|
|
662
|
-
const jql_cd = `project = CID AND issuetype = CD AND
|
|
663
|
-
const jql_gr = `project = CID AND issuetype = GrayRelease AND
|
|
730
|
+
const jql_cd = `project = CID AND issuetype = CD AND text ~ "${systemCode}" AND status = Done AND issueKey != "${excludeKey}" ORDER BY updated DESC`;
|
|
731
|
+
const jql_gr = `project = CID AND issuetype = GrayRelease AND text ~ "${systemCode}" AND status = Done ORDER BY updated DESC`;
|
|
664
732
|
|
|
665
733
|
const [cdResults, grResults] = await Promise.all([
|
|
666
734
|
jira.searchIssues(jql_cd, ['customfield_13436', 'updated'], 1).catch(() => []),
|
|
@@ -705,10 +773,10 @@ export async function executeTool(name, args, deps) {
|
|
|
705
773
|
// 依序觸發的 Deployment transitions
|
|
706
774
|
// conditionalWait: true → 依 needSwitch 決定是否執行並等待 180s
|
|
707
775
|
const DEPLOY_TRANSITIONS = [
|
|
708
|
-
{name: 'To Pretask'},
|
|
709
|
-
{name: 'Swtich Execution Node', conditionalWait: true},
|
|
710
|
-
{name: 'To AutoDeploy'},
|
|
711
|
-
{name: 'Trigger AutoDeploy'},
|
|
776
|
+
{ name: 'To Pretask' },
|
|
777
|
+
{ name: 'Swtich Execution Node', conditionalWait: true },
|
|
778
|
+
{ name: 'To AutoDeploy' },
|
|
779
|
+
{ name: 'Trigger AutoDeploy' },
|
|
712
780
|
];
|
|
713
781
|
// 環境字串對照(用於 sub-task summary 比對)
|
|
714
782
|
const envUpper = environment.toUpperCase().replace('/', '').replace('&', '');
|
|
@@ -854,7 +922,7 @@ export async function executeTool(name, args, deps) {
|
|
|
854
922
|
}
|
|
855
923
|
|
|
856
924
|
case 'prepare_cd_deployment': {
|
|
857
|
-
const {issueKey} = args;
|
|
925
|
+
const { issueKey } = args;
|
|
858
926
|
// 正規化環境名稱(prd&dr → prd/dr)
|
|
859
927
|
const envCode = args.environment.toLowerCase().replace('&', '/');
|
|
860
928
|
const log = [];
|
|
@@ -901,7 +969,7 @@ export async function executeTool(name, args, deps) {
|
|
|
901
969
|
// Step 2: 更新 CD 單的環境欄位(customfield_13436)
|
|
902
970
|
if (CD_FIELD_IDS.env && ENV_CODES[envCode]) {
|
|
903
971
|
await jira.updateIssue(issueKey, {
|
|
904
|
-
[CD_FIELD_IDS.env]: {id: ENV_CODES[envCode]},
|
|
972
|
+
[CD_FIELD_IDS.env]: { id: ENV_CODES[envCode] },
|
|
905
973
|
});
|
|
906
974
|
log.push(`✅ 環境欄位更新為 ${envCode.toUpperCase()}`);
|
|
907
975
|
}
|
|
@@ -1047,8 +1115,8 @@ export async function executeTool(name, args, deps) {
|
|
|
1047
1115
|
}
|
|
1048
1116
|
|
|
1049
1117
|
case 'update_assignee': {
|
|
1050
|
-
const {issueKey} = args;
|
|
1051
|
-
let {accountId, displayName} = args;
|
|
1118
|
+
const { issueKey } = args;
|
|
1119
|
+
let { accountId, displayName } = args;
|
|
1052
1120
|
|
|
1053
1121
|
// 若只提供 displayName(或名稱),嘗試從 USER_MAP 查找 accountId
|
|
1054
1122
|
if (!accountId && displayName) {
|
|
@@ -1069,7 +1137,7 @@ export async function executeTool(name, args, deps) {
|
|
|
1069
1137
|
await jira.updateAssignee(issueKey, accountId);
|
|
1070
1138
|
return ok({
|
|
1071
1139
|
issueKey,
|
|
1072
|
-
assignee: {accountId, displayName: displayName ?? accountId},
|
|
1140
|
+
assignee: { accountId, displayName: displayName ?? accountId },
|
|
1073
1141
|
message: `✅ ${issueKey} Assignee 已更新為 ${displayName ? `${displayName}(${accountId})` : accountId}`,
|
|
1074
1142
|
});
|
|
1075
1143
|
} catch (err) {
|
|
@@ -1078,7 +1146,7 @@ export async function executeTool(name, args, deps) {
|
|
|
1078
1146
|
}
|
|
1079
1147
|
|
|
1080
1148
|
case 'cancel_release': {
|
|
1081
|
-
const {systemCode} = args;
|
|
1149
|
+
const { systemCode } = args;
|
|
1082
1150
|
let ciIssueKey = args.ciIssueKey ?? null;
|
|
1083
1151
|
|
|
1084
1152
|
try {
|
|
@@ -1170,46 +1238,46 @@ export async function executeTool(name, args, deps) {
|
|
|
1170
1238
|
}
|
|
1171
1239
|
|
|
1172
1240
|
case 'get_release_status': {
|
|
1173
|
-
const {systemCode} = args;
|
|
1241
|
+
const { systemCode } = args;
|
|
1174
1242
|
|
|
1175
1243
|
// 狀態 → 說明 + 建議下一步
|
|
1176
1244
|
const describeStatus = (issueType, status) => {
|
|
1177
1245
|
const s = status.toLowerCase();
|
|
1178
1246
|
if (issueType === 'Library') {
|
|
1179
|
-
if (s === 'to do') return {emoji: '⏳', desc: '尚未開始', next: `執行 build_ticket`};
|
|
1180
|
-
if (s === 'upload lib report') return {emoji: '⏳', desc: '等待上傳 Lib Report', next: `執行 build_ticket`};
|
|
1181
|
-
if (s === 'wait approval') return {emoji: '⏳', desc: '等待主管核准', next: `wait_for_approval`};
|
|
1182
|
-
if (s === 'wait for lib build') return {emoji: '🔨', desc: '等待 Jenkins Build', next: `執行 build_ticket`};
|
|
1183
|
-
if (s === 'released') return {emoji: '✅', desc: '已完成', next: '可開 CI'};
|
|
1184
|
-
if (s === 'cancelled') return {emoji: '❌', desc: '已取消', next: null};
|
|
1185
|
-
return {emoji: '🔄', desc: status, next: null};
|
|
1247
|
+
if (s === 'to do') return { emoji: '⏳', desc: '尚未開始', next: `執行 build_ticket` };
|
|
1248
|
+
if (s === 'upload lib report') return { emoji: '⏳', desc: '等待上傳 Lib Report', next: `執行 build_ticket` };
|
|
1249
|
+
if (s === 'wait approval') return { emoji: '⏳', desc: '等待主管核准', next: `wait_for_approval` };
|
|
1250
|
+
if (s === 'wait for lib build') return { emoji: '🔨', desc: '等待 Jenkins Build', next: `執行 build_ticket` };
|
|
1251
|
+
if (s === 'released') return { emoji: '✅', desc: '已完成', next: '可開 CI' };
|
|
1252
|
+
if (s === 'cancelled') return { emoji: '❌', desc: '已取消', next: null };
|
|
1253
|
+
return { emoji: '🔄', desc: status, next: null };
|
|
1186
1254
|
}
|
|
1187
1255
|
if (issueType === 'CI') {
|
|
1188
|
-
if (s === 'to do') return {emoji: '⏳', desc: '尚未開始', next: '執行 build_ticket'};
|
|
1189
|
-
if (s === 'wait for build') return {emoji: '⏳', desc: '等待 Jenkins Build', next: '執行 build_ticket'};
|
|
1190
|
-
if (s === 'compliance scan') return {emoji: '🔍', desc: '掃描中', next: '執行 wait_to_stg'};
|
|
1191
|
-
if (s === 'upload report') return {emoji: '📋', desc: '上傳掃描報告', next: '執行 wait_to_stg'};
|
|
1192
|
-
if (s === 'wait to dev') return {emoji: '🔄', desc: 'Build 完成,等待 DEV', next: '執行 wait_to_stg'};
|
|
1193
|
-
if (s === 'wait to stg') return {emoji: '✅', desc: 'Build 完成,等待 STG 部署', next: '可建 CD(STG) 並執行 prepare_cd_deployment'};
|
|
1194
|
-
if (s === 'wait to uat') return {emoji: '✅', desc: 'STG 完成,等待 UAT', next: '可建 CD(UAT)'};
|
|
1195
|
-
if (s === 'wait for upload') return {emoji: '📦', desc: '等待上傳 Pre-Release', next: null};
|
|
1196
|
-
if (s === 'wait to prd/dr') return {emoji: '✅', desc: 'UAT 完成,等待 PRD/DR', next: '可建 CD(PRD/DR)'};
|
|
1197
|
-
if (s === 'done') return {emoji: '✅', desc: '已完成', next: null};
|
|
1198
|
-
if (s === 'cancelled') return {emoji: '❌', desc: '已取消', next: null};
|
|
1199
|
-
return {emoji: '🔄', desc: status, next: null};
|
|
1256
|
+
if (s === 'to do') return { emoji: '⏳', desc: '尚未開始', next: '執行 build_ticket' };
|
|
1257
|
+
if (s === 'wait for build') return { emoji: '⏳', desc: '等待 Jenkins Build', next: '執行 build_ticket' };
|
|
1258
|
+
if (s === 'compliance scan') return { emoji: '🔍', desc: '掃描中', next: '執行 wait_to_stg' };
|
|
1259
|
+
if (s === 'upload report') return { emoji: '📋', desc: '上傳掃描報告', next: '執行 wait_to_stg' };
|
|
1260
|
+
if (s === 'wait to dev') return { emoji: '🔄', desc: 'Build 完成,等待 DEV', next: '執行 wait_to_stg' };
|
|
1261
|
+
if (s === 'wait to stg') return { emoji: '✅', desc: 'Build 完成,等待 STG 部署', next: '可建 CD(STG) 並執行 prepare_cd_deployment' };
|
|
1262
|
+
if (s === 'wait to uat') return { emoji: '✅', desc: 'STG 完成,等待 UAT', next: '可建 CD(UAT)' };
|
|
1263
|
+
if (s === 'wait for upload') return { emoji: '📦', desc: '等待上傳 Pre-Release', next: null };
|
|
1264
|
+
if (s === 'wait to prd/dr') return { emoji: '✅', desc: 'UAT 完成,等待 PRD/DR', next: '可建 CD(PRD/DR)' };
|
|
1265
|
+
if (s === 'done') return { emoji: '✅', desc: '已完成', next: null };
|
|
1266
|
+
if (s === 'cancelled') return { emoji: '❌', desc: '已取消', next: null };
|
|
1267
|
+
return { emoji: '🔄', desc: status, next: null };
|
|
1200
1268
|
}
|
|
1201
1269
|
if (issueType === 'CD') {
|
|
1202
|
-
if (s === 'to do') return {emoji: '⏳', desc: '尚未開始', next: '執行 prepare_cd_deployment'};
|
|
1203
|
-
if (s === 'wait for send notice email') return {emoji: '⏳', desc: '等待通知信', next: '執行 prepare_cd_deployment'};
|
|
1204
|
-
if (s === 'prepare for deploy') return {emoji: '⏳', desc: '部署單建立中', next: null};
|
|
1205
|
-
if (s === 'wait approval') return {emoji: '⏳', desc: '等待主管核准', next: 'wait_for_approval'};
|
|
1206
|
-
if (s === 'wait deploy') return {emoji: '✅', desc: '已核准,等待部署', next: '執行 trigger_deployment'};
|
|
1207
|
-
if (s === 'deploying') return {emoji: '🚀', desc: '部署中', next: null};
|
|
1208
|
-
if (s === 'done') return {emoji: '✅', desc: '部署完成', next: null};
|
|
1209
|
-
if (s === 'cancelled') return {emoji: '❌', desc: '已取消', next: null};
|
|
1210
|
-
return {emoji: '🔄', desc: status, next: null};
|
|
1270
|
+
if (s === 'to do') return { emoji: '⏳', desc: '尚未開始', next: '執行 prepare_cd_deployment' };
|
|
1271
|
+
if (s === 'wait for send notice email') return { emoji: '⏳', desc: '等待通知信', next: '執行 prepare_cd_deployment' };
|
|
1272
|
+
if (s === 'prepare for deploy') return { emoji: '⏳', desc: '部署單建立中', next: null };
|
|
1273
|
+
if (s === 'wait approval') return { emoji: '⏳', desc: '等待主管核准', next: 'wait_for_approval' };
|
|
1274
|
+
if (s === 'wait deploy') return { emoji: '✅', desc: '已核准,等待部署', next: '執行 trigger_deployment' };
|
|
1275
|
+
if (s === 'deploying') return { emoji: '🚀', desc: '部署中', next: null };
|
|
1276
|
+
if (s === 'done') return { emoji: '✅', desc: '部署完成', next: null };
|
|
1277
|
+
if (s === 'cancelled') return { emoji: '❌', desc: '已取消', next: null };
|
|
1278
|
+
return { emoji: '🔄', desc: status, next: null };
|
|
1211
1279
|
}
|
|
1212
|
-
return {emoji: '🔄', desc: status, next: null};
|
|
1280
|
+
return { emoji: '🔄', desc: status, next: null };
|
|
1213
1281
|
};
|
|
1214
1282
|
|
|
1215
1283
|
try {
|
|
@@ -1249,9 +1317,8 @@ export async function executeTool(name, args, deps) {
|
|
|
1249
1317
|
const linked = l.inwardIssue ?? l.outwardIssue;
|
|
1250
1318
|
const d = describeStatus('Library', linked.fields.status?.name ?? '');
|
|
1251
1319
|
const gitBranch = libraryExtraFields[i]?.customfield_13431 ?? '(無)';
|
|
1252
|
-
return ` ${linked.key} [${linked.fields.status?.name}] ${d.emoji} ${d.desc}${
|
|
1253
|
-
|
|
1254
|
-
}\n branch: ${gitBranch}\n ${linked.fields.summary ?? ''}`;
|
|
1320
|
+
return ` ${linked.key} [${linked.fields.status?.name}] ${d.emoji} ${d.desc}${d.next ? ` → ${d.next}(${linked.key})` : ''
|
|
1321
|
+
}\n branch: ${gitBranch}\n ${linked.fields.summary ?? ''}`;
|
|
1255
1322
|
});
|
|
1256
1323
|
|
|
1257
1324
|
// Step 3: 查近期 CD 單(取 5 筆)
|
|
@@ -1270,9 +1337,8 @@ export async function executeTool(name, args, deps) {
|
|
|
1270
1337
|
|
|
1271
1338
|
const cdLines = Object.entries(cdByEnv).map(([env, cd]) => {
|
|
1272
1339
|
const d = describeStatus('CD', cd.fields.status.name);
|
|
1273
|
-
return ` ${env.toUpperCase()}:${cd.key} [${cd.fields.status.name}] ${d.emoji} ${d.desc}${
|
|
1274
|
-
|
|
1275
|
-
}`;
|
|
1340
|
+
return ` ${env.toUpperCase()}:${cd.key} [${cd.fields.status.name}] ${d.emoji} ${d.desc}${d.next ? ` → ${d.next}` : ''
|
|
1341
|
+
}`;
|
|
1276
1342
|
});
|
|
1277
1343
|
|
|
1278
1344
|
// Step 4: 組合輸出
|
|
@@ -1281,8 +1347,7 @@ export async function executeTool(name, args, deps) {
|
|
|
1281
1347
|
`📦 ${systemCode} Release 現況(${today})`,
|
|
1282
1348
|
'',
|
|
1283
1349
|
'── CI ──────────────────────────────',
|
|
1284
|
-
` ${ciIssue.key} [${ciIssue.fields.status.name}] ${ciStatus.emoji} ${ciStatus.desc}${
|
|
1285
|
-
ciStatus.next ? ` → ${ciStatus.next}` : ''
|
|
1350
|
+
` ${ciIssue.key} [${ciIssue.fields.status.name}] ${ciStatus.emoji} ${ciStatus.desc}${ciStatus.next ? ` → ${ciStatus.next}` : ''
|
|
1286
1351
|
}`,
|
|
1287
1352
|
` 版本:${ciVersion}`,
|
|
1288
1353
|
'',
|
|
@@ -1293,17 +1358,17 @@ export async function executeTool(name, args, deps) {
|
|
|
1293
1358
|
...(cdLines.length ? cdLines : [' (尚無 CD 單)']),
|
|
1294
1359
|
];
|
|
1295
1360
|
|
|
1296
|
-
return ok({summary: lines.join('\n'), ci: ciIssue.key, version: ciVersion});
|
|
1361
|
+
return ok({ summary: lines.join('\n'), ci: ciIssue.key, version: ciVersion });
|
|
1297
1362
|
} catch (err) {
|
|
1298
1363
|
return error(`get_release_status 失敗: ${err.message}`);
|
|
1299
1364
|
}
|
|
1300
1365
|
}
|
|
1301
1366
|
|
|
1302
1367
|
case 'get_next_lib_version':
|
|
1303
|
-
return handleGetNextLibVersion(args, {jira});
|
|
1368
|
+
return handleGetNextLibVersion(args, { jira });
|
|
1304
1369
|
|
|
1305
1370
|
case 'get_next_ci_version':
|
|
1306
|
-
return handleGetNextCIVersion(args, {jira});
|
|
1371
|
+
return handleGetNextCIVersion(args, { jira });
|
|
1307
1372
|
|
|
1308
1373
|
default:
|
|
1309
1374
|
throw new Error(`Unknown tool: ${name}`);
|
|
@@ -1312,13 +1377,13 @@ export async function executeTool(name, args, deps) {
|
|
|
1312
1377
|
|
|
1313
1378
|
function ok(data) {
|
|
1314
1379
|
return {
|
|
1315
|
-
content: [{type: 'text', text: JSON.stringify(data, null, 2)}],
|
|
1380
|
+
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
1316
1381
|
};
|
|
1317
1382
|
}
|
|
1318
1383
|
|
|
1319
1384
|
function error(msg) {
|
|
1320
1385
|
return {
|
|
1321
|
-
content: [{type: 'text', text: `❌ 錯誤: ${msg}`}],
|
|
1386
|
+
content: [{ type: 'text', text: `❌ 錯誤: ${msg}` }],
|
|
1322
1387
|
isError: true,
|
|
1323
1388
|
};
|
|
1324
1389
|
}
|