Автоматизируем выбор ревьюра с помощью GitLab CI и Danger JS

от автора

Всем здравствуйте! Меня зовут Михаил Авдеев и я работаю в проекте Облако Mail.ru! Я расскажу о том, как решал задачу ускорения проверки merge request’ов(MR)  в нашей команде. Почему вообще это понадобилось? Потому что разработчики ленивы экономят силы и обычно не стремятся брать новые MR на проверку, либо выбирают что попроще. Так что я решил сделать бота, который для каждого MR автоматически расставлял бы приоритеты и назначал проверяющего.

Идея

Сначала мы пытались решить задачу с помощью бота, идею для которого мы почерпнули у команды Календаря. Он должен был выбирать открытые merge request’ы и слать их в рабочий чат. Написали бота на TypeScript и создали чат. Но схема была рабочая, пока команда была небольшой, а когда она разраслась и количество MR увеличилось, люди смотрели на список задач в чате и старались выбрать ту, что поменьше да попроще. В результате я решил делать бота для GitLab, который принудительно распределял бы обязанности в коллективе высокомотивированных и высокоинтеллектуальных работников умственного труда. Заодно бот избавил бы авторов от творческих мук выбора проверяющих для своего кода.

Ещё бот должен был быть универсальным, чтобы им могли пользоваться не только в нашей команде. Для этого требовалось продумать его настройки и интеграцию с сервисом VK Teams. Идея была в том, что бот не только назначает проверяющего для MR, но и шлёт об этом сообщение ему и автору кода.

Решение

У меня уже был опыт создания ботов, поэтому я выбрал знакомые мне технологии: Gitlab CI и Danger-js. Сколько времени у вас может уйти на создание подобного инструмента, не подскажу, всё зависит от вашего CI. Мне нужно было интегрировать бота с VK Teams, а у вас может быть другой мессенджер, Slack или Telegram, к примеру. В настройки я заложил возможность выбирать количество проверяющих для каждого MR, чтобы бот подходил под разные рабочие конвейеры.

Мой бот шлёт в мессенджер такие сообщения:

А в конвейер GitLab — такие:

Также бот позволяет добавлять к каждому MR какой-нибудь тег, чтобы удобно было классифицировать изменения в коде. В нашем проекте, к примеру используется тег need-review. Если в настройках вашего проекта это предусмотрено, вы можете выделить специальный тег, позволяющий пропускать проверку конкретного merge request’а. Также бот должен пропускать запуск, если MR помечен как draft или wip.

Бот интегрирован в нашу внутреннюю платформу Mail Core CI, чтобы его могли использовать многие другие команды в компании, но вы можете приспособить его и под вашу среду. Как это сделано у нас:

# Джоба для вызова dangerjs в пайплайне через нашу обертку (npx ts-node cli danger) .mail-core:job:danger-init:  stage: test  needs: []  variables:    DANGER_ID: "<<required>>"    DANGERFILE: "<<required>>"  script:    - echo "danger:" $([[ -f ./node_modules/.bin/danger ]] && (./node_modules/.bin/danger --version 2>/dev/null || "[--version not found]") || echo "[file not found]")    - echo "dangerfile:" $DANGERFILE    - time [[ -d cli/command/danger ]] && npx ts-node cli danger ci --dangerfile=$DANGERFILE --id=$DANGER_ID -f   # Джоба для вызова ревью рулетки .mail-core:job:review-roulette:  extends: .mail-core:job:danger-init  variables:    # Минимальное количество Review Approvers    REVIEW_ROULETTE_MIN_APPROVERS: 1    DANGER_ID: "review-roulette"    DANGERFILE: "./node_modules/@mail-core/ci/dangerfiles/review-roulette/index.js"

Логика работы бота

При появлении нового merge request’а Gitlab СI автоматически запускает свой runner. Бот просматривает настройки проекта:

const {REVIEW_ROULETTE_LABEL, REVIEW_ROULETTE_MIN_APPROVERS} = process.env; const {mr, approvals, api, metadata} = danger.gitlab; const {repoSlug, pullRequestID} = metadata; const {iid, author, reviewers, web_url, title, description, labels, draft} = mr as MR; const {suggested_approvers, project_id, approved_by, approvals_required} = approvals as Approvals; const mrLink = `[${repoSlug}!${pullRequestID}](${web_url})`; const approvalsCount = approvals_required || Number(REVIEW_ROULETTE_MIN_APPROVERS);

И проверяет, можно ли пропустить проверку (в зависимости от присвоенного тега, количества ревьюеров и т.д.):

const skipReview = draft || isWip || hasApprove || hasSkipReviewLabel || reviewers.length !== 0;

Если пропустить нельзя, то бот берёт список доступных сейчас проверяющих:       

// алгоритм подбора рекомендуемых апруверов (у нас он реализован на основе файла CODEOWNERS) const approvers = getSuggestedApprovers();

Случайным образом выбирает нужное количество людей:

// алгоритм выбора ревьюеров на ваше усмотрение const reviewers = getReviewers(approvers, approvalsCount);

и отправляет им сообщения в мессенджеры (если те интегрированы) и в сам MR.

// Достаем из массива ревьеюров только их id (необходимы для отправки в gitlab api) const reviewerIds = reviewers.map(({id}) => id); const reviewerNames = reviewers.map(({name}) => name).join(', '); const reviewerCountText = reviewers.length > 1 ? 'ревьюеров' : 'ревьюера'; // Получаем на основе ревьюеров email const reviewerEmails = await getReviewerEmails(reviewers, api.Users); const mentionedUsers = reviewerEmails.map((v) => `@[${v}]`).join(',');   // Добавляем выбранных ревьюеров в MR await api.MergeRequests.edit(project_id, iid, {reviewer_ids: reviewerIds});   // Информируем автора MR о выбраных ревьюерах в VK Teams await communicator.sendMessage(       'author',       `       ${REVIEW_ICON} Я подобрал для *${mrLink}* (${title}) ${reviewerCountText}: ${mentionedUsers} ??? `, );  // Информируем ревьюеров в VK Teams await communicator.sendMessage(       reviewerEmails,        `        ${REVIEW_ICON} Вы выбраны ревьюером ?          *${mrLink}: ${title}*         ${description} `,  );  // Отправляем сообщение о выбранных ревьюера в MR message(        `Я подобрал Вам ${reviewerCountText} ? \n` +             '\n' +             `Встречайте бурными аплодисментами - ${reviewerNames}! ???`,        {icon: REVIEW_ICON},  );

А этот фрагмент кода отвечает за обработку ошибок:

// Получаем ссылку на документацию об ошибке const docLink = getDocLink('ci', {fragment: '#review-roulette'});  // Информируем автора MR об ошибке В VK Teams  await communicator.sendMessage(        'author',        `        ${REVIEW_ICON} Мне не удалось выбрать ревьюеров для ${mrLink} ?        ${errorMessage}          Подробнее можно почитать в [документации](${docLink}) `, );   // Отправляем ошибку в MR fail(      `Мне не удалось выбрать ревьюеров: \n` +            `${errorMessage} \n` +            '\n' +            `Подробнее можно почитать в [документации](${docLink})`,  );

Локальный запуск для тестирования

Чтобы не тестировать бота в Gitlab СI, можно запустить его локально и проверить настройки и корректность работы. Так получается гораздо быстрее, потому что иначе придётся создавать тестовый merge request в проекте, ждать его загрузки, пушить и надеяться, что подключение не отвалится. А для локального запуска достаточно нескольких команд.

Первым делом необходимо настроить локальное окружение:

Для этого настройте переменные окружения.

export DANGER_GITLAB_API_TOKEN=токен export DANGER_GITLAB_HOST=урл/репозитория

Ещё вам пригодятся ссылка на merge request, чтобы бот мог брать из него необходимую информацию.

Локальное тестирование запускается командой npx danger pr “ссылка/на/mr”--dangerfile ‘путь/до/файла/с/ботом”. Результат выполнения в консоли:

Интеграция в конвейер

Бот отлажен, пора запускать его в эксплуатацию. Сначала сгенерируем и добавим переменную окружения DANGER_GITLAB_API_TOKEN в ваш проект.

Про генерацию токена можно прочитать тут, а про добавление в проект — тут .

Затем интегрируем бота.

Проблемы и их решение

Первая проблема была в том, что при выполнении в конвейере нескольких скриптов с ботом последний выполняющийся переписывает результаты работы всех предыдущих. Поэтому я добавил в бот индентификатор, передаваемый через переменную окружения.

npx ts-node cli danger ci --dangerfile=$DANGERFILE --id=$DANGER_ID -f

Вторая проблема оказалась посерьёзнее. Авторы DangerJs заявляют поддержку TypeScript, но на самом деле она работает, если вы не пользуетесь модулями. Иначе вот что происходит:

Danger: ⅹ Failing the build, there is 1 fail. ## Failures Danger failed to run `./dangerfiles/review-roulette/index.ts`. ## Markdowns ## Error SyntaxError   Cannot use import statement outside a module ./dangerfiles/review-roulette/index.ts:3 import { Communicator } from 'communicator'; ^^^^^^   SyntaxError: Cannot use import statement outside a module     at wrapSafe (internal/modules/cjs/loader.js:1001:16)     at Module._compile (internal/modules/cjs/loader.js:1049:27)     at requireFromString (/Users/mikhail.avdeev/ci/node_modules/require-from-string/index.js:28:4)     at /Users/mikhail.avdeev/ci/node_modules/danger/distribution/runner/runners/inline.js:157:68     at step (/Users/mikhail.avdeev/ci/node_modules/danger/distribution/runner/runners/inline.js:52:23)     at Object.next (/Users/mikhail.avdeev/ci/node_modules/danger/distribution/runner/runners/inline.js:33:53)     at /Users/mikhail.avdeev/ci/node_modules/danger/distribution/runner/runners/inline.js:27:71     at new Promise (<anonymous>)     at __awaiter (/Users/mikhail.avdeev/ci/node_modules/danger/distribution/runner/runners/inline.js:23:12)     at Object.runDangerfileEnvironment (/Users/mikhail.avdeev/ci/node_modules/danger/distribution/runner/runners/inline.js:118:132) ``` ### Dangerfile ``` --------------------^ ```

Это решается компиляцией TS в JS файлы.

Ещё одна проблема заключалась в том, что бот автоматически сам себя вырезал (используя регулярные выражения).

// 1. Оригинальный TS import {fail, message, warn, danger} from 'danger'; // 2. Преобразование в JS const dangerjs_1 = require('danger'); // 3. Библиотека вырезает импорт регуляркой dangerjs_1 = ?; // 4. Ошибки вызова библиотечных функций dangerjs_1.fail // Error dangerjs_1.message // Error Dangerjs_1.warn  // Error

Чтобы среда не «ругалась», я прописал декларации типов и вынес импорт dangerjs в самый верх файла:

import type * as dangerjs from 'danger';   declare const danger: typeof dangerjs.danger; declare const message: typeof dangerjs.message; declare const fail: typeof dangerjs.fail; declare const warn: typeof dangerjs.warn;

Заключение

Как говорил Сальвадор Дали: “Тот кто не хочет никого имитировать, ничего не создаст.”
Поэтому вперед, создавать своих ботов!

Полезные ссылки


ссылка на оригинал статьи https://habr.com/ru/company/vk/blog/672372/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *