Настраиваем approve rules для merge request в бесплатной версии GitLab CE

Введение

Всем привет! Я Виктор, DevOps‑инженер в компании Nixys. Мы помогаем различным компаниям внедрять передовые практики DevOps, MLOps и DevSecOps. 

Сегодня я хочу рассказать вам о нашем интересном и, надеюсь, полезном опыте, который мы получили в одном из недавних проектов.

К нам обратились с задачей: «Хотим реализовать approve rules для merge requests в бесплатной версии GitLab». Конечно, я переформулировал ТЗ, поскольку оно звучало более неопределённо и расплывчато, как это обычно бывает. На первый взгляд, задача показалась довольно интересной, но, как это часто бывает в нашей работе, в процессе мы столкнулись с рядом вызовов и ограничений.

0c7c8808edab4baee3c4589535b5aa74.png

Сразу отмечу, что мы предложили рассмотреть возможность перехода на GitLab Premium, так как это позволило бы использовать встроенные функции approve rules без необходимости разработки дополнительных решений. Однако после оценки всех факторов бизнес решил, что для них важно получить именно approve rules, а остальные преимущества платной версии не столь критичны. Соответственно, если получится реализовать данный функционал, их это устроит. Это вполне оправданное решение для молодых или небольших команд, которым нужны только конкретные функции без излишних дополнительных возможностей.

Основная цель этой статьи — помочь командам, использующим бесплатную версию GitLab, внедрить эффективные механизмы проверки и рецензирования кода, сохраняя при этом гибкость и эффективность рабочих процессов. Я расскажу, как мы использовали GitLab API и CI/CD для автоматизации процесса approve rules, какие инструменты и методы мы применяли, а также поделюсь советами по настройке и интеграции решения в ваш рабочий процесс.

Если у вас нет желания читать всю статью, можете сразу перейти к реализации нашего решения → Наше решение

Сравнение бесплатной и платной версий GitLab

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

Основные различия между платной и бесплатной версиями GitLab:

Функция

GitLab CE (бесплатная)

GitLab Premium (платная)

Управление исходным кодом

Да

Да

CI/CD

Да

Да

Настройка approve rules

Нет

Да

Уведомления о безопасности

Нет

Да

Расширенная аналитика

Нет

Да

Поддержка и обновления

Сообщество

Профессиональная поддержка

Платный GitLab предоставляет дополнительные функции, которые могут существенно улучшить процессы разработки, управления проектами и обеспечения безопасности. В частности, возможность использования approve rules является значительным преимуществом платной версии, однако с нашим решением для автоматизации проверки merge requests можно добиться аналогичной функциональности даже в бесплатной версии GitLab.

Зачем вообще использовать approve rules?

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

Approve rules позволяют гарантировать, что изменения в кодовой базе проходят через процесс рецензирования и одобрения перед слиянием. Это помогает избежать попадания некачественного кода в основную ветку, обеспечивает соответствие кода стандартам и улучшает общую стабильность и безопасность проекта.

Контроль качества

Одной из главных задач approve rules является улучшение контроля качества кода. В рамках этих правил определённые люди или группы должны одобрить изменения перед их слиянием в основную ветку. Это помогает:

  • Обнаруживать ошибки на раннем этапе. Рецензенты могут заметить ошибки или потенциальные проблемы в коде, которые могли быть упущены автором.

  • Соблюдать код-стандартов. Рецензенты могут проверять, соответствует ли код принятым в команде стандартам и соглашениям о стилях написания.

  • Повышать качества кода. Регулярные проверки и обсуждения кода способствуют улучшению его качества и читабельности.

Безопасность

В крупных проектах, где над кодом работает много разработчиков, approve rules играют важную роль в обеспечении безопасности.

  • Защита от вредоносных изменений: обязательные проверки кода снижают риск внедрения вредоносного или ненадежного кода.

  • Контроль доступа: ограничение права на проведение merge requests только с разрешения доверенными лицами или группами предотвращает несанкционированные слияния.

  • Снижение уязвимостей: рецензенты могут выявить и устранить потенциальные уязвимости в коде до их появления в продуктивной среде.

Командная работа и обучение

Approve rules способствуют улучшению командной работы и развитию навыков членов команды:

  • Совместная работа: рецензирование кода способствует обмену знаниями и идеями внутри команды, что улучшает общую производительность и сплочённость.

  • Обучение и менторство: младшие разработчики получают обратную связь и советы от более опытных коллег, что помогает им быстрее расти профессионально.

  • Коллективная ответственность: все члены команды участвуют в процессе проверки и одобрения кода, что повышает общий уровень ответственности за качество проекта.

Хотя в платной версии GitLab такие правила встроены из коробки, в бесплатной версии их отсутствие не должно становиться препятствием для реализации эффективных процессов в молодых или небольших командах.

Поиск готового решения

Конечно, как это обычно бывает, первым делом мы изучили доступную документацию, а также поискали информацию на форумах и в других открытых источниках.

Сначала я нашел несколько предложений по использованию сторонних инструментов, скриптов или вовсе ботов для расширения функциональности GitLab CE. Эти инструменты включали в себя разнообразные плагины и интеграции с другими системами, но большинство из них имели свои недостатки:

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

  • Ограниченная функциональность. Некоторые из найденных решений были слишком простыми и не обеспечивали достаточного уровня гибкости и контроля, который нам был нужен. Например, они могли проверять только количество поставленных лайков на merge request, но не позволяли настраивать, кто именно должен дать это одобрение.

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

Поэтому мы решили разработать собственное решение, которое будет полностью удовлетворять нашим требованиям и легко интегрироваться в систему.

Проба готового решения

Изначально мы начали с интеграции уже готового решения, в этом очень помогла вот эта статья на Хабре. Здесь автор берёт за основу странную идею подсчета лайков и дизлайков. Возможно, на момент написания этой статьи не было возможности получать другую информацию через API, но это уже нас не волнует.

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

ci-mr:
  stage: test
  tags:
    - docker
  script:
    - echo "${GITLAB_TOKEN_FOR_CI}"
    - echo "${CI_API_V4_URL}"
    - echo "CI_PROJECT_ID ${CI_PROJECT_ID}"
    - echo "CI_COMMIT_SHA ${CI_COMMIT_SHA}"
    - "export MR_INFO=$(curl --silent --request GET --header \"PRIVATE-TOKEN: $GITLAB_TOKEN_FOR_CI\" ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/merge_requests | jq -c \".[] | select(.sha == \\\"${CI_COMMIT_SHA}\\\" and .state == \\\"opened\\\")\")"
    - echo "${MR_INFO}"
    - export MR_ID=$(echo $MR_INFO | jq '.iid')
    - echo "${MR_ID}"
    - export MR_TITLE=$(echo $MR_INFO | jq -r '.title')
    - echo "${MR_TITLE}"
    - export MR_WIP=$(echo $MR_INFO | jq -r '.work_in_progress')
    - echo "${MR_WIP}"
    - export MR_UPVOTES=$(echo $MR_INFO | jq '.upvotes')
    - echo "${MR_UPVOTES}"
    - export MR_DOWNVOTES=$(echo $MR_INFO | jq '.downvotes')
    - echo "${MR_DOWNVOTES}"
    - MR_VOTES=$((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 "${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 == "main" || $CI_MERGE_REQUEST_TARGET_BRANCH_NAME =~ /^release\/.*$/'

Реализация следующая:

  1. Создается отдельный пользователь (я создал с именем «check-approve») с доступом в необходимый репозиторий и минимальными правами — Reporter.

  2. Для него генерируется токен, который мы кладем в переменную GITLAB_TOKEN_FOR_CI.

  3. Производится настройка репозитория, отключается возможность пуша напрямую в необходимую ветку и включается проверка слияния (Merge checks).

  4. Создается отдельный пайплайн, который инклюдится в существующие. Он посредством запросов к API гитлаба получает необходимую информацию о merge request и подсчитывает количество «лайков» и «дизлайков». Далее производится суммирование значений, и оно сравнивается с выставленным нами параметром (я в качестве теста поставил 1, т. е. достаточно только одного лайка или одного дизлайка и двух лайков). Если значение выше или равно нашему параметру, то разблокируется возможность мержить, если нет, то нужно больше лайков.

Из нюансов:

  • В голосовании может участвовать каждый пользователь с доступом в репозиторий. Все голоса будут равнозначны.

  • Каждый пользователь может поставить и лайк, и дизлайк одновременно.

  • После успешного мержа пользователи имеют возможность убрать свои голоса или поменять их.

  • При простом перезапуске пайплайна количество подсчитанных в предыдущий раз голосов не изменится, если не нажать перед этим кнопку «Approve». Для последующих пересчетов нужно прожимать «Revoke approval» и снова «Approve» и только после этого перезапускать пайплайн.

Это спорное решение, и я сомневаюсь, что оно подойдёт кому-то. Поэтому я решил переписать метод проверки. Нужно было зацепиться за персонализированную информацию по merge request, которую можно вытащить из API. 

В первую очередь я начал смотреть в сторону комментариев, потому как был уверен, что можно вытащить информацию о пользователе и теле комментария к merge request. Соответственно, на этих данных я и хотел сделать проверку, к примеру, если пользователь ivan.ivanov оставил комментарий с телом «Approve». Я нашёл решение и даже внедрил. 

Во время тестирования я думал о том, что, вероятно, можно получить данные о пользователях, которые нажали кнопку «Approve». Однако я постоянно отгонял эту мысль, будучи уверенным, что если бы такая возможность существовала, то поддержку approve rules не стали бы выносить в GitLab Premium. Для меня это казалось очевидной истиной. И каково же было мое удивление, когда я, не выдержав, зарылся в документацию и обнаружил, что такая возможность действительно существует!

Только представьте себе: GitLab делает approve rules платной функцией, но при этом оставляет возможность получать информацию о подтверждениях через свой API. Официально! Я, конечно, знатно поплевался, когда осознал, что до этого занимался полной ерундой, пытаясь обойти ограничения бесплатной версии. С одной стороны, я понял, сколько времени и усилий было потрачено впустую на разработку обходных путей. С другой стороны, у меня появилось четкое понимание, как можно решить задачу простым и элегантным способом, используя доступные возможности API.

Этот момент стал переломным. Я смог интегрировать процесс проверки и апрува так, что всё заработало как «из коробки», без необходимости в сложных и ненадёжных обходных путях. Осознание этой возможности в API GitLab дало нам новый инструмент, который значительно упростил нашу работу и сделал решение задачи гораздо более эффективным.

Наше решение

В этом примере мы предполагаем, что у вас уже настроен проект в GitLab и есть базовое понимание работы с CI/CD.

Шаг 1: Включаем проверку слияния

Settings → Merge requests. Для «Merge checks» ставим галочку «Pipelines must succeed».

Шаг 2: Настраиваем «Protected branches»

Переходим Settings → Repository → Protected branches и для необходимых веток включаем защиту. У меня это ветки main, stage и dev, которые я создал заранее.

Шаг 3: Настройка токена доступа

Для доступа к API GitLab нам понадобится персональный токен с минимальными правами.

Создайте токен отдельного пользователя, создайте для него токен, далее в настройках GitLab выдайте ему минимальные права («Reporter») от своего рабочего репозитория.

Шаг 4: Настройка переменных

Добавьте переменную для списка пользователей, которые могут апрувить merge request (APPROVAL_AUTHORS) и переменную с токеном созданным на предыдущем шаге (GITLAB_TOKEN_FOR_CI). Это можно сделать через интерфейс GitLab или в файле .gitlab-ci.yml.

variables:
  GITLAB_TOKEN_FOR_CI: ""
  APPROVAL_AUTHORS: "ivan.ivanov,alex.admin,super.develop"

Шаг 5: Настройка pipeline

Создайте или отредактируйте файл .gitlab-ci-check-approve.yml, добавив следующий скрипт (конечно, можно просто добавить стадию в уже имеющийся pipeline, тут кому как удобнее):

ci-mr:
  stage: test
  tags:
    - docker
  script:
    - echo "APPROVAL_AUTHORS '${APPROVAL_AUTHORS}'"
    - echo "CI_MERGE_REQUEST_TARGET_BRANCH_NAME '${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}'"
    - |
      MR_INFO=$(curl --silent --request GET --header "PRIVATE-TOKEN: $GITLAB_TOKEN_FOR_CI" \
      ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/merge_requests | jq -c ".[] \
      | select(.sha == \"${CI_COMMIT_SHA}\" and .state == \"opened\" and .target_branch == \"${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}\")")
    - MR_ID=$(echo $MR_INFO | jq '.iid')
    - |
      APPROVALS=$(curl --silent --request GET --header "PRIVATE-TOKEN: ${GITLAB_TOKEN_FOR_CI}" \
      "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/merge_requests/${MR_ID}/approvals")
    - echo "${APPROVALS}"
    - |
      APPROVAL_AUTHORS_ARRAY=(${APPROVAL_AUTHORS//,/ })
      APPROVED=false
      for AUTHOR in "${APPROVAL_AUTHORS_ARRAY[@]}"; do
        if echo "${APPROVALS}" | jq -e ".approved_by[] | select(.user.username == \"${AUTHOR}\")" > /dev/null; then
          APPROVED=true
          break
        fi
      done
    - |
      if [ "${APPROVED}" = true ]; then
        echo "Great job! Your merge request has been approved!";
      else
        echo "Almost there! Please get approval from one of the following users: ${APPROVAL_AUTHORS//,/, } to proceed.";
        exit 1;
      fi
  image: laptevss/gitlab-api-util
  rules:
    - if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "main" || $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "stage" || $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "dev"'

Объяснение кода

  • Переменные: Переменные GITLAB_TOKEN_FOR_CI и APPROVAL_AUTHORS задаются через настройки проекта.

  • Получение информации о merge request (MR_INFO): Используется curl для запроса информации о merge requests. Фильтруется по SHA коммита, статусу «opened» и целевой ветке (target_branch). По итогу получаем информацию по нашему открытому merge request.

  • Получение ID merge request (MR_ID): Извлекается iid из MR_INFO.

  • Получение информации об одобрениях (APPROVALS): Используется curl для запроса информации о имеющихся апрувах необходимого merge request. 

  • Проверка одобрений: Перебирается массив переменной APPROVAL_AUTHORS и проверяется, есть ли среди реально одобривших хотя бы один из них. Если таких апрувов не найдено, выводится сообщение с просьбой получить одобрение от необходимых пользователей, если же совпадение есть, то выводится сообщение об успешном апруве.

  • Правила запуска стадии: в блоке rules указаны ветки, для которых эта стадия будет запускаться.

Собственно, мы и переработали метод из статьи из-за того, что получаемая из API GitLab информация по апрувам содержит информацию о пользователе, включая его username, в отличие от лайков/дизлайков, которые обезличены. В итоге при таком подходе нет возможности выделить конкретного привилегированного пользователя.

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

Интеграция и использование

Этот скрипт можно интегрировать в существующие пайплайны GitLab, добавив его как инклюд:

include:
  - project: 'test/pipelines'
    file: '.gitlab-ci-check-approve.yml'

Выглядит это так:

stages:
  - build
  - test

include:
  - project: 'test/pipelines'
    file: '.gitlab-ci-check-approve.yml'

run-myapp:
  stage: build
  script: 
  - echo "Hello world"
  tags:
  - docker

В результате, мы получаем следующую структуру:

test (группа)
│
├── pipelines (репозиторий с пайплайнами)
│ ├── .gitlab-ci-build-myapp.yml
│ └── .gitlab-ci-check-approve.yml
│
└── myapp (репозиторий с приложением)
  ├── .gitlab-ci.yml
  └── text.txt

Содержимое  test/myapp/.gitlab-ci.yml

include:
  - project: 'test/pipelines'
    file: '.gitlab-ci-build-myapp.yml'

Содержимое  test/pipelines/.gitlab-ci-build-myapp.yml

stages:
  - build
  - test

include:
  - project: 'test/pipelines'
    file: '.gitlab-ci-check-approve.yml'

run-myapp:
  stage: build
  script: 
  - echo "Hello world"
  tags:
  - docker

Содержимое  .gitlab-ci-check-approve.yml

ci-mr:
  stage: test
  tags:
    - docker
  script:
    - echo "APPROVAL_AUTHORS '${APPROVAL_AUTHORS}'"
    - echo "CI_MERGE_REQUEST_TARGET_BRANCH_NAME '${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}'"
    - |
      MR_INFO=$(curl --silent --request GET --header "PRIVATE-TOKEN: $GITLAB_TOKEN_FOR_CI" \
      ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/merge_requests | jq -c ".[] \
      | select(.sha == \"${CI_COMMIT_SHA}\" and .state == \"opened\" and .target_branch == \"${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}\")")
    - MR_ID=$(echo $MR_INFO | jq '.iid')
    - |
      APPROVALS=$(curl --silent --request GET --header "PRIVATE-TOKEN: ${GITLAB_TOKEN_FOR_CI}" \
      "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/merge_requests/${MR_ID}/approvals")
    - echo "${APPROVALS}"
    - |
      APPROVAL_AUTHORS_ARRAY=(${APPROVAL_AUTHORS//,/ })
      APPROVED=false
      for AUTHOR in "${APPROVAL_AUTHORS_ARRAY[@]}"; do
        if echo "${APPROVALS}" | jq -e ".approved_by[] | select(.user.username == \"${AUTHOR}\")" > /dev/null; then
          APPROVED=true
          break
        fi
      done
    - |
      if [ "${APPROVED}" = true ]; then
        echo "Great job! Your merge request has been approved!";
      else
        echo "Almost there! Please get approval from one of the following users: ${APPROVAL_AUTHORS//,/, } to proceed.";
        exit 1;
      fi
  image: laptevss/gitlab-api-util
  rules:
    - if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "main" || $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "stage" || $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "dev"'

Проверка результата:

  1. Итак, на текущий момент у нас есть только protected ветки main, stage и dev. Создадим еще пару веток помимо них, пусть будут test и test2.

  1. Сейчас все ветки идентичны. Вливаем в ветку test2 изменения в файлике text.txt.

  1. Теперь создаем merge request в ветку test. Кнопка merge активна, слияние доступно. Так и должно быть, ведь ветка test не защищена и она не внесена в условие стадии в rules.

  1. Попробуем влить изменения в любую из защищенных веток main, stage или dev. Поскольку в пайплайне  .gitlab-ci-check-approve.yml в блоке rules эти ветки указаны, то стадия проверки апрувов запустится самостоятельно.

  2. В результате, если мы не получим одобрения от указанных пользователей, наш merge request будет заблокирован.

Almost there! Please get approval from one of the following users: ivan.ivanov,alex.admin,super.develop to proceed.
Cleaning up project directory and file based variables
ERROR: Job failed: exit code 1

7c7950bf1d0fdef0269fbf710e66b482.png

  1. Если же получен апрув от указанных пользователей, нужно лишь перезапустить пайплайн:

Great job! Your merge request has been approved!
Cleaning up project directory and file based variables
Job succeeded

ce3e61c2e393bb0e21c3af885a5111d6.png

Все готово, можно вливать изменения.

Заключение

Автоматизация проверки одобрений merge requests с помощью GitLab CI/CD и GitLab API — это эффективный и надёжный способ оптимизировать процесс управления кодом в вашей команде. Несмотря на первоначальные трудности и неверные предположения, мы смогли найти решение, которое делает процесс проверки одобрения merge request в бесплатной версии GitLab максимально близким к функционалу премиум-версии.

Да, наше решение требует создания отдельных пользователей, генерации токенов доступа, настройки переменных и пайплайнов, а также использования скриптов для проверки одобрений. Этот процесс может показаться сложным на этапе внедрения, но все эти шаги нужно выполнить только один раз. После этого настроенный пайплайн можно просто инклюдить в ваши проекты. Такое решение позволяет существенно сократить расходы на платное программное обеспечение, если для ваших нужд критически важна только функция approve rules для merge request.

Независимо от того, используете ли вы бесплатную или платную версию GitLab, этот подход может быть полезен для организации и автоматизации ваших рабочих процессов. Мы уверены, что наше решение поможет вам поддерживать высокие стандарты качества кода, повысить безопасность и эффективность работы вашей команды.

Надеюсь, что данная статья будет полезна и поможет вам в автоматизации процессов контроля слияний в ваших проектах. Если у вас возникнут вопросы или предложения, не стесняйтесь оставлять их в комментариях. Желаю вам успешной разработки и продуктивной работы с GitLab!

© Habrahabr.ru