diff --git a/.github/actions/sonar/action.yml b/.github/actions/sonar/action.yml
new file mode 100644
index 00000000000..97e1fac9707
--- /dev/null
+++ b/.github/actions/sonar/action.yml
@@ -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 "Unresolved Issues (click to expand)
"
+ 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 " "
+ fi
+ } > sonar_result.md
+
+ {
+ echo 'COMMENT_BODY<> "$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::"
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index cfb53d5344a..7ab4623adcb 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -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:
diff --git a/.github/workflows/angular.yml b/.github/workflows/angular.yml
index 817642abfca..74348d03c04 100644
--- a/.github/workflows/angular.yml
+++ b/.github/workflows/angular.yml
@@ -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'
@@ -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
diff --git a/generators/spring-boot/templates/src/test/java/_package_/_entityPackage_/web/rest/_entityClass_ResourceIT.java.ejs b/generators/spring-boot/templates/src/test/java/_package_/_entityPackage_/web/rest/_entityClass_ResourceIT.java.ejs
index 7ddc72f7a51..1ba83316dc5 100644
--- a/generators/spring-boot/templates/src/test/java/_package_/_entityPackage_/web/rest/_entityClass_ResourceIT.java.ejs
+++ b/generators/spring-boot/templates/src/test/java/_package_/_entityPackage_/web/rest/_entityClass_ResourceIT.java.ejs
@@ -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) { _%>
diff --git a/renovate.json b/renovate.json
index 39f697da67d..ce5d87c7f0d 100644
--- a/renovate.json
+++ b/renovate.json
@@ -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=(?[a-z-]+?)(?: depName=(?.+?))? packageName=(?.+?)(?: versioning=(?[a-z-]+?))?\\s(?:ENV|ARG) .+?_VERSION=(?.+?)\\s"
+ ]
+ }
+ ]
}
diff --git a/test-integration/sonar-pr/Dockerfile b/test-integration/sonar-pr/Dockerfile
new file mode 100644
index 00000000000..b40ebe55b50
--- /dev/null
+++ b/test-integration/sonar-pr/Dockerfile
@@ -0,0 +1,19 @@
+# Use the official SonarQube 10.6.0 community image as the base
+FROM sonarqube:10.6.0-community
+
+# Define version and plugin JAR name as environment variables
+# renovate: datasource=github-releases depName=sonarqube-community-branch-plugin packageName=mc1arke/sonarqube-community-branch-plugin
+ENV SONAR_PLUGIN_VERSION=1.21.0
+ENV SONAR_PLUGIN_JAR=sonarqube-community-branch-plugin-${SONAR_PLUGIN_VERSION}.jar
+
+# Download and place the SonarQube Community Branch Plugin in the correct directory
+RUN mkdir -p /opt/sonarqube/extensions/plugins \
+ && curl -L -o /opt/sonarqube/extensions/plugins/${SONAR_PLUGIN_JAR} https://github.com/mc1arke/sonarqube-community-branch-plugin/releases/download/${SONAR_PLUGIN_VERSION}/${SONAR_PLUGIN_JAR}
+
+# Set environment variables for SonarQube configurations
+ENV SONAR_ES_BOOTSTRAP_CHECKS_DISABLE=true \
+ SONAR_WEB_JAVAADDITIONALOPTS="-javaagent:./extensions/plugins/${SONAR_PLUGIN_JAR}=web" \
+ SONAR_CE_JAVAADDITIONALOPTS="-javaagent:./extensions/plugins/${SONAR_PLUGIN_JAR}=ce"
+
+# Expose SonarQube's default port
+EXPOSE 9000
diff --git a/test-integration/sonar-pr/docker-compose.yml b/test-integration/sonar-pr/docker-compose.yml
new file mode 100644
index 00000000000..1dba3825b42
--- /dev/null
+++ b/test-integration/sonar-pr/docker-compose.yml
@@ -0,0 +1,11 @@
+services:
+ sonarqube:
+ build: .
+ container_name: sonar-server
+ ports:
+ - '9000:9000'
+ healthcheck:
+ test: wget -qO- http://localhost:9000/api/system/status | grep -q -e '"status":"UP"' -e '"status":"DB_MIGRATION_NEEDED"' -e '"status":"DB_MIGRATION_RUNNING"' > /dev/null
+ interval: 5s
+ timeout: 5s
+ retries: 30