Featured image of post GitHub Actions 全自动 CI/CD 流水线

GitHub Actions 全自动 CI/CD 流水线

每次 push 自动跑测试、自动部署到 Cloudflare Workers。这篇记录 RuiToolAI 的 CI/CD 搭建过程。

流水线设计

RuiToolAI 的 CI/CD 分两个阶段:

1
2
3
4
5
6
7
Push to main
[CI] Lint + TypeCheck + Unit Tests
    ↓ 通过
[CD] Build + e2e Tests + Deploy to Cloudflare Workers
    ↓ 通过
生产环境更新

PR 只跑 CI,不部署。合并到 main 才触发部署。


基础 CI 配置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# .github/workflows/ci.yml
name: CI

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

jobs:
  lint-and-typecheck:
    name: Lint & TypeCheck
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v4

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

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "20"
          cache: "pnpm"

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

      - name: Lint
        run: pnpm run lint

      - name: TypeCheck
        run: pnpm run typecheck

  unit-tests:
    name: Unit Tests
    runs-on: ubuntu-latest
    needs: lint-and-typecheck

    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
        with:
          version: 9
      - uses: actions/setup-node@v4
        with:
          node-version: "20"
          cache: "pnpm"
      - run: pnpm install --frozen-lockfile
      - run: pnpm run test

Cloudflare Workers 自动部署

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    name: Deploy to Cloudflare Workers
    runs-on: ubuntu-latest
    # 只在 CI 通过后部署
    needs: [lint-and-typecheck, unit-tests]

    steps:
      - name: Checkout
        uses: actions/checkout@v4

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

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "20"
          cache: "pnpm"

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

      - name: Build
        run: pnpm run build
        env:
          # 构建时需要的环境变量
          NODE_ENV: production

      - name: Deploy to Cloudflare Workers
        run: pnpm run deploy
        env:
          CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}

Secrets 管理

GitHub Actions 里的 Secrets 分两种:

  • Repository Secrets:只有这个仓库能用
  • Environment Secrets:绑定到特定环境(production/staging),可以设置审批流程

在 GitHub 仓库 → Settings → Secrets and variables → Actions 里配置。

RuiToolAI 需要的 Secrets:

1
2
3
4
CLOUDFLARE_API_TOKEN      # Cloudflare API Token(Workers 部署权限)
CLOUDFLARE_ACCOUNT_ID     # Cloudflare 账户 ID
TEST_USER_EMAIL           # e2e 测试用的测试账号
TEST_USER_PASSWORD        # e2e 测试用的测试密码

生成 Cloudflare API Token

  1. 登录 Cloudflare Dashboard
  2. My Profile → API Tokens → Create Token
  3. 选择 “Edit Cloudflare Workers” 模板
  4. 复制 Token,粘贴到 GitHub Secrets

注意:Token 只显示一次,要立即保存。


e2e 测试集成

e2e 测试需要启动完整的应用,比较耗时,放在单独的 job 里:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# .github/workflows/e2e.yml
name: E2E Tests

on:
  push:
    branches: [main]

jobs:
  e2e:
    name: End-to-End Tests
    runs-on: ubuntu-latest
    timeout-minutes: 30

    steps:
      - uses: actions/checkout@v4

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

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

      - run: pnpm install --frozen-lockfile

      # 安装 Playwright 浏览器
      - name: Install Playwright Browsers
        run: pnpm exec playwright install --with-deps chromium

      # 初始化本地测试数据库
      - name: Setup test database
        run: pnpm db:migrate:local

      # 运行 e2e 测试
      - name: Run e2e tests
        run: pnpm test:e2e
        env:
          TEST_USER_EMAIL: ${{ secrets.TEST_USER_EMAIL }}
          TEST_USER_PASSWORD: ${{ secrets.TEST_USER_PASSWORD }}

      # 失败时上传报告
      - name: Upload Playwright Report
        uses: actions/upload-artifact@v4
        if: failure()
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 7

数据库迁移自动化

部署前需要先跑数据库迁移,确保 schema 是最新的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
- name: Run database migrations
  run: pnpm db:migrate:remote
  env:
    CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
    CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}

- name: Deploy to Cloudflare Workers
  run: pnpm run deploy
  env:
    CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
    CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}

package.json 里的迁移命令:

1
2
3
4
5
6
{
  "scripts": {
    "db:migrate:local": "wrangler d1 migrations apply DB --local",
    "db:migrate:remote": "wrangler d1 migrations apply DB --remote"
  }
}

部署通知

部署成功或失败时发通知,方便及时知道状态:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
- name: Notify on success
  if: success()
  run: |
    curl -X POST ${{ secrets.SLACK_WEBHOOK_URL }} \
      -H 'Content-type: application/json' \
      -d '{"text": "✅ RuiToolAI 部署成功!"}'

- name: Notify on failure
  if: failure()
  run: |
    curl -X POST ${{ secrets.SLACK_WEBHOOK_URL }} \
      -H 'Content-type: application/json' \
      -d '{"text": "❌ RuiToolAI 部署失败,请检查 GitHub Actions 日志。"}'

完整流水线示例

把所有 job 整合到一个文件里,用 needs 控制依赖关系:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# .github/workflows/main.yml
name: CI/CD

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

jobs:
  # 1. 代码质量检查(所有 push 和 PR 都跑)
  quality:
    name: Lint & TypeCheck
    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" }
      - run: pnpm install --frozen-lockfile
      - run: pnpm run lint
      - run: pnpm run typecheck

  # 2. 单元测试
  test:
    name: Unit Tests
    runs-on: ubuntu-latest
    needs: quality
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
        with: { version: 9 }
      - uses: actions/setup-node@v4
        with: { node-version: "20", cache: "pnpm" }
      - run: pnpm install --frozen-lockfile
      - run: pnpm run test

  # 3. e2e 测试(只在 main 分支跑)
  e2e:
    name: E2E Tests
    runs-on: ubuntu-latest
    needs: test
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
        with: { version: 9 }
      - uses: actions/setup-node@v4
        with: { node-version: "20", cache: "pnpm" }
      - run: pnpm install --frozen-lockfile
      - run: pnpm exec playwright install --with-deps chromium
      - run: pnpm db:migrate:local
      - run: pnpm test:e2e
        env:
          TEST_USER_EMAIL: ${{ secrets.TEST_USER_EMAIL }}
          TEST_USER_PASSWORD: ${{ secrets.TEST_USER_PASSWORD }}
      - uses: actions/upload-artifact@v4
        if: failure()
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 7

  # 4. 部署(只在 main 分支,所有测试通过后)
  deploy:
    name: Deploy
    runs-on: ubuntu-latest
    needs: [test, e2e]
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
        with: { version: 9 }
      - uses: actions/setup-node@v4
        with: { node-version: "20", cache: "pnpm" }
      - run: pnpm install --frozen-lockfile
      - run: pnpm run build
      - name: Run DB migrations
        run: pnpm db:migrate:remote
        env:
          CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
      - name: Deploy to Workers
        run: pnpm run deploy
        env:
          CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}

常见问题排查

问题 1:pnpm install 很慢

加缓存:

1
2
3
4
- uses: actions/setup-node@v4
  with:
    node-version: "20"
    cache: "pnpm"  # 这行很重要

问题 2:Playwright 浏览器每次都重新下载

1
2
3
4
5
6
7
8
- name: Cache Playwright browsers
  uses: actions/cache@v4
  with:
    path: ~/.cache/ms-playwright
    key: playwright-${{ runner.os }}-${{ hashFiles('**/package.json') }}

- name: Install Playwright Browsers
  run: pnpm exec playwright install --with-deps chromium

问题 3:部署失败但没有详细错误信息

1
2
3
4
5
6
- name: Deploy
  run: pnpm run deploy
  env:
    CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
  # 加上 --verbose 看详细日志
  # 或者检查 wrangler 版本是否兼容

问题 4:e2e 测试超时

1
2
3
jobs:
  e2e:
    timeout-minutes: 30  # 增加超时时间

总结

CI/CD 流水线的核心原则:

  1. 快速反馈:Lint 和 TypeCheck 最快,先跑;e2e 最慢,最后跑
  2. 依赖关系:用 needs 控制 job 顺序,测试通过才部署
  3. 分支策略:PR 只跑 CI,main 分支才部署
  4. Secrets 安全:敏感信息放 GitHub Secrets,不要硬编码
  5. 失败保留证据:上传测试报告,方便排查

参考资源

使用 Hugo 构建
主题 StackJimmy 设计