Code review в Gitlab CE: если Merge request approvals нет, но очень хочется
Одной из самых нужных функций, которой нет в бесплатной версии GitLab, является возможность голосования против обнуления репозитория контролировать Merge request (MR), используя обязательный code review.
Сделаем минимальный функционал сами — запретим Merge, пока несколько разработчиков не поставят «палец вверх» на MR.
Зачем это вообще?
Наша организация вполне может позволить себе купить лицензию GitLab. Но, так как разработка ведется в закрытом контуре без доступа в интернет, и есть жесткое планирование бюджета, закупка лицензий self-managed с нужным функционалом может затянуться на многие месяцы, а работать нужно уже сейчас.
В итоге приходится:
- либо совсем запрещать Merge в защищенные ветки для части разработчиков, но тогда разработчики, имеющие право на Merge, получают конфликты при слиянии чужих MR как бонус;
- либо давать возможность делать бесконтрольные слияния с вашей мастер-веткой без code review, даже если это Junior, устроившийся только вчера.
Первым же делом отправился гуглить, полагая, что уж точно кто-то что-то подобное уже сделал (без доработки кода), но оказалось, что подобной реализации в community версии еще не было.
Общая схема работы
В качестве примера настроим Merge request approvals на тестовом репозитории myapp:
- Создадим токен для доступа к API GitLab (через него будем получать информацию о количестве голосов «за» и «против»)
- Добавим токен в переменные GitLab
- Запретим Merge при ошибках в пайплайне (если голосов «за» недостаточно)
- Настроим проверку голосов как часть пайплайна CI/CD
- Запретим делать коммиты в защищенные ветки, все изменения проводим только через MR
- Проверим, что получилось в итоге
1. Создаем токен для доступа к API
Заходим в Настройки пользователя → Токены доступа и записываем токен:
Доступ к API позволяет делать практически все с вашими репозиториями, поэтому советую создать отдельную учетную запись Gitlab, дать ей минимальные права на ваши репозтории (например, Reporter) и получить токен для этой учетной записи.
2. Добавляем токен в переменные Gitlab
Например, на предыдущем шаге мы получили токен QmN2Y0NOUFlfeXhvd21ZS01aQzgK
Открываем Настройки → CI/CD → Переменные → Добавить переменную → GITLAB_TOKEN_FOR_CI
В итоге получим:
Это можно сделать как на одном репозитории, так и на группе репозиториев.
3. Ставим запрет на Merge, если не получены одобрения коллег после проведенного code review
В нашем случае запретом на Merge будет являться то, что сборочный конвейер вернет ошибку при недостаточном количестве голосов.
Заходим в Настройки → Основные → Запросы на слияние → Проверки слияния и включаем опцию Сборочные линии должны успешно выполниться.
4. Настраиваем пайплайн
stages:
- build
- test
variables:
NEED_VOTES: 1
include:
- remote: "https://gitlab.com/gitlab-ce-mr-approvals/ci/-/raw/master/check-approve.gitlab-ci.yml"
run-myapp:
stage: build
script: echo "Hello world"
Расположение нового файла конвейера нужно будет указать, зайдя в репозиторий myapp — Настройки — CI/CD — Сборочные линии — Пользовательский путь конфигурации CI — указать новый файл, например myapp.gitlab-ci.yml@gitlab-ce-mr-approvals/Ci
Пример контейнеров с линтерами, которые вы можете встроить в ваш пайплайн:
hub.docker.com/r/gableroux/gitlab-ci-lint
hub.docker.com/r/sebiwi/gitlab-ci-validate
И пример стадии проверки:
stages:
- lint
lint:
image: sebiwi/gitlab-ci-validate:1.3.0
variables:
GITLAB_HOST: https://gitlab.com
script:
- CI_FILES=(./*.yml)
- for f in "${CI_FILES[@]}"; do
gitlab-ci-validate $f;
done;
Осталось добавить в ваш пайплайн несколько параметров, чтобы все заработало:
stages:
- test
variables:
NEED_VOTES: 1
include:
— remote: «https://gitlab.com/gitlab-ce-mr-approvals/ci/-/raw/master/check-approve.gitlab-ci.yml»
Переменная NEED_VOTES определяет сколько «пальцев вверх» должно быть у MR, чтобы был доступен Merge. Значение, равное единице, означает, что вы сами можете одобрить свой MR, «лайкнув» его.
include подключает стадию test, проверяющую количество «лайков».
stages:
- build
- test
variables:
NEED_VOTES: 0
include:
- remote: "https://gitlab.com/gitlab-ce-mr-approvals/ci/-/raw/master/check-approve.gitlab-ci.yml"
run-myapp:
stage: build
image: openjdk
script:
- echo CI_MERGE_REQUEST_TARGET_BRANCH_NAME $CI_MERGE_REQUEST_TARGET_BRANCH_NAME
- java HelloWorld.java
ci-mr:
stage: test
script:
- echo ${CI_API_V4_URL}
- echo "CI_PROJECT_ID ${CI_PROJECT_ID}"
- echo "CI_COMMIT_SHA ${CI_COMMIT_SHA}"
- "export MR_ID=$(curl --silent --request GET --header \"PRIVATE-TOKEN: $GITLAB_TOKEN_FOR_CI\" ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/merge_requests | jq \".[] | if .sha == \\\"${CI_COMMIT_SHA}\\\" then .id else {} end\" | grep --invert-match {})"
- "export MR_TITLE=$(curl --silent --request GET --header \"PRIVATE-TOKEN: $GITLAB_TOKEN_FOR_CI\" ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/merge_requests | jq \".[] | if .sha == \\\"${CI_COMMIT_SHA}\\\" then .title else {} end\" | grep --invert-match {})"
- "export MR_WIP=$(curl --silent --request GET --header \"PRIVATE-TOKEN: $GITLAB_TOKEN_FOR_CI\" ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/merge_requests | jq \".[] | if .sha == \\\"${CI_COMMIT_SHA}\\\" then .work_in_progress else {} end\" | grep --invert-match {})"
- "export MR_UPVOTES=$(curl --silent --request GET --header \"PRIVATE-TOKEN: $GITLAB_TOKEN_FOR_CI\" ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/merge_requests | jq \".[] | if .sha == \\\"${CI_COMMIT_SHA}\\\" then .upvotes else {} end\" | grep --invert-match {})"
- "export MR_DOWNVOTES=$(curl --silent --request GET --header \"PRIVATE-TOKEN: $GITLAB_TOKEN_FOR_CI\" ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/merge_requests | jq \".[] | if .sha == \\\"${CI_COMMIT_SHA}\\\" then .downvotes else {} end\" | grep --invert-match {})"
- MR_VOTES=$(expr ${MR_UPVOTES} - ${MR_DOWNVOTES})
- NEED_VOTES_REAL=${NEED_VOTES:-1}
- echo "MR_ID ${MR_ID} MR_TITLE ${MR_TITLE} MR_WIP ${MR_WIP} MR_UPVOTES ${MR_UPVOTES} MR_DOWNVOTES ${MR_DOWNVOTES}"
- echo "MR_VOTES ${MR_VOTES} Up vote = 1, down vote = -1, MR OK if votes >=${NEED_VOTES_REAL}"
- if [ "${MR_VOTES}" -ge "$(expr ${NEED_VOTES_REAL})" ];
then
echo "MR OK";
else
echo "MR ERROR Need more votes";
exit 1;
fi
image: laptevss/gitlab-api-util
rules:
- if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "master" || $CI_MERGE_REQUEST_TARGET_BRANCH_NAME =~ /^release\/.*$/'
Подробнее о том, что происходит при проверке:
- установлено ограничение, что проверка будет только при создании MR в ветки master или release/*
- используя API GitLab, получаем количество «лайков» и «дизлайков»
- вычисляем разность между положительными и отрицательными откликами
- если разность меньше заданного нами значения в NEED_VOTES, то блокируем возможность сделать слияние
5. Запрещаем коммиты в защищенные ветки
Определяем ветки, для которых мы должны проводить code review и указываем, что работать с ними можно только через MR.
Для этого заходим в Настройки → Репозиторий → Protected Branches:
6. Проверяем
Зададим NEED_VOTES: 0
Делаем MR и ставим «дизлайк».
В логах сборки:
Теперь ставим «лайк» и запускаем повторную проверку: