github-actions

GitHub Actions Node.js Tests

GitHub Actions workflow to run Node.js tests across multiple versions with caching, coverage reports, and status checks.

Overview

A GitHub Actions workflow for running Node.js tests on every push and pull request. Tests across multiple Node.js versions, caches dependencies, uploads coverage reports, and provides clear status checks for pull request reviews.

Configuration

# .github/workflows/test.yml

name: Tests

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

# Cancel previous runs for the same branch/PR
concurrency:
  group: test-${{ github.ref }}
  cancel-in-progress: true

jobs:
  test:
    name: Node ${{ matrix.node-version }}
    runs-on: ubuntu-latest

    strategy:
      fail-fast: false                 # Don't cancel other versions if one fails
      matrix:
        node-version: [18, 20, 22]     # Test across LTS and current versions

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup 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 linting
        run: npm run lint

      - name: Run type checking
        run: npx tsc --noEmit

      - name: Run tests with coverage
        run: npm test -- --ci --coverage --reporters=default --reporters=jest-junit
        env:
          JEST_JUNIT_OUTPUT_DIR: ./reports
          JEST_JUNIT_OUTPUT_NAME: test-results.xml

      - name: Upload coverage to Codecov
        if: matrix.node-version == 20   # Only upload once
        uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}
          files: ./coverage/lcov.info
          fail_ci_if_error: false

      - name: Upload test results
        if: always()                    # Upload even if tests fail
        uses: actions/upload-artifact@v4
        with:
          name: test-results-node-${{ matrix.node-version }}
          path: |
            ./reports/
            ./coverage/
          retention-days: 7

  # Separate job for integration tests
  integration:
    name: Integration Tests
    runs-on: ubuntu-latest
    needs: test                        # Run after unit tests pass

    services:
      postgres:
        image: postgres:16-alpine
        env:
          POSTGRES_USER: test
          POSTGRES_PASSWORD: test
          POSTGRES_DB: testdb
        ports:
          - 5432:5432
        options: >-
          --health-cmd "pg_isready -U test"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

      redis:
        image: redis:7-alpine
        ports:
          - 6379:6379
        options: >-
          --health-cmd "redis-cli ping"
          --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

      - name: Run integration tests
        run: npm run test:integration
        env:
          DATABASE_URL: postgresql://test:test@localhost:5432/testdb
          REDIS_URL: redis://localhost:6379

Key Options Explained

  • fail-fast: false — If Node 18 tests fail, Node 20 and 22 tests still run. Useful for identifying version-specific issues.
  • cache: npm — Caches the npm global cache directory between runs, significantly speeding up npm ci.
  • --ci flag — Tells Jest to run in CI mode: no interactive prompts, fails on snapshot mismatches, and optimizes for single-run execution.
  • services — Spins up PostgreSQL and Redis containers alongside the test runner. Accessible via localhost on the mapped ports.
  • if: always() — Uploads test artifacts even when tests fail, so you can inspect reports and coverage from failing runs.
  • needs: test — Integration tests wait for all unit test matrix jobs to complete successfully before starting.

Common Modifications

  • Vitest: Replace npm test -- --ci --coverage with npx vitest run --coverage and adjust the reporter settings.
  • pnpm or yarn: Change cache: npm to cache: pnpm and npm ci to pnpm install --frozen-lockfile.
  • Database migrations: Add npx prisma migrate deploy or npm run db:migrate before integration tests.
  • E2E tests: Add a Playwright or Cypress job with npx playwright install --with-deps and browser-specific runners.
  • Required checks: Configure branch protection rules to require the test job to pass before merging PRs.