把代码从开发者的本地机器推送到生产环境,如果每一步都靠人工操作,不仅繁琐易错,还会拖慢迭代速度。CI/CD(持续集成与持续部署)就是来解决这个问题的:通过自动化流水线,让代码变更在经过一系列质量检查后,安全、快速地到达服务器上。 对于 Node.js 项目,这套流程的搭建并不复杂,而且收益巨大。
19.3.1 理解 CI/CD 的核心环节
在 Node.js 项目中,一个典型的 CI/CD 流水线通常包含以下步骤:
- 代码推送触发:开发者将代码推送到 Git 仓库的某个分支(如
main或develop)。 - 持续集成(CI):
- 安装项目依赖
- 运行代码风格检查(ESLint + Prettier)
- 执行单元测试和集成测试
- 构建 TypeScript 编译或打包(如果需要)
- 生成测试覆盖率报告
- 制品构建:将应用打包为可部署的形式,可能是压缩包、Docker 镜像,或直接将源代码推送到服务器。
- 持续部署(CD):将构建好的制品发布到目标环境(测试环境、预发环境、生产环境),并执行启动或更新操作。
每一道关卡都是自动化执行的,只要有一个步骤失败,流水线就会中止并通知开发者,这样可以尽早发现问题,避免坏代码蔓延。
19.3.2 选择 CI/CD 工具
市面上主流的 CI/CD 工具很多,对于 Node.js 项目来说,选择相对简单:
- GitHub Actions:与 GitHub 仓库深度集成,配置即代码(YAML 文件),免费额度对中小项目足够,是目前最流行的选择。
- GitLab CI/CD:如果团队使用 GitLab,其内置的 CI/CD 功能同样强大,配置文件为
.gitlab-ci.yml。 - Jenkins:功能最灵活但需要自行维护服务器,更适合大型企业或有定制需求。
- 其他托管服务:Travis CI、CircleCI、Bitbucket Pipelines 等,也都对 Node.js 有良好支持。
本节以 GitHub Actions 为例展开说明,因为它的使用门槛最低,而且社区提供了大量现成的 Action,可以像搭积木一样组装流水线。
19.3.3 编写第一个 Node.js CI 流水线
在项目根目录创建 .github/workflows/ci.yml,内容如下:
name: Node.js CI
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [ 16, 18, 20 ] # 在多个 Node 版本上测试
steps:
- name: 检出代码
uses: actions/checkout@v3
- name: 设置 Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm' # 自动缓存 node_modules
- name: 安装依赖
run: npm ci # 使用锁文件安装,更快且确定
- name: 代码风格检查
run: npm run lint
- name: 运行单元测试
run: npm test
- name: 上传测试覆盖率
if: matrix.node-version == '18'
uses: coverallsapp/github-action@v2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
这个流水线会做几件事:
- 当
push到main或develop分支,或者创建针对main的 PR 时触发。 - 在三个 Node.js 版本(16、18、20)上并行运行。
- 每个版本都执行一遍
npm ci(严格按 lock 文件安装)、lint 检查、单元测试。 - 仅在 Node 18 版本上,将测试覆盖率报告上传到 Coveralls,方便跟踪代码质量变化。
这里有几个实用细节需要注意:
- 使用
npm ci而不是npm install,是为了保证依赖安装的稳定一致,并且速度更快。 actions/setup-node的cache: 'npm'会自动缓存node_modules,下次运行时会大幅提速。- 多版本测试可以有效避免因 Node.js 版本升级引起的兼容性问题。
19.3.4 构建 Docker 镜像并推送
对于容器化部署(如 Kubernetes、Docker Swarm),CI 流水线还需要构建 Docker 镜像并推送到镜像仓库。以下示例在测试通过后,构建并推送镜像到 Docker Hub:
build-and-push:
needs: test # 必须等测试通过
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' # 仅 main 分支执行
steps:
- uses: actions/checkout@v3
- name: 登录 Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: 构建并推送镜像
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: |
myapp:latest
myapp:${{ github.sha }} # 用提交 SHA 标记版本
这里引入了几个关键点:
needs: test确保只有测试通过后才构建镜像,避免将未通过测试的代码打包。- 使用 GitHub Secrets(
DOCKER_USERNAME、DOCKER_PASSWORD)存储敏感信息,绝不硬编码。 - 给镜像打上
latest标签和 Git 提交 SHA,便于追踪与回滚。
19.3.5 部署到测试/生产环境
当构建好的镜像推送到仓库后,后续的部署方式取决于实际运行环境。
场景一:部署到自有服务器(如裸机或云主机)
可以通过 SSH 执行远程命令,或使用 Docker Compose 更新服务。使用 appleboy/ssh-action 可简化 SSH 操作:
deploy:
needs: build-and-push
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- name: 远程部署
uses: appleboy/ssh-action@v0
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd /opt/myapp
docker-compose pull
docker-compose up -d --remove-orphans
这需要事先在服务器上准备好 docker-compose.yml,确保应用可以通过 Compose 拉起并更新。
场景二:部署到 Kubernetes
可以使用 kubectl 或 Helm 更新部署。例如,使用 azure/k8s-deploy 或直接执行命令:
- name: 部署到 K8s
run: |
kubectl set image deployment/myapp myapp=myapp:${{ github.sha }}
同样,Kubeconfig 等敏感凭证需通过 Secrets 安全传递。
场景三:部署到 Serverless 平台(如阿里云函数计算、Vercel)
如果是无服务器平台,通常有专属的 CLI 或 Action。例如部署到 Vercel:
- name: 部署到 Vercel
uses: amondnet/vercel-action@v25
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
19.3.6 环境区分与配置管理
生产、预发、测试环境通常需要不同的配置(数据库连接、密钥等)。CI/CD 流水线应当根据分支或标签区分目标环境:
develop分支 → 部署到测试环境release/*分支或main分支 → 部署到预发/生产环境
在流水线中可以通过环境变量注入对应配置,例如在 Node.js 中常见的是使用 dotenv 或配置中心。CI 环境中的敏感配置也通过 GitHub Environments 或 Secrets 来区分:
deploy-staging:
runs-on: ubuntu-latest
environment: staging
steps:
- run: npm run deploy
env:
DB_URL: ${{ secrets.STAGING_DB_URL }}
GitHub 的 Environment 功能允许针对每个环境设置不同的保护规则(如审批人、限制分支),这样生产环境的部署可以要求手动确认,确保发布可控。
19.3.7 实践中的关键点与常见问题
1. 尽量保持快速
一个 CI/CD 流水线如果每次都要跑 20 分钟,开发的节奏就会被打乱。优化手段包括:
- 利用依赖缓存(
actions/setup-node的cache、Docker 分层缓存) - 并行执行不相关的任务(如 lint 和 test 可以分两个 job)
- 只运行与变更相关的测试(利用 Monorepo 工具或路径过滤)
2. 回滚策略
自动部署最大的风险是“一键上线,一键炸服”。好的 CD 流程一定包含快速回滚能力:
- Docker 镜像每次打上唯一标签(如 Git SHA),回滚时只需重新部署旧版本镜像。
- Kubernetes 可以直接
rollout undo。 - 裸机部署可保留之前版本的部署目录,利用软链接切换。
3. 数据库迁移
如果部署代码伴随数据库结构变更,需要将数据库迁移步骤也纳入流水线。通常是先运行迁移脚本(如 Prisma 的 prisma migrate deploy),再更新应用代码。确保迁移脚本是幂等的、可回滚的,并做好备份。
4. 安全扫描
建议在流水线中加入依赖漏洞扫描:
- name: npm 安全审计
run: npm audit --audit-level=high
如果发现高危漏洞,可让流水线失败,禁止部署。也可以集成 Snyk、Dependabot 等更全面的工具。
5. 通知与日志
配置失败或成功后的通知(如 Slack、钉钉、企业微信),让团队及时了解部署状态。同时保留日志与部署记录,方便追溯。
19.3.8 小结:从手动到自动的思维转变
搭建 CI/CD 流水线,本质上是用代码来定义发布流程,把“人肉操作”转化为“自动化脚本”。对于 Node.js 项目来说,整个流程实现起来非常平滑,因为绝大多数工具链(lint、test、build)都是 npm 脚本的一部分,CI 机器上只要有 Node.js 就能跑起来。
一次配置,永久受益。一旦流水线运转起来,团队就可以专注于写代码和业务逻辑,而把检查、构建、部署这种重复性工作交给机器。这才是工程化的核心价值:让可靠的流程成为日常,而不是依赖个别人的小心与记忆。