Repository: RedisInsight/RedisInsight
Pull request #4555: RI-7038: Update Github flow to show code coverage reports to each PR
Author: login: KrumTy
Created: 2025-05-16T07:53:14Z | Merged: 2025-05-23T12:30:47Z

## Description

Adds code coverage results from unit (ui+api) and integration tests as comments to each PR. This is merely informative and not for blocking PRs.


Diff:
diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml
new file mode 100644
index 0000000000..b437b0d023
--- /dev/null
+++ b/.github/workflows/code-coverage.yml
@@ -0,0 +1,140 @@
+name: 'Code Coverage'
+on:
+  workflow_call:
+    inputs:
+      type:
+        description: Type of report (unit or integration)
+        type: string
+      resource_name:
+        description: Resource name of coverage report
+        type: string
+
+jobs:
+  coverage-unit:
+    runs-on: ubuntu-latest
+    name: Unit tests coverage
+    if: ${{ inputs.type == 'unit' }}
+    steps:
+      - uses: actions/checkout@v4
+
+      - name: Download Coverage Report
+        uses: actions/download-artifact@v4
+        with:
+          name: ${{ inputs.resource_name }}
+          path: report
+
+      - uses: jwalton/gh-find-current-pr@v1
+        id: findPr
+
+      - uses: ArtiomTr/jest-coverage-report-action@v2
+        with:
+          prnumber: ${{ steps.findPr.outputs.number }}
+          coverage-file: report/coverage/report.json
+          base-coverage-file: report/coverage/report.json
+          github-token: ${{ secrets.GITHUB_TOKEN }}
+          skip-step: all
+          custom-title: Code Coverage - ${{ inputs.resource_name == 'report-be' && 'Backend' || 'Frontend' }} unit tests
+
+  coverage-integration:
+    runs-on: ubuntu-latest
+    name: Integration tests coverage
+    if: ${{ inputs.type == 'integration' }}
+    steps:
+      - uses: actions/checkout@v4
+
+      - name: Download Coverage Report
+        uses: actions/download-artifact@v4
+        with:
+          name: ${{ inputs.resource_name }}
+
+      - name: Parse Coverage Summary
+        id: parse-coverage
+        run: |
+          # Extract coverage data from file.
+          # Example of processed row:
+          #   Statements   : 81.75% ( 16130/19730 )
+          # field '$3' = 81.75%, field '$5' = 16130
+          extract_coverage_data() {
+              local keyword=$1
+              local field=$2
+              awk "/$keyword/ {print $field}" integration-coverage.txt | tr -d '\n|%'
+          }
+
+          # Determine status based on percentage
+          get_status() {
+              if [ "$(echo "$1 < 50" | bc)" -eq 1 ]; then
+                  echo "🔴"
+              elif [ "$(echo "$1 < 80" | bc)" -eq 1 ]; then
+                  echo "🟡"
+              else
+                  echo "🟢"
+              fi
+          }
+
+          # Extract coverage data from the summary
+          STATEMENTS_PERCENT=$(extract_coverage_data "Statements" '$3')
+          STATEMENTS_COVERED=$(extract_coverage_data "Statements" '$5')
+          STATEMENTS_STATUS=$(get_status $STATEMENTS_PERCENT)
+
+          BRANCHES_PERCENT=$(extract_coverage_data "Branches" '$3')
+          BRANCHES_COVERED=$(extract_coverage_data "Branches" '$5')
+          BRANCHES_STATUS=$(get_status $BRANCHES_PERCENT)
+
+          FUNCTIONS_PERCENT=$(extract_coverage_data "Functions" '$3')
+          FUNCTIONS_COVERED=$(extract_coverage_data "Functions" '$5')
+          FUNCTIONS_STATUS=$(get_status $FUNCTIONS_PERCENT)
+
+          LINES_PERCENT=$(extract_coverage_data "Lines" '$3')
+          LINES_COVERED=$(extract_coverage_data "Lines" '$5')
+          LINES_STATUS=$(get_status $LINES_PERCENT)
+
+          # Format as a Markdown table
+          echo "| Status      | Category    | Percentage  | Covered / Total |" > coverage-table.md
+          echo "|-------------|-------------|-------------|-----------------|" >> coverage-table.md
+          echo "| $STATEMENTS_STATUS | Statements  | ${STATEMENTS_PERCENT}% | ${STATEMENTS_COVERED} |" >> coverage-table.md
+          echo "| $BRANCHES_STATUS | Branches    | ${BRANCHES_PERCENT}% | ${BRANCHES_COVERED} |" >> coverage-table.md
+          echo "| $FUNCTIONS_STATUS | Functions   | ${FUNCTIONS_PERCENT}% | ${FUNCTIONS_COVERED} |" >> coverage-table.md
+          echo "| $LINES_STATUS | Lines       | ${LINES_PERCENT}% | ${LINES_COVERED} |" >> coverage-table.md
+
+      - uses: jwalton/gh-find-current-pr@v1
+        id: findPr
+
+      - name: Post or Update Coverage Summary Comment
+        uses: actions/github-script@v7
+        with:
+          script: |
+            const fs = require('fs');
+            const table = fs.readFileSync('coverage-table.md', 'utf8');
+            const commentBody = `### Code Coverage - Integration Tests\n\n${table}`;
+
+            // Fetch existing comments on the pull request
+            const { data: comments } = await github.rest.issues.listComments({
+              owner: context.repo.owner,
+              repo: context.repo.repo,
+              issue_number: process.env.RR_Number,
+            });
+
+            // Check if a comment with the same header already exists
+            const existingComment = comments.find(comment =>
+              comment.body.startsWith('### Code Coverage - Integration Tests')
+            );
+
+            if (existingComment) {
+              // Update the existing comment
+              await github.rest.issues.updateComment({
+                owner: context.repo.owner,
+                repo: context.repo.repo,
+                comment_id: existingComment.id,
+                body: commentBody,
+              });
+            } else {
+              // Create a new comment
+              await github.rest.issues.createComment({
+                owner: context.repo.owner,
+                repo: context.repo.repo,
+                issue_number: process.env.RR_Number,
+                body: commentBody,
+              });
+            }
+        env:
+          RR_Number: ${{ steps.findPr.outputs.number }}
diff --git a/.github/workflows/tests-integration.yml b/.github/workflows/tests-integration.yml
index 45513874c2..8099e0eade 100644
--- a/.github/workflows/tests-integration.yml
+++ b/.github/workflows/tests-integration.yml
@@ -191,7 +191,14 @@ jobs:
           sudo mkdir -p /usr/src/app
           sudo cp -a ./redisinsight/api/. /usr/src/app/
           sudo cp -R ./coverages /usr/src/app && sudo chmod 777 -R /usr/src/app
-          cd /usr/src/app && npx nyc report -t ./coverages -r text -r text-summary
+          cd /usr/src/app && npx nyc report -t ./coverages -r text -r text-summary > integration-coverage.txt
+          cp integration-coverage.txt $GITHUB_WORKSPACE/integration-coverage.txt
+
+      - name: Upload integration-coverage as artifact
+        uses: actions/upload-artifact@v4
+        with:
+          name: integration-coverage
+          path: integration-coverage.txt
 
       - name: Delete Artifact
         uses: actions/github-script@v7
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 5d38bd21b3..7040d652c8 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -94,12 +94,28 @@ jobs:
     uses: ./.github/workflows/tests-frontend.yml
     secrets: inherit
 
+  frontend-tests-coverage:
+    needs: frontend-tests
+    uses: ./.github/workflows/code-coverage.yml
+    secrets: inherit
+    with:
+      resource_name: report-fe
+      type: unit
+
   backend-tests:
     needs: changes
     if: inputs.group_tests == 'all' || inputs.group_tests == 'without_e2e' || startsWith(github.ref_name, 'be/') || startsWith(github.ref_name, 'fe-be/') || startsWith(github.ref_name, 'feature/') || startsWith(github.ref_name, 'bugfix/') || startsWith(github.ref_name, 'ric/')
     uses: ./.github/workflows/tests-backend.yml
     secrets: inherit
 
+  backend-tests-coverage:
+    needs: backend-tests
+    uses: ./.github/workflows/code-coverage.yml
+    secrets: inherit
+    with:
+      resource_name: report-be
+      type: unit
+
   integration-tests:
     needs: changes
     if: inputs.group_tests == 'all' || inputs.group_tests == 'without_e2e' || startsWith(github.ref_name, 'be/') || startsWith(github.ref_name, 'fe-be/') || startsWith(github.ref_name, 'feature/') || startsWith(github.ref_name, 'bugfix/') || startsWith(github.ref_name, 'ric/')
@@ -110,6 +126,14 @@ jobs:
       redis_client: ${{ inputs.redis_client || '' }}
       debug: ${{ inputs.debug || false }}
 
+  integration-tests-coverage:
+    needs: integration-tests
+    uses: ./.github/workflows/code-coverage.yml
+    secrets: inherit
+    with:
+      resource_name: integration-coverage
+      type: integration
+
   # # E2E Approve
   e2e-approve:
     runs-on: ubuntu-latest
diff --git a/package.json b/package.json
index f7310ef097..368c880143 100644
--- a/package.json
+++ b/package.json
@@ -48,7 +48,7 @@
     "test:api": "yarn --cwd redisinsight/api test",
     "test:api:integration": "yarn --cwd redisinsight/api test:api",
     "test:watch": "jest ./redisinsight/ui --watch -w 1",
-    "test:cov": "cross-env NODE_OPTIONS='' jest ./redisinsight/ui --silent --coverage --no-cache --forceExit -w 3",
+    "test:cov": "cross-env NODE_OPTIONS='' jest ./redisinsight/ui --testLocationInResults --json --outputFile=\"report/coverage/report.json\" --silent --coverage --no-cache --forceExit -w 3",
     "test:cov:unit": "jest ./redisinsight/ui --group=-component --coverage -w 1",
     "test:cov:component": "jest ./redisinsight/ui --group=component --coverage -w 1",
     "type-check:ui": "tsc --project redisinsight/ui --noEmit"
diff --git a/redisinsight/api/package.json b/redisinsight/api/package.json
index 167800402a..ea968bfacd 100644
--- a/redisinsight/api/package.json
+++ b/redisinsight/api/package.json
@@ -29,7 +29,7 @@
     "start:prod": "cross-env NODE_ENV=production node dist/src/main",
     "test": "cross-env NODE_ENV=test ./node_modules/.bin/jest -w 1",
     "test:watch": "cross-env NODE_ENV=test jest --watch -w 1",
-    "test:cov": "cross-env NODE_ENV=test ./node_modules/.bin/jest --forceExit --coverage --runInBand",
+    "test:cov": "cross-env NODE_ENV=test ./node_modules/.bin/jest --testLocationInResults --json --outputFile=\"report/coverage/report.json\" --forceExit --coverage --runInBand",
     "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand -w 1",
     "test:e2e": "jest --config ./test/jest-e2e.json -w 1",
     "typeorm": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js -d ./config/ormconfig.ts",


Reviews:
user: login: ArtemHoruzhenko
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T10:51:28Z
user: login: KrumTy
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T10:58:02Z
user: login: pawelangelow
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T11:43:24Z
user: login: pawelangelow
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T11:44:52Z
user: login: pawelangelow
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T11:48:39Z
user: login: pawelangelow
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T11:48:45Z
user: login: pawelangelow
body: 🚀 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T12:03:33Z

Review comments:
diff_hunk: @@ -0,0 +1,138 @@
+name: 'Code Coverage'
+on:
+  workflow_call:
+    inputs:
+      type:
+        description: Type of report (unit or integration)
+        type: string
+        default: unit
+      resource_name:
+        description: Resource name of report folder
+        type: string
+
+jobs:
+  coverage-unit:
+    runs-on: ubuntu-latest
+    name: Jest tests coverage
+    if: ${{ inputs.type == 'unit' }}
+    steps:
+      - uses: actions/checkout@v4
+
+      - name: Download Coverage Report
+        uses: actions/download-artifact@v4
+        with:
+          name: ${{ inputs.resource_name }}
+          path: report
+
+      - uses: jwalton/gh-find-current-pr@v1
+        id: findPr
+
+      - uses: ArtiomTr/jest-coverage-report-action@v2
+        with:
+          prnumber: ${{ steps.findPr.outputs.number }}
+          coverage-file: report/coverage/report.json
+          base-coverage-file: report/coverage/report.json
+          github-token: ${{ secrets.GITHUB_TOKEN }}
+          skip-step: all
+          custom-title: Code Coverage - ${{ inputs.resource_name == 'report-be' && 'Backend' || 'Frontend' }} unit tests
+
+  coverage-integration:
+    runs-on: ubuntu-latest
+    name: Jest tests coverage
+    if: ${{ inputs.type == 'integration' }}
+    steps:
+      - uses: actions/checkout@v4
+
+      - name: Download Coverage Report
+        uses: actions/download-artifact@v4
+        with:
+          name: ${{ inputs.resource_name }}
+
+      - name: Parse Coverage Summary
+        id: parse-coverage
+        run: |
+          # Define a reusable function to extract coverage data
+          extract_coverage_data() {
+              local keyword=$1
+              local field=$2
+              awk "/$keyword/ {print $field}" integration-coverage.txt | tr -d '\n|%'
+          }
+
+          # Determine status based on percentage
+          get_status() {
+              if [ "$(echo "$1 < 50" | bc)" -eq 1 ]; then
+                  echo "🔴"
+              elif [ "$(echo "$1 < 80" | bc)" -eq 1 ]; then
+                  echo "🟡"
+              else
+                  echo "🟢"
+              fi
+          }
+
path: .github/workflows/code-coverage.yml
user: login: ArtemHoruzhenko
body: why do we need custom parser for integration tests when it is not needed for unit tests?
created_at: 2025-05-21T10:50:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,138 @@
+name: 'Code Coverage'
+on:
+  workflow_call:
+    inputs:
+      type:
+        description: Type of report (unit or integration)
+        type: string
+        default: unit
+      resource_name:
+        description: Resource name of report folder
+        type: string
+
+jobs:
+  coverage-unit:
+    runs-on: ubuntu-latest
+    name: Jest tests coverage
+    if: ${{ inputs.type == 'unit' }}
+    steps:
+      - uses: actions/checkout@v4
+
+      - name: Download Coverage Report
+        uses: actions/download-artifact@v4
+        with:
+          name: ${{ inputs.resource_name }}
+          path: report
+
+      - uses: jwalton/gh-find-current-pr@v1
+        id: findPr
+
+      - uses: ArtiomTr/jest-coverage-report-action@v2
+        with:
+          prnumber: ${{ steps.findPr.outputs.number }}
+          coverage-file: report/coverage/report.json
+          base-coverage-file: report/coverage/report.json
+          github-token: ${{ secrets.GITHUB_TOKEN }}
+          skip-step: all
+          custom-title: Code Coverage - ${{ inputs.resource_name == 'report-be' && 'Backend' || 'Frontend' }} unit tests
+
+  coverage-integration:
+    runs-on: ubuntu-latest
+    name: Jest tests coverage
+    if: ${{ inputs.type == 'integration' }}
+    steps:
+      - uses: actions/checkout@v4
+
+      - name: Download Coverage Report
+        uses: actions/download-artifact@v4
+        with:
+          name: ${{ inputs.resource_name }}
+
+      - name: Parse Coverage Summary
+        id: parse-coverage
+        run: |
+          # Define a reusable function to extract coverage data
+          extract_coverage_data() {
+              local keyword=$1
+              local field=$2
+              awk "/$keyword/ {print $field}" integration-coverage.txt | tr -d '\n|%'
+          }
+
+          # Determine status based on percentage
+          get_status() {
+              if [ "$(echo "$1 < 50" | bc)" -eq 1 ]; then
+                  echo "🔴"
+              elif [ "$(echo "$1 < 80" | bc)" -eq 1 ]; then
+                  echo "🟡"
+              else
+                  echo "🟢"
+              fi
+          }
+
path: .github/workflows/code-coverage.yml
user: login: KrumTy
body: for unit tests (jest) I used an external action - https://github.com/ArtiomTr/jest-coverage-report-action
for integration tests (mocha) there wasn't one available so I had to set them up manually
created_at: 2025-05-21T10:58:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -94,12 +94,27 @@ jobs:
     uses: ./.github/workflows/tests-frontend.yml
     secrets: inherit
 
+  frontend-tests-coverage:
+    needs: frontend-tests
+    uses: ./.github/workflows/code-coverage.yml
+    secrets: inherit
+    with:
+      resource_name: report-fe
+      type: unit
path: .github/workflows/tests.yml
user: login: pawelangelow
body: ```suggestion
```

since the `unit` is the default value.

Or add `type: unit` on the backend-test-coverage.
created_at: 2025-05-21T11:43:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,138 @@
+name: 'Code Coverage'
+on:
+  workflow_call:
+    inputs:
+      type:
+        description: Type of report (unit or integration)
+        type: string
+        default: unit
+      resource_name:
+        description: Resource name of report folder
+        type: string
+
+jobs:
+  coverage-unit:
+    runs-on: ubuntu-latest
+    name: Jest tests coverage
+    if: ${{ inputs.type == 'unit' }}
+    steps:
+      - uses: actions/checkout@v4
+
+      - name: Download Coverage Report
+        uses: actions/download-artifact@v4
+        with:
+          name: ${{ inputs.resource_name }}
+          path: report
+
+      - uses: jwalton/gh-find-current-pr@v1
+        id: findPr
+
+      - uses: ArtiomTr/jest-coverage-report-action@v2
+        with:
+          prnumber: ${{ steps.findPr.outputs.number }}
+          coverage-file: report/coverage/report.json
+          base-coverage-file: report/coverage/report.json
+          github-token: ${{ secrets.GITHUB_TOKEN }}
+          skip-step: all
+          custom-title: Code Coverage - ${{ inputs.resource_name == 'report-be' && 'Backend' || 'Frontend' }} unit tests
+
+  coverage-integration:
+    runs-on: ubuntu-latest
+    name: Jest tests coverage
path: .github/workflows/code-coverage.yml
user: login: pawelangelow
body: ```suggestion
    name: Integration tests coverage
```
created_at: 2025-05-21T11:44:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,138 @@
+name: 'Code Coverage'
+on:
+  workflow_call:
+    inputs:
+      type:
+        description: Type of report (unit or integration)
+        type: string
+        default: unit
+      resource_name:
+        description: Resource name of report folder
+        type: string
+
+jobs:
+  coverage-unit:
+    runs-on: ubuntu-latest
+    name: Jest tests coverage
+    if: ${{ inputs.type == 'unit' }}
+    steps:
+      - uses: actions/checkout@v4
+
+      - name: Download Coverage Report
+        uses: actions/download-artifact@v4
+        with:
+          name: ${{ inputs.resource_name }}
+          path: report
+
+      - uses: jwalton/gh-find-current-pr@v1
+        id: findPr
+
+      - uses: ArtiomTr/jest-coverage-report-action@v2
+        with:
+          prnumber: ${{ steps.findPr.outputs.number }}
+          coverage-file: report/coverage/report.json
+          base-coverage-file: report/coverage/report.json
+          github-token: ${{ secrets.GITHUB_TOKEN }}
+          skip-step: all
+          custom-title: Code Coverage - ${{ inputs.resource_name == 'report-be' && 'Backend' || 'Frontend' }} unit tests
+
+  coverage-integration:
+    runs-on: ubuntu-latest
+    name: Jest tests coverage
+    if: ${{ inputs.type == 'integration' }}
+    steps:
+      - uses: actions/checkout@v4
+
+      - name: Download Coverage Report
+        uses: actions/download-artifact@v4
+        with:
+          name: ${{ inputs.resource_name }}
+
+      - name: Parse Coverage Summary
+        id: parse-coverage
+        run: |
+          # Define a reusable function to extract coverage data
+          extract_coverage_data() {
+              local keyword=$1
+              local field=$2
+              awk "/$keyword/ {print $field}" integration-coverage.txt | tr -d '\n|%'
+          }
+
+          # Determine status based on percentage
+          get_status() {
+              if [ "$(echo "$1 < 50" | bc)" -eq 1 ]; then
+                  echo "🔴"
+              elif [ "$(echo "$1 < 80" | bc)" -eq 1 ]; then
+                  echo "🟡"
+              else
+                  echo "🟢"
+              fi
+          }
+
+          # Extract coverage data from the summary
+          STATEMENTS_PERCENT=$(extract_coverage_data "Statements" '$3')
path: .github/workflows/code-coverage.yml
user: login: pawelangelow
body: Let's add a comment above to help with the placeholder stuff:
Example: Statements : 78.90% (123/156)
$1 -> Statements
$2 -> :
$3 ...
created_at: 2025-05-21T11:48:39Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-05-20T12:18:16Z
body: <!-- jest coverage report action for options with hash eb13555c88f6951420fafc671fad9c4c -->

## Code Coverage - Backend unit tests


| <div title="Status of coverage:&#10; 🟢 - ok&#10; 🟡 - slightly more than threshold&#10; 🔴 - under the threshold">St.<sup>:grey_question:</sup></div> | Category | Percentage | Covered / Total |
| :-: | :- | :- | :-: |
| 🟢 | Statements | 92.28% | 13666/14810 |
| 🟡 | Branches | 73.85% | 4100/5552 |
| 🟢 | Functions | 85.75% | 2107/2457 |
| 🟢 | Lines | 92.06% | 13060/14186 |


## Test suite run success
2906 tests passing in 285 suites.

<p align="right">Report generated by <a href="https://github.com/ArtiomTr/jest-coverage-report-action">🧪jest coverage report action</a> from c207a2d335fd98fb01513a86749cd9d468f231e6</p>

author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-20T12:24:52Z
body: <!-- jest coverage report action for options with hash 1b99e3f04f9c3466e36be937f6eff3f2 -->

## Code Coverage - Frontend unit tests


| <div title="Status of coverage:&#10; 🟢 - ok&#10; 🟡 - slightly more than threshold&#10; 🔴 - under the threshold">St.<sup>:grey_question:</sup></div> | Category | Percentage | Covered / Total |
| :-: | :- | :- | :-: |
| 🟢 | Statements | 81.3% | 18291/22498 |
| 🟡 | Branches | 66.98% | 8023/11979 |
| 🟡 | Functions | 75.06% | 4836/6443 |
| 🟢 | Lines | 81.69% | 17896/21906 |


## Test suite run success
4757 tests passing in 626 suites.

<p align="right">Report generated by <a href="https://github.com/ArtiomTr/jest-coverage-report-action">🧪jest coverage report action</a> from c207a2d335fd98fb01513a86749cd9d468f231e6</p>

author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-20T14:08:09Z
body: ### Code Coverage - Integration Tests

| Status      | Category    | Percentage  | Covered / Total |
|-------------|-------------|-------------|-----------------|
| 🟢 | Statements  | 81.73% | 16126/19730 |
| 🟡 | Branches    | 64.86% | 7279/11222 |
| 🟡 | Functions   | 70.88% | 2271/3204 |
| 🟢 | Lines       | 81.37% | 15165/18636 |

author_association: CONTRIBUTOR
user: login: pd-redis
created_at: 2025-05-23T09:08:41Z
body: So with this, are we going to get the summary on each commit to the PR branch?
author_association: CONTRIBUTOR