fe-build-cli 1.2.5 → 1.5.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 +248 -11
- package/package.json +1 -1
- package/src/cli.js +188 -20
- package/src/config-template.js +14 -1
- package/src/deploy-core.js +310 -64
- package/src/dingtalk.js +31 -17
- package/src/git-branch.js +191 -34
- package/src/index.js +13 -1
- package/src/logger.js +381 -0
- package/src/ssh-client.js +69 -0
package/src/deploy-core.js
CHANGED
|
@@ -1,33 +1,161 @@
|
|
|
1
1
|
import { execSync } from 'node:child_process';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
3
4
|
import process from 'node:process';
|
|
4
5
|
import SSHClient from './ssh-client.js';
|
|
6
|
+
import { DeployLogger, cleanLocalBackups } from './logger.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 获取服务器备份列表
|
|
10
|
+
* @param {SSHClient} ssh - SSH 客户端
|
|
11
|
+
* @param {object} envConfig - 环境配置
|
|
12
|
+
* @returns {Promise<Array>} 备份文件列表
|
|
13
|
+
*/
|
|
14
|
+
export async function getServerBackupList(ssh, envConfig) {
|
|
15
|
+
const listCommand = `ls -t ${envConfig.backupDir}/${envConfig.backupPrefix}*.tar.gz 2>/dev/null`;
|
|
16
|
+
try {
|
|
17
|
+
const result = await ssh.execCommand(listCommand);
|
|
18
|
+
const files = result.trim().split('\n').filter(f => f.trim());
|
|
19
|
+
|
|
20
|
+
// 解析文件名获取版本和时间信息
|
|
21
|
+
return files.map(file => {
|
|
22
|
+
const filename = path.basename(file);
|
|
23
|
+
// 提取版本号:backup-production-build-20260618-abc123.tar.gz
|
|
24
|
+
const match = filename.match(/^(.+)-build-(.+)\.tar\.gz$/);
|
|
25
|
+
if (match) {
|
|
26
|
+
return {
|
|
27
|
+
file,
|
|
28
|
+
filename,
|
|
29
|
+
prefix: match[1],
|
|
30
|
+
version: match[2],
|
|
31
|
+
isServer: true
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
file,
|
|
36
|
+
filename,
|
|
37
|
+
version: filename.replace(/\.tar\.gz$/, ''),
|
|
38
|
+
isServer: true
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
} catch (error) {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 获取本地备份列表
|
|
48
|
+
* @param {string} localBackupDir - 本地备份目录
|
|
49
|
+
* @param {string} backupPrefix - 备份文件前缀
|
|
50
|
+
* @returns {Array} 备份文件列表
|
|
51
|
+
*/
|
|
52
|
+
export function getLocalBackupList(localBackupDir, backupPrefix) {
|
|
53
|
+
if (!fs.existsSync(localBackupDir)) {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const files = fs.readdirSync(localBackupDir);
|
|
58
|
+
const backupFiles = files.filter(f =>
|
|
59
|
+
f.endsWith('.tar.gz') && f.startsWith(backupPrefix)
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
// 按修改时间排序(最新的在前)
|
|
63
|
+
backupFiles.sort((a, b) => {
|
|
64
|
+
const statA = fs.statSync(path.join(localBackupDir, a));
|
|
65
|
+
const statB = fs.statSync(path.join(localBackupDir, b));
|
|
66
|
+
return statB.mtimeMs - statA.mtimeMs;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
return backupFiles.map(filename => {
|
|
70
|
+
const filePath = path.join(localBackupDir, filename);
|
|
71
|
+
const stats = fs.statSync(filePath);
|
|
72
|
+
|
|
73
|
+
// 提取版本号
|
|
74
|
+
const match = filename.match(/^(.+)-build-(.+)\.tar\.gz$/);
|
|
75
|
+
if (match) {
|
|
76
|
+
return {
|
|
77
|
+
file: filePath,
|
|
78
|
+
filename,
|
|
79
|
+
prefix: match[1],
|
|
80
|
+
version: match[2],
|
|
81
|
+
mtime: stats.mtime,
|
|
82
|
+
size: stats.size,
|
|
83
|
+
isServer: false
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
file: filePath,
|
|
88
|
+
filename,
|
|
89
|
+
version: filename.replace(/\.tar\.gz$/, ''),
|
|
90
|
+
mtime: stats.mtime,
|
|
91
|
+
size: stats.size,
|
|
92
|
+
isServer: false
|
|
93
|
+
};
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 从本地备份执行回滚(上传到服务器后回滚)
|
|
99
|
+
* @param {object} options - 回滚选项
|
|
100
|
+
*/
|
|
101
|
+
export async function rollbackFromLocal(options) {
|
|
102
|
+
const { ssh, envConfig, localBackupFile, logger } = options;
|
|
103
|
+
|
|
104
|
+
console.log('\n[步骤] 上传本地备份到服务器...');
|
|
105
|
+
|
|
106
|
+
const remoteFile = `${envConfig.backupDir}/${path.basename(localBackupFile)}`;
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
await ssh.uploadFile(localBackupFile, remoteFile);
|
|
110
|
+
logger.logUpload(localBackupFile, remoteFile, fs.statSync(localBackupFile).size, 0, true);
|
|
111
|
+
console.log('✅ 本地备份已上传到服务器');
|
|
112
|
+
|
|
113
|
+
// 执行回滚
|
|
114
|
+
return remoteFile;
|
|
115
|
+
} catch (error) {
|
|
116
|
+
logger.logUpload(localBackupFile, remoteFile, 0, 0, false);
|
|
117
|
+
throw new Error(`上传本地备份失败: ${error.message}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
5
120
|
|
|
6
121
|
/**
|
|
7
122
|
* 构建项目
|
|
8
123
|
* @param {object} envConfig - 环境配置
|
|
9
124
|
* @param {string} buildVersion - 构建版本号
|
|
125
|
+
* @param {DeployLogger} logger - 日志记录器
|
|
10
126
|
*/
|
|
11
|
-
export function buildProject(envConfig, buildVersion) {
|
|
127
|
+
export function buildProject(envConfig, buildVersion, logger) {
|
|
12
128
|
console.log('\n[步骤 1/8] 构建项目...');
|
|
13
129
|
const buildMode = envConfig.buildMode || 'production';
|
|
14
130
|
const buildCommand = envConfig.buildCommand || (buildMode === 'production' ? 'yarn build-only' : 'yarn build-test');
|
|
15
131
|
console.log(`构建模式: ${buildMode} → ${buildCommand}`);
|
|
132
|
+
|
|
133
|
+
const startTime = Date.now();
|
|
16
134
|
process.env.VITE_APP_VERSION = buildVersion;
|
|
17
|
-
|
|
18
|
-
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
execSync(buildCommand, { stdio: 'inherit' });
|
|
138
|
+
const duration = Math.round((Date.now() - startTime) / 1000);
|
|
139
|
+
logger.logBuild(buildMode, buildVersion, true, duration);
|
|
140
|
+
console.log('✅ 构建完成');
|
|
141
|
+
} catch (error) {
|
|
142
|
+
logger.logBuild(buildMode, buildVersion, false, 0);
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
19
145
|
}
|
|
20
146
|
|
|
21
147
|
/**
|
|
22
148
|
* 验证构建输出
|
|
23
149
|
* @param {boolean} skipBuild - 是否跳过构建
|
|
150
|
+
* @param {DeployLogger} logger - 日志记录器
|
|
24
151
|
*/
|
|
25
|
-
export function verifyBuildOutput(skipBuild) {
|
|
152
|
+
export function verifyBuildOutput(skipBuild, logger) {
|
|
26
153
|
console.log(skipBuild ? '\n[步骤 1/7] 验证构建输出...' : '\n[步骤 2/8] 验证构建输出...');
|
|
27
154
|
if (!fs.existsSync('dist')) {
|
|
28
|
-
|
|
155
|
+
logger.log('ERROR', '验证构建', '构建目录不存在');
|
|
29
156
|
process.exit(1);
|
|
30
157
|
}
|
|
158
|
+
logger.log('SUCCESS', '验证构建', '构建目录验证成功');
|
|
31
159
|
console.log('✅ 验证完成');
|
|
32
160
|
}
|
|
33
161
|
|
|
@@ -35,11 +163,20 @@ export function verifyBuildOutput(skipBuild) {
|
|
|
35
163
|
* 压缩构建产物
|
|
36
164
|
* @param {string} localZipFile - 本地压缩包路径
|
|
37
165
|
* @param {boolean} skipBuild - 是否跳过构建
|
|
166
|
+
* @param {DeployLogger} logger - 日志记录器
|
|
38
167
|
*/
|
|
39
|
-
export function compressBuild(localZipFile, skipBuild) {
|
|
168
|
+
export function compressBuild(localZipFile, skipBuild, logger) {
|
|
40
169
|
console.log(skipBuild ? '\n[步骤 2/7] 压缩本地构建产物...' : '\n[步骤 3/8] 压缩本地构建产物...');
|
|
41
|
-
|
|
42
|
-
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
execSync(`tar -czf ${localZipFile} -C dist .`, { stdio: 'inherit' });
|
|
173
|
+
const stats = fs.statSync(localZipFile);
|
|
174
|
+
logger.logCompress(stats.size, true);
|
|
175
|
+
console.log('✅ 压缩完成');
|
|
176
|
+
} catch (error) {
|
|
177
|
+
logger.logCompress(0, false);
|
|
178
|
+
throw error;
|
|
179
|
+
}
|
|
43
180
|
}
|
|
44
181
|
|
|
45
182
|
/**
|
|
@@ -47,7 +184,7 @@ export function compressBuild(localZipFile, skipBuild) {
|
|
|
47
184
|
* @param {object} options - 选项
|
|
48
185
|
*/
|
|
49
186
|
export async function backupExistingDeployment(options) {
|
|
50
|
-
const { ssh, envConfig, buildVersion, skipBuild } = options;
|
|
187
|
+
const { ssh, envConfig, buildVersion, skipBuild, logger } = options;
|
|
51
188
|
const stepNum = skipBuild ? '3' : '4';
|
|
52
189
|
console.log(`\n[步骤 ${stepNum}/8] 备份现有部署...`);
|
|
53
190
|
const backupFile = `${envConfig.backupDir}/${envConfig.backupPrefix}-${buildVersion}.tar.gz`;
|
|
@@ -64,6 +201,7 @@ export async function backupExistingDeployment(options) {
|
|
|
64
201
|
const protectedDirs = envConfig.protectedDirs || [];
|
|
65
202
|
const excludeArgs = protectedDirs.map(d => `--exclude='./${d}'`).join(' ');
|
|
66
203
|
await ssh.execCommand(`tar -czf ${backupFile} ${excludeArgs} -C ${envConfig.deployDir} .`);
|
|
204
|
+
logger.logBackup(backupFile, true);
|
|
67
205
|
console.log('✅ 备份完成');
|
|
68
206
|
|
|
69
207
|
await ssh.execCommand(
|
|
@@ -71,23 +209,33 @@ export async function backupExistingDeployment(options) {
|
|
|
71
209
|
);
|
|
72
210
|
console.log('✅ 清理旧备份完成');
|
|
73
211
|
} else {
|
|
212
|
+
logger.log('INFO', '服务器备份', '部署目录为空或不存在,跳过备份');
|
|
74
213
|
console.log('部署目录为空或不存在,跳过备份');
|
|
75
214
|
}
|
|
76
215
|
}
|
|
77
216
|
|
|
78
217
|
/**
|
|
79
218
|
* 上传构建产物
|
|
80
|
-
* @param {
|
|
81
|
-
* @param {string} localZipFile - 本地压缩包路径
|
|
82
|
-
* @param {string} remoteZipFile - 远程压缩包路径
|
|
83
|
-
* @param {boolean} skipBuild - 是否跳过构建
|
|
219
|
+
* @param {object} options - 选项
|
|
84
220
|
*/
|
|
85
|
-
export async function uploadBuild(
|
|
221
|
+
export async function uploadBuild(options) {
|
|
222
|
+
const { ssh, localZipFile, remoteZipFile, skipBuild, logger } = options;
|
|
86
223
|
const stepNum = skipBuild ? '4' : '5';
|
|
87
224
|
console.log(`\n[步骤 ${stepNum}/8] 上传压缩包...`);
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
225
|
+
|
|
226
|
+
const startTime = Date.now();
|
|
227
|
+
const stats = fs.statSync(localZipFile);
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
await ssh.uploadFile(localZipFile, remoteZipFile);
|
|
231
|
+
const duration = Math.round((Date.now() - startTime) / 1000);
|
|
232
|
+
logger.logUpload(localZipFile, remoteZipFile, stats.size, duration, true);
|
|
233
|
+
await ssh.execCommand(`ls -lh ${remoteZipFile}`);
|
|
234
|
+
console.log('✅ 上传完成');
|
|
235
|
+
} catch (error) {
|
|
236
|
+
logger.logUpload(localZipFile, remoteZipFile, stats.size, 0, false);
|
|
237
|
+
throw error;
|
|
238
|
+
}
|
|
91
239
|
}
|
|
92
240
|
|
|
93
241
|
/**
|
|
@@ -95,7 +243,7 @@ export async function uploadBuild(ssh, localZipFile, remoteZipFile, skipBuild) {
|
|
|
95
243
|
* @param {object} options - 选项
|
|
96
244
|
*/
|
|
97
245
|
export async function deployAndExtract(options) {
|
|
98
|
-
const { ssh, envConfig, remoteZipFile, skipBuild } = options;
|
|
246
|
+
const { ssh, envConfig, remoteZipFile, skipBuild, logger } = options;
|
|
99
247
|
const stepNum = skipBuild ? '5' : '6';
|
|
100
248
|
console.log(`\n[步骤 ${stepNum}/8] 清理并解压新版本...`);
|
|
101
249
|
|
|
@@ -117,8 +265,14 @@ export async function deployAndExtract(options) {
|
|
|
117
265
|
}
|
|
118
266
|
|
|
119
267
|
// 解压新版本
|
|
120
|
-
|
|
121
|
-
|
|
268
|
+
try {
|
|
269
|
+
await ssh.execCommand(`tar -xzf ${remoteZipFile} -C ${envConfig.deployDir}`);
|
|
270
|
+
logger.logDeploy(envConfig.deployDir, true);
|
|
271
|
+
console.log('✅ 清理并解压完成');
|
|
272
|
+
} catch (error) {
|
|
273
|
+
logger.logDeploy(envConfig.deployDir, false);
|
|
274
|
+
throw error;
|
|
275
|
+
}
|
|
122
276
|
}
|
|
123
277
|
|
|
124
278
|
/**
|
|
@@ -126,14 +280,63 @@ export async function deployAndExtract(options) {
|
|
|
126
280
|
* @param {object} options - 选项
|
|
127
281
|
*/
|
|
128
282
|
export async function cleanupFiles(options) {
|
|
129
|
-
const { ssh, remoteZipFile, localZipFile, skipLocalCleanup, skipBuild } = options;
|
|
283
|
+
const { ssh, remoteZipFile, localZipFile, skipLocalCleanup, skipBuild, logger } = options;
|
|
130
284
|
const stepNum = skipBuild ? '6' : '7';
|
|
131
285
|
console.log(`\n[步骤 ${stepNum}/8] 删除压缩包...`);
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
286
|
+
|
|
287
|
+
try {
|
|
288
|
+
await ssh.execCommand(`rm -f ${remoteZipFile}`);
|
|
289
|
+
if (!skipLocalCleanup) {
|
|
290
|
+
fs.unlinkSync(localZipFile);
|
|
291
|
+
}
|
|
292
|
+
logger.log('SUCCESS', '清理临时文件', '压缩包已删除');
|
|
293
|
+
console.log('✅ 删除完成');
|
|
294
|
+
} catch (error) {
|
|
295
|
+
logger.log('WARN', '清理临时文件', '删除压缩包失败,但不影响部署');
|
|
296
|
+
console.warn('⚠️ 删除压缩包失败,但不影响部署');
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* 下载线上备份到本地
|
|
302
|
+
* @param {object} options - 选项
|
|
303
|
+
*/
|
|
304
|
+
export async function downloadBackup(options) {
|
|
305
|
+
const { ssh, envConfig, buildVersion, localBackupDir, logger } = options;
|
|
306
|
+
|
|
307
|
+
console.log('\n[步骤] 下载线上备份到本地...');
|
|
308
|
+
|
|
309
|
+
// 确保本地备份目录存在
|
|
310
|
+
if (!fs.existsSync(localBackupDir)) {
|
|
311
|
+
fs.mkdirSync(localBackupDir, { recursive: true });
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const remoteBackupFile = `${envConfig.backupDir}/${envConfig.backupPrefix}-${buildVersion}.tar.gz`;
|
|
315
|
+
const localBackupFile = path.join(localBackupDir, `${envConfig.backupPrefix}-${buildVersion}.tar.gz`);
|
|
316
|
+
|
|
317
|
+
// 检查远程备份文件是否存在
|
|
318
|
+
const checkCommand = `test -f '${remoteBackupFile}' && echo 'FILE_YES' || echo 'FILE_NO'`;
|
|
319
|
+
const exists = await ssh.execCommand(checkCommand);
|
|
320
|
+
|
|
321
|
+
if (!exists.includes('FILE_YES')) {
|
|
322
|
+
logger.log('WARN', '备份下载', '远程备份文件不存在');
|
|
323
|
+
console.log('⚠️ 远程备份文件不存在,跳过下载');
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
try {
|
|
328
|
+
await ssh.downloadFile(remoteBackupFile, localBackupFile);
|
|
329
|
+
logger.logBackup(localBackupFile, true, true);
|
|
330
|
+
|
|
331
|
+
// 清理本地旧备份(保留7天)
|
|
332
|
+
cleanLocalBackups(localBackupDir, 7);
|
|
333
|
+
|
|
334
|
+
return localBackupFile;
|
|
335
|
+
} catch (error) {
|
|
336
|
+
logger.logBackup(remoteBackupFile, false, true);
|
|
337
|
+
console.error('❌ 下载备份失败:', error.message);
|
|
338
|
+
return null;
|
|
135
339
|
}
|
|
136
|
-
console.log('✅ 删除完成');
|
|
137
340
|
}
|
|
138
341
|
|
|
139
342
|
/**
|
|
@@ -144,29 +347,33 @@ export async function cleanupFiles(options) {
|
|
|
144
347
|
* @param {string} options.buildVersion - 构建版本
|
|
145
348
|
* @param {boolean} options.skipBuild - 是否跳过构建
|
|
146
349
|
* @param {boolean} options.skipLocalCleanup - 是否跳过本地清理
|
|
350
|
+
* @param {DeployLogger} options.logger - 日志记录器
|
|
351
|
+
* @param {string} options.localBackupDir - 本地备份目录
|
|
147
352
|
*/
|
|
148
353
|
export async function deployToServer(options) {
|
|
149
|
-
const { environment, envConfig, buildVersion, skipBuild = false, skipLocalCleanup = false } = options;
|
|
354
|
+
const { environment, envConfig, buildVersion, skipBuild = false, skipLocalCleanup = false, logger, localBackupDir } = options;
|
|
150
355
|
|
|
151
356
|
const localZipFile = `dist-${buildVersion}.tar.gz`;
|
|
152
357
|
const remoteZipFile = `${envConfig.backupDir}/${localZipFile}`;
|
|
153
358
|
|
|
154
359
|
if (!skipBuild) {
|
|
155
|
-
buildProject(envConfig, buildVersion);
|
|
360
|
+
buildProject(envConfig, buildVersion, logger);
|
|
156
361
|
}
|
|
157
362
|
|
|
158
|
-
verifyBuildOutput(skipBuild);
|
|
159
|
-
compressBuild(localZipFile, skipBuild);
|
|
363
|
+
verifyBuildOutput(skipBuild, logger);
|
|
364
|
+
compressBuild(localZipFile, skipBuild, logger);
|
|
160
365
|
|
|
161
366
|
const ssh = new SSHClient(envConfig);
|
|
162
367
|
|
|
163
368
|
try {
|
|
164
369
|
await ssh.connect();
|
|
165
|
-
|
|
166
|
-
|
|
370
|
+
logger.logSSHConnect(envConfig.sshHost, true);
|
|
371
|
+
|
|
372
|
+
await backupExistingDeployment({ ssh, envConfig, buildVersion, skipBuild, logger });
|
|
373
|
+
await uploadBuild({ ssh, localZipFile, remoteZipFile, skipBuild, logger });
|
|
167
374
|
|
|
168
375
|
try {
|
|
169
|
-
await deployAndExtract({ ssh, envConfig, remoteZipFile, skipBuild });
|
|
376
|
+
await deployAndExtract({ ssh, envConfig, remoteZipFile, skipBuild, logger });
|
|
170
377
|
} catch (error) {
|
|
171
378
|
console.error('❌ 清理或解压失败!');
|
|
172
379
|
await ssh.disconnect();
|
|
@@ -174,11 +381,16 @@ export async function deployToServer(options) {
|
|
|
174
381
|
}
|
|
175
382
|
|
|
176
383
|
try {
|
|
177
|
-
await cleanupFiles({ ssh, remoteZipFile, localZipFile, skipLocalCleanup, skipBuild });
|
|
384
|
+
await cleanupFiles({ ssh, remoteZipFile, localZipFile, skipLocalCleanup, skipBuild, logger });
|
|
178
385
|
} catch (error) {
|
|
179
386
|
console.warn('⚠️ 删除压缩包失败,但不影响部署');
|
|
180
387
|
}
|
|
181
388
|
|
|
389
|
+
// 下载线上备份到本地
|
|
390
|
+
if (localBackupDir) {
|
|
391
|
+
await downloadBackup({ ssh, envConfig, buildVersion, localBackupDir, logger });
|
|
392
|
+
}
|
|
393
|
+
|
|
182
394
|
await ssh.disconnect();
|
|
183
395
|
|
|
184
396
|
if (!skipBuild) {
|
|
@@ -192,8 +404,11 @@ export async function deployToServer(options) {
|
|
|
192
404
|
console.log(`服务器: ${envConfig.sshHost}`);
|
|
193
405
|
console.log(`地址: ${envConfig.deployUrl}`);
|
|
194
406
|
console.log('========================================');
|
|
407
|
+
|
|
408
|
+
logger.log('SUCCESS', '部署完成', `环境: ${environment}, 版本: ${buildVersion}`);
|
|
195
409
|
} catch (error) {
|
|
196
410
|
console.error('部署失败:', error);
|
|
411
|
+
logger.log('ERROR', '部署失败', error.message);
|
|
197
412
|
await ssh.disconnect();
|
|
198
413
|
throw error;
|
|
199
414
|
}
|
|
@@ -205,54 +420,69 @@ export async function deployToServer(options) {
|
|
|
205
420
|
* @param {string} options.environment - 环境名称
|
|
206
421
|
* @param {object} options.envConfig - 环境配置
|
|
207
422
|
* @param {string} options.specifiedVersion - 指定版本(可选)
|
|
423
|
+
* @param {string} options.backupFile - 备份文件路径(可选,已选择)
|
|
424
|
+
* @param {SSHClient} options.ssh - SSH 客户端(可选,已连接)
|
|
425
|
+
* @param {DeployLogger} options.logger - 日志记录器
|
|
208
426
|
*/
|
|
209
427
|
export async function rollbackDeployment(options) {
|
|
210
|
-
const { environment, envConfig, specifiedVersion } = options;
|
|
428
|
+
const { environment, envConfig, specifiedVersion, backupFile, ssh: existingSsh, logger } = options;
|
|
211
429
|
|
|
212
430
|
console.log('========================================');
|
|
213
431
|
console.log(`开始回滚 ${environment} 环境`);
|
|
214
432
|
console.log(`服务器: ${envConfig.sshHost}`);
|
|
215
433
|
console.log('========================================');
|
|
216
434
|
|
|
217
|
-
|
|
435
|
+
// 使用已连接的 ssh 或创建新连接
|
|
436
|
+
const ssh = existingSsh || new SSHClient(envConfig);
|
|
437
|
+
let needDisconnect = !existingSsh;
|
|
218
438
|
|
|
219
439
|
try {
|
|
220
|
-
|
|
440
|
+
if (!existingSsh) {
|
|
441
|
+
await ssh.connect();
|
|
442
|
+
logger.logSSHConnect(envConfig.sshHost, true);
|
|
443
|
+
}
|
|
221
444
|
|
|
222
445
|
console.log('\n[步骤 1/4] 获取备份文件...');
|
|
223
446
|
|
|
224
|
-
let backupFile;
|
|
225
|
-
if (
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
447
|
+
let finalBackupFile = backupFile;
|
|
448
|
+
if (!finalBackupFile) {
|
|
449
|
+
if (specifiedVersion) {
|
|
450
|
+
finalBackupFile = `${envConfig.backupDir}/${envConfig.backupPrefix}-${specifiedVersion}.tar.gz`;
|
|
451
|
+
console.log(`使用指定版本: ${specifiedVersion}`);
|
|
452
|
+
} else {
|
|
453
|
+
const listCommand = `ls -t ${envConfig.backupDir}/${envConfig.backupPrefix}*.tar.gz 2>/dev/null | head -n 1`;
|
|
454
|
+
try {
|
|
455
|
+
const listResult = await ssh.execCommand(listCommand);
|
|
456
|
+
finalBackupFile = listResult.trim();
|
|
457
|
+
|
|
458
|
+
if (!finalBackupFile) {
|
|
459
|
+
logger.log('ERROR', '获取备份', '未找到备份文件');
|
|
460
|
+
console.error('❌ 未找到备份文件!');
|
|
461
|
+
if (needDisconnect) await ssh.disconnect();
|
|
462
|
+
process.exit(1);
|
|
463
|
+
}
|
|
464
|
+
console.log(`找到最新备份: ${finalBackupFile}`);
|
|
465
|
+
logger.log('SUCCESS', '获取备份', `找到最新备份: ${finalBackupFile}`);
|
|
466
|
+
} catch (error) {
|
|
467
|
+
logger.log('ERROR', '获取备份', '获取备份文件失败');
|
|
468
|
+
console.error('❌ 获取备份文件失败!');
|
|
469
|
+
if (needDisconnect) await ssh.disconnect();
|
|
237
470
|
process.exit(1);
|
|
238
471
|
}
|
|
239
|
-
console.log(`找到最新备份: ${backupFile}`);
|
|
240
|
-
} catch (error) {
|
|
241
|
-
console.error('❌ 获取备份文件失败!');
|
|
242
|
-
await ssh.disconnect();
|
|
243
|
-
process.exit(1);
|
|
244
472
|
}
|
|
245
473
|
}
|
|
246
474
|
|
|
247
475
|
console.log('\n[步骤 2/4] 验证备份文件...');
|
|
248
|
-
const checkCommand = `test -f '${
|
|
476
|
+
const checkCommand = `test -f '${finalBackupFile}' && echo 'FILE_YES' || echo 'FILE_NO'`;
|
|
249
477
|
const exists = await ssh.execCommand(checkCommand);
|
|
250
478
|
|
|
251
479
|
if (!exists.includes('FILE_YES')) {
|
|
252
|
-
|
|
253
|
-
|
|
480
|
+
logger.log('ERROR', '验证备份', `备份文件不存在: ${finalBackupFile}`);
|
|
481
|
+
console.error(`❌ 备份文件不存在: ${finalBackupFile}`);
|
|
482
|
+
if (needDisconnect) await ssh.disconnect();
|
|
254
483
|
process.exit(1);
|
|
255
484
|
}
|
|
485
|
+
logger.log('SUCCESS', '验证备份', '备份文件验证完成');
|
|
256
486
|
console.log('✅ 备份文件验证完成');
|
|
257
487
|
|
|
258
488
|
const protectedDirs = envConfig.protectedDirs || [];
|
|
@@ -275,14 +505,16 @@ export async function rollbackDeployment(options) {
|
|
|
275
505
|
|
|
276
506
|
if (protectedDirs.length > 0) {
|
|
277
507
|
const excludeArgs = protectedDirs.map(d => `--exclude='./${d}'`).join(' ');
|
|
278
|
-
await ssh.execCommand(`tar -xzf ${
|
|
508
|
+
await ssh.execCommand(`tar -xzf ${finalBackupFile} ${excludeArgs} -C ${envConfig.deployDir}`);
|
|
279
509
|
} else {
|
|
280
|
-
await ssh.execCommand(`tar -xzf ${
|
|
510
|
+
await ssh.execCommand(`tar -xzf ${finalBackupFile} -C ${envConfig.deployDir}`);
|
|
281
511
|
}
|
|
512
|
+
logger.logDeploy(envConfig.deployDir, true);
|
|
282
513
|
console.log('✅ 回滚成功');
|
|
283
514
|
} catch (error) {
|
|
515
|
+
logger.logDeploy(envConfig.deployDir, false);
|
|
284
516
|
console.error('❌ 回滚失败!');
|
|
285
|
-
await ssh.disconnect();
|
|
517
|
+
if (needDisconnect) await ssh.disconnect();
|
|
286
518
|
process.exit(1);
|
|
287
519
|
}
|
|
288
520
|
|
|
@@ -291,24 +523,37 @@ export async function rollbackDeployment(options) {
|
|
|
291
523
|
const verifyResult = await ssh.execCommand(`ls -la ${envConfig.deployDir} | head -n 20`);
|
|
292
524
|
console.log('=== 验证回滚后的文件 ===');
|
|
293
525
|
console.log(verifyResult);
|
|
526
|
+
logger.log('SUCCESS', '验证回滚', '验证完成');
|
|
294
527
|
console.log('✅ 验证完成');
|
|
295
528
|
} catch (error) {
|
|
529
|
+
logger.log('ERROR', '验证回滚', '验证失败');
|
|
296
530
|
console.error('❌ 验证失败!');
|
|
297
|
-
await ssh.disconnect();
|
|
531
|
+
if (needDisconnect) await ssh.disconnect();
|
|
298
532
|
process.exit(1);
|
|
299
533
|
}
|
|
300
534
|
|
|
301
|
-
|
|
535
|
+
if (needDisconnect) {
|
|
536
|
+
await ssh.disconnect();
|
|
537
|
+
}
|
|
302
538
|
|
|
303
539
|
console.log('\n========================================');
|
|
304
540
|
console.log('✅ 回滚成功完成!');
|
|
305
541
|
console.log(`环境: ${environment}`);
|
|
306
542
|
console.log(`服务器: ${envConfig.sshHost}`);
|
|
307
|
-
console.log(`备份文件: ${
|
|
543
|
+
console.log(`备份文件: ${finalBackupFile}`);
|
|
308
544
|
console.log('========================================');
|
|
545
|
+
|
|
546
|
+
logger.log('SUCCESS', '回滚完成', `备份文件: ${finalBackupFile}`);
|
|
309
547
|
} catch (error) {
|
|
310
548
|
console.error('回滚失败:', error);
|
|
311
|
-
|
|
549
|
+
logger.log('ERROR', '回滚失败', error.message);
|
|
550
|
+
if (needDisconnect) {
|
|
551
|
+
try {
|
|
552
|
+
await ssh.disconnect();
|
|
553
|
+
} catch (e) {
|
|
554
|
+
// 忽略
|
|
555
|
+
}
|
|
556
|
+
}
|
|
312
557
|
throw error;
|
|
313
558
|
}
|
|
314
559
|
}
|
|
@@ -321,6 +566,7 @@ export default {
|
|
|
321
566
|
uploadBuild,
|
|
322
567
|
deployAndExtract,
|
|
323
568
|
cleanupFiles,
|
|
569
|
+
downloadBackup,
|
|
324
570
|
deployToServer,
|
|
325
571
|
rollbackDeployment
|
|
326
572
|
};
|
package/src/dingtalk.js
CHANGED
|
@@ -43,6 +43,7 @@ export async function sendDingTalkMessage(webhookUrl, message) {
|
|
|
43
43
|
* @param {string} options.deployUrl - 部署后的访问地址
|
|
44
44
|
* @param {string} options.branch - 分支名称
|
|
45
45
|
* @param {string} options.deployMode - 发布模式
|
|
46
|
+
* @param {string} options.commitMessage - 提交信息(本次修改内容)
|
|
46
47
|
* @param {string} options.duration - 部署耗时(可选)
|
|
47
48
|
* @param {string} options.keyword - 安全关键词(可选)
|
|
48
49
|
*/
|
|
@@ -54,6 +55,7 @@ export async function sendDeploySuccessNotification(webhookUrl, options) {
|
|
|
54
55
|
deployUrl,
|
|
55
56
|
branch,
|
|
56
57
|
deployMode,
|
|
58
|
+
commitMessage,
|
|
57
59
|
duration,
|
|
58
60
|
keyword = '部署'
|
|
59
61
|
} = options;
|
|
@@ -71,6 +73,8 @@ export async function sendDeploySuccessNotification(webhookUrl, options) {
|
|
|
71
73
|
// 标题必须包含关键词,否则钉钉会拒绝
|
|
72
74
|
const title = `${keyword}成功 - ${environment}`;
|
|
73
75
|
|
|
76
|
+
const deployModeText = deployMode === 'main' ? '主分支发布' : deployMode === 'test' ? 'Test环境发布' : '当前分支发布';
|
|
77
|
+
|
|
74
78
|
const message = {
|
|
75
79
|
msgtype: 'markdown',
|
|
76
80
|
markdown: {
|
|
@@ -86,13 +90,17 @@ export async function sendDeploySuccessNotification(webhookUrl, options) {
|
|
|
86
90
|
|
|
87
91
|
### ${keyword}详情
|
|
88
92
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
93
|
+
构建版本: ${buildVersion}
|
|
94
|
+
发布分支: ${branch}
|
|
95
|
+
发布模式: ${deployModeText}
|
|
96
|
+
服务器: ${serverHost}
|
|
97
|
+
${duration ? `${keyword}耗时: ${duration}` : ''}
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
### 本次修改内容
|
|
102
|
+
|
|
103
|
+
${commitMessage || '无提交信息'}
|
|
96
104
|
|
|
97
105
|
---
|
|
98
106
|
|
|
@@ -116,6 +124,7 @@ ${duration ? `| ${keyword}耗时 | ${duration} |` : ''}
|
|
|
116
124
|
* @param {string} options.buildVersion - 构建版本
|
|
117
125
|
* @param {string} options.serverHost - 服务器地址
|
|
118
126
|
* @param {string} options.branch - 分支名称
|
|
127
|
+
* @param {string} options.commitMessage - 提交信息(本次修改内容)
|
|
119
128
|
* @param {string} options.error - 错误信息
|
|
120
129
|
* @param {string} options.keyword - 安全关键词(可选)
|
|
121
130
|
*/
|
|
@@ -125,6 +134,7 @@ export async function sendDeployFailureNotification(webhookUrl, options) {
|
|
|
125
134
|
buildVersion,
|
|
126
135
|
serverHost,
|
|
127
136
|
branch,
|
|
137
|
+
commitMessage,
|
|
128
138
|
error,
|
|
129
139
|
keyword = '部署'
|
|
130
140
|
} = options;
|
|
@@ -157,11 +167,15 @@ export async function sendDeployFailureNotification(webhookUrl, options) {
|
|
|
157
167
|
|
|
158
168
|
### 失败详情
|
|
159
169
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
170
|
+
构建版本: ${buildVersion || '未完成'}
|
|
171
|
+
发布分支: ${branch}
|
|
172
|
+
服务器: ${serverHost}
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
### 本次修改内容
|
|
177
|
+
|
|
178
|
+
${commitMessage || '无提交信息'}
|
|
165
179
|
|
|
166
180
|
---
|
|
167
181
|
|
|
@@ -226,16 +240,16 @@ export async function sendRollbackNotification(webhookUrl, options) {
|
|
|
226
240
|
|
|
227
241
|
### 回滚详情
|
|
228
242
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
| 服务器 | ${serverHost} |
|
|
232
|
-
| 备份文件 | ${backupFile} |
|
|
243
|
+
服务器: ${serverHost}
|
|
244
|
+
备份文件: ${backupFile}
|
|
233
245
|
|
|
234
246
|
---
|
|
235
247
|
|
|
236
248
|
${success ? `### 访问地址
|
|
237
249
|
|
|
238
|
-
[${deployUrl}](${deployUrl})` :
|
|
250
|
+
[${deployUrl}](${deployUrl})` : `### 错误信息
|
|
251
|
+
|
|
252
|
+
回滚失败,请检查备份文件是否存在或手动处理。`}
|
|
239
253
|
|
|
240
254
|
> ${success ? '回滚完成,请验证功能是否正常。' : '回滚失败,请手动处理。'}(${keyword}系统)
|
|
241
255
|
`.trim()
|