yj-deploy 0.0.11 → 0.0.12
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/README.md +62 -1
- package/bin/yj-deploy.cjs +9 -0
- package/dist/yj-deploy.mjs +266 -270
- package/dist/yj-deploy.umd.js +8 -8
- package/lib/cli.cjs +277 -0
- package/package.json +12 -1
package/README.md
CHANGED
|
@@ -204,6 +204,53 @@ export default defineConfig({
|
|
|
204
204
|
// 使用 yarn deploy 或 npm run deploy
|
|
205
205
|
```
|
|
206
206
|
|
|
207
|
+
### 直接部署已有产物(无需重新编译)
|
|
208
|
+
|
|
209
|
+
升级项目中的 `yj-deploy` 后,可以直接复用现有的 Vite、Webpack 或 Vue CLI 插件配置,不需要新增部署脚本,也不需要提取单独的部署配置。运行和构建支持 Node.js 14.18 或更高版本,Node 14 项目建议使用 14.21.3。项目自身的 Vite、Webpack、Vue CLI 及配置依赖也需要支持当前 Node 版本(例如 Vite 4 支持 Node 14.18,新版本 Vite 的要求以其文档为准)。
|
|
210
|
+
|
|
211
|
+
在业务项目根目录运行:
|
|
212
|
+
|
|
213
|
+
```sh
|
|
214
|
+
# 检查选中的配置和本地产物,不连接服务器
|
|
215
|
+
npx yj-deploy --mode development --dry-run
|
|
216
|
+
|
|
217
|
+
# 上传已有的 dist,构建并推送镜像
|
|
218
|
+
npx yj-deploy --mode development
|
|
219
|
+
|
|
220
|
+
# 推送成功后更新镜像地址并重启服务
|
|
221
|
+
npx yj-deploy --mode development --restart
|
|
222
|
+
|
|
223
|
+
# 沿用镜像环境、镜像仓库等动态参数
|
|
224
|
+
npx yj-deploy --mode test --tmpName=test --imageStore=dev-images --restart
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
`--mode` 默认是 `production`,用于加载构建配置,请传入生成当前产物时使用的 mode。它不会重新生成产物,也不会自动改变镜像环境 `--tmpName`。例如,原命令使用 `vite build --mode development`,直接部署时也使用 `--mode development`。
|
|
228
|
+
|
|
229
|
+
CLI 自动识别以下配置:
|
|
230
|
+
|
|
231
|
+
- Vite:`vite.config.js`、`.ts`、`.mjs`、`.cjs`、`.mts`、`.cts`,使用项目自身的 Vite 加载配置,支持配置函数和配置中的 `loadEnv` 调用。
|
|
232
|
+
- Webpack:`webpack.config.js`、`.cjs`、`.mjs`,支持对象、异步配置函数和多配置数组;`.ts`、`.mts`、`.cts` 交给项目中的 `webpack-cli` 和项目已配置的 TypeScript 加载器处理。
|
|
233
|
+
- Vue CLI:`vue.config.js`,通过项目自身的 `@vue/cli-service` 解析 `configureWebpack` 和 `chainWebpack`;也支持没有独立配置文件、使用 `package.json` 的 `vue` 字段的项目。
|
|
234
|
+
|
|
235
|
+
CLI 加载配置时提供 `--deploy` 标记,兼容根据该标记注册插件的写法。Vite 部署插件需要出现在配置返回的 `plugins` 中,CLI 不执行其他插件的构建钩子。配置文件本身的顶层代码和配置函数会正常执行。
|
|
236
|
+
|
|
237
|
+
配置文件不在根目录、使用自定义名称,或存在多个配置时,显式指定:
|
|
238
|
+
|
|
239
|
+
```sh
|
|
240
|
+
npx yj-deploy --config config/vite.config.ts --mode development
|
|
241
|
+
npx yj-deploy --config config/webpack.prod.js --tool webpack
|
|
242
|
+
|
|
243
|
+
# Webpack 配置函数需要的 env 参数
|
|
244
|
+
npx yj-deploy --tool webpack --env region=cn --env feature
|
|
245
|
+
|
|
246
|
+
# 有多个部署插件时,按原插件配置中的 namespace 选择唯一目标
|
|
247
|
+
npx yj-deploy --namespace your-project --mode development
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
参数同时支持 `--mode=development` 和 `--mode development` 两种写法。原有的 `--tmpName`、`--imageStore`、`--restart`、`--reset`、`--lazyUpload`、`--allLog` 均可使用。执行 `npx yj-deploy --help` 查看完整帮助。
|
|
251
|
+
|
|
252
|
+
CLI 在上传前检查本地产物目录是否存在且有文件。未找到配置或插件、目标不唯一、上传或服务更新失败时,命令返回非零退出码。`--dry-run` 只检查配置选择和本地产物,不验证远程服务器连接。
|
|
253
|
+
|
|
207
254
|
### 上传任意项目
|
|
208
255
|
```javascript
|
|
209
256
|
// 1、在项目中创建uploader.js
|
|
@@ -226,6 +273,8 @@ node uploader.js
|
|
|
226
273
|
node uploader.js --tmpName=test
|
|
227
274
|
```
|
|
228
275
|
|
|
276
|
+
`upload()` 现在返回 `Promise<boolean>`,成功为 `true`,失败为 `false`。原有直接调用方式仍可使用;需要等待部署结果时,可通过 `await deploy.upload()` 判断是否成功。
|
|
277
|
+
|
|
229
278
|
|
|
230
279
|
## 动态配置项
|
|
231
280
|
由于部分项目可能需要区分环境,所以需要动态配置项,目前支持动态配置项如下:
|
|
@@ -313,6 +362,12 @@ node uploader.js --tmpName=test
|
|
|
313
362
|
|
|
314
363
|
<br />
|
|
315
364
|
|
|
365
|
+
## 开发验证
|
|
366
|
+
|
|
367
|
+
本工具的运行和构建支持 Node.js 14.18 及以上版本。维护本仓库时,`npm test` 使用 Node 内置的 `node:test` 测试框架,需要 Node.js 18.18 或更高版本;测试文件不会发布到 npm 包,不影响业务项目在 Node 14 下使用插件或 CLI。
|
|
368
|
+
|
|
369
|
+
兼容性测试可通过环境变量 `YJ_DEPLOY_TEST_NODE` 指定旧版 Node 可执行文件路径。测试框架使用当前 Node,CLI 和模块入口用指定的 Node 启动验证。
|
|
370
|
+
|
|
316
371
|
# 最后
|
|
317
372
|
如有意见或建议,或bug或文档不清晰等问题,可随时向我反馈,或直接修改后提交,共同完善这个工具。
|
|
318
373
|
|
|
@@ -322,6 +377,12 @@ node uploader.js --tmpName=test
|
|
|
322
377
|
|
|
323
378
|
## 升级日志
|
|
324
379
|
|
|
380
|
+
### v0.0.12
|
|
381
|
+
|
|
382
|
+
- 新增 `npx yj-deploy` 命令,复用 Vite、Webpack 或 Vue CLI 配置直接部署已有产物,无需重新编译或新增脚本。
|
|
383
|
+
- 支持 `--dry-run` 检查、指定配置和部署目标;部署失败返回非零退出码。
|
|
384
|
+
- 修复镜像重试并发执行问题,完善上传结果反馈和 CommonJS / ESM 构建;兼容 Node.js 14.18 及以上版本。
|
|
385
|
+
|
|
325
386
|
### v0.0.9
|
|
326
387
|
支持自动重启服务,上传完后,可以根据配置自动重启rancher服务
|
|
327
388
|
|
|
@@ -338,4 +399,4 @@ node uploader.js --tmpName=test
|
|
|
338
399
|
解决因为并行上传文件太多导致上传失败的问题,支持动态配置并行上传数据量
|
|
339
400
|
|
|
340
401
|
### v0.0.3
|
|
341
|
-
解决文件夹嵌套太深,可能会导致上传文件时文件夹还没有创建导致报错的问题
|
|
402
|
+
解决文件夹嵌套太深,可能会导致上传文件时文件夹还没有创建导致报错的问题
|
package/dist/yj-deploy.mjs
CHANGED
|
@@ -1,88 +1,98 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
import x from "single-line-log";
|
|
2
|
+
import L from "path";
|
|
3
|
+
import U from "fs";
|
|
4
|
+
import F from "ssh2";
|
|
5
|
+
import N from "axios";
|
|
6
|
+
import R from "https";
|
|
7
|
+
function E(m) {
|
|
8
|
+
return m && m.__esModule && Object.prototype.hasOwnProperty.call(m, "default") ? m.default : m;
|
|
9
|
+
}
|
|
10
|
+
const { stdout: q } = x, z = L, b = U, { Client: B } = F, M = N, O = R;
|
|
11
|
+
var T = class {
|
|
12
|
+
config = {
|
|
13
|
+
host: "",
|
|
14
|
+
port: "",
|
|
15
|
+
username: "",
|
|
16
|
+
password: "",
|
|
17
|
+
namespace: "",
|
|
18
|
+
// 项目命名空间,等同于镜像地址目录名称
|
|
19
|
+
imageStore: "dev-images",
|
|
20
|
+
// 镜像仓库
|
|
21
|
+
tmpName: "dev",
|
|
22
|
+
// 镜像环境
|
|
23
|
+
delay: 0,
|
|
24
|
+
// 延迟上传时间
|
|
25
|
+
// 本地文件目录(必填)
|
|
26
|
+
fileDir: "",
|
|
27
|
+
// 远程sftp服务根目录
|
|
28
|
+
rootDir: "/home/yjweb",
|
|
29
|
+
// dockerfile文件信息
|
|
30
|
+
dockerfile: {
|
|
31
|
+
name: "Dockerfile",
|
|
32
|
+
content: [
|
|
33
|
+
"FROM harbor.yunjingtech.cn:30002/yj-base/nginx:latest",
|
|
34
|
+
"RUN rm -rf /usr/share/nginx/html/*",
|
|
35
|
+
"COPY dist /usr/share/nginx/html/"
|
|
36
|
+
]
|
|
37
|
+
},
|
|
38
|
+
// upload.sh文件信息
|
|
39
|
+
upload: {
|
|
40
|
+
name: "upload.sh",
|
|
41
|
+
content: [
|
|
42
|
+
"#!/bin/sh",
|
|
43
|
+
"tag=`basename \\`pwd\\``:$2-`date +%Y%m%d`",
|
|
44
|
+
'echo "- 镜像仓库: $1"',
|
|
45
|
+
'echo "- 镜像环境: $2"',
|
|
46
|
+
"docker build -t harbor.yunjingtech.cn:30002/$1/$tag .",
|
|
47
|
+
"docker push harbor.yunjingtech.cn:30002/$1/$tag",
|
|
48
|
+
"if [ $? -eq 0 ];then",
|
|
49
|
+
"echo - 推送成功",
|
|
50
|
+
"echo - 镜像地址: harbor.yunjingtech.cn:30002/$1/$tag",
|
|
51
|
+
"else",
|
|
52
|
+
"echo - 镜像推送失败,请重试或联系运维人员,如需查看全部docker日志,请在命令后添加 --allLog查看更多信息",
|
|
53
|
+
"fi"
|
|
54
|
+
]
|
|
55
|
+
},
|
|
56
|
+
// 上传dist目录信息
|
|
57
|
+
dist: {
|
|
58
|
+
name: "dist"
|
|
59
|
+
},
|
|
60
|
+
parallelDir: 20,
|
|
61
|
+
// 文件夹并行创建数量,如果报错,可以减少此配置
|
|
62
|
+
parallelFile: 50,
|
|
63
|
+
// 文件并行上传数量,如果报错,可以减少此配置
|
|
64
|
+
allLog: !1,
|
|
65
|
+
// 是否显示全部日志
|
|
66
|
+
lazyUpload: !1,
|
|
67
|
+
// 是否开启懒上传
|
|
68
|
+
rePushNum: 3,
|
|
69
|
+
// 重新推送次数,为0代表不重新推送
|
|
70
|
+
// 仅能通过命令启用
|
|
71
|
+
rancherConfig: {
|
|
72
|
+
// baseUrl: '', // Rancher API 基础地址
|
|
73
|
+
// clusterId: '', // Rancher 集群 ID
|
|
74
|
+
token: "",
|
|
75
|
+
// Rancher API 访问令牌
|
|
12
76
|
namespace: "",
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
name: "Dockerfile",
|
|
27
|
-
content: [
|
|
28
|
-
"FROM harbor.yunjingtech.cn:30002/yj-base/nginx:latest",
|
|
29
|
-
"RUN rm -rf /usr/share/nginx/html/*",
|
|
30
|
-
"COPY dist /usr/share/nginx/html/"
|
|
31
|
-
]
|
|
32
|
-
},
|
|
33
|
-
// upload.sh文件信息
|
|
34
|
-
upload: {
|
|
35
|
-
name: "upload.sh",
|
|
36
|
-
content: [
|
|
37
|
-
"#!/bin/sh",
|
|
38
|
-
"tag=`basename \\`pwd\\``:$2-`date +%Y%m%d`",
|
|
39
|
-
'echo "- 镜像仓库: $1"',
|
|
40
|
-
'echo "- 镜像环境: $2"',
|
|
41
|
-
"docker build -t harbor.yunjingtech.cn:30002/$1/$tag .",
|
|
42
|
-
"docker push harbor.yunjingtech.cn:30002/$1/$tag",
|
|
43
|
-
"if [ $? -eq 0 ];then",
|
|
44
|
-
"echo - 推送成功",
|
|
45
|
-
"echo - 镜像地址: harbor.yunjingtech.cn:30002/$1/$tag",
|
|
46
|
-
"else",
|
|
47
|
-
"echo - 镜像推送失败,请重试或联系运维人员,如需查看全部docker日志,请在命令后添加 --allLog查看更多信息",
|
|
48
|
-
"fi"
|
|
49
|
-
]
|
|
50
|
-
},
|
|
51
|
-
// 上传dist目录信息
|
|
52
|
-
dist: {
|
|
53
|
-
name: "dist"
|
|
54
|
-
},
|
|
55
|
-
parallelDir: 20,
|
|
56
|
-
// 文件夹并行创建数量,如果报错,可以减少此配置
|
|
57
|
-
parallelFile: 50,
|
|
58
|
-
// 文件并行上传数量,如果报错,可以减少此配置
|
|
59
|
-
allLog: !1,
|
|
60
|
-
// 是否显示全部日志
|
|
61
|
-
lazyUpload: !1,
|
|
62
|
-
// 是否开启懒上传
|
|
63
|
-
rePushNum: 3,
|
|
64
|
-
// 重新推送次数,为0代表不重新推送
|
|
65
|
-
// 仅能通过命令启用
|
|
66
|
-
rancherConfig: {
|
|
67
|
-
// baseUrl: '', // Rancher API 基础地址
|
|
68
|
-
// clusterId: '', // Rancher 集群 ID
|
|
69
|
-
token: "",
|
|
70
|
-
// Rancher API 访问令牌
|
|
71
|
-
namespace: "",
|
|
72
|
-
// 服务所在的命名空间
|
|
73
|
-
workload: ""
|
|
74
|
-
// 工作负载名称
|
|
75
|
-
}
|
|
76
|
-
});
|
|
77
|
-
w(this, "ssh2Conn", null);
|
|
78
|
-
// 上传状态
|
|
79
|
-
w(this, "uploading", !1);
|
|
80
|
-
// 定时器
|
|
81
|
-
w(this, "trim", null);
|
|
77
|
+
// 服务所在的命名空间
|
|
78
|
+
workload: ""
|
|
79
|
+
// 工作负载名称
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
ssh2Conn = null;
|
|
83
|
+
// 上传状态
|
|
84
|
+
uploading = !1;
|
|
85
|
+
// 保存本次部署的完成结果,供 CLI 等待,并防止连接期间重复发起部署。
|
|
86
|
+
uploadPromise = null;
|
|
87
|
+
// 定时器
|
|
88
|
+
trim = null;
|
|
89
|
+
constructor(e) {
|
|
82
90
|
return this.config = Object.assign(this.config, e), {
|
|
83
91
|
name: "yj-deploy",
|
|
84
92
|
isPut: this.isPut.bind(this),
|
|
85
93
|
upload: this.upload.bind(this),
|
|
94
|
+
// CLI 只读取目标名称和本地产物目录,不暴露连接凭据。
|
|
95
|
+
getDeployInfo: () => ({ namespace: this.config.namespace, fileDir: this.config.fileDir }),
|
|
86
96
|
// webpack钩子
|
|
87
97
|
apply: this.apply.bind(this),
|
|
88
98
|
// vite上传钩子
|
|
@@ -109,42 +119,40 @@ module.exports = class {
|
|
|
109
119
|
* 连接跳板机并开始上传
|
|
110
120
|
*/
|
|
111
121
|
upload() {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
122
|
+
return this.uploadPromise ? this.uploadPromise : (console.log("-------------- deploy-start --------------"), this.uploading = !0, this.uploadPromise = this.runUpload().catch((e) => (console.error(`- 部署失败:${e.message || e}`), !1)).finally(() => {
|
|
123
|
+
this.breakConnect(), this.uploadPromise = null;
|
|
124
|
+
}), this.uploadPromise);
|
|
125
|
+
}
|
|
126
|
+
// 在建立远程连接前校验配置和产物,已有产物才能进入部署流程。
|
|
127
|
+
async runUpload() {
|
|
128
|
+
for (const o of ["host", "port", "username", "password", "namespace", "fileDir"])
|
|
129
|
+
if (!this.config[o])
|
|
130
|
+
throw new Error(`请配置 ${o}`);
|
|
131
|
+
const e = await this.getFilesInDirectory(this.config.fileDir);
|
|
132
|
+
if (!e.localFileList.length)
|
|
133
|
+
throw new Error("待上传目录没有文件,请先编译项目");
|
|
134
|
+
return new Promise((o, t) => {
|
|
135
|
+
const s = new B();
|
|
136
|
+
this.ssh2Conn = s, s.on("error", t), s.on("close", () => t(new Error("SSH 连接在部署完成前关闭"))), s.on("ready", () => {
|
|
137
|
+
console.log("- 跳板机连接成功!"), (async () => {
|
|
138
|
+
const n = await new Promise((c, l) => {
|
|
139
|
+
s.sftp((u, f) => u ? l(u) : c(f));
|
|
140
|
+
});
|
|
141
|
+
return this.getArgv("--reset") !== void 0 && (console.log("- 重置项目"), await this.onShell(`rm -r ${this.config.rootDir}/${this.config.namespace}`)), await this.upLoadProject(n, `${this.config.rootDir}/${this.config.namespace}`, e) !== null;
|
|
142
|
+
})().then(o, t);
|
|
143
|
+
}), s.connect({
|
|
144
|
+
host: this.config.host,
|
|
145
|
+
port: this.config.port,
|
|
146
|
+
username: this.config.username,
|
|
147
|
+
password: this.config.password
|
|
131
148
|
});
|
|
132
|
-
})
|
|
133
|
-
host: this.config.host,
|
|
134
|
-
port: this.config.port,
|
|
135
|
-
username: this.config.username,
|
|
136
|
-
password: this.config.password
|
|
137
|
-
}), this.ssh2Conn.on("error", (e) => {
|
|
138
|
-
console.log(`- 连接失败: ${e}`), this.breakConnect();
|
|
139
|
-
}), this.ssh2Conn.on("end", () => {
|
|
140
|
-
this.uploading = !1;
|
|
141
|
-
}));
|
|
149
|
+
});
|
|
142
150
|
}
|
|
143
151
|
/**
|
|
144
152
|
* 断开连接
|
|
145
153
|
*/
|
|
146
154
|
breakConnect() {
|
|
147
|
-
this.uploading = !1, console.log("- 已断开连接"), console.log("-------------- deploy-end --------------")
|
|
155
|
+
this.uploading = !1, this.ssh2Conn && (this.ssh2Conn.end(), this.ssh2Conn = null, console.log("- 已断开连接")), console.log("-------------- deploy-end --------------");
|
|
148
156
|
}
|
|
149
157
|
/**
|
|
150
158
|
* 运行shell命令
|
|
@@ -153,23 +161,23 @@ module.exports = class {
|
|
|
153
161
|
*/
|
|
154
162
|
onShell(e) {
|
|
155
163
|
const o = this.getArgv("--allLog") !== void 0;
|
|
156
|
-
return new Promise((
|
|
157
|
-
this.ssh2Conn.shell((
|
|
158
|
-
if (
|
|
159
|
-
console.log("- 远程命令错误:" +
|
|
164
|
+
return new Promise((t, s) => {
|
|
165
|
+
this.ssh2Conn.shell((a, n) => {
|
|
166
|
+
if (a) {
|
|
167
|
+
console.log("- 远程命令错误:" + a, e), s(a);
|
|
160
168
|
return;
|
|
161
169
|
}
|
|
162
170
|
let r = [];
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
}).on("data", (c) => {
|
|
166
|
-
let
|
|
167
|
-
(o || this.config.allLog ||
|
|
168
|
-
` +
|
|
171
|
+
n.on("close", () => {
|
|
172
|
+
t(r.toString());
|
|
173
|
+
}).on("error", s).on("data", (c) => {
|
|
174
|
+
let l = c.toString();
|
|
175
|
+
(o || this.config.allLog || l.startsWith("- ")) && r.push(`
|
|
176
|
+
` + l);
|
|
169
177
|
}).stderr.on("data", (c) => {
|
|
170
178
|
console.log(`- 远程命令错误:
|
|
171
|
-
` + c),
|
|
172
|
-
}),
|
|
179
|
+
` + c), s(c);
|
|
180
|
+
}), n.end(e + `
|
|
173
181
|
exit
|
|
174
182
|
`);
|
|
175
183
|
});
|
|
@@ -180,22 +188,16 @@ exit
|
|
|
180
188
|
* @param {*} sftp
|
|
181
189
|
* @param {*} dir
|
|
182
190
|
*/
|
|
183
|
-
projectInit(e, o) {
|
|
184
|
-
|
|
185
|
-
e.mkdir(o,
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
`)}' > ${
|
|
193
|
-
const r = `${o}/${this.config.upload.name}`;
|
|
194
|
-
await this.onShell(`touch ${r}`), await this.onShell(`echo '${this.config.upload.content.join(`
|
|
195
|
-
`)}' > ${r}`), n();
|
|
196
|
-
}
|
|
197
|
-
});
|
|
198
|
-
});
|
|
191
|
+
async projectInit(e, o) {
|
|
192
|
+
await new Promise((a, n) => {
|
|
193
|
+
e.mkdir(o, (r) => r ? n(r) : a());
|
|
194
|
+
}), console.log("- 正在创建dockerfile");
|
|
195
|
+
const t = `${o}/${this.config.dockerfile.name}`;
|
|
196
|
+
await this.onShell(`touch ${t}`), await this.onShell(`echo '${this.config.dockerfile.content.join(`
|
|
197
|
+
`)}' > ${t}`), console.log("- 正在创建upload.sh脚本");
|
|
198
|
+
const s = `${o}/${this.config.upload.name}`;
|
|
199
|
+
await this.onShell(`touch ${s}`), await this.onShell(`echo '${this.config.upload.content.join(`
|
|
200
|
+
`)}' > ${s}`);
|
|
199
201
|
}
|
|
200
202
|
/**
|
|
201
203
|
* 判断文件是否存在
|
|
@@ -204,9 +206,9 @@ exit
|
|
|
204
206
|
* @returns
|
|
205
207
|
*/
|
|
206
208
|
checkRemoteFile(e, o) {
|
|
207
|
-
return new Promise((
|
|
208
|
-
e.stat(o, (
|
|
209
|
-
|
|
209
|
+
return new Promise((t) => {
|
|
210
|
+
e.stat(o, (s) => {
|
|
211
|
+
t(!s);
|
|
210
212
|
});
|
|
211
213
|
});
|
|
212
214
|
}
|
|
@@ -215,84 +217,74 @@ exit
|
|
|
215
217
|
* @param {*} sftp
|
|
216
218
|
* @param {*} dir
|
|
217
219
|
*/
|
|
218
|
-
upLoadProject(e, o) {
|
|
219
|
-
let
|
|
220
|
-
typeof this.config.rePushNum == "number" && this.config.rePushNum > 0 && (
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
console.log(`- ${this.config.namespace}项目创建成功`), this.upLoadProject(e, o);
|
|
224
|
-
}).catch((l) => {
|
|
225
|
-
console.log(`- 创建项目失败: ${l}`), this.breakConnect();
|
|
226
|
-
});
|
|
227
|
-
return;
|
|
228
|
-
}
|
|
229
|
-
const s = this.getArgv("--lazyUpload") !== void 0 || this.config.lazyUpload, t = `${o}/${this.config.dist.name}`;
|
|
230
|
-
if (s || await this.onShell(`rm -rf ${t}`), await this.onShell(`mkdir ${t}`), !this.config.fileDir) {
|
|
231
|
-
console.log("- 请配置待上传文件目录 fileDir"), this.breakConnect();
|
|
232
|
-
return;
|
|
233
|
-
}
|
|
234
|
-
const { localFileList: r, dirList: c } = await this.getFilesInDirectory(this.config.fileDir);
|
|
235
|
-
if (r.length === 0) {
|
|
236
|
-
console.log("- 待上传目录没有获取到文件,请检查"), this.breakConnect();
|
|
237
|
-
return;
|
|
238
|
-
}
|
|
239
|
-
let a = 0, h = r.length;
|
|
240
|
-
const p = (l) => new Promise(async (d, f) => {
|
|
241
|
-
try {
|
|
242
|
-
if (s && await this.checkRemoteFile(e, `${t}${l.remotePath}`))
|
|
243
|
-
return d();
|
|
244
|
-
const g = S.createReadStream(l.localPath), b = e.createWriteStream(`${t}${l.remotePath}`);
|
|
245
|
-
b.on("close", () => {
|
|
246
|
-
this.progressBar(++a, h), d();
|
|
247
|
-
}), b.on("error", (C) => {
|
|
248
|
-
console.log(`- 文件 ${l.remotePath} 上传失败:${C}`), f(C), this.breakConnect();
|
|
249
|
-
}), g.pipe(b);
|
|
250
|
-
} catch (g) {
|
|
251
|
-
f(g);
|
|
252
|
-
}
|
|
253
|
-
}), m = (l, d) => {
|
|
254
|
-
let f = [];
|
|
255
|
-
for (let g = 0; g < l.length; g += d)
|
|
256
|
-
f.push(l.slice(g, g + d));
|
|
257
|
-
return f;
|
|
258
|
-
}, $ = (l) => {
|
|
259
|
-
if (l.length === 0)
|
|
260
|
-
return Promise.resolve();
|
|
261
|
-
const d = l.shift();
|
|
262
|
-
return Promise.all(d.map((f) => p(f))).then(() => $(l));
|
|
263
|
-
}, k = (l) => {
|
|
264
|
-
if (l.length === 0)
|
|
265
|
-
return Promise.resolve();
|
|
266
|
-
let f = l.shift().map((g) => `${t}${g}`).join(" ");
|
|
267
|
-
return this.onShell(`mkdir -p ${f}`).then(() => k(l));
|
|
268
|
-
};
|
|
269
|
-
let y = [...m(c, this.config.parallelDir || 20)];
|
|
270
|
-
await k(y), console.log("- 创建目录完成"), await new Promise((l) => {
|
|
271
|
-
setTimeout(() => {
|
|
272
|
-
l();
|
|
273
|
-
}, 500);
|
|
220
|
+
async upLoadProject(e, o, t) {
|
|
221
|
+
let s = 0;
|
|
222
|
+
typeof this.config.rePushNum == "number" && this.config.rePushNum > 0 && (s = this.config.rePushNum), await new Promise((i, d) => {
|
|
223
|
+
e.readdir(o, (h) => {
|
|
224
|
+
h ? h.code === 2 || h.code === "ENOENT" ? i(!1) : d(h) : i(!0);
|
|
274
225
|
});
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
226
|
+
}) || (console.log("- 跳板机不存在项目, 开始创建项目"), await this.projectInit(e, o), console.log(`- ${this.config.namespace}项目创建成功`));
|
|
227
|
+
const n = this.getArgv("--lazyUpload") !== void 0 || this.config.lazyUpload, r = `${o}/${this.config.dist.name}`;
|
|
228
|
+
n || await this.onShell(`rm -rf ${r}`), await this.onShell(`mkdir -p ${r}`);
|
|
229
|
+
const { localFileList: c, dirList: l } = t;
|
|
230
|
+
let u = 0, f = c.length;
|
|
231
|
+
const $ = (i) => new Promise(async (d, h) => {
|
|
232
|
+
try {
|
|
233
|
+
if (n && await this.checkRemoteFile(e, `${r}${i.remotePath}`))
|
|
234
|
+
return d();
|
|
235
|
+
const g = b.createReadStream(i.localPath), P = e.createWriteStream(`${r}${i.remotePath}`);
|
|
236
|
+
g.on("error", (y) => {
|
|
237
|
+
h(y), P.destroy();
|
|
238
|
+
}), P.on("close", () => {
|
|
239
|
+
this.progressBar(++u, f), d();
|
|
240
|
+
}), P.on("error", (y) => {
|
|
241
|
+
console.log(`- 文件 ${i.remotePath} 上传失败:${y}`), h(y), g.destroy();
|
|
242
|
+
}), g.pipe(P);
|
|
243
|
+
} catch (g) {
|
|
244
|
+
h(g);
|
|
245
|
+
}
|
|
246
|
+
}), p = (i, d) => {
|
|
247
|
+
let h = [];
|
|
248
|
+
for (let g = 0; g < i.length; g += d)
|
|
249
|
+
h.push(i.slice(g, g + d));
|
|
250
|
+
return h;
|
|
251
|
+
}, k = (i) => {
|
|
252
|
+
if (i.length === 0)
|
|
253
|
+
return Promise.resolve();
|
|
254
|
+
const d = i.shift();
|
|
255
|
+
return Promise.all(d.map((h) => $(h))).then(() => k(i));
|
|
256
|
+
}, w = (i) => {
|
|
257
|
+
if (i.length === 0)
|
|
258
|
+
return Promise.resolve();
|
|
259
|
+
let h = i.shift().map((g) => `${r}${g}`).join(" ");
|
|
260
|
+
return this.onShell(`mkdir -p ${h}`).then(() => w(i));
|
|
261
|
+
};
|
|
262
|
+
let I = [...p(l, this.config.parallelDir || 20)];
|
|
263
|
+
await w(I), console.log("- 创建目录完成"), await new Promise((i) => {
|
|
264
|
+
setTimeout(() => {
|
|
265
|
+
i();
|
|
266
|
+
}, 500);
|
|
288
267
|
});
|
|
268
|
+
let j = [...p(c, this.config.parallelFile || 50)];
|
|
269
|
+
await k(j), n && (this.progressBar(f, f), console.log("\x1B[32m%s\x1B[0m", `- 已开启懒上传,本次共上传 ${u} 个文件`)), console.log("- 文件上传成功"), console.log("- 开始推送镜像");
|
|
270
|
+
let S = `cd ${o} && sh ${this.config.upload.name}`;
|
|
271
|
+
if (await this.isNewShell(e, o)) {
|
|
272
|
+
const i = this.getArgv("--imageStore") || this.config.imageStore;
|
|
273
|
+
S += ` ${i}`;
|
|
274
|
+
} else
|
|
275
|
+
console.log(`
|
|
276
|
+
- warning 检测到当前脚本为手动创建,不支持镜像仓库配置,imageStore将失效`), console.log("- warning 如需支持imageStore参数,请在命令行后添加 --reset 重新生成脚本");
|
|
277
|
+
const v = this.getArgv("--tmpName") || this.config.tmpName;
|
|
278
|
+
S += ` ${v}`;
|
|
279
|
+
const D = Array.from({ length: s + 1 }, () => () => this.onShell(S));
|
|
280
|
+
return this.pushMirrorImage(D, D.length);
|
|
289
281
|
}
|
|
290
282
|
// 推送镜像
|
|
291
283
|
async pushMirrorImage(e, o) {
|
|
292
284
|
if (e.length === 0)
|
|
293
285
|
return console.log("- 镜像推送失败,请添加 --allLog 查看全部日志"), null;
|
|
294
|
-
const
|
|
295
|
-
return
|
|
286
|
+
const t = await e[0]();
|
|
287
|
+
return t.includes("- 镜像推送失败") ? (console.log(t), e.length > 1 && console.log(`- 正在尝试第 ${o - e.length + 1} 次推送...`), this.pushMirrorImage(e.slice(1), o)) : (console.log(t), console.log("- 镜像推送结束"), this.getArgv("--restart") !== void 0 && !await W(t, this.config.rancherConfig) ? null : t);
|
|
296
288
|
}
|
|
297
289
|
/**
|
|
298
290
|
* 获取脚本内容是不是自动创建的脚本
|
|
@@ -300,13 +292,13 @@ exit
|
|
|
300
292
|
* 有 $2代表是自动创建的版本,否则老版本
|
|
301
293
|
*/
|
|
302
294
|
async isNewShell(e, o) {
|
|
303
|
-
return new Promise((
|
|
304
|
-
e.readFile(`${o}/${this.config.upload.name}`, (
|
|
305
|
-
if (
|
|
306
|
-
return
|
|
307
|
-
const r =
|
|
295
|
+
return new Promise((t, s) => {
|
|
296
|
+
e.readFile(`${o}/${this.config.upload.name}`, (a, n) => {
|
|
297
|
+
if (a)
|
|
298
|
+
return s(!1);
|
|
299
|
+
const r = n.toString();
|
|
308
300
|
r.includes("- 镜像推送失败") || console.log(`
|
|
309
|
-
- warning 检测到当前脚本可能不是最新版本,推送失败可能不会提示,且没有失败自动重试功能,请在命令后添加 --reset 重新生成脚本`),
|
|
301
|
+
- warning 检测到当前脚本可能不是最新版本,推送失败可能不会提示,且没有失败自动重试功能,请在命令后添加 --reset 重新生成脚本`), t(r.includes("$2"));
|
|
310
302
|
});
|
|
311
303
|
});
|
|
312
304
|
}
|
|
@@ -315,41 +307,41 @@ exit
|
|
|
315
307
|
* @param description 命令行开头的文字信息
|
|
316
308
|
* @param bar_length 进度条的长度(单位:字符),默认设为 25
|
|
317
309
|
*/
|
|
318
|
-
progressBar(e, o,
|
|
319
|
-
let
|
|
320
|
-
for (let
|
|
321
|
-
|
|
310
|
+
progressBar(e, o, t = 25) {
|
|
311
|
+
let s = (e / o).toFixed(4), a = Math.floor(s * t), n = "";
|
|
312
|
+
for (let l = 0; l < a; l++)
|
|
313
|
+
n += "█";
|
|
322
314
|
let r = "";
|
|
323
|
-
for (let
|
|
315
|
+
for (let l = 0; l < t - a; l++)
|
|
324
316
|
r += "░";
|
|
325
|
-
let c = `- 文件上传进度: ${
|
|
326
|
-
|
|
317
|
+
let c = `- 文件上传进度: ${n}${r} (${e}/${o}) ${(100 * s).toFixed(2)}%`;
|
|
318
|
+
q(c), e == o && console.log("");
|
|
327
319
|
}
|
|
328
320
|
/**
|
|
329
321
|
* 获取目录下所有文件并分类
|
|
330
322
|
* @param {*} dir
|
|
331
323
|
*/
|
|
332
324
|
getFilesInDirectory(e) {
|
|
333
|
-
const o = [],
|
|
334
|
-
return new Promise((
|
|
335
|
-
const
|
|
336
|
-
const
|
|
337
|
-
let
|
|
338
|
-
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
localPath:
|
|
325
|
+
const o = [], t = [];
|
|
326
|
+
return new Promise((s, a) => {
|
|
327
|
+
const n = (r, c, l) => {
|
|
328
|
+
const u = b.readdirSync(r);
|
|
329
|
+
let f = !1;
|
|
330
|
+
u.forEach(($) => {
|
|
331
|
+
const p = z.join(r, $), w = b.statSync(p).isDirectory();
|
|
332
|
+
w ? f = !0 : o.push({
|
|
333
|
+
localPath: p,
|
|
342
334
|
// 本地路径
|
|
343
|
-
remotePath: `${c}${
|
|
335
|
+
remotePath: `${c}${$}`,
|
|
344
336
|
// 远程路径
|
|
345
|
-
level:
|
|
337
|
+
level: l
|
|
346
338
|
// 文件或目录层级
|
|
347
|
-
}),
|
|
348
|
-
}),
|
|
339
|
+
}), w && n(p, `${c}${$}/`, l + 1);
|
|
340
|
+
}), f || t.push(c);
|
|
349
341
|
};
|
|
350
|
-
|
|
342
|
+
n(e, "/", 0), s({
|
|
351
343
|
localFileList: o,
|
|
352
|
-
dirList:
|
|
344
|
+
dirList: t
|
|
353
345
|
});
|
|
354
346
|
});
|
|
355
347
|
}
|
|
@@ -359,22 +351,22 @@ exit
|
|
|
359
351
|
*/
|
|
360
352
|
getArgv(e) {
|
|
361
353
|
const o = process.argv;
|
|
362
|
-
let
|
|
363
|
-
return o.forEach((
|
|
364
|
-
|
|
365
|
-
}),
|
|
354
|
+
let t;
|
|
355
|
+
return o.slice(2).forEach((s) => {
|
|
356
|
+
s === e ? t = "" : s.startsWith(`${e}=`) && (t = s.slice(e.length + 1));
|
|
357
|
+
}), t;
|
|
366
358
|
}
|
|
367
359
|
};
|
|
368
|
-
const A = new
|
|
360
|
+
const A = new O.Agent({
|
|
369
361
|
rejectUnauthorized: !1
|
|
370
|
-
}),
|
|
362
|
+
}), C = M.create({
|
|
371
363
|
proxy: !1
|
|
372
364
|
// 禁用代理
|
|
373
365
|
});
|
|
374
|
-
async function
|
|
375
|
-
var o,
|
|
366
|
+
async function W(m, e) {
|
|
367
|
+
var o, t, s;
|
|
376
368
|
try {
|
|
377
|
-
const
|
|
369
|
+
const n = {
|
|
378
370
|
// baseUrl: 'https://paas-test.ymygz.com:10443/v3', // Rancher API 基础地址
|
|
379
371
|
// token: 'token-2gqbg:pwpqvzqfbj26h9flwzbptcpz5sqn7jwh5c9ht2lfx46j4xz58875fr', // Rancher API 访问令牌
|
|
380
372
|
// clusterId: 'local:p-r7j28', // Rancher 集群 ID
|
|
@@ -388,46 +380,50 @@ async function z(u, e) {
|
|
|
388
380
|
},
|
|
389
381
|
...e
|
|
390
382
|
};
|
|
391
|
-
if (!
|
|
383
|
+
if (!n.baseUrl)
|
|
392
384
|
return console.error("- Rancher API 基础地址未配置,请检查 rancherConfig.baseUrl 是否正确");
|
|
393
|
-
if (!
|
|
385
|
+
if (!n.token)
|
|
394
386
|
return console.error("- Rancher API 访问令牌未配置,请检查 rancherConfig.token 是否正确");
|
|
395
|
-
if (!
|
|
387
|
+
if (!n.clusterId)
|
|
396
388
|
return console.error("- Rancher 集群 ID 未配置,请检查 rancherConfig.clusterId 是否正确");
|
|
397
|
-
if (!
|
|
389
|
+
if (!n.namespace)
|
|
398
390
|
return console.error("- 服务所在的命名空间未配置,请检查 rancherConfig.namespace 是否正确");
|
|
399
|
-
if (!
|
|
391
|
+
if (!n.workload)
|
|
400
392
|
return console.error("- 工作负载名称未配置,请检查 rancherConfig.workload 是否正确");
|
|
401
|
-
if (!
|
|
393
|
+
if (!m)
|
|
402
394
|
return console.error("- 内部错误");
|
|
403
|
-
const r =
|
|
395
|
+
const r = m.match(/- 镜像地址:\s*(.+)/);
|
|
404
396
|
let c = "";
|
|
405
397
|
if (r && r[1])
|
|
406
398
|
c = r[1].trim();
|
|
407
399
|
else
|
|
408
400
|
return console.error("- 未提取到镜像地址");
|
|
409
401
|
console.log("- 开始调用 Rancher API 更新服务镜像地址");
|
|
410
|
-
const
|
|
411
|
-
console.log("- 请求的工作负载 URL:",
|
|
412
|
-
const
|
|
413
|
-
headers: { Authorization: `Bearer ${
|
|
402
|
+
const l = `${n.baseUrl}/projects/${n.clusterId}/workloads/deployment:${n.namespace}:${n.workload}`;
|
|
403
|
+
console.log("- 请求的工作负载 URL:", l);
|
|
404
|
+
const u = await C.get(l, {
|
|
405
|
+
headers: { Authorization: `Bearer ${n.token}` },
|
|
414
406
|
httpsAgent: A
|
|
415
407
|
});
|
|
416
|
-
if (!
|
|
408
|
+
if (!u.data || !u.data.containers || u.data.containers.length === 0)
|
|
417
409
|
return console.error("- 未找到工作负载的容器信息,请检查 namespace 和 workload 是否正确");
|
|
418
|
-
const
|
|
419
|
-
|
|
420
|
-
headers: { Authorization: `Bearer ${
|
|
410
|
+
const f = u.data;
|
|
411
|
+
return f.containers[0].image = c, await C.put(l, f, {
|
|
412
|
+
headers: { Authorization: `Bearer ${n.token}` },
|
|
421
413
|
httpsAgent: A
|
|
422
|
-
}), console.log("- 镜像地址更新成功,正在重启服务..."), await
|
|
423
|
-
`${
|
|
414
|
+
}), console.log("- 镜像地址更新成功,正在重启服务..."), await C.post(
|
|
415
|
+
`${l}?action=redeploy`,
|
|
424
416
|
{},
|
|
425
417
|
{
|
|
426
|
-
headers: { Authorization: `Bearer ${
|
|
418
|
+
headers: { Authorization: `Bearer ${n.token}` },
|
|
427
419
|
httpsAgent: A
|
|
428
420
|
}
|
|
429
|
-
), console.log("- 服务重启成功");
|
|
430
|
-
} catch (
|
|
431
|
-
console.error("- 调用 Rancher API 失败,请检查以下信息:"), console.error(" - 错误代码:",
|
|
421
|
+
), console.log("- 服务重启成功"), !0;
|
|
422
|
+
} catch (a) {
|
|
423
|
+
return console.error("- 调用 Rancher API 失败,请检查以下信息:"), console.error(" - 错误代码:", a.code), console.error(" - 错误信息:", a.message), console.error(" - 请求 URL:", (o = a.config) == null ? void 0 : o.url), console.error(" - 响应状态:", (t = a.response) == null ? void 0 : t.status), console.error(" - 响应数据:", (s = a.response) == null ? void 0 : s.data), !1;
|
|
432
424
|
}
|
|
433
425
|
}
|
|
426
|
+
const X = /* @__PURE__ */ E(T);
|
|
427
|
+
export {
|
|
428
|
+
X as default
|
|
429
|
+
};
|
package/dist/yj-deploy.umd.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
`+
|
|
3
|
-
`+c),
|
|
1
|
+
"use strict";const x=require("single-line-log"),L=require("path"),U=require("fs"),q=require("ssh2"),F=require("axios"),N=require("https");function R(p){return p&&p.__esModule&&Object.prototype.hasOwnProperty.call(p,"default")?p.default:p}const{stdout:E}=x,z=L,b=U,{Client:B}=q,M=F,O=N;var T=class{config={host:"",port:"",username:"",password:"",namespace:"",imageStore:"dev-images",tmpName:"dev",delay:0,fileDir:"",rootDir:"/home/yjweb",dockerfile:{name:"Dockerfile",content:["FROM harbor.yunjingtech.cn:30002/yj-base/nginx:latest","RUN rm -rf /usr/share/nginx/html/*","COPY dist /usr/share/nginx/html/"]},upload:{name:"upload.sh",content:["#!/bin/sh","tag=`basename \\`pwd\\``:$2-`date +%Y%m%d`",'echo "- 镜像仓库: $1"','echo "- 镜像环境: $2"',"docker build -t harbor.yunjingtech.cn:30002/$1/$tag .","docker push harbor.yunjingtech.cn:30002/$1/$tag","if [ $? -eq 0 ];then","echo - 推送成功","echo - 镜像地址: harbor.yunjingtech.cn:30002/$1/$tag","else","echo - 镜像推送失败,请重试或联系运维人员,如需查看全部docker日志,请在命令后添加 --allLog查看更多信息","fi"]},dist:{name:"dist"},parallelDir:20,parallelFile:50,allLog:!1,lazyUpload:!1,rePushNum:3,rancherConfig:{token:"",namespace:"",workload:""}};ssh2Conn=null;uploading=!1;uploadPromise=null;trim=null;constructor(e){return this.config=Object.assign(this.config,e),{name:"yj-deploy",isPut:this.isPut.bind(this),upload:this.upload.bind(this),getDeployInfo:()=>({namespace:this.config.namespace,fileDir:this.config.fileDir}),apply:this.apply.bind(this),closeBundle:this.isPut.bind(this)}}apply(e){return e&&e.hooks&&e.hooks.done&&e.hooks.done.tap("yj-deploy",()=>{this.isPut()}),"build"}isPut(){this.getArgv("--deploy")!=null&&(clearTimeout(this.trim),this.trim=setTimeout(()=>{this.upload()},this.config.delay||0))}upload(){return this.uploadPromise?this.uploadPromise:(console.log("-------------- deploy-start --------------"),this.uploading=!0,this.uploadPromise=this.runUpload().catch(e=>(console.error(`- 部署失败:${e.message||e}`),!1)).finally(()=>{this.breakConnect(),this.uploadPromise=null}),this.uploadPromise)}async runUpload(){for(const o of["host","port","username","password","namespace","fileDir"])if(!this.config[o])throw new Error(`请配置 ${o}`);const e=await this.getFilesInDirectory(this.config.fileDir);if(!e.localFileList.length)throw new Error("待上传目录没有文件,请先编译项目");return new Promise((o,t)=>{const s=new B;this.ssh2Conn=s,s.on("error",t),s.on("close",()=>t(new Error("SSH 连接在部署完成前关闭"))),s.on("ready",()=>{console.log("- 跳板机连接成功!"),(async()=>{const n=await new Promise((c,l)=>{s.sftp((u,f)=>u?l(u):c(f))});return this.getArgv("--reset")!==void 0&&(console.log("- 重置项目"),await this.onShell(`rm -r ${this.config.rootDir}/${this.config.namespace}`)),await this.upLoadProject(n,`${this.config.rootDir}/${this.config.namespace}`,e)!==null})().then(o,t)}),s.connect({host:this.config.host,port:this.config.port,username:this.config.username,password:this.config.password})})}breakConnect(){this.uploading=!1,this.ssh2Conn&&(this.ssh2Conn.end(),this.ssh2Conn=null,console.log("- 已断开连接")),console.log("-------------- deploy-end --------------")}onShell(e){const o=this.getArgv("--allLog")!==void 0;return new Promise((t,s)=>{this.ssh2Conn.shell((a,n)=>{if(a){console.log("- 远程命令错误:"+a,e),s(a);return}let r=[];n.on("close",()=>{t(r.toString())}).on("error",s).on("data",c=>{let l=c.toString();(o||this.config.allLog||l.startsWith("- "))&&r.push(`
|
|
2
|
+
`+l)}).stderr.on("data",c=>{console.log(`- 远程命令错误:
|
|
3
|
+
`+c),s(c)}),n.end(e+`
|
|
4
4
|
exit
|
|
5
|
-
`)})})}projectInit(e,o){
|
|
6
|
-
`)}' > ${t}`),console.log("- 正在创建upload.sh");const
|
|
7
|
-
`)}' > ${
|
|
8
|
-
- warning 检测到当前脚本为手动创建,不支持镜像仓库配置,imageStore将失效`),console.log("- warning 如需支持imageStore参数,请在命令行后添加 --reset 重新生成脚本");const
|
|
9
|
-
- warning 检测到当前脚本可能不是最新版本,推送失败可能不会提示,且没有失败自动重试功能,请在命令后添加 --reset 重新生成脚本`),
|
|
5
|
+
`)})})}async projectInit(e,o){await new Promise((a,n)=>{e.mkdir(o,r=>r?n(r):a())}),console.log("- 正在创建dockerfile");const t=`${o}/${this.config.dockerfile.name}`;await this.onShell(`touch ${t}`),await this.onShell(`echo '${this.config.dockerfile.content.join(`
|
|
6
|
+
`)}' > ${t}`),console.log("- 正在创建upload.sh脚本");const s=`${o}/${this.config.upload.name}`;await this.onShell(`touch ${s}`),await this.onShell(`echo '${this.config.upload.content.join(`
|
|
7
|
+
`)}' > ${s}`)}checkRemoteFile(e,o){return new Promise(t=>{e.stat(o,s=>{t(!s)})})}async upLoadProject(e,o,t){let s=0;typeof this.config.rePushNum=="number"&&this.config.rePushNum>0&&(s=this.config.rePushNum),await new Promise((i,d)=>{e.readdir(o,h=>{h?h.code===2||h.code==="ENOENT"?i(!1):d(h):i(!0)})})||(console.log("- 跳板机不存在项目, 开始创建项目"),await this.projectInit(e,o),console.log(`- ${this.config.namespace}项目创建成功`));const n=this.getArgv("--lazyUpload")!==void 0||this.config.lazyUpload,r=`${o}/${this.config.dist.name}`;n||await this.onShell(`rm -rf ${r}`),await this.onShell(`mkdir -p ${r}`);const{localFileList:c,dirList:l}=t;let u=0,f=c.length;const $=i=>new Promise(async(d,h)=>{try{if(n&&await this.checkRemoteFile(e,`${r}${i.remotePath}`))return d();const g=b.createReadStream(i.localPath),P=e.createWriteStream(`${r}${i.remotePath}`);g.on("error",y=>{h(y),P.destroy()}),P.on("close",()=>{this.progressBar(++u,f),d()}),P.on("error",y=>{console.log(`- 文件 ${i.remotePath} 上传失败:${y}`),h(y),g.destroy()}),g.pipe(P)}catch(g){h(g)}}),m=(i,d)=>{let h=[];for(let g=0;g<i.length;g+=d)h.push(i.slice(g,g+d));return h},k=i=>{if(i.length===0)return Promise.resolve();const d=i.shift();return Promise.all(d.map(h=>$(h))).then(()=>k(i))},w=i=>{if(i.length===0)return Promise.resolve();let h=i.shift().map(g=>`${r}${g}`).join(" ");return this.onShell(`mkdir -p ${h}`).then(()=>w(i))};let I=[...m(l,this.config.parallelDir||20)];await w(I),console.log("- 创建目录完成"),await new Promise(i=>{setTimeout(()=>{i()},500)});let j=[...m(c,this.config.parallelFile||50)];await k(j),n&&(this.progressBar(f,f),console.log("\x1B[32m%s\x1B[0m",`- 已开启懒上传,本次共上传 ${u} 个文件`)),console.log("- 文件上传成功"),console.log("- 开始推送镜像");let S=`cd ${o} && sh ${this.config.upload.name}`;if(await this.isNewShell(e,o)){const i=this.getArgv("--imageStore")||this.config.imageStore;S+=` ${i}`}else console.log(`
|
|
8
|
+
- warning 检测到当前脚本为手动创建,不支持镜像仓库配置,imageStore将失效`),console.log("- warning 如需支持imageStore参数,请在命令行后添加 --reset 重新生成脚本");const v=this.getArgv("--tmpName")||this.config.tmpName;S+=` ${v}`;const D=Array.from({length:s+1},()=>()=>this.onShell(S));return this.pushMirrorImage(D,D.length)}async pushMirrorImage(e,o){if(e.length===0)return console.log("- 镜像推送失败,请添加 --allLog 查看全部日志"),null;const t=await e[0]();return t.includes("- 镜像推送失败")?(console.log(t),e.length>1&&console.log(`- 正在尝试第 ${o-e.length+1} 次推送...`),this.pushMirrorImage(e.slice(1),o)):(console.log(t),console.log("- 镜像推送结束"),this.getArgv("--restart")!==void 0&&!await W(t,this.config.rancherConfig)?null:t)}async isNewShell(e,o){return new Promise((t,s)=>{e.readFile(`${o}/${this.config.upload.name}`,(a,n)=>{if(a)return s(!1);const r=n.toString();r.includes("- 镜像推送失败")||console.log(`
|
|
9
|
+
- warning 检测到当前脚本可能不是最新版本,推送失败可能不会提示,且没有失败自动重试功能,请在命令后添加 --reset 重新生成脚本`),t(r.includes("$2"))})})}progressBar(e,o,t=25){let s=(e/o).toFixed(4),a=Math.floor(s*t),n="";for(let l=0;l<a;l++)n+="█";let r="";for(let l=0;l<t-a;l++)r+="░";let c=`- 文件上传进度: ${n}${r} (${e}/${o}) ${(100*s).toFixed(2)}%`;E(c),e==o&&console.log("")}getFilesInDirectory(e){const o=[],t=[];return new Promise((s,a)=>{const n=(r,c,l)=>{const u=b.readdirSync(r);let f=!1;u.forEach($=>{const m=z.join(r,$),w=b.statSync(m).isDirectory();w?f=!0:o.push({localPath:m,remotePath:`${c}${$}`,level:l}),w&&n(m,`${c}${$}/`,l+1)}),f||t.push(c)};n(e,"/",0),s({localFileList:o,dirList:t})})}getArgv(e){const o=process.argv;let t;return o.slice(2).forEach(s=>{s===e?t="":s.startsWith(`${e}=`)&&(t=s.slice(e.length+1))}),t}};const A=new O.Agent({rejectUnauthorized:!1}),C=M.create({proxy:!1});async function W(p,e){var o,t,s;try{const n={...{baseUrl:"https://paas-test.ymygz.com:10443/v3",clusterId:"local:p-r7j28"},...e};if(!n.baseUrl)return console.error("- Rancher API 基础地址未配置,请检查 rancherConfig.baseUrl 是否正确");if(!n.token)return console.error("- Rancher API 访问令牌未配置,请检查 rancherConfig.token 是否正确");if(!n.clusterId)return console.error("- Rancher 集群 ID 未配置,请检查 rancherConfig.clusterId 是否正确");if(!n.namespace)return console.error("- 服务所在的命名空间未配置,请检查 rancherConfig.namespace 是否正确");if(!n.workload)return console.error("- 工作负载名称未配置,请检查 rancherConfig.workload 是否正确");if(!p)return console.error("- 内部错误");const r=p.match(/- 镜像地址:\s*(.+)/);let c="";if(r&&r[1])c=r[1].trim();else return console.error("- 未提取到镜像地址");console.log("- 开始调用 Rancher API 更新服务镜像地址");const l=`${n.baseUrl}/projects/${n.clusterId}/workloads/deployment:${n.namespace}:${n.workload}`;console.log("- 请求的工作负载 URL:",l);const u=await C.get(l,{headers:{Authorization:`Bearer ${n.token}`},httpsAgent:A});if(!u.data||!u.data.containers||u.data.containers.length===0)return console.error("- 未找到工作负载的容器信息,请检查 namespace 和 workload 是否正确");const f=u.data;return f.containers[0].image=c,await C.put(l,f,{headers:{Authorization:`Bearer ${n.token}`},httpsAgent:A}),console.log("- 镜像地址更新成功,正在重启服务..."),await C.post(`${l}?action=redeploy`,{},{headers:{Authorization:`Bearer ${n.token}`},httpsAgent:A}),console.log("- 服务重启成功"),!0}catch(a){return console.error("- 调用 Rancher API 失败,请检查以下信息:"),console.error(" - 错误代码:",a.code),console.error(" - 错误信息:",a.message),console.error(" - 请求 URL:",(o=a.config)==null?void 0:o.url),console.error(" - 响应状态:",(t=a.response)==null?void 0:t.status),console.error(" - 响应数据:",(s=a.response)==null?void 0:s.data),!1}}const Y=R(T);module.exports=Y;
|
package/lib/cli.cjs
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
const fs = require('node:fs')
|
|
2
|
+
const path = require('node:path')
|
|
3
|
+
const { createRequire } = require('node:module')
|
|
4
|
+
const { pathToFileURL } = require('node:url')
|
|
5
|
+
|
|
6
|
+
const configNames = {
|
|
7
|
+
vite: ['js', 'mjs', 'ts', 'cjs', 'mts', 'cts'].map(ext => `vite.config.${ext}`),
|
|
8
|
+
webpack: ['js', 'cjs', 'mjs', 'ts', 'mts', 'cts'].map(ext => `webpack.config.${ext}`),
|
|
9
|
+
'vue-cli': ['vue.config.js'],
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const help = `用法:yj-deploy [选项]
|
|
13
|
+
|
|
14
|
+
直接部署已有的构建产物,复用 Vite / Webpack / Vue CLI 中的 yj-deploy 配置。
|
|
15
|
+
|
|
16
|
+
-c, --config <文件> 指定现有构建配置文件
|
|
17
|
+
-m, --mode <环境> 加载配置时使用的 mode,默认 production
|
|
18
|
+
--tool <类型> vite、webpack 或 vue-cli(自定义配置名时可指定)
|
|
19
|
+
--namespace <名称> 有多个部署插件时,选择其 namespace
|
|
20
|
+
--env <键[=值]> 传给 Webpack 配置函数的 env,可重复指定
|
|
21
|
+
--dry-run 仅检查配置和产物,不连接服务器
|
|
22
|
+
--tmpName <环境> 覆盖镜像环境
|
|
23
|
+
--imageStore <仓库> 覆盖镜像仓库
|
|
24
|
+
--restart 推送成功后更新并重启 Rancher 服务
|
|
25
|
+
--reset 重置远程项目目录
|
|
26
|
+
--lazyUpload 跳过远程已有文件
|
|
27
|
+
--allLog 显示完整远程日志
|
|
28
|
+
-h, --help 显示帮助
|
|
29
|
+
-v, --version 显示版本
|
|
30
|
+
|
|
31
|
+
示例:
|
|
32
|
+
yj-deploy --mode development --restart
|
|
33
|
+
yj-deploy --config config/webpack.prod.js --tool webpack
|
|
34
|
+
yj-deploy --dry-run
|
|
35
|
+
`
|
|
36
|
+
|
|
37
|
+
// 使用 Node 14 可用的语法解析参数,避免依赖 Node 18 才提供的 util.parseArgs。
|
|
38
|
+
function parseOptions(args) {
|
|
39
|
+
const stringOptions = new Set(['config', 'mode', 'tool', 'namespace', 'env', 'tmpName', 'imageStore'])
|
|
40
|
+
const booleanOptions = new Set(['dry-run', 'restart', 'reset', 'lazyUpload', 'allLog', 'deploy', 'help', 'version'])
|
|
41
|
+
const aliases = { '-c': 'config', '-m': 'mode', '-h': 'help', '-v': 'version' }
|
|
42
|
+
const values = Object.assign(Object.create(null), { mode: 'production' })
|
|
43
|
+
|
|
44
|
+
for (let index = 0; index < args.length; index++) {
|
|
45
|
+
const argument = args[index]
|
|
46
|
+
if (argument === '--' && index === args.length - 1) break
|
|
47
|
+
const separator = argument.indexOf('=')
|
|
48
|
+
const option = separator < 0 ? argument : argument.slice(0, separator)
|
|
49
|
+
const name = option.startsWith('--') ? option.slice(2) : aliases[option]
|
|
50
|
+
if (!stringOptions.has(name) && !booleanOptions.has(name)) {
|
|
51
|
+
throw new Error(`不支持的参数:${option},请使用 --help 查看用法`)
|
|
52
|
+
}
|
|
53
|
+
if (booleanOptions.has(name)) {
|
|
54
|
+
if (separator >= 0) throw new Error(`--${name} 是开关参数,不需要赋值`)
|
|
55
|
+
values[name] = true
|
|
56
|
+
continue
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 只在第一个等号处分割,保留 env 等参数值中可能包含的其他等号。
|
|
60
|
+
const value = separator >= 0 ? argument.slice(separator + 1) : args[++index]
|
|
61
|
+
if (!value || (separator < 0 && value.startsWith('-'))) {
|
|
62
|
+
throw new Error(`--${name} 需要参数值;以 - 开头的值请使用 --${name}=值 的形式`)
|
|
63
|
+
}
|
|
64
|
+
if (name === 'env') {
|
|
65
|
+
if (!values.env) values.env = []
|
|
66
|
+
values.env.push(value)
|
|
67
|
+
} else {
|
|
68
|
+
values[name] = value
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (values.tool && !Object.prototype.hasOwnProperty.call(configNames, values.tool)) {
|
|
72
|
+
throw new Error('--tool 只能是 vite、webpack 或 vue-cli')
|
|
73
|
+
}
|
|
74
|
+
return values
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 从业务项目解析依赖,确保配置使用项目自身安装的 Vite / Vue CLI 版本。
|
|
78
|
+
function resolveProjectModule(projectRequire, name) {
|
|
79
|
+
try {
|
|
80
|
+
return projectRequire.resolve(name)
|
|
81
|
+
} catch (error) {
|
|
82
|
+
if (error.code !== 'MODULE_NOT_FOUND') throw error
|
|
83
|
+
throw new Error(`当前项目缺少 ${name},请先安装该项目的构建依赖`)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function findConfig(cwd, options, projectRequire) {
|
|
88
|
+
if (options.config) {
|
|
89
|
+
const filename = path.resolve(cwd, options.config)
|
|
90
|
+
if (!fs.existsSync(filename) || !fs.statSync(filename).isFile()) {
|
|
91
|
+
throw new Error(`配置文件不存在:${filename}`)
|
|
92
|
+
}
|
|
93
|
+
const basename = path.basename(filename)
|
|
94
|
+
const tool = options.tool || Object.keys(configNames).find(name => configNames[name].includes(basename))
|
|
95
|
+
if (!tool) throw new Error('自定义配置文件名需要通过 --tool 指定 vite、webpack 或 vue-cli')
|
|
96
|
+
return { tool, filename }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const matches = []
|
|
100
|
+
for (const [tool, names] of Object.entries(configNames)) {
|
|
101
|
+
if (options.tool && tool !== options.tool) continue
|
|
102
|
+
for (const name of names) {
|
|
103
|
+
const filename = path.join(cwd, name)
|
|
104
|
+
if (fs.existsSync(filename) && fs.statSync(filename).isFile()) matches.push({ tool, filename })
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (matches.length > 1) {
|
|
108
|
+
throw new Error(`找到多个构建配置,请用 --config 明确选择:${matches.map(item => path.basename(item.filename)).join('、')}`)
|
|
109
|
+
}
|
|
110
|
+
if (matches.length === 1) return matches[0]
|
|
111
|
+
|
|
112
|
+
// Vue CLI 也允许把配置放在 package.json 的 vue 字段中,此时没有独立配置文件。
|
|
113
|
+
if (!options.tool || options.tool === 'vue-cli') {
|
|
114
|
+
try {
|
|
115
|
+
projectRequire.resolve('@vue/cli-service/lib/Service')
|
|
116
|
+
return { tool: 'vue-cli', filename: undefined }
|
|
117
|
+
} catch (error) {
|
|
118
|
+
if (error.code !== 'MODULE_NOT_FOUND') throw error
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
throw new Error('未找到构建配置,请在项目根目录执行,或使用 --config 和 --tool 指定配置')
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function webpackEnv(values = []) {
|
|
125
|
+
// 使用无原型对象,避免命令行键名覆盖对象的原型属性。
|
|
126
|
+
const env = Object.assign(Object.create(null), { WEBPACK_BUNDLE: true, WEBPACK_BUILD: true })
|
|
127
|
+
for (const value of values) {
|
|
128
|
+
const separator = value.indexOf('=')
|
|
129
|
+
const key = separator < 0 ? value : value.slice(0, separator)
|
|
130
|
+
if (!key) throw new Error('--env 的键名不能为空')
|
|
131
|
+
env[key] = separator < 0 ? true : value.slice(separator + 1)
|
|
132
|
+
}
|
|
133
|
+
return env
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function loadConfig(selection, options, cwd, projectRequire) {
|
|
137
|
+
const { tool, filename } = selection
|
|
138
|
+
if (tool === 'vite') {
|
|
139
|
+
const imported = await import(pathToFileURL(resolveProjectModule(projectRequire, 'vite')).href)
|
|
140
|
+
const vite = imported.loadConfigFromFile ? imported : imported.default
|
|
141
|
+
// 只加载配置,不调用 build / resolveConfig,也不运行插件的构建钩子。
|
|
142
|
+
const loaded = await vite.loadConfigFromFile({ command: 'build', mode: options.mode }, filename, cwd)
|
|
143
|
+
if (!loaded) throw new Error('Vite 配置加载失败')
|
|
144
|
+
return loaded.config
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (tool === 'vue-cli') {
|
|
148
|
+
const Service = projectRequire(resolveProjectModule(projectRequire, '@vue/cli-service/lib/Service'))
|
|
149
|
+
if (filename) process.env.VUE_CLI_SERVICE_CONFIG_PATH = filename
|
|
150
|
+
process.env.VUE_CLI_SERVICE_BUILD_TARGET = 'app'
|
|
151
|
+
const service = new Service(cwd)
|
|
152
|
+
// init 会加载 .env 和执行 configureWebpack / chainWebpack,但不会启动编译。
|
|
153
|
+
await service.init(options.mode)
|
|
154
|
+
return service.resolveWebpackConfig()
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const env = webpackEnv(options.env)
|
|
158
|
+
if (/\.(?:ts|mts|cts)$/.test(filename)) {
|
|
159
|
+
// TypeScript 配置交给项目的 webpack-cli,复用项目已配置的 TS 加载器。
|
|
160
|
+
const WebpackCLI = projectRequire(resolveProjectModule(projectRequire, 'webpack-cli'))
|
|
161
|
+
const cli = new WebpackCLI()
|
|
162
|
+
cli.webpack = projectRequire(resolveProjectModule(projectRequire, 'webpack'))
|
|
163
|
+
if (typeof cli.loadConfig !== 'function') throw new Error('当前 webpack-cli 版本不支持加载 TypeScript 配置')
|
|
164
|
+
const loaded = await cli.loadConfig({ config: [filename], mode: options.mode, env })
|
|
165
|
+
return loaded.options
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Node 的动态导入同时支持 CommonJS 和 ESM;配置函数接收与构建时一致的 env / argv。
|
|
169
|
+
const imported = await import(pathToFileURL(filename).href)
|
|
170
|
+
async function resolveConfig(value) {
|
|
171
|
+
value = await value
|
|
172
|
+
if (typeof value === 'function') return resolveConfig(value(env, { mode: options.mode, env }))
|
|
173
|
+
if (Array.isArray(value)) return Promise.all(value.map(resolveConfig))
|
|
174
|
+
return value
|
|
175
|
+
}
|
|
176
|
+
return resolveConfig(imported.default ?? imported)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function findDeployPlugins(config) {
|
|
180
|
+
const plugins = new Set()
|
|
181
|
+
async function visitPlugins(value) {
|
|
182
|
+
value = await value
|
|
183
|
+
if (Array.isArray(value)) {
|
|
184
|
+
for (const item of value) await visitPlugins(item)
|
|
185
|
+
} else if (value && value.name === 'yj-deploy' && typeof value.upload === 'function') {
|
|
186
|
+
plugins.add(value)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
async function visitConfig(value) {
|
|
190
|
+
value = await value
|
|
191
|
+
if (Array.isArray(value)) {
|
|
192
|
+
for (const item of value) await visitConfig(item)
|
|
193
|
+
} else if (value) {
|
|
194
|
+
await visitPlugins(value.plugins)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// 支持 Webpack 多配置,以及 Vite 插件数组中的嵌套数组、Promise 和 false。
|
|
198
|
+
await visitConfig(config)
|
|
199
|
+
return [...plugins]
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function checkArtifacts(fileDir) {
|
|
203
|
+
if (!fileDir || !fs.existsSync(fileDir) || !fs.statSync(fileDir).isDirectory()) {
|
|
204
|
+
throw new Error(`待部署目录不存在,请先编译项目:${fileDir || '(未配置 fileDir)'}`)
|
|
205
|
+
}
|
|
206
|
+
const pending = [fileDir]
|
|
207
|
+
while (pending.length) {
|
|
208
|
+
const directory = pending.pop()
|
|
209
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
210
|
+
if (entry.isFile()) return
|
|
211
|
+
if (entry.isDirectory()) pending.push(path.join(directory, entry.name))
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
throw new Error(`待部署目录没有文件,请先编译项目:${fileDir}`)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function run(args = process.argv.slice(2), cwd = process.cwd()) {
|
|
218
|
+
const options = parseOptions(args)
|
|
219
|
+
if (options.help) return console.log(help)
|
|
220
|
+
if (options.version) return console.log(require('../package.json').version)
|
|
221
|
+
|
|
222
|
+
cwd = path.resolve(cwd)
|
|
223
|
+
const projectRequire = createRequire(path.join(cwd, 'package.json'))
|
|
224
|
+
const selection = findConfig(cwd, options, projectRequire)
|
|
225
|
+
const previousArgv = process.argv
|
|
226
|
+
const previousCwd = process.cwd()
|
|
227
|
+
const envNames = ['NODE_ENV', 'VUE_CLI_SERVICE_CONFIG_PATH', 'VUE_CLI_SERVICE_BUILD_TARGET']
|
|
228
|
+
const previousEnv = Object.fromEntries(envNames.map(name => [name, process.env[name]]))
|
|
229
|
+
try {
|
|
230
|
+
process.chdir(cwd)
|
|
231
|
+
// 规范化参数供原有 getArgv 使用;--deploy 也让按该标记注册的插件正常出现在配置中。
|
|
232
|
+
process.argv = [process.execPath, previousArgv[1], ...args, '--deploy']
|
|
233
|
+
for (const [key, value] of Object.entries(options)) {
|
|
234
|
+
if (['tmpName', 'imageStore'].includes(key)) process.argv.push(`--${key}=${value}`)
|
|
235
|
+
if (['restart', 'reset', 'lazyUpload', 'allLog'].includes(key) && value) process.argv.push(`--${key}`)
|
|
236
|
+
}
|
|
237
|
+
if (!process.env.NODE_ENV) {
|
|
238
|
+
process.env.NODE_ENV = selection.tool === 'vue-cli'
|
|
239
|
+
? (['production', 'test'].includes(options.mode) ? options.mode : 'development')
|
|
240
|
+
: (selection.tool === 'webpack' && options.mode === 'development' ? 'development' : 'production')
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const config = await loadConfig(selection, options, cwd, projectRequire)
|
|
244
|
+
let plugins = await findDeployPlugins(config)
|
|
245
|
+
if (!plugins.length) throw new Error('当前配置中未找到 yj-deploy 插件,请检查 --mode 和插件注册条件')
|
|
246
|
+
if (plugins.some(plugin => typeof plugin.getDeployInfo !== 'function')) {
|
|
247
|
+
throw new Error('配置中的 yj-deploy 版本过旧,请先升级当前项目的 yj-deploy')
|
|
248
|
+
}
|
|
249
|
+
if (options.namespace) plugins = plugins.filter(plugin => plugin.getDeployInfo().namespace === options.namespace)
|
|
250
|
+
if (!plugins.length) throw new Error(`未找到 namespace 为 ${options.namespace} 的部署插件`)
|
|
251
|
+
if (plugins.length > 1) throw new Error('找到多个部署插件,请用 --namespace 选择唯一的部署目标')
|
|
252
|
+
|
|
253
|
+
const plugin = plugins[0]
|
|
254
|
+
const info = plugin.getDeployInfo()
|
|
255
|
+
checkArtifacts(info.fileDir)
|
|
256
|
+
console.log(`- 使用 ${selection.tool} 配置:${selection.filename || 'package.json / Vue CLI 默认配置'}`)
|
|
257
|
+
// 只展示定位部署所需的信息,不输出主机密码、Rancher token 等敏感配置。
|
|
258
|
+
console.log(`- 部署项目:${info.namespace || '(未配置 namespace)'}`)
|
|
259
|
+
console.log(`- 部署目录:${path.resolve(info.fileDir)}`)
|
|
260
|
+
if (options['dry-run']) {
|
|
261
|
+
console.log('- 检查通过,未执行上传或编译')
|
|
262
|
+
return
|
|
263
|
+
}
|
|
264
|
+
const success = await plugin.upload()
|
|
265
|
+
if (success !== true) throw new Error('部署未完成,请根据上方日志检查后重试')
|
|
266
|
+
} finally {
|
|
267
|
+
// 便于程序化调用和测试,执行完后恢复 CLI 临时设置的进程状态。
|
|
268
|
+
process.argv = previousArgv
|
|
269
|
+
process.chdir(previousCwd)
|
|
270
|
+
for (const name of envNames) {
|
|
271
|
+
if (previousEnv[name] === undefined) delete process.env[name]
|
|
272
|
+
else process.env[name] = previousEnv[name]
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
module.exports = { run }
|
package/package.json
CHANGED
|
@@ -1,17 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yj-deploy",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.12",
|
|
4
4
|
"description": "ssh sftp",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"build": "vite build",
|
|
7
|
+
"prepack": "npm run build",
|
|
8
|
+
"pretest": "npm run build",
|
|
9
|
+
"test": "node --test tests/cli.test.cjs tests/upload.test.cjs",
|
|
7
10
|
"deploy": "node test.js --restart"
|
|
8
11
|
},
|
|
9
12
|
"files": [
|
|
10
13
|
"dist",
|
|
14
|
+
"bin",
|
|
15
|
+
"lib/cli.cjs",
|
|
11
16
|
"package.json",
|
|
12
17
|
"README.md"
|
|
13
18
|
],
|
|
14
19
|
"main": "./dist/yj-deploy.umd.js",
|
|
20
|
+
"bin": {
|
|
21
|
+
"yj-deploy": "./bin/yj-deploy.cjs"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=14.18.0"
|
|
25
|
+
},
|
|
15
26
|
"module": "./dist/yj-deploy.mjs",
|
|
16
27
|
"exports": {
|
|
17
28
|
".": {
|