@jira-deploy/core 1.0.1 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jira-deploy/core",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "type": "module",
5
5
  "main": "./index.js",
6
6
  "repository": {
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', 'ciTicket'],
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
@@ -5,7 +5,10 @@ export function ok(data) {
5
5
  }
6
6
 
7
7
  export function error(msg) {
8
- return {content: [{type: 'text', text: `❌ 錯誤: ${msg}`}]};
8
+ return {
9
+ content: [{type: 'text', text: `❌ 錯誤: ${msg}`}],
10
+ isError: true,
11
+ };
9
12
  }
10
13
 
11
14
  export function today() {
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, {jira, notifier}) {
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
- // transition 不存在但狀態已超前,跳過
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', 'module', 'gitBranch'],
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: [{ id: '11', name: 'To AutoDeploy', to: { name: 'Pre Auto Deploy' } }],
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: [{ id: '11', name: 'To AutoDeploy', to: { name: 'Pre Auto Deploy' } }],
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: [{ id: '11', name: 'To AutoDeploy', to: { name: 'Pre Auto Deploy' } }],
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('找不到 transition → 跳過,不中斷,仍回傳成功', async () => {
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.ok(!result.content[0].text.startsWith('❌'), '應成功回傳(跳過所有 transition)');
1592
- const data = JSON.parse(result.content[0].text);
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: [{ id: '11', name: 'To AutoDeploy', to: { name: 'Pre Auto Deploy' } }],
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: [{ id: '11', name: 'To AutoDeploy', to: { name: 'Pre Auto Deploy' } }],
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(