@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.test.js
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { test, describe } from 'node:test';
|
|
6
6
|
import assert from 'node:assert/strict';
|
|
7
|
-
import
|
|
7
|
+
import http from 'node:http';
|
|
8
|
+
import { executeTool, getToolDefinitions } from './tools/index.js';
|
|
8
9
|
|
|
9
10
|
process.env.JIRA_BASE_URL = 'https://jira.test';
|
|
10
11
|
|
|
@@ -118,6 +119,154 @@ function makeMockJira({
|
|
|
118
119
|
|
|
119
120
|
const mockNotifier = { notify: async () => [] };
|
|
120
121
|
|
|
122
|
+
function getToolDefinition(name) {
|
|
123
|
+
return getToolDefinitions().find((tool) => tool.name === name);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
describe('tool schemas — agent contract', () => {
|
|
127
|
+
test('create_cd_ticket schema matches handler-supported arguments', () => {
|
|
128
|
+
const tool = getToolDefinition('create_cd_ticket');
|
|
129
|
+
|
|
130
|
+
assert.deepEqual(tool.inputSchema.required, ['systemCode', 'environment']);
|
|
131
|
+
assert.ok(tool.inputSchema.properties.environment.enum.includes('dev'));
|
|
132
|
+
for (const property of [
|
|
133
|
+
'ciTicket',
|
|
134
|
+
'linkedCiKey',
|
|
135
|
+
'clusterDeploy',
|
|
136
|
+
'moduleChild',
|
|
137
|
+
'restartOnly',
|
|
138
|
+
'extraVars',
|
|
139
|
+
]) {
|
|
140
|
+
assert.ok(tool.inputSchema.properties[property], `missing ${property}`);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('create_library_ticket schema treats module as defaultable', () => {
|
|
145
|
+
const tool = getToolDefinition('create_library_ticket');
|
|
146
|
+
|
|
147
|
+
assert.deepEqual(tool.inputSchema.required, ['systemCode', 'gitBranch']);
|
|
148
|
+
assert.match(tool.inputSchema.properties.module.description, /預設/);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('release workflows are exposed as MCP tools', () => {
|
|
152
|
+
assert.ok(getToolDefinition('run_stg_full_release'));
|
|
153
|
+
assert.ok(getToolDefinition('run_lib_to_stg_release'));
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('wait_to_dev is exposed as a CI workflow tool', () => {
|
|
157
|
+
assert.ok(getToolDefinition('wait_to_dev'));
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('run_stg_full_release dispatches shared workflow steps', async () => {
|
|
161
|
+
const calls = [];
|
|
162
|
+
const result = await executeTool('run_stg_full_release', {systemCode: 'IBK'}, {
|
|
163
|
+
jira: makeMockJira(),
|
|
164
|
+
notifier: mockNotifier,
|
|
165
|
+
executeToolImpl: async (name, args) => {
|
|
166
|
+
calls.push({name, args});
|
|
167
|
+
const outputs = {
|
|
168
|
+
create_ci_ticket: {issueKey: 'CID-100'},
|
|
169
|
+
build_ticket: {issueKey: args.issueKey, status: 'Compliance Scan'},
|
|
170
|
+
wait_to_stg: {issueKey: args.issueKey, status: 'Wait To STG'},
|
|
171
|
+
create_cd_ticket: {issueKey: 'CID-200'},
|
|
172
|
+
prepare_cd_deployment: {issueKey: args.issueKey, status: 'Deployment Created'},
|
|
173
|
+
trigger_deployment: {cdIssueKey: args.cdIssueKey, status: 'Done'},
|
|
174
|
+
};
|
|
175
|
+
return {content: [{type: 'text', text: JSON.stringify(outputs[name])}]};
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const output = JSON.parse(result.content[0].text);
|
|
180
|
+
assert.equal(output.type, 'STG Full Release');
|
|
181
|
+
assert.equal(output.ciIssueKey, 'CID-100');
|
|
182
|
+
assert.equal(output.cdIssueKey, 'CID-200');
|
|
183
|
+
assert.deepEqual(calls.map((call) => call.name), [
|
|
184
|
+
'create_ci_ticket',
|
|
185
|
+
'build_ticket',
|
|
186
|
+
'wait_to_stg',
|
|
187
|
+
'create_cd_ticket',
|
|
188
|
+
'prepare_cd_deployment',
|
|
189
|
+
'trigger_deployment',
|
|
190
|
+
]);
|
|
191
|
+
assert.equal(calls[3].args.linkedCiKey, 'CID-100');
|
|
192
|
+
assert.equal(calls[5].args.applyForClose, true);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test('run_lib_to_stg_release waits for the Library ticket to reach Released', async () => {
|
|
196
|
+
const calls = [];
|
|
197
|
+
const result = await executeTool('run_lib_to_stg_release', {
|
|
198
|
+
systemCode: 'IBK',
|
|
199
|
+
gitBranch: 'release/v1.5.2.0',
|
|
200
|
+
}, {
|
|
201
|
+
jira: {
|
|
202
|
+
...makeMockJira(),
|
|
203
|
+
getIssue: async () => ({fields: {status: {name: 'WAIT FOR LIB BUILD'}}}),
|
|
204
|
+
},
|
|
205
|
+
notifier: mockNotifier,
|
|
206
|
+
workflowWaitOptions: {timeoutMs: 1, intervalMs: 1},
|
|
207
|
+
executeToolImpl: async (name, args) => {
|
|
208
|
+
calls.push({name, args});
|
|
209
|
+
const outputs = {
|
|
210
|
+
create_library_ticket: {issueKey: 'LIB-100'},
|
|
211
|
+
build_ticket: {issueKey: args.issueKey, status: 'WAIT FOR LIB BUILD'},
|
|
212
|
+
};
|
|
213
|
+
return {content: [{type: 'text', text: JSON.stringify(outputs[name])}]};
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
assert.equal(result.isError, true);
|
|
218
|
+
assert.match(result.content[0].text, /等待 LIB-100 狀態 Released 超時/);
|
|
219
|
+
assert.deepEqual(calls.map((call) => call.name), [
|
|
220
|
+
'create_library_ticket',
|
|
221
|
+
'build_ticket',
|
|
222
|
+
]);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test('workflow tool failures are marked as MCP errors', async () => {
|
|
226
|
+
const result = await executeTool('run_stg_full_release', {systemCode: 'IBK'}, {
|
|
227
|
+
jira: makeMockJira(),
|
|
228
|
+
notifier: mockNotifier,
|
|
229
|
+
executeToolImpl: async () => ({
|
|
230
|
+
content: [{type: 'text', text: '❌ 錯誤: nested tool failed'}],
|
|
231
|
+
isError: true,
|
|
232
|
+
}),
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
assert.equal(result.isError, true);
|
|
236
|
+
assert.match(result.content[0].text, /run_stg_full_release 失敗/);
|
|
237
|
+
assert.match(result.content[0].text, /nested tool failed/);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test('core dispatcher tool failures are marked as MCP errors', async () => {
|
|
241
|
+
const result = await executeTool(
|
|
242
|
+
'prepare_cd_deployment',
|
|
243
|
+
{issueKey: 'CID-1', environment: 'qa'},
|
|
244
|
+
{jira: makeMockJira(), notifier: mockNotifier},
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
assert.equal(result.isError, true);
|
|
248
|
+
assert.match(result.content[0].text, /不支援的 CD 部署環境/);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test('read-only tools expose readOnlyHint metadata for CLI confirmation', () => {
|
|
252
|
+
for (const name of [
|
|
253
|
+
'get_issue_status',
|
|
254
|
+
'list_transitions',
|
|
255
|
+
'get_release_status',
|
|
256
|
+
'get_unreleased_versions',
|
|
257
|
+
'get_release_manager',
|
|
258
|
+
'wait_for_comment',
|
|
259
|
+
]) {
|
|
260
|
+
assert.equal(getToolDefinition(name).annotations?.readOnlyHint, true, `${name} should be read-only`);
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test('write workflow tools are not marked read-only', () => {
|
|
265
|
+
assert.notEqual(getToolDefinition('run_stg_full_release').annotations?.readOnlyHint, true);
|
|
266
|
+
assert.notEqual(getToolDefinition('run_lib_to_stg_release').annotations?.readOnlyHint, true);
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
121
270
|
/** 執行 tool,回傳 createIssue 收到的 fields(合併 updateIssue 的欄位,或 throw 若 ❌) */
|
|
122
271
|
async function getCreatedFields(toolName, args, jiraOpts = {}) {
|
|
123
272
|
const jira = makeMockJira(jiraOpts);
|
|
@@ -224,6 +373,22 @@ describe('create_library_ticket — fields', () => {
|
|
|
224
373
|
const f = await getCreatedFields('create_library_ticket', BASE);
|
|
225
374
|
assert.equal(f['customfield_13431'], 'release/v1.5.2.0');
|
|
226
375
|
});
|
|
376
|
+
test('通知與回傳使用預設 module,不顯示 undefined', async () => {
|
|
377
|
+
const notifications = [];
|
|
378
|
+
const result = await executeTool(
|
|
379
|
+
'create_library_ticket',
|
|
380
|
+
{systemCode: 'CWA', gitBranch: 'release/v1.0.0'},
|
|
381
|
+
{
|
|
382
|
+
jira: makeMockJira(),
|
|
383
|
+
notifier: {notify: async (key, message) => notifications.push({key, message})},
|
|
384
|
+
},
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
const data = JSON.parse(result.content[0].text);
|
|
388
|
+
assert.equal(data.module, 'cwa');
|
|
389
|
+
assert.match(notifications.at(-1).message, /模組: cwa/);
|
|
390
|
+
assert.doesNotMatch(notifications.at(-1).message, /undefined/);
|
|
391
|
+
});
|
|
227
392
|
});
|
|
228
393
|
|
|
229
394
|
// ═══════════════════════════════════════════════════════════════════
|
|
@@ -448,6 +613,23 @@ describe('create_cd_ticket — fields', () => {
|
|
|
448
613
|
});
|
|
449
614
|
assert.equal(f['customfield_13442'], 'tvdev-ibk-web01');
|
|
450
615
|
});
|
|
616
|
+
test('通知與回傳使用自動推導 cluster,不顯示 undefined', async () => {
|
|
617
|
+
const notifications = [];
|
|
618
|
+
const result = await executeTool(
|
|
619
|
+
'create_cd_ticket',
|
|
620
|
+
{systemCode: 'CWA', environment: 'dev'},
|
|
621
|
+
{
|
|
622
|
+
jira: makeMockJira(),
|
|
623
|
+
notifier: {notify: async (key, message) => notifications.push({key, message})},
|
|
624
|
+
},
|
|
625
|
+
);
|
|
626
|
+
|
|
627
|
+
const data = JSON.parse(result.content[0].text);
|
|
628
|
+
assert.deepEqual(data.clusterDeploy, ['tvdev-cwa-web01']);
|
|
629
|
+
assert.equal(data.isClusterDeploy, false);
|
|
630
|
+
assert.match(notifications.at(-1).message, /Cluster: tvdev-cwa-web01/);
|
|
631
|
+
assert.doesNotMatch(notifications.at(-1).message, /undefined/);
|
|
632
|
+
});
|
|
451
633
|
test('customfield_13437 (CID_extra_vars) IBK STG 自動生成', async () => {
|
|
452
634
|
const f = await getCreatedFields('create_cd_ticket', BASE);
|
|
453
635
|
const ev = JSON.parse(f['customfield_13437']);
|
|
@@ -1205,6 +1387,352 @@ describe('create_grayrelease_ticket — fields', () => {
|
|
|
1205
1387
|
});
|
|
1206
1388
|
});
|
|
1207
1389
|
|
|
1390
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
1391
|
+
// auto_grayrelease approval payload handling
|
|
1392
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
1393
|
+
describe('auto_grayrelease — approval payload handling', () => {
|
|
1394
|
+
const savedEnv = {
|
|
1395
|
+
CONF_BASE_URL: process.env.CONF_BASE_URL,
|
|
1396
|
+
CONF_TOKEN: process.env.CONF_TOKEN,
|
|
1397
|
+
JABBER_NOTIFY_SCRIPT: process.env.JABBER_NOTIFY_SCRIPT,
|
|
1398
|
+
POLL_INTERVAL_MS: process.env.POLL_INTERVAL_MS,
|
|
1399
|
+
POLL_TIMEOUT_MS: process.env.POLL_TIMEOUT_MS,
|
|
1400
|
+
};
|
|
1401
|
+
|
|
1402
|
+
function restoreEnv() {
|
|
1403
|
+
for (const [key, value] of Object.entries(savedEnv)) {
|
|
1404
|
+
if (value === undefined) {
|
|
1405
|
+
delete process.env[key];
|
|
1406
|
+
} else {
|
|
1407
|
+
process.env[key] = value;
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
function makeApprovalJira(environment, {comments = []} = {}) {
|
|
1413
|
+
let status = 'WAIT APPROVAL';
|
|
1414
|
+
const calls = {updateAssignee: [], transitionByName: []};
|
|
1415
|
+
|
|
1416
|
+
return {
|
|
1417
|
+
calls,
|
|
1418
|
+
getIssueFields: async () => ({
|
|
1419
|
+
customfield_13436: {value: environment},
|
|
1420
|
+
customfield_13443: {value: 'CWA'},
|
|
1421
|
+
CID_build_result: 'pass',
|
|
1422
|
+
CID_deploy_result: 'pass',
|
|
1423
|
+
}),
|
|
1424
|
+
getIssue: async () => ({fields: {status: {name: status}, summary: 'GrayRelease'}}),
|
|
1425
|
+
getTransitions: async () => [],
|
|
1426
|
+
updateAssignee: async (issueKey, accountId) => {
|
|
1427
|
+
calls.updateAssignee.push({issueKey, accountId});
|
|
1428
|
+
if (environment === 'stg' || calls.updateAssignee.length >= 2) {
|
|
1429
|
+
status = 'WAIT DEPLOY';
|
|
1430
|
+
}
|
|
1431
|
+
},
|
|
1432
|
+
getComments: async () => comments,
|
|
1433
|
+
searchIssues: async () => [{fields: {customfield_13436: {value: 'stg'}}}],
|
|
1434
|
+
transitionByName: async (issueKey, transitionName) => {
|
|
1435
|
+
calls.transitionByName.push({issueKey, transitionName});
|
|
1436
|
+
if (transitionName === 'To Verify') {
|
|
1437
|
+
status = 'VERIFY';
|
|
1438
|
+
}
|
|
1439
|
+
return {transitioned: transitionName, toStatus: status};
|
|
1440
|
+
},
|
|
1441
|
+
};
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
test('STG approval parses get_release_manager MCP content payload', async () => {
|
|
1445
|
+
const server = http.createServer((_req, res) => {
|
|
1446
|
+
res.setHeader('content-type', 'application/json');
|
|
1447
|
+
res.end(JSON.stringify({events: [{what: 'Sign off staff', who: 'Alvin Wang'}]}));
|
|
1448
|
+
});
|
|
1449
|
+
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
1450
|
+
|
|
1451
|
+
try {
|
|
1452
|
+
process.env.CONF_BASE_URL = `http://127.0.0.1:${server.address().port}`;
|
|
1453
|
+
process.env.CONF_TOKEN = 'test-token';
|
|
1454
|
+
process.env.JABBER_NOTIFY_SCRIPT = '/dev/null';
|
|
1455
|
+
process.env.POLL_INTERVAL_MS = '0';
|
|
1456
|
+
process.env.POLL_TIMEOUT_MS = '10';
|
|
1457
|
+
|
|
1458
|
+
const jira = makeApprovalJira('stg');
|
|
1459
|
+
const result = await executeTool(
|
|
1460
|
+
'auto_grayrelease',
|
|
1461
|
+
{issueKey: 'CID-100', autoVerify: false},
|
|
1462
|
+
{jira, notifier: mockNotifier},
|
|
1463
|
+
);
|
|
1464
|
+
|
|
1465
|
+
assert.ok(!result.content[0].text.startsWith('❌'), 'STG approval should continue');
|
|
1466
|
+
const data = JSON.parse(result.content[0].text);
|
|
1467
|
+
assert.equal(data.finalStatus, 'VERIFY');
|
|
1468
|
+
assert.deepEqual(jira.calls.updateAssignee, [{issueKey: 'CID-100', accountId: 'BK00619'}]);
|
|
1469
|
+
} finally {
|
|
1470
|
+
await new Promise((resolve) => server.close(resolve));
|
|
1471
|
+
restoreEnv();
|
|
1472
|
+
}
|
|
1473
|
+
});
|
|
1474
|
+
|
|
1475
|
+
test('UAT approval parses wait_for_comment MCP content payload', async () => {
|
|
1476
|
+
try {
|
|
1477
|
+
process.env.JABBER_NOTIFY_SCRIPT = '/dev/null';
|
|
1478
|
+
process.env.POLL_INTERVAL_MS = '0';
|
|
1479
|
+
process.env.POLL_TIMEOUT_MS = '10';
|
|
1480
|
+
|
|
1481
|
+
const jira = makeApprovalJira('uat', {
|
|
1482
|
+
comments: [{body: 'Approved, please proceed', author: {name: 'BK00619', displayName: 'James Yu'}}],
|
|
1483
|
+
});
|
|
1484
|
+
const result = await executeTool(
|
|
1485
|
+
'auto_grayrelease',
|
|
1486
|
+
{issueKey: 'CID-101', autoVerify: false},
|
|
1487
|
+
{jira, notifier: mockNotifier},
|
|
1488
|
+
);
|
|
1489
|
+
|
|
1490
|
+
assert.ok(!result.content[0].text.startsWith('❌'), 'UAT approval should continue');
|
|
1491
|
+
const data = JSON.parse(result.content[0].text);
|
|
1492
|
+
assert.equal(data.finalStatus, 'VERIFY');
|
|
1493
|
+
assert.deepEqual(jira.calls.updateAssignee, [
|
|
1494
|
+
{issueKey: 'CID-101', accountId: 'BK00619'},
|
|
1495
|
+
{issueKey: 'CID-101', accountId: 'BK00619'},
|
|
1496
|
+
]);
|
|
1497
|
+
} finally {
|
|
1498
|
+
restoreEnv();
|
|
1499
|
+
}
|
|
1500
|
+
});
|
|
1501
|
+
|
|
1502
|
+
test('WAIT APPROVAL fails when GrayRelease environment field is missing', async () => {
|
|
1503
|
+
const transitions = [];
|
|
1504
|
+
const jira = {
|
|
1505
|
+
getIssueFields: async () => ({
|
|
1506
|
+
customfield_13443: {value: 'CWA'},
|
|
1507
|
+
}),
|
|
1508
|
+
getIssue: async () => ({fields: {status: {name: 'WAIT APPROVAL'}, summary: 'GrayRelease'}}),
|
|
1509
|
+
transitionByName: async (_issueKey, transitionName) => {
|
|
1510
|
+
transitions.push(transitionName);
|
|
1511
|
+
},
|
|
1512
|
+
};
|
|
1513
|
+
|
|
1514
|
+
const result = await executeTool(
|
|
1515
|
+
'auto_grayrelease',
|
|
1516
|
+
{issueKey: 'CID-102', autoVerify: false},
|
|
1517
|
+
{jira, notifier: mockNotifier},
|
|
1518
|
+
);
|
|
1519
|
+
|
|
1520
|
+
assert.ok(result.content[0].text.startsWith('❌'), 'missing environment should fail closed');
|
|
1521
|
+
assert.match(result.content[0].text, /無法讀取 GrayRelease 環境欄位/);
|
|
1522
|
+
assert.deepEqual(transitions, []);
|
|
1523
|
+
});
|
|
1524
|
+
});
|
|
1525
|
+
|
|
1526
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
1527
|
+
// auto_grayrelease build/deploy completion result fields
|
|
1528
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
1529
|
+
describe('auto_grayrelease — build/deploy result fields', () => {
|
|
1530
|
+
const savedEnv = {
|
|
1531
|
+
POLL_INTERVAL_MS: process.env.POLL_INTERVAL_MS,
|
|
1532
|
+
POLL_TIMEOUT_MS: process.env.POLL_TIMEOUT_MS,
|
|
1533
|
+
SWITCH_EXECUTION_NODE_WAIT_MS: process.env.SWITCH_EXECUTION_NODE_WAIT_MS,
|
|
1534
|
+
};
|
|
1535
|
+
|
|
1536
|
+
function useFastPolling() {
|
|
1537
|
+
process.env.POLL_INTERVAL_MS = '0';
|
|
1538
|
+
process.env.POLL_TIMEOUT_MS = '10';
|
|
1539
|
+
process.env.SWITCH_EXECUTION_NODE_WAIT_MS = '0';
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
function restoreEnv() {
|
|
1543
|
+
for (const [key, value] of Object.entries(savedEnv)) {
|
|
1544
|
+
if (value === undefined) {
|
|
1545
|
+
delete process.env[key];
|
|
1546
|
+
} else {
|
|
1547
|
+
process.env[key] = value;
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
test('Build waits until CID_build_result becomes pass before Apply to approval', async () => {
|
|
1553
|
+
useFastPolling();
|
|
1554
|
+
let status = 'WAIT FOR BUILD';
|
|
1555
|
+
let buildResultCalls = 0;
|
|
1556
|
+
const transitions = [];
|
|
1557
|
+
const jira = {
|
|
1558
|
+
getIssueFields: async (_issueKey, fieldNames = []) => {
|
|
1559
|
+
if (fieldNames.includes('CID_build_result')) {
|
|
1560
|
+
buildResultCalls++;
|
|
1561
|
+
return {CID_build_result: buildResultCalls >= 3 ? 'pass' : 'starting'};
|
|
1562
|
+
}
|
|
1563
|
+
return {
|
|
1564
|
+
customfield_13436: {value: 'dev'},
|
|
1565
|
+
customfield_13443: {value: 'CWA'},
|
|
1566
|
+
};
|
|
1567
|
+
},
|
|
1568
|
+
getIssue: async () => ({fields: {status: {name: status}, summary: 'GrayRelease'}}),
|
|
1569
|
+
transitionByName: async (_issueKey, transitionName) => {
|
|
1570
|
+
transitions.push(transitionName);
|
|
1571
|
+
if (transitionName === 'Apply to approval') {
|
|
1572
|
+
status = 'DONE';
|
|
1573
|
+
}
|
|
1574
|
+
},
|
|
1575
|
+
};
|
|
1576
|
+
|
|
1577
|
+
try {
|
|
1578
|
+
const result = await executeTool(
|
|
1579
|
+
'auto_grayrelease',
|
|
1580
|
+
{issueKey: 'CID-200', autoVerify: false},
|
|
1581
|
+
{jira, notifier: mockNotifier},
|
|
1582
|
+
);
|
|
1583
|
+
|
|
1584
|
+
assert.ok(!result.content[0].text.startsWith('❌'), 'Build flow should succeed');
|
|
1585
|
+
assert.equal(buildResultCalls, 3);
|
|
1586
|
+
assert.deepEqual(transitions, ['GrayRelease Build', 'Apply to approval']);
|
|
1587
|
+
} finally {
|
|
1588
|
+
restoreEnv();
|
|
1589
|
+
}
|
|
1590
|
+
});
|
|
1591
|
+
|
|
1592
|
+
test('Deploy waits until CID_deploy_result becomes pass before To Verify', async () => {
|
|
1593
|
+
useFastPolling();
|
|
1594
|
+
let status = 'WAIT DEPLOY';
|
|
1595
|
+
let deployResultCalls = 0;
|
|
1596
|
+
const transitions = [];
|
|
1597
|
+
const jira = {
|
|
1598
|
+
getIssueFields: async (_issueKey, fieldNames = []) => {
|
|
1599
|
+
if (fieldNames.includes('CID_deploy_result')) {
|
|
1600
|
+
deployResultCalls++;
|
|
1601
|
+
return {CID_deploy_result: deployResultCalls >= 3 ? 'pass' : 'starting'};
|
|
1602
|
+
}
|
|
1603
|
+
return {
|
|
1604
|
+
customfield_13436: {value: 'stg'},
|
|
1605
|
+
customfield_13443: {value: 'CWA'},
|
|
1606
|
+
};
|
|
1607
|
+
},
|
|
1608
|
+
getIssue: async () => ({fields: {status: {name: status}, summary: 'GrayRelease'}}),
|
|
1609
|
+
searchIssues: async () => [{fields: {customfield_13436: {value: 'stg'}}}],
|
|
1610
|
+
transitionByName: async (_issueKey, transitionName) => {
|
|
1611
|
+
transitions.push(transitionName);
|
|
1612
|
+
if (transitionName === 'To Verify') {
|
|
1613
|
+
status = 'VERIFY';
|
|
1614
|
+
}
|
|
1615
|
+
},
|
|
1616
|
+
};
|
|
1617
|
+
|
|
1618
|
+
try {
|
|
1619
|
+
const result = await executeTool(
|
|
1620
|
+
'auto_grayrelease',
|
|
1621
|
+
{issueKey: 'CID-201', autoVerify: false},
|
|
1622
|
+
{jira, notifier: mockNotifier},
|
|
1623
|
+
);
|
|
1624
|
+
|
|
1625
|
+
assert.ok(!result.content[0].text.startsWith('❌'), 'Deploy flow should succeed');
|
|
1626
|
+
assert.equal(deployResultCalls, 3);
|
|
1627
|
+
assert.deepEqual(transitions, ['GrayRelease Deploy', 'To Verify']);
|
|
1628
|
+
} finally {
|
|
1629
|
+
restoreEnv();
|
|
1630
|
+
}
|
|
1631
|
+
});
|
|
1632
|
+
|
|
1633
|
+
test('Deploy skips Switch Execution Node when recent cid jira worker success comment exists', async () => {
|
|
1634
|
+
useFastPolling();
|
|
1635
|
+
let status = 'WAIT DEPLOY';
|
|
1636
|
+
const transitions = [];
|
|
1637
|
+
const jira = {
|
|
1638
|
+
getIssueFields: async (_issueKey, fieldNames = []) => {
|
|
1639
|
+
if (fieldNames.includes('CID_deploy_result')) {
|
|
1640
|
+
return {CID_deploy_result: 'pass'};
|
|
1641
|
+
}
|
|
1642
|
+
return {
|
|
1643
|
+
customfield_13436: {value: 'stg'},
|
|
1644
|
+
customfield_13443: {value: 'IBK'},
|
|
1645
|
+
};
|
|
1646
|
+
},
|
|
1647
|
+
getIssue: async () => ({fields: {status: {name: status}, summary: 'GrayRelease'}}),
|
|
1648
|
+
getComments: async () => [
|
|
1649
|
+
{
|
|
1650
|
+
body: "Trigger update IBK's instance_group to [ING]NonPRD_ExecutionNode success, please wait about 3 mins before trigger deploy.",
|
|
1651
|
+
author: {displayName: 'cid jira worker'},
|
|
1652
|
+
created: new Date().toISOString(),
|
|
1653
|
+
},
|
|
1654
|
+
],
|
|
1655
|
+
searchIssues: async () => [{fields: {customfield_13436: {value: 'prd'}}}],
|
|
1656
|
+
transitionByName: async (_issueKey, transitionName) => {
|
|
1657
|
+
transitions.push(transitionName);
|
|
1658
|
+
if (transitionName === 'To Verify') {
|
|
1659
|
+
status = 'VERIFY';
|
|
1660
|
+
}
|
|
1661
|
+
},
|
|
1662
|
+
};
|
|
1663
|
+
|
|
1664
|
+
try {
|
|
1665
|
+
const result = await executeTool(
|
|
1666
|
+
'auto_grayrelease',
|
|
1667
|
+
{issueKey: 'CID-202', autoVerify: false},
|
|
1668
|
+
{jira, notifier: mockNotifier},
|
|
1669
|
+
);
|
|
1670
|
+
|
|
1671
|
+
assert.ok(!result.content[0].text.startsWith('❌'), 'Deploy flow should succeed');
|
|
1672
|
+
assert.deepEqual(transitions, ['GrayRelease Deploy', 'To Verify']);
|
|
1673
|
+
} finally {
|
|
1674
|
+
restoreEnv();
|
|
1675
|
+
}
|
|
1676
|
+
});
|
|
1677
|
+
|
|
1678
|
+
test('Deploy waits for worker success comment after Switch Execution Node then deploys directly', async () => {
|
|
1679
|
+
useFastPolling();
|
|
1680
|
+
let status = 'WAIT DEPLOY';
|
|
1681
|
+
let getCommentsCalls = 0;
|
|
1682
|
+
let searchIssuesCalls = 0;
|
|
1683
|
+
const transitions = [];
|
|
1684
|
+
const jira = {
|
|
1685
|
+
getIssueFields: async (_issueKey, fieldNames = []) => {
|
|
1686
|
+
if (fieldNames.includes('CID_deploy_result')) {
|
|
1687
|
+
return {CID_deploy_result: 'pass'};
|
|
1688
|
+
}
|
|
1689
|
+
return {
|
|
1690
|
+
customfield_13436: {value: 'stg'},
|
|
1691
|
+
customfield_13443: {value: 'IBK'},
|
|
1692
|
+
};
|
|
1693
|
+
},
|
|
1694
|
+
getIssue: async () => ({fields: {status: {name: status}, summary: 'GrayRelease'}}),
|
|
1695
|
+
getComments: async () => {
|
|
1696
|
+
getCommentsCalls++;
|
|
1697
|
+
if (getCommentsCalls < 2) {
|
|
1698
|
+
return [];
|
|
1699
|
+
}
|
|
1700
|
+
return [
|
|
1701
|
+
{
|
|
1702
|
+
body: "Trigger update IBK's instance_group to [ING]NonPRD_ExecutionNode success, please wait about 3 mins before trigger deploy.",
|
|
1703
|
+
author: {displayName: 'CID Jira Worker'},
|
|
1704
|
+
created: new Date().toISOString(),
|
|
1705
|
+
},
|
|
1706
|
+
];
|
|
1707
|
+
},
|
|
1708
|
+
searchIssues: async () => {
|
|
1709
|
+
searchIssuesCalls++;
|
|
1710
|
+
return [{fields: {customfield_13436: {value: 'prd'}}}];
|
|
1711
|
+
},
|
|
1712
|
+
transitionByName: async (_issueKey, transitionName) => {
|
|
1713
|
+
transitions.push(transitionName);
|
|
1714
|
+
if (transitionName === 'To Verify') {
|
|
1715
|
+
status = 'VERIFY';
|
|
1716
|
+
}
|
|
1717
|
+
},
|
|
1718
|
+
};
|
|
1719
|
+
|
|
1720
|
+
try {
|
|
1721
|
+
const result = await executeTool(
|
|
1722
|
+
'auto_grayrelease',
|
|
1723
|
+
{issueKey: 'CID-203', autoVerify: false},
|
|
1724
|
+
{jira, notifier: mockNotifier},
|
|
1725
|
+
);
|
|
1726
|
+
|
|
1727
|
+
assert.ok(!result.content[0].text.startsWith('❌'), 'Deploy flow should succeed');
|
|
1728
|
+
assert.equal(searchIssuesCalls, 1);
|
|
1729
|
+
assert.deepEqual(transitions, ['Switch Execution Node', 'GrayRelease Deploy', 'To Verify']);
|
|
1730
|
+
} finally {
|
|
1731
|
+
restoreEnv();
|
|
1732
|
+
}
|
|
1733
|
+
});
|
|
1734
|
+
});
|
|
1735
|
+
|
|
1208
1736
|
// ═══════════════════════════════════════════════════════════════════
|
|
1209
1737
|
// prepare_cd_deployment
|
|
1210
1738
|
// ═══════════════════════════════════════════════════════════════════
|
|
@@ -1237,6 +1765,19 @@ describe('prepare_cd_deployment', () => {
|
|
|
1237
1765
|
};
|
|
1238
1766
|
}
|
|
1239
1767
|
|
|
1768
|
+
test('dev:更新 customfield_13436 為 id 14355', async () => {
|
|
1769
|
+
const jira = makeDeployMock();
|
|
1770
|
+
await executeTool(
|
|
1771
|
+
'prepare_cd_deployment',
|
|
1772
|
+
{ issueKey: 'CID-1697', environment: 'dev' },
|
|
1773
|
+
{
|
|
1774
|
+
jira,
|
|
1775
|
+
notifier: mockNotifier,
|
|
1776
|
+
},
|
|
1777
|
+
);
|
|
1778
|
+
assert.deepEqual(jira.calls.updateIssue[0].fields['customfield_13436'], { id: '14355' });
|
|
1779
|
+
});
|
|
1780
|
+
|
|
1240
1781
|
test('stg:更新 customfield_13436 為 id 14356', async () => {
|
|
1241
1782
|
const jira = makeDeployMock();
|
|
1242
1783
|
await executeTool(
|
|
@@ -1430,6 +1971,146 @@ describe('prepare_cd_deployment', () => {
|
|
|
1430
1971
|
'第 2 次應觸發 Prepare to create deployment ticket',
|
|
1431
1972
|
);
|
|
1432
1973
|
});
|
|
1974
|
+
|
|
1975
|
+
test('dev:建立 deployment 後自助執行 Apply for approval 與 Approved 到 Wait Deploy', async () => {
|
|
1976
|
+
let callCount = 0;
|
|
1977
|
+
let status = 'TO DO';
|
|
1978
|
+
const calls = { transitionById: [] };
|
|
1979
|
+
const jira = {
|
|
1980
|
+
updateIssue: async () => {},
|
|
1981
|
+
getTransitions: async () => {
|
|
1982
|
+
callCount++;
|
|
1983
|
+
if (callCount === 1) {
|
|
1984
|
+
return [{ id: '71', name: 'Prepare to create deployment ticket', to: { name: 'Prepare For Deploy' } }];
|
|
1985
|
+
}
|
|
1986
|
+
if (callCount === 2) {
|
|
1987
|
+
return [{ id: '21', name: 'Apply for approval', to: { name: 'Wait Approval' } }];
|
|
1988
|
+
}
|
|
1989
|
+
if (callCount === 3) {
|
|
1990
|
+
return [{ id: '72', name: 'Approved', to: { name: 'Wait Deploy' } }];
|
|
1991
|
+
}
|
|
1992
|
+
return [];
|
|
1993
|
+
},
|
|
1994
|
+
transitionById: async (key, id) => {
|
|
1995
|
+
calls.transitionById.push({ key, id });
|
|
1996
|
+
if (id === '71') status = 'Prepare For Deploy';
|
|
1997
|
+
if (id === '21') status = 'Wait Approval';
|
|
1998
|
+
if (id === '72') status = 'Wait Deploy';
|
|
1999
|
+
return {};
|
|
2000
|
+
},
|
|
2001
|
+
getIssue: async () => ({ fields: { status: { name: status }, summary: 'CD' } }),
|
|
2002
|
+
addComment: async () => {},
|
|
2003
|
+
};
|
|
2004
|
+
const result = await executeTool(
|
|
2005
|
+
'prepare_cd_deployment',
|
|
2006
|
+
{ issueKey: 'CID-1697', environment: 'dev' },
|
|
2007
|
+
{
|
|
2008
|
+
jira,
|
|
2009
|
+
notifier: mockNotifier,
|
|
2010
|
+
},
|
|
2011
|
+
);
|
|
2012
|
+
assert.ok(!result.content[0].text.startsWith('❌'), '應成功觸發');
|
|
2013
|
+
const data = JSON.parse(result.content[0].text);
|
|
2014
|
+
assert.equal(data.status, 'Wait Deploy');
|
|
2015
|
+
assert.deepEqual(calls.transitionById.map((call) => call.id), ['71', '21', '72']);
|
|
2016
|
+
});
|
|
2017
|
+
|
|
2018
|
+
test('dev:deployment ticket 已存在時續跑 approval 到 Wait Deploy', async () => {
|
|
2019
|
+
let callCount = 0;
|
|
2020
|
+
let status = 'Prepare For Deploy';
|
|
2021
|
+
const calls = { transitionById: [] };
|
|
2022
|
+
const jira = {
|
|
2023
|
+
updateIssue: async () => {},
|
|
2024
|
+
getTransitions: async () => {
|
|
2025
|
+
callCount++;
|
|
2026
|
+
if (callCount === 1) {
|
|
2027
|
+
return [{ id: '71', name: 'Prepare to create deployment ticket', to: { name: 'Prepare For Deploy' } }];
|
|
2028
|
+
}
|
|
2029
|
+
if (callCount === 2) {
|
|
2030
|
+
return [{ id: '21', name: 'Apply for approval', to: { name: 'Wait Approval' } }];
|
|
2031
|
+
}
|
|
2032
|
+
if (callCount === 3) {
|
|
2033
|
+
return [{ id: '72', name: 'Approved', to: { name: 'Wait Deploy' } }];
|
|
2034
|
+
}
|
|
2035
|
+
return [];
|
|
2036
|
+
},
|
|
2037
|
+
transitionById: async (key, id) => {
|
|
2038
|
+
calls.transitionById.push({ key, id });
|
|
2039
|
+
if (id === '71') throw new Error('Transition id=71 failed: {"errorMessages":["Already create deployment ticket"],"errors":{}}');
|
|
2040
|
+
if (id === '21') status = 'Wait Approval';
|
|
2041
|
+
if (id === '72') status = 'Wait Deploy';
|
|
2042
|
+
return {};
|
|
2043
|
+
},
|
|
2044
|
+
getIssue: async () => ({ fields: { status: { name: status }, summary: 'CD' } }),
|
|
2045
|
+
addComment: async () => {},
|
|
2046
|
+
};
|
|
2047
|
+
const result = await executeTool(
|
|
2048
|
+
'prepare_cd_deployment',
|
|
2049
|
+
{ issueKey: 'CID-1910', environment: 'dev' },
|
|
2050
|
+
{
|
|
2051
|
+
jira,
|
|
2052
|
+
notifier: mockNotifier,
|
|
2053
|
+
},
|
|
2054
|
+
);
|
|
2055
|
+
|
|
2056
|
+
assert.ok(!result.content[0].text.startsWith('❌'), '應成功續跑');
|
|
2057
|
+
const data = JSON.parse(result.content[0].text);
|
|
2058
|
+
assert.equal(data.status, 'Wait Deploy');
|
|
2059
|
+
assert.deepEqual(calls.transitionById.map((call) => call.id), ['71', '21', '72']);
|
|
2060
|
+
assert.ok(data.steps.some((step) => step.includes('Deployment ticket 已存在')));
|
|
2061
|
+
});
|
|
2062
|
+
});
|
|
2063
|
+
|
|
2064
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
2065
|
+
// wait_to_dev
|
|
2066
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
2067
|
+
describe('wait_to_dev', () => {
|
|
2068
|
+
test('執行 Upload Scan Report 與 Accept,停在 Wait To DEV', async () => {
|
|
2069
|
+
const calls = [];
|
|
2070
|
+
const jira = {
|
|
2071
|
+
getTransitions: async () => [
|
|
2072
|
+
{ id: '10', name: 'Upload Scan Report', to: { name: 'Upload Report' } },
|
|
2073
|
+
{ id: '11', name: 'Accept', to: { name: 'Wait To DEV' } },
|
|
2074
|
+
],
|
|
2075
|
+
transitionById: async (key, id) => {
|
|
2076
|
+
calls.push({ key, id });
|
|
2077
|
+
},
|
|
2078
|
+
getIssue: async () => ({ fields: { status: { name: 'Wait To DEV' }, summary: 'CI' } }),
|
|
2079
|
+
addComment: async () => {},
|
|
2080
|
+
};
|
|
2081
|
+
|
|
2082
|
+
const result = await executeTool(
|
|
2083
|
+
'wait_to_dev',
|
|
2084
|
+
{ issueKey: 'CID-1709' },
|
|
2085
|
+
{ jira, notifier: mockNotifier },
|
|
2086
|
+
);
|
|
2087
|
+
|
|
2088
|
+
assert.ok(!result.content[0].text.startsWith('❌'), '應成功觸發');
|
|
2089
|
+
const data = JSON.parse(result.content[0].text);
|
|
2090
|
+
assert.equal(data.status, 'Wait To DEV');
|
|
2091
|
+
assert.deepEqual(calls.map((call) => call.id), ['10', '11']);
|
|
2092
|
+
});
|
|
2093
|
+
|
|
2094
|
+
test('已是 Wait To DEV 時可跳過缺少的 Accept transition', async () => {
|
|
2095
|
+
const calls = [];
|
|
2096
|
+
const jira = {
|
|
2097
|
+
getTransitions: async () => [],
|
|
2098
|
+
transitionById: async (key, id) => {
|
|
2099
|
+
calls.push({ key, id });
|
|
2100
|
+
},
|
|
2101
|
+
getIssue: async () => ({ fields: { status: { name: 'Wait To DEV' }, summary: 'CI' } }),
|
|
2102
|
+
addComment: async () => {},
|
|
2103
|
+
};
|
|
2104
|
+
|
|
2105
|
+
const result = await executeTool(
|
|
2106
|
+
'wait_to_dev',
|
|
2107
|
+
{ issueKey: 'CID-1709' },
|
|
2108
|
+
{ jira, notifier: mockNotifier },
|
|
2109
|
+
);
|
|
2110
|
+
|
|
2111
|
+
assert.ok(!result.content[0].text.startsWith('❌'), '應成功');
|
|
2112
|
+
assert.equal(calls.length, 0);
|
|
2113
|
+
});
|
|
1433
2114
|
});
|
|
1434
2115
|
|
|
1435
2116
|
// ═══════════════════════════════════════════════════════════════════
|
|
@@ -1523,7 +2204,10 @@ describe('trigger_deployment', () => {
|
|
|
1523
2204
|
test('環境比對:[STG] summary 對應 stg → 選擇 STG sub-task,不選 UAT', async () => {
|
|
1524
2205
|
const jira = makeTriggerMock({
|
|
1525
2206
|
subTasks: [makeSubTask('CID-9001', '[STG]'), makeSubTask('CID-9002', '[UAT]')],
|
|
1526
|
-
deployTrans: [
|
|
2207
|
+
deployTrans: [
|
|
2208
|
+
{ id: '11', name: 'To AutoDeploy', to: { name: 'Pre Auto Deploy' } },
|
|
2209
|
+
{ id: '12', name: 'Trigger AutoDeploy', to: { name: 'Auto Deploy' } },
|
|
2210
|
+
],
|
|
1527
2211
|
});
|
|
1528
2212
|
const result = await executeTool(
|
|
1529
2213
|
'trigger_deployment',
|
|
@@ -1540,7 +2224,10 @@ describe('trigger_deployment', () => {
|
|
|
1540
2224
|
test('環境比對:[UAT] summary 對應 uat → 選擇 UAT sub-task', async () => {
|
|
1541
2225
|
const jira = makeTriggerMock({
|
|
1542
2226
|
subTasks: [makeSubTask('CID-9001', '[STG]'), makeSubTask('CID-9002', '[UAT]')],
|
|
1543
|
-
deployTrans: [
|
|
2227
|
+
deployTrans: [
|
|
2228
|
+
{ id: '11', name: 'To AutoDeploy', to: { name: 'Pre Auto Deploy' } },
|
|
2229
|
+
{ id: '12', name: 'Trigger AutoDeploy', to: { name: 'Auto Deploy' } },
|
|
2230
|
+
],
|
|
1544
2231
|
});
|
|
1545
2232
|
const result = await executeTool(
|
|
1546
2233
|
'trigger_deployment',
|
|
@@ -1554,10 +2241,33 @@ describe('trigger_deployment', () => {
|
|
|
1554
2241
|
assert.equal(data.deploymentKey, 'CID-9002', '應選 UAT sub-task');
|
|
1555
2242
|
});
|
|
1556
2243
|
|
|
2244
|
+
test('環境比對:[DEV] summary 對應 dev → 選擇 DEV sub-task', async () => {
|
|
2245
|
+
const jira = makeTriggerMock({
|
|
2246
|
+
subTasks: [makeSubTask('CID-9001', '[DEV]'), makeSubTask('CID-9002', '[STG]')],
|
|
2247
|
+
deployTrans: [
|
|
2248
|
+
{ id: '11', name: 'To AutoDeploy', to: { name: 'Pre Auto Deploy' } },
|
|
2249
|
+
{ id: '12', name: 'Trigger AutoDeploy', to: { name: 'Auto Deploy' } },
|
|
2250
|
+
],
|
|
2251
|
+
});
|
|
2252
|
+
const result = await executeTool(
|
|
2253
|
+
'trigger_deployment',
|
|
2254
|
+
{ cdIssueKey: 'CID-9000', environment: 'dev' },
|
|
2255
|
+
{
|
|
2256
|
+
jira,
|
|
2257
|
+
notifier: mockNotifier,
|
|
2258
|
+
},
|
|
2259
|
+
);
|
|
2260
|
+
const data = JSON.parse(result.content[0].text);
|
|
2261
|
+
assert.equal(data.deploymentKey, 'CID-9001', '應選 DEV sub-task');
|
|
2262
|
+
});
|
|
2263
|
+
|
|
1557
2264
|
test('無符合環境的 sub-task → fallback 取第一個', async () => {
|
|
1558
2265
|
const jira = makeTriggerMock({
|
|
1559
2266
|
subTasks: [makeSubTask('CID-9001', '[DEV]')],
|
|
1560
|
-
deployTrans: [
|
|
2267
|
+
deployTrans: [
|
|
2268
|
+
{ id: '11', name: 'To AutoDeploy', to: { name: 'Pre Auto Deploy' } },
|
|
2269
|
+
{ id: '12', name: 'Trigger AutoDeploy', to: { name: 'Auto Deploy' } },
|
|
2270
|
+
],
|
|
1561
2271
|
});
|
|
1562
2272
|
const result = await executeTool(
|
|
1563
2273
|
'trigger_deployment',
|
|
@@ -1575,7 +2285,7 @@ describe('trigger_deployment', () => {
|
|
|
1575
2285
|
);
|
|
1576
2286
|
});
|
|
1577
2287
|
|
|
1578
|
-
test('
|
|
2288
|
+
test('找不到必要 deployment transition → 回傳錯誤', async () => {
|
|
1579
2289
|
const jira = makeTriggerMock({
|
|
1580
2290
|
subTasks: [makeSubTask('CID-9001', '[STG]')],
|
|
1581
2291
|
deployTrans: [], // 完全沒有 transitions
|
|
@@ -1588,12 +2298,8 @@ describe('trigger_deployment', () => {
|
|
|
1588
2298
|
notifier: mockNotifier,
|
|
1589
2299
|
},
|
|
1590
2300
|
);
|
|
1591
|
-
assert.
|
|
1592
|
-
|
|
1593
|
-
assert.ok(
|
|
1594
|
-
data.steps.some((s) => s.includes('跳過')),
|
|
1595
|
-
'應記錄跳過',
|
|
1596
|
-
);
|
|
2301
|
+
assert.equal(result.isError, true);
|
|
2302
|
+
assert.match(result.content[0].text, /找不到必要部署 transition/);
|
|
1597
2303
|
});
|
|
1598
2304
|
|
|
1599
2305
|
test('applyForClose=true → 觸發 CD 單的 Apply for close', async () => {
|
|
@@ -1620,7 +2326,10 @@ describe('trigger_deployment', () => {
|
|
|
1620
2326
|
test('applyForClose 預設 false → 不觸發 CD 單 transition', async () => {
|
|
1621
2327
|
const jira = makeTriggerMock({
|
|
1622
2328
|
subTasks: [makeSubTask('CID-9001', '[STG]')],
|
|
1623
|
-
deployTrans: [
|
|
2329
|
+
deployTrans: [
|
|
2330
|
+
{ id: '11', name: 'To AutoDeploy', to: { name: 'Pre Auto Deploy' } },
|
|
2331
|
+
{ id: '12', name: 'Trigger AutoDeploy', to: { name: 'Auto Deploy' } },
|
|
2332
|
+
],
|
|
1624
2333
|
cdTrans: [{ id: '99', name: 'Apply for close', to: { name: 'Wait For Close' } }],
|
|
1625
2334
|
});
|
|
1626
2335
|
await executeTool(
|
|
@@ -1638,7 +2347,10 @@ describe('trigger_deployment', () => {
|
|
|
1638
2347
|
test('applyForClose=true 但 CD 單找不到 Apply for close → 記錄警告不中斷', async () => {
|
|
1639
2348
|
const jira = makeTriggerMock({
|
|
1640
2349
|
subTasks: [makeSubTask('CID-9001', '[STG]')],
|
|
1641
|
-
deployTrans: [
|
|
2350
|
+
deployTrans: [
|
|
2351
|
+
{ id: '11', name: 'To AutoDeploy', to: { name: 'Pre Auto Deploy' } },
|
|
2352
|
+
{ id: '12', name: 'Trigger AutoDeploy', to: { name: 'Auto Deploy' } },
|
|
2353
|
+
],
|
|
1642
2354
|
cdTrans: [], // CD 單沒有 Apply for close
|
|
1643
2355
|
});
|
|
1644
2356
|
const result = await executeTool(
|