← Back to articles

How to Set Up CI/CD with GitHub Actions (2026)

GitHub Actions lets you automate testing, building, and deploying your code every time you push. No external CI service needed — it's built into GitHub. Here's a practical setup for web applications in 2026.

What You'll Build

A CI/CD pipeline that:

  1. Runs on every pull request: Lint, type-check, test
  2. Runs on merge to main: Build and deploy to production
  3. Caches dependencies: Fast builds
  4. Notifies on failure: Slack or email alerts

Prerequisites

  • A GitHub repository
  • A web app (Next.js, Astro, or similar)
  • A deployment target (Vercel, Cloudflare, AWS, etc.)

Step 1: Basic CI Workflow

Create .github/workflows/ci.yml:

name: CI

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

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

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

      - run: npm ci

      - name: Lint
        run: npm run lint

      - name: Type Check
        run: npm run typecheck

      - name: Test
        run: npm test

This runs lint, type checking, and tests on every PR and push to main.

Step 2: Add Caching

npm ci is slow without caching. The cache: 'npm' in setup-node handles this, but for more control:

      - uses: actions/cache@v4
        with:
          path: |
            node_modules
            .next/cache
          key: ${{ runner.os }}-nextjs-${{ hashFiles('package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-nextjs-

This caches node_modules and Next.js build cache. Cuts build time by 50-70%.

Step 3: Deploy on Merge

Add a separate deployment job that only runs on main:

name: Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    needs: ci  # Wait for CI to pass
    steps:
      - uses: actions/checkout@v4

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

      - run: npm ci
      - run: npm run build

      # Deploy to Vercel
      - name: Deploy to Vercel
        uses: amondnet/vercel-action@v25
        with:
          vercel-token: ${{ secrets.VERCEL_TOKEN }}
          vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
          vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
          vercel-args: '--prod'

Alternative: Deploy to Cloudflare Pages

      - name: Deploy to Cloudflare
        uses: cloudflare/wrangler-action@v3
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          command: pages deploy ./out --project-name=my-app

Alternative: Deploy to AWS S3 + CloudFront

      - name: Deploy to S3
        uses: jakejarvis/s3-sync-action@v0.5.1
        with:
          args: --delete
        env:
          AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }}
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          SOURCE_DIR: 'out'

Step 4: Add Secrets

Go to your repo → Settings → Secrets and variables → Actions. Add:

  • VERCEL_TOKEN (or equivalent for your platform)
  • Any API keys your build needs

Never put secrets in workflow files. Always use ${{ secrets.NAME }}.

Step 5: Add Status Checks

In your repo settings → Branches → Branch protection rules:

  1. Require status checks to pass before merging
  2. Select your CI workflow
  3. Now PRs can't merge if CI fails

Step 6: Matrix Testing (Optional)

Test across multiple Node versions or operating systems:

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

Step 7: Slack Notifications (Optional)

Add failure notifications:

      - name: Notify Slack on Failure
        if: failure()
        uses: 8398a7/action-slack@v3
        with:
          status: failure
          channel: '#deployments'
          text: 'CI failed on ${{ github.ref }}'
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}

Complete CI/CD Workflow

Here's everything combined:

name: CI/CD

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

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

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

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

      - run: npm ci

      - name: Lint
        run: npm run lint

      - name: Type Check
        run: npx tsc --noEmit

      - name: Test
        run: npm test -- --coverage

      - name: Build
        run: npm run build

  deploy:
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    needs: ci
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

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

      - run: npm ci
      - run: npm run build

      - name: Deploy
        uses: amondnet/vercel-action@v25
        with:
          vercel-token: ${{ secrets.VERCEL_TOKEN }}
          vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
          vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
          vercel-args: '--prod'

Key features:

  • concurrency cancels in-progress runs when new commits push (saves minutes)
  • Deploy only runs on push to main (not on PRs)
  • Build runs in CI to catch build errors before deploy

Common Issues & Fixes

"npm ci" fails

Make sure package-lock.json is committed. npm ci requires it.

Slow builds

Add caching (Step 2). Also consider running lint and test in parallel using separate jobs.

Secret not found

Double-check the secret name matches exactly (case-sensitive). Secrets aren't available in forked PRs.

Workflow doesn't trigger

Check the on: section. Common mistake: indentation errors in YAML.

GitHub Actions Pricing

PlanIncluded MinutesExtra Cost
Free2,000 min/monthN/A
Team3,000 min/month$0.008/min
Enterprise50,000 min/month$0.008/min

For most projects, the free tier is more than enough. A typical CI run takes 2-5 minutes.

FAQ

Should I use GitHub Actions or a separate CI service?

For most projects, GitHub Actions is sufficient and free. Consider CircleCI or BuildKite if you need advanced features (GPU runners, massive parallelism).

How do I debug a failing workflow?

Click the failed run in the Actions tab. Expand the failing step. Add - run: env to see environment variables. Use actions/upload-artifact to save logs.

Can I run workflows locally?

Yes, use act (https://github.com/nektos/act). Runs GitHub Actions locally in Docker. Great for debugging.

How do I handle monorepos?

Use path filters: on: push: paths: ['packages/api/**'] to only run when specific paths change.

Bottom Line

GitHub Actions gives you free CI/CD that's deeply integrated with your GitHub workflow. Start with the basic CI workflow (Step 1), add deployment (Step 3), and iterate. Most web apps need under 50 lines of YAML to have a complete CI/CD pipeline.

Get AI tool guides in your inbox

Weekly deep-dives on the best AI coding tools, automation platforms, and productivity software.