0%
Tutorial 10 of 10
100% complete

GitHub Actions and CI/CD

Set up continuous integration and continuous deployment workflows directly in your GitHub repositories.

GitHub Actions and CI/CD

GitHub Actions transforms software development automation by building CI/CD directly into your repositories. This tutorial will teach you how to create powerful workflows to automate tests, builds, and deployments.

Introduction to GitHub Actions

What is GitHub Actions?

GitHub Actions is an automation platform that lets you:

  • Run workflows on Git events
  • Automate tests and validation
  • Deploy your applications automatically
  • Manage releases and notifications
Push/PRGitHub ActionsTestsBuildDeployNotifications

Key concepts

Workflow: An automated process defined in YAML Job: A set of steps executed on a runner Step: An individual action within a job Action: A reusable component Runner: A virtual machine that executes jobs

Workflow structure

Basic file

# .github/workflows/ci.yml
name: CI Pipeline

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: '18'
          
      - name: Install dependencies
        run: npm ci
        
      - name: Run tests
        run: npm test

Triggers

# Git events
on:
  push:
    branches: [main]
    tags: ['v*']
  pull_request:
    branches: [main]
  release:
    types: [published]

# Scheduled events
on:
  schedule:
    - cron: '0 2 * * 1'  # Every Monday at 2:00 AM

# Manual trigger
on:
  workflow_dispatch:
    inputs:
      environment:
        description: 'Environment to deploy'
        required: true
        default: 'staging'

Essential workflows

Basic CI for Node.js

# .github/workflows/node-ci.yml
name: Node.js CI

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

jobs:
  test:
    runs-on: ubuntu-latest
    
    strategy:
      matrix:
        node-version: [16, 18, 20]
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'
          
      - name: Install dependencies
        run: npm ci
        
      - name: Run linter
        run: npm run lint
        
      - name: Run tests
        run: npm test
        
      - name: Run build
        run: npm run build
        
      - name: Upload coverage
        uses: codecov/codecov-action@v3
        with:
          file: ./coverage/lcov.info

CI for Python

# .github/workflows/python-ci.yml
name: Python CI

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    
    strategy:
      matrix:
        python-version: [3.8, 3.9, '3.10', 3.11]
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Set up Python ${{ matrix.python-version }}
        uses: actions/setup-python@v4
        with:
          python-version: ${{ matrix.python-version }}
          
      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install -r requirements.txt
          pip install -r requirements-dev.txt
          
      - name: Lint with flake8
        run: |
          flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
          flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127
          
      - name: Test with pytest
        run: |
          pytest --cov=./ --cov-report=xml
          
      - name: Upload coverage
        uses: codecov/codecov-action@v3

Automatic deployment

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

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: '18'
          
      - name: Install and build
        run: |
          npm ci
          npm run build
          
      - name: Deploy to Vercel
        uses: amondnet/vercel-action@v25
        with:
          vercel-token: ${{ secrets.VERCEL_TOKEN }}
          vercel-org-id: ${{ secrets.ORG_ID }}
          vercel-project-id: ${{ secrets.PROJECT_ID }}
          vercel-args: '--prod'

Advanced actions

Test matrix

jobs:
  test:
    runs-on: ${{ matrix.os }}
    
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
        node-version: [16, 18, 20]
        include:
          - os: ubuntu-latest
            node-version: 20
            coverage: true
        exclude:
          - os: windows-latest
            node-version: 16
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          
      - name: Run tests
        run: npm test
        
      - name: Upload coverage
        if: matrix.coverage
        uses: codecov/codecov-action@v3

Conditions and expressions

jobs:
  deploy:
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    runs-on: ubuntu-latest
    
    steps:
      - name: Deploy to staging
        if: contains(github.event.head_commit.message, '[staging]')
        run: echo "Deploying to staging"
        
      - name: Deploy to production
        if: startsWith(github.ref, 'refs/tags/v')
        run: echo "Deploying to production"
        
      - name: Notify on failure
        if: failure()
        uses: 8398a7/action-slack@v3
        with:
          status: failure
          webhook_url: ${{ secrets.SLACK_WEBHOOK }}

Artifacts and cache

jobs:
  build:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Cache dependencies
        uses: actions/cache@v3
        with:
          path: ~/.npm
          key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-node-
            
      - name: Install and build
        run: |
          npm ci
          npm run build
          
      - name: Upload build artifacts
        uses: actions/upload-artifact@v3
        with:
          name: build-files
          path: dist/
          
  deploy:
    needs: build
    runs-on: ubuntu-latest
    
    steps:
      - name: Download artifacts
        uses: actions/download-artifact@v3
        with:
          name: build-files
          path: dist/
          
      - name: Deploy
        run: echo "Deploying files from dist/"

Security and secrets

Managing secrets

# Configuring secrets in GitHub
# Settings > Secrets and variables > Actions

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

Protected environments

jobs:
  deploy-production:
    runs-on: ubuntu-latest
    environment: production
    
    steps:
      - name: Deploy to production
        run: echo "Deploying to production environment"

GITHUB_TOKEN permissions

name: Secure Workflow

on: [push]

permissions:
  contents: read
  issues: write
  pull-requests: write

jobs:
  security-scan:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Run security scan
        uses: github/super-linter@v4
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Custom actions

JavaScript action

// .github/actions/hello-world/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);
  
  const payload = JSON.stringify(github.context.payload, undefined, 2);
  console.log(`The event payload: ${payload}`);
} catch (error) {
  core.setFailed(error.message);
}
# .github/actions/hello-world/action.yml
name: 'Hello World'
description: 'Greet someone and record the time'

inputs:
  who-to-greet:
    description: 'Who to greet'
    required: true
    default: 'World'

outputs:
  time:
    description: 'The time we greeted you'

runs:
  using: 'node20'
  main: 'index.js'

Using the action

jobs:
  hello_world_job:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Hello world action step
        uses: ./.github/actions/hello-world
        id: hello
        with:
          who-to-greet: 'GitHub Actions'
          
      - name: Get the output time
        run: echo "The time was ${{ steps.hello.outputs.time }}"

Complex workflows

Multi-environment pipeline

# .github/workflows/multi-env-deploy.yml
name: Multi-Environment Deploy

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

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v4
      - name: Run tests
        run: npm test
  
  deploy-staging:
    needs: test
    if: github.ref == 'refs/heads/develop'
    runs-on: ubuntu-latest
    environment: staging
    
    steps:
      - uses: actions/checkout@v4
      - name: Deploy to staging
        run: echo "Deploying to staging"
        
  deploy-production:
    needs: test
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment: production
    
    steps:
      - uses: actions/checkout@v4
      - name: Deploy to production
        run: echo "Deploying to production"
        
  notify:
    needs: [deploy-staging, deploy-production]
    if: always()
    runs-on: ubuntu-latest
    
    steps:
      - name: Notify team
        uses: 8398a7/action-slack@v3
        with:
          status: ${{ job.status }}
          webhook_url: ${{ secrets.SLACK_WEBHOOK }}

Automatic release

# .github/workflows/release.yml
name: Release

on:
  push:
    tags:
      - 'v*'

jobs:
  release:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
          
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '18'
          
      - name: Install dependencies
        run: npm ci
        
      - name: Build
        run: npm run build
        
      - name: Generate changelog
        id: changelog
        uses: metcalfc/changelog-generator@v4.0.1
        with:
          myToken: ${{ secrets.GITHUB_TOKEN }}
          
      - name: Create Release
        uses: actions/create-release@v1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          tag_name: ${{ github.ref }}
          release_name: Release ${{ github.ref }}
          body: ${{ steps.changelog.outputs.changelog }}
          draft: false
          prerelease: false
          
      - name: Upload Release Asset
        uses: actions/upload-release-asset@v1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          upload_url: ${{ steps.create_release.outputs.upload_url }}
          asset_path: ./dist/app.zip
          asset_name: app.zip
          asset_content_type: application/zip

Hands-on exercises

Exercise 1: Basic CI

# .github/workflows/basic-ci.yml
name: Basic CI

on:
  push:
    branches: [main]
  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: '18'
          
      - name: Install dependencies
        run: |
          echo '{"name": "test-app", "scripts": {"test": "echo \"Tests passed!\""}}' > package.json
          npm install
          
      - name: Run tests
        run: npm test
        
      - name: Success message
        run: echo "✅ All tests passed!"

Exercise 2: Conditional deployment

# .github/workflows/conditional-deploy.yml
name: Conditional Deploy

on:
  push:
    branches: [main, staging]

jobs:
  deploy:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Deploy to staging
        if: github.ref == 'refs/heads/staging'
        run: |
          echo "🚀 Deploying to staging environment"
          echo "Environment: staging"
          
      - name: Deploy to production
        if: github.ref == 'refs/heads/main'
        run: |
          echo "🚀 Deploying to production environment"
          echo "Environment: production"
          
      - name: Notify deployment
        run: |
          echo "📧 Sending notification"
          echo "Deployment completed for branch: ${{ github.ref_name }}"

Exercise 3: Workflow with artifacts

# .github/workflows/artifacts-demo.yml
name: Artifacts Demo

on: [push]

jobs:
  build:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Create build artifacts
        run: |
          mkdir -p dist
          echo "Built application v1.0.0" > dist/app.txt
          echo "Build timestamp: $(date)" > dist/build-info.txt
          
      - name: Upload artifacts
        uses: actions/upload-artifact@v3
        with:
          name: build-artifacts
          path: dist/
          
  test:
    needs: build
    runs-on: ubuntu-latest
    
    steps:
      - name: Download artifacts
        uses: actions/download-artifact@v3
        with:
          name: build-artifacts
          path: ./downloaded/
          
      - name: Test artifacts
        run: |
          echo "📦 Testing downloaded artifacts:"
          ls -la ./downloaded/
          cat ./downloaded/app.txt
          cat ./downloaded/build-info.txt

Monitoring and debugging

Logs and debugging

jobs:
  debug:
    runs-on: ubuntu-latest
    
    steps:
      - name: Debug information
        run: |
          echo "🔍 Debug Information:"
          echo "Runner OS: ${{ runner.os }}"
          echo "GitHub Actor: ${{ github.actor }}"
          echo "Repository: ${{ github.repository }}"
          echo "Branch: ${{ github.ref_name }}"
          echo "Commit SHA: ${{ github.sha }}"
          
      - name: Enable debug logging
        run: echo "::debug::This is a debug message"
        
      - name: Set environment variable
        run: echo "CUSTOM_VAR=hello-world" >> $GITHUB_ENV
        
      - name: Use environment variable
        run: echo "Custom variable: $CUSTOM_VAR"

Notifications

jobs:
  notify:
    runs-on: ubuntu-latest
    if: always()
    
    steps:
      - name: Slack notification
        uses: 8398a7/action-slack@v3
        with:
          status: ${{ job.status }}
          channel: '#deployments'
          webhook_url: ${{ secrets.SLACK_WEBHOOK }}
          
      - name: Email notification
        uses: dawidd6/action-send-mail@v3
        with:
          server_address: smtp.gmail.com
          server_port: 465
          username: ${{ secrets.MAIL_USERNAME }}
          password: ${{ secrets.MAIL_PASSWORD }}
          subject: "Build ${{ job.status }}"
          body: "Build completed with status: ${{ job.status }}"
          to: team@company.com

Best Practices

Workflow Organization

.github/
├── workflows/
│   ├── ci.yml              # Tests and validation
│   ├── deploy-staging.yml  # Staging deployment
│   ├── deploy-prod.yml     # Production deployment
│   ├── release.yml         # Release management
│   └── security.yml        # Security scans
├── actions/
│   └── custom-action/      # Custom actions
└── ISSUE_TEMPLATE/         # Issue templates

Performance Optimization

jobs:
  optimized:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v4
      
      # Dependency caching
      - name: Cache Node modules
        uses: actions/cache@v3
        with:
          path: ~/.npm
          key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
          
      # Parallelization
      - name: Install dependencies
        run: npm ci --prefer-offline --no-audit
        
      # Avoid unnecessary steps
      - name: Run tests
        if: github.event_name == 'pull_request'
        run: npm test

Security

# Minimal permissions
permissions:
  contents: read
  
jobs:
  secure:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v4
      
      # Security scan
      - name: Run Trivy vulnerability scanner
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: 'fs'
          scan-ref: '.'
          
      # Secret validation
      - name: Check for secrets
        uses: trufflesecurity/trufflehog@main
        with:
          path: ./

Additional Resources

  • actions/checkout: Check out your code
  • actions/setup-node: Set up Node.js
  • actions/cache: Cache dependencies
  • docker/build-push-action: Build and push Docker images
  • peaceiris/actions-gh-pages: Deploy to GitHub Pages

Marketplace

Documentation

Conclusion

GitHub Actions turns your repository into a complete DevOps platform. By mastering these concepts, you can automate your entire development pipeline, from continuous integration to production deployment.

Congratulations! You have completed this comprehensive course on Git and GitHub. You now have all the skills you need to manage your projects effectively with Git, collaborate on GitHub, and automate your development workflows.


Estimated reading time: 40 minutes
Level: Advanced
Previous step: Git Submodules and Subtrees
🎉 Course complete! You now master Git and GitHub!