Skip to content
完整流水线搭建
本篇搭建的流水线完成一件事:代码推送到 main 分支后,自动经过检查、测试、构建镜像、推送注册表,最后把新版本跑起来。整条链路拆成五个阶段——代码质量、容器构建、制品晋升、部署、调试排障,每个阶段对应一到两个 Job。
最终产出的文件:
.github/workflows/ci.yml
Dockerfile
docker-compose.prod.yml工作流定义集中在 ci.yml 中。
工作流骨架:触发条件与作业拓扑
工作流由三个事件触发:
push到main分支- 向
main分支发起 Pull Request workflow_dispatch手动触发,可传入deploy布尔参数决定是否执行部署
yaml
name: CI/CD
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
inputs:
deploy:
description: '部署到目标环境'
required: true
type: boolean
default: falseJob 间的依赖关系通过 needs 字段声明:
lint → test → build-and-push → deploylint 和 test 串行执行:test 等待 lint 通过后再跑,避免在代码格式都有问题时浪费资源跑测试。build-and-push 仅在 push 到 main 或手动触发时执行——PR 不推送镜像,只跑到 test 为止。deploy 仅在手动触发且 deploy 为 true 时执行。
yaml
jobs:
lint:
runs-on: ubuntu-latest
steps: [...]
test:
runs-on: ubuntu-latest
needs: lint
steps: [...]
build-and-push:
runs-on: ubuntu-latest
needs: test
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
steps: [...]
deploy:
runs-on: ubuntu-latest
needs: build-and-push
if: github.event_name == 'workflow_dispatch' && github.event.inputs.deploy == 'true'
environment: production
steps: [...]deploy Job 上的 environment: production 启用了环境保护规则,在实际仓库中可配置审批流程,本文不展开。
代码质量关卡:lint、测试与依赖缓存
lint 与 test 两个 Job 结构相似:检出代码、设置 Node.js、安装依赖、执行脚本。
核心在于缓存。actions/setup-node 内置 cache: 'npm',它会根据 package-lock.json 的哈希自动生成缓存键,将 ~/.npm 目录缓存起来。后续运行时 npm ci 直接从缓存取包,跳过网络下载。
yaml
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npm run lintnpm ci 会先删除 node_modules,然后严格按照 package-lock.json 安装,保证 CI 环境与锁文件完全一致。
测试 Job 的写法一样:
yaml
test:
runs-on: ubuntu-latest
needs: lint
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npm testpackage.json 中需要预先定义对应的脚本:
json
{
"scripts": {
"lint": "eslint . --ext .ts",
"test": "jest"
}
}如果 lint 失败,整个运行在对应步骤标红,后续 Job 不会触发。日志中直接显示 ESLint 的错误输出,点击失败步骤即可定位。
容器化构建:多阶段 Dockerfile 与层缓存
Docker 构建耗时集中在依赖安装和编译。多阶段构建把编译和依赖下载放在一个阶段,运行时需要的部分拷进精简镜像,最终镜像不包含构建工具。
dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package*.json ./
EXPOSE 3000
CMD ["node", "dist/index.js"]关键在 COPY 的分层顺序:先拷贝 package*.json 并执行 npm ci,再拷贝源码和构建。只要 package-lock.json 不变,Docker 就能复用依赖安装层的缓存,跳过 npm ci 这一步。
在 CI 中构建时,docker/build-push-action 可以结合 GitHub Actions 缓存进一步复用层:
yaml
- name: 构建并推送镜像
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
cache-from: type=gha
cache-to: type=gha,mode=maxtype=gha 将构建缓存存放在 Actions 缓存中,跨运行复用。mode=max 会缓存所有中间层,对多阶段构建有效。
制品晋升:构建镜像并推送至注册表
镜像构建后推送到 GitHub Container Registry(GHCR),使用 GITHUB_TOKEN 认证,无需额外注册账号。
准备步骤:登录、生成元数据(标签)、构建并推送。
yaml
build-and-push:
runs-on: ubuntu-latest
needs: test
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/metadata-action@v5
id: meta
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=sha
type=ref,event=branch
- uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
cache-from: type=gha
cache-to: type=gha,mode=maxdocker/metadata-action 根据触发事件自动生成标签。push 到 main 后会生成 sha-<短哈希> 和 main 两个标签,最终镜像地址类似 ghcr.io/user/repo:main。
此处必须显式声明 packages: write 权限,否则 GITHUB_TOKEN 无权推送镜像到 GHCR。
部署:环境变量注入与滚动更新
部署阶段依赖 build-and-push 完成,且要求手动触发时传入 deploy=true。目标环境通过 Docker Compose 管理,远程主机需提前装好 Docker 和 Compose。
部署步骤通过 SSH 连接目标机器,拉取最新镜像,再用 docker compose up -d 重建容器:
yaml
deploy:
runs-on: ubuntu-latest
needs: build-and-push
if: github.event_name == 'workflow_dispatch' && github.event.inputs.deploy == 'true'
environment: production
steps:
- uses: actions/checkout@v4
- name: 部署到远程主机
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd /opt/app
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -d --remove-orphansdocker-compose.prod.yml 中指定镜像标签,并通过环境变量文件加载应用配置:
yaml
services:
app:
image: ghcr.io/user/repo:main
env_file:
- .env.production
ports:
- "3000:3000".env.production 包含数据库连接串、API 密钥等敏感配置,存放在远程主机上,不进入仓库。
docker compose up -d 检测到镜像更新后,停止旧容器并创建新容器,完成一次滚动替换。这种方式在单实例场景下会产生短暂中断,但对于非高可用要求的服务是可接受的更新策略。
流水线调试:日志定位、本地验证与任务重跑
流水线失败时,先打开 Actions 页面对应运行记录。每个 Job 的步骤按顺序展开,失败步骤标红,点进去可见具体命令的 stdout/stderr。
排查顺序建议从后往前:先看失败步骤的原始输出,再检查它前面一步是否有不明显的问题(例如缓存未命中导致依赖没装全)。如果日志信息不够,可在仓库 Secrets 中设置 ACTIONS_STEP_DEBUG=true 开启调试日志,步骤内部会输出完整命令和变量值,日志量大但信息密度高。
在推送代码之前,可以用 act 在本地验证工作流:
bash
act pull_request --job lint这会模拟 PR 事件仅运行 lint Job,失败直接在本地改,不需要反复推送触发线上流水线。act -n 进行干跑,只打印步骤不执行命令,适合快速验证 Job 结构。
如果某个 Job 因网络波动或外部服务暂时不可用而失败,不需要重跑整条流水线。GitHub 提供"重跑失败作业"按钮,或通过 CLI:
bash
gh run rerun <run-id> --failed仅重跑红掉的 Job。配合 gh run view <run-id> --log 可以直接在终端查看日志,不必切回浏览器。
常见故障场景
测试失败:最常见的原因是本地环境与 CI 环境的 Node 版本不一致导致快照不匹配,或用例依赖时间、随机值。检查 actions/setup-node 的 node-version 是否与本地 .nvmrc 或 package.json 的 engines 字段对齐。如果用了内存数据库或文件系统操作,还需确认 CI 的运行系统(ubuntu/macos/windows)与本地一致。
构建失败:多发生在 Docker 构建阶段。COPY --from=builder /app/dist 报找不到目录,往往是因为构建阶段未执行 npm run build 或脚本未正确输出到 dist。另一个常见问题是私有包安装失败——npm ci 需要访问私有注册表,但构建容器内没有认证令牌。解决方法是通过 docker/build-push-action 的 secrets 输入传入 token:
yaml
secrets: |
"NPM_TOKEN=${{ secrets.NPM_TOKEN }}"并在 Dockerfile 构建阶段使用:
dockerfile
RUN --mount=type=secret,id=NPM_TOKEN \
echo "//registry.npmjs.org/:_authToken=$(cat /run/secrets/NPM_TOKEN)" > ~/.npmrc部署连接失败:SSH 连接不上主机,先确认密钥是否有效、主机地址和用户名是否正确。appleboy/ssh-action 会输出连接调试信息。如果不通,检查安全组或防火墙是否放行了 GitHub Actions 的 IP 段。docker login 失败则通常是 GITHUB_TOKEN 权限不足——需要在仓库设置中开启 packages: write,或检查 token 是否过期。
参考链接
- [1] https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions
- [2] https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows
- [3] https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows
- [4] https://github.com/actions/checkout
- [5] https://github.com/actions/setup-node
- [6] https://docs.npmjs.com/cli/v9/commands/npm-ci
- [7] https://docs.npmjs.com/cli/v9/commands/npm-run-script
- [8] https://docs.docker.com/build/building/multi-stage/
- [9] https://docs.docker.com/build/cache/
- [10] https://github.com/docker/build-push-action
- [11] https://github.com/docker/login-action
- [12] https://github.com/docker/metadata-action
- [13] https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry
- [14] https://docs.docker.com/engine/reference/commandline/image_push/
- [15] https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment
- [16] https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions
- [17] https://docs.docker.com/engine/reference/run/#env-environment-variables
- [18] https://docs.docker.com/compose/environment-variables/
- [19] https://docs.docker.com/reference/cli/docker/compose/up/
- [20] https://docs.github.com/en/actions/monitoring-and-troubleshooting-workflows/using-workflow-run-logs
- [21] https://docs.github.com/en/actions/monitoring-and-troubleshooting-workflows/troubleshooting-workflows/enabling-debug-logging
- [22] https://github.com/nektos/act
- [23] https://docs.github.com/en/actions/managing-workflow-runs/re-running-workflows-and-jobs
- [24] https://cli.github.com/manual/gh_run_rerun
- [25] https://cli.github.com/manual/gh_run_view
- [26] https://docs.github.com/en/actions/learn-github-actions/contexts
- [27] https://docs.github.com/en/actions/learn-github-actions/environment-variables
- [28] https://github.com/nodejs/docker-node
