docs: define hardened deployment and CI design

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
zyycn
2026-07-11 16:00:04 +08:00
co-authored by Codex
parent 4d48a7bca1
commit 49120d0efb
+300 -41
View File
@@ -1,61 +1,320 @@
# 部署与 CI 审查(deploy/ + .github/workflows/)
# 部署与 CI 改造设计
> 审查于 `5f2839db`,覆盖 `deploy/`(Dockerfile / docker-compose.yml / dockerignore / README)与 `.github/workflows/`(ci.yml / release.yml / security-scan.yml)。
> 设计基线:`e4b274a2a6095f22a7f526732ba4886f60441218`
>
> 适用范围:`deploy/`、`.github/workflows/`、`release/publish`、运行时配置加载与部署说明。
整体底子相当好:多阶段构建、BuildKit cache mount、非 root 用户、`cap_drop: ALL`、`no-new-privileges`、PG18 新卷布局挂法正确(`/var/lib/postgresql` 而非 `/data` 子目录)、发布侧 SBOM + provenance + cosign keyless 签名齐全。以下按优先级列出值得动的点。
本文是本轮部署与 CI 改造的权威规格。实现必须保持 feature 分支 push 检查,消除同一提交的重复镜像构建,保证发布提交经过完整质量门禁,并用真实 PostgreSQL、Redis 和应用容器验证镜像能够无警告、无错误地启动和退出。
## P0 — 每次 push 两次全冷 Docker 构建(最大浪费)
## 1. 已确认的现状
- 现状:`ci.yml` 的 `deploy` job 与 `security-scan.yml` 的 `docker-security` job,**每次 push 到任意分支**都各自 `docker build --target runtime` 一次。BuildKit 的 `RUN --mount=type=cache` 在 CI runner 上不跨 run 持久,所以这是两次完全从零的 release 编译(backend + frontend),每次十余分钟 × 2。
- 建议:
- `docker-security`(trivy)只在 `schedule` + main push 跑;
- `ci.yml` 的 `deploy` job 加 `paths` 过滤(`deploy/**`、`backend/Cargo.lock`、workflow 自身)或只在 PR / main 跑;
- 更进一步:security-scan 复用 ci 构建好的镜像(artifact 传递或按 digest 从 registry 拉),不重复构建。
- 顺带(触发面收敛):`push: branches: ['**']` + `pull_request:` 双触发——开 PR 的分支会全量跑两遍(push ref 与 PR merge ref 的 concurrency group 不同,互不取消)。走 PR 流程就把 push 限成 main;从不开 PR 就删掉 `pull_request:`。二选一。
### 1.1 重复构建真实存在,但原审计定级和耗时不准确
## P1 — 供应链输入侧与输出侧不对称
- `ci.yml` 的 `deploy` 与 `security-scan.yml` 的 `docker-security` 会在每次 push 和 pull request 中分别构建一次 `runtime` 镜像。
- 已成功运行的远端记录显示,两次构建合计约占 8 分钟 runner 时间;两个 workflow 并行执行,不能按墙钟时间简单相加为“十余分钟乘二”。
- 最近两次失败分别在 9 秒和 6 秒落到同一个镜像构建错误,证明失败路径也被重复执行。
- GitHub Actions 不支持给普通 job 直接配置 `paths`。路径判断必须由独立 job 产生输出,或使用 workflow 级路径过滤。
- 现状:发布产物做到了 cosign + attestation 水准,但 workflow 里所有第三方 action(`dtolnay/rust-toolchain`、`Swatinem/rust-cache`、`pnpm/action-setup`、`docker/*`、`softprops/action-gh-release`、`aquasecurity/trivy-action`、`sigstore/cosign-installer`)都是 **tag 引用**。tag 可被 force-push,tj-actions 供应链事件即由此而来。
- 建议:非 `actions/*` 官方之外的 action 一律 pin 到 commit SHA(注释保留版本号)。
- 顺带:`.github/` 下**没有 dependabot.yml**,action 版本更新目前全靠手动。加 `package-ecosystem: github-actions`(可顺带 cargo + npm)自动提更新 PR,与 SHA pin 配合正好。
### 1.2 Compose 密码与应用连接串没有真正同步
## P1 — Redis 裸奔,与 Postgres 不一致
- PostgreSQL 和 Redis 服务端密码来自 `CPR_POSTGRES_PASSWORD`、`CPR_REDIS_PASSWORD`。
- 应用当前只读取 YAML 中的固定 URL;仅修改 Compose 环境变量会导致服务端密码和客户端连接串不一致。
- Redis 部署说明要求人工同步 URL,PostgreSQL 没有同等说明;这种双写方式必须删除。
> 状态:2026-07-11 已完成。Compose 使用 `CPR_REDIS_PASSWORD` 配置 `requirepass`,健康检查与应用连接串已同步。
### 1.3 发布门禁与发布镜像扫描缺失
- 现状:`docker-compose.yml` 中 Postgres 有密码,Redis 没有 `requirepass`——同 compose 网络内任何容器、以及宿主机本地进程(绑了 `127.0.0.1:6379`)均可无认证读写。
- 建议:`command` 加 `--requirepass ${CPR_REDIS_PASSWORD:-...}`,应用侧连接串同步。
- 顺带:pg / redis 的宿主端口映射(`127.0.0.1:5432` / `127.0.0.1:6379`)若只是偶尔调试用,建议注释掉——应用走 compose 内网,不需要它们。
- `release/publish` 会生成新的版本提交和 tag,并原子推送分支与 tag。这个新提交此前没有经过 CI。
- 通过查询 check-runs 判断“曾经绿过”存在竞态,也无法证明版本提交本身通过测试。
- Security Scan 扫描的是源码构建的 `runtime`,Release 发布的是由预编译产物组装的 `runtime-prebuilt`。二者不是同一镜像。
## P1 — 日志与资源限制不一致
### 1.4 CI 没有运行态烟测
> 状态:2026-07-11 已完成日志上限;PostgreSQL / Redis 资源上限仍待单独评估。三服务统一使用 `json-file` `10m × 5`。
当前 deploy job 只执行 Compose 配置解析和 Docker 构建,不能发现以下问题:
- 现状:三个服务的 stdout 走默认 `json-file` driver,没配 `max-size` / `max-file`,长期运行吃满磁盘(容器内 `/app/logs` 还写一份);postgres / redis 没有任何资源限制,而 app 有(`cpus` / `mem_limit` / `pids_limit`)——PG 内存失控会拖垮宿主。
- 建议:三个服务统一加 `logging` 限制(或宿主 daemon.json 全局配);给 pg / redis 补资源上限;Postgres 大查询会碰默认 64MB shm,可按需加 `shm_size`。
- entrypoint 或动态链接错误;
- UID/GID `10001:10001` 无法写入 bind mount;
- PostgreSQL 迁移或 Redis 连接失败;
- `/healthz` 无法通过;
- 启动或关闭期间产生 WARN、ERROR、panic;
- PostgreSQL、Redis 数据卷在容器替换后丢失。
## P2 — release 无测试门禁
### 1.5 发布标签会被预发布版本污染
- 现状:`release.yml` tag 一推即构建发布,不校验该 commit 的 CI 是否绿过。从未过 CI 的 commit 打 tag 也能发版。
- 建议:`release-info` 后加一个 job 用 `gh api` 查该 SHA 的 check-runs 结论,不绿即 fail;或在 release 流程内跑一次 `cargo test`。
当前 SemVer 校验允许 `1.2.0-rc.1`,但镜像 manifest 无条件更新 `latest`,GitHub Release 也没有显式标记 prerelease。预发布版本会错误占用稳定通道。
## P2 — 小件(顺手可做)
### 1.6 供应链输入没有固定
- `ci.yml` 与 `release.yml` 所有 job 都没有 `timeout-minutes`(security-scan 有)——挂住的 job 默认吃满 6 小时。
- `cargo install cargo-audit` 首次 / 缓存失效时现编译数分钟,换 `taiki-e/install-action`(预编译秒装);或升级成 `cargo-deny`,advisory + license + 重复依赖一起管。
- trivy 只做门禁不留记录,加 SARIF 输出 + `security-events: write`,可在 GitHub Security tab 看历史趋势。
- `release.yml` L222 / L237 `mkdir -p ../dist/binaries/...` 重复两次;`build-and-push` 的 `id` 无人引用。纯洁癖。
- `Dockerfile` 的 `CPR_VERSION` 默认空串,手动构建 `release-asset` target 不传参会产出 `codex-proxy-rs__linux_amd64.tar.gz`(双下划线)。加个默认值或 fail-fast 均可。
- workflow 中所有 action 均使用可移动 tag,而不是完整 commit SHA。
- Dockerfile、Compose 和 CI service container 均使用可移动镜像 tag。
- 仓库没有 Dependabot 配置,固定后的依赖无法形成自动更新闭环。
## 核实过的误报(记录一下,免得反复怀疑)
## 2. 目标与验收口径
- **CI 只跑 `cargo test --test main` 不算漏测**:`backend/src` 零 `#[cfg(test)]`,所有测试都走 `tests/main.rs` 单入口,`--test main` 即全量。
- **frontend CI 没单独 typecheck 步骤不算漏**:`build` 脚本本身是 `vue-tsc -b && vite build`,typecheck 已含在 build 里。
- **postgres:18 挂 `/var/lib/postgresql` 是对的**:PG18 官方镜像改了数据布局(`PGDATA=/var/lib/postgresql/<major>/docker`),挂整个父目录才能支持原地 pg_upgrade;挂 `/data` 子目录反而是旧习惯踩坑。
- **rust 版本一致性没问题**:Dockerfile `rust:1.95-bookworm`、CI `dtolnay/rust-toolchain@1.95`、release 构建容器 `rust:1.95-bookworm` 三处对齐。
本轮实现必须同时满足以下条件:
## 建议动手顺序
1. feature 分支 push 与 pull request 都保留;同一分支的两个事件共享 concurrency group,后到事件取消先到事件,不并行重复执行完整检查。
2. 普通 CI 对同一提交最多构建一次源码版 `runtime` 镜像;该镜像同时用于 Trivy 门禁和 Compose 烟测。
3. Release 对 tag 指向的精确 SHA 重新执行完整质量门禁,不依赖历史 check-runs。
4. Release 扫描实际推送的每个平台 `runtime-prebuilt` digest,扫描通过后才能创建多架构 manifest 和 GitHub Release。
5. Rust format、clippy、测试,前端格式和构建,workflow 静态检查全部为 0 错误、0 警告。
6. Compose 烟测使用真实 PostgreSQL 和 Redis,应用健康检查返回 HTTP 204,三个容器日志中不出现 WARN、WARNING、ERROR、FATAL、PANIC 严重级别。
7. 应用容器以 UID/GID `10001:10001` 运行,数据和日志目录可写,SIGTERM 后在 30 秒内以退出码 0 结束。
8. PostgreSQL 和 Redis 密码只需在 Compose 环境中设置一次;包含 URL 特殊字符的密码也能正确连接,错误信息不得泄露密码。
9. PostgreSQL 和 Redis 的命名卷在服务容器强制重建后仍保留探针数据。
10. 所有 action 和容器镜像输入固定到不可变 SHA 或 digest,并由 Dependabot 持续提出更新。
1. **P0**:CI 构建去重 + 触发收敛(纯 workflow 改动,立竿见影省分钟数)。
2. **P1 三条**:SHA pin + dependabot、Redis 密码 + 端口收敛、日志/资源限制(前者动 workflow,后两者动 compose,互不冲突可一批)。
3. **P2**:release 门禁与小件,顺手时清掉。
## 3. 非目标
以下内容不在本轮实施范围内:
- 不删除 PostgreSQL、Redis 的 `127.0.0.1` 宿主端口映射;本地集成测试仍依赖它们。
- 不凭经验给 PostgreSQL、Redis 增加 CPU、内存或 `shm_size` 上限。资源预算应基于真实负载单独评审。
- 不改变 PostgreSQL、Redis 的命名卷持久化方案,也不把数据库数据迁移到应用的 `.runtime/` bind mount。
- 不改变日志表和统计表保留策略;该范围已由日志与保留策略改造处理。
- 不引入兼容配置键、旧密码路径或 YAML 与环境变量双写逻辑。
## 4. 工作流架构
目标工作流由两个可复用门禁和三个入口组成:
```text
CI push / pull_request
-> changes
-> _quality.yml(ref)
-> _container.yml(ref), 仅镜像相关路径变化时执行
Security Scan push / pull_request
-> Cargo 与 pnpm 依赖审计
Security Scan schedule
-> Cargo 与 pnpm 依赖审计
-> _container.yml(ref)
Release tag / workflow_dispatch
-> release-info
-> _quality.yml(tag SHA, 上传 frontend-dist)
-> 平台二进制与 release assets
-> runtime-prebuilt 平台镜像
-> 精确 digest Trivy 门禁
-> 多架构 manifest、签名、attestation、GitHub Release
```
### 4.1 `.github/workflows/_quality.yml`
`_quality.yml` 只通过 `workflow_call` 调用,接收:
- `ref`:必填的完整提交 SHA;所有 checkout 都显式使用它。
- `upload_frontend`:布尔值,默认 `false`;Release 传 `true`。
它包含以下边界清晰的 job:
- `backend`:固定 Rust 1.95,执行 `cargo fmt --check`、`cargo clippy --all-targets --all-features --locked -- -D warnings`、`cargo test --test main --locked`。
- `frontend`:固定 Node 24 和 pnpm 11.7.0,执行 frozen install、`format:check`、`build`。`build` 已包含 `vue-tsc`。
- `workflow-lint`:对 `.github/workflows/*.yml` 执行 actionlint。
Release 请求 artifact 时,`frontend` job 上传由同一 SHA 产生的 `frontend-dist`。Release 不再维护第二套前端构建 job。
`backend`、`frontend`、`workflow-lint` 的 timeout 分别为 30、15、5 分钟。Rust 测试使用固定 digest 的 PostgreSQL 18 和 Redis 8 service container,并显式传递测试连接串。actionlint 使用固定版本和 digest,不在运行时下载浮动的 latest 二进制。
### 4.2 `.github/workflows/_container.yml`
`_container.yml` 只通过 `workflow_call` 调用,接收必填的完整提交 SHA,job timeout 为 45 分钟。单次调用按以下顺序执行:
1. checkout 精确 SHA;
2. 校验 `docker compose config`;
3. 使用 Buildx、GHA cache 构建一次 `runtime` 并 `load` 到本地 tag;
4. 对该本地镜像生成 SARIF;
5. 对同一镜像执行 HIGH/CRITICAL、`ignore-unfixed` 的表格门禁,命中即失败;
6. 使用同一镜像执行 Compose 烟测;
7. 无论成功或失败都收集容器状态和日志,最后执行 `down -v --remove-orphans`。
SARIF 通过固定 SHA 的 CodeQL upload action 写入 GitHub Security。来自 fork 的 pull request 没有 `security-events: write`,因此只跳过 SARIF 上传,不跳过表格漏洞门禁。
### 4.3 `.github/workflows/ci.yml`
CI 保留所有分支的 `push` 与 `pull_request`。两种事件统一使用 head repository 与 branch name 组成 concurrency group,并启用 `cancel-in-progress`,使同一 feature 分支的 push 和 PR 不同时跑两套完整门禁。
入口统一计算 `source_sha`:push 使用 `github.sha`,pull request 使用 `pull_request.head.sha`。质量门禁和容器门禁必须 checkout 同一个值。
`changes` job 的 timeout 为 5 分钟,使用固定 SHA 的 paths-filter,仅决定是否调用 `_container.yml`。镜像相关路径固定为:
- `backend/**`
- `frontend/**`
- `deploy/**`
- `.dockerignore`
- `release/version.yaml`
- `.github/workflows/_container.yml`
- `.github/workflows/ci.yml`
`_quality.yml` 始终执行;不能因只改文档而跳过格式、编译和测试。
### 4.4 `.github/workflows/security-scan.yml`
push 和 pull request 只执行 Cargo 与 pnpm 依赖审计,不再构建镜像。周一定时任务执行两类依赖审计,并额外调用 `_container.yml` 扫描当前默认分支的精确 SHA。
push 与 pull request 使用和 CI 相同的 head repository、branch name concurrency 策略,避免同一 feature 分支同时执行两套依赖审计。
依赖审计保留 HIGH 级别门禁,backend、frontend timeout 分别为 20、15 分钟。`cargo-audit` 使用显式版本和 `--locked` 安装,不为节省几十秒新增不必要的 action。
### 4.5 权限边界
入口 workflow 默认只有 `contents: read`。仅以下 job 提升权限:
- SARIF 上传需要 `security-events: write`,fork pull request 不执行上传;
- Release 平台镜像 job 需要 `packages: write` 与 `id-token: write`;
- Release publish job 需要 `contents: write`、`packages: write`、`id-token: write`、`attestations: write`。
任何执行 pull request 中不可信脚本的 job 都不能获得 package、content 或 attestation 写权限。
## 5. 运行时密码与配置
### 5.1 单一配置来源
`AppConfig` 保持当前 YAML 结构,新增两个可选的运行时环境变量:
- `CPR_POSTGRES_PASSWORD`
- `CPR_REDIS_PASSWORD`
加载顺序固定为:
1. 严格解析完整 YAML;
2. 读取两个可选环境变量;
3. 使用 `url::Url` 仅替换 `database.url`、`redis.url` 的 password 部分;
4. 返回最终 `AppConfig`。
禁止用字符串拼接或替换 URL。用户名、scheme、host、port、path、query 均保持 YAML 原值。密码按原始字节内容交给 URL API 编码,不 trim;空值视为无效配置。URL 解析失败、URL 不支持密码或环境变量为空时,返回明确但不包含密码内容的配置错误。
`AppConfig::load()` 与 `AppConfig::load_from_dir()` 使用同一加载路径,不允许产生两套行为。相关测试全部放在 `backend/tests/bootstrap/config/`,环境变量场景使用隔离子进程,禁止在 `backend/src` 写测试代码。
### 5.2 Compose 传递
Compose 对数据库服务与应用服务使用完全相同的展开值:
- PostgreSQL 的 `POSTGRES_PASSWORD` 与应用的 `CPR_POSTGRES_PASSWORD` 都来自 `${CPR_POSTGRES_PASSWORD:-codex_proxy}`。
- Redis 的 `--requirepass`、healthcheck 与应用的 `CPR_REDIS_PASSWORD` 都来自 `${CPR_REDIS_PASSWORD:-codex_proxy}`。
因此默认配置无需修改 URL;生产环境只设置两个 Compose 变量,不再人工编辑 YAML 密码。`deploy/config.example.yaml` 保留可独立运行的默认 URL,环境变量在加载后安全覆盖其中的 password。
### 5.3 bind mount 权限
部署文档必须给出 Linux 首次初始化命令,并明确以下权限:
- `.runtime/data`、`.runtime/logs` 归 UID/GID `10001:10001` 所有且不可被其他用户写入;
- `.runtime/config.yaml` 对容器 GID 10001 可读,对其他用户不可读;
- 修改配置后保持原 owner、group 和 mode。
这不是文档提示项,而是应用以非 root 用户启动的前置条件;Compose 烟测必须从新建目录验证该条件。
## 6. Compose 烟测
烟测使用临时工作目录和专用 Compose project,不能复用开发机 `.runtime/` 数据。配置文件通过 Ruby YAML API 从模板生成,写入至少 32 个 URL-safe 随机字符的管理员密码;禁止用 `sed` 修改 YAML。
PostgreSQL 与 Redis 密码必须包含 `@`、`:`、`/`、`?`、`#`、`%` 中的多种字符,以证明 URL 覆盖正确编码。执行顺序如下:
1. 创建 owner 为 `10001:10001`、mode 为 `0750` 的数据和日志目录;配置文件 mode 为 `0640`,owner 为 runner UID,group 为 10001;
2. 只启动 PostgreSQL、Redis 并等待健康;
3. 分别写入 PostgreSQL 表探针和 Redis key;
4. 强制重建 PostgreSQL、Redis 服务容器;
5. 验证两个探针仍存在,再删除探针;
6. 以刚构建的本地镜像和 `--no-build` 启动应用;
7. 验证 `/healthz` 返回 HTTP 204;
8. 验证容器配置和进程实际 UID/GID 均为 `10001:10001`;
9. 验证 `/app/data`、`/app/logs` 可写;
10. 按日志级别或独立严重度单词扫描三个容器完整日志,不允许出现 WARN、WARNING、ERROR、FATAL、PANIC;字段名中的 `ops_error_logs` 等普通文本不构成失败;
11. 向应用发送 SIGTERM,验证 30 秒内退出且退出码为 0。
失败时必须上传 Compose config、`docker compose ps -a`、inspect 结果和三个容器日志。清理步骤使用 `if: always()`,不得因前序失败遗留容器或命名卷。
## 7. Release 设计
### 7.1 精确 SHA 质量门禁
`release-info` 继续校验 tag 与 `release/version.yaml` 一致,并输出 tag 指向的完整 `git_sha`。新增 `quality` reusable job,以该 SHA 调用 `_quality.yml` 且上传 `frontend-dist`。
所有二进制、release asset 和镜像 job 都依赖 `quality`。任一格式、警告、测试、前端构建或 workflow 检查失败,Release 在产生架构暂存镜像或版本级发布产物前终止。禁止查询历史 check-runs 代替该门禁。
### 7.2 精确发布镜像扫描
每个平台的 `build-and-push` 输出 digest 必须被实际使用。平台 job 按以下顺序执行:
1. 从同一 release SHA 的二进制和 `frontend-dist` 组装 `runtime-prebuilt`;
2. 推送架构级 `sha-<git_sha>-<arch>` 暂存 tag;
3. 使用 `registry/image@sha256:...` 扫描 Buildx 返回的精确 digest;
4. 仅扫描通过的架构级 tag 可进入 manifest 汇总。
漏洞门禁失败时允许留下不可变 SHA 暂存 tag,但不得创建版本 manifest、`latest`、签名、attestation 或 GitHub Release。
### 7.3 稳定版与预发布版
`release-info` 增加明确的 `prerelease` 布尔输出。标签规则如下:
| 发布类型 | 多架构 tag | GitHub Release |
| --- | --- | --- |
| 稳定版 | `<version>`、`sha-<git_sha>`、`latest` | `prerelease=false`,设为 latest |
| 预发布版 | `<version>`、`sha-<git_sha>` | `prerelease=true`,不得设为 latest |
Cosign 只签名本次实际创建的 tag。预发布版本不得在 manifest、签名参数或 Release metadata 中引用 `latest`。
### 7.4 发布清理
- `release-info`、`package-assets`、`publish` timeout 分别为 10、20、20 分钟;二进制矩阵 job 为 90 分钟,平台镜像 job 为 45 分钟。
- 删除重复的 release asset 目录创建。
- Dockerfile 的 `release-asset` target 在 `CPR_VERSION` 为空时立即失败,不能产生双下划线文件名。
- `build-and-push` 的 step id 保留并用于 digest 扫描,不再是死标识。
## 8. 供应链固定与自动更新
### 8.1 不可变输入
以下输入全部固定:
- 所有 `uses:`,包括 `actions/*`,使用完整 40 位 commit SHA,同行注释保留可读版本号;
- `dtolnay/rust-toolchain` 固定 action SHA,并显式设置 `toolchain: '1.95'`,不再依赖 ref 名携带工具链版本;
- Dockerfile syntax frontend、全部 `FROM`、Compose service image、CI service container 和 release build container 使用 `tag@sha256:digest`;
- pnpm 继续固定为 11.7.0,Rust 继续固定为 1.95。
禁止在安全修复中把可移动 tag 换成另一个可移动 tag。
### 8.2 Dependabot
新增 `.github/dependabot.yml`,每周检查:
- `/` 的 `github-actions`;
- `/backend` 的 Cargo;
- `/frontend` 的 npm/pnpm;
- `/deploy` 的 Docker。
同生态系统的小版本和补丁更新分组,减少 PR 噪声。Dependabot 更新后仍必须经过相同质量、漏洞和烟测门禁。
## 9. 验证矩阵
实现完成后必须按顺序验证,发现问题先修复再继续:
1. `git diff --check`,并扫描 `TBD`、`TODO`、未固定 action tag、未固定容器 tag;
2. `cargo fmt --manifest-path backend/Cargo.toml -- --check`;
3. `cargo clippy --manifest-path backend/Cargo.toml --all-targets --all-features --locked -- -D warnings`;
4. 使用带密码 PostgreSQL、Redis 执行 `cargo test --manifest-path backend/Cargo.toml --test main --locked`;
5. `pnpm --dir frontend install --frozen-lockfile`、`format:check`、`build`、生产依赖 audit;
6. Cargo audit;
7. actionlint 与 YAML 解析;
8. 使用特殊字符密码执行 `docker compose config`;
9. 构建 `runtime`,对同一镜像执行 Trivy HIGH/CRITICAL 门禁;
10. 执行完整 Compose 烟测、持久化探针、日志扫描和 SIGTERM 验证;
11. 构建 `release-asset`,分别验证缺少版本时失败、提供版本时文件名正确;
12. 静态验证稳定版和预发布版的 tag、签名和 GitHub Release 条件没有交叉污染;
13. 扫描 `backend/src`,确认没有新增 `#[test]` 或 `#[cfg(test)]`。
本地验证通过后再推送,由远端 CI 证明 reusable workflow 权限、SARIF、GHA cache、artifact 和 Release job 依赖在 GitHub Actions 环境中同样成立。
## 10. 完成定义
只有满足以下全部条件,本轮改造才算完成:
- 普通 CI 不再重复构建同一源码镜像;
- Release 的质量门禁和发布镜像扫描针对同一个精确 SHA;
- 默认版与预发布版标签行为正确;
- 密码单点配置和特殊字符密码真实启动通过;
- PostgreSQL、Redis 重建后数据仍存在;
- 应用以非 root 身份健康启动并优雅退出;
- Rust、前端、workflow、漏洞扫描和运行日志均达到本文定义的 0 错误、0 警告;
- 所有测试代码位于 `backend/tests`,没有兼容路径和重复实现。