Привет, я Татьяна Родионова, Android-разработчица в Lamoda. Как-то раз передо мной появилась задача упростить ревью пул-реквестов с помощью Danger. Я решила добавить автоматическую проверку кодстайла, используя ktlint. Но оказалось, что Danger не поддерживает такое решение, поэтому я добавила такую проверку сама 🙂
Моя статья поможет разобраться в том, как настроить Danger и как заставить его выполнять задачи немного сложнее тех, которые есть в официальном туториале.
Что такое Danger и как его установить
Danger — это система для автоматизации сообщений во время код-ревью, которая запускается на CI. Она позволяет избавиться от написания однотипных комментариев о кодстайле, ошибках в описании пул-реквеста, или, например, о его размере.
Для установки потребуется nix-система. На MacOS можно воспользоваться командой:
brew install danger/tap/danger-kotlin
А на Linux:
bash <(curl -s https://raw.githubusercontent.com/danger/kotlin/master/scripts/install.sh) source ~/.bash_profile
Настройка контейнера с Danger
Для настройки конфигурации Danger используется Dangerfile.df.kts
, который лежит в корне проекта. Язык — Kotlin DSL, с поддержкой автодополнения и подсветкой синтаксиса в Android Studio.
Конфигурацию нужно написать самому и включить в нее те элементы, которые необходимы для конкретной задачи. Вот пример стандартной конфигурации с Github:
mport systems.danger.kotlin.* danger(args) { val allSourceFiles = git.modifiedFiles + git.createdFiles val changelogChanged = allSourceFiles.contains("CHANGELOG.md") val sourceChanges = allSourceFiles.firstOrNull { it.contains("src") } onGitHub { val isTrivial = pullRequest.title.contains("#trivial") // Changelog if (!isTrivial && !changelogChanged && sourceChanges != null) { warn(WordUtils.capitalize("any changes to library code should be reflected in the Changelog.")) } // Big PR Check if ((pullRequest.additions ?: 0) - (pullRequest.deletions ?: 0) > 300) { warn("Big PR, try to keep changes smaller if you can") } // Work in progress check if (pullRequest.title.contains("WIP", false)) { warn("PR is classed as Work in Progress") } } }
Эта конфигурация для Github проверяет, нужно ли добавлять информацию в changelog
, не слишком ли это большой пул-реквест (>300 изменений) и есть ли WIP (Work In Progress) в названии пул-реквеста.
В нашем проекте было решено запускать Danger изолированно, поэтому он настроен в Docker-контейнере. На это было несколько причин:
-
Это удобно, так как docker-контейнер содержит только Danger, и необходимое окружение.
-
У Danger есть конфликтующие имплементации. При запуске Danger вызывается команда
which danger
, которая определяет, какую вызвать имплементацию. На нашем CI была установлена ruby-имплементация Danger для iOS, вызываемая по умолчанию, поэтому для запуска Danger под Android пришлось бы ее удалять (Android требует JS-имплементацию). С docker таких проблем нет.
Для запуска необходима конфигурация в Dockerfile и скрипты запуска build.sh
и env.list
со списком переменных окружения.
Начнем с настройки конфигурации docker-файла. Устанавливаем make
, nodejs
и danger js
:
RUN apt-get update \ && apt-get install make \ && apt-get install -y ca-certificates \ && curl -sL https://deb.nodesource.com/setup_12.x | bash - \ && apt-get install -y make zip nodejs \ && npm install -g danger
Далее устанавливаем kotlinc
, скачиваем danger-kotlin
и собираем его из исходного кода из репозитория:
RUN curl -o kotlinc_inst.zip -L $KOTLINC_URL \ && mkdir -p ${ANDROID_SDK_ROOT}/kotlinc/ \ && unzip -q kotlinc_inst.zip -d /tmp/ \ && mv /tmp/kotlinc/ ${ANDROID_SDK_ROOT}/kotlinc/ \ && rm kotlinc_inst.zip \ && git clone https://github.com/danger/kotlin.git --branch 1.0.0 --depth 1 _danger-kotlin \ && cd _danger-kotlin \ && sed -i 's/val emailAddress: String,/val emailAddress: String? = null,/g' $FILE \ && make install
Тут можно заметить фикс в виде вызова sed
. Это исправление кейса, когда у автора пул-реквеста нет email (например, он уже уволился). В таком случае пул-реквест не запустится из-за бага, который не починили разработчики.
Скрипт запуска build.sh
выглядит следующим образом:
#!/bin/sh set -e echo "Running danger" ./gradlew ktlint danger-kotlin $DANGER_ARG echo "Build successfully finished" exit 0
Кстати, перед запуском Danger нужно запустить ktlint. Сам по себе Danger не запускает gradle-таски, но может интерпретировать их результаты с помощью плагинов.
В файл с переменными env.list
добавляем переменные окружения, необходимые для запуска контейнера. В нашем случае используются переменные для Bitbucket, но они с легкостью могут быть заменены на другие поддерживаемые веб-сервисы для хостинга — Github или Gitlab.
Для корректной работы в нужно указать host
, username
и token
аккаунта, который будет постить сообщения. Дополнительно я добавила DANGER_ARG
, чтобы была возможность менять тип команды и указывать дополнительные параметры: например, pr
— отображает вывод Danger в консоль, а ci
постит комментарий в пул-реквест:
DANGER_BITBUCKETSERVER_HOST=https://stash.lamoda.ru DANGER_BITBUCKETSERVER_USERNAME=La Cat DANGER_BITBUCKETSERVER_TOKEN=Заполнить перед сборкой DANGER_ARG=pr ci
Все готово к запуску, теперь собираем наш контейнер! Для этого запускаем команду:
docker build -t mobile.docker.lamoda.ru/android/build/android-danger:1.0
mobile.docker.lamoda.ru/android/build/android-danger:1.0
— имя образа Docker. Запускаем его:
docker run --name android-danger -it -v /path/to/project/source/files:/src -w /src --env-file env.list mobile.docker.lamoda.ru/android/build/android-danger:1.0
Чтобы залить образ в хранилище артефактов, исполняем эту команду:
docker push mobile.docker.lamoda.ru/android/build/android-danger:1.0
Теперь у нас есть готовый образ, который можно использовать на CI. Открываем готовый план (мы в Lamoda используем bamboo), идем в настройки и указываем образ для скачивания:
Далее добавляем шаг для запуска контейнера со скаченным образом:
Дополнительно нужно указать в переменных окружения bitbucket host
, username
и token
, чтобы была возможность быстро изменить их без пересборки контейнера. Также в переменные окружения контейнера нужно добавить номер пул-реквеста pr_key, repositoryUrl и имя плана — без них Danger не увидит нужные ему параметры пул-реквеста при запуске.
bamboo_repository_pr_key=${bamboo.repository.pr.key} bamboo_planRepository_repositoryUrl=${bamboo.planRepository.repositoryUrl} bamboo_buildPlanName=${bamboo.buildPlanName}
Контейнер на CI настроен. По умолчанию установим триггер bamboo на создание и обновление пул-реквеста: Danger должен запускаться при создании пул-реквеста и его обновлении.
Теперь осталось разобраться с конфигурацией Danger.
Плагины Danger
Проверки Danger ограничиваются встроенным API. Используя его, можно проверять данные, которые касаются пул-реквеста: например, коммиты и их авторов. Но более сложные задачи реализуются плагинами. Их написано немного, но с их помощью можно запустить Android Lint, Detect или получить отчет о запуске JUnit на вашем пул-реквесте.
Для подключения плагина перед написанием кода в Dangerfile
нужно указать его зависимости – репозиторий, где лежит файл, а также его название, версию, и зарегистрировать используемый плагин. Например:
@file:Repository("https://repo.maven.apache.org") @file:DependsOn("groupId:artifactId:version") register plugin TestPlugin
Мне хотелось запустить ktlint, но нужного плагина не было. Поэтому я решила написать свой простой плагин для отображения результатов. Я создавала плагин отдельно от проекта, используя композитный билд.
Так как Danger не может запускать таски самостоятельно, а только интерпретирует результаты, то для начала нужно запустить ktlint, используя gradle в контейнере. Напомню, как выглядит скрипт запуска контейнера:
#!/bin/sh set -e echo "Running danger" ./gradlew ktlint danger-kotlin $DANGER_ARG echo "Build successfully finished" exit 0
Так как отчет представляет из себя xml-файл, то основную работу будет выполнять парсер. Пример ktlint-отчета:
Подключим зависимость от Danger SDK в build.gradle
:
dependencies { implementation "systems.danger:danger-kotlin-sdk:1.2" }
Создаем kotlin-object
и наследуемся от DangerPlugin
. Переопределяем обязательный id
:
object DangerKtlintPlugin : DangerPlugin() { override val id: String get() = "systems.danger.ktlint.plugin" }
Добавляем три функции — print
, parse
и parseAll
. В print
будем передавать путь (или список путей) к отчету, а parse
будет вызывать парсер.
fun print(vararg lintFiles: String) { report(parseAll(*lintFiles)) } private fun parse(lintFilePath: String): ErrorType = LintParser.parse(lintFilePath).type private fun parseAll(vararg lintFilePaths: String): List<ErrorType> = lintFilePaths.map(::parse)
Теперь напишем сам парсер. Обходим все узлы в xml-файле, и собираем данные в модель:
internal object LintParser { fun parse(filePath: String): KtlintStructure { val factory = DocumentBuilderFactory.newInstance() val builder = factory.newDocumentBuilder() val document = builder.parse(java.io.File(filePath)) val rootElement = document.documentElement val files = arrayListOf<File>() val type = rootElement.nodeName rootElement.childNodes.forEach { file -> val fileName = file.attributes.getNamedItem("name") val lintErrors = arrayListOf<LintError>() file.childNodes.forEach { lintError -> lintErrors.add( LintError( line = lintError.attributes.getNamedItem("line").nodeValue, col = lintError.attributes.getNamedItem("column").nodeValue, severity = lintError.attributes.getNamedItem("severity").nodeValue, ruleId = lintError.attributes.getNamedItem("source").nodeValue, detail = lintError.attributes.getNamedItem("message").nodeValue, ), ) } files.add(File(lintErrors = lintErrors, name = fileName.nodeValue)) } return KtlintStructure(ErrorType(type = type, files = files)) }
Далее добавляем функцию для вывода сообщения:
private fun report(errors: List<ErrorType>) { errors.forEach { errorType -> errorType.files.forEach { file -> file.lintErrors.forEach { lintError -> val message = "⚠️ ${errorType.type} ${lintError.severity}: " + "line: ${lintError.line}, column: ${lintError.col}, " + "message: ${lintError.detail}," + "\n" + "path:${file.name} " context.fail( message, ) } } } }
Если есть ошибки в ktlint, то Danger выводит об этом сообщение. В таком случае помечаем сборку как неудачную.
Плагин написан, осталось собрать его. Настраиваем конфигурацию для build.gradle
:
buildscript { repositories { // ссылка на ваше хранилище } dependencies { classpath "systems.danger:danger-plugin-installer:0.1" } } plugins { id 'org.jetbrains.kotlin.jvm' version '1.5.0' } apply plugin: 'danger-kotlin-plugin-installer' group 'systems.danger.ktlint' version '1.0' repositories { // ссылка на ваше хранилище } dangerPlugin { outputJar = "${buildDir}/libs/danger-ktlint-plugin.jar" } dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib" implementation "systems.danger:danger-kotlin-sdk:1.2" }
Для сборки необходим jar-файл danger-plugin-installer
. Его можно собрать в одноименном модуле из официального репозитория, вызвав ./gradlew jar
.
После необходимо указать этот jar в качестве зависимости для плагина и добавить его в репозиторий. Как только зависимость danger-plugin-installer
будет установлена, нужно указать путь outputJar
для написанного плагина, а также запустить команды ./gradlew build
и ./gradlew installDangerPlugin
.
Теперь в /build/libs
появился jar библиотеки — danger-ktlint-plugin-1.0.jar
. Для использования плагина его можно залить в приватное хранилище, подключить локально или можно опубликовать в opensource, используя maven publish.
Результат
В итоге Dangerfile выглядит следующим образом. Импортируем необходимые пакеты, регистрируем плагин — и все готово к использованию.
@file:Repository(*ссылка на ваше хранилище*) @file:DependsOn("com.lamoda:danger-ktlint-plugin:1.0") import com.lamoda.danger_ktlint_plugin.DangerKtlintPlugin import systems.danger.kotlin.* register plugin DangerKtlintPlugin danger(args) { DangerKtlintPlugin.print("lamoda/build/reports/ktlint.xml") }
Теперь при запуске Danger плагин будет интерпретировать отчет ktlint и выводить в пул-реквесте в следующем виде:
Если вы исправите ошибки в следующем коммите, то сообщение обновится, и билд Danger можно считать успешным.
Надеюсь, моя статья была для вас полезной. Если остались вопросы, пишите их в комментариях!
ссылка на оригинал статьи https://habr.com/ru/company/lamoda/blog/681564/
Добавить комментарий