Skip to content

CI/CD 与部署

代码从提交到生产容器的完整流水线。

1. 分支策略与 Workflow 触发关系

mermaid
flowchart LR
    FT["push → feature/**"] --> CF["ci-feature\nlint + test(via ci-reusable)"]
    DV["push → dev"] --> CIDEV["ci-dev\nlint + test(via ci-reusable)"]
    CIDEV -- "成功" --> CDDEV["cd-dev\n构建 + 开发环境部署"]
    RL["push → release/**"] --> CR["ci-release\nlint + test + 版本检查(via ci-reusable)"]
    MN["push → main"] --> CP["ci-prod\nlint + test(via ci-reusable)"]
    PRD["PR 合并 → main\n(from release/*)"] --> AT["auto-tag-release\n版本提取 + tag 创建"]
    AT -- "tag 创建" --> CDP["cd-prod\n生产部署"]

2. Workflow 清单

文件触发条件核心步骤含 DB 服务
ci-reusable.yamlworkflow_call(被其他 CI 复用)lint + build + test + E2E
ci-feature.yamlpush → feature/**, fix/**, refactor/**lint + test(via ci-reusable)是(via ci-reusable)
ci-dev.yamlpush → devlint + test(via ci-reusable)是(via ci-reusable)
cd-dev.yamlci-dev 成功完成构建后端镜像 → 导出 OpenAPI JSON → 构建文档镜像(Dockerfile.dev)→ 同步 Apifox → 部署至开发环境否(export job 含 DB 服务容器)
ci-release.yamlpush → release/**lint + test + 版本号校验(via ci-reusable)是(via ci-reusable)
ci-prod.yamlpush → mainlint + test(via ci-reusable)是(via ci-reusable)
auto-tag-release.yamlPR 合并至 main(from release/*版本提取 + 创建 tag
cd-prod.yamltag 创建事件构建后端镜像 → 导出 OpenAPI JSON → 构建文档镜像(Dockerfile.prod,含内嵌 Scalar)→ 部署至生产环境否(export job 含 DB 服务容器)
pr-check-dev.yamlPR → dev规范性检查
pr-check-prod.yamlPR → main规范性检查 + 版本号检查

公共配置:Node.js 22、pnpm 10、Runner: ubuntu-latest

3. 含 DB 服务的 CI 环境

ci-releaseci-prod 中启动 PostgreSQL 服务容器供 E2E 测试使用:

配置项
镜像postgres:18
POSTGRES_USERci_test
POSTGRES_PASSWORDci_test_password
POSTGRES_DBnestjs_demo_basic_test
映射端口5432
健康检查pg_isready -U ci_test -d nestjs_demo_basic_test,间隔 10s,超时 5s,start-period 10s,重试 5 次

注入到 CI Job 的 DATABASE_URL

postgresql://ci_test:ci_test_password@localhost:5432/nestjs_demo_basic_test?schema=public

4. 自动版本标签(auto-tag-release)

release/* 分支的 PR 被合并到 main 时自动触发:

mermaid
flowchart TD
    A["PR merged: release/X.Y → main"] --> B["从 package.json 提取版本号"]
    B --> C["node scripts/create-release-tag.cjs"]
    C --> D{tag 已存在?}
    D -- 是 --> E["跳过,幂等退出"]
    D -- 否 --> F["git tag vX.Y.Z 目标 commit"]
    F --> G{有 PAT 可用?}
    G -- 是 --> H["使用 PAT push tag(触发 cd-prod)"]
    G -- 否 --> I["使用 GITHUB_TOKEN push tag"]
    H & I --> J["输出 tag 短 SHA(7 位)"]

create-release-tag.cjs 做幂等保护:tag 已存在时直接退出,不报错。

5. Docker 多阶段构建

阶段示意

mermaid
flowchart LR
    subgraph builder["Stage: builder(node:22-slim)"]
        direction TB
        B1["apt-get install openssl"] --> B2["COPY package.json + pnpm files"]
        B2 --> B3["pnpm install(全量 deps)"]
        B3 --> B4["pnpm prisma generate"]
        B4 --> B5["pnpm build\n(tsc + tsc-alias)"]
        B5 --> B6["pnpm prune --prod\n移除 devDependencies"]
    end

    subgraph runner["Stage: runner(node:22-slim)"]
        direction TB
        R1["COPY node_modules(已裁剪)"] --> R2["COPY dist/"]
        R2 --> R3["COPY .env.*"]
        R3 --> R4["apt-get install openssl curl"]
        R4 --> R5["EXPOSE ${PORT}"]
        R5 --> R6["HEALTHCHECK /health"]
        R6 --> R7["CMD node dist/src/main"]
    end

    builder --> runner

构建 ARG

参数默认值用途
DATABASE_URLpostgresql 占位符Prisma generate 所需
SHADOW_DATABASE_URLpostgresql 占位符Prisma migrate 所需
APP_VERSION注入应用版本
APP_NAME注入应用名
GIT_COMMIT注入 Git 提交哈希
NODE_ENVproduction环境标识
PORT3000服务监听端口

健康检查配置

dockerfile
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
    CMD curl --fail http://localhost:${PORT}/health || exit 1

健康检查端点 GET /health 返回 DB 连接状态与应用版本,由 AppController 提供。

6. 文档站镜像构建

6.1 Dockerfile 拆分

文档站镜像分为两套,分别服务于开发环境和生产环境:

文件定位API Reference 来源
website/Dockerfile.dev开发镜像,体积精简外链 Apifox 文档站(由 VITE_API_REFERENCE_URL ARG 注入)
website/Dockerfile.prod生产镜像,离线可用内嵌 Scalar 静态页(/reference/api/)+ openapi.json

两套都采用多阶段构建:builder(node:22.22-slim)执行 VitePress 构建,runner(nginx:alpine)托管静态文件。

Dockerfile.prod 构建时注入 openapi.json

dockerfile
ARG OPENAPI_JSON_PATH=website/api-reference/openapi.json
COPY ${OPENAPI_JSON_PATH} /usr/share/nginx/html/reference/api/openapi.json

CI 通过 --build-arg OPENAPI_JSON_PATH=assets/openapi.json 覆盖默认路径,从上游 artifact 注入最新导出的 JSON。

6.2 nginx 配置差异

配置项nginx.dev.confnginx.prod.conf
gzip 压缩
安全 headers
location /SPA fallback(/index.htmlSPA fallback(/index.html
location /reference/apitry_files … =404(静态文件,不回退 SPA)
location ~* ^/assets/1 年强缓存 + immutable1 年强缓存 + immutable

nginx.prod.conf/reference/api 使用 =404 fallback 而非 SPA 兜底,避免文件不存在时静默返回 200。

6.3 CD 文档镜像构建流程(以 cd-dev 为例)

mermaid
flowchart TD
    A["export-openapi job"] --> B["curl /api-doc-json → openapi.json"]
    B --> C["upload-artifact: path=openapi.json"]
    C --> D["build-docs job / sync-apifox job"]
    D --> E["download-artifact: path=assets"]
    E --> F["文件落点: assets/openapi.json"]
    F --> G["docker build\n--build-arg OPENAPI_JSON_PATH=assets/openapi.json"]
    F --> H["jq --slurpfile spec assets/openapi.json\ncurl → Apifox"]

artifact 路径规则upload-artifactpath 指定文件download-artifactpath 指定目标目录,文件会自动放入该目录下,最终路径为 assets/openapi.json

6.4 cd-prod OpenAPI 导出容器环境变量注入

导出步骤临时启动后端镜像以采集 OpenAPI JSON,环境变量注入方式经历了如下修复(v0.7.3):

方式问题
dotenvx decrypt --stdout > /tmp/.env.cd_export + --env-fileDocker --env-file 不支持多行值,EC 私钥含换行符导致容器启动失败
-e DOTENV_PRIVATE_KEY_TEST=…(当前)容器内 dotenvx 自动解密 .env.test,无中间文件

当前 docker run 关键参数:

yaml
docker run -d \
  -e DOTENV_PRIVATE_KEY_TEST=${{ secrets.DOTENV_PRIVATE_KEY_TEST }} \
  -e NODE_ENV=test \
  -e DATABASE_URL="postgresql://…" \
  -e PORT=3000 \
  ${{ env.BACKEND_IMAGE }}:latest

NODE_ENV=test 使 dotenvx 匹配解密 .env.test,对应私钥名为 DOTENV_PRIVATE_KEY_TEST