fe-build-cli 1.1.0 → 1.2.3

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "fe-build-cli",
3
- "version": "1.1.0",
4
- "description": "前端项目打包部署 CLI 工具,支持多服务器部署、分支管理、回滚、钉钉通知等功能",
3
+ "version": "1.2.3",
4
+ "description": "前端项目打包部署 CLI 工具,支持多服务器部署、分支管理、回滚、钉钉通知、智能处理本地改动等功能",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
7
7
  "bin": {
@@ -27,7 +27,8 @@
27
27
  "build",
28
28
  "ssh",
29
29
  "git",
30
- "dingtalk"
30
+ "dingtalk",
31
+ "stash"
31
32
  ],
32
33
  "author": "",
33
34
  "license": "MIT",
package/src/cli.js CHANGED
@@ -11,7 +11,9 @@ import {
11
11
  getGitSha,
12
12
  executeMainBranchFlow,
13
13
  executeCurrentBranchFlow,
14
- restoreBranch
14
+ executeTestBranchFlow,
15
+ restoreBranch,
16
+ stashPop
15
17
  } from './git-branch.js';
16
18
  import {
17
19
  sendDeploySuccessNotification,
@@ -169,13 +171,19 @@ fe-build-cli - 前端项目打包部署工具
169
171
  --config <路径> 指定配置文件路径
170
172
  --current-branch 使用当前分支发布(不切换分支)
171
173
  --main-branch 使用主分支发布流程(合并到测试分支再合并到主分支)
174
+ --test-branch 使用 test 环境发布流程(智能处理本地改动)
175
+ --merge test 发布时合并本地改动(提交+推送+合并)
176
+ --no-merge test 发布时不合并,使用 stash 储藏本地改动
172
177
  --skip-build 跳过构建步骤
173
- --no-push 主分支发布时不推送到远程
178
+ --no-push 发布时不推送到远程
174
179
 
175
180
  示例:
176
181
  fe-build # 交互式选择环境部署
177
182
  fe-build deploy production # 部署到生产环境
178
- fe-build deploy all # 部署到所有环境
183
+ fe-build deploy test # 部署到测试环境(使用配置的 deployMode)
184
+ fe-build --test-branch # test 环境发布(智能处理本地改动)
185
+ fe-build --test-branch --merge # test 发布,合并本地改动
186
+ fe-build --test-branch --no-merge # test 发布,stash 储藏改动
179
187
  fe-build --current-branch # 当前分支发布
180
188
  fe-build --main-branch # 主分支发布流程
181
189
  fe-build rollback production # 回滚生产环境
@@ -222,13 +230,18 @@ async function deployCommand(config) {
222
230
  const args = process.argv.slice(2);
223
231
  const useCurrentBranch = args.includes('--current-branch');
224
232
  const useMainBranch = args.includes('--main-branch');
233
+ const useTestBranch = args.includes('--test-branch');
234
+ const useMerge = args.includes('--merge');
235
+ const useNoMerge = args.includes('--no-merge');
225
236
  const skipBuild = args.includes('--skip-build');
226
237
  const noPush = args.includes('--no-push');
227
238
 
228
239
  // 确定发布模式
229
240
  let deployMode = config.deployMode || 'main'; // 默认主分支发布
230
241
 
231
- if (useCurrentBranch) {
242
+ if (useTestBranch) {
243
+ deployMode = 'test';
244
+ } else if (useCurrentBranch) {
232
245
  deployMode = 'current';
233
246
  } else if (useMainBranch) {
234
247
  deployMode = 'main';
@@ -268,35 +281,85 @@ async function deployCommand(config) {
268
281
 
269
282
  // 执行分支发布流程
270
283
  let branchResult = null;
271
- let originalBranch = null;
284
+ let originalBranch = getCurrentBranch(); // 记录原始分支
285
+ let needRestore = false;
286
+ let hasStash = false;
287
+ let autoRestore = false; // 是否自动切回(合并模式)
272
288
 
273
- if (deployMode === 'main' && !skipBuild) {
274
- // 主分支发布模式
275
- const branches = config.branches || { test: 'test', main: 'main' };
276
- console.log('\n========================================');
277
- console.log(' 🌿 主分支发布模式');
278
- console.log('========================================');
279
- console.log(`测试分支: ${branches.test}`);
280
- console.log(`主分支: ${branches.main}`);
281
- console.log('========================================');
289
+ // 分支流程(Git 操作)
290
+ try {
291
+ if (deployMode === 'test' && !skipBuild) {
292
+ // test 环境发布模式(智能处理本地改动)
293
+ const branches = config.branches || { test: 'test', main: 'main' };
294
+
295
+ // 确定合并选项
296
+ let mergeChanges = undefined;
297
+ if (useMerge) {
298
+ mergeChanges = true;
299
+ } else if (useNoMerge) {
300
+ mergeChanges = false;
301
+ }
282
302
 
283
- const confirmAnswer = await prompt('确认执行主分支发布流程? (y/n): ');
284
- if (confirmAnswer.toLowerCase() !== 'y') {
285
- console.log('已取消发布');
286
- process.exit(0);
303
+ branchResult = await executeTestBranchFlow({
304
+ testBranch: branches.test,
305
+ mergeChanges,
306
+ pushToRemote: !noPush,
307
+ prompt
308
+ });
309
+ originalBranch = branchResult.originalBranch;
310
+ needRestore = branchResult.needRestore;
311
+ hasStash = branchResult.hasStash;
312
+ autoRestore = branchResult.autoRestore || false;
313
+ } else if (deployMode === 'main' && !skipBuild) {
314
+ // 主分支发布模式
315
+ const branches = config.branches || { test: 'test', main: 'main' };
316
+ console.log('\n========================================');
317
+ console.log(' 🌿 主分支发布模式');
318
+ console.log('========================================');
319
+ console.log(`测试分支: ${branches.test}`);
320
+ console.log(`主分支: ${branches.main}`);
321
+ console.log('========================================');
322
+
323
+ const confirmAnswer = await prompt('确认执行主分支发布流程? (y/n): ');
324
+ if (confirmAnswer.toLowerCase() !== 'y') {
325
+ console.log('已取消发布');
326
+ process.exit(0);
327
+ }
328
+
329
+ branchResult = executeMainBranchFlow({
330
+ testBranch: branches.test,
331
+ mainBranch: branches.main,
332
+ pushToRemote: !noPush
333
+ });
334
+ originalBranch = branchResult.originalBranch;
335
+ needRestore = true;
336
+ } else if (deployMode === 'current') {
337
+ // 当前分支发布模式
338
+ branchResult = executeCurrentBranchFlow();
339
+ originalBranch = branchResult.currentBranch;
340
+ needRestore = false;
341
+ console.log('📌 当前分支发布模式:不切换分支');
287
342
  }
343
+ } catch (branchError) {
344
+ // 分支流程失败,发送钉钉通知
345
+ console.error(`❌ 分支流程失败:`, branchError.message);
288
346
 
289
- branchResult = executeMainBranchFlow({
290
- testBranch: branches.test,
291
- mainBranch: branches.main,
292
- pushToRemote: !noPush
293
- });
294
- originalBranch = branchResult.originalBranch;
295
- } else if (deployMode === 'current') {
296
- // 当前分支发布模式
297
- branchResult = executeCurrentBranchFlow();
298
- originalBranch = branchResult.currentBranch;
299
- console.log('📌 当前分支发布模式:不切换分支');
347
+ if (config.dingtalk && config.dingtalk.enabled && config.dingtalk.webhook) {
348
+ console.log('\n发送钉钉失败通知...');
349
+ const envConfig = getServerConfig(config, selectedServers[0] || serverNames[0]);
350
+ await sendDeployFailureNotification(config.dingtalk.webhook, {
351
+ environment: selectedServers[0] || serverNames[0],
352
+ buildVersion: '未完成',
353
+ serverHost: envConfig?.sshHost || '未知',
354
+ branch: originalBranch,
355
+ error: `分支流程失败: ${branchError.message}`,
356
+ keyword: config.dingtalk.keyword || '部署'
357
+ });
358
+ }
359
+
360
+ // 切回原分支
361
+ restoreBranch(originalBranch, hasStash);
362
+ process.exit(1);
300
363
  }
301
364
 
302
365
  // 生成构建版本
@@ -347,7 +410,8 @@ async function deployCommand(config) {
347
410
  deployUrl: envConfig.deployUrl,
348
411
  branch: currentBranch,
349
412
  deployMode,
350
- duration: `${duration}秒`
413
+ duration: `${duration}秒`,
414
+ keyword: config.dingtalk.keyword || '部署'
351
415
  });
352
416
  }
353
417
  } catch (error) {
@@ -362,23 +426,35 @@ async function deployCommand(config) {
362
426
  buildVersion,
363
427
  serverHost: envConfig.sshHost,
364
428
  branch: currentBranch,
365
- error: error.message
429
+ error: error.message,
430
+ keyword: config.dingtalk.keyword || '部署'
366
431
  });
367
432
  }
368
433
 
369
- // 如果是主分支发布模式,尝试切回原分支
370
- if (originalBranch) {
371
- restoreBranch(originalBranch);
434
+ // 出错时切回原分支
435
+ if (needRestore && originalBranch) {
436
+ restoreBranch(originalBranch, hasStash);
372
437
  }
373
438
  process.exit(1);
374
439
  }
375
440
  }
376
441
 
377
- // 主分支发布模式:部署完成后切回原分支
378
- if (deployMode === 'main' && originalBranch) {
379
- const returnAnswer = await prompt('\n是否切回原分支? (y/n): ');
380
- if (returnAnswer.toLowerCase() === 'y') {
381
- restoreBranch(originalBranch);
442
+ // 部署完成后切回原分支
443
+ if (needRestore && originalBranch) {
444
+ // 合并模式:自动切回原分支
445
+ if (autoRestore) {
446
+ console.log('\n📌 自动切回原分支...');
447
+ restoreBranch(originalBranch, false);
448
+ console.log(`✅ 已切回 ${originalBranch},可继续开发`);
449
+ } else {
450
+ // stash 模式:询问是否切回
451
+ const returnAnswer = await prompt('\n是否切回原分支? (y/n): ');
452
+ if (returnAnswer.toLowerCase() === 'y') {
453
+ restoreBranch(originalBranch, hasStash);
454
+ } else if (hasStash) {
455
+ console.log('\n💡 提示: 本地改动已储藏,执行以下命令恢复:');
456
+ console.log(' git stash pop');
457
+ }
382
458
  }
383
459
  }
384
460
  }
@@ -438,7 +514,8 @@ async function rollbackCommand(config) {
438
514
  backupFile: backupFile || '最新备份',
439
515
  serverHost: envConfig.sshHost,
440
516
  deployUrl: envConfig.deployUrl,
441
- success: true
517
+ success: true,
518
+ keyword: config.dingtalk.keyword || '部署'
442
519
  });
443
520
  }
444
521
  } catch (error) {
@@ -453,7 +530,8 @@ async function rollbackCommand(config) {
453
530
  backupFile: backupFile || '未知',
454
531
  serverHost: envConfig.sshHost,
455
532
  deployUrl: envConfig.deployUrl,
456
- success: false
533
+ success: false,
534
+ keyword: config.dingtalk.keyword || '部署'
457
535
  });
458
536
  }
459
537
 
@@ -78,6 +78,7 @@ export default {
78
78
  */
79
79
  dingtalk: {
80
80
  webhook: 'https://oapi.dingtalk.com/robot/send?access_token=your-token', // 钉钉机器人 webhook URL
81
- enabled: true // 是否启用钉钉通知,默认 true
81
+ enabled: true, // 是否启用钉钉通知,默认 true
82
+ keyword: '部署' // 安全设置关键词(如果机器人设置了关键词,必须配置此项)
82
83
  }
83
84
  };
package/src/dingtalk.js CHANGED
@@ -44,6 +44,7 @@ export async function sendDingTalkMessage(webhookUrl, message) {
44
44
  * @param {string} options.branch - 分支名称
45
45
  * @param {string} options.deployMode - 发布模式
46
46
  * @param {string} options.duration - 部署耗时(可选)
47
+ * @param {string} options.keyword - 安全关键词(可选)
47
48
  */
48
49
  export async function sendDeploySuccessNotification(webhookUrl, options) {
49
50
  const {
@@ -53,7 +54,8 @@ export async function sendDeploySuccessNotification(webhookUrl, options) {
53
54
  deployUrl,
54
55
  branch,
55
56
  deployMode,
56
- duration
57
+ duration,
58
+ keyword = '部署'
57
59
  } = options;
58
60
 
59
61
  const now = new Date();
@@ -66,12 +68,15 @@ export async function sendDeploySuccessNotification(webhookUrl, options) {
66
68
  second: '2-digit'
67
69
  });
68
70
 
71
+ // 标题必须包含关键词,否则钉钉会拒绝
72
+ const title = `${keyword}成功 - ${environment}`;
73
+
69
74
  const message = {
70
75
  msgtype: 'markdown',
71
76
  markdown: {
72
- title: `部署成功 - ${environment}`,
77
+ title,
73
78
  text: `
74
- ## 🚀 部署成功通知
79
+ ## 🚀 ${keyword}成功通知
75
80
 
76
81
  **环境**: ${environment}
77
82
  **状态**: ✅ 成功
@@ -79,15 +84,15 @@ export async function sendDeploySuccessNotification(webhookUrl, options) {
79
84
 
80
85
  ---
81
86
 
82
- ### 部署详情
87
+ ### ${keyword}详情
83
88
 
84
89
  | 项目 | 内容 |
85
90
  |:---:|:---|
86
91
  | 构建版本 | ${buildVersion} |
87
92
  | 发布分支 | ${branch} |
88
- | 发布模式 | ${deployMode === 'main' ? '主分支发布' : '当前分支发布'} |
93
+ | 发布模式 | ${deployMode === 'main' ? '主分支发布' : deployMode === 'test' ? 'Test环境发布' : '当前分支发布'} |
89
94
  | 服务器 | ${serverHost} |
90
- ${duration ? `| 部署耗时 | ${duration} |` : ''}
95
+ ${duration ? `| ${keyword}耗时 | ${duration} |` : ''}
91
96
 
92
97
  ---
93
98
 
@@ -95,7 +100,7 @@ ${duration ? `| 部署耗时 | ${duration} |` : ''}
95
100
 
96
101
  [${deployUrl}](${deployUrl})
97
102
 
98
- > 部署完成,请及时验证功能是否正常。
103
+ > ${keyword}完成,请及时验证功能是否正常。
99
104
  `.trim()
100
105
  }
101
106
  };
@@ -112,6 +117,7 @@ ${duration ? `| 部署耗时 | ${duration} |` : ''}
112
117
  * @param {string} options.serverHost - 服务器地址
113
118
  * @param {string} options.branch - 分支名称
114
119
  * @param {string} options.error - 错误信息
120
+ * @param {string} options.keyword - 安全关键词(可选)
115
121
  */
116
122
  export async function sendDeployFailureNotification(webhookUrl, options) {
117
123
  const {
@@ -119,7 +125,8 @@ export async function sendDeployFailureNotification(webhookUrl, options) {
119
125
  buildVersion,
120
126
  serverHost,
121
127
  branch,
122
- error
128
+ error,
129
+ keyword = '部署'
123
130
  } = options;
124
131
 
125
132
  const now = new Date();
@@ -132,12 +139,15 @@ export async function sendDeployFailureNotification(webhookUrl, options) {
132
139
  second: '2-digit'
133
140
  });
134
141
 
142
+ // 标题必须包含关键词
143
+ const title = `${keyword}失败 - ${environment}`;
144
+
135
145
  const message = {
136
146
  msgtype: 'markdown',
137
147
  markdown: {
138
- title: `部署失败 - ${environment}`,
148
+ title,
139
149
  text: `
140
- ## ❌ 部署失败通知
150
+ ## ❌ ${keyword}失败通知
141
151
 
142
152
  **环境**: ${environment}
143
153
  **状态**: ❌ 失败
@@ -159,7 +169,7 @@ export async function sendDeployFailureNotification(webhookUrl, options) {
159
169
 
160
170
  ${error}
161
171
 
162
- > 请及时排查问题并重新部署。
172
+ > 请及时排查问题并重新${keyword}。
163
173
  `.trim()
164
174
  }
165
175
  };
@@ -176,6 +186,7 @@ ${error}
176
186
  * @param {string} options.serverHost - 服务器地址
177
187
  * @param {string} options.deployUrl - 部署后的访问地址
178
188
  * @param {boolean} options.success - 是否成功
189
+ * @param {string} options.keyword - 安全关键词(可选)
179
190
  */
180
191
  export async function sendRollbackNotification(webhookUrl, options) {
181
192
  const {
@@ -183,7 +194,8 @@ export async function sendRollbackNotification(webhookUrl, options) {
183
194
  backupFile,
184
195
  serverHost,
185
196
  deployUrl,
186
- success
197
+ success,
198
+ keyword = '部署'
187
199
  } = options;
188
200
 
189
201
  const now = new Date();
@@ -196,10 +208,13 @@ export async function sendRollbackNotification(webhookUrl, options) {
196
208
  second: '2-digit'
197
209
  });
198
210
 
211
+ // 标题必须包含关键词
212
+ const title = `回滚${success ? '成功' : '失败'} - ${environment}`;
213
+
199
214
  const message = {
200
215
  msgtype: 'markdown',
201
216
  markdown: {
202
- title: `回滚${success ? '成功' : '失败'} - ${environment}`,
217
+ title,
203
218
  text: `
204
219
  ## ${success ? '🔄' : '❌'} 回滚${success ? '成功' : '失败'}通知
205
220
 
@@ -222,7 +237,7 @@ ${success ? `### 访问地址
222
237
 
223
238
  [${deployUrl}](${deployUrl})` : ''}
224
239
 
225
- > ${success ? '回滚完成,请验证功能是否正常。' : '回滚失败,请手动处理。'}
240
+ > ${success ? '回滚完成,请验证功能是否正常。' : '回滚失败,请手动处理。'}(${keyword}系统)
226
241
  `.trim()
227
242
  }
228
243
  };
package/src/git-branch.js CHANGED
@@ -127,6 +127,63 @@ export function pushBranch(branchName) {
127
127
  }
128
128
  }
129
129
 
130
+ /**
131
+ * 储藏本地改动(stash)
132
+ * @param {string} branchName - 当前分支名(用于备注)
133
+ * @returns {boolean} 是否成功
134
+ */
135
+ export function stashChanges(branchName = '') {
136
+ try {
137
+ console.log('\n📦 储藏本地改动...');
138
+ const stashMessage = branchName
139
+ ? `stash for test deploy from ${branchName}`
140
+ : 'fe-build-cli-auto-stash';
141
+ execSync(`git stash push -m "${stashMessage}"`, { stdio: 'inherit' });
142
+ console.log('✅ 本地改动已储藏');
143
+ return true;
144
+ } catch (error) {
145
+ console.error('❌ 储藏失败:', error.message);
146
+ return false;
147
+ }
148
+ }
149
+
150
+ /**
151
+ * 恢复储藏的改动(stash pop)
152
+ * @returns {boolean} 是否成功
153
+ */
154
+ export function stashPop() {
155
+ try {
156
+ console.log('\n📦 恢复储藏的改动...');
157
+ execSync('git stash pop', { stdio: 'inherit' });
158
+ console.log('✅ 本地改动已恢复');
159
+ return true;
160
+ } catch (error) {
161
+ console.error('❌ 恢复失败:', error.message);
162
+ console.log('💡 提示: 可能存在冲突,请手动执行 git stash pop 解决');
163
+ return false;
164
+ }
165
+ }
166
+
167
+ /**
168
+ * 自动提交当前分支的所有改动
169
+ * @param {string} message - 提交信息
170
+ * @returns {boolean} 是否成功
171
+ */
172
+ export function autoCommit(message) {
173
+ try {
174
+ console.log('\n📝 自动提交改动...');
175
+ // 添加所有改动
176
+ execSync('git add -A', { stdio: 'inherit' });
177
+ // 提交
178
+ execSync(`git commit -m "${message}"`, { stdio: 'inherit' });
179
+ console.log('✅ 改动已提交');
180
+ return true;
181
+ } catch (error) {
182
+ console.error('❌ 提交失败:', error.message);
183
+ return false;
184
+ }
185
+ }
186
+
130
187
  /**
131
188
  * 执行主分支发布流程
132
189
  * 流程:当前分支 -> 测试分支 -> 主分支
@@ -232,16 +289,208 @@ export function executeCurrentBranchFlow() {
232
289
  };
233
290
  }
234
291
 
292
+ /**
293
+ * 执行 test 环境发布流程(智能模式)
294
+ * @param {object} config - 配置
295
+ * @param {string} config.testBranch - test 分支名
296
+ * @param {boolean} config.mergeChanges - 是否合并本地改动
297
+ * @param {boolean} config.pushToRemote - 是否推送到远程
298
+ * @param {function} config.prompt - 交互提示函数
299
+ * @returns {object} 执行结果
300
+ */
301
+ export async function executeTestBranchFlow(config) {
302
+ const { testBranch, mergeChanges, pushToRemote = true, prompt } = config;
303
+ const originalBranch = getCurrentBranch();
304
+ const { clean, changes } = checkUncommittedChanges();
305
+
306
+ console.log('\n========================================');
307
+ console.log(' 🌿 Test 环境发布流程');
308
+ console.log('========================================');
309
+ console.log(`当前分支: ${originalBranch}`);
310
+ console.log(`Test 分支: ${testBranch}`);
311
+ console.log(`工作区状态: ${clean ? '干净' : '有未提交改动'}`);
312
+ console.log('========================================');
313
+
314
+ // 情况1:当前分支本身就是 test
315
+ if (originalBranch === testBranch) {
316
+ console.log('\n✓ 当前已在 test 分支');
317
+ pullBranch(testBranch);
318
+ return {
319
+ success: true,
320
+ originalBranch,
321
+ currentBranch: testBranch,
322
+ needRestore: false,
323
+ hasStash: false
324
+ };
325
+ }
326
+
327
+ // 情况2:当前分支 ≠ test,无本地改动
328
+ if (clean) {
329
+ console.log('\n✓ 无本地改动,直接切换发布');
330
+ checkoutBranch(testBranch);
331
+ pullBranch(testBranch);
332
+ return {
333
+ success: true,
334
+ originalBranch,
335
+ currentBranch: testBranch,
336
+ needRestore: true,
337
+ hasStash: false,
338
+ autoRestore: true // 无改动模式自动切回
339
+ };
340
+ }
341
+
342
+ // 情况3:当前分支 ≠ test,有本地改动
343
+ console.log('\n⚠️ 存在未提交的改动:');
344
+ changes.forEach(change => console.log(` ${change}`));
345
+
346
+ // 如果已确定合并选项,直接执行
347
+ if (mergeChanges === true) {
348
+ return await executeTestMergeFlow(originalBranch, testBranch, pushToRemote);
349
+ } else if (mergeChanges === false) {
350
+ return await executeTestStashFlow(originalBranch, testBranch);
351
+ }
352
+
353
+ // 否则交互询问
354
+ if (prompt) {
355
+ console.log('\n请选择处理方式:');
356
+ console.log(' 1. 合并改动到 test 发布(提交当前分支,合并到 test)');
357
+ console.log(' 2. 不合并,暂存改动(stash 储藏,用纯净 test 发布)');
358
+
359
+ const answer = await prompt('请输入选项 (1/2): ');
360
+
361
+ if (answer === '1') {
362
+ return await executeTestMergeFlow(originalBranch, testBranch, pushToRemote);
363
+ } else if (answer === '2') {
364
+ return await executeTestStashFlow(originalBranch, testBranch);
365
+ } else {
366
+ throw new Error('无效选项,已取消发布');
367
+ }
368
+ }
369
+
370
+ throw new Error('存在未提交改动,请选择处理方式');
371
+ }
372
+
373
+ /**
374
+ * 执行合并流程(提交当前分支,合并到 test)
375
+ */
376
+ async function executeTestMergeFlow(originalBranch, testBranch, pushToRemote) {
377
+ console.log('\n========================================');
378
+ console.log(' 🔀 合并改动发布模式');
379
+ console.log('========================================');
380
+
381
+ try {
382
+ // 1. 自动提交当前分支改动(备注包含原分支名)
383
+ const commitMessage = `auto commit: deploy test from ${originalBranch}`;
384
+ if (!autoCommit(commitMessage)) {
385
+ throw new Error('自动提交失败');
386
+ }
387
+
388
+ // 2. 推送当前分支到远程
389
+ if (pushToRemote) {
390
+ pushBranch(originalBranch);
391
+ }
392
+
393
+ // 3. 切换到 test 分支
394
+ checkoutBranch(testBranch);
395
+ pullBranch(testBranch);
396
+
397
+ // 4. 合并当前分支到 test
398
+ if (!mergeBranch(originalBranch, testBranch)) {
399
+ // 合并冲突,切回原分支
400
+ checkoutBranch(originalBranch);
401
+ throw new Error(`合并冲突: ${originalBranch} 到 ${testBranch},请手动解决冲突后重新发布`);
402
+ }
403
+
404
+ // 5. 推送 test 分支
405
+ if (pushToRemote) {
406
+ pushBranch(testBranch);
407
+ }
408
+
409
+ console.log('\n========================================');
410
+ console.log('✅ 合并发布流程完成');
411
+ console.log(`当前分支: ${testBranch}`);
412
+ console.log('========================================');
413
+
414
+ return {
415
+ success: true,
416
+ originalBranch,
417
+ currentBranch: testBranch,
418
+ needRestore: true,
419
+ hasStash: false,
420
+ merged: true,
421
+ autoRestore: true // 合并模式自动切回
422
+ };
423
+ } catch (error) {
424
+ // 出错时切回原分支
425
+ try {
426
+ checkoutBranch(originalBranch);
427
+ } catch (e) {
428
+ // 忽略
429
+ }
430
+ throw error;
431
+ }
432
+ }
433
+
434
+ /**
435
+ * 执行暂存流程(stash 储藏,用纯净 test 发布)
436
+ */
437
+ async function executeTestStashFlow(originalBranch, testBranch) {
438
+ console.log('\n========================================');
439
+ console.log(' 📦 暂存改动发布模式');
440
+ console.log('========================================');
441
+
442
+ try {
443
+ // 1. 储藏本地改动(备注包含原分支名)
444
+ if (!stashChanges(originalBranch)) {
445
+ throw new Error('储藏改动失败');
446
+ }
447
+
448
+ // 2. 切换到 test 分支
449
+ checkoutBranch(testBranch);
450
+ pullBranch(testBranch);
451
+
452
+ console.log('\n========================================');
453
+ console.log('✅ 暂存发布流程完成');
454
+ console.log(`当前分支: ${testBranch}`);
455
+ console.log('========================================');
456
+
457
+ return {
458
+ success: true,
459
+ originalBranch,
460
+ currentBranch: testBranch,
461
+ needRestore: true,
462
+ hasStash: true,
463
+ autoRestore: false // stash 模式询问是否切回
464
+ };
465
+ } catch (error) {
466
+ // 出错时切回原分支并恢复 stash
467
+ try {
468
+ checkoutBranch(originalBranch);
469
+ stashPop();
470
+ } catch (e) {
471
+ // 忽略
472
+ }
473
+ throw error;
474
+ }
475
+ }
476
+
235
477
  /**
236
478
  * 发布后切回原分支
237
479
  * @param {string} originalBranch - 原分支名
480
+ * @param {boolean} hasStash - 是否有储藏的改动
238
481
  */
239
- export function restoreBranch(originalBranch) {
482
+ export function restoreBranch(originalBranch, hasStash = false) {
240
483
  const currentBranch = getCurrentBranch();
241
484
  if (currentBranch !== originalBranch) {
242
485
  console.log(`\n📌 切回原分支: ${originalBranch}`);
243
486
  checkoutBranch(originalBranch);
244
487
  }
488
+
489
+ // 如果有储藏的改动,提示恢复
490
+ if (hasStash) {
491
+ console.log('\n💡 提示: 本地改动已储藏,执行以下命令恢复:');
492
+ console.log(' git stash pop');
493
+ }
245
494
  }
246
495
 
247
496
  export default {
@@ -253,7 +502,11 @@ export default {
253
502
  pullBranch,
254
503
  mergeBranch,
255
504
  pushBranch,
505
+ stashChanges,
506
+ stashPop,
507
+ autoCommit,
256
508
  executeMainBranchFlow,
257
509
  executeCurrentBranchFlow,
510
+ executeTestBranchFlow,
258
511
  restoreBranch
259
512
  };
package/src/index.d.ts CHANGED
@@ -48,6 +48,8 @@ export interface DingtalkConfig {
48
48
  webhook: string;
49
49
  /** 是否启用钉钉通知,默认 true */
50
50
  enabled?: boolean;
51
+ /** 安全设置关键词(如果机器人设置了关键词,必须配置此项) */
52
+ keyword?: string;
51
53
  }
52
54
 
53
55
  /**
package/src/index.js CHANGED
@@ -17,8 +17,12 @@ export {
17
17
  pullBranch,
18
18
  mergeBranch,
19
19
  pushBranch,
20
+ stashChanges,
21
+ stashPop,
22
+ autoCommit,
20
23
  executeMainBranchFlow,
21
24
  executeCurrentBranchFlow,
25
+ executeTestBranchFlow,
22
26
  restoreBranch
23
27
  } from './git-branch.js';
24
28