@infly/libs 2.0.53 → 2.1.0

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 CHANGED
@@ -204,9 +204,34 @@ infly-libs project build --env stage --target SQ223
204
204
 
205
205
  `build.environments` 推荐使用 `选择键:实际 mode` 数组,例如 `["prod:production", "stage:staging"]`;CLI 使用冒号前的值进行选择,并把冒号后的值作为 `--mode` 参数。没有冒号时选择键和 mode 同名。对象形式继续兼容,用于自定义 `label`、`mode` 或 `args`。项目如果自行管理构建入口,应设置 `infly.buildConfigs.manageScripts=false`,避免安装阶段重新写入传统 `build:prod/build:stage`。
206
206
 
207
+ ### 通用终端单选与多选
208
+
209
+ `@infly/libs/cli/select-option` 提供不包含业务选项的 TTY 交互适配,调用方负责传入稳定的 `value` 和展示 `label`:
210
+
211
+ ```js
212
+ const {
213
+ EXIT_SELECTION,
214
+ selectOption,
215
+ selectOptions,
216
+ } = require("@infly/libs/cli/select-option");
217
+
218
+ const platform = await selectOption("请选择平台", [
219
+ { value: "a", label: "平台 A" },
220
+ { value: "b", label: "平台 B" },
221
+ ]);
222
+ if (platform === EXIT_SELECTION) return;
223
+
224
+ const components = await selectOptions("请选择构建范围", [
225
+ { value: "frontend", label: "前端" },
226
+ { value: "backend", label: "后端" },
227
+ ]);
228
+ ```
229
+
230
+ `selectOptions` 至少要求选择一项,并返回所选 `value` 数组;取消提示时与 `selectOption` 一样返回 `EXIT_SELECTION`。两者都只允许在 TTY 中交互,CI 等非交互环境必须由调用方提供显式参数,不得猜测默认选项。
231
+
207
232
  ### Docker Compose 版本发布
208
233
 
209
- `docker:compose` 面向已有 Compose 文件的镜像版本递增、配置校验和构建,与上面的 Dockerfile/commit-tag `build:docker` 流程互不替代:
234
+ `docker:compose` 面向已有 Compose 文件的镜像版本固定或递增、配置校验和构建,与上面的 Dockerfile/commit-tag `build:docker` 流程互不替代:
210
235
 
211
236
  ```bash
212
237
  infly-libs docker:compose --target yhqy --bump patch
@@ -219,8 +244,95 @@ infly-libs docker:compose --target yhqy --bump patch
219
244
  - `--target`:选择 `infly.docker.targets` 或外部配置中的目标;非交互环境必须显式传入。
220
245
  - `--config`:可选的外部 JS 配置文件,使用 `targets.<name>.file` 结构。
221
246
  - `--project-dir` + `--file`:不使用 target 配置时的兼容直传方式,Compose 文件不能越出 `project-dir`。
222
- - `--bump patch`:目前只支持 patch 递增,且 Compose 中必须恰好有一个 `x.y.z` 镜像 tag。
247
+ - `--bump none|patch`:`none` 保持 Compose 中的镜像版本不变,`patch` 将补丁版本递增;两种模式都要求 Compose 中恰好有一个 `x.y.z` 镜像 tag。
223
248
  - `--push`:显式推送;未传时只执行 `docker compose config` 和 `build`。
224
249
  - `--dry-run`:只打印版本变化和计划命令,不写文件、不调用 Docker。
225
250
 
226
- 构建、校验或推送失败时,命令会恢复原 Compose 内容。公共包不内置 yhqy、qdxy 等业务目标映射;目标配置和单一发布 script 应归 Compose 所在项目所有。
251
+ 递增模式下构建、校验或推送失败时,命令会恢复原 Compose 内容;固定模式不会重写 Compose。公共包不内置 yhqy、qdxy 等业务目标映射;目标配置和发布 scripts 应归 Compose 所在项目所有。
252
+
253
+ ## 配置驱动的 Docker 构建与部署
254
+
255
+ 维护归属:公共构建工具;以 `bin/cli.js`、`build/docker/target-release.js` 和 `deployment-bundle.js` 为实现契约。新增配置字段时同步此节及测试。
256
+
257
+ `infly-libs docker:release` 先单选 target,再多选该 target 的 components。构建在执行命令的机器进行。默认不推送;`--push` 构建、验证后推送 `qc-<12位commit>`;`--dry-run` 只解析配置,不联网、不运行 Git/Docker、不创建目录。
258
+
259
+ ```json
260
+ {
261
+ "scripts": {
262
+ "docker:build": "infly-libs docker:release --no-push",
263
+ "docker:release": "infly-libs docker:release --push"
264
+ },
265
+ "infly": {
266
+ "docker": {
267
+ "registry": "registry.example.com",
268
+ "targets": {
269
+ "demo": {
270
+ "label": "示例平台",
271
+ "components": {
272
+ "frontend": {
273
+ "label": "前端",
274
+ "repository": "registry.example.com/demo/web",
275
+ "expectedRef": "origin/master",
276
+ "dockerfile": "Dockerfile",
277
+ "files": [
278
+ { "from": "deploy/Dockerfile", "to": "Dockerfile" },
279
+ { "from": "dist", "to": "html" }
280
+ ],
281
+ "check": { "entrypoint": "nginx", "args": ["-t"] },
282
+ "imageEnv": "WEB_IMAGE",
283
+ "services": ["web"]
284
+ },
285
+ "backend": {
286
+ "label": "后端",
287
+ "repository": "registry.example.com/demo/api",
288
+ "source": {
289
+ "directory": "../api",
290
+ "url": "https://example.com/api.git",
291
+ "branch": "release/demo"
292
+ },
293
+ "dockerfile": "Dockerfile",
294
+ "context": ".",
295
+ "imageEnv": "API_IMAGE",
296
+ "services": ["api", "worker"]
297
+ }
298
+ },
299
+ "deployment": {
300
+ "ready": true,
301
+ "server": "example.com",
302
+ "sshUser": "ubuntu",
303
+ "directory": "/opt/demo",
304
+ "composeFile": "compose.yml",
305
+ "secretEnvFile": ".env",
306
+ "releaseEnvFile": "deploy.env",
307
+ "releaseComposeFile": "deploy/compose.release.yml",
308
+ "health": { "urls": ["https://example.com/"] }
309
+ }
310
+ }
311
+ }
312
+ }
313
+ }
314
+ }
315
+ ```
316
+
317
+ - `files`:从组件源码仓复制到独立临时上下文,仅发送指定产物;配置后 `dockerfile` 相对临时上下文。未配置时 `context`、`dockerfile` 相对组件源码仓。
318
+ - `requiredFiles`:构建前必须存在的源码仓相对路径,例如前端产物的 `index.html`;缺失时停止构建。
319
+ - `source`:可选独立源码仓;从项目目录解析 `directory`,fetch 后使用指定分支的临时 detached worktree,不切换当前开发分支。缺少源码仓时打印准备步骤,全部构建停止,CLI 返回 2。
320
+ - 无 `source` 时构建当前项目,要求干净工作树且 HEAD 等于 `expectedRef`。有 `source` 时校验临时工作树对应的 origin 分支。
321
+ - `check`:可选镜像内校验,使用 `entrypoint`、`args` 数组及 `addHosts` 数组;失败不推送。配置命令具有代码执行权限,应与源码一样评审,不能来自不可信输入。
322
+ - `deployment`:仅用于部署包生成;`ready=false` 会阻止生成。可设置 `lockName`,默认 `.infly-release.lock`。服务归属和 imageEnv 必须唯一。
323
+ - `health`:容器运行检查、可选 `exec: [{service, command: [...]}]`,以及 HTTP URL 重试;默认 attempts=12、intervalSeconds=5、timeoutSeconds=20。不要配置迁移、业务写入或数据库命令。
324
+ - 路径、分支、镜像、服务、健康检查由项目配置决定;公共包不内置项目映射。Dockerfile、Compose、Nginx 由项目保留。
325
+
326
+ ```bash
327
+ infly-libs docker:release --target demo --components frontend,backend --push
328
+ infly-libs docker:release --platform demo --components backend --backend-dir ../api --no-push
329
+ infly-libs docker:release --project-dir ./deployment --config docker.json --target demo --components frontend --dry-run
330
+ infly-libs docker:bundle --target demo --output .infly-deploy-123
331
+ infly-libs docker:bundle --target demo --dry-run
332
+ ```
333
+
334
+ `--platform` 是 `--target` 的兼容别名;`--config` 读取 JSON 文件,其顶层直接为 docker 配置。CLI 参数优先于项目配置;`--backend-dir` 仅覆盖名为 backend 的组件源码目录。
335
+
336
+ `docker:bundle` 只生成本地脚本包,不自动部署;输出须是项目内尚不存在的独立子目录。包内包含 config.sh、Compose 覆盖文件和公共 shell 脚本。Jenkins 固定公共包版本后生成,使用既有凭据及人工确认,调用 `deploy.sh component=repository:commit` 传输并执行;`promote-image.sh` 接受组件名、qc 镜像、正式镜像和完整 commit。运行部署脚本会操作服务器,必须由开发者手动触发既定任务。
337
+
338
+ 远端发布持锁后读取全部组件镜像基线,保存 .release-history,仅更新传入组件的服务(up -d --no-deps)。失败恢复镜像环境文件及选中服务;回滚验证失败明确报错。不创建/修改数据库,不更新未配置的基础设施。服务器不需要 npm/Node,仍需 Bash、Docker Compose 和 curl。
@@ -1,56 +1,56 @@
1
1
  /**
2
2
  * Webpack require.context 适配器。
3
- * 自动导出目录下所有文件
4
- * @param {Function} requireContext - require.context 返回的函数
5
- * @returns {Object} 导出对象,key 为文件名(不含扩展名),value 为模块内容
6
- *
7
- * @example
8
- * // 在 components/Dialog/index.js 中使用
3
+ * 自动导出目录下所有文件
4
+ * @param {Function} requireContext - require.context 返回的函数
5
+ * @returns {Object} 导出对象,key 为文件名(不含扩展名),value 为模块内容
6
+ *
7
+ * @example
8
+ * // 在 components/Dialog/index.js 中使用
9
9
  * import { autoExportFiles } from '@infly/libs/adapters/webpack/auto-export';
10
- * const components = autoExportFiles(require.context('./', false, /\.vue$/));
11
- * Object.keys(components).forEach(name => {
12
- * module.exports[name] = components[name];
13
- * });
14
- *
15
- * // 或者直接一行
16
- * Object.assign(module.exports, autoExportFiles(require.context('./', false, /\.vue$/)));
17
- */
18
- function autoExportFiles(requireContext) {
19
- if (!requireContext || typeof requireContext !== 'function') {
20
- console.error('autoExportFiles: requireContext 必须是 require.context 返回的函数');
21
- return {};
22
- }
23
-
24
- const exports = {};
25
-
26
- requireContext.keys().forEach(fileName => {
27
- // 跳过 index 文件自身
28
- if (fileName.includes('index')) return;
29
-
30
- const componentConfig = requireContext(fileName);
31
- const componentName = fileName
32
- .replace(/^\.\//, '') // 移除 './'
33
- .replace(/\.\w+$/, ''); // 移除扩展名
34
-
35
- exports[componentName] = componentConfig.default || componentConfig;
36
- });
37
-
38
- return exports;
39
- }
40
-
41
- /**
42
- * 简化版:直接挂载到 module.exports
43
- * @param {Function} requireContext - require.context 返回的函数
44
- * @param {Object} targetExports - 目标导出对象(通常是 module.exports)
45
- *
46
- * @example
47
- * // 在 components/Dialog/index.js 中使用
10
+ * const components = autoExportFiles(require.context('./', false, /\.vue$/));
11
+ * Object.keys(components).forEach(name => {
12
+ * module.exports[name] = components[name];
13
+ * });
14
+ *
15
+ * // 或者直接一行
16
+ * Object.assign(module.exports, autoExportFiles(require.context('./', false, /\.vue$/)));
17
+ */
18
+ function autoExportFiles(requireContext) {
19
+ if (!requireContext || typeof requireContext !== 'function') {
20
+ console.error('autoExportFiles: requireContext 必须是 require.context 返回的函数');
21
+ return {};
22
+ }
23
+
24
+ const exports = {};
25
+
26
+ requireContext.keys().forEach(fileName => {
27
+ // 跳过 index 文件自身
28
+ if (fileName.includes('index')) return;
29
+
30
+ const componentConfig = requireContext(fileName);
31
+ const componentName = fileName
32
+ .replace(/^\.\//, '') // 移除 './'
33
+ .replace(/\.\w+$/, ''); // 移除扩展名
34
+
35
+ exports[componentName] = componentConfig.default || componentConfig;
36
+ });
37
+
38
+ return exports;
39
+ }
40
+
41
+ /**
42
+ * 简化版:直接挂载到 module.exports
43
+ * @param {Function} requireContext - require.context 返回的函数
44
+ * @param {Object} targetExports - 目标导出对象(通常是 module.exports)
45
+ *
46
+ * @example
47
+ * // 在 components/Dialog/index.js 中使用
48
48
  * import { autoExportToModule } from '@infly/libs/adapters/webpack/auto-export';
49
- * autoExportToModule(require.context('./', false, /\.vue$/), module.exports);
50
- */
51
- function autoExportToModule(requireContext, targetExports) {
52
- const files = autoExportFiles(requireContext);
53
- Object.assign(targetExports, files);
54
- }
55
-
56
- export { autoExportFiles, autoExportToModule };
49
+ * autoExportToModule(require.context('./', false, /\.vue$/), module.exports);
50
+ */
51
+ function autoExportToModule(requireContext, targetExports) {
52
+ const files = autoExportFiles(requireContext);
53
+ Object.assign(targetExports, files);
54
+ }
55
+
56
+ export { autoExportFiles, autoExportToModule };
package/bin/cli.js CHANGED
@@ -99,6 +99,18 @@ function parseProjectArgs(argv) {
99
99
  async function main(argv = process.argv.slice(2)) {
100
100
  const command = argv[0];
101
101
  const commandMap = {
102
+ "docker:bundle": () => {
103
+ const { parseReleaseArgs } = require("../build/docker/target-release");
104
+ const result = require("../build/docker/deployment-bundle").createDeploymentBundle(parseReleaseArgs(argv.slice(1)));
105
+ console.log(JSON.stringify(result, null, 2));
106
+ return result;
107
+ },
108
+ "docker:release": async () => {
109
+ const { parseReleaseArgs, runTargetRelease } = require("../build/docker/target-release");
110
+ const result = await runTargetRelease(parseReleaseArgs(argv.slice(1)));
111
+ if (result.missingSources?.length) process.exitCode = 2;
112
+ return result;
113
+ },
102
114
  afterBuild: () => loadBuildDist().init(),
103
115
  initZip: () => loadBuildDist().init(),
104
116
  preview: () => loadPreview().init(),
@@ -1,57 +1,57 @@
1
- const { spawn } = require("child_process");
2
-
3
- function run(command, args, options = {}) {
4
- return new Promise((resolve, reject) => {
5
- const useShell = options.shell !== undefined ? options.shell : process.platform === "win32";
6
- // Avoid DEP0190: when shell=true, pass command+args as single string
7
- const [cmd, cmdArgs] = useShell
8
- ? [[command, ...args].join(" "), []]
9
- : [command, args];
10
- const child = spawn(cmd, cmdArgs, {
11
- cwd: options.cwd || process.cwd(),
12
- env: {
13
- ...process.env,
14
- ...(options.env || {})
15
- },
16
- stdio: options.stdio || "inherit",
17
- shell: useShell
18
- });
19
-
20
- child.on("close", (code) => {
21
- if (code === 0) {
22
- resolve();
23
- } else {
24
- reject(new Error(`${command} ${args.join(" ")} failed with exit code ${code}`));
25
- }
26
- });
27
-
28
- child.on("error", reject);
29
- });
30
- }
31
-
32
- function spawnBackground(command, args, options = {}) {
33
- const useShell = options.shell !== undefined ? options.shell : process.platform === "win32";
34
- const [cmd, cmdArgs] = useShell
35
- ? [[command, ...args].join(" "), []]
36
- : [command, args];
37
- const child = spawn(cmd, cmdArgs, {
38
- cwd: options.cwd || process.cwd(),
39
- env: {
40
- ...process.env,
41
- ...(options.env || {})
42
- },
43
- stdio: options.stdio || "inherit",
44
- shell: useShell
45
- });
46
-
47
- child.on("error", (error) => {
48
- console.error(error.message);
49
- });
50
-
51
- return child;
52
- }
53
-
54
- module.exports = {
55
- run,
56
- spawnBackground
57
- };
1
+ const { spawn } = require("child_process");
2
+
3
+ function run(command, args, options = {}) {
4
+ return new Promise((resolve, reject) => {
5
+ const useShell = options.shell !== undefined ? options.shell : process.platform === "win32";
6
+ // Avoid DEP0190: when shell=true, pass command+args as single string
7
+ const [cmd, cmdArgs] = useShell
8
+ ? [[command, ...args].join(" "), []]
9
+ : [command, args];
10
+ const child = spawn(cmd, cmdArgs, {
11
+ cwd: options.cwd || process.cwd(),
12
+ env: {
13
+ ...process.env,
14
+ ...(options.env || {})
15
+ },
16
+ stdio: options.stdio || "inherit",
17
+ shell: useShell
18
+ });
19
+
20
+ child.on("close", (code) => {
21
+ if (code === 0) {
22
+ resolve();
23
+ } else {
24
+ reject(new Error(`${command} ${args.join(" ")} failed with exit code ${code}`));
25
+ }
26
+ });
27
+
28
+ child.on("error", reject);
29
+ });
30
+ }
31
+
32
+ function spawnBackground(command, args, options = {}) {
33
+ const useShell = options.shell !== undefined ? options.shell : process.platform === "win32";
34
+ const [cmd, cmdArgs] = useShell
35
+ ? [[command, ...args].join(" "), []]
36
+ : [command, args];
37
+ const child = spawn(cmd, cmdArgs, {
38
+ cwd: options.cwd || process.cwd(),
39
+ env: {
40
+ ...process.env,
41
+ ...(options.env || {})
42
+ },
43
+ stdio: options.stdio || "inherit",
44
+ shell: useShell
45
+ });
46
+
47
+ child.on("error", (error) => {
48
+ console.error(error.message);
49
+ });
50
+
51
+ return child;
52
+ }
53
+
54
+ module.exports = {
55
+ run,
56
+ spawnBackground
57
+ };