Hooked up monorepo to SonarQube

| React, website, SonarQube, monorepo, Jest, GitHub

Just in time for Christmas, SonarQube extended their free tier to private repositories on GitHub. This meant I was able to hookup the monorepo containing my websites to SonarQube!

In the end, this was pretty easy, but it took me a while to figure it out. Most things worked right out of the box. The difficult piece was getting the Code Coverage hooked up. It seemed to me that I had three real choices:

  1. Configure seperate projects within SonarQube
  2. Use 'nyc' to merge coverage reports (I hadn't hear of that package until today)
  3. Configure Jest in the root to work on projects

It took me a while to realize that the third option was even a possiblity, but once I did the advantages seemed clear. So here's what I needed to do. First let's start with the GitHub workflow:

name: Node.js CI

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

jobs:
  build:

    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [22.x]

    steps:
    - uses: actions/checkout@v4
    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v4
      with:
        node-version: ${{ matrix.node-version }}
        cache: 'yarn'
    - run: yarn install --frozen-lockfile
    - run: yarn build
    - run: yarn test:coverage
    - name: Upload coverage report
      uses: actions/upload-artifact@v4
      with:
        name: coverage-report
        path: coverage

  sonarqube:
    name: SonarQube
    runs-on: ubuntu-latest
    needs: build  # Ensure this job runs after the build job
    steps:
      # We don't want to checkout the code again, it's already checked out
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: Download coverage report
        uses: actions/download-artifact@v4
        with:
          name: coverage-report
          path: coverage
      - name: SonarQube Scan
        uses: SonarSource/sonarqube-scan-action@v4
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

The key things to notice here are that:

  1. After running 'yarn test:coverage' during the build, the coverage folder gets uploaded.
  2. And that before the SonarQube Scan, the coverage folder is downloaded.

You could combine the build and scan into one step and skip the upload/download if you prefer.

Then for the (root) jest.config.js file you'll need something like this:

module.exports = {
  preset: 'ts-jest',
  projects: [
    '<rootDir>/apps/example-website-one',
    '<rootDir>/apps/example-website-two'
  ],
  transform: {
    '^.+\\.(ts|tsx)$': 'ts-jest',
    '^.+\\.(js|jsx)$': 'babel-jest',
  },
  testEnvironment: 'jsdom',
  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
  setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
};

And in each project, something like this:

module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
  testPathIgnorePatterns: [
    '<rootDir>/tests/*',
    // Add more patterns here if needed
  ],
};

I haven't mentioned various packages that need to be installed and that sort of thing, since this post is already a bit longish ... your favorite AI should be able to get you over those final hurdles.