GitHub Actions Workflow Creation

Module 28: DevOps & Deployment - Monday, Lecture 2

Introduction to GitHub Actions

In our previous lecture, we explored the concepts behind Continuous Integration and Continuous Delivery. Now, we'll dive into GitHub Actions, one of the most accessible and powerful CI/CD platforms available today.

GitHub Actions is like having a dedicated assistant who watches your code repository and automatically performs tasks whenever certain events occur. It's deeply integrated with GitHub, making it an excellent choice for teams already using GitHub for version control.

graph TB A[GitHub Repository] --> B[Events] B --> C[Workflows] C --> D[Jobs] D --> E[Steps] E --> F[Actions] B --> B1[push] B --> B2[pull_request] B --> B3[schedule] B --> B4[workflow_dispatch]

GitHub Actions organizes automation in a hierarchical structure:

Anatomy of GitHub Actions

Workflow Files

GitHub Actions workflows are defined in YAML files stored in the .github/workflows directory of your repository. Each workflow file contains the configuration for a complete automated process.

# Basic workflow structure
name: CI Pipeline

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

jobs:
  build:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v3
    - name: Set up Node.js
      uses: actions/setup-node@v3
      with:
        node-version: '18'
    - name: Install dependencies
      run: npm ci
    - name: Run tests
      run: npm test

Key Components Explained

GitHub Actions Event Types

Workflows are triggered by events. Here are some common event types:

mindmap root((GitHub Events)) Code Events push pull_request merge_group Issue Events issues issue_comment Release Events release created published Scheduled Events schedule cron Manual Events workflow_dispatch repository_dispatch

Event triggers can be refined with filters:

on:
  push:
    branches: [ main, develop ]  # Only trigger on specific branches
    paths:
      - 'src/**'                # Only trigger when files in src/ change
      - 'package.json'          # Or when package.json changes
    tags:
      - 'v*'                    # Trigger on version tags
  pull_request:
    types: [opened, synchronize] # Specific PR activities
    paths-ignore:
      - 'docs/**'              # Ignore changes to documentation

Jobs and Steps in GitHub Actions

Job Configuration

Jobs are the building blocks of workflows. Each job runs in a fresh instance of the virtual environment.

jobs:
  build:                 # Job ID
    name: Build and Test # Job display name
    runs-on: ubuntu-latest
    timeout-minutes: 30  # Maximum runtime
    
    # Job environment variables
    env:
      NODE_ENV: test
      
    # Conditional execution
    if: github.event_name == 'push'
    
    # Job outputs (can be used by other jobs)
    outputs:
      build_id: ${{ steps.build.outputs.id }}

Runners and Environments

GitHub provides several virtual environments for running jobs:

Steps and Actions

Steps are individual tasks within a job. They can either run commands or use actions.

steps:
  # Using an action
  - name: Checkout code
    uses: actions/checkout@v3
  
  # Running commands
  - name: Install dependencies
    run: |
      npm ci
      npm install -g jest
      
  # Using outputs between steps
  - name: Generate build number
    id: build
    run: echo "id=$(date +%s)" >> $GITHUB_OUTPUT
    
  - name: Use build number
    run: echo "Build ID is ${{ steps.build.outputs.id }}"

Job Dependencies and Parallelism

Jobs can run in parallel or sequentially depending on their configuration:

jobs:
  test:
    runs-on: ubuntu-latest
    # ... test job steps ...
    
  build:
    runs-on: ubuntu-latest
    # ... build job steps ...
    
  deploy:
    needs: [test, build]  # This job runs only after test and build complete
    runs-on: ubuntu-latest
    # ... deployment steps ...
graph TD A[test] --> C[deploy] B[build] --> C

Practical GitHub Actions Workflows

Example 1: JavaScript Project CI Pipeline

Let's create a complete CI workflow for a typical JavaScript project:

name: JavaScript CI

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

jobs:
  validate:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Set up Node.js
      uses: actions/setup-node@v3
      with:
        node-version: '18'
        cache: 'npm'
        
    - name: Install dependencies
      run: npm ci
      
    - name: Lint code
      run: npm run lint
      
    - name: Check types
      run: npm run type-check
      
    - name: Run unit tests
      run: npm test
      
    - name: Build project
      run: npm run build
      
    - name: Upload build artifacts
      uses: actions/upload-artifact@v3
      with:
        name: build-files
        path: dist/

Example 2: Full-Stack Application Workflow

A more complex example for a full-stack application with frontend, backend, and deployment:

name: Full-Stack CI/CD

on:
  push:
    branches: [ main ]
  
jobs:
  test-backend:
    runs-on: ubuntu-latest
    
    services:
      mongodb:
        image: mongo:4.4
        ports:
          - 27017:27017
          
    steps:
    - uses: actions/checkout@v3
    - name: Set up Node.js
      uses: actions/setup-node@v3
      with:
        node-version: '18'
    - name: Install backend dependencies
      working-directory: ./backend
      run: npm ci
    - name: Run backend tests
      working-directory: ./backend
      run: npm test
      env:
        MONGODB_URI: mongodb://localhost:27017/test
  
  test-frontend:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v3
    - name: Set up Node.js
      uses: actions/setup-node@v3
      with:
        node-version: '18'
    - name: Install frontend dependencies
      working-directory: ./frontend
      run: npm ci
    - name: Run frontend tests
      working-directory: ./frontend
      run: npm test
  
  build-and-deploy:
    needs: [test-backend, test-frontend]
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Build backend
      working-directory: ./backend
      run: |
        npm ci
        npm run build
    
    - name: Build frontend
      working-directory: ./frontend
      run: |
        npm ci
        npm run build
        
    - name: Deploy to production
      uses: some-deployment-action@v1
      with:
        server: ${{ secrets.PRODUCTION_SERVER }}
        username: ${{ secrets.SSH_USERNAME }}
        key: ${{ secrets.SSH_PRIVATE_KEY }}

Example 3: Matrix Strategy for Multi-Environment Testing

Testing across multiple Node.js versions and operating systems:

name: Cross-Platform Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ${{ matrix.os }}
    
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
        node-version: [16.x, 18.x, 20.x]
        # Exclude specific combinations
        exclude:
          - os: windows-latest
            node-version: 16.x
    
    steps:
    - uses: actions/checkout@v3
    - name: Set up Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v3
      with:
        node-version: ${{ matrix.node-version }}
    - name: Install dependencies
      run: npm ci
    - name: Run tests
      run: npm test
graph TD A[Matrix Jobs] --> B["ubuntu + Node 16"] A --> C["ubuntu + Node 18"] A --> D["ubuntu + Node 20"] A --> E["windows + Node 18"] A --> F["windows + Node 20"] A --> G["macos + Node 16"] A --> H["macos + Node 18"] A --> I["macos + Node 20"]

Advanced GitHub Actions Features

Secrets and Environment Variables

Secure sensitive information using GitHub's secrets management:

jobs:
  deploy:
    runs-on: ubuntu-latest
    
    env:
      # Public environment variables
      APP_ENV: production
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Deploy application
      env:
        # Job-level environment variables
        DEPLOY_REGION: us-west-2
      run: |
        # Access repository secrets
        echo "Deploying with API key: ${{ secrets.API_KEY }}"
        # Access environment variables
        echo "Environment: $APP_ENV, Region: $DEPLOY_REGION"

Secrets are configured in your repository settings and can be accessed using the ${{ secrets.SECRET_NAME }} syntax.

Reusable Workflows

Create reusable workflow templates to avoid duplication:

# .github/workflows/reusable.yml
name: Reusable Workflow

on:
  workflow_call:
    inputs:
      node-version:
        required: true
        type: string
    secrets:
      npm-token:
        required: true

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: ${{ inputs.node-version }}
      - run: npm ci
        env:
          NODE_AUTH_TOKEN: ${{ secrets.npm-token }}

Using the reusable workflow in another workflow:

# .github/workflows/caller.yml
name: Caller Workflow

on: [push]

jobs:
  call-reusable:
    uses: ./.github/workflows/reusable.yml
    with:
      node-version: '18'
    secrets:
      npm-token: ${{ secrets.NPM_TOKEN }}

Caching Dependencies

Speed up workflows by caching dependencies between runs:

jobs:
  build:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Set up Node.js
      uses: actions/setup-node@v3
      with:
        node-version: '18'
        
    - name: Cache node modules
      uses: actions/cache@v3
      with:
        path: ~/.npm
        key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
        restore-keys: |
          ${{ runner.os }}-node-
          
    - name: Install dependencies
      run: npm ci
      
    # Rest of the workflow

Scheduled Workflows

Run workflows on a schedule using cron syntax:

name: Nightly Build

on:
  schedule:
    # Run at 2:30 AM UTC every day
    - cron: '30 2 * * *'

jobs:
  build:
    runs-on: ubuntu-latest
    
    steps:
    # Workflow steps

Common cron patterns:

GitHub Actions Best Practices

Workflow Organization

Performance Optimization

Security Considerations

# Limit workflow permissions
permissions:
  contents: read
  packages: write
  # Don't grant unnecessary permissions
  # actions: none
  # deployments: none
  # etc.

Debugging Workflows

steps:
  - name: Debug information
    run: |
      echo "GitHub Context: ${{ toJSON(github) }}"
      echo "Runner OS: ${{ runner.os }}"
      echo "Working Directory: $(pwd)"
      echo "Path: $PATH"
      echo "Node Version: $(node -v)"
      echo "NPM Version: $(npm -v)"

Practical Exercise: Creating Your First GitHub Actions Workflow

Exercise Brief

Create a GitHub Actions workflow for a JavaScript application that:

  1. Triggers on push to the main branch and on pull requests
  2. Installs dependencies
  3. Runs linting
  4. Executes tests
  5. Builds the application
  6. Deploys to GitHub Pages (for the main branch only)

Step-by-Step Approach

  1. Create a new file at .github/workflows/ci-cd.yml in your repository
  2. Define the trigger events
  3. Set up a job for validation and testing
  4. Add steps for installation, linting, testing, and building
  5. Create a conditional deployment job depending on the branch
  6. Use the GitHub Pages deployment action

Solution Outline

name: CI/CD Pipeline

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

jobs:
  build-and-test:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Set up Node.js
      uses: actions/setup-node@v3
      with:
        node-version: '18'
        cache: 'npm'
        
    - name: Install dependencies
      run: npm ci
      
    - name: Run linting
      run: npm run lint
      
    - name: Run tests
      run: npm test
      
    - name: Build project
      run: npm run build
      
    - name: Upload build artifacts
      uses: actions/upload-artifact@v3
      with:
        name: build-files
        path: ./build
        
  deploy:
    needs: build-and-test
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    runs-on: ubuntu-latest
    
    steps:
    - name: Download build artifacts
      uses: actions/download-artifact@v3
      with:
        name: build-files
        path: ./build
        
    - name: Deploy to GitHub Pages
      uses: peaceiris/actions-gh-pages@v3
      with:
        github_token: ${{ secrets.GITHUB_TOKEN }}
        publish_dir: ./build

Challenge Extension

Once you have the basic workflow working, try extending it with these features:

Real-World GitHub Actions Examples

Example 1: React Application at Airbnb

Airbnb uses GitHub Actions for their React component library with:

Example 2: API Service at Shopify

Shopify's backend services use GitHub Actions for:

Example 3: Mobile Application at Spotify

Spotify's mobile team uses GitHub Actions to:

These companies customize GitHub Actions to fit their specific needs and workflows, demonstrating the platform's flexibility.

Conclusion

GitHub Actions provides a powerful, flexible platform for implementing CI/CD workflows directly within your GitHub repository. The key advantages include:

As you build your CI/CD pipelines with GitHub Actions, start simple and gradually add complexity as needed. Remember that the goal is to automate repetitive tasks and provide quick feedback, enabling your team to deliver high-quality software more efficiently.

In our next lecture, we'll explore Jenkins Pipeline, another powerful CI/CD tool that offers different capabilities and is particularly strong for enterprise environments with complex requirements.

Additional Resources