From 93a55bf03daf7df64eb1bba2465b002697773ad4 Mon Sep 17 00:00:00 2001
From: Ross Oliver <ross.oliver36@gmail.com>
Date: Fri, 31 Jan 2025 15:17:42 +0000
Subject: [PATCH] Integrate Sonarcloud to scan pull requests

We want to integrate Sonarcloud so that it can scan raised PRs and flag any potential issues/aid in reviewing them.

- Integrate `lint` workflow into `rspec` workflow so that we can grab the Rubocop output.
- Add `rspec-sonarqube-formatter` gem so that we can output rspec results in a format Sonarcloud understands.
- Add `coverage.rake` task so that we can collate coverage reports from multiple test runners.
- Add `sonar-project.properties` with a basic config for `sonarscanner`
---
 .github/workflows/deploy.yml |   9 +-
 .github/workflows/lint.yml   | 104 -----------------
 .github/workflows/rspec.yml  | 209 ++++++++++++++++++++++++++++++++++-
 Gemfile                      |   1 +
 Gemfile.lock                 |   9 ++
 lib/tasks/coverage.rake      |  17 +++
 sonar-project.properties     |   8 ++
 spec/rails_helper.rb         |   1 +
 8 files changed, 247 insertions(+), 111 deletions(-)
 delete mode 100644 .github/workflows/lint.yml
 create mode 100644 lib/tasks/coverage.rake
 create mode 100644 sonar-project.properties

diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 90a99570e2..f23a455210 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -27,10 +27,6 @@ permissions:
   security-events: write
 
 jobs:
-  lint:
-    name: Lint
-    uses: ./.github/workflows/lint.yml
-
   openapi:
     name: Lead Provider OpenAPI Check
     uses: ./.github/workflows/lead_provider_openapi_check.yml
@@ -38,10 +34,11 @@ jobs:
   rspec:
     name: Run the RSpec tests
     uses: ./.github/workflows/rspec.yml
+    secrets: inherit
 
   permit-merge:
     name: Permit merge
-    needs: [lint, rspec]
+    needs: [rspec]
     runs-on: ubuntu-latest
     steps:
       - run: "echo 'Linting and tests passed, this branch is ready to be merged'"
@@ -117,7 +114,7 @@ jobs:
 
   deploy_staging:
     name: Deploy staging
-    needs: [docker, rspec, lint, brakeman]
+    needs: [docker, rspec, brakeman]
     runs-on: ubuntu-latest
     if: github.ref == 'refs/heads/main'
     environment:
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
deleted file mode 100644
index 930812b2b8..0000000000
--- a/.github/workflows/lint.yml
+++ /dev/null
@@ -1,104 +0,0 @@
-name: "Lint"
-on:
-  workflow_call:
-    inputs:
-      ruby-version:
-        description: Ruby version
-        type: string
-        required: false
-        default: "3.2.4"
-      node-version:
-        description: Node version
-        type: string
-        required: false
-        default: "18.18.x"
-
-jobs:
-  ruby_linting:
-    name: "Lint ruby"
-    env:
-      GOVUK_NOTIFY_API_KEY: Test
-    runs-on: ubuntu-latest
-
-    steps:
-      - uses: actions/checkout@v3
-        name: Checkout Code
-
-      - name: Set up Ruby
-        uses: ruby/setup-ruby@v1.215.0
-        with:
-          ruby-version: ${{ inputs.ruby-version }}
-
-      - name: Install dependencies
-        run: bundle install
-
-      - name: Lint Ruby
-        run: bundle exec rubocop
-
-  js_linting:
-    name: "Lint JS"
-    env:
-      GOVUK_NOTIFY_API_KEY: Test
-    runs-on: ubuntu-latest
-
-    steps:
-      - uses: actions/checkout@v3
-        name: Checkout Code
-
-      - name: Set up Node
-        uses: actions/setup-node@v4.2.0
-        with:
-          node-version: ${{ inputs.node-version }}
-          cache: "yarn"
-
-      - name: Yarn install
-        run: npm i -g yarn && yarn
-
-      - name: Lint JS
-        run: |-
-          yarn lint
-
-  scss_linting:
-    name: "Lint SCSS"
-    env:
-      GOVUK_NOTIFY_API_KEY: Test
-    runs-on: ubuntu-latest
-
-    steps:
-      - uses: actions/checkout@v3
-        name: Checkout Code
-
-      - name: Set up Ruby
-        uses: ruby/setup-ruby@v1.215.0
-        with:
-          ruby-version: ${{ inputs.ruby-version }}
-
-      - name: Install dependencies
-        run: bundle install
-
-      - name: Lint SCSS
-        run: |-
-          bundle exec rake lint:scss
-
-  erb_linting:
-    name: "Lint ERB"
-    env:
-      GOVUK_NOTIFY_API_KEY: Test
-    runs-on: ubuntu-latest
-
-    steps:
-      - uses: actions/checkout@v3
-        name: Checkout Code
-
-      - name: Set up Ruby
-        uses: ruby/setup-ruby@v1.215.0
-        with:
-          ruby-version: ${{ inputs.ruby-version }}
-
-      - name: Install dependencies
-        run: bundle install
-
-      - name: Lint ERB Templates
-        if: false
-        run: |-
-          bundle exec erblint --lint-all
diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml
index 0fb626879c..4b08bfcf59 100644
--- a/.github/workflows/rspec.yml
+++ b/.github/workflows/rspec.yml
@@ -7,8 +7,110 @@ on:
         type: boolean
         required: false
         default: true
-        
+
+env:
+  code-coverage-artifact-name: code_coverage_${{github.run_number}}
+  unit-tests-artifact-name: unit_tests_${{github.run_number}}
+  rubocop-artifact-name: rubocop_results_${{github.run_number}}
+
 jobs:
+  ruby-linting:
+    name: "Lint ruby"
+    env:
+      GOVUK_NOTIFY_API_KEY: Test
+    runs-on: ubuntu-latest
+
+    steps:
+      - uses: actions/checkout@v3
+        name: Checkout Code
+
+      - name: Set up Ruby
+        uses: ruby/setup-ruby@v1.215.0
+        with:
+          ruby-version: ${{ inputs.ruby-version }}
+
+      - name: Install dependencies
+        run: bundle install
+
+      - name: Lint Ruby
+        run: bundle exec rubocop --format json --out=out/rubocop-result.json
+
+      - name: Keep Rubocop output
+        if: always()
+        uses: actions/upload-artifact@v4
+        with:
+          name: ${{ env.rubocop-artifact-name }}
+          path: ${{ github.workspace }}/out/rubocop-result.json
+          include-hidden-files: true
+
+  js-linting:
+    name: "Lint JS"
+    env:
+      GOVUK_NOTIFY_API_KEY: Test
+    runs-on: ubuntu-latest
+
+    steps:
+      - uses: actions/checkout@v3
+        name: Checkout Code
+
+      - name: Set up Node
+        uses: actions/setup-node@v4.2.0
+        with:
+          node-version: ${{ inputs.node-version }}
+          cache: "yarn"
+
+      - name: Yarn install
+        run: npm i -g yarn && yarn
+
+      - name: Lint JS
+        run: |-
+          yarn lint
+
+  scss-linting:
+    name: "Lint SCSS"
+    env:
+      GOVUK_NOTIFY_API_KEY: Test
+    runs-on: ubuntu-latest
+
+    steps:
+      - uses: actions/checkout@v3
+        name: Checkout Code
+
+      - name: Set up Ruby
+        uses: ruby/setup-ruby@v1.215.0
+        with:
+          ruby-version: ${{ inputs.ruby-version }}
+
+      - name: Install dependencies
+        run: bundle install
+
+      - name: Lint SCSS
+        run: |-
+          bundle exec rake lint:scss
+
+  erb_linting:
+    name: "Lint ERB"
+    env:
+      GOVUK_NOTIFY_API_KEY: Test
+    runs-on: ubuntu-latest
+
+    steps:
+      - uses: actions/checkout@v3
+        name: Checkout Code
+
+      - name: Set up Ruby
+        uses: ruby/setup-ruby@v1.215.0
+        with:
+          ruby-version: ${{ inputs.ruby-version }}
+
+      - name: Install dependencies
+        run: bundle install
+
+      - name: Lint ERB Templates
+        if: false
+        run: |-
+          bundle exec erblint --lint-all
+
   tests:
     name: Run rspec
     runs-on: ubuntu-20.04
@@ -59,6 +161,22 @@ jobs:
         run: |-
           bundle exec rake "knapsack:rspec[--tag ~type:feature]"
 
+      - name:  Keep Code Coverage Report
+        if: always()
+        uses: actions/upload-artifact@v4
+        with:
+          name: ${{ env.code-coverage-artifact-name }}_${{ matrix.ci_node_index }}_tests
+          path: ./coverage
+          include-hidden-files: true
+
+      - name:  Keep Unit Tests Results
+        if: always()
+        uses: actions/upload-artifact@v4
+        with:
+          name: ${{ env.unit-tests-artifact-name }}_${{ matrix.ci_node_index }}_tests
+          path: ./test-report/*
+          include-hidden-files: true
+
   feature-tests:
     name: Run rspec (features)
     runs-on: ubuntu-20.04
@@ -109,6 +227,22 @@ jobs:
         run: |-
           bundle exec rake "knapsack:rspec[--tag type:feature --fail-fast]"
 
+      - name:  Keep Code Coverage Report
+        if: always()
+        uses: actions/upload-artifact@v4
+        with:
+          name: ${{ env.code-coverage-artifact-name }}_${{ matrix.ci_node_index }}_feature_tests
+          path: ./coverage
+          include-hidden-files: true
+
+      - name:  Keep Unit Tests Results
+        if: always()
+        uses: actions/upload-artifact@v4
+        with:
+          name: ${{ env.unit-tests-artifact-name }}_${{ matrix.ci_node_index }}_feature_tests
+          path: ./test-report/*
+          include-hidden-files: true
+
   e2e-scenarios:
     if: ${{ inputs.run-end-to-end-tests }}
     name: Run end to end scenarios
@@ -158,3 +292,76 @@ jobs:
           CI_NODE_TOTAL: ${{ matrix.ci_node_total }}
           CI_NODE_INDEX: ${{ matrix.ci_node_index }}
         run: bundle exec bin/scenarios_ci
+
+  sonar-scanner:
+    name: Sonar Scanner
+    runs-on: ubuntu-24.04
+    needs: [ tests, feature-tests, ruby-linting ]
+    if: github.ref != 'refs/heads/main' && github.actor != 'dependabot[bot]'
+    environment:
+      name: staging
+    steps:
+      - name: Checkout code
+        uses: actions/checkout@v4
+
+      - name: Set up Ruby
+        uses: ruby/setup-ruby@v1
+        with:
+          ruby-version: ${{ inputs.ruby-version }}
+
+      - name: Install gems
+        run: |
+          bundle config path vendor/bundle
+          bundle install --jobs 4 --retry 3
+
+      - name: Setup sonarqube
+        uses: warchant/setup-sonar-scanner@v8
+
+      - name: Download Artifacts
+        uses: actions/download-artifact@v4
+
+      - name: Combine Coverage Reports
+        run: |-
+          # Copy files from separate artifacts into one directory
+          mkdir ${{github.workspace}}/code_coverage
+          cp -r ${{github.workspace}}/${{ env.code-coverage-artifact-name }}_*/ ${{github.workspace}}/code_coverage
+          bundle exec rake coverage:collate
+        env:
+          GOVUK_NOTIFY_API_KEY: Test
+          COVERAGE_DIR: ${{github.workspace}}/code_coverage
+
+      - name: Login Azure
+        uses: azure/login@v2
+        with:
+          creds: ${{ secrets.AZURE_CREDENTIALS }}
+
+      - name: Fetch secrets from key vault
+        uses: azure/CLI@v2
+        id: keyvault-yaml-secret
+        with:
+          inlineScript: |
+            SONAR_TOKEN=$(az keyvault secret show --name "SONAR-TOKEN" --vault-name "s189t01-cpdecf-rv-kv" --query "value" -o tsv)
+            echo "::add-mask::$SONAR_TOKEN"
+            echo "SONAR_TOKEN=$SONAR_TOKEN" >> $GITHUB_OUTPUT
+
+      - name: Run sonarqube
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        run: sonar-scanner
+           -Dsonar.token=${{ steps.keyvault-yaml-secret.outputs.SONAR_TOKEN }}
+           -Dsonar.organization=dfe-digital
+           -Dsonar.host.url=https://sonarcloud.io/
+           -Dsonar.projectKey=DFE-Digital_early-careers-framework
+           -Dsonar.testExecutionReportPaths=${{github.workspace}}/${{env.unit-tests-artifact-name}}_0_tests/test-report-0.xml,\
+            ${{github.workspace}}/${{env.unit-tests-artifact-name}}_1_tests/test-report-1.xml,\
+            ${{github.workspace}}/${{env.unit-tests-artifact-name}}_2_tests/test-report-2.xml,\
+            ${{github.workspace}}/${{env.unit-tests-artifact-name}}_3_tests/test-report-3.xml,\
+            ${{github.workspace}}/${{env.unit-tests-artifact-name}}_4_tests/test-report-4.xml,\
+            ${{github.workspace}}/${{env.unit-tests-artifact-name}}_5_tests/test-report-5.xml,\
+            ${{github.workspace}}/${{env.unit-tests-artifact-name}}_6_tests/test-report-5.xml,\
+            ${{github.workspace}}/${{env.unit-tests-artifact-name}}_0_feature_tests/test-report-1.xml,\
+            ${{github.workspace}}/${{env.unit-tests-artifact-name}}_1_feature_tests/test-report-2.xml,\
+            ${{github.workspace}}/${{env.unit-tests-artifact-name}}_2_feature_tests/test-report-3.xml,\
+            ${{github.workspace}}/${{env.unit-tests-artifact-name}}_3_feature_tests/test-report-4.xml
+           -Dsonar.ruby.coverage.reportPaths=${{github.workspace}}/coverage/coverage.json
+           -Dsonar.ruby.rubocop.reportPaths=${{github.workspace}}/${{env.rubocop-artifact-name}}/rubocop-result.json
diff --git a/Gemfile b/Gemfile
index d5a87b1c36..177b917d54 100644
--- a/Gemfile
+++ b/Gemfile
@@ -178,6 +178,7 @@ group :test do
   gem "pundit-matchers", "~> 1.9.0"
   gem "rails-controller-testing", "~> 1.0.5"
   gem "rspec-default_http_header", "~> 0.0.6"
+  gem "rspec-sonarqube-formatter", require: false
   gem "selenium-webdriver"
   gem "shoulda-matchers", "~> 5.3"
   gem "simplecov"
diff --git a/Gemfile.lock b/Gemfile.lock
index 44044b89bb..01e85e4745 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -286,6 +286,7 @@ GEM
     hashie (5.0.0)
     html-attributes-utils (1.0.2)
       activesupport (>= 6.1.4.4)
+    htmlentities (4.3.4)
     httparty (0.22.0)
       csv
       mini_mime (>= 1.0.0)
@@ -503,6 +504,10 @@ GEM
       nokogiri
     rexml (3.4.0)
     rouge (4.5.1)
+    rspec (3.13.0)
+      rspec-core (~> 3.13.0)
+      rspec-expectations (~> 3.13.0)
+      rspec-mocks (~> 3.13.0)
     rspec-core (3.13.2)
       rspec-support (~> 3.13.0)
     rspec-default_http_header (0.0.6)
@@ -521,6 +526,9 @@ GEM
       rspec-expectations (~> 3.13)
       rspec-mocks (~> 3.13)
       rspec-support (~> 3.13)
+    rspec-sonarqube-formatter (1.6.3)
+      htmlentities (~> 4.3)
+      rspec (~> 3.0)
     rspec-support (3.13.1)
     rswag-specs (2.16.0)
       activesupport (>= 5.2, < 8.1)
@@ -790,6 +798,7 @@ DEPENDENCIES
   rouge
   rspec-default_http_header (~> 0.0.6)
   rspec-rails (~> 6.1.5)
+  rspec-sonarqube-formatter
   rswag-specs (~> 2.16)
   rubocop-govuk (>= 4.8)
   rubyzip (~> 2.4)
diff --git a/lib/tasks/coverage.rake b/lib/tasks/coverage.rake
new file mode 100644
index 0000000000..7dd5b94159
--- /dev/null
+++ b/lib/tasks/coverage.rake
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+desc "Code Coverage tasks"
+namespace :coverage do
+  desc "Collates all result sets generated by the different test runners"
+  task collate: :environment do
+    require "simplecov"
+    require "simplecov_json_formatter"
+
+    SimpleCov.collate Dir["#{ENV['COVERAGE_DIR']}/**/.resultset.json"], "rails" do
+      formatter SimpleCov::Formatter::MultiFormatter.new([
+        SimpleCov::Formatter::HTMLFormatter,
+        SimpleCov::Formatter::JSONFormatter,
+      ])
+    end
+  end
+end
diff --git a/sonar-project.properties b/sonar-project.properties
new file mode 100644
index 0000000000..e583fdda37
--- /dev/null
+++ b/sonar-project.properties
@@ -0,0 +1,8 @@
+sonar.projectKey=DFE-Digital_npq-registration
+sonar.organization=dfe-digital
+sonar.exclusions=
+sonar.sources=app,lib
+sonar.tests=./spec
+sonar.ruby.coverage.framework=RSpec
+sonar.ruby.rubocop.reportPath=rubocop-result.json
+sonar.ruby.coverage.reportPaths=code_coverage/.resultset.json
diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb
index fd99dd9eee..9a86845c12 100644
--- a/spec/rails_helper.rb
+++ b/spec/rails_helper.rb
@@ -1,6 +1,7 @@
 # frozen_string_literal: true
 
 require "simplecov"
+require "simplecov_json_formatter"
 SimpleCov.start
 
 # This file is copied to spec/ when you run 'rails generate rspec:install'