⚡ TECH BLOG
Home
Blog
Tags
About
⚡

Powered by Next.js 15 & Modern Web Tech ⚡

Back to Home

CI/CD with GitHub Actions: Complete Guide

September 15, 2022
ci-cdgithub-actionsdevopsautomation
CI/CD with GitHub Actions: Complete Guide

CI/CD with GitHub Actions: Complete Guide

GitHub Actions provides powerful automation capabilities for continuous integration and continuous deployment. This guide will help you master CI/CD workflows from basics to advanced patterns.

Understanding GitHub Actions

Key Concepts

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

Workflow File Location

.github/
└── workflows/
    ├── ci.yml
    ├── deploy.yml
    └── release.yml

Basic Workflow Structure

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

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

jobs:
  build:
    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 tests
        run: npm test
      
      - name: Build
        run: npm run build

Triggers

Push and Pull Request

on:
  push:
    branches: [main]
    paths:
      - 'src/**'
      - 'package.json'
  pull_request:
    branches: [main]
    types: [opened, synchronize, reopened]

Scheduled Workflows

on:
  schedule:
    # Every day at 2 AM UTC
    - cron: '0 2 * * *'
    # 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
      version:
        description: 'Version to deploy'
        required: true

Reusable Workflows

# .github/workflows/called.yml
on:
  workflow_call:
    inputs:
      node-version:
        required: true
        type: string

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ inputs.node-version }}
# .github/workflows/caller.yml
jobs:
  call-workflow:
    uses: ./.github/workflows/called.yml
    with:
      node-version: '20'

Jobs and Dependencies

Sequential Jobs

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - run: npm run build

  test:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - run: npm test

  deploy:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - run: npm run deploy

Parallel Jobs

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - run: npm run lint

  test:
    runs-on: ubuntu-latest
    steps:
      - run: npm test

  type-check:
    runs-on: ubuntu-latest
    steps:
      - run: npm run type-check

  deploy:
    needs: [lint, test, type-check]
    runs-on: ubuntu-latest
    steps:
      - run: npm run deploy

Matrix Strategy

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
        node: [18, 20, 22]
        exclude:
          - os: macos-latest
            node: 18
        include:
          - os: ubuntu-latest
            node: 20
            experimental: true
      fail-fast: false
    
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node }}
      - run: npm ci && npm test

Environment Variables and Secrets

Workflow Environment Variables

env:
  NODE_ENV: test
  CI: true

jobs:
  build:
    runs-on: ubuntu-latest
    env:
      DATABASE_URL: postgres://localhost:5432/test
    steps:
      - run: echo $NODE_ENV

Secrets

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Deploy to production
        env:
          AWS_ACCESS_KEY: ${{ secrets.AWS_ACCESS_KEY }}
          AWS_SECRET_KEY: ${{ secrets.AWS_SECRET_KEY }}
        run: |
          aws s3 sync ./build s3://my-bucket

Storing Secrets

  1. Go to repository Settings
  2. Navigate to Secrets and variables > Actions
  3. Click "New repository secret"
  4. Add name and value

Caching

NPM Cache

steps:
  - uses: actions/checkout@v4
  
  - uses: actions/setup-node@v4
    with:
      node-version: '20'
      cache: 'npm'
  
  - run: npm ci

Custom Cache

steps:
  - uses: actions/checkout@v4
  
  - name: Cache node modules
    uses: actions/cache@v4
    with:
      path: |
        node_modules
        ~/.cache/puppeteer
      key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
      restore-keys: |
        ${{ runner.os }}-node-
  
  - run: npm ci

Cache for Multiple Paths

- uses: actions/cache@v4
  with:
    path: |
      ~/.npm
      ~/.cache
      node_modules
    key: ${{ runner.os }}-deps-${{ hashFiles('**/package-lock.json') }}

Artifacts

Uploading Artifacts

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm run build
      
      - name: Upload build artifacts
        uses: actions/upload-artifact@v4
        with:
          name: build-output
          path: dist/
          retention-days: 5

Downloading Artifacts

jobs:
  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: Download build artifacts
        uses: actions/download-artifact@v4
        with:
          name: build-output
          path: dist/
      
      - run: npm run deploy

Conditional Execution

jobs:
  deploy:
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    steps:
      - run: npm run deploy

  notify:
    runs-on: ubuntu-latest
    if: failure()
    needs: [deploy]
    steps:
      - run: echo "Deployment failed!"

Using Context and Expressions

steps:
  - name: Production only
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    run: npm run deploy:production

  - name: Skip draft PRs
    if: github.event.pull_request.draft == false
    run: npm test

  - name: Conditional with environment
    if: env.DEPLOY_ENV == 'production'
    run: echo "Production deployment"

Deployment Examples

Deploy to GitHub Pages

name: Deploy to GitHub Pages

on:
  push:
    branches: [main]

permissions:
  contents: read
  pages: write
  id-token: write

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm ci
      - run: npm run build
      
      - name: Setup Pages
        uses: actions/configure-pages@v4
      
      - name: Upload artifact
        uses: actions/upload-pages-artifact@v3
        with:
          path: './dist'

  deploy:
    needs: build
    runs-on: ubuntu-latest
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    steps:
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v4

Deploy to AWS

name: Deploy to AWS

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - 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: Login to ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2
      
      - name: Build and push Docker image
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          ECR_REPOSITORY: my-app
          IMAGE_TAG: ${{ github.sha }}
        run: |
          docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
      
      - name: Deploy to ECS
        uses: aws-actions/amazon-ecs-deploy-task-definition@v1
        with:
          task-definition: task-definition.json
          service: my-service
          cluster: my-cluster

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'

Notifications

Slack Notifications

jobs:
  notify:
    runs-on: ubuntu-latest
    if: always()
    steps:
      - name: Notify Slack
        uses: 8398a7/action-slack@v3
        with:
          status: ${{ job.status }}
          fields: repo,message,commit,author,action,eventName,ref,workflow
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}

Best Practices

1. Use Pin Versions

# Good - Pinned version
- uses: actions/checkout@v4

# Better - Full SHA
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11

# Bad - Floating version
- uses: actions/checkout@main

2. Limit Permissions

jobs:
  build:
    permissions:
      contents: read
      pull-requests: write
    runs-on: ubuntu-latest

3. Use Concurrency

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

4. Secure Secrets

# Never echo secrets
- name: Safe secret usage
  env:
    SECRET_TOKEN: ${{ secrets.SECRET_TOKEN }}
  run: |
    # Use secret in command
    curl -H "Authorization: Bearer $SECRET_TOKEN" https://api.example.com

5. Use Reusable Workflows

# .github/workflows/shared-setup.yml
on:
  workflow_call:
    inputs:
      node-version:
        type: string
        default: '20'

jobs:
  setup:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ inputs.node-version }}
          cache: 'npm'
      - run: npm ci

Debugging Workflows

Enable Debug Logging

# Add as secret
ACTIONS_STEP_DEBUG: true
ACTIONS_RUNNER_DEBUG: true

SSH into Runner

- name: SSH Debug
  uses: mxschmitt/action-tmate@v3
  with:
    limit-access-to-actor: true

Conclusion

GitHub Actions provides a powerful, flexible platform for CI/CD automation. By mastering these patterns, you can create robust, maintainable pipelines that improve your development workflow.

Key Takeaways

  • Use matrix strategies for testing multiple configurations
  • Cache dependencies to speed up workflows
  • Store sensitive data in secrets
  • Pin action versions for stability
  • Use reusable workflows for DRY principles

Start automating your workflows today and focus on what matters: writing great code!

Share:

💬 Comments