Coverage Analysis goes to 0% on parallel analysis

I have an NX Monorepo with multiple projects and am encountering an issue where all the previous coverage % goes back to zero on running the github workflow below.
Am running tests using jest to generate lcov files, Sonar is able to find the same lcov files but the analysis somewhat does not happen or fails without an error (On adding debug to sonar properties for each project; analysis, upload and execution is successful).

Using the workflow below the matrix triggers multiple jobs, with each app having its own parallel isolated job all the way to analysis.

  • ALM used (GitHub)
  • CI system used (Github)
  • Languages of the repository - Typescript
  • Steps to reproduce - running this work in an nx monorepo with Angular apps and jest for testing
  • Projects have unique projectKey and projectName but they share the SONAR_TOKEN
name: CI Pipeline
on:
  push:
    branches:
      - main
  pull_request:
    branches: [main]
    types: [opened, synchronize, reopened]

# To cancel a currently running workflow from the same PR, branch or tag when a new workflow is triggered
concurrency:
  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: true

jobs:
  check-app-affected:
    name: Run Lint, Tests, Sonar analysis
    runs-on: ubuntu-latest
    strategy:
      matrix:
        app: [app1, app2, app3]
        node-version: [20]
    steps:
      - uses: actions/checkout@v4
        name: Checkout Code
        if: ${{ github.event_name == 'pull_request' }}
        with:
          ref: ${{ github.event.pull_request.head.sha }}
          fetch-depth: 0
  
      - uses: actions/checkout@v4
        name: Checkout Main Branch
        if: ${{ github.event_name == 'push' }}
        with:
          ref: main
          fetch-depth: 0

      - name: Derive SHAs for `nx affected` base and head SHAs for PR
        id: derive-shas
        run: |
          if [ "${{ github.event_name }}" == "pull_request" ]; then
            BASE=${{ github.event.pull_request.base.sha }}
            HEAD=${{ github.event.pull_request.head.sha }}
          else
            git fetch origin main --prune
            BASE=$(git rev-parse HEAD~1)
            HEAD=$(git rev-parse HEAD)
          fi
          echo "NX_BASE=$BASE" >> $GITHUB_ENV
          echo "NX_HEAD=$HEAD" >> $GITHUB_ENV

      ################### Determine the affected apps ##########################
      - name: Determine the Affected Apps/Projects by Opened or Closed PR
        id: determine-affected-apps
        run: |
          # Fetch the full history
          git fetch --prune --unshallow || true
      
          # Get the list of changed files
          affected_files=$(git diff --name-only "${{ env.NX_BASE }}" "${{ env.NX_HEAD }}" | tr -d '\r')
          
          # Initialize affected_apps variable
          affected_apps=""

          if echo "$affected_files" | grep -qE "^apps/(app1)/"; then
            affected_apps="${affected_apps:+$affected_apps,}app1"
          fi

          if echo "$affected_files" | grep -qE "^apps/(app2)/"; then
            affected_apps="${affected_apps:+$affected_apps,}app2"
          fi

          if echo "$affected_files" | grep -qE "^apps/(app3)/"; then
            affected_apps="${affected_apps:+$affected_apps,}app3"
          fi

          echo "Affected apps: $affected_apps"
      
          # Remove any trailing commas
          affected_apps=$(echo "$affected_apps" | sed 's/,$//')
      
          # Export affected_apps to the environment
          echo "affected_apps=$affected_apps" >> $GITHUB_ENV
            
      ################### Determine if the workflow should continue by checking if the an app has been affected ##########################
      - name: Determine if the workflow should continue by checking if the an app has been affected
        id: check
        run: |
          echo "Check if ${{ matrix.app }}" has been affected
          if [[ "${{ env.affected_apps }}" =~ (^|,)"${{ matrix.app }}"($|,) ]]; then
          echo "skip=false" >> $GITHUB_ENV
          echo "App is affected. Continuing workflow."
          else
            echo "skip=true" >> $GITHUB_ENV
            echo "App is not affected. Exiting..."
            exit 0
          fi

      - uses: actions/checkout@v4
        name: Checkout Code
        if: ${{ github.event_name == 'pull_request' }}
        with:
          ref: ${{ github.event.pull_request.head.sha }}
          fetch-depth: 0
  
      - uses: actions/checkout@v4
        name: Checkout Main Branch
        if: ${{ github.event_name == 'push' }}
        with:
          ref: main
          fetch-depth: 0

      ################### NPM SETUP ##########################
      - name: Install pnpm
        uses: pnpm/action-setup@v4
        with:
          version: 9

      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'pnpm'

      - name: Install dependencies
        run: pnpm install --frozen-lockfile
      
      ################### Linting and Testing ################
      - name: Linting Code
        if: ${{ github.event_name == 'pull_request' }}
        run: |
          echo "Run Lint for ${{ matrix.app }}"
          if [[ "${{ env.affected_apps }}" == *"${{ matrix.app }}"* ]]; then
            pnpm nx lint ${{ matrix.app }} || exit 1
          else
            echo "No linting needed for ${{ matrix.app }}"
          fi

      - name: Run Tests for Affected Apps / Libs
        if: ${{ github.event_name == 'pull_request' || github.event_name == 'push' }}
        run: |
          echo "Running tests for ${{ matrix.app }}"
          if [[ "${{ env.affected_apps }}" == *"${{ matrix.app }}"* ]]; then
            pnpm nx test ${{ matrix.app }} --ci --code-coverage --coverageReporters=lcov --parallel --maxParallel=4 --runInBand --maxWorkers=4 || exit 1
          else
            echo "No tests for ${{ matrix.app }}"
          fi
          pnpm run replace-lcov-paths "${{ matrix.app }}"

      # Debug to see if the lcov file generated matches what is produced when running tests locally
      - name: Check Coverage Files
        run: |
          ls -lh coverage/apps/${{ matrix.app }} || echo "Coverage file not found!"
          cat coverage/apps/${{ matrix.app }}/lcov.info || echo "Coverage report is empty!"
        
        ################### SonarCloud Analysis ################
      - name: Determine if affected for SonarCloud Scan
        id: sonar_check
        run: |
          if [[ "${{ env.affected_apps }}" == *"${{ matrix.app }}"* ]]; then
            echo "Running SonarCloud scan for ${{ matrix.app }}"
            echo "RUN_SONAR=true" >> $GITHUB_ENV
          else
            echo "Skipping SonarCloud scan for ${{ matrix.app }}"
            echo "RUN_SONAR=false" >> $GITHUB_ENV
          fi

      - name: Run SonarCloud Scan Analysis
        if: ${{ env.RUN_SONAR == 'true' }}
        uses: SonarSource/sonarqube-scan-action@v4
        with:
          projectBaseDir: apps/${{ matrix.app }}
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

Thank you.

Hey there.

When coverage is successfully imported – is it using a different workflow than the one you’ve shared here, or is this your first implementation of running multiple SQ analyses in a single workflow?

Another way of asking the question – is the problem only when multiple projects are affected?

“When coverage is successfully imported – is it using a different workflow than the one you’ve shared here…?” → Yes
“is this your first implementation of running multiple SQ analyses in a single workflow?” → Yes

For the workflow using matrix, whether one or multiple apps are affected, the coverage defaults to zero for the affected app.

Hey @alex-migwi

So far I’ve been unable to reproduce this issue. It would be great if you could build a small reproducer (publicly available on GitHub) where the issue can be reproduced.

What is this doing, precisely? I can’t find any documentation online.

I use this to fix the coverage file paths in the github runner workspace, sonar was trying to read lcov files but was throwing an error ‘lcov file not found’.

For any one who comes across this issue (end goal was to run linting, test and analysis in parallel for each of the affected projects in the monorepo), here is how I solved it.
Reviews and comments are welcome.
Assuming that you are working in an nx monorepo, you need to:

  1. Get the list of all affected apps
  2. Run tests and generate coverage info also
  3. Install sonar scanner npm module
  4. Pass the list of affected apps to the git workflow job matrix for Sonar analysis

Here is an example:

name: Lint, Test and Run Sonar Analysis
on:
  pull_request:
    branches: [main]
    types: [opened, synchronize, reopened]
  
  push:
    branches: [main]
    
jobs:
  determine-affected:
    name: Check for Changed MFEs Apps / Libs
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4
        name: Checkout Code
        with:
          fetch-depth: 0

      - name: List affected applications/projects
        id: affected
        run: |
          affected_apps= // Add your nx command here to get affected applications
          echo "Affected apps: $affected_apps"
          echo "apps=$affected_apps" >> $GITHUB_OUTPUT

    outputs:
      apps: ${{ steps.affected.outputs.apps }}

  lint-test-and-run-sonarscan-analyse-project:
    name: Run Lint, Tests, Sonar analysis
    if: ${{ needs.determine-affected.outputs.apps != '[]' && needs.determine-affected.outputs.apps != '' }}
    needs: determine-affected
    runs-on: ubuntu-latest
    strategy:
      matrix:
        app: ${{ fromJSON(needs.determine-affected.outputs.apps || '[]') }}
        node-version: [20]

    steps:
      - uses: actions/checkout@v4
        name: Checkout Code
        with:
          fetch-depth: 0

      ################### NPM SETUP ##########################
      - name: Install pnpm
        uses: pnpm/action-setup@v4
        with:
          version: 9

      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'pnpm'

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      ################### Linting and Testing ################
      - name: Linting Code
        if: ${{ github.event_name == 'pull_request' }}
        run: |
          echo "Run Lint for ${{ matrix.app }}"
          pnpm nx lint ${{ matrix.app }} || exit 1

      - name: Run Tests for Affected Apps / Libs
        if: ${{ github.event_name == 'pull_request' || github.event_name == 'push' }}
        run: |
          echo "Running tests for ${{ matrix.app }}"
          pnpm nx test ${{ matrix.app }} --ci --code-coverage --coverageReporters=lcov --maxWorkers=4 || exit 1
          pnpm run replace-lcov-paths "${{ matrix.app }}"  // we needed a custom script to fix paths on the coverage info that could not be found

      - name: Check Coverage Files
        run: |
          ls -lh coverage/apps/${{ matrix.app }} || echo "Coverage file not found!"
          cat coverage/apps/${{ matrix.app }}/lcov.info || echo "Coverage report is empty! or has not been generated."

      ################### SonarCloud Analysis ################
      - name: Install Sonar Scanner
        run: |
          npm install -g sonar-scanner

      - name: Run SonarCloud Scan Analysis
        env:
            SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
        run: |
          echo "Running SonarCloud scan for ${{ matrix.app }}"
          sonar-scanner -X \
            -Dsonar.projectKey=${{ matrix.app }} \
            -Dsonar.sources=apps/${{ matrix.app }}/src \
            -Dsonar.tests=apps/${{ matrix.app }}/src \
            -Dsonar.test.inclusions=**/*.spec.ts,**/*.test.ts \
            -Dsonar.exclusions=**/*.spec.ts,**/*.test.ts,**/*.mock.ts,**/node_modules/**,**/generated_ops/**,**/*imports.ts,**/*.routes.ts,**/*.module.ts \
            -Dsonar.javascript.lcov.reportPaths=coverage/apps/${{ matrix.app }}/lcov.info \
            -Dsonar.organization=organization-name-on-sonar \
            -Dsonar.host.url=https://sonarcloud.io \
            -Dsonar.scanner.skipSystemTruststore=true
      

The parallel analysis in my case was failing when using

- uses: SonarSource/sonarqube-scan-action@<action version>

Am not sure if this would be the case with any other project, but in this specific case it was.
Using sonar scanner npm module was part of the solution.

Hi,

What was the version of the SonarSource/sonarqube-scan-action you were using? We changed it recently to move away from Docker. The problem of Docker is that you usually only mount your project source folder, and if your coverage reports are located somewhere else, the Scanner CLI inside the Docker image won’t be able to access it.

It might also be a problem of path. If you have absolute paths in the coverage report, inside the Docker image they would likely be invalid.

Now if you were using the latest version of the SonarSource/sonarqube-scan-action (v4 or later), this should not be using Docker, and so I don’t really understand what would be the difference compared to the use of the SonarScanner for NPM.

I was using V4 previously (when parallel analysis failed).

With the change to the SonarScanner for NPM, parallel analysis gives the correct coverage although I still need to fix lcov file path.

This topic was automatically closed 7 days after the last reply. New replies are no longer allowed.