@jira-deploy/core 1.0.0 → 1.0.2
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/package.json +1 -1
- package/tools/cd.js +21 -1
- package/tools/helpers.js +4 -1
- package/tools/index.js +56 -4
- package/tools/library.js +1 -1
- package/tools/workflows.js +239 -0
- package/tools.test.js +167 -13
package/package.json
CHANGED
package/tools/cd.js
CHANGED
|
@@ -30,7 +30,7 @@ export function getCDToolDefinitions() {
|
|
|
30
30
|
description: '建立 CD Deploy 上版單。專用工具,提前驗證必填欄位和叢集配置',
|
|
31
31
|
inputSchema: {
|
|
32
32
|
type: 'object',
|
|
33
|
-
required: ['systemCode', '
|
|
33
|
+
required: ['systemCode', 'environment'],
|
|
34
34
|
properties: {
|
|
35
35
|
systemCode: {
|
|
36
36
|
type: 'string',
|
|
@@ -56,6 +56,26 @@ export function getCDToolDefinitions() {
|
|
|
56
56
|
description:
|
|
57
57
|
'關聯的 CI 單 issue key,例如 CID-1677。自動:① 取 release_version 填入 summary/CID_deploy_version ② 查 CI 所有 relates Library 單,各自取 CID_branch 找 LBPRJ 版本頁 → 加 Web Link',
|
|
58
58
|
},
|
|
59
|
+
linkedCiKey: {
|
|
60
|
+
type: 'string',
|
|
61
|
+
description: '關聯的 CI 單 issue key;等同 ciTicket,保留給 agent/CLI workflow 使用',
|
|
62
|
+
},
|
|
63
|
+
clusterDeploy: {
|
|
64
|
+
type: 'string',
|
|
65
|
+
description: '(選填) 逗號分隔的 cluster 清單;不填時依 systemCode/environment 自動推導',
|
|
66
|
+
},
|
|
67
|
+
moduleChild: {
|
|
68
|
+
type: 'string',
|
|
69
|
+
description: '(選填) 模組 child 名稱;不填時預設同 systemCode 小寫',
|
|
70
|
+
},
|
|
71
|
+
restartOnly: {
|
|
72
|
+
type: 'boolean',
|
|
73
|
+
description: '(選填) 是否只重啟不部署',
|
|
74
|
+
},
|
|
75
|
+
extraVars: {
|
|
76
|
+
type: 'string',
|
|
77
|
+
description: '(選填) 自訂部署 extra vars JSON 字串;不填時依 system/module/env 自動生成',
|
|
78
|
+
},
|
|
59
79
|
metaTest: {
|
|
60
80
|
type: 'boolean',
|
|
61
81
|
description:
|
package/tools/helpers.js
CHANGED
package/tools/index.js
CHANGED
|
@@ -26,6 +26,35 @@ import {
|
|
|
26
26
|
handleWaitForComment,
|
|
27
27
|
} from './release.js';
|
|
28
28
|
import {getJabberToolDefinitions, handleSendJabberMessage} from './jabber.js';
|
|
29
|
+
import {
|
|
30
|
+
getWorkflowToolDefinitions,
|
|
31
|
+
handleRunLibToStgReleaseWorkflow,
|
|
32
|
+
handleRunStgFullReleaseWorkflow,
|
|
33
|
+
} from './workflows.js';
|
|
34
|
+
|
|
35
|
+
const READ_ONLY_TOOL_NAMES = new Set([
|
|
36
|
+
'get_issue_status',
|
|
37
|
+
'list_transitions',
|
|
38
|
+
'get_release_status',
|
|
39
|
+
'get_unreleased_versions',
|
|
40
|
+
'get_release_manager',
|
|
41
|
+
'wait_for_comment',
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
function withToolAnnotations(tools) {
|
|
45
|
+
return tools.map((tool) => {
|
|
46
|
+
if (!READ_ONLY_TOOL_NAMES.has(tool.name)) {
|
|
47
|
+
return tool;
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
...tool,
|
|
51
|
+
annotations: {
|
|
52
|
+
...tool.annotations,
|
|
53
|
+
readOnlyHint: true,
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
}
|
|
29
58
|
|
|
30
59
|
/**
|
|
31
60
|
* 回傳所有 tool 定義(schema)給 MCP Server 註冊
|
|
@@ -40,13 +69,14 @@ import {getJabberToolDefinitions, handleSendJabberMessage} from './jabber.js';
|
|
|
40
69
|
* - defaults.js: 預設值及範本
|
|
41
70
|
*/
|
|
42
71
|
export function getToolDefinitions() {
|
|
43
|
-
return [
|
|
72
|
+
return withToolAnnotations([
|
|
44
73
|
...getLibraryToolDefinitions(),
|
|
45
74
|
...getCIToolDefinitions(),
|
|
46
75
|
...getCDToolDefinitions(),
|
|
47
76
|
...getGrayReleaseToolDefinitions(),
|
|
48
77
|
...getReleaseToolDefinitions(),
|
|
49
78
|
...getJabberToolDefinitions(),
|
|
79
|
+
...getWorkflowToolDefinitions(),
|
|
50
80
|
{
|
|
51
81
|
name: 'transition_issue',
|
|
52
82
|
description: '切換 Jira issue 狀態,用名稱指定(不需要知道 transition ID)',
|
|
@@ -271,13 +301,14 @@ export function getToolDefinitions() {
|
|
|
271
301
|
},
|
|
272
302
|
},
|
|
273
303
|
},
|
|
274
|
-
];
|
|
304
|
+
]);
|
|
275
305
|
}
|
|
276
306
|
|
|
277
307
|
/**
|
|
278
308
|
* 執行 tool,回傳 { content: [{ type: 'text', text }] }
|
|
279
309
|
*/
|
|
280
|
-
export async function executeTool(name, args,
|
|
310
|
+
export async function executeTool(name, args, deps) {
|
|
311
|
+
const {jira, notifier} = deps;
|
|
281
312
|
const poller = new Poller(jira);
|
|
282
313
|
|
|
283
314
|
switch (name) {
|
|
@@ -361,6 +392,22 @@ export async function executeTool(name, args, {jira, notifier}) {
|
|
|
361
392
|
case 'send_jabber_message':
|
|
362
393
|
return handleSendJabberMessage(args, {});
|
|
363
394
|
|
|
395
|
+
case 'run_stg_full_release':
|
|
396
|
+
return handleRunStgFullReleaseWorkflow(args, {
|
|
397
|
+
jira,
|
|
398
|
+
notifier,
|
|
399
|
+
executeToolImpl: deps.executeToolImpl ?? executeTool,
|
|
400
|
+
workflowWaitOptions: deps.workflowWaitOptions,
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
case 'run_lib_to_stg_release':
|
|
404
|
+
return handleRunLibToStgReleaseWorkflow(args, {
|
|
405
|
+
jira,
|
|
406
|
+
notifier,
|
|
407
|
+
executeToolImpl: deps.executeToolImpl ?? executeTool,
|
|
408
|
+
workflowWaitOptions: deps.workflowWaitOptions,
|
|
409
|
+
});
|
|
410
|
+
|
|
364
411
|
case 'link_issues': {
|
|
365
412
|
try {
|
|
366
413
|
const linkType = args.linkType ?? 'Relates';
|
|
@@ -669,7 +716,11 @@ export async function executeTool(name, args, {jira, notifier}) {
|
|
|
669
716
|
if (!t) {
|
|
670
717
|
const issue = await jira.getIssue(deploymentKey);
|
|
671
718
|
const available = transitions.map((t) => t.name).join(', ');
|
|
672
|
-
|
|
719
|
+
if (['To AutoDeploy', 'Trigger AutoDeploy'].includes(step.name)) {
|
|
720
|
+
return error(
|
|
721
|
+
`找不到必要部署 transition「${step.name}」,目前狀態:${issue.fields.status.name},可用:${available || '無'}`,
|
|
722
|
+
);
|
|
723
|
+
}
|
|
673
724
|
log.push(
|
|
674
725
|
` ⚠️ 找不到「${step.name}」(目前狀態:${issue.fields.status.name},可用:${available || '無'}),跳過`,
|
|
675
726
|
);
|
|
@@ -1115,5 +1166,6 @@ function ok(data) {
|
|
|
1115
1166
|
function error(msg) {
|
|
1116
1167
|
return {
|
|
1117
1168
|
content: [{type: 'text', text: `❌ 錯誤: ${msg}`}],
|
|
1169
|
+
isError: true,
|
|
1118
1170
|
};
|
|
1119
1171
|
}
|
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',
|
|
@@ -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
|
+
}
|
package/tools.test.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { test, describe } from 'node:test';
|
|
6
6
|
import assert from 'node:assert/strict';
|
|
7
|
-
import { executeTool } from './tools/index.js';
|
|
7
|
+
import { executeTool, getToolDefinitions } from './tools/index.js';
|
|
8
8
|
|
|
9
9
|
process.env.JIRA_BASE_URL = 'https://jira.test';
|
|
10
10
|
|
|
@@ -118,6 +118,149 @@ function makeMockJira({
|
|
|
118
118
|
|
|
119
119
|
const mockNotifier = { notify: async () => [] };
|
|
120
120
|
|
|
121
|
+
function getToolDefinition(name) {
|
|
122
|
+
return getToolDefinitions().find((tool) => tool.name === name);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
describe('tool schemas — agent contract', () => {
|
|
126
|
+
test('create_cd_ticket schema matches handler-supported arguments', () => {
|
|
127
|
+
const tool = getToolDefinition('create_cd_ticket');
|
|
128
|
+
|
|
129
|
+
assert.deepEqual(tool.inputSchema.required, ['systemCode', 'environment']);
|
|
130
|
+
for (const property of [
|
|
131
|
+
'ciTicket',
|
|
132
|
+
'linkedCiKey',
|
|
133
|
+
'clusterDeploy',
|
|
134
|
+
'moduleChild',
|
|
135
|
+
'restartOnly',
|
|
136
|
+
'extraVars',
|
|
137
|
+
]) {
|
|
138
|
+
assert.ok(tool.inputSchema.properties[property], `missing ${property}`);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('create_library_ticket schema treats module as defaultable', () => {
|
|
143
|
+
const tool = getToolDefinition('create_library_ticket');
|
|
144
|
+
|
|
145
|
+
assert.deepEqual(tool.inputSchema.required, ['systemCode', 'gitBranch']);
|
|
146
|
+
assert.match(tool.inputSchema.properties.module.description, /預設/);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test('release workflows are exposed as MCP tools', () => {
|
|
150
|
+
assert.ok(getToolDefinition('run_stg_full_release'));
|
|
151
|
+
assert.ok(getToolDefinition('run_lib_to_stg_release'));
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test('run_stg_full_release dispatches shared workflow steps', async () => {
|
|
155
|
+
const calls = [];
|
|
156
|
+
const result = await executeTool('run_stg_full_release', {systemCode: 'IBK'}, {
|
|
157
|
+
jira: makeMockJira(),
|
|
158
|
+
notifier: mockNotifier,
|
|
159
|
+
executeToolImpl: async (name, args) => {
|
|
160
|
+
calls.push({name, args});
|
|
161
|
+
const outputs = {
|
|
162
|
+
create_ci_ticket: {issueKey: 'CID-100'},
|
|
163
|
+
build_ticket: {issueKey: args.issueKey, status: 'Compliance Scan'},
|
|
164
|
+
wait_to_stg: {issueKey: args.issueKey, status: 'Wait To STG'},
|
|
165
|
+
create_cd_ticket: {issueKey: 'CID-200'},
|
|
166
|
+
prepare_cd_deployment: {issueKey: args.issueKey, status: 'Deployment Created'},
|
|
167
|
+
trigger_deployment: {cdIssueKey: args.cdIssueKey, status: 'Done'},
|
|
168
|
+
};
|
|
169
|
+
return {content: [{type: 'text', text: JSON.stringify(outputs[name])}]};
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const output = JSON.parse(result.content[0].text);
|
|
174
|
+
assert.equal(output.type, 'STG Full Release');
|
|
175
|
+
assert.equal(output.ciIssueKey, 'CID-100');
|
|
176
|
+
assert.equal(output.cdIssueKey, 'CID-200');
|
|
177
|
+
assert.deepEqual(calls.map((call) => call.name), [
|
|
178
|
+
'create_ci_ticket',
|
|
179
|
+
'build_ticket',
|
|
180
|
+
'wait_to_stg',
|
|
181
|
+
'create_cd_ticket',
|
|
182
|
+
'prepare_cd_deployment',
|
|
183
|
+
'trigger_deployment',
|
|
184
|
+
]);
|
|
185
|
+
assert.equal(calls[3].args.linkedCiKey, 'CID-100');
|
|
186
|
+
assert.equal(calls[5].args.applyForClose, true);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test('run_lib_to_stg_release waits for the Library ticket to reach Released', async () => {
|
|
190
|
+
const calls = [];
|
|
191
|
+
const result = await executeTool('run_lib_to_stg_release', {
|
|
192
|
+
systemCode: 'IBK',
|
|
193
|
+
gitBranch: 'release/v1.5.2.0',
|
|
194
|
+
}, {
|
|
195
|
+
jira: {
|
|
196
|
+
...makeMockJira(),
|
|
197
|
+
getIssue: async () => ({fields: {status: {name: 'WAIT FOR LIB BUILD'}}}),
|
|
198
|
+
},
|
|
199
|
+
notifier: mockNotifier,
|
|
200
|
+
workflowWaitOptions: {timeoutMs: 1, intervalMs: 1},
|
|
201
|
+
executeToolImpl: async (name, args) => {
|
|
202
|
+
calls.push({name, args});
|
|
203
|
+
const outputs = {
|
|
204
|
+
create_library_ticket: {issueKey: 'LIB-100'},
|
|
205
|
+
build_ticket: {issueKey: args.issueKey, status: 'WAIT FOR LIB BUILD'},
|
|
206
|
+
};
|
|
207
|
+
return {content: [{type: 'text', text: JSON.stringify(outputs[name])}]};
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
assert.equal(result.isError, true);
|
|
212
|
+
assert.match(result.content[0].text, /等待 LIB-100 狀態 Released 超時/);
|
|
213
|
+
assert.deepEqual(calls.map((call) => call.name), [
|
|
214
|
+
'create_library_ticket',
|
|
215
|
+
'build_ticket',
|
|
216
|
+
]);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test('workflow tool failures are marked as MCP errors', async () => {
|
|
220
|
+
const result = await executeTool('run_stg_full_release', {systemCode: 'IBK'}, {
|
|
221
|
+
jira: makeMockJira(),
|
|
222
|
+
notifier: mockNotifier,
|
|
223
|
+
executeToolImpl: async () => ({
|
|
224
|
+
content: [{type: 'text', text: '❌ 錯誤: nested tool failed'}],
|
|
225
|
+
isError: true,
|
|
226
|
+
}),
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
assert.equal(result.isError, true);
|
|
230
|
+
assert.match(result.content[0].text, /run_stg_full_release 失敗/);
|
|
231
|
+
assert.match(result.content[0].text, /nested tool failed/);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test('core dispatcher tool failures are marked as MCP errors', async () => {
|
|
235
|
+
const result = await executeTool(
|
|
236
|
+
'prepare_cd_deployment',
|
|
237
|
+
{issueKey: 'CID-1', environment: 'qa'},
|
|
238
|
+
{jira: makeMockJira(), notifier: mockNotifier},
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
assert.equal(result.isError, true);
|
|
242
|
+
assert.match(result.content[0].text, /不支援的 CD 部署環境/);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test('read-only tools expose readOnlyHint metadata for CLI confirmation', () => {
|
|
246
|
+
for (const name of [
|
|
247
|
+
'get_issue_status',
|
|
248
|
+
'list_transitions',
|
|
249
|
+
'get_release_status',
|
|
250
|
+
'get_unreleased_versions',
|
|
251
|
+
'get_release_manager',
|
|
252
|
+
'wait_for_comment',
|
|
253
|
+
]) {
|
|
254
|
+
assert.equal(getToolDefinition(name).annotations?.readOnlyHint, true, `${name} should be read-only`);
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test('write workflow tools are not marked read-only', () => {
|
|
259
|
+
assert.notEqual(getToolDefinition('run_stg_full_release').annotations?.readOnlyHint, true);
|
|
260
|
+
assert.notEqual(getToolDefinition('run_lib_to_stg_release').annotations?.readOnlyHint, true);
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
121
264
|
/** 執行 tool,回傳 createIssue 收到的 fields(合併 updateIssue 的欄位,或 throw 若 ❌) */
|
|
122
265
|
async function getCreatedFields(toolName, args, jiraOpts = {}) {
|
|
123
266
|
const jira = makeMockJira(jiraOpts);
|
|
@@ -1523,7 +1666,10 @@ describe('trigger_deployment', () => {
|
|
|
1523
1666
|
test('環境比對:[STG] summary 對應 stg → 選擇 STG sub-task,不選 UAT', async () => {
|
|
1524
1667
|
const jira = makeTriggerMock({
|
|
1525
1668
|
subTasks: [makeSubTask('CID-9001', '[STG]'), makeSubTask('CID-9002', '[UAT]')],
|
|
1526
|
-
deployTrans: [
|
|
1669
|
+
deployTrans: [
|
|
1670
|
+
{ id: '11', name: 'To AutoDeploy', to: { name: 'Pre Auto Deploy' } },
|
|
1671
|
+
{ id: '12', name: 'Trigger AutoDeploy', to: { name: 'Auto Deploy' } },
|
|
1672
|
+
],
|
|
1527
1673
|
});
|
|
1528
1674
|
const result = await executeTool(
|
|
1529
1675
|
'trigger_deployment',
|
|
@@ -1540,7 +1686,10 @@ describe('trigger_deployment', () => {
|
|
|
1540
1686
|
test('環境比對:[UAT] summary 對應 uat → 選擇 UAT sub-task', async () => {
|
|
1541
1687
|
const jira = makeTriggerMock({
|
|
1542
1688
|
subTasks: [makeSubTask('CID-9001', '[STG]'), makeSubTask('CID-9002', '[UAT]')],
|
|
1543
|
-
deployTrans: [
|
|
1689
|
+
deployTrans: [
|
|
1690
|
+
{ id: '11', name: 'To AutoDeploy', to: { name: 'Pre Auto Deploy' } },
|
|
1691
|
+
{ id: '12', name: 'Trigger AutoDeploy', to: { name: 'Auto Deploy' } },
|
|
1692
|
+
],
|
|
1544
1693
|
});
|
|
1545
1694
|
const result = await executeTool(
|
|
1546
1695
|
'trigger_deployment',
|
|
@@ -1557,7 +1706,10 @@ describe('trigger_deployment', () => {
|
|
|
1557
1706
|
test('無符合環境的 sub-task → fallback 取第一個', async () => {
|
|
1558
1707
|
const jira = makeTriggerMock({
|
|
1559
1708
|
subTasks: [makeSubTask('CID-9001', '[DEV]')],
|
|
1560
|
-
deployTrans: [
|
|
1709
|
+
deployTrans: [
|
|
1710
|
+
{ id: '11', name: 'To AutoDeploy', to: { name: 'Pre Auto Deploy' } },
|
|
1711
|
+
{ id: '12', name: 'Trigger AutoDeploy', to: { name: 'Auto Deploy' } },
|
|
1712
|
+
],
|
|
1561
1713
|
});
|
|
1562
1714
|
const result = await executeTool(
|
|
1563
1715
|
'trigger_deployment',
|
|
@@ -1575,7 +1727,7 @@ describe('trigger_deployment', () => {
|
|
|
1575
1727
|
);
|
|
1576
1728
|
});
|
|
1577
1729
|
|
|
1578
|
-
test('
|
|
1730
|
+
test('找不到必要 deployment transition → 回傳錯誤', async () => {
|
|
1579
1731
|
const jira = makeTriggerMock({
|
|
1580
1732
|
subTasks: [makeSubTask('CID-9001', '[STG]')],
|
|
1581
1733
|
deployTrans: [], // 完全沒有 transitions
|
|
@@ -1588,12 +1740,8 @@ describe('trigger_deployment', () => {
|
|
|
1588
1740
|
notifier: mockNotifier,
|
|
1589
1741
|
},
|
|
1590
1742
|
);
|
|
1591
|
-
assert.
|
|
1592
|
-
|
|
1593
|
-
assert.ok(
|
|
1594
|
-
data.steps.some((s) => s.includes('跳過')),
|
|
1595
|
-
'應記錄跳過',
|
|
1596
|
-
);
|
|
1743
|
+
assert.equal(result.isError, true);
|
|
1744
|
+
assert.match(result.content[0].text, /找不到必要部署 transition/);
|
|
1597
1745
|
});
|
|
1598
1746
|
|
|
1599
1747
|
test('applyForClose=true → 觸發 CD 單的 Apply for close', async () => {
|
|
@@ -1620,7 +1768,10 @@ describe('trigger_deployment', () => {
|
|
|
1620
1768
|
test('applyForClose 預設 false → 不觸發 CD 單 transition', async () => {
|
|
1621
1769
|
const jira = makeTriggerMock({
|
|
1622
1770
|
subTasks: [makeSubTask('CID-9001', '[STG]')],
|
|
1623
|
-
deployTrans: [
|
|
1771
|
+
deployTrans: [
|
|
1772
|
+
{ id: '11', name: 'To AutoDeploy', to: { name: 'Pre Auto Deploy' } },
|
|
1773
|
+
{ id: '12', name: 'Trigger AutoDeploy', to: { name: 'Auto Deploy' } },
|
|
1774
|
+
],
|
|
1624
1775
|
cdTrans: [{ id: '99', name: 'Apply for close', to: { name: 'Wait For Close' } }],
|
|
1625
1776
|
});
|
|
1626
1777
|
await executeTool(
|
|
@@ -1638,7 +1789,10 @@ describe('trigger_deployment', () => {
|
|
|
1638
1789
|
test('applyForClose=true 但 CD 單找不到 Apply for close → 記錄警告不中斷', async () => {
|
|
1639
1790
|
const jira = makeTriggerMock({
|
|
1640
1791
|
subTasks: [makeSubTask('CID-9001', '[STG]')],
|
|
1641
|
-
deployTrans: [
|
|
1792
|
+
deployTrans: [
|
|
1793
|
+
{ id: '11', name: 'To AutoDeploy', to: { name: 'Pre Auto Deploy' } },
|
|
1794
|
+
{ id: '12', name: 'Trigger AutoDeploy', to: { name: 'Auto Deploy' } },
|
|
1795
|
+
],
|
|
1642
1796
|
cdTrans: [], // CD 單沒有 Apply for close
|
|
1643
1797
|
});
|
|
1644
1798
|
const result = await executeTool(
|