GitHub Actions CI/CD: Automate Your Build, Test, and Deploy Pipeline

GitHub Actions CI/CD: Automate Your Build, Test, and Deploy Pipeline


GitHub Actions is a CI/CD platform built directly into GitHub. Every time you push code, open a pull request, or create a release, Actions can automatically build your project, run tests, and deploy to production. It’s free for public repositories and includes 2,000 minutes/month for private repos.

How GitHub Actions Works

Workflows are defined in YAML files inside .github/workflows/ in your repository. Each workflow contains:

  • Triggers — Events that start the workflow (push, PR, schedule, manual)
  • Jobs — Groups of steps that run on a virtual machine
  • Steps — Individual commands or actions to execute

Your First Workflow

Create .github/workflows/ci.yml:

name: CI

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

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        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 linter
        run: npm run lint

      - name: Run tests
        run: npm test

      - name: Build
        run: npm run build

Push this file and GitHub will automatically run the workflow on every push to main and on every pull request.

Workflow Triggers

Push and Pull Request

on:
  push:
    branches: [main, develop]
    paths:
      - 'src/**'
      - 'package.json'
  pull_request:
    branches: [main]

Scheduled (Cron)

on:
  schedule:
    - cron: '0 3 * * *'  # Daily at 3 AM UTC

Manual Dispatch

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

Trigger manually from the Actions tab in GitHub.

Release

on:
  release:
    types: [published]

Real-World Examples

Node.js with Testing and Coverage

name: Node.js CI

on:
  push:
    branches: [main]
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest

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

    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'

      - run: npm ci
      - run: npm test -- --coverage

      - name: Upload coverage
        if: matrix.node-version == 20
        uses: actions/upload-artifact@v4
        with:
          name: coverage
          path: coverage/

This tests against Node.js 18, 20, and 22 simultaneously using a matrix strategy.

Build and Push Docker Image

name: Docker Build

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

jobs:
  docker:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

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

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

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

Deploy Static Site to Cloudflare Pages

name: Deploy to Cloudflare Pages

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 build

      - name: Deploy to Cloudflare Pages
        uses: cloudflare/wrangler-action@v3
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          command: pages deploy dist --project-name=my-site

Deploy to a VPS via SSH

name: Deploy to VPS

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Deploy via SSH
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            cd /var/www/myapp
            git pull origin main
            npm ci --production
            pm2 restart myapp

Secrets and Environment Variables

Store sensitive data in GitHub Secrets:

  1. Go to repository SettingsSecrets and variablesActions
  2. Click New repository secret
  3. Add secrets like DOCKER_TOKEN, SSH_PRIVATE_KEY, API_KEY

Use them in workflows:

env:
  API_URL: https://api.example.com

steps:
  - name: Deploy
    env:
      API_KEY: ${{ secrets.API_KEY }}
    run: curl -H "Authorization: $API_KEY" $API_URL/deploy

Caching Dependencies

Speed up workflows by caching dependencies:

- name: Setup Node.js
  uses: actions/setup-node@v4
  with:
    node-version: '20'
    cache: 'npm'  # Built-in npm caching

For custom caching:

- name: Cache pip packages
  uses: actions/cache@v4
  with:
    path: ~/.cache/pip
    key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }}
    restore-keys: |
      ${{ runner.os }}-pip-

Job Dependencies and Conditions

Sequential Jobs

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

  deploy:
    needs: test  # Only runs after 'test' succeeds
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'  # Only on main branch
    steps:
      - run: echo "Deploying..."

Conditional Steps

steps:
  - name: Deploy to staging
    if: github.event_name == 'pull_request'
    run: deploy-staging.sh

  - name: Deploy to production
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    run: deploy-production.sh

  - name: Notify on failure
    if: failure()
    run: curl -X POST $SLACK_WEBHOOK -d '{"text":"Build failed!"}'

Reusable Workflows

Create shared workflows that other repos can call:

.github/workflows/reusable-deploy.yml:

name: Reusable Deploy

on:
  workflow_call:
    inputs:
      environment:
        required: true
        type: string
    secrets:
      deploy-key:
        required: true

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Deploying to ${{ inputs.environment }}"

Call it from another workflow:

jobs:
  deploy:
    uses: org/repo/.github/workflows/reusable-deploy.yml@main
    with:
      environment: production
    secrets:
      deploy-key: ${{ secrets.DEPLOY_KEY }}

Useful Actions from the Marketplace

ActionPurpose
actions/checkout@v4Checkout repository code
actions/setup-node@v4Set up Node.js
actions/cache@v4Cache dependencies
actions/upload-artifact@v4Save build artifacts
docker/build-push-action@v5Build and push Docker images
cloudflare/wrangler-action@v3Deploy to Cloudflare
appleboy/ssh-action@v1Run commands via SSH
peaceiris/actions-gh-pages@v3Deploy to GitHub Pages
slackapi/slack-github-action@v1Send Slack notifications

Browse more at github.com/marketplace?type=actions.

Debugging Workflows

Enable debug logging

Add a repository secret ACTIONS_STEP_DEBUG with value true for verbose output.

Re-run failed jobs

Click Re-run jobs in the Actions tab. You can re-run only failed jobs to save time.

Local testing with act

Test workflows locally using act:

brew install act    # macOS
act                 # Run default workflow
act -j test         # Run specific job
act pull_request    # Simulate PR event

Pricing

PlanMinutes/monthRunners
Free2,000Linux only
Team3,000Linux, Windows, macOS
Enterprise50,000All + self-hosted

Public repositories get unlimited free minutes.

Conclusion

GitHub Actions eliminates the need for separate CI/CD tools like Jenkins or CircleCI. With workflows defined as code in your repository, every team member can see, modify, and review the automation pipeline. Start with a basic build-and-test workflow, then gradually add deployment, notifications, and scheduled tasks as your project grows.