fe-build-cli 1.1.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/README.md +430 -0
- package/package.json +41 -0
- package/src/cli.js +492 -0
- package/src/config-template.js +83 -0
- package/src/deploy-core.js +326 -0
- package/src/dingtalk.js +238 -0
- package/src/git-branch.js +259 -0
- package/src/index.d.ts +330 -0
- package/src/index.js +47 -0
- package/src/ssh-client.js +153 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import process from 'node:process';
|
|
4
|
+
import SSHClient from './ssh-client.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 构建项目
|
|
8
|
+
* @param {object} envConfig - 环境配置
|
|
9
|
+
* @param {string} buildVersion - 构建版本号
|
|
10
|
+
*/
|
|
11
|
+
export function buildProject(envConfig, buildVersion) {
|
|
12
|
+
console.log('\n[步骤 1/8] 构建项目...');
|
|
13
|
+
const buildMode = envConfig.buildMode || 'production';
|
|
14
|
+
const buildCommand = envConfig.buildCommand || (buildMode === 'production' ? 'yarn build-only' : 'yarn build-test');
|
|
15
|
+
console.log(`构建模式: ${buildMode} → ${buildCommand}`);
|
|
16
|
+
process.env.VITE_APP_VERSION = buildVersion;
|
|
17
|
+
execSync(buildCommand, { stdio: 'inherit' });
|
|
18
|
+
console.log('✅ 构建完成');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 验证构建输出
|
|
23
|
+
* @param {boolean} skipBuild - 是否跳过构建
|
|
24
|
+
*/
|
|
25
|
+
export function verifyBuildOutput(skipBuild) {
|
|
26
|
+
console.log(skipBuild ? '\n[步骤 1/7] 验证构建输出...' : '\n[步骤 2/8] 验证构建输出...');
|
|
27
|
+
if (!fs.existsSync('dist')) {
|
|
28
|
+
console.error('❌ 构建目录不存在!');
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
console.log('✅ 验证完成');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 压缩构建产物
|
|
36
|
+
* @param {string} localZipFile - 本地压缩包路径
|
|
37
|
+
* @param {boolean} skipBuild - 是否跳过构建
|
|
38
|
+
*/
|
|
39
|
+
export function compressBuild(localZipFile, skipBuild) {
|
|
40
|
+
console.log(skipBuild ? '\n[步骤 2/7] 压缩本地构建产物...' : '\n[步骤 3/8] 压缩本地构建产物...');
|
|
41
|
+
execSync(`tar -czf ${localZipFile} -C dist .`, { stdio: 'inherit' });
|
|
42
|
+
console.log('✅ 压缩完成');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 备份现有部署
|
|
47
|
+
* @param {object} options - 选项
|
|
48
|
+
*/
|
|
49
|
+
export async function backupExistingDeployment(options) {
|
|
50
|
+
const { ssh, envConfig, buildVersion, skipBuild } = options;
|
|
51
|
+
const stepNum = skipBuild ? '3' : '4';
|
|
52
|
+
console.log(`\n[步骤 ${stepNum}/8] 备份现有部署...`);
|
|
53
|
+
const backupFile = `${envConfig.backupDir}/${envConfig.backupPrefix}-${buildVersion}.tar.gz`;
|
|
54
|
+
|
|
55
|
+
await ssh.execCommand(`mkdir -p ${envConfig.backupDir}`);
|
|
56
|
+
await ssh.execCommand(`ls -la ${envConfig.deployDir} || echo '部署目录可能不存在'`);
|
|
57
|
+
|
|
58
|
+
const checkDirCommand = `[ -d ${envConfig.deployDir} ] && [ "$(ls -A ${envConfig.deployDir} 2>/dev/null)" ] && echo 'has_files' || echo 'empty'`;
|
|
59
|
+
const checkResult = await ssh.execCommand(checkDirCommand);
|
|
60
|
+
|
|
61
|
+
if (checkResult.includes('has_files')) {
|
|
62
|
+
console.log('部署目录非空,开始备份...');
|
|
63
|
+
// 排除受保护目录,减小备份体积并避免权限问题
|
|
64
|
+
const protectedDirs = envConfig.protectedDirs || [];
|
|
65
|
+
const excludeArgs = protectedDirs.map(d => `--exclude='./${d}'`).join(' ');
|
|
66
|
+
await ssh.execCommand(`tar -czf ${backupFile} ${excludeArgs} -C ${envConfig.deployDir} .`);
|
|
67
|
+
console.log('✅ 备份完成');
|
|
68
|
+
|
|
69
|
+
await ssh.execCommand(
|
|
70
|
+
`ls -t ${envConfig.backupDir}/${envConfig.backupPrefix}*.tar.gz 2>/dev/null | tail -n +2 | xargs rm -f`
|
|
71
|
+
);
|
|
72
|
+
console.log('✅ 清理旧备份完成');
|
|
73
|
+
} else {
|
|
74
|
+
console.log('部署目录为空或不存在,跳过备份');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 上传构建产物
|
|
80
|
+
* @param {SSHClient} ssh - SSH 客户端实例
|
|
81
|
+
* @param {string} localZipFile - 本地压缩包路径
|
|
82
|
+
* @param {string} remoteZipFile - 远程压缩包路径
|
|
83
|
+
* @param {boolean} skipBuild - 是否跳过构建
|
|
84
|
+
*/
|
|
85
|
+
export async function uploadBuild(ssh, localZipFile, remoteZipFile, skipBuild) {
|
|
86
|
+
const stepNum = skipBuild ? '4' : '5';
|
|
87
|
+
console.log(`\n[步骤 ${stepNum}/8] 上传压缩包...`);
|
|
88
|
+
await ssh.uploadFile(localZipFile, remoteZipFile);
|
|
89
|
+
await ssh.execCommand(`ls -lh ${remoteZipFile}`);
|
|
90
|
+
console.log('✅ 上传完成');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 清理部署目录并解压新版本
|
|
95
|
+
* @param {object} options - 选项
|
|
96
|
+
*/
|
|
97
|
+
export async function deployAndExtract(options) {
|
|
98
|
+
const { ssh, envConfig, remoteZipFile, skipBuild } = options;
|
|
99
|
+
const stepNum = skipBuild ? '5' : '6';
|
|
100
|
+
console.log(`\n[步骤 ${stepNum}/8] 清理并解压新版本...`);
|
|
101
|
+
|
|
102
|
+
const protectedDirs = envConfig.protectedDirs || [];
|
|
103
|
+
|
|
104
|
+
// 确保部署目录存在
|
|
105
|
+
await ssh.execCommand(`mkdir -p ${envConfig.deployDir}`);
|
|
106
|
+
|
|
107
|
+
// 清空部署目录,但跳过受保护目录
|
|
108
|
+
if (protectedDirs.length > 0) {
|
|
109
|
+
console.log(`🔒 保护目录: ${protectedDirs.join(', ')}`);
|
|
110
|
+
const excludeArgs = protectedDirs.map(d => `! -name '${d}'`).join(' ');
|
|
111
|
+
await ssh.execCommand(
|
|
112
|
+
`find ${envConfig.deployDir} -maxdepth 1 -mindepth 1 ${excludeArgs} -exec rm -rf {} +`
|
|
113
|
+
);
|
|
114
|
+
console.log('✅ 已清理非保护目录的文件');
|
|
115
|
+
} else {
|
|
116
|
+
await ssh.execCommand(`rm -rf ${envConfig.deployDir}/*`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 解压新版本
|
|
120
|
+
await ssh.execCommand(`tar -xzf ${remoteZipFile} -C ${envConfig.deployDir}`);
|
|
121
|
+
console.log('✅ 清理并解压完成');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 清理临时文件
|
|
126
|
+
* @param {object} options - 选项
|
|
127
|
+
*/
|
|
128
|
+
export async function cleanupFiles(options) {
|
|
129
|
+
const { ssh, remoteZipFile, localZipFile, skipLocalCleanup, skipBuild } = options;
|
|
130
|
+
const stepNum = skipBuild ? '6' : '7';
|
|
131
|
+
console.log(`\n[步骤 ${stepNum}/8] 删除压缩包...`);
|
|
132
|
+
await ssh.execCommand(`rm -f ${remoteZipFile}`);
|
|
133
|
+
if (!skipLocalCleanup) {
|
|
134
|
+
fs.unlinkSync(localZipFile);
|
|
135
|
+
}
|
|
136
|
+
console.log('✅ 删除完成');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* 执行部署到服务器
|
|
141
|
+
* @param {object} options - 部署选项
|
|
142
|
+
* @param {string} options.environment - 环境名称
|
|
143
|
+
* @param {object} options.envConfig - 环境配置
|
|
144
|
+
* @param {string} options.buildVersion - 构建版本
|
|
145
|
+
* @param {boolean} options.skipBuild - 是否跳过构建
|
|
146
|
+
* @param {boolean} options.skipLocalCleanup - 是否跳过本地清理
|
|
147
|
+
*/
|
|
148
|
+
export async function deployToServer(options) {
|
|
149
|
+
const { environment, envConfig, buildVersion, skipBuild = false, skipLocalCleanup = false } = options;
|
|
150
|
+
|
|
151
|
+
const localZipFile = `dist-${buildVersion}.tar.gz`;
|
|
152
|
+
const remoteZipFile = `${envConfig.backupDir}/${localZipFile}`;
|
|
153
|
+
|
|
154
|
+
if (!skipBuild) {
|
|
155
|
+
buildProject(envConfig, buildVersion);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
verifyBuildOutput(skipBuild);
|
|
159
|
+
compressBuild(localZipFile, skipBuild);
|
|
160
|
+
|
|
161
|
+
const ssh = new SSHClient(envConfig);
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
await ssh.connect();
|
|
165
|
+
await backupExistingDeployment({ ssh, envConfig, buildVersion, skipBuild });
|
|
166
|
+
await uploadBuild(ssh, localZipFile, remoteZipFile, skipBuild);
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
await deployAndExtract({ ssh, envConfig, remoteZipFile, skipBuild });
|
|
170
|
+
} catch (error) {
|
|
171
|
+
console.error('❌ 清理或解压失败!');
|
|
172
|
+
await ssh.disconnect();
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
await cleanupFiles({ ssh, remoteZipFile, localZipFile, skipLocalCleanup, skipBuild });
|
|
178
|
+
} catch (error) {
|
|
179
|
+
console.warn('⚠️ 删除压缩包失败,但不影响部署');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
await ssh.disconnect();
|
|
183
|
+
|
|
184
|
+
if (!skipBuild) {
|
|
185
|
+
fs.writeFileSync('deployed_version.txt', buildVersion);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
console.log('\n========================================');
|
|
189
|
+
console.log('✅ 部署成功完成!');
|
|
190
|
+
console.log(`环境: ${environment}`);
|
|
191
|
+
console.log(`版本: ${buildVersion}`);
|
|
192
|
+
console.log(`服务器: ${envConfig.sshHost}`);
|
|
193
|
+
console.log(`地址: ${envConfig.deployUrl}`);
|
|
194
|
+
console.log('========================================');
|
|
195
|
+
} catch (error) {
|
|
196
|
+
console.error('部署失败:', error);
|
|
197
|
+
await ssh.disconnect();
|
|
198
|
+
throw error;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* 执行回滚
|
|
204
|
+
* @param {object} options - 回滚选项
|
|
205
|
+
* @param {string} options.environment - 环境名称
|
|
206
|
+
* @param {object} options.envConfig - 环境配置
|
|
207
|
+
* @param {string} options.specifiedVersion - 指定版本(可选)
|
|
208
|
+
*/
|
|
209
|
+
export async function rollbackDeployment(options) {
|
|
210
|
+
const { environment, envConfig, specifiedVersion } = options;
|
|
211
|
+
|
|
212
|
+
console.log('========================================');
|
|
213
|
+
console.log(`开始回滚 ${environment} 环境`);
|
|
214
|
+
console.log(`服务器: ${envConfig.sshHost}`);
|
|
215
|
+
console.log('========================================');
|
|
216
|
+
|
|
217
|
+
const ssh = new SSHClient(envConfig);
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
await ssh.connect();
|
|
221
|
+
|
|
222
|
+
console.log('\n[步骤 1/4] 获取备份文件...');
|
|
223
|
+
|
|
224
|
+
let backupFile;
|
|
225
|
+
if (specifiedVersion) {
|
|
226
|
+
backupFile = `${envConfig.backupDir}/${envConfig.backupPrefix}-${specifiedVersion}.tar.gz`;
|
|
227
|
+
console.log(`使用指定版本: ${specifiedVersion}`);
|
|
228
|
+
} else {
|
|
229
|
+
const listCommand = `ls -t ${envConfig.backupDir}/${envConfig.backupPrefix}*.tar.gz 2>/dev/null | head -n 1`;
|
|
230
|
+
try {
|
|
231
|
+
const listResult = await ssh.execCommand(listCommand);
|
|
232
|
+
backupFile = listResult.trim();
|
|
233
|
+
|
|
234
|
+
if (!backupFile) {
|
|
235
|
+
console.error('❌ 未找到备份文件!');
|
|
236
|
+
await ssh.disconnect();
|
|
237
|
+
process.exit(1);
|
|
238
|
+
}
|
|
239
|
+
console.log(`找到最新备份: ${backupFile}`);
|
|
240
|
+
} catch (error) {
|
|
241
|
+
console.error('❌ 获取备份文件失败!');
|
|
242
|
+
await ssh.disconnect();
|
|
243
|
+
process.exit(1);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
console.log('\n[步骤 2/4] 验证备份文件...');
|
|
248
|
+
const checkCommand = `test -f '${backupFile}' && echo 'FILE_YES' || echo 'FILE_NO'`;
|
|
249
|
+
const exists = await ssh.execCommand(checkCommand);
|
|
250
|
+
|
|
251
|
+
if (!exists.includes('FILE_YES')) {
|
|
252
|
+
console.error(`❌ 备份文件不存在: ${backupFile}`);
|
|
253
|
+
await ssh.disconnect();
|
|
254
|
+
process.exit(1);
|
|
255
|
+
}
|
|
256
|
+
console.log('✅ 备份文件验证完成');
|
|
257
|
+
|
|
258
|
+
const protectedDirs = envConfig.protectedDirs || [];
|
|
259
|
+
|
|
260
|
+
console.log('\n[步骤 3/4] 执行回滚...');
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
await ssh.execCommand(`mkdir -p ${envConfig.deployDir}`);
|
|
264
|
+
|
|
265
|
+
if (protectedDirs.length > 0) {
|
|
266
|
+
console.log(`🔒 保护目录: ${protectedDirs.join(', ')}`);
|
|
267
|
+
const excludeArgs = protectedDirs.map(d => `! -name '${d}'`).join(' ');
|
|
268
|
+
await ssh.execCommand(
|
|
269
|
+
`find ${envConfig.deployDir} -maxdepth 1 -mindepth 1 ${excludeArgs} -exec rm -rf {} +`
|
|
270
|
+
);
|
|
271
|
+
console.log('✅ 已清理非保护目录的文件');
|
|
272
|
+
} else {
|
|
273
|
+
await ssh.execCommand(`rm -rf ${envConfig.deployDir}/*`);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (protectedDirs.length > 0) {
|
|
277
|
+
const excludeArgs = protectedDirs.map(d => `--exclude='./${d}'`).join(' ');
|
|
278
|
+
await ssh.execCommand(`tar -xzf ${backupFile} ${excludeArgs} -C ${envConfig.deployDir}`);
|
|
279
|
+
} else {
|
|
280
|
+
await ssh.execCommand(`tar -xzf ${backupFile} -C ${envConfig.deployDir}`);
|
|
281
|
+
}
|
|
282
|
+
console.log('✅ 回滚成功');
|
|
283
|
+
} catch (error) {
|
|
284
|
+
console.error('❌ 回滚失败!');
|
|
285
|
+
await ssh.disconnect();
|
|
286
|
+
process.exit(1);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
console.log('\n[步骤 4/4] 验证回滚...');
|
|
290
|
+
try {
|
|
291
|
+
const verifyResult = await ssh.execCommand(`ls -la ${envConfig.deployDir} | head -n 20`);
|
|
292
|
+
console.log('=== 验证回滚后的文件 ===');
|
|
293
|
+
console.log(verifyResult);
|
|
294
|
+
console.log('✅ 验证完成');
|
|
295
|
+
} catch (error) {
|
|
296
|
+
console.error('❌ 验证失败!');
|
|
297
|
+
await ssh.disconnect();
|
|
298
|
+
process.exit(1);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
await ssh.disconnect();
|
|
302
|
+
|
|
303
|
+
console.log('\n========================================');
|
|
304
|
+
console.log('✅ 回滚成功完成!');
|
|
305
|
+
console.log(`环境: ${environment}`);
|
|
306
|
+
console.log(`服务器: ${envConfig.sshHost}`);
|
|
307
|
+
console.log(`备份文件: ${backupFile}`);
|
|
308
|
+
console.log('========================================');
|
|
309
|
+
} catch (error) {
|
|
310
|
+
console.error('回滚失败:', error);
|
|
311
|
+
await ssh.disconnect();
|
|
312
|
+
throw error;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export default {
|
|
317
|
+
buildProject,
|
|
318
|
+
verifyBuildOutput,
|
|
319
|
+
compressBuild,
|
|
320
|
+
backupExistingDeployment,
|
|
321
|
+
uploadBuild,
|
|
322
|
+
deployAndExtract,
|
|
323
|
+
cleanupFiles,
|
|
324
|
+
deployToServer,
|
|
325
|
+
rollbackDeployment
|
|
326
|
+
};
|
package/src/dingtalk.js
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 钉钉机器人通知模块
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 发送钉钉消息
|
|
7
|
+
* @param {string} webhookUrl - 钉钉机器人 webhook URL
|
|
8
|
+
* @param {object} message - 消息内容
|
|
9
|
+
* @returns {Promise<object>} 发送结果
|
|
10
|
+
*/
|
|
11
|
+
export async function sendDingTalkMessage(webhookUrl, message) {
|
|
12
|
+
try {
|
|
13
|
+
const response = await fetch(webhookUrl, {
|
|
14
|
+
method: 'POST',
|
|
15
|
+
headers: {
|
|
16
|
+
'Content-Type': 'application/json'
|
|
17
|
+
},
|
|
18
|
+
body: JSON.stringify(message)
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const result = await response.json();
|
|
22
|
+
|
|
23
|
+
if (result.errcode !== 0) {
|
|
24
|
+
console.error('❌ 钉钉消息发送失败:', result.errmsg);
|
|
25
|
+
return { success: false, error: result.errmsg };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
console.log('✅ 钉钉消息发送成功');
|
|
29
|
+
return { success: true, data: result };
|
|
30
|
+
} catch (error) {
|
|
31
|
+
console.error('❌ 钉钉消息发送失败:', error.message);
|
|
32
|
+
return { success: false, error: error.message };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 发送部署成功通知(Markdown 格式)
|
|
38
|
+
* @param {string} webhookUrl - 钉钉机器人 webhook URL
|
|
39
|
+
* @param {object} options - 部署信息
|
|
40
|
+
* @param {string} options.environment - 环境名称
|
|
41
|
+
* @param {string} options.buildVersion - 构建版本
|
|
42
|
+
* @param {string} options.serverHost - 服务器地址
|
|
43
|
+
* @param {string} options.deployUrl - 部署后的访问地址
|
|
44
|
+
* @param {string} options.branch - 分支名称
|
|
45
|
+
* @param {string} options.deployMode - 发布模式
|
|
46
|
+
* @param {string} options.duration - 部署耗时(可选)
|
|
47
|
+
*/
|
|
48
|
+
export async function sendDeploySuccessNotification(webhookUrl, options) {
|
|
49
|
+
const {
|
|
50
|
+
environment,
|
|
51
|
+
buildVersion,
|
|
52
|
+
serverHost,
|
|
53
|
+
deployUrl,
|
|
54
|
+
branch,
|
|
55
|
+
deployMode,
|
|
56
|
+
duration
|
|
57
|
+
} = options;
|
|
58
|
+
|
|
59
|
+
const now = new Date();
|
|
60
|
+
const timeStr = now.toLocaleString('zh-CN', {
|
|
61
|
+
year: 'numeric',
|
|
62
|
+
month: '2-digit',
|
|
63
|
+
day: '2-digit',
|
|
64
|
+
hour: '2-digit',
|
|
65
|
+
minute: '2-digit',
|
|
66
|
+
second: '2-digit'
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const message = {
|
|
70
|
+
msgtype: 'markdown',
|
|
71
|
+
markdown: {
|
|
72
|
+
title: `部署成功 - ${environment}`,
|
|
73
|
+
text: `
|
|
74
|
+
## 🚀 部署成功通知
|
|
75
|
+
|
|
76
|
+
**环境**: ${environment}
|
|
77
|
+
**状态**: ✅ 成功
|
|
78
|
+
**时间**: ${timeStr}
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
### 部署详情
|
|
83
|
+
|
|
84
|
+
| 项目 | 内容 |
|
|
85
|
+
|:---:|:---|
|
|
86
|
+
| 构建版本 | ${buildVersion} |
|
|
87
|
+
| 发布分支 | ${branch} |
|
|
88
|
+
| 发布模式 | ${deployMode === 'main' ? '主分支发布' : '当前分支发布'} |
|
|
89
|
+
| 服务器 | ${serverHost} |
|
|
90
|
+
${duration ? `| 部署耗时 | ${duration} |` : ''}
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
### 访问地址
|
|
95
|
+
|
|
96
|
+
[${deployUrl}](${deployUrl})
|
|
97
|
+
|
|
98
|
+
> 部署完成,请及时验证功能是否正常。
|
|
99
|
+
`.trim()
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
return sendDingTalkMessage(webhookUrl, message);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 发送部署失败通知(Markdown 格式)
|
|
108
|
+
* @param {string} webhookUrl - 钉钉机器人 webhook URL
|
|
109
|
+
* @param {object} options - 部署信息
|
|
110
|
+
* @param {string} options.environment - 环境名称
|
|
111
|
+
* @param {string} options.buildVersion - 构建版本
|
|
112
|
+
* @param {string} options.serverHost - 服务器地址
|
|
113
|
+
* @param {string} options.branch - 分支名称
|
|
114
|
+
* @param {string} options.error - 错误信息
|
|
115
|
+
*/
|
|
116
|
+
export async function sendDeployFailureNotification(webhookUrl, options) {
|
|
117
|
+
const {
|
|
118
|
+
environment,
|
|
119
|
+
buildVersion,
|
|
120
|
+
serverHost,
|
|
121
|
+
branch,
|
|
122
|
+
error
|
|
123
|
+
} = options;
|
|
124
|
+
|
|
125
|
+
const now = new Date();
|
|
126
|
+
const timeStr = now.toLocaleString('zh-CN', {
|
|
127
|
+
year: 'numeric',
|
|
128
|
+
month: '2-digit',
|
|
129
|
+
day: '2-digit',
|
|
130
|
+
hour: '2-digit',
|
|
131
|
+
minute: '2-digit',
|
|
132
|
+
second: '2-digit'
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const message = {
|
|
136
|
+
msgtype: 'markdown',
|
|
137
|
+
markdown: {
|
|
138
|
+
title: `部署失败 - ${environment}`,
|
|
139
|
+
text: `
|
|
140
|
+
## ❌ 部署失败通知
|
|
141
|
+
|
|
142
|
+
**环境**: ${environment}
|
|
143
|
+
**状态**: ❌ 失败
|
|
144
|
+
**时间**: ${timeStr}
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
### 失败详情
|
|
149
|
+
|
|
150
|
+
| 项目 | 内容 |
|
|
151
|
+
|:---:|:---|
|
|
152
|
+
| 构建版本 | ${buildVersion || '未完成'} |
|
|
153
|
+
| 发布分支 | ${branch} |
|
|
154
|
+
| 服务器 | ${serverHost} |
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
### 错误信息
|
|
159
|
+
|
|
160
|
+
${error}
|
|
161
|
+
|
|
162
|
+
> 请及时排查问题并重新部署。
|
|
163
|
+
`.trim()
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
return sendDingTalkMessage(webhookUrl, message);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* 发送回滚通知(Markdown 格式)
|
|
172
|
+
* @param {string} webhookUrl - 钉钉机器人 webhook URL
|
|
173
|
+
* @param {object} options - 回滚信息
|
|
174
|
+
* @param {string} options.environment - 环境名称
|
|
175
|
+
* @param {string} options.backupFile - 备份文件
|
|
176
|
+
* @param {string} options.serverHost - 服务器地址
|
|
177
|
+
* @param {string} options.deployUrl - 部署后的访问地址
|
|
178
|
+
* @param {boolean} options.success - 是否成功
|
|
179
|
+
*/
|
|
180
|
+
export async function sendRollbackNotification(webhookUrl, options) {
|
|
181
|
+
const {
|
|
182
|
+
environment,
|
|
183
|
+
backupFile,
|
|
184
|
+
serverHost,
|
|
185
|
+
deployUrl,
|
|
186
|
+
success
|
|
187
|
+
} = options;
|
|
188
|
+
|
|
189
|
+
const now = new Date();
|
|
190
|
+
const timeStr = now.toLocaleString('zh-CN', {
|
|
191
|
+
year: 'numeric',
|
|
192
|
+
month: '2-digit',
|
|
193
|
+
day: '2-digit',
|
|
194
|
+
hour: '2-digit',
|
|
195
|
+
minute: '2-digit',
|
|
196
|
+
second: '2-digit'
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const message = {
|
|
200
|
+
msgtype: 'markdown',
|
|
201
|
+
markdown: {
|
|
202
|
+
title: `回滚${success ? '成功' : '失败'} - ${environment}`,
|
|
203
|
+
text: `
|
|
204
|
+
## ${success ? '🔄' : '❌'} 回滚${success ? '成功' : '失败'}通知
|
|
205
|
+
|
|
206
|
+
**环境**: ${environment}
|
|
207
|
+
**状态**: ${success ? '✅ 成功' : '❌ 失败'}
|
|
208
|
+
**时间**: ${timeStr}
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
### 回滚详情
|
|
213
|
+
|
|
214
|
+
| 项目 | 内容 |
|
|
215
|
+
|:---:|:---|
|
|
216
|
+
| 服务器 | ${serverHost} |
|
|
217
|
+
| 备份文件 | ${backupFile} |
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
${success ? `### 访问地址
|
|
222
|
+
|
|
223
|
+
[${deployUrl}](${deployUrl})` : ''}
|
|
224
|
+
|
|
225
|
+
> ${success ? '回滚完成,请验证功能是否正常。' : '回滚失败,请手动处理。'}
|
|
226
|
+
`.trim()
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
return sendDingTalkMessage(webhookUrl, message);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export default {
|
|
234
|
+
sendDingTalkMessage,
|
|
235
|
+
sendDeploySuccessNotification,
|
|
236
|
+
sendDeployFailureNotification,
|
|
237
|
+
sendRollbackNotification
|
|
238
|
+
};
|