@wjwjq/release-helper 0.2.97 → 0.2.99

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.
@@ -0,0 +1,861 @@
1
+ # Release-Helper 重构方案
2
+
3
+ ## Context
4
+
5
+ 当前 release-helper 是一个前端部署打包工具,核心流程:`init` → `pack` → `release`(GitLab发布)→ 服务端 `install.sh` 安装。
6
+
7
+ **主要问题**:
8
+ 1. **二进制膨胀**:Nginx 二进制直接提交到 Git 仓库(`src/deploy/pkg/nginx_binary/binary/`),仓库体积大,管理混乱
9
+ 2. **硬编码依赖**:`publish.ts` 硬编码 GitLab,无法支持其他平台
10
+ 3. **占位符替换脆弱**:`String.replace()` 简单替换,命名不统一(`__user__` vs `_user_` vs `__APP_NAME__`)
11
+ 4. **无自动化测试**
12
+ 5. **配置校验手写**:易遗漏字段
13
+ 6. **Changelog 解析简陋**:仅支持 `feat:/fix:`
14
+ 7. **Bash 脚本问题**:install.sh = upgrade.sh(`callBy` 参数未使用)、`log_warn()` 拼写 bug、`__root__` 占位符未实际替换、大量硬编码路径
15
+ 8. **安全合规**:在严格安全环境下,部署包需要能被审计(deps.txt 依赖声明)
16
+
17
+ **重构目标**:保持核心能力不变的前提下,解耦架构、规范化流程、增强可维护性和安全合规性。
18
+
19
+ ---
20
+
21
+ ## 第一阶段:架构解耦(Plugin 化)
22
+
23
+ ### 1.1 定义平台抽象层
24
+
25
+ 新建 `src/platform/types.ts`:
26
+
27
+ ```typescript
28
+ interface Platform {
29
+ name: string;
30
+ createTag(version: string, branch: string): Promise<void>;
31
+ createRelease(version: string, description: string): Promise<Release>;
32
+ uploadAsset(releaseId: string, file: Buffer, filename: string): Promise<AssetRef>;
33
+ getTags(): Promise<string[]>;
34
+ getChangelog(range: string): Promise<string>;
35
+ }
36
+ ```
37
+
38
+ ### 1.2 GitLab 实现
39
+
40
+ 将 `src/publish.ts` → `src/platforms/gitlab.ts`:
41
+ - 现有 `@gitbeaker/rest` 调用封装为 `GitlabPlatform implements Platform`
42
+ - 接口不变,只是从自由函数改为类方法
43
+
44
+ ### 1.3 平台选择
45
+
46
+ `release.conf.yaml` 新增字段:
47
+ ```yaml
48
+ platform: gitlab # 可选: gitlab | github | gitee
49
+ ```
50
+
51
+ CLI 根据 `platform` 字段动态加载对应实现。
52
+
53
+ ---
54
+
55
+ ## 第二阶段:配置系统升级
56
+
57
+ ### 2.1 Zod Schema 校验
58
+
59
+ 依赖新增:`zod`
60
+
61
+ `src/config/schema.ts`:
62
+ ```typescript
63
+ const ReleaseConfSchema = z.object({
64
+ platform: z.enum(['gitlab', 'github', 'gitee']).default('gitlab'),
65
+ host: z.string().url(),
66
+ token: z.string().optional(), // 可从环境变量读取
67
+ buildCmd: z.string().default('npm run build'),
68
+ assetsDir: z.string().default('dist'),
69
+ user: z.string().default('root'),
70
+ userGroup: z.string().default('root'),
71
+ installMode: z.enum(['standalone', 'cluster', 'static', 'all', 'selectable']).default('all'),
72
+ installDir: z.string().default('/opt').refine(isValidLinuxPath),
73
+ nginxInstallMode: z.enum(['source', 'binary']).default('binary'),
74
+ dockerInstallDir: z.string().optional(),
75
+ dockerBackupDir: z.string().optional(),
76
+ binaryRegistry: z.object({
77
+ // 新字段:二进制制品库地址
78
+ url: z.string().url().optional(),
79
+ type: z.enum(['http', 'nexus', 'minio']).default('http'),
80
+ }).optional(),
81
+ });
82
+ ```
83
+
84
+ ### 2.2 敏感信息环境变量支持
85
+
86
+ `release.conf.yaml` 支持 `${ENV_VAR}` 语法:
87
+ ```yaml
88
+ host: https://gitlab.example.com
89
+ token: ${GITLAB_TOKEN} # 从环境变量读取,不写死在文件里
90
+ ```
91
+
92
+ 解析时替换:`parseConf()` 增加 `resolveEnvVars()` 步骤。
93
+
94
+ ---
95
+
96
+ ## 第三阶段:打包逻辑重构
97
+
98
+ ### 3.1 模板引擎替换 String.replace
99
+
100
+ 依赖新增:`handlebars`
101
+
102
+ 统一占位符语法:`{{APP_NAME}}`、`{{INSTALL_PATH}}`、`{{USER}}` 等。
103
+
104
+ | 旧语法 | 新语法 |
105
+ |--------|--------|
106
+ | `__APP_NAME__` | `{{APP_NAME}}` |
107
+ | `_user_` | `{{USER}}` |
108
+ | `__user__` | `{{USER}}` |
109
+ | `__usergroup__` | `{{USER_GROUP}}` |
110
+ | `__root__` | `{{ASSETS_ROOT}}` |
111
+ | `#ERROR_LOG_PLACEHOLDER` | `{{ERROR_LOG_PATH}}` |
112
+
113
+ 改造 `src/pack.ts`:
114
+ ```typescript
115
+ function renderTemplate(template: string, data: TemplateData): string {
116
+ return Handlebars.compile(template, { noEscape: true })(data);
117
+ }
118
+ ```
119
+
120
+ ### 3.2 二进制从制品库下载
121
+
122
+ `src/pack.ts` 修改 pack 流程:
123
+
124
+ ```
125
+ 旧流程:
126
+ fs.cpSync(src/deploy, projectDir) ← 直接复制 Git 仓库中的二进制
127
+
128
+ 新流程:
129
+ 1. 读取 binaryRegistry.url
130
+ 2. 下载 manifest.json 获取当前版本的所有平台二进制列表
131
+ 3. 并发下载所有平台二进制到临时目录
132
+ 4. 复制到 projectDir/pkg/nginx_binary/
133
+ 5. 写入 deps.txt(每个二进制的 ldd 输出)供审计
134
+ ```
135
+
136
+ 制品库 manifest.json 格式:
137
+ ```json
138
+ {
139
+ "version": "1.31.0",
140
+ "binaries": [
141
+ {
142
+ "id": "x86_64-el7",
143
+ "arch": "x86_64",
144
+ "os": "centos7",
145
+ "openssl": "1.0.2k",
146
+ "url": "https://registry.internal/nginx/1.31.0/x86_64-el7/nginx.tar.gz",
147
+ "sha256": "abc123...",
148
+ "deps": "libssl.so.10 => /lib64/libssl.so.10\n..."
149
+ },
150
+ { "id": "x86_64-el9", "arch": "x86_64", "os": "centos9", "openssl": "3.0.7", ... },
151
+ { "id": "aarch64-el8", "arch": "aarch64", "os": "centos8", "openssl": "1.1.1k", ... }
152
+ ]
153
+ }
154
+ ```
155
+
156
+ ### 3.3 打包产物结构
157
+
158
+ ```
159
+ .release/{projectName}/
160
+ ├── pkg/
161
+ │ ├── nginx_binary/
162
+ │ │ ├── x86_64-el7/
163
+ │ │ │ ├── nginx.tar.gz
164
+ │ │ │ └── deps.txt ← 安全审计用
165
+ │ │ ├── x86_64-el9/
166
+ │ │ └── aarch64-el8/
167
+ │ ├── nginx/
168
+ │ │ ├── nginx.service.tpl ← 统一占位符为 {{...}}
169
+ │ │ └── nginx.logrotate.tpl
170
+ │ └── assets/ ← 前端构建产物
171
+ ├── script/
172
+ │ ├── install.sh
173
+ │ ├── upgrade.sh
174
+ │ ├── common.sh
175
+ │ ├── nginx.sh
176
+ │ ├── prompt.sh
177
+ │ └── after.sh ← 用户自定义(可选)
178
+ └── version
179
+ ```
180
+
181
+ ---
182
+
183
+ ## 第四阶段:Bash 部署脚本修复
184
+
185
+ ### 4.1 Bug 修复
186
+
187
+ | 文件 | 问题 | 修复 |
188
+ |------|------|------|
189
+ | `common.sh:33` | `log_warn()` 输出 `[success]` | 改为 `[warn]` |
190
+ | `common.sh:164` | `start()` 不区分 install/upgrade | 增加差异化逻辑(upgrade 时跳过用户创建等) |
191
+ | `nginx.sh:70-73` | `__root__` 占位符替换代码被注释 | 启用并正确使用 Handlebars 风格的 `{{ASSETS_ROOT}}` |
192
+
193
+ ### 4.2 二进制选择逻辑重构
194
+
195
+ `nginx.sh` 中的二进制选择改为基于 manifest:
196
+
197
+ ```bash
198
+ # 旧:按文件名模式匹配
199
+ tar_file="nginx-${arch}-ssl${version}.tar.gz"
200
+
201
+ # 新:读取 deps.txt 和 manifest
202
+ select_binary() {
203
+ local os_id=$(. /etc/os-release && echo "${ID}-${VERSION_ID%%.*}")
204
+ local arch=$(uname -m)
205
+
206
+ local match_dir="${PROJECT_PATH}/pkg/nginx_binary/${arch}-${os_id}"
207
+ if [ -d "$match_dir" ] && [ -f "$match_dir/deps.txt" ]; then
208
+ echo "$match_dir/nginx.tar.gz"
209
+ return 0
210
+ fi
211
+
212
+ # 回退:尝试模糊匹配
213
+ # ...
214
+ }
215
+ ```
216
+
217
+ ### 4.3 install.sh 与 upgrade.sh 差异化
218
+
219
+ ```bash
220
+ # install.sh 新增行为:
221
+ # - 创建系统用户
222
+ # - 首次安装 nginx
223
+ # - 注册 systemd 服务
224
+
225
+ # upgrade.sh 新增行为:
226
+ # - 跳过用户创建
227
+ # - 备份旧版本
228
+ # - 平滑重启(nginx -s reload 而非 systemctl restart)
229
+ # - 保留原有 nginx.conf 中的自定义配置(diff 合并)
230
+ ```
231
+
232
+ ### 4.4 统一占位符
233
+
234
+ 所有模板文件统一使用 `{{VAR}}` 语法:
235
+ - `nginx.service.tpl`:`{{USER}}`、`{{USER_GROUP}}`、`{{EXEC_CMD}}`、`{{LOG_PATH}}`、`{{PID_FILE}}`、`{{APP_NAME}}`
236
+ - `nginx.logrotate.tpl`:`{{APP_NAME}}`、`{{USER}}`、`{{USER_GROUP}}`、`{{PID_FILE}}`
237
+ - 安装脚本中在解压后用 `sed` 或内嵌的 shell 函数做替换(避免在部署端引入 handlebars 依赖)
238
+
239
+ ### 4.5 移除硬编码路径
240
+
241
+ ```bash
242
+ # 旧
243
+ NGINX_BINARY_INSTALL_PATH="/etc/nginx/"
244
+ NGINX_CONF_INSTALL_PATH="$prefix/opt/${APP_NAME}/nginx/"
245
+
246
+ # 新 — 在 release.conf.yaml 中可配置,打包时替换
247
+ NGINX_BINARY_INSTALL_PATH="{{NGINX_BINARY_DIR}}"
248
+ NGINX_CONF_INSTALL_PATH="{{NGINX_CONF_DIR}}"
249
+ ```
250
+
251
+ ---
252
+
253
+ ## 第五阶段:Changelog 与发布
254
+
255
+ ### 5.1 Conventional Commits 支持
256
+
257
+ `src/platforms/gitlab.ts` 中的 `getChangeLog()` 改为标准 Conventional Commits 解析:
258
+
259
+ ```
260
+ 支持的 type:
261
+ feat → Features
262
+ fix → Bug Fixes
263
+ perf → Performance Improvements
264
+ refactor → Code Refactoring (折叠显示)
265
+ docs → Documentation (折叠显示)
266
+ chore/ci → 不显示
267
+ BREAKING CHANGE → 置顶高亮显示
268
+
269
+ scope 支持: feat(auth): xxx → * **auth**: xxx
270
+ ```
271
+
272
+ 使用正则 `^(feat|fix|perf|refactor|docs|chore|ci)(\(.+\))?: (.+)$` 解析。
273
+
274
+ ### 5.2 dry-run 模式
275
+
276
+ CLI 新增 `--dry-run` 标志:
277
+ ```bash
278
+ release-helper release --dry-run
279
+ # 输出:将要执行的操作列表,不实际执行
280
+ # - 将切换到 master 分支
281
+ # - 将创建 tag v1.2.3
282
+ # - 将打包: posidon-frontend_v1.2.3.tar.gz
283
+ # - 将发布到 GitLab: ...
284
+ ```
285
+
286
+ ---
287
+
288
+ ## 制品库设计与实现方案
289
+
290
+ > 制品库解决的核心问题:Nginx 二进制不在 Git 仓库中,但 pack 时必须能获取到所有平台的二进制。
291
+
292
+ ### 一、存储方案选择
293
+
294
+ 提供 3 种后端,按复杂度递增:
295
+
296
+ | 方案 | 适用场景 | 复杂度 | 依赖 |
297
+ |------|---------|--------|------|
298
+ | **A. 静态文件 + HTTP** | 小型团队,无额外基础设施 | 最低 | nginx/caddy/httpd |
299
+ | **B. MinIO (S3 兼容)** | 已有或愿意部署对象存储 | 中 | docker run minio |
300
+ | **C. Nexus Repository** | 企业已有 Nexus 统一管控 | 中 | docker run nexus |
301
+
302
+ 推荐 **方案 A** 起步,后续无缝切换到 B 或 C(通过 `binaryRegistry.type` 配置切换)。
303
+
304
+ ---
305
+
306
+ ### 二、制品库目录结构
307
+
308
+ ```
309
+ /storage/nginx-binaries/ # 存储根目录
310
+ ├── manifest.json # 全局入口,描述所有可用版本
311
+ ├── versions.json # 版本列表(用于版本查询/回滚)
312
+ ├── 1.31.0/ # 按 nginx 版本分组
313
+ │ ├── manifest.json # 该版本的所有平台二进制描述
314
+ │ ├── x86_64-el7/
315
+ │ │ ├── nginx.tar.gz
316
+ │ │ ├── nginx.tar.gz.sha256 # 校验文件
317
+ │ │ └── deps.txt # ldd 输出(安全审计用)
318
+ │ ├── x86_64-el9/
319
+ │ │ ├── nginx.tar.gz
320
+ │ │ ├── nginx.tar.gz.sha256
321
+ │ │ └── deps.txt
322
+ │ ├── aarch64-el8/
323
+ │ │ ├── nginx.tar.gz
324
+ │ │ ├── nginx.tar.gz.sha256
325
+ │ │ └── deps.txt
326
+ │ └── aarch64-el9/
327
+ │ └── ...
328
+ ├── 1.22.1/ # 历史版本保留
329
+ │ └── ...
330
+ └── source/
331
+ └── nginx-1.31.0.tar.gz # 源码归档(可选,用于 source 模式安装)
332
+ ```
333
+
334
+ ### 三、manifest.json 完整格式
335
+
336
+ **全局 manifest.json** (`/storage/nginx-binaries/manifest.json`):
337
+ ```json
338
+ {
339
+ "latest": "1.31.0",
340
+ "versions": ["1.31.0", "1.22.1"],
341
+ "updated_at": "2025-01-15T10:30:00Z"
342
+ }
343
+ ```
344
+
345
+ **版本级 manifest.json** (`/storage/nginx-binaries/1.31.0/manifest.json`):
346
+ ```json
347
+ {
348
+ "nginx_version": "1.31.0",
349
+ "built_at": "2025-01-15T10:30:00Z",
350
+ "binaries": [
351
+ {
352
+ "id": "x86_64-el7",
353
+ "arch": "x86_64",
354
+ "os": "centos",
355
+ "os_version": "7",
356
+ "openssl": "1.0.2k",
357
+ "glibc": "2.17",
358
+ "file_url": "1.31.0/x86_64-el7/nginx.tar.gz",
359
+ "sha256": "a1b2c3d4e5f6...",
360
+ "file_size": 5242880,
361
+ "deps": "libssl.so.10 => /lib64/libssl.so.10\nlibcrypto.so.10 => /lib64/libcrypto.so.10\nlibpcre.so.1 => /lib64/libpcre.so.1\nlibz.so.1 => /lib64/libz.so.1\nlibc.so.6 => /lib64/libc.so.6\n/lib64/ld-linux-x86-64.so.2",
362
+ "compile_info": {
363
+ "docker_image": "centos:7",
364
+ "configure_args": "--prefix=/etc/nginx --with-http_ssl_module --with-http_v2_module --with-http_gzip_static_module --with-http_stub_status_module --with-stream",
365
+ "compiler": "gcc 4.8.5"
366
+ }
367
+ },
368
+ {
369
+ "id": "x86_64-el9",
370
+ "arch": "x86_64",
371
+ "os": "rocky",
372
+ "os_version": "9",
373
+ "openssl": "3.0.7",
374
+ "glibc": "2.34",
375
+ "file_url": "1.31.0/x86_64-el9/nginx.tar.gz",
376
+ "sha256": "...",
377
+ "deps": "libssl.so.3 => /lib64/libssl.so.3\nlibcrypto.so.3 => /lib64/libcrypto.so.3\n...",
378
+ "compile_info": { "docker_image": "rockylinux:9", "compiler": "gcc 11.4.1" }
379
+ },
380
+ {
381
+ "id": "aarch64-el8",
382
+ "arch": "aarch64",
383
+ "os": "rocky",
384
+ "os_version": "8",
385
+ "openssl": "1.1.1k",
386
+ "glibc": "2.28",
387
+ "file_url": "1.31.0/aarch64-el8/nginx.tar.gz",
388
+ "sha256": "...",
389
+ "deps": "...",
390
+ "compile_info": { "docker_image": "rockylinux:8", "qemu": true, "compiler": "gcc 8.5.0" }
391
+ }
392
+ ]
393
+ }
394
+ ```
395
+
396
+ ### 四、HTTP API 设计
397
+
398
+ 方案 A(静态文件)直接通过 URL 访问,无额外 API:
399
+
400
+ ```
401
+ GET /nginx-binaries/manifest.json → 全局版本列表
402
+ GET /nginx-binaries/1.31.0/manifest.json → 某版本的所有平台二进制
403
+ GET /nginx-binaries/1.31.0/x86_64-el7/nginx.tar.gz → 下载
404
+ GET /nginx-binaries/1.31.0/x86_64-el7/nginx.tar.gz.sha256 → 校验
405
+ GET /nginx-binaries/1.31.0/x86_64-el7/deps.txt → 审计信息
406
+ ```
407
+
408
+ 方案 B (MinIO) / C (Nexus) 通过 S3 API / Nexus API 访问,`binary-registry.ts` 封装差异。
409
+
410
+ ---
411
+
412
+ ### 五、Nginx 服务端实现(方案 A 示例)
413
+
414
+ ```nginx
415
+ # 在内部服务器上提供制品库 HTTP 访问
416
+ server {
417
+ listen 8080;
418
+ server_name registry.internal;
419
+
420
+ root /storage/nginx-binaries;
421
+
422
+ # 禁止目录浏览
423
+ autoindex off;
424
+
425
+ # 允许访问的文件类型
426
+ location ~ \.(json|gz|sha256|txt)$ {
427
+ autoindex on; # manifest.json 需要目录索引(或显式列出文件)
428
+ add_header Cache-Control "public, max-age=3600";
429
+ }
430
+
431
+ # 认证(可选,内网可关闭)
432
+ # auth_basic "Binary Registry";
433
+ # auth_basic_user_file /etc/nginx/.htpasswd;
434
+ }
435
+ ```
436
+
437
+ ### 六、二进制构建与上传流程(CI 侧)
438
+
439
+ 这部分**不在 release-helper 项目中**,而是单独的 CI 仓库或 GitLab CI 配置。
440
+
441
+ #### 6.1 编译矩阵配置
442
+
443
+ ```yaml
444
+ # nginx-binary-ci/build-config.yml
445
+ nginx_version: "1.31.0"
446
+ targets:
447
+ - id: x86_64-el7
448
+ arch: x86_64
449
+ os: centos
450
+ os_version: "7"
451
+ docker_image: centos:7
452
+ openssl_version: "1.0.2k"
453
+ - id: x86_64-el8
454
+ arch: x86_64
455
+ os: rocky
456
+ os_version: "8"
457
+ docker_image: rockylinux:8
458
+ openssl_version: "1.1.1k"
459
+ - id: x86_64-el9
460
+ arch: x86_64
461
+ os: rocky
462
+ os_version: "9"
463
+ docker_image: rockylinux:9
464
+ openssl_version: "3.0.7"
465
+ - id: aarch64-el8
466
+ arch: aarch64
467
+ os: rocky
468
+ os_version: "8"
469
+ docker_image: rockylinux:8
470
+ openssl_version: "1.1.1k"
471
+ qemu: true
472
+ - id: aarch64-el9
473
+ arch: aarch64
474
+ os: rocky
475
+ os_version: "9"
476
+ docker_image: rockylinux:9
477
+ openssl_version: "3.0.7"
478
+ qemu: true
479
+ ```
480
+
481
+ #### 6.2 构建脚本(每个目标执行一次)
482
+
483
+ ```bash
484
+ #!/bin/bash
485
+ # nginx-binary-ci/build.sh
486
+ set -euo pipefail
487
+
488
+ NGINX_VER="$1" # 1.31.0
489
+ TARGET_ID="$2" # x86_64-el7
490
+ DOCKER_IMG="$3" # centos:7
491
+ ARCH="$4" # x86_64
492
+
493
+ OUTPUT_DIR="output/${NGINX_VER}/${TARGET_ID}"
494
+ mkdir -p "$OUTPUT_DIR"
495
+
496
+ # 下载 nginx 源码(若未缓存)
497
+ [ -f "cache/nginx-${NGINX_VER}.tar.gz" ] || \
498
+ curl -fSL "https://nginx.org/download/nginx-${NGINX_VER}.tar.gz" -o "cache/nginx-${NGINX_VER}.tar.gz"
499
+
500
+ # 在 Docker 中编译
501
+ docker run --rm \
502
+ ${ARCH:+--platform "linux/${ARCH}"} \
503
+ -v "$(pwd)/cache:/src:ro" \
504
+ -v "$(pwd)/${OUTPUT_DIR}:/output" \
505
+ "$DOCKER_IMG" \
506
+ bash -c '
507
+ set -euo pipefail
508
+ yum install -y gcc gcc-c++ make pcre-devel zlib-devel openssl-devel
509
+ cd /tmp
510
+ tar xzf /src/nginx-'"${NGINX_VER}"'.tar.gz
511
+ cd nginx-'"${NGINX_VER}"'
512
+ ./configure \
513
+ --prefix=/etc/nginx \
514
+ --with-http_ssl_module \
515
+ --with-http_v2_module \
516
+ --with-http_gzip_static_module \
517
+ --with-http_stub_status_module \
518
+ --with-stream
519
+ make -j$(nproc)
520
+ make install
521
+
522
+ # 打包
523
+ cd /etc/nginx
524
+ tar czf /output/nginx.tar.gz .
525
+
526
+ # 生成依赖声明
527
+ ldd sbin/nginx > /output/deps.txt 2>&1 || true
528
+
529
+ # 生成 sha256
530
+ sha256sum /output/nginx.tar.gz | awk "{print \$1}" > /output/nginx.tar.gz.sha256
531
+ '
532
+
533
+ echo "Build complete: ${OUTPUT_DIR}/"
534
+ ```
535
+
536
+ #### 6.3 上传脚本
537
+
538
+ ```bash
539
+ #!/bin/bash
540
+ # nginx-binary-ci/upload.sh
541
+ # 上传到制品库(方案 A:scp/rsync 到内部服务器)
542
+
543
+ REGISTRY_HOST="registry.internal"
544
+ REGISTRY_PATH="/storage/nginx-binaries"
545
+
546
+ NGINX_VER="$1"
547
+
548
+ # 上传所有文件
549
+ rsync -avz "output/${NGINX_VER}/" \
550
+ "${REGISTRY_HOST}:${REGISTRY_PATH}/${NGINX_VER}/"
551
+
552
+ # 重新生成版本级 manifest.json
553
+ node scripts/generate-manifest.js "$NGINX_VER"
554
+
555
+ # 上传 manifest
556
+ scp "output/${NGINX_VER}/manifest.json" \
557
+ "${REGISTRY_HOST}:${REGISTRY_PATH}/${NGINX_VER}/manifest.json"
558
+
559
+ # 更新全局 manifest
560
+ scp "output/manifest.json" \
561
+ "${REGISTRY_HOST}:${REGISTRY_PATH}/manifest.json"
562
+ ```
563
+
564
+ #### 6.4 manifest 自动生成
565
+
566
+ ```javascript
567
+ // nginx-binary-ci/scripts/generate-manifest.js
568
+ import { readdirSync, readFileSync, statSync, writeFileSync } from 'fs';
569
+
570
+ const nginxBinaryVersion = process.argv[2]; // "1.31.0"
571
+ const outputDir = `output/${nginxBinaryVersion}`;
572
+
573
+ const targets = readdirSync(outputDir).filter(d =>
574
+ statSync(`${outputDir}/${d}`).isDirectory()
575
+ );
576
+
577
+ const binaries = targets.map(dir => {
578
+ const deps = readFileSync(`${outputDir}/${dir}/deps.txt`, 'utf-8');
579
+ const sha256 = readFileSync(`${outputDir}/${dir}/nginx.tar.gz.sha256`, 'utf-8').trim();
580
+ const size = statSync(`${outputDir}/${dir}/nginx.tar.gz`).size;
581
+
582
+ return {
583
+ id: dir,
584
+ arch: dir.split('-')[0],
585
+ os: extractOS(dir),
586
+ file_url: `${nginxBinaryVersion}/${dir}/nginx.tar.gz`,
587
+ sha256,
588
+ file_size: size,
589
+ deps
590
+ };
591
+ });
592
+
593
+ writeFileSync(`${outputDir}/manifest.json`, JSON.stringify({
594
+ nginx_version: nginxBinaryVersion,
595
+ built_at: new Date().toISOString(),
596
+ binaries
597
+ }, null, 2));
598
+ ```
599
+
600
+ ---
601
+
602
+ ### 七、release-helper 侧:下载与集成
603
+
604
+ #### 7.1 `src/binary-registry.ts` 实现
605
+
606
+ ```typescript
607
+ interface BinaryRegistryConfig {
608
+ url: string; // e.g. "http://registry.internal:8080"
609
+ type: 'http' | 'minio' | 'nexus';
610
+ auth?: { username: string; password: string };
611
+ }
612
+
613
+ interface BinaryInfo {
614
+ id: string;
615
+ arch: string;
616
+ os: string;
617
+ osVersion: string;
618
+ openssl: string;
619
+ fileUrl: string;
620
+ sha256: string;
621
+ deps: string;
622
+ }
623
+
624
+ class BinaryRegistry {
625
+ constructor(private config: BinaryRegistryConfig) {}
626
+
627
+ // 获取指定 nginx 版本的所有平台二进制信息
628
+ async listBinaries(nginxVersion: string): Promise<BinaryInfo[]> {
629
+ const manifestUrl = `${this.config.url}/${nginxVersion}/manifest.json`;
630
+ const resp = await fetch(manifestUrl);
631
+ const manifest = await resp.json();
632
+ return manifest.binaries;
633
+ }
634
+
635
+ // 下载单个二进制并校验
636
+ async downloadBinary(info: BinaryInfo, destDir: string): Promise<string> {
637
+ const url = `${this.config.url}/${info.fileUrl}`;
638
+ const destPath = `${destDir}/${info.id}/nginx.tar.gz`;
639
+
640
+ // 下载
641
+ await downloadFile(url, destPath);
642
+
643
+ // 校验 sha256
644
+ const actualSha256 = await sha256File(destPath);
645
+ if (actualSha256 !== info.sha256) {
646
+ throw new Error(`SHA256 mismatch for ${info.id}: expected ${info.sha256}, got ${actualSha256}`);
647
+ }
648
+
649
+ // 写入 deps.txt 供审计
650
+ await writeFile(`${destDir}/${info.id}/deps.txt`, info.deps);
651
+
652
+ return destPath;
653
+ }
654
+
655
+ // pack 流程调用:下载所有平台二进制
656
+ async downloadAll(nginxVersion: string, destDir: string): Promise<void> {
657
+ const binaries = await this.listBinaries(nginxVersion);
658
+ logger.info(`Found ${binaries.length} platform binaries for nginx ${nginxVersion}`);
659
+
660
+ // 并发下载
661
+ await Promise.all(
662
+ binaries.map(b => this.downloadBinary(b, destDir))
663
+ );
664
+ }
665
+ }
666
+ ```
667
+
668
+ #### 7.2 pack 流程改造
669
+
670
+ ```typescript
671
+ // src/pack.ts 核心逻辑
672
+ export async function pack(version?: string) {
673
+ // ... 前面的逻辑(版本输入、校验配置等)不变 ...
674
+
675
+ // 复制安装脚本和 nginx 模板(不再复制二进制)
676
+ fs.cpSync(resolve(__dirname, 'deploy/script'), resolve(projectDir, 'script'), { recursive: true });
677
+ fs.cpSync(resolve(__dirname, 'deploy/pkg/nginx'), resolve(projectDir, 'pkg/nginx'), { recursive: true });
678
+
679
+ // 从制品库下载所有平台 nginx 二进制
680
+ if (releaseConf.binaryRegistry?.url) {
681
+ const registry = new BinaryRegistry(releaseConf.binaryRegistry);
682
+ const nginxVersion = '1.31.0'; // 可从配置指定
683
+ await registry.downloadAll(nginxVersion, resolve(projectDir, 'pkg/nginx_binary'));
684
+ } else {
685
+ // 回退:如果没有配置制品库,从本地 src/deploy/pkg/nginx_binary 复制
686
+ // (兼容开发环境或离线场景)
687
+ fs.cpSync(resolve(__dirname, 'deploy/pkg/nginx_binary'), resolve(projectDir, 'pkg/nginx_binary'), { recursive: true });
688
+ }
689
+
690
+ // ... 后续打包逻辑不变 ...
691
+ }
692
+ ```
693
+
694
+ #### 7.3 离线场景支持
695
+
696
+ ```yaml
697
+ # release.conf.yaml
698
+ binaryRegistry:
699
+ url: "" # 为空时使用本地二进制回退
700
+ type: "http"
701
+
702
+ # 离线场景:制品库不可达时使用本地备份
703
+ # 本地备份放在 src/deploy/pkg/nginx_binary/(不提交到 git,但可手动放置)
704
+ ```
705
+
706
+ ### 八、版本管理策略
707
+
708
+ ```
709
+ 1. 每次编译新的 nginx 二进制时,创建新的版本目录(如 1.31.0/)
710
+ 2. 旧版本目录保留不删除(支持回滚)
711
+ 3. manifest.json 中的 versions 数组维护可用版本列表
712
+ 4. release.conf.yaml 中可指定使用哪个 nginx 版本:
713
+
714
+ nginxBinaryVersion: "1.31.0" # 默认使用 latest
715
+ ```
716
+
717
+ ### 九、安全与权限
718
+
719
+ | 层面 | 措施 |
720
+ |------|------|
721
+ | 传输 | HTTPS(内网可用自签名证书,`rejectUnauthorized: false`) |
722
+ | 完整性 | SHA256 校验,下载后验证 |
723
+ | 访问控制 | 方案 A:nginx basic auth 或 IP 白名单;方案 B/C:各自认证机制 |
724
+ | 审计 | 每个二进制附带 deps.txt,记录完整动态链接依赖 |
725
+ | 上传 | CI 专用 token/SSH key,非人工上传 |
726
+
727
+ ---
728
+
729
+ ## 第六阶段:构建与测试
730
+
731
+ ### 6.1 二进制矩阵构建(CI 侧,非项目代码)
732
+
733
+ 在制品库/CI 中维护编译矩阵:
734
+
735
+ ```yaml
736
+ # nginx-binary-build.yml (放在单独的 CI 仓库)
737
+ targets:
738
+ - id: x86_64-el7
739
+ docker_image: centos:7
740
+ arch: x86_64
741
+ - id: x86_64-el8
742
+ docker_image: rockylinux:8
743
+ arch: x86_64
744
+ - id: x86_64-el9
745
+ docker_image: rockylinux:9
746
+ arch: x86_64
747
+ - id: aarch64-el8
748
+ docker_image: rockylinux:8
749
+ arch: aarch64
750
+ qemu: true
751
+ - id: aarch64-el9
752
+ docker_image: rockylinux:9
753
+ arch: aarch64
754
+ qemu: true
755
+ ```
756
+
757
+ 每个 Docker 容器中:动态链接编译 nginx,输出 `nginx.tar.gz` + `deps.txt`。
758
+
759
+ ### 6.2 测试(本项目新增)
760
+
761
+ 依赖新增:`vitest`、`memfs`
762
+
763
+ ```
764
+ src/
765
+ ├── pack.test.ts # 测试 pack 流程(memfs mock 文件系统)
766
+ ├── config/schema.test.ts # 测试 Zod schema 校验
767
+ ├── platforms/gitlab.test.ts # 测试 GitLab API 调用(nock mock)
768
+ └── templates.test.ts # 测试 Handlebars 模板渲染
769
+ ```
770
+
771
+ ---
772
+
773
+ ## 文件变更清单
774
+
775
+ ### 新建文件
776
+ ```
777
+ # 核心功能
778
+ src/config/schema.ts # Zod 配置 schema
779
+ src/config/env.ts # 环境变量解析
780
+ src/platform/types.ts # Platform 接口定义
781
+ src/platforms/gitlab.ts # GitLab 实现(从 publish.ts 迁移)
782
+ src/templates.ts # Handlebars 模板渲染
783
+ src/binary-registry.ts # 制品库下载逻辑
784
+
785
+ # 制品库 CI 侧(单独仓库)
786
+ nginx-binary-ci/build-config.yml # 编译矩阵配置
787
+ nginx-binary-ci/build.sh # Docker 中编译 nginx
788
+ nginx-binary-ci/upload.sh # 上传到制品库
789
+ nginx-binary-ci/scripts/generate-manifest.js # manifest 自动生成
790
+
791
+ # 制品库服务端(方案 A)
792
+ nginx-binary-registry/nginx.conf # nginx 配置,提供 HTTP 访问
793
+
794
+ # 测试
795
+ src/pack.test.ts
796
+ src/config/schema.test.ts
797
+ src/templates.test.ts
798
+ src/binary-registry.test.ts
799
+ ```
800
+
801
+ ### 修改文件
802
+ ```
803
+ src/cli.ts # 新增 --dry-run, --platform 选项
804
+ src/prepare.ts # 改用 Zod 校验, 支持 ${ENV} 语法
805
+ src/pack.ts # 重构:模板引擎 + 制品库下载
806
+ src/publish.ts # 简化,委托给 Platform 实现
807
+ src/release.ts # 适配新接口
808
+ src/deploy/script/common.sh # 修复 bug, install/upgrade 差异化
809
+ src/deploy/script/nginx.sh # 重构二进制选择, 启用 __root__ 替换
810
+ src/deploy/script/install.sh # 差异化逻辑
811
+ src/deploy/script/upgrade.sh # 差异化逻辑
812
+ src/deploy/script/prompt.sh # 统一占位符
813
+ src/deploy/pkg/nginx/*.tpl # 统一 {{VAR}} 占位符
814
+ src/.release/release.conf.yaml # 新增 platform, binaryRegistry 字段
815
+ src/.release/nginx/nginx.conf # 统一 {{VAR}} 占位符
816
+ package.json # 新增 zod, handlebars, vitest 依赖
817
+ rollup.config.js # 可能需要调整 handlebars 打包
818
+ ```
819
+
820
+ ### 删除/清理
821
+ ```
822
+ src/deploy/pkg/nginx_binary/ # 从 Git 仓库移除(移到制品库)
823
+ src/deploy/pkg/nginx_binary/binary/bak_1.22.1/ # 清理旧版本
824
+ ```
825
+
826
+ ---
827
+
828
+ ## 实施顺序与阶段
829
+
830
+ | 阶段 | 内容 | 风险 | 可回滚 |
831
+ |------|------|------|--------|
832
+ | 0. 制品库搭建 | 存储部署 + CI 构建流水线 + manifest | 中 | 是(保留旧二进制在 Git 中) |
833
+ | 1. Plugin 化 | Platform 接口 + GitLab 迁移 | 低 | 是 |
834
+ | 2. 配置系统 | Zod + 环境变量 | 低 | 是 |
835
+ | 3. Bash 脚本修复 | Bug fix + 差异化 | 中 | 是(需测试) |
836
+ | 4. 模板统一 | Handlebars + 占位符统一 | 中 | 是(兼容旧语法过渡) |
837
+ | 5. 制品库集成 | binary-registry.ts + pack 改造 | 高 | 是(无制品库配置时回退本地) |
838
+ | 6. Changelog + dry-run | Conventional Commits | 低 | 是 |
839
+ | 7. 测试 | vitest + memfs | 低 | 是 |
840
+
841
+ **阶段 0(制品库)必须先于阶段 5 完成**,否则 pack 无法获取二进制。过渡期保留 `src/deploy/pkg/nginx_binary/` 作为本地回退。
842
+
843
+ ---
844
+
845
+ ## 验证方案
846
+
847
+ ### 制品库侧(阶段 0)
848
+ 1. **构建验证**:CI 流水线跑完,每个目标目录包含 `nginx.tar.gz` + `nginx.tar.gz.sha256` + `deps.txt`
849
+ 2. **HTTP 验证**:`curl http://registry.internal:8080/1.31.0/manifest.json` 返回正确 JSON
850
+ 3. **下载校验**:手动下载 `nginx.tar.gz`,比对 sha256 与 `.sha256` 文件一致
851
+ 4. **部署验证**:下载的 nginx 二进制在对应 Docker 容器中 `ldd` 检查依赖匹配 `deps.txt`
852
+
853
+ ### release-helper 侧
854
+ 5. **本地测试**:在示例项目中执行 `release-helper init → pack`,验证 tar.gz 结构正确
855
+ 6. **脚本测试**:在 CentOS 7/8/9 Docker 容器中运行 `install.sh`,验证 nginx 安装和功能正常
856
+ 7. **模板测试**:运行 `vitest src/templates.test.ts`,验证所有占位符正确替换
857
+ 8. **配置测试**:运行 `vitest src/config/schema.test.ts`,验证必填字段和类型校验
858
+ 9. **制品库下载测试**:运行 `vitest src/binary-registry.test.ts`,mock HTTP 下载并校验
859
+ 10. **dry-run 测试**:`release-helper release --dry-run` 输出操作列表但不执行
860
+ 11. **离线回退测试**:制品库不可达时,pack 自动回退到本地 `src/deploy/pkg/nginx_binary/`
861
+ 12. **平台测试**:切换 `platform: gitlab` 后,完整 release 流程正常