Skip to content

Commit

Permalink
Merge pull request #26993 from anarsultanov/issue-19467-sonar
Browse files Browse the repository at this point in the history
Add SonarQube Analysis for PRs
  • Loading branch information
mshima authored Aug 22, 2024
2 parents 1ffb5de + 09dc622 commit d1415ad
Show file tree
Hide file tree
Showing 7 changed files with 360 additions and 8 deletions.
282 changes: 282 additions & 0 deletions .github/actions/sonar/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,282 @@
#
# Copyright 2013-2024 the original author or authors from the JHipster project.
#
# This file is part of the JHipster project, see https://www.jhipster.tech/
# for more information.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
name: 'SonarQube PR Analysis'
description: 'A GitHub Action to perform SonarQube analysis on a PR with caching and metrics retrieval.'

inputs:
sonar-project-key:
description: 'SonarQube project key'
required: true
application-dir:
description: 'Application directory'
required: true
docker-compose-file:
description: 'Path to the Docker Compose file'
required: true
comment-token:
description: 'GitHub token for commenting on the PR'
required: true

runs:
using: composite
steps:
# Make sure the sonar project folder exists.
- run: mkdir -p ./${{ inputs.sonar-project-key }}
shell: bash

# Cache the main branch project for future PR analysis
- name: 'Cache project on main branch'
if: github.ref == 'refs/heads/main'
shell: bash
run: |
cp -r ${{ inputs.application-dir }}/. .
rm -rf node_modules \
package-lock.json \
target/node \
target/*.original \
target/*.jar
working-directory: ${{ inputs.sonar-project-key }}

- name: 'Store cache (main branch)'
if: github.ref == 'refs/heads/main'
uses: actions/cache/save@v4
with:
path: ./${{ inputs.sonar-project-key }}
key: application-${{ inputs.sonar-project-key }}-${{ github.sha }}

# Restore the cache for PR analysis
- name: 'Restore project cache for PR analysis'
if: github.event_name == 'pull_request'
id: restore_cache
uses: actions/cache/restore@v4
with:
path: ./${{ inputs.sonar-project-key }}
key: application-${{ inputs.sonar-project-key }}-${{ github.event.pull_request.base.sha }}
restore-keys: |
application-${{ inputs.sonar-project-key }}-
# Start SonarQube server using Docker Compose
- name: 'Start SonarQube server'
if: github.event_name == 'pull_request'
shell: bash
run: |
echo "::group::Starting sonarqube"
docker compose -f ${{ inputs.docker-compose-file }} up --build -d --wait
echo "::endgroup::"
# Create SonarQube project
- name: 'Create SonarQube project'
if: github.event_name == 'pull_request'
shell: bash
run: |
curl -s -u admin:admin -X POST "http://localhost:9000/api/projects/create?name=${{ inputs.sonar-project-key }}&project=${{ inputs.sonar-project-key }}" || true
# Run SonarQube analysis on the main branch or add an empty scan
- name: 'Run SonarQube analysis on main branch'
if: github.event_name == 'pull_request' && steps.restore_cache.outputs.cache-hit == 'true'
shell: bash
run: >-
echo "::group::Scanning main branch application" &&
./mvnw --batch-mode initialize org.jacoco:jacoco-maven-plugin:prepare-agent sonar:sonar
-Dsonar.host.url=http://localhost:9000
-Dsonar.projectKey=${{ inputs.sonar-project-key }}
-Dsonar.login=admin
-Dsonar.password=admin
-Dsonar.branch.name=main &&
echo "::endgroup::"
working-directory: ${{ inputs.sonar-project-key }}

- name: Add an empty scan to sonar main branch
if: github.event_name == 'pull_request' && steps.restore_cache.outputs.cache-hit != 'true'
shell: bash
run: >-
echo "::group::Scanning empty commit" &&
git init &&
git commit -m "Initial commit" --allow-empty &&
docker run --net=host -v ".:/usr/src" --rm sonarsource/sonar-scanner-cli
-Dsonar.host.url=http://localhost:9000
-Dsonar.projectKey=${{ inputs.sonar-project-key }}
-Dsonar.login=admin
-Dsonar.password=admin
-Dsonar.branch.name=main &&
echo "::endgroup::"
working-directory: ${{ inputs.sonar-project-key }}

# Prepare the repository for the PR changes
- name: 'Clean repository for PR changes'
if: github.event_name == 'pull_request'
shell: bash
run: find . -mindepth 1 -not -path "./.git*" -delete
working-directory: ${{ inputs.sonar-project-key }}

- name: 'Apply PR changes to the repository'
if: github.event_name == 'pull_request'
shell: bash
run: |
echo "::group::Creating changes commit"
rm -rf "${{ inputs.application-dir }}/.git"
cp -r ${{ inputs.application-dir }}/. .
git checkout -b dev
git add -A
git commit -m "Apply changes from PR branch"
echo "::endgroup::"
working-directory: ${{ inputs.sonar-project-key }}

# Run SonarQube analysis on the PR changes
- name: 'Run SonarQube analysis on PR changes'
if: github.event_name == 'pull_request'
shell: bash
run: >-
echo "::group::Scanning PR application changes" &&
./mvnw --batch-mode initialize org.jacoco:jacoco-maven-plugin:prepare-agent sonar:sonar
-Dsonar.host.url=http://localhost:9000
-Dsonar.projectKey=${{ inputs.sonar-project-key }}
-Dsonar.login=admin
-Dsonar.password=admin
-Dsonar.pullrequest.key=${{github.event.pull_request.number}}
-Dsonar.pullrequest.branch=dev
-Dsonar.pullrequest.base=main
-Dsonar.scm.revision=$(git rev-parse HEAD) &&
echo "::endgroup::"
working-directory: ${{ inputs.sonar-project-key }}

# Wait for SonarQube tasks to complete
- name: 'Wait for SonarQube tasks to complete'
if: github.event_name == 'pull_request'
shell: bash
run: |
timeout 300s bash -c 'while :; do
response=$(curl -s -u admin:admin "http://localhost:9000/api/ce/component?component=${{ inputs.sonar-project-key }}")
queue_status=$(echo "$response" | jq -r ".queue[]?.status")
current_status=$(echo "$response" | jq -r ".current.status")
if [[ "$queue_status" == "" && "$current_status" != "IN_PROGRESS" ]]; then
echo "All tasks completed or no tasks pending."
break
fi
if [[ "$queue_status" == "PENDING" || "$queue_status" == "IN_PROGRESS" || "$current_status" == "IN_PROGRESS" ]]; then
echo "Tasks are still in progress or pending. Waiting..."
sleep 10
else
echo "All tasks completed."
break
fi
done' || (echo "SonarQube tasks failed to complete in time" && exit 1)
# Retrieve SonarQube metrics for the PR
- name: 'Retrieve SonarQube metrics'
if: github.event_name == 'pull_request'
id: sonar_metrics
shell: bash
run: |
SONAR_RESPONSE=$(curl -s -u admin:admin \
"http://localhost:9000/api/measures/component?component=${{ inputs.sonar-project-key }}&pullRequest=${{ github.event.pull_request.number }}&metricKeys=new_bugs,new_vulnerabilities,new_code_smells,new_coverage,new_duplicated_lines_density,new_violations")
export_measure() {
METRIC_NAME=$1
ENV_VAR_NAME=$2
METRIC_VALUE=$(echo "$SONAR_RESPONSE" | jq -r ".component.measures[] | select(.metric == \"$METRIC_NAME\") | .period.value")
if [ -z "$METRIC_VALUE" ] || [ "$METRIC_VALUE" == "null" ]; then
METRIC_VALUE="N/A"
fi
echo "$ENV_VAR_NAME=$METRIC_VALUE" >> $GITHUB_ENV
export $ENV_VAR_NAME=$METRIC_VALUE
}
export_measure "new_vulnerabilities" "NEW_VUL"
export_measure "new_code_smells" "NEW_CSM"
export_measure "new_bugs" "NEW_BUG"
export_measure "new_violations" "NEW_VIOLATIONS"
export_measure "new_coverage" "NEW_COV"
export_measure "new_duplicated_lines_density" "NEW_DUP"
{
echo "## :bar_chart: SonarQube Analysis for ${{ inputs.sonar-project-key }}"
echo ""
echo "| Metric | Value |"
echo "|-----------------------------|-------------|"
echo "| **New Vulnerabilities** | ${NEW_VUL} |"
echo "| **New Bugs** | ${NEW_BUG} |"
echo "| **New Code smells** | ${NEW_CSM} |"
echo "| **Coverage on New Code** | ${NEW_COV}% |"
echo "| **Duplication on New Code** | ${NEW_DUP}% |"
if [[ "${NEW_VIOLATIONS}" != "N/A" && "${NEW_VIOLATIONS}" != "0" ]]; then
echo ""
echo "<details><summary>Unresolved Issues (click to expand)</summary>"
echo ""
ISSUES=$(curl -s -u admin:admin \
"http://localhost:9000/api/issues/search?componentKeys=${{ inputs.sonar-project-key }}&resolved=false&pullRequest=${{ github.event.pull_request.number }}" | \
jq -r '.issues[] | "File: \(.component) Line: \(.line)\n [\(.rule)] \(.message)\n"')
echo "$ISSUES"
echo "</details>"
fi
} > sonar_result.md
{
echo 'COMMENT_BODY<<EOF'
cat sonar_result.md
echo 'EOF'
} >> "$GITHUB_ENV"
cat sonar_result.md
# Find previous SonarQube analysis comment
- name: 'Find existing PR comment with SonarQube results'
if: github.event_name == 'pull_request' && inputs.comment-token && steps.sonar_metrics.outcome == 'success'
id: find_comment
uses: peter-evans/find-comment@v3
with:
issue-number: ${{ github.event.pull_request.number }}
body-includes: '## :bar_chart: SonarQube Analysis for ${{ inputs.sonar-project-key }}'
token: ${{ inputs.comment-token }}

# Create or update PR comment with SonarQube results
- name: 'Post SonarQube results as PR comment'
if: github.event_name == 'pull_request' && inputs.comment-token && steps.sonar_metrics.outcome == 'success'
uses: peter-evans/create-or-update-comment@v4
with:
issue-number: ${{ github.event.pull_request.number }}
body: ${{ env.COMMENT_BODY }}
comment-id: ${{ steps.find_comment.outputs.comment-id }}
edit-mode: replace
token: ${{ inputs.comment-token }}

# Fail the action if there are unresolved issues
- name: 'Fail PR if unresolved issues are found'
if: >-
github.event_name == 'pull_request' &&
steps.sonar_metrics.outcome == 'success' &&
env.NEW_VIOLATIONS != 'N/A' && env.NEW_VIOLATIONS != 0 &&
!contains(github.event.pull_request.labels.*.name, 'pr: disable-sonar')
shell: bash
run: |
echo "SonarQube PR Analysis failed due to unresolved issues."
exit 1
# Stop the SonarQube server
- name: 'Stop SonarQube server'
if: github.event_name == 'pull_request'
shell: bash
run: |
echo "::group::Stopping SonarQube"
docker compose -f ${{ inputs.docker-compose-file }} down
echo "::endgroup::"
11 changes: 11 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,17 @@ updates:
- dependency-name: 'mongo'
versions: ['>=7.0.6']

- package-ecosystem: 'docker'
directory: '/test-integration/sonar-pr/'
schedule:
interval: 'daily'
time: '00:30'
open-pull-requests-limit: 5
labels:
- 'theme: dependencies'
- 'theme: docker :whale:'
- 'skip-changelog'

- package-ecosystem: 'maven'
directory: '/generators/server/resources/'
schedule:
Expand Down
18 changes: 15 additions & 3 deletions .github/workflows/angular.yml
Original file line number Diff line number Diff line change
Expand Up @@ -132,16 +132,16 @@ jobs:
jhipster-bom-ref: ${{ matrix.jhipster-bom-branch }}
- name: 'TESTS: backend'
id: backend
if: steps.compare.outputs.equals != 'true' && matrix.skip-backend-tests != 'true' && needs.build-matrix.outputs.server != 'false'
if: steps.compare.outputs.equals != 'true' && matrix.skip-backend-tests != 'true' && (matrix.sonar-analyse == 'true' || needs.build-matrix.outputs.server != 'false')
run: npm run ci:backend:test
continue-on-error: ${{matrix.continue-on-backend-tests-error || false}}
timeout-minutes: 15
- name: 'PREPARE: npm install'
if: steps.compare.outputs.equals != 'true' && matrix.skip-frontend-tests != 'true' && needs.build-matrix.outputs.client != 'false'
if: steps.compare.outputs.equals != 'true' && matrix.skip-frontend-tests != 'true' && (matrix.sonar-analyse == 'true' || needs.build-matrix.outputs.client != 'false')
run: ${{ (matrix.workspaces == 'true' && 'npm') || './npmw' }} install
timeout-minutes: 7
- name: 'TESTS: frontend'
if: steps.compare.outputs.equals != 'true' && matrix.skip-frontend-tests != 'true' && needs.build-matrix.outputs.client != 'false'
if: steps.compare.outputs.equals != 'true' && matrix.skip-frontend-tests != 'true' && (matrix.sonar-analyse == 'true' || needs.build-matrix.outputs.client != 'false')
run: npm run ci:frontend:test
timeout-minutes: 15
- name: 'TESTS: packaging'
Expand Down Expand Up @@ -191,6 +191,18 @@ jobs:
-Dsonar.login=$SONAR_TOKEN
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

- name: 'ANALYSIS: Local SonarQube PR Analysis'
if: >-
matrix.sonar-analyse == 'true' &&
steps.compare.outputs.equals != 'true'
uses: ./generator-jhipster/.github/actions/sonar
with:
sonar-project-key: ${{ matrix.name }}
application-dir: ${{ github.workspace }}/app
docker-compose-file: ${{ github.workspace }}/generator-jhipster/test-integration/sonar-pr/docker-compose.yml
comment-token: ${{ secrets.PAT_PR_ISSUES_TOKEN }}

check-angular:
permissions:
contents: none
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -524,16 +524,23 @@ filterTestableRelationships.filter(rel => !rel.otherEntity.builtInUser).forEach(
*/
public static <%= persistClass %> create<% if (fieldStatus === 'UPDATED_') { _%>Updated<%_ } %>Entity(<% if (databaseTypeSql) { %>EntityManager em<% } %>) {
<%_ if (fluentMethods) { _%>
<%= persistClass %> <%= persistInstance %> = new <%= persistClass %>()
<% if (persistableRelationships.length === 0) { %>return <% } else { %><%= persistClass %> <%= persistInstance %> = <% } %>new <%= persistClass %>()
<%_ if (reactive && databaseTypeSql && primaryKey.typeUUID && !isUsingMapsId) { _%>
.<%= primaryKey.name %>(UUID.randomUUID())
<%_ } _%>
<%_ if (primaryKey.typeString && !isUsingMapsId && !primaryKey.autoGenerate) { _%>
.<%= primaryKey.name %>(UUID.randomUUID().toString())
<%_ } _%>
<% for (field of fieldsToTest) { %>
.<%= field.fieldName %>(<%= fieldStatus + field.fieldNameUnderscored.toUpperCase() %>)<% if (field.fieldTypeBinary && !field.blobContentTypeText) { %>
.<%= field.fieldName %>ContentType(<%= fieldStatus + field.fieldNameUnderscored.toUpperCase() %>_CONTENT_TYPE)<% } %><% } %>;
<%_ for (field of fieldsToTest) { _%>
.<%= field.fieldName %>(<%= fieldStatus + field.fieldNameUnderscored.toUpperCase() %>)
<%_ if (field.fieldTypeBinary && !field.blobContentTypeText) { _%>
.<%= field.fieldName %>ContentType(<%= fieldStatus + field.fieldNameUnderscored.toUpperCase() %>_CONTENT_TYPE)
<%_ } _%>
<%_ } _%>;
<%_ if (persistableRelationships.length === 0) { _%>
}
<%_ return; _%>
<%_ } _%>
<%_ } else { _%>
<%= persistClass %> <%= persistInstance %> = new <%= persistClass %>();
<%_ if (reactive && databaseTypeSql && primaryKey.typeUUID && !isUsingMapsId) { _%>
Expand Down
12 changes: 11 additions & 1 deletion renovate.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended", ":disableDependencyDashboard"],
"enabledManagers": ["maven-wrapper", "gradle-wrapper", "nodenv"]
"enabledManagers": ["maven-wrapper", "gradle-wrapper", "nodenv", "regex"],
"customManagers": [
{
"customType": "regex",
"description": "Update _VERSION variables in Dockerfiles",
"fileMatch": ["(^|/|\\.)Dockerfile$", "(^|/)Dockerfile\\.[^/]*$"],
"matchStrings": [
"# renovate: datasource=(?<datasource>[a-z-]+?)(?: depName=(?<depName>.+?))? packageName=(?<packageName>.+?)(?: versioning=(?<versioning>[a-z-]+?))?\\s(?:ENV|ARG) .+?_VERSION=(?<currentValue>.+?)\\s"
]
}
]
}
Loading

0 comments on commit d1415ad

Please sign in to comment.