Привет, Хабр! Концепция Code Ownership известна уже много лет. Если за код никто не отвечает, то изменения добавляются неконтролируемо, и со временем код превращается в месиво, в котором очень тяжело разобраться и с которым никто не хочет связываться. Такой код является вечным источником дефектов, замедляет добавления новых фич и служит демотиватором для всех, кто с ним взаимодействует. Хочу рассказать как мы реализуем владение кодом в нашем проекте и какую пользу это нам приносит.

Как это работает?

Мы используем многомодульную API/impl-архитектуру: модули API предоставляют данные и методы другим модулям, а impl-модули делают большую часть работы и к ним нельзя напрямую подключаться. Написали плагин для Gradle, который позволяет задать владельца модуля в build.gradle.kts. Все наши самописные плагины находятся в составной сборке внутри проекта:

Наши плагины для Gradle.
Наши плагины для Gradle.

Подробнее про составные сборки можно почитать тут. В нашем случае, каждый владелец — это кросс-функциональная команда, отвечающая за какую-либо функциональность в проекте (например, портфель или рынок).

description = "Модуль плагинов для определения владельцев кода"
group = "investor.buildlogic"

gradlePlugin {
    plugins {
        create("inv.ownership") {
            id = "inv.ownership"
            implementationClass = "inv.ownership.OwnershipPlugin"
        }
    }
}
class OwnershipPlugin : Plugin<Project> {

    override fun apply(target: Project) {
        if (target == target.rootProject) {
            target.subprojects
                .filter { it.projectDir.resolve("build.gradle.kts").exists() }
                .forEach { subProject ->
                    subProject.extensions.create("ownership", OwnershipExtension::class.java)
                }
        }
    }
}
ownership {
    owner.set(Team.FINANCIAL_INSTRUMENTS)
}

Далее другие плагины для Gradle могут распарсить эту информацию и сделать с ней что-нибудь полезное.

val extension = project.extensions.findByType(OwnershipExtension::class.java)
val owner: Owner = extension?.owner?.orNull ?: emptyOwnerError(project)

Как мы это используем

Загрузка документации в Confluence

Плагин для Gradlе, который собирает всю Javadoc-информацию в модулях API, генерирует HTML-страницы с помощью Dokka и загружает в Confluence через Rest API. Признак Code ownership используется для распределения документации по владельцам. Очень полезная вещь для аналитиков и владельцев продукта, помогающая находить необходимую функциональность без возвращения в исходный код проекта.

Автоматическое добавление владельцев кода к проверке pull request

Используется в связке с ещё одним плагином для Gradle, который мы написали для анализа влияния. Позволяет отслеживать зависимости от внешних библиотек и между модулям проекта.

private fun fillDependencies(rootProject: Project) {
    rootProject.subprojects.filter { it.hasBuildGradleKts() }.forEach { subProject -> 
        val module = modules.first { it.name == subProject.fullName() }
        subProject.configurations.forEach { configuration ->
            configuration.dependencies.forEach { dependency ->
                val targetName = "${dependency.group?.removePrefix("${rootProject.name}.")}:${dependency.name}"
                val dependencyModule =
                    modules.find {
                        it.name.endsWith(targetName) 
                                && dependency.group?.startsWith(rootProject.name) == true
                    }
                if (dependencyModule != null) {
                    // Тестовые плагины добавляют модуль в зависимый classpath к себе самому (debugAndroidTestCompileClasspath/debugUnitTestCompileClasspath/releaseUnitTestRuntimeClasspath...)
                    if (module.name != dependencyModule.name) {
                        val dependencies = moduleDependencies[module] ?: HashSet()
                        dependencies.add(dependencyModule)
                        moduleDependencies[module] = dependencies
                    }
                } else {
                    val library = LibraryDependency(
                        dependency.group ?: "unspecified",
                        dependency.name,
                        dependency.version ?: "unspecified"
                    )
                    val dependencies = libraryDependencies[module] ?: HashSet()
                    dependencies.add(library)
                    libraryDependencies[module] = dependencies
                }
            }
        }
    }
}

Когда в pull request изменяется код, мы автоматически добавляем к проверке владельцев этого кода, и без их утверждения этот pull request влить не получится. Также мы отслеживаем добавление зависимостей от модулей API и подключаем их владельцев к проверке, что позволяет командам отслеживать нагрузку на их функциональность и понимать, как её используют (например, когда нужно добавить пару серверов для микросервиса, чтобы удержать нагрузку). Для этого мы собираем граф зависимостей в pull request, сравниваем его с графом зависимостей целевой ветки и собираем дифф:

private fun calculateDiff(
    currentDependencies: Map<String, List<String>>,
    developDependencies: Map<String, List<String>>
): DependencyGraphDiff {
    val addedDependencies = HashMap<String, List<String>>()
    val removedDependencies = HashMap<String, List<String>>()
    currentDependencies.keys.forEach { key ->
        val current = currentDependencies[key].orEmpty()
        val inDevelop = developDependencies[key].orEmpty()
        val addedDiff = current - inDevelop.toSet()
        if (addedDiff.isNotEmpty()) {
            addedDependencies[key] = addedDiff
        }
        val removedDiff = inDevelop - current.toSet()
        if (removedDiff.isNotEmpty()) {
            removedDependencies[key] = removedDiff
        }
    }
    return DependencyGraphDiff(addedDependencies, removedDependencies)
}

После этого по диффу мы находим владельцев и записываем в файл:

val ownerLabels = (dependencyDiff.addedDependencies.values.asSequence() + dependencyDiff.removedDependencies.values.asSequence())
    .flatten()
    .toSet()
    .map { moduleName ->
        var result: Owner = UNKNOWN
        val module = service.findModule(project, moduleName)
        if (module != null) {
            val extension = module.project.extensions.findByType(OwnershipExtension::class.java)
            val owner: Owner = extension?.owner?.orNull ?: emptyOwnerError(project)
            result = owner
        }
        result
    }
    .toSet()
val reportFile = project.buildDir.resolve(NEW_API_DEPENDENCIES_OWNERS_REPORT_PATH)
reportFile.parentFile.mkdirs()
reportFile.writeText(ownerLabels.map { it.prHandlerLabel() }.filter { it.isNotEmpty() }.joinToString(separator = ","))

Затем стейдж в JenkinsFile читает файл с владельцами и добавляет к проверке через Rest API BitBucket.

Автоматическое отслеживание техдолга в проекте

У нас есть автоматический учёт техдолга во всём проекте. Мы собираем проблемы обнаружения и линтинга, выключенные тесты, устаревшие feature toggle, использование устаревшего кода, самописные проверки (например, в тестах это позволяет уходить от использования mock в пользу фейков и собирать все использования mock) и многое другое. Данные собираются раз в день c помощью задачи в Jenkins, которая запускает Gradle-плагин, собирающий данные по техдолгу, и отправляет информацию в Kibana.

Владение кодом позволяет легко разделить, кому что из техдолга принадлежит, и создать красивые дашборды, чтобы лидеры команд и владельцы продуктов могли легко отслеживать состояния техдолга в принадлежащих им функциональностях.

Техдолг по категориям.
Техдолг по категориям.
Техдолг по модулям.
Техдолг по модулям.

Вариантов использования концепции владения кодом очень много. К примеру, сбор статистики по проекту позволяет посмотреть, кто за какую часть проекта отвечает и не пора ли добавить рабочую силу. И вдобавок бысто найти исполнителя для исправления какого-либо дефекта.

Если есть вопросы, готов ответить на них в комментариях.

Вафин Марат Камилевич

СберИнвестиции, команда Android-платформы

Комментарии (0)