@taole/deploy-helper 0.7.2 → 0.7.4-beta.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 +93 -50
- package/lib/offlinePkg.mjs +18 -10
- package/lib/pipelineApi.mjs +1 -0
- package/lib/tagRelease.mjs +190 -0
- package/lib/util.mjs +64 -39
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { Client } from 'node-scp'
|
|
4
3
|
import fs from 'fs';
|
|
5
4
|
import { join, basename, dirname } from "path";
|
|
6
5
|
import { simpleGit } from 'simple-git';
|
|
7
|
-
import { homedir } from 'os'
|
|
8
6
|
import { runPipeline, checkYunxiaoToken, triggerPipeline } from './lib/pipelineApi.mjs';
|
|
9
|
-
import { setDebug, log, getUserDeployHelperConfig } from './lib/util.mjs';
|
|
7
|
+
import { setDebug, log, getUserDeployHelperConfig, TEST_SERVER_HOST, getScpClient } from './lib/util.mjs';
|
|
10
8
|
import path from 'path';
|
|
11
9
|
import { fileURLToPath } from 'url';
|
|
12
10
|
import { checkOfflinePkg, syncApi as syncOfflinePkgApi } from './lib/offlinePkg.mjs';
|
|
11
|
+
import tagRelease from './lib/tagRelease.mjs';
|
|
12
|
+
|
|
13
|
+
|
|
13
14
|
|
|
14
15
|
const __filename = fileURLToPath(import.meta.url);
|
|
15
16
|
const __dirname = path.dirname(__filename);
|
|
16
17
|
|
|
17
18
|
|
|
18
|
-
const TEST_SERVER_HOST = "192.168.0.35";
|
|
19
19
|
|
|
20
20
|
function fmtAssetsCommit(msg) {
|
|
21
21
|
if (msg.startsWith("-#")) {
|
|
@@ -28,13 +28,16 @@ function fmtAssetsCommit(msg) {
|
|
|
28
28
|
* 加载配置
|
|
29
29
|
* @returns 配置对象
|
|
30
30
|
*/
|
|
31
|
-
async function loadConfig() {
|
|
32
|
-
|
|
31
|
+
async function loadConfig(configFileName) {
|
|
32
|
+
if(!configFileName){
|
|
33
|
+
configFileName = 'deploy.config.json';
|
|
34
|
+
}
|
|
35
|
+
const configJsonPath = join(process.cwd(), configFileName);
|
|
33
36
|
if (fs.existsSync(configJsonPath)) {
|
|
34
37
|
const configJson = JSON.parse(fs.readFileSync(configJsonPath, "utf-8"));
|
|
35
38
|
return configJson;
|
|
36
39
|
}
|
|
37
|
-
log(
|
|
40
|
+
log(`文件${configFileName}不存在, 请先创建`);
|
|
38
41
|
return null;
|
|
39
42
|
}
|
|
40
43
|
|
|
@@ -84,25 +87,6 @@ function hasChangeMe(content) {
|
|
|
84
87
|
return content.includes("CHANGE_ME");
|
|
85
88
|
}
|
|
86
89
|
|
|
87
|
-
async function getScpClient() {
|
|
88
|
-
let scpClient = null;
|
|
89
|
-
// 不能放密码。。残念
|
|
90
|
-
const scpClientConfig = {
|
|
91
|
-
host: TEST_SERVER_HOST,
|
|
92
|
-
username: 'root',
|
|
93
|
-
tryKeyboard: false,
|
|
94
|
-
password: 'tuwan123!@#',
|
|
95
|
-
}
|
|
96
|
-
// const sshKeyPath = join(homedir(), ".ssh/id_rsa");
|
|
97
|
-
// if (fs.existsSync(sshKeyPath)) {
|
|
98
|
-
// scpClientConfig.privateKey = fs.readFileSync(sshKeyPath, "utf-8");
|
|
99
|
-
// } else {
|
|
100
|
-
// log(`${sshKeyPath}不存在, 建议配置本机ssh免密登录, 参考地址:https://foochane.cn/article/2019061601.html`);
|
|
101
|
-
// }
|
|
102
|
-
scpClient = await Client(scpClientConfig);
|
|
103
|
-
return scpClient;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
90
|
async function getVersion() {
|
|
107
91
|
const packageJsonPath = join(__dirname, "package.json");
|
|
108
92
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
@@ -124,16 +108,17 @@ async function main() {
|
|
|
124
108
|
// return;
|
|
125
109
|
// other commands
|
|
126
110
|
let command = process.argv[2];
|
|
127
|
-
if (!["init", "prod", "test", "scp", "scpevt", "pipeline", "offlinepkgrm"].includes(command)) {
|
|
111
|
+
if (!["init", "prod", "test", "scp", "scpevt", "pipeline", "offlinepkgrm", "rollback"].includes(command)) {
|
|
128
112
|
command = "help";
|
|
129
113
|
}
|
|
130
114
|
|
|
131
115
|
if (command === "help") {
|
|
132
116
|
console.log(`deploy-helper v${version}`);
|
|
133
117
|
console.log(`usage: deploy-helper [command]`);
|
|
134
|
-
console.log(`command: init 初始化配置`);
|
|
135
|
-
console.log(`command: prod 生产环境部署`);
|
|
118
|
+
console.log(`command: init [-c {configFileName}] 初始化配置`);
|
|
119
|
+
console.log(`command: prod [-c {configFileName}] 生产环境部署`);
|
|
136
120
|
console.log(`command: test 测试环境部署`);
|
|
121
|
+
console.log(`command: rollback {prod|test} {tag} 回滚到指定tag, 注意不处理离线包`);
|
|
137
122
|
console.log(`command: scp {file} 复制文件{file}到OfficialSite测试服务器{file}`);
|
|
138
123
|
console.log(`command: scp {file} {dest} 复制文件{file}到OfficialSite测试服务器{dest}`);
|
|
139
124
|
console.log(`command: scpevt {file} 复制文件{file}到events测试服务器{file}`);
|
|
@@ -144,6 +129,12 @@ async function main() {
|
|
|
144
129
|
}
|
|
145
130
|
log(`deploy-helper v${version} start`);
|
|
146
131
|
|
|
132
|
+
const workDir = process.cwd(); // 当前项目根目录
|
|
133
|
+
let mode = '';
|
|
134
|
+
let configFileName = '';
|
|
135
|
+
let isDoRollback = false;
|
|
136
|
+
let rollbackCleanFunc = null;
|
|
137
|
+
|
|
147
138
|
// 其他命令
|
|
148
139
|
if (command === "init") {
|
|
149
140
|
await initConfigJson();
|
|
@@ -154,7 +145,6 @@ async function main() {
|
|
|
154
145
|
log(`file参数不能为空`);
|
|
155
146
|
process.exit(1);
|
|
156
147
|
}
|
|
157
|
-
const workDir = process.cwd(); // 当前项目根目录
|
|
158
148
|
|
|
159
149
|
const srcFilePath = join(workDir, file);
|
|
160
150
|
if (!fs.existsSync(srcFilePath)) {
|
|
@@ -224,15 +214,43 @@ async function main() {
|
|
|
224
214
|
}
|
|
225
215
|
await syncOfflinePkgApi(userDeployHelperConfig, mode, { name, remove: true, platform }, null);
|
|
226
216
|
process.exit(0);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
217
|
+
} else if(command === "rollback") {
|
|
218
|
+
// 回滚
|
|
219
|
+
mode = process.argv[3] === 'prod' ? 'prod' : 'test'; // 部署环境
|
|
220
|
+
if(!['prod', 'test'].includes(mode)){
|
|
221
|
+
log(`mode参数只能是prod或test`);
|
|
222
|
+
process.exit(1);
|
|
223
|
+
}
|
|
224
|
+
const tag = process.argv[4];
|
|
225
|
+
if(!tag){
|
|
226
|
+
log(`tag参数不能为空`);
|
|
227
|
+
process.exit(1);
|
|
228
|
+
}
|
|
229
|
+
// 做好回滚操作
|
|
230
|
+
rollbackCleanFunc = await tagRelease.rollback(workDir, tag);
|
|
231
|
+
isDoRollback = true;
|
|
232
|
+
} else {
|
|
233
|
+
// 部署
|
|
234
|
+
mode = process.argv[2] === 'prod' ? 'prod' : 'test'; // 部署环境
|
|
231
235
|
log(`deploy mode: ${mode}`);
|
|
232
236
|
log(`workDir: ${workDir}`);
|
|
233
|
-
|
|
237
|
+
if(process.argv[3] === '-c'){
|
|
238
|
+
// 尝试从-c中获取配置文件名
|
|
239
|
+
configFileName = process.argv[4];
|
|
240
|
+
if(!configFileName){
|
|
241
|
+
log(`-c参数必须指定配置文件名`);
|
|
242
|
+
process.exit(1);
|
|
243
|
+
} else if(configFileName){
|
|
244
|
+
log(`将使用配置文件: ${configFileName}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
try {
|
|
234
249
|
// 读取配置
|
|
235
|
-
const config = await loadConfig();
|
|
250
|
+
const config = await loadConfig(configFileName);
|
|
251
|
+
if(!config){
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
236
254
|
// log(`config`, config);
|
|
237
255
|
|
|
238
256
|
const result = hasChangeMe(JSON.stringify(config));
|
|
@@ -241,8 +259,15 @@ async function main() {
|
|
|
241
259
|
process.exit(1);
|
|
242
260
|
}
|
|
243
261
|
|
|
262
|
+
if(!isDoRollback){ // 正常部署的时候,才会处理打tag操作
|
|
263
|
+
const canDoTag = await tagRelease.check(workDir, config, mode);
|
|
264
|
+
if(canDoTag){
|
|
265
|
+
await tagRelease.createRelease(workDir,config, mode);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
244
268
|
|
|
245
|
-
|
|
269
|
+
// rollback的时候,只处理entry文件的回滚操作,assets文件不处理
|
|
270
|
+
const needHandleAssets = !isDoRollback && config.assets && config.assets.dest;
|
|
246
271
|
|
|
247
272
|
// 需要处理entry
|
|
248
273
|
const needHandleEntry = config.entry && config.entry.dest && (mode === 'prod' || (mode === 'test' && !config.entry.onlyProd));
|
|
@@ -257,6 +282,7 @@ async function main() {
|
|
|
257
282
|
const offlinePkgResult = checkOfflinePkg({
|
|
258
283
|
workDir: workDir,
|
|
259
284
|
mode,
|
|
285
|
+
isDoRollback,
|
|
260
286
|
});
|
|
261
287
|
if (!offlinePkgResult.canBuild && offlinePkgResult.errorMsg) {
|
|
262
288
|
log(`${offlinePkgResult.errorMsg}`);
|
|
@@ -383,9 +409,12 @@ async function main() {
|
|
|
383
409
|
log(`entry: ${srcPath}是目录,不支持entry为目录的部署`);
|
|
384
410
|
return;
|
|
385
411
|
}
|
|
386
|
-
const
|
|
387
|
-
|
|
388
|
-
|
|
412
|
+
const dests = Array.isArray(dest) ? dest : [dest];
|
|
413
|
+
for (const dest of dests) {
|
|
414
|
+
const destPath = join(entryDest, dest);
|
|
415
|
+
fs.copyFileSync(srcPath, destPath);
|
|
416
|
+
log(`entry copy: ${srcPath} -> ${destPath}`);
|
|
417
|
+
}
|
|
389
418
|
}
|
|
390
419
|
// log(`entry copy done.`);
|
|
391
420
|
}
|
|
@@ -451,25 +480,39 @@ async function main() {
|
|
|
451
480
|
const scpClient = await getScpClient();
|
|
452
481
|
for (const [src, dest] of Object.entries(syncTestFiles)) {
|
|
453
482
|
const srcPath = join(workDir, src);
|
|
454
|
-
const
|
|
455
|
-
|
|
456
|
-
|
|
483
|
+
const dests = Array.isArray(dest) ? dest : [dest];
|
|
484
|
+
for (const dest of dests) {
|
|
485
|
+
const destPath = "/home/web/website/tuwan_www/templets/static/play/" + dest;
|
|
486
|
+
log(`scp: ${srcPath} -> ${TEST_SERVER_HOST}:${destPath}`);
|
|
487
|
+
await scpClient.uploadFile(srcPath, destPath)
|
|
488
|
+
}
|
|
457
489
|
}
|
|
458
490
|
}
|
|
459
491
|
} catch (error) {
|
|
460
492
|
log(`scp error: ${error}`);
|
|
461
|
-
log(`复制文件到测试环境服务器失败, 建议配置本机ssh免密登录, 参考地址:https://foochane.cn/article/2019061601.html`);
|
|
462
|
-
if (assetsFilesCount > 0) {
|
|
463
|
-
|
|
464
|
-
}
|
|
493
|
+
// log(`复制文件到测试环境服务器失败, 建议配置本机ssh免密登录, 参考地址:https://foochane.cn/article/2019061601.html`);
|
|
494
|
+
// if (assetsFilesCount > 0) {
|
|
495
|
+
// log(`不过请放心,构建产物的assets已经处理完成,只要手动处理index.html文件即可。`);
|
|
496
|
+
// }
|
|
465
497
|
process.exit(1);
|
|
466
498
|
}
|
|
467
499
|
|
|
500
|
+
if(isDoRollback && rollbackCleanFunc){
|
|
501
|
+
await rollbackCleanFunc();
|
|
502
|
+
}
|
|
468
503
|
|
|
469
|
-
|
|
470
|
-
|
|
504
|
+
try {
|
|
505
|
+
// 处理触发流水线的任务
|
|
506
|
+
await runPipeline(config, mode);
|
|
507
|
+
} catch (error) {
|
|
508
|
+
log(`触发流水线失败: `, error);
|
|
509
|
+
}
|
|
471
510
|
|
|
472
|
-
|
|
511
|
+
if(isDoRollback){
|
|
512
|
+
log(`deploy-helper rollback done.`);
|
|
513
|
+
} else {
|
|
514
|
+
log(`deploy-helper deploy done.`);
|
|
515
|
+
}
|
|
473
516
|
process.exit(0);
|
|
474
517
|
|
|
475
518
|
} catch (error) {
|
package/lib/offlinePkg.mjs
CHANGED
|
@@ -154,9 +154,17 @@ if(item.attrs.id=='vite-legacy-polyfill')childNode.onload=function(){System.impo
|
|
|
154
154
|
* @param {Object} config 配置
|
|
155
155
|
* @param {string} config.workDir 工作目录
|
|
156
156
|
* @param {"prod"|"test"} config.mode 部署模式
|
|
157
|
+
* @param {boolean} config.isDoRollback 是否是回滚操作
|
|
157
158
|
* @returns {CheckOfflinePkgResult} 结果
|
|
158
159
|
*/
|
|
159
160
|
export function checkOfflinePkg(config) {
|
|
161
|
+
if(config.isDoRollback){
|
|
162
|
+
log(`【注意】回滚操作,不处理离线包`);
|
|
163
|
+
return {
|
|
164
|
+
canBuild: false,
|
|
165
|
+
errorMsg: "",
|
|
166
|
+
}
|
|
167
|
+
}
|
|
160
168
|
let canBuildOfflinePkg = true;
|
|
161
169
|
let errorMsg = "";
|
|
162
170
|
const offlineConfig = getJsonConfig(config.workDir, "offline.config.json");
|
|
@@ -202,16 +210,8 @@ export function checkOfflinePkg(config) {
|
|
|
202
210
|
errorMsg = "啊没有package.json?";
|
|
203
211
|
canBuildOfflinePkg = false;
|
|
204
212
|
} else {
|
|
205
|
-
if (!allDeps['
|
|
206
|
-
errorMsg = "检查到项目中未安装依赖@
|
|
207
|
-
canBuildOfflinePkg = false;
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
let ossToken = "";
|
|
211
|
-
if (canBuildOfflinePkg) {
|
|
212
|
-
ossToken = getOssToken();
|
|
213
|
-
if (!ossToken) {
|
|
214
|
-
errorMsg = "获取ossToken失败";
|
|
213
|
+
if (!allDeps['vite-plugin-dynamic-base'] && !allDeps['@taole/vite-plugin-dynamic-base']) {
|
|
214
|
+
errorMsg = "检查到项目中未安装依赖@taole/vite-plugin-dynamic-base, 请先安装依赖并调整构建代码以支持离线化,相关文档:https://alidocs.dingtalk.com/i/nodes/ydxXB52LJqexwD71F9K5XNMrJqjMp697?doc_type=wiki_doc";
|
|
215
215
|
canBuildOfflinePkg = false;
|
|
216
216
|
}
|
|
217
217
|
}
|
|
@@ -235,6 +235,14 @@ export function checkOfflinePkg(config) {
|
|
|
235
235
|
}
|
|
236
236
|
}
|
|
237
237
|
}
|
|
238
|
+
let ossToken = "";
|
|
239
|
+
if (canBuildOfflinePkg) {
|
|
240
|
+
ossToken = getOssToken();
|
|
241
|
+
if (!ossToken) {
|
|
242
|
+
errorMsg = "获取ossToken失败";
|
|
243
|
+
canBuildOfflinePkg = false;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
238
246
|
if (!canBuildOfflinePkg) {
|
|
239
247
|
return {
|
|
240
248
|
canBuild: false,
|
package/lib/pipelineApi.mjs
CHANGED
|
@@ -355,6 +355,7 @@ export async function runPipeline(config, mode) {
|
|
|
355
355
|
const token = getYunxiaoToken(pipelineConfig);
|
|
356
356
|
if (!token) {
|
|
357
357
|
log(`未设置云效token, 此次流水线未自动执行: ${JSON.stringify(pipelineConfig)}`);
|
|
358
|
+
return;
|
|
358
359
|
}
|
|
359
360
|
process.env.YUNXIAO_ACCESS_TOKEN = token;
|
|
360
361
|
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// 处理项目发布时的构建产物问题
|
|
2
|
+
|
|
3
|
+
import { log } from "./util.mjs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
import simpleGit from "simple-git";
|
|
7
|
+
|
|
8
|
+
// 前置条件,项目当前是干净的,没有未提交的修改
|
|
9
|
+
|
|
10
|
+
function getTagName(packageJson, commitId, mode) {
|
|
11
|
+
const date = new Date();
|
|
12
|
+
const timeStr =
|
|
13
|
+
date.getMonth() +
|
|
14
|
+
1 +
|
|
15
|
+
"_" +
|
|
16
|
+
date.getDate() +
|
|
17
|
+
"_" +
|
|
18
|
+
date.getHours() +
|
|
19
|
+
"_" +
|
|
20
|
+
date.getMinutes() +
|
|
21
|
+
"_" +
|
|
22
|
+
date.getSeconds();
|
|
23
|
+
return `release_${mode}_${packageJson.name}@${
|
|
24
|
+
packageJson.version
|
|
25
|
+
}_${commitId.slice(0, 8)}_${timeStr}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 检查是否需要打tag
|
|
30
|
+
* @param {*} config 配置对象
|
|
31
|
+
* @returns 是否需要打tag
|
|
32
|
+
*/
|
|
33
|
+
async function check(workDir, config, mode) {
|
|
34
|
+
const tagConfig = config.tag;
|
|
35
|
+
if (!tagConfig) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
let needTag = false;
|
|
39
|
+
if (tagConfig.test && mode === "test") {
|
|
40
|
+
needTag = true;
|
|
41
|
+
}
|
|
42
|
+
if (tagConfig.prod && mode === "prod") {
|
|
43
|
+
needTag = true;
|
|
44
|
+
}
|
|
45
|
+
const skipCleanCheck = false;
|
|
46
|
+
if (needTag && !skipCleanCheck) {
|
|
47
|
+
// 检查当前仓库是否干净
|
|
48
|
+
const git = simpleGit(workDir);
|
|
49
|
+
const status = await git.status();
|
|
50
|
+
if (!status.isClean()) {
|
|
51
|
+
log(`【tag】当前仓库有未提交的修改,请先提交`);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return needTag;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 回滚到指定tag
|
|
59
|
+
* @param {string} workDir 工作目录
|
|
60
|
+
* @param {string} mode 部署模式
|
|
61
|
+
* @param {string} tag 要回滚的tag
|
|
62
|
+
* @returns Promise<function> 回滚后需要执行的清理函数
|
|
63
|
+
*/
|
|
64
|
+
async function rollback(workDir, tag) {
|
|
65
|
+
const git = simpleGit(workDir);
|
|
66
|
+
const commitId = await git.revparse("HEAD");
|
|
67
|
+
const branchInfo = await git.branch();
|
|
68
|
+
const status = await git.status();
|
|
69
|
+
if (!status.isClean()) {
|
|
70
|
+
log(`【rollback】回滚前请确保当前仓库是干净的`);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
// log("回滚前状态:");
|
|
74
|
+
log(`branch: ${branchInfo.current}`);
|
|
75
|
+
log(`commitId: ${commitId}`);
|
|
76
|
+
const cleanfunc = async () => {
|
|
77
|
+
await git.reset(["--hard", commitId]);
|
|
78
|
+
await git.checkout(branchInfo.current);
|
|
79
|
+
};
|
|
80
|
+
try {
|
|
81
|
+
await git.checkout(tag);
|
|
82
|
+
const releaseDir = path.join(process.cwd(), ".taole_release");
|
|
83
|
+
if (!fs.existsSync(releaseDir)) {
|
|
84
|
+
log(`【rollback】.taole_release目录不存在,无法回滚`);
|
|
85
|
+
await cleanfunc();
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
const config = JSON.parse(
|
|
89
|
+
fs.readFileSync(path.join(releaseDir, "deploy.config.json"), "utf-8")
|
|
90
|
+
);
|
|
91
|
+
const entryFiles = config.entry.files;
|
|
92
|
+
Object.keys(entryFiles).forEach((key) => {
|
|
93
|
+
const src = `_${key}`;
|
|
94
|
+
const dest = key;
|
|
95
|
+
log(`rollback file: ${dest}`);
|
|
96
|
+
fs.cpSync(path.join(releaseDir, src), path.join(workDir, dest), {
|
|
97
|
+
recursive: true,
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
fs.cpSync(
|
|
101
|
+
path.join(releaseDir, "deploy.config.json"),
|
|
102
|
+
path.join(workDir, "deploy.config.json"),
|
|
103
|
+
{
|
|
104
|
+
recursive: true,
|
|
105
|
+
force: true,
|
|
106
|
+
}
|
|
107
|
+
);
|
|
108
|
+
log(`rollback file: deploy.config.json`);
|
|
109
|
+
log(`ready to re-deploy`);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
await cleanfunc();
|
|
112
|
+
log(`【rollback】回滚失败: ${error}`);
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
return cleanfunc;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* 创建tag记录,用于后续的回退
|
|
120
|
+
* @param {*} config
|
|
121
|
+
*/
|
|
122
|
+
async function createRelease(workDir, config, mode) {
|
|
123
|
+
// 操作步骤:
|
|
124
|
+
// 0. 记录当前git的commit id, 记为commitId
|
|
125
|
+
// 1. 在项目目录检查是否有.taole_release目录,有则删除然后新建
|
|
126
|
+
// 2. 将配置中entry.files定义的后缀为html的文件复制到.taole_release目录下
|
|
127
|
+
// 3. 提交.taole_release目录到git,commmit 信息为 -#DH-2 整理构建产物
|
|
128
|
+
// 4. 打tag,tag名称为 v{package.json的version}.{commitId}.{yyyy_mm_dd_HH_MM}, 并推到远程仓库
|
|
129
|
+
// 5. 回退当前git的commit id到commitId
|
|
130
|
+
|
|
131
|
+
const git = simpleGit(workDir);
|
|
132
|
+
const commitId = await git.revparse("HEAD");
|
|
133
|
+
log(`commitId: ${commitId}`);
|
|
134
|
+
|
|
135
|
+
const cleanfunc = async () => {
|
|
136
|
+
await git.reset(["--hard", commitId]);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
// 1. 在项目目录检查是否有.taole_release目录,有则删除然后新建
|
|
141
|
+
const releaseDir = path.join(process.cwd(), ".taole_release");
|
|
142
|
+
if (fs.existsSync(releaseDir)) {
|
|
143
|
+
fs.rmSync(releaseDir, { recursive: true });
|
|
144
|
+
}
|
|
145
|
+
fs.mkdirSync(releaseDir);
|
|
146
|
+
|
|
147
|
+
// 2. 将配置中entry.files定义的后缀为html的文件涉及的所有html文件复制到.taole_release目录下
|
|
148
|
+
const entryFiles = config.entry.files;
|
|
149
|
+
Object.keys(entryFiles).forEach((key) => {
|
|
150
|
+
const file = key;
|
|
151
|
+
const htmlFilePath = path.join(workDir, file);
|
|
152
|
+
log(`htmlFilePath: ${htmlFilePath}`);
|
|
153
|
+
if (fs.existsSync(htmlFilePath)) {
|
|
154
|
+
const destFile = `_${file}`;
|
|
155
|
+
fs.cpSync(htmlFilePath, path.join(releaseDir, destFile), {
|
|
156
|
+
recursive: true,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
// 2.1 将当前的config也写入.taole_release目录下
|
|
161
|
+
fs.writeFileSync(
|
|
162
|
+
path.join(releaseDir, "deploy.config.json"),
|
|
163
|
+
JSON.stringify(config, null, 2)
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
await git.add(releaseDir);
|
|
167
|
+
await git.commit("-#DH-2 整理构建产物");
|
|
168
|
+
// log(`commitResult: ${JSON.stringify(commitResult)}`);
|
|
169
|
+
|
|
170
|
+
const packageJson = JSON.parse(
|
|
171
|
+
fs.readFileSync(path.join(workDir, "package.json"), "utf-8")
|
|
172
|
+
);
|
|
173
|
+
const tagName = getTagName(packageJson, commitId, mode);
|
|
174
|
+
// log(`tagName: ${tagName}`);
|
|
175
|
+
await git.tag([tagName]);
|
|
176
|
+
await git.push("origin", tagName);
|
|
177
|
+
log(`打tag成功: ${tagName}, 可用于后续回滚`);
|
|
178
|
+
await cleanfunc();
|
|
179
|
+
} catch (error) {
|
|
180
|
+
log(`创建tag记录失败: ${error}`);
|
|
181
|
+
await cleanfunc();
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export default {
|
|
187
|
+
check,
|
|
188
|
+
createRelease,
|
|
189
|
+
rollback,
|
|
190
|
+
};
|
package/lib/util.mjs
CHANGED
|
@@ -1,46 +1,49 @@
|
|
|
1
|
-
import fs from
|
|
2
|
-
import { join } from "node:path"
|
|
3
|
-
import { homedir } from "node:os"
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { Client } from "node-scp";
|
|
4
5
|
|
|
5
6
|
let _isDebug = false;
|
|
6
7
|
export function setDebug(debug) {
|
|
7
|
-
|
|
8
|
+
_isDebug = debug;
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
export function isDebug() {
|
|
11
|
-
|
|
12
|
+
return _isDebug;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
export function log(...args) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
if (_isDebug) {
|
|
17
|
+
// 打印时间
|
|
18
|
+
console.log(`[${new Date().toLocaleTimeString()}]`, ...args);
|
|
19
|
+
} else {
|
|
20
|
+
console.log(...args);
|
|
21
|
+
}
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
export function getOssToken() {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
let token = "";
|
|
26
|
+
token = process.env.TW_DH_OSS_TOKEN || "";
|
|
27
|
+
if (token) {
|
|
28
|
+
log(`将使用来自环境变量TW_DH_OSS_TOKEN的token`);
|
|
29
|
+
return token;
|
|
30
|
+
}
|
|
31
|
+
const userDeployHelperDir = join(homedir(), "deploy-helper.config.json");
|
|
32
|
+
if (fs.existsSync(userDeployHelperDir)) {
|
|
33
|
+
try {
|
|
34
|
+
const userDeployHelperConfig = JSON.parse(
|
|
35
|
+
fs.readFileSync(userDeployHelperDir, "utf-8")
|
|
36
|
+
);
|
|
37
|
+
token = userDeployHelperConfig.ossToken || "";
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.error(`读取${userDeployHelperDir}配置失败: ${error}`);
|
|
29
40
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const userDeployHelperConfig = JSON.parse(fs.readFileSync(userDeployHelperDir, "utf-8"));
|
|
34
|
-
token = userDeployHelperConfig.ossToken || "";
|
|
35
|
-
} catch (error) {
|
|
36
|
-
console.error(`读取${userDeployHelperDir}配置失败: ${error}`);
|
|
37
|
-
}
|
|
38
|
-
if (token) {
|
|
39
|
-
log(`将使用来自${userDeployHelperDir}的ossToken`);
|
|
40
|
-
return token;
|
|
41
|
-
}
|
|
41
|
+
if (token) {
|
|
42
|
+
log(`将使用来自${userDeployHelperDir}的ossToken`);
|
|
43
|
+
return token;
|
|
42
44
|
}
|
|
43
|
-
|
|
45
|
+
}
|
|
46
|
+
return token;
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
/**
|
|
@@ -48,14 +51,36 @@ export function getOssToken() {
|
|
|
48
51
|
* @returns {Object|null} 用户配置
|
|
49
52
|
*/
|
|
50
53
|
export function getUserDeployHelperConfig() {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
54
|
+
const userDeployHelperDir = join(homedir(), "deploy-helper.config.json");
|
|
55
|
+
if (fs.existsSync(userDeployHelperDir)) {
|
|
56
|
+
try {
|
|
57
|
+
const userDeployHelperConfig = JSON.parse(
|
|
58
|
+
fs.readFileSync(userDeployHelperDir, "utf-8")
|
|
59
|
+
);
|
|
60
|
+
return userDeployHelperConfig;
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.error(`读取${userDeployHelperDir}配置失败: ${error}`);
|
|
59
63
|
}
|
|
60
|
-
|
|
61
|
-
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const TEST_SERVER_HOST = "192.168.0.35";
|
|
69
|
+
export async function getScpClient() {
|
|
70
|
+
let scpClient = null;
|
|
71
|
+
// 不能放密码。。残念
|
|
72
|
+
const scpClientConfig = {
|
|
73
|
+
host: TEST_SERVER_HOST,
|
|
74
|
+
username: "root",
|
|
75
|
+
tryKeyboard: false,
|
|
76
|
+
password: "tuwan123!@#",
|
|
77
|
+
};
|
|
78
|
+
// const sshKeyPath = join(homedir(), ".ssh/id_rsa");
|
|
79
|
+
// if (fs.existsSync(sshKeyPath)) {
|
|
80
|
+
// scpClientConfig.privateKey = fs.readFileSync(sshKeyPath, "utf-8");
|
|
81
|
+
// } else {
|
|
82
|
+
// log(`${sshKeyPath}不存在, 建议配置本机ssh免密登录, 参考地址:https://foochane.cn/article/2019061601.html`);
|
|
83
|
+
// }
|
|
84
|
+
scpClient = await Client(scpClientConfig);
|
|
85
|
+
return scpClient;
|
|
86
|
+
}
|