經過昨天學習 GitHub Runner 的部署流程,今天我們要深入了解 GitHub Actions 的完整 CI/CD 體系。就像現代農場的自動化生產線一樣,從種子進入到產品出貨,每個環節都有自動化的品質控制和流程管理。
# .github/workflows/ci.yml
name: Continuous Integration
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
env:
PYTHON_VERSION: '3.9'
jobs:
# 程式碼品質檢查
lint:
name: Code Quality Checks
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: 'pip'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install flake8 black isort mypy bandit safety
pip install -r requirements.txt
- name: Code formatting check (Black)
run: black --check --diff src/
- name: Import sorting check (isort)
run: isort --check-only --diff src/
- name: Linting (flake8)
run: flake8 src/ --statistics
- name: Type checking (mypy)
run: mypy src/ --ignore-missing-imports
- name: Security check (Bandit)
run: bandit -r src/ -f json -o bandit-report.json
- name: Dependency vulnerability check
run: safety check --json --output safety-report.json
- name: Upload security reports
uses: actions/upload-artifact@v3
if: always()
with:
name: security-reports
path: |
bandit-report.json
safety-report.json
# 單元測試
unit-tests:
name: Unit Tests
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.8', '3.9', '3.10']
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pytest pytest-cov pytest-xdist pytest-mock
pip install -r requirements.txt
pip install -r requirements-test.txt
- name: Run unit tests
run: |
pytest src/tests/unit/ \
--cov=src/ \
--cov-report=xml \
--cov-report=html \
--junitxml=test-results.xml \
-v \
--tb=short
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
flags: unittests
name: codecov-${{ matrix.python-version }}
- name: Upload test results
uses: actions/upload-artifact@v3
if: always()
with:
name: test-results-${{ matrix.python-version }}
path: |
test-results.xml
htmlcov/
# 整合測試
integration-tests:
name: Integration Tests
runs-on: ubuntu-latest
needs: [lint, unit-tests]
services:
redis:
image: redis:6.2
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 6379:6379
postgres:
image: postgres:13
env:
POSTGRES_PASSWORD: testpass
POSTGRES_DB: trading_test
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: 'pip'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install -r requirements-test.txt
- name: Set up test environment
env:
DATABASE_URL: postgresql://postgres:testpass@localhost:5432/trading_test
REDIS_URL: redis://localhost:6379
BYBIT_API_KEY: test_key
BYBIT_SECRET_KEY: test_secret
run: |
# 初始化測試資料庫
python scripts/init_test_db.py
# 載入測試資料
python scripts/load_test_data.py
- name: Run integration tests
env:
DATABASE_URL: postgresql://postgres:testpass@localhost:5432/trading_test
REDIS_URL: redis://localhost:6379
run: |
pytest src/tests/integration/ \
--tb=short \
-v \
--durations=10
# Docker 映像建置測試
docker-build:
name: Docker Build Test
runs-on: ubuntu-latest
needs: [lint, unit-tests]
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Build Docker image
uses: docker/build-push-action@v4
with:
context: .
file: ./Dockerfile
push: false
tags: trading-bot:test
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Test Docker image
run: |
# 測試 Docker 映像是否能正常啟動
docker run --rm -d --name test-container trading-bot:test
sleep 10
# 檢查容器健康狀態
if docker ps | grep test-container; then
echo "✅ Docker 映像測試通過"
docker stop test-container
else
echo "❌ Docker 映像測試失敗"
docker logs test-container
exit 1
fi
# 條件執行範例
jobs:
deploy-staging:
if: github.ref == 'refs/heads/develop'
runs-on: ubuntu-latest
# ... 部署到 staging 環境
deploy-production:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
# ... 部署到 production 環境
# 矩陣策略測試多種環境
test-matrix:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ['3.8', '3.9', '3.10', '3.11']
include:
- os: ubuntu-latest
python-version: '3.11'
experimental: true
exclude:
- os: windows-latest
python-version: '3.8'
runs-on: ${{ matrix.os }}
continue-on-error: ${{ matrix.experimental || false }}
# .github/actions/setup-trading-env/action.yml
name: 'Setup Trading Environment'
description: 'Set up Python environment for trading bot'
inputs:
python-version:
description: 'Python version to use'
required: true
default: '3.9'
cache-dependency-path:
description: 'Path to dependency file'
required: false
default: 'requirements.txt'
outputs:
cache-hit:
description: 'Whether dependencies were restored from cache'
value: ${{ steps.cache.outputs.cache-hit }}
runs:
using: 'composite'
steps:
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: ${{ inputs.python-version }}
- name: Cache dependencies
id: cache
uses: actions/cache@v3
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles(inputs.cache-dependency-path) }}
restore-keys: |
${{ runner.os }}-pip-
- name: Install dependencies
shell: bash
run: |
python -m pip install --upgrade pip
pip install -r ${{ inputs.cache-dependency-path }}
# .github/workflows/cd.yml
name: Continuous Deployment
on:
workflow_run:
workflows: ["Continuous Integration"]
branches: [main, develop]
types: [completed]
jobs:
# 決定部署目標
determine-deployment:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
outputs:
environment: ${{ steps.determine.outputs.environment }}
should-deploy: ${{ steps.determine.outputs.should-deploy }}
steps:
- name: Determine deployment environment
id: determine
run: |
if [[ "${{ github.event.workflow_run.head_branch }}" == "main" ]]; then
echo "environment=production" >> $GITHUB_OUTPUT
echo "should-deploy=true" >> $GITHUB_OUTPUT
elif [[ "${{ github.event.workflow_run.head_branch }}" == "develop" ]]; then
echo "environment=staging" >> $GITHUB_OUTPUT
echo "should-deploy=true" >> $GITHUB_OUTPUT
else
echo "should-deploy=false" >> $GITHUB_OUTPUT
fi
# 建置和推送映像
build-and-push:
runs-on: ubuntu-latest
needs: determine-deployment
if: needs.determine-deployment.outputs.should-deploy == 'true'
outputs:
image-uri: ${{ steps.build.outputs.image-uri }}
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v1
- name: Extract metadata
id: meta
uses: docker/metadata-action@v4
with:
images: ${{ steps.login-ecr.outputs.registry }}/trading-bot
tags: |
type=ref,event=branch
type=sha,prefix={{branch}}-
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push
id: build
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
BUILDTIME=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.created'] }}
VERSION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}
REVISION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }}
- name: Output image URI
run: |
IMAGE_URI="${{ steps.login-ecr.outputs.registry }}/trading-bot:${{ github.sha }}"
echo "image-uri=$IMAGE_URI" >> $GITHUB_OUTPUT
# 部署到目標環境
deploy:
runs-on: ubuntu-latest
needs: [determine-deployment, build-and-push]
environment: ${{ needs.determine-deployment.outputs.environment }}
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
role-to-assume: ${{ secrets.DEPLOY_ROLE_ARN }}
- name: Deploy to ECS
uses: ./.github/actions/deploy-ecs
with:
cluster-name: trading-${{ needs.determine-deployment.outputs.environment }}
service-name: trading-bot-service
image-uri: ${{ needs.build-and-push.outputs.image-uri }}
task-definition-family: trading-bot-${{ needs.determine-deployment.outputs.environment }}
# 部署後測試
post-deployment-tests:
runs-on: ubuntu-latest
needs: [determine-deployment, deploy]
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Run smoke tests
env:
ENVIRONMENT: ${{ needs.determine-deployment.outputs.environment }}
API_BASE_URL: ${{ vars.API_BASE_URL }}
run: |
python -m pytest tests/smoke/ \
--base-url=$API_BASE_URL \
--environment=$ENVIRONMENT \
-v
- name: Run performance tests
if: needs.determine-deployment.outputs.environment == 'production'
run: |
# 使用 k6 進行效能測試
k6 run --vus 10 --duration 60s tests/performance/load-test.js
# 監控和告警設定
setup-monitoring:
runs-on: ubuntu-latest
needs: [determine-deployment, post-deployment-tests]
if: needs.determine-deployment.outputs.environment == 'production'
steps:
- name: Update CloudWatch alarms
run: |
# 更新 CloudWatch 告警閾值
aws cloudwatch put-metric-alarm \
--alarm-name "TradingBot-HighCPU" \
--alarm-description "High CPU utilization" \
--metric-name CPUUtilization \
--namespace AWS/ECS \
--statistic Average \
--period 300 \
--threshold 80 \
--comparison-operator GreaterThanThreshold \
--evaluation-periods 2
- name: Update monitoring dashboard
run: |
# 更新 Grafana Dashboard
curl -X POST "${{ secrets.GRAFANA_URL }}/api/dashboards/db" \
-H "Authorization: Bearer ${{ secrets.GRAFANA_TOKEN }}" \
-H "Content-Type: application/json" \
-d @monitoring/dashboard.json
# 多層次快取策略
- name: Cache node modules
uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Cache Python packages
uses: actions/cache@v3
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Cache Docker layers
uses: actions/cache@v3
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
# 並行執行不相依的工作
jobs:
lint-python:
runs-on: ubuntu-latest
# ... Python linting
lint-javascript:
runs-on: ubuntu-latest
# ... JavaScript linting
test-unit:
runs-on: ubuntu-latest
# ... Unit tests
test-integration:
runs-on: ubuntu-latest
needs: [lint-python, lint-javascript]
# ... Integration tests
build-docker:
runs-on: ubuntu-latest
needs: [test-unit]
# ... Docker build
# .github/workflows/security.yml
name: Security Scan
on:
push:
pull_request:
schedule:
- cron: '0 2 * * *' # 每日凌晨 2 點執行
jobs:
secret-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Run TruffleHog
uses: trufflesecurity/trufflehog@main
with:
path: ./
base: main
head: HEAD
- name: Run GitLeaks
uses: zricethezav/gitleaks-action@master
dependency-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run Snyk
uses: snyk/actions/python@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
- name: Upload SARIF file
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: snyk.sarif
# 確保部署符合合規要求
compliance-check:
runs-on: ubuntu-latest
steps:
- name: Check deployment window
run: |
# 檢查是否在允許的部署時間窗口
CURRENT_HOUR=$(date +%H)
CURRENT_DAY=$(date +%u)
# 週一到週五,早上 9 點到下午 5 點允許部署
if [ $CURRENT_DAY -ge 1 ] && [ $CURRENT_DAY -le 5 ] &&
[ $CURRENT_HOUR -ge 9 ] && [ $CURRENT_HOUR -le 17 ]; then
echo "✅ 在允許的部署時間窗口內"
else
echo "❌ 超出允許的部署時間窗口"
exit 1
fi
- name: Verify approval
if: github.ref == 'refs/heads/main'
run: |
# 檢查是否有必要的審批
APPROVALS=$(gh pr view ${{ github.event.number }} --json reviews --jq '.reviews | length')
if [ $APPROVALS -lt 2 ]; then
echo "❌ 需要至少 2 個審批"
exit 1
fi
今天我們深入學習了 GitHub Actions 的完整 CI/CD 體系,就像建立了一個現代化的自動化生產線。重要概念包括:
明天我們將學習 GitHub Environment 的設定,了解如何管理不同環境的配置和權限!
下一篇:Day 14 - Github Setup Environment