GeekFleet.Dev
DevOps

GitHub Actions Tutorial: Complete CI/CD Guide for Modern Development

Complete GitHub Actions tutorial from basics to advanced CI/CD pipelines. Learn workflows, testing, deployment automation, and best practices.

PK
Punit Kumar
Senior DevOps Engineer
18 min read
#github-actions#cicd#automation#testing#deployment#workflows#devops

GitHub Actions Tutorial: Complete CI/CD Guide for Modern Development

GitHub Actions has revolutionized how developers build, test, and deploy applications. This comprehensive tutorial will take you from GitHub Actions basics to building sophisticated CI/CD pipelines that can handle any project scale.

What are GitHub Actions?

GitHub Actions is a powerful automation platform integrated directly into GitHub repositories. It enables you to create custom workflows that automatically build, test, package, release, and deploy your code.

Key Benefits

  • Native Integration: Built into GitHub, no external CI/CD setup required
  • Marketplace: 10,000+ pre-built actions from the community
  • Matrix Builds: Test across multiple operating systems and versions
  • Cost-Effective: 2,000 free minutes per month for public repositories
  • Self-Hosted Runners: Run on your own infrastructure
  • Event-Driven: Trigger on any GitHub event

Core Concepts

Component Description Example
Workflow Automated process defined in YAML CI/CD pipeline
Job Set of steps executed on same runner Build, Test, Deploy
Step Individual task within a job Run tests, Build Docker image
Action Reusable unit of code Checkout code, Setup Node.js
Runner Server executing workflows Ubuntu, Windows, macOS

Getting Started with GitHub Actions

Workflow File Structure

GitHub Actions workflows are defined in .github/workflows/ directory using YAML files.

# .github/workflows/basic.yml
name: Basic Workflow

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

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Run a simple command
        run: echo "Hello, GitHub Actions!"

Workflow Triggers

Push and Pull Request Triggers

on:
  push:
    branches: [main, develop]
    paths: ['src/**', 'tests/**']
  pull_request:
    branches: [main]
    types: [opened, synchronize, reopened]

Scheduled Triggers

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

Manual Triggers

on:
  workflow_dispatch:
    inputs:
      environment:
        description: 'Deployment environment'
        required: true
        default: 'staging'
        type: choice
        options:
          - staging
          - production

Building Your First CI/CD Pipeline

Node.js Application Example

# .github/workflows/nodejs.yml
name: Node.js CI/CD

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

jobs:
  test:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [16, 18, 20]

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Setup 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 linting
        run: npm run lint

      - name: Run tests
        run: npm test -- --coverage

      - name: Upload coverage reports
        uses: codecov/codecov-action@v3
        with:
          file: ./coverage/lcov.info
          flags: unittests
          name: codecov-umbrella

  build:
    needs: test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '18'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Build application
        run: npm run build

      - name: Upload build artifacts
        uses: actions/upload-artifact@v4
        with:
          name: build-files
          path: dist/

  deploy:
    needs: build
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    environment: production

    steps:
      - name: Download build artifacts
        uses: actions/download-artifact@v4
        with:
          name: build-files
          path: dist/

      - name: Deploy to production
        run: |
          echo "Deploying to production..."
          # Add your deployment commands here

Python Application Pipeline

# .github/workflows/python.yml
name: Python CI/CD

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

jobs:
  test:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        python-version: ["3.9", "3.10", "3.11", "3.12"]

    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 flake8 pytest pytest-cov
          if [ -f requirements.txt ]; then pip install -r requirements.txt; fi

      - 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 --statistics

      - name: Test with pytest
        run: |
          pytest --cov=src --cov-report=xml

      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v3

Advanced GitHub Actions Patterns

Reusable Workflows

Create reusable workflows to avoid duplication across repositories.

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

on:
  workflow_call:
    inputs:
      environment:
        required: true
        type: string
      image-tag:
        required: true
        type: string
    secrets:
      DEPLOY_TOKEN:
        required: true

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: ${{ inputs.environment }}

    steps:
      - name: Deploy application
        run: |
          echo "Deploying ${{ inputs.image-tag }} to ${{ inputs.environment }}"
          # Deployment logic here
        env:
          DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}

Using Reusable Workflow

# .github/workflows/main.yml
name: Main Pipeline

on:
  push:
    branches: [main]

jobs:
  deploy-staging:
    uses: ./.github/workflows/reusable-deploy.yml
    with:
      environment: staging
      image-tag: ${{ github.sha }}
    secrets:
      DEPLOY_TOKEN: ${{ secrets.STAGING_DEPLOY_TOKEN }}

Custom Actions

Create custom actions for reusable functionality.

# .github/actions/setup-app/action.yml
name: 'Setup Application'
description: 'Setup Node.js and install dependencies'
inputs:
  node-version:
    description: 'Node.js version'
    required: false
    default: '18'
outputs:
  cache-hit:
    description: 'Whether dependencies were cached'
    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: ~/.npm
        key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}

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

Matrix Builds for Cross-Platform Testing

name: Cross-Platform Testing

on: [push, pull_request]

jobs:
  test:
    runs-on: ${{ matrix.os }}

    strategy:
      fail-fast: false
      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 ${{ matrix.node-version }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}

      - name: Install dependencies
        run: npm ci

      - name: Run tests
        run: npm test

      - name: Generate coverage report
        if: matrix.coverage
        run: npm run test:coverage

Docker Integration

Building and Pushing Docker Images

# .github/workflows/docker.yml
name: Docker Build and Push

on:
  push:
    branches: [main]
    tags: ['v*']

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Log into registry ${{ env.REGISTRY }}
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=ref,event=branch
            type=ref,event=pr
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}

      - name: Build and push Docker image
        uses: docker/build-push-action@v5
        with:
          context: .
          platforms: linux/amd64,linux/arm64
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

Multi-Stage Docker Builds

# Dockerfile
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

FROM node:18-alpine AS production
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY . .
EXPOSE 3000
CMD ["npm", "start"]

Security Best Practices

Using Secrets Safely

name: Secure Deployment

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production

    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: ${{ vars.AWS_REGION }}
        run: |
          aws s3 sync ./dist s3://${{ vars.S3_BUCKET_NAME }}

OIDC Authentication

name: Deploy with OIDC

on:
  push:
    branches: [main]

permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
          aws-region: us-east-1

      - name: Deploy to AWS
        run: |
          aws s3 sync ./dist s3://${{ vars.S3_BUCKET_NAME }}

Dependency Scanning

name: Security Scanning

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

jobs:
  security:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Run Trivy vulnerability scanner
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: 'fs'
          scan-ref: '.'
          format: 'sarif'
          output: 'trivy-results.sarif'

      - name: Upload Trivy scan results to GitHub Security tab
        uses: github/codeql-action/upload-sarif@v2
        if: always()
        with:
          sarif_file: 'trivy-results.sarif'

Environment Management

Environment Protection Rules

name: Multi-Environment Deployment

on:
  push:
    branches: [main, develop]

jobs:
  deploy-staging:
    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:
    if: github.ref == 'refs/heads/main'
    needs: [deploy-staging]
    runs-on: ubuntu-latest
    environment:
      name: production
      url: https://myapp.com

    steps:
      - uses: actions/checkout@v4
      - name: Deploy to production
        run: echo "Deploying to production..."

Dynamic Environment URLs

- name: Deploy Preview
  id: deploy
  run: |
    # Deploy logic here
    echo "url=https://pr-${{ github.event.number }}.preview.myapp.com" >> $GITHUB_OUTPUT

- name: Update deployment status
  uses: actions/github-script@v7
  with:
    script: |
      github.rest.repos.createDeploymentStatus({
        owner: context.repo.owner,
        repo: context.repo.repo,
        deployment_id: context.payload.deployment.id,
        state: 'success',
        environment_url: '${{ steps.deploy.outputs.url }}'
      });

Performance Optimization

Caching Strategies

Dependency Caching

- name: Cache dependencies
  uses: actions/cache@v3
  with:
    path: |
      ~/.npm
      ${{ github.workspace }}/.next/cache
    key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }}
    restore-keys: |
      ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-

Build Caching

- name: Cache build output
  uses: actions/cache@v3
  with:
    path: |
      dist
      .next
    key: ${{ runner.os }}-build-${{ github.sha }}
    restore-keys: |
      ${{ runner.os }}-build-

Conditional Job Execution

jobs:
  changes:
    runs-on: ubuntu-latest
    outputs:
      frontend: ${{ steps.changes.outputs.frontend }}
      backend: ${{ steps.changes.outputs.backend }}
    steps:
      - uses: actions/checkout@v4
      - uses: dorny/paths-filter@v2
        id: changes
        with:
          filters: |
            frontend:
              - 'frontend/**'
            backend:
              - 'backend/**'

  test-frontend:
    needs: changes
    if: needs.changes.outputs.frontend == 'true'
    runs-on: ubuntu-latest
    steps:
      - name: Test frontend
        run: echo "Testing frontend..."

  test-backend:
    needs: changes
    if: needs.changes.outputs.backend == 'true'
    runs-on: ubuntu-latest
    steps:
      - name: Test backend
        run: echo "Testing backend..."

Monitoring and Notifications

Slack Notifications

- name: Notify Slack on failure
  if: failure()
  uses: 8398a7/action-slack@v3
  with:
    status: failure
    channel: '#deployments'
    message: |
      Deployment failed for ${{ github.repository }}
      Branch: ${{ github.ref }}
      Commit: ${{ github.sha }}
  env:
    SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}

GitHub Status Checks

- name: Update commit status
  uses: actions/github-script@v7
  with:
    script: |
      github.rest.repos.createCommitStatus({
        owner: context.repo.owner,
        repo: context.repo.repo,
        sha: context.sha,
        state: 'success',
        target_url: 'https://example.com/build/123',
        description: 'Build completed successfully',
        context: 'continuous-integration/github-actions'
      });

Advanced Deployment Patterns

Blue-Green Deployment

name: Blue-Green Deployment

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Determine target environment
        id: target
        run: |
          CURRENT=$(curl -s https://api.myapp.com/health | jq -r '.environment')
          if [ "$CURRENT" == "blue" ]; then
            echo "target=green" >> $GITHUB_OUTPUT
          else
            echo "target=blue" >> $GITHUB_OUTPUT
          fi

      - name: Deploy to ${{ steps.target.outputs.target }}
        run: |
          echo "Deploying to ${{ steps.target.outputs.target }} environment"
          # Deployment logic here

      - name: Run health checks
        run: |
          curl -f https://${{ steps.target.outputs.target }}.myapp.com/health

      - name: Switch traffic
        run: |
          # Update load balancer to point to new environment
          echo "Switching traffic to ${{ steps.target.outputs.target }}"

Canary Deployment

name: Canary Deployment

on:
  push:
    branches: [main]

jobs:
  canary:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Deploy canary (10% traffic)
        run: |
          echo "Deploying canary with 10% traffic"
          # Deploy with 10% traffic

      - name: Monitor metrics
        run: |
          sleep 300  # Wait 5 minutes
          # Check error rates, response times, etc.

      - name: Promote to full deployment
        run: |
          echo "Promoting canary to 100% traffic"
          # Promote if metrics are good

Troubleshooting Common Issues

Debug Actions

- name: Debug information
  run: |
    echo "GitHub context:"
    echo "Event: ${{ github.event_name }}"
    echo "Ref: ${{ github.ref }}"
    echo "SHA: ${{ github.sha }}"
    echo "Actor: ${{ github.actor }}"
    echo "Repository: ${{ github.repository }}"

    echo "Runner information:"
    echo "OS: ${{ runner.os }}"
    echo "Architecture: ${{ runner.arch }}"

    echo "Environment variables:"
    env | sort

Common Error Solutions

1. Permission Denied

# Add required permissions to job
permissions:
  contents: read
  packages: write
  id-token: write

2. Artifact Upload/Download Issues

- name: Upload with proper path
  uses: actions/upload-artifact@v4
  with:
    name: build-artifacts
    path: |
      dist/
      !dist/**/*.map
    retention-days: 30

3. Matrix Build Failures

strategy:
  fail-fast: false  # Continue other jobs if one fails
  matrix:
    include:
      - os: ubuntu-latest
        experimental: false
      - os: windows-latest
        experimental: true

continue-on-error: ${{ matrix.experimental }}

Best Practices Summary

Workflow Organization

  • Use descriptive names for workflows and jobs
  • Organize workflows by trigger type (CI, CD, scheduled)
  • Use reusable workflows to avoid duplication
  • Document complex workflows with comments

Performance

  • Cache dependencies and build outputs
  • Use conditional execution for changed files
  • Minimize checkout depth when possible
  • Use matrix builds efficiently

Security

  • Use OIDC instead of long-lived credentials
  • Store secrets in GitHub Secrets
  • Limit permissions to minimum required
  • Scan for vulnerabilities regularly

Maintenance

  • Pin action versions using commit SHA
  • Update actions regularly with Dependabot
  • Monitor workflow run times and costs
  • Clean up old artifacts automatically

Real-World Example: Full Stack Application

# .github/workflows/fullstack.yml
name: Full Stack CI/CD

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

env:
  NODE_VERSION: '18'
  PYTHON_VERSION: '3.11'

jobs:
  test-frontend:
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: ./frontend

    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'
          cache-dependency-path: frontend/package-lock.json

      - name: Install dependencies
        run: npm ci

      - name: Run tests
        run: npm run test:ci

      - name: Build
        run: npm run build

  test-backend:
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: ./backend

    services:
      postgres:
        image: postgres:13
        env:
          POSTGRES_PASSWORD: postgres
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

    steps:
      - uses: actions/checkout@v4

      - name: Setup Python
        uses: actions/setup-python@v4
        with:
          python-version: ${{ env.PYTHON_VERSION }}
          cache: 'pip'

      - name: Install dependencies
        run: |
          pip install -r requirements.txt

      - name: Run tests
        run: |
          pytest --cov=src --cov-report=xml
        env:
          DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test

  deploy:
    needs: [test-frontend, test-backend]
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment: production

    steps:
      - uses: actions/checkout@v4

      - name: Deploy to production
        run: |
          echo "Deploying full stack application..."
          # Add deployment commands here

Conclusion

GitHub Actions provides a powerful, integrated CI/CD platform that can handle any development workflow. By mastering these patterns and best practices, you'll be able to:

  • Automate entire development lifecycle from code to production
  • Build robust, secure pipelines with proper error handling
  • Scale workflows across multiple environments and platforms
  • Implement advanced deployment strategies like blue-green and canary
  • Monitor and optimize pipeline performance and costs

Next Steps

  1. Start Simple: Begin with basic CI workflows
  2. Iterate and Improve: Add complexity as needed
  3. Monitor Performance: Track workflow execution times
  4. Security First: Implement scanning and secret management
  5. Scale Gradually: Move to advanced patterns as you grow

Need help implementing GitHub Actions for your team? Schedule a consultation to design custom CI/CD pipelines that accelerate your development workflow.

Share this article

Related Articles

Continue your learning journey with these related posts