Quy trình công việc đơn giản là điều tuyệt vời để bắt đầu, nhưng các dự án thực tế thường cần tự động hóa phức tạp hơn. Bạn có thể cần thử nghiệm trên nhiều hệ điều hành, điều phối các trình tự triển khai phức tạp hoặc xây dựng các phiên bản ứng dụng khác nhau cho các nền tảng khác nhau. Khi các dự án của bạn phát triển, khả năng tự động hóa của bạn cũng cần phát triển theo chúng.
Thách thức là xây dựng các quy trình làm việc phức tạp mà vẫn có thể duy trì và sửa lỗi được. Thật dễ dàng để tạo các quy trình công việc hoạt động nhưng sau này không thể hiểu hoặc sửa đổi được. Hiểu các mẫu Hành động GitHub nâng cao giúp bạn xây dựng khả năng tự động hóa mạnh mẽ mà nhóm của bạn thực sự có thể duy trì.
Bản dựng ma trận để thử nghiệm đa nền tảng
Bản dựng ma trận cho phép bạn chạy cùng một quy trình công việc trên nhiều cấu hình cùng một lúc. Thay vì tạo các quy trình làm việc riêng biệt cho từng nền tảng hoặc phiên bản, bạn xác định một ma trận các biến và Tác vụ GitHub sẽ chạy quy trình làm việc của bạn cho từng kết hợp.
Đây là cách xây dựng ma trận hoạt động:
Single Configuration (Traditional):
┌─────────────────────────────────┐
│ One Job │
│ OS: ubuntu-latest │
│ Node: 18 │
│ │
│ ┌─────────────────────────┐ │
│ │ 1. Checkout │ │
│ │ 2. Setup Node.js 18 │ │
│ │ 3. Install deps │ │
│ │ 4. Run tests │ │
│ └─────────────────────────┘ │
└─────────────────────────────────┘
Matrix Configuration (Parallel):
Matrix: { os: [ubuntu-latest, windows-latest, macos-latest],
node: [16, 18, 20] }
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Job 1-1 │ │ Job 1-2 │ │ Job 1-3 │
│ ubuntu + node16 │ │ ubuntu + node18 │ │ ubuntu + node20 │
│ │ │ │ │ │
│ ┌─────────────┐ │ │ ┌─────────────┐ │ │ ┌─────────────┐ │
│ │1. Checkout │ │ │ │1. Checkout │ │ │ │1. Checkout │ │
│ │2. Setup N16 │ │ │ │2. Setup N18 │ │ │ │2. Setup N20 │ │
│ │3. Install │ │ │ │3. Install │ │ │ │3. Install │ │
│ │4. Test │ │ │ │4. Test │ │ │ │4. Test │ │
│ └─────────────┘ │ │ └─────────────┘ │ │ └─────────────┘ │
└─────────────────┘ └─────────────────┘ └─────────────────┘
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Job 2-1 │ │ Job 2-2 │ │ Job 2-3 │
│ windows + node16│ │ windows + node18│ │ windows + node20│
│ │ │ │ │ │
│ ┌─────────────┐ │ │ ┌─────────────┐ │ │ ┌─────────────┐ │
│ │1. Checkout │ │ │ │1. Checkout │ │ │ │1. Checkout │ │
│ │2. Setup N16 │ │ │ │2. Setup N18 │ │ │ │2. Setup N20 │ │
│ │3. Install │ │ │ │3. Install │ │ │ │3. Install │ │
│ │4. Test │ │ │ │4. Test │ │ │ │4. Test │ │
│ └─────────────┘ │ │ └─────────────┘ │ │ └─────────────┘ │
└─────────────────┘ └─────────────────┘ └─────────────────┘
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Job 3-1 │ │ Job 3-2 │ │ Job 3-3 │
│ macos + node16 │ │ macos + node18 │ │ macos + node20 │
│ │ │ │ │ │
│ ┌─────────────┐ │ │ ┌─────────────┐ │ │ ┌─────────────┐ │
│ │1. Checkout │ │ │ │1. Checkout │ │ │ │1. Checkout │ │
│ │2. Setup N16 │ │ │ │2. Setup N18 │ │ │ │2. Setup N20 │ │
│ │3. Install │ │ │ │3. Install │ │ │ │3. Install │ │
│ │4. Test │ │ │ │4. Test │ │ │ │4. Test │ │
│ └─────────────┘ │ │ └─────────────┘ │ │ └─────────────┘ │
└─────────────────┘ └─────────────────┘ └─────────────────┘
Result: 9 parallel jobs test all combinations!
Đây là một ví dụ thực tế kiểm tra ứng dụng Node.js trên nhiều phiên bản và hệ điều hành:
name: Cross-Platform Testing
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
# Don't cancel other matrix jobs if one fails
fail-fast: false
matrix:
# Test on multiple operating systems
os: [ubuntu-latest, windows-latest, macos-latest]
# Test multiple Node.js versions
node-version: [16, 18, 20]
# Exclude problematic combinations
exclude:
# Node 16 has issues on Windows in our specific setup
- os: windows-latest
node-version: 16
# Add specific configurations
include:
# Test latest Node.js on Ubuntu with additional checks
- os: ubuntu-latest
node-version: 20
run-integration-tests: true
run-performance-tests: true
steps:
- uses: actions/checkout@v4
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run unit tests
run: npm test
- name: Run integration tests
if: matrix.run-integration-tests
run: npm run test:integration
- name: Run performance tests
if: matrix.run-performance-tests
run: npm run test:performance
- name: Build application
run: npm run build
# Upload test results with matrix info in the name
- name: Upload test results
uses: actions/upload-artifact@v4
if: always()
with:
name: test-results-${{ matrix.os }}-node${{ matrix.node-version }}
path: |
coverage/
test-results.xml
Chiến lược ma trận tạo ra các công việc riêng biệt cho từng sự kết hợp giữa phiên bản OS và Node.js. Cài đặt fail-fast: false đảm bảo rằng nếu một kết hợp không thành công thì những kết hợp khác vẫn tiếp tục chạy để bạn có thể xem toàn cảnh những gì hoạt động và những gì không.
Tạo ma trận động
Đôi khi bạn cần tạo cấu hình ma trận một cách linh hoạt dựa trên nội dung kho lưu trữ hoặc dữ liệu bên ngoài của mình:
name: Dynamic Multi-Service Build
on:
push:
branches: [main]
jobs:
# Generate the matrix based on directories in the repository
generate-matrix:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.generate.outputs.matrix }}
steps:
- uses: actions/checkout@v4
- name: Generate service matrix
id: generate
run: |
# Find all directories that contain a Dockerfile
services=$(find . -name "Dockerfile" -type f | \
sed 's|/Dockerfile||' | \
sed 's|^\./||' | \
jq -R -s -c 'split("\n")[:-1]')
echo "Found services: $services"
echo "matrix={\"service\":$services}" >> $GITHUB_OUTPUT
# Build each service found in the previous job
build-services:
runs-on: ubuntu-latest
needs: generate-matrix
if: needs.generate-matrix.outputs.matrix != '{"service":[]}'
strategy:
matrix: ${{ fromJson(needs.generate-matrix.outputs.matrix) }}
steps:
- uses: actions/checkout@v4
- name: Build ${{ matrix.service }}
run: |
echo "Building service: ${{ matrix.service }}"
cd ${{ matrix.service }}
# Build Docker image
docker build -t ${{ matrix.service }}:${{ github.sha }} .
# Run service-specific tests if they exist
if [ -f "test.sh" ]; then
echo "Running tests for ${{ matrix.service }}"
chmod +x test.sh
./test.sh
fi
- name: Upload service artifacts
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.service }}-build
path: ${{ matrix.service }}/dist/
Mẫu này hữu ích cho các monorepos nơi bạn muốn tự động xây dựng và kiểm tra tất cả các dịch vụ mà không cần duy trì danh sách những gì tồn tại theo cách thủ công.
Quy trình làm việc có điều kiện và Trình kích hoạt thông minh
Không phải mọi quy trình làm việc đều cần chạy trên mọi thay đổi. Logic điều kiện thông minh giúp bạn chỉ chạy những quy trình công việc thực sự cần thiết:
name: Smart Conditional Builds
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
# Detect what changed to decide what to build
detect-changes:
runs-on: ubuntu-latest
outputs:
frontend-changed: ${{ steps.changes.outputs.frontend }}
backend-changed: ${{ steps.changes.outputs.backend }}
docs-changed: ${{ steps.changes.outputs.docs }}
docker-changed: ${{ steps.changes.outputs.docker }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Detect changed files
id: changes
run: |
# Compare with the merge base for PRs, or previous commit for pushes
if [ "${{ github.event_name }}" = "pull_request" ]; then
BASE="${{ github.event.pull_request.base.sha }}"
else
BASE="HEAD~1"
fi
# Check if frontend files changed
if git diff --name-only $BASE HEAD | grep -E '^(frontend/|src/.*\.(js|jsx|ts|tsx|css|scss))'; then
echo "frontend=true" >> $GITHUB_OUTPUT
else
echo "frontend=false" >> $GITHUB_OUTPUT
fi
# Check if backend files changed
if git diff --name-only $BASE HEAD | grep -E '^(backend/|api/|server/)'; then
echo "backend=true" >> $GITHUB_OUTPUT
else
echo "backend=false" >> $GITHUB_OUTPUT
fi
# Check if documentation changed
if git diff --name-only $BASE HEAD | grep -E '\.(md|rst|txt)$'; then
echo "docs=true" >> $GITHUB_OUTPUT
else
echo "docs=false" >> $GITHUB_OUTPUT
fi
# Check if Docker files changed
if git diff --name-only $BASE HEAD | grep -E '(Dockerfile|docker-compose)'; then
echo "docker=true" >> $GITHUB_OUTPUT
else
echo "docker=false" >> $GITHUB_OUTPUT
fi
# Only build frontend if frontend files changed
build-frontend:
runs-on: ubuntu-latest
needs: detect-changes
if: needs.detect-changes.outputs.frontend-changed == 'true'
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
- name: Build frontend
run: |
cd frontend
npm ci
npm run build
- name: Upload frontend build
uses: actions/upload-artifact@v4
with:
name: frontend-build
path: frontend/dist/
# Only build backend if backend files changed
build-backend:
runs-on: ubuntu-latest
needs: detect-changes
if: needs.detect-changes.outputs.backend-changed == 'true'
steps:
- uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v4
with:
go-version: '1.21'
- name: Build backend
run: |
cd backend
go mod download
go build -o app ./cmd/server
- name: Upload backend build
uses: actions/upload-artifact@v4
with:
name: backend-build
path: backend/app
# Only build Docker images if Docker files or application code changed
build-docker:
runs-on: ubuntu-latest
needs: detect-changes
if: |
needs.detect-changes.outputs.docker-changed == 'true' ||
needs.detect-changes.outputs.frontend-changed == 'true' ||
needs.detect-changes.outputs.backend-changed == 'true'
steps:
- uses: actions/checkout@v4
- name: Build Docker images
run: |
echo "Building Docker images..."
docker build -t myapp:${{ github.sha }} .
# Deploy only if application code changed and we're on main branch
deploy:
runs-on: ubuntu-latest
needs: [detect-changes, build-frontend, build-backend]
if: |
always() &&
github.ref == 'refs/heads/main' &&
(needs.detect-changes.outputs.frontend-changed == 'true' ||
needs.detect-changes.outputs.backend-changed == 'true') &&
(needs.build-frontend.result == 'success' || needs.build-frontend.result == 'skipped') &&
(needs.build-backend.result == 'success' || needs.build-backend.result == 'skipped')
steps:
- name: Deploy application
run: echo "Deploying application..."
Cách tiếp cận này tiết kiệm đáng kể thời gian và tài nguyên CI/CD bằng cách chỉ chạy các bản dựng khi chúng thực sự cần thiết.
Điều phối quy trình làm việc với quy trình làm việc có thể tái sử dụng
Khi quá trình tự động hóa của bạn phát triển, bạn sẽ thấy mình lặp lại các mẫu tương tự trên nhiều kho lưu trữ. Quy trình công việc có thể sử dụng lại cho phép bạn chia sẻ các mẫu tự động hóa phổ biến:
# .github/workflows/reusable-node-build.yml
name: Reusable Node.js Build
on:
workflow_call:
inputs:
node-version:
description: 'Node.js version to use'
required: false
default: '18'
type: string
working-directory:
description: 'Working directory for Node.js app'
required: false
default: '.'
type: string
run-tests:
description: 'Whether to run tests'
required: false
default: true
type: boolean
secrets:
NPM_TOKEN:
description: 'NPM authentication token'
required: false
outputs:
build-version:
description: 'Version of the built application'
value: ${{ jobs.build.outputs.version }}
jobs:
build:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
cache: 'npm'
cache-dependency-path: ${{ inputs.working-directory }}/package-lock.json
- name: Configure npm authentication
if: secrets.NPM_TOKEN != ''
run: |
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc
- name: Install dependencies
working-directory: ${{ inputs.working-directory }}
run: npm ci
- name: Run tests
if: inputs.run-tests
working-directory: ${{ inputs.working-directory }}
run: npm test
- name: Build application
working-directory: ${{ inputs.working-directory }}
run: npm run build
- name: Get version
id: version
working-directory: ${{ inputs.working-directory }}
run: |
VERSION=$(node -p "require('./package.json').version")
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: build-artifacts-${{ steps.version.outputs.version }}
path: ${{ inputs.working-directory }}/dist/
Bây giờ bạn có thể sử dụng quy trình làm việc có thể tái sử dụng này trong nhiều kho lưu trữ:
# In any repository
name: Build and Deploy
on:
push:
branches: [main]
jobs:
build-frontend:
uses: ./.github/workflows/reusable-node-build.yml
with:
working-directory: frontend
node-version: '18'
run-tests: true
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
build-backend:
uses: ./.github/workflows/reusable-node-build.yml
with:
working-directory: backend
node-version: '20'
run-tests: false
deploy:
needs: [build-frontend, build-backend]
runs-on: ubuntu-latest
steps:
- name: Deploy application
run: |
echo "Frontend version: ${{ needs.build-frontend.outputs.build-version }}"
echo "Backend version: ${{ needs.build-backend.outputs.build-version }}"
# Deploy logic here
Logic có điều kiện phức tạp
Đôi khi bạn cần logic điều kiện phức tạp hơn câu lệnh if đơn giản có thể cung cấp:
name: Advanced Conditional Logic
on:
push:
branches: [main, develop, 'feature/*', 'hotfix/*']
pull_request:
branches: [main, develop]
jobs:
determine-strategy:
runs-on: ubuntu-latest
outputs:
deploy-environment: ${{ steps.strategy.outputs.environment }}
run-integration-tests: ${{ steps.strategy.outputs.integration-tests }}
notify-team: ${{ steps.strategy.outputs.notify }}
deployment-strategy: ${{ steps.strategy.outputs.strategy }}
steps:
- name: Determine deployment strategy
id: strategy
run: |
# Complex logic to determine what to do based on branch and event
BRANCH="${{ github.ref_name }}"
EVENT="${{ github.event_name }}"
echo "Branch: $BRANCH, Event: $EVENT"
# Determine environment
if [[ "$BRANCH" == "main" && "$EVENT" == "push" ]]; then
echo "environment=production" >> $GITHUB_OUTPUT
echo "integration-tests=true" >> $GITHUB_OUTPUT
echo "notify=true" >> $GITHUB_OUTPUT
echo "strategy=blue-green" >> $GITHUB_OUTPUT
elif [[ "$BRANCH" == "develop" && "$EVENT" == "push" ]]; then
echo "environment=staging" >> $GITHUB_OUTPUT
echo "integration-tests=true" >> $GITHUB_OUTPUT
echo "notify=false" >> $GITHUB_OUTPUT
echo "strategy=rolling" >> $GITHUB_OUTPUT
elif [[ "$BRANCH" =~ ^feature/ ]]; then
echo "environment=review" >> $GITHUB_OUTPUT
echo "integration-tests=false" >> $GITHUB_OUTPUT
echo "notify=false" >> $GITHUB_OUTPUT
echo "strategy=direct" >> $GITHUB_OUTPUT
elif [[ "$BRANCH" =~ ^hotfix/ ]]; then
echo "environment=hotfix" >> $GITHUB_OUTPUT
echo "integration-tests=true" >> $GITHUB_OUTPUT
echo "notify=true" >> $GITHUB_OUTPUT
echo "strategy=direct" >> $GITHUB_OUTPUT
elif [[ "$EVENT" == "pull_request" ]]; then
echo "environment=preview" >> $GITHUB_OUTPUT
echo "integration-tests=false" >> $GITHUB_OUTPUT
echo "notify=false" >> $GITHUB_OUTPUT
echo "strategy=direct" >> $GITHUB_OUTPUT
else
echo "environment=none" >> $GITHUB_OUTPUT
echo "integration-tests=false" >> $GITHUB_OUTPUT
echo "notify=false" >> $GITHUB_OUTPUT
echo "strategy=none" >> $GITHUB_OUTPUT
fi
deploy:
needs: determine-strategy
runs-on: ubuntu-latest
if: needs.determine-strategy.outputs.deploy-environment != 'none'
environment: ${{ needs.determine-strategy.outputs.deploy-environment }}
steps:
- name: Deploy using ${{ needs.determine-strategy.outputs.deployment-strategy }} strategy
run: |
echo "Deploying to ${{ needs.determine-strategy.outputs.deploy-environment }}"
echo "Using ${{ needs.determine-strategy.outputs.deployment-strategy }} strategy"
# Execute deployment based on strategy
case "${{ needs.determine-strategy.outputs.deployment-strategy }}" in
"blue-green")
echo "Executing blue-green deployment..."
;;
"rolling")
echo "Executing rolling deployment..."
;;
"direct")
echo "Executing direct deployment..."
;;
esac
integration-tests:
needs: [determine-strategy, deploy]
runs-on: ubuntu-latest
if: needs.determine-strategy.outputs.run-integration-tests == 'true'
steps:
- name: Run integration tests
run: echo "Running integration tests for ${{ needs.determine-strategy.outputs.deploy-environment }}"
notify:
needs: [determine-strategy, deploy, integration-tests]
runs-on: ubuntu-latest
if: always() && needs.determine-strategy.outputs.notify-team == 'true'
steps:
- name: Send notification
run: |
if [[ "${{ needs.deploy.result }}" == "success" && "${{ needs.integration-tests.result }}" == "success" ]]; then
echo "✅ Deployment successful!"
else
echo "❌ Deployment failed!"
fi
Quy trình làm việc song song với sự phối hợp
Đôi khi bạn cần nhiều quy trình công việc để điều phối các hoạt động của họ:
# Primary build workflow
name: Build Application
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build application
run: echo "Building..."
- name: Trigger deployment workflow
uses: actions/github-script@v7
with:
script: |
await github.rest.actions.createWorkflowDispatch({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'deploy.yml',
ref: 'main',
inputs: {
build_sha: context.sha,
environment: 'staging'
}
});
# Separate deployment workflow
name: Deploy Application
on:
workflow_dispatch:
inputs:
build_sha:
description: 'Build SHA to deploy'
required: true
environment:
description: 'Environment to deploy to'
required: true
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy build ${{ github.event.inputs.build_sha }}
run: |
echo "Deploying ${{ github.event.inputs.build_sha }} to ${{ github.event.inputs.environment }}"
Sự tách biệt này cho phép bạn kích hoạt triển khai độc lập với các bản dựng, điều này rất hữu ích để quảng bá cùng một bản dựng artifact thông qua các môi trường khác nhau.
Xử lý và khôi phục lỗi
Quy trình công việc nâng cao cần xử lý lỗi phức tạp:
name: Robust Workflow with Error Handling
jobs:
deploy-with-retry:
runs-on: ubuntu-latest
steps:
- name: Deploy with automatic retry
uses: nick-fields/retry@v2
with:
timeout_minutes: 10
max_attempts: 3
retry_wait_seconds: 30
command: |
echo "Attempting deployment..."
# Your deployment command here
if [ $RANDOM -lt 16384 ]; then
echo "Deployment failed, will retry..."
exit 1
else
echo "Deployment successful!"
fi
- name: Handle deployment failure
if: failure()
run: |
echo "Deployment failed after all retries"
# Send alert, create incident, etc.
- name: Cleanup on failure
if: failure()
run: |
echo "Running cleanup after failure..."
# Cleanup partially deployed resources
- name: Success notification
if: success()
run: |
echo "Deployment completed successfully"
Quy trình làm việc nâng cao mang đến cho bạn khả năng xử lý các tình huống phức tạp nhưng chúng yêu cầu lập kế hoạch và thử nghiệm cẩn thận. Bắt đầu với các mẫu đơn giản hơn và tăng dần độ phức tạp khi nhu cầu của bạn tăng lên cũng như sự tự tin của bạn với GitHub Actions tăng lên.
Trong phần tiếp theo, chúng ta sẽ khám phá các kỹ thuật giám sát, gỡ lỗi và tối ưu hóa giúp bạn duy trì khả năng tự động hóa đáng tin cậy khi quy trình công việc của bạn trở nên phức tạp hơn.