WSの小屋

从零搭建 CI/CD:GitHub Actions 实战指南

GitHub Actions 是目前 GitHub 生态最主流的 CI/CD 工具。本文不只是罗列语法,而是从真实项目出发,搭建一套完整的 CI/CD 流水线——包含代码检查、测试、构建、部署全流程。

核心概念

概念 说明
Workflow 一个 .yml 文件,定义完整的自动化流程
Job Workflow 中的任务单元,并行执行(默认)
Step Job 中的步骤,串行执行
Action 可复用的 Step,如 actions/checkout
Runner 执行 Job 的机器(GitHub 托管或自建)
Artifact 构建产物,可在 Job 间传递

基础:CI 流水线

代码检查 + 测试 + 构建

# .github/workflows/ci.yml
name: CI

on:
  pull_request:
    branches: [main, develop]
  push:
    branches: [main, develop]

# 同一 PR 的新推送取消旧的运行
concurrency:
  group: ci-${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  ci:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: pnpm/action-setup@v4
        with:
          version: 9

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Lint
        run: pnpm lint

      - name: Type check
        run: pnpm typecheck

      - name: Test
        run: pnpm test --coverage

      - name: Build
        run: pnpm build

      - name: Upload coverage
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: coverage
          path: coverage/

矩阵测试(多版本兼容性)

jobs:
  test:
    strategy:
      matrix:
        node-version: [18, 20, 22]
        os: [ubuntu-latest, windows-latest]
      fail-fast: false  # 一个失败不取消其他

    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'pnpm'
      - run: pnpm install --frozen-lockfile
      - run: pnpm test

进阶:CD 自动部署

Docker 镜像构建与推送

# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches: [main]
    tags: ['v*']

jobs:
  docker:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    steps:
      - uses: actions/checkout@v4

      - uses: docker/setup-qemu-action@v3

      - uses: docker/setup-buildx-action@v3

      - name: Login to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ghcr.io/${{ github.repository }}
          tags: |
            type=ref,event=branch
            type=semver,pattern={{version}}
            type=sha,prefix=commit-

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          platforms: linux/amd64,linux/arm64
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

部署到 K8s

  deploy:
    needs: docker
    runs-on: ubuntu-latest
    if: startsWith(github.ref, 'refs/tags/v')

    steps:
      - uses: actions/checkout@v4

      - name: Deploy to K8s
        uses: steebchen/kubectl@v2.1.1
        with:
          config: ${{ secrets.KUBE_CONFIG }}
          command: set image deployment/myapp
            app=ghcr.io/${{ github.repository }}:${{ github.sha }}
            -n production

      - name: Verify deployment
        run: |
          kubectl rollout status deployment/myapp -n production
          kubectl get pods -n production -l app=myapp

部署到服务器(SSH)

  deploy-ssh:
    needs: docker
    runs-on: ubuntu-latest
    steps:
      - name: Deploy via SSH
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            cd /app/myapp
            docker compose pull
            docker compose up -d --remove-orphans
            docker image prune -f

环境变量与 Secrets 管理

分环境配置

jobs:
  deploy:
    environment:
      name: production  # 需要在 GitHub 仓库 Settings → Environments 中配置
    env:
      DATABASE_URL: ${{ secrets.PROD_DATABASE_URL }}
      NODE_ENV: production
    steps:
      - name: Deploy
        env:
          API_KEY: ${{ secrets.API_KEY }}
        run: ./deploy.sh

动态环境变量

jobs:
  deploy:
    runs-on: ubuntu-latest
    outputs:
      image-tag: ${{ steps.meta.outputs.version }}
    steps:
      - id: meta
        run: echo "version=$(date +%Y%m%d)-${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"

  notify:
    needs: deploy
    runs-on: ubuntu-latest
    steps:
      - run: echo "Deployed version ${{ needs.deploy.outputs.image-tag }}"

Monorepo CI/CD

变更检测

jobs:
  changes:
    runs-on: ubuntu-latest
    outputs:
      web: ${{ steps.filter.outputs.web }}
      api: ${{ steps.filter.outputs.api }}
    steps:
      - uses: actions/checkout@v4
      - uses: dorny/paths-filter@v3
        id: filter
        with:
          filters: |
            web:
              - 'apps/web/**'
              - 'packages/ui/**'
            api:
              - 'apps/api/**'
              - 'packages/utils/**'

  deploy-web:
    needs: changes
    if: ${{ needs.changes.outputs.web == 'true' }}
    runs-on: ubuntu-latest
    steps:
      - run: echo "Deploy web"

  deploy-api:
    needs: changes
    if: ${{ needs.changes.outputs.api == 'true' }}
    runs-on: ubuntu-latest
    steps:
      - run: echo "Deploy api"

Turborepo 增量构建

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # turbo 需要完整 git 历史

      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'

      - run: pnpm install --frozen-lockfile

      # 只构建相比 main 分支有变化的包
      - run: pnpm build --filter=...[origin/main]
      - run: pnpm test --filter=...[origin/main]

实用技巧

缓存依赖

# setup-node 已经内置 pnpm 缓存
- uses: pnpm/action-setup@v4
  with:
    version: 9
- uses: actions/setup-node@v4
  with:
    node-version: 20
    cache: 'pnpm'  # 自动缓存 pnpm store

并行 Job + 串行部署

jobs:
  lint:
    runs-on: ubuntu-latest
    steps: [...]

  test:
    runs-on: ubuntu-latest
    steps: [...]

  build:
    runs-on: ubuntu-latest
    steps: [...]

  deploy:
    needs: [lint, test, build]  # 三个并行完成后才执行
    runs-on: ubuntu-latest
    steps: [...]

通知(飞书/钉钉/Slack)

  notify:
    needs: [deploy]
    if: always()
    runs-on: ubuntu-latest
    steps:
      - name: Notify Feishu
        if: ${{ needs.deploy.result == 'success' }}
        uses: foxundermoon/feishu-action@v2
        with:
          url: ${{ secrets.FEISHU_WEBHOOK }}
          msg_type: text
          content: |
            ✅ 部署成功
            仓库: ${{ github.repository }}
            提交: ${{ github.event.head_commit.message }}
            作者: ${{ github.actor }}

      - name: Notify on failure
        if: ${{ needs.deploy.result == 'failure' }}
        uses: foxundermoon/feishu-action@v2
        with:
          url: ${{ secrets.FEISHU_WEBHOOK }}
          msg_type: text
          content: |
            ❌ 部署失败
            仓库: ${{ github.repository }}
            查看: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}

自动创建 Release

name: Release

on:
  push:
    tags: ['v*']

jobs:
  release:
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@v4
      - uses: softprops/action-gh-release@v2
        with:
          generate_release_notes: true  # 自动从 PR 生成 changelog
          draft: false
          prerelease: ${{ contains(github.ref, '-rc') }}

自托管 Runner

如果 GitHub 托管 Runner 满足不了需求(如需要访问内网服务器、GPU 计算等):

# 在自建服务器上
mkdir actions-runner && cd actions-runner
curl -o actions-runner.tar.gz -L https://github.com/actions/runner/releases/download/v2.317.0/actions-runner-linux-x64-2.317.0.tar.gz
tar xzf actions-runner.tar.gz

./config.sh --url https://github.com/owner/repo --token TOKEN
./run.sh

# 注册为系统服务
sudo ./svc.sh install
sudo ./svc.sh start
# 使用自托管 Runner
jobs:
  deploy:
    runs-on: self-hosted  # 或 ['self-hosted', 'linux', 'x64']
    steps:
      - run: ./deploy.sh

安全最佳实践

1. 最小权限原则

permissions:
  contents: read       # 默认只读
  packages: write      # 只有需要推送镜像的 Job 才给
  pull-requests: write  # 只有需要评论 PR 的 Job 才给

2. Secrets 不出现在日志中

- name: Deploy
  env:
    TOKEN: ${{ secrets.DEPLOY_TOKEN }}
  run: |
    # ✅ 使用环境变量
    curl -H "Authorization: Bearer $TOKEN" https://api.example.com/deploy

    # ❌ 不要 echo secrets
    # echo $TOKEN

3. 第三方 Action 固定版本

# ❌ 不安全(可能被篡改)
- uses: some/action@main

# ✅ 固定到 commit hash(最安全)
- uses: some/action@a1b2c3d4e5f6...

# ✅ 固定到 tag(可接受)
- uses: actions/checkout@v4.1.4

总结

一套完整的 CI/CD 流水线应该包含:

PR 提交 → Lint + TypeCheck + Test(并行)→ Build → 审核通过 → 合并 main
                                                              ↓
                                                       Docker 镜像构建
                                                              ↓
                                                       部署到 Staging
                                                              ↓
                                                       手动确认 → 部署到 Production

关键原则:

  • 快速反馈:CI 在 PR 时运行,5 分钟内完成
  • 只构建变化:Monorepo 中用 paths-filter / turbo --filter
  • 缓存一切:依赖缓存、Docker 层缓存、构建缓存
  • 安全第一:最小权限、固定 Action 版本、Secrets 不暴露
  • 可观测:部署通知、失败告警、日志可追溯

GitHub Actions 的免费额度(2000 分钟/月)对个人项目绰绰有余,团队建议直接用 Team plan。

Comments | 0条评论