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.