Hooked up monorepo to SonarQube
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:
- Configure seperate projects within SonarQube
- Use 'nyc' to merge coverage reports (I hadn't hear of that package until today)
- 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:
- After running 'yarn test:coverage' during the build, the coverage folder gets uploaded.
- 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.