@infly/libs 2.0.46 → 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
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
const fs = require("node:fs");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
const { execFileSync, spawnSync } = require("node:child_process");
|
|
4
|
+
const dotenv = require("dotenv");
|
|
5
|
+
const { dockerBuildPush } = require("./docker-build-push");
|
|
6
|
+
|
|
7
|
+
const SAFE_SCRIPT_NAME = /^[A-Za-z0-9][A-Za-z0-9:_-]*$/;
|
|
8
|
+
|
|
9
|
+
function normalizeEmergencyOptions({
|
|
10
|
+
emergency = false,
|
|
11
|
+
emergencyReason,
|
|
12
|
+
standalone = false,
|
|
13
|
+
commitOnly = false,
|
|
14
|
+
healthCheck = false,
|
|
15
|
+
} = {}) {
|
|
16
|
+
if (!emergency) return { emergency: false, emergencyReason: "" };
|
|
17
|
+
if (!standalone || !commitOnly || !healthCheck) {
|
|
18
|
+
throw new Error(
|
|
19
|
+
"紧急构建必须同时启用 --standalone、--commit-only 和 --health-check"
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
if (typeof emergencyReason !== "string") {
|
|
23
|
+
throw new Error("紧急构建必须通过 --emergency-reason 填写原因或审批/故障单号");
|
|
24
|
+
}
|
|
25
|
+
const normalizedReason = emergencyReason.trim();
|
|
26
|
+
if (normalizedReason.length < 8 || normalizedReason.length > 200) {
|
|
27
|
+
throw new Error("紧急构建原因长度必须为 8 到 200 个字符");
|
|
28
|
+
}
|
|
29
|
+
if (/[\u0000-\u001f\u007f]/.test(normalizedReason)) {
|
|
30
|
+
throw new Error("紧急构建原因不能包含控制字符");
|
|
31
|
+
}
|
|
32
|
+
return { emergency: true, emergencyReason: normalizedReason };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function run(command, args, label, cwd) {
|
|
36
|
+
console.log(`\n>>> ${label}`);
|
|
37
|
+
const result = spawnSync(command, args, {
|
|
38
|
+
cwd,
|
|
39
|
+
env: process.env,
|
|
40
|
+
stdio: "inherit",
|
|
41
|
+
shell: false,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
if (result.error) throw new Error(`${label}无法启动: ${result.error.message}`);
|
|
45
|
+
if (result.status !== 0) throw new Error(`${label}失败,退出码: ${result.status}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function runShellCommand(command, label, cwd) {
|
|
49
|
+
if (/[\r\n&|<>^]/.test(command)) {
|
|
50
|
+
throw new Error(`${label}包含不安全的 Windows shell 字符`);
|
|
51
|
+
}
|
|
52
|
+
run(process.env.ComSpec || "cmd.exe", ["/d", "/s", "/c", command], label, cwd);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function runPnpmScript(scriptName, cwd) {
|
|
56
|
+
if (!SAFE_SCRIPT_NAME.test(scriptName)) {
|
|
57
|
+
throw new Error(`dockerRelease.checks 包含非法脚本名: ${scriptName}`);
|
|
58
|
+
}
|
|
59
|
+
const label = `项目检查: pnpm run ${scriptName}`;
|
|
60
|
+
if (process.platform === "win32") {
|
|
61
|
+
runShellCommand(`pnpm run ${scriptName}`, label, cwd);
|
|
62
|
+
} else {
|
|
63
|
+
run("pnpm", ["run", scriptName], label, cwd);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function runSonarScanner(cwd, projectVersion) {
|
|
68
|
+
const label = "SonarQube 扫描与质量门禁";
|
|
69
|
+
const scannerArgs = [`-Dsonar.projectVersion=${projectVersion}`];
|
|
70
|
+
if (process.platform === "win32") {
|
|
71
|
+
runShellCommand(`sonar-scanner ${scannerArgs[0]}`, label, cwd);
|
|
72
|
+
} else {
|
|
73
|
+
run("sonar-scanner", scannerArgs, label, cwd);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function resolveProjectVersion(projectRoot) {
|
|
78
|
+
const fullCommit = execFileSync("git", ["rev-parse", "HEAD"], {
|
|
79
|
+
cwd: projectRoot,
|
|
80
|
+
encoding: "utf-8",
|
|
81
|
+
}).trim();
|
|
82
|
+
const commitHash = execFileSync("git", ["rev-parse", "--short=12", "HEAD"], {
|
|
83
|
+
cwd: projectRoot,
|
|
84
|
+
encoding: "utf-8",
|
|
85
|
+
}).trim();
|
|
86
|
+
if (!/^[0-9a-f]{7,40}$/i.test(fullCommit) || !/^[0-9a-f]{7,40}$/i.test(commitHash)) {
|
|
87
|
+
throw new Error("无法从当前项目解析合法的 Git commit hash");
|
|
88
|
+
}
|
|
89
|
+
return { commitHash: commitHash.toLowerCase(), fullCommit: fullCommit.toLowerCase() };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function assertCleanProject(projectRoot) {
|
|
93
|
+
const changes = execFileSync("git", ["status", "--porcelain", "--untracked-files=all"], {
|
|
94
|
+
cwd: projectRoot,
|
|
95
|
+
encoding: "utf-8",
|
|
96
|
+
}).trim();
|
|
97
|
+
if (changes) {
|
|
98
|
+
throw new Error("当前项目存在未提交改动;为保证镜像、Sonar revision 与 commit 标签一致,发布已终止");
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function resolveInsideProject(projectRoot, relativePath, fieldName) {
|
|
103
|
+
if (typeof relativePath !== "string" || !relativePath.trim()) {
|
|
104
|
+
throw new Error(`${fieldName} 必须是非空相对路径`);
|
|
105
|
+
}
|
|
106
|
+
const resolved = path.resolve(projectRoot, relativePath);
|
|
107
|
+
const relative = path.relative(projectRoot, resolved);
|
|
108
|
+
if (path.isAbsolute(relative) || relative.startsWith("..")) {
|
|
109
|
+
throw new Error(`${fieldName} 不得指向项目目录外`);
|
|
110
|
+
}
|
|
111
|
+
return resolved;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function readReleaseConfig(projectRoot) {
|
|
115
|
+
const packagePath = path.join(projectRoot, "package.json");
|
|
116
|
+
if (!fs.existsSync(packagePath)) throw new Error("当前目录下缺少 package.json");
|
|
117
|
+
const pkg = JSON.parse(fs.readFileSync(packagePath, "utf-8"));
|
|
118
|
+
const config = pkg.infly && pkg.infly.dockerRelease;
|
|
119
|
+
if (!config || !Array.isArray(config.checks) || config.checks.length === 0) {
|
|
120
|
+
throw new Error("package.json 必须配置 infly.dockerRelease.checks");
|
|
121
|
+
}
|
|
122
|
+
config.checks.forEach((scriptName) => {
|
|
123
|
+
if (typeof scriptName !== "string" || !SAFE_SCRIPT_NAME.test(scriptName)) {
|
|
124
|
+
throw new Error(`dockerRelease.checks 包含非法脚本名: ${scriptName}`);
|
|
125
|
+
}
|
|
126
|
+
if (!pkg.scripts || !pkg.scripts[scriptName]) {
|
|
127
|
+
throw new Error(`package.json 缺少检查脚本: ${scriptName}`);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const coverageReport = config.coverageReport
|
|
132
|
+
? resolveInsideProject(projectRoot, config.coverageReport, "dockerRelease.coverageReport")
|
|
133
|
+
: undefined;
|
|
134
|
+
return {
|
|
135
|
+
checks: [...config.checks],
|
|
136
|
+
coverageReport,
|
|
137
|
+
sonar: config.sonar !== false,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function loadLocalReleaseEnv(projectRoot) {
|
|
142
|
+
const envPath = path.join(projectRoot, ".env.production.local");
|
|
143
|
+
if (!fs.existsSync(envPath)) return false;
|
|
144
|
+
|
|
145
|
+
const result = dotenv.config({ path: envPath, override: false });
|
|
146
|
+
if (result.error) {
|
|
147
|
+
throw new Error(`无法读取本地发布环境文件 ${envPath}: ${result.error.message}`);
|
|
148
|
+
}
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function releaseDocker(options = {}) {
|
|
153
|
+
const projectRoot = path.resolve(process.cwd());
|
|
154
|
+
loadLocalReleaseEnv(projectRoot);
|
|
155
|
+
const config = readReleaseConfig(projectRoot);
|
|
156
|
+
assertCleanProject(projectRoot);
|
|
157
|
+
const projectVersion = resolveProjectVersion(projectRoot);
|
|
158
|
+
const emergency = normalizeEmergencyOptions(options);
|
|
159
|
+
|
|
160
|
+
if (!emergency.emergency && config.sonar && (!process.env.SONAR_TOKEN || !process.env.SONAR_TOKEN.trim())) {
|
|
161
|
+
throw new Error(
|
|
162
|
+
"缺少 SONAR_TOKEN;请通过当前进程环境或被 Git 忽略的 .env.production.local 提供"
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (emergency.emergency) {
|
|
167
|
+
console.warn("\n⚠️ 紧急构建模式:已跳过项目检查和 SonarQube");
|
|
168
|
+
console.warn(`紧急原因: ${emergency.emergencyReason}`);
|
|
169
|
+
console.warn(`目标 commit: ${projectVersion.fullCommit}`);
|
|
170
|
+
} else {
|
|
171
|
+
for (const scriptName of config.checks) runPnpmScript(scriptName, projectRoot);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (!emergency.emergency && config.coverageReport) {
|
|
175
|
+
if (!fs.existsSync(config.coverageReport) || fs.statSync(config.coverageReport).size === 0) {
|
|
176
|
+
throw new Error(`覆盖率报告不存在或为空: ${config.coverageReport}`);
|
|
177
|
+
}
|
|
178
|
+
console.log(`✓ 覆盖率报告已生成: ${config.coverageReport}`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (!emergency.emergency && config.sonar) {
|
|
182
|
+
runSonarScanner(projectRoot, projectVersion.commitHash);
|
|
183
|
+
}
|
|
184
|
+
return dockerBuildPush({
|
|
185
|
+
...options,
|
|
186
|
+
version: projectVersion.commitHash,
|
|
187
|
+
revision: projectVersion.fullCommit,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
module.exports = {
|
|
192
|
+
assertCleanProject,
|
|
193
|
+
loadLocalReleaseEnv,
|
|
194
|
+
normalizeEmergencyOptions,
|
|
195
|
+
readReleaseConfig,
|
|
196
|
+
releaseDocker,
|
|
197
|
+
resolveInsideProject,
|
|
198
|
+
resolveProjectVersion,
|
|
199
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@infly/libs",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.47",
|
|
4
4
|
"description": "不受前端框架限制的独立工具库",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -62,6 +62,7 @@
|
|
|
62
62
|
"dependencies": {
|
|
63
63
|
"@infly/ts-libs": "workspace:*",
|
|
64
64
|
"connect": "^3.7.0",
|
|
65
|
+
"dotenv": "16.6.1",
|
|
65
66
|
"enquirer": "^2.4.1",
|
|
66
67
|
"path-browserify": "^1.0.1",
|
|
67
68
|
"serve-static": "^1.15.0",
|