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.
@@ -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
- execSync(buildCommand, { stdio: 'inherit' });
18
- console.log('✅ 构建完成');
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
- console.error(' 构建目录不存在!');
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
- execSync(`tar -czf ${localZipFile} -C dist .`, { stdio: 'inherit' });
42
- console.log('✅ 压缩完成');
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 {SSHClient} ssh - SSH 客户端实例
81
- * @param {string} localZipFile - 本地压缩包路径
82
- * @param {string} remoteZipFile - 远程压缩包路径
83
- * @param {boolean} skipBuild - 是否跳过构建
219
+ * @param {object} options - 选项
84
220
  */
85
- export async function uploadBuild(ssh, localZipFile, remoteZipFile, skipBuild) {
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
- await ssh.uploadFile(localZipFile, remoteZipFile);
89
- await ssh.execCommand(`ls -lh ${remoteZipFile}`);
90
- console.log('✅ 上传完成');
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
- await ssh.execCommand(`tar -xzf ${remoteZipFile} -C ${envConfig.deployDir}`);
121
- console.log('✅ 清理并解压完成');
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
- await ssh.execCommand(`rm -f ${remoteZipFile}`);
133
- if (!skipLocalCleanup) {
134
- fs.unlinkSync(localZipFile);
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
- await backupExistingDeployment({ ssh, envConfig, buildVersion, skipBuild });
166
- await uploadBuild(ssh, localZipFile, remoteZipFile, skipBuild);
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
- const ssh = new SSHClient(envConfig);
435
+ // 使用已连接的 ssh 或创建新连接
436
+ const ssh = existingSsh || new SSHClient(envConfig);
437
+ let needDisconnect = !existingSsh;
218
438
 
219
439
  try {
220
- await ssh.connect();
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 (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();
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 '${backupFile}' && echo 'FILE_YES' || echo 'FILE_NO'`;
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
- console.error(`❌ 备份文件不存在: ${backupFile}`);
253
- await ssh.disconnect();
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 ${backupFile} ${excludeArgs} -C ${envConfig.deployDir}`);
508
+ await ssh.execCommand(`tar -xzf ${finalBackupFile} ${excludeArgs} -C ${envConfig.deployDir}`);
279
509
  } else {
280
- await ssh.execCommand(`tar -xzf ${backupFile} -C ${envConfig.deployDir}`);
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
- await ssh.disconnect();
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(`备份文件: ${backupFile}`);
543
+ console.log(`备份文件: ${finalBackupFile}`);
308
544
  console.log('========================================');
545
+
546
+ logger.log('SUCCESS', '回滚完成', `备份文件: ${finalBackupFile}`);
309
547
  } catch (error) {
310
548
  console.error('回滚失败:', error);
311
- await ssh.disconnect();
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
- | 构建版本 | ${buildVersion} |
92
- | 发布分支 | ${branch} |
93
- | 发布模式 | ${deployMode === 'main' ? '主分支发布' : deployMode === 'test' ? 'Test环境发布' : '当前分支发布'} |
94
- | 服务器 | ${serverHost} |
95
- ${duration ? `| ${keyword}耗时 | ${duration} |` : ''}
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
- | 构建版本 | ${buildVersion || '未完成'} |
163
- | 发布分支 | ${branch} |
164
- | 服务器 | ${serverHost} |
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()