@jira-deploy/core 1.0.1 → 1.0.3
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/environments.js +1 -1
- package/constants/field-ids.js +3 -0
- package/dry-run.js +4 -0
- package/jira-client.js +13 -0
- package/package.json +1 -1
- package/tools/cd.js +24 -2
- package/tools/grayrelease.js +781 -0
- package/tools/helpers.js +4 -1
- package/tools/index.js +214 -9
- package/tools/library.js +3 -2
- package/tools/workflows.js +239 -0
- package/tools.test.js +725 -13
package/tools/helpers.js
CHANGED
package/tools/index.js
CHANGED
|
@@ -17,6 +17,9 @@ import {
|
|
|
17
17
|
getGrayReleaseToolDefinitions,
|
|
18
18
|
handleCreateGrayReleaseTicket,
|
|
19
19
|
handleLinkStgGrayRelease,
|
|
20
|
+
handleAutoGrayRelease,
|
|
21
|
+
handleGetGrayReleaseStatus,
|
|
22
|
+
handleContinueGrayRelease,
|
|
20
23
|
} from './grayrelease.js';
|
|
21
24
|
import {
|
|
22
25
|
getReleaseToolDefinitions,
|
|
@@ -26,6 +29,36 @@ import {
|
|
|
26
29
|
handleWaitForComment,
|
|
27
30
|
} from './release.js';
|
|
28
31
|
import {getJabberToolDefinitions, handleSendJabberMessage} from './jabber.js';
|
|
32
|
+
import {
|
|
33
|
+
getWorkflowToolDefinitions,
|
|
34
|
+
handleRunLibToStgReleaseWorkflow,
|
|
35
|
+
handleRunStgFullReleaseWorkflow,
|
|
36
|
+
} from './workflows.js';
|
|
37
|
+
|
|
38
|
+
const READ_ONLY_TOOL_NAMES = new Set([
|
|
39
|
+
'get_issue_status',
|
|
40
|
+
'list_transitions',
|
|
41
|
+
'get_release_status',
|
|
42
|
+
'get_unreleased_versions',
|
|
43
|
+
'get_release_manager',
|
|
44
|
+
'wait_for_comment',
|
|
45
|
+
'get_grayrelease_status',
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
function withToolAnnotations(tools) {
|
|
49
|
+
return tools.map((tool) => {
|
|
50
|
+
if (!READ_ONLY_TOOL_NAMES.has(tool.name)) {
|
|
51
|
+
return tool;
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
...tool,
|
|
55
|
+
annotations: {
|
|
56
|
+
...tool.annotations,
|
|
57
|
+
readOnlyHint: true,
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
}
|
|
29
62
|
|
|
30
63
|
/**
|
|
31
64
|
* 回傳所有 tool 定義(schema)給 MCP Server 註冊
|
|
@@ -40,13 +73,14 @@ import {getJabberToolDefinitions, handleSendJabberMessage} from './jabber.js';
|
|
|
40
73
|
* - defaults.js: 預設值及範本
|
|
41
74
|
*/
|
|
42
75
|
export function getToolDefinitions() {
|
|
43
|
-
return [
|
|
76
|
+
return withToolAnnotations([
|
|
44
77
|
...getLibraryToolDefinitions(),
|
|
45
78
|
...getCIToolDefinitions(),
|
|
46
79
|
...getCDToolDefinitions(),
|
|
47
80
|
...getGrayReleaseToolDefinitions(),
|
|
48
81
|
...getReleaseToolDefinitions(),
|
|
49
82
|
...getJabberToolDefinitions(),
|
|
83
|
+
...getWorkflowToolDefinitions(),
|
|
50
84
|
{
|
|
51
85
|
name: 'transition_issue',
|
|
52
86
|
description: '切換 Jira issue 狀態,用名稱指定(不需要知道 transition ID)',
|
|
@@ -155,6 +189,21 @@ export function getToolDefinitions() {
|
|
|
155
189
|
},
|
|
156
190
|
},
|
|
157
191
|
},
|
|
192
|
+
{
|
|
193
|
+
name: 'wait_to_dev',
|
|
194
|
+
description:
|
|
195
|
+
'CI 單 build 完成後,自動走完掃描流程切到 Wait To DEV 狀態。流程:Compliance Scan → Upload Scan Report → Accept → Wait To DEV(不執行 Dev Done)',
|
|
196
|
+
inputSchema: {
|
|
197
|
+
type: 'object',
|
|
198
|
+
required: ['issueKey'],
|
|
199
|
+
properties: {
|
|
200
|
+
issueKey: {
|
|
201
|
+
type: 'string',
|
|
202
|
+
description: 'CI issue key,例如 CID-1668',
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
},
|
|
158
207
|
{
|
|
159
208
|
name: 'wait_to_stg',
|
|
160
209
|
description:
|
|
@@ -271,13 +320,14 @@ export function getToolDefinitions() {
|
|
|
271
320
|
},
|
|
272
321
|
},
|
|
273
322
|
},
|
|
274
|
-
];
|
|
323
|
+
]);
|
|
275
324
|
}
|
|
276
325
|
|
|
277
326
|
/**
|
|
278
327
|
* 執行 tool,回傳 { content: [{ type: 'text', text }] }
|
|
279
328
|
*/
|
|
280
|
-
export async function executeTool(name, args,
|
|
329
|
+
export async function executeTool(name, args, deps) {
|
|
330
|
+
const {jira, notifier} = deps;
|
|
281
331
|
const poller = new Poller(jira);
|
|
282
332
|
|
|
283
333
|
switch (name) {
|
|
@@ -346,6 +396,15 @@ export async function executeTool(name, args, {jira, notifier}) {
|
|
|
346
396
|
case 'link_stg_grayrelease':
|
|
347
397
|
return handleLinkStgGrayRelease(args, {jira, notifier});
|
|
348
398
|
|
|
399
|
+
case 'auto_grayrelease':
|
|
400
|
+
return handleAutoGrayRelease(args, {jira, notifier});
|
|
401
|
+
|
|
402
|
+
case 'get_grayrelease_status':
|
|
403
|
+
return handleGetGrayReleaseStatus(args, {jira});
|
|
404
|
+
|
|
405
|
+
case 'continue_grayrelease':
|
|
406
|
+
return handleContinueGrayRelease(args, {jira, notifier});
|
|
407
|
+
|
|
349
408
|
case 'get_unreleased_versions':
|
|
350
409
|
return handleGetUnreleasedVersions(args, {jira});
|
|
351
410
|
|
|
@@ -361,6 +420,22 @@ export async function executeTool(name, args, {jira, notifier}) {
|
|
|
361
420
|
case 'send_jabber_message':
|
|
362
421
|
return handleSendJabberMessage(args, {});
|
|
363
422
|
|
|
423
|
+
case 'run_stg_full_release':
|
|
424
|
+
return handleRunStgFullReleaseWorkflow(args, {
|
|
425
|
+
jira,
|
|
426
|
+
notifier,
|
|
427
|
+
executeToolImpl: deps.executeToolImpl ?? executeTool,
|
|
428
|
+
workflowWaitOptions: deps.workflowWaitOptions,
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
case 'run_lib_to_stg_release':
|
|
432
|
+
return handleRunLibToStgReleaseWorkflow(args, {
|
|
433
|
+
jira,
|
|
434
|
+
notifier,
|
|
435
|
+
executeToolImpl: deps.executeToolImpl ?? executeTool,
|
|
436
|
+
workflowWaitOptions: deps.workflowWaitOptions,
|
|
437
|
+
});
|
|
438
|
+
|
|
364
439
|
case 'link_issues': {
|
|
365
440
|
try {
|
|
366
441
|
const linkType = args.linkType ?? 'Relates';
|
|
@@ -491,6 +566,48 @@ export async function executeTool(name, args, {jira, notifier}) {
|
|
|
491
566
|
}
|
|
492
567
|
}
|
|
493
568
|
|
|
569
|
+
case 'wait_to_dev': {
|
|
570
|
+
const {issueKey} = args;
|
|
571
|
+
const log = [];
|
|
572
|
+
|
|
573
|
+
const STEPS = [
|
|
574
|
+
{transition: 'Upload Scan Report', targetStatus: 'Upload Report'},
|
|
575
|
+
{transition: 'Accept', targetStatus: 'Wait To DEV'},
|
|
576
|
+
];
|
|
577
|
+
const finalTargetStatus = STEPS.at(-1).targetStatus;
|
|
578
|
+
|
|
579
|
+
try {
|
|
580
|
+
for (const step of STEPS) {
|
|
581
|
+
const transitions = await jira.getTransitions(issueKey);
|
|
582
|
+
const t = transitions.find((t) => t.name.toLowerCase() === step.transition.toLowerCase());
|
|
583
|
+
if (!t) {
|
|
584
|
+
const issue = await jira.getIssue(issueKey);
|
|
585
|
+
const current = issue.fields.status.name;
|
|
586
|
+
if (current.toLowerCase() === step.targetStatus.toLowerCase()) {
|
|
587
|
+
log.push(` 已是 ${current},跳過「${step.transition}」`);
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
if (current.toLowerCase() === finalTargetStatus.toLowerCase()) {
|
|
591
|
+
log.push(` 已是 ${current},流程已完成`);
|
|
592
|
+
break;
|
|
593
|
+
}
|
|
594
|
+
return error(`找不到 transition「${step.transition}」,目前狀態:${current}`);
|
|
595
|
+
}
|
|
596
|
+
log.push(`執行「${t.name}」→ ${step.targetStatus}`);
|
|
597
|
+
await jira.transitionById(issueKey, t.id);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const issue = await jira.getIssue(issueKey);
|
|
601
|
+
const finalStatus = issue.fields.status.name;
|
|
602
|
+
log.push(`✅ 完成,目前狀態:${finalStatus}`);
|
|
603
|
+
await notifier.notify(issueKey, `已切換至 ${finalStatus},可進行 DEV 部署`);
|
|
604
|
+
|
|
605
|
+
return ok({issueKey, status: finalStatus, steps: log});
|
|
606
|
+
} catch (err) {
|
|
607
|
+
return error(`wait_to_dev 失敗: ${err.message}`);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
494
611
|
case 'wait_to_stg': {
|
|
495
612
|
const {issueKey} = args;
|
|
496
613
|
const log = [];
|
|
@@ -669,7 +786,11 @@ export async function executeTool(name, args, {jira, notifier}) {
|
|
|
669
786
|
if (!t) {
|
|
670
787
|
const issue = await jira.getIssue(deploymentKey);
|
|
671
788
|
const available = transitions.map((t) => t.name).join(', ');
|
|
672
|
-
|
|
789
|
+
if (['To AutoDeploy', 'Trigger AutoDeploy'].includes(step.name)) {
|
|
790
|
+
return error(
|
|
791
|
+
`找不到必要部署 transition「${step.name}」,目前狀態:${issue.fields.status.name},可用:${available || '無'}`,
|
|
792
|
+
);
|
|
793
|
+
}
|
|
673
794
|
log.push(
|
|
674
795
|
` ⚠️ 找不到「${step.name}」(目前狀態:${issue.fields.status.name},可用:${available || '無'}),跳過`,
|
|
675
796
|
);
|
|
@@ -739,19 +860,28 @@ export async function executeTool(name, args, {jira, notifier}) {
|
|
|
739
860
|
const log = [];
|
|
740
861
|
|
|
741
862
|
// CD 部署前置 transitions(依序嘗試,直到找到部署 transition 或完成申請流程)
|
|
742
|
-
//
|
|
863
|
+
// DEV 流程會跳過通知主管簽核,建立 deployment 後直接嘗試自助 Approved / To Wait Deploy。
|
|
864
|
+
// STG/UAT/PRD 完整流程:
|
|
743
865
|
// TO DO → (Accept) → Wait For Send Notice Email
|
|
744
866
|
// → (Prepare to create deployment ticket) → Prepare For Deploy ← 建立 Deployment sub-task
|
|
745
867
|
// → (Apply for approval) → Wait Approval ← 等 Reviewer 核准
|
|
746
868
|
// → Wait Deploy ← 核准後自動切換,再呼叫 trigger_deployment
|
|
747
869
|
// ⚠️ 不加 'Approved':Approved 需要其他主管才能執行
|
|
748
|
-
const CD_PRE_TRANSITIONS = [
|
|
870
|
+
const CD_PRE_TRANSITIONS = envCode === 'dev' ? [
|
|
871
|
+
'Accept',
|
|
872
|
+
] : [
|
|
749
873
|
'Accept',
|
|
750
874
|
'Prepare to create deployment ticket',
|
|
751
875
|
'Apply for approval',
|
|
752
876
|
'To Wait Deploy',
|
|
753
877
|
];
|
|
754
878
|
|
|
879
|
+
const DEV_SELF_SERVICE_TRANSITIONS = [
|
|
880
|
+
'Apply for approval',
|
|
881
|
+
'Approved',
|
|
882
|
+
'To Wait Deploy',
|
|
883
|
+
];
|
|
884
|
+
|
|
755
885
|
// 部署 transition 名稱(支援多種命名)
|
|
756
886
|
const DEPLOY_TRANSITION_NAMES = [
|
|
757
887
|
'Prepare to create deployment ticket', // 實際 Jira transition 名稱
|
|
@@ -788,6 +918,45 @@ export async function executeTool(name, args, {jira, notifier}) {
|
|
|
788
918
|
|
|
789
919
|
let deployTrans = await findDeployTrans();
|
|
790
920
|
|
|
921
|
+
const getCurrentStatus = async () => {
|
|
922
|
+
const issue = await jira.getIssue(issueKey);
|
|
923
|
+
return issue.fields.status.name;
|
|
924
|
+
};
|
|
925
|
+
|
|
926
|
+
const runDevSelfServiceTransitions = async () => {
|
|
927
|
+
if (envCode !== 'dev') return;
|
|
928
|
+
for (const transitionName of DEV_SELF_SERVICE_TRANSITIONS) {
|
|
929
|
+
const currentStatus = await getCurrentStatus();
|
|
930
|
+
if (currentStatus.toLowerCase() === 'wait deploy') {
|
|
931
|
+
log.push(' DEV 自助流程已到 Wait Deploy,停止續跑');
|
|
932
|
+
break;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
const transitions = await jira.getTransitions(issueKey);
|
|
936
|
+
const next = transitions.find(
|
|
937
|
+
(t) => t.name.toLowerCase() === transitionName.toLowerCase(),
|
|
938
|
+
);
|
|
939
|
+
if (!next) continue;
|
|
940
|
+
|
|
941
|
+
log.push(` DEV 自助流程觸發「${next.name}」...`);
|
|
942
|
+
await jira.transitionById(issueKey, next.id);
|
|
943
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
944
|
+
}
|
|
945
|
+
};
|
|
946
|
+
|
|
947
|
+
if (envCode === 'dev') {
|
|
948
|
+
const currentStatus = await getCurrentStatus();
|
|
949
|
+
if (currentStatus.toLowerCase() === 'wait deploy') {
|
|
950
|
+
log.push('✅ CD 單已在 Wait Deploy,無需重複 prepare');
|
|
951
|
+
return ok({
|
|
952
|
+
issueKey,
|
|
953
|
+
environment: envCode,
|
|
954
|
+
status: currentStatus,
|
|
955
|
+
steps: log,
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
791
960
|
if (!deployTrans) {
|
|
792
961
|
log.push('未找到部署 transition,逐步觸發前置狀態...');
|
|
793
962
|
for (const preName of CD_PRE_TRANSITIONS) {
|
|
@@ -807,6 +976,25 @@ export async function executeTool(name, args, {jira, notifier}) {
|
|
|
807
976
|
}
|
|
808
977
|
}
|
|
809
978
|
|
|
979
|
+
if (!deployTrans && envCode === 'dev') {
|
|
980
|
+
await runDevSelfServiceTransitions();
|
|
981
|
+
const currentStatus = await getCurrentStatus();
|
|
982
|
+
if (currentStatus.toLowerCase() === 'wait deploy') {
|
|
983
|
+
log.push(`✅ DEV 自助流程完成,目前狀態:${currentStatus}`);
|
|
984
|
+
await notifier.notify(
|
|
985
|
+
issueKey,
|
|
986
|
+
`CD 部署已觸發(環境: ${envCode.toUpperCase()},狀態: ${currentStatus})`,
|
|
987
|
+
);
|
|
988
|
+
return ok({
|
|
989
|
+
issueKey,
|
|
990
|
+
environment: envCode,
|
|
991
|
+
status: currentStatus,
|
|
992
|
+
steps: log,
|
|
993
|
+
});
|
|
994
|
+
}
|
|
995
|
+
deployTrans = await findDeployTrans();
|
|
996
|
+
}
|
|
997
|
+
|
|
810
998
|
if (!deployTrans) {
|
|
811
999
|
const issue = await jira.getIssue(issueKey);
|
|
812
1000
|
const transitions = await jira.getTransitions(issueKey);
|
|
@@ -820,21 +1008,37 @@ export async function executeTool(name, args, {jira, notifier}) {
|
|
|
820
1008
|
|
|
821
1009
|
// Step 4: 觸发部署 transition
|
|
822
1010
|
log.push(`執行「${deployTrans.name}」transition(id: ${deployTrans.id})...`);
|
|
823
|
-
|
|
1011
|
+
try {
|
|
1012
|
+
await jira.transitionById(issueKey, deployTrans.id);
|
|
1013
|
+
} catch (err) {
|
|
1014
|
+
if (envCode === 'dev' && err.message.includes('Already create deployment ticket')) {
|
|
1015
|
+
log.push(' Deployment ticket 已存在,繼續 DEV approval 續跑流程');
|
|
1016
|
+
} else {
|
|
1017
|
+
throw err;
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
824
1020
|
|
|
825
1021
|
const issue = await jira.getIssue(issueKey);
|
|
826
1022
|
const newStatus = issue.fields.status.name;
|
|
827
1023
|
log.push(`✅ 部署已觸发,目前狀態:${newStatus}`);
|
|
828
1024
|
|
|
1025
|
+
await runDevSelfServiceTransitions();
|
|
1026
|
+
|
|
1027
|
+
const finalIssue = await jira.getIssue(issueKey);
|
|
1028
|
+
const finalStatus = finalIssue.fields.status.name;
|
|
1029
|
+
if (finalStatus !== newStatus) {
|
|
1030
|
+
log.push(`✅ DEV 自助流程完成,目前狀態:${finalStatus}`);
|
|
1031
|
+
}
|
|
1032
|
+
|
|
829
1033
|
await notifier.notify(
|
|
830
1034
|
issueKey,
|
|
831
|
-
`CD 部署已觸發(環境: ${envCode.toUpperCase()},狀態: ${
|
|
1035
|
+
`CD 部署已觸發(環境: ${envCode.toUpperCase()},狀態: ${finalStatus})`,
|
|
832
1036
|
);
|
|
833
1037
|
|
|
834
1038
|
return ok({
|
|
835
1039
|
issueKey,
|
|
836
1040
|
environment: envCode,
|
|
837
|
-
status:
|
|
1041
|
+
status: finalStatus,
|
|
838
1042
|
steps: log,
|
|
839
1043
|
});
|
|
840
1044
|
} catch (err) {
|
|
@@ -1115,5 +1319,6 @@ function ok(data) {
|
|
|
1115
1319
|
function error(msg) {
|
|
1116
1320
|
return {
|
|
1117
1321
|
content: [{type: 'text', text: `❌ 錯誤: ${msg}`}],
|
|
1322
|
+
isError: true,
|
|
1118
1323
|
};
|
|
1119
1324
|
}
|
package/tools/library.js
CHANGED
|
@@ -31,7 +31,7 @@ export function getLibraryToolDefinitions() {
|
|
|
31
31
|
'建立 Library Release 上版單。專用工具,提前驗證必填欄位。當使用者說「幫我開 Lib」、「開 Library」時優先使用這個 tool',
|
|
32
32
|
inputSchema: {
|
|
33
33
|
type: 'object',
|
|
34
|
-
required: ['systemCode', '
|
|
34
|
+
required: ['systemCode', 'gitBranch'],
|
|
35
35
|
properties: {
|
|
36
36
|
systemCode: {
|
|
37
37
|
type: 'string',
|
|
@@ -210,7 +210,7 @@ export async function handleCreateLibraryTicket(args, {jira, notifier}) {
|
|
|
210
210
|
const issue = await jira.createIssue(fields);
|
|
211
211
|
await notifier.notify(
|
|
212
212
|
issue.key,
|
|
213
|
-
`Library Release 單已建立。系統: ${
|
|
213
|
+
`Library Release 單已建立。系統: ${normalizedArgs.systemCode}, 模組: ${normalizedArgs.module}, 環境: ${envCode}`,
|
|
214
214
|
);
|
|
215
215
|
return ok({
|
|
216
216
|
issueKey: issue.key,
|
|
@@ -218,6 +218,7 @@ export async function handleCreateLibraryTicket(args, {jira, notifier}) {
|
|
|
218
218
|
url: `${process.env.JIRA_BASE_URL}/browse/${issue.key}`,
|
|
219
219
|
type: 'Library Release',
|
|
220
220
|
system: normalizedArgs.systemCode,
|
|
221
|
+
module: normalizedArgs.module,
|
|
221
222
|
});
|
|
222
223
|
} catch (err) {
|
|
223
224
|
return error(`無法建立 Library 單: ${err.message}`);
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import {getClusterList, ok, error} from './helpers.js';
|
|
2
|
+
|
|
3
|
+
export function getWorkflowToolDefinitions() {
|
|
4
|
+
return [
|
|
5
|
+
{
|
|
6
|
+
name: 'run_stg_full_release',
|
|
7
|
+
description:
|
|
8
|
+
'執行標準 STG 全流程:建立 CI 單、Build、Wait To STG、建立 CD 單、Prepare CD Deployment,最後 trigger deployment 並 Apply for close。',
|
|
9
|
+
inputSchema: {
|
|
10
|
+
type: 'object',
|
|
11
|
+
required: ['systemCode'],
|
|
12
|
+
properties: {
|
|
13
|
+
systemCode: {
|
|
14
|
+
type: 'string',
|
|
15
|
+
description: '系統代碼,例如 IBK、CWA、EIB、EVT、NPM、BOF',
|
|
16
|
+
},
|
|
17
|
+
environment: {
|
|
18
|
+
type: 'string',
|
|
19
|
+
enum: ['stg'],
|
|
20
|
+
default: 'stg',
|
|
21
|
+
description: '固定為 stg;保留此參數讓自然語言 planner 可以映射使用者提到的 STG。',
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: 'run_lib_to_stg_release',
|
|
28
|
+
description:
|
|
29
|
+
'執行 Library 到 STG 的完整流程:建立 Library 單、Build library、等待 library build 狀態、建立關聯 CI、Build CI、Wait To STG、建立 CD、Prepare CD Deployment,最後 trigger deployment 並 Apply for close。',
|
|
30
|
+
inputSchema: {
|
|
31
|
+
type: 'object',
|
|
32
|
+
required: ['systemCode', 'gitBranch'],
|
|
33
|
+
properties: {
|
|
34
|
+
systemCode: {
|
|
35
|
+
type: 'string',
|
|
36
|
+
description: '系統代碼,例如 IBK、CWA、EIB、EVT、NPM、BOF',
|
|
37
|
+
},
|
|
38
|
+
module: {
|
|
39
|
+
type: 'string',
|
|
40
|
+
description: 'Library 模組;未提供時預設使用 systemCode 小寫,例如 IBK -> ibk。',
|
|
41
|
+
},
|
|
42
|
+
gitBranch: {
|
|
43
|
+
type: 'string',
|
|
44
|
+
description: 'Library release/hotfix/feature branch,例如 release/v1.5.2.0。',
|
|
45
|
+
},
|
|
46
|
+
environment: {
|
|
47
|
+
type: 'string',
|
|
48
|
+
enum: ['stg'],
|
|
49
|
+
default: 'stg',
|
|
50
|
+
description: '固定為 stg;保留此參數讓自然語言 planner 可以映射使用者提到的 STG。',
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function extractText(result) {
|
|
59
|
+
if (!result?.content?.length) {
|
|
60
|
+
return '';
|
|
61
|
+
}
|
|
62
|
+
return result.content
|
|
63
|
+
.filter((item) => item.type === 'text')
|
|
64
|
+
.map((item) => item.text)
|
|
65
|
+
.join('\n')
|
|
66
|
+
.trim();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function formatJson(value) {
|
|
70
|
+
return JSON.stringify(value, null, 2);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function parseToolJson(result) {
|
|
74
|
+
const text = extractText(result);
|
|
75
|
+
if (result?.isError || text.startsWith('❌')) {
|
|
76
|
+
throw new Error(text);
|
|
77
|
+
}
|
|
78
|
+
return JSON.parse(text);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function runToolOrThrow(name, args, deps, workflowLog) {
|
|
82
|
+
workflowLog.push(`- ${name}: ${formatJson(args)}`);
|
|
83
|
+
const result = await deps.executeToolImpl(name, args, deps);
|
|
84
|
+
return parseToolJson(result);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function waitForIssueStatus(issueKey, targetStatuses, deps, options = {}) {
|
|
88
|
+
const timeoutMs = options.timeoutMs ?? 10 * 60 * 1000;
|
|
89
|
+
const intervalMs = options.intervalMs ?? 5000;
|
|
90
|
+
const startedAt = Date.now();
|
|
91
|
+
const wanted = targetStatuses.map((status) => status.toLowerCase());
|
|
92
|
+
|
|
93
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
94
|
+
const issue = await deps.jira.getIssue(issueKey);
|
|
95
|
+
const current = issue.fields?.status?.name ?? '';
|
|
96
|
+
if (wanted.includes(current.toLowerCase())) {
|
|
97
|
+
return current;
|
|
98
|
+
}
|
|
99
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
throw new Error(`等待 ${issueKey} 狀態 ${targetStatuses.join(' / ')} 超時`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function handleRunStgFullReleaseWorkflow(args, deps) {
|
|
106
|
+
try {
|
|
107
|
+
const workflowLog = [];
|
|
108
|
+
const systemCode = String(args.systemCode ?? '').trim().toUpperCase();
|
|
109
|
+
const ciArgs = {systemCode, environment: 'stg'};
|
|
110
|
+
const ci = await runToolOrThrow('create_ci_ticket', ciArgs, deps, workflowLog);
|
|
111
|
+
|
|
112
|
+
const build = await runToolOrThrow('build_ticket', {issueKey: ci.issueKey}, deps, workflowLog);
|
|
113
|
+
const toStg = await runToolOrThrow('wait_to_stg', {issueKey: ci.issueKey}, deps, workflowLog);
|
|
114
|
+
|
|
115
|
+
const clusters = getClusterList(systemCode, 'stg');
|
|
116
|
+
if (!clusters.length) {
|
|
117
|
+
throw new Error(`找不到 ${systemCode} STG 的 cluster 設定`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const cdArgs = {
|
|
121
|
+
systemCode,
|
|
122
|
+
environment: 'stg',
|
|
123
|
+
linkedCiKey: ci.issueKey,
|
|
124
|
+
clusterDeploy: clusters.join(','),
|
|
125
|
+
moduleChild: systemCode.toLowerCase(),
|
|
126
|
+
};
|
|
127
|
+
const cd = await runToolOrThrow('create_cd_ticket', cdArgs, deps, workflowLog);
|
|
128
|
+
const prepared = await runToolOrThrow(
|
|
129
|
+
'prepare_cd_deployment',
|
|
130
|
+
{issueKey: cd.issueKey, environment: 'stg'},
|
|
131
|
+
deps,
|
|
132
|
+
workflowLog,
|
|
133
|
+
);
|
|
134
|
+
const deployed = await runToolOrThrow(
|
|
135
|
+
'trigger_deployment',
|
|
136
|
+
{cdIssueKey: cd.issueKey, environment: 'stg', applyForClose: true},
|
|
137
|
+
deps,
|
|
138
|
+
workflowLog,
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
return ok({
|
|
142
|
+
type: 'STG Full Release',
|
|
143
|
+
systemCode,
|
|
144
|
+
ciIssueKey: ci.issueKey,
|
|
145
|
+
ciStatus: toStg.status ?? build.status,
|
|
146
|
+
cdIssueKey: cd.issueKey,
|
|
147
|
+
cdStatus: prepared.status,
|
|
148
|
+
deploymentStatus: deployed.status,
|
|
149
|
+
assumptions: [`CD cluster 自動帶入 ${systemCode} STG 全部 cluster`],
|
|
150
|
+
workflowLog,
|
|
151
|
+
});
|
|
152
|
+
} catch (err) {
|
|
153
|
+
return error(`run_stg_full_release 失敗: ${err.message}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export async function handleRunLibToStgReleaseWorkflow(args, deps) {
|
|
158
|
+
try {
|
|
159
|
+
const workflowLog = [];
|
|
160
|
+
const assumptions = [];
|
|
161
|
+
const systemCode = String(args.systemCode ?? '').trim().toUpperCase();
|
|
162
|
+
const module = args.module || systemCode.toLowerCase();
|
|
163
|
+
|
|
164
|
+
if (!args.module) {
|
|
165
|
+
assumptions.push(`未指定 module,已依 systemCode 帶入 ${module}`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const libArgs = {
|
|
169
|
+
systemCode,
|
|
170
|
+
module,
|
|
171
|
+
gitBranch: args.gitBranch,
|
|
172
|
+
environment: 'stg',
|
|
173
|
+
jenkinsBranch: 'master',
|
|
174
|
+
};
|
|
175
|
+
const lib = await runToolOrThrow('create_library_ticket', libArgs, deps, workflowLog);
|
|
176
|
+
await runToolOrThrow('build_ticket', {issueKey: lib.issueKey}, deps, workflowLog);
|
|
177
|
+
const libFinalStatus = await waitForIssueStatus(
|
|
178
|
+
lib.issueKey,
|
|
179
|
+
['Released'],
|
|
180
|
+
deps,
|
|
181
|
+
{timeoutMs: 10 * 60 * 1000, ...deps.workflowWaitOptions},
|
|
182
|
+
);
|
|
183
|
+
workflowLog.push(`- wait_library_status: ${lib.issueKey} -> ${libFinalStatus}`);
|
|
184
|
+
|
|
185
|
+
const ciArgs = {
|
|
186
|
+
systemCode,
|
|
187
|
+
environment: 'stg',
|
|
188
|
+
relatesTo: lib.issueKey,
|
|
189
|
+
};
|
|
190
|
+
const ci = await runToolOrThrow('create_ci_ticket', ciArgs, deps, workflowLog);
|
|
191
|
+
const build = await runToolOrThrow('build_ticket', {issueKey: ci.issueKey}, deps, workflowLog);
|
|
192
|
+
const toStg = await runToolOrThrow('wait_to_stg', {issueKey: ci.issueKey}, deps, workflowLog);
|
|
193
|
+
|
|
194
|
+
const clusters = getClusterList(systemCode, 'stg');
|
|
195
|
+
if (!clusters.length) {
|
|
196
|
+
throw new Error(`找不到 ${systemCode} STG 的 cluster 設定`);
|
|
197
|
+
}
|
|
198
|
+
assumptions.push(`CD cluster 自動帶入 ${systemCode} STG 全部 cluster`);
|
|
199
|
+
|
|
200
|
+
const cdArgs = {
|
|
201
|
+
systemCode,
|
|
202
|
+
environment: 'stg',
|
|
203
|
+
linkedCiKey: ci.issueKey,
|
|
204
|
+
clusterDeploy: clusters.join(','),
|
|
205
|
+
moduleChild: systemCode.toLowerCase(),
|
|
206
|
+
};
|
|
207
|
+
const cd = await runToolOrThrow('create_cd_ticket', cdArgs, deps, workflowLog);
|
|
208
|
+
const prepared = await runToolOrThrow(
|
|
209
|
+
'prepare_cd_deployment',
|
|
210
|
+
{issueKey: cd.issueKey, environment: 'stg'},
|
|
211
|
+
deps,
|
|
212
|
+
workflowLog,
|
|
213
|
+
);
|
|
214
|
+
const deployed = await runToolOrThrow(
|
|
215
|
+
'trigger_deployment',
|
|
216
|
+
{cdIssueKey: cd.issueKey, environment: 'stg', applyForClose: true},
|
|
217
|
+
deps,
|
|
218
|
+
workflowLog,
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
return ok({
|
|
222
|
+
type: 'Library To STG Full Release',
|
|
223
|
+
systemCode,
|
|
224
|
+
module,
|
|
225
|
+
gitBranch: args.gitBranch,
|
|
226
|
+
libraryIssueKey: lib.issueKey,
|
|
227
|
+
libraryStatus: libFinalStatus,
|
|
228
|
+
ciIssueKey: ci.issueKey,
|
|
229
|
+
ciStatus: toStg.status ?? build.status,
|
|
230
|
+
cdIssueKey: cd.issueKey,
|
|
231
|
+
cdStatus: prepared.status,
|
|
232
|
+
deploymentStatus: deployed.status,
|
|
233
|
+
assumptions,
|
|
234
|
+
workflowLog,
|
|
235
|
+
});
|
|
236
|
+
} catch (err) {
|
|
237
|
+
return error(`run_lib_to_stg_release 失敗: ${err.message}`);
|
|
238
|
+
}
|
|
239
|
+
}
|