SonarQube: делаем код лучше
Привет! Меня зовут Сергей, я один из разработчиков продукта «Сервис персонализации» в компании Sportmaster Lab, и в этом посте я расскажу про SonarQube — платформу для непрерывного анализа и измерения качества программного кода, разработанную компанией SonarSource.
Сейчас SonarQube является чем-то вроде отраслевого стандарта. В частности, это стандарт центра компетенций Sportmaster Lab. В своей работе SonarQube использует статический анализ кода: реальное его выполнение не требуется, так как анализируются именно «исходники». Предмет анализа этого инструмента — потенциальные ошибки и уязвимости, стандарты оформления кода, наличие тестов и уровень покрытия ими, а также дублирование кода и его поддерживаемость. SonarQube поддерживает большое количество языков программирования, его можно встраивать в конвейер CI/CD и в вашу среду разработки. А еще — файлы отчетов сторонних инструментов (Eslint, Stylelint, OWASP Dependency Check и многие другие).
В этой статье вы узнаете:
как работать с SonarQube;
как он может помочь вам в работе и сделать ваш код лучше.
Эта статья — мой сугубо личный опыт работы с SonarQube в качестве пользователя, поэтому я не буду останавливаться на таких «административных» моментах, как развертывание SonarQube, создание в нем нового проекта и настройка его интеграции с GitLab.
Что у нас есть на старте:
SonarQube v9.9 и предварительно созданный пустой проект.
GitLab v15.11.3 в качестве хранилища кода и конвейера CI/CD.
Настроенная интеграция между SonarQube и GitLab.
Два логически взаимосвязанных проекта: бэкенд (Kotlin 1.6, Spring Boot 2.6, JUnit 5) и фронтенд (Typescript 4.7, Vue 3, Jest 29). Оба проекта собираются при помощи Gradle (сборка фронтенда производится при помощи плагина Gradle Plugin for Node).
Настройка проекта в SonarQube
Создаем главную ветку, относительно которой будут рассчитываться все изменения в коде (master или main — зависит от того, как давно существует репозиторий):
Настраиваем профили качества («quality profile») (опционально) — наборы правил для используемых в проекте языков. По этим правилам производится анализ кода. Найденные в процессе анализа ошибки имеют классификацию по приоритету. Например, «bug» — самая приоритетная ошибка, которую нужно исправить как можно скорее. Также есть уязвимости, связанные с безопасностью. Еще есть «code smell» — незначительные огрехи, которые больше относятся к культуре кода и его оформлению, и потенциальные уязвимости. Настройка профилей качества на данном этапе является необязательной, так как после первого анализа кода SonarQube сам определяет языки, которые используются в проекте, и для каждого из них назначает профиль качества по умолчанию, который при желании можно изменить (это отразится на всех проектах, которые его используют) или создать свой собственный со своим набором правил.
Настраиваем границы качества («quality gate») — набор пороговых значений метрик проекта. Границы качества по умолчанию включают в себя следующие значения метрик: покрытие тестами («coverage») не менее 80%, дублирование кода («duplicated lines») не более 3%, рейтинги поддерживаемости («maintainability»), надежности («reliability») и безопасности («security») не должны быть ниже класса A, и также должны быть отсмотрены все потенциальные уязвимости («security hotspots»). Если хотя бы одна из этих метрик после анализа кода не соответствует указанным выше значениям, то весь код считается неподходящим для развертывания. Как и в случае с профилем качества, при желании можно изменить текущие границы качества или создать свои собственные со своими значениями пороговых метрик.
Связывание проектов SonarQube и GitLab
Как я уже писал, у нас есть два проекта в GitLab — бэкенд и фронтенд. А также настроена интеграция между SonarQube и GitLab (подробнее об этом можно почитать здесь). Теперь нам надо связать друг с другом конкретные проекты на этих платформах:
Добавляем в проект GitLab специального пользователя (в нашем случае это пользователь «Sonar Gitlab Bot») с ролью «Reporter» (как минимум). Этот пользователь используется SonarQube для взаимодействия с GitLab, а также для «декорирования» создаваемых в нем merge request«ов (MR): после завершения этапа анализа кода в MR появляется сообщение от данного пользователя с результатами.
Идем в настройки проекта SonarQube. В разделе «DevOps Platform Integration» выбираем нужную конфигурацию GitLab, указываем ID проекта в GitLab и сохраняем.
Генерируем в SonarQube специальный токен доступа: идем в свой аккаунт, открываем раздел «Security» и попадаем в раздел «Tokens», в котором представлены все сгенерированные вами токены. В самом верху таблицы вы увидите форму создания нового токена. Выбираем тип «Project Analysis», нужный проект и срок действия токена. Затем даем токену имя, нажимаем кнопку «Generate» и обязательно сохраняем где-нибудь значение получившегося токена — больше вы его не увидите!
Настройка этапа анализа кода в GitLab
Теперь приступим к самому главному — собственно анализу кода.
В корне проекта должен быть файл sonar-project.properties — именно в нем указываются все настройки, необходимые для SonarQube. Так как проектов у нас два, то и содержимое файлов для них будет несколько различаться, но можно выделить несколько ключевых настроек, которые являются общими для всех проектов:
sonar.host.url=<адрес экземпляра SonarQube>
sonar.projectName=<название проекта в SonarQube>
sonar.projectKey=<ключ проекта в SonarQube, можно найти в разделе информации о проекте>
sonar.login=<тот самый токен доступа, который был сгенерирован ранее>
sonar.sourceEncoding=UTF-8
sonar.qualitygate.wait=true
sonar.sources=<список относительных путей, где хранится исходный код>
sonar.tests=<список относительных путей, где хранятся тесты>
Обратите внимание на один важный параметр — sonar.qualitygate.wait.
Если по результатам анализа код не проходит quality gate и значение данного параметра равно «true», то этап анализа завершается с ошибкой –, а вместе с ним с ошибкой завершается и вся сборка.
Еще есть настройки вида xxx.inclusions
и xxx.exclusions
— они позволяют добавить или наоборот, исключить из анализа определенные файлы и/или директории проекта. Также есть и настройки, специфичные для конкретных языков и инструментов.
Вот так выглядит файл sonar-project.properties для бэкенда:
Здесь мы видим, что для получения информации о покрытии кода тестами используется JaCoCo.
А вот так выглядит файл sonar-project.properties для фронтенда:
Здесь же мы видим, что для получения информации о покрытии кода тестами используется файл «lcov.info», формируемый Jest«ом во время прогона тестов. А для получения информации о том, сколько тестов было выполнено и с какими результатами, используется jest-sonar. Также можно заметить, что в проекте используются линтеры Eslint и Stylelint, чьи файлы отчетов также направляются в SonarQube.
Теперь нам надо скорректировать наш конвейеры CI/CD в обоих проектах, а именно — указать задачам, отвечающим за сборку, что они должны по завершении сохранить определенные артефакты. Для этого надо внести правки в файлы .gitlab-ci.yml в каждом проекте.
Бэкенд:
stages:
- schedule
- build
variables:
GRADLE_CACHE_KEY: GRADLE_CACHE_$CI_PROJECT_ID
default:
tags: [ docker ]
image: <путь к нужному образу с DockerHub’а>
workflow:
rules:
- if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS
when: never
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
- if: $CI_PIPELINE_SOURCE =~ /^(merge_request_event|web|api|chat|schedule)$/
.gradlew:
variables:
GRADLE_OPTS: "-Dorg.gradle.daemon=false -Dorg.gradle.parallel=true -Dorg.gradle.caching=true"
before_script:
- export GRADLE_USER_HOME=`pwd`/.gradle
cache-update:
extends: [ .gradlew ]
stage: schedule
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"
cache:
- key: ${GRADLE_CACHE_KEY}
policy: push
paths:
- .gradle/wrapper
- .gradle/caches
script:
- ./gradlew clean --refresh-dependencies
build:
extends: [ .gradlew ]
stage: build
rules:
- if: $CI_PIPELINE_SOURCE != "schedule"
cache:
- key: ${GRADLE_CACHE_KEY}
policy: pull
paths:
- .gradle/wrapper
- .gradle/caches
script:
- ./gradlew build
artifacts:
when: always
paths:
- build/reports
- build/test-results
Внесенные изменения — вот эти строки:
artifacts:
when: always
paths:
- build/reports
- build/test-results
Фронтенд:
stages:
- schedule
- build
variables:
GRADLE_CACHE_KEY: GRADLE_CACHE_$CI_PROJECT_ID
default:
tags: [ docker ]
image: <путь к нужному образу с DockerHub’а>
workflow:
rules:
- if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS
when: never
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
- if: $CI_PIPELINE_SOURCE =~ /^(merge_request_event|web|api|chat|schedule)$/
.gradlew:
variables:
GRADLE_OPTS: "-Dorg.gradle.daemon=false -Dorg.gradle.parallel=true -Dorg.gradle.caching=true"
before_script:
- export GRADLE_USER_HOME=`pwd`/.gradle
cache-update:
extends: [ .gradlew ]
stage: schedule
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"
cache:
- key: ${GRADLE_CACHE_KEY}
policy: push
paths:
- .gradle/wrapper
- .gradle/caches
- key:
files:
- package-lock.json
policy: push
paths:
- node_modules
script:
- ./gradlew npmCi --refresh-dependencies
build:
extends: [ .gradlew ]
stage: build
rules:
- if: $CI_PIPELINE_SOURCE != "schedule"
cache:
- key: ${GRADLE_CACHE_KEY}
policy: pull
paths:
- .gradle/wrapper
- .gradle/caches
- key:
files:
- package-lock.json
policy: pull
paths:
- node_modules
script:
- if [[ ! -d "$CI_PROJECT_DIR/node_modules" ]]; then
./gradlew npmCi;
fi
- ./gradlew build
artifacts:
when: always
paths:
- build
Изменили мы вот этот блок:
artifacts:
when: always
paths:
- build
В списке артефактов надо указать пути к тем файлам и/или директориям, которые указаны в файле sonar-project.properties в качестве источника какой-либо дополнительной информации (отчеты линтеров, отчеты о покрытии кода тестами, отчеты о результатах выполнения тестов и т.д.). Конкретно в нашем случае для фронтенда я настроил сборку так, что все подобные артефакты сохраняются в одну общую директорию build/reports с разбиением на субдиректории.
А теперь добавим в конвейеры CI/CD задачу анализа кода. SonarQube позволяет проводить анализ несколькими способами:
В нашем случае будет использоваться последний вариант — он универсален и не зависит от того, каким инструментом производится сборка проекта.
Бэкенд и фронтенд (внесенные изменения — все после 7-й строки):
stages:
- schedule
- build
- analyze
................
sonarqube:
stage: analyze
image:
name: sonarsource/sonar-scanner-cli:4.8
entrypoint: [ "" ]
variables:
GIT_STRATEGY: clone
GIT_DEPTH: 0
rules:
- if: $CI_PIPELINE_SOURCE != "schedule"
needs: [ build ]
script:
- sonar-scanner
Обратите внимание на переменные GIT_STRATEGY и GIT_DEPTH — для корректной работы SonarQube требуется полная история изменений по той ветке репозитория, которая подвергается анализу.
Сохраняем все внесенные нами изменения и ждем завершения сборки. Посмотрим, что у нас получилось, на примере проекта бэкенда:
Мы видим, что у нас одна потенциальная уязвимость, 9 недочетов («code smells»), покрытие тестами меньше 80%, 104 теста и нет дублей в коде — выглядит вполне прилично. Для получения деталей можно кликнуть на нужную цифру и посмотреть отчет по каждой найденной проблеме.
Анализ кода в монорепозиториях
Раз уж два наших проекта логически связаны друг с другом, то давайте попробуем объединить их в один проект-монорепозиторий (довольно распространенная практика организации хранения кода) и корректно проанализировать его код, чтобы иметь агрегированное представление о состоянии кода всего продукта, а не отдельных его модулей.
Теперь файл sonar-project.properties выглядит так:
А вот так теперь выглядят результаты анализа кода:
Теперь мы имеем не два отдельных отчета о состоянии кода каждого из модулей продукта, а один суммарный, который отражает общее состояние всей кодовой базы.
Если вы хотите дополнительно использовать SonarQube в своей IDE, то устанавливайте плагин SonarLint.
Зачем все это вообще нужно
Преимущества использования SonarQube:
контроль объема технического долга (сколько кода не покрыто тестами, сколько кода дублируется, есть ли в коде уязвимости, есть ли код «с душком»);
контроль соответствия кода принятым в компании стандартам оформления;
возможность отслеживать историю изменений метрик кода, его «эволюцию/деградацию».
Если у вашего продукта большая кодовая база, а SonarQube к нему подключили далеко не сразу, то вполне может оказаться, что код не проходит quality gate (например, мало тестов) — в этом случае можно временно разрешить задаче анализа кода завершаться с ошибкой, чтобы не блокировать доставку нового функционала в продукт. Для этого в задачу sonarqube в GitLab добавляем параметр allow_failure со значением «true».
Как только будут внесены правки, приводящие код в состояние, при котором он проходит quality gate, этот параметр нужно убрать. Или же можно создать свой собственный quality gate со своим набором значений метрик, если вы уверены, что для вашего продукта, например, достаточно минимального уровня покрытия тестами в 60%. Но, как бы то ни было, повторюсь: если код не проходит quality gate, то ему не место в главной ветке продукта.
Если вы тоже используете SonarQube для улучшения качества кода — поделитесь своим впечатлением в комментариях.