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/src/cli.js CHANGED
@@ -5,10 +5,12 @@ import { execSync } from 'node:child_process';
5
5
  import fs from 'node:fs';
6
6
  import path from 'node:path';
7
7
  import { fileURLToPath, pathToFileURL } from 'node:url';
8
- import { deployToServer, rollbackDeployment } from './deploy-core.js';
8
+ import { deployToServer, rollbackDeployment, getServerBackupList, getLocalBackupList, rollbackFromLocal } from './deploy-core.js';
9
+ import SSHClient from './ssh-client.js';
9
10
  import {
10
11
  getCurrentBranch,
11
12
  getGitSha,
13
+ getGitCommitMessage,
12
14
  executeMainBranchFlow,
13
15
  executeCurrentBranchFlow,
14
16
  executeTestBranchFlow,
@@ -20,6 +22,7 @@ import {
20
22
  sendDeployFailureNotification,
21
23
  sendRollbackNotification
22
24
  } from './dingtalk.js';
25
+ import { DeployLogger } from './logger.js';
23
26
 
24
27
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
25
28
 
@@ -164,7 +167,7 @@ fe-build-cli - 前端项目打包部署工具
164
167
 
165
168
  命令:
166
169
  deploy [环境] 部署到指定环境(默认命令)
167
- rollback [环境] 回滚到上一版本
170
+ rollback [环境] 回滚到指定版本(交互选择备份来源)
168
171
  help 显示帮助信息
169
172
 
170
173
  选项:
@@ -176,6 +179,9 @@ fe-build-cli - 前端项目打包部署工具
176
179
  --no-merge test 发布时不合并,使用 stash 储藏本地改动
177
180
  --skip-build 跳过构建步骤
178
181
  --no-push 发布时不推送到远程
182
+ --server 回滚时使用服务器备份(默认)
183
+ --local 回滚时使用本地备份
184
+ --version <版本号> 回滚到指定版本
179
185
 
180
186
  示例:
181
187
  fe-build # 交互式选择环境部署
@@ -186,7 +192,9 @@ fe-build-cli - 前端项目打包部署工具
186
192
  fe-build --test-branch --no-merge # test 发布,stash 储藏改动
187
193
  fe-build --current-branch # 当前分支发布
188
194
  fe-build --main-branch # 主分支发布流程
189
- fe-build rollback production # 回滚生产环境
195
+ fe-build rollback production # 回滚生产环境(交互选择)
196
+ fe-build rollback production --server # 回滚生产环境(服务器备份)
197
+ fe-build rollback production --local # 回滚生产环境(本地备份)
190
198
 
191
199
  配置文件 (fe-build.config.js):
192
200
  export default {
@@ -295,6 +303,12 @@ async function deployCommand(config) {
295
303
  }
296
304
  }
297
305
 
306
+ // 创建日志记录器(在分支操作之前)
307
+ const logDir = config.logDir || 'logs';
308
+ const localBackupDir = config.localBackupDir || 'D:\\备份';
309
+ const logger = new DeployLogger({ logDir, localBackupDir });
310
+ logger.start();
311
+
298
312
  // 执行分支发布流程
299
313
  let branchResult = null;
300
314
  let originalBranch = getCurrentBranch(); // 记录原始分支
@@ -320,7 +334,8 @@ async function deployCommand(config) {
320
334
  testBranch: branches.test,
321
335
  mergeChanges,
322
336
  pushToRemote: !noPush,
323
- prompt
337
+ prompt,
338
+ logger // 传递 logger
324
339
  });
325
340
  originalBranch = branchResult.originalBranch;
326
341
  needRestore = branchResult.needRestore;
@@ -339,19 +354,21 @@ async function deployCommand(config) {
339
354
  const confirmAnswer = await prompt('确认执行主分支发布流程? (y/n): ');
340
355
  if (confirmAnswer.toLowerCase() !== 'y') {
341
356
  console.log('已取消发布');
357
+ logger.end('cancelled');
342
358
  process.exit(0);
343
359
  }
344
360
 
345
361
  branchResult = executeMainBranchFlow({
346
362
  testBranch: branches.test,
347
363
  mainBranch: branches.main,
348
- pushToRemote: !noPush
364
+ pushToRemote: !noPush,
365
+ logger // 传递 logger
349
366
  });
350
367
  originalBranch = branchResult.originalBranch;
351
368
  needRestore = true;
352
369
  } else if (deployMode === 'current') {
353
370
  // 当前分支发布模式
354
- branchResult = executeCurrentBranchFlow();
371
+ branchResult = executeCurrentBranchFlow(logger);
355
372
  originalBranch = branchResult.currentBranch;
356
373
  needRestore = false;
357
374
  console.log('📌 当前分支发布模式:不切换分支');
@@ -359,7 +376,10 @@ async function deployCommand(config) {
359
376
  } catch (branchError) {
360
377
  // 分支流程失败,发送钉钉通知
361
378
  console.error(`❌ 分支流程失败:`, branchError.message);
379
+ logger.log('ERROR', '分支流程失败', branchError.message);
380
+ logger.end('failed');
362
381
 
382
+ const commitMessage = getGitCommitMessage();
363
383
  if (config.dingtalk && config.dingtalk.enabled && config.dingtalk.webhook) {
364
384
  console.log('\n发送钉钉失败通知...');
365
385
  const envConfig = getServerConfig(config, selectedServers[0] || serverNames[0]);
@@ -368,13 +388,14 @@ async function deployCommand(config) {
368
388
  buildVersion: '未完成',
369
389
  serverHost: envConfig?.sshHost || '未知',
370
390
  branch: originalBranch,
391
+ commitMessage,
371
392
  error: `分支流程失败: ${branchError.message}`,
372
393
  keyword: config.dingtalk.keyword || '部署'
373
394
  });
374
395
  }
375
396
 
376
397
  // 切回原分支
377
- restoreBranch(originalBranch, hasStash);
398
+ restoreBranch(originalBranch, hasStash, logger);
378
399
  process.exit(1);
379
400
  }
380
401
 
@@ -404,18 +425,25 @@ async function deployCommand(config) {
404
425
  }
405
426
  console.log('========================================');
406
427
 
428
+ // 部署到服务器(使用前面创建的 logger)
407
429
  try {
408
430
  await deployToServer({
409
431
  environment: serverName,
410
432
  envConfig,
411
433
  buildVersion,
412
434
  skipBuild: skipBuild || !isFirst,
413
- skipLocalCleanup: i < selectedServers.length - 1
435
+ skipLocalCleanup: i < selectedServers.length - 1,
436
+ logger,
437
+ localBackupDir
414
438
  });
415
439
 
440
+ // 部署成功,结束日志记录
441
+ logger.end('success');
442
+
416
443
  // 部署成功,发送钉钉通知
417
444
  const duration = Math.round((Date.now() - startTime) / 1000);
418
445
  const currentBranch = getCurrentBranch();
446
+ const commitMessage = getGitCommitMessage();
419
447
 
420
448
  if (config.dingtalk && config.dingtalk.enabled && config.dingtalk.webhook) {
421
449
  console.log('\n发送钉钉通知...');
@@ -426,15 +454,21 @@ async function deployCommand(config) {
426
454
  deployUrl: envConfig.deployUrl,
427
455
  branch: currentBranch,
428
456
  deployMode,
457
+ commitMessage,
429
458
  duration: `${duration}秒`,
430
459
  keyword: config.dingtalk.keyword || '部署'
431
460
  });
461
+ logger.logDingTalk(true);
432
462
  }
433
463
  } catch (error) {
434
464
  console.error(`❌ 部署到 ${serverName} 失败:`, error.message);
435
465
 
466
+ // 部署失败,结束日志记录
467
+ logger.end('failed');
468
+
436
469
  // 部署失败,发送钉钉通知
437
470
  const currentBranch = getCurrentBranch();
471
+ const commitMessage = getGitCommitMessage();
438
472
  if (config.dingtalk && config.dingtalk.enabled && config.dingtalk.webhook) {
439
473
  console.log('\n发送钉钉失败通知...');
440
474
  await sendDeployFailureNotification(config.dingtalk.webhook, {
@@ -442,14 +476,16 @@ async function deployCommand(config) {
442
476
  buildVersion,
443
477
  serverHost: envConfig.sshHost,
444
478
  branch: currentBranch,
479
+ commitMessage,
445
480
  error: error.message,
446
481
  keyword: config.dingtalk.keyword || '部署'
447
482
  });
483
+ logger.logDingTalk(false, error.message);
448
484
  }
449
485
 
450
486
  // 出错时切回原分支
451
487
  if (needRestore && originalBranch) {
452
- restoreBranch(originalBranch, hasStash);
488
+ restoreBranch(originalBranch, hasStash, logger);
453
489
  }
454
490
  process.exit(1);
455
491
  }
@@ -460,13 +496,13 @@ async function deployCommand(config) {
460
496
  // 合并模式:自动切回原分支
461
497
  if (autoRestore) {
462
498
  console.log('\n📌 自动切回原分支...');
463
- restoreBranch(originalBranch, false);
499
+ restoreBranch(originalBranch, false, logger);
464
500
  console.log(`✅ 已切回 ${originalBranch},可继续开发`);
465
501
  } else {
466
502
  // stash 模式:询问是否切回
467
503
  const returnAnswer = await prompt('\n是否切回原分支? (y/n): ');
468
504
  if (returnAnswer.toLowerCase() === 'y') {
469
- restoreBranch(originalBranch, hasStash);
505
+ restoreBranch(originalBranch, hasStash, logger);
470
506
  } else if (hasStash) {
471
507
  console.log('\n💡 提示: 本地改动已储藏,执行以下命令恢复:');
472
508
  console.log(' git stash pop');
@@ -491,10 +527,12 @@ async function rollbackCommand(config) {
491
527
  const environment = args.find(arg => arg !== 'rollback' && !arg.startsWith('--'));
492
528
  const versionIndex = args.indexOf('--version');
493
529
  const specifiedVersion = versionIndex !== -1 ? args[versionIndex + 1] : undefined;
530
+ const useLocalBackup = args.includes('--local'); // 是否使用本地备份
531
+ const useServerBackup = args.includes('--server'); // 是否使用服务器备份
494
532
 
495
533
  if (!environment || !serverNames.includes(environment)) {
496
534
  console.error(`❌ 请指定服务器: ${serverNames.join(' 或 ')}`);
497
- console.error(`用法: fe-build rollback [${serverNames.join('|')}] [--version <版本号>]`);
535
+ console.error(`用法: fe-build rollback [${serverNames.join('|')}] [--server|--local] [--version <版本号>]`);
498
536
  process.exit(1);
499
537
  }
500
538
 
@@ -505,22 +543,134 @@ async function rollbackCommand(config) {
505
543
  process.exit(1);
506
544
  }
507
545
 
546
+ // 创建日志记录器
547
+ const logDir = config.logDir || 'logs';
548
+ const localBackupDir = config.localBackupDir || 'D:\\备份';
549
+ const logger = new DeployLogger({ logDir, localBackupDir });
550
+ logger.start();
551
+
552
+ console.log('========================================');
553
+ console.log(`开始回滚 ${environment} 环境`);
554
+ console.log(`服务器: ${envConfig.sshHost}`);
555
+ console.log('========================================');
556
+
557
+ // 连接服务器
558
+ const ssh = new SSHClient(envConfig);
559
+ await ssh.connect();
560
+ logger.logSSHConnect(envConfig.sshHost, true);
561
+
508
562
  let backupFile = '';
509
- let success = false;
563
+ let selectedBackup = null;
564
+ let backupSource = 'server'; // 默认服务器备份
510
565
 
511
566
  try {
512
- // 执行回滚,获取备份文件信息
513
- backupFile = specifiedVersion
514
- ? `${envConfig.backupDir}/${envConfig.backupPrefix}-${specifiedVersion}.tar.gz`
515
- : '';
567
+ // 如果指定了版本号,直接使用
568
+ if (specifiedVersion) {
569
+ backupFile = `${envConfig.backupDir}/${envConfig.backupPrefix}-${specifiedVersion}.tar.gz`;
570
+ console.log(`\n使用指定版本: ${specifiedVersion}`);
571
+ logger.log('INFO', '回滚版本', `指定版本: ${specifiedVersion}`);
572
+ } else {
573
+ // 获取备份列表
574
+ let serverBackups = [];
575
+ let localBackups = [];
576
+
577
+ // 获取服务器备份列表
578
+ console.log('\n[步骤 1] 获取服务器备份列表...');
579
+ serverBackups = await getServerBackupList(ssh, envConfig);
580
+ console.log(`找到 ${serverBackups.length} 个服务器备份`);
581
+
582
+ // 获取本地备份列表
583
+ console.log('\n[步骤 2] 获取本地备份列表...');
584
+ localBackups = getLocalBackupList(localBackupDir, envConfig.backupPrefix);
585
+ console.log(`找到 ${localBackups.length} 个本地备份`);
586
+
587
+ // 如果没有备份
588
+ if (serverBackups.length === 0 && localBackups.length === 0) {
589
+ logger.log('ERROR', '获取备份', '未找到任何备份文件');
590
+ console.error('❌ 未找到任何备份文件!');
591
+ await ssh.disconnect();
592
+ logger.end('failed');
593
+ process.exit(1);
594
+ }
595
+
596
+ // 确定备份来源
597
+ if (useLocalBackup && localBackups.length > 0) {
598
+ backupSource = 'local';
599
+ } else if (useServerBackup && serverBackups.length > 0) {
600
+ backupSource = 'server';
601
+ } else if (!useLocalBackup && !useServerBackup) {
602
+ // 交互选择备份来源(默认服务器)
603
+ console.log('\n========================================');
604
+ console.log(' 📦 选择备份来源');
605
+ console.log('========================================');
606
+ console.log(` 1. 服务器备份 (${serverBackups.length} 个) - 默认`);
607
+ if (localBackups.length > 0) {
608
+ console.log(` 2. 本地备份 (${localBackups.length} 个)`);
609
+ }
610
+ console.log('========================================');
611
+
612
+ const sourceAnswer = await prompt(`请选择备份来源 (1${localBackups.length > 0 ? '/2' : ''}): `);
613
+ if (sourceAnswer === '2' && localBackups.length > 0) {
614
+ backupSource = 'local';
615
+ } else {
616
+ backupSource = 'server';
617
+ }
618
+ }
516
619
 
620
+ // 显示备份列表供选择
621
+ const backups = backupSource === 'server' ? serverBackups : localBackups;
622
+
623
+ console.log(`\n========================================`);
624
+ console.log(` 📦 ${backupSource === 'server' ? '服务器' : '本地'}备份列表`);
625
+ console.log(`========================================`);
626
+
627
+ backups.forEach((backup, index) => {
628
+ const sizeStr = backup.size ? ` (${formatFileSize(backup.size)})` : '';
629
+ const timeStr = backup.mtime ? ` - ${backup.mtime.toLocaleDateString('zh-CN')}` : '';
630
+ console.log(` ${index + 1}. ${backup.version}${sizeStr}${timeStr}`);
631
+ });
632
+ console.log(`========================================`);
633
+
634
+ const backupAnswer = await prompt(`请选择要回滚的备份 (1-${backups.length}): `);
635
+ const selectedIndex = parseInt(backupAnswer, 10) - 1;
636
+
637
+ if (selectedIndex < 0 || selectedIndex >= backups.length) {
638
+ console.error('❌ 无效选择');
639
+ await ssh.disconnect();
640
+ logger.end('failed');
641
+ process.exit(1);
642
+ }
643
+
644
+ selectedBackup = backups[selectedIndex];
645
+ backupFile = selectedBackup.file;
646
+
647
+ console.log(`\n已选择: ${selectedBackup.version}`);
648
+ logger.log('INFO', '选择备份', `来源: ${backupSource}, 版本: ${selectedBackup.version}`);
649
+ }
650
+
651
+ // 如果是本地备份,需要先上传到服务器
652
+ if (backupSource === 'local' && selectedBackup) {
653
+ const remoteFile = await rollbackFromLocal({
654
+ ssh,
655
+ envConfig,
656
+ localBackupFile: backupFile,
657
+ logger
658
+ });
659
+ backupFile = remoteFile;
660
+ }
661
+
662
+ // 执行回滚
517
663
  await rollbackDeployment({
518
664
  environment,
519
665
  envConfig,
520
- specifiedVersion
666
+ specifiedVersion: specifiedVersion || (selectedBackup ? selectedBackup.version : undefined),
667
+ backupFile,
668
+ logger,
669
+ ssh // 传递已连接的 ssh
521
670
  });
522
671
 
523
- success = true;
672
+ await ssh.disconnect();
673
+ logger.end('success');
524
674
 
525
675
  // 回滚成功,发送钉钉通知
526
676
  if (config.dingtalk && config.dingtalk.enabled && config.dingtalk.webhook) {
@@ -533,10 +683,18 @@ async function rollbackCommand(config) {
533
683
  success: true,
534
684
  keyword: config.dingtalk.keyword || '部署'
535
685
  });
686
+ logger.logDingTalk(true);
536
687
  }
537
688
  } catch (error) {
538
689
  console.error('❌ 回滚失败:', error.message);
539
- success = false;
690
+ logger.log('ERROR', '回滚失败', error.message);
691
+ logger.end('failed');
692
+
693
+ try {
694
+ await ssh.disconnect();
695
+ } catch (e) {
696
+ // 忽略
697
+ }
540
698
 
541
699
  // 回滚失败,发送钉钉通知
542
700
  if (config.dingtalk && config.dingtalk.enabled && config.dingtalk.webhook) {
@@ -549,12 +707,22 @@ async function rollbackCommand(config) {
549
707
  success: false,
550
708
  keyword: config.dingtalk.keyword || '部署'
551
709
  });
710
+ logger.logDingTalk(false, error.message);
552
711
  }
553
712
 
554
713
  process.exit(1);
555
714
  }
556
715
  }
557
716
 
717
+ /**
718
+ * 格式化文件大小
719
+ */
720
+ function formatFileSize(bytes) {
721
+ if (bytes < 1024) return bytes + ' B';
722
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
723
+ return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
724
+ }
725
+
558
726
  /**
559
727
  * 主入口
560
728
  */
@@ -80,5 +80,18 @@ export default {
80
80
  webhook: 'https://oapi.dingtalk.com/robot/send?access_token=your-token', // 钉钉机器人 webhook URL
81
81
  enabled: true, // 是否启用钉钉通知,默认 true
82
82
  keyword: '部署' // 安全设置关键词(如果机器人设置了关键词,必须配置此项)
83
- }
83
+ },
84
+
85
+ /**
86
+ * 日志配置(可选)
87
+ * 部署日志存储目录,默认项目根目录下的 logs 目录
88
+ */
89
+ logDir: 'logs',
90
+
91
+ /**
92
+ * 本地备份目录(可选)
93
+ * 线上备份下载到本地的存储目录,默认 D:\备份
94
+ * 保留 7 天内的备份,自动清理旧备份
95
+ */
96
+ localBackupDir: 'D:\\备份'
84
97
  };