How to Set Up a CI/CD Pipeline with GitHub Actions

Setting up continuous integration and continuous deployment (CI/CD) is essential for modern software development. GitHub Actions provides a powerful, integrated platform for automating your build, test, and deployment workflows directly within your repository. This guide will walk you through creating a production-ready CI/CD pipeline from scratch.

Understanding GitHub Actions

GitHub Actions is a CI/CD platform that allows you to automate your software development workflows. Every push, pull request, or other GitHub event can trigger automated workflows that build, test, and deploy your code.

Key Concepts

Before diving in, let’s understand the core components:

  • Workflow: An automated process defined in YAML that runs in your repository
  • Job: A set of steps that execute on the same runner
  • Step: An individual task that can run commands or actions
  • Action: A reusable unit of code that can be shared across workflows
  • Runner: A server that runs your workflows (GitHub-hosted or self-hosted)

Prerequisites

To follow this tutorial, you’ll need:

  • A GitHub account and repository
  • Basic knowledge of YAML syntax
  • A project with tests (we’ll use Node.js as an example)
  • Understanding of your deployment target (cloud platform, server, etc.)

Step 1: Create Your First Workflow

Create a .github/workflows directory in your repository root and add a workflow file:

mkdir -p .github/workflows
touch .github/workflows/ci.yml

Here’s a basic CI workflow for a Node.js application:

name: CI Pipeline

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

jobs:
  build-and-test:
    runs-on: ubuntu-latest
    
    strategy:
      matrix:
        node-version: [16.x, 18.x, 20.x]
    
    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: Build application
      run: npm run build

This workflow:

  • Triggers on pushes to main and develop branches and on pull requests
  • Tests against multiple Node.js versions using a matrix strategy
  • Runs linting, tests, and builds your application

Step 2: Add Code Quality Checks

Enhance your pipeline with code quality tools:

    - name: Run tests with coverage
      run: npm test -- --coverage
    
    - name: Upload coverage to Codecov
      uses: codecov/codecov-action@v3
      with:
        token: ${{ secrets.CODECOV_TOKEN }}
        files: ./coverage/coverage-final.json
        fail_ci_if_error: true

Pro Tip: Code coverage tools help identify untested code paths and improve overall code quality.

Step 3: Implement Deployment

Add deployment to your workflow. Here’s an example deploying to AWS:

  deploy:
    needs: build-and-test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    
    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: Deploy to AWS
      run: |
        npm ci
        npm run build
        aws s3 sync ./build s3://my-app-bucket --delete
        [aws cloudfront](https://terabyte.systems/posts/how-to-deploy-react-app-to-aws-s3-cloudfront/) create-invalidation --distribution-id ${{ secrets.CLOUDFRONT_DIST_ID }} --paths "/*"        

Step 4: Manage Secrets Securely

Never commit credentials to your repository. Use GitHub Secrets:

  1. Navigate to your repository settings
  2. Select Secrets and variablesActions
  3. Click New repository secret
  4. Add your secrets (API keys, credentials, tokens)

Reference secrets in your workflow using ${{ secrets.SECRET_NAME }}.

Step 5: Optimize Your Pipeline

Caching Dependencies

Speed up your workflows by caching dependencies:

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

Conditional Execution

Run steps only when needed:

    - name: Deploy to production
      if: github.ref == 'refs/heads/main'
      run: npm run deploy
    
    - name: Deploy to staging
      if: github.ref == 'refs/heads/develop'
      run: npm run deploy:staging

Advanced Patterns

Multi-Stage Pipeline

Separate build, test, and deployment into distinct jobs:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm run build
      - uses: actions/upload-artifact@v3
        with:
          name: build-artifact
          path: ./build
  
  test:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/download-artifact@v3
        with:
          name: build-artifact
      - run: npm test
  
  deploy:
    needs: [build, test]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/download-artifact@v3
      - run: ./deploy.sh

Reusable Workflows

Create reusable workflows for common patterns:

# .github/workflows/reusable-deploy.yml
on:
  workflow_call:
    inputs:
      environment:
        required: true
        type: string

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: ${{ inputs.environment }}
    steps:
      - uses: actions/checkout@v4
      - run: ./deploy.sh ${{ inputs.environment }}

Call it from another workflow:

jobs:
  deploy-staging:
    uses: ./.github/workflows/reusable-deploy.yml
    with:
      environment: staging

Comparison: GitHub Actions vs Other CI/CD Tools

FeatureGitHub ActionsJenkinsGitLab CICircleCI
IntegrationNative GitHubSelf-hostedNative GitLabThird-party
CostFree tier generousFree (self-hosted)Free tier availableLimited free tier
Setup ComplexityLowHighLowMedium
Matrix BuildsYesWith pluginsYesYes
MarketplaceExtensivePluginsLimitedOrbs

Monitoring and Debugging

View Workflow Runs

Monitor your workflows in the Actions tab of your repository. You can:

  • View real-time logs
  • Re-run failed jobs
  • Download artifacts
  • Cancel running workflows

Debug Mode

Enable debug logging for troubleshooting:

    - name: Debug step
      run: echo "Debugging value: ${{ secrets.DEBUG_VALUE }}"
      env:
        ACTIONS_STEP_DEBUG: true

Best Practices

  1. Keep workflows fast: Use caching, parallel jobs, and efficient commands
  2. Fail fast: Run quick checks (linting) before expensive operations (building)
  3. Use matrix strategies: Test across multiple environments simultaneously
  4. Secure your secrets: Never log or expose sensitive information
  5. Version your actions: Pin action versions for reproducibility (actions/checkout@v4)
  6. Monitor workflow usage: GitHub Actions has usage limits; optimize to stay within quotas
  7. Use branch protection: Require CI checks to pass before merging

Common Pitfalls and Solutions

Problem: Workflows run too long

Solution: Implement caching, use matrix strategies wisely, and parallelize independent jobs.

Problem: Intermittent test failures

Solution: Use retry mechanisms and ensure tests are deterministic (avoid time-based tests).

Problem: Secrets not available

Solution: Verify secrets are set at the repository level and referenced correctly.

Conclusion

Setting up a CI/CD pipeline with GitHub Actions streamlines your development workflow, catches bugs early, and automates deployment. Start simple with a basic build-and-test workflow, then gradually add complexity as your needs grow.

The native integration with GitHub makes Actions particularly powerful—no third-party authentication, seamless PR integration, and a vast marketplace of pre-built actions. Whether you’re deploying a simple static site or orchestrating complex microservices, GitHub Actions provides the flexibility and power you need.

As you build more workflows, explore the GitHub Actions marketplace for community-maintained actions that can save you time and effort. Remember: the best CI/CD pipeline is one that’s reliable, fast, and continuously improving alongside your project.

Thank you for reading! If you have any feedback or comments, please send them to [email protected].