πŸ“ Code πŸ”¨ Build πŸ§ͺ Test πŸš€ Deploy ⚑ GitHub Actions
DevOps

Setting Up CI/CD with GitHub Actions

Mayur Dabhi
Mayur Dabhi
March 19, 2026
22 min read

In modern software development, manually testing, building, and deploying your code is not just inefficientβ€”it's a recipe for errors and delays. CI/CD (Continuous Integration/Continuous Deployment) automates these processes, ensuring that every code change is automatically validated, tested, and deployed. And with GitHub Actions, setting up powerful automation pipelines has never been easier.

Whether you're working on a solo project or collaborating with a large team, GitHub Actions provides a flexible, powerful, and free way to automate your development workflow directly within your repository. In this comprehensive guide, we'll walk through everything you need to know to set up professional CI/CD pipelines from scratch.

What You'll Learn
  • Understanding CI/CD concepts and why they matter
  • GitHub Actions core concepts: workflows, jobs, and steps
  • Creating your first workflow from scratch
  • Setting up automated testing pipelines
  • Building and deploying applications automatically
  • Working with secrets and environment variables
  • Advanced patterns: matrix builds, caching, and artifacts
  • Real-world workflow examples for different tech stacks

Understanding CI/CD

Before diving into GitHub Actions, let's clarify what CI/CD actually means and why it's become essential for modern development teams.

Continuous Integration (CI)

Automatically build and test code changes when developers push to the repository. Catches bugs early and ensures code quality.

Continuous Deployment (CD)

Automatically deploy validated code to staging or production environments. Reduces manual work and speeds up release cycles.

The CI/CD Pipeline Flow Continuous Integration πŸ‘¨β€πŸ’» Push Code πŸ”¨ Build πŸ§ͺ Test βœ… Code passes all tests β†’ Ready to deploy ❌ Tests fail β†’ Alert developer, block merge Continuous Deployment 🎭 Staging πŸš€ Production πŸ”„ Automatic deployment to servers πŸ“Š Monitoring & rollback ready πŸ‘₯ Users

CI/CD automates the entire journey from code push to production deployment

Benefits of CI/CD

Benefit Description Impact
πŸ› Early Bug Detection Automated tests catch issues immediately after code changes 80% reduction in production bugs
⚑ Faster Releases Automated deployment eliminates manual steps Deploy multiple times per day
🀝 Better Collaboration Every PR is validated automatically before merge Confident code reviews
πŸ“ Consistent Process Same build/test/deploy process every time Eliminates "works on my machine"
↩️ Easy Rollbacks Versioned deployments with quick rollback capability Minimal downtime on issues

Introduction to GitHub Actions

GitHub Actions is GitHub's built-in CI/CD platform that allows you to automate workflows directly in your repository. Unlike external CI/CD tools, GitHub Actions is deeply integrated with GitHub, making it incredibly convenient for any project hosted on GitHub.

Why GitHub Actions?
  • Free for public repos - Unlimited minutes for open source projects
  • 2,000 minutes/month free - For private repos on free tier
  • Native integration - Works seamlessly with PRs, issues, and releases
  • Marketplace - Thousands of pre-built actions to use
  • Matrix builds - Test across multiple OS/versions simultaneously

Core Concepts

Before writing your first workflow, let's understand the key terminology:

GitHub Actions Structure πŸ“‹ Workflow (YAML file) .github/workflows/ci.yml ⚑ Trigger Event push, pull_request πŸ–₯️ Job: test runs-on: ubuntu-latest Step 1: Checkout code Step 2: Setup Node.js Step 3: Run tests πŸš€ Job: deploy needs: test Step 1: Build app Step 2: Deploy βœ… Complete!

A workflow contains jobs, jobs contain steps, and steps run commands or actions

Creating Your First Workflow

Let's create a simple CI workflow that runs tests whenever code is pushed to the repository. We'll start with a basic example and build up from there.

Step 1: Create the Workflow Directory

GitHub Actions workflows live in a special directory in your repository:

your-project/
.github/
workflows/
ci.yml
deploy.yml
src/
package.json

Step 2: Write Your First Workflow

Create a file at .github/workflows/ci.yml with the following content:

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

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

jobs:
  test:
    # The type of runner the job will run on
    runs-on: ubuntu-latest

    steps:
      # Check out the repository code
      - name: Checkout code
        uses: actions/checkout@v4

      # Set up Node.js environment
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      # Install dependencies
      - name: Install dependencies
        run: npm ci

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

      # Run linting
      - name: Run linter
        run: npm run lint
Understanding the Workflow

Let's break down each section:

  • name - Display name shown in GitHub UI
  • on - Events that trigger the workflow (push, PR, schedule, etc.)
  • jobs - One or more jobs that run in parallel (or sequentially with needs)
  • runs-on - The virtual machine type (ubuntu, windows, macos)
  • steps - Sequential list of commands or actions to execute
  • uses - Use a pre-built action from the marketplace
  • run - Execute a shell command

Multi-Job Workflows

Real-world pipelines often need multiple jobs: one for testing, one for building, and one for deployment. Jobs can run in parallel or depend on each other.

.github/workflows/full-pipeline.yml
name: Full CI/CD Pipeline

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

jobs:
  # Job 1: Run tests
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      - run: npm ci
      - run: npm test

  # Job 2: Build the application
  build:
    runs-on: ubuntu-latest
    needs: test  # Waits for test job to complete
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      - run: npm ci
      - run: npm run build
      
      # Upload build artifacts
      - name: Upload build artifacts
        uses: actions/upload-artifact@v4
        with:
          name: build-output
          path: dist/

  # Job 3: Deploy to production
  deploy:
    runs-on: ubuntu-latest
    needs: build
    if: github.ref == 'refs/heads/main'  # Only deploy on main
    steps:
      - name: Download build artifacts
        uses: actions/download-artifact@v4
        with:
          name: build-output
          path: dist/

      - name: Deploy to server
        run: |
          echo "Deploying to production..."
          # Add your deployment commands here
Job Dependencies Flow πŸ§ͺ test needs: test πŸ”¨ build needs: build πŸš€ deploy if: github.ref == 'main'

Jobs run sequentially when they depend on each other using the needs keyword

Working with Secrets

Never hardcode sensitive data like API keys or passwords in your workflow files. GitHub provides encrypted secrets that are securely injected at runtime.

1

Add Secrets in Repository Settings

Go to Settings β†’ Secrets and variables β†’ Actions β†’ New repository secret

2

Use Secrets in Workflows

Access secrets using the ${{ secrets.SECRET_NAME }} syntax

Using secrets in deployment
name: Deploy with Secrets

on:
  push:
    branches: [ main ]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Deploy to server
        env:
          SERVER_HOST: ${{ secrets.SERVER_HOST }}
          SSH_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
          DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
        run: |
          echo "$SSH_KEY" > key.pem
          chmod 600 key.pem
          scp -i key.pem -r ./dist/* user@$SERVER_HOST:/var/www/app/
          rm key.pem

      - name: Notify deployment
        env:
          SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
        run: |
          curl -X POST -H 'Content-type: application/json' \
            --data '{"text":"Deployment successful! πŸš€"}' \
            $SLACK_WEBHOOK
Security Best Practices
  • Never echo or log secret values
  • Use environment-specific secrets for staging vs production
  • Rotate secrets regularly and after team member changes
  • Limit secret access to necessary workflows only
  • Use GITHUB_TOKEN for GitHub API operations (auto-provided)

Matrix Builds

Matrix builds allow you to test your code across multiple versions, operating systems, or configurations simultaneously. This is incredibly powerful for ensuring cross-platform compatibility.

Matrix build example
name: Matrix CI

on: [push, pull_request]

jobs:
  test:
    runs-on: ${{ matrix.os }}
    
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
        node-version: [18, 20, 22]
        exclude:
          # Skip Node 18 on Windows to save time
          - os: windows-latest
            node-version: 18
      fail-fast: false  # Don't cancel other jobs if one fails

    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'
      
      - run: npm ci
      - run: npm test

      - name: Report results
        run: |
          echo "βœ… Tests passed on ${{ matrix.os }} with Node ${{ matrix.node-version }}"

This configuration creates 8 parallel jobs (3 OS Γ— 3 Node versions, minus 1 excluded combination), testing your code across all combinations simultaneously!

Matrix Build Visualization Node 18 Node 20 Node 22 🐧 Ubuntu βœ“ Running βœ“ Running βœ“ Running πŸͺŸ Windows ⊘ Excluded βœ“ Running βœ“ Running 🍎 macOS βœ“ Running βœ“ Running βœ“ Running 8 parallel jobs

Matrix builds test all combinations in parallel, catching platform-specific bugs early

Caching for Faster Builds

Caching dependencies can dramatically speed up your workflows. Instead of downloading packages on every run, cache them between workflow runs.

Caching dependencies
name: CI with Caching

on: [push, pull_request]

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

      # Method 1: Built-in cache in setup-node
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'  # Automatically caches node_modules

      # Method 2: Manual cache action (more control)
      - name: Cache node modules
        uses: actions/cache@v4
        with:
          path: ~/.npm
          key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-node-

      - run: npm ci
      - run: npm run build
      - run: npm test

Cache Performance Impact

With proper caching, you can reduce build times significantly:

  • Node.js projects: 30-60% faster (npm/yarn cache)
  • Docker builds: 50-80% faster (layer caching)
  • Compiled languages: 40-70% faster (build cache)

Real-World Workflow Examples

Let's look at complete workflow examples for different technology stacks.

React/Next.js Deployment

name: Deploy Next.js to Vercel

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: '20'
          cache: 'npm'

      - run: npm ci
      - run: npm run lint
      - run: npm run test
      - run: 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'

Node.js API with PostgreSQL

name: API CI/CD

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    
    services:
      postgres:
        image: postgres:15
        env:
          POSTGRES_PASSWORD: postgres
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 5432:5432

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

      - run: npm ci
      - run: npm run migrate
        env:
          DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test
      - run: npm test
        env:
          DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test

Docker Build & Push

name: Docker Build & Push

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

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

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

      - name: Login to DockerHub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: user/app:latest,user/app:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

Best Practices & Tips

Security Best Practices

  • Pin action versions - Use actions/checkout@v4.1.1 instead of @v4 or @main
  • Audit third-party actions - Review source code before using marketplace actions
  • Use GITHUB_TOKEN - Prefer built-in token over personal access tokens
  • Limit token permissions - Set minimum required permissions in workflow
  • Environment protection rules - Require approvals for production deployments

Performance Optimization

  • Use caching aggressively - Cache dependencies, build outputs, and Docker layers
  • Run jobs in parallel - Independent jobs run simultaneously by default
  • Use matrix strategy wisely - Exclude unnecessary combinations
  • Fail fast - Use fail-fast: true to cancel on first failure
  • Choose the right runner - ubuntu-latest is fastest and cheapest

Debugging Workflows

  • Enable debug logging - Set ACTIONS_RUNNER_DEBUG secret to true
  • Use step outputs - Print variables with echo "value=$VALUE" >> $GITHUB_OUTPUT
  • SSH debugging - Use mxschmitt/action-tmate for interactive debugging
  • Check run artifacts - Upload logs and reports as artifacts for inspection
  • Use act locally - Test workflows locally with nektos/act tool

Conclusion

GitHub Actions transforms how we build, test, and deploy software. With workflows defined as code, living alongside your application in the repository, you get full transparency, version control, and automation superpowers.

We've covered everything from basic concepts to advanced patterns like matrix builds, caching, and multi-environment deployments. The key takeaways are:

Key Takeaways
  • Start simple - Begin with basic test automation, then add complexity
  • Use the marketplace - Don't reinvent the wheel; leverage existing actions
  • Cache everything - Caching dramatically improves build times
  • Secure your secrets - Never expose sensitive data in logs or code
  • Monitor and iterate - Review workflow runs and optimize over time

Now it's your turn! Create a .github/workflows/ directory in your repository, add your first workflow, and watch the magic happen. Happy automating! πŸš€

CI/CD GitHub Actions Automation DevOps Workflows
Mayur Dabhi

Mayur Dabhi

Full-stack developer passionate about clean code, automation, and building great developer experiences. I write about web development, DevOps, and software engineering best practices.