fe-build-cli 1.2.4 → 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 {
@@ -268,20 +276,23 @@ async function deployCommand(config) {
268
276
  }
269
277
  }
270
278
 
271
- // 确定发布模式(根据部署环境自动选择)
272
- let deployMode = config.deployMode || 'main'; // 默认主分支发布
279
+ // 确定发布模式
280
+ // 优先级:命令行参数 > 配置文件 deployMode > 自动识别
281
+ let deployMode = 'main'; // 默认值
273
282
 
274
- // 命令行参数优先
283
+ // 1. 命令行参数优先
275
284
  if (useTestBranch) {
276
285
  deployMode = 'test';
277
286
  } else if (useCurrentBranch) {
278
287
  deployMode = 'current';
279
288
  } else if (useMainBranch) {
280
289
  deployMode = 'main';
290
+ } else if (config.deployMode) {
291
+ // 2. 使用配置文件的 deployMode
292
+ deployMode = config.deployMode;
293
+ console.log(`\n📌 使用配置文件发布模式: ${deployMode}`);
281
294
  } else {
282
- // 自动根据部署环境选择发布模式
283
- // test 环境 → test 发布模式
284
- // production 环境 → main 发布模式
295
+ // 3. 自动根据部署环境选择发布模式(仅当配置文件未设置时)
285
296
  const targetEnv = selectedServers[0];
286
297
  if (targetEnv === 'test') {
287
298
  deployMode = 'test';
@@ -292,6 +303,12 @@ async function deployCommand(config) {
292
303
  }
293
304
  }
294
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
+
295
312
  // 执行分支发布流程
296
313
  let branchResult = null;
297
314
  let originalBranch = getCurrentBranch(); // 记录原始分支
@@ -317,7 +334,8 @@ async function deployCommand(config) {
317
334
  testBranch: branches.test,
318
335
  mergeChanges,
319
336
  pushToRemote: !noPush,
320
- prompt
337
+ prompt,
338
+ logger // 传递 logger
321
339
  });
322
340
  originalBranch = branchResult.originalBranch;
323
341
  needRestore = branchResult.needRestore;
@@ -336,19 +354,21 @@ async function deployCommand(config) {
336
354
  const confirmAnswer = await prompt('确认执行主分支发布流程? (y/n): ');
337
355
  if (confirmAnswer.toLowerCase() !== 'y') {
338
356
  console.log('已取消发布');
357
+ logger.end('cancelled');
339
358
  process.exit(0);
340
359
  }
341
360
 
342
361
  branchResult = executeMainBranchFlow({
343
362
  testBranch: branches.test,
344
363
  mainBranch: branches.main,
345
- pushToRemote: !noPush
364
+ pushToRemote: !noPush,
365
+ logger // 传递 logger
346
366
  });
347
367
  originalBranch = branchResult.originalBranch;
348
368
  needRestore = true;
349
369
  } else if (deployMode === 'current') {
350
370
  // 当前分支发布模式
351
- branchResult = executeCurrentBranchFlow();
371
+ branchResult = executeCurrentBranchFlow(logger);
352
372
  originalBranch = branchResult.currentBranch;
353
373
  needRestore = false;
354
374
  console.log('📌 当前分支发布模式:不切换分支');
@@ -356,7 +376,10 @@ async function deployCommand(config) {
356
376
  } catch (branchError) {
357
377
  // 分支流程失败,发送钉钉通知
358
378
  console.error(`❌ 分支流程失败:`, branchError.message);
379
+ logger.log('ERROR', '分支流程失败', branchError.message);
380
+ logger.end('failed');
359
381
 
382
+ const commitMessage = getGitCommitMessage();
360
383
  if (config.dingtalk && config.dingtalk.enabled && config.dingtalk.webhook) {
361
384
  console.log('\n发送钉钉失败通知...');
362
385
  const envConfig = getServerConfig(config, selectedServers[0] || serverNames[0]);
@@ -365,13 +388,14 @@ async function deployCommand(config) {
365
388
  buildVersion: '未完成',
366
389
  serverHost: envConfig?.sshHost || '未知',
367
390
  branch: originalBranch,
391
+ commitMessage,
368
392
  error: `分支流程失败: ${branchError.message}`,
369
393
  keyword: config.dingtalk.keyword || '部署'
370
394
  });
371
395
  }
372
396
 
373
397
  // 切回原分支
374
- restoreBranch(originalBranch, hasStash);
398
+ restoreBranch(originalBranch, hasStash, logger);
375
399
  process.exit(1);
376
400
  }
377
401
 
@@ -401,18 +425,25 @@ async function deployCommand(config) {
401
425
  }
402
426
  console.log('========================================');
403
427
 
428
+ // 部署到服务器(使用前面创建的 logger)
404
429
  try {
405
430
  await deployToServer({
406
431
  environment: serverName,
407
432
  envConfig,
408
433
  buildVersion,
409
434
  skipBuild: skipBuild || !isFirst,
410
- skipLocalCleanup: i < selectedServers.length - 1
435
+ skipLocalCleanup: i < selectedServers.length - 1,
436
+ logger,
437
+ localBackupDir
411
438
  });
412
439
 
440
+ // 部署成功,结束日志记录
441
+ logger.end('success');
442
+
413
443
  // 部署成功,发送钉钉通知
414
444
  const duration = Math.round((Date.now() - startTime) / 1000);
415
445
  const currentBranch = getCurrentBranch();
446
+ const commitMessage = getGitCommitMessage();
416
447
 
417
448
  if (config.dingtalk && config.dingtalk.enabled && config.dingtalk.webhook) {
418
449
  console.log('\n发送钉钉通知...');
@@ -423,15 +454,21 @@ async function deployCommand(config) {
423
454
  deployUrl: envConfig.deployUrl,
424
455
  branch: currentBranch,
425
456
  deployMode,
457
+ commitMessage,
426
458
  duration: `${duration}秒`,
427
459
  keyword: config.dingtalk.keyword || '部署'
428
460
  });
461
+ logger.logDingTalk(true);
429
462
  }
430
463
  } catch (error) {
431
464
  console.error(`❌ 部署到 ${serverName} 失败:`, error.message);
432
465
 
466
+ // 部署失败,结束日志记录
467
+ logger.end('failed');
468
+
433
469
  // 部署失败,发送钉钉通知
434
470
  const currentBranch = getCurrentBranch();
471
+ const commitMessage = getGitCommitMessage();
435
472
  if (config.dingtalk && config.dingtalk.enabled && config.dingtalk.webhook) {
436
473
  console.log('\n发送钉钉失败通知...');
437
474
  await sendDeployFailureNotification(config.dingtalk.webhook, {
@@ -439,14 +476,16 @@ async function deployCommand(config) {
439
476
  buildVersion,
440
477
  serverHost: envConfig.sshHost,
441
478
  branch: currentBranch,
479
+ commitMessage,
442
480
  error: error.message,
443
481
  keyword: config.dingtalk.keyword || '部署'
444
482
  });
483
+ logger.logDingTalk(false, error.message);
445
484
  }
446
485
 
447
486
  // 出错时切回原分支
448
487
  if (needRestore && originalBranch) {
449
- restoreBranch(originalBranch, hasStash);
488
+ restoreBranch(originalBranch, hasStash, logger);
450
489
  }
451
490
  process.exit(1);
452
491
  }
@@ -457,13 +496,13 @@ async function deployCommand(config) {
457
496
  // 合并模式:自动切回原分支
458
497
  if (autoRestore) {
459
498
  console.log('\n📌 自动切回原分支...');
460
- restoreBranch(originalBranch, false);
499
+ restoreBranch(originalBranch, false, logger);
461
500
  console.log(`✅ 已切回 ${originalBranch},可继续开发`);
462
501
  } else {
463
502
  // stash 模式:询问是否切回
464
503
  const returnAnswer = await prompt('\n是否切回原分支? (y/n): ');
465
504
  if (returnAnswer.toLowerCase() === 'y') {
466
- restoreBranch(originalBranch, hasStash);
505
+ restoreBranch(originalBranch, hasStash, logger);
467
506
  } else if (hasStash) {
468
507
  console.log('\n💡 提示: 本地改动已储藏,执行以下命令恢复:');
469
508
  console.log(' git stash pop');
@@ -488,10 +527,12 @@ async function rollbackCommand(config) {
488
527
  const environment = args.find(arg => arg !== 'rollback' && !arg.startsWith('--'));
489
528
  const versionIndex = args.indexOf('--version');
490
529
  const specifiedVersion = versionIndex !== -1 ? args[versionIndex + 1] : undefined;
530
+ const useLocalBackup = args.includes('--local'); // 是否使用本地备份
531
+ const useServerBackup = args.includes('--server'); // 是否使用服务器备份
491
532
 
492
533
  if (!environment || !serverNames.includes(environment)) {
493
534
  console.error(`❌ 请指定服务器: ${serverNames.join(' 或 ')}`);
494
- console.error(`用法: fe-build rollback [${serverNames.join('|')}] [--version <版本号>]`);
535
+ console.error(`用法: fe-build rollback [${serverNames.join('|')}] [--server|--local] [--version <版本号>]`);
495
536
  process.exit(1);
496
537
  }
497
538
 
@@ -502,22 +543,134 @@ async function rollbackCommand(config) {
502
543
  process.exit(1);
503
544
  }
504
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
+
505
562
  let backupFile = '';
506
- let success = false;
563
+ let selectedBackup = null;
564
+ let backupSource = 'server'; // 默认服务器备份
507
565
 
508
566
  try {
509
- // 执行回滚,获取备份文件信息
510
- backupFile = specifiedVersion
511
- ? `${envConfig.backupDir}/${envConfig.backupPrefix}-${specifiedVersion}.tar.gz`
512
- : '';
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
+ }
513
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
+ // 执行回滚
514
663
  await rollbackDeployment({
515
664
  environment,
516
665
  envConfig,
517
- specifiedVersion
666
+ specifiedVersion: specifiedVersion || (selectedBackup ? selectedBackup.version : undefined),
667
+ backupFile,
668
+ logger,
669
+ ssh // 传递已连接的 ssh
518
670
  });
519
671
 
520
- success = true;
672
+ await ssh.disconnect();
673
+ logger.end('success');
521
674
 
522
675
  // 回滚成功,发送钉钉通知
523
676
  if (config.dingtalk && config.dingtalk.enabled && config.dingtalk.webhook) {
@@ -530,10 +683,18 @@ async function rollbackCommand(config) {
530
683
  success: true,
531
684
  keyword: config.dingtalk.keyword || '部署'
532
685
  });
686
+ logger.logDingTalk(true);
533
687
  }
534
688
  } catch (error) {
535
689
  console.error('❌ 回滚失败:', error.message);
536
- success = false;
690
+ logger.log('ERROR', '回滚失败', error.message);
691
+ logger.end('failed');
692
+
693
+ try {
694
+ await ssh.disconnect();
695
+ } catch (e) {
696
+ // 忽略
697
+ }
537
698
 
538
699
  // 回滚失败,发送钉钉通知
539
700
  if (config.dingtalk && config.dingtalk.enabled && config.dingtalk.webhook) {
@@ -546,12 +707,22 @@ async function rollbackCommand(config) {
546
707
  success: false,
547
708
  keyword: config.dingtalk.keyword || '部署'
548
709
  });
710
+ logger.logDingTalk(false, error.message);
549
711
  }
550
712
 
551
713
  process.exit(1);
552
714
  }
553
715
  }
554
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
+
555
726
  /**
556
727
  * 主入口
557
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
  };