Danger. Автоматизируем ревью на CI и пишем свой плагин

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!

Привет, я Татьяна Родионова, 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/


Интересные статьи

Интересные статьи

Если вы не читали первую часть статьи, то сделайте это. Часть 2: создаём бэкдор Хватит ждать 28 апреля 2022 года выпустили новые версии обновлений прошивок Display Audio для автомобилей Hyundai и...
Первый лонгрид 2021 год посвящается бесстрашным беларуским журналисткам (Катерине Борисевич, Дарье Чульцовой, Катерине Андреевой), которые сейчас находятся под стражей и «ждут длинных инт...
В этой статье я хочу показать и подробно объяснить пример создания шеллкода на ассемблере в ОС Windows 7 x86. Не смотря на солидный возраст данной темы, она остаётся актуальной и по с...
До третьей международной конференции по практической кибербезопасности OFFZONE 2020 остался всего 71 день. У организаторов кипит работа, но и участникам есть чем себя занять в ожидании. В сегодня...
Со временем, каждый проект растет и реализовывать новый функционал в существующий монолит становится все сложнее, дольше и дороже для бизнеса. Один из вариантов решения данной проблемы — использ...