GitHub Actions CI/CD Complete Guide

📚 Official Docs ← Docs 2024+
1

Basic Building Blocks and Components

Fundamentals

GitHub Actions is a CI/CD platform that automates your build, test, and deployment pipeline. Workflows are defined in YAML files stored in .github/workflows/.

Core Components

🔄 Workflow

An automated process defined in YAML. Can contain one or more jobs.

⚡ Event

Triggers that start a workflow (push, pull_request, schedule, etc.)

💼 Job

A set of steps that execute on the same runner. Jobs run in parallel by default.

📝 Step

Individual task that runs commands or actions. Steps run sequentially.

🎬 Action

Reusable unit of code. Can be from marketplace or custom-built.

🖥️ Runner

Server that runs your workflows. Can be GitHub-hosted or self-hosted.

Basic Workflow Structure

yaml
name: CI Pipeline

on: [push, pull_request]

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

Workflow File Location

text
my-repo/
├── .github/
│   └── workflows/
│       ├── ci.yml
│       ├── deploy.yml
│       └── release.yml
├── src/
└── package.json
💡 Best Practice: Use descriptive workflow names and organize them by purpose (CI, deployment, release, etc.). Keep workflows focused on a single responsibility.
2

Workflows and Events Deep Dive

Events

Events are specific activities that trigger workflows. GitHub Actions supports dozens of event types for different scenarios.

Common Trigger Events

yaml
# Single event
on: push

# Multiple events
on: [push, pull_request, workflow_dispatch]

# Event with filters
on:
  push:
    branches:
      - main
      - 'releases/**'
    paths:
      - 'src/**'
      - '!src/docs/**'
    tags:
      - v1.*
  
  pull_request:
    branches:
      - main
    types: [opened, synchronize, reopened]

# Scheduled workflows (cron)
on:
  schedule:
    - cron: '0 2 * * *'  # Daily at 2 AM UTC
    - cron: '0 */6 * * *'  # Every 6 hours

# Manual trigger
on:
  workflow_dispatch:
    inputs:
      environment:
        description: 'Environment to deploy'
        required: true
        default: 'staging'
        type: choice
        options:
          - staging
          - production
      debug_enabled:
        description: 'Enable debug mode'
        required: false
        type: boolean

Event Types Reference

Code Events

  • push - Code pushed to repo
  • pull_request - PR opened/updated
  • pull_request_target - PR from fork
  • create - Branch/tag created
  • delete - Branch/tag deleted

Issue & PR Events

  • issues - Issue activity
  • issue_comment - Comment added
  • pull_request_review - PR reviewed
  • pull_request_review_comment

Release Events

  • release - Release published
  • workflow_run - After workflow
  • repository_dispatch - External

Other Events

  • schedule - Cron-based
  • workflow_dispatch - Manual
  • workflow_call - Reusable

Activity Types

yaml
on:
  pull_request:
    types:
      - opened        # PR first opened
      - synchronize   # New commits pushed
      - reopened      # Closed PR reopened
      - closed        # PR closed/merged
      - labeled       # Label added
      - unlabeled     # Label removed
      - assigned      # Assignee added
  
  issues:
    types: [opened, edited, deleted, closed, reopened]
  
  release:
    types: [published, created, edited, deleted]
⚠️ Warning: Be careful with pull_request_target — it runs in the context of the base repository and has access to secrets, even for PRs from forks. Only use it when necessary and validate inputs carefully.
3

Job Artifacts and Outputs

Data Flow

Artifacts allow you to persist data after a job completes and share data between jobs. Outputs let you pass data between jobs in the same workflow.

Uploading Artifacts

yaml
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Build application
        run: npm run build
      
      - name: Upload build artifacts
        uses: actions/upload-artifact@v4
        with:
          name: build-output
          path: |
            dist/
            build/
          retention-days: 7
      
      - name: Upload test results
        if: always()  # Upload even if tests fail
        uses: actions/upload-artifact@v4
        with:
          name: test-results
          path: test-results/
          if-no-files-found: warn

Downloading Artifacts

yaml
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Build
        run: npm run build
      - uses: actions/upload-artifact@v4
        with:
          name: dist
          path: dist/
  
  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: Download build artifacts
        uses: actions/download-artifact@v4
        with:
          name: dist
          path: ./dist
      
      - name: Deploy
        run: ./deploy.sh

Job Outputs

yaml
jobs:
  build:
    runs-on: ubuntu-latest
    outputs:
      version: ${{ steps.get_version.outputs.version }}
      build_number: ${{ steps.build.outputs.number }}
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Get version
        id: get_version
        run: |
          VERSION=$(node -p "require('./package.json').version")
          echo "version=$VERSION" >> $GITHUB_OUTPUT
      
      - name: Build
        id: build
        run: |
          npm run build
          BUILD_NUM=$(date +%Y%m%d%H%M%S)
          echo "number=$BUILD_NUM" >> $GITHUB_OUTPUT
  
  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: Deploy version
        run: |
          echo "Deploying version ${{ needs.build.outputs.version }}"
          echo "Build number: ${{ needs.build.outputs.build_number }}"

Matrix Outputs

yaml
jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
    outputs:
      # Outputs from matrix jobs need special handling
      test-results: ${{ steps.test.outputs.results }}
    steps:
      - name: Run tests
        id: test
        run: npm test
💡 Tip: Artifacts are stored for 90 days by default (configurable with retention-days). They count against your storage quota. Clean up old artifacts regularly or use shorter retention periods for temporary data.
4

Using Environment Variables and Secrets

Configuration

Environment variables and secrets allow you to configure workflows without hardcoding values. Secrets are encrypted and should be used for sensitive data.

Environment Variables

yaml
# Workflow-level env vars
env:
  NODE_VERSION: '20'
  APP_NAME: 'my-app'

jobs:
  build:
    runs-on: ubuntu-latest
    # Job-level env vars
    env:
      BUILD_ENV: production
      DEBUG: false
    
    steps:
      - uses: actions/checkout@v4
      
      # Step-level env vars
      - name: Build
        env:
          API_URL: https://api.example.com
        run: |
          echo "Building $APP_NAME"
          echo "Node version: $NODE_VERSION"
          echo "Environment: $BUILD_ENV"
          npm run build
      
      # Using env vars in expressions
      - name: Deploy
        if: env.BUILD_ENV == 'production'
        run: npm run deploy

Default Environment Variables

yaml
steps:
  - name: Print GitHub context
    run: |
      echo "Repository: $GITHUB_REPOSITORY"
      echo "Branch: $GITHUB_REF_NAME"
      echo "Commit SHA: $GITHUB_SHA"
      echo "Actor: $GITHUB_ACTOR"
      echo "Workflow: $GITHUB_WORKFLOW"
      echo "Run ID: $GITHUB_RUN_ID"
      echo "Run Number: $GITHUB_RUN_NUMBER"
      echo "Event: $GITHUB_EVENT_NAME"

Using Secrets

yaml
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Deploy to AWS
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          AWS_REGION: ${{ secrets.AWS_REGION }}
        run: |
          aws s3 sync ./dist s3://my-bucket
      
      - name: Notify Slack
        uses: slackapi/slack-github-action@v1
        with:
          webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
          payload: |
            {
              "text": "Deployment completed!"
            }

GitHub Context Variables

yaml
steps:
  - name: Use GitHub context
    run: |
      echo "Event: ${{ github.event_name }}"
      echo "Ref: ${{ github.ref }}"
      echo "SHA: ${{ github.sha }}"
      echo "Actor: ${{ github.actor }}"
      echo "Repository: ${{ github.repository }}"
      echo "Repository Owner: ${{ github.repository_owner }}"
      echo "Head Ref: ${{ github.head_ref }}"
      echo "Base Ref: ${{ github.base_ref }}"
  
  - name: PR information
    if: github.event_name == 'pull_request'
    run: |
      echo "PR Number: ${{ github.event.pull_request.number }}"
      echo "PR Title: ${{ github.event.pull_request.title }}"
      echo "PR Author: ${{ github.event.pull_request.user.login }}"

Setting Up Secrets

  1. Go to your repository on GitHub
  2. Click SettingsSecrets and variablesActions
  3. Click New repository secret
  4. Enter name and value, then click Add secret
🔒 Security: Never log secrets or use them in URLs. GitHub automatically masks secrets in logs, but be cautious. Use environment-specific secrets and rotate them regularly.
5

Controlling Workflow and Job Execution

Control Flow

Control when and how jobs and steps run using conditions, dependencies, and concurrency controls.

Job Dependencies

yaml
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - run: npm run build
  
  test:
    needs: build  # Wait for build to complete
    runs-on: ubuntu-latest
    steps:
      - run: npm test
  
  deploy-staging:
    needs: [build, test]  # Wait for multiple jobs
    runs-on: ubuntu-latest
    steps:
      - run: ./deploy-staging.sh
  
  deploy-production:
    needs: deploy-staging
    runs-on: ubuntu-latest
    steps:
      - run: ./deploy-production.sh

Conditional Execution

yaml
jobs:
  deploy:
    runs-on: ubuntu-latest
    # Job-level condition
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    
    steps:
      - uses: actions/checkout@v4
      
      # Step runs only on success (default)
      - name: Build
        run: npm run build
      
      # Step runs even if previous steps failed
      - name: Upload logs
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: logs
          path: logs/
      
      # Step runs only on failure
      - name: Notify on failure
        if: failure()
        run: ./notify-failure.sh
      
      # Step runs only on success
      - name: Deploy
        if: success()
        run: ./deploy.sh
      
      # Step runs only if job was cancelled
      - name: Cleanup
        if: cancelled()
        run: ./cleanup.sh
      
      # Complex conditions
      - name: Deploy to production
        if: |
          github.ref == 'refs/heads/main' &&
          github.event_name == 'push' &&
          !contains(github.event.head_commit.message, '[skip ci]')
        run: ./deploy-prod.sh

Concurrency Control

yaml
# Workflow-level concurrency
name: Deploy

on:
  push:
    branches: [main]

concurrency:
  group: production-deploy
  cancel-in-progress: false  # Don't cancel running deployments

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - run: ./deploy.sh

---
# Per-branch concurrency
name: CI

on: [push, pull_request]

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true  # Cancel old runs for same branch

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

Timeouts and Continue on Error

yaml
jobs:
  test:
    runs-on: ubuntu-latest
    timeout-minutes: 30  # Job timeout
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Run tests
        timeout-minutes: 10  # Step timeout
        run: npm test
      
      - name: Optional linting
        continue-on-error: true  # Don't fail job if this fails
        run: npm run lint
      
      - name: Deploy
        run: ./deploy.sh

Status Check Functions

Condition Functions

  • success() - All previous steps succeeded
  • failure() - Any previous step failed
  • cancelled() - Workflow was cancelled
  • always() - Always run

Expression Functions

  • contains() - String/array contains
  • startsWith() - String starts with
  • endsWith() - String ends with
  • format() - Format string
  • join() - Join array
💡 Tip: Use concurrency to prevent multiple deployments from running simultaneously or to cancel outdated CI runs when new commits are pushed.
6

Jobs and Docker Containers

Containers

Run jobs inside Docker containers for consistent environments or use service containers for dependencies like databases.

Running Jobs in Containers

yaml
jobs:
  test:
    runs-on: ubuntu-latest
    container:
      image: node:20-alpine
      env:
        NODE_ENV: test
      options: --cpus 2 --memory 4g
    
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm test

Service Containers

yaml
jobs:
  test:
    runs-on: ubuntu-latest
    
    services:
      postgres:
        image: postgres:15
        env:
          POSTGRES_PASSWORD: postgres
          POSTGRES_DB: testdb
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 5432:5432
      
      redis:
        image: redis:7-alpine
        options: >-
          --health-cmd "redis-cli ping"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 6379:6379
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Run tests
        env:
          DATABASE_URL: postgresql://postgres:postgres@localhost:5432/testdb
          REDIS_URL: redis://localhost:6379
        run: npm test

Using Private Container Registry

yaml
jobs:
  build:
    runs-on: ubuntu-latest
    container:
      image: ghcr.io/myorg/myimage:latest
      credentials:
        username: ${{ github.actor }}
        password: ${{ secrets.GITHUB_TOKEN }}
    
    steps:
      - run: echo "Running in private container"

Building and Pushing Docker Images

yaml
jobs:
  docker:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
      
      - name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}
      
      - name: Login to GitHub 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: |
            myorg/myapp:latest
            myorg/myapp:${{ github.sha }}
            ghcr.io/${{ github.repository }}:latest
          cache-from: type=gha
          cache-to: type=gha,mode=max
🐳 Note: Service containers are only available on Linux runners. For Windows/macOS, you'll need to install and run services directly in steps.
7

Building and Using Custom Actions

Actions

Create reusable actions to share logic across workflows or with the community. Actions can be JavaScript, Docker, or composite (shell scripts).

Types of Actions

JavaScript Actions

Fast, cross-platform. Run directly on runner. Best for most use cases.

Docker Actions

Consistent environment. Linux only. Good for specific dependencies.

Composite Actions

Combine multiple steps. Simple, no code required. Great for reusing workflow patterns.

Composite Action Example

yaml
# .github/actions/setup-node-cache/action.yml
name: 'Setup Node with Cache'
description: 'Setup Node.js and cache dependencies'

inputs:
  node-version:
    description: 'Node.js version'
    required: false
    default: '20'
  
  working-directory:
    description: 'Working directory'
    required: false
    default: '.'

outputs:
  cache-hit:
    description: 'Whether cache was hit'
    value: ${{ steps.cache.outputs.cache-hit }}

runs:
  using: 'composite'
  steps:
    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: ${{ inputs.node-version }}
    
    - name: Cache dependencies
      id: cache
      uses: actions/cache@v3
      with:
        path: ${{ inputs.working-directory }}/node_modules
        key: ${{ runner.os }}-node-${{ hashFiles(format('{0}/package-lock.json', inputs.working-directory)) }}
    
    - name: Install dependencies
      if: steps.cache.outputs.cache-hit != 'true'
      shell: bash
      working-directory: ${{ inputs.working-directory }}
      run: npm ci

Using Custom Composite Action

yaml
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Node with caching
        uses: ./.github/actions/setup-node-cache
        with:
          node-version: '20'
          working-directory: './frontend'
      
      - run: npm run build

JavaScript Action Structure

yaml
# action.yml
name: 'Hello World'
description: 'Greet someone'
inputs:
  who-to-greet:
    description: 'Who to greet'
    required: true
    default: 'World'
outputs:
  time:
    description: 'The time we greeted you'
runs:
  using: 'node20'
  main: 'dist/index.js'
javascript
// index.js
const core = require('@actions/core');
const github = require('@actions/github');

try {
  const nameToGreet = core.getInput('who-to-greet');
  console.log(`Hello ${nameToGreet}!`);
  
  const time = (new Date()).toTimeString();
  core.setOutput('time', time);
  
  // Get the JSON webhook payload
  const payload = JSON.stringify(github.context.payload, undefined, 2);
  console.log(`The event payload: ${payload}`);
} catch (error) {
  core.setFailed(error.message);
}

Docker Action Example

yaml
# action.yml
name: 'Container Action'
description: 'Run in Docker container'
inputs:
  myInput:
    description: 'Input parameter'
    required: true
runs:
  using: 'docker'
  image: 'Dockerfile'
  args:
    - ${{ inputs.myInput }}
dockerfile
# Dockerfile
FROM alpine:3.18

COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

ENTRYPOINT ["/entrypoint.sh"]
💡 Best Practice: Use composite actions for simple reusable workflows. Use JavaScript actions for complex logic. Use Docker actions only when you need specific system dependencies.
8

Security and Permissions

Security

Secure your workflows with proper permissions, secret management, and security best practices.

GITHUB_TOKEN Permissions

yaml
# Workflow-level permissions (applies to all jobs)
permissions:
  contents: read
  pull-requests: write
  issues: write

jobs:
  build:
    runs-on: ubuntu-latest
    # Job-level permissions (overrides workflow-level)
    permissions:
      contents: read
      packages: write
    
    steps:
      - uses: actions/checkout@v4
      - run: npm run build

---
# Minimal permissions (recommended default)
permissions:
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      deployments: write
      id-token: write  # For OIDC
    steps:
      - run: ./deploy.sh

Available Permission Scopes

Common Permissions

  • contents - Repository contents
  • pull-requests - PRs
  • issues - Issues
  • packages - GitHub Packages
  • deployments - Deployments

Advanced Permissions

  • id-token - OIDC token
  • actions - Workflow runs
  • checks - Check runs
  • statuses - Commit statuses
  • security-events - Code scanning

Environment Protection Rules

yaml
jobs:
  deploy-staging:
    runs-on: ubuntu-latest
    environment:
      name: staging
      url: https://staging.example.com
    steps:
      - run: ./deploy.sh staging
  
  deploy-production:
    needs: deploy-staging
    runs-on: ubuntu-latest
    environment:
      name: production
      url: https://example.com
    steps:
      - run: ./deploy.sh production

Configure environment protection rules in repository settings:

  • Required reviewers - Require approval before deployment
  • Wait timer - Delay deployment by specified time
  • Deployment branches - Restrict which branches can deploy
  • Environment secrets - Secrets specific to environment

OpenID Connect (OIDC) for Cloud Authentication

yaml
jobs:
  deploy-aws:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
          aws-region: us-east-1
      
      - name: Deploy to AWS
        run: |
          aws s3 sync ./dist s3://my-bucket
          aws cloudfront create-invalidation --distribution-id ABCD1234

---
  deploy-azure:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Azure Login
        uses: azure/login@v1
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
      
      - name: Deploy to Azure
        run: az webapp deploy --name myapp --resource-group mygroup

Security Best Practices

🔒 Security Checklist:
  • ✅ Use minimal permissions (permissions: contents: read by default)
  • ✅ Pin actions to full commit SHA, not tags (actions/checkout@8e5e7e5a...)
  • ✅ Never log secrets or use them in URLs
  • ✅ Use OIDC instead of long-lived credentials when possible
  • ✅ Review third-party actions before using them
  • ✅ Use environment protection rules for production deployments
  • ✅ Enable branch protection rules
  • ✅ Use pull_request instead of pull_request_target when possible
  • ✅ Validate and sanitize inputs in custom actions
  • ✅ Use Dependabot to keep actions updated

Script Injection Prevention

yaml
# ❌ VULNERABLE - Don't do this
- name: Print PR title
  run: echo "PR title is ${{ github.event.pull_request.title }}"

# ✅ SAFE - Use environment variables
- name: Print PR title
  env:
    PR_TITLE: ${{ github.event.pull_request.title }}
  run: echo "PR title is $PR_TITLE"

# ✅ SAFE - Use intermediate step output
- name: Get PR title
  id: pr
  run: echo "title=${{ github.event.pull_request.title }}" >> $GITHUB_OUTPUT

- name: Print PR title
  run: echo "PR title is ${{ steps.pr.outputs.title }}"
9

Matrix Builds and Strategy

Scaling

Matrix strategies let you run the same job across multiple configurations in parallel, perfect for testing across different OS, language versions, or configurations.

Basic Matrix

yaml
jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
        node-version: [18, 20, 21]
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
      
      - run: npm ci
      - run: npm test

This creates 9 jobs (3 OS × 3 Node versions) that run in parallel.

Matrix with Include/Exclude

yaml
jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
        node-version: [18, 20]
        
        # Exclude specific combinations
        exclude:
          - os: macos-latest
            node-version: 18
        
        # Add specific combinations with extra properties
        include:
          - os: ubuntu-latest
            node-version: 21
            experimental: true
          
          - os: windows-latest
            node-version: 20
            npm-cache: 'C:\npm-cache'
    
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
      - run: npm test

Matrix with Custom Variables

yaml
jobs:
  build:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        include:
          - target: linux-x64
            os: ubuntu-latest
            artifact: myapp-linux-x64
          
          - target: linux-arm64
            os: ubuntu-latest
            artifact: myapp-linux-arm64
          
          - target: darwin-x64
            os: macos-latest
            artifact: myapp-darwin-x64
          
          - target: win-x64
            os: windows-latest
            artifact: myapp-win-x64.exe
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Build for ${{ matrix.target }}
        run: npm run build -- --target=${{ matrix.target }}
      
      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: ${{ matrix.artifact }}
          path: dist/${{ matrix.artifact }}

Fail-Fast and Continue on Error

yaml
jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      # Don't cancel all jobs if one fails
      fail-fast: false
      
      # Limit concurrent jobs
      max-parallel: 3
      
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
        node-version: [18, 20, 21]
        include:
          - os: ubuntu-latest
            node-version: 22
            experimental: true
    
    # Allow experimental builds to fail
    continue-on-error: ${{ matrix.experimental == true }}
    
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
      - run: npm test

Dynamic Matrix from JSON

yaml
jobs:
  generate-matrix:
    runs-on: ubuntu-latest
    outputs:
      matrix: ${{ steps.set-matrix.outputs.matrix }}
    steps:
      - uses: actions/checkout@v4
      
      - name: Generate matrix
        id: set-matrix
        run: |
          MATRIX=$(jq -c . < .github/test-matrix.json)
          echo "matrix=$MATRIX" >> $GITHUB_OUTPUT
  
  test:
    needs: generate-matrix
    runs-on: ${{ matrix.os }}
    strategy:
      matrix: ${{ fromJSON(needs.generate-matrix.outputs.matrix) }}
    
    steps:
      - run: echo "Testing on ${{ matrix.os }} with ${{ matrix.version }}"
💡 Tip: Use fail-fast: false when you want to see all test results even if some fail. Use max-parallel to limit concurrent jobs and avoid overwhelming external services or hitting rate limits.
10

Caching Dependencies

Performance

Caching speeds up workflows by reusing dependencies and build outputs between runs. GitHub provides 10GB of cache storage per repository.

Basic Caching with actions/cache

yaml
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Cache node modules
        uses: actions/cache@v3
        with:
          path: node_modules
          key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-node-
      
      - name: Install dependencies
        run: npm ci
      
      - name: Build
        run: npm run build

Built-in Caching with Setup Actions

yaml
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      # Node.js with built-in caching
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'  # or 'yarn', 'pnpm'
      
      - run: npm ci
      - run: npm run build

---
  python-build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      # Python with built-in caching
      - uses: actions/setup-python@v5
        with:
          python-version: '3.11'
          cache: 'pip'
      
      - run: pip install -r requirements.txt
      - run: python -m pytest

---
  java-build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      # Java with built-in caching
      - uses: actions/setup-java@v4
        with:
          distribution: 'temurin'
          java-version: '17'
          cache: 'maven'  # or 'gradle'
      
      - run: mvn clean install

Multiple Cache Paths

yaml
- name: Cache dependencies and build
  uses: actions/cache@v3
  with:
    path: |
      ~/.npm
      ~/.cache
      node_modules
      .next/cache
    key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }}
    restore-keys: |
      ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-
      ${{ runner.os }}-nextjs-

Cache Management

yaml
- name: Cache with save/restore
  id: cache
  uses: actions/cache@v3
  with:
    path: node_modules
    key: ${{ runner.os }}-deps-${{ hashFiles('package-lock.json') }}

- name: Install if cache miss
  if: steps.cache.outputs.cache-hit != 'true'
  run: npm ci

# Explicitly save cache (useful for conditional caching)
- name: Save cache
  if: success()
  uses: actions/cache/save@v3
  with:
    path: node_modules
    key: ${{ runner.os }}-deps-${{ hashFiles('package-lock.json') }}

# Restore cache only (don't save)
- name: Restore cache
  uses: actions/cache/restore@v3
  with:
    path: node_modules
    key: ${{ runner.os }}-deps-${{ hashFiles('package-lock.json') }}

Docker Layer Caching

yaml
jobs:
  docker:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
      
      - name: Build with cache
        uses: docker/build-push-action@v5
        with:
          context: .
          push: false
          tags: myapp:latest
          cache-from: type=gha
          cache-to: type=gha,mode=max

Language-Specific Cache Paths

Node.js / npm

  • ~/.npm (Linux/macOS)
  • %AppData%/npm-cache (Windows)
  • node_modules

Python / pip

  • ~/.cache/pip (Linux/macOS)
  • ~\AppData\Local\pip\Cache (Windows)

Ruby / Bundler

  • vendor/bundle
  • ~/.bundle

Go

  • ~/go/pkg/mod
  • ~/.cache/go-build
💡 Best Practices:
  • Use hashFiles() to create cache keys based on dependency files
  • Provide restore-keys for partial cache matches
  • Cache is immutable - changing key creates new cache
  • Caches are scoped to branch and can be accessed by child branches
  • Unused caches are evicted after 7 days
  • Total cache size limit is 10GB per repository
11

Reusable Workflows

DRY

Reusable workflows let you call entire workflows from other workflows, reducing duplication and centralizing common patterns.

Creating a Reusable Workflow

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

on:
  workflow_call:
    inputs:
      environment:
        description: 'Environment to deploy to'
        required: true
        type: string
      
      node-version:
        description: 'Node.js version'
        required: false
        type: string
        default: '20'
      
      dry-run:
        description: 'Perform dry run'
        required: false
        type: boolean
        default: false
    
    secrets:
      deploy-token:
        description: 'Deployment token'
        required: true
      
      api-key:
        description: 'API key'
        required: false
    
    outputs:
      deployment-url:
        description: 'URL of deployment'
        value: ${{ jobs.deploy.outputs.url }}

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: ${{ inputs.environment }}
    outputs:
      url: ${{ steps.deploy.outputs.url }}
    
    steps:
      - uses: actions/checkout@v4
      
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ inputs.node-version }}
      
      - run: npm ci
      - run: npm run build
      
      - name: Deploy
        id: deploy
        env:
          DEPLOY_TOKEN: ${{ secrets.deploy-token }}
          API_KEY: ${{ secrets.api-key }}
          DRY_RUN: ${{ inputs.dry-run }}
        run: |
          if [ "$DRY_RUN" = "true" ]; then
            echo "Dry run mode - skipping actual deployment"
            URL="https://dry-run.example.com"
          else
            ./deploy.sh ${{ inputs.environment }}
            URL="https://${{ inputs.environment }}.example.com"
          fi
          echo "url=$URL" >> $GITHUB_OUTPUT

Calling a Reusable Workflow

yaml
# .github/workflows/deploy-staging.yml
name: Deploy to Staging

on:
  push:
    branches: [develop]

jobs:
  deploy-staging:
    uses: ./.github/workflows/reusable-deploy.yml
    with:
      environment: staging
      node-version: '20'
      dry-run: false
    secrets:
      deploy-token: ${{ secrets.STAGING_DEPLOY_TOKEN }}
      api-key: ${{ secrets.STAGING_API_KEY }}
  
  notify:
    needs: deploy-staging
    runs-on: ubuntu-latest
    steps:
      - name: Notify team
        run: |
          echo "Deployed to ${{ needs.deploy-staging.outputs.deployment-url }}"

---
# .github/workflows/deploy-production.yml
name: Deploy to Production

on:
  release:
    types: [published]

jobs:
  deploy-prod:
    uses: ./.github/workflows/reusable-deploy.yml
    with:
      environment: production
      node-version: '20'
    secrets:
      deploy-token: ${{ secrets.PROD_DEPLOY_TOKEN }}
      api-key: ${{ secrets.PROD_API_KEY }}

Calling Workflows from Other Repositories

yaml
jobs:
  call-workflow:
    uses: octo-org/shared-workflows/.github/workflows/deploy.yml@main
    with:
      environment: production
    secrets:
      token: ${{ secrets.DEPLOY_TOKEN }}

  # Pin to specific version for stability
  call-workflow-pinned:
    uses: octo-org/shared-workflows/.github/workflows/deploy.yml@v1.2.3
    with:
      environment: staging
    secrets: inherit  # Pass all secrets

Matrix Strategy with Reusable Workflows

yaml
jobs:
  generate-matrix:
    runs-on: ubuntu-latest
    outputs:
      matrix: ${{ steps.set-matrix.outputs.matrix }}
    steps:
      - id: set-matrix
        run: echo 'matrix=["staging", "production"]' >> $GITHUB_OUTPUT
  
  deploy:
    needs: generate-matrix
    strategy:
      matrix:
        environment: ${{ fromJSON(needs.generate-matrix.outputs.matrix) }}
    uses: ./.github/workflows/reusable-deploy.yml
    with:
      environment: ${{ matrix.environment }}
    secrets: inherit
📋 Limitations:
  • Can nest reusable workflows up to 4 levels deep
  • Can call maximum of 20 reusable workflows from a single workflow file
  • Environment variables set in env context are not passed to called workflows
  • The GITHUB_TOKEN permissions are passed to the called workflow
12

Monitoring and Debugging

Debugging

Effective debugging and monitoring help you quickly identify and fix workflow issues.

Enable Debug Logging

Set repository secrets to enable detailed logging:

  • ACTIONS_STEP_DEBUG = true - Enable step debug logging
  • ACTIONS_RUNNER_DEBUG = true - Enable runner debug logging

Debugging Techniques

yaml
jobs:
  debug:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      # Print all environment variables
      - name: Dump env
        run: env | sort
      
      # Print GitHub context
      - name: Dump GitHub context
        env:
          GITHUB_CONTEXT: ${{ toJSON(github) }}
        run: echo "$GITHUB_CONTEXT"
      
      # Print job context
      - name: Dump job context
        env:
          JOB_CONTEXT: ${{ toJSON(job) }}
        run: echo "$JOB_CONTEXT"
      
      # Print steps context
      - name: Dump steps context
        env:
          STEPS_CONTEXT: ${{ toJSON(steps) }}
        run: echo "$STEPS_CONTEXT"
      
      # Print runner context
      - name: Dump runner context
        env:
          RUNNER_CONTEXT: ${{ toJSON(runner) }}
        run: echo "$RUNNER_CONTEXT"
      
      # Print strategy context
      - name: Dump strategy context
        env:
          STRATEGY_CONTEXT: ${{ toJSON(strategy) }}
        run: echo "$STRATEGY_CONTEXT"
      
      # Print matrix context
      - name: Dump matrix context
        env:
          MATRIX_CONTEXT: ${{ toJSON(matrix) }}
        run: echo "$MATRIX_CONTEXT"

Conditional Debugging

yaml
on:
  workflow_dispatch:
    inputs:
      debug_enabled:
        description: 'Enable debug mode'
        type: boolean
        default: false

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Debug information
        if: ${{ inputs.debug_enabled }}
        run: |
          echo "Debug mode enabled"
          echo "Working directory: $(pwd)"
          echo "Files:"
          ls -la
          echo "Git status:"
          git status
      
      - name: Build
        run: npm run build

SSH Debugging with tmate

yaml
jobs:
  debug:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup tmate session
        if: ${{ failure() }}  # Only on failure
        uses: mxschmitt/action-tmate@v3
        timeout-minutes: 30
        with:
          limit-access-to-actor: true  # Only workflow initiator can access

Workflow Status Badges

markdown

![CI](https://github.com/username/repo/actions/workflows/ci.yml/badge.svg)
![CI](https://github.com/username/repo/actions/workflows/ci.yml/badge.svg?branch=main)
![CI](https://github.com/username/repo/actions/workflows/ci.yml/badge.svg?event=push)

Notifications

yaml
jobs:
  notify:
    runs-on: ubuntu-latest
    if: always()
    needs: [build, test, deploy]
    steps:
      - name: Notify Slack on failure
        if: ${{ contains(needs.*.result, 'failure') }}
        uses: slackapi/slack-github-action@v1
        with:
          webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
          payload: |
            {
              "text": "❌ Workflow failed: ${{ github.workflow }}",
              "blocks": [
                {
                  "type": "section",
                  "text": {
                    "type": "mrkdwn",
                    "text": "*Workflow:* ${{ github.workflow }}\n*Status:* Failed\n*Branch:* ${{ github.ref_name }}\n*Commit:* ${{ github.sha }}\n*Author:* ${{ github.actor }}"
                  }
                },
                {
                  "type": "actions",
                  "elements": [
                    {
                      "type": "button",
                      "text": {
                        "type": "plain_text",
                        "text": "View Workflow"
                      },
                      "url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
                    }
                  ]
                }
              ]
            }
      
      - name: Notify on success
        if: ${{ !contains(needs.*.result, 'failure') }}
        run: echo "✅ All jobs succeeded!"
⚠️ Warning: Be careful with SSH debugging actions like tmate in public repositories. Always use limit-access-to-actor: true and set reasonable timeouts.
13

GitHub-hosted vs Self-hosted Runners

Infrastructure

Choose between GitHub-hosted runners (managed by GitHub) or self-hosted runners (managed by you) based on your needs.

GitHub-hosted Runners

✅ Advantages

  • Zero maintenance
  • Automatic updates
  • Clean environment every run
  • Multiple OS options
  • Free for public repos

❌ Limitations

  • Limited customization
  • No persistent storage
  • Fixed hardware specs
  • Costs for private repos
  • No access to internal resources

Available GitHub-hosted Runners

yaml
jobs:
  # Ubuntu (latest = 22.04)
  ubuntu-job:
    runs-on: ubuntu-latest  # or ubuntu-22.04, ubuntu-20.04
  
  # Windows
  windows-job:
    runs-on: windows-latest  # or windows-2022, windows-2019
  
  # macOS
  macos-job:
    runs-on: macos-latest  # or macos-13, macos-12, macos-11
  
  # macOS with Apple Silicon (M1)
  macos-arm-job:
    runs-on: macos-14  # M1 chip

Runner Specifications

Linux & Windows

  • 2-core CPU
  • 7 GB RAM
  • 14 GB SSD

macOS

  • 3-core CPU
  • 14 GB RAM
  • 14 GB SSD

Self-hosted Runners

✅ Advantages

  • Custom hardware
  • Access internal resources
  • Persistent caching
  • Pre-installed tools
  • Cost control

❌ Considerations

  • Maintenance required
  • Security responsibility
  • Manual updates
  • Infrastructure costs
  • Cleanup needed

Setting Up Self-hosted Runner

  1. Go to repository SettingsActionsRunners
  2. Click New self-hosted runner
  3. Select OS and architecture
  4. Follow the provided installation commands
  5. Configure and start the runner
bash
# Download
mkdir actions-runner && cd actions-runner
curl -o actions-runner-linux-x64-2.311.0.tar.gz -L \
  https://github.com/actions/runner/releases/download/v2.311.0/actions-runner-linux-x64-2.311.0.tar.gz
tar xzf ./actions-runner-linux-x64-2.311.0.tar.gz

# Configure
./config.sh --url https://github.com/owner/repo --token YOUR_TOKEN

# Run
./run.sh

# Or install as service
sudo ./svc.sh install
sudo ./svc.sh start

Using Self-hosted Runners

yaml
jobs:
  build:
    runs-on: self-hosted
    steps:
      - uses: actions/checkout@v4
      - run: npm run build

  # Use specific labels
  build-gpu:
    runs-on: [self-hosted, linux, x64, gpu]
    steps:
      - run: python train_model.py

  # Combine with matrix
  test:
    runs-on: ${{ matrix.runner }}
    strategy:
      matrix:
        runner: [ubuntu-latest, [self-hosted, linux]]
    steps:
      - run: npm test

Runner Groups (Enterprise)

yaml
jobs:
  deploy:
    runs-on:
      group: production-runners
      labels: [self-hosted, linux, x64]
    steps:
      - run: ./deploy.sh
🔒 Security Warning: Self-hosted runners should NOT be used with public repositories. Anyone can create a PR and execute arbitrary code on your runner. Use self-hosted runners only for private repositories with trusted contributors.
14

Deployment Strategies

CD

Implement various deployment strategies for different scenarios and risk tolerances.

Basic Deployment Pipeline

yaml
name: Deploy Pipeline

on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm run build
      - uses: actions/upload-artifact@v4
        with:
          name: dist
          path: dist/
  
  test:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm test
  
  deploy-staging:
    needs: [build, test]
    runs-on: ubuntu-latest
    environment:
      name: staging
      url: https://staging.example.com
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: dist
      - run: ./deploy.sh staging
  
  deploy-production:
    needs: deploy-staging
    runs-on: ubuntu-latest
    environment:
      name: production
      url: https://example.com
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: dist
      - run: ./deploy.sh production

Blue-Green Deployment

yaml
jobs:
  deploy-blue-green:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Deploy to green environment
        run: |
          ./deploy.sh green
          echo "GREEN_URL=https://green.example.com" >> $GITHUB_ENV
      
      - name: Run smoke tests on green
        run: |
          npm run smoke-test -- --url=$GREEN_URL
      
      - name: Switch traffic to green
        run: |
          ./switch-traffic.sh green
      
      - name: Monitor for 5 minutes
        run: |
          sleep 300
          ./check-metrics.sh
      
      - name: Rollback on failure
        if: failure()
        run: |
          ./switch-traffic.sh blue
          ./cleanup.sh green

Canary Deployment

yaml
jobs:
  canary-deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Deploy canary (10% traffic)
        run: ./deploy-canary.sh --percentage=10
      
      - name: Monitor canary for 10 minutes
        run: |
          sleep 600
          ERROR_RATE=$(./get-error-rate.sh canary)
          if [ "$ERROR_RATE" -gt "1" ]; then
            echo "Error rate too high, rolling back"
            exit 1
          fi
      
      - name: Increase to 50%
        run: ./deploy-canary.sh --percentage=50
      
      - name: Monitor for 10 minutes
        run: sleep 600 && ./check-metrics.sh
      
      - name: Full rollout
        run: ./deploy-canary.sh --percentage=100
      
      - name: Rollback on failure
        if: failure()
        run: ./rollback-canary.sh

Rolling Deployment

yaml
jobs:
  rolling-deploy:
    runs-on: ubuntu-latest
    strategy:
      max-parallel: 1  # Deploy one at a time
      matrix:
        instance: [instance-1, instance-2, instance-3, instance-4]
    steps:
      - uses: actions/checkout@v4
      
      - name: Deploy to ${{ matrix.instance }}
        run: |
          ./deploy-to-instance.sh ${{ matrix.instance }}
      
      - name: Health check
        run: |
          ./health-check.sh ${{ matrix.instance }}
      
      - name: Wait before next
        run: sleep 60

Feature Flag Deployment

yaml
jobs:
  deploy-with-flags:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Deploy with feature flags disabled
        env:
          FEATURE_NEW_UI: false
          FEATURE_BETA_API: false
        run: ./deploy.sh
      
      - name: Enable for internal users
        run: |
          ./set-feature-flag.sh NEW_UI --users=internal
      
      - name: Monitor metrics
        run: ./monitor.sh --duration=1h
      
      - name: Gradual rollout
        run: |
          ./set-feature-flag.sh NEW_UI --percentage=10
          sleep 600
          ./set-feature-flag.sh NEW_UI --percentage=50
          sleep 600
          ./set-feature-flag.sh NEW_UI --percentage=100

Multi-Region Deployment

yaml
jobs:
  deploy-regions:
    runs-on: ubuntu-latest
    strategy:
      max-parallel: 2
      matrix:
        region: [us-east-1, us-west-2, eu-west-1, ap-southeast-1]
    steps:
      - uses: actions/checkout@v4
      
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_ROLE }}
          aws-region: ${{ matrix.region }}
      
      - name: Deploy to ${{ matrix.region }}
        run: |
          ./deploy-region.sh ${{ matrix.region }}
      
      - name: Verify deployment
        run: |
          ./verify-region.sh ${{ matrix.region }}
💡 Best Practices:
  • Always include health checks and smoke tests
  • Implement automated rollback on failure
  • Use environment protection rules for production
  • Monitor key metrics during deployment
  • Keep deployment artifacts for quick rollback
  • Use deployment slots or blue-green for zero-downtime
15

Integration with External Services

Integrations

Connect GitHub Actions with cloud providers, notification services, and other tools.

AWS Integration

yaml
jobs:
  deploy-aws:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
    steps:
      - uses: actions/checkout@v4
      
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
          aws-region: us-east-1
      
      - name: Deploy to S3
        run: |
          aws s3 sync ./dist s3://my-bucket --delete
      
      - name: Invalidate CloudFront
        run: |
          aws cloudfront create-invalidation \
            --distribution-id E1234567890ABC \
            --paths "/*"
      
      - name: Deploy Lambda
        run: |
          aws lambda update-function-code \
            --function-name my-function \
            --zip-file fileb://function.zip
      
      - name: Update ECS service
        run: |
          aws ecs update-service \
            --cluster my-cluster \
            --service my-service \
            --force-new-deployment

Azure Integration

yaml
jobs:
  deploy-azure:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Azure Login
        uses: azure/login@v1
        with:
          creds: ${{ secrets.AZURE_CREDENTIALS }}
      
      - name: Deploy to Azure Web App
        uses: azure/webapps-deploy@v2
        with:
          app-name: my-app
          package: ./dist
      
      - name: Deploy to Azure Functions
        uses: Azure/functions-action@v1
        with:
          app-name: my-function-app
          package: ./function-app
      
      - name: Deploy to AKS
        run: |
          az aks get-credentials --resource-group myRG --name myAKS
          kubectl apply -f k8s/
          kubectl rollout status deployment/my-app

Google Cloud Integration

yaml
jobs:
  deploy-gcp:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
    steps:
      - uses: actions/checkout@v4
      
      - name: Authenticate to Google Cloud
        uses: google-github-actions/auth@v2
        with:
          workload_identity_provider: 'projects/123/locations/global/workloadIdentityPools/my-pool/providers/my-provider'
          service_account: 'my-service-account@my-project.iam.gserviceaccount.com'
      
      - name: Set up Cloud SDK
        uses: google-github-actions/setup-gcloud@v2
      
      - name: Deploy to Cloud Run
        run: |
          gcloud run deploy my-service \
            --image gcr.io/my-project/my-image:${{ github.sha }} \
            --region us-central1 \
            --platform managed
      
      - name: Deploy to App Engine
        run: gcloud app deploy app.yaml --quiet
      
      - name: Deploy to GKE
        run: |
          gcloud container clusters get-credentials my-cluster --region us-central1
          kubectl apply -f k8s/
          kubectl rollout status deployment/my-app

Slack Notifications

yaml
jobs:
  notify-slack:
    runs-on: ubuntu-latest
    steps:
      - name: Notify Slack
        uses: slackapi/slack-github-action@v1
        with:
          webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
          payload: |
            {
              "text": "Deployment Status",
              "blocks": [
                {
                  "type": "header",
                  "text": {
                    "type": "plain_text",
                    "text": "🚀 Deployment Complete"
                  }
                },
                {
                  "type": "section",
                  "fields": [
                    {
                      "type": "mrkdwn",
                      "text": "*Repository:*\n${{ github.repository }}"
                    },
                    {
                      "type": "mrkdwn",
                      "text": "*Branch:*\n${{ github.ref_name }}"
                    },
                    {
                      "type": "mrkdwn",
                      "text": "*Commit:*\n<${{ github.event.head_commit.url }}|${{ github.sha }}>"
                    },
                    {
                      "type": "mrkdwn",
                      "text": "*Author:*\n${{ github.actor }}"
                    }
                  ]
                }
              ]
            }

Discord Notifications

yaml
- name: Notify Discord
  run: |
    curl -H "Content-Type: application/json" \
      -d '{
        "username": "GitHub Actions",
        "avatar_url": "https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png",
        "embeds": [{
          "title": "Deployment Complete",
          "description": "Successfully deployed to production",
          "color": 3066993,
          "fields": [
            {
              "name": "Repository",
              "value": "${{ github.repository }}",
              "inline": true
            },
            {
              "name": "Branch",
              "value": "${{ github.ref_name }}",
              "inline": true
            },
            {
              "name": "Commit",
              "value": "[${{ github.sha }}](${{ github.event.head_commit.url }})"
            }
          ]
        }]
      }' \
      ${{ secrets.DISCORD_WEBHOOK_URL }}

Terraform Integration

yaml
jobs:
  terraform:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: 1.6.0
      
      - name: Terraform Init
        run: terraform init
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
      
      - name: Terraform Plan
        run: terraform plan -out=tfplan
      
      - name: Terraform Apply
        if: github.ref == 'refs/heads/main'
        run: terraform apply -auto-approve tfplan

Database Migrations

yaml
jobs:
  migrate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Run database migrations
        env:
          DATABASE_URL: ${{ secrets.DATABASE_URL }}
        run: |
          npm run migrate
      
      # Or with Flyway
      - name: Flyway migrate
        uses: joshuaavalon/flyway-action@v3
        with:
          url: ${{ secrets.DATABASE_URL }}
          user: ${{ secrets.DB_USER }}
          password: ${{ secrets.DB_PASSWORD }}
          locations: filesystem:./migrations
      
      # Or with Liquibase
      - name: Liquibase update
        uses: liquibase/liquibase-github-action@v7
        with:
          operation: 'update'
          classpath: 'changelog'
          changeLogFile: 'changelog.xml'
          username: ${{ secrets.DB_USER }}
          password: ${{ secrets.DB_PASSWORD }}
          url: ${{ secrets.DATABASE_URL }}
🔗 Popular Actions:
  • AWS: aws-actions/configure-aws-credentials
  • Azure: azure/login, azure/webapps-deploy
  • GCP: google-github-actions/auth
  • Docker: docker/build-push-action
  • Kubernetes: azure/k8s-deploy
  • Terraform: hashicorp/setup-terraform
  • Notifications: slackapi/slack-github-action