@jira-deploy/core 1.0.0
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/defaults.js +111 -0
- package/constants/environments.js +37 -0
- package/constants/field-ids.js +63 -0
- package/constants/index.js +53 -0
- package/constants/issue-types.js +23 -0
- package/constants/modules.js +55 -0
- package/constants/repos.js +55 -0
- package/constants/server.js +100 -0
- package/constants/system-codes.js +79 -0
- package/constants/users.js +47 -0
- package/dry-run.js +691 -0
- package/index.js +6 -0
- package/jira-client.js +313 -0
- package/notifier.js +56 -0
- package/package.json +34 -0
- package/platform-config.js +64 -0
- package/poller.js +64 -0
- package/tools/cd.js +666 -0
- package/tools/ci.js +204 -0
- package/tools/ci.test.js +154 -0
- package/tools/grayrelease.js +296 -0
- package/tools/helpers.js +78 -0
- package/tools/index.js +1119 -0
- package/tools/jabber.js +97 -0
- package/tools/library.js +225 -0
- package/tools/release.js +320 -0
- package/tools/release.test.js +137 -0
- package/tools.test.js +1711 -0
package/tools.test.js
ADDED
|
@@ -0,0 +1,1711 @@
|
|
|
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 { executeTool } from './tools/index.js';
|
|
8
|
+
|
|
9
|
+
process.env.JIRA_BASE_URL = 'https://jira.test';
|
|
10
|
+
|
|
11
|
+
// ── Mock factories ──────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {{
|
|
15
|
+
* releaseVersion?: string|null,
|
|
16
|
+
* deployVersionJson?: string|null,
|
|
17
|
+
* issueLinks?: object[],
|
|
18
|
+
* libraryBranches?: Record<string, string>,
|
|
19
|
+
* libraryModules?: Record<string, {parentId:string, childId:string}>,
|
|
20
|
+
* projectVersions?: {id:string, name:string}[],
|
|
21
|
+
* libraryComments?: Record<string, string>,
|
|
22
|
+
* bitbucketLastTag?: string,
|
|
23
|
+
* searchIssuesResult?: object[], // 回傳給 findPrevLibraryDeployedToEnv 的前一張 Library 清單
|
|
24
|
+
* ciFieldsMap?: Record<string, object>, // ciKey → { status, issuelinks }(給 findPrevLibraryDeployedToEnv 用)
|
|
25
|
+
* ciSummary?: string|null, // CI 單 summary(用於 ciReleaseVersion 3rd fallback 測試)
|
|
26
|
+
* }} opts
|
|
27
|
+
*/
|
|
28
|
+
function makeMockJira({
|
|
29
|
+
releaseVersion = null,
|
|
30
|
+
deployVersionJson = null,
|
|
31
|
+
issueLinks = [],
|
|
32
|
+
libraryBranches = {},
|
|
33
|
+
libraryModules = {},
|
|
34
|
+
projectVersions = [],
|
|
35
|
+
libraryComments = {},
|
|
36
|
+
bitbucketLastTag = null,
|
|
37
|
+
searchIssuesResult = [],
|
|
38
|
+
ciFieldsMap = {},
|
|
39
|
+
ciSummary = null,
|
|
40
|
+
} = {}) {
|
|
41
|
+
const calls = {
|
|
42
|
+
createIssue: [],
|
|
43
|
+
linkIssue: [],
|
|
44
|
+
addRemoteLink: [],
|
|
45
|
+
getBitbucketTags: [],
|
|
46
|
+
getBitbucketBranches: [],
|
|
47
|
+
updateIssue: [],
|
|
48
|
+
};
|
|
49
|
+
return {
|
|
50
|
+
calls,
|
|
51
|
+
createIssue: async (fields) => {
|
|
52
|
+
calls.createIssue.push(fields);
|
|
53
|
+
return { id: '999', key: 'CID-9999', self: '' };
|
|
54
|
+
},
|
|
55
|
+
updateIssue: async (key, fields) => {
|
|
56
|
+
calls.updateIssue.push({ key, fields });
|
|
57
|
+
return {};
|
|
58
|
+
},
|
|
59
|
+
getIssueFields: async (key, fieldNames) => {
|
|
60
|
+
// ciFieldsMap 優先(供 findPrevLibraryDeployedToEnv 的 CI 查詢使用)
|
|
61
|
+
if (ciFieldsMap[key]) return ciFieldsMap[key];
|
|
62
|
+
const field = fieldNames[0];
|
|
63
|
+
if (field === 'issuelinks') return { issuelinks: issueLinks };
|
|
64
|
+
if (field === 'customfield_13431') {
|
|
65
|
+
const result = { customfield_13431: libraryBranches[key] ?? '' };
|
|
66
|
+
// 只在明確請求 'comment' 時才回傳(模擬真實 Jira API 只回傳請求的欄位)
|
|
67
|
+
if (fieldNames.includes('comment') && libraryComments[key]) {
|
|
68
|
+
result.comment = { comments: [{ body: libraryComments[key] }] };
|
|
69
|
+
}
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
// customfield_13702:Library 模組階層欄位(parentId + child.id,用於 extraVars 動態推導)
|
|
73
|
+
if (field === 'customfield_13702') {
|
|
74
|
+
const mod = libraryModules[key];
|
|
75
|
+
if (mod) return { customfield_13702: { id: mod.parentId, child: { id: mod.childId } } };
|
|
76
|
+
return { customfield_13702: null };
|
|
77
|
+
}
|
|
78
|
+
// customfield_13438 = CI_FIELD_IDS.deployVersion(CI 的 deploy version JSON)
|
|
79
|
+
if (field === 'customfield_13438') return { customfield_13438: deployVersionJson };
|
|
80
|
+
// summary field(用於 ciReleaseVersion 3rd fallback)
|
|
81
|
+
if (field === 'summary') return { summary: ciSummary };
|
|
82
|
+
return { [field]: releaseVersion };
|
|
83
|
+
},
|
|
84
|
+
searchIssues: async () => searchIssuesResult,
|
|
85
|
+
getBitbucketTags: async (project, repo, opts) => {
|
|
86
|
+
calls.getBitbucketTags.push({ project, repo, opts });
|
|
87
|
+
return bitbucketLastTag ? [{ displayId: bitbucketLastTag }] : [];
|
|
88
|
+
},
|
|
89
|
+
getBitbucketBranches: async (project, repo, opts) => {
|
|
90
|
+
calls.getBitbucketBranches.push({ project, repo, opts });
|
|
91
|
+
return bitbucketLastTag ? [{ displayId: bitbucketLastTag }] : [];
|
|
92
|
+
},
|
|
93
|
+
getProjectVersions: async (key, { name } = {}) => {
|
|
94
|
+
const v = projectVersions.find((v) => v.name === name);
|
|
95
|
+
return v?.id ?? null;
|
|
96
|
+
},
|
|
97
|
+
getBitbucketFileContent: async (project, repo, filePath, branch) => {
|
|
98
|
+
// 預設:回傳與 branch 對應的 mock XML(版本 = branch 去前綴後 + -0.0.1)
|
|
99
|
+
const fixVer = branch.replace(/^release[\/\-]/, '');
|
|
100
|
+
return `<root><version>${fixVer}-0.0.1</version></root>`;
|
|
101
|
+
},
|
|
102
|
+
linkIssue: async (inward, outward, type) => {
|
|
103
|
+
calls.linkIssue.push({ inward, outward, type });
|
|
104
|
+
return {};
|
|
105
|
+
},
|
|
106
|
+
addRemoteLink: async (issueKey, url, title) => {
|
|
107
|
+
calls.addRemoteLink.push({ issueKey, url, title });
|
|
108
|
+
return {};
|
|
109
|
+
},
|
|
110
|
+
getIssue: async () => ({ fields: { status: { name: 'TO DO' }, summary: 'test' } }),
|
|
111
|
+
getTransitions: async () => [],
|
|
112
|
+
transitionById: async () => {},
|
|
113
|
+
transitionByName: async () => ({ transitioned: 'Test', toStatus: 'Done' }),
|
|
114
|
+
addComment: async () => {},
|
|
115
|
+
getSubTasks: async () => [],
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const mockNotifier = { notify: async () => [] };
|
|
120
|
+
|
|
121
|
+
/** 執行 tool,回傳 createIssue 收到的 fields(合併 updateIssue 的欄位,或 throw 若 ❌) */
|
|
122
|
+
async function getCreatedFields(toolName, args, jiraOpts = {}) {
|
|
123
|
+
const jira = makeMockJira(jiraOpts);
|
|
124
|
+
const result = await executeTool(toolName, args, { jira, notifier: mockNotifier });
|
|
125
|
+
const text = result.content[0].text;
|
|
126
|
+
if (text.startsWith('❌')) throw new Error(`Tool returned error: ${text}`);
|
|
127
|
+
const created = { ...jira.calls.createIssue[0] };
|
|
128
|
+
// 合併 updateIssue 的欄位(例如 description 是事後透過 updateIssue 寫入)
|
|
129
|
+
for (const update of jira.calls.updateIssue) {
|
|
130
|
+
Object.assign(created, update.fields);
|
|
131
|
+
}
|
|
132
|
+
return created;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** 建立模擬 Library issue link(inward direction) */
|
|
136
|
+
function makeLibraryLink(key) {
|
|
137
|
+
return {
|
|
138
|
+
type: { name: 'Relates', inward: 'is related to', outward: 'relates to' },
|
|
139
|
+
inwardIssue: {
|
|
140
|
+
key,
|
|
141
|
+
fields: { issuetype: { name: 'Library' }, summary: `[IBK][Lib] mock ${key}` },
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
147
|
+
// create_library_ticket
|
|
148
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
149
|
+
describe('create_library_ticket — fields', () => {
|
|
150
|
+
const BASE = { systemCode: 'IBK', moduleChild: 'ibk', repo: 'release/v1.5.2.0' };
|
|
151
|
+
|
|
152
|
+
test('project.key = CID', async () => {
|
|
153
|
+
const f = await getCreatedFields('create_library_ticket', BASE);
|
|
154
|
+
assert.deepEqual(f.project, { key: 'CID' });
|
|
155
|
+
});
|
|
156
|
+
test('issuetype.id = 12501 (Library)', async () => {
|
|
157
|
+
const f = await getCreatedFields('create_library_ticket', BASE);
|
|
158
|
+
assert.deepEqual(f.issuetype, { id: '12501' });
|
|
159
|
+
});
|
|
160
|
+
test('duedate = today (YYYY-MM-DD)', async () => {
|
|
161
|
+
const f = await getCreatedFields('create_library_ticket', BASE);
|
|
162
|
+
assert.match(f.duedate, /^\d{4}-\d{2}-\d{2}$/);
|
|
163
|
+
});
|
|
164
|
+
test('summary 自動生成:[IBK][Lib] Release for v1.5.2.0-0.0.2(XML 現版 0.0.1 +1)', async () => {
|
|
165
|
+
const f = await getCreatedFields('create_library_ticket', BASE);
|
|
166
|
+
assert.equal(f.summary, '[IBK][Lib] Release for v1.5.2.0-0.0.2');
|
|
167
|
+
});
|
|
168
|
+
test('summary 自訂覆蓋', async () => {
|
|
169
|
+
const f = await getCreatedFields('create_library_ticket', {
|
|
170
|
+
...BASE,
|
|
171
|
+
summary: '[Custom] My Lib',
|
|
172
|
+
});
|
|
173
|
+
assert.equal(f.summary, '[Custom] My Lib');
|
|
174
|
+
});
|
|
175
|
+
test('customfield_13702 (libModuleParent) IBK parent=15600, ibk child=15601', async () => {
|
|
176
|
+
const f = await getCreatedFields('create_library_ticket', BASE);
|
|
177
|
+
assert.deepEqual(f['customfield_13702'], { id: '15600', child: { id: '15601' } });
|
|
178
|
+
});
|
|
179
|
+
test('customfield_13702 CWA parent=15513, cwa child=15514', async () => {
|
|
180
|
+
const f = await getCreatedFields('create_library_ticket', {
|
|
181
|
+
systemCode: 'CWA',
|
|
182
|
+
moduleChild: 'cwa',
|
|
183
|
+
repo: 'release/v1.0.0',
|
|
184
|
+
});
|
|
185
|
+
assert.deepEqual(f['customfield_13702'], { id: '15513', child: { id: '15514' } });
|
|
186
|
+
});
|
|
187
|
+
test('customfield_13436 (env) stg → id 14356', async () => {
|
|
188
|
+
const f = await getCreatedFields('create_library_ticket', { ...BASE, environment: 'stg' });
|
|
189
|
+
assert.deepEqual(f['customfield_13436'], { id: '14356' });
|
|
190
|
+
});
|
|
191
|
+
test('customfield_13436 (env) uat → id 14357', async () => {
|
|
192
|
+
const f = await getCreatedFields('create_library_ticket', { ...BASE, environment: 'uat' });
|
|
193
|
+
assert.deepEqual(f['customfield_13436'], { id: '14357' });
|
|
194
|
+
});
|
|
195
|
+
test('customfield_13434 (deptCode) IBK → CH014 → id 14520', async () => {
|
|
196
|
+
const f = await getCreatedFields('create_library_ticket', BASE);
|
|
197
|
+
assert.deepEqual(f['customfield_13434'], { id: '14520' });
|
|
198
|
+
});
|
|
199
|
+
test('customfield_13434 (deptCode) EVT → CH015 → id 14521', async () => {
|
|
200
|
+
const f = await getCreatedFields('create_library_ticket', {
|
|
201
|
+
systemCode: 'EVT',
|
|
202
|
+
moduleChild: 'evt007',
|
|
203
|
+
repo: 'release/v1.0.0',
|
|
204
|
+
});
|
|
205
|
+
assert.deepEqual(f['customfield_13434'], { id: '14521' });
|
|
206
|
+
});
|
|
207
|
+
test('customfield_13807 (fortifyScan) 預設 = scanned (14602)', async () => {
|
|
208
|
+
const f = await getCreatedFields('create_library_ticket', BASE);
|
|
209
|
+
assert.deepEqual(f['customfield_13807'], { id: '14602' });
|
|
210
|
+
});
|
|
211
|
+
test('customfield_13807 (fortifyScan) fortifyScan=true → scanned (14602)', async () => {
|
|
212
|
+
const f = await getCreatedFields('create_library_ticket', { ...BASE, fortifyScan: true });
|
|
213
|
+
assert.deepEqual(f['customfield_13807'], { id: '14602' });
|
|
214
|
+
});
|
|
215
|
+
test('customfield_13807 (fortifyScan) fortifyScan=false → notScanned (14603)', async () => {
|
|
216
|
+
const f = await getCreatedFields('create_library_ticket', { ...BASE, fortifyScan: false });
|
|
217
|
+
assert.deepEqual(f['customfield_13807'], { id: '14603' });
|
|
218
|
+
});
|
|
219
|
+
test('customfield_14702 (jenkinsBranch) = master', async () => {
|
|
220
|
+
const f = await getCreatedFields('create_library_ticket', BASE);
|
|
221
|
+
assert.equal(f['customfield_14702'], 'master');
|
|
222
|
+
});
|
|
223
|
+
test('customfield_13431 (gitBranch) = repo arg', async () => {
|
|
224
|
+
const f = await getCreatedFields('create_library_ticket', BASE);
|
|
225
|
+
assert.equal(f['customfield_13431'], 'release/v1.5.2.0');
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
230
|
+
// create_ci_ticket
|
|
231
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
232
|
+
describe('create_ci_ticket — fields', () => {
|
|
233
|
+
const BASE = { systemCode: 'IBK' };
|
|
234
|
+
|
|
235
|
+
test('project.key = CID', async () => {
|
|
236
|
+
const f = await getCreatedFields('create_ci_ticket', BASE);
|
|
237
|
+
assert.deepEqual(f.project, { key: 'CID' });
|
|
238
|
+
});
|
|
239
|
+
test('issuetype.id = 12305 (CI)', async () => {
|
|
240
|
+
const f = await getCreatedFields('create_ci_ticket', BASE);
|
|
241
|
+
assert.deepEqual(f.issuetype, { id: '12305' });
|
|
242
|
+
});
|
|
243
|
+
test('duedate = today (YYYY-MM-DD)', async () => {
|
|
244
|
+
const f = await getCreatedFields('create_ci_ticket', BASE);
|
|
245
|
+
assert.match(f.duedate, /^\d{4}-\d{2}-\d{2}$/);
|
|
246
|
+
});
|
|
247
|
+
test('summary with goldenImageVersion:[IBK][CI] IBK & WEALTH & SSR & A11Y for 0.0.1', async () => {
|
|
248
|
+
const f = await getCreatedFields('create_ci_ticket', { ...BASE, goldenImageVersion: '0.0.1' });
|
|
249
|
+
assert.equal(f.summary, '[IBK][CI] IBK & WEALTH & SSR & A11Y for 0.0.1');
|
|
250
|
+
});
|
|
251
|
+
test('summary without goldenImageVersion:[IBK][CI] IBK & WEALTH & SSR & A11Y', async () => {
|
|
252
|
+
const f = await getCreatedFields('create_ci_ticket', BASE);
|
|
253
|
+
assert.equal(f.summary, '[IBK][CI] IBK & WEALTH & SSR & A11Y');
|
|
254
|
+
});
|
|
255
|
+
test('summary CWA single module:[CWA][CI] CWA for 1.0.0', async () => {
|
|
256
|
+
const f = await getCreatedFields('create_ci_ticket', {
|
|
257
|
+
systemCode: 'CWA',
|
|
258
|
+
goldenImageVersion: '1.0.0',
|
|
259
|
+
});
|
|
260
|
+
assert.equal(f.summary, '[CWA][CI] CWA for 1.0.0');
|
|
261
|
+
});
|
|
262
|
+
test('summary 自訂覆蓋', async () => {
|
|
263
|
+
const f = await getCreatedFields('create_ci_ticket', { ...BASE, summary: '[Custom] CI' });
|
|
264
|
+
assert.equal(f.summary, '[Custom] CI');
|
|
265
|
+
});
|
|
266
|
+
test('customfield_13443 (systemCode) = { value: IBK }', async () => {
|
|
267
|
+
const f = await getCreatedFields('create_ci_ticket', BASE);
|
|
268
|
+
assert.deepEqual(f['customfield_13443'], { value: 'IBK' });
|
|
269
|
+
});
|
|
270
|
+
test('customfield_13436 (env) 預設 stg → id 14356', async () => {
|
|
271
|
+
const f = await getCreatedFields('create_ci_ticket', BASE);
|
|
272
|
+
assert.deepEqual(f['customfield_13436'], { id: '14356' });
|
|
273
|
+
});
|
|
274
|
+
test('customfield_13436 (env) uat → id 14357', async () => {
|
|
275
|
+
const f = await getCreatedFields('create_ci_ticket', { ...BASE, environment: 'uat' });
|
|
276
|
+
assert.deepEqual(f['customfield_13436'], { id: '14357' });
|
|
277
|
+
});
|
|
278
|
+
test('customfield_13434 (deptCode) IBK → CH014 → id 14520', async () => {
|
|
279
|
+
const f = await getCreatedFields('create_ci_ticket', BASE);
|
|
280
|
+
assert.deepEqual(f['customfield_13434'], { id: '14520' });
|
|
281
|
+
});
|
|
282
|
+
test('customfield_13434 (deptCode) BOF → CH015 → id 14521', async () => {
|
|
283
|
+
const f = await getCreatedFields('create_ci_ticket', { systemCode: 'BOF' });
|
|
284
|
+
assert.deepEqual(f['customfield_13434'], { id: '14521' });
|
|
285
|
+
});
|
|
286
|
+
test('customfield_13444 (systemModule) = assembly (id 14369)', async () => {
|
|
287
|
+
const f = await getCreatedFields('create_ci_ticket', BASE);
|
|
288
|
+
assert.deepEqual(f['customfield_13444'], { id: '14369' });
|
|
289
|
+
});
|
|
290
|
+
test('customfield_13431 (gitBranch) = master', async () => {
|
|
291
|
+
const f = await getCreatedFields('create_ci_ticket', BASE);
|
|
292
|
+
assert.equal(f['customfield_13431'], 'master');
|
|
293
|
+
});
|
|
294
|
+
test('relatesTo: 呼叫 linkIssue(ci, lib, Relates)', async () => {
|
|
295
|
+
const jira = makeMockJira();
|
|
296
|
+
await executeTool(
|
|
297
|
+
'create_ci_ticket',
|
|
298
|
+
{ ...BASE, relatesTo: 'CID-1675' },
|
|
299
|
+
{ jira, notifier: mockNotifier },
|
|
300
|
+
);
|
|
301
|
+
assert.equal(jira.calls.linkIssue.length, 1);
|
|
302
|
+
assert.equal(jira.calls.linkIssue[0].inward, 'CID-9999');
|
|
303
|
+
assert.equal(jira.calls.linkIssue[0].outward, 'CID-1675');
|
|
304
|
+
assert.equal(jira.calls.linkIssue[0].type, 'Relates');
|
|
305
|
+
});
|
|
306
|
+
test('沒有 relatesTo: 不呼叫 linkIssue', async () => {
|
|
307
|
+
const jira = makeMockJira();
|
|
308
|
+
await executeTool('create_ci_ticket', BASE, { jira, notifier: mockNotifier });
|
|
309
|
+
assert.equal(jira.calls.linkIssue.length, 0);
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
314
|
+
// create_cd_ticket — fields
|
|
315
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
316
|
+
describe('create_cd_ticket — fields', () => {
|
|
317
|
+
const BASE = {
|
|
318
|
+
systemCode: 'IBK',
|
|
319
|
+
environment: 'stg',
|
|
320
|
+
clusterDeploy: 'tvstg-ibk-web01,tvstg-ibk-web02',
|
|
321
|
+
linkedCiKey: 'CID-1677',
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
test('project.key = CID', async () => {
|
|
325
|
+
const f = await getCreatedFields('create_cd_ticket', BASE);
|
|
326
|
+
assert.deepEqual(f.project, { key: 'CID' });
|
|
327
|
+
});
|
|
328
|
+
test('issuetype.id = 12306 (CD)', async () => {
|
|
329
|
+
const f = await getCreatedFields('create_cd_ticket', BASE);
|
|
330
|
+
assert.deepEqual(f.issuetype, { id: '12306' });
|
|
331
|
+
});
|
|
332
|
+
test('summary 使用 CI release_version:包含版號且前綴 [IBK][STG]', async () => {
|
|
333
|
+
const f = await getCreatedFields('create_cd_ticket', BASE, { releaseVersion: '1.5.2.0' });
|
|
334
|
+
assert.ok(f.summary.startsWith('[IBK][STG]'), `prefix 錯誤: "${f.summary}"`);
|
|
335
|
+
assert.ok(f.summary.includes('1.5.2.0'), `版號缺失: "${f.summary}"`);
|
|
336
|
+
});
|
|
337
|
+
test('summary fallback to linkedCiKey when release_version 為空', async () => {
|
|
338
|
+
const f = await getCreatedFields('create_cd_ticket', BASE, { releaseVersion: null });
|
|
339
|
+
assert.ok(f.summary.includes('CID-1677'), `fallback 失敗: "${f.summary}"`);
|
|
340
|
+
});
|
|
341
|
+
test('summary 使用 CI deploy_version JSON:包含版號且前綴 [IBK][STG]', async () => {
|
|
342
|
+
const f = await getCreatedFields('create_cd_ticket', BASE, {
|
|
343
|
+
deployVersionJson: '{"ibk_ap_version":"1.5.2.0"}',
|
|
344
|
+
});
|
|
345
|
+
assert.ok(f.summary.startsWith('[IBK][STG]'), `prefix 錯誤: "${f.summary}"`);
|
|
346
|
+
assert.ok(f.summary.includes('1.5.2.0'), `版號缺失: "${f.summary}"`);
|
|
347
|
+
});
|
|
348
|
+
test('summary fallback to linkedCiKey when deploy_version 為空', async () => {
|
|
349
|
+
const f = await getCreatedFields('create_cd_ticket', BASE, { deployVersionJson: null });
|
|
350
|
+
assert.ok(f.summary.includes('CID-1677'), `fallback 失敗: "${f.summary}"`);
|
|
351
|
+
});
|
|
352
|
+
test('summary prd env → displayEnv = PRD/DR', async () => {
|
|
353
|
+
const f = await getCreatedFields('create_cd_ticket', {
|
|
354
|
+
systemCode: 'IBK',
|
|
355
|
+
environment: 'prd',
|
|
356
|
+
clusterDeploy: 'tvprd-ibk-web01',
|
|
357
|
+
});
|
|
358
|
+
assert.ok(f.summary.includes('[IBK][PRD/DR]'), `displayEnv 錯誤: "${f.summary}"`);
|
|
359
|
+
});
|
|
360
|
+
test('summary prd/dr env → displayEnv = PRD/DR', async () => {
|
|
361
|
+
const f = await getCreatedFields('create_cd_ticket', {
|
|
362
|
+
systemCode: 'IBK',
|
|
363
|
+
environment: 'prd/dr',
|
|
364
|
+
clusterDeploy:
|
|
365
|
+
'tvprd-ibk-web01,tvprd-ibk-web02,tvprd-ibk-web03,tvprd-ibk-web04,tvprd-ibk-web05,lvprd-ibk-web01,lvprd-ibk-web02',
|
|
366
|
+
});
|
|
367
|
+
assert.ok(f.summary.includes('[IBK][PRD/DR]'), `prd/dr displayEnv 錯誤: "${f.summary}"`);
|
|
368
|
+
});
|
|
369
|
+
test('summary 自訂覆蓋', async () => {
|
|
370
|
+
const f = await getCreatedFields('create_cd_ticket', { ...BASE, summary: '[Custom] CD' });
|
|
371
|
+
assert.equal(f.summary, '[Custom] CD');
|
|
372
|
+
});
|
|
373
|
+
test('customfield_13436 (CID_env) stg → id 14356', async () => {
|
|
374
|
+
const f = await getCreatedFields('create_cd_ticket', BASE);
|
|
375
|
+
assert.deepEqual(f['customfield_13436'], { id: '14356' });
|
|
376
|
+
});
|
|
377
|
+
test('customfield_13436 (CID_env) uat → id 14357', async () => {
|
|
378
|
+
const f = await getCreatedFields('create_cd_ticket', {
|
|
379
|
+
systemCode: 'IBK',
|
|
380
|
+
environment: 'uat',
|
|
381
|
+
clusterDeploy: 'tvuat-ibk-web01',
|
|
382
|
+
});
|
|
383
|
+
assert.deepEqual(f['customfield_13436'], { id: '14357' });
|
|
384
|
+
});
|
|
385
|
+
test('customfield_13434 (CID_dept_code) IBK → CH014 → id 14520', async () => {
|
|
386
|
+
const f = await getCreatedFields('create_cd_ticket', BASE);
|
|
387
|
+
assert.deepEqual(f['customfield_13434'], { id: '14520' });
|
|
388
|
+
});
|
|
389
|
+
test('customfield_13443 (CID_system_code) = { value: IBK }', async () => {
|
|
390
|
+
const f = await getCreatedFields('create_cd_ticket', BASE);
|
|
391
|
+
assert.deepEqual(f['customfield_13443'], { value: 'IBK' });
|
|
392
|
+
});
|
|
393
|
+
test('customfield_14704 (CID_cluster_deploy) STG → true (id 15506)', async () => {
|
|
394
|
+
const f = await getCreatedFields('create_cd_ticket', BASE);
|
|
395
|
+
assert.deepEqual(f['customfield_14704'], { id: '15506' });
|
|
396
|
+
});
|
|
397
|
+
test('customfield_14704 (CID_cluster_deploy) DEV → false (id 15505)', async () => {
|
|
398
|
+
const f = await getCreatedFields('create_cd_ticket', {
|
|
399
|
+
systemCode: 'IBK',
|
|
400
|
+
environment: 'dev',
|
|
401
|
+
clusterDeploy: 'tvdev-ibk-web01',
|
|
402
|
+
});
|
|
403
|
+
assert.deepEqual(f['customfield_14704'], { id: '15505' });
|
|
404
|
+
});
|
|
405
|
+
test('customfield_14704 (CID_cluster_deploy) UAT → true (id 15506)', async () => {
|
|
406
|
+
const f = await getCreatedFields('create_cd_ticket', {
|
|
407
|
+
systemCode: 'IBK',
|
|
408
|
+
environment: 'uat',
|
|
409
|
+
clusterDeploy: 'tvuat-ibk-web01',
|
|
410
|
+
});
|
|
411
|
+
assert.deepEqual(f['customfield_14704'], { id: '15506' });
|
|
412
|
+
});
|
|
413
|
+
test('customfield_14101 (CID_deploy_version) = CI deploy_version JSON(完整字串)', async () => {
|
|
414
|
+
const deployJson = '{"ibk_ap_version":"1.5.2.0"}';
|
|
415
|
+
const f = await getCreatedFields('create_cd_ticket', BASE, { deployVersionJson: deployJson });
|
|
416
|
+
assert.equal(f['customfield_14101'], deployJson);
|
|
417
|
+
});
|
|
418
|
+
test('customfield_14101 不設定 when deploy_version 為空', async () => {
|
|
419
|
+
const f = await getCreatedFields('create_cd_ticket', BASE, { deployVersionJson: null });
|
|
420
|
+
assert.equal(f['customfield_14101'], undefined);
|
|
421
|
+
});
|
|
422
|
+
test('customfield_14101 (CID_deploy_version) = CI release_version', async () => {
|
|
423
|
+
const f = await getCreatedFields('create_cd_ticket', BASE, { releaseVersion: '1.5.2.0' });
|
|
424
|
+
assert.equal(f['customfield_14101'], '1.5.2.0');
|
|
425
|
+
});
|
|
426
|
+
test('customfield_14101 不設定 when release_version 為空', async () => {
|
|
427
|
+
const f = await getCreatedFields('create_cd_ticket', BASE, { releaseVersion: null });
|
|
428
|
+
assert.equal(f['customfield_14101'], undefined);
|
|
429
|
+
});
|
|
430
|
+
test('customfield_13436 (CID_env) prd/dr → id 14360', async () => {
|
|
431
|
+
const f = await getCreatedFields('create_cd_ticket', {
|
|
432
|
+
systemCode: 'IBK',
|
|
433
|
+
environment: 'prd/dr',
|
|
434
|
+
clusterDeploy:
|
|
435
|
+
'tvprd-ibk-web01,tvprd-ibk-web02,tvprd-ibk-web03,tvprd-ibk-web04,tvprd-ibk-web05,lvprd-ibk-web01,lvprd-ibk-web02',
|
|
436
|
+
});
|
|
437
|
+
assert.deepEqual(f['customfield_13436'], { id: '14360' });
|
|
438
|
+
});
|
|
439
|
+
test('customfield_13442 (CID_server_list) = 換行分隔 cluster 清單', async () => {
|
|
440
|
+
const f = await getCreatedFields('create_cd_ticket', BASE);
|
|
441
|
+
assert.equal(f['customfield_13442'], 'tvstg-ibk-web01\ntvstg-ibk-web02');
|
|
442
|
+
});
|
|
443
|
+
test('customfield_13442 單台 cluster = 無換行', async () => {
|
|
444
|
+
const f = await getCreatedFields('create_cd_ticket', {
|
|
445
|
+
systemCode: 'IBK',
|
|
446
|
+
environment: 'dev',
|
|
447
|
+
clusterDeploy: 'tvdev-ibk-web01',
|
|
448
|
+
});
|
|
449
|
+
assert.equal(f['customfield_13442'], 'tvdev-ibk-web01');
|
|
450
|
+
});
|
|
451
|
+
test('customfield_13437 (CID_extra_vars) IBK STG 自動生成', async () => {
|
|
452
|
+
const f = await getCreatedFields('create_cd_ticket', BASE);
|
|
453
|
+
const ev = JSON.parse(f['customfield_13437']);
|
|
454
|
+
const serverMap = { 'tvstg-ibk-web01': true, 'tvstg-ibk-web02': true };
|
|
455
|
+
assert.deepEqual(ev, {
|
|
456
|
+
ibk_ibk_installation: serverMap,
|
|
457
|
+
ibk_wealth_installation: serverMap,
|
|
458
|
+
ibk_ssr_installation: serverMap,
|
|
459
|
+
ibk_a11y_installation: serverMap,
|
|
460
|
+
});
|
|
461
|
+
});
|
|
462
|
+
test('customfield_13437 CWA STG 自動生成', async () => {
|
|
463
|
+
const f = await getCreatedFields('create_cd_ticket', {
|
|
464
|
+
systemCode: 'CWA',
|
|
465
|
+
environment: 'stg',
|
|
466
|
+
clusterDeploy: 'tvstg-cwa-web01,tvstg-cwa-web02',
|
|
467
|
+
});
|
|
468
|
+
const ev = JSON.parse(f['customfield_13437']);
|
|
469
|
+
assert.deepEqual(ev, {
|
|
470
|
+
cwa_cwa_installation: { 'tvstg-cwa-web01': true, 'tvstg-cwa-web02': true },
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
test('customfield_13437 extraVars 自訂覆蓋', async () => {
|
|
474
|
+
const custom = '{"ibk_ibk_installation":{"tvstg-ibk-web05":true}}';
|
|
475
|
+
const f = await getCreatedFields('create_cd_ticket', { ...BASE, extraVars: custom });
|
|
476
|
+
assert.equal(f['customfield_13437'], custom);
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
// ── extraVars 動態模組推導(從 CI 關聯 Library customfield_13702)────
|
|
480
|
+
test('extraVars 動態推導:IBK CI 關聯 ibk+ssr → 只含 ibk & ssr,不含 wealth/a11y', async () => {
|
|
481
|
+
// IBK_MODULE_IDS: ibk=15601, a11y=15602, landing=15603, ssr=15604, wealth=15605
|
|
482
|
+
const f = await getCreatedFields('create_cd_ticket', BASE, {
|
|
483
|
+
issueLinks: [makeLibraryLink('CID-1675'), makeLibraryLink('CID-1676')],
|
|
484
|
+
libraryModules: {
|
|
485
|
+
'CID-1675': { parentId: '15600', childId: '15601' }, // ibk
|
|
486
|
+
'CID-1676': { parentId: '15600', childId: '15604' }, // ssr
|
|
487
|
+
},
|
|
488
|
+
});
|
|
489
|
+
const ev = JSON.parse(f['customfield_13437']);
|
|
490
|
+
const serverMap = { 'tvstg-ibk-web01': true, 'tvstg-ibk-web02': true };
|
|
491
|
+
assert.deepEqual(
|
|
492
|
+
ev,
|
|
493
|
+
{
|
|
494
|
+
ibk_ibk_installation: serverMap,
|
|
495
|
+
ibk_ssr_installation: serverMap,
|
|
496
|
+
},
|
|
497
|
+
`extraVars 應只含 ibk+ssr: ${JSON.stringify(ev)}`,
|
|
498
|
+
);
|
|
499
|
+
assert.ok(!('ibk_wealth_installation' in ev), 'extraVars 不應含 wealth');
|
|
500
|
+
assert.ok(!('ibk_a11y_installation' in ev), 'extraVars 不應含 a11y');
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
test('extraVars 動態推導:CWA CI 關聯單一 Library → 只含 cwa', async () => {
|
|
504
|
+
// CWA_MODULE_IDS: cwa=15514, parent=15513
|
|
505
|
+
const f = await getCreatedFields(
|
|
506
|
+
'create_cd_ticket',
|
|
507
|
+
{
|
|
508
|
+
systemCode: 'CWA',
|
|
509
|
+
environment: 'stg',
|
|
510
|
+
clusterDeploy: 'tvstg-cwa-web01,tvstg-cwa-web02',
|
|
511
|
+
linkedCiKey: 'CID-1668',
|
|
512
|
+
},
|
|
513
|
+
{
|
|
514
|
+
issueLinks: [makeLibraryLink('CID-2000')],
|
|
515
|
+
libraryModules: { 'CID-2000': { parentId: '15513', childId: '15514' } }, // cwa
|
|
516
|
+
},
|
|
517
|
+
);
|
|
518
|
+
const ev = JSON.parse(f['customfield_13437']);
|
|
519
|
+
assert.deepEqual(
|
|
520
|
+
ev,
|
|
521
|
+
{
|
|
522
|
+
cwa_cwa_installation: { 'tvstg-cwa-web01': true, 'tvstg-cwa-web02': true },
|
|
523
|
+
},
|
|
524
|
+
`extraVars 應只含 cwa: ${JSON.stringify(ev)}`,
|
|
525
|
+
);
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
test('extraVars 動態推導:Library customfield_13702 為空 → fallback 到 SYSTEM_MODULES 全模組', async () => {
|
|
529
|
+
const f = await getCreatedFields('create_cd_ticket', BASE, {
|
|
530
|
+
issueLinks: [makeLibraryLink('CID-1675')],
|
|
531
|
+
// libraryModules 未設定 → customfield_13702 = null → detectedModules 空 → fallback
|
|
532
|
+
});
|
|
533
|
+
const ev = JSON.parse(f['customfield_13437']);
|
|
534
|
+
const serverMap = { 'tvstg-ibk-web01': true, 'tvstg-ibk-web02': true };
|
|
535
|
+
assert.deepEqual(
|
|
536
|
+
ev,
|
|
537
|
+
{
|
|
538
|
+
ibk_ibk_installation: serverMap,
|
|
539
|
+
ibk_wealth_installation: serverMap,
|
|
540
|
+
ibk_ssr_installation: serverMap,
|
|
541
|
+
ibk_a11y_installation: serverMap,
|
|
542
|
+
},
|
|
543
|
+
'fallback 應回退到全部 SYSTEM_MODULES',
|
|
544
|
+
);
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
test('extraVars 動態推導:無 Library 關聯(issueLinks 空)→ fallback 到 SYSTEM_MODULES', async () => {
|
|
548
|
+
const f = await getCreatedFields('create_cd_ticket', BASE, { issueLinks: [] });
|
|
549
|
+
const ev = JSON.parse(f['customfield_13437']);
|
|
550
|
+
assert.ok('ibk_ibk_installation' in ev, 'fallback 應含 ibk');
|
|
551
|
+
assert.ok('ibk_wealth_installation' in ev, 'fallback 應含 wealth');
|
|
552
|
+
assert.ok('ibk_ssr_installation' in ev, 'fallback 應含 ssr');
|
|
553
|
+
assert.ok('ibk_a11y_installation' in ev, 'fallback 應含 a11y');
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
test('extraVars 動態推導:重複模組 ID 去重', async () => {
|
|
557
|
+
// 兩個 Library 都對應同一個 module(ibk)
|
|
558
|
+
const f = await getCreatedFields('create_cd_ticket', BASE, {
|
|
559
|
+
issueLinks: [makeLibraryLink('CID-1675'), makeLibraryLink('CID-1676')],
|
|
560
|
+
libraryModules: {
|
|
561
|
+
'CID-1675': { parentId: '15600', childId: '15601' }, // ibk
|
|
562
|
+
'CID-1676': { parentId: '15600', childId: '15601' }, // ibk(重複)
|
|
563
|
+
},
|
|
564
|
+
});
|
|
565
|
+
const ev = JSON.parse(f['customfield_13437']);
|
|
566
|
+
const keys = Object.keys(ev);
|
|
567
|
+
assert.equal(
|
|
568
|
+
keys.filter((k) => k === 'ibk_ibk_installation').length,
|
|
569
|
+
1,
|
|
570
|
+
'重複模組應去重為 1 筆',
|
|
571
|
+
);
|
|
572
|
+
});
|
|
573
|
+
test('customfield_14102 (CID_restart_only) true → id 14902', async () => {
|
|
574
|
+
const f = await getCreatedFields('create_cd_ticket', { ...BASE, restartOnly: true });
|
|
575
|
+
assert.deepEqual(f['customfield_14102'], { id: '14902' });
|
|
576
|
+
});
|
|
577
|
+
test('customfield_14102 (CID_restart_only) false → id 14901', async () => {
|
|
578
|
+
const f = await getCreatedFields('create_cd_ticket', { ...BASE, restartOnly: false });
|
|
579
|
+
assert.deepEqual(f['customfield_14102'], { id: '14901' });
|
|
580
|
+
});
|
|
581
|
+
test('customfield_14102 未傳 restartOnly → 不設定', async () => {
|
|
582
|
+
const f = await getCreatedFields('create_cd_ticket', BASE);
|
|
583
|
+
assert.equal(f['customfield_14102'], undefined);
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
// ── CI ←→ CD linkIssue ───────────────────────────────────────
|
|
587
|
+
test('linkedCiKey: 呼叫 linkIssue(ci, cd, Hierarchy link (WBSGantt))', async () => {
|
|
588
|
+
const jira = makeMockJira();
|
|
589
|
+
await executeTool('create_cd_ticket', BASE, { jira, notifier: mockNotifier });
|
|
590
|
+
assert.equal(jira.calls.linkIssue.length, 1);
|
|
591
|
+
assert.equal(jira.calls.linkIssue[0].inward, 'CID-1677'); // CI key
|
|
592
|
+
assert.equal(jira.calls.linkIssue[0].outward, 'CID-9999'); // CD key
|
|
593
|
+
assert.equal(jira.calls.linkIssue[0].type, 'Hierarchy link (WBSGantt)');
|
|
594
|
+
});
|
|
595
|
+
test('沒有 linkedCiKey: 不呼叫 linkIssue', async () => {
|
|
596
|
+
const jira = makeMockJira();
|
|
597
|
+
await executeTool(
|
|
598
|
+
'create_cd_ticket',
|
|
599
|
+
{
|
|
600
|
+
systemCode: 'IBK',
|
|
601
|
+
environment: 'stg',
|
|
602
|
+
clusterDeploy: 'tvstg-ibk-web01',
|
|
603
|
+
},
|
|
604
|
+
{ jira, notifier: mockNotifier },
|
|
605
|
+
);
|
|
606
|
+
assert.equal(jira.calls.linkIssue.length, 0);
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
// ── Remote Link:CI → Library 單 → LBPRJ version → Web Link ──
|
|
610
|
+
test('2 個 Library 單 → 2 筆 remote link,URL 含正確 version id', async () => {
|
|
611
|
+
const jira = makeMockJira({
|
|
612
|
+
issueLinks: [makeLibraryLink('CID-1675'), makeLibraryLink('CID-1676')],
|
|
613
|
+
libraryBranches: {
|
|
614
|
+
'CID-1675': 'release/IBK_ibk_1.5.2.0',
|
|
615
|
+
'CID-1676': 'release/IBK_ssr_1.3.1.0',
|
|
616
|
+
},
|
|
617
|
+
projectVersions: [
|
|
618
|
+
{ id: '16157', name: 'IBK_ibk_1.5.2.0' },
|
|
619
|
+
{ id: '16158', name: 'IBK_ssr_1.3.1.0' },
|
|
620
|
+
],
|
|
621
|
+
});
|
|
622
|
+
await executeTool('create_cd_ticket', BASE, { jira, notifier: mockNotifier });
|
|
623
|
+
|
|
624
|
+
assert.equal(jira.calls.addRemoteLink.length, 2);
|
|
625
|
+
assert.equal(jira.calls.addRemoteLink[0].issueKey, 'CID-9999');
|
|
626
|
+
assert.equal(
|
|
627
|
+
jira.calls.addRemoteLink[0].url,
|
|
628
|
+
'https://jira.test/projects/LBPRJ/versions/16157',
|
|
629
|
+
);
|
|
630
|
+
assert.equal(jira.calls.addRemoteLink[0].title, 'IBK_ibk_1.5.2.0');
|
|
631
|
+
assert.equal(
|
|
632
|
+
jira.calls.addRemoteLink[1].url,
|
|
633
|
+
'https://jira.test/projects/LBPRJ/versions/16158',
|
|
634
|
+
);
|
|
635
|
+
assert.equal(jira.calls.addRemoteLink[1].title, 'IBK_ssr_1.3.1.0');
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
test('CID_branch 有 release/ 前綴 → 正確去除後比對版本', async () => {
|
|
639
|
+
const jira = makeMockJira({
|
|
640
|
+
issueLinks: [makeLibraryLink('CID-1675')],
|
|
641
|
+
libraryBranches: { 'CID-1675': 'release/IBK_ibk_1.5.2.0' }, // has release/ prefix
|
|
642
|
+
projectVersions: [{ id: '16157', name: 'IBK_ibk_1.5.2.0' }], // version without prefix
|
|
643
|
+
});
|
|
644
|
+
await executeTool('create_cd_ticket', BASE, { jira, notifier: mockNotifier });
|
|
645
|
+
|
|
646
|
+
assert.equal(jira.calls.addRemoteLink.length, 1);
|
|
647
|
+
assert.equal(jira.calls.addRemoteLink[0].title, 'IBK_ibk_1.5.2.0');
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
test('CI 關聯的非 Library 單不產生 remote link', async () => {
|
|
651
|
+
const jira = makeMockJira({
|
|
652
|
+
issueLinks: [
|
|
653
|
+
{
|
|
654
|
+
type: { name: 'Relates' },
|
|
655
|
+
inwardIssue: {
|
|
656
|
+
key: 'CID-1680',
|
|
657
|
+
fields: { issuetype: { name: 'CI' }, summary: 'CI ticket' },
|
|
658
|
+
},
|
|
659
|
+
},
|
|
660
|
+
],
|
|
661
|
+
projectVersions: [{ id: '16157', name: 'IBK_ibk_1.5.2.0' }],
|
|
662
|
+
});
|
|
663
|
+
await executeTool('create_cd_ticket', BASE, { jira, notifier: mockNotifier });
|
|
664
|
+
assert.equal(jira.calls.addRemoteLink.length, 0);
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
test('LBPRJ 找不到對應版本 → 跳過不中斷,回傳成功', async () => {
|
|
668
|
+
const jira = makeMockJira({
|
|
669
|
+
issueLinks: [makeLibraryLink('CID-1675')],
|
|
670
|
+
libraryBranches: { 'CID-1675': 'release/IBK_ibk_1.5.2.0' },
|
|
671
|
+
projectVersions: [], // 空,找不到版本
|
|
672
|
+
});
|
|
673
|
+
const result = await executeTool('create_cd_ticket', BASE, { jira, notifier: mockNotifier });
|
|
674
|
+
assert.ok(!result.content[0].text.startsWith('❌'), '不應回傳錯誤');
|
|
675
|
+
assert.equal(jira.calls.addRemoteLink.length, 0);
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
test('CI issueLinks 空 → 不呼叫 addRemoteLink', async () => {
|
|
679
|
+
const jira = makeMockJira({ issueLinks: [], projectVersions: [] });
|
|
680
|
+
await executeTool('create_cd_ticket', BASE, { jira, notifier: mockNotifier });
|
|
681
|
+
assert.equal(jira.calls.addRemoteLink.length, 0);
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
test('沒有 linkedCiKey → 不呼叫 addRemoteLink', async () => {
|
|
685
|
+
const jira = makeMockJira();
|
|
686
|
+
await executeTool(
|
|
687
|
+
'create_cd_ticket',
|
|
688
|
+
{
|
|
689
|
+
systemCode: 'IBK',
|
|
690
|
+
environment: 'stg',
|
|
691
|
+
clusterDeploy: 'tvstg-ibk-web01',
|
|
692
|
+
},
|
|
693
|
+
{ jira, notifier: mockNotifier },
|
|
694
|
+
);
|
|
695
|
+
assert.equal(jira.calls.addRemoteLink.length, 0);
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
// ── description 自動生成(generateCDDescription)────────────────
|
|
699
|
+
test('description 自動生成:開頭含「一般資訊作業申請單」', async () => {
|
|
700
|
+
const f = await getCreatedFields('create_cd_ticket', BASE, { releaseVersion: '0.0.11' });
|
|
701
|
+
assert.ok(
|
|
702
|
+
f.description.startsWith('||一般資訊作業申請單'),
|
|
703
|
+
`description 開頭錯誤: "${f.description.slice(0, 40)}"`,
|
|
704
|
+
);
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
test('description 自動生成:*上版版本號* = ciReleaseVersion (0.0.11)', async () => {
|
|
708
|
+
const f = await getCreatedFields('create_cd_ticket', BASE, { releaseVersion: '0.0.11' });
|
|
709
|
+
assert.ok(f.description.includes('|*上版版本號*|0.0.11|'), `缺少版本號: "${f.description}"`);
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
test('description 自動生成:包含所有必要欄位', async () => {
|
|
713
|
+
const f = await getCreatedFields('create_cd_ticket', BASE, { releaseVersion: '0.0.11' });
|
|
714
|
+
const requiredParts = [
|
|
715
|
+
'|*相關電子單號*|參照連結|',
|
|
716
|
+
'|*Purpose*|參照連結|',
|
|
717
|
+
'|*測試報告 (檢附前一環境 CD 單驗證結果並附上連結)*|參照連結|',
|
|
718
|
+
'|*SIT 測試涵蓋檢核表 (UAT 申請時需確認)*|參照連結|',
|
|
719
|
+
'|*UAT 黑箱掃描報告 (PRD 申請時需檢附,需白箱則請附上報告連結)*|不適用|',
|
|
720
|
+
'|*操作步驟說明*|Ansible 程式上版。|',
|
|
721
|
+
'|*簽核流程*|',
|
|
722
|
+
];
|
|
723
|
+
for (const part of requiredParts) {
|
|
724
|
+
assert.ok(f.description.includes(part), `description 缺少: "${part}"`);
|
|
725
|
+
}
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
test('description 當 ciReleaseVersion 為空 → *上版版本號* 後接空值', async () => {
|
|
729
|
+
const f = await getCreatedFields('create_cd_ticket', BASE, { releaseVersion: null });
|
|
730
|
+
assert.ok(
|
|
731
|
+
f.description.includes('|*上版版本號*||'),
|
|
732
|
+
`應含空版本號行: "${f.description.slice(0, 80)}"`,
|
|
733
|
+
);
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
test('description args.description 附加在自動描述後(以 \\n\\n 分隔)', async () => {
|
|
737
|
+
const extra = 'release notes: https://bitbucket.example.com/diff';
|
|
738
|
+
const f = await getCreatedFields(
|
|
739
|
+
'create_cd_ticket',
|
|
740
|
+
{ ...BASE, description: extra },
|
|
741
|
+
{ releaseVersion: '0.0.11' },
|
|
742
|
+
);
|
|
743
|
+
assert.ok(f.description.includes('|*上版版本號*|0.0.11|'), '應含自動描述');
|
|
744
|
+
assert.ok(
|
|
745
|
+
f.description.endsWith(`\n\n${extra}`),
|
|
746
|
+
`應以 \\n\\n + extra 結尾: "${f.description.slice(-60)}"`,
|
|
747
|
+
);
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
test('description 沒有 args.description → 不含多餘換行', async () => {
|
|
751
|
+
const f = await getCreatedFields('create_cd_ticket', BASE, { releaseVersion: '0.0.11' });
|
|
752
|
+
assert.ok(!f.description.includes('\n\n\n'), '不應有三個以上連續換行');
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
// ── ciReleaseVersion 3rd fallback(從 CI summary 解析版號)─────
|
|
756
|
+
test('ciReleaseVersion 3rd fallback:CI summary "for 0.0.11" → 版本號填入 description 與 summary', async () => {
|
|
757
|
+
const f = await getCreatedFields('create_cd_ticket', BASE, {
|
|
758
|
+
releaseVersion: null,
|
|
759
|
+
deployVersionJson: null,
|
|
760
|
+
ciSummary: '[IBK][CI] SSR & IBK for 0.0.11',
|
|
761
|
+
});
|
|
762
|
+
assert.ok(f.summary.includes('0.0.11'), `3rd fallback summary 失敗: "${f.summary}"`);
|
|
763
|
+
assert.ok(f.description.includes('|*上版版本號*|0.0.11|'), `3rd fallback description 失敗`);
|
|
764
|
+
assert.equal(f['customfield_14101'], '0.0.11', '3rd fallback deploy_version 欄位應填入');
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
test('ciReleaseVersion 3rd fallback:CI summary 無版號 → 仍回傳成功(空版本)', async () => {
|
|
768
|
+
const f = await getCreatedFields('create_cd_ticket', BASE, {
|
|
769
|
+
releaseVersion: null,
|
|
770
|
+
deployVersionJson: null,
|
|
771
|
+
ciSummary: '[IBK][CI] SSR & IBK', // 無 "for x.y.z"
|
|
772
|
+
});
|
|
773
|
+
assert.ok(f.description.includes('|*上版版本號*||'), '應含空版本號');
|
|
774
|
+
});
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
778
|
+
// release notes 產生(generateReleaseNotes)
|
|
779
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
780
|
+
describe('create_cd_ticket — release notes', () => {
|
|
781
|
+
const BASE_CD = {
|
|
782
|
+
systemCode: 'IBK',
|
|
783
|
+
environment: 'stg',
|
|
784
|
+
clusterDeploy: 'tvstg-ibk-web01',
|
|
785
|
+
linkedCiKey: 'CID-1677',
|
|
786
|
+
};
|
|
787
|
+
|
|
788
|
+
// Jenkins comment 格式:ch014.ibk.{module}-{branch}-{CID_version}
|
|
789
|
+
const IBK_COMMENT =
|
|
790
|
+
'Release version [ch014.ibk.ibk-release-v1.5.2.0-0.0.1|https://jenkins/job/1] pass in [Jenkins #1|url]';
|
|
791
|
+
const SSR_COMMENT =
|
|
792
|
+
'Release version [ch014.ibk.ssr-release-v1.3.1.0-0.0.1|https://jenkins/job/2] pass in [Jenkins #2|url]';
|
|
793
|
+
// CWA 格式:branch 已包含 CID_version 後綴(release-v1.5.2.0-0.0.1)
|
|
794
|
+
const CWA_COMMENT =
|
|
795
|
+
'Release version [ch014.cwa.cwa-release-v1.5.2.0-0.0.1|https://jenkins/job/3] pass in [Jenkins #3|url]';
|
|
796
|
+
// moduleKey 無法解析的 comment(無 -release- 結構)
|
|
797
|
+
const BAD_COMMENT = 'Release version [something-invalid-format|url] pass in [Jenkins #3|url]';
|
|
798
|
+
|
|
799
|
+
test('無 Library 單 → description 不含 release notes', async () => {
|
|
800
|
+
const f = await getCreatedFields('create_cd_ticket', BASE_CD, { issueLinks: [] });
|
|
801
|
+
assert.ok(!f.description.includes('release notes:'), 'description 不應含 release notes');
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
test('Library 單缺少 Jenkins comment → 跳過,不含 release notes(mock 模擬真實 Jira:未請求 comment 時不回傳)', async () => {
|
|
805
|
+
// mock 只在 fieldNames.includes('comment') 時才回傳 comment,
|
|
806
|
+
// 確保 generateReleaseNotes 有明確請求 'comment' 欄位,否則無法解析 CID_version
|
|
807
|
+
const f = await getCreatedFields('create_cd_ticket', BASE_CD, {
|
|
808
|
+
issueLinks: [makeLibraryLink('CID-1675')],
|
|
809
|
+
libraryBranches: { 'CID-1675': 'release/v1.5.2.0' },
|
|
810
|
+
// 沒有 libraryComments → CID_version/moduleKey 解析失敗 → 跳過
|
|
811
|
+
});
|
|
812
|
+
assert.ok(!f.description.includes('release notes:'), 'description 不應含 release notes');
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
test('Library Jenkins comment 無法解析 moduleKey → 跳過,不含 release notes', async () => {
|
|
816
|
+
const f = await getCreatedFields('create_cd_ticket', BASE_CD, {
|
|
817
|
+
issueLinks: [makeLibraryLink('CID-1675')],
|
|
818
|
+
libraryBranches: { 'CID-1675': 'release/v1.5.2.0' },
|
|
819
|
+
libraryComments: { 'CID-1675': BAD_COMMENT },
|
|
820
|
+
});
|
|
821
|
+
assert.ok(!f.description.includes('release notes:'), 'description 不應含 release notes');
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
test('單模組 IBK(如果沒有):description 含 release notes: + Bitbucket compare URL', async () => {
|
|
825
|
+
const f = await getCreatedFields('create_cd_ticket', BASE_CD, {
|
|
826
|
+
issueLinks: [makeLibraryLink('CID-1675')],
|
|
827
|
+
libraryBranches: { 'CID-1675': 'release/v1.5.2.0' },
|
|
828
|
+
libraryComments: { 'CID-1675': IBK_COMMENT },
|
|
829
|
+
bitbucketLastTag: 'release/v1.5.1.0',
|
|
830
|
+
});
|
|
831
|
+
assert.ok(f.description.includes('release notes:'), 'description 應含 release notes:');
|
|
832
|
+
assert.ok(f.description.includes('IBK:'), 'description 應含 IBK: label');
|
|
833
|
+
assert.ok(f.description.includes('tw-bank-web'), 'URL 應含 repo');
|
|
834
|
+
assert.ok(
|
|
835
|
+
f.description.includes('ci%2Frelease-v1.5.2.0-0.0.1'),
|
|
836
|
+
'URL 應含 sourceBranch(current tag)',
|
|
837
|
+
);
|
|
838
|
+
assert.ok(
|
|
839
|
+
f.description.includes('release%2Fv1.5.1.0'),
|
|
840
|
+
'URL 應含 targetBranch(last release/ tag)',
|
|
841
|
+
);
|
|
842
|
+
assert.ok(f.description.includes('targetRepoId=110'), 'URL 應含 repoId=110');
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
test('雙模組 IBK + SSR(如果沒有):description 含兩個 compare URL', async () => {
|
|
846
|
+
const f = await getCreatedFields('create_cd_ticket', BASE_CD, {
|
|
847
|
+
issueLinks: [makeLibraryLink('CID-1675'), makeLibraryLink('CID-1676')],
|
|
848
|
+
libraryBranches: {
|
|
849
|
+
'CID-1675': 'release/v1.5.2.0',
|
|
850
|
+
'CID-1676': 'release/v1.3.1.0',
|
|
851
|
+
},
|
|
852
|
+
libraryComments: {
|
|
853
|
+
'CID-1675': IBK_COMMENT,
|
|
854
|
+
'CID-1676': SSR_COMMENT,
|
|
855
|
+
},
|
|
856
|
+
bitbucketLastTag: 'release/v1.5.1.0',
|
|
857
|
+
});
|
|
858
|
+
assert.ok(f.description.includes('IBK:'), 'description 應含 IBK:');
|
|
859
|
+
assert.ok(f.description.includes('SSR:'), 'description 應含 SSR:');
|
|
860
|
+
assert.ok(f.description.includes('tw-bank-web-ssr'), 'URL 應含 SSR repo');
|
|
861
|
+
assert.ok(f.description.includes('ci%2Frelease-v1.3.1.0-0.0.1'), '應含 SSR current tag');
|
|
862
|
+
assert.ok(f.description.includes('targetRepoId=787'), '應含 SSR repoId=787');
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
test('release notes 在 description 末尾,以 \\n\\n 分隔基礎描述', async () => {
|
|
866
|
+
const f = await getCreatedFields('create_cd_ticket', BASE_CD, {
|
|
867
|
+
issueLinks: [makeLibraryLink('CID-1675')],
|
|
868
|
+
libraryBranches: { 'CID-1675': 'release/v1.5.2.0' },
|
|
869
|
+
libraryComments: { 'CID-1675': IBK_COMMENT },
|
|
870
|
+
bitbucketLastTag: 'release/v1.5.1.0',
|
|
871
|
+
});
|
|
872
|
+
const idx = f.description.indexOf('\n\nrelease notes:');
|
|
873
|
+
assert.ok(idx > 0, `release notes 應在基礎描述之後以 \\n\\n 分隔,idx=${idx}`);
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
test('Bitbucket API 查無 tag → 跳過該 module', async () => {
|
|
877
|
+
const f = await getCreatedFields('create_cd_ticket', BASE_CD, {
|
|
878
|
+
issueLinks: [makeLibraryLink('CID-1675')],
|
|
879
|
+
libraryBranches: { 'CID-1675': 'release/v1.5.2.0' },
|
|
880
|
+
libraryComments: { 'CID-1675': IBK_COMMENT },
|
|
881
|
+
bitbucketLastTag: null,
|
|
882
|
+
});
|
|
883
|
+
assert.ok(!f.description.includes('release notes:'), '查無 tag → 跳過,不含 release notes');
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
test('getBitbucketBranches 呼叫時使用 release/ filterValue', async () => {
|
|
887
|
+
const jira = makeMockJira({
|
|
888
|
+
issueLinks: [makeLibraryLink('CID-1675')],
|
|
889
|
+
libraryBranches: { 'CID-1675': 'release/v1.5.2.0' },
|
|
890
|
+
libraryComments: { 'CID-1675': IBK_COMMENT },
|
|
891
|
+
bitbucketLastTag: 'release/v1.5.1.0',
|
|
892
|
+
});
|
|
893
|
+
await executeTool('create_cd_ticket', BASE_CD, { jira, notifier: mockNotifier });
|
|
894
|
+
const branchCall = jira.calls.getBitbucketBranches[0];
|
|
895
|
+
assert.ok(branchCall, 'getBitbucketBranches 應被呼叫');
|
|
896
|
+
assert.equal(branchCall.project, 'CHANNEL');
|
|
897
|
+
assert.equal(branchCall.repo, 'tw-bank-web');
|
|
898
|
+
assert.ok(
|
|
899
|
+
branchCall.opts?.filterValue?.startsWith('release/'),
|
|
900
|
+
'filterValue 應以 release/ 開頭',
|
|
901
|
+
);
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
// ── moduleKey 解析 ────────────────────────────────────────────
|
|
905
|
+
test('SSR module → label=SSR, repo=tw-bank-web-ssr, repoId=787', async () => {
|
|
906
|
+
const f = await getCreatedFields('create_cd_ticket', BASE_CD, {
|
|
907
|
+
issueLinks: [makeLibraryLink('CID-1676')],
|
|
908
|
+
libraryBranches: { 'CID-1676': 'release/v1.3.1.0' },
|
|
909
|
+
libraryComments: { 'CID-1676': SSR_COMMENT },
|
|
910
|
+
bitbucketLastTag: 'release/v1.3.0.0',
|
|
911
|
+
});
|
|
912
|
+
assert.ok(f.description.includes('SSR:'), 'label 應為 SSR');
|
|
913
|
+
assert.ok(f.description.includes('tw-bank-web-ssr'), 'repo 應為 tw-bank-web-ssr');
|
|
914
|
+
assert.ok(f.description.includes('targetRepoId=787'), 'repoId 應為 787');
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
test('CWA module → label=CWA, repo=tw-bank-webapp, repoId=36', async () => {
|
|
918
|
+
const CWA_COMMENT = 'Release version [ch014.cwa.cwa-release-v1.5.0.0-0.0.1|url] pass';
|
|
919
|
+
const f = await getCreatedFields(
|
|
920
|
+
'create_cd_ticket',
|
|
921
|
+
{
|
|
922
|
+
...BASE_CD,
|
|
923
|
+
systemCode: 'CWA',
|
|
924
|
+
environment: 'stg',
|
|
925
|
+
clusterDeploy: 'tvstg-cwa-web01',
|
|
926
|
+
},
|
|
927
|
+
{
|
|
928
|
+
issueLinks: [makeLibraryLink('CID-2000')],
|
|
929
|
+
libraryBranches: { 'CID-2000': 'release/v1.5.0.0' },
|
|
930
|
+
libraryComments: { 'CID-2000': CWA_COMMENT },
|
|
931
|
+
bitbucketLastTag: 'release/v1.4.0.0',
|
|
932
|
+
},
|
|
933
|
+
);
|
|
934
|
+
assert.ok(f.description.includes('CWA:'), 'label 應為 CWA');
|
|
935
|
+
assert.ok(f.description.includes('tw-bank-webapp'), 'repo 應為 tw-bank-webapp');
|
|
936
|
+
assert.ok(f.description.includes('targetRepoId=36'), 'repoId 應為 36');
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
test('lib_branch 中的 / 轉成 - 產生正確 ci/ tag', async () => {
|
|
940
|
+
// release/v1.5.2.0 → release-v1.5.2.0,ci tag = ci/release-v1.5.2.0-0.0.1
|
|
941
|
+
const f = await getCreatedFields('create_cd_ticket', BASE_CD, {
|
|
942
|
+
issueLinks: [makeLibraryLink('CID-1675')],
|
|
943
|
+
libraryBranches: { 'CID-1675': 'release/v1.5.2.0' },
|
|
944
|
+
libraryComments: { 'CID-1675': IBK_COMMENT },
|
|
945
|
+
bitbucketLastTag: 'release/v1.5.1.0',
|
|
946
|
+
});
|
|
947
|
+
assert.ok(
|
|
948
|
+
f.description.includes('ci%2Frelease-v1.5.2.0-0.0.1'),
|
|
949
|
+
`sourceBranch 應含 ci%2Frelease-v1.5.2.0-0.0.1,實際: ${f.description.match(/sourceBranch=[^&]+/)?.[0]}`,
|
|
950
|
+
);
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
// ── CWA 格式:branch 已內含 CID_version 後綴(release-v1.5.2.0-0.0.1)──
|
|
954
|
+
test('CWA 格式 branch(release-{ver}-{CID_version}):sourceBranch 不重複版本', async () => {
|
|
955
|
+
// CWA Library branch = 'release-v1.5.2.0-0.0.1'(已含 CID_version=0.0.1)
|
|
956
|
+
// 預期 sourceBranch = ci/release-v1.5.2.0-0.0.1(只出現一次)
|
|
957
|
+
const BASE_CWA = {
|
|
958
|
+
systemCode: 'CWA',
|
|
959
|
+
environment: 'stg',
|
|
960
|
+
clusterDeploy: 'tvstg-cwa-web01',
|
|
961
|
+
linkedCiKey: 'CID-1668',
|
|
962
|
+
};
|
|
963
|
+
const f = await getCreatedFields('create_cd_ticket', BASE_CWA, {
|
|
964
|
+
issueLinks: [makeLibraryLink('CID-1667')],
|
|
965
|
+
libraryBranches: { 'CID-1667': 'release-v1.5.2.0-0.0.1' },
|
|
966
|
+
libraryComments: { 'CID-1667': CWA_COMMENT },
|
|
967
|
+
bitbucketLastTag: 'release/v1.5.1.0',
|
|
968
|
+
});
|
|
969
|
+
assert.ok(f.description.includes('release notes:'), 'description 應含 release notes:');
|
|
970
|
+
assert.ok(f.description.includes('CWA:'), 'label 應為 CWA');
|
|
971
|
+
// sourceBranch 應為 ci/release-v1.5.2.0-0.0.1(不是 ci/release-v1.5.2.0-0.0.1-0.0.1)
|
|
972
|
+
assert.ok(f.description.includes('ci%2Frelease-v1.5.2.0-0.0.1'), '應含正確 sourceBranch');
|
|
973
|
+
assert.ok(!f.description.includes('0.0.1-0.0.1'), '版本號不應重複出現');
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
test('CWA 格式 branch:Web Link LBPRJ 版本查找使用去除後綴後的 versionName', async () => {
|
|
977
|
+
// branch = 'release-v1.5.2.0-0.0.1' → branchBase = 'release-v1.5.2.0'
|
|
978
|
+
// → strip 'release-' → versionName = 'v1.5.2.0'
|
|
979
|
+
const BASE_CWA = {
|
|
980
|
+
systemCode: 'CWA',
|
|
981
|
+
environment: 'stg',
|
|
982
|
+
clusterDeploy: 'tvstg-cwa-web01',
|
|
983
|
+
linkedCiKey: 'CID-1668',
|
|
984
|
+
};
|
|
985
|
+
const jira = makeMockJira({
|
|
986
|
+
issueLinks: [makeLibraryLink('CID-1667')],
|
|
987
|
+
libraryBranches: { 'CID-1667': 'release-v1.5.2.0-0.0.1' },
|
|
988
|
+
libraryComments: { 'CID-1667': CWA_COMMENT },
|
|
989
|
+
projectVersions: [{ id: '20001', name: 'v1.5.2.0' }], // LBPRJ 版本
|
|
990
|
+
});
|
|
991
|
+
await executeTool('create_cd_ticket', BASE_CWA, { jira, notifier: mockNotifier });
|
|
992
|
+
assert.equal(jira.calls.addRemoteLink.length, 1, 'Web Link 應被加入');
|
|
993
|
+
assert.equal(jira.calls.addRemoteLink[0].title, 'v1.5.2.0', '版本名稱應為 v1.5.2.0');
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
// ── 「如果有」:N > 0.0.1,N-1 曾部署到相同 env ───────────────
|
|
997
|
+
test('「如果有」N=0.0.2 且 N-1 曾部署同 env → targetBranch = ci/{branch}-0.0.1,不查 Bitbucket tags', async () => {
|
|
998
|
+
const COMMENT_N2 = 'Release version [ch014.ibk.ibk-release-v1.5.2.0-0.0.2|url] pass';
|
|
999
|
+
const COMMENT_N1 = 'Release version [ch014.ibk.ibk-release-v1.5.2.0-0.0.1|url] pass';
|
|
1000
|
+
|
|
1001
|
+
// searchIssues 回傳前一張 Library(CID_version=0.0.1),其 CI 含有 STG CD
|
|
1002
|
+
const prevLibrary = {
|
|
1003
|
+
key: 'CID-1670',
|
|
1004
|
+
fields: {
|
|
1005
|
+
comment: { comments: [{ body: COMMENT_N1 }] },
|
|
1006
|
+
status: { name: 'Released' },
|
|
1007
|
+
issuelinks: [
|
|
1008
|
+
{
|
|
1009
|
+
inwardIssue: {
|
|
1010
|
+
key: 'CID-1671',
|
|
1011
|
+
fields: { issuetype: { name: 'CI' }, status: { name: 'Done' } },
|
|
1012
|
+
},
|
|
1013
|
+
},
|
|
1014
|
+
],
|
|
1015
|
+
},
|
|
1016
|
+
};
|
|
1017
|
+
// CID-1671(CI)的 contains 有一張 STG CD
|
|
1018
|
+
const ciFields = {
|
|
1019
|
+
status: { name: 'Done' },
|
|
1020
|
+
issuelinks: [
|
|
1021
|
+
{
|
|
1022
|
+
outwardIssue: {
|
|
1023
|
+
key: 'CID-1672',
|
|
1024
|
+
fields: {
|
|
1025
|
+
issuetype: { name: 'CD' },
|
|
1026
|
+
status: { name: 'Done' },
|
|
1027
|
+
summary: '[IBK][STG] 程式上版作業申請單_20260301 (CD deployment with 0.0.3)',
|
|
1028
|
+
},
|
|
1029
|
+
},
|
|
1030
|
+
},
|
|
1031
|
+
],
|
|
1032
|
+
};
|
|
1033
|
+
|
|
1034
|
+
const jira = makeMockJira({
|
|
1035
|
+
issueLinks: [makeLibraryLink('CID-1675')],
|
|
1036
|
+
libraryBranches: { 'CID-1675': 'release/v1.5.2.0' },
|
|
1037
|
+
libraryComments: { 'CID-1675': COMMENT_N2 },
|
|
1038
|
+
searchIssuesResult: [prevLibrary],
|
|
1039
|
+
ciFieldsMap: { 'CID-1671': ciFields },
|
|
1040
|
+
// bitbucketLastTag 不設定(如果有 → 不應查 Bitbucket)
|
|
1041
|
+
});
|
|
1042
|
+
await executeTool('create_cd_ticket', BASE_CD, { jira, notifier: mockNotifier });
|
|
1043
|
+
const description = jira.calls.updateIssue[0]?.fields?.description ?? '';
|
|
1044
|
+
|
|
1045
|
+
assert.ok(description.includes('ci%2Frelease-v1.5.2.0-0.0.2'), '應含 sourceBranch N=0.0.2');
|
|
1046
|
+
assert.ok(description.includes('ci%2Frelease-v1.5.2.0-0.0.1'), '應含 targetBranch N-1=0.0.1');
|
|
1047
|
+
assert.equal(jira.calls.getBitbucketBranches.length, 0, '如果有 → 不應查 Bitbucket branches');
|
|
1048
|
+
});
|
|
1049
|
+
|
|
1050
|
+
test('「如果有」N=0.0.2 但 N-1 未部署同 env → fallback 到 Bitbucket 最後 release/ tag', async () => {
|
|
1051
|
+
const COMMENT_N2 = 'Release version [ch014.ibk.ibk-release-v1.5.2.0-0.0.2|url] pass';
|
|
1052
|
+
const COMMENT_N1 = 'Release version [ch014.ibk.ibk-release-v1.5.2.0-0.0.1|url] pass';
|
|
1053
|
+
|
|
1054
|
+
// searchIssues 回傳前一張 Library,但其 CI 只有 UAT CD(不是 STG)
|
|
1055
|
+
const prevLibrary = {
|
|
1056
|
+
key: 'CID-1670',
|
|
1057
|
+
fields: {
|
|
1058
|
+
comment: { comments: [{ body: COMMENT_N1 }] },
|
|
1059
|
+
status: { name: 'Released' },
|
|
1060
|
+
issuelinks: [
|
|
1061
|
+
{
|
|
1062
|
+
inwardIssue: {
|
|
1063
|
+
key: 'CID-1671',
|
|
1064
|
+
fields: { issuetype: { name: 'CI' }, status: { name: 'Done' } },
|
|
1065
|
+
},
|
|
1066
|
+
},
|
|
1067
|
+
],
|
|
1068
|
+
},
|
|
1069
|
+
};
|
|
1070
|
+
// CI 只含 UAT CD(env 不同)
|
|
1071
|
+
const ciFields = {
|
|
1072
|
+
status: { name: 'Done' },
|
|
1073
|
+
issuelinks: [
|
|
1074
|
+
{
|
|
1075
|
+
outwardIssue: {
|
|
1076
|
+
key: 'CID-1673',
|
|
1077
|
+
fields: {
|
|
1078
|
+
issuetype: { name: 'CD' },
|
|
1079
|
+
status: { name: 'Done' },
|
|
1080
|
+
summary: '[IBK][UAT] 程式上版作業申請單_20260301', // UAT 不是 STG
|
|
1081
|
+
},
|
|
1082
|
+
},
|
|
1083
|
+
},
|
|
1084
|
+
],
|
|
1085
|
+
};
|
|
1086
|
+
|
|
1087
|
+
const f = await getCreatedFields('create_cd_ticket', BASE_CD, {
|
|
1088
|
+
issueLinks: [makeLibraryLink('CID-1675')],
|
|
1089
|
+
libraryBranches: { 'CID-1675': 'release/v1.5.2.0' },
|
|
1090
|
+
libraryComments: { 'CID-1675': COMMENT_N2 },
|
|
1091
|
+
searchIssuesResult: [prevLibrary],
|
|
1092
|
+
ciFieldsMap: { 'CID-1671': ciFields },
|
|
1093
|
+
bitbucketLastTag: 'release/v1.5.1.0',
|
|
1094
|
+
});
|
|
1095
|
+
assert.ok(
|
|
1096
|
+
f.description.includes('release%2Fv1.5.1.0'),
|
|
1097
|
+
'應 fallback 到 Bitbucket 最後 release/ tag',
|
|
1098
|
+
);
|
|
1099
|
+
assert.ok(
|
|
1100
|
+
!f.description.includes('ci%2Frelease-v1.5.2.0-0.0.1'),
|
|
1101
|
+
'不應含 ci N-1 tag(未部署同 env)',
|
|
1102
|
+
);
|
|
1103
|
+
});
|
|
1104
|
+
});
|
|
1105
|
+
|
|
1106
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
1107
|
+
// create_grayrelease_ticket
|
|
1108
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
1109
|
+
describe('create_grayrelease_ticket — fields', () => {
|
|
1110
|
+
const BASE = { systemCode: 'CWA', grayVersion: 'feature/test-sdd', environment: 'stg' };
|
|
1111
|
+
|
|
1112
|
+
test('project.key = CID', async () => {
|
|
1113
|
+
const f = await getCreatedFields('create_grayrelease_ticket', BASE);
|
|
1114
|
+
assert.deepEqual(f.project, { key: 'CID' });
|
|
1115
|
+
});
|
|
1116
|
+
test('issuetype.id = 12601 (GrayRelease)', async () => {
|
|
1117
|
+
const f = await getCreatedFields('create_grayrelease_ticket', BASE);
|
|
1118
|
+
assert.deepEqual(f.issuetype, { id: '12601' });
|
|
1119
|
+
});
|
|
1120
|
+
test('duedate = today (YYYY-MM-DD)', async () => {
|
|
1121
|
+
const f = await getCreatedFields('create_grayrelease_ticket', BASE);
|
|
1122
|
+
assert.match(f.duedate, /^\d{4}-\d{2}-\d{2}$/);
|
|
1123
|
+
});
|
|
1124
|
+
test('summary 自動生成格式:[STG][GrayRelease] CWA_CWA_YYYYMMDD 程式上版作業申請單', async () => {
|
|
1125
|
+
const f = await getCreatedFields('create_grayrelease_ticket', BASE);
|
|
1126
|
+
assert.match(f.summary, /^\[STG\]\[GrayRelease\] CWA_CWA_\d{8} 程式上版作業申請單$/);
|
|
1127
|
+
});
|
|
1128
|
+
test('summary 自訂覆蓋', async () => {
|
|
1129
|
+
const f = await getCreatedFields('create_grayrelease_ticket', {
|
|
1130
|
+
...BASE,
|
|
1131
|
+
summary: '[Custom] GR',
|
|
1132
|
+
});
|
|
1133
|
+
assert.equal(f.summary, '[Custom] GR');
|
|
1134
|
+
});
|
|
1135
|
+
test('customfield_13443 (systemCode) = { value: CWA }', async () => {
|
|
1136
|
+
const f = await getCreatedFields('create_grayrelease_ticket', BASE);
|
|
1137
|
+
assert.deepEqual(f['customfield_13443'], { value: 'CWA' });
|
|
1138
|
+
});
|
|
1139
|
+
test('customfield_13431 (gitBranch) = 原始 grayVersion(含 /)', async () => {
|
|
1140
|
+
const f = await getCreatedFields('create_grayrelease_ticket', BASE);
|
|
1141
|
+
assert.equal(f['customfield_13431'], 'feature/test-sdd');
|
|
1142
|
+
});
|
|
1143
|
+
test('customfield_14700 (grayReleaseVersion) / 替換為 -', async () => {
|
|
1144
|
+
const f = await getCreatedFields('create_grayrelease_ticket', BASE);
|
|
1145
|
+
assert.equal(f['customfield_14700'], 'feature-test-sdd');
|
|
1146
|
+
});
|
|
1147
|
+
test('customfield_14700 純版號不含 / 不變', async () => {
|
|
1148
|
+
const f = await getCreatedFields('create_grayrelease_ticket', {
|
|
1149
|
+
...BASE,
|
|
1150
|
+
grayVersion: '1.0.0',
|
|
1151
|
+
});
|
|
1152
|
+
assert.equal(f['customfield_14700'], '1.0.0');
|
|
1153
|
+
});
|
|
1154
|
+
test('customfield_13434 (deptCode) CWA → { value: CH014 }', async () => {
|
|
1155
|
+
const f = await getCreatedFields('create_grayrelease_ticket', BASE);
|
|
1156
|
+
assert.deepEqual(f['customfield_13434'], { value: 'CH014' });
|
|
1157
|
+
});
|
|
1158
|
+
test('customfield_13436 (env) stg → { value: stg }', async () => {
|
|
1159
|
+
const f = await getCreatedFields('create_grayrelease_ticket', BASE);
|
|
1160
|
+
assert.deepEqual(f['customfield_13436'], { value: 'stg' });
|
|
1161
|
+
});
|
|
1162
|
+
test('customfield_14704 (clusterDeploy) 永遠 true (id 15506)', async () => {
|
|
1163
|
+
const f = await getCreatedFields('create_grayrelease_ticket', BASE);
|
|
1164
|
+
assert.deepEqual(f['customfield_14704'], { id: '15506' });
|
|
1165
|
+
});
|
|
1166
|
+
test('customfield_14701 (clusterList) CWA STG = 換行分隔兩台', async () => {
|
|
1167
|
+
const f = await getCreatedFields('create_grayrelease_ticket', BASE);
|
|
1168
|
+
assert.equal(f['customfield_14701'], 'tvstg-cwa-web01\ntvstg-cwa-web02');
|
|
1169
|
+
});
|
|
1170
|
+
test('customfield_13444 (systemModule) CWA = { id: 15515 }', async () => {
|
|
1171
|
+
const f = await getCreatedFields('create_grayrelease_ticket', BASE);
|
|
1172
|
+
assert.deepEqual(f['customfield_13444'], { id: '15515' });
|
|
1173
|
+
});
|
|
1174
|
+
test('IBK STG: clusterList = tvstg-ibk-web01\\ntvstg-ibk-web02', async () => {
|
|
1175
|
+
const f = await getCreatedFields('create_grayrelease_ticket', {
|
|
1176
|
+
systemCode: 'IBK',
|
|
1177
|
+
grayVersion: 'feature/x',
|
|
1178
|
+
environment: 'stg',
|
|
1179
|
+
});
|
|
1180
|
+
assert.equal(f['customfield_14701'], 'tvstg-ibk-web01\ntvstg-ibk-web02');
|
|
1181
|
+
});
|
|
1182
|
+
test('IBK systemModule = { id: 15500 }', async () => {
|
|
1183
|
+
const f = await getCreatedFields('create_grayrelease_ticket', {
|
|
1184
|
+
systemCode: 'IBK',
|
|
1185
|
+
grayVersion: 'feature/x',
|
|
1186
|
+
environment: 'stg',
|
|
1187
|
+
});
|
|
1188
|
+
assert.deepEqual(f['customfield_13444'], { id: '15500' });
|
|
1189
|
+
});
|
|
1190
|
+
test('IBK deptCode → { value: CH014 }', async () => {
|
|
1191
|
+
const f = await getCreatedFields('create_grayrelease_ticket', {
|
|
1192
|
+
systemCode: 'IBK',
|
|
1193
|
+
grayVersion: '1.0.0',
|
|
1194
|
+
environment: 'stg',
|
|
1195
|
+
});
|
|
1196
|
+
assert.deepEqual(f['customfield_13434'], { value: 'CH014' });
|
|
1197
|
+
});
|
|
1198
|
+
test('EVT deptCode → { value: CH015 }', async () => {
|
|
1199
|
+
const f = await getCreatedFields('create_grayrelease_ticket', {
|
|
1200
|
+
systemCode: 'EVT',
|
|
1201
|
+
grayVersion: '1.0.0',
|
|
1202
|
+
environment: 'stg',
|
|
1203
|
+
});
|
|
1204
|
+
assert.deepEqual(f['customfield_13434'], { value: 'CH015' });
|
|
1205
|
+
});
|
|
1206
|
+
});
|
|
1207
|
+
|
|
1208
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
1209
|
+
// prepare_cd_deployment
|
|
1210
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
1211
|
+
describe('prepare_cd_deployment', () => {
|
|
1212
|
+
const DEPLOY_TRANS = {
|
|
1213
|
+
id: '999',
|
|
1214
|
+
name: 'Prepare to Create Deployment',
|
|
1215
|
+
to: { name: 'Deploying' },
|
|
1216
|
+
};
|
|
1217
|
+
|
|
1218
|
+
/** 建立含 updateIssue / transitionById / getIssue / getTransitions 的 mock */
|
|
1219
|
+
function makeDeployMock({
|
|
1220
|
+
transitionName = 'Prepare to Create Deployment',
|
|
1221
|
+
resultStatus = 'Deploying',
|
|
1222
|
+
} = {}) {
|
|
1223
|
+
const calls = { updateIssue: [], transitionById: [] };
|
|
1224
|
+
return {
|
|
1225
|
+
calls,
|
|
1226
|
+
updateIssue: async (key, fields) => {
|
|
1227
|
+
calls.updateIssue.push({ key, fields });
|
|
1228
|
+
return {};
|
|
1229
|
+
},
|
|
1230
|
+
getTransitions: async () => [{ id: '999', name: transitionName, to: { name: resultStatus } }],
|
|
1231
|
+
transitionById: async (key, id) => {
|
|
1232
|
+
calls.transitionById.push({ key, id });
|
|
1233
|
+
return {};
|
|
1234
|
+
},
|
|
1235
|
+
getIssue: async () => ({ fields: { status: { name: resultStatus }, summary: 'CD test' } }),
|
|
1236
|
+
addComment: async () => {},
|
|
1237
|
+
};
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
test('stg:更新 customfield_13436 為 id 14356', async () => {
|
|
1241
|
+
const jira = makeDeployMock();
|
|
1242
|
+
await executeTool(
|
|
1243
|
+
'prepare_cd_deployment',
|
|
1244
|
+
{ issueKey: 'CID-1697', environment: 'stg' },
|
|
1245
|
+
{
|
|
1246
|
+
jira,
|
|
1247
|
+
notifier: mockNotifier,
|
|
1248
|
+
},
|
|
1249
|
+
);
|
|
1250
|
+
assert.deepEqual(jira.calls.updateIssue[0].fields['customfield_13436'], { id: '14356' });
|
|
1251
|
+
});
|
|
1252
|
+
|
|
1253
|
+
test('uat:更新 customfield_13436 為 id 14357', async () => {
|
|
1254
|
+
const jira = makeDeployMock();
|
|
1255
|
+
await executeTool(
|
|
1256
|
+
'prepare_cd_deployment',
|
|
1257
|
+
{ issueKey: 'CID-1697', environment: 'uat' },
|
|
1258
|
+
{
|
|
1259
|
+
jira,
|
|
1260
|
+
notifier: mockNotifier,
|
|
1261
|
+
},
|
|
1262
|
+
);
|
|
1263
|
+
assert.deepEqual(jira.calls.updateIssue[0].fields['customfield_13436'], { id: '14357' });
|
|
1264
|
+
});
|
|
1265
|
+
|
|
1266
|
+
test('prd:更新 customfield_13436 為 id 14358', async () => {
|
|
1267
|
+
const jira = makeDeployMock();
|
|
1268
|
+
await executeTool(
|
|
1269
|
+
'prepare_cd_deployment',
|
|
1270
|
+
{ issueKey: 'CID-1697', environment: 'prd' },
|
|
1271
|
+
{
|
|
1272
|
+
jira,
|
|
1273
|
+
notifier: mockNotifier,
|
|
1274
|
+
},
|
|
1275
|
+
);
|
|
1276
|
+
assert.deepEqual(jira.calls.updateIssue[0].fields['customfield_13436'], { id: '14358' });
|
|
1277
|
+
});
|
|
1278
|
+
|
|
1279
|
+
test('dr:更新 customfield_13436 為 id 14359', async () => {
|
|
1280
|
+
const jira = makeDeployMock();
|
|
1281
|
+
await executeTool(
|
|
1282
|
+
'prepare_cd_deployment',
|
|
1283
|
+
{ issueKey: 'CID-1697', environment: 'dr' },
|
|
1284
|
+
{
|
|
1285
|
+
jira,
|
|
1286
|
+
notifier: mockNotifier,
|
|
1287
|
+
},
|
|
1288
|
+
);
|
|
1289
|
+
assert.deepEqual(jira.calls.updateIssue[0].fields['customfield_13436'], { id: '14359' });
|
|
1290
|
+
});
|
|
1291
|
+
|
|
1292
|
+
test('prd/dr:更新 customfield_13436 為 id 14360', async () => {
|
|
1293
|
+
const jira = makeDeployMock();
|
|
1294
|
+
await executeTool(
|
|
1295
|
+
'prepare_cd_deployment',
|
|
1296
|
+
{ issueKey: 'CID-1697', environment: 'prd/dr' },
|
|
1297
|
+
{
|
|
1298
|
+
jira,
|
|
1299
|
+
notifier: mockNotifier,
|
|
1300
|
+
},
|
|
1301
|
+
);
|
|
1302
|
+
assert.deepEqual(jira.calls.updateIssue[0].fields['customfield_13436'], { id: '14360' });
|
|
1303
|
+
});
|
|
1304
|
+
|
|
1305
|
+
test('prd&dr 正規化為 prd/dr → id 14360', async () => {
|
|
1306
|
+
const jira = makeDeployMock();
|
|
1307
|
+
await executeTool(
|
|
1308
|
+
'prepare_cd_deployment',
|
|
1309
|
+
{ issueKey: 'CID-1697', environment: 'prd&dr' },
|
|
1310
|
+
{
|
|
1311
|
+
jira,
|
|
1312
|
+
notifier: mockNotifier,
|
|
1313
|
+
},
|
|
1314
|
+
);
|
|
1315
|
+
assert.deepEqual(jira.calls.updateIssue[0].fields['customfield_13436'], { id: '14360' });
|
|
1316
|
+
});
|
|
1317
|
+
|
|
1318
|
+
test('觸發 "Prepare to Create Deployment" transition', async () => {
|
|
1319
|
+
const jira = makeDeployMock();
|
|
1320
|
+
const result = await executeTool(
|
|
1321
|
+
'prepare_cd_deployment',
|
|
1322
|
+
{ issueKey: 'CID-1697', environment: 'stg' },
|
|
1323
|
+
{
|
|
1324
|
+
jira,
|
|
1325
|
+
notifier: mockNotifier,
|
|
1326
|
+
},
|
|
1327
|
+
);
|
|
1328
|
+
assert.equal(jira.calls.transitionById[0].key, 'CID-1697');
|
|
1329
|
+
assert.equal(jira.calls.transitionById[0].id, '999');
|
|
1330
|
+
const data = JSON.parse(result.content[0].text);
|
|
1331
|
+
assert.equal(data.issueKey, 'CID-1697');
|
|
1332
|
+
assert.equal(data.environment, 'stg');
|
|
1333
|
+
assert.equal(data.status, 'Deploying');
|
|
1334
|
+
});
|
|
1335
|
+
|
|
1336
|
+
test('fallback:transition 名稱為 "Deploy" 也能觸發', async () => {
|
|
1337
|
+
const jira = makeDeployMock({ transitionName: 'Deploy' });
|
|
1338
|
+
const result = await executeTool(
|
|
1339
|
+
'prepare_cd_deployment',
|
|
1340
|
+
{ issueKey: 'CID-1697', environment: 'uat' },
|
|
1341
|
+
{
|
|
1342
|
+
jira,
|
|
1343
|
+
notifier: mockNotifier,
|
|
1344
|
+
},
|
|
1345
|
+
);
|
|
1346
|
+
assert.ok(!result.content[0].text.startsWith('❌'), '應成功觸發');
|
|
1347
|
+
assert.equal(jira.calls.transitionById.length, 1);
|
|
1348
|
+
});
|
|
1349
|
+
|
|
1350
|
+
test('找不到任何 deploy transition → 回傳錯誤', async () => {
|
|
1351
|
+
const jira = {
|
|
1352
|
+
updateIssue: async () => {},
|
|
1353
|
+
getTransitions: async () => [
|
|
1354
|
+
{ id: '1', name: 'Some Other Transition', to: { name: 'Other' } },
|
|
1355
|
+
],
|
|
1356
|
+
getIssue: async () => ({ fields: { status: { name: 'Approved' }, summary: 'CD test' } }),
|
|
1357
|
+
addComment: async () => {},
|
|
1358
|
+
};
|
|
1359
|
+
const result = await executeTool(
|
|
1360
|
+
'prepare_cd_deployment',
|
|
1361
|
+
{ issueKey: 'CID-1697', environment: 'stg' },
|
|
1362
|
+
{
|
|
1363
|
+
jira,
|
|
1364
|
+
notifier: mockNotifier,
|
|
1365
|
+
},
|
|
1366
|
+
);
|
|
1367
|
+
assert.ok(result.content[0].text.startsWith('❌'), '應回傳錯誤');
|
|
1368
|
+
assert.ok(result.content[0].text.includes('找不到部署 transition'), '應說明找不到 transition');
|
|
1369
|
+
});
|
|
1370
|
+
|
|
1371
|
+
test('不合法的環境 → 回傳錯誤', async () => {
|
|
1372
|
+
const jira = makeDeployMock();
|
|
1373
|
+
const result = await executeTool(
|
|
1374
|
+
'prepare_cd_deployment',
|
|
1375
|
+
{ issueKey: 'CID-1697', environment: 'invalid' },
|
|
1376
|
+
{
|
|
1377
|
+
jira,
|
|
1378
|
+
notifier: mockNotifier,
|
|
1379
|
+
},
|
|
1380
|
+
);
|
|
1381
|
+
assert.ok(result.content[0].text.startsWith('❌'));
|
|
1382
|
+
assert.ok(result.content[0].text.includes('不支援的 CD 部署環境'));
|
|
1383
|
+
});
|
|
1384
|
+
|
|
1385
|
+
test('pre-transition:Accept 後出現 Prepare to create deployment ticket → 觸發', async () => {
|
|
1386
|
+
let callCount = 0;
|
|
1387
|
+
const calls = { transitionById: [] };
|
|
1388
|
+
const jira = {
|
|
1389
|
+
updateIssue: async () => {},
|
|
1390
|
+
getTransitions: async () => {
|
|
1391
|
+
callCount++;
|
|
1392
|
+
// findDeployTrans(1) → 無;pre-transition check(2) → Accept;findDeployTrans after Accept(3) → deploy
|
|
1393
|
+
if (callCount <= 2)
|
|
1394
|
+
return [{ id: '10', name: 'Accept', to: { name: 'Wait For Send Notice Email' } }];
|
|
1395
|
+
return [
|
|
1396
|
+
{
|
|
1397
|
+
id: '71',
|
|
1398
|
+
name: 'Prepare to create deployment ticket',
|
|
1399
|
+
to: { name: 'Prepare For Deploy' },
|
|
1400
|
+
},
|
|
1401
|
+
];
|
|
1402
|
+
},
|
|
1403
|
+
transitionById: async (key, id) => {
|
|
1404
|
+
calls.transitionById.push({ key, id });
|
|
1405
|
+
return {};
|
|
1406
|
+
},
|
|
1407
|
+
getIssue: async () => ({ fields: { status: { name: 'Prepare For Deploy' }, summary: 'CD' } }),
|
|
1408
|
+
addComment: async () => {},
|
|
1409
|
+
};
|
|
1410
|
+
const result = await executeTool(
|
|
1411
|
+
'prepare_cd_deployment',
|
|
1412
|
+
{ issueKey: 'CID-1697', environment: 'stg' },
|
|
1413
|
+
{
|
|
1414
|
+
jira,
|
|
1415
|
+
notifier: mockNotifier,
|
|
1416
|
+
},
|
|
1417
|
+
);
|
|
1418
|
+
assert.ok(!result.content[0].text.startsWith('❌'), '應成功觸發');
|
|
1419
|
+
const data = JSON.parse(result.content[0].text);
|
|
1420
|
+
assert.equal(data.status, 'Prepare For Deploy');
|
|
1421
|
+
assert.equal(
|
|
1422
|
+
calls.transitionById.length,
|
|
1423
|
+
2,
|
|
1424
|
+
'應觸發 Accept 與 Prepare to create deployment ticket 共 2 次',
|
|
1425
|
+
);
|
|
1426
|
+
assert.equal(calls.transitionById[0].id, '10', '第 1 次應觸發 Accept');
|
|
1427
|
+
assert.equal(
|
|
1428
|
+
calls.transitionById[1].id,
|
|
1429
|
+
'71',
|
|
1430
|
+
'第 2 次應觸發 Prepare to create deployment ticket',
|
|
1431
|
+
);
|
|
1432
|
+
});
|
|
1433
|
+
});
|
|
1434
|
+
|
|
1435
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
1436
|
+
// trigger_deployment
|
|
1437
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
1438
|
+
describe('trigger_deployment', () => {
|
|
1439
|
+
/** 建立 sub-task mock helper */
|
|
1440
|
+
function makeSubTask(key, summaryPrefix = '[STG]') {
|
|
1441
|
+
return {
|
|
1442
|
+
id: 'ST-001',
|
|
1443
|
+
key,
|
|
1444
|
+
fields: {
|
|
1445
|
+
summary: `${summaryPrefix} Deployment`,
|
|
1446
|
+
status: { name: 'Open' },
|
|
1447
|
+
issuetype: { name: 'Deployment' },
|
|
1448
|
+
},
|
|
1449
|
+
};
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
/** mock jira for trigger_deployment */
|
|
1453
|
+
function makeTriggerMock({
|
|
1454
|
+
subTasks = [],
|
|
1455
|
+
deployTrans = [],
|
|
1456
|
+
cdTrans = [],
|
|
1457
|
+
finalStatus = 'Auto Deploy',
|
|
1458
|
+
} = {}) {
|
|
1459
|
+
const calls = { transitionById: [], getTransitions: [] };
|
|
1460
|
+
return {
|
|
1461
|
+
calls,
|
|
1462
|
+
getSubTasks: async () => subTasks,
|
|
1463
|
+
getTransitions: async (key) => {
|
|
1464
|
+
calls.getTransitions.push(key);
|
|
1465
|
+
if (subTasks.some((t) => t.key === key)) return deployTrans;
|
|
1466
|
+
return cdTrans;
|
|
1467
|
+
},
|
|
1468
|
+
transitionById: async (key, id) => {
|
|
1469
|
+
calls.transitionById.push({ key, id });
|
|
1470
|
+
return {};
|
|
1471
|
+
},
|
|
1472
|
+
getIssue: async (key) => ({
|
|
1473
|
+
fields: { status: { name: finalStatus }, summary: `[MOCK] ${key}` },
|
|
1474
|
+
}),
|
|
1475
|
+
addComment: async () => {},
|
|
1476
|
+
};
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
test('成功觸發:To AutoDeploy → Trigger AutoDeploy,回傳 deploymentKey', async () => {
|
|
1480
|
+
const jira = makeTriggerMock({
|
|
1481
|
+
subTasks: [makeSubTask('CID-9001', '[STG]')],
|
|
1482
|
+
deployTrans: [
|
|
1483
|
+
{ id: '11', name: 'To AutoDeploy', to: { name: 'Pre Auto Deploy' } },
|
|
1484
|
+
{ id: '12', name: 'Trigger AutoDeploy', to: { name: 'Auto Deploy' } },
|
|
1485
|
+
],
|
|
1486
|
+
});
|
|
1487
|
+
const result = await executeTool(
|
|
1488
|
+
'trigger_deployment',
|
|
1489
|
+
{ cdIssueKey: 'CID-9000', environment: 'stg' },
|
|
1490
|
+
{
|
|
1491
|
+
jira,
|
|
1492
|
+
notifier: mockNotifier,
|
|
1493
|
+
},
|
|
1494
|
+
);
|
|
1495
|
+
assert.ok(!result.content[0].text.startsWith('❌'), '不應回傳錯誤');
|
|
1496
|
+
const data = JSON.parse(result.content[0].text);
|
|
1497
|
+
assert.equal(data.deploymentKey, 'CID-9001');
|
|
1498
|
+
assert.equal(data.environment, 'stg');
|
|
1499
|
+
assert.ok(
|
|
1500
|
+
data.steps.some((s) => s.includes('To AutoDeploy')),
|
|
1501
|
+
'應記錄 To AutoDeploy',
|
|
1502
|
+
);
|
|
1503
|
+
assert.ok(
|
|
1504
|
+
data.steps.some((s) => s.includes('Trigger AutoDeploy')),
|
|
1505
|
+
'應記錄 Trigger AutoDeploy',
|
|
1506
|
+
);
|
|
1507
|
+
});
|
|
1508
|
+
|
|
1509
|
+
test('無 sub-task → 回傳錯誤', async () => {
|
|
1510
|
+
const jira = makeTriggerMock({ subTasks: [] });
|
|
1511
|
+
const result = await executeTool(
|
|
1512
|
+
'trigger_deployment',
|
|
1513
|
+
{ cdIssueKey: 'CID-9000', environment: 'stg' },
|
|
1514
|
+
{
|
|
1515
|
+
jira,
|
|
1516
|
+
notifier: mockNotifier,
|
|
1517
|
+
},
|
|
1518
|
+
);
|
|
1519
|
+
assert.ok(result.content[0].text.startsWith('❌'));
|
|
1520
|
+
assert.ok(result.content[0].text.includes('Deployment sub-task'));
|
|
1521
|
+
});
|
|
1522
|
+
|
|
1523
|
+
test('環境比對:[STG] summary 對應 stg → 選擇 STG sub-task,不選 UAT', async () => {
|
|
1524
|
+
const jira = makeTriggerMock({
|
|
1525
|
+
subTasks: [makeSubTask('CID-9001', '[STG]'), makeSubTask('CID-9002', '[UAT]')],
|
|
1526
|
+
deployTrans: [{ id: '11', name: 'To AutoDeploy', to: { name: 'Pre Auto Deploy' } }],
|
|
1527
|
+
});
|
|
1528
|
+
const result = await executeTool(
|
|
1529
|
+
'trigger_deployment',
|
|
1530
|
+
{ cdIssueKey: 'CID-9000', environment: 'stg' },
|
|
1531
|
+
{
|
|
1532
|
+
jira,
|
|
1533
|
+
notifier: mockNotifier,
|
|
1534
|
+
},
|
|
1535
|
+
);
|
|
1536
|
+
const data = JSON.parse(result.content[0].text);
|
|
1537
|
+
assert.equal(data.deploymentKey, 'CID-9001', '應選 STG sub-task');
|
|
1538
|
+
});
|
|
1539
|
+
|
|
1540
|
+
test('環境比對:[UAT] summary 對應 uat → 選擇 UAT sub-task', async () => {
|
|
1541
|
+
const jira = makeTriggerMock({
|
|
1542
|
+
subTasks: [makeSubTask('CID-9001', '[STG]'), makeSubTask('CID-9002', '[UAT]')],
|
|
1543
|
+
deployTrans: [{ id: '11', name: 'To AutoDeploy', to: { name: 'Pre Auto Deploy' } }],
|
|
1544
|
+
});
|
|
1545
|
+
const result = await executeTool(
|
|
1546
|
+
'trigger_deployment',
|
|
1547
|
+
{ cdIssueKey: 'CID-9000', environment: 'uat' },
|
|
1548
|
+
{
|
|
1549
|
+
jira,
|
|
1550
|
+
notifier: mockNotifier,
|
|
1551
|
+
},
|
|
1552
|
+
);
|
|
1553
|
+
const data = JSON.parse(result.content[0].text);
|
|
1554
|
+
assert.equal(data.deploymentKey, 'CID-9002', '應選 UAT sub-task');
|
|
1555
|
+
});
|
|
1556
|
+
|
|
1557
|
+
test('無符合環境的 sub-task → fallback 取第一個', async () => {
|
|
1558
|
+
const jira = makeTriggerMock({
|
|
1559
|
+
subTasks: [makeSubTask('CID-9001', '[DEV]')],
|
|
1560
|
+
deployTrans: [{ id: '11', name: 'To AutoDeploy', to: { name: 'Pre Auto Deploy' } }],
|
|
1561
|
+
});
|
|
1562
|
+
const result = await executeTool(
|
|
1563
|
+
'trigger_deployment',
|
|
1564
|
+
{ cdIssueKey: 'CID-9000', environment: 'stg' },
|
|
1565
|
+
{
|
|
1566
|
+
jira,
|
|
1567
|
+
notifier: mockNotifier,
|
|
1568
|
+
},
|
|
1569
|
+
);
|
|
1570
|
+
const data = JSON.parse(result.content[0].text);
|
|
1571
|
+
assert.equal(data.deploymentKey, 'CID-9001', 'fallback 應取第一個');
|
|
1572
|
+
assert.ok(
|
|
1573
|
+
data.steps.some((s) => s.includes('⚠️') && s.includes('未找到明確符合')),
|
|
1574
|
+
'應有 fallback 警告',
|
|
1575
|
+
);
|
|
1576
|
+
});
|
|
1577
|
+
|
|
1578
|
+
test('找不到 transition → 跳過,不中斷,仍回傳成功', async () => {
|
|
1579
|
+
const jira = makeTriggerMock({
|
|
1580
|
+
subTasks: [makeSubTask('CID-9001', '[STG]')],
|
|
1581
|
+
deployTrans: [], // 完全沒有 transitions
|
|
1582
|
+
});
|
|
1583
|
+
const result = await executeTool(
|
|
1584
|
+
'trigger_deployment',
|
|
1585
|
+
{ cdIssueKey: 'CID-9000', environment: 'stg' },
|
|
1586
|
+
{
|
|
1587
|
+
jira,
|
|
1588
|
+
notifier: mockNotifier,
|
|
1589
|
+
},
|
|
1590
|
+
);
|
|
1591
|
+
assert.ok(!result.content[0].text.startsWith('❌'), '應成功回傳(跳過所有 transition)');
|
|
1592
|
+
const data = JSON.parse(result.content[0].text);
|
|
1593
|
+
assert.ok(
|
|
1594
|
+
data.steps.some((s) => s.includes('跳過')),
|
|
1595
|
+
'應記錄跳過',
|
|
1596
|
+
);
|
|
1597
|
+
});
|
|
1598
|
+
|
|
1599
|
+
test('applyForClose=true → 觸發 CD 單的 Apply for close', async () => {
|
|
1600
|
+
const jira = makeTriggerMock({
|
|
1601
|
+
subTasks: [makeSubTask('CID-9001', '[STG]')],
|
|
1602
|
+
deployTrans: [
|
|
1603
|
+
{ id: '11', name: 'To AutoDeploy', to: { name: 'Pre Auto Deploy' } },
|
|
1604
|
+
{ id: '12', name: 'Trigger AutoDeploy', to: { name: 'Auto Deploy' } },
|
|
1605
|
+
],
|
|
1606
|
+
cdTrans: [{ id: '99', name: 'Apply for close', to: { name: 'Wait For Close' } }],
|
|
1607
|
+
});
|
|
1608
|
+
await executeTool(
|
|
1609
|
+
'trigger_deployment',
|
|
1610
|
+
{ cdIssueKey: 'CID-9000', environment: 'stg', applyForClose: true },
|
|
1611
|
+
{
|
|
1612
|
+
jira,
|
|
1613
|
+
notifier: mockNotifier,
|
|
1614
|
+
},
|
|
1615
|
+
);
|
|
1616
|
+
const closeCall = jira.calls.transitionById.find((c) => c.key === 'CID-9000' && c.id === '99');
|
|
1617
|
+
assert.ok(closeCall, 'applyForClose=true 應觸發 CD 單的 Apply for close(id 99)');
|
|
1618
|
+
});
|
|
1619
|
+
|
|
1620
|
+
test('applyForClose 預設 false → 不觸發 CD 單 transition', async () => {
|
|
1621
|
+
const jira = makeTriggerMock({
|
|
1622
|
+
subTasks: [makeSubTask('CID-9001', '[STG]')],
|
|
1623
|
+
deployTrans: [{ id: '11', name: 'To AutoDeploy', to: { name: 'Pre Auto Deploy' } }],
|
|
1624
|
+
cdTrans: [{ id: '99', name: 'Apply for close', to: { name: 'Wait For Close' } }],
|
|
1625
|
+
});
|
|
1626
|
+
await executeTool(
|
|
1627
|
+
'trigger_deployment',
|
|
1628
|
+
{ cdIssueKey: 'CID-9000', environment: 'stg' },
|
|
1629
|
+
{
|
|
1630
|
+
jira,
|
|
1631
|
+
notifier: mockNotifier,
|
|
1632
|
+
},
|
|
1633
|
+
);
|
|
1634
|
+
const cdCall = jira.calls.transitionById.find((c) => c.key === 'CID-9000');
|
|
1635
|
+
assert.ok(!cdCall, 'applyForClose 未設定 → 不應觸發 CD 單 transition');
|
|
1636
|
+
});
|
|
1637
|
+
|
|
1638
|
+
test('applyForClose=true 但 CD 單找不到 Apply for close → 記錄警告不中斷', async () => {
|
|
1639
|
+
const jira = makeTriggerMock({
|
|
1640
|
+
subTasks: [makeSubTask('CID-9001', '[STG]')],
|
|
1641
|
+
deployTrans: [{ id: '11', name: 'To AutoDeploy', to: { name: 'Pre Auto Deploy' } }],
|
|
1642
|
+
cdTrans: [], // CD 單沒有 Apply for close
|
|
1643
|
+
});
|
|
1644
|
+
const result = await executeTool(
|
|
1645
|
+
'trigger_deployment',
|
|
1646
|
+
{
|
|
1647
|
+
cdIssueKey: 'CID-9000',
|
|
1648
|
+
environment: 'stg',
|
|
1649
|
+
applyForClose: true,
|
|
1650
|
+
},
|
|
1651
|
+
{ jira, notifier: mockNotifier },
|
|
1652
|
+
);
|
|
1653
|
+
assert.ok(!result.content[0].text.startsWith('❌'), '不應因找不到 Apply for close 而報錯');
|
|
1654
|
+
const data = JSON.parse(result.content[0].text);
|
|
1655
|
+
assert.ok(
|
|
1656
|
+
data.steps.some((s) => s.includes('⚠️') && s.includes('Apply for close')),
|
|
1657
|
+
'應有警告記錄',
|
|
1658
|
+
);
|
|
1659
|
+
});
|
|
1660
|
+
});
|
|
1661
|
+
|
|
1662
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
1663
|
+
// create_cd_ticket — moduleChild fallback
|
|
1664
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
1665
|
+
describe('create_cd_ticket — moduleChild fallback', () => {
|
|
1666
|
+
test('CWA 未傳 moduleChild → 預設 cwa (systemCode.toLowerCase())', async () => {
|
|
1667
|
+
// 驗證 extraVars 產生正確(cwa 系統 → cwa_cwa_installation)
|
|
1668
|
+
const f = await getCreatedFields('create_cd_ticket', {
|
|
1669
|
+
systemCode: 'CWA',
|
|
1670
|
+
environment: 'stg',
|
|
1671
|
+
clusterDeploy: 'tvstg-cwa-web01,tvstg-cwa-web02',
|
|
1672
|
+
// 不傳 moduleChild
|
|
1673
|
+
});
|
|
1674
|
+
const ev = JSON.parse(f['customfield_13437']);
|
|
1675
|
+
assert.ok(
|
|
1676
|
+
'cwa_cwa_installation' in ev,
|
|
1677
|
+
'moduleChild fallback 為 cwa → 應有 cwa_cwa_installation',
|
|
1678
|
+
);
|
|
1679
|
+
});
|
|
1680
|
+
|
|
1681
|
+
test('IBK 未傳 moduleChild → 預設 ibk,extraVars 含全 IBK 模組', async () => {
|
|
1682
|
+
const f = await getCreatedFields('create_cd_ticket', {
|
|
1683
|
+
systemCode: 'IBK',
|
|
1684
|
+
environment: 'stg',
|
|
1685
|
+
clusterDeploy: 'tvstg-ibk-web01',
|
|
1686
|
+
// 不傳 moduleChild
|
|
1687
|
+
});
|
|
1688
|
+
const ev = JSON.parse(f['customfield_13437']);
|
|
1689
|
+
assert.ok('ibk_ibk_installation' in ev, 'IBK fallback → 應有 ibk_ibk_installation');
|
|
1690
|
+
});
|
|
1691
|
+
|
|
1692
|
+
test('明確傳入 moduleChild 優先於 fallback', async () => {
|
|
1693
|
+
// 若使用者明確傳 moduleChild='ssr',不應被 fallback 覆蓋
|
|
1694
|
+
const f = await getCreatedFields(
|
|
1695
|
+
'create_cd_ticket',
|
|
1696
|
+
{
|
|
1697
|
+
systemCode: 'IBK',
|
|
1698
|
+
environment: 'stg',
|
|
1699
|
+
clusterDeploy: 'tvstg-ibk-web01',
|
|
1700
|
+
moduleChild: 'ssr',
|
|
1701
|
+
linkedCiKey: 'CID-1677',
|
|
1702
|
+
},
|
|
1703
|
+
{
|
|
1704
|
+
issueLinks: [],
|
|
1705
|
+
},
|
|
1706
|
+
);
|
|
1707
|
+
// extraVars 應仍走 fallback(無 Library 關聯),但 moduleChild 欄位已為 'ssr'
|
|
1708
|
+
// 驗證票已正常建立(不因 moduleChild='ssr' 而失敗)
|
|
1709
|
+
assert.ok(f.project, '票應正常建立');
|
|
1710
|
+
});
|
|
1711
|
+
});
|