•10 min read

Cách tôi thiết lập CI/CD với GitHub Actions

Cách tôi thiết lập CI/CD với GitHub Actions

Tôi đã đến với CI/CD khá muộn. Nhiều năm liền tôi chạy thử nghiệm cục bộ, cầu nguyện, rồi đẩy code lên. Mọi thứ vẫn ổn cho đến một ngày tôi đẩy một commit làm hỏng bản build và mất cả tiếng đồng hồ để tìm ra thay đổi dependency nào đã gây ra lỗi đó.

GitHub Actions miễn phí cho các kho lưu trữ công khai và là lựa chọn rẻ nhất mà tôi tìm thấy cho các kho lưu trữ riêng tư. Đây là thiết lập tôi đã áp dụng sau khi chạy nó trên hàng chục dự án khác nhau — từ ứng dụng Next.js đến API Python và dịch vụ Go.

Audio Briefing
0:00 / 0:00

Tại sao lại là GitHub Actions mà không phải các lựa chọn khác?

Trước khi chọn một công cụ CI: GitHub Actions chạy trực tiếp trên kho lưu trữ của bạn, không cần kết nối dịch vụ bên thứ ba nào. Jenkins yêu cầu một máy chủ. CircleCI có các gói miễn phí tốt hơn cho một số trường hợp sử dụng. Travis CI đã chuyển sang trả phí hoàn toàn. GitLab CI rất tuyệt nếu bạn đã sử dụng GitLab.

Đối với các dự án được lưu trữ trên GitHub, Actions thắng thế về sự tiện lợi: không cần thiết lập webhook, không cần quản lý token, các trigger được định nghĩa ngay cạnh code của bạn.


Advertisement

Workflow đầu tiên của bạn

Các workflow của GitHub Actions nằm trong .github/workflows/ dưới dạng tệp YAML. Tạo .github/workflows/ci.yml:

name: CI

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

jobs:
  build-and-test:
    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 test
      - run: npm run build

Đẩy code này lên và commit tiếp theo của bạn sẽ kích hoạt workflow. Mỗi PR sẽ có một dấu kiểm màu xanh lá cây hoặc một dấu X màu đỏ, và bạn sẽ không bao giờ hợp nhất code bị lỗi nữa.

npm ci so với npm install

Sử dụng npm ci trong CI, không phải npm install. npm ci nghiêm ngặt hơn: nó cài đặt chính xác những gì có trong package-lock.json, sẽ thất bại nếu có sự không khớp, và không bao giờ cập nhật tệp lockfile. Điều này có nghĩa là môi trường CI của bạn khớp với những gì bạn đã thử nghiệm cục bộ.


Bộ nhớ đệm giúp tăng tốc

Dòng cache: 'npm' ở trên lưu vào bộ nhớ đệm node_modules giữa các lần chạy dựa trên hàm băm của package-lock.json của bạn. Nếu không có nó, mỗi lần chạy workflow sẽ cài đặt lại tất cả các dependency từ đầu — mất 60–120 giây mỗi lần chạy tùy thuộc vào số lượng dependency của bạn.

Với bộ nhớ đệm, các lần truy cập bộ nhớ đệm giảm thời gian đó xuống còn 5–10 giây.

Đối với các hệ sinh thái khác:

# Python (pip)
- uses: actions/setup-python@v5
  with:
    python-version: '3.12'
    cache: 'pip'

# Python (uv)
- uses: astral-sh/setup-uv@v3
  with:
    enable-cache: true

# Go
- uses: actions/setup-go@v5
  with:
    go-version: '1.22'
    cache: true

# Rust
- uses: actions/cache@v4
  with:
    path: |
      ~/.cargo/registry
      ~/.cargo/git
      target/
    key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}

Bộ nhớ đệm được khóa bằng hàm băm của lockfile của bạn. Khi các dependency thay đổi, bộ nhớ đệm sẽ bỏ qua và xây dựng lại. Khi chúng không thay đổi, bộ nhớ đệm sẽ truy cập và CI sẽ nhanh chóng.


Kiểm thử song song

Đối với các bộ kiểm thử lớn hơn, hãy chia các kiểm thử trên nhiều runner:

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        shard: [1, 2, 3, 4]
    
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      - run: npm ci
      - run: npm test -- --shard=${{ matrix.shard }}/4

Điều này chạy 4 job đồng thời, mỗi job xử lý một phần tư số kiểm thử của bạn. Tổng thời gian kiểm thử giảm từ 4× xuống còn ~1×.

Playwright hỗ trợ điều này nguyên bản với --shard. Đối với Jest, sử dụng --testPathPattern hoặc jest-runner-groups. Đối với pytest, sử dụng pytest-split.


Advertisement

Triển khai tự động

Tách CI (kiểm thử) khỏi CD (triển khai). Chúng có ngữ nghĩa lỗi khác nhau — một lỗi kiểm thử nên chặn một PR, nhưng một lỗi triển khai không nên chặn các PR khác.

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

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm run build
      - name: Deploy to Vercel
        env:
          VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
          VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
          VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
        run: npx vercel --prod --token $VERCEL_TOKEN
Sử dụng Secrets, không phải Hardcoded Tokens

Không bao giờ đặt khóa API hoặc token triển khai trực tiếp vào tệp YAML của bạn. Lưu trữ chúng trong GitHub → Settings → Secrets and variables → Actions, sau đó tham chiếu chúng dưới dạng ${{ secrets.MY_TOKEN }}. Secrets được mã hóa khi lưu trữ, được che trong nhật ký và không bao giờ bị lộ cho các PR từ fork.

Chỉ triển khai khi kiểm thử thành công

Nối CI và CD với từ khóa needs:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm test

  deploy:
    needs: test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm run build
      - run: npx vercel --prod --token ${{ secrets.VERCEL_TOKEN }}

Job deploy chỉ chạy nếu test thành công và nếu chúng ta đang ở trên nhánh main.


Biến môi trường và Secrets

Ba cấp độ để xử lý cấu hình trong GitHub Actions:

1. Mã hóa cứng trong YAML — Chỉ dành cho các giá trị không nhạy cảm, phổ quát:

env:
  NODE_ENV: production
  PORT: 3000

2. Repository secrets — Các giá trị nhạy cảm (khóa API, token):

env:
  DATABASE_URL: ${{ secrets.DATABASE_URL }}
  STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }}

3. Environment secrets — Các giá trị khác nhau cho mỗi môi trường triển khai (staging so với production):

jobs:
  deploy:
    environment: production  # uses the "production" environment's secrets
    steps:
      - run: deploy.sh
        env:
          API_URL: ${{ secrets.API_URL }}  # production-specific value

Environment secrets yêu cầu cổng phê duyệt: cấu hình người đánh giá bắt buộc trong Settings → Environments → protection rules. Hữu ích để ngăn chặn các triển khai sản xuất ngẫu nhiên.


Những điều tôi đã học được một cách khó khăn

Chạy CI trên mọi PR, không chỉ trên main. Phát hiện lỗi kiểm thử trên nhánh PR rẻ hơn nhiều so với phát hiện nó sau khi hợp nhất. Thêm pull_request: branches: [main] vào mọi workflow.

Giữ CI và CD riêng biệt. Workflow CI của bạn chạy mỗi lần đẩy và nên hoàn thành trong vòng 5 phút. Workflow CD của bạn triển khai và có thể mất nhiều thời gian hơn. Việc trộn lẫn chúng có nghĩa là một triển khai chậm sẽ chặn vòng lặp phản hồi PR của bạn.

Matrix builds làm tăng hóa đơn của bạn. Kiểm thử trên nhiều phiên bản Node hoặc hệ điều hành với chiến lược ma trận làm tăng thời gian chạy của bạn. Tôi chỉ sử dụng nó cho các thư viện mà khả năng tương thích giữa các phiên bản thực sự quan trọng. Đối với các ứng dụng, một môi trường mục tiêu là đủ.

# Only do this for libraries, not apps
strategy:
  matrix:
    node-version: [18, 20, 22]
    os: [ubuntu-latest, windows-latest, macos-latest]

Đó là 9 runner mỗi lần đẩy. Với 2 phút mỗi runner, đó là 18 phút tính toán.

Theo dõi số phút của bạn. GitHub Actions cung cấp 2.000 phút miễn phí/tháng cho các kho lưu trữ riêng tư trên gói miễn phí. Một dự án với matrix builds và 10 lần đẩy/ngày có thể đốt hết số phút đó trong một tuần.

Thất bại nhanh chóng với fail-fast: true (mặc định trong matrix builds). Nếu một job ma trận thất bại, GitHub sẽ hủy các job khác. Tiết kiệm thời gian khi bạn có một lỗi cơ bản.

Sử dụng actions/cache rõ ràng cho các lớp Docker:

- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v5
  with:
    context: .
    cache-from: type=gha
    cache-to: type=gha,mode=max

Nếu không có điều này, Docker sẽ xây dựng lại toàn bộ hình ảnh của bạn từ đầu trong mỗi lần chạy.


Một Workflow hoàn chỉnh sẵn sàng cho sản xuất

Đây là workflow đầy đủ mà tôi sử dụng cho một dự án Next.js:

name: CI

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

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  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 lint
      - run: npm run type-check

  test:
    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 test -- --coverage
      - uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}

  build:
    runs-on: ubuntu-latest
    needs: [lint, test]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      - run: npm ci
      - run: npm run build
      - uses: actions/upload-artifact@v4
        with:
          name: build-output
          path: .next/
          retention-days: 1

Các tính năng chính:

  • concurrency hủy các lần chạy đang diễn ra khi có một lần đẩy mới — ngăn chặn việc xếp hàng quá nhiều trong quá trình phát triển tích cực
  • lint và test chạy song song, build chờ cả hai
  • Báo cáo độ bao phủ được gửi đến Codecov trong mỗi lần đẩy
  • Các artifact bản build được tải lên trong 1 ngày (hữu ích cho các job triển khai để sử dụng mà không cần xây dựng lại)

Gỡ lỗi các Workflow bị lỗi

Khi một workflow bị lỗi và nhật ký không rõ ràng:

Bật ghi nhật ký gỡ lỗi bằng cách đặt một repository secret ACTIONS_STEP_DEBUG=true. Điều này sẽ xuất ra thông tin chi tiết từ mỗi bước.

SSH vào runner bằng cách sử dụng tmate:

- uses: mxschmitt/action-tmate@v3
  if: ${{ failure() }}

Điều này tạm dừng workflow và cung cấp cho bạn kết nối SSH đến môi trường runner chính xác khi một bước bị lỗi. Vô giá để gỡ lỗi các vấn đề cụ thể về môi trường.

In chi tiết môi trường:

- run: node --version && npm --version && env | sort

Bạn cũng có thể thích

Share this article:

Stay Updated

Get the latest posts delivered straight to your inbox.

Free Developer Utilities

Free In-Browser Developer Tools

Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.

Explore Tools
Advertisement