MervCodes

Tech Reviews From A Programmer

How to Set Up GitHub Actions for CI/CD (Beginner-Friendly Guide)

10 min read

If you're still manually running tests and deploying via SSH, you're burning time you'll never get back. GitHub Actions lets you automate your entire build-test-deploy pipeline directly from your repo — no separate CI server, no Jenkins maintenance headaches, and no third-party accounts. It's built into GitHub, it's free for public repos, and the free tier for private repos (2,000 minutes/month) is generous enough for most teams.

This guide walks you through setting up GitHub Actions from scratch. We'll build a real CI/CD pipeline for a Node.js project — but the concepts apply to any stack.

TL;DR — Key Takeaways

  • GitHub Actions uses YAML workflow files stored in .github/workflows/ in your repo
  • Workflows trigger on events like push, pull_request, or on a schedule
  • The free tier gives you 2,000 CI/CD minutes per month on private repos (unlimited on public repos)
  • You can go from zero to a working CI/CD pipeline in under 15 minutes
  • Use caching and matrix strategies to keep builds fast as your project scales

What Is GitHub Actions and Why Should You Care?

GitHub Actions is GitHub's built-in CI/CD platform that lets you automate workflows directly from your repository. It launched in 2019 and has since become the most popular CI/CD tool for open-source projects, with over 90% of the top 1,000 GitHub repos using it as of 2025.

The key advantage over tools like Jenkins or CircleCI is zero infrastructure. Your workflow definitions live alongside your code, version-controlled and reviewable in PRs. No separate server to maintain, no plugins to update, no security patches to worry about on a Jenkins box.

GitHub Actions is event-driven. Push code? Run tests. Open a PR? Lint and preview. Merge to main? Deploy to production. You define these automations in YAML files, and GitHub handles the rest on hosted runners (Ubuntu, macOS, or Windows).

How Much Does GitHub Actions Cost?

For public repositories, GitHub Actions is completely free with unlimited minutes. For private repos, the free tier includes 2,000 minutes per month on Linux runners. If you need more, the Team plan at $4/user/month bumps that to 3,000 minutes. Most small-to-medium teams never exceed the free tier.

Step 1: Create Your First Workflow File

Every GitHub Actions workflow lives in the .github/workflows/ directory at the root of your repo. Let's create a basic CI pipeline.

Create the file .github/workflows/ci.yml:

name: CI

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

jobs:
  test:
    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"

      - name: Install dependencies
        run: npm ci

      - name: Run linter
        run: npm run lint

      - name: Run tests
        run: npm test

That's it. Commit this file, push it to GitHub, and your pipeline is live. Every push to main and every PR targeting main will trigger this workflow.

Let's break down the key parts:

  • on — Defines the trigger events. Here we run on pushes and PRs to main.
  • jobs — A workflow has one or more jobs. Each job runs on a fresh virtual machine.
  • runs-on — Specifies the runner. ubuntu-latest is the most common (and cheapest).
  • steps — Sequential actions within a job. Each step is either a pre-built action (uses) or a shell command (run).

Step 2: Add Caching to Speed Up Builds

Without caching, npm ci downloads every dependency on every run. For a typical Node.js project, that's 30-90 seconds wasted. Let's fix that.

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

The cache: "npm" option on actions/setup-node automatically caches your ~/.npm directory. In practice, this cuts install times by 50-70% on subsequent runs. For monorepos or larger projects, you can use actions/cache directly for more control.

Step 3: Use a Build Matrix for Multiple Node Versions

If you maintain a library or want to ensure compatibility across Node.js versions, use a matrix strategy:

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

This spins up three parallel jobs — one for each Node version. All three run simultaneously, so your total pipeline time is roughly the same as a single run.

Step 4: Add a Deploy Job

Now let's add continuous deployment. We'll deploy to production only when code is merged to main (not on PRs), and only after tests pass.

name: CI/CD

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

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

  deploy:
    runs-on: ubuntu-latest
    needs: test
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'

    steps:
      - uses: actions/checkout@v4

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

      - run: npm ci
      - run: npm run build

      - name: Deploy to production
        run: |
          npx vercel --prod --token=${{ secrets.VERCEL_TOKEN }}

Key concepts here:

  • needs: test — The deploy job won't start until the test job passes.
  • if condition — Ensures deployment only happens on pushes to main, not on PR checks.
  • secrets.VERCEL_TOKEN — Sensitive values like API keys go in GitHub Secrets (Settings > Secrets and variables > Actions).

If you're deploying to AWS EC2 instead, check out our guide on deploying Node.js apps to EC2 which covers SSH-based deployments and integrates well with GitHub Actions.

Step 5: Store Secrets Securely

Never hardcode credentials in your workflow files. GitHub Secrets are encrypted, only exposed to running workflows, and masked in logs.

To add a secret:

  1. Go to your repo on GitHub
  2. Navigate to Settings > Secrets and variables > Actions
  3. Click New repository secret
  4. Add your key (e.g., VERCEL_TOKEN, AWS_ACCESS_KEY_ID)

Reference them in workflows with ${{ secrets.YOUR_SECRET_NAME }}. For organization-wide secrets, set them at the org level and they'll be available across all repos.

Common Pitfalls and How to Avoid Them

1. Running Out of Minutes

If your pipeline runs on every push to every branch, minutes add up fast. Limit triggers to the branches that matter:

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

2. Slow Pipelines

A CI pipeline that takes 15+ minutes kills developer productivity. Some quick wins:

  • Cache dependencies (as shown above) — saves 30-90 seconds per run
  • Use npm ci instead of npm install — it's faster and deterministic
  • Run jobs in parallel — lint and test can often run as separate parallel jobs
  • Skip unnecessary runs — use path filters to avoid running the full pipeline when only docs change
on:
  push:
    branches: [main]
    paths-ignore:
      - "**.md"
      - "docs/**"

3. Failing to Pin Action Versions

Don't use @main or @latest for actions. Pin to a specific major version or commit SHA:

# Good
- uses: actions/checkout@v4

# Better (pinned to commit SHA)
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683

# Bad - could break without warning
- uses: actions/checkout@main

4. Not Using Environment Protection Rules

For production deployments, enable environment protection rules in GitHub. You can require manual approval before a deploy job runs, restrict which branches can deploy, and add wait timers. Go to Settings > Environments to configure this.

Real-World Workflow: Full-Stack App with Docker

If you're running a full-stack app with Docker (maybe following our Docker Compose for full-stack apps setup), here's a more production-ready workflow:

name: CI/CD

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

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

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

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "20"
          cache: "npm"
      - run: npm ci
      - run: npm test
        env:
          DATABASE_URL: postgresql://postgres:testpass@localhost:5432/testdb

  build-and-push:
    runs-on: ubuntu-latest
    needs: test
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    permissions:
      contents: read
      packages: write

    steps:
      - uses: actions/checkout@v4

      - name: Log in to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push Docker image
        uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest

This workflow spins up a real Postgres service container for integration tests, then builds and pushes a Docker image to GitHub's container registry on merge. The services block is one of GitHub Actions' most underrated features — it lets you run database containers alongside your tests without any Docker Compose setup in CI.

Useful GitHub Actions to Know

Here are the actions we reach for most often:

  • actions/checkout@v4 — Checks out your repo (you'll use this in every workflow)
  • actions/setup-node@v4 — Sets up Node.js with built-in caching
  • actions/cache@v4 — General-purpose caching for any directory
  • docker/build-push-action@v6 — Builds and pushes Docker images
  • github/codeql-action@v3 — Automated security scanning
  • actions/upload-artifact@v4 — Stores build outputs between jobs

For AI-assisted development, tools like Claude Code or GitHub Copilot can generate workflow files for you — but understanding the fundamentals matters for debugging when things go wrong.

Debugging Failed Workflows

When a workflow fails:

  1. Click the failed run in the Actions tab
  2. Expand the failed step to see logs
  3. Use ACTIONS_RUNNER_DEBUG secret set to true for verbose logging
  4. For local testing, use act to run workflows on your machine

If you're working with a team and need help setting up more complex pipelines — multi-environment deployments, custom runners, or infrastructure-as-code integrations — Adaptels offers DevOps consulting for teams that want to ship faster without the trial and error.

Wrapping Up

GitHub Actions removes the friction between writing code and shipping it. Start with a simple test workflow, add caching, then layer on deployment. You don't need to build the perfect pipeline on day one — iterate on it like you would any other piece of code.

The workflow files in this guide are production-ready starting points. Copy them, tweak the Node version and deploy step for your stack, and you'll have CI/CD running in minutes.

Sources

  1. GitHub Actions Documentation — Official docs covering workflows, syntax, and runner specs
  2. GitHub Actions Billing and Usage — Free tier limits and pricing details
  3. actions/setup-node — Official Node.js setup action with built-in caching
  4. Docker Build Push Action — Official Docker action for building and pushing images
  5. GitHub Actions Security Hardening — Best practices for securing your CI/CD pipelines