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.
GitHub Actions organizes automation in a hierarchical structure:
- Workflows: Automated procedures triggered by events
- Jobs: Sets of steps that execute on the same runner
- Steps: Individual tasks that run commands or actions
- Actions: Reusable units of code for common tasks
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
- name: A descriptive name for the workflow, displayed in the GitHub UI
- on: Events that trigger the workflow (push, pull_request, schedule, etc.)
- jobs: Groups of steps that run on the same runner
- runs-on: The type of runner (virtual environment) to use
- steps: Individual tasks to perform
- uses: References to actions to execute
- with: Parameters for actions
- run: Shell commands to execute
GitHub Actions Event Types
Workflows are triggered by events. Here are some common event types:
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:
- ubuntu-latest: Linux environment
- windows-latest: Windows environment
- macos-latest: macOS environment
- self-hosted: Your own runners
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 ...
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
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:
0 0 * * *- Daily at midnight UTC0 0 * * 0- Weekly on Sunday at midnight UTC0 0 1 * *- Monthly on the 1st at midnight UTC*/15 * * * *- Every 15 minutes
GitHub Actions Best Practices
Workflow Organization
- Keep workflows focused: Create separate workflows for different purposes (CI, CD, documentation)
- Use descriptive names: Name your workflows, jobs, and steps clearly
- Comment complex sections: Add comments to explain non-obvious configuration
- Structure repositories consistently: Follow a standard pattern for all projects
Performance Optimization
- Use caching: Cache dependencies, build outputs, and other artifacts
- Limit trigger events: Only run workflows when necessary
- Parallelize jobs: Run independent tasks simultaneously
- Choose appropriate runners: Use the smallest/fastest runner that meets your needs
- Filter paths: Only trigger workflows when relevant files change
Security Considerations
- Limit workflow permissions: Use the principle of least privilege
- Scan for secrets: Ensure no credentials are accidentally committed
- Validate external actions: Use trusted actions or pin to specific commit SHAs
- Secure your workflows: Be cautious with workflow inputs, especially from PRs
# Limit workflow permissions
permissions:
contents: read
packages: write
# Don't grant unnecessary permissions
# actions: none
# deployments: none
# etc.
Debugging Workflows
- Use debug logging: Enable debug mode for troubleshooting
- Add diagnostic steps: Output environment information
- Review workflow runs: Check the detailed logs in GitHub UI
- Local testing: Use act or similar tools to test workflows locally
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:
- Triggers on push to the main branch and on pull requests
- Installs dependencies
- Runs linting
- Executes tests
- Builds the application
- Deploys to GitHub Pages (for the main branch only)
Step-by-Step Approach
- Create a new file at
.github/workflows/ci-cd.ymlin your repository - Define the trigger events
- Set up a job for validation and testing
- Add steps for installation, linting, testing, and building
- Create a conditional deployment job depending on the branch
- 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:
- Add cache for npm dependencies to speed up the build
- Create a matrix configuration to test on multiple Node.js versions
- Add code coverage reporting
- Configure Slack notifications for workflow results
- Create environment-specific deployments (staging vs production)
Real-World GitHub Actions Examples
Example 1: React Application at Airbnb
Airbnb uses GitHub Actions for their React component library with:
- Parallel test execution across multiple browsers
- Visual regression testing with screenshots
- Automated package publishing with changeset management
- Documentation generation and deployment
Example 2: API Service at Shopify
Shopify's backend services use GitHub Actions for:
- Database migration validation
- API contract testing
- Performance benchmarking
- Gradual rollout with canary deployments
- Automated rollbacks based on monitoring
Example 3: Mobile Application at Spotify
Spotify's mobile team uses GitHub Actions to:
- Build iOS and Android versions
- Run device farm testing
- Manage beta distribution
- Automate App Store and Play Store submissions
- Track release metrics and user feedback
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:
- Tight integration with GitHub's ecosystem
- Marketplace of actions for common tasks
- Flexible configuration via YAML files
- Free tier for public repositories and limited usage in private repositories
- Self-hosted runners for custom environments
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.