@taole/deploy-helper 1.0.0 → 1.0.1

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/index.mjs CHANGED
@@ -1,557 +1,557 @@
1
- #!/usr/bin/env node
2
-
3
- import { Client } from 'node-scp'
4
- import fs from 'fs';
5
- import { join, basename, dirname } from "path";
6
- import { simpleGit } from 'simple-git';
7
- import { homedir } from 'os'
8
- import { runPipeline, checkYunxiaoToken, triggerPipeline } from './lib/pipelineApi.mjs';
9
- import { setDebug, log, getUserDeployHelperConfig } from './lib/util.mjs';
10
- import path from 'path';
11
- import { fileURLToPath } from 'url';
12
- import { checkOfflinePkg, syncApi as syncOfflinePkgApi } from './lib/offlinePkg.mjs';
13
- import { cmdWhoami, cmdLogout, cmdLogin } from './lib/login.mjs';
14
- import { cmdProjectCreate, cmdProjectPublish, cmdProjectPull } from './lib/project.mjs';
15
-
16
- const __filename = fileURLToPath(import.meta.url);
17
- const __dirname = path.dirname(__filename);
18
-
19
-
20
- const TEST_SERVER_HOST = "192.168.0.35";
21
-
22
- function fmtAssetsCommit(msg) {
23
- if (msg.startsWith("-#")) {
24
- return msg;
25
- }
26
- return `-#DH-1 ${msg}`;
27
- }
28
-
29
- /**
30
- * 加载配置
31
- * @returns 配置对象
32
- */
33
- async function loadConfig(configFileName) {
34
- if(!configFileName){
35
- configFileName = 'deploy.config.json';
36
- }
37
- const configJsonPath = join(process.cwd(), configFileName);
38
- if (fs.existsSync(configJsonPath)) {
39
- const configJson = JSON.parse(fs.readFileSync(configJsonPath, "utf-8"));
40
- return configJson;
41
- }
42
- log(`文件${configFileName}不存在, 请先创建`);
43
- return null;
44
- }
45
-
46
- async function initConfigJson() {
47
- // check if deploy.config.json exists
48
- const configJsonPath = join(process.cwd(), "deploy.config.json");
49
- if (fs.existsSync(configJsonPath)) {
50
- log(`deploy.config.json已存在, 请勿重复创建`);
51
- return;
52
- }
53
-
54
- const content = `
55
- {
56
- "assets": {
57
- "dest": "../Static_2025",
58
- "commit": "feat:部署资源文件",
59
- "files": {
60
- "dist/assets": "CHANGE_ME"
61
- }
62
- },
63
- "entry": {
64
- "onlyProd": true,
65
- "dest": "../events",
66
- "commit": "feat:部署入口文件",
67
- "files": {
68
- "dist/index.html": "CHANGE_ME"
69
- }
70
- },
71
- "testSync": {
72
- "dist/index.html": "events/CHANGE_ME.htm"
73
- },
74
- "prodPipeline":{
75
- "pipelines": [
76
- {
77
- "name": "OfficialSite OR SOMEOTHER",
78
- "waitResult": true
79
- }
80
- ]
81
- }
82
- }`;
83
- fs.writeFileSync(configJsonPath, content, { encoding: "utf-8" });
84
- log(`deploy.config.json创建成功`);
85
- process.exit(0);
86
- }
87
-
88
- function hasChangeMe(content) {
89
- return content.includes("CHANGE_ME");
90
- }
91
-
92
- async function getScpClient() {
93
- let scpClient = null;
94
- // 不能放密码。。残念
95
- const scpClientConfig = {
96
- host: TEST_SERVER_HOST,
97
- username: 'root',
98
- tryKeyboard: false,
99
- password: 'tuwan123!@#',
100
- }
101
- // const sshKeyPath = join(homedir(), ".ssh/id_rsa");
102
- // if (fs.existsSync(sshKeyPath)) {
103
- // scpClientConfig.privateKey = fs.readFileSync(sshKeyPath, "utf-8");
104
- // } else {
105
- // log(`${sshKeyPath}不存在, 建议配置本机ssh免密登录, 参考地址:https://foochane.cn/article/2019061601.html`);
106
- // }
107
- scpClient = await Client(scpClientConfig);
108
- return scpClient;
109
- }
110
-
111
- async function getVersion() {
112
- const packageJsonPath = join(__dirname, "package.json");
113
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
114
- return packageJson.version;
115
- }
116
-
117
- const commandMap = {};
118
- function registerCommand(command, handler, help) {
119
- if(!command || !handler){
120
- return;
121
- }
122
- commandMap[command] = {
123
- handler,
124
- help,
125
- };
126
- }
127
-
128
- // 注册命令
129
- function doRegisterCommands() {
130
- registerCommand("create", cmdProjectCreate, "创建项目");
131
- registerCommand("publish", cmdProjectPublish, "更新项目到H5平台");
132
- registerCommand("pull", cmdProjectPull, "拉取已有项目到本地");
133
- registerCommand("whoami", cmdWhoami, "查看当前登录用户信息");
134
- registerCommand("logout", cmdLogout, "退出登录");
135
- registerCommand("login", cmdLogin, "登录");
136
- }
137
-
138
- doRegisterCommands();
139
-
140
- async function main() {
141
- const version = await getVersion();
142
-
143
- // process.argv.includes("--debug") ||
144
- // 默认开启debug debug目前只是打印日志的时候额外打印时间
145
- setDebug(true);
146
- log(`deploy-helper v${version}`);
147
-
148
-
149
- // await devTest();
150
- // log(`process.argv: ${process.argv[2]} ${process.argv[3]} ${process.argv[4]}`);
151
-
152
- // return;
153
- // other commands
154
- let command = process.argv[2];
155
- const registeredCommand = commandMap[command];
156
- if(registeredCommand){
157
- // pass
158
- } else if (!["init", "prod", "test", "scp", "scpevt", "pipeline", "offlinepkgrm"].includes(command)) {
159
- command = "help";
160
- }
161
-
162
- if(registeredCommand){
163
- const handler = registeredCommand.handler;
164
- try {
165
- await Promise.resolve(handler());
166
- process.exit(0);
167
- } catch (error) {
168
- log(`执行命令失败: ${error}`);
169
- console.log(error.stack);
170
- process.exit(1);
171
- }
172
- }
173
-
174
- if (command === "help") {
175
- console.log(`deploy-helper v${version}`);
176
- console.log(`usage: deploy-helper [command]`);
177
- console.log(`command: init [-c {configFileName}] 初始化配置`);
178
- console.log(`command: prod [-c {configFileName}] 生产环境部署`);
179
- console.log(`command: test [-c {configFileName}] 测试环境部署`);
180
- console.log(`command: scp {file} 复制文件{file}到OfficialSite测试服务器{file}`);
181
- console.log(`command: scp {file} {dest} 复制文件{file}到OfficialSite测试服务器{dest}`);
182
- console.log(`command: scpevt {file} 复制文件{file}到events测试服务器{file}`);
183
- console.log(`command: scpevt {file} {dest} 复制文件{file}到events测试服务器{dest}`);
184
- console.log(`command: pipeline {pipelineName|pipelineId} [branch] 触发流水线{pipelineName|pipelineId}, 指定分支(可省略)`);
185
- console.log(`command: offlinepkgrm {name} {platform} [mode] 删除离线包{name}, 指定平台{platform}, 指定模式(可省略,默认test)`);
186
- Object.keys(commandMap).forEach(command => {
187
- const cmdObj = commandMap[command];
188
- console.log(`command: ${command} ${cmdObj.help}`);
189
- })
190
- process.exit(0);
191
- }
192
-
193
- // 其他命令
194
- if (command === "init") {
195
- await initConfigJson();
196
- process.exit(0);
197
- } else if (command === "whoami") {
198
- await cmdWhoami();
199
- process.exit(0);
200
- } else if (command === "logout") {
201
- await cmdLogout();
202
- process.exit(0);
203
- } else if (command === "login") {
204
- await cmdLogin();
205
- process.exit(0);
206
- } else if (["scp", "scpevt"].includes(command)) {
207
- const file = process.argv[3];
208
- if (!file) {
209
- log(`file参数不能为空`);
210
- process.exit(1);
211
- }
212
- const workDir = process.cwd(); // 当前项目根目录
213
-
214
- const srcFilePath = join(workDir, file);
215
- if (!fs.existsSync(srcFilePath)) {
216
- log(`${srcFilePath}不存在`);
217
- process.exit(1);
218
- }
219
- if (fs.statSync(srcFilePath).isDirectory()) {
220
- log(`${srcFilePath}是目录, 不支持scp目录`);
221
- process.exit(1);
222
- }
223
- // 获取srcFilePath的目录
224
- const fileDir = dirname(srcFilePath);
225
- const fileName = basename(srcFilePath);
226
- const dest = process.argv[4] || fileName;
227
- if (dest.includes("/") || dest.includes("\\")) {
228
- log(`dest不能包含/或\\`);
229
- process.exit(1);
230
- }
231
-
232
- if (workDir !== fileDir) {
233
- log(`仅支持scp当前目录下的文件(不带子目录)`);
234
- process.exit(1);
235
- }
236
-
237
- const destPath = "/home/web/website/tuwan_www/templets/static/play/" + (command === 'scp' ? '' : 'events/') + dest;
238
-
239
- const scpClient = await getScpClient();
240
- log(`scp: ${srcFilePath} -> ${TEST_SERVER_HOST}:${destPath}`);
241
- await scpClient.uploadFile(srcFilePath, destPath);
242
- log(`scp done.`);
243
- process.exit(0);
244
- } else if (command === "pipeline") {
245
- const pipelineName = process.argv[3];
246
- if (!pipelineName) {
247
- log(`pipeline参数不能为空`);
248
- process.exit(1);
249
- }
250
- const branch = process.argv[4] || "";
251
- try {
252
- await triggerPipeline(pipelineName, { waitResult: true, branch });
253
- process.exit(0);
254
- } catch (error) {
255
- log(`触发流水线失败: `, error);
256
- process.exit(1);
257
- }
258
-
259
- } else if (command === "offlinepkgrm") {
260
- const name = process.argv[3];
261
- if (!name) {
262
- log(`name参数不能为空`);
263
- process.exit(1);
264
- }
265
- const platform = process.argv[4];
266
- if (!platform) {
267
- log(`platform参数不能为空`);
268
- process.exit(1);
269
- }
270
- const mode = process.argv[5] || "test";
271
- if (!["prod", "test"].includes(mode)) {
272
- log(`mode参数只能是prod或test`);
273
- process.exit(1);
274
- }
275
- const userDeployHelperConfig = getUserDeployHelperConfig();
276
- if (!userDeployHelperConfig || !userDeployHelperConfig.offlineApi || !userDeployHelperConfig.offlineApi.get || !userDeployHelperConfig.offlineApi.set) {
277
- log(`配置文件未配置离线包api接口, 请先配置`);
278
- process.exit(1);
279
- }
280
- await syncOfflinePkgApi(userDeployHelperConfig, mode, { name, remove: true, platform }, null);
281
- process.exit(0);
282
- }
283
- try {
284
- const workDir = process.cwd(); // 当前项目根目录
285
- const mode = process.argv[2] === 'prod' ? 'prod' : 'test'; // 部署环境
286
- log(`deploy mode: ${mode}`);
287
- log(`workDir: ${workDir}`);
288
- let configFileName = '';
289
- if(process.argv[3] === '-c'){
290
- // 尝试从-c中获取配置文件名
291
- configFileName = process.argv[4];
292
- if(!configFileName){
293
- log(`-c参数必须指定配置文件名`);
294
- process.exit(1);
295
- } else if(configFileName){
296
- log(`将使用配置文件: ${configFileName}`);
297
- }
298
- }
299
- // 读取配置
300
- const config = await loadConfig(configFileName);
301
- if(!config){
302
- return;
303
- }
304
- // log(`config`, config);
305
-
306
- const result = hasChangeMe(JSON.stringify(config));
307
- if (result) {
308
- log(`配置中存在默认值(CHANGE_ME), 请先修改配置`);
309
- process.exit(1);
310
- }
311
-
312
-
313
- const needHandleAssets = config.assets && config.assets.dest;
314
-
315
- // 需要处理entry
316
- const needHandleEntry = config.entry && config.entry.dest && (mode === 'prod' || (mode === 'test' && !config.entry.onlyProd));
317
- const entryTestBranch = (config.entry && config.entry.testBranch) || "test";
318
- const entryProdBranch = (config.entry && config.entry.prodBranch) || "master";
319
- const entryTargetBranch = mode === 'prod' ? entryProdBranch : entryTestBranch;
320
- // const currentProdBranch = config.prodBranch || "master";
321
-
322
-
323
- // 检查流水线配置
324
- checkYunxiaoToken(config, mode);
325
- const offlinePkgResult = checkOfflinePkg({
326
- workDir: workDir,
327
- mode,
328
- });
329
- if (!offlinePkgResult.canBuild && offlinePkgResult.errorMsg) {
330
- log(`${offlinePkgResult.errorMsg}`);
331
- process.exit(1);
332
- }
333
- if (offlinePkgResult.canBuild) {
334
- await offlinePkgResult.hookPostBuild();
335
- }
336
-
337
- // 检查项目
338
- // 1. 检查项目是否存在
339
- let assetsDest = null;
340
- let entryDest = null;
341
- if (needHandleAssets) {
342
- assetsDest = join(workDir, config.assets.dest);
343
- if (!fs.existsSync(assetsDest)) {
344
- log(`assets.dest路径不存在: ${assetsDest}, 请先创建`);
345
- process.exit(1);
346
- }
347
- }
348
- // entry.dest
349
- if (needHandleEntry) {
350
- entryDest = join(workDir, config.entry.dest);
351
- if (!fs.existsSync(entryDest)) {
352
- log(`entry.dest路径不存在: ${entryDest}, 请先创建`);
353
- process.exit(1);
354
- }
355
- }
356
-
357
- // 2. 检查项目是否clean
358
- let assetsGit = null;
359
- let assetsStatus = null;
360
- if (needHandleAssets) {
361
- assetsGit = simpleGit(assetsDest);
362
- assetsStatus = await assetsGit.status();
363
- if (!assetsStatus.isClean()) {
364
- log(`${assetsDest}目前有未提交内容, 请先处理`);
365
- return;
366
- }
367
- }
368
-
369
- let entryGit;
370
- let entryStatus;
371
- if (needHandleEntry) {
372
- entryGit = simpleGit(entryDest);
373
- entryStatus = await entryGit.status();
374
- if (!entryStatus.isClean()) {
375
- log(`${entryDest}目前有未提交内容, 请先处理`);
376
- return;
377
- }
378
- }
379
-
380
- // 2.1如果是prod模式,检查当前项目是不是处于主分支
381
- // if (mode === 'prod') {
382
- // const currentGit = simpleGit(workDir);
383
- // const currentStatus = await currentGit.status();
384
- // if (!currentStatus.isClean()) {
385
- // log(`当前项目有未提交内容, 请先处理`);
386
- // process.exit(1);
387
- // }
388
- // if (currentStatus.branch !== currentProdBranch) {
389
- // log(`当前项目分支${currentStatus.branch}不是${currentProdBranch}分支, 请切换至${currentProdBranch}分支再执行prod命令`);
390
- // process.exit(1);
391
- // }
392
- // }
393
-
394
- // 3. 检查assets项目分支,并切换至master分支
395
- // assets项目固定使用master分支
396
- if (needHandleAssets && assetsStatus.current !== "master") {
397
- log(`${assetsDest}当前分支不是master, 切换至master分支`);
398
- await assetsGit.checkout("master");
399
- log(`${assetsDest}切换至master分支成功`);
400
- }
401
- if (needHandleEntry && entryStatus.current !== entryTargetBranch) {
402
- log(`${entryDest}当前分支不是${entryTargetBranch}, 切换至${entryTargetBranch}分支`);
403
- await entryGit.checkout(entryTargetBranch);
404
- log(`${entryDest}切换至${entryTargetBranch}分支成功`);
405
- }
406
-
407
- // 4. 执行git pull
408
- if (assetsGit) {
409
- log(`${assetsDest}执行git pull`);
410
- await assetsGit.pull();
411
- log(`${assetsDest} git pull done`);
412
- }
413
- if (entryGit) {
414
- log(`${entryDest}执行git pull`);
415
- await entryGit.pull();
416
- log(`${entryDest} git pull done`);
417
- }
418
-
419
- let assetsFilesCount = 0;
420
- // 5. 复制构建产物
421
- // 5.1 复制assets
422
- if (needHandleAssets) {
423
- const assetsFiles = config.assets.files;
424
- for (const [src, dest] of Object.entries(assetsFiles)) {
425
- const srcPath = join(workDir, src);
426
- if (!fs.existsSync(srcPath)) {
427
- log(`${srcPath}不存在,请确认构建产物是否正确`);
428
- return;
429
- }
430
- const destPath = join(assetsDest, dest);
431
- if (fs.statSync(srcPath).isDirectory()) {
432
- fs.cpSync(srcPath, destPath, { recursive: true });
433
- } else {
434
- fs.copyFileSync(srcPath, destPath);
435
- }
436
- log(`assets copy: ${srcPath} -> ${destPath}`);
437
- assetsFilesCount++;
438
- }
439
- // log(`assets copy done.`);
440
- }
441
- // 5.2 复制entry
442
- if (needHandleEntry) {
443
- const entryFiles = config.entry.files;
444
- for (const [src, dest] of Object.entries(entryFiles)) {
445
- const srcPath = join(workDir, src);
446
- if (!fs.existsSync(srcPath)) {
447
- log(`${srcPath}不存在,请确认构建产物是否正确`);
448
- return;
449
- }
450
- if (fs.statSync(srcPath).isDirectory()) {
451
- log(`entry: ${srcPath}是目录,不支持entry为目录的部署`);
452
- return;
453
- }
454
- const dests = Array.isArray(dest) ? dest : [dest];
455
- for (const dest of dests) {
456
- const destPath = join(entryDest, dest);
457
- fs.copyFileSync(srcPath, destPath);
458
- log(`entry copy: ${srcPath} -> ${destPath}`);
459
- }
460
- }
461
- // log(`entry copy done.`);
462
- }
463
-
464
- // 6. 提交
465
- let canPushAssets = false;
466
- let canPushEntry = false;
467
- if (needHandleAssets) {
468
- assetsStatus = await assetsGit.status();
469
- if (!assetsStatus.isClean()) {
470
- canPushAssets = true;
471
- await assetsGit.add(".");
472
- let assetsCommit = `${config.assets.commit || "feat:部署资源文件"} by deploy-helper`;
473
- assetsCommit = fmtAssetsCommit(assetsCommit);
474
- const assetsCommitResult = await assetsGit.commit(assetsCommit);
475
- log(`assets commit: ${assetsCommit}`);
476
- log(`assets commit: ${JSON.stringify(assetsCommitResult.summary)}`);
477
- } else {
478
- log(`${assetsDest} 未发现修改内容,跳过提交`);
479
- }
480
- }
481
- if (needHandleEntry) {
482
- entryStatus = await entryGit.status();
483
- if (!entryStatus.isClean()) {
484
- canPushEntry = true;
485
- await entryGit.add(".");
486
- let entryCommit = `${config.entry.commit || "feat:部署入口文件"} by deploy-helper`;
487
- entryCommit = fmtAssetsCommit(entryCommit);
488
- const entryCommitResult = await entryGit.commit(entryCommit);
489
- log(`entry commit: ${entryCommit}`);
490
- log(`entry commit: ${JSON.stringify(entryCommitResult.summary)}`);
491
- } else {
492
- log(`${entryDest} 未发现修改内容,跳过提交`);
493
- }
494
- }
495
-
496
- // 7. 推送
497
- if (canPushAssets) {
498
- log(`assets push start`);
499
- try {
500
- await assetsGit.push();
501
- } catch (error) {
502
- await assetsGit.pull();
503
- await assetsGit.push();
504
- }
505
- log(`assets push done.`);
506
- }
507
- if (canPushEntry) {
508
- log(`entry push start`);
509
- try {
510
- await entryGit.push();
511
- } catch (error) {
512
- await entryGit.pull();
513
- await entryGit.push();
514
- }
515
- log(`entry push done.`);
516
- }
517
-
518
- try {
519
- // 8 测试环境将entry scp至测试环境
520
- if (mode === 'test' && config.testSync) {
521
- const syncTestFiles = config.testSync;
522
- const scpClient = await getScpClient();
523
- for (const [src, dest] of Object.entries(syncTestFiles)) {
524
- const srcPath = join(workDir, src);
525
- const dests = Array.isArray(dest) ? dest : [dest];
526
- for (const dest of dests) {
527
- const destPath = "/home/web/website/tuwan_www/templets/static/play/" + dest;
528
- log(`scp: ${srcPath} -> ${TEST_SERVER_HOST}:${destPath}`);
529
- await scpClient.uploadFile(srcPath, destPath)
530
- }
531
- }
532
- }
533
- } catch (error) {
534
- log(`scp error: ${error}`);
535
- log(`复制文件到测试环境服务器失败, 建议配置本机ssh免密登录, 参考地址:https://foochane.cn/article/2019061601.html`);
536
- if (assetsFilesCount > 0) {
537
- log(`不过请放心,构建产物的assets已经处理完成,只要手动处理index.html文件即可。`);
538
- }
539
- process.exit(1);
540
- }
541
-
542
-
543
- // 处理触发流水线的任务
544
- await runPipeline(config, mode);
545
-
546
- log(`deploy-helper deploy done.`);
547
- process.exit(0);
548
-
549
- } catch (error) {
550
- log(`error occurred in deploy-helper`, error);
551
- process.exit(1);
552
- }
553
- }
554
-
555
-
556
- main();
557
-
1
+ #!/usr/bin/env node
2
+
3
+ import { Client } from 'node-scp'
4
+ import fs from 'fs';
5
+ import { join, basename, dirname } from "path";
6
+ import { simpleGit } from 'simple-git';
7
+ import { homedir } from 'os'
8
+ import { runPipeline, checkYunxiaoToken, triggerPipeline } from './lib/pipelineApi.mjs';
9
+ import { setDebug, log, getUserDeployHelperConfig } from './lib/util.mjs';
10
+ import path from 'path';
11
+ import { fileURLToPath } from 'url';
12
+ import { checkOfflinePkg, syncApi as syncOfflinePkgApi } from './lib/offlinePkg.mjs';
13
+ import { cmdWhoami, cmdLogout, cmdLogin } from './lib/login.mjs';
14
+ import { cmdProjectCreate, cmdProjectPublish, cmdProjectPull } from './lib/project.mjs';
15
+
16
+ const __filename = fileURLToPath(import.meta.url);
17
+ const __dirname = path.dirname(__filename);
18
+
19
+
20
+ const TEST_SERVER_HOST = "192.168.0.35";
21
+
22
+ function fmtAssetsCommit(msg) {
23
+ if (msg.startsWith("-#")) {
24
+ return msg;
25
+ }
26
+ return `-#DH-1 ${msg}`;
27
+ }
28
+
29
+ /**
30
+ * 加载配置
31
+ * @returns 配置对象
32
+ */
33
+ async function loadConfig(configFileName) {
34
+ if(!configFileName){
35
+ configFileName = 'deploy.config.json';
36
+ }
37
+ const configJsonPath = join(process.cwd(), configFileName);
38
+ if (fs.existsSync(configJsonPath)) {
39
+ const configJson = JSON.parse(fs.readFileSync(configJsonPath, "utf-8"));
40
+ return configJson;
41
+ }
42
+ log(`文件${configFileName}不存在, 请先创建`);
43
+ return null;
44
+ }
45
+
46
+ async function initConfigJson() {
47
+ // check if deploy.config.json exists
48
+ const configJsonPath = join(process.cwd(), "deploy.config.json");
49
+ if (fs.existsSync(configJsonPath)) {
50
+ log(`deploy.config.json已存在, 请勿重复创建`);
51
+ return;
52
+ }
53
+
54
+ const content = `
55
+ {
56
+ "assets": {
57
+ "dest": "../Static_2025",
58
+ "commit": "feat:部署资源文件",
59
+ "files": {
60
+ "dist/assets": "CHANGE_ME"
61
+ }
62
+ },
63
+ "entry": {
64
+ "onlyProd": true,
65
+ "dest": "../events",
66
+ "commit": "feat:部署入口文件",
67
+ "files": {
68
+ "dist/index.html": "CHANGE_ME"
69
+ }
70
+ },
71
+ "testSync": {
72
+ "dist/index.html": "events/CHANGE_ME.htm"
73
+ },
74
+ "prodPipeline":{
75
+ "pipelines": [
76
+ {
77
+ "name": "OfficialSite OR SOMEOTHER",
78
+ "waitResult": true
79
+ }
80
+ ]
81
+ }
82
+ }`;
83
+ fs.writeFileSync(configJsonPath, content, { encoding: "utf-8" });
84
+ log(`deploy.config.json创建成功`);
85
+ process.exit(0);
86
+ }
87
+
88
+ function hasChangeMe(content) {
89
+ return content.includes("CHANGE_ME");
90
+ }
91
+
92
+ async function getScpClient() {
93
+ let scpClient = null;
94
+ // 不能放密码。。残念
95
+ const scpClientConfig = {
96
+ host: TEST_SERVER_HOST,
97
+ username: 'root',
98
+ tryKeyboard: false,
99
+ password: 'tuwan123!@#',
100
+ }
101
+ // const sshKeyPath = join(homedir(), ".ssh/id_rsa");
102
+ // if (fs.existsSync(sshKeyPath)) {
103
+ // scpClientConfig.privateKey = fs.readFileSync(sshKeyPath, "utf-8");
104
+ // } else {
105
+ // log(`${sshKeyPath}不存在, 建议配置本机ssh免密登录, 参考地址:https://foochane.cn/article/2019061601.html`);
106
+ // }
107
+ scpClient = await Client(scpClientConfig);
108
+ return scpClient;
109
+ }
110
+
111
+ async function getVersion() {
112
+ const packageJsonPath = join(__dirname, "package.json");
113
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
114
+ return packageJson.version;
115
+ }
116
+
117
+ const commandMap = {};
118
+ function registerCommand(command, handler, help) {
119
+ if(!command || !handler){
120
+ return;
121
+ }
122
+ commandMap[command] = {
123
+ handler,
124
+ help,
125
+ };
126
+ }
127
+
128
+ // 注册命令
129
+ function doRegisterCommands() {
130
+ registerCommand("create", cmdProjectCreate, "创建项目");
131
+ registerCommand("publish", cmdProjectPublish, "更新项目到H5平台");
132
+ registerCommand("pull", cmdProjectPull, "拉取已有项目到本地");
133
+ registerCommand("whoami", cmdWhoami, "查看当前登录用户信息");
134
+ registerCommand("logout", cmdLogout, "退出登录");
135
+ registerCommand("login", cmdLogin, "登录");
136
+ }
137
+
138
+ doRegisterCommands();
139
+
140
+ async function main() {
141
+ const version = await getVersion();
142
+
143
+ // process.argv.includes("--debug") ||
144
+ // 默认开启debug debug目前只是打印日志的时候额外打印时间
145
+ setDebug(true);
146
+ log(`deploy-helper v${version}`);
147
+
148
+
149
+ // await devTest();
150
+ // log(`process.argv: ${process.argv[2]} ${process.argv[3]} ${process.argv[4]}`);
151
+
152
+ // return;
153
+ // other commands
154
+ let command = process.argv[2];
155
+ const registeredCommand = commandMap[command];
156
+ if(registeredCommand){
157
+ // pass
158
+ } else if (!["init", "prod", "test", "scp", "scpevt", "pipeline", "offlinepkgrm"].includes(command)) {
159
+ command = "help";
160
+ }
161
+
162
+ if(registeredCommand){
163
+ const handler = registeredCommand.handler;
164
+ try {
165
+ await Promise.resolve(handler());
166
+ process.exit(0);
167
+ } catch (error) {
168
+ log(`执行命令失败: ${error}`);
169
+ console.log(error.stack);
170
+ process.exit(1);
171
+ }
172
+ }
173
+
174
+ if (command === "help") {
175
+ console.log(`deploy-helper v${version}`);
176
+ console.log(`usage: deploy-helper [command]`);
177
+ console.log(`command: init [-c {configFileName}] 初始化配置`);
178
+ console.log(`command: prod [-c {configFileName}] 生产环境部署`);
179
+ console.log(`command: test [-c {configFileName}] 测试环境部署`);
180
+ console.log(`command: scp {file} 复制文件{file}到OfficialSite测试服务器{file}`);
181
+ console.log(`command: scp {file} {dest} 复制文件{file}到OfficialSite测试服务器{dest}`);
182
+ console.log(`command: scpevt {file} 复制文件{file}到events测试服务器{file}`);
183
+ console.log(`command: scpevt {file} {dest} 复制文件{file}到events测试服务器{dest}`);
184
+ console.log(`command: pipeline {pipelineName|pipelineId} [branch] 触发流水线{pipelineName|pipelineId}, 指定分支(可省略)`);
185
+ console.log(`command: offlinepkgrm {name} {platform} [mode] 删除离线包{name}, 指定平台{platform}, 指定模式(可省略,默认test)`);
186
+ Object.keys(commandMap).forEach(command => {
187
+ const cmdObj = commandMap[command];
188
+ console.log(`command: ${command} ${cmdObj.help}`);
189
+ })
190
+ process.exit(0);
191
+ }
192
+
193
+ // 其他命令
194
+ if (command === "init") {
195
+ await initConfigJson();
196
+ process.exit(0);
197
+ } else if (command === "whoami") {
198
+ await cmdWhoami();
199
+ process.exit(0);
200
+ } else if (command === "logout") {
201
+ await cmdLogout();
202
+ process.exit(0);
203
+ } else if (command === "login") {
204
+ await cmdLogin();
205
+ process.exit(0);
206
+ } else if (["scp", "scpevt"].includes(command)) {
207
+ const file = process.argv[3];
208
+ if (!file) {
209
+ log(`file参数不能为空`);
210
+ process.exit(1);
211
+ }
212
+ const workDir = process.cwd(); // 当前项目根目录
213
+
214
+ const srcFilePath = join(workDir, file);
215
+ if (!fs.existsSync(srcFilePath)) {
216
+ log(`${srcFilePath}不存在`);
217
+ process.exit(1);
218
+ }
219
+ if (fs.statSync(srcFilePath).isDirectory()) {
220
+ log(`${srcFilePath}是目录, 不支持scp目录`);
221
+ process.exit(1);
222
+ }
223
+ // 获取srcFilePath的目录
224
+ const fileDir = dirname(srcFilePath);
225
+ const fileName = basename(srcFilePath);
226
+ const dest = process.argv[4] || fileName;
227
+ if (dest.includes("/") || dest.includes("\\")) {
228
+ log(`dest不能包含/或\\`);
229
+ process.exit(1);
230
+ }
231
+
232
+ if (workDir !== fileDir) {
233
+ log(`仅支持scp当前目录下的文件(不带子目录)`);
234
+ process.exit(1);
235
+ }
236
+
237
+ const destPath = "/home/web/website/tuwan_www/templets/static/play/" + (command === 'scp' ? '' : 'events/') + dest;
238
+
239
+ const scpClient = await getScpClient();
240
+ log(`scp: ${srcFilePath} -> ${TEST_SERVER_HOST}:${destPath}`);
241
+ await scpClient.uploadFile(srcFilePath, destPath);
242
+ log(`scp done.`);
243
+ process.exit(0);
244
+ } else if (command === "pipeline") {
245
+ const pipelineName = process.argv[3];
246
+ if (!pipelineName) {
247
+ log(`pipeline参数不能为空`);
248
+ process.exit(1);
249
+ }
250
+ const branch = process.argv[4] || "";
251
+ try {
252
+ await triggerPipeline(pipelineName, { waitResult: true, branch });
253
+ process.exit(0);
254
+ } catch (error) {
255
+ log(`触发流水线失败: `, error);
256
+ process.exit(1);
257
+ }
258
+
259
+ } else if (command === "offlinepkgrm") {
260
+ const name = process.argv[3];
261
+ if (!name) {
262
+ log(`name参数不能为空`);
263
+ process.exit(1);
264
+ }
265
+ const platform = process.argv[4];
266
+ if (!platform) {
267
+ log(`platform参数不能为空`);
268
+ process.exit(1);
269
+ }
270
+ const mode = process.argv[5] || "test";
271
+ if (!["prod", "test"].includes(mode)) {
272
+ log(`mode参数只能是prod或test`);
273
+ process.exit(1);
274
+ }
275
+ const userDeployHelperConfig = getUserDeployHelperConfig();
276
+ if (!userDeployHelperConfig || !userDeployHelperConfig.offlineApi || !userDeployHelperConfig.offlineApi.get || !userDeployHelperConfig.offlineApi.set) {
277
+ log(`配置文件未配置离线包api接口, 请先配置`);
278
+ process.exit(1);
279
+ }
280
+ await syncOfflinePkgApi(userDeployHelperConfig, mode, { name, remove: true, platform }, null);
281
+ process.exit(0);
282
+ }
283
+ try {
284
+ const workDir = process.cwd(); // 当前项目根目录
285
+ const mode = process.argv[2] === 'prod' ? 'prod' : 'test'; // 部署环境
286
+ log(`deploy mode: ${mode}`);
287
+ log(`workDir: ${workDir}`);
288
+ let configFileName = '';
289
+ if(process.argv[3] === '-c'){
290
+ // 尝试从-c中获取配置文件名
291
+ configFileName = process.argv[4];
292
+ if(!configFileName){
293
+ log(`-c参数必须指定配置文件名`);
294
+ process.exit(1);
295
+ } else if(configFileName){
296
+ log(`将使用配置文件: ${configFileName}`);
297
+ }
298
+ }
299
+ // 读取配置
300
+ const config = await loadConfig(configFileName);
301
+ if(!config){
302
+ return;
303
+ }
304
+ // log(`config`, config);
305
+
306
+ const result = hasChangeMe(JSON.stringify(config));
307
+ if (result) {
308
+ log(`配置中存在默认值(CHANGE_ME), 请先修改配置`);
309
+ process.exit(1);
310
+ }
311
+
312
+
313
+ const needHandleAssets = config.assets && config.assets.dest;
314
+
315
+ // 需要处理entry
316
+ const needHandleEntry = config.entry && config.entry.dest && (mode === 'prod' || (mode === 'test' && !config.entry.onlyProd));
317
+ const entryTestBranch = (config.entry && config.entry.testBranch) || "test";
318
+ const entryProdBranch = (config.entry && config.entry.prodBranch) || "master";
319
+ const entryTargetBranch = mode === 'prod' ? entryProdBranch : entryTestBranch;
320
+ // const currentProdBranch = config.prodBranch || "master";
321
+
322
+
323
+ // 检查流水线配置
324
+ checkYunxiaoToken(config, mode);
325
+ const offlinePkgResult = checkOfflinePkg({
326
+ workDir: workDir,
327
+ mode,
328
+ });
329
+ if (!offlinePkgResult.canBuild && offlinePkgResult.errorMsg) {
330
+ log(`${offlinePkgResult.errorMsg}`);
331
+ process.exit(1);
332
+ }
333
+ if (offlinePkgResult.canBuild) {
334
+ await offlinePkgResult.hookPostBuild();
335
+ }
336
+
337
+ // 检查项目
338
+ // 1. 检查项目是否存在
339
+ let assetsDest = null;
340
+ let entryDest = null;
341
+ if (needHandleAssets) {
342
+ assetsDest = join(workDir, config.assets.dest);
343
+ if (!fs.existsSync(assetsDest)) {
344
+ log(`assets.dest路径不存在: ${assetsDest}, 请先创建`);
345
+ process.exit(1);
346
+ }
347
+ }
348
+ // entry.dest
349
+ if (needHandleEntry) {
350
+ entryDest = join(workDir, config.entry.dest);
351
+ if (!fs.existsSync(entryDest)) {
352
+ log(`entry.dest路径不存在: ${entryDest}, 请先创建`);
353
+ process.exit(1);
354
+ }
355
+ }
356
+
357
+ // 2. 检查项目是否clean
358
+ let assetsGit = null;
359
+ let assetsStatus = null;
360
+ if (needHandleAssets) {
361
+ assetsGit = simpleGit(assetsDest);
362
+ assetsStatus = await assetsGit.status();
363
+ if (!assetsStatus.isClean()) {
364
+ log(`${assetsDest}目前有未提交内容, 请先处理`);
365
+ return;
366
+ }
367
+ }
368
+
369
+ let entryGit;
370
+ let entryStatus;
371
+ if (needHandleEntry) {
372
+ entryGit = simpleGit(entryDest);
373
+ entryStatus = await entryGit.status();
374
+ if (!entryStatus.isClean()) {
375
+ log(`${entryDest}目前有未提交内容, 请先处理`);
376
+ return;
377
+ }
378
+ }
379
+
380
+ // 2.1如果是prod模式,检查当前项目是不是处于主分支
381
+ // if (mode === 'prod') {
382
+ // const currentGit = simpleGit(workDir);
383
+ // const currentStatus = await currentGit.status();
384
+ // if (!currentStatus.isClean()) {
385
+ // log(`当前项目有未提交内容, 请先处理`);
386
+ // process.exit(1);
387
+ // }
388
+ // if (currentStatus.branch !== currentProdBranch) {
389
+ // log(`当前项目分支${currentStatus.branch}不是${currentProdBranch}分支, 请切换至${currentProdBranch}分支再执行prod命令`);
390
+ // process.exit(1);
391
+ // }
392
+ // }
393
+
394
+ // 3. 检查assets项目分支,并切换至master分支
395
+ // assets项目固定使用master分支
396
+ if (needHandleAssets && assetsStatus.current !== "master") {
397
+ log(`${assetsDest}当前分支不是master, 切换至master分支`);
398
+ await assetsGit.checkout("master");
399
+ log(`${assetsDest}切换至master分支成功`);
400
+ }
401
+ if (needHandleEntry && entryStatus.current !== entryTargetBranch) {
402
+ log(`${entryDest}当前分支不是${entryTargetBranch}, 切换至${entryTargetBranch}分支`);
403
+ await entryGit.checkout(entryTargetBranch);
404
+ log(`${entryDest}切换至${entryTargetBranch}分支成功`);
405
+ }
406
+
407
+ // 4. 执行git pull
408
+ if (assetsGit) {
409
+ log(`${assetsDest}执行git pull`);
410
+ await assetsGit.pull();
411
+ log(`${assetsDest} git pull done`);
412
+ }
413
+ if (entryGit) {
414
+ log(`${entryDest}执行git pull`);
415
+ await entryGit.pull();
416
+ log(`${entryDest} git pull done`);
417
+ }
418
+
419
+ let assetsFilesCount = 0;
420
+ // 5. 复制构建产物
421
+ // 5.1 复制assets
422
+ if (needHandleAssets) {
423
+ const assetsFiles = config.assets.files;
424
+ for (const [src, dest] of Object.entries(assetsFiles)) {
425
+ const srcPath = join(workDir, src);
426
+ if (!fs.existsSync(srcPath)) {
427
+ log(`${srcPath}不存在,请确认构建产物是否正确`);
428
+ return;
429
+ }
430
+ const destPath = join(assetsDest, dest);
431
+ if (fs.statSync(srcPath).isDirectory()) {
432
+ fs.cpSync(srcPath, destPath, { recursive: true });
433
+ } else {
434
+ fs.copyFileSync(srcPath, destPath);
435
+ }
436
+ log(`assets copy: ${srcPath} -> ${destPath}`);
437
+ assetsFilesCount++;
438
+ }
439
+ // log(`assets copy done.`);
440
+ }
441
+ // 5.2 复制entry
442
+ if (needHandleEntry) {
443
+ const entryFiles = config.entry.files;
444
+ for (const [src, dest] of Object.entries(entryFiles)) {
445
+ const srcPath = join(workDir, src);
446
+ if (!fs.existsSync(srcPath)) {
447
+ log(`${srcPath}不存在,请确认构建产物是否正确`);
448
+ return;
449
+ }
450
+ if (fs.statSync(srcPath).isDirectory()) {
451
+ log(`entry: ${srcPath}是目录,不支持entry为目录的部署`);
452
+ return;
453
+ }
454
+ const dests = Array.isArray(dest) ? dest : [dest];
455
+ for (const dest of dests) {
456
+ const destPath = join(entryDest, dest);
457
+ fs.copyFileSync(srcPath, destPath);
458
+ log(`entry copy: ${srcPath} -> ${destPath}`);
459
+ }
460
+ }
461
+ // log(`entry copy done.`);
462
+ }
463
+
464
+ // 6. 提交
465
+ let canPushAssets = false;
466
+ let canPushEntry = false;
467
+ if (needHandleAssets) {
468
+ assetsStatus = await assetsGit.status();
469
+ if (!assetsStatus.isClean()) {
470
+ canPushAssets = true;
471
+ await assetsGit.add(".");
472
+ let assetsCommit = `${config.assets.commit || "feat:部署资源文件"} by deploy-helper`;
473
+ assetsCommit = fmtAssetsCommit(assetsCommit);
474
+ const assetsCommitResult = await assetsGit.commit(assetsCommit);
475
+ log(`assets commit: ${assetsCommit}`);
476
+ log(`assets commit: ${JSON.stringify(assetsCommitResult.summary)}`);
477
+ } else {
478
+ log(`${assetsDest} 未发现修改内容,跳过提交`);
479
+ }
480
+ }
481
+ if (needHandleEntry) {
482
+ entryStatus = await entryGit.status();
483
+ if (!entryStatus.isClean()) {
484
+ canPushEntry = true;
485
+ await entryGit.add(".");
486
+ let entryCommit = `${config.entry.commit || "feat:部署入口文件"} by deploy-helper`;
487
+ entryCommit = fmtAssetsCommit(entryCommit);
488
+ const entryCommitResult = await entryGit.commit(entryCommit);
489
+ log(`entry commit: ${entryCommit}`);
490
+ log(`entry commit: ${JSON.stringify(entryCommitResult.summary)}`);
491
+ } else {
492
+ log(`${entryDest} 未发现修改内容,跳过提交`);
493
+ }
494
+ }
495
+
496
+ // 7. 推送
497
+ if (canPushAssets) {
498
+ log(`assets push start`);
499
+ try {
500
+ await assetsGit.push();
501
+ } catch (error) {
502
+ await assetsGit.pull();
503
+ await assetsGit.push();
504
+ }
505
+ log(`assets push done.`);
506
+ }
507
+ if (canPushEntry) {
508
+ log(`entry push start`);
509
+ try {
510
+ await entryGit.push();
511
+ } catch (error) {
512
+ await entryGit.pull();
513
+ await entryGit.push();
514
+ }
515
+ log(`entry push done.`);
516
+ }
517
+
518
+ try {
519
+ // 8 测试环境将entry scp至测试环境
520
+ if (mode === 'test' && config.testSync) {
521
+ const syncTestFiles = config.testSync;
522
+ const scpClient = await getScpClient();
523
+ for (const [src, dest] of Object.entries(syncTestFiles)) {
524
+ const srcPath = join(workDir, src);
525
+ const dests = Array.isArray(dest) ? dest : [dest];
526
+ for (const dest of dests) {
527
+ const destPath = "/home/web/website/tuwan_www/templets/static/play/" + dest;
528
+ log(`scp: ${srcPath} -> ${TEST_SERVER_HOST}:${destPath}`);
529
+ await scpClient.uploadFile(srcPath, destPath)
530
+ }
531
+ }
532
+ }
533
+ } catch (error) {
534
+ log(`scp error: ${error}`);
535
+ log(`复制文件到测试环境服务器失败, 建议配置本机ssh免密登录, 参考地址:https://foochane.cn/article/2019061601.html`);
536
+ if (assetsFilesCount > 0) {
537
+ log(`不过请放心,构建产物的assets已经处理完成,只要手动处理index.html文件即可。`);
538
+ }
539
+ process.exit(1);
540
+ }
541
+
542
+
543
+ // 处理触发流水线的任务
544
+ await runPipeline(config, mode);
545
+
546
+ log(`deploy-helper deploy done.`);
547
+ process.exit(0);
548
+
549
+ } catch (error) {
550
+ log(`error occurred in deploy-helper`, error);
551
+ process.exit(1);
552
+ }
553
+ }
554
+
555
+
556
+ main();
557
+