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