Home / Notebooks / DevOps
DevOps
intermediate

GitHub Actions Essentials

Essential GitHub Actions concepts for CI/CD automation and workflows

March 10, 2024
Updated regularly

GitHub Actions Essentials

Quick reference guide for GitHub Actions automation.

What is GitHub Actions?

GitHub Actions is a CI/CD platform built into GitHub that:

  • Automates workflows (build, test, deploy)
  • Triggered by events (push, pull request, schedule)
  • Runs on GitHub-hosted or self-hosted runners
  • Reusable actions from marketplace
  • Free tier included for public repos
  • Integrated with GitHub ecosystem
  • Core Concepts

    Workflow
    ├── Events (triggers)
    ├── Jobs (run in parallel)
    │   ├── Runs-on (runner)
    │   └── Steps (sequential tasks)
    │       ├── Uses (action)
    │       └── Run (command)
    └── Artifacts (outputs)
    

    Key Components

  • Workflow: Automated process defined in YAML
  • Event: Trigger that starts a workflow
  • Job: Set of steps executed on the same runner
  • Step: Individual task within a job
  • Action: Reusable unit of code
  • Runner: Server that runs workflows
  • Basic Workflow

    Simple Example

    # .github/workflows/hello.yml
    name: Hello World
    
    on: [push]
    
    jobs:
      greet:
        runs-on: ubuntu-latest
        steps:
          - name: Say hello
            run: echo "Hello, World!"
    

    Complete CI Workflow

    # .github/workflows/ci.yml
    name: CI
    
    on:
      push:
        branches: [main, develop]
      pull_request:
        branches: [main]
    
    jobs:
      test:
        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'
              cache: 'npm'
          
          - name: Install dependencies
            run: npm ci
          
          - name: Run linter
            run: npm run lint
          
          - name: Run tests
            run: npm test
          
          - name: Build
            run: npm run build
    

    Events (Triggers)

    Push Events

    # Trigger on any push
    on: push
    
    # Trigger on specific branches
    on:
      push:
        branches:
          - main
          - develop
          - 'release/**'
    
    # Trigger on specific paths
    on:
      push:
        paths:
          - 'src/**'
          - 'package.json'
    
    # Ignore specific paths
    on:
      push:
        paths-ignore:
          - 'docs/**'
          - '**.md'
    

    Pull Request Events

    on:
      pull_request:
        types: [opened, synchronize, reopened]
        branches: [main]
    
    # Run on PR to specific branches
    on:
      pull_request:
        branches:
          - main
          - develop
    

    Schedule (Cron)

    on:
      schedule:
        # Run every day at 2 AM UTC
        - cron: '0 2 * * *'
        # Run every Monday at 9 AM
        - cron: '0 9 * * 1'
    

    Manual Trigger

    on:
      workflow_dispatch:
        inputs:
          environment:
            description: 'Deployment environment'
            required: true
            type: choice
            options:
              - development
              - staging
              - production
          version:
            description: 'Version to deploy'
            required: false
            default: 'latest'
    

    Multiple Events

    on:
      push:
        branches: [main]
      pull_request:
        branches: [main]
      schedule:
        - cron: '0 0 * * 0'  # Weekly
      workflow_dispatch:
    

    Jobs

    Parallel Jobs

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

    Sequential Jobs (Dependencies)

    jobs:
      build:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
          - run: npm run build
      
      test:
        needs: build
        runs-on: ubuntu-latest
        steps:
          - run: npm test
      
      deploy:
        needs: [build, test]
        runs-on: ubuntu-latest
        steps:
          - run: npm run deploy
    

    Conditional Jobs

    jobs:
      deploy:
        if: github.ref == 'refs/heads/main'
        runs-on: ubuntu-latest
        steps:
          - run: echo "Deploying to production"
      
      test:
        if: github.event_name == 'pull_request'
        runs-on: ubuntu-latest
        steps:
          - run: npm test
    

    Matrix Strategy

    Multiple Versions

    jobs:
      test:
        runs-on: ubuntu-latest
        strategy:
          matrix:
            node-version: [16, 18, 20]
        
        steps:
          - uses: actions/checkout@v4
          - uses: actions/setup-node@v4
            with:
              node-version: ${{ matrix.node-version }}
          - run: npm test
    

    Multiple OS and Versions

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

    Matrix with Include/Exclude

    jobs:
      test:
        strategy:
          matrix:
            os: [ubuntu-latest, windows-latest]
            node-version: [16, 18, 20]
            include:
              # Add extra combination
              - os: macos-latest
                node-version: 20
            exclude:
              # Remove combination
              - os: windows-latest
                node-version: 16
        
        runs-on: ${{ matrix.os }}
        steps:
          - run: npm test
    

    Environment Variables and Secrets

    Environment Variables

    jobs:
      build:
        runs-on: ubuntu-latest
        env:
          NODE_ENV: production
          API_URL: https://api.example.com
        
        steps:
          - name: Build
            run: npm run build
          
          - name: Use environment variable
            run: echo "API URL is $API_URL"
          
          - name: Set step-level env
            env:
              CUSTOM_VAR: value
            run: echo $CUSTOM_VAR
    

    Using Secrets

    jobs:
      deploy:
        runs-on: ubuntu-latest
        steps:
          - name: Deploy
            env:
              AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
              AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
            run: |
              aws s3 sync ./dist s3://my-bucket
    

    GitHub Context

    steps:
      - name: Print context
        run: |
          echo "Repository: ${{ github.repository }}"
          echo "Branch: ${{ github.ref }}"
          echo "Commit SHA: ${{ github.sha }}"
          echo "Actor: ${{ github.actor }}"
          echo "Event: ${{ github.event_name }}"
    

    Common Actions

    Checkout Code

    steps:
      - name: Checkout code
        uses: actions/checkout@v4
        with:
          fetch-depth: 0  # Fetch all history
          submodules: true  # Include submodules
    

    Setup Languages

    # Node.js
    - uses: actions/setup-node@v4
      with:
        node-version: '20'
        cache: 'npm'
    
    # Python
    - uses: actions/setup-python@v4
      with:
        python-version: '3.11'
        cache: 'pip'
    
    # Java
    - uses: actions/setup-java@v3
      with:
        distribution: 'temurin'
        java-version: '17'
        cache: 'maven'
    
    # Go
    - uses: actions/setup-go@v4
      with:
        go-version: '1.21'
        cache: true
    

    Caching

    # NPM cache
    - uses: actions/cache@v3
      with:
        path: ~/.npm
        key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
        restore-keys: |
          ${{ runner.os }}-node-
    
    # Custom cache
    - uses: actions/cache@v3
      with:
        path: |
          ~/.cache
          ./node_modules
        key: ${{ runner.os }}-cache-${{ hashFiles('**/*.lock') }}
    

    Upload/Download Artifacts

    jobs:
      build:
        runs-on: ubuntu-latest
        steps:
          - run: npm run build
          
          - name: Upload artifacts
            uses: actions/upload-artifact@v3
            with:
              name: build-files
              path: dist/
              retention-days: 7
      
      deploy:
        needs: build
        runs-on: ubuntu-latest
        steps:
          - name: Download artifacts
            uses: actions/download-artifact@v3
            with:
              name: build-files
              path: dist/
          
          - run: npm run deploy
    

    Language-Specific Workflows

    Node.js

    name: Node.js CI
    
    on: [push, pull_request]
    
    jobs:
      test:
        runs-on: ubuntu-latest
        
        strategy:
          matrix:
            node-version: [18, 20]
        
        steps:
          - uses: actions/checkout@v4
          
          - name: Setup Node.js
            uses: actions/setup-node@v4
            with:
              node-version: ${{ matrix.node-version }}
              cache: 'npm'
          
          - name: Install dependencies
            run: npm ci
          
          - name: Run tests
            run: npm test
          
          - name: Build
            run: npm run build
    

    Python

    name: Python CI
    
    on: [push, pull_request]
    
    jobs:
      test:
        runs-on: ubuntu-latest
        
        strategy:
          matrix:
            python-version: ['3.9', '3.10', '3.11']
        
        steps:
          - uses: actions/checkout@v4
          
          - name: Setup Python
            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 -r requirements.txt
          
          - name: Lint with flake8
            run: flake8 .
          
          - name: Test with pytest
            run: pytest
    

    Docker Build

    name: Docker Build
    
    on:
      push:
        branches: [main]
    
    jobs:
      build:
        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.DOCKER_USERNAME }}
              password: ${{ secrets.DOCKER_PASSWORD }}
          
          - name: Build and push
            uses: docker/build-push-action@v5
            with:
              context: .
              push: true
              tags: user/app:latest
              cache-from: type=registry,ref=user/app:cache
              cache-to: type=registry,ref=user/app:cache,mode=max
    

    Deployment Workflows

    Deploy to AWS S3

    name: Deploy to S3
    
    on:
      push:
        branches: [main]
    
    jobs:
      deploy:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
          
          - name: Setup Node.js
            uses: actions/setup-node@v4
            with:
              node-version: '20'
          
          - name: Build
            run: |
              npm ci
              npm run build
          
          - name: Configure AWS credentials
            uses: aws-actions/configure-aws-credentials@v4
            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: Deploy to S3
            run: |
              aws s3 sync ./dist s3://my-bucket --delete
    

    Deploy to Vercel

    name: Deploy to Vercel
    
    on:
      push:
        branches: [main]
    
    jobs:
      deploy:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
          
          - name: Deploy to Vercel
            uses: amondnet/vercel-action@v25
            with:
              vercel-token: ${{ secrets.VERCEL_TOKEN }}
              vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
              vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
              vercel-args: '--prod'
    

    Deploy to Kubernetes

    name: Deploy to K8s
    
    on:
      push:
        branches: [main]
    
    jobs:
      deploy:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
          
          - name: Configure kubectl
            uses: azure/k8s-set-context@v3
            with:
              method: kubeconfig
              kubeconfig: ${{ secrets.KUBE_CONFIG }}
          
          - name: Deploy to K8s
            run: |
              kubectl apply -f k8s/
              kubectl rollout status deployment/my-app
    

    Testing and Coverage

    Run Tests with Coverage

    jobs:
      test:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
          
          - uses: actions/setup-node@v4
            with:
              node-version: '20'
          
          - run: npm ci
          
          - name: Run tests with coverage
            run: npm test -- --coverage
          
          - name: Upload coverage to Codecov
            uses: codecov/codecov-action@v3
            with:
              files: ./coverage/lcov.info
              fail_ci_if_error: true
    

    Reusable Workflows

    Define Reusable Workflow

    # .github/workflows/reusable-deploy.yml
    name: Reusable Deploy
    
    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
            env:
              TOKEN: ${{ secrets.deploy-token }}
            run: ./deploy.sh
    

    Use Reusable Workflow

    # .github/workflows/production.yml
    name: Production Deploy
    
    on:
      push:
        branches: [main]
    
    jobs:
      deploy-prod:
        uses: ./.github/workflows/reusable-deploy.yml
        with:
          environment: production
        secrets:
          deploy-token: ${{ secrets.PROD_DEPLOY_TOKEN }}
    

    Composite Actions

    Create Custom Action

    # .github/actions/setup-app/action.yml
    name: 'Setup App'
    description: 'Setup Node.js and install dependencies'
    
    inputs:
      node-version:
        description: 'Node.js version'
        required: false
        default: '20'
    
    runs:
      using: 'composite'
      steps:
        - uses: actions/setup-node@v4
          with:
            node-version: ${{ inputs.node-version }}
            cache: 'npm'
        
        - run: npm ci
          shell: bash
    

    Use Custom Action

    jobs:
      build:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
          - uses: ./.github/actions/setup-app
            with:
              node-version: '20'
          - run: npm run build
    

    Advanced Features

    Conditional Steps

    steps:
      - name: Run on main only
        if: github.ref == 'refs/heads/main'
        run: echo "Main branch"
      
      - name: Run on PR
        if: github.event_name == 'pull_request'
        run: echo "Pull request"
      
      - name: Run if previous step succeeded
        if: success()
        run: echo "Previous step passed"
      
      - name: Run if previous step failed
        if: failure()
        run: echo "Previous step failed"
    

    Continue on Error

    steps:
      - name: Run tests
        continue-on-error: true
        run: npm test
      
      - name: This runs even if tests fail
        run: echo "Continuing..."
    

    Timeout

    jobs:
      test:
        runs-on: ubuntu-latest
        timeout-minutes: 10
        steps:
          - run: npm test
            timeout-minutes: 5
    

    Concurrency Control

    concurrency:
      group: ${{ github.workflow }}-${{ github.ref }}
      cancel-in-progress: true
    
    jobs:
      deploy:
        runs-on: ubuntu-latest
        steps:
          - run: ./deploy.sh
    

    Security Best Practices

    Pin Actions to SHA

    steps:
      # ❌ BAD: Using branch or tag
      - uses: actions/checkout@v4
      
      # ✅ GOOD: Using SHA
      - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
    

    Least Privilege Permissions

    permissions:
      contents: read
      pull-requests: write
      issues: write
    
    jobs:
      test:
        runs-on: ubuntu-latest
        permissions:
          contents: read
        steps:
          - run: npm test
    

    Use GitHub Environment Secrets

    jobs:
      deploy:
        runs-on: ubuntu-latest
        environment: production
        steps:
          - name: Deploy
            env:
              SECRET: ${{ secrets.PRODUCTION_SECRET }}
            run: ./deploy.sh
    

    Debugging

    Enable Debug Logging

    # Set repository secrets:
    ACTIONS_STEP_DEBUG=true
    ACTIONS_RUNNER_DEBUG=true
    

    Debug Step

    steps:
      - name: Debug info
        run: |
          echo "Event: ${{ github.event_name }}"
          echo "Ref: ${{ github.ref }}"
          echo "SHA: ${{ github.sha }}"
          echo "Actor: ${{ github.actor }}"
          env
    

    Use tmate for SSH Debug

    steps:
      - name: Setup tmate session
        if: failure()
        uses: mxschmitt/action-tmate@v3
    

    Tips

  • Use caching to speed up workflows
  • Pin action versions for security and stability
  • Keep secrets in GitHub Secrets, never in code
  • Use matrix builds for testing multiple versions
  • Set timeouts to prevent stuck workflows
  • Use concurrency groups to cancel duplicate runs
  • Leverage reusable workflows to avoid duplication
  • Monitor workflow usage to stay within limits
  • Use environments for deployment protection
  • Add status badges to README for visibility
  • Common Patterns

    Monorepo with Path Filters

    on:
      push:
        paths:
          - 'packages/api/**'
    
    jobs:
      test-api:
        if: contains(github.event.head_commit.modified, 'packages/api')
        runs-on: ubuntu-latest
        steps:
          - run: npm test -- packages/api
    

    Release Automation

    name: Release
    
    on:
      push:
        tags:
          - 'v*'
    
    jobs:
      release:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
          
          - name: Create Release
            uses: actions/create-release@v1
            env:
              GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
            with:
              tag_name: ${{ github.ref }}
              release_name: Release ${{ github.ref }}
              draft: false
              prerelease: false
    

    Resources

  • GitHub Actions Documentation
  • GitHub Actions Marketplace
  • Awesome GitHub Actions
  • GitHub Actions Toolkit
  • Workflow Syntax Reference
  • Topics

    GitHub ActionsCI/CDDevOpsAutomationTesting

    Found This Helpful?

    If you have questions or suggestions for improving these notes, I'd love to hear from you.