@jira-deploy/core 1.0.11 → 1.0.13
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/config.js +183 -0
- package/constants/defaults.js +4 -38
- package/constants/environments.js +7 -29
- package/constants/field-ids.js +9 -41
- package/constants/index.js +3 -1
- package/constants/issue-types.js +7 -19
- package/constants/modules.js +5 -32
- package/constants/repos.js +5 -53
- package/constants/server.js +5 -98
- package/constants/system-codes.js +9 -68
- package/constants/users.js +4 -24
- package/jira-client.js +6 -3
- package/package.json +17 -5
- package/platform-config.js +2 -7
- package/scripts/jabber_notify.py +2 -2
- package/tools/grayrelease.js +43 -32
- package/tools/helpers.js +6 -22
- package/tools/index.js +16 -16
- package/tools/jabber.js +3 -3
- package/tools/release.js +17 -11
- package/dry-run.js +0 -695
- package/tools/ci.test.js +0 -154
- package/tools/jabber.test.js +0 -54
- package/tools/release.test.js +0 -137
- package/tools.test.js +0 -2775
package/tools.test.js
DELETED
|
@@ -1,2775 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* index.js fields 單元測試
|
|
3
|
-
* 執行:node --test src/tools.test.js
|
|
4
|
-
*/
|
|
5
|
-
import { test, describe } from 'node:test';
|
|
6
|
-
import assert from 'node:assert/strict';
|
|
7
|
-
import http from 'node:http';
|
|
8
|
-
import { executeTool, getToolDefinitions } from './tools/index.js';
|
|
9
|
-
import { Poller } from './poller.js';
|
|
10
|
-
|
|
11
|
-
process.env.JIRA_BASE_URL = 'https://jira.test';
|
|
12
|
-
|
|
13
|
-
// ── Mock factories ──────────────────────────────────────────────────────────
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* @param {{
|
|
17
|
-
* releaseVersion?: string|null,
|
|
18
|
-
* deployVersionJson?: string|null,
|
|
19
|
-
* issueLinks?: object[],
|
|
20
|
-
* libraryBranches?: Record<string, string>,
|
|
21
|
-
* libraryModules?: Record<string, {parentId:string, childId:string}>,
|
|
22
|
-
* projectVersions?: {id:string, name:string}[],
|
|
23
|
-
* libraryComments?: Record<string, string>,
|
|
24
|
-
* bitbucketLastTag?: string,
|
|
25
|
-
* searchIssuesResult?: object[], // 回傳給 findPrevLibraryDeployedToEnv 的前一張 Library 清單
|
|
26
|
-
* ciFieldsMap?: Record<string, object>, // ciKey → { status, issuelinks }(給 findPrevLibraryDeployedToEnv 用)
|
|
27
|
-
* ciSummary?: string|null, // CI 單 summary(用於 ciReleaseVersion 3rd fallback 測試)
|
|
28
|
-
* }} opts
|
|
29
|
-
*/
|
|
30
|
-
function makeMockJira({
|
|
31
|
-
releaseVersion = null,
|
|
32
|
-
deployVersionJson = null,
|
|
33
|
-
issueLinks = [],
|
|
34
|
-
libraryBranches = {},
|
|
35
|
-
libraryModules = {},
|
|
36
|
-
projectVersions = [],
|
|
37
|
-
libraryComments = {},
|
|
38
|
-
bitbucketLastTag = null,
|
|
39
|
-
searchIssuesResult = [],
|
|
40
|
-
ciFieldsMap = {},
|
|
41
|
-
ciSummary = null,
|
|
42
|
-
} = {}) {
|
|
43
|
-
const calls = {
|
|
44
|
-
createIssue: [],
|
|
45
|
-
linkIssue: [],
|
|
46
|
-
addRemoteLink: [],
|
|
47
|
-
getBitbucketTags: [],
|
|
48
|
-
getBitbucketBranches: [],
|
|
49
|
-
updateIssue: [],
|
|
50
|
-
};
|
|
51
|
-
return {
|
|
52
|
-
calls,
|
|
53
|
-
createIssue: async (fields) => {
|
|
54
|
-
calls.createIssue.push(fields);
|
|
55
|
-
return { id: '999', key: 'CID-9999', self: '' };
|
|
56
|
-
},
|
|
57
|
-
updateIssue: async (key, fields) => {
|
|
58
|
-
calls.updateIssue.push({ key, fields });
|
|
59
|
-
return {};
|
|
60
|
-
},
|
|
61
|
-
getIssueFields: async (key, fieldNames) => {
|
|
62
|
-
// ciFieldsMap 優先(供 findPrevLibraryDeployedToEnv 的 CI 查詢使用)
|
|
63
|
-
if (ciFieldsMap[key]) return ciFieldsMap[key];
|
|
64
|
-
const field = fieldNames[0];
|
|
65
|
-
if (field === 'issuelinks') return { issuelinks: issueLinks };
|
|
66
|
-
if (field === 'customfield_13431') {
|
|
67
|
-
const result = { customfield_13431: libraryBranches[key] ?? '' };
|
|
68
|
-
// 只在明確請求 'comment' 時才回傳(模擬真實 Jira API 只回傳請求的欄位)
|
|
69
|
-
if (fieldNames.includes('comment') && libraryComments[key]) {
|
|
70
|
-
result.comment = { comments: [{ body: libraryComments[key] }] };
|
|
71
|
-
}
|
|
72
|
-
return result;
|
|
73
|
-
}
|
|
74
|
-
// customfield_13702:Library 模組階層欄位(parentId + child.id,用於 extraVars 動態推導)
|
|
75
|
-
if (field === 'customfield_13702') {
|
|
76
|
-
const mod = libraryModules[key];
|
|
77
|
-
if (mod) return { customfield_13702: { id: mod.parentId, child: { id: mod.childId } } };
|
|
78
|
-
return { customfield_13702: null };
|
|
79
|
-
}
|
|
80
|
-
// customfield_13438 = CI_FIELD_IDS.deployVersion(CI 的 deploy version JSON)
|
|
81
|
-
if (field === 'customfield_13438') return { customfield_13438: deployVersionJson };
|
|
82
|
-
// summary field(用於 ciReleaseVersion 3rd fallback)
|
|
83
|
-
if (field === 'summary') return { summary: ciSummary };
|
|
84
|
-
return { [field]: releaseVersion };
|
|
85
|
-
},
|
|
86
|
-
searchIssues: async () => searchIssuesResult,
|
|
87
|
-
getBitbucketTags: async (project, repo, opts) => {
|
|
88
|
-
calls.getBitbucketTags.push({ project, repo, opts });
|
|
89
|
-
return bitbucketLastTag ? [{ displayId: bitbucketLastTag }] : [];
|
|
90
|
-
},
|
|
91
|
-
getBitbucketBranches: async (project, repo, opts) => {
|
|
92
|
-
calls.getBitbucketBranches.push({ project, repo, opts });
|
|
93
|
-
return bitbucketLastTag ? [{ displayId: bitbucketLastTag }] : [];
|
|
94
|
-
},
|
|
95
|
-
getProjectVersions: async (key, { name } = {}) => {
|
|
96
|
-
const v = projectVersions.find((v) => v.name === name);
|
|
97
|
-
return v?.id ?? null;
|
|
98
|
-
},
|
|
99
|
-
getBitbucketFileContent: async (project, repo, filePath, branch) => {
|
|
100
|
-
// 預設:回傳與 branch 對應的 mock XML(版本 = branch 去前綴後 + -0.0.1)
|
|
101
|
-
const fixVer = branch.replace(/^release[\/\-]/, '');
|
|
102
|
-
return `<root><version>${fixVer}-0.0.1</version></root>`;
|
|
103
|
-
},
|
|
104
|
-
linkIssue: async (inward, outward, type) => {
|
|
105
|
-
calls.linkIssue.push({ inward, outward, type });
|
|
106
|
-
return {};
|
|
107
|
-
},
|
|
108
|
-
addRemoteLink: async (issueKey, url, title) => {
|
|
109
|
-
calls.addRemoteLink.push({ issueKey, url, title });
|
|
110
|
-
return {};
|
|
111
|
-
},
|
|
112
|
-
getIssue: async () => ({ fields: { status: { name: 'TO DO' }, summary: 'test' } }),
|
|
113
|
-
getTransitions: async () => [],
|
|
114
|
-
transitionById: async () => { },
|
|
115
|
-
transitionByName: async () => ({ transitioned: 'Test', toStatus: 'Done' }),
|
|
116
|
-
addComment: async () => { },
|
|
117
|
-
getSubTasks: async () => [],
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const mockNotifier = { notify: async () => [] };
|
|
122
|
-
|
|
123
|
-
function getToolDefinition(name) {
|
|
124
|
-
return getToolDefinitions().find((tool) => tool.name === name);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
describe('tool schemas — agent contract', () => {
|
|
128
|
-
test('create_cd_ticket schema matches handler-supported arguments', () => {
|
|
129
|
-
const tool = getToolDefinition('create_cd_ticket');
|
|
130
|
-
|
|
131
|
-
assert.deepEqual(tool.inputSchema.required, ['systemCode', 'environment']);
|
|
132
|
-
assert.ok(tool.inputSchema.properties.environment.enum.includes('dev'));
|
|
133
|
-
for (const property of [
|
|
134
|
-
'ciTicket',
|
|
135
|
-
'linkedCiKey',
|
|
136
|
-
'clusterDeploy',
|
|
137
|
-
'moduleChild',
|
|
138
|
-
'restartOnly',
|
|
139
|
-
'extraVars',
|
|
140
|
-
]) {
|
|
141
|
-
assert.ok(tool.inputSchema.properties[property], `missing ${property}`);
|
|
142
|
-
}
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
test('create_cd_ticket schema requires explicit CD intent and CI ticket context', () => {
|
|
146
|
-
const tool = getToolDefinition('create_cd_ticket');
|
|
147
|
-
|
|
148
|
-
assert.match(tool.description, /明確要求或確認/);
|
|
149
|
-
assert.match(tool.description, /deploy_grayrelease/);
|
|
150
|
-
assert.match(tool.description, /不可直接開 CD 單/);
|
|
151
|
-
assert.match(tool.inputSchema.properties.environment.description, /應先詢問/);
|
|
152
|
-
assert.match(tool.inputSchema.properties.environment.description, /不可.*自行補 stg/);
|
|
153
|
-
assert.match(tool.inputSchema.properties.ciTicket.description, /必須是 CI 單/);
|
|
154
|
-
assert.match(tool.inputSchema.properties.ciTicket.description, /不可使用 GrayRelease 單/);
|
|
155
|
-
assert.match(tool.inputSchema.properties.linkedCiKey.description, /不可使用 GrayRelease 單/);
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
test('create_library_ticket schema treats module as defaultable', () => {
|
|
159
|
-
const tool = getToolDefinition('create_library_ticket');
|
|
160
|
-
|
|
161
|
-
assert.deepEqual(tool.inputSchema.required, ['systemCode', 'gitBranch']);
|
|
162
|
-
assert.match(tool.inputSchema.properties.module.description, /預設/);
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
test('release workflows are exposed as MCP tools', () => {
|
|
166
|
-
assert.ok(getToolDefinition('run_stg_full_release'));
|
|
167
|
-
assert.ok(getToolDefinition('run_lib_to_stg_release'));
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
test('wait_to_dev is exposed as a CI workflow tool', () => {
|
|
171
|
-
assert.ok(getToolDefinition('wait_to_dev'));
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
test('deploy_grayrelease is exposed as the GrayRelease deployment tool', () => {
|
|
175
|
-
const deployTool = getToolDefinition('deploy_grayrelease');
|
|
176
|
-
const buildTool = getToolDefinition('build_ticket');
|
|
177
|
-
|
|
178
|
-
assert.ok(deployTool);
|
|
179
|
-
assert.match(deployTool.description, /與 CD 部署完全分離/);
|
|
180
|
-
assert.match(deployTool.description, /只說 deploy\/部署/);
|
|
181
|
-
assert.match(deployTool.description, /先確認是否部署該 GrayRelease 單/);
|
|
182
|
-
assert.match(deployTool.description, /不會建立 CD 單/);
|
|
183
|
-
assert.match(deployTool.description, /不接受 CI 單/);
|
|
184
|
-
assert.match(deployTool.description, /不會重新 build/);
|
|
185
|
-
assert.match(buildTool.description, /deploy_grayrelease/);
|
|
186
|
-
assert.match(buildTool.description, /不可用此 tool 觸發 rebuild/);
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
test('build_ticket schema is limited to explicit GrayRelease build or rebuild', () => {
|
|
190
|
-
const tool = getToolDefinition('build_ticket');
|
|
191
|
-
|
|
192
|
-
assert.match(tool.description, /GrayRelease/);
|
|
193
|
-
assert.match(tool.description, /build\/rebuild/);
|
|
194
|
-
assert.doesNotMatch(tool.description, /build\/deploy/);
|
|
195
|
-
assert.match(tool.inputSchema.properties.issueKey.description, /GrayRelease/);
|
|
196
|
-
assert.equal(tool.inputSchema.properties.rebuild.type, 'boolean');
|
|
197
|
-
assert.match(tool.inputSchema.properties.rebuild.description, /rebuild=true/);
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
test('build_ticket refuses GrayRelease VERIFY rebuild without rebuild flag', async () => {
|
|
201
|
-
let status = 'VERIFY';
|
|
202
|
-
const transitions = [];
|
|
203
|
-
const jira = {
|
|
204
|
-
...makeMockJira(),
|
|
205
|
-
getIssue: async () => ({ fields: { status: { name: status }, summary: 'GrayRelease' } }),
|
|
206
|
-
getTransitions: async () => {
|
|
207
|
-
if (status === 'VERIFY') return [{ id: '10', name: 'Verify fail', to: { name: 'PLANNING' } }];
|
|
208
|
-
return [];
|
|
209
|
-
},
|
|
210
|
-
transitionById: async (_issueKey, transitionId) => {
|
|
211
|
-
transitions.push(transitionId);
|
|
212
|
-
},
|
|
213
|
-
};
|
|
214
|
-
|
|
215
|
-
const result = await executeTool(
|
|
216
|
-
'build_ticket',
|
|
217
|
-
{ issueKey: 'CID-822' },
|
|
218
|
-
{ jira, notifier: mockNotifier },
|
|
219
|
-
);
|
|
220
|
-
|
|
221
|
-
assert.equal(result.isError, true);
|
|
222
|
-
assert.match(result.content[0].text, /rebuild=true/);
|
|
223
|
-
assert.deepEqual(transitions, []);
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
test('build_ticket resets GrayRelease VERIFY flow before GrayRelease Build when rebuild is explicit', async () => {
|
|
227
|
-
let status = 'VERIFY';
|
|
228
|
-
const transitions = [];
|
|
229
|
-
const jira = {
|
|
230
|
-
...makeMockJira(),
|
|
231
|
-
getIssue: async () => ({ fields: { status: { name: status }, summary: 'GrayRelease' } }),
|
|
232
|
-
getTransitions: async () => {
|
|
233
|
-
if (status === 'VERIFY') return [{ id: '10', name: 'Verify fail', to: { name: 'PLANNING' } }];
|
|
234
|
-
if (status === 'PLANNING') return [{ id: '11', name: 'Accept', to: { name: 'WAIT FOR BUILD' } }];
|
|
235
|
-
if (status === 'WAIT FOR BUILD') {
|
|
236
|
-
return [{ id: '12', name: 'GrayRelease Build', to: { name: 'WAIT FOR BUILD' } }];
|
|
237
|
-
}
|
|
238
|
-
return [];
|
|
239
|
-
},
|
|
240
|
-
transitionById: async (_issueKey, transitionId) => {
|
|
241
|
-
transitions.push(transitionId);
|
|
242
|
-
if (transitionId === '10') status = 'PLANNING';
|
|
243
|
-
if (transitionId === '11') status = 'WAIT FOR BUILD';
|
|
244
|
-
},
|
|
245
|
-
};
|
|
246
|
-
|
|
247
|
-
const result = await executeTool(
|
|
248
|
-
'build_ticket',
|
|
249
|
-
{ issueKey: 'CID-822', rebuild: true },
|
|
250
|
-
{ jira, notifier: mockNotifier },
|
|
251
|
-
);
|
|
252
|
-
|
|
253
|
-
assert.ok(!result.isError, result.content[0].text);
|
|
254
|
-
const output = JSON.parse(result.content[0].text);
|
|
255
|
-
assert.equal(output.status, 'WAIT FOR BUILD');
|
|
256
|
-
assert.deepEqual(transitions, ['10', '11', '12']);
|
|
257
|
-
assert.match(output.steps.join('\n'), /GrayRelease Build/);
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
test('run_stg_full_release dispatches shared workflow steps', async () => {
|
|
261
|
-
const calls = [];
|
|
262
|
-
const result = await executeTool('run_stg_full_release', { systemCode: 'IBK' }, {
|
|
263
|
-
jira: makeMockJira(),
|
|
264
|
-
notifier: mockNotifier,
|
|
265
|
-
executeToolImpl: async (name, args) => {
|
|
266
|
-
calls.push({ name, args });
|
|
267
|
-
const outputs = {
|
|
268
|
-
create_ci_ticket: { issueKey: 'CID-100' },
|
|
269
|
-
build_ticket: { issueKey: args.issueKey, status: 'Compliance Scan' },
|
|
270
|
-
wait_to_stg: { issueKey: args.issueKey, status: 'Wait To STG' },
|
|
271
|
-
create_cd_ticket: { issueKey: 'CID-200' },
|
|
272
|
-
prepare_cd_deployment: { issueKey: args.issueKey, status: 'Deployment Created' },
|
|
273
|
-
trigger_deployment: { cdIssueKey: args.cdIssueKey, status: 'Done' },
|
|
274
|
-
};
|
|
275
|
-
return { content: [{ type: 'text', text: JSON.stringify(outputs[name]) }] };
|
|
276
|
-
},
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
const output = JSON.parse(result.content[0].text);
|
|
280
|
-
assert.equal(output.type, 'STG Full Release');
|
|
281
|
-
assert.equal(output.ciIssueKey, 'CID-100');
|
|
282
|
-
assert.equal(output.cdIssueKey, 'CID-200');
|
|
283
|
-
assert.deepEqual(calls.map((call) => call.name), [
|
|
284
|
-
'create_ci_ticket',
|
|
285
|
-
'build_ticket',
|
|
286
|
-
'wait_to_stg',
|
|
287
|
-
'create_cd_ticket',
|
|
288
|
-
'prepare_cd_deployment',
|
|
289
|
-
'trigger_deployment',
|
|
290
|
-
]);
|
|
291
|
-
assert.equal(calls[3].args.linkedCiKey, 'CID-100');
|
|
292
|
-
assert.equal(calls[5].args.applyForClose, true);
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
test('run_stg_full_release emits route progress for each workflow step', async () => {
|
|
296
|
-
const events = [];
|
|
297
|
-
const result = await executeTool('run_stg_full_release', { systemCode: 'IBK' }, {
|
|
298
|
-
jira: makeMockJira(),
|
|
299
|
-
notifier: mockNotifier,
|
|
300
|
-
progress: (event) => events.push(event),
|
|
301
|
-
executeToolImpl: async (name, args) => {
|
|
302
|
-
const outputs = {
|
|
303
|
-
create_ci_ticket: { issueKey: 'CID-100' },
|
|
304
|
-
build_ticket: { issueKey: args.issueKey, status: 'Compliance Scan' },
|
|
305
|
-
wait_to_stg: { issueKey: args.issueKey, status: 'Wait To STG' },
|
|
306
|
-
create_cd_ticket: { issueKey: 'CID-200' },
|
|
307
|
-
prepare_cd_deployment: { issueKey: args.issueKey, status: 'Deployment Created' },
|
|
308
|
-
trigger_deployment: { cdIssueKey: args.cdIssueKey, status: 'Done' },
|
|
309
|
-
};
|
|
310
|
-
return { content: [{ type: 'text', text: JSON.stringify(outputs[name]) }] };
|
|
311
|
-
},
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
assert.ok(!result.isError, result.content[0].text);
|
|
315
|
-
assert.deepEqual(events.map((event) => event.toolName), [
|
|
316
|
-
'create_ci_ticket',
|
|
317
|
-
'build_ticket',
|
|
318
|
-
'wait_to_stg',
|
|
319
|
-
'create_cd_ticket',
|
|
320
|
-
'prepare_cd_deployment',
|
|
321
|
-
'trigger_deployment',
|
|
322
|
-
]);
|
|
323
|
-
assert.ok(events.every((event) => event.phase === 'action'));
|
|
324
|
-
assert.equal(events[0].title, '執行 workflow step: create_ci_ticket');
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
test('run_lib_to_stg_release waits for the Library ticket to reach Released', async () => {
|
|
328
|
-
const calls = [];
|
|
329
|
-
const result = await executeTool('run_lib_to_stg_release', {
|
|
330
|
-
systemCode: 'IBK',
|
|
331
|
-
gitBranch: 'release/v1.5.2.0',
|
|
332
|
-
}, {
|
|
333
|
-
jira: {
|
|
334
|
-
...makeMockJira(),
|
|
335
|
-
getIssue: async () => ({ fields: { status: { name: 'WAIT FOR LIB BUILD' } } }),
|
|
336
|
-
},
|
|
337
|
-
notifier: mockNotifier,
|
|
338
|
-
workflowWaitOptions: { timeoutMs: 1, intervalMs: 1 },
|
|
339
|
-
executeToolImpl: async (name, args) => {
|
|
340
|
-
calls.push({ name, args });
|
|
341
|
-
const outputs = {
|
|
342
|
-
create_library_ticket: { issueKey: 'LIB-100' },
|
|
343
|
-
build_ticket: { issueKey: args.issueKey, status: 'WAIT FOR LIB BUILD' },
|
|
344
|
-
};
|
|
345
|
-
return { content: [{ type: 'text', text: JSON.stringify(outputs[name]) }] };
|
|
346
|
-
},
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
assert.equal(result.isError, true);
|
|
350
|
-
assert.match(result.content[0].text, /等待 LIB-100 狀態 Released 超時/);
|
|
351
|
-
assert.deepEqual(calls.map((call) => call.name), [
|
|
352
|
-
'create_library_ticket',
|
|
353
|
-
'build_ticket',
|
|
354
|
-
]);
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
test('workflow tool failures are marked as MCP errors', async () => {
|
|
358
|
-
const result = await executeTool('run_stg_full_release', { systemCode: 'IBK' }, {
|
|
359
|
-
jira: makeMockJira(),
|
|
360
|
-
notifier: mockNotifier,
|
|
361
|
-
executeToolImpl: async () => ({
|
|
362
|
-
content: [{ type: 'text', text: '❌ 錯誤: nested tool failed' }],
|
|
363
|
-
isError: true,
|
|
364
|
-
}),
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
assert.equal(result.isError, true);
|
|
368
|
-
assert.match(result.content[0].text, /run_stg_full_release 失敗/);
|
|
369
|
-
assert.match(result.content[0].text, /nested tool failed/);
|
|
370
|
-
});
|
|
371
|
-
|
|
372
|
-
test('core dispatcher tool failures are marked as MCP errors', async () => {
|
|
373
|
-
const result = await executeTool(
|
|
374
|
-
'prepare_cd_deployment',
|
|
375
|
-
{ issueKey: 'CID-1', environment: 'qa' },
|
|
376
|
-
{ jira: makeMockJira(), notifier: mockNotifier },
|
|
377
|
-
);
|
|
378
|
-
|
|
379
|
-
assert.equal(result.isError, true);
|
|
380
|
-
assert.match(result.content[0].text, /不支援的 CD 部署環境/);
|
|
381
|
-
});
|
|
382
|
-
|
|
383
|
-
test('read-only tools expose readOnlyHint metadata for CLI confirmation', () => {
|
|
384
|
-
for (const name of [
|
|
385
|
-
'get_issue_status',
|
|
386
|
-
'list_transitions',
|
|
387
|
-
'get_release_status',
|
|
388
|
-
'get_unreleased_versions',
|
|
389
|
-
'get_release_manager',
|
|
390
|
-
'wait_for_comment',
|
|
391
|
-
]) {
|
|
392
|
-
assert.equal(getToolDefinition(name).annotations?.readOnlyHint, true, `${name} should be read-only`);
|
|
393
|
-
}
|
|
394
|
-
});
|
|
395
|
-
|
|
396
|
-
test('write workflow tools are not marked read-only', () => {
|
|
397
|
-
assert.notEqual(getToolDefinition('run_stg_full_release').annotations?.readOnlyHint, true);
|
|
398
|
-
assert.notEqual(getToolDefinition('run_lib_to_stg_release').annotations?.readOnlyHint, true);
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
test('Poller reports each polling attempt with current status and timing', async () => {
|
|
402
|
-
const events = [];
|
|
403
|
-
let calls = 0;
|
|
404
|
-
const jira = {
|
|
405
|
-
getIssue: async () => {
|
|
406
|
-
calls += 1;
|
|
407
|
-
return {
|
|
408
|
-
fields: {
|
|
409
|
-
status: {
|
|
410
|
-
name: calls === 1 ? 'WAIT APPROVAL' : 'WAIT DEPLOY',
|
|
411
|
-
},
|
|
412
|
-
},
|
|
413
|
-
};
|
|
414
|
-
},
|
|
415
|
-
};
|
|
416
|
-
|
|
417
|
-
const result = await new Poller(jira).waitForStatus('CID-822', 'WAIT DEPLOY', {
|
|
418
|
-
intervalMs: 1,
|
|
419
|
-
timeoutMs: 100,
|
|
420
|
-
onProgress: (event) => events.push(event),
|
|
421
|
-
});
|
|
422
|
-
|
|
423
|
-
assert.equal(result.attempts, 2);
|
|
424
|
-
assert.deepEqual(events.map((event) => event.attempts), [1, 2]);
|
|
425
|
-
assert.equal(events[0].phase, 'polling');
|
|
426
|
-
assert.equal(events[0].issueKey, 'CID-822');
|
|
427
|
-
assert.equal(events[0].currentStatus, 'WAIT APPROVAL');
|
|
428
|
-
assert.equal(events[0].targetStatus, 'WAIT DEPLOY');
|
|
429
|
-
assert.equal(events[0].nextPollMs, 1);
|
|
430
|
-
assert.equal(events[1].currentStatus, 'WAIT DEPLOY');
|
|
431
|
-
});
|
|
432
|
-
|
|
433
|
-
test('wait_for_comment reports each polling attempt before the comment is found', async () => {
|
|
434
|
-
const events = [];
|
|
435
|
-
let calls = 0;
|
|
436
|
-
const jira = {
|
|
437
|
-
getComments: async () => {
|
|
438
|
-
calls += 1;
|
|
439
|
-
if (calls === 1) return [];
|
|
440
|
-
return [
|
|
441
|
-
{
|
|
442
|
-
body: 'Approved',
|
|
443
|
-
author: { name: 'BK00178', displayName: 'James Yu' },
|
|
444
|
-
},
|
|
445
|
-
];
|
|
446
|
-
},
|
|
447
|
-
};
|
|
448
|
-
|
|
449
|
-
const result = await executeTool('wait_for_comment', {
|
|
450
|
-
issueKey: 'CID-822',
|
|
451
|
-
keyword: 'approved',
|
|
452
|
-
authorAccountId: 'BK00178',
|
|
453
|
-
intervalMs: 1,
|
|
454
|
-
timeoutMs: 100,
|
|
455
|
-
}, {
|
|
456
|
-
jira,
|
|
457
|
-
notifier: mockNotifier,
|
|
458
|
-
progress: (event) => events.push(event),
|
|
459
|
-
});
|
|
460
|
-
|
|
461
|
-
const data = JSON.parse(result.content[0].text);
|
|
462
|
-
assert.equal(data.found, true);
|
|
463
|
-
assert.deepEqual(events.map((event) => event.attempts), [1, 2]);
|
|
464
|
-
assert.equal(events[0].phase, 'polling');
|
|
465
|
-
assert.equal(events[0].title, '等待 Jira comment');
|
|
466
|
-
assert.equal(events[0].detail, 'keyword="approved", author=BK00178');
|
|
467
|
-
assert.equal(events[0].issueKey, 'CID-822');
|
|
468
|
-
});
|
|
469
|
-
});
|
|
470
|
-
|
|
471
|
-
/** 執行 tool,回傳 createIssue 收到的 fields(合併 updateIssue 的欄位,或 throw 若 ❌) */
|
|
472
|
-
async function getCreatedFields(toolName, args, jiraOpts = {}) {
|
|
473
|
-
const jira = makeMockJira(jiraOpts);
|
|
474
|
-
const result = await executeTool(toolName, args, { jira, notifier: mockNotifier });
|
|
475
|
-
const text = result.content[0].text;
|
|
476
|
-
if (text.startsWith('❌')) throw new Error(`Tool returned error: ${text}`);
|
|
477
|
-
const created = { ...jira.calls.createIssue[0] };
|
|
478
|
-
// 合併 updateIssue 的欄位(例如 description 是事後透過 updateIssue 寫入)
|
|
479
|
-
for (const update of jira.calls.updateIssue) {
|
|
480
|
-
Object.assign(created, update.fields);
|
|
481
|
-
}
|
|
482
|
-
return created;
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
/** 建立模擬 Library issue link(inward direction) */
|
|
486
|
-
function makeLibraryLink(key) {
|
|
487
|
-
return {
|
|
488
|
-
type: { name: 'Relates', inward: 'is related to', outward: 'relates to' },
|
|
489
|
-
inwardIssue: {
|
|
490
|
-
key,
|
|
491
|
-
fields: { issuetype: { name: 'Library' }, summary: `[IBK][Lib] mock ${key}` },
|
|
492
|
-
},
|
|
493
|
-
};
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
497
|
-
// create_library_ticket
|
|
498
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
499
|
-
describe('create_library_ticket — fields', () => {
|
|
500
|
-
const BASE = { systemCode: 'IBK', moduleChild: 'ibk', repo: 'release/v1.5.2.0' };
|
|
501
|
-
|
|
502
|
-
test('project.key = CID', async () => {
|
|
503
|
-
const f = await getCreatedFields('create_library_ticket', BASE);
|
|
504
|
-
assert.deepEqual(f.project, { key: 'CID' });
|
|
505
|
-
});
|
|
506
|
-
test('issuetype.id = 12501 (Library)', async () => {
|
|
507
|
-
const f = await getCreatedFields('create_library_ticket', BASE);
|
|
508
|
-
assert.deepEqual(f.issuetype, { id: '12501' });
|
|
509
|
-
});
|
|
510
|
-
test('duedate = today (YYYY-MM-DD)', async () => {
|
|
511
|
-
const f = await getCreatedFields('create_library_ticket', BASE);
|
|
512
|
-
assert.match(f.duedate, /^\d{4}-\d{2}-\d{2}$/);
|
|
513
|
-
});
|
|
514
|
-
test('summary 自動生成:[IBK][Lib] Release for v1.5.2.0-0.0.2(XML 現版 0.0.1 +1)', async () => {
|
|
515
|
-
const f = await getCreatedFields('create_library_ticket', BASE);
|
|
516
|
-
assert.equal(f.summary, '[IBK][Lib] Release for v1.5.2.0-0.0.2');
|
|
517
|
-
});
|
|
518
|
-
test('summary 自訂覆蓋', async () => {
|
|
519
|
-
const f = await getCreatedFields('create_library_ticket', {
|
|
520
|
-
...BASE,
|
|
521
|
-
summary: '[Custom] My Lib',
|
|
522
|
-
});
|
|
523
|
-
assert.equal(f.summary, '[Custom] My Lib');
|
|
524
|
-
});
|
|
525
|
-
test('customfield_13702 (libModuleParent) IBK parent=15600, ibk child=15601', async () => {
|
|
526
|
-
const f = await getCreatedFields('create_library_ticket', BASE);
|
|
527
|
-
assert.deepEqual(f['customfield_13702'], { id: '15600', child: { id: '15601' } });
|
|
528
|
-
});
|
|
529
|
-
test('customfield_13702 CWA parent=15513, cwa child=15514', async () => {
|
|
530
|
-
const f = await getCreatedFields('create_library_ticket', {
|
|
531
|
-
systemCode: 'CWA',
|
|
532
|
-
moduleChild: 'cwa',
|
|
533
|
-
repo: 'release/v1.0.0',
|
|
534
|
-
});
|
|
535
|
-
assert.deepEqual(f['customfield_13702'], { id: '15513', child: { id: '15514' } });
|
|
536
|
-
});
|
|
537
|
-
test('customfield_13436 (env) stg → id 14356', async () => {
|
|
538
|
-
const f = await getCreatedFields('create_library_ticket', { ...BASE, environment: 'stg' });
|
|
539
|
-
assert.deepEqual(f['customfield_13436'], { id: '14356' });
|
|
540
|
-
});
|
|
541
|
-
test('customfield_13436 (env) uat → id 14357', async () => {
|
|
542
|
-
const f = await getCreatedFields('create_library_ticket', { ...BASE, environment: 'uat' });
|
|
543
|
-
assert.deepEqual(f['customfield_13436'], { id: '14357' });
|
|
544
|
-
});
|
|
545
|
-
test('customfield_13434 (deptCode) IBK → CH014 → id 14520', async () => {
|
|
546
|
-
const f = await getCreatedFields('create_library_ticket', BASE);
|
|
547
|
-
assert.deepEqual(f['customfield_13434'], { id: '14520' });
|
|
548
|
-
});
|
|
549
|
-
test('customfield_13434 (deptCode) EVT → CH015 → id 14521', async () => {
|
|
550
|
-
const f = await getCreatedFields('create_library_ticket', {
|
|
551
|
-
systemCode: 'EVT',
|
|
552
|
-
moduleChild: 'evt007',
|
|
553
|
-
repo: 'release/v1.0.0',
|
|
554
|
-
});
|
|
555
|
-
assert.deepEqual(f['customfield_13434'], { id: '14521' });
|
|
556
|
-
});
|
|
557
|
-
test('customfield_13807 (fortifyScan) 預設 = scanned (14602)', async () => {
|
|
558
|
-
const f = await getCreatedFields('create_library_ticket', BASE);
|
|
559
|
-
assert.deepEqual(f['customfield_13807'], { id: '14602' });
|
|
560
|
-
});
|
|
561
|
-
test('customfield_13807 (fortifyScan) fortifyScan=true → scanned (14602)', async () => {
|
|
562
|
-
const f = await getCreatedFields('create_library_ticket', { ...BASE, fortifyScan: true });
|
|
563
|
-
assert.deepEqual(f['customfield_13807'], { id: '14602' });
|
|
564
|
-
});
|
|
565
|
-
test('customfield_13807 (fortifyScan) fortifyScan=false → notScanned (14603)', async () => {
|
|
566
|
-
const f = await getCreatedFields('create_library_ticket', { ...BASE, fortifyScan: false });
|
|
567
|
-
assert.deepEqual(f['customfield_13807'], { id: '14603' });
|
|
568
|
-
});
|
|
569
|
-
test('customfield_14702 (jenkinsBranch) = master', async () => {
|
|
570
|
-
const f = await getCreatedFields('create_library_ticket', BASE);
|
|
571
|
-
assert.equal(f['customfield_14702'], 'master');
|
|
572
|
-
});
|
|
573
|
-
test('customfield_13431 (gitBranch) = repo arg', async () => {
|
|
574
|
-
const f = await getCreatedFields('create_library_ticket', BASE);
|
|
575
|
-
assert.equal(f['customfield_13431'], 'release/v1.5.2.0');
|
|
576
|
-
});
|
|
577
|
-
test('通知與回傳使用預設 module,不顯示 undefined', async () => {
|
|
578
|
-
const notifications = [];
|
|
579
|
-
const result = await executeTool(
|
|
580
|
-
'create_library_ticket',
|
|
581
|
-
{ systemCode: 'CWA', gitBranch: 'release/v1.0.0' },
|
|
582
|
-
{
|
|
583
|
-
jira: makeMockJira(),
|
|
584
|
-
notifier: { notify: async (key, message) => notifications.push({ key, message }) },
|
|
585
|
-
},
|
|
586
|
-
);
|
|
587
|
-
|
|
588
|
-
const data = JSON.parse(result.content[0].text);
|
|
589
|
-
assert.equal(data.module, 'cwa');
|
|
590
|
-
assert.match(notifications.at(-1).message, /模組: cwa/);
|
|
591
|
-
assert.doesNotMatch(notifications.at(-1).message, /undefined/);
|
|
592
|
-
});
|
|
593
|
-
});
|
|
594
|
-
|
|
595
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
596
|
-
// create_ci_ticket
|
|
597
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
598
|
-
describe('create_ci_ticket — fields', () => {
|
|
599
|
-
const BASE = { systemCode: 'IBK' };
|
|
600
|
-
|
|
601
|
-
test('project.key = CID', async () => {
|
|
602
|
-
const f = await getCreatedFields('create_ci_ticket', BASE);
|
|
603
|
-
assert.deepEqual(f.project, { key: 'CID' });
|
|
604
|
-
});
|
|
605
|
-
test('issuetype.id = 12305 (CI)', async () => {
|
|
606
|
-
const f = await getCreatedFields('create_ci_ticket', BASE);
|
|
607
|
-
assert.deepEqual(f.issuetype, { id: '12305' });
|
|
608
|
-
});
|
|
609
|
-
test('duedate = today (YYYY-MM-DD)', async () => {
|
|
610
|
-
const f = await getCreatedFields('create_ci_ticket', BASE);
|
|
611
|
-
assert.match(f.duedate, /^\d{4}-\d{2}-\d{2}$/);
|
|
612
|
-
});
|
|
613
|
-
test('summary with goldenImageVersion:[IBK][CI] IBK & WEALTH & SSR & A11Y for 0.0.1', async () => {
|
|
614
|
-
const f = await getCreatedFields('create_ci_ticket', { ...BASE, goldenImageVersion: '0.0.1' });
|
|
615
|
-
assert.equal(f.summary, '[IBK][CI] IBK & WEALTH & SSR & A11Y for 0.0.1');
|
|
616
|
-
});
|
|
617
|
-
test('summary without goldenImageVersion:[IBK][CI] IBK & WEALTH & SSR & A11Y', async () => {
|
|
618
|
-
const f = await getCreatedFields('create_ci_ticket', BASE);
|
|
619
|
-
assert.equal(f.summary, '[IBK][CI] IBK & WEALTH & SSR & A11Y');
|
|
620
|
-
});
|
|
621
|
-
test('summary CWA single module:[CWA][CI] CWA for 1.0.0', async () => {
|
|
622
|
-
const f = await getCreatedFields('create_ci_ticket', {
|
|
623
|
-
systemCode: 'CWA',
|
|
624
|
-
goldenImageVersion: '1.0.0',
|
|
625
|
-
});
|
|
626
|
-
assert.equal(f.summary, '[CWA][CI] CWA for 1.0.0');
|
|
627
|
-
});
|
|
628
|
-
test('summary 自訂覆蓋', async () => {
|
|
629
|
-
const f = await getCreatedFields('create_ci_ticket', { ...BASE, summary: '[Custom] CI' });
|
|
630
|
-
assert.equal(f.summary, '[Custom] CI');
|
|
631
|
-
});
|
|
632
|
-
test('customfield_13443 (systemCode) = { value: IBK }', async () => {
|
|
633
|
-
const f = await getCreatedFields('create_ci_ticket', BASE);
|
|
634
|
-
assert.deepEqual(f['customfield_13443'], { value: 'IBK' });
|
|
635
|
-
});
|
|
636
|
-
test('customfield_13436 (env) 預設 stg → id 14356', async () => {
|
|
637
|
-
const f = await getCreatedFields('create_ci_ticket', BASE);
|
|
638
|
-
assert.deepEqual(f['customfield_13436'], { id: '14356' });
|
|
639
|
-
});
|
|
640
|
-
test('customfield_13436 (env) uat → id 14357', async () => {
|
|
641
|
-
const f = await getCreatedFields('create_ci_ticket', { ...BASE, environment: 'uat' });
|
|
642
|
-
assert.deepEqual(f['customfield_13436'], { id: '14357' });
|
|
643
|
-
});
|
|
644
|
-
test('customfield_13434 (deptCode) IBK → CH014 → id 14520', async () => {
|
|
645
|
-
const f = await getCreatedFields('create_ci_ticket', BASE);
|
|
646
|
-
assert.deepEqual(f['customfield_13434'], { id: '14520' });
|
|
647
|
-
});
|
|
648
|
-
test('customfield_13434 (deptCode) BOF → CH015 → id 14521', async () => {
|
|
649
|
-
const f = await getCreatedFields('create_ci_ticket', { systemCode: 'BOF' });
|
|
650
|
-
assert.deepEqual(f['customfield_13434'], { id: '14521' });
|
|
651
|
-
});
|
|
652
|
-
test('customfield_13444 (systemModule) = assembly (id 14369)', async () => {
|
|
653
|
-
const f = await getCreatedFields('create_ci_ticket', BASE);
|
|
654
|
-
assert.deepEqual(f['customfield_13444'], { id: '14369' });
|
|
655
|
-
});
|
|
656
|
-
test('customfield_13431 (gitBranch) = master', async () => {
|
|
657
|
-
const f = await getCreatedFields('create_ci_ticket', BASE);
|
|
658
|
-
assert.equal(f['customfield_13431'], 'master');
|
|
659
|
-
});
|
|
660
|
-
test('relatesTo: 呼叫 linkIssue(ci, lib, Relates)', async () => {
|
|
661
|
-
const jira = makeMockJira();
|
|
662
|
-
await executeTool(
|
|
663
|
-
'create_ci_ticket',
|
|
664
|
-
{ ...BASE, relatesTo: 'CID-1675' },
|
|
665
|
-
{ jira, notifier: mockNotifier },
|
|
666
|
-
);
|
|
667
|
-
assert.equal(jira.calls.linkIssue.length, 1);
|
|
668
|
-
assert.equal(jira.calls.linkIssue[0].inward, 'CID-9999');
|
|
669
|
-
assert.equal(jira.calls.linkIssue[0].outward, 'CID-1675');
|
|
670
|
-
assert.equal(jira.calls.linkIssue[0].type, 'Relates');
|
|
671
|
-
});
|
|
672
|
-
test('沒有 relatesTo: 不呼叫 linkIssue', async () => {
|
|
673
|
-
const jira = makeMockJira();
|
|
674
|
-
await executeTool('create_ci_ticket', BASE, { jira, notifier: mockNotifier });
|
|
675
|
-
assert.equal(jira.calls.linkIssue.length, 0);
|
|
676
|
-
});
|
|
677
|
-
});
|
|
678
|
-
|
|
679
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
680
|
-
// create_cd_ticket — fields
|
|
681
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
682
|
-
describe('create_cd_ticket — fields', () => {
|
|
683
|
-
const BASE = {
|
|
684
|
-
systemCode: 'IBK',
|
|
685
|
-
environment: 'stg',
|
|
686
|
-
clusterDeploy: 'tvstg-ibk-web01,tvstg-ibk-web02',
|
|
687
|
-
linkedCiKey: 'CID-1677',
|
|
688
|
-
};
|
|
689
|
-
|
|
690
|
-
test('project.key = CID', async () => {
|
|
691
|
-
const f = await getCreatedFields('create_cd_ticket', BASE);
|
|
692
|
-
assert.deepEqual(f.project, { key: 'CID' });
|
|
693
|
-
});
|
|
694
|
-
test('issuetype.id = 12306 (CD)', async () => {
|
|
695
|
-
const f = await getCreatedFields('create_cd_ticket', BASE);
|
|
696
|
-
assert.deepEqual(f.issuetype, { id: '12306' });
|
|
697
|
-
});
|
|
698
|
-
test('summary 使用 CI release_version:包含版號且前綴 [IBK][STG]', async () => {
|
|
699
|
-
const f = await getCreatedFields('create_cd_ticket', BASE, { releaseVersion: '1.5.2.0' });
|
|
700
|
-
assert.ok(f.summary.startsWith('[IBK][STG]'), `prefix 錯誤: "${f.summary}"`);
|
|
701
|
-
assert.ok(f.summary.includes('1.5.2.0'), `版號缺失: "${f.summary}"`);
|
|
702
|
-
});
|
|
703
|
-
test('summary fallback to linkedCiKey when release_version 為空', async () => {
|
|
704
|
-
const f = await getCreatedFields('create_cd_ticket', BASE, { releaseVersion: null });
|
|
705
|
-
assert.ok(f.summary.includes('CID-1677'), `fallback 失敗: "${f.summary}"`);
|
|
706
|
-
});
|
|
707
|
-
test('summary 使用 CI deploy_version JSON:包含版號且前綴 [IBK][STG]', async () => {
|
|
708
|
-
const f = await getCreatedFields('create_cd_ticket', BASE, {
|
|
709
|
-
deployVersionJson: '{"ibk_ap_version":"1.5.2.0"}',
|
|
710
|
-
});
|
|
711
|
-
assert.ok(f.summary.startsWith('[IBK][STG]'), `prefix 錯誤: "${f.summary}"`);
|
|
712
|
-
assert.ok(f.summary.includes('1.5.2.0'), `版號缺失: "${f.summary}"`);
|
|
713
|
-
});
|
|
714
|
-
test('summary fallback to linkedCiKey when deploy_version 為空', async () => {
|
|
715
|
-
const f = await getCreatedFields('create_cd_ticket', BASE, { deployVersionJson: null });
|
|
716
|
-
assert.ok(f.summary.includes('CID-1677'), `fallback 失敗: "${f.summary}"`);
|
|
717
|
-
});
|
|
718
|
-
test('summary prd env → displayEnv = PRD/DR', async () => {
|
|
719
|
-
const f = await getCreatedFields('create_cd_ticket', {
|
|
720
|
-
systemCode: 'IBK',
|
|
721
|
-
environment: 'prd',
|
|
722
|
-
clusterDeploy: 'tvprd-ibk-web01',
|
|
723
|
-
});
|
|
724
|
-
assert.ok(f.summary.includes('[IBK][PRD/DR]'), `displayEnv 錯誤: "${f.summary}"`);
|
|
725
|
-
});
|
|
726
|
-
test('summary prd/dr env → displayEnv = PRD/DR', async () => {
|
|
727
|
-
const f = await getCreatedFields('create_cd_ticket', {
|
|
728
|
-
systemCode: 'IBK',
|
|
729
|
-
environment: 'prd/dr',
|
|
730
|
-
clusterDeploy:
|
|
731
|
-
'tvprd-ibk-web01,tvprd-ibk-web02,tvprd-ibk-web03,tvprd-ibk-web04,tvprd-ibk-web05,lvprd-ibk-web01,lvprd-ibk-web02',
|
|
732
|
-
});
|
|
733
|
-
assert.ok(f.summary.includes('[IBK][PRD/DR]'), `prd/dr displayEnv 錯誤: "${f.summary}"`);
|
|
734
|
-
});
|
|
735
|
-
test('summary 自訂覆蓋', async () => {
|
|
736
|
-
const f = await getCreatedFields('create_cd_ticket', { ...BASE, summary: '[Custom] CD' });
|
|
737
|
-
assert.equal(f.summary, '[Custom] CD');
|
|
738
|
-
});
|
|
739
|
-
test('customfield_13436 (CID_env) stg → id 14356', async () => {
|
|
740
|
-
const f = await getCreatedFields('create_cd_ticket', BASE);
|
|
741
|
-
assert.deepEqual(f['customfield_13436'], { id: '14356' });
|
|
742
|
-
});
|
|
743
|
-
test('customfield_13436 (CID_env) uat → id 14357', async () => {
|
|
744
|
-
const f = await getCreatedFields('create_cd_ticket', {
|
|
745
|
-
systemCode: 'IBK',
|
|
746
|
-
environment: 'uat',
|
|
747
|
-
clusterDeploy: 'tvuat-ibk-web01',
|
|
748
|
-
});
|
|
749
|
-
assert.deepEqual(f['customfield_13436'], { id: '14357' });
|
|
750
|
-
});
|
|
751
|
-
test('customfield_13434 (CID_dept_code) IBK → CH014 → id 14520', async () => {
|
|
752
|
-
const f = await getCreatedFields('create_cd_ticket', BASE);
|
|
753
|
-
assert.deepEqual(f['customfield_13434'], { id: '14520' });
|
|
754
|
-
});
|
|
755
|
-
test('customfield_13443 (CID_system_code) = { value: IBK }', async () => {
|
|
756
|
-
const f = await getCreatedFields('create_cd_ticket', BASE);
|
|
757
|
-
assert.deepEqual(f['customfield_13443'], { value: 'IBK' });
|
|
758
|
-
});
|
|
759
|
-
test('customfield_14704 (CID_cluster_deploy) STG → true (id 15506)', async () => {
|
|
760
|
-
const f = await getCreatedFields('create_cd_ticket', BASE);
|
|
761
|
-
assert.deepEqual(f['customfield_14704'], { id: '15506' });
|
|
762
|
-
});
|
|
763
|
-
test('customfield_14704 (CID_cluster_deploy) DEV → false (id 15505)', async () => {
|
|
764
|
-
const f = await getCreatedFields('create_cd_ticket', {
|
|
765
|
-
systemCode: 'IBK',
|
|
766
|
-
environment: 'dev',
|
|
767
|
-
clusterDeploy: 'tvdev-ibk-web01',
|
|
768
|
-
});
|
|
769
|
-
assert.deepEqual(f['customfield_14704'], { id: '15505' });
|
|
770
|
-
});
|
|
771
|
-
test('customfield_14704 (CID_cluster_deploy) UAT → true (id 15506)', async () => {
|
|
772
|
-
const f = await getCreatedFields('create_cd_ticket', {
|
|
773
|
-
systemCode: 'IBK',
|
|
774
|
-
environment: 'uat',
|
|
775
|
-
clusterDeploy: 'tvuat-ibk-web01',
|
|
776
|
-
});
|
|
777
|
-
assert.deepEqual(f['customfield_14704'], { id: '15506' });
|
|
778
|
-
});
|
|
779
|
-
test('customfield_14101 (CID_deploy_version) = CI deploy_version JSON(完整字串)', async () => {
|
|
780
|
-
const deployJson = '{"ibk_ap_version":"1.5.2.0"}';
|
|
781
|
-
const f = await getCreatedFields('create_cd_ticket', BASE, { deployVersionJson: deployJson });
|
|
782
|
-
assert.equal(f['customfield_14101'], deployJson);
|
|
783
|
-
});
|
|
784
|
-
test('customfield_14101 不設定 when deploy_version 為空', async () => {
|
|
785
|
-
const f = await getCreatedFields('create_cd_ticket', BASE, { deployVersionJson: null });
|
|
786
|
-
assert.equal(f['customfield_14101'], undefined);
|
|
787
|
-
});
|
|
788
|
-
test('customfield_14101 (CID_deploy_version) = CI release_version', async () => {
|
|
789
|
-
const f = await getCreatedFields('create_cd_ticket', BASE, { releaseVersion: '1.5.2.0' });
|
|
790
|
-
assert.equal(f['customfield_14101'], '1.5.2.0');
|
|
791
|
-
});
|
|
792
|
-
test('customfield_14101 不設定 when release_version 為空', async () => {
|
|
793
|
-
const f = await getCreatedFields('create_cd_ticket', BASE, { releaseVersion: null });
|
|
794
|
-
assert.equal(f['customfield_14101'], undefined);
|
|
795
|
-
});
|
|
796
|
-
test('customfield_13436 (CID_env) prd/dr → id 14360', async () => {
|
|
797
|
-
const f = await getCreatedFields('create_cd_ticket', {
|
|
798
|
-
systemCode: 'IBK',
|
|
799
|
-
environment: 'prd/dr',
|
|
800
|
-
clusterDeploy:
|
|
801
|
-
'tvprd-ibk-web01,tvprd-ibk-web02,tvprd-ibk-web03,tvprd-ibk-web04,tvprd-ibk-web05,lvprd-ibk-web01,lvprd-ibk-web02',
|
|
802
|
-
});
|
|
803
|
-
assert.deepEqual(f['customfield_13436'], { id: '14360' });
|
|
804
|
-
});
|
|
805
|
-
test('customfield_13442 (CID_server_list) = 換行分隔 cluster 清單', async () => {
|
|
806
|
-
const f = await getCreatedFields('create_cd_ticket', BASE);
|
|
807
|
-
assert.equal(f['customfield_13442'], 'tvstg-ibk-web01\ntvstg-ibk-web02');
|
|
808
|
-
});
|
|
809
|
-
test('customfield_13442 單台 cluster = 無換行', async () => {
|
|
810
|
-
const f = await getCreatedFields('create_cd_ticket', {
|
|
811
|
-
systemCode: 'IBK',
|
|
812
|
-
environment: 'dev',
|
|
813
|
-
clusterDeploy: 'tvdev-ibk-web01',
|
|
814
|
-
});
|
|
815
|
-
assert.equal(f['customfield_13442'], 'tvdev-ibk-web01');
|
|
816
|
-
});
|
|
817
|
-
test('通知與回傳使用自動推導 cluster,不顯示 undefined', async () => {
|
|
818
|
-
const notifications = [];
|
|
819
|
-
const result = await executeTool(
|
|
820
|
-
'create_cd_ticket',
|
|
821
|
-
{ systemCode: 'CWA', environment: 'dev' },
|
|
822
|
-
{
|
|
823
|
-
jira: makeMockJira(),
|
|
824
|
-
notifier: { notify: async (key, message) => notifications.push({ key, message }) },
|
|
825
|
-
},
|
|
826
|
-
);
|
|
827
|
-
|
|
828
|
-
const data = JSON.parse(result.content[0].text);
|
|
829
|
-
assert.deepEqual(data.clusterDeploy, ['tvdev-cwa-web01']);
|
|
830
|
-
assert.equal(data.isClusterDeploy, false);
|
|
831
|
-
assert.match(notifications.at(-1).message, /Cluster: tvdev-cwa-web01/);
|
|
832
|
-
assert.doesNotMatch(notifications.at(-1).message, /undefined/);
|
|
833
|
-
});
|
|
834
|
-
test('customfield_13437 (CID_extra_vars) IBK STG 自動生成', async () => {
|
|
835
|
-
const f = await getCreatedFields('create_cd_ticket', BASE);
|
|
836
|
-
const ev = JSON.parse(f['customfield_13437']);
|
|
837
|
-
const serverMap = { 'tvstg-ibk-web01': true, 'tvstg-ibk-web02': true };
|
|
838
|
-
assert.deepEqual(ev, {
|
|
839
|
-
ibk_ibk_installation: serverMap,
|
|
840
|
-
ibk_wealth_installation: serverMap,
|
|
841
|
-
ibk_ssr_installation: serverMap,
|
|
842
|
-
ibk_a11y_installation: serverMap,
|
|
843
|
-
});
|
|
844
|
-
});
|
|
845
|
-
test('customfield_13437 CWA STG 自動生成', async () => {
|
|
846
|
-
const f = await getCreatedFields('create_cd_ticket', {
|
|
847
|
-
systemCode: 'CWA',
|
|
848
|
-
environment: 'stg',
|
|
849
|
-
clusterDeploy: 'tvstg-cwa-web01,tvstg-cwa-web02',
|
|
850
|
-
});
|
|
851
|
-
const ev = JSON.parse(f['customfield_13437']);
|
|
852
|
-
assert.deepEqual(ev, {
|
|
853
|
-
cwa_cwa_installation: { 'tvstg-cwa-web01': true, 'tvstg-cwa-web02': true },
|
|
854
|
-
});
|
|
855
|
-
});
|
|
856
|
-
test('customfield_13437 extraVars 自訂覆蓋', async () => {
|
|
857
|
-
const custom = '{"ibk_ibk_installation":{"tvstg-ibk-web05":true}}';
|
|
858
|
-
const f = await getCreatedFields('create_cd_ticket', { ...BASE, extraVars: custom });
|
|
859
|
-
assert.equal(f['customfield_13437'], custom);
|
|
860
|
-
});
|
|
861
|
-
|
|
862
|
-
// ── extraVars 動態模組推導(從 CI 關聯 Library customfield_13702)────
|
|
863
|
-
test('extraVars 動態推導:IBK CI 關聯 ibk+ssr → 只含 ibk & ssr,不含 wealth/a11y', async () => {
|
|
864
|
-
// IBK_MODULE_IDS: ibk=15601, a11y=15602, landing=15603, ssr=15604, wealth=15605
|
|
865
|
-
const f = await getCreatedFields('create_cd_ticket', BASE, {
|
|
866
|
-
issueLinks: [makeLibraryLink('CID-1675'), makeLibraryLink('CID-1676')],
|
|
867
|
-
libraryModules: {
|
|
868
|
-
'CID-1675': { parentId: '15600', childId: '15601' }, // ibk
|
|
869
|
-
'CID-1676': { parentId: '15600', childId: '15604' }, // ssr
|
|
870
|
-
},
|
|
871
|
-
});
|
|
872
|
-
const ev = JSON.parse(f['customfield_13437']);
|
|
873
|
-
const serverMap = { 'tvstg-ibk-web01': true, 'tvstg-ibk-web02': true };
|
|
874
|
-
assert.deepEqual(
|
|
875
|
-
ev,
|
|
876
|
-
{
|
|
877
|
-
ibk_ibk_installation: serverMap,
|
|
878
|
-
ibk_ssr_installation: serverMap,
|
|
879
|
-
},
|
|
880
|
-
`extraVars 應只含 ibk+ssr: ${JSON.stringify(ev)}`,
|
|
881
|
-
);
|
|
882
|
-
assert.ok(!('ibk_wealth_installation' in ev), 'extraVars 不應含 wealth');
|
|
883
|
-
assert.ok(!('ibk_a11y_installation' in ev), 'extraVars 不應含 a11y');
|
|
884
|
-
});
|
|
885
|
-
|
|
886
|
-
test('extraVars 動態推導:CWA CI 關聯單一 Library → 只含 cwa', async () => {
|
|
887
|
-
// CWA_MODULE_IDS: cwa=15514, parent=15513
|
|
888
|
-
const f = await getCreatedFields(
|
|
889
|
-
'create_cd_ticket',
|
|
890
|
-
{
|
|
891
|
-
systemCode: 'CWA',
|
|
892
|
-
environment: 'stg',
|
|
893
|
-
clusterDeploy: 'tvstg-cwa-web01,tvstg-cwa-web02',
|
|
894
|
-
linkedCiKey: 'CID-1668',
|
|
895
|
-
},
|
|
896
|
-
{
|
|
897
|
-
issueLinks: [makeLibraryLink('CID-2000')],
|
|
898
|
-
libraryModules: { 'CID-2000': { parentId: '15513', childId: '15514' } }, // cwa
|
|
899
|
-
},
|
|
900
|
-
);
|
|
901
|
-
const ev = JSON.parse(f['customfield_13437']);
|
|
902
|
-
assert.deepEqual(
|
|
903
|
-
ev,
|
|
904
|
-
{
|
|
905
|
-
cwa_cwa_installation: { 'tvstg-cwa-web01': true, 'tvstg-cwa-web02': true },
|
|
906
|
-
},
|
|
907
|
-
`extraVars 應只含 cwa: ${JSON.stringify(ev)}`,
|
|
908
|
-
);
|
|
909
|
-
});
|
|
910
|
-
|
|
911
|
-
test('extraVars 動態推導:Library customfield_13702 為空 → fallback 到 SYSTEM_MODULES 全模組', async () => {
|
|
912
|
-
const f = await getCreatedFields('create_cd_ticket', BASE, {
|
|
913
|
-
issueLinks: [makeLibraryLink('CID-1675')],
|
|
914
|
-
// libraryModules 未設定 → customfield_13702 = null → detectedModules 空 → fallback
|
|
915
|
-
});
|
|
916
|
-
const ev = JSON.parse(f['customfield_13437']);
|
|
917
|
-
const serverMap = { 'tvstg-ibk-web01': true, 'tvstg-ibk-web02': true };
|
|
918
|
-
assert.deepEqual(
|
|
919
|
-
ev,
|
|
920
|
-
{
|
|
921
|
-
ibk_ibk_installation: serverMap,
|
|
922
|
-
ibk_wealth_installation: serverMap,
|
|
923
|
-
ibk_ssr_installation: serverMap,
|
|
924
|
-
ibk_a11y_installation: serverMap,
|
|
925
|
-
},
|
|
926
|
-
'fallback 應回退到全部 SYSTEM_MODULES',
|
|
927
|
-
);
|
|
928
|
-
});
|
|
929
|
-
|
|
930
|
-
test('extraVars 動態推導:無 Library 關聯(issueLinks 空)→ fallback 到 SYSTEM_MODULES', async () => {
|
|
931
|
-
const f = await getCreatedFields('create_cd_ticket', BASE, { issueLinks: [] });
|
|
932
|
-
const ev = JSON.parse(f['customfield_13437']);
|
|
933
|
-
assert.ok('ibk_ibk_installation' in ev, 'fallback 應含 ibk');
|
|
934
|
-
assert.ok('ibk_wealth_installation' in ev, 'fallback 應含 wealth');
|
|
935
|
-
assert.ok('ibk_ssr_installation' in ev, 'fallback 應含 ssr');
|
|
936
|
-
assert.ok('ibk_a11y_installation' in ev, 'fallback 應含 a11y');
|
|
937
|
-
});
|
|
938
|
-
|
|
939
|
-
test('extraVars 動態推導:重複模組 ID 去重', async () => {
|
|
940
|
-
// 兩個 Library 都對應同一個 module(ibk)
|
|
941
|
-
const f = await getCreatedFields('create_cd_ticket', BASE, {
|
|
942
|
-
issueLinks: [makeLibraryLink('CID-1675'), makeLibraryLink('CID-1676')],
|
|
943
|
-
libraryModules: {
|
|
944
|
-
'CID-1675': { parentId: '15600', childId: '15601' }, // ibk
|
|
945
|
-
'CID-1676': { parentId: '15600', childId: '15601' }, // ibk(重複)
|
|
946
|
-
},
|
|
947
|
-
});
|
|
948
|
-
const ev = JSON.parse(f['customfield_13437']);
|
|
949
|
-
const keys = Object.keys(ev);
|
|
950
|
-
assert.equal(
|
|
951
|
-
keys.filter((k) => k === 'ibk_ibk_installation').length,
|
|
952
|
-
1,
|
|
953
|
-
'重複模組應去重為 1 筆',
|
|
954
|
-
);
|
|
955
|
-
});
|
|
956
|
-
test('customfield_14102 (CID_restart_only) true → id 14902', async () => {
|
|
957
|
-
const f = await getCreatedFields('create_cd_ticket', { ...BASE, restartOnly: true });
|
|
958
|
-
assert.deepEqual(f['customfield_14102'], { id: '14902' });
|
|
959
|
-
});
|
|
960
|
-
test('customfield_14102 (CID_restart_only) false → id 14901', async () => {
|
|
961
|
-
const f = await getCreatedFields('create_cd_ticket', { ...BASE, restartOnly: false });
|
|
962
|
-
assert.deepEqual(f['customfield_14102'], { id: '14901' });
|
|
963
|
-
});
|
|
964
|
-
test('customfield_14102 未傳 restartOnly → 不設定', async () => {
|
|
965
|
-
const f = await getCreatedFields('create_cd_ticket', BASE);
|
|
966
|
-
assert.equal(f['customfield_14102'], undefined);
|
|
967
|
-
});
|
|
968
|
-
|
|
969
|
-
// ── CI ←→ CD linkIssue ───────────────────────────────────────
|
|
970
|
-
test('linkedCiKey: 呼叫 linkIssue(ci, cd, Hierarchy link (WBSGantt))', async () => {
|
|
971
|
-
const jira = makeMockJira();
|
|
972
|
-
await executeTool('create_cd_ticket', BASE, { jira, notifier: mockNotifier });
|
|
973
|
-
assert.equal(jira.calls.linkIssue.length, 1);
|
|
974
|
-
assert.equal(jira.calls.linkIssue[0].inward, 'CID-1677'); // CI key
|
|
975
|
-
assert.equal(jira.calls.linkIssue[0].outward, 'CID-9999'); // CD key
|
|
976
|
-
assert.equal(jira.calls.linkIssue[0].type, 'Hierarchy link (WBSGantt)');
|
|
977
|
-
});
|
|
978
|
-
test('沒有 linkedCiKey: 不呼叫 linkIssue', async () => {
|
|
979
|
-
const jira = makeMockJira();
|
|
980
|
-
await executeTool(
|
|
981
|
-
'create_cd_ticket',
|
|
982
|
-
{
|
|
983
|
-
systemCode: 'IBK',
|
|
984
|
-
environment: 'stg',
|
|
985
|
-
clusterDeploy: 'tvstg-ibk-web01',
|
|
986
|
-
},
|
|
987
|
-
{ jira, notifier: mockNotifier },
|
|
988
|
-
);
|
|
989
|
-
assert.equal(jira.calls.linkIssue.length, 0);
|
|
990
|
-
});
|
|
991
|
-
|
|
992
|
-
// ── Remote Link:CI → Library 單 → LBPRJ version → Web Link ──
|
|
993
|
-
test('2 個 Library 單 → 2 筆 remote link,URL 含正確 version id', async () => {
|
|
994
|
-
const jira = makeMockJira({
|
|
995
|
-
issueLinks: [makeLibraryLink('CID-1675'), makeLibraryLink('CID-1676')],
|
|
996
|
-
libraryBranches: {
|
|
997
|
-
'CID-1675': 'release/IBK_ibk_1.5.2.0',
|
|
998
|
-
'CID-1676': 'release/IBK_ssr_1.3.1.0',
|
|
999
|
-
},
|
|
1000
|
-
projectVersions: [
|
|
1001
|
-
{ id: '16157', name: 'IBK_ibk_1.5.2.0' },
|
|
1002
|
-
{ id: '16158', name: 'IBK_ssr_1.3.1.0' },
|
|
1003
|
-
],
|
|
1004
|
-
});
|
|
1005
|
-
await executeTool('create_cd_ticket', BASE, { jira, notifier: mockNotifier });
|
|
1006
|
-
|
|
1007
|
-
assert.equal(jira.calls.addRemoteLink.length, 2);
|
|
1008
|
-
assert.equal(jira.calls.addRemoteLink[0].issueKey, 'CID-9999');
|
|
1009
|
-
assert.equal(
|
|
1010
|
-
jira.calls.addRemoteLink[0].url,
|
|
1011
|
-
'https://jira.test/projects/LBPRJ/versions/16157',
|
|
1012
|
-
);
|
|
1013
|
-
assert.equal(jira.calls.addRemoteLink[0].title, 'IBK_ibk_1.5.2.0');
|
|
1014
|
-
assert.equal(
|
|
1015
|
-
jira.calls.addRemoteLink[1].url,
|
|
1016
|
-
'https://jira.test/projects/LBPRJ/versions/16158',
|
|
1017
|
-
);
|
|
1018
|
-
assert.equal(jira.calls.addRemoteLink[1].title, 'IBK_ssr_1.3.1.0');
|
|
1019
|
-
});
|
|
1020
|
-
|
|
1021
|
-
test('CID_branch 有 release/ 前綴 → 正確去除後比對版本', async () => {
|
|
1022
|
-
const jira = makeMockJira({
|
|
1023
|
-
issueLinks: [makeLibraryLink('CID-1675')],
|
|
1024
|
-
libraryBranches: { 'CID-1675': 'release/IBK_ibk_1.5.2.0' }, // has release/ prefix
|
|
1025
|
-
projectVersions: [{ id: '16157', name: 'IBK_ibk_1.5.2.0' }], // version without prefix
|
|
1026
|
-
});
|
|
1027
|
-
await executeTool('create_cd_ticket', BASE, { jira, notifier: mockNotifier });
|
|
1028
|
-
|
|
1029
|
-
assert.equal(jira.calls.addRemoteLink.length, 1);
|
|
1030
|
-
assert.equal(jira.calls.addRemoteLink[0].title, 'IBK_ibk_1.5.2.0');
|
|
1031
|
-
});
|
|
1032
|
-
|
|
1033
|
-
test('CI 關聯的非 Library 單不產生 remote link', async () => {
|
|
1034
|
-
const jira = makeMockJira({
|
|
1035
|
-
issueLinks: [
|
|
1036
|
-
{
|
|
1037
|
-
type: { name: 'Relates' },
|
|
1038
|
-
inwardIssue: {
|
|
1039
|
-
key: 'CID-1680',
|
|
1040
|
-
fields: { issuetype: { name: 'CI' }, summary: 'CI ticket' },
|
|
1041
|
-
},
|
|
1042
|
-
},
|
|
1043
|
-
],
|
|
1044
|
-
projectVersions: [{ id: '16157', name: 'IBK_ibk_1.5.2.0' }],
|
|
1045
|
-
});
|
|
1046
|
-
await executeTool('create_cd_ticket', BASE, { jira, notifier: mockNotifier });
|
|
1047
|
-
assert.equal(jira.calls.addRemoteLink.length, 0);
|
|
1048
|
-
});
|
|
1049
|
-
|
|
1050
|
-
test('LBPRJ 找不到對應版本 → 跳過不中斷,回傳成功', async () => {
|
|
1051
|
-
const jira = makeMockJira({
|
|
1052
|
-
issueLinks: [makeLibraryLink('CID-1675')],
|
|
1053
|
-
libraryBranches: { 'CID-1675': 'release/IBK_ibk_1.5.2.0' },
|
|
1054
|
-
projectVersions: [], // 空,找不到版本
|
|
1055
|
-
});
|
|
1056
|
-
const result = await executeTool('create_cd_ticket', BASE, { jira, notifier: mockNotifier });
|
|
1057
|
-
assert.ok(!result.content[0].text.startsWith('❌'), '不應回傳錯誤');
|
|
1058
|
-
assert.equal(jira.calls.addRemoteLink.length, 0);
|
|
1059
|
-
});
|
|
1060
|
-
|
|
1061
|
-
test('CI issueLinks 空 → 不呼叫 addRemoteLink', async () => {
|
|
1062
|
-
const jira = makeMockJira({ issueLinks: [], projectVersions: [] });
|
|
1063
|
-
await executeTool('create_cd_ticket', BASE, { jira, notifier: mockNotifier });
|
|
1064
|
-
assert.equal(jira.calls.addRemoteLink.length, 0);
|
|
1065
|
-
});
|
|
1066
|
-
|
|
1067
|
-
test('沒有 linkedCiKey → 不呼叫 addRemoteLink', async () => {
|
|
1068
|
-
const jira = makeMockJira();
|
|
1069
|
-
await executeTool(
|
|
1070
|
-
'create_cd_ticket',
|
|
1071
|
-
{
|
|
1072
|
-
systemCode: 'IBK',
|
|
1073
|
-
environment: 'stg',
|
|
1074
|
-
clusterDeploy: 'tvstg-ibk-web01',
|
|
1075
|
-
},
|
|
1076
|
-
{ jira, notifier: mockNotifier },
|
|
1077
|
-
);
|
|
1078
|
-
assert.equal(jira.calls.addRemoteLink.length, 0);
|
|
1079
|
-
});
|
|
1080
|
-
|
|
1081
|
-
// ── description 自動生成(generateCDDescription)────────────────
|
|
1082
|
-
test('description 自動生成:開頭含「一般資訊作業申請單」', async () => {
|
|
1083
|
-
const f = await getCreatedFields('create_cd_ticket', BASE, { releaseVersion: '0.0.11' });
|
|
1084
|
-
assert.ok(
|
|
1085
|
-
f.description.startsWith('||一般資訊作業申請單'),
|
|
1086
|
-
`description 開頭錯誤: "${f.description.slice(0, 40)}"`,
|
|
1087
|
-
);
|
|
1088
|
-
});
|
|
1089
|
-
|
|
1090
|
-
test('description 自動生成:*上版版本號* = ciReleaseVersion (0.0.11)', async () => {
|
|
1091
|
-
const f = await getCreatedFields('create_cd_ticket', BASE, { releaseVersion: '0.0.11' });
|
|
1092
|
-
assert.ok(f.description.includes('|*上版版本號*|0.0.11|'), `缺少版本號: "${f.description}"`);
|
|
1093
|
-
});
|
|
1094
|
-
|
|
1095
|
-
test('description 自動生成:包含所有必要欄位', async () => {
|
|
1096
|
-
const f = await getCreatedFields('create_cd_ticket', BASE, { releaseVersion: '0.0.11' });
|
|
1097
|
-
const requiredParts = [
|
|
1098
|
-
'|*相關電子單號*|參照連結|',
|
|
1099
|
-
'|*Purpose*|參照連結|',
|
|
1100
|
-
'|*測試報告 (檢附前一環境 CD 單驗證結果並附上連結)*|參照連結|',
|
|
1101
|
-
'|*SIT 測試涵蓋檢核表 (UAT 申請時需確認)*|參照連結|',
|
|
1102
|
-
'|*UAT 黑箱掃描報告 (PRD 申請時需檢附,需白箱則請附上報告連結)*|不適用|',
|
|
1103
|
-
'|*操作步驟說明*|Ansible 程式上版。|',
|
|
1104
|
-
'|*簽核流程*|',
|
|
1105
|
-
];
|
|
1106
|
-
for (const part of requiredParts) {
|
|
1107
|
-
assert.ok(f.description.includes(part), `description 缺少: "${part}"`);
|
|
1108
|
-
}
|
|
1109
|
-
});
|
|
1110
|
-
|
|
1111
|
-
test('description 當 ciReleaseVersion 為空 → *上版版本號* 後接空值', async () => {
|
|
1112
|
-
const f = await getCreatedFields('create_cd_ticket', BASE, { releaseVersion: null });
|
|
1113
|
-
assert.ok(
|
|
1114
|
-
f.description.includes('|*上版版本號*||'),
|
|
1115
|
-
`應含空版本號行: "${f.description.slice(0, 80)}"`,
|
|
1116
|
-
);
|
|
1117
|
-
});
|
|
1118
|
-
|
|
1119
|
-
test('description args.description 附加在自動描述後(以 \\n\\n 分隔)', async () => {
|
|
1120
|
-
const extra = 'release notes: https://bitbucket.example.com/diff';
|
|
1121
|
-
const f = await getCreatedFields(
|
|
1122
|
-
'create_cd_ticket',
|
|
1123
|
-
{ ...BASE, description: extra },
|
|
1124
|
-
{ releaseVersion: '0.0.11' },
|
|
1125
|
-
);
|
|
1126
|
-
assert.ok(f.description.includes('|*上版版本號*|0.0.11|'), '應含自動描述');
|
|
1127
|
-
assert.ok(
|
|
1128
|
-
f.description.endsWith(`\n\n${extra}`),
|
|
1129
|
-
`應以 \\n\\n + extra 結尾: "${f.description.slice(-60)}"`,
|
|
1130
|
-
);
|
|
1131
|
-
});
|
|
1132
|
-
|
|
1133
|
-
test('description 沒有 args.description → 不含多餘換行', async () => {
|
|
1134
|
-
const f = await getCreatedFields('create_cd_ticket', BASE, { releaseVersion: '0.0.11' });
|
|
1135
|
-
assert.ok(!f.description.includes('\n\n\n'), '不應有三個以上連續換行');
|
|
1136
|
-
});
|
|
1137
|
-
|
|
1138
|
-
// ── ciReleaseVersion 3rd fallback(從 CI summary 解析版號)─────
|
|
1139
|
-
test('ciReleaseVersion 3rd fallback:CI summary "for 0.0.11" → 版本號填入 description 與 summary', async () => {
|
|
1140
|
-
const f = await getCreatedFields('create_cd_ticket', BASE, {
|
|
1141
|
-
releaseVersion: null,
|
|
1142
|
-
deployVersionJson: null,
|
|
1143
|
-
ciSummary: '[IBK][CI] SSR & IBK for 0.0.11',
|
|
1144
|
-
});
|
|
1145
|
-
assert.ok(f.summary.includes('0.0.11'), `3rd fallback summary 失敗: "${f.summary}"`);
|
|
1146
|
-
assert.ok(f.description.includes('|*上版版本號*|0.0.11|'), `3rd fallback description 失敗`);
|
|
1147
|
-
assert.equal(f['customfield_14101'], '0.0.11', '3rd fallback deploy_version 欄位應填入');
|
|
1148
|
-
});
|
|
1149
|
-
|
|
1150
|
-
test('ciReleaseVersion 3rd fallback:CI summary 無版號 → 仍回傳成功(空版本)', async () => {
|
|
1151
|
-
const f = await getCreatedFields('create_cd_ticket', BASE, {
|
|
1152
|
-
releaseVersion: null,
|
|
1153
|
-
deployVersionJson: null,
|
|
1154
|
-
ciSummary: '[IBK][CI] SSR & IBK', // 無 "for x.y.z"
|
|
1155
|
-
});
|
|
1156
|
-
assert.ok(f.description.includes('|*上版版本號*||'), '應含空版本號');
|
|
1157
|
-
});
|
|
1158
|
-
});
|
|
1159
|
-
|
|
1160
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
1161
|
-
// release notes 產生(generateReleaseNotes)
|
|
1162
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
1163
|
-
describe('create_cd_ticket — release notes', () => {
|
|
1164
|
-
const BASE_CD = {
|
|
1165
|
-
systemCode: 'IBK',
|
|
1166
|
-
environment: 'stg',
|
|
1167
|
-
clusterDeploy: 'tvstg-ibk-web01',
|
|
1168
|
-
linkedCiKey: 'CID-1677',
|
|
1169
|
-
};
|
|
1170
|
-
|
|
1171
|
-
// Jenkins comment 格式:ch014.ibk.{module}-{branch}-{CID_version}
|
|
1172
|
-
const IBK_COMMENT =
|
|
1173
|
-
'Release version [ch014.ibk.ibk-release-v1.5.2.0-0.0.1|https://jenkins/job/1] pass in [Jenkins #1|url]';
|
|
1174
|
-
const SSR_COMMENT =
|
|
1175
|
-
'Release version [ch014.ibk.ssr-release-v1.3.1.0-0.0.1|https://jenkins/job/2] pass in [Jenkins #2|url]';
|
|
1176
|
-
// CWA 格式:branch 已包含 CID_version 後綴(release-v1.5.2.0-0.0.1)
|
|
1177
|
-
const CWA_COMMENT =
|
|
1178
|
-
'Release version [ch014.cwa.cwa-release-v1.5.2.0-0.0.1|https://jenkins/job/3] pass in [Jenkins #3|url]';
|
|
1179
|
-
// moduleKey 無法解析的 comment(無 -release- 結構)
|
|
1180
|
-
const BAD_COMMENT = 'Release version [something-invalid-format|url] pass in [Jenkins #3|url]';
|
|
1181
|
-
|
|
1182
|
-
test('無 Library 單 → description 不含 release notes', async () => {
|
|
1183
|
-
const f = await getCreatedFields('create_cd_ticket', BASE_CD, { issueLinks: [] });
|
|
1184
|
-
assert.ok(!f.description.includes('release notes:'), 'description 不應含 release notes');
|
|
1185
|
-
});
|
|
1186
|
-
|
|
1187
|
-
test('Library 單缺少 Jenkins comment → 跳過,不含 release notes(mock 模擬真實 Jira:未請求 comment 時不回傳)', async () => {
|
|
1188
|
-
// mock 只在 fieldNames.includes('comment') 時才回傳 comment,
|
|
1189
|
-
// 確保 generateReleaseNotes 有明確請求 'comment' 欄位,否則無法解析 CID_version
|
|
1190
|
-
const f = await getCreatedFields('create_cd_ticket', BASE_CD, {
|
|
1191
|
-
issueLinks: [makeLibraryLink('CID-1675')],
|
|
1192
|
-
libraryBranches: { 'CID-1675': 'release/v1.5.2.0' },
|
|
1193
|
-
// 沒有 libraryComments → CID_version/moduleKey 解析失敗 → 跳過
|
|
1194
|
-
});
|
|
1195
|
-
assert.ok(!f.description.includes('release notes:'), 'description 不應含 release notes');
|
|
1196
|
-
});
|
|
1197
|
-
|
|
1198
|
-
test('Library Jenkins comment 無法解析 moduleKey → 跳過,不含 release notes', async () => {
|
|
1199
|
-
const f = await getCreatedFields('create_cd_ticket', BASE_CD, {
|
|
1200
|
-
issueLinks: [makeLibraryLink('CID-1675')],
|
|
1201
|
-
libraryBranches: { 'CID-1675': 'release/v1.5.2.0' },
|
|
1202
|
-
libraryComments: { 'CID-1675': BAD_COMMENT },
|
|
1203
|
-
});
|
|
1204
|
-
assert.ok(!f.description.includes('release notes:'), 'description 不應含 release notes');
|
|
1205
|
-
});
|
|
1206
|
-
|
|
1207
|
-
test('單模組 IBK(如果沒有):description 含 release notes: + Bitbucket compare URL', async () => {
|
|
1208
|
-
const f = await getCreatedFields('create_cd_ticket', BASE_CD, {
|
|
1209
|
-
issueLinks: [makeLibraryLink('CID-1675')],
|
|
1210
|
-
libraryBranches: { 'CID-1675': 'release/v1.5.2.0' },
|
|
1211
|
-
libraryComments: { 'CID-1675': IBK_COMMENT },
|
|
1212
|
-
bitbucketLastTag: 'release/v1.5.1.0',
|
|
1213
|
-
});
|
|
1214
|
-
assert.ok(f.description.includes('release notes:'), 'description 應含 release notes:');
|
|
1215
|
-
assert.ok(f.description.includes('IBK:'), 'description 應含 IBK: label');
|
|
1216
|
-
assert.ok(f.description.includes('tw-bank-web'), 'URL 應含 repo');
|
|
1217
|
-
assert.ok(
|
|
1218
|
-
f.description.includes('ci%2Frelease-v1.5.2.0-0.0.1'),
|
|
1219
|
-
'URL 應含 sourceBranch(current tag)',
|
|
1220
|
-
);
|
|
1221
|
-
assert.ok(
|
|
1222
|
-
f.description.includes('release%2Fv1.5.1.0'),
|
|
1223
|
-
'URL 應含 targetBranch(last release/ tag)',
|
|
1224
|
-
);
|
|
1225
|
-
assert.ok(f.description.includes('targetRepoId=110'), 'URL 應含 repoId=110');
|
|
1226
|
-
});
|
|
1227
|
-
|
|
1228
|
-
test('雙模組 IBK + SSR(如果沒有):description 含兩個 compare URL', async () => {
|
|
1229
|
-
const f = await getCreatedFields('create_cd_ticket', BASE_CD, {
|
|
1230
|
-
issueLinks: [makeLibraryLink('CID-1675'), makeLibraryLink('CID-1676')],
|
|
1231
|
-
libraryBranches: {
|
|
1232
|
-
'CID-1675': 'release/v1.5.2.0',
|
|
1233
|
-
'CID-1676': 'release/v1.3.1.0',
|
|
1234
|
-
},
|
|
1235
|
-
libraryComments: {
|
|
1236
|
-
'CID-1675': IBK_COMMENT,
|
|
1237
|
-
'CID-1676': SSR_COMMENT,
|
|
1238
|
-
},
|
|
1239
|
-
bitbucketLastTag: 'release/v1.5.1.0',
|
|
1240
|
-
});
|
|
1241
|
-
assert.ok(f.description.includes('IBK:'), 'description 應含 IBK:');
|
|
1242
|
-
assert.ok(f.description.includes('SSR:'), 'description 應含 SSR:');
|
|
1243
|
-
assert.ok(f.description.includes('tw-bank-web-ssr'), 'URL 應含 SSR repo');
|
|
1244
|
-
assert.ok(f.description.includes('ci%2Frelease-v1.3.1.0-0.0.1'), '應含 SSR current tag');
|
|
1245
|
-
assert.ok(f.description.includes('targetRepoId=787'), '應含 SSR repoId=787');
|
|
1246
|
-
});
|
|
1247
|
-
|
|
1248
|
-
test('release notes 在 description 末尾,以 \\n\\n 分隔基礎描述', async () => {
|
|
1249
|
-
const f = await getCreatedFields('create_cd_ticket', BASE_CD, {
|
|
1250
|
-
issueLinks: [makeLibraryLink('CID-1675')],
|
|
1251
|
-
libraryBranches: { 'CID-1675': 'release/v1.5.2.0' },
|
|
1252
|
-
libraryComments: { 'CID-1675': IBK_COMMENT },
|
|
1253
|
-
bitbucketLastTag: 'release/v1.5.1.0',
|
|
1254
|
-
});
|
|
1255
|
-
const idx = f.description.indexOf('\n\nrelease notes:');
|
|
1256
|
-
assert.ok(idx > 0, `release notes 應在基礎描述之後以 \\n\\n 分隔,idx=${idx}`);
|
|
1257
|
-
});
|
|
1258
|
-
|
|
1259
|
-
test('Bitbucket API 查無 tag → 跳過該 module', async () => {
|
|
1260
|
-
const f = await getCreatedFields('create_cd_ticket', BASE_CD, {
|
|
1261
|
-
issueLinks: [makeLibraryLink('CID-1675')],
|
|
1262
|
-
libraryBranches: { 'CID-1675': 'release/v1.5.2.0' },
|
|
1263
|
-
libraryComments: { 'CID-1675': IBK_COMMENT },
|
|
1264
|
-
bitbucketLastTag: null,
|
|
1265
|
-
});
|
|
1266
|
-
assert.ok(!f.description.includes('release notes:'), '查無 tag → 跳過,不含 release notes');
|
|
1267
|
-
});
|
|
1268
|
-
|
|
1269
|
-
test('getBitbucketBranches 呼叫時使用 release/ filterValue', async () => {
|
|
1270
|
-
const jira = makeMockJira({
|
|
1271
|
-
issueLinks: [makeLibraryLink('CID-1675')],
|
|
1272
|
-
libraryBranches: { 'CID-1675': 'release/v1.5.2.0' },
|
|
1273
|
-
libraryComments: { 'CID-1675': IBK_COMMENT },
|
|
1274
|
-
bitbucketLastTag: 'release/v1.5.1.0',
|
|
1275
|
-
});
|
|
1276
|
-
await executeTool('create_cd_ticket', BASE_CD, { jira, notifier: mockNotifier });
|
|
1277
|
-
const branchCall = jira.calls.getBitbucketBranches[0];
|
|
1278
|
-
assert.ok(branchCall, 'getBitbucketBranches 應被呼叫');
|
|
1279
|
-
assert.equal(branchCall.project, 'CHANNEL');
|
|
1280
|
-
assert.equal(branchCall.repo, 'tw-bank-web');
|
|
1281
|
-
assert.ok(
|
|
1282
|
-
branchCall.opts?.filterValue?.startsWith('release/'),
|
|
1283
|
-
'filterValue 應以 release/ 開頭',
|
|
1284
|
-
);
|
|
1285
|
-
});
|
|
1286
|
-
|
|
1287
|
-
// ── moduleKey 解析 ────────────────────────────────────────────
|
|
1288
|
-
test('SSR module → label=SSR, repo=tw-bank-web-ssr, repoId=787', async () => {
|
|
1289
|
-
const f = await getCreatedFields('create_cd_ticket', BASE_CD, {
|
|
1290
|
-
issueLinks: [makeLibraryLink('CID-1676')],
|
|
1291
|
-
libraryBranches: { 'CID-1676': 'release/v1.3.1.0' },
|
|
1292
|
-
libraryComments: { 'CID-1676': SSR_COMMENT },
|
|
1293
|
-
bitbucketLastTag: 'release/v1.3.0.0',
|
|
1294
|
-
});
|
|
1295
|
-
assert.ok(f.description.includes('SSR:'), 'label 應為 SSR');
|
|
1296
|
-
assert.ok(f.description.includes('tw-bank-web-ssr'), 'repo 應為 tw-bank-web-ssr');
|
|
1297
|
-
assert.ok(f.description.includes('targetRepoId=787'), 'repoId 應為 787');
|
|
1298
|
-
});
|
|
1299
|
-
|
|
1300
|
-
test('CWA module → label=CWA, repo=tw-bank-webapp, repoId=36', async () => {
|
|
1301
|
-
const CWA_COMMENT = 'Release version [ch014.cwa.cwa-release-v1.5.0.0-0.0.1|url] pass';
|
|
1302
|
-
const f = await getCreatedFields(
|
|
1303
|
-
'create_cd_ticket',
|
|
1304
|
-
{
|
|
1305
|
-
...BASE_CD,
|
|
1306
|
-
systemCode: 'CWA',
|
|
1307
|
-
environment: 'stg',
|
|
1308
|
-
clusterDeploy: 'tvstg-cwa-web01',
|
|
1309
|
-
},
|
|
1310
|
-
{
|
|
1311
|
-
issueLinks: [makeLibraryLink('CID-2000')],
|
|
1312
|
-
libraryBranches: { 'CID-2000': 'release/v1.5.0.0' },
|
|
1313
|
-
libraryComments: { 'CID-2000': CWA_COMMENT },
|
|
1314
|
-
bitbucketLastTag: 'release/v1.4.0.0',
|
|
1315
|
-
},
|
|
1316
|
-
);
|
|
1317
|
-
assert.ok(f.description.includes('CWA:'), 'label 應為 CWA');
|
|
1318
|
-
assert.ok(f.description.includes('tw-bank-webapp'), 'repo 應為 tw-bank-webapp');
|
|
1319
|
-
assert.ok(f.description.includes('targetRepoId=36'), 'repoId 應為 36');
|
|
1320
|
-
});
|
|
1321
|
-
|
|
1322
|
-
test('lib_branch 中的 / 轉成 - 產生正確 ci/ tag', async () => {
|
|
1323
|
-
// release/v1.5.2.0 → release-v1.5.2.0,ci tag = ci/release-v1.5.2.0-0.0.1
|
|
1324
|
-
const f = await getCreatedFields('create_cd_ticket', BASE_CD, {
|
|
1325
|
-
issueLinks: [makeLibraryLink('CID-1675')],
|
|
1326
|
-
libraryBranches: { 'CID-1675': 'release/v1.5.2.0' },
|
|
1327
|
-
libraryComments: { 'CID-1675': IBK_COMMENT },
|
|
1328
|
-
bitbucketLastTag: 'release/v1.5.1.0',
|
|
1329
|
-
});
|
|
1330
|
-
assert.ok(
|
|
1331
|
-
f.description.includes('ci%2Frelease-v1.5.2.0-0.0.1'),
|
|
1332
|
-
`sourceBranch 應含 ci%2Frelease-v1.5.2.0-0.0.1,實際: ${f.description.match(/sourceBranch=[^&]+/)?.[0]}`,
|
|
1333
|
-
);
|
|
1334
|
-
});
|
|
1335
|
-
|
|
1336
|
-
// ── CWA 格式:branch 已內含 CID_version 後綴(release-v1.5.2.0-0.0.1)──
|
|
1337
|
-
test('CWA 格式 branch(release-{ver}-{CID_version}):sourceBranch 不重複版本', async () => {
|
|
1338
|
-
// CWA Library branch = 'release-v1.5.2.0-0.0.1'(已含 CID_version=0.0.1)
|
|
1339
|
-
// 預期 sourceBranch = ci/release-v1.5.2.0-0.0.1(只出現一次)
|
|
1340
|
-
const BASE_CWA = {
|
|
1341
|
-
systemCode: 'CWA',
|
|
1342
|
-
environment: 'stg',
|
|
1343
|
-
clusterDeploy: 'tvstg-cwa-web01',
|
|
1344
|
-
linkedCiKey: 'CID-1668',
|
|
1345
|
-
};
|
|
1346
|
-
const f = await getCreatedFields('create_cd_ticket', BASE_CWA, {
|
|
1347
|
-
issueLinks: [makeLibraryLink('CID-1667')],
|
|
1348
|
-
libraryBranches: { 'CID-1667': 'release-v1.5.2.0-0.0.1' },
|
|
1349
|
-
libraryComments: { 'CID-1667': CWA_COMMENT },
|
|
1350
|
-
bitbucketLastTag: 'release/v1.5.1.0',
|
|
1351
|
-
});
|
|
1352
|
-
assert.ok(f.description.includes('release notes:'), 'description 應含 release notes:');
|
|
1353
|
-
assert.ok(f.description.includes('CWA:'), 'label 應為 CWA');
|
|
1354
|
-
// sourceBranch 應為 ci/release-v1.5.2.0-0.0.1(不是 ci/release-v1.5.2.0-0.0.1-0.0.1)
|
|
1355
|
-
assert.ok(f.description.includes('ci%2Frelease-v1.5.2.0-0.0.1'), '應含正確 sourceBranch');
|
|
1356
|
-
assert.ok(!f.description.includes('0.0.1-0.0.1'), '版本號不應重複出現');
|
|
1357
|
-
});
|
|
1358
|
-
|
|
1359
|
-
test('CWA 格式 branch:Web Link LBPRJ 版本查找使用去除後綴後的 versionName', async () => {
|
|
1360
|
-
// branch = 'release-v1.5.2.0-0.0.1' → branchBase = 'release-v1.5.2.0'
|
|
1361
|
-
// → strip 'release-' → versionName = 'v1.5.2.0'
|
|
1362
|
-
const BASE_CWA = {
|
|
1363
|
-
systemCode: 'CWA',
|
|
1364
|
-
environment: 'stg',
|
|
1365
|
-
clusterDeploy: 'tvstg-cwa-web01',
|
|
1366
|
-
linkedCiKey: 'CID-1668',
|
|
1367
|
-
};
|
|
1368
|
-
const jira = makeMockJira({
|
|
1369
|
-
issueLinks: [makeLibraryLink('CID-1667')],
|
|
1370
|
-
libraryBranches: { 'CID-1667': 'release-v1.5.2.0-0.0.1' },
|
|
1371
|
-
libraryComments: { 'CID-1667': CWA_COMMENT },
|
|
1372
|
-
projectVersions: [{ id: '20001', name: 'v1.5.2.0' }], // LBPRJ 版本
|
|
1373
|
-
});
|
|
1374
|
-
await executeTool('create_cd_ticket', BASE_CWA, { jira, notifier: mockNotifier });
|
|
1375
|
-
assert.equal(jira.calls.addRemoteLink.length, 1, 'Web Link 應被加入');
|
|
1376
|
-
assert.equal(jira.calls.addRemoteLink[0].title, 'v1.5.2.0', '版本名稱應為 v1.5.2.0');
|
|
1377
|
-
});
|
|
1378
|
-
|
|
1379
|
-
// ── 「如果有」:N > 0.0.1,N-1 曾部署到相同 env ───────────────
|
|
1380
|
-
test('「如果有」N=0.0.2 且 N-1 曾部署同 env → targetBranch = ci/{branch}-0.0.1,不查 Bitbucket tags', async () => {
|
|
1381
|
-
const COMMENT_N2 = 'Release version [ch014.ibk.ibk-release-v1.5.2.0-0.0.2|url] pass';
|
|
1382
|
-
const COMMENT_N1 = 'Release version [ch014.ibk.ibk-release-v1.5.2.0-0.0.1|url] pass';
|
|
1383
|
-
|
|
1384
|
-
// searchIssues 回傳前一張 Library(CID_version=0.0.1),其 CI 含有 STG CD
|
|
1385
|
-
const prevLibrary = {
|
|
1386
|
-
key: 'CID-1670',
|
|
1387
|
-
fields: {
|
|
1388
|
-
comment: { comments: [{ body: COMMENT_N1 }] },
|
|
1389
|
-
status: { name: 'Released' },
|
|
1390
|
-
issuelinks: [
|
|
1391
|
-
{
|
|
1392
|
-
inwardIssue: {
|
|
1393
|
-
key: 'CID-1671',
|
|
1394
|
-
fields: { issuetype: { name: 'CI' }, status: { name: 'Done' } },
|
|
1395
|
-
},
|
|
1396
|
-
},
|
|
1397
|
-
],
|
|
1398
|
-
},
|
|
1399
|
-
};
|
|
1400
|
-
// CID-1671(CI)的 contains 有一張 STG CD
|
|
1401
|
-
const ciFields = {
|
|
1402
|
-
status: { name: 'Done' },
|
|
1403
|
-
issuelinks: [
|
|
1404
|
-
{
|
|
1405
|
-
outwardIssue: {
|
|
1406
|
-
key: 'CID-1672',
|
|
1407
|
-
fields: {
|
|
1408
|
-
issuetype: { name: 'CD' },
|
|
1409
|
-
status: { name: 'Done' },
|
|
1410
|
-
summary: '[IBK][STG] 程式上版作業申請單_20260301 (CD deployment with 0.0.3)',
|
|
1411
|
-
},
|
|
1412
|
-
},
|
|
1413
|
-
},
|
|
1414
|
-
],
|
|
1415
|
-
};
|
|
1416
|
-
|
|
1417
|
-
const jira = makeMockJira({
|
|
1418
|
-
issueLinks: [makeLibraryLink('CID-1675')],
|
|
1419
|
-
libraryBranches: { 'CID-1675': 'release/v1.5.2.0' },
|
|
1420
|
-
libraryComments: { 'CID-1675': COMMENT_N2 },
|
|
1421
|
-
searchIssuesResult: [prevLibrary],
|
|
1422
|
-
ciFieldsMap: { 'CID-1671': ciFields },
|
|
1423
|
-
// bitbucketLastTag 不設定(如果有 → 不應查 Bitbucket)
|
|
1424
|
-
});
|
|
1425
|
-
await executeTool('create_cd_ticket', BASE_CD, { jira, notifier: mockNotifier });
|
|
1426
|
-
const description = jira.calls.updateIssue[0]?.fields?.description ?? '';
|
|
1427
|
-
|
|
1428
|
-
assert.ok(description.includes('ci%2Frelease-v1.5.2.0-0.0.2'), '應含 sourceBranch N=0.0.2');
|
|
1429
|
-
assert.ok(description.includes('ci%2Frelease-v1.5.2.0-0.0.1'), '應含 targetBranch N-1=0.0.1');
|
|
1430
|
-
assert.equal(jira.calls.getBitbucketBranches.length, 0, '如果有 → 不應查 Bitbucket branches');
|
|
1431
|
-
});
|
|
1432
|
-
|
|
1433
|
-
test('「如果有」N=0.0.2 但 N-1 未部署同 env → fallback 到 Bitbucket 最後 release/ tag', async () => {
|
|
1434
|
-
const COMMENT_N2 = 'Release version [ch014.ibk.ibk-release-v1.5.2.0-0.0.2|url] pass';
|
|
1435
|
-
const COMMENT_N1 = 'Release version [ch014.ibk.ibk-release-v1.5.2.0-0.0.1|url] pass';
|
|
1436
|
-
|
|
1437
|
-
// searchIssues 回傳前一張 Library,但其 CI 只有 UAT CD(不是 STG)
|
|
1438
|
-
const prevLibrary = {
|
|
1439
|
-
key: 'CID-1670',
|
|
1440
|
-
fields: {
|
|
1441
|
-
comment: { comments: [{ body: COMMENT_N1 }] },
|
|
1442
|
-
status: { name: 'Released' },
|
|
1443
|
-
issuelinks: [
|
|
1444
|
-
{
|
|
1445
|
-
inwardIssue: {
|
|
1446
|
-
key: 'CID-1671',
|
|
1447
|
-
fields: { issuetype: { name: 'CI' }, status: { name: 'Done' } },
|
|
1448
|
-
},
|
|
1449
|
-
},
|
|
1450
|
-
],
|
|
1451
|
-
},
|
|
1452
|
-
};
|
|
1453
|
-
// CI 只含 UAT CD(env 不同)
|
|
1454
|
-
const ciFields = {
|
|
1455
|
-
status: { name: 'Done' },
|
|
1456
|
-
issuelinks: [
|
|
1457
|
-
{
|
|
1458
|
-
outwardIssue: {
|
|
1459
|
-
key: 'CID-1673',
|
|
1460
|
-
fields: {
|
|
1461
|
-
issuetype: { name: 'CD' },
|
|
1462
|
-
status: { name: 'Done' },
|
|
1463
|
-
summary: '[IBK][UAT] 程式上版作業申請單_20260301', // UAT 不是 STG
|
|
1464
|
-
},
|
|
1465
|
-
},
|
|
1466
|
-
},
|
|
1467
|
-
],
|
|
1468
|
-
};
|
|
1469
|
-
|
|
1470
|
-
const f = await getCreatedFields('create_cd_ticket', BASE_CD, {
|
|
1471
|
-
issueLinks: [makeLibraryLink('CID-1675')],
|
|
1472
|
-
libraryBranches: { 'CID-1675': 'release/v1.5.2.0' },
|
|
1473
|
-
libraryComments: { 'CID-1675': COMMENT_N2 },
|
|
1474
|
-
searchIssuesResult: [prevLibrary],
|
|
1475
|
-
ciFieldsMap: { 'CID-1671': ciFields },
|
|
1476
|
-
bitbucketLastTag: 'release/v1.5.1.0',
|
|
1477
|
-
});
|
|
1478
|
-
assert.ok(
|
|
1479
|
-
f.description.includes('release%2Fv1.5.1.0'),
|
|
1480
|
-
'應 fallback 到 Bitbucket 最後 release/ tag',
|
|
1481
|
-
);
|
|
1482
|
-
assert.ok(
|
|
1483
|
-
!f.description.includes('ci%2Frelease-v1.5.2.0-0.0.1'),
|
|
1484
|
-
'不應含 ci N-1 tag(未部署同 env)',
|
|
1485
|
-
);
|
|
1486
|
-
});
|
|
1487
|
-
});
|
|
1488
|
-
|
|
1489
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
1490
|
-
// create_grayrelease_ticket
|
|
1491
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
1492
|
-
describe('create_grayrelease_ticket — fields', () => {
|
|
1493
|
-
const BASE = { systemCode: 'CWA', grayVersion: 'feature/test-sdd', environment: 'stg' };
|
|
1494
|
-
|
|
1495
|
-
test('project.key = CID', async () => {
|
|
1496
|
-
const f = await getCreatedFields('create_grayrelease_ticket', BASE);
|
|
1497
|
-
assert.deepEqual(f.project, { key: 'CID' });
|
|
1498
|
-
});
|
|
1499
|
-
test('issuetype.id = 12601 (GrayRelease)', async () => {
|
|
1500
|
-
const f = await getCreatedFields('create_grayrelease_ticket', BASE);
|
|
1501
|
-
assert.deepEqual(f.issuetype, { id: '12601' });
|
|
1502
|
-
});
|
|
1503
|
-
test('duedate = today (YYYY-MM-DD)', async () => {
|
|
1504
|
-
const f = await getCreatedFields('create_grayrelease_ticket', BASE);
|
|
1505
|
-
assert.match(f.duedate, /^\d{4}-\d{2}-\d{2}$/);
|
|
1506
|
-
});
|
|
1507
|
-
test('summary 自動生成格式:[STG][GrayRelease] CWA_CWA_YYYYMMDD 程式上版作業申請單', async () => {
|
|
1508
|
-
const f = await getCreatedFields('create_grayrelease_ticket', BASE);
|
|
1509
|
-
assert.match(f.summary, /^\[STG\]\[GrayRelease\] CWA_CWA_\d{8} 程式上版作業申請單$/);
|
|
1510
|
-
});
|
|
1511
|
-
test('summary 自訂覆蓋', async () => {
|
|
1512
|
-
const f = await getCreatedFields('create_grayrelease_ticket', {
|
|
1513
|
-
...BASE,
|
|
1514
|
-
summary: '[Custom] GR',
|
|
1515
|
-
});
|
|
1516
|
-
assert.equal(f.summary, '[Custom] GR');
|
|
1517
|
-
});
|
|
1518
|
-
test('customfield_13443 (systemCode) = { value: CWA }', async () => {
|
|
1519
|
-
const f = await getCreatedFields('create_grayrelease_ticket', BASE);
|
|
1520
|
-
assert.deepEqual(f['customfield_13443'], { value: 'CWA' });
|
|
1521
|
-
});
|
|
1522
|
-
test('customfield_13431 (gitBranch) = 原始 grayVersion(含 /)', async () => {
|
|
1523
|
-
const f = await getCreatedFields('create_grayrelease_ticket', BASE);
|
|
1524
|
-
assert.equal(f['customfield_13431'], 'feature/test-sdd');
|
|
1525
|
-
});
|
|
1526
|
-
test('customfield_14700 (grayReleaseVersion) / 替換為 -', async () => {
|
|
1527
|
-
const f = await getCreatedFields('create_grayrelease_ticket', BASE);
|
|
1528
|
-
assert.equal(f['customfield_14700'], 'feature-test-sdd');
|
|
1529
|
-
});
|
|
1530
|
-
test('customfield_14700 純版號不含 / 不變', async () => {
|
|
1531
|
-
const f = await getCreatedFields('create_grayrelease_ticket', {
|
|
1532
|
-
...BASE,
|
|
1533
|
-
grayVersion: '1.0.0',
|
|
1534
|
-
});
|
|
1535
|
-
assert.equal(f['customfield_14700'], '1.0.0');
|
|
1536
|
-
});
|
|
1537
|
-
test('customfield_13434 (deptCode) CWA → { value: CH014 }', async () => {
|
|
1538
|
-
const f = await getCreatedFields('create_grayrelease_ticket', BASE);
|
|
1539
|
-
assert.deepEqual(f['customfield_13434'], { value: 'CH014' });
|
|
1540
|
-
});
|
|
1541
|
-
test('customfield_13436 (env) stg → { value: stg }', async () => {
|
|
1542
|
-
const f = await getCreatedFields('create_grayrelease_ticket', BASE);
|
|
1543
|
-
assert.deepEqual(f['customfield_13436'], { value: 'stg' });
|
|
1544
|
-
});
|
|
1545
|
-
test('customfield_14704 (clusterDeploy) 永遠 true (id 15506)', async () => {
|
|
1546
|
-
const f = await getCreatedFields('create_grayrelease_ticket', BASE);
|
|
1547
|
-
assert.deepEqual(f['customfield_14704'], { id: '15506' });
|
|
1548
|
-
});
|
|
1549
|
-
test('customfield_14701 (clusterList) CWA STG = 換行分隔兩台', async () => {
|
|
1550
|
-
const f = await getCreatedFields('create_grayrelease_ticket', BASE);
|
|
1551
|
-
assert.equal(f['customfield_14701'], 'tvstg-cwa-web01\ntvstg-cwa-web02');
|
|
1552
|
-
});
|
|
1553
|
-
test('customfield_13444 (systemModule) CWA = { id: 15515 }', async () => {
|
|
1554
|
-
const f = await getCreatedFields('create_grayrelease_ticket', BASE);
|
|
1555
|
-
assert.deepEqual(f['customfield_13444'], { id: '15515' });
|
|
1556
|
-
});
|
|
1557
|
-
test('IBK STG: clusterList = tvstg-ibk-web01\\ntvstg-ibk-web02', async () => {
|
|
1558
|
-
const f = await getCreatedFields('create_grayrelease_ticket', {
|
|
1559
|
-
systemCode: 'IBK',
|
|
1560
|
-
grayVersion: 'feature/x',
|
|
1561
|
-
environment: 'stg',
|
|
1562
|
-
});
|
|
1563
|
-
assert.equal(f['customfield_14701'], 'tvstg-ibk-web01\ntvstg-ibk-web02');
|
|
1564
|
-
});
|
|
1565
|
-
test('IBK systemModule = { id: 15500 }', async () => {
|
|
1566
|
-
const f = await getCreatedFields('create_grayrelease_ticket', {
|
|
1567
|
-
systemCode: 'IBK',
|
|
1568
|
-
grayVersion: 'feature/x',
|
|
1569
|
-
environment: 'stg',
|
|
1570
|
-
});
|
|
1571
|
-
assert.deepEqual(f['customfield_13444'], { id: '15500' });
|
|
1572
|
-
});
|
|
1573
|
-
test('IBK deptCode → { value: CH014 }', async () => {
|
|
1574
|
-
const f = await getCreatedFields('create_grayrelease_ticket', {
|
|
1575
|
-
systemCode: 'IBK',
|
|
1576
|
-
grayVersion: '1.0.0',
|
|
1577
|
-
environment: 'stg',
|
|
1578
|
-
});
|
|
1579
|
-
assert.deepEqual(f['customfield_13434'], { value: 'CH014' });
|
|
1580
|
-
});
|
|
1581
|
-
test('EVT deptCode → { value: CH015 }', async () => {
|
|
1582
|
-
const f = await getCreatedFields('create_grayrelease_ticket', {
|
|
1583
|
-
systemCode: 'EVT',
|
|
1584
|
-
grayVersion: '1.0.0',
|
|
1585
|
-
environment: 'stg',
|
|
1586
|
-
});
|
|
1587
|
-
assert.deepEqual(f['customfield_13434'], { value: 'CH015' });
|
|
1588
|
-
});
|
|
1589
|
-
});
|
|
1590
|
-
|
|
1591
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
1592
|
-
// auto_grayrelease approval payload handling
|
|
1593
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
1594
|
-
describe('auto_grayrelease — approval payload handling', () => {
|
|
1595
|
-
const savedEnv = {
|
|
1596
|
-
CONF_BASE_URL: process.env.CONF_BASE_URL,
|
|
1597
|
-
CONF_TOKEN: process.env.CONF_TOKEN,
|
|
1598
|
-
JABBER_NOTIFY_SCRIPT: process.env.JABBER_NOTIFY_SCRIPT,
|
|
1599
|
-
POLL_INTERVAL_MS: process.env.POLL_INTERVAL_MS,
|
|
1600
|
-
POLL_TIMEOUT_MS: process.env.POLL_TIMEOUT_MS,
|
|
1601
|
-
};
|
|
1602
|
-
|
|
1603
|
-
function restoreEnv() {
|
|
1604
|
-
for (const [key, value] of Object.entries(savedEnv)) {
|
|
1605
|
-
if (value === undefined) {
|
|
1606
|
-
delete process.env[key];
|
|
1607
|
-
} else {
|
|
1608
|
-
process.env[key] = value;
|
|
1609
|
-
}
|
|
1610
|
-
}
|
|
1611
|
-
}
|
|
1612
|
-
|
|
1613
|
-
function makeApprovalJira(environment, { comments = [] } = {}) {
|
|
1614
|
-
let status = 'WAIT APPROVAL';
|
|
1615
|
-
const calls = { updateAssignee: [], transitionByName: [] };
|
|
1616
|
-
|
|
1617
|
-
return {
|
|
1618
|
-
calls,
|
|
1619
|
-
getIssueFields: async () => ({
|
|
1620
|
-
customfield_13436: { value: environment },
|
|
1621
|
-
customfield_13443: { value: 'CWA' },
|
|
1622
|
-
customfield_13432: 'pass',
|
|
1623
|
-
customfield_13433: 'pass',
|
|
1624
|
-
}),
|
|
1625
|
-
getIssue: async () => ({ fields: { status: { name: status }, summary: 'GrayRelease' } }),
|
|
1626
|
-
getTransitions: async () => [],
|
|
1627
|
-
updateAssignee: async (issueKey, accountId) => {
|
|
1628
|
-
calls.updateAssignee.push({ issueKey, accountId });
|
|
1629
|
-
if (environment === 'stg' || calls.updateAssignee.length >= 2) {
|
|
1630
|
-
status = 'WAIT DEPLOY';
|
|
1631
|
-
}
|
|
1632
|
-
},
|
|
1633
|
-
getComments: async () => comments,
|
|
1634
|
-
searchIssues: async () => [{ fields: { customfield_13436: { value: 'stg' } } }],
|
|
1635
|
-
transitionByName: async (issueKey, transitionName) => {
|
|
1636
|
-
calls.transitionByName.push({ issueKey, transitionName });
|
|
1637
|
-
if (transitionName === 'GrayRelease Deploy') {
|
|
1638
|
-
status = 'VERIFY';
|
|
1639
|
-
}
|
|
1640
|
-
return { transitioned: transitionName, toStatus: status };
|
|
1641
|
-
},
|
|
1642
|
-
};
|
|
1643
|
-
}
|
|
1644
|
-
|
|
1645
|
-
test('STG approval parses get_release_manager MCP content payload', async () => {
|
|
1646
|
-
const server = http.createServer((_req, res) => {
|
|
1647
|
-
res.setHeader('content-type', 'application/json');
|
|
1648
|
-
res.end(JSON.stringify({ events: [{ what: 'Sign off staff', who: 'Alvin Wang' }] }));
|
|
1649
|
-
});
|
|
1650
|
-
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
1651
|
-
|
|
1652
|
-
try {
|
|
1653
|
-
process.env.CONF_BASE_URL = `http://127.0.0.1:${server.address().port}`;
|
|
1654
|
-
process.env.CONF_TOKEN = 'test-token';
|
|
1655
|
-
process.env.JABBER_NOTIFY_SCRIPT = '/dev/null';
|
|
1656
|
-
process.env.POLL_INTERVAL_MS = '0';
|
|
1657
|
-
process.env.POLL_TIMEOUT_MS = '10';
|
|
1658
|
-
|
|
1659
|
-
const jira = makeApprovalJira('stg');
|
|
1660
|
-
const result = await executeTool(
|
|
1661
|
-
'auto_grayrelease',
|
|
1662
|
-
{ issueKey: 'CID-100', autoVerify: false },
|
|
1663
|
-
{ jira, notifier: mockNotifier },
|
|
1664
|
-
);
|
|
1665
|
-
|
|
1666
|
-
assert.ok(!result.content[0].text.startsWith('❌'), 'STG approval should continue');
|
|
1667
|
-
const data = JSON.parse(result.content[0].text);
|
|
1668
|
-
assert.equal(data.finalStatus, 'VERIFY');
|
|
1669
|
-
assert.deepEqual(jira.calls.updateAssignee, [{ issueKey: 'CID-100', accountId: 'BK00325' }]);
|
|
1670
|
-
} finally {
|
|
1671
|
-
await new Promise((resolve, reject) => {
|
|
1672
|
-
server.close((err) => (err ? reject(err) : resolve()));
|
|
1673
|
-
server.closeAllConnections?.();
|
|
1674
|
-
});
|
|
1675
|
-
restoreEnv();
|
|
1676
|
-
}
|
|
1677
|
-
});
|
|
1678
|
-
|
|
1679
|
-
test('UAT approval parses wait_for_comment MCP content payload', async () => {
|
|
1680
|
-
try {
|
|
1681
|
-
process.env.JABBER_NOTIFY_SCRIPT = '/dev/null';
|
|
1682
|
-
process.env.POLL_INTERVAL_MS = '0';
|
|
1683
|
-
process.env.POLL_TIMEOUT_MS = '10';
|
|
1684
|
-
|
|
1685
|
-
const jira = makeApprovalJira('uat', {
|
|
1686
|
-
comments: [{ body: 'Approved, please proceed', author: { name: 'BK00325', displayName: 'James Yu' } }],
|
|
1687
|
-
});
|
|
1688
|
-
const result = await executeTool(
|
|
1689
|
-
'auto_grayrelease',
|
|
1690
|
-
{ issueKey: 'CID-101', autoVerify: false },
|
|
1691
|
-
{ jira, notifier: mockNotifier },
|
|
1692
|
-
);
|
|
1693
|
-
|
|
1694
|
-
assert.ok(!result.content[0].text.startsWith('❌'), 'UAT approval should continue');
|
|
1695
|
-
const data = JSON.parse(result.content[0].text);
|
|
1696
|
-
assert.equal(data.finalStatus, 'VERIFY');
|
|
1697
|
-
assert.deepEqual(jira.calls.updateAssignee, [
|
|
1698
|
-
{ issueKey: 'CID-101', accountId: 'BK00325' },
|
|
1699
|
-
{ issueKey: 'CID-101', accountId: 'BK00325' },
|
|
1700
|
-
]);
|
|
1701
|
-
} finally {
|
|
1702
|
-
restoreEnv();
|
|
1703
|
-
}
|
|
1704
|
-
});
|
|
1705
|
-
|
|
1706
|
-
test('UAT approval emits progress for assignee, notification, comment wait, and final approval wait', async () => {
|
|
1707
|
-
const events = [];
|
|
1708
|
-
try {
|
|
1709
|
-
process.env.JABBER_NOTIFY_SCRIPT = '/dev/null';
|
|
1710
|
-
process.env.POLL_INTERVAL_MS = '0';
|
|
1711
|
-
process.env.POLL_TIMEOUT_MS = '10';
|
|
1712
|
-
|
|
1713
|
-
const jira = makeApprovalJira('uat', {
|
|
1714
|
-
comments: [{ body: 'Approved, please proceed', author: { name: 'BK00325', displayName: 'James Yu' } }],
|
|
1715
|
-
});
|
|
1716
|
-
const result = await executeTool(
|
|
1717
|
-
'auto_grayrelease',
|
|
1718
|
-
{ issueKey: 'CID-101', autoVerify: false },
|
|
1719
|
-
{
|
|
1720
|
-
jira,
|
|
1721
|
-
notifier: mockNotifier,
|
|
1722
|
-
progress: (event) => events.push(event),
|
|
1723
|
-
},
|
|
1724
|
-
);
|
|
1725
|
-
|
|
1726
|
-
assert.ok(!result.content[0].text.startsWith('❌'), result.content[0].text);
|
|
1727
|
-
assert.ok(events.some((event) => event.title === '指派 GrayRelease 簽核人' && event.detail.includes('James Yu')));
|
|
1728
|
-
assert.ok(events.some((event) => event.title === '發送 GrayRelease 簽核通知' && event.detail.includes('James Yu')));
|
|
1729
|
-
assert.ok(events.some((event) => event.title === '等待 Jira comment' && event.attempts === 1));
|
|
1730
|
-
assert.ok(events.some((event) => event.title === '指派 GrayRelease 簽核人' && event.detail.includes('Solar Chen')));
|
|
1731
|
-
assert.ok(events.some((event) => event.phase === 'polling' && event.targetStatus === 'WAIT DEPLOY'));
|
|
1732
|
-
} finally {
|
|
1733
|
-
restoreEnv();
|
|
1734
|
-
}
|
|
1735
|
-
});
|
|
1736
|
-
|
|
1737
|
-
test('WAIT APPROVAL fails when GrayRelease environment field is missing', async () => {
|
|
1738
|
-
const transitions = [];
|
|
1739
|
-
const jira = {
|
|
1740
|
-
getIssueFields: async () => ({
|
|
1741
|
-
customfield_13443: { value: 'CWA' },
|
|
1742
|
-
}),
|
|
1743
|
-
getIssue: async () => ({ fields: { status: { name: 'WAIT APPROVAL' }, summary: 'GrayRelease' } }),
|
|
1744
|
-
transitionByName: async (_issueKey, transitionName) => {
|
|
1745
|
-
transitions.push(transitionName);
|
|
1746
|
-
},
|
|
1747
|
-
};
|
|
1748
|
-
|
|
1749
|
-
const result = await executeTool(
|
|
1750
|
-
'auto_grayrelease',
|
|
1751
|
-
{ issueKey: 'CID-102', autoVerify: false },
|
|
1752
|
-
{ jira, notifier: mockNotifier },
|
|
1753
|
-
);
|
|
1754
|
-
|
|
1755
|
-
assert.ok(result.content[0].text.startsWith('❌'), 'missing environment should fail closed');
|
|
1756
|
-
assert.match(result.content[0].text, /無法讀取 GrayRelease 環境欄位/);
|
|
1757
|
-
assert.deepEqual(transitions, []);
|
|
1758
|
-
});
|
|
1759
|
-
});
|
|
1760
|
-
|
|
1761
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
1762
|
-
// auto_grayrelease build/deploy completion result fields
|
|
1763
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
1764
|
-
describe('auto_grayrelease — build/deploy result fields', () => {
|
|
1765
|
-
const savedEnv = {
|
|
1766
|
-
POLL_INTERVAL_MS: process.env.POLL_INTERVAL_MS,
|
|
1767
|
-
POLL_TIMEOUT_MS: process.env.POLL_TIMEOUT_MS,
|
|
1768
|
-
SWITCH_EXECUTION_NODE_WAIT_MS: process.env.SWITCH_EXECUTION_NODE_WAIT_MS,
|
|
1769
|
-
};
|
|
1770
|
-
|
|
1771
|
-
function useFastPolling() {
|
|
1772
|
-
process.env.POLL_INTERVAL_MS = '0';
|
|
1773
|
-
process.env.POLL_TIMEOUT_MS = '10';
|
|
1774
|
-
process.env.SWITCH_EXECUTION_NODE_WAIT_MS = '0';
|
|
1775
|
-
}
|
|
1776
|
-
|
|
1777
|
-
function restoreEnv() {
|
|
1778
|
-
for (const [key, value] of Object.entries(savedEnv)) {
|
|
1779
|
-
if (value === undefined) {
|
|
1780
|
-
delete process.env[key];
|
|
1781
|
-
} else {
|
|
1782
|
-
process.env[key] = value;
|
|
1783
|
-
}
|
|
1784
|
-
}
|
|
1785
|
-
}
|
|
1786
|
-
|
|
1787
|
-
test('get_grayrelease_status normalizes Jira display status names', async () => {
|
|
1788
|
-
const jira = {
|
|
1789
|
-
getIssueFields: async () => ({
|
|
1790
|
-
customfield_13436: { value: 'dev' },
|
|
1791
|
-
customfield_13443: { value: 'IBK' },
|
|
1792
|
-
}),
|
|
1793
|
-
getIssue: async () => ({ fields: { status: { name: 'Wait for Build' }, summary: 'GrayRelease' } }),
|
|
1794
|
-
getTransitions: async () => [
|
|
1795
|
-
{ id: '1', name: 'Apply to approval', to: { name: 'Wait Approval' } },
|
|
1796
|
-
{ id: '2', name: 'GrayRelease Build', to: { name: 'Wait for Build' } },
|
|
1797
|
-
],
|
|
1798
|
-
};
|
|
1799
|
-
|
|
1800
|
-
const result = await executeTool(
|
|
1801
|
-
'get_grayrelease_status',
|
|
1802
|
-
{ issueKey: 'CID-822' },
|
|
1803
|
-
{ jira, notifier: mockNotifier },
|
|
1804
|
-
);
|
|
1805
|
-
|
|
1806
|
-
assert.ok(!result.isError, result.content[0].text);
|
|
1807
|
-
const output = JSON.parse(result.content[0].text);
|
|
1808
|
-
assert.equal(output.currentStatus, 'Wait for Build');
|
|
1809
|
-
assert.equal(output.normalizedStatus, 'WAIT FOR BUILD');
|
|
1810
|
-
assert.deepEqual(output.nextSteps, ['GrayRelease Build']);
|
|
1811
|
-
});
|
|
1812
|
-
|
|
1813
|
-
test('deploy_grayrelease restarts from Planning and advances via approval without rebuilding', async () => {
|
|
1814
|
-
useFastPolling();
|
|
1815
|
-
let status = 'Wait for Build';
|
|
1816
|
-
const transitions = [];
|
|
1817
|
-
const jira = {
|
|
1818
|
-
getIssueFields: async (_issueKey, fieldNames = []) => {
|
|
1819
|
-
if (fieldNames.includes('customfield_13433')) {
|
|
1820
|
-
return { customfield_13433: 'pass' };
|
|
1821
|
-
}
|
|
1822
|
-
return {
|
|
1823
|
-
customfield_13436: { value: 'dev' },
|
|
1824
|
-
customfield_13443: { value: 'IBK' },
|
|
1825
|
-
};
|
|
1826
|
-
},
|
|
1827
|
-
getIssue: async () => ({ fields: { status: { name: status }, summary: 'GrayRelease' } }),
|
|
1828
|
-
getTransitions: async () => {
|
|
1829
|
-
if (status === 'Wait for Build') {
|
|
1830
|
-
return [
|
|
1831
|
-
{ id: '1', name: 'Apply to approval', to: { name: 'Wait Approval' } },
|
|
1832
|
-
{ id: '2', name: 'GrayRelease Build', to: { name: 'Wait for Build' } },
|
|
1833
|
-
];
|
|
1834
|
-
}
|
|
1835
|
-
return [];
|
|
1836
|
-
},
|
|
1837
|
-
searchIssues: async () => [{ fields: { customfield_13436: { value: 'stg' } } }],
|
|
1838
|
-
transitionByName: async (_issueKey, transitionName) => {
|
|
1839
|
-
transitions.push(transitionName);
|
|
1840
|
-
if (transitionName === 'Planning') status = 'Planning';
|
|
1841
|
-
if (transitionName === 'Accept') status = 'Wait for Build';
|
|
1842
|
-
if (transitionName === 'Apply to approval') status = 'Wait Approval';
|
|
1843
|
-
if (transitionName === 'Approve') status = 'Wait Deploy';
|
|
1844
|
-
if (transitionName === 'GrayRelease Deploy') status = 'VERIFY';
|
|
1845
|
-
},
|
|
1846
|
-
};
|
|
1847
|
-
|
|
1848
|
-
try {
|
|
1849
|
-
const result = await executeTool(
|
|
1850
|
-
'deploy_grayrelease',
|
|
1851
|
-
{ issueKey: 'CID-822' },
|
|
1852
|
-
{ jira, notifier: mockNotifier },
|
|
1853
|
-
);
|
|
1854
|
-
|
|
1855
|
-
assert.ok(!result.isError, result.content[0].text);
|
|
1856
|
-
const output = JSON.parse(result.content[0].text);
|
|
1857
|
-
assert.equal(output.finalStatus, 'VERIFY');
|
|
1858
|
-
assert.deepEqual(transitions, [
|
|
1859
|
-
'Planning',
|
|
1860
|
-
'Accept',
|
|
1861
|
-
'Apply to approval',
|
|
1862
|
-
'Approve',
|
|
1863
|
-
'GrayRelease Deploy',
|
|
1864
|
-
]);
|
|
1865
|
-
} finally {
|
|
1866
|
-
restoreEnv();
|
|
1867
|
-
}
|
|
1868
|
-
});
|
|
1869
|
-
|
|
1870
|
-
test('deploy_grayrelease keeps rc deploy behavior by resetting from VERIFY to Planning first', async () => {
|
|
1871
|
-
const transitions = [];
|
|
1872
|
-
const jira = {
|
|
1873
|
-
getIssueFields: async () => ({
|
|
1874
|
-
customfield_13436: { value: 'dev' },
|
|
1875
|
-
customfield_13443: { value: 'IBK' },
|
|
1876
|
-
}),
|
|
1877
|
-
getIssue: async () => ({ fields: { status: { name: 'VERIFY' }, summary: 'GrayRelease' } }),
|
|
1878
|
-
transitionByName: async (_issueKey, transitionName) => {
|
|
1879
|
-
transitions.push(transitionName);
|
|
1880
|
-
},
|
|
1881
|
-
};
|
|
1882
|
-
|
|
1883
|
-
const result = await executeTool(
|
|
1884
|
-
'deploy_grayrelease',
|
|
1885
|
-
{ issueKey: 'CID-822' },
|
|
1886
|
-
{ jira, notifier: mockNotifier },
|
|
1887
|
-
);
|
|
1888
|
-
|
|
1889
|
-
assert.ok(!result.isError, result.content[0].text);
|
|
1890
|
-
const output = JSON.parse(result.content[0].text);
|
|
1891
|
-
assert.equal(output.finalStatus, 'VERIFY');
|
|
1892
|
-
assert.deepEqual(transitions, ['Planning']);
|
|
1893
|
-
assert.match(output.log.join('\n'), /確保每次都會從 build 開始/);
|
|
1894
|
-
});
|
|
1895
|
-
|
|
1896
|
-
test('Build waits until customfield_13432 becomes pass before Apply to approval', async () => {
|
|
1897
|
-
useFastPolling();
|
|
1898
|
-
const events = [];
|
|
1899
|
-
let status = 'WAIT FOR BUILD';
|
|
1900
|
-
let buildResultCalls = 0;
|
|
1901
|
-
const transitions = [];
|
|
1902
|
-
const jira = {
|
|
1903
|
-
getIssueFields: async (_issueKey, fieldNames = []) => {
|
|
1904
|
-
if (fieldNames.includes('customfield_13432')) {
|
|
1905
|
-
buildResultCalls++;
|
|
1906
|
-
return { customfield_13432: buildResultCalls >= 3 ? 'pass' : 'starting' };
|
|
1907
|
-
}
|
|
1908
|
-
return {
|
|
1909
|
-
customfield_13436: { value: 'dev' },
|
|
1910
|
-
customfield_13443: { value: 'CWA' },
|
|
1911
|
-
};
|
|
1912
|
-
},
|
|
1913
|
-
getIssue: async () => ({ fields: { status: { name: status }, summary: 'GrayRelease' } }),
|
|
1914
|
-
transitionByName: async (_issueKey, transitionName) => {
|
|
1915
|
-
transitions.push(transitionName);
|
|
1916
|
-
if (transitionName === 'Apply to approval') {
|
|
1917
|
-
status = 'DONE';
|
|
1918
|
-
}
|
|
1919
|
-
},
|
|
1920
|
-
};
|
|
1921
|
-
|
|
1922
|
-
try {
|
|
1923
|
-
const result = await executeTool(
|
|
1924
|
-
'auto_grayrelease',
|
|
1925
|
-
{ issueKey: 'CID-200', autoVerify: false },
|
|
1926
|
-
{ jira, notifier: mockNotifier, progress: (event) => events.push(event) },
|
|
1927
|
-
);
|
|
1928
|
-
|
|
1929
|
-
assert.ok(!result.content[0].text.startsWith('❌'), 'Build flow should succeed');
|
|
1930
|
-
assert.equal(buildResultCalls, 3);
|
|
1931
|
-
assert.deepEqual(transitions, ['GrayRelease Build', 'Apply to approval']);
|
|
1932
|
-
assert.ok(events.some((event) => (
|
|
1933
|
-
event.phase === 'polling' &&
|
|
1934
|
-
event.title === '等待 GrayRelease Build 結果' &&
|
|
1935
|
-
event.attempts === 3
|
|
1936
|
-
)));
|
|
1937
|
-
} finally {
|
|
1938
|
-
restoreEnv();
|
|
1939
|
-
}
|
|
1940
|
-
});
|
|
1941
|
-
|
|
1942
|
-
test('Deploy waits until cid jira worker moves status to VERIFY with pass result', async () => {
|
|
1943
|
-
useFastPolling();
|
|
1944
|
-
let status = 'WAIT DEPLOY';
|
|
1945
|
-
let deployResultCalls = 0;
|
|
1946
|
-
const transitions = [];
|
|
1947
|
-
const jira = {
|
|
1948
|
-
getIssueFields: async (_issueKey, fieldNames = []) => {
|
|
1949
|
-
if (fieldNames.includes('customfield_13433')) {
|
|
1950
|
-
deployResultCalls++;
|
|
1951
|
-
return { customfield_13433: deployResultCalls >= 3 ? 'pass' : 'starting' };
|
|
1952
|
-
}
|
|
1953
|
-
return {
|
|
1954
|
-
customfield_13436: { value: 'stg' },
|
|
1955
|
-
customfield_13443: { value: 'CWA' },
|
|
1956
|
-
};
|
|
1957
|
-
},
|
|
1958
|
-
getIssue: async () => ({
|
|
1959
|
-
fields: {
|
|
1960
|
-
status: { name: deployResultCalls >= 2 ? 'VERIFY' : status },
|
|
1961
|
-
summary: 'GrayRelease',
|
|
1962
|
-
},
|
|
1963
|
-
}),
|
|
1964
|
-
searchIssues: async () => [{ fields: { customfield_13436: { value: 'stg' } } }],
|
|
1965
|
-
transitionByName: async (_issueKey, transitionName) => {
|
|
1966
|
-
transitions.push(transitionName);
|
|
1967
|
-
},
|
|
1968
|
-
};
|
|
1969
|
-
|
|
1970
|
-
try {
|
|
1971
|
-
const result = await executeTool(
|
|
1972
|
-
'auto_grayrelease',
|
|
1973
|
-
{ issueKey: 'CID-201', autoVerify: false },
|
|
1974
|
-
{ jira, notifier: mockNotifier },
|
|
1975
|
-
);
|
|
1976
|
-
|
|
1977
|
-
assert.ok(!result.content[0].text.startsWith('❌'), 'Deploy flow should succeed');
|
|
1978
|
-
assert.equal(deployResultCalls, 3);
|
|
1979
|
-
assert.deepEqual(transitions, ['GrayRelease Deploy']);
|
|
1980
|
-
} finally {
|
|
1981
|
-
restoreEnv();
|
|
1982
|
-
}
|
|
1983
|
-
});
|
|
1984
|
-
|
|
1985
|
-
test('Deploy skips Switch Execution Node when recent cid jira worker success comment exists', async () => {
|
|
1986
|
-
useFastPolling();
|
|
1987
|
-
let status = 'WAIT DEPLOY';
|
|
1988
|
-
const transitions = [];
|
|
1989
|
-
const jira = {
|
|
1990
|
-
getIssueFields: async (_issueKey, fieldNames = []) => {
|
|
1991
|
-
if (fieldNames.includes('customfield_13433')) {
|
|
1992
|
-
return { customfield_13433: 'pass' };
|
|
1993
|
-
}
|
|
1994
|
-
return {
|
|
1995
|
-
customfield_13436: { value: 'stg' },
|
|
1996
|
-
customfield_13443: { value: 'IBK' },
|
|
1997
|
-
};
|
|
1998
|
-
},
|
|
1999
|
-
getIssue: async () => ({ fields: { status: { name: status }, summary: 'GrayRelease' } }),
|
|
2000
|
-
getComments: async () => [
|
|
2001
|
-
{
|
|
2002
|
-
body: "Trigger update IBK's instance_group to [ING]NonPRD_ExecutionNode success, please wait about 3 mins before trigger deploy.",
|
|
2003
|
-
author: { displayName: 'cid jira worker' },
|
|
2004
|
-
created: new Date().toISOString(),
|
|
2005
|
-
},
|
|
2006
|
-
],
|
|
2007
|
-
searchIssues: async () => [{ fields: { customfield_13436: { value: 'prd' } } }],
|
|
2008
|
-
transitionByName: async (_issueKey, transitionName) => {
|
|
2009
|
-
transitions.push(transitionName);
|
|
2010
|
-
if (transitionName === 'GrayRelease Deploy') {
|
|
2011
|
-
status = 'VERIFY';
|
|
2012
|
-
}
|
|
2013
|
-
},
|
|
2014
|
-
};
|
|
2015
|
-
|
|
2016
|
-
try {
|
|
2017
|
-
const result = await executeTool(
|
|
2018
|
-
'auto_grayrelease',
|
|
2019
|
-
{ issueKey: 'CID-202', autoVerify: false },
|
|
2020
|
-
{ jira, notifier: mockNotifier },
|
|
2021
|
-
);
|
|
2022
|
-
|
|
2023
|
-
assert.ok(!result.content[0].text.startsWith('❌'), 'Deploy flow should succeed');
|
|
2024
|
-
assert.deepEqual(transitions, ['GrayRelease Deploy']);
|
|
2025
|
-
} finally {
|
|
2026
|
-
restoreEnv();
|
|
2027
|
-
}
|
|
2028
|
-
});
|
|
2029
|
-
|
|
2030
|
-
test('Deploy waits for worker success comment after Switch Execution Node then deploys directly', async () => {
|
|
2031
|
-
useFastPolling();
|
|
2032
|
-
let status = 'WAIT DEPLOY';
|
|
2033
|
-
let getCommentsCalls = 0;
|
|
2034
|
-
let searchIssuesCalls = 0;
|
|
2035
|
-
const transitions = [];
|
|
2036
|
-
const jira = {
|
|
2037
|
-
getIssueFields: async (_issueKey, fieldNames = []) => {
|
|
2038
|
-
if (fieldNames.includes('customfield_13433')) {
|
|
2039
|
-
return { customfield_13433: 'pass' };
|
|
2040
|
-
}
|
|
2041
|
-
return {
|
|
2042
|
-
customfield_13436: { value: 'stg' },
|
|
2043
|
-
customfield_13443: { value: 'IBK' },
|
|
2044
|
-
};
|
|
2045
|
-
},
|
|
2046
|
-
getIssue: async () => ({ fields: { status: { name: status }, summary: 'GrayRelease' } }),
|
|
2047
|
-
getComments: async () => {
|
|
2048
|
-
getCommentsCalls++;
|
|
2049
|
-
if (getCommentsCalls < 2) {
|
|
2050
|
-
return [];
|
|
2051
|
-
}
|
|
2052
|
-
return [
|
|
2053
|
-
{
|
|
2054
|
-
body: "Trigger update IBK's instance_group to [ING]NonPRD_ExecutionNode success, please wait about 3 mins before trigger deploy.",
|
|
2055
|
-
author: { displayName: 'CID Jira Worker' },
|
|
2056
|
-
created: new Date().toISOString(),
|
|
2057
|
-
},
|
|
2058
|
-
];
|
|
2059
|
-
},
|
|
2060
|
-
searchIssues: async () => {
|
|
2061
|
-
searchIssuesCalls++;
|
|
2062
|
-
return [{ fields: { customfield_13436: { value: 'prd' } } }];
|
|
2063
|
-
},
|
|
2064
|
-
transitionByName: async (_issueKey, transitionName) => {
|
|
2065
|
-
transitions.push(transitionName);
|
|
2066
|
-
if (transitionName === 'GrayRelease Deploy') {
|
|
2067
|
-
status = 'VERIFY';
|
|
2068
|
-
}
|
|
2069
|
-
},
|
|
2070
|
-
};
|
|
2071
|
-
|
|
2072
|
-
try {
|
|
2073
|
-
const result = await executeTool(
|
|
2074
|
-
'auto_grayrelease',
|
|
2075
|
-
{ issueKey: 'CID-203', autoVerify: false },
|
|
2076
|
-
{ jira, notifier: mockNotifier },
|
|
2077
|
-
);
|
|
2078
|
-
|
|
2079
|
-
assert.ok(!result.content[0].text.startsWith('❌'), 'Deploy flow should succeed');
|
|
2080
|
-
assert.equal(searchIssuesCalls, 1);
|
|
2081
|
-
assert.deepEqual(transitions, ['Switch Execution Node', 'GrayRelease Deploy']);
|
|
2082
|
-
} finally {
|
|
2083
|
-
restoreEnv();
|
|
2084
|
-
}
|
|
2085
|
-
});
|
|
2086
|
-
});
|
|
2087
|
-
|
|
2088
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
2089
|
-
// prepare_cd_deployment
|
|
2090
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
2091
|
-
describe('prepare_cd_deployment', () => {
|
|
2092
|
-
const DEPLOY_TRANS = {
|
|
2093
|
-
id: '999',
|
|
2094
|
-
name: 'Prepare to Create Deployment',
|
|
2095
|
-
to: { name: 'Deploying' },
|
|
2096
|
-
};
|
|
2097
|
-
|
|
2098
|
-
/** 建立含 updateIssue / transitionById / getIssue / getTransitions 的 mock */
|
|
2099
|
-
function makeDeployMock({
|
|
2100
|
-
transitionName = 'Prepare to Create Deployment',
|
|
2101
|
-
resultStatus = 'Deploying',
|
|
2102
|
-
} = {}) {
|
|
2103
|
-
const calls = { updateIssue: [], transitionById: [] };
|
|
2104
|
-
return {
|
|
2105
|
-
calls,
|
|
2106
|
-
updateIssue: async (key, fields) => {
|
|
2107
|
-
calls.updateIssue.push({ key, fields });
|
|
2108
|
-
return {};
|
|
2109
|
-
},
|
|
2110
|
-
getTransitions: async () => [{ id: '999', name: transitionName, to: { name: resultStatus } }],
|
|
2111
|
-
transitionById: async (key, id) => {
|
|
2112
|
-
calls.transitionById.push({ key, id });
|
|
2113
|
-
return {};
|
|
2114
|
-
},
|
|
2115
|
-
getIssue: async () => ({ fields: { status: { name: resultStatus }, summary: 'CD test' } }),
|
|
2116
|
-
addComment: async () => { },
|
|
2117
|
-
};
|
|
2118
|
-
}
|
|
2119
|
-
|
|
2120
|
-
test('dev:更新 customfield_13436 為 id 14355', async () => {
|
|
2121
|
-
const jira = makeDeployMock();
|
|
2122
|
-
await executeTool(
|
|
2123
|
-
'prepare_cd_deployment',
|
|
2124
|
-
{ issueKey: 'CID-1697', environment: 'dev' },
|
|
2125
|
-
{
|
|
2126
|
-
jira,
|
|
2127
|
-
notifier: mockNotifier,
|
|
2128
|
-
},
|
|
2129
|
-
);
|
|
2130
|
-
assert.deepEqual(jira.calls.updateIssue[0].fields['customfield_13436'], { id: '14355' });
|
|
2131
|
-
});
|
|
2132
|
-
|
|
2133
|
-
test('stg:更新 customfield_13436 為 id 14356', async () => {
|
|
2134
|
-
const jira = makeDeployMock();
|
|
2135
|
-
await executeTool(
|
|
2136
|
-
'prepare_cd_deployment',
|
|
2137
|
-
{ issueKey: 'CID-1697', environment: 'stg' },
|
|
2138
|
-
{
|
|
2139
|
-
jira,
|
|
2140
|
-
notifier: mockNotifier,
|
|
2141
|
-
},
|
|
2142
|
-
);
|
|
2143
|
-
assert.deepEqual(jira.calls.updateIssue[0].fields['customfield_13436'], { id: '14356' });
|
|
2144
|
-
});
|
|
2145
|
-
|
|
2146
|
-
test('uat:更新 customfield_13436 為 id 14357', async () => {
|
|
2147
|
-
const jira = makeDeployMock();
|
|
2148
|
-
await executeTool(
|
|
2149
|
-
'prepare_cd_deployment',
|
|
2150
|
-
{ issueKey: 'CID-1697', environment: 'uat' },
|
|
2151
|
-
{
|
|
2152
|
-
jira,
|
|
2153
|
-
notifier: mockNotifier,
|
|
2154
|
-
},
|
|
2155
|
-
);
|
|
2156
|
-
assert.deepEqual(jira.calls.updateIssue[0].fields['customfield_13436'], { id: '14357' });
|
|
2157
|
-
});
|
|
2158
|
-
|
|
2159
|
-
test('prd:更新 customfield_13436 為 id 14358', async () => {
|
|
2160
|
-
const jira = makeDeployMock();
|
|
2161
|
-
await executeTool(
|
|
2162
|
-
'prepare_cd_deployment',
|
|
2163
|
-
{ issueKey: 'CID-1697', environment: 'prd' },
|
|
2164
|
-
{
|
|
2165
|
-
jira,
|
|
2166
|
-
notifier: mockNotifier,
|
|
2167
|
-
},
|
|
2168
|
-
);
|
|
2169
|
-
assert.deepEqual(jira.calls.updateIssue[0].fields['customfield_13436'], { id: '14358' });
|
|
2170
|
-
});
|
|
2171
|
-
|
|
2172
|
-
test('dr:更新 customfield_13436 為 id 14359', async () => {
|
|
2173
|
-
const jira = makeDeployMock();
|
|
2174
|
-
await executeTool(
|
|
2175
|
-
'prepare_cd_deployment',
|
|
2176
|
-
{ issueKey: 'CID-1697', environment: 'dr' },
|
|
2177
|
-
{
|
|
2178
|
-
jira,
|
|
2179
|
-
notifier: mockNotifier,
|
|
2180
|
-
},
|
|
2181
|
-
);
|
|
2182
|
-
assert.deepEqual(jira.calls.updateIssue[0].fields['customfield_13436'], { id: '14359' });
|
|
2183
|
-
});
|
|
2184
|
-
|
|
2185
|
-
test('prd/dr:更新 customfield_13436 為 id 14360', async () => {
|
|
2186
|
-
const jira = makeDeployMock();
|
|
2187
|
-
await executeTool(
|
|
2188
|
-
'prepare_cd_deployment',
|
|
2189
|
-
{ issueKey: 'CID-1697', environment: 'prd/dr' },
|
|
2190
|
-
{
|
|
2191
|
-
jira,
|
|
2192
|
-
notifier: mockNotifier,
|
|
2193
|
-
},
|
|
2194
|
-
);
|
|
2195
|
-
assert.deepEqual(jira.calls.updateIssue[0].fields['customfield_13436'], { id: '14360' });
|
|
2196
|
-
});
|
|
2197
|
-
|
|
2198
|
-
test('prd&dr 正規化為 prd/dr → id 14360', async () => {
|
|
2199
|
-
const jira = makeDeployMock();
|
|
2200
|
-
await executeTool(
|
|
2201
|
-
'prepare_cd_deployment',
|
|
2202
|
-
{ issueKey: 'CID-1697', environment: 'prd&dr' },
|
|
2203
|
-
{
|
|
2204
|
-
jira,
|
|
2205
|
-
notifier: mockNotifier,
|
|
2206
|
-
},
|
|
2207
|
-
);
|
|
2208
|
-
assert.deepEqual(jira.calls.updateIssue[0].fields['customfield_13436'], { id: '14360' });
|
|
2209
|
-
});
|
|
2210
|
-
|
|
2211
|
-
test('觸發 "Prepare to Create Deployment" transition', async () => {
|
|
2212
|
-
const jira = makeDeployMock();
|
|
2213
|
-
const result = await executeTool(
|
|
2214
|
-
'prepare_cd_deployment',
|
|
2215
|
-
{ issueKey: 'CID-1697', environment: 'stg' },
|
|
2216
|
-
{
|
|
2217
|
-
jira,
|
|
2218
|
-
notifier: mockNotifier,
|
|
2219
|
-
},
|
|
2220
|
-
);
|
|
2221
|
-
assert.equal(jira.calls.transitionById[0].key, 'CID-1697');
|
|
2222
|
-
assert.equal(jira.calls.transitionById[0].id, '999');
|
|
2223
|
-
const data = JSON.parse(result.content[0].text);
|
|
2224
|
-
assert.equal(data.issueKey, 'CID-1697');
|
|
2225
|
-
assert.equal(data.environment, 'stg');
|
|
2226
|
-
assert.equal(data.status, 'Deploying');
|
|
2227
|
-
});
|
|
2228
|
-
|
|
2229
|
-
test('fallback:transition 名稱為 "Deploy" 也能觸發', async () => {
|
|
2230
|
-
const jira = makeDeployMock({ transitionName: 'Deploy' });
|
|
2231
|
-
const result = await executeTool(
|
|
2232
|
-
'prepare_cd_deployment',
|
|
2233
|
-
{ issueKey: 'CID-1697', environment: 'uat' },
|
|
2234
|
-
{
|
|
2235
|
-
jira,
|
|
2236
|
-
notifier: mockNotifier,
|
|
2237
|
-
},
|
|
2238
|
-
);
|
|
2239
|
-
assert.ok(!result.content[0].text.startsWith('❌'), '應成功觸發');
|
|
2240
|
-
assert.equal(jira.calls.transitionById.length, 1);
|
|
2241
|
-
});
|
|
2242
|
-
|
|
2243
|
-
test('找不到任何 deploy transition → 回傳錯誤', async () => {
|
|
2244
|
-
const jira = {
|
|
2245
|
-
updateIssue: async () => { },
|
|
2246
|
-
getTransitions: async () => [
|
|
2247
|
-
{ id: '1', name: 'Some Other Transition', to: { name: 'Other' } },
|
|
2248
|
-
],
|
|
2249
|
-
getIssue: async () => ({ fields: { status: { name: 'Approved' }, summary: 'CD test' } }),
|
|
2250
|
-
addComment: async () => { },
|
|
2251
|
-
};
|
|
2252
|
-
const result = await executeTool(
|
|
2253
|
-
'prepare_cd_deployment',
|
|
2254
|
-
{ issueKey: 'CID-1697', environment: 'stg' },
|
|
2255
|
-
{
|
|
2256
|
-
jira,
|
|
2257
|
-
notifier: mockNotifier,
|
|
2258
|
-
},
|
|
2259
|
-
);
|
|
2260
|
-
assert.ok(result.content[0].text.startsWith('❌'), '應回傳錯誤');
|
|
2261
|
-
assert.ok(result.content[0].text.includes('找不到部署 transition'), '應說明找不到 transition');
|
|
2262
|
-
});
|
|
2263
|
-
|
|
2264
|
-
test('不合法的環境 → 回傳錯誤', async () => {
|
|
2265
|
-
const jira = makeDeployMock();
|
|
2266
|
-
const result = await executeTool(
|
|
2267
|
-
'prepare_cd_deployment',
|
|
2268
|
-
{ issueKey: 'CID-1697', environment: 'invalid' },
|
|
2269
|
-
{
|
|
2270
|
-
jira,
|
|
2271
|
-
notifier: mockNotifier,
|
|
2272
|
-
},
|
|
2273
|
-
);
|
|
2274
|
-
assert.ok(result.content[0].text.startsWith('❌'));
|
|
2275
|
-
assert.ok(result.content[0].text.includes('不支援的 CD 部署環境'));
|
|
2276
|
-
});
|
|
2277
|
-
|
|
2278
|
-
test('pre-transition:Accept 後出現 Prepare to create deployment ticket → 觸發', async () => {
|
|
2279
|
-
let callCount = 0;
|
|
2280
|
-
const calls = { transitionById: [] };
|
|
2281
|
-
const jira = {
|
|
2282
|
-
updateIssue: async () => { },
|
|
2283
|
-
getTransitions: async () => {
|
|
2284
|
-
callCount++;
|
|
2285
|
-
// findDeployTrans(1) → 無;pre-transition check(2) → Accept;findDeployTrans after Accept(3) → deploy
|
|
2286
|
-
if (callCount <= 2)
|
|
2287
|
-
return [{ id: '10', name: 'Accept', to: { name: 'Wait For Send Notice Email' } }];
|
|
2288
|
-
return [
|
|
2289
|
-
{
|
|
2290
|
-
id: '71',
|
|
2291
|
-
name: 'Prepare to create deployment ticket',
|
|
2292
|
-
to: { name: 'Prepare For Deploy' },
|
|
2293
|
-
},
|
|
2294
|
-
];
|
|
2295
|
-
},
|
|
2296
|
-
transitionById: async (key, id) => {
|
|
2297
|
-
calls.transitionById.push({ key, id });
|
|
2298
|
-
return {};
|
|
2299
|
-
},
|
|
2300
|
-
getIssue: async () => ({ fields: { status: { name: 'Prepare For Deploy' }, summary: 'CD' } }),
|
|
2301
|
-
addComment: async () => { },
|
|
2302
|
-
};
|
|
2303
|
-
const result = await executeTool(
|
|
2304
|
-
'prepare_cd_deployment',
|
|
2305
|
-
{ issueKey: 'CID-1697', environment: 'stg' },
|
|
2306
|
-
{
|
|
2307
|
-
jira,
|
|
2308
|
-
notifier: mockNotifier,
|
|
2309
|
-
},
|
|
2310
|
-
);
|
|
2311
|
-
assert.ok(!result.content[0].text.startsWith('❌'), '應成功觸發');
|
|
2312
|
-
const data = JSON.parse(result.content[0].text);
|
|
2313
|
-
assert.equal(data.status, 'Prepare For Deploy');
|
|
2314
|
-
assert.equal(
|
|
2315
|
-
calls.transitionById.length,
|
|
2316
|
-
2,
|
|
2317
|
-
'應觸發 Accept 與 Prepare to create deployment ticket 共 2 次',
|
|
2318
|
-
);
|
|
2319
|
-
assert.equal(calls.transitionById[0].id, '10', '第 1 次應觸發 Accept');
|
|
2320
|
-
assert.equal(
|
|
2321
|
-
calls.transitionById[1].id,
|
|
2322
|
-
'71',
|
|
2323
|
-
'第 2 次應觸發 Prepare to create deployment ticket',
|
|
2324
|
-
);
|
|
2325
|
-
});
|
|
2326
|
-
|
|
2327
|
-
test('dev:建立 deployment 後自助執行 Apply for approval 與 Approved 到 Wait Deploy', async () => {
|
|
2328
|
-
let callCount = 0;
|
|
2329
|
-
let status = 'TO DO';
|
|
2330
|
-
const calls = { transitionById: [] };
|
|
2331
|
-
const jira = {
|
|
2332
|
-
updateIssue: async () => { },
|
|
2333
|
-
getTransitions: async () => {
|
|
2334
|
-
callCount++;
|
|
2335
|
-
if (callCount === 1) {
|
|
2336
|
-
return [{ id: '71', name: 'Prepare to create deployment ticket', to: { name: 'Prepare For Deploy' } }];
|
|
2337
|
-
}
|
|
2338
|
-
if (callCount === 2) {
|
|
2339
|
-
return [{ id: '21', name: 'Apply for approval', to: { name: 'Wait Approval' } }];
|
|
2340
|
-
}
|
|
2341
|
-
if (callCount === 3) {
|
|
2342
|
-
return [{ id: '72', name: 'Approved', to: { name: 'Wait Deploy' } }];
|
|
2343
|
-
}
|
|
2344
|
-
return [];
|
|
2345
|
-
},
|
|
2346
|
-
transitionById: async (key, id) => {
|
|
2347
|
-
calls.transitionById.push({ key, id });
|
|
2348
|
-
if (id === '71') status = 'Prepare For Deploy';
|
|
2349
|
-
if (id === '21') status = 'Wait Approval';
|
|
2350
|
-
if (id === '72') status = 'Wait Deploy';
|
|
2351
|
-
return {};
|
|
2352
|
-
},
|
|
2353
|
-
getIssue: async () => ({ fields: { status: { name: status }, summary: 'CD' } }),
|
|
2354
|
-
addComment: async () => { },
|
|
2355
|
-
};
|
|
2356
|
-
const result = await executeTool(
|
|
2357
|
-
'prepare_cd_deployment',
|
|
2358
|
-
{ issueKey: 'CID-1697', environment: 'dev' },
|
|
2359
|
-
{
|
|
2360
|
-
jira,
|
|
2361
|
-
notifier: mockNotifier,
|
|
2362
|
-
},
|
|
2363
|
-
);
|
|
2364
|
-
assert.ok(!result.content[0].text.startsWith('❌'), '應成功觸發');
|
|
2365
|
-
const data = JSON.parse(result.content[0].text);
|
|
2366
|
-
assert.equal(data.status, 'Wait Deploy');
|
|
2367
|
-
assert.deepEqual(calls.transitionById.map((call) => call.id), ['71', '21', '72']);
|
|
2368
|
-
});
|
|
2369
|
-
|
|
2370
|
-
test('dev:deployment ticket 已存在時續跑 approval 到 Wait Deploy', async () => {
|
|
2371
|
-
let callCount = 0;
|
|
2372
|
-
let status = 'Prepare For Deploy';
|
|
2373
|
-
const calls = { transitionById: [] };
|
|
2374
|
-
const jira = {
|
|
2375
|
-
updateIssue: async () => { },
|
|
2376
|
-
getTransitions: async () => {
|
|
2377
|
-
callCount++;
|
|
2378
|
-
if (callCount === 1) {
|
|
2379
|
-
return [{ id: '71', name: 'Prepare to create deployment ticket', to: { name: 'Prepare For Deploy' } }];
|
|
2380
|
-
}
|
|
2381
|
-
if (callCount === 2) {
|
|
2382
|
-
return [{ id: '21', name: 'Apply for approval', to: { name: 'Wait Approval' } }];
|
|
2383
|
-
}
|
|
2384
|
-
if (callCount === 3) {
|
|
2385
|
-
return [{ id: '72', name: 'Approved', to: { name: 'Wait Deploy' } }];
|
|
2386
|
-
}
|
|
2387
|
-
return [];
|
|
2388
|
-
},
|
|
2389
|
-
transitionById: async (key, id) => {
|
|
2390
|
-
calls.transitionById.push({ key, id });
|
|
2391
|
-
if (id === '71') throw new Error('Transition id=71 failed: {"errorMessages":["Already create deployment ticket"],"errors":{}}');
|
|
2392
|
-
if (id === '21') status = 'Wait Approval';
|
|
2393
|
-
if (id === '72') status = 'Wait Deploy';
|
|
2394
|
-
return {};
|
|
2395
|
-
},
|
|
2396
|
-
getIssue: async () => ({ fields: { status: { name: status }, summary: 'CD' } }),
|
|
2397
|
-
addComment: async () => { },
|
|
2398
|
-
};
|
|
2399
|
-
const result = await executeTool(
|
|
2400
|
-
'prepare_cd_deployment',
|
|
2401
|
-
{ issueKey: 'CID-1910', environment: 'dev' },
|
|
2402
|
-
{
|
|
2403
|
-
jira,
|
|
2404
|
-
notifier: mockNotifier,
|
|
2405
|
-
},
|
|
2406
|
-
);
|
|
2407
|
-
|
|
2408
|
-
assert.ok(!result.content[0].text.startsWith('❌'), '應成功續跑');
|
|
2409
|
-
const data = JSON.parse(result.content[0].text);
|
|
2410
|
-
assert.equal(data.status, 'Wait Deploy');
|
|
2411
|
-
assert.deepEqual(calls.transitionById.map((call) => call.id), ['71', '21', '72']);
|
|
2412
|
-
assert.ok(data.steps.some((step) => step.includes('Deployment ticket 已存在')));
|
|
2413
|
-
});
|
|
2414
|
-
});
|
|
2415
|
-
|
|
2416
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
2417
|
-
// wait_to_dev
|
|
2418
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
2419
|
-
describe('wait_to_dev', () => {
|
|
2420
|
-
test('執行 Upload Scan Report 與 Accept,停在 Wait To DEV', async () => {
|
|
2421
|
-
const calls = [];
|
|
2422
|
-
const jira = {
|
|
2423
|
-
getTransitions: async () => [
|
|
2424
|
-
{ id: '10', name: 'Upload Scan Report', to: { name: 'Upload Report' } },
|
|
2425
|
-
{ id: '11', name: 'Accept', to: { name: 'Wait To DEV' } },
|
|
2426
|
-
],
|
|
2427
|
-
transitionById: async (key, id) => {
|
|
2428
|
-
calls.push({ key, id });
|
|
2429
|
-
},
|
|
2430
|
-
getIssue: async () => ({ fields: { status: { name: 'Wait To DEV' }, summary: 'CI' } }),
|
|
2431
|
-
addComment: async () => { },
|
|
2432
|
-
};
|
|
2433
|
-
|
|
2434
|
-
const result = await executeTool(
|
|
2435
|
-
'wait_to_dev',
|
|
2436
|
-
{ issueKey: 'CID-1709' },
|
|
2437
|
-
{ jira, notifier: mockNotifier },
|
|
2438
|
-
);
|
|
2439
|
-
|
|
2440
|
-
assert.ok(!result.content[0].text.startsWith('❌'), '應成功觸發');
|
|
2441
|
-
const data = JSON.parse(result.content[0].text);
|
|
2442
|
-
assert.equal(data.status, 'Wait To DEV');
|
|
2443
|
-
assert.deepEqual(calls.map((call) => call.id), ['10', '11']);
|
|
2444
|
-
});
|
|
2445
|
-
|
|
2446
|
-
test('已是 Wait To DEV 時可跳過缺少的 Accept transition', async () => {
|
|
2447
|
-
const calls = [];
|
|
2448
|
-
const jira = {
|
|
2449
|
-
getTransitions: async () => [],
|
|
2450
|
-
transitionById: async (key, id) => {
|
|
2451
|
-
calls.push({ key, id });
|
|
2452
|
-
},
|
|
2453
|
-
getIssue: async () => ({ fields: { status: { name: 'Wait To DEV' }, summary: 'CI' } }),
|
|
2454
|
-
addComment: async () => { },
|
|
2455
|
-
};
|
|
2456
|
-
|
|
2457
|
-
const result = await executeTool(
|
|
2458
|
-
'wait_to_dev',
|
|
2459
|
-
{ issueKey: 'CID-1709' },
|
|
2460
|
-
{ jira, notifier: mockNotifier },
|
|
2461
|
-
);
|
|
2462
|
-
|
|
2463
|
-
assert.ok(!result.content[0].text.startsWith('❌'), '應成功');
|
|
2464
|
-
assert.equal(calls.length, 0);
|
|
2465
|
-
});
|
|
2466
|
-
});
|
|
2467
|
-
|
|
2468
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
2469
|
-
// trigger_deployment
|
|
2470
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
2471
|
-
describe('trigger_deployment', () => {
|
|
2472
|
-
/** 建立 sub-task mock helper */
|
|
2473
|
-
function makeSubTask(key, summaryPrefix = '[STG]') {
|
|
2474
|
-
return {
|
|
2475
|
-
id: 'ST-001',
|
|
2476
|
-
key,
|
|
2477
|
-
fields: {
|
|
2478
|
-
summary: `${summaryPrefix} Deployment`,
|
|
2479
|
-
status: { name: 'Open' },
|
|
2480
|
-
issuetype: { name: 'Deployment' },
|
|
2481
|
-
},
|
|
2482
|
-
};
|
|
2483
|
-
}
|
|
2484
|
-
|
|
2485
|
-
/** mock jira for trigger_deployment */
|
|
2486
|
-
function makeTriggerMock({
|
|
2487
|
-
subTasks = [],
|
|
2488
|
-
deployTrans = [],
|
|
2489
|
-
cdTrans = [],
|
|
2490
|
-
finalStatus = 'Auto Deploy',
|
|
2491
|
-
} = {}) {
|
|
2492
|
-
const calls = { transitionById: [], getTransitions: [] };
|
|
2493
|
-
return {
|
|
2494
|
-
calls,
|
|
2495
|
-
getSubTasks: async () => subTasks,
|
|
2496
|
-
getTransitions: async (key) => {
|
|
2497
|
-
calls.getTransitions.push(key);
|
|
2498
|
-
if (subTasks.some((t) => t.key === key)) return deployTrans;
|
|
2499
|
-
return cdTrans;
|
|
2500
|
-
},
|
|
2501
|
-
transitionById: async (key, id) => {
|
|
2502
|
-
calls.transitionById.push({ key, id });
|
|
2503
|
-
return {};
|
|
2504
|
-
},
|
|
2505
|
-
getIssue: async (key) => ({
|
|
2506
|
-
fields: { status: { name: finalStatus }, summary: `[MOCK] ${key}` },
|
|
2507
|
-
}),
|
|
2508
|
-
addComment: async () => { },
|
|
2509
|
-
};
|
|
2510
|
-
}
|
|
2511
|
-
|
|
2512
|
-
test('成功觸發:To AutoDeploy → Trigger AutoDeploy,回傳 deploymentKey', async () => {
|
|
2513
|
-
const jira = makeTriggerMock({
|
|
2514
|
-
subTasks: [makeSubTask('CID-9001', '[STG]')],
|
|
2515
|
-
deployTrans: [
|
|
2516
|
-
{ id: '11', name: 'To AutoDeploy', to: { name: 'Pre Auto Deploy' } },
|
|
2517
|
-
{ id: '12', name: 'Trigger AutoDeploy', to: { name: 'Auto Deploy' } },
|
|
2518
|
-
],
|
|
2519
|
-
});
|
|
2520
|
-
const result = await executeTool(
|
|
2521
|
-
'trigger_deployment',
|
|
2522
|
-
{ cdIssueKey: 'CID-9000', environment: 'stg' },
|
|
2523
|
-
{
|
|
2524
|
-
jira,
|
|
2525
|
-
notifier: mockNotifier,
|
|
2526
|
-
},
|
|
2527
|
-
);
|
|
2528
|
-
assert.ok(!result.content[0].text.startsWith('❌'), '不應回傳錯誤');
|
|
2529
|
-
const data = JSON.parse(result.content[0].text);
|
|
2530
|
-
assert.equal(data.deploymentKey, 'CID-9001');
|
|
2531
|
-
assert.equal(data.environment, 'stg');
|
|
2532
|
-
assert.ok(
|
|
2533
|
-
data.steps.some((s) => s.includes('To AutoDeploy')),
|
|
2534
|
-
'應記錄 To AutoDeploy',
|
|
2535
|
-
);
|
|
2536
|
-
assert.ok(
|
|
2537
|
-
data.steps.some((s) => s.includes('Trigger AutoDeploy')),
|
|
2538
|
-
'應記錄 Trigger AutoDeploy',
|
|
2539
|
-
);
|
|
2540
|
-
});
|
|
2541
|
-
|
|
2542
|
-
test('無 sub-task → 回傳錯誤', async () => {
|
|
2543
|
-
const jira = makeTriggerMock({ subTasks: [] });
|
|
2544
|
-
const result = await executeTool(
|
|
2545
|
-
'trigger_deployment',
|
|
2546
|
-
{ cdIssueKey: 'CID-9000', environment: 'stg' },
|
|
2547
|
-
{
|
|
2548
|
-
jira,
|
|
2549
|
-
notifier: mockNotifier,
|
|
2550
|
-
},
|
|
2551
|
-
);
|
|
2552
|
-
assert.ok(result.content[0].text.startsWith('❌'));
|
|
2553
|
-
assert.ok(result.content[0].text.includes('Deployment sub-task'));
|
|
2554
|
-
});
|
|
2555
|
-
|
|
2556
|
-
test('環境比對:[STG] summary 對應 stg → 選擇 STG sub-task,不選 UAT', async () => {
|
|
2557
|
-
const jira = makeTriggerMock({
|
|
2558
|
-
subTasks: [makeSubTask('CID-9001', '[STG]'), makeSubTask('CID-9002', '[UAT]')],
|
|
2559
|
-
deployTrans: [
|
|
2560
|
-
{ id: '11', name: 'To AutoDeploy', to: { name: 'Pre Auto Deploy' } },
|
|
2561
|
-
{ id: '12', name: 'Trigger AutoDeploy', to: { name: 'Auto Deploy' } },
|
|
2562
|
-
],
|
|
2563
|
-
});
|
|
2564
|
-
const result = await executeTool(
|
|
2565
|
-
'trigger_deployment',
|
|
2566
|
-
{ cdIssueKey: 'CID-9000', environment: 'stg' },
|
|
2567
|
-
{
|
|
2568
|
-
jira,
|
|
2569
|
-
notifier: mockNotifier,
|
|
2570
|
-
},
|
|
2571
|
-
);
|
|
2572
|
-
const data = JSON.parse(result.content[0].text);
|
|
2573
|
-
assert.equal(data.deploymentKey, 'CID-9001', '應選 STG sub-task');
|
|
2574
|
-
});
|
|
2575
|
-
|
|
2576
|
-
test('環境比對:[UAT] summary 對應 uat → 選擇 UAT sub-task', async () => {
|
|
2577
|
-
const jira = makeTriggerMock({
|
|
2578
|
-
subTasks: [makeSubTask('CID-9001', '[STG]'), makeSubTask('CID-9002', '[UAT]')],
|
|
2579
|
-
deployTrans: [
|
|
2580
|
-
{ id: '11', name: 'To AutoDeploy', to: { name: 'Pre Auto Deploy' } },
|
|
2581
|
-
{ id: '12', name: 'Trigger AutoDeploy', to: { name: 'Auto Deploy' } },
|
|
2582
|
-
],
|
|
2583
|
-
});
|
|
2584
|
-
const result = await executeTool(
|
|
2585
|
-
'trigger_deployment',
|
|
2586
|
-
{ cdIssueKey: 'CID-9000', environment: 'uat' },
|
|
2587
|
-
{
|
|
2588
|
-
jira,
|
|
2589
|
-
notifier: mockNotifier,
|
|
2590
|
-
},
|
|
2591
|
-
);
|
|
2592
|
-
const data = JSON.parse(result.content[0].text);
|
|
2593
|
-
assert.equal(data.deploymentKey, 'CID-9002', '應選 UAT sub-task');
|
|
2594
|
-
});
|
|
2595
|
-
|
|
2596
|
-
test('環境比對:[DEV] summary 對應 dev → 選擇 DEV sub-task', async () => {
|
|
2597
|
-
const jira = makeTriggerMock({
|
|
2598
|
-
subTasks: [makeSubTask('CID-9001', '[DEV]'), makeSubTask('CID-9002', '[STG]')],
|
|
2599
|
-
deployTrans: [
|
|
2600
|
-
{ id: '11', name: 'To AutoDeploy', to: { name: 'Pre Auto Deploy' } },
|
|
2601
|
-
{ id: '12', name: 'Trigger AutoDeploy', to: { name: 'Auto Deploy' } },
|
|
2602
|
-
],
|
|
2603
|
-
});
|
|
2604
|
-
const result = await executeTool(
|
|
2605
|
-
'trigger_deployment',
|
|
2606
|
-
{ cdIssueKey: 'CID-9000', environment: 'dev' },
|
|
2607
|
-
{
|
|
2608
|
-
jira,
|
|
2609
|
-
notifier: mockNotifier,
|
|
2610
|
-
},
|
|
2611
|
-
);
|
|
2612
|
-
const data = JSON.parse(result.content[0].text);
|
|
2613
|
-
assert.equal(data.deploymentKey, 'CID-9001', '應選 DEV sub-task');
|
|
2614
|
-
});
|
|
2615
|
-
|
|
2616
|
-
test('無符合環境的 sub-task → fallback 取第一個', async () => {
|
|
2617
|
-
const jira = makeTriggerMock({
|
|
2618
|
-
subTasks: [makeSubTask('CID-9001', '[DEV]')],
|
|
2619
|
-
deployTrans: [
|
|
2620
|
-
{ id: '11', name: 'To AutoDeploy', to: { name: 'Pre Auto Deploy' } },
|
|
2621
|
-
{ id: '12', name: 'Trigger AutoDeploy', to: { name: 'Auto Deploy' } },
|
|
2622
|
-
],
|
|
2623
|
-
});
|
|
2624
|
-
const result = await executeTool(
|
|
2625
|
-
'trigger_deployment',
|
|
2626
|
-
{ cdIssueKey: 'CID-9000', environment: 'stg' },
|
|
2627
|
-
{
|
|
2628
|
-
jira,
|
|
2629
|
-
notifier: mockNotifier,
|
|
2630
|
-
},
|
|
2631
|
-
);
|
|
2632
|
-
const data = JSON.parse(result.content[0].text);
|
|
2633
|
-
assert.equal(data.deploymentKey, 'CID-9001', 'fallback 應取第一個');
|
|
2634
|
-
assert.ok(
|
|
2635
|
-
data.steps.some((s) => s.includes('⚠️') && s.includes('未找到明確符合')),
|
|
2636
|
-
'應有 fallback 警告',
|
|
2637
|
-
);
|
|
2638
|
-
});
|
|
2639
|
-
|
|
2640
|
-
test('找不到必要 deployment transition → 回傳錯誤', async () => {
|
|
2641
|
-
const jira = makeTriggerMock({
|
|
2642
|
-
subTasks: [makeSubTask('CID-9001', '[STG]')],
|
|
2643
|
-
deployTrans: [], // 完全沒有 transitions
|
|
2644
|
-
});
|
|
2645
|
-
const result = await executeTool(
|
|
2646
|
-
'trigger_deployment',
|
|
2647
|
-
{ cdIssueKey: 'CID-9000', environment: 'stg' },
|
|
2648
|
-
{
|
|
2649
|
-
jira,
|
|
2650
|
-
notifier: mockNotifier,
|
|
2651
|
-
},
|
|
2652
|
-
);
|
|
2653
|
-
assert.equal(result.isError, true);
|
|
2654
|
-
assert.match(result.content[0].text, /找不到必要部署 transition/);
|
|
2655
|
-
});
|
|
2656
|
-
|
|
2657
|
-
test('applyForClose=true → 觸發 CD 單的 Apply for close', async () => {
|
|
2658
|
-
const jira = makeTriggerMock({
|
|
2659
|
-
subTasks: [makeSubTask('CID-9001', '[STG]')],
|
|
2660
|
-
deployTrans: [
|
|
2661
|
-
{ id: '11', name: 'To AutoDeploy', to: { name: 'Pre Auto Deploy' } },
|
|
2662
|
-
{ id: '12', name: 'Trigger AutoDeploy', to: { name: 'Auto Deploy' } },
|
|
2663
|
-
],
|
|
2664
|
-
cdTrans: [{ id: '99', name: 'Apply for close', to: { name: 'Wait For Close' } }],
|
|
2665
|
-
});
|
|
2666
|
-
await executeTool(
|
|
2667
|
-
'trigger_deployment',
|
|
2668
|
-
{ cdIssueKey: 'CID-9000', environment: 'stg', applyForClose: true },
|
|
2669
|
-
{
|
|
2670
|
-
jira,
|
|
2671
|
-
notifier: mockNotifier,
|
|
2672
|
-
},
|
|
2673
|
-
);
|
|
2674
|
-
const closeCall = jira.calls.transitionById.find((c) => c.key === 'CID-9000' && c.id === '99');
|
|
2675
|
-
assert.ok(closeCall, 'applyForClose=true 應觸發 CD 單的 Apply for close(id 99)');
|
|
2676
|
-
});
|
|
2677
|
-
|
|
2678
|
-
test('applyForClose 預設 false → 不觸發 CD 單 transition', async () => {
|
|
2679
|
-
const jira = makeTriggerMock({
|
|
2680
|
-
subTasks: [makeSubTask('CID-9001', '[STG]')],
|
|
2681
|
-
deployTrans: [
|
|
2682
|
-
{ id: '11', name: 'To AutoDeploy', to: { name: 'Pre Auto Deploy' } },
|
|
2683
|
-
{ id: '12', name: 'Trigger AutoDeploy', to: { name: 'Auto Deploy' } },
|
|
2684
|
-
],
|
|
2685
|
-
cdTrans: [{ id: '99', name: 'Apply for close', to: { name: 'Wait For Close' } }],
|
|
2686
|
-
});
|
|
2687
|
-
await executeTool(
|
|
2688
|
-
'trigger_deployment',
|
|
2689
|
-
{ cdIssueKey: 'CID-9000', environment: 'stg' },
|
|
2690
|
-
{
|
|
2691
|
-
jira,
|
|
2692
|
-
notifier: mockNotifier,
|
|
2693
|
-
},
|
|
2694
|
-
);
|
|
2695
|
-
const cdCall = jira.calls.transitionById.find((c) => c.key === 'CID-9000');
|
|
2696
|
-
assert.ok(!cdCall, 'applyForClose 未設定 → 不應觸發 CD 單 transition');
|
|
2697
|
-
});
|
|
2698
|
-
|
|
2699
|
-
test('applyForClose=true 但 CD 單找不到 Apply for close → 記錄警告不中斷', async () => {
|
|
2700
|
-
const jira = makeTriggerMock({
|
|
2701
|
-
subTasks: [makeSubTask('CID-9001', '[STG]')],
|
|
2702
|
-
deployTrans: [
|
|
2703
|
-
{ id: '11', name: 'To AutoDeploy', to: { name: 'Pre Auto Deploy' } },
|
|
2704
|
-
{ id: '12', name: 'Trigger AutoDeploy', to: { name: 'Auto Deploy' } },
|
|
2705
|
-
],
|
|
2706
|
-
cdTrans: [], // CD 單沒有 Apply for close
|
|
2707
|
-
});
|
|
2708
|
-
const result = await executeTool(
|
|
2709
|
-
'trigger_deployment',
|
|
2710
|
-
{
|
|
2711
|
-
cdIssueKey: 'CID-9000',
|
|
2712
|
-
environment: 'stg',
|
|
2713
|
-
applyForClose: true,
|
|
2714
|
-
},
|
|
2715
|
-
{ jira, notifier: mockNotifier },
|
|
2716
|
-
);
|
|
2717
|
-
assert.ok(!result.content[0].text.startsWith('❌'), '不應因找不到 Apply for close 而報錯');
|
|
2718
|
-
const data = JSON.parse(result.content[0].text);
|
|
2719
|
-
assert.ok(
|
|
2720
|
-
data.steps.some((s) => s.includes('⚠️') && s.includes('Apply for close')),
|
|
2721
|
-
'應有警告記錄',
|
|
2722
|
-
);
|
|
2723
|
-
});
|
|
2724
|
-
});
|
|
2725
|
-
|
|
2726
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
2727
|
-
// create_cd_ticket — moduleChild fallback
|
|
2728
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
2729
|
-
describe('create_cd_ticket — moduleChild fallback', () => {
|
|
2730
|
-
test('CWA 未傳 moduleChild → 預設 cwa (systemCode.toLowerCase())', async () => {
|
|
2731
|
-
// 驗證 extraVars 產生正確(cwa 系統 → cwa_cwa_installation)
|
|
2732
|
-
const f = await getCreatedFields('create_cd_ticket', {
|
|
2733
|
-
systemCode: 'CWA',
|
|
2734
|
-
environment: 'stg',
|
|
2735
|
-
clusterDeploy: 'tvstg-cwa-web01,tvstg-cwa-web02',
|
|
2736
|
-
// 不傳 moduleChild
|
|
2737
|
-
});
|
|
2738
|
-
const ev = JSON.parse(f['customfield_13437']);
|
|
2739
|
-
assert.ok(
|
|
2740
|
-
'cwa_cwa_installation' in ev,
|
|
2741
|
-
'moduleChild fallback 為 cwa → 應有 cwa_cwa_installation',
|
|
2742
|
-
);
|
|
2743
|
-
});
|
|
2744
|
-
|
|
2745
|
-
test('IBK 未傳 moduleChild → 預設 ibk,extraVars 含全 IBK 模組', async () => {
|
|
2746
|
-
const f = await getCreatedFields('create_cd_ticket', {
|
|
2747
|
-
systemCode: 'IBK',
|
|
2748
|
-
environment: 'stg',
|
|
2749
|
-
clusterDeploy: 'tvstg-ibk-web01',
|
|
2750
|
-
// 不傳 moduleChild
|
|
2751
|
-
});
|
|
2752
|
-
const ev = JSON.parse(f['customfield_13437']);
|
|
2753
|
-
assert.ok('ibk_ibk_installation' in ev, 'IBK fallback → 應有 ibk_ibk_installation');
|
|
2754
|
-
});
|
|
2755
|
-
|
|
2756
|
-
test('明確傳入 moduleChild 優先於 fallback', async () => {
|
|
2757
|
-
// 若使用者明確傳 moduleChild='ssr',不應被 fallback 覆蓋
|
|
2758
|
-
const f = await getCreatedFields(
|
|
2759
|
-
'create_cd_ticket',
|
|
2760
|
-
{
|
|
2761
|
-
systemCode: 'IBK',
|
|
2762
|
-
environment: 'stg',
|
|
2763
|
-
clusterDeploy: 'tvstg-ibk-web01',
|
|
2764
|
-
moduleChild: 'ssr',
|
|
2765
|
-
linkedCiKey: 'CID-1677',
|
|
2766
|
-
},
|
|
2767
|
-
{
|
|
2768
|
-
issueLinks: [],
|
|
2769
|
-
},
|
|
2770
|
-
);
|
|
2771
|
-
// extraVars 應仍走 fallback(無 Library 關聯),但 moduleChild 欄位已為 'ssr'
|
|
2772
|
-
// 驗證票已正常建立(不因 moduleChild='ssr' 而失敗)
|
|
2773
|
-
assert.ok(f.project, '票應正常建立');
|
|
2774
|
-
});
|
|
2775
|
-
});
|