← Back to All Articles 2026 UPDATED

GitHub Actions is the most popular CI/CD platform with over 90 million workflow runs daily. This comprehensive guide covers workflow syntax, advanced deployment patterns, caching strategies, matrix builds, and enterprise DevOps automation with production-ready examples.

πŸš€ Getting Started with GitHub Actions

GitHub Actions is GitHub's built-in CI/CD and automation platform. It allows you to define workflows as code in YAML files stored in your repository, trigger them on events (push, pull request, release, schedule), and execute jobs on GitHub-hosted or self-hosted runners.

Core Concepts

Workflow An automated process defined in a YAML file in .github/workflows/
Job A set of steps that execute on the same runner. Jobs can run in parallel or sequentially
Step Individual tasks within a job (shell commands or actions)
Action Reusable unit of code (can be from marketplace or custom)
Runner Server that executes workflow jobs (GitHub-hosted or self-hosted)
Event Trigger that starts a workflow (push, PR, schedule, manual, etc.)

Your First Workflow

Create .github/workflows/ci.yml in your repository:

name: CI Pipeline

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

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Run tests
        run: npm test

Workflow Syntax Deep Dive

Understanding the full workflow syntax enables complex automation scenarios:

Trigger Events

on:
  # Multiple events
  push:
    branches: [main]
    tags: ['v*']
  pull_request:
    paths:
      - 'src/**'
      - 'tests/**'
  schedule:
    - cron: '0 2 * * 1-5'  # 2 AM weekdays
  workflow_dispatch:  # Manual trigger
    inputs:
      environment:
        description: 'Deploy environment'
        required: true
        default: 'staging'

Environment Variables

env:
  NODE_ENV: production
  API_URL: https://api.example.com

jobs:
  build:
    env:
      NODE_ENV: development
    steps:
      - name: Debug env
        run: echo "Node env is $NODE_ENV"

Conditional Execution

steps:
  - name: Deploy to production
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    run: ./deploy.sh production
  
  - name: Notify on failure
    if: failure()
    run: ./notify.sh

πŸ”„ CI Pipeline Patterns

Multi-Stage Testing Pipeline

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

  type-check:
    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 type-check

  test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:15
        env:
          POSTGRES_PASSWORD: testpass
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      - run: npm ci
      - run: npm test
        env:
          DATABASE_URL: postgres://postgres:testpass@localhost:5432/test

  build:
    runs-on: ubuntu-latest
    needs: [lint, type-check, test]  # Depends on all previous jobs
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      - run: npm ci
      - run: npm run build

Docker Build & Push

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
      
      - name: Login to Container Registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      
      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: |
            ghcr.io/${{ github.repository }}:latest
            ghcr.io/${{ github.repository }}:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

πŸš€ CD & Deployment Strategies

Environment-Based Deployment

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment:
      name: ${{ github.ref == 'refs/heads/main' && 'production' || 'staging' }}
    steps:
      - uses: actions/checkout@v4
      
      - name: Deploy
        run: |
          echo "Deploying to ${{ github.ref }}"
          ./deploy.sh ${{ github.ref_name }}
        env:
          DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}

Blue-Green Deployment

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Deploy to blue environment
        run: |
          # Deploy to blue (idle) environment
          ./deploy.sh blue
          
          # Run smoke tests
          ./smoke-tests.sh blue
          
          # Switch traffic
          ./switch-traffic.sh blue green
          
          # Keep old version running briefly for rollback
          sleep 300
          
          # Decommission old (green) version
          ./decommission.sh green

Canary Deployment

jobs:
  canary-deploy:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Deploy canary (10% traffic)
        run: ./deploy.sh --weight=10 --version=${{ github.sha }}
      
      - name: Monitor canary metrics
        run: |
          echo "Monitoring for 10 minutes..."
          sleep 600
          
          # Check error rates
          ERROR_RATE=$(./check-metrics.sh --metric=error_rate)
          if (( $(echo "$ERROR_RATE > 0.05" | bc -l) )); then
            echo "Error rate too high, rolling back"
            ./rollback.sh
            exit 1
          fi
          
          # Promote to 100%
          ./deploy.sh --weight=100 --version=${{ github.sha }}

πŸ’Ύ Caching Strategies

Caching dramatically speeds up workflows. GitHub Actions provides a cache action for this purpose.

npm Cache

steps:
  - uses: actions/checkout@v4
  
  - name: Setup Node
    uses: actions/setup-node@v4
    with:
      node-version: '20'
      cache: 'npm'  # Built-in npm caching
  
  - run: npm ci

pip Cache (Python)

steps:
  - uses: actions/checkout@v4
  
  - name: Setup Python
    uses: actions/setup-python@v5
    with:
      python-version: '3.11'
      cache: 'pip'
  
  - run: pip install -r requirements.txt

Build Cache with Docker Layer Caching

steps:
  - uses: actions/checkout@v4
  
  - name: Build Docker image
    uses: docker/build-push-action@v5
    with:
      context: .
      push: false
      load: true
      tags: myapp:${{ github.sha }}
      cache-from: type=gha
      cache-to: type=gha,mode=max

Restore Cache Manually

steps:
  - uses: actions/checkout@v4
  
  - name: Cache dependencies
    uses: actions/cache@v4
    id: cache-dependencies
    with:
      path: |
        node_modules
        .venv
      key: ${{ runner.os }}-deps-${{ hashFiles('**/package-lock.json', '**/requirements.txt') }}
      restore-keys: |
        ${{ runner.os }}-deps-
  
  - name: Install dependencies
    if: steps.cache-dependencies.outputs.cache-hit != 'true'
    run: npm ci && npm run build

πŸ“Š Matrix Builds

Matrix builds let you test across multiple configurations in parallel:

Multi-Version Testing

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [18, 20, 22]
        operating-system: [ubuntu-latest, windows-latest]
        exclude:
          - node-version: 22
            operating-system: windows-latest  # Node 22 doesn't support Windows
  
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Node.js ${{ matrix.node-version }} on ${{ matrix.operating-system }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
      
      - name: Install and test
        run: |
          npm ci
          npm test

Multi-Database Testing

jobs:
  integration-tests:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        db: [postgres:14, postgres:15, mysql:8, mariadb:10.11]
    
    services:
      database:
        image: ${{ matrix.db }}
        env:
          MYSQL_ROOT_PASSWORD: testpass
          POSTGRES_PASSWORD: testpass
        options: >-
          --health-cmd pg_isready || mysqladmin ping
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 5432:5432
          - 3306:3306
    
    steps:
      - uses: actions/checkout@v4
      - name: Run tests
        run: npm run integration-tests
        env:
          DATABASE_URL: ${{ startsWith(matrix.db, 'postgres') && 'postgres://postgres:testpass@localhost:5432/test' || 'mysql://root:testpass@localhost:3306/test' }}

πŸ”’ Security Best Practices

⚠️ Important: Never log secrets or environment variables containing sensitive data. GitHub Actions automatically redacts secrets in workflow logs, but be cautious with custom logging.

Using Secrets

steps:
  - name: Deploy with secrets
    env:
      API_KEY: ${{ secrets.API_KEY }}
      DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
    run: ./deploy.sh production

Encrypting Sensitive Data

# Using GPG to decrypt secrets at runtime
steps:
  - name: Decrypt secrets
    env:
      GPG_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
    run: |
      echo "$GPG_KEY" | gpg --decrypt --quiet > secrets.json
      # Use secrets.json in subsequent steps
  
  - name: Deploy
    env:
      SECRETS_FILE: secrets.json
    run: ./deploy.sh production

Limiting Permissions

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

jobs:
  deploy:
    permissions:
      contents: read
      id-token: write  # Only what's needed for this job
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
      - name: Deploy via OIDC
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789:role/github-actions-role
          aws-region: us-east-1

Preventing Secret Leaks

# Add to workflow
- name: Prevent secret leaks
  run: |
    # Fail workflow if secrets are printed
    echo "::add-matcher::.github/secret-leak-matcher.json"

# .github/secret-leak-matcher.json
{
  "problemMatcher": [
    {
      "pattern": [
        {
          "regexp": "(?i)(secrets?|token|key|password|passwd|api.?key)",
          "filePath": "test.txt"
        }
      ],
      "severity": "error"
    }
  ]
}

🏒 Enterprise Patterns

Reusable Workflows

Create reusable deployment workflows in a central repository:

# .github/workflows/deploy.yml (in centralized repo)
on:
  workflow_call:
    inputs:
      environment:
        required: true
        type: string
    secrets:
      DEPLOY_TOKEN:
        required: true

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: ${{ inputs.environment }}
    steps:
      - uses: actions/checkout@v4
      
      - name: Deploy
        run: ./deploy.sh ${{ inputs.environment }}
        env:
          DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}

Call it from other repos:

# In consuming repository
jobs:
  deploy-staging:
    uses: acme-corp/deployment-workflows/.github/workflows/deploy.yml@main
    with:
      environment: staging
    secrets:
      DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}

OpenID Connect (OIDC) for Cloud Auth

# Configure AWS OIDC
- name: Configure AWS credentials
  uses: aws-actions/configure-aws-credentials@v4
  with:
    role-to-assume: arn:aws:iam::ACCOUNT_ID:role/GitHubActionsRole
    aws-region: us-east-1

# No long-lived secrets neededβ€”the token is temporary

πŸ’‘ Our Verdict

GitHub Actions: The CI/CD Standard

Best for: Teams already using GitHub, open source projects, startups needing free CI/CD

Key strengths: Native GitHub integration, generous free tier, massive marketplace

Consider alternatives: For very complex enterprise needs, Jenkins or CircleCI may offer more advanced features

GitHub Actions has become the de facto standard for CI/CD on GitHub repositories. The combination of native integration, generous free tier (2,000 minutes/month for private repos, unlimited for public), and extensive marketplace makes it the default choice for most teams.

πŸš€ Automate Your Deployments Today

GitHub Actions is free for open source and has generous limits for teams

Explore GitHub Actions

Disclosure: This guide contains affiliate links. We earn commissions from qualifying purchases at no additional cost to you.