@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/ci.js
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CI Golden Image 相關 tools
|
|
3
|
+
* - create_ci_ticket
|
|
4
|
+
*/
|
|
5
|
+
import {error, ok, today} from './helpers.js';
|
|
6
|
+
import {
|
|
7
|
+
CI_FIELD_IDS,
|
|
8
|
+
DEPT_CODES,
|
|
9
|
+
ENV_CODES,
|
|
10
|
+
FIELD_OPTIONS,
|
|
11
|
+
ISSUE_TYPE_IDS,
|
|
12
|
+
JIRA_PROJECT_ID,
|
|
13
|
+
REPO_MAPS,
|
|
14
|
+
SYSTEM_CODES,
|
|
15
|
+
SYSTEM_MODULES,
|
|
16
|
+
SYSTEM_TO_CI_REPO_MAP,
|
|
17
|
+
SYSTEM_TO_DEPT_MAP,
|
|
18
|
+
} from '../constants/index.js';
|
|
19
|
+
|
|
20
|
+
// ── Schema definitions ───────────────────────────────────────────
|
|
21
|
+
export function getCIToolDefinitions() {
|
|
22
|
+
return [
|
|
23
|
+
{
|
|
24
|
+
name: 'create_ci_ticket',
|
|
25
|
+
description: '建立 CI Release 上版單。專用工具,提前驗證必填欄位。',
|
|
26
|
+
inputSchema: {
|
|
27
|
+
type: 'object',
|
|
28
|
+
required: ['systemCode'],
|
|
29
|
+
properties: {
|
|
30
|
+
systemCode: {
|
|
31
|
+
type: 'string',
|
|
32
|
+
enum: Object.values(SYSTEM_CODES),
|
|
33
|
+
description: '系統代碼',
|
|
34
|
+
},
|
|
35
|
+
branch: {
|
|
36
|
+
type: 'string',
|
|
37
|
+
description: '(選填) Git 分支名稱,預設 master',
|
|
38
|
+
},
|
|
39
|
+
modules: {
|
|
40
|
+
type: 'string',
|
|
41
|
+
description:
|
|
42
|
+
'(選填) 模組名稱,例如 ibk、cwa、ssr(對應 SYSTEM_MODULES[systemCode]),可能同時多個模組,預設為 SYSTEM_MODULES[systemCode] 的全部模組',
|
|
43
|
+
},
|
|
44
|
+
environment: {
|
|
45
|
+
type: 'string',
|
|
46
|
+
enum: Object.keys(ENV_CODES),
|
|
47
|
+
description: '(選填) 部署環境,預設 stg',
|
|
48
|
+
},
|
|
49
|
+
summary: {
|
|
50
|
+
type: 'string',
|
|
51
|
+
description:
|
|
52
|
+
'(選填)自訂 Ticket 標題,不填則自動生成,格式:[IBK][CI] IBK & WEALTH & SSR & A11Y for 0.0.1',
|
|
53
|
+
},
|
|
54
|
+
goldenImageVersion: {
|
|
55
|
+
type: 'string',
|
|
56
|
+
description:
|
|
57
|
+
'Golden Image 版本號,例如 0.0.1(用於 summary:[IBK][CI] IBK & WEALTH for 0.0.1)',
|
|
58
|
+
},
|
|
59
|
+
antiScanRequired: {
|
|
60
|
+
type: 'boolean',
|
|
61
|
+
description: '是否需要反掃描檢測(選填,預設 true)',
|
|
62
|
+
},
|
|
63
|
+
relatesTo: {
|
|
64
|
+
type: 'array',
|
|
65
|
+
items: {type: 'string'},
|
|
66
|
+
description:
|
|
67
|
+
'關聯的 Library 單 issue key,有可能多個,例如 CID-1178 CID-1182(建立後自動加上 relates to link)',
|
|
68
|
+
},
|
|
69
|
+
dryRun: {
|
|
70
|
+
type: 'boolean',
|
|
71
|
+
description: '(選填) 預覽模式,不實際建立 Jira 單,回傳會送出的 payload',
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 建立 CI Golden Image 上版單
|
|
81
|
+
*/
|
|
82
|
+
export async function handleCreateCITicket(args, {jira, notifier}) {
|
|
83
|
+
try {
|
|
84
|
+
const branch = args.branch || 'master';
|
|
85
|
+
const envCode = (args.environment ?? 'stg').toLowerCase();
|
|
86
|
+
// Summary 格式對齊 Confluence:[IBK][CI] Wealth & SSR & IBK & A11y for 0.0.1
|
|
87
|
+
const modules = Array.isArray(args.modules)
|
|
88
|
+
? args.modules
|
|
89
|
+
: typeof args.modules === 'string'
|
|
90
|
+
? args.modules.split(',').map((module) => module.trim()).filter(Boolean)
|
|
91
|
+
: SYSTEM_MODULES[args.systemCode];
|
|
92
|
+
const modulesStr = modules.map((m) => m.toUpperCase()).join(' & ');
|
|
93
|
+
|
|
94
|
+
const versionSuffix = args.goldenImageVersion ? ` for ${args.goldenImageVersion}` : '';
|
|
95
|
+
const autoSummary = args.summary ?? `[${args.systemCode}][CI] ${modulesStr}${versionSuffix}`;
|
|
96
|
+
|
|
97
|
+
const fields = {
|
|
98
|
+
project: {key: JIRA_PROJECT_ID},
|
|
99
|
+
issuetype: {id: ISSUE_TYPE_IDS.CI},
|
|
100
|
+
summary: autoSummary,
|
|
101
|
+
duedate: today(),
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// systemCode
|
|
105
|
+
if (CI_FIELD_IDS.systemCode) {
|
|
106
|
+
fields[CI_FIELD_IDS.systemCode] = {value: args.systemCode};
|
|
107
|
+
}
|
|
108
|
+
// env (必填)
|
|
109
|
+
if (CI_FIELD_IDS.env && ENV_CODES[envCode]) {
|
|
110
|
+
fields[CI_FIELD_IDS.env] = {id: ENV_CODES[envCode]};
|
|
111
|
+
}
|
|
112
|
+
// dept_code (必填,由 systemCode 推導)
|
|
113
|
+
const deptStr = SYSTEM_TO_DEPT_MAP[args.systemCode];
|
|
114
|
+
if (CI_FIELD_IDS.deptCode && deptStr && DEPT_CODES[deptStr]) {
|
|
115
|
+
fields[CI_FIELD_IDS.deptCode] = {id: DEPT_CODES[deptStr]};
|
|
116
|
+
}
|
|
117
|
+
// system_module (必填,預設 assembly)
|
|
118
|
+
if (CI_FIELD_IDS.systemModule) {
|
|
119
|
+
fields[CI_FIELD_IDS.systemModule] = {id: FIELD_OPTIONS.systemModule.assembly};
|
|
120
|
+
}
|
|
121
|
+
// git branch → customfield_13431 (14702 不在 CI screen)
|
|
122
|
+
if (CI_FIELD_IDS.gitBranch && branch) {
|
|
123
|
+
fields[CI_FIELD_IDS.gitBranch] = branch;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// antiScan
|
|
127
|
+
fields[CI_FIELD_IDS.antiScanRequired] = {
|
|
128
|
+
id: args.antiScanRequired === false
|
|
129
|
+
? FIELD_OPTIONS.antiScan.false
|
|
130
|
+
: FIELD_OPTIONS.antiScan.true,
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
if (args.dryRun)
|
|
134
|
+
return ok({dryRun: true, summary: autoSummary, fields, relatesTo: args.relatesTo ?? []});
|
|
135
|
+
|
|
136
|
+
const issue = await jira.createIssue(fields);
|
|
137
|
+
|
|
138
|
+
// 自動 link 到 Library 單
|
|
139
|
+
const relatesTo = Array.isArray(args.relatesTo) ? args.relatesTo : args.relatesTo ? [args.relatesTo] : [];
|
|
140
|
+
if (relatesTo.length > 0) {
|
|
141
|
+
for (const key of relatesTo) {
|
|
142
|
+
await jira.linkIssue(issue.key, key, 'Relates');
|
|
143
|
+
await notifier.notify(issue.key, `已關聯 Library 單 ${key}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
await notifier.notify(
|
|
148
|
+
issue.key,
|
|
149
|
+
`CI Release 單已建立。系統: ${args.systemCode}, 分支: ${branch}, 環境: ${envCode}`,
|
|
150
|
+
);
|
|
151
|
+
return ok({
|
|
152
|
+
issueKey: issue.key,
|
|
153
|
+
issueId: issue.id,
|
|
154
|
+
url: `${process.env.JIRA_BASE_URL}/browse/${issue.key}`,
|
|
155
|
+
type: 'CI Release',
|
|
156
|
+
system: args.systemCode,
|
|
157
|
+
...(relatesTo.length > 0 && {relatesTo}),
|
|
158
|
+
});
|
|
159
|
+
} catch (err) {
|
|
160
|
+
return error(`無法建立 CI 單: ${err.message}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* 計算下一個 Golden Image Release 版號
|
|
166
|
+
*/
|
|
167
|
+
export async function handleGetNextCIVersion(args, {jira}) {
|
|
168
|
+
const {systemCode, branch} = args;
|
|
169
|
+
|
|
170
|
+
const repoName = SYSTEM_TO_CI_REPO_MAP[systemCode];
|
|
171
|
+
if (!repoName) {
|
|
172
|
+
return error(`找不到系統 "${systemCode}" 的 Golden Image repo,請確認 SYSTEM_TO_CI_REPO_MAP`);
|
|
173
|
+
}
|
|
174
|
+
const repoMeta = REPO_MAPS[repoName];
|
|
175
|
+
if (!repoMeta) {
|
|
176
|
+
return error(`找不到 repo "${repoName}" 的 Bitbucket 設定`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
let xmlVersion = null;
|
|
180
|
+
try {
|
|
181
|
+
const xmlContent = await jira.getBitbucketFileContent(
|
|
182
|
+
repoMeta.project,
|
|
183
|
+
repoName,
|
|
184
|
+
'pom.xml',
|
|
185
|
+
branch,
|
|
186
|
+
);
|
|
187
|
+
// 移除 <parent> 區塊,避免誤抓 parent 版號
|
|
188
|
+
const contentWithoutParent = xmlContent.replace(/<parent>[\s\S]*?<\/parent>/m, '');
|
|
189
|
+
const match = contentWithoutParent.match(/<version>(\d+\.\d+\.\d+)(?:-[^<]+)?<\/version>/);
|
|
190
|
+
if (!match) {
|
|
191
|
+
return error(`專案 (${repoName}),pom.xml 中找不到 <version> 標籤(branch: ${branch})`);
|
|
192
|
+
}
|
|
193
|
+
xmlVersion = match[1].trim();
|
|
194
|
+
} catch (err) {
|
|
195
|
+
return error(`無法取得 pom.xml:${err.message}`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return ok({
|
|
199
|
+
systemCode,
|
|
200
|
+
branch,
|
|
201
|
+
summaryVersion: xmlVersion,
|
|
202
|
+
message: `✅ 下一個版本:${xmlVersion}(XML 現版:${xmlVersion})`,
|
|
203
|
+
});
|
|
204
|
+
}
|
package/tools/ci.test.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* handleGetNextCIVersion 單元測試
|
|
3
|
+
* 執行:node --test src/tools/ci.test.js
|
|
4
|
+
*/
|
|
5
|
+
import {test, mock} from 'node:test';
|
|
6
|
+
import assert from 'node:assert/strict';
|
|
7
|
+
|
|
8
|
+
// helpers.js 內部 import '../constants'(directory import)在 ESM 直接執行時不支援,
|
|
9
|
+
// 用 mock.module 提前替換,避免 ERR_UNSUPPORTED_DIR_IMPORT。
|
|
10
|
+
await mock.module('./helpers.js', {
|
|
11
|
+
namedExports: {
|
|
12
|
+
ok: (data) => ({content: [{type: 'text', text: JSON.stringify(data, null, 2)}]}),
|
|
13
|
+
error: (msg) => ({content: [{type: 'text', text: `❌ 錯誤: ${msg}`}]}),
|
|
14
|
+
today: () => new Date().toISOString().slice(0, 10),
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const {handleGetNextCIVersion, handleCreateCITicket} = await import('./ci.js');
|
|
19
|
+
|
|
20
|
+
// ── Mock helpers ─────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
function makePom(version) {
|
|
23
|
+
return `<project>
|
|
24
|
+
<parent><groupId>com.example</groupId><artifactId>parent</artifactId><version>0.0.1</version></parent>
|
|
25
|
+
<artifactId>assembly</artifactId>
|
|
26
|
+
<version>${version}-SNAPSHOT</version>
|
|
27
|
+
</project>`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function makeJira(pomContent) {
|
|
31
|
+
return {getBitbucketFileContent: async () => pomContent};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function makeJiraThrows(msg) {
|
|
35
|
+
return {
|
|
36
|
+
getBitbucketFileContent: async () => {
|
|
37
|
+
throw new Error(msg);
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function parseOk(result) {
|
|
43
|
+
return JSON.parse(result.content[0].text);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ── Tests ────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
test('IBK - 正常解析 pom.xml 版本號', async () => {
|
|
49
|
+
const result = await handleGetNextCIVersion(
|
|
50
|
+
{systemCode: 'IBK', branch: 'master'},
|
|
51
|
+
{jira: makeJira(makePom('0.0.12'))},
|
|
52
|
+
);
|
|
53
|
+
const data = parseOk(result);
|
|
54
|
+
assert.equal(data.systemCode, 'IBK');
|
|
55
|
+
assert.equal(data.summaryVersion, '0.0.12');
|
|
56
|
+
assert.equal(data.branch, 'master');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('CWA - 正常解析 pom.xml 版本號', async () => {
|
|
60
|
+
const result = await handleGetNextCIVersion(
|
|
61
|
+
{systemCode: 'CWA', branch: 'master'},
|
|
62
|
+
{jira: makeJira(makePom('1.2.3'))},
|
|
63
|
+
);
|
|
64
|
+
assert.equal(parseOk(result).summaryVersion, '1.2.3');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('systemCode 不存在 SYSTEM_TO_CI_REPO_MAP → 回傳 error', async () => {
|
|
68
|
+
const result = await handleGetNextCIVersion(
|
|
69
|
+
{systemCode: 'NPM', branch: 'master'},
|
|
70
|
+
{jira: makeJira('')},
|
|
71
|
+
);
|
|
72
|
+
const text = result.content[0].text;
|
|
73
|
+
assert.match(text, /❌/);
|
|
74
|
+
assert.match(text, /SYSTEM_TO_CI_REPO_MAP/);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('pom.xml 沒有 <version> 標籤 → 回傳 error', async () => {
|
|
78
|
+
const result = await handleGetNextCIVersion(
|
|
79
|
+
{systemCode: 'IBK', branch: 'master'},
|
|
80
|
+
{jira: makeJira('<project><name>no-version</name></project>')},
|
|
81
|
+
);
|
|
82
|
+
const text = result.content[0].text;
|
|
83
|
+
assert.match(text, /❌/);
|
|
84
|
+
assert.match(text, /pom\.xml/);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('Bitbucket 拋例外 → 回傳 error', async () => {
|
|
88
|
+
const result = await handleGetNextCIVersion(
|
|
89
|
+
{systemCode: 'IBK', branch: 'master'},
|
|
90
|
+
{jira: makeJiraThrows('network timeout')},
|
|
91
|
+
);
|
|
92
|
+
const text = result.content[0].text;
|
|
93
|
+
assert.match(text, /❌/);
|
|
94
|
+
assert.match(text, /network timeout/);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// ── handleCreateCITicket tests ────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
function makeFullJira(pomVersion = '0.0.12') {
|
|
100
|
+
const linked = [];
|
|
101
|
+
return {
|
|
102
|
+
getBitbucketFileContent: async () => `<project>
|
|
103
|
+
<parent><version>0.0.1</version></parent>
|
|
104
|
+
<version>${pomVersion}-SNAPSHOT</version>
|
|
105
|
+
</project>`,
|
|
106
|
+
createIssue: async () => ({id: 'TEST-ID', key: 'CID-TEST'}),
|
|
107
|
+
linkIssue: async (from, to, type) => {
|
|
108
|
+
linked.push({from, to, type});
|
|
109
|
+
},
|
|
110
|
+
_linked: linked,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function makeNotifier() {
|
|
115
|
+
const logs = [];
|
|
116
|
+
return {
|
|
117
|
+
notify: async (key, msg) => {
|
|
118
|
+
logs.push(msg);
|
|
119
|
+
},
|
|
120
|
+
_logs: logs,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
test('create_ci_ticket - relatesTo 單一 key 呼叫一次 linkIssue', async () => {
|
|
125
|
+
const jira = makeFullJira();
|
|
126
|
+
const notifier = makeNotifier();
|
|
127
|
+
const result = await handleCreateCITicket(
|
|
128
|
+
{systemCode: 'IBK', relatesTo: ['CID-1708']},
|
|
129
|
+
{jira, notifier},
|
|
130
|
+
);
|
|
131
|
+
const data = parseOk(result);
|
|
132
|
+
assert.equal(data.issueKey, 'CID-TEST');
|
|
133
|
+
assert.equal(jira._linked.length, 1);
|
|
134
|
+
assert.equal(jira._linked[0].to, 'CID-1708');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('create_ci_ticket - relatesTo 多個 key 各呼叫一次 linkIssue', async () => {
|
|
138
|
+
const jira = makeFullJira();
|
|
139
|
+
const notifier = makeNotifier();
|
|
140
|
+
await handleCreateCITicket(
|
|
141
|
+
{systemCode: 'IBK', relatesTo: ['CID-1178', 'CID-1182']},
|
|
142
|
+
{jira, notifier},
|
|
143
|
+
);
|
|
144
|
+
assert.equal(jira._linked.length, 2);
|
|
145
|
+
assert.equal(jira._linked[0].to, 'CID-1178');
|
|
146
|
+
assert.equal(jira._linked[1].to, 'CID-1182');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test('create_ci_ticket - 無 relatesTo 不呼叫 linkIssue', async () => {
|
|
150
|
+
const jira = makeFullJira();
|
|
151
|
+
const notifier = makeNotifier();
|
|
152
|
+
await handleCreateCITicket({systemCode: 'IBK'}, {jira, notifier});
|
|
153
|
+
assert.equal(jira._linked.length, 0);
|
|
154
|
+
});
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gray Release 相關 tools
|
|
3
|
+
* - create_grayrelease_ticket
|
|
4
|
+
*/
|
|
5
|
+
import {
|
|
6
|
+
SYSTEM_CODES,
|
|
7
|
+
ENV_CODES,
|
|
8
|
+
SUPPORTED_ENVS,
|
|
9
|
+
DEPT_CODES,
|
|
10
|
+
GRAY_RELEASE_MODULE_IDS,
|
|
11
|
+
ISSUE_TYPE_IDS,
|
|
12
|
+
JIRA_PROJECT_ID,
|
|
13
|
+
GRAY_RELEASE_FIELD_IDS,
|
|
14
|
+
SYSTEM_TO_DEPT_MAP,
|
|
15
|
+
FIELD_OPTIONS,
|
|
16
|
+
SIGN_VALUES,
|
|
17
|
+
NOTES_TEMPLATES,
|
|
18
|
+
JIRA_DEFAULTS,
|
|
19
|
+
} from '../constants/index.js';
|
|
20
|
+
import {error, ok, today, getServerList} from './helpers.js';
|
|
21
|
+
|
|
22
|
+
// ── Schema definitions ───────────────────────────────────────────
|
|
23
|
+
export function getGrayReleaseToolDefinitions() {
|
|
24
|
+
return [
|
|
25
|
+
{
|
|
26
|
+
name: 'create_grayrelease_ticket',
|
|
27
|
+
description:
|
|
28
|
+
'建立 GrayRelease 灰度上版單。適用於單一 module 開發與測試佈署,不支援 prd/dr 環境。',
|
|
29
|
+
inputSchema: {
|
|
30
|
+
type: 'object',
|
|
31
|
+
required: ['systemCode', 'module', 'gitBranch'],
|
|
32
|
+
properties: {
|
|
33
|
+
systemCode: {
|
|
34
|
+
type: 'string',
|
|
35
|
+
enum: Object.values(SYSTEM_CODES),
|
|
36
|
+
description: '系統代碼',
|
|
37
|
+
},
|
|
38
|
+
module: {
|
|
39
|
+
type: 'string',
|
|
40
|
+
description: '模組名稱,例如 ibk、ssr、wealth、a11y、cwa、web、web_cust、web_agnt',
|
|
41
|
+
},
|
|
42
|
+
gitBranch: {
|
|
43
|
+
type: 'string',
|
|
44
|
+
description:
|
|
45
|
+
'Git branch 名稱,例如 feature/my-feature。/ 會自動替換為 - 作為 grayReleaseVersion',
|
|
46
|
+
},
|
|
47
|
+
environment: {
|
|
48
|
+
type: 'string',
|
|
49
|
+
enum: SUPPORTED_ENVS.grayRelease,
|
|
50
|
+
description: '部署環境(只允許 dev / stg / uat,不支援 prd/dr)',
|
|
51
|
+
},
|
|
52
|
+
jenkinsBranch: {
|
|
53
|
+
type: 'string',
|
|
54
|
+
description: 'Jenkins branch(選填,預設 master)',
|
|
55
|
+
},
|
|
56
|
+
dryRun: {
|
|
57
|
+
type: 'boolean',
|
|
58
|
+
description: '(選填) 預覽模式,不實際建立 Jira 單,回傳會送出的 payload',
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: 'link_stg_grayrelease',
|
|
65
|
+
description:
|
|
66
|
+
'將 STG 灰度單關聯(relates to)到已建立的 UAT 灰度單。' +
|
|
67
|
+
'通常在 create_grayrelease_ticket 建立 UAT 單後,由使用者確認 suggest_link 的建議時呼叫。',
|
|
68
|
+
inputSchema: {
|
|
69
|
+
type: 'object',
|
|
70
|
+
required: ['uatKey', 'stgKeys'],
|
|
71
|
+
properties: {
|
|
72
|
+
uatKey: {
|
|
73
|
+
type: 'string',
|
|
74
|
+
description: 'UAT 灰度單的 issue key,例如 CID-9999',
|
|
75
|
+
},
|
|
76
|
+
stgKeys: {
|
|
77
|
+
type: 'array',
|
|
78
|
+
items: {type: 'string'},
|
|
79
|
+
description: '要關聯的 STG 灰度單 key 陣列,例如 ["CID-1234", "CID-1235"]',
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── Handler ──────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 搜尋相同 module/gitBranch 的 STG 灰度單,並排除已被任何 UAT 灰度單 relates 的。
|
|
91
|
+
* @param {string} gitBranch
|
|
92
|
+
* @param {string} moduleId - GRAY_RELEASE_MODULE_IDS 的值(Jira field option id)
|
|
93
|
+
* @param {object} jira
|
|
94
|
+
* @returns {Promise<{key:string, summary:string}[]>}
|
|
95
|
+
*/
|
|
96
|
+
async function findUnlinkedStgGrayReleases(gitBranch, moduleId, jira) {
|
|
97
|
+
const STG_ENV_ID = '14356';
|
|
98
|
+
const GR_ISSUE_TYPE = '12601';
|
|
99
|
+
const jql = [
|
|
100
|
+
'project = CID',
|
|
101
|
+
`issuetype = ${GR_ISSUE_TYPE}`,
|
|
102
|
+
`cf[13436] = ${STG_ENV_ID}`,
|
|
103
|
+
`cf[13431] = "${gitBranch.replace(/"/g, '\\"')}"`,
|
|
104
|
+
`cf[13444] = ${moduleId}`,
|
|
105
|
+
'ORDER BY created DESC',
|
|
106
|
+
].join(' AND ');
|
|
107
|
+
|
|
108
|
+
let candidates;
|
|
109
|
+
try {
|
|
110
|
+
candidates = await jira.searchIssues(jql, ['summary', 'issuelinks'], 20);
|
|
111
|
+
} catch {
|
|
112
|
+
return []; // 搜尋失敗時靜默,不影響主流程
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return candidates
|
|
116
|
+
.filter((issue) => {
|
|
117
|
+
const links = issue.fields?.issuelinks ?? [];
|
|
118
|
+
// 排除已有任何 GrayRelease UAT 關聯的票
|
|
119
|
+
return !links.some((link) => {
|
|
120
|
+
const linked = link.inwardIssue ?? link.outwardIssue;
|
|
121
|
+
return linked?.fields?.issuetype?.id === GR_ISSUE_TYPE;
|
|
122
|
+
});
|
|
123
|
+
})
|
|
124
|
+
.map((issue) => ({key: issue.key, summary: issue.fields?.summary ?? ''}));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* 建立 Gray Release 上版單
|
|
129
|
+
*/
|
|
130
|
+
export async function handleCreateGrayReleaseTicket(args, {jira, notifier}) {
|
|
131
|
+
try {
|
|
132
|
+
const normalizedArgs = {
|
|
133
|
+
...args,
|
|
134
|
+
module: args.module ?? args.systemCode?.toLowerCase(),
|
|
135
|
+
gitBranch: args.gitBranch ?? args.grayVersion,
|
|
136
|
+
environment: args.environment?.toLowerCase(),
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// 未指定環境時,詢問使用者是否使用 STG
|
|
140
|
+
if (!normalizedArgs.environment) {
|
|
141
|
+
return ok({
|
|
142
|
+
confirm_environment: true,
|
|
143
|
+
message: '請問要部署到 STG 環境嗎?(STG / UAT / DEV)',
|
|
144
|
+
hint: '若要使用 STG,請回覆「是」或直接告訴我環境名稱',
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const moduleId = GRAY_RELEASE_MODULE_IDS[normalizedArgs.module];
|
|
149
|
+
if (!moduleId) {
|
|
150
|
+
if (args.module || args.gitBranch) {
|
|
151
|
+
return error(
|
|
152
|
+
`找不到模組 "${normalizedArgs.module}" 的 GrayRelease ID,請確認模組名稱(支援: ${Object.keys(GRAY_RELEASE_MODULE_IDS).join(', ')})`,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const envCode = normalizedArgs.environment;
|
|
158
|
+
if (!SUPPORTED_ENVS.grayRelease.includes(envCode)) {
|
|
159
|
+
return error(
|
|
160
|
+
`GrayRelease 不支援環境 "${envCode}",僅支援: ${SUPPORTED_ENVS.grayRelease.join(', ')}`,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const deptStr = SYSTEM_TO_DEPT_MAP[normalizedArgs.systemCode];
|
|
165
|
+
const dateStr = today().replace(/-/g, '');
|
|
166
|
+
const autoSummary = `[${envCode.toUpperCase()}][GrayRelease] ${normalizedArgs.systemCode}_${normalizedArgs.module.toUpperCase()}_${dateStr} 程式上版作業申請單`;
|
|
167
|
+
const grayReleaseVersion = normalizedArgs.gitBranch.replace(/\//g, '-');
|
|
168
|
+
const serverList = getServerList(normalizedArgs.systemCode, envCode, false, normalizedArgs.module) || [];
|
|
169
|
+
|
|
170
|
+
const fields = {
|
|
171
|
+
project: {key: JIRA_PROJECT_ID},
|
|
172
|
+
issuetype: {id: ISSUE_TYPE_IDS.GrayRelease},
|
|
173
|
+
summary: normalizedArgs.summary ?? autoSummary,
|
|
174
|
+
duedate: today(),
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
// systemCode
|
|
178
|
+
fields[GRAY_RELEASE_FIELD_IDS.systemCode] = {value: normalizedArgs.systemCode};
|
|
179
|
+
|
|
180
|
+
// sign fields
|
|
181
|
+
fields[GRAY_RELEASE_FIELD_IDS.deptManagerSign] = SIGN_VALUES.deptManagerSign;
|
|
182
|
+
fields[GRAY_RELEASE_FIELD_IDS.authManagerSign] = SIGN_VALUES.authManagerSign;
|
|
183
|
+
fields[GRAY_RELEASE_FIELD_IDS.releaseInfo] = SIGN_VALUES.releaseInfo;
|
|
184
|
+
|
|
185
|
+
// env
|
|
186
|
+
if (ENV_CODES[envCode]) {
|
|
187
|
+
fields[GRAY_RELEASE_FIELD_IDS.env] = {value: envCode};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// dept code
|
|
191
|
+
if (deptStr && DEPT_CODES[deptStr]) {
|
|
192
|
+
fields[GRAY_RELEASE_FIELD_IDS.deptCode] = {value: deptStr};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// system module
|
|
196
|
+
if (moduleId) {
|
|
197
|
+
fields[GRAY_RELEASE_FIELD_IDS.systemModule] = {id: moduleId};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// jenkins branch
|
|
201
|
+
fields[GRAY_RELEASE_FIELD_IDS.jenkinsBranch] =
|
|
202
|
+
normalizedArgs.jenkinsBranch || JIRA_DEFAULTS.jenkinsBranch;
|
|
203
|
+
|
|
204
|
+
// git branch
|
|
205
|
+
fields[GRAY_RELEASE_FIELD_IDS.gitBranch] = normalizedArgs.gitBranch;
|
|
206
|
+
|
|
207
|
+
// grayReleaseVersion (/ → -)
|
|
208
|
+
fields[GRAY_RELEASE_FIELD_IDS.grayReleaseVersion] = grayReleaseVersion;
|
|
209
|
+
|
|
210
|
+
// cluster deploy (always true)
|
|
211
|
+
fields[GRAY_RELEASE_FIELD_IDS.clusterDeploy] = {id: FIELD_OPTIONS.clusterDeploy.true};
|
|
212
|
+
|
|
213
|
+
// cluster list
|
|
214
|
+
fields[GRAY_RELEASE_FIELD_IDS.clusterList] = serverList.join('\n');
|
|
215
|
+
|
|
216
|
+
// gray release notes
|
|
217
|
+
fields[GRAY_RELEASE_FIELD_IDS.grayReleaseNotes] = NOTES_TEMPLATES.grayRelease;
|
|
218
|
+
|
|
219
|
+
if (normalizedArgs.dryRun) {
|
|
220
|
+
return ok({dryRun: true, summary: fields.summary, grayReleaseVersion, fields});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const issue = await jira.createIssue(fields);
|
|
224
|
+
await notifier.notify(
|
|
225
|
+
issue.key,
|
|
226
|
+
`Gray Release 單已建立。系統: ${normalizedArgs.systemCode}, 模組: ${normalizedArgs.module}, 環境: ${envCode}, 分支: ${normalizedArgs.gitBranch}`,
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
const result = {
|
|
230
|
+
issueKey: issue.key,
|
|
231
|
+
issueId: issue.id,
|
|
232
|
+
url: `${process.env.JIRA_BASE_URL}/browse/${issue.key}`,
|
|
233
|
+
type: 'GrayRelease',
|
|
234
|
+
system: normalizedArgs.systemCode,
|
|
235
|
+
module: normalizedArgs.module,
|
|
236
|
+
environment: envCode,
|
|
237
|
+
grayReleaseVersion,
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
// 開 UAT 單後,主動搜尋尚未關聯的同 branch STG 灰度單
|
|
241
|
+
if (envCode === 'uat') {
|
|
242
|
+
const stgTickets = await findUnlinkedStgGrayReleases(normalizedArgs.gitBranch, moduleId, jira);
|
|
243
|
+
if (stgTickets.length > 0) {
|
|
244
|
+
result.suggest_link = {
|
|
245
|
+
message: `找到以下 STG 灰度單(branch: ${args.gitBranch})尚未關聯到任何 UAT 灰度單,是否要建立 relates to 關聯到 ${issue.key}?`,
|
|
246
|
+
stgTickets,
|
|
247
|
+
hint: '回覆「是」或 yes 即可自動關聯,或直接呼叫 link_stg_grayrelease',
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return ok(result);
|
|
253
|
+
} catch (err) {
|
|
254
|
+
return error(`無法建立 GrayRelease 單: ${err.message}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* 將 STG 灰度單關聯(relates to)到 UAT 灰度單
|
|
260
|
+
*/
|
|
261
|
+
export async function handleLinkStgGrayRelease(args, {jira, notifier}) {
|
|
262
|
+
try {
|
|
263
|
+
const {uatKey, stgKeys} = args;
|
|
264
|
+
if (!Array.isArray(stgKeys) || stgKeys.length === 0) {
|
|
265
|
+
return error('stgKeys 不可為空');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const linked = [];
|
|
269
|
+
const failed = [];
|
|
270
|
+
|
|
271
|
+
for (const stgKey of stgKeys) {
|
|
272
|
+
try {
|
|
273
|
+
await jira.linkIssue(stgKey, uatKey, 'Relates');
|
|
274
|
+
linked.push(stgKey);
|
|
275
|
+
} catch (e) {
|
|
276
|
+
failed.push({key: stgKey, reason: e.message});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (linked.length > 0) {
|
|
281
|
+
await notifier.notify(uatKey, `已將 STG 灰度單 ${linked.join(', ')} relates to ${uatKey}`);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return ok({
|
|
285
|
+
uatKey,
|
|
286
|
+
linked,
|
|
287
|
+
failed,
|
|
288
|
+
message:
|
|
289
|
+
linked.length > 0
|
|
290
|
+
? `✅ 已關聯 ${linked.join(', ')} → ${uatKey}`
|
|
291
|
+
: '⚠️ 沒有成功關聯任何 STG 灰度單',
|
|
292
|
+
});
|
|
293
|
+
} catch (err) {
|
|
294
|
+
return error(`無法建立關聯: ${err.message}`);
|
|
295
|
+
}
|
|
296
|
+
}
|