@infly/libs 2.0.45 → 2.0.47
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 +186 -142
- package/bin/cli.js +148 -120
- package/build/docker/docker-build-push.js +373 -240
- package/build/docker/release-docker.js +199 -0
- package/package.json +2 -1
|
@@ -1,240 +1,373 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Docker 镜像构建与推送(infly-libs 公共模块)
|
|
3
|
-
*
|
|
4
|
-
* 支持两种运行模式(自动检测):
|
|
5
|
-
*
|
|
6
|
-
* 1. Monorepo 模式 — 检测到 git submodule (--show-superproject-working-tree 有值)
|
|
7
|
-
* 构建上下文 = superproject 根,Dockerfile 使用 Dockerfile.monorepo
|
|
8
|
-
* @infly/* 包从 workspace 源码解析
|
|
9
|
-
*
|
|
10
|
-
* 2. 独立模式 — 非 submodule 且 cwd = git 根
|
|
11
|
-
* 构建上下文 = 当前目录,Dockerfile 使用 Dockerfile
|
|
12
|
-
* @infly/* 包从 npm registry 安装
|
|
13
|
-
*
|
|
14
|
-
* 用法:
|
|
15
|
-
* infly-libs build:docker [--local] [--
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Docker 镜像构建与推送(infly-libs 公共模块)
|
|
3
|
+
*
|
|
4
|
+
* 支持两种运行模式(自动检测):
|
|
5
|
+
*
|
|
6
|
+
* 1. Monorepo 模式 — 检测到 git submodule (--show-superproject-working-tree 有值)
|
|
7
|
+
* 构建上下文 = superproject 根,Dockerfile 使用 Dockerfile.monorepo
|
|
8
|
+
* @infly/* 包从 workspace 源码解析
|
|
9
|
+
*
|
|
10
|
+
* 2. 独立模式 — 非 submodule 且 cwd = git 根
|
|
11
|
+
* 构建上下文 = 当前目录,Dockerfile 使用 Dockerfile
|
|
12
|
+
* @infly/* 包从 npm registry 安装
|
|
13
|
+
*
|
|
14
|
+
* 用法:
|
|
15
|
+
* infly-libs build:docker [--local] [--standalone] [--commit-only]
|
|
16
|
+
* [--env prod|dev] [--dry-run] [--cicd-url URL]
|
|
17
|
+
* [--health-check] [--health-timeout SECONDS] [--health-add-host HOST:IP]
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const { execSync } = require("child_process");
|
|
21
|
+
const fs = require("fs");
|
|
22
|
+
const path = require("path");
|
|
23
|
+
|
|
24
|
+
// ---------- 默认配置 ----------
|
|
25
|
+
const DEFAULT_REGISTRY = "ccr.ccs.tencentyun.com";
|
|
26
|
+
const DEFAULT_NAMESPACE = "points";
|
|
27
|
+
|
|
28
|
+
function formatCicdLink(value) {
|
|
29
|
+
if (value === undefined) {
|
|
30
|
+
return "";
|
|
31
|
+
}
|
|
32
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
33
|
+
throw new Error("--cicd-url 缺少参数值");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const rawUrl = value.trim();
|
|
37
|
+
if (/[\u0000-\u001f\u007f]/.test(rawUrl)) {
|
|
38
|
+
throw new Error("--cicd-url 不能包含控制字符");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let parsedUrl;
|
|
42
|
+
try {
|
|
43
|
+
parsedUrl = new URL(rawUrl);
|
|
44
|
+
} catch {
|
|
45
|
+
throw new Error("--cicd-url 必须是合法的 HTTP/HTTPS URL");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!['http:', 'https:'].includes(parsedUrl.protocol)) {
|
|
49
|
+
throw new Error("--cicd-url 只支持 HTTP/HTTPS URL");
|
|
50
|
+
}
|
|
51
|
+
if (parsedUrl.username || parsedUrl.password) {
|
|
52
|
+
throw new Error("--cicd-url 不能包含用户名或密码凭据");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return `发布系统:\x1b[34m${decodeURI(parsedUrl.href)}\x1b[0m`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function detectMode({ standalone = false } = {}) {
|
|
59
|
+
const cwd = path.resolve(process.cwd());
|
|
60
|
+
|
|
61
|
+
if (standalone) {
|
|
62
|
+
const gitRoot = execSync("git rev-parse --show-toplevel", { encoding: "utf-8" }).trim();
|
|
63
|
+
if (path.resolve(cwd) !== path.resolve(gitRoot)) {
|
|
64
|
+
throw new Error("--standalone 必须在当前 Git 仓库根目录执行");
|
|
65
|
+
}
|
|
66
|
+
return { isStandalone: true, contextRoot: cwd };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// git submodule → monorepo 模式,contextRoot = superproject 根
|
|
70
|
+
try {
|
|
71
|
+
const superproject = execSync("git rev-parse --show-superproject-working-tree", {
|
|
72
|
+
encoding: "utf-8", stdio: "pipe",
|
|
73
|
+
}).trim();
|
|
74
|
+
if (superproject) {
|
|
75
|
+
return { isStandalone: false, contextRoot: superproject };
|
|
76
|
+
}
|
|
77
|
+
} catch {}
|
|
78
|
+
|
|
79
|
+
// 非 submodule:cwd = git 根 → 独立模式,否则 monorepo 模式
|
|
80
|
+
const gitRoot = execSync("git rev-parse --show-toplevel", { encoding: "utf-8" }).trim();
|
|
81
|
+
const isStandalone = path.resolve(cwd) === path.resolve(gitRoot);
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
isStandalone,
|
|
85
|
+
contextRoot: isStandalone ? cwd : gitRoot,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function run(cmd, { silent = false, dryRun = false, cwd } = {}) {
|
|
90
|
+
if (dryRun) {
|
|
91
|
+
console.log(` [DRY RUN] ${cmd}`);
|
|
92
|
+
return "";
|
|
93
|
+
}
|
|
94
|
+
return execSync(cmd, {
|
|
95
|
+
stdio: silent ? "pipe" : "inherit",
|
|
96
|
+
cwd: cwd || process.cwd(),
|
|
97
|
+
encoding: "utf-8",
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function normalizeHealthOptions({ healthTimeout = "60", healthAddHost } = {}) {
|
|
102
|
+
const timeoutSeconds = Number(healthTimeout);
|
|
103
|
+
if (!Number.isInteger(timeoutSeconds) || timeoutSeconds < 1 || timeoutSeconds > 600) {
|
|
104
|
+
throw new Error("--health-timeout 必须是 1 到 600 之间的整数秒");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (
|
|
108
|
+
healthAddHost !== undefined &&
|
|
109
|
+
!/^[A-Za-z0-9][A-Za-z0-9._-]*:(?:[A-Fa-f0-9:.]+|host-gateway)$/.test(healthAddHost)
|
|
110
|
+
) {
|
|
111
|
+
throw new Error("--health-add-host 必须使用安全的 HOST:IP 或 HOST:host-gateway 格式");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return { timeoutSeconds, healthAddHost };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function normalizeTagPrefix(value) {
|
|
118
|
+
if (value === undefined || value === null) return "";
|
|
119
|
+
const prefix = String(value).trim();
|
|
120
|
+
if (!prefix) return "";
|
|
121
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,39}$/.test(prefix)) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
"--tag-prefix 必须以字母或数字开头,最多 40 字符,且只包含字母、数字、点、下划线和连字符"
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
return prefix;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function resolveImageTags(imageBase, commitHash, env, commitOnly = false, tagPrefix = "") {
|
|
130
|
+
const tags = [`${imageBase}:${tagPrefix}${commitHash}`];
|
|
131
|
+
if (commitOnly) return tags;
|
|
132
|
+
tags.push(`${imageBase}:${tagPrefix}${env}`);
|
|
133
|
+
if (env === "prod") tags.push(`${imageBase}:${tagPrefix}latest`);
|
|
134
|
+
return tags;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function wait(milliseconds) {
|
|
138
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function healthCheckImage(imageTag, options = {}) {
|
|
142
|
+
const { dryRun = false, imageName = "image" } = options;
|
|
143
|
+
const { timeoutSeconds, healthAddHost } = normalizeHealthOptions(options);
|
|
144
|
+
const safeImageName = imageName.replace(/[^A-Za-z0-9_.-]/g, "-").slice(0, 40);
|
|
145
|
+
const containerName = `infly-health-${safeImageName}-${process.pid}-${Date.now()}`;
|
|
146
|
+
const addHostArg = healthAddHost ? ` --add-host ${healthAddHost}` : "";
|
|
147
|
+
|
|
148
|
+
console.log("");
|
|
149
|
+
console.log("\x1b[0;32m>>> 启动临时容器并等待镜像 HEALTHCHECK\x1b[0m");
|
|
150
|
+
run(`docker run --detach --name ${containerName}${addHostArg} ${imageTag}`, { dryRun });
|
|
151
|
+
|
|
152
|
+
if (dryRun) {
|
|
153
|
+
run(`docker inspect --format health-status ${containerName}`, { dryRun });
|
|
154
|
+
run(`docker rm --force ${containerName}`, { dryRun });
|
|
155
|
+
console.log("\x1b[0;32m✓ [DRY RUN] 镜像健康检查计划完成\x1b[0m");
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const deadline = Date.now() + timeoutSeconds * 1000;
|
|
160
|
+
let lastStatus = "starting";
|
|
161
|
+
try {
|
|
162
|
+
while (Date.now() < deadline) {
|
|
163
|
+
lastStatus = run(
|
|
164
|
+
`docker inspect --format "{{if .State.Health}}{{.State.Health.Status}}{{else}}missing{{end}}" ${containerName}`,
|
|
165
|
+
{ silent: true }
|
|
166
|
+
).trim();
|
|
167
|
+
|
|
168
|
+
if (lastStatus === "healthy") {
|
|
169
|
+
console.log("\x1b[0;32m✓ 镜像健康检查通过\x1b[0m");
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (lastStatus === "unhealthy" || lastStatus === "missing") {
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
wait(1000);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
console.error(`\x1b[0;31m镜像健康检查失败,最终状态: ${lastStatus}\x1b[0m`);
|
|
179
|
+
run(`docker logs ${containerName}`);
|
|
180
|
+
throw new Error(`镜像 ${imageTag} 未通过健康检查,已阻止推送`);
|
|
181
|
+
} finally {
|
|
182
|
+
try {
|
|
183
|
+
run(`docker rm --force ${containerName}`, { silent: true });
|
|
184
|
+
} catch (cleanupError) {
|
|
185
|
+
console.warn(`警告: 临时容器 ${containerName} 清理失败: ${cleanupError.message}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function startDockerDesktop() {
|
|
191
|
+
console.log("\x1b[1;33mDocker Desktop 未运行,正在启动...\x1b[0m");
|
|
192
|
+
try {
|
|
193
|
+
try {
|
|
194
|
+
execSync(
|
|
195
|
+
`powershell.exe -Command "Start-Process 'C:\\Program Files\\Docker\\Docker\\Docker Desktop.exe'"`,
|
|
196
|
+
{ stdio: "ignore" }
|
|
197
|
+
);
|
|
198
|
+
} catch {
|
|
199
|
+
execSync(
|
|
200
|
+
`cmd.exe /c start "" "C:\\Program Files\\Docker\\Docker\\Docker Desktop.exe"`,
|
|
201
|
+
{ stdio: "ignore" }
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
process.stdout.write("等待 Docker 引擎就绪");
|
|
206
|
+
for (let i = 0; i < 30; i++) {
|
|
207
|
+
try {
|
|
208
|
+
execSync("docker info", { stdio: "ignore" });
|
|
209
|
+
console.log("\n\x1b[0;32m✓ Docker 已就绪\x1b[0m");
|
|
210
|
+
return true;
|
|
211
|
+
} catch {}
|
|
212
|
+
process.stdout.write(".");
|
|
213
|
+
execSync("sleep 2", { stdio: "ignore" });
|
|
214
|
+
}
|
|
215
|
+
console.log("");
|
|
216
|
+
return false;
|
|
217
|
+
} catch {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function dockerBuildPush(options = {}) {
|
|
223
|
+
const {
|
|
224
|
+
env = "prod",
|
|
225
|
+
registry = DEFAULT_REGISTRY,
|
|
226
|
+
namespace = DEFAULT_NAMESPACE,
|
|
227
|
+
local = false,
|
|
228
|
+
dryRun = false,
|
|
229
|
+
cicdUrl,
|
|
230
|
+
healthCheck = false,
|
|
231
|
+
healthTimeout,
|
|
232
|
+
healthAddHost,
|
|
233
|
+
version,
|
|
234
|
+
revision,
|
|
235
|
+
standalone = false,
|
|
236
|
+
commitOnly = false,
|
|
237
|
+
tagPrefix,
|
|
238
|
+
} = options;
|
|
239
|
+
const cicdLink = formatCicdLink(cicdUrl);
|
|
240
|
+
|
|
241
|
+
// 1. 检测运行模式并定位关键路径
|
|
242
|
+
const cwd = path.resolve(process.cwd());
|
|
243
|
+
const { isStandalone, contextRoot } = detectMode({ standalone });
|
|
244
|
+
|
|
245
|
+
const appPkgPath = path.join(cwd, "package.json");
|
|
246
|
+
if (!fs.existsSync(appPkgPath)) {
|
|
247
|
+
console.error("错误: 当前目录下缺少 package.json,请在 app 目录内执行");
|
|
248
|
+
process.exit(1);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
let buildContext, dockerfileArg, modeLabel;
|
|
252
|
+
if (isStandalone) {
|
|
253
|
+
if (!fs.existsSync(path.join(cwd, "Dockerfile"))) {
|
|
254
|
+
console.error("错误: 当前目录下缺少 Dockerfile");
|
|
255
|
+
process.exit(1);
|
|
256
|
+
}
|
|
257
|
+
buildContext = cwd;
|
|
258
|
+
dockerfileArg = "Dockerfile";
|
|
259
|
+
modeLabel = "独立模式";
|
|
260
|
+
} else {
|
|
261
|
+
const appDir = path.relative(contextRoot, cwd).replace(/\\/g, "/");
|
|
262
|
+
if (!fs.existsSync(path.join(contextRoot, appDir, "Dockerfile.monorepo"))) {
|
|
263
|
+
console.error("错误: 缺少 Dockerfile.monorepo");
|
|
264
|
+
process.exit(1);
|
|
265
|
+
}
|
|
266
|
+
buildContext = contextRoot;
|
|
267
|
+
dockerfileArg = `${appDir}/Dockerfile.monorepo`;
|
|
268
|
+
modeLabel = `monorepo 模式 (${appDir})`;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// 2. 从 package.json 推导镜像名
|
|
272
|
+
const pkg = JSON.parse(fs.readFileSync(appPkgPath, "utf-8"));
|
|
273
|
+
const imageName = pkg.name.replace(/^@[^/]+\//, "");
|
|
274
|
+
|
|
275
|
+
// 3. 获取版本号(monorepo 模式取 superproject 的 commit)
|
|
276
|
+
const commitHash = version || execSync("git rev-parse --short HEAD", {
|
|
277
|
+
cwd: contextRoot, encoding: "utf-8",
|
|
278
|
+
}).trim();
|
|
279
|
+
const fullCommit = revision || execSync("git rev-parse HEAD", {
|
|
280
|
+
cwd: contextRoot, encoding: "utf-8",
|
|
281
|
+
}).trim();
|
|
282
|
+
if (!/^[0-9a-f]{7,40}$/i.test(commitHash) || !/^[0-9a-f]{7,40}$/i.test(fullCommit)) {
|
|
283
|
+
throw new Error("Docker 镜像版本和 revision 必须是合法的 Git commit hash");
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// 4. 镜像标签
|
|
287
|
+
const imageBase = `${registry}/${namespace}/${imageName}`;
|
|
288
|
+
const normalizedPrefix = normalizeTagPrefix(tagPrefix);
|
|
289
|
+
const tags = resolveImageTags(imageBase, commitHash, env, commitOnly, normalizedPrefix);
|
|
290
|
+
|
|
291
|
+
// 5. Docker Desktop 自动启动(仅本地模式)
|
|
292
|
+
if (local) {
|
|
293
|
+
console.log("\x1b[0;32m>>> 检查 Docker 运行状态\x1b[0m");
|
|
294
|
+
try {
|
|
295
|
+
execSync("docker info", { stdio: "ignore" });
|
|
296
|
+
console.log("\x1b[0;32m✓ Docker 已运行\x1b[0m");
|
|
297
|
+
} catch {
|
|
298
|
+
const ok = startDockerDesktop();
|
|
299
|
+
if (!ok) {
|
|
300
|
+
console.error("\x1b[0;31m错误: Docker Desktop 启动超时,请手动启动后重试\x1b[0m");
|
|
301
|
+
process.exit(1);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// 6. 打印信息
|
|
307
|
+
console.log("\x1b[0;34m==========================================");
|
|
308
|
+
console.log("构建并推送 Docker 镜像");
|
|
309
|
+
console.log("==========================================\x1b[0m");
|
|
310
|
+
console.log(`运行模式: ${modeLabel}`);
|
|
311
|
+
console.log(`镜像名称: ${imageName}`);
|
|
312
|
+
console.log(`环境: ${env}`);
|
|
313
|
+
console.log(`版本号: ${commitHash}`);
|
|
314
|
+
if (normalizedPrefix) {
|
|
315
|
+
console.log(`标签前缀: ${normalizedPrefix}(全部标签带此前缀,发布标签由 CI 晋升)`);
|
|
316
|
+
}
|
|
317
|
+
console.log("标签:");
|
|
318
|
+
tags.forEach((t) => console.log(` ${t}`));
|
|
319
|
+
console.log("");
|
|
320
|
+
|
|
321
|
+
// 7. 构建
|
|
322
|
+
console.log("\x1b[0;32m>>> 开始构建 Docker 镜像\x1b[0m");
|
|
323
|
+
const tagArgs = tags.map((t) => `-t ${t}`).join(" ");
|
|
324
|
+
const buildCmd = `docker build -f ${dockerfileArg} ${tagArgs} --build-arg GIT_COMMIT=${fullCommit} --label org.opencontainers.image.revision=${fullCommit} --label org.opencontainers.image.version=${commitHash} .`;
|
|
325
|
+
run(buildCmd, { cwd: buildContext, dryRun });
|
|
326
|
+
console.log("\x1b[0;32m✓ 镜像构建完成\x1b[0m");
|
|
327
|
+
|
|
328
|
+
// 8. 可选镜像健康门禁(失败会抛错,任何标签都不会推送)
|
|
329
|
+
if (healthCheck) {
|
|
330
|
+
healthCheckImage(tags[0], {
|
|
331
|
+
dryRun,
|
|
332
|
+
imageName,
|
|
333
|
+
healthTimeout,
|
|
334
|
+
healthAddHost,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// 9. 推送
|
|
339
|
+
console.log("");
|
|
340
|
+
console.log(`\x1b[0;32m>>> 推送镜像到 ${registry}\x1b[0m`);
|
|
341
|
+
for (const tag of tags) {
|
|
342
|
+
run(`docker push ${tag}`, { dryRun });
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// 10. 结果摘要
|
|
346
|
+
console.log("");
|
|
347
|
+
console.log("\x1b[0;34m==========================================");
|
|
348
|
+
console.log("✅ 完成");
|
|
349
|
+
console.log("==========================================\x1b[0m");
|
|
350
|
+
console.log("可用标签:");
|
|
351
|
+
console.log(` ${tags[0]} (commit 精确版本)`);
|
|
352
|
+
if (!commitOnly) {
|
|
353
|
+
console.log(` ${tags[1]} (${env} 环境)`);
|
|
354
|
+
if (env === "prod") console.log(` ${tags[2]} (最新)`);
|
|
355
|
+
}
|
|
356
|
+
if (cicdLink) {
|
|
357
|
+
console.log("");
|
|
358
|
+
console.log(cicdLink);
|
|
359
|
+
}
|
|
360
|
+
console.log("");
|
|
361
|
+
|
|
362
|
+
return { mode: isStandalone ? "standalone" : "monorepo", buildContext, imageName, commitHash, tags };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
module.exports = {
|
|
366
|
+
dockerBuildPush,
|
|
367
|
+
detectMode,
|
|
368
|
+
formatCicdLink,
|
|
369
|
+
healthCheckImage,
|
|
370
|
+
normalizeHealthOptions,
|
|
371
|
+
normalizeTagPrefix,
|
|
372
|
+
resolveImageTags,
|
|
373
|
+
};
|