DevOps

Automating Your CI/CD Pipeline with GitHub Actions

Tech Setup2 min read
TS

Tech Setup

Reviewed July 27, 2026

Automating Your CI/CD Pipeline with GitHub Actions

Introduction

Continuous Integration and Continuous Deployment (CI/CD) is the practice of automatically building, testing, and deploying code changes. Every time a developer pushes to the main branch, the pipeline runs the test suite, builds the application, and deploys it to production — without manual intervention.

GitHub Actions is GitHub's built-in CI/CD platform. It is tightly integrated with your repository, supports any language or framework, and has a massive marketplace of pre-built actions. In this guide, you will build a complete CI/CD pipeline from scratch.

How GitHub Actions Works

GitHub Actions uses YAML workflow files stored in .github/workflows/. Each workflow is triggered by an event (push, pull request, schedule, etc.) and runs a series of jobs. Each job runs on a virtual machine (runner) and executes a sequence of steps.

Event → Workflow → Job → Steps → Actions

Step 1: Create Your First Workflow

Create the directory structure:

mkdir -p .github/workflows

Create .github/workflows/ci.yml:

name: CI

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

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

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

      - name: Install dependencies
        run: npm ci

      - name: Run linting
        run: npm run lint

      - name: Run tests
        run: npm test

      - name: Build
        run: npm run build

Push this file and watch the workflow run in the Actions tab of your repository.

Step 2: Add Caching

Caching dependencies speeds up your pipeline significantly. GitHub Actions can cache npm, pip, or any package manager:

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

Or use the built-in caching in setup-node:

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

Step 3: Add Docker Build and Push

If your application uses Docker, build and push the image on every merge to main:

  build-and-push:
    needs: test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4

      - name: Log in to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          push: true
          tags: ${{ secrets.DOCKER_USERNAME }}/myapp:latest,${{ secrets.DOCKER_USERNAME }}/myapp:${{ github.sha }}

Store your Docker credentials as repository secrets in Settings → Secrets → Actions.

Step 4: Deploy to Production

Add a deployment job that runs after the build:

  deploy:
    needs: build-and-push
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    steps:
      - name: Deploy to server
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SERVER_SSH_KEY }}
          script: |
            cd /opt/myapp
            docker compose pull
            docker compose up -d --remove-orphans
            docker image prune -f

Step 5: Add Notifications

Get notified when the pipeline fails:

  notify:
    needs: [test, build-and-push, deploy]
    if: failure()
    runs-on: ubuntu-latest
    steps:
      - name: Send Slack notification
        uses: slackapi/slack-github-action@v1
        with:
          payload: |
            {
              "text": "CI/CD failed for ${{ github.repository }}: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
            }
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}

Step 6: Environment-Specific Deployments

Use branches to control which environment gets deployed:

name: Deploy

on:
  push:
    branches: [main, staging]

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: ${{ github.ref == 'refs/heads/main' && 'production' || 'staging' }}
    steps:
      - name: Deploy
        run: |
          echo "Deploying to ${{ github.ref_name }}"
          # Deploy based on branch

Configure environments in Settings → Environments with required reviewers for production.

Advanced Patterns

Reusable Workflows

Extract common steps into reusable workflows:

# .github/workflows/reusable-test.yml
name: Reusable Test
on:
  workflow_call:
    inputs:
      node-version:
        type: string
        default: '20'

jobs:
  test:
    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
      - run: npm test

Use it in other workflows:

jobs:
  test:
    uses: ./.github/workflows/reusable-test.yml
    with:
      node-version: '20'

Matrix Builds

Test across multiple versions:

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [18, 20, 22]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: npm
      - run: npm ci
      - run: npm test

Conclusion

GitHub Actions makes CI/CD accessible to every developer. With a single YAML file, you can automate testing, building, deployment, and notifications. Start with the basic CI workflow, then incrementally add Docker builds, deployment, and environment-specific configurations. The time you invest in setting up CI/CD will pay for itself within the first week.