正在加载,请稍候…

Advanced CI/CD with GitHub Actions: Secrets, Environments, Matrix Builds, and Reusable Workflows

Master GitHub Actions beyond basics: environment secrets, deployment gates, matrix strategies, reusable workflows, caching, OIDC authentication, and building a production-grade CI/CD pipeline.

Moving Beyond Basic GitHub Actions

Most GitHub Actions tutorials cover the same ground: a workflow that runs tests on push, maybe builds a Docker image. But production pipelines need much more: environment-based deployment gates, secure secret management, efficient caching, and maintainable workflow organization.

This guide covers the patterns that make GitHub Actions work at scale.

Environments and Deployment Gates

Environments add protection rules to your deployments:

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

on:
  push:
    branches: [main]

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

  deploy-staging:
    needs: test
    runs-on: ubuntu-latest
    environment: staging    # Uses staging environment secrets
    steps:
    - name: Deploy to staging
      run: ./deploy.sh staging
      env:
        DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}   # From staging environment

  deploy-production:
    needs: deploy-staging
    runs-on: ubuntu-latest
    environment: production  # Can require manual approval
    # Production environment can be configured with:
    # - Required reviewers (must approve before job runs)
    # - Wait timer (delay before deployment)
    # - Branch restrictions (only from main)
    steps:
    - name: Deploy to production
      run: ./deploy.sh production
      env:
        DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}   # From production environment

In GitHub Settings → Environments, configure:

  • Required reviewers: Named people who must approve
  • Wait timer: Delay (useful for canary monitoring periods)
  • Deployment branches: Only specific branches can deploy here

OIDC Authentication (No Long-Lived Secrets)

Instead of storing cloud credentials as GitHub secrets, use OIDC to get temporary tokens:

name: Deploy with OIDC

on:
  push:
    branches: [main]

permissions:
  id-token: write   # Required for OIDC
  contents: read

jobs:
  deploy-aws:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    
    # Get temporary AWS credentials via OIDC (no stored AWS secrets!)
    - name: Configure AWS Credentials
      uses: aws-actions/configure-aws-credentials@v4
      with:
        role-to-assume: arn:aws:iam::123456789:role/github-actions-deploy
        aws-region: us-east-1
        # No access key or secret key needed!
    
    - name: Deploy to ECS
      run: aws ecs update-service --cluster prod --service my-app --force-new-deployment

  deploy-gcp:
    runs-on: ubuntu-latest
    steps:
    - uses: google-github-actions/auth@v2
      with:
        workload_identity_provider: projects/123/locations/global/workloadIdentityPools/my-pool/providers/my-provider
        service_account: deploy@my-project.iam.gserviceaccount.com
    
    - run: gcloud run deploy my-service --image gcr.io/my-project/app:${{ github.sha }}

Matrix Strategies

Run jobs across multiple combinations:

jobs:
  test:
    strategy:
      fail-fast: false       # Don't cancel other matrix jobs on failure
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
        node: [18, 20, 22]
        exclude:
        - os: macos-latest
          node: 18            # Don't test Node 18 on macOS
        include:
        - os: ubuntu-latest
          node: 20
          experimental: true  # Add extra variable for specific combo
    
    runs-on: ${{ matrix.os }}
    
    steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-node@v4
      with:
        node-version: ${{ matrix.node }}
    - run: npm test

  build:
    strategy:
      matrix:
        platform: [linux/amd64, linux/arm64]  # Multi-arch Docker builds
    steps:
    - uses: docker/setup-buildx-action@v3
    - uses: docker/build-push-action@v5
      with:
        platforms: ${{ matrix.platform }}
        tags: my-app:${{ github.sha }}-${{ matrix.platform }}

Caching: Making Workflows Fast

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    
    # Node.js — cache node_modules
    - uses: actions/setup-node@v4
      with:
        node-version: '20'
        cache: 'npm'          # Built-in caching for npm/yarn/pnpm
    
    - run: npm ci             # Uses cache on subsequent runs
    
    # Explicit cache for more control
    - name: Cache Next.js build
      uses: actions/cache@v4
      with:
        path: |
          .next/cache
          ~/.npm
        key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**/*.ts', '**/*.tsx') }}
        restore-keys: |
          ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-
          ${{ runner.os }}-nextjs-
    
    # Docker layer caching with GitHub Actions cache
    - uses: docker/setup-buildx-action@v3
    - uses: docker/build-push-action@v5
      with:
        context: .
        cache-from: type=gha          # Read from GitHub Actions cache
        cache-to: type=gha,mode=max   # Write to GitHub Actions cache
        push: true
        tags: my-app:${{ github.sha }}

Reusable Workflows

DRY principle for workflows — define once, call from multiple workflows:

# .github/workflows/reusable-deploy.yml
name: Reusable Deploy

on:
  workflow_call:
    inputs:
      environment:
        required: true
        type: string
      image-tag:
        required: true
        type: string
    secrets:
      DEPLOY_TOKEN:
        required: true

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: ${{ inputs.environment }}
    steps:
    - name: Deploy ${{ inputs.image-tag }} to ${{ inputs.environment }}
      run: |
        curl -X POST https://deploy.example.com/deploy           -H "Authorization: Bearer ${{ secrets.DEPLOY_TOKEN }}"           -d '{"image": "${{ inputs.image-tag }}", "env": "${{ inputs.environment }}"}'
# .github/workflows/main.yml
name: Main Pipeline

on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    outputs:
      image-tag: ${{ steps.meta.outputs.tags }}
    steps:
    - id: meta
      uses: docker/metadata-action@v5
      with:
        images: my-app
        tags: type=sha
    
    - uses: docker/build-push-action@v5
      with:
        tags: ${{ steps.meta.outputs.tags }}
        push: true
  
  deploy-staging:
    needs: build
    uses: ./.github/workflows/reusable-deploy.yml    # Call reusable workflow
    with:
      environment: staging
      image-tag: ${{ needs.build.outputs.image-tag }}
    secrets: inherit    # Pass all secrets through
  
  deploy-production:
    needs: deploy-staging
    uses: ./.github/workflows/reusable-deploy.yml
    with:
      environment: production
      image-tag: ${{ needs.build.outputs.image-tag }}
    secrets: inherit

Composite Actions

For reusable step sequences within a workflow:

# .github/actions/setup-app/action.yml
name: Setup Application
description: Install dependencies and build the app

inputs:
  node-version:
    description: Node.js version
    default: '20'
  environment:
    description: Build environment
    required: true

runs:
  using: composite
  steps:
  - uses: actions/setup-node@v4
    with:
      node-version: ${{ inputs.node-version }}
      cache: npm
  
  - run: npm ci
    shell: bash
  
  - run: npm run build
    shell: bash
    env:
      NODE_ENV: ${{ inputs.environment }}
  
  - run: npm test -- --coverage
    shell: bash
# Use the composite action
- uses: ./.github/actions/setup-app
  with:
    node-version: '20'
    environment: production

Advanced Job Dependencies and Conditions

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
    - run: npm run lint

  test:
    runs-on: ubuntu-latest
    steps:
    - run: npm test

  build:
    needs: [lint, test]    # Wait for both to pass
    if: github.ref == 'refs/heads/main'   # Only on main branch
    runs-on: ubuntu-latest
    steps:
    - run: npm run build

  notify-failure:
    needs: [build]
    if: failure()          # Run if any previous job failed
    runs-on: ubuntu-latest
    steps:
    - name: Notify team on Slack
      uses: slackapi/slack-github-action@v1
      with:
        payload: |
          {
            "text": "🚨 Build failed: ${{ github.repository }}@${{ github.sha }}"
          }
      env:
        SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}

  cleanup:
    needs: [build]
    if: always()           # Run regardless of success/failure
    runs-on: ubuntu-latest
    steps:
    - run: ./cleanup.sh

Workflow Security Best Practices

# Principle of least privilege for permissions
permissions:
  contents: read           # Default — restrict further in jobs
  packages: write          # Only if pushing to ghcr.io
  id-token: write          # Only if using OIDC
  # Don't give pull-requests: write unless explicitly needed

# Pin action versions to exact SHAs (not @main or @v1)
# ❌ Vulnerable to supply chain attacks:
- uses: some-action/action@main

# ✅ Pinned to specific commit:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11  # v4.1.1

# Validate inputs for PR-triggered workflows (untrusted code)
- name: Validate PR title
  run: |
    TITLE="${{ github.event.pull_request.title }}"
    # Sanitize — don't use PR content directly in shell commands
    echo "PR title length: ${#TITLE}"

# Use GITHUB_TOKEN with minimal scope
env:
  GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

A well-designed CI/CD pipeline is invisible in day-to-day development — it just works, runs fast, and deploys reliably. The investment in patterns like OIDC auth, reusable workflows, and proper caching pays dividends over the months and years of a project's lifetime.

→ Generate UUIDs for unique identifiers in your workflows with the UUID Generator.