Как стать автором
Обновить
Контур
Делаем сервисы для бизнеса

Переезд с TeamCity на GitLab CI + K8s

Уровень сложностиСредний
Время на прочтение14 мин
Количество просмотров2.4K

Привет, Хабр! Меня зовут Даниил Мильков, я старший C# разработчик в команде Формы, которая входит в состав продукта Контур.Экстерн. Если вкратце, Экстерн позволяет бизнесу сдавать отчётность в контролирующие органы (ФНС, Росстат и тп.) через интернет.

Сразу хочу предупредить читателей, что про взаимодействие с k8s здесь сказано достаточно мало, разве что в разделе Kubernetes и PVC. На эту тему будет отдельная статья.

Начнём. Однажды наша команда решила перейти с TeamCity на GitLab CI…

Почему мы решили перейти на GitLab CI

  1. TeamCity ушёл из России. Лицензии сложно получить, обновления недоступны, нет техподдержки, проблемы приходится исправлять на ощупь.

  2. Одновременно с этими событиями из нашей команды ушли все инженеры-эксплуататоры и остались только разработчики. Им не хотелось заниматься поддержкой огромного парка виртуалок.

  3. Контейнеры – мировой стандарт. В мире активно развивается использование контейнеров и оркестраторов, в частности Kubernetes. Мы тоже это хотим. Тем более, это согласуется как с нашим нежеланием заниматься виртуалками, так и с целями команды техкачества по переходу на Linux и shared-хосты. А ещё мы хотим, чтобы наши сервисы были доступны как on-premise решение, которое очень удобно (а чаще – необходимо) распространять, как контейнеры. В общем, без этого никуда.

Миграция

Переезжать мы хотели так, чтобы пользовательский опыт не менялся радикальным образом, и поэтому постарались сделать интерфейс CI в GitLab максимально приближенным к ТС. Как будто что-то поменялось, стало чуть-чуть непривычно, но в целом кнопочки остались те же самые.

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

  • Инструмент (TeamCity на GitLab)

  • Операционная система (Windows на Linux)

  • Способ хостинга рабочей нагрузки (виртуальные машины на контейнеризацию в куберовском кластере)

Общие шаблоны

К тому моменту, как мы решили заезжать на GitLab, команда девопсов базовой инфраструктуры уже подготовила общие шаблоны конфигураций GitLab CI.

В GitLab CI шаблоны (templates) — это готовые части конфигураций, которые можно переиспользовать в разных проектах. На основе этих шаблонов получилось быстро спрототипировать первые джобы на новой платформе. Но сейчас мы практически не используем общие шаблоны, потому что:

  • Они ориентированы только на небольшие проекты без хитрых сценариев, сложно кастомизируемы.

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

Эти ограничения касаются всех общих шаблонов, которые может сделать кто угодно. Сделать их более удобными и при этом функциональными очень сложно. Может быть, вовсе невозможно. Не зря gitlab добавили в CI конфигурацию step’ы.

Несмотря на то что использование таких шаблонов зачастую неудобно, есть сценарии, когда это оправдано. Например, мы с их помощью запускаем анализатор кода для дотнетовского солюшена. Поэтому посмотрим, как выглядит минимальная CI конфигурация с использованием общих шаблонов:

include:
  - project: 'gitlab/path/to/ci-templates'  # Проект в gitlab, из которого подключается шаблон
    file: 'all.yaml'                        # Файл, который подключает все доступные шаблоны в вашем проекте
 
stages:
  - inspect-code
 
Inspect code:                               # имя джобы
  stage: inspect-code                       # в какой stage входит эта джоба 
  extends:
    - .dotnet_inspect_code                  # имя шаблона, который хотим использовать 
  variables:                                # переменные, с помощью которых настроим поведение шаблона
      DOTNET_PROJECT_OR_SOLUTION_PATH: "path/to/solution/file"    # относительно корня вашего репозитория

Можно вставить это в файл .gitlab-ci.yml в корне вашего проекта, подставить путь до солюшена, и всё заработает.

Найти свой пайплайн можно в разделе Pipelines вашего репозитория (Build -> Pipelines)

Как попасть в список Pipeline'ов
Как попасть в список Pipeline'ов

Пример нашей джобы

А вот как выглядит джоба уже без общих шаблонов:

Integration tests:
 extends: .test-base
 variables:                               # Конфигурируем лимиты и реквесты для кубера
     KUBERNETES_CPU_REQUEST: "3"
     KUBERNETES_CPU_LIMIT: "10"
     KUBERNETES_MEMORY_REQUEST: "12Gi"
     KUBERNETES_MEMORY_LIMIT: "24Gi"
     KUBERNETES_EPHEMERAL_STORAGE_REQUEST: "17Gi"
 script: |                                # Скрипт с основной работой
   #
   .gitlab-ci/common/scripts/dotnet_restore_and_build.sh
   .gitlab-ci/common/scripts/resource_build.sh
   .gitlab-ci/common/scripts/start-services.sh
   .gitlab-ci/stages/tests/integration-tests.sh

Команды вынесены в файлы .sh только для того, чтобы оформить удобное логирование.

И примерно так, например, выглядит dotnet_restore_and_build.sh

#!/bin/bash
source $(dirname "$0")/logging.sh

solution="${1:-Main.sln}"
args="${3:-""}"

# В функции startLogCollapsedSection в консоль пишется специальная страшная строка (приведу её ниже)
# по ней гитлаб понимает, что надо начать секцию, которую можно
# схлапывать и для которой автоматически посчитается время выполнения
section_id="dotnet build $solution"
startLogCollapsedSection "$section_id"   

dotnet build $solution --configuration Release $args
return_code=$?

# Здесь в консоль пишется строка для закрытия секции
endLogSection "$section_id"

if [ $return_code -ne 0 ]; then
   logError "Ошибка при билде $solution (exitCode $return_code)"
fi

exit $return_code

Страшные строки для закрытия и открытия секций:

echo -e "\e[0Ksection_start:`date +%s`:section_id_dotnet_build_main_sln[collapsed=true]\r\e[0K\e[36;1mdotnet build Main.sln\e[0;m\e[0;m"
echo -e "\e[0Ksection_end:`date +%s`:section_id_dotnet_build_main_sln\r\e[0K"

А за подключенным в секции extends уже нашим шаблоном .test-base скрываются такие строки (общие для всех наших тестовых джоб):

.test-base:
 stage: tests
 needs: []                                                   # Говорит о том, что джоба не должна ждать никакие джобы в предыдущем stage
 services:                                                   # рядом с контейнером джобы будут запущены контейнеры с указанными образами
     - name: docker-proxy.host/elasticsearch:6.6.0           # в данном случае c elasticsearch - он нужен для ряда интеграционных тестов
       command: [ "bin/elasticsearch", "-Expack.security.enabled=false", "-Ediscovery.type=single-node" ]
 before_script:                                              # команды, которые выполняются до основной секции script
   - !reference [.common_before_script, before_script]       # здесь шаблон общий вообще для всех джоб, так выполняется преднастройка джобы.
   - # В этой части мы скачиваем дополнительные репозитории, которые нужны джобе (см. "Работа с несколькими репозиториями")
 script:
   - echo "Hello, Dan! Replace 'script' section in your new job"
 artifacts:                                                  # настройка артефактов, которые надо будет загрузить в хранилище после выполнения джобы
   when: always                                              # always - загружать всегда, даже если джоба упала
   expire_in: 1 week                                         # ttl
   paths:                                                    # маски путей, где надо искать артефакты для загрузки
     - $CI_PROJECT_DIR/junit/*.xml
     - $CI_PROJECT_DIR/annotations/*.json
     - $CI_PROJECT_DIR/logs/**/*.log
   reports:                                                  # некоторые из артефактов гитлаб может обрабатывать по особому
     junit: $CI_PROJECT_DIR/junit/*.xml                      # строить тестовый отчёт
     annotations: $CI_PROJECT_DIR/annotations/*.json         # добавлять кастомную информацию в джобу

Что наделали, чтобы переехать

Аналитика тестовой истории

Гитлаб коммитоцентричен. Это здорово. Но вот такого разреза данных, то есть истории по запускам какой-то джобы, из коробки там нет.

История запусков конфигурации в TeamCity
История запусков конфигурации в TeamCity

Как и истории конкретного тест-кейса. Единственное встроенное, что есть в гитлабе, это количество падений тест-кейса за последние две недели. Не очень информативно. 

Тестовая история в GitLab CI
Тестовая история в GitLab CI

Рассмотрели существующие сторонние решения, по тем или иным причинам они нам не подошли. Но окей, раз этой фичи нет, надо её сделать. Сделали. Получился сервис, реализующий нужную функциональность в похожем на TeamCity стиле.

Рассмотрим несколько страниц, как это выглядит. История по конкретному типу джобы:

История запусков конфигурации в нашем сервисе (TestCity)
История запусков конфигурации в нашем сервисе (TestCity)

Подробный тестовый отчёт. В нём можно отфильтровать тест-кейсы по имени, по статусу или отсортировать по длительности выполнения. Встроенное в гитлаб отображение тестового репорта такие фокусы делать не умеет. А ещё тут можно скачать список тест-кейсов в csv-формате и даже проанализировать, какие массивы тест-кейсов отнимают больше всего времени с использованием диаграммы treemap.

Подробный тестовый отчёт по запуску джобы
Подробный тестовый отчёт по запуску джобы

А это история конкретного тест-кейса:

История тест-кейса
История тест-кейса

Чтобы из джобы был удобный доступ к этому сервису, добавили в неё пару аннотаций со ссылками на подробный тестовый отчёт и на историю по конкретному типу джобы:

Аннотации к джобе со ссылками на наш TestCity
Аннотации к джобе со ссылками на наш TestCity

Пара слов про внутрянку:

  • Данные хранятся в ClickHouse (CH). Бекенд проксирует SQL-запросы с фронта до CH

  • Попадают данные в базу через краулер, который ходит через GitLab API по джобам, скачивает их артефакты и парсит JUnit xml отчёт внутри. То есть для того чтобы ваши тестовые отчёты появились в сервисе, вам ничего не надо менять ни в вашей ci конфигурации, ни в коде. Достаточно поднять сам сервис, CH и краулер (либо отправлять данные в CH любым способом по вашему желанию)

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

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

Отчёт о кодовых инспекциях

В GitLab нет возможности просматривать отчёт о кодовых инспекциях, как это опять же реализовано в TeamCity:

Отчёт о кодовых инспекциях в TeamCity
Отчёт о кодовых инспекциях в TeamCity

На самом деле возможность есть, но только в платной версии GitLab. Мы сделали удобное отображение этого отчёта через банальный рендер его в html.

О каких вообще инспекциях идёт речь. Мы для анализа кода запускаем утилиту jetbrains’a – inspectcode, которая распространяется через dotnet tool JetBrains.ReSharper.GlobalTools. Иногда эти проверки показывают действительно серьёзные проблемы, так что очень рекомендую их использовать.

Чтобы воспользоваться этим рендером необходимо:

  1. Выполнить в джобе проверки с помощью inspectcode, сохранив при этом файл с отчётом по пути $CI_PROJECT_DIR/inspectcode/code-inspection.xml

  2. Добавить себе в репозиторий файл code-inspection-after-script.sh

  3. Вызвать этот скрипт после завершения работы анализатора. Он сгенерирует рендер отчёта в $CI_PROJECT_DIR/inspectcode/code-inspection.html и аннотацию в $CI_PROJECT_DIR/annotations/$(date +%s).json

  4. Загрузить получившуюся html страницу с отчётом в артефакты

  5. Добавить аннотацию в джобу

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

Inspect code:
 stage: inspect-code
 extends:
   - .dotnet_inspect_code
 variables:
     DOTNET_PROJECT_OR_SOLUTION_PATH: “path/to/solution/file”
 after_script: code-inspection-after-script.sh      # путь до скачанного скрипта
 artifacts:
   when: always
   expire_in: 1 week
   paths:
     - $CI_PROJECT_DIR/inspectcode/*
     - $CI_PROJECT_DIR/annotations/*.json
   reports:
     codequality:
       - $CI_PROJECT_DIR/inspectcode/gl-code-quality-report.json
     annotations: $CI_PROJECT_DIR/annotations/*.json

В джобе появится вот такая ссылка, которая ведёт на отрендеренный отчёт:

Аннотация к джобе со ссылкой на наш отчёт
Аннотация к джобе со ссылкой на наш отчёт

Сам отчёт выглядит так:

Наш отчёт о кодовых инспекциях. Сори, что много заблюренного. NDA и всё такое…
Наш отчёт о кодовых инспекциях. Сори, что много заблюренного. NDA и всё такое…

Работа с несколькими репозиториями

Для большинства наших конфигураций требуются сразу несколько реп: репа с сервисами, несколько реп с ресурсами, репа с фронтом. Соответственно, нужна была логика работы с репозиториями, похожая на работу VCS Roots в TeamCity – чтобы дополнительные репозитории в репе фетчились и чекаутились на нужную ветку, если её нет, то фетчились на дефолтную, которая может быть разной для каждого репозитория, если репа ещё не склонирована – склонировать, когда надо – почистить репу и тд. И отвечая на очевидный вопрос – нет, мы не хотели связываться с git submodules.

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

Пример использования:

ci-utils clone --remotePath="gitlab/repo/path/" --localPath="$CI_PROJECT_DIR/../repo" --branch=$CI_COMMIT_BRANCH --defaultBranch="master"

Shared resources

Shared resources – полезная фича TeamCity. Вы указываете массив строк, например имена площадок. Конфиги, которые используют этот shared resources, могут брать один из этих ресурсов и блокировать его. В этом случае остальные конфигурации не смогут получить доступ к занятому ресурсы, а если свободных ресурсов нет, то встанут в очередь.

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

Зачем нам эти окружения. Каждое из окружений соответствует площадке (VM), на которых у нас развернут ключевой сервис продукта, назовём его K. Cервис K, к сожалению, ещё на старой технологии хостинга и не готов к динамическим окружениям. Именно для этого нам и нужны shared resources – чтобы брать свободную площадку с сервисом K.

Что мы с этим сделали: реализовали занимание ресурсов на основе распределенной блокировки и json-хранилища также в виде консольной утилиты.

Пример использования:

ci-utils getResource --resources="stg1,stg2,stg3,stg4" --user=$CI_JOB_ID --output="$CI_PROJECT_DIR/captured_resource"
ci-utils freeResource --resources="stg1" --user=$CI_JOB_ID

Эти две утилиты и ещё пара полезных команд находятся в составе одной cli'шки, которую распространяем через dotnet tool.

Если подобная утилита нужна кому-то ещё, пишите в комменты.

Сходу вынести её за пределы Контура немного сложно, так как например для работы shared resources используются наши внутренние сервисы распределенной блокировки и хранилища json’ов. Но при большом отклике мы что-нибудь придумаем.

Kubernetes и PVC

Как использовать Kubernetes в GitLab CI — это тема для отдельной статьи. А сейчас поговорим только об одном аспекте: хранении данных в джобах.

В гитлабе в связке с кубером джобы по умолчанию запускаются в подах. Под – это контейнер для контейнеров, он эфемерный. После завершения джобы под вместе со всеми данными внутри себя (например склонированные репозитории) удаляется. Если вы хотите поведение, похожее на то, как это работает в ТeamCity, вам нужны постоянные хранилища. Они позволят хранить данные между запусками джоб. Под удаляется, хранилище с файлами остаётся жить и затем подключается к следующему поду. Особенно это полезно на толстых репах, потому что позволяет не клонировать их при каждом запуске, а всего лишь фетчить. А также можно сохранять кэш NuGet-пакетов и вообще всё, что хочется.

Называются эти постоянные хранилища – PV (persistent volume). В рамках подов нельзя использовать PV напрямую. Необходимо использовать Persistent Volume Claim (PVC), который позволяет запросить постоянный том с нужными параметрами и затем использовать его в рабочих нагрузках.

На данный момент мы не используем PVC в своём основном CI, а просто каждый раз клонируем нужные репозитории. Плюс у этого подхода в том, что не надо думать про очистку файлов, как в постоянном хранилище. Минус понятно какой – отсутствие кэша. Но кажется, что использование PVC поможет достаточно существенно сократить время подготовки джобы (скачивание реп, билд солюшена), поэтому сейчас мы экспериментируем с подключением PVC, а итогами этого поделимся отдельно, думаю, как раз в статье про Кубер.

Для тех, кто тоже хочет подключить себе PVC, приведу набор действий, которые для этого нужны:

  1. Вы должны использовать свои куберовские раннеры, которые контролируете именно вы, а не, например, базовая инфраструктура вашей компании. Раннер – это штуковина, которая порождает\контролирует\убивает поды для джоб в кубере.

  2. В конфигурацию раннера нужно добавить следующие настройки:

[runners.custom_build_dir]
   enabled = true
[[runners.kubernetes.volumes.pvc]]
   name = "build-pvc-$CI_CONCURRENT_ID"
   mount_path = "/builds"

3. При этом значение переменной $CI_CONCURRENT_ID надо задать для CI конфигурации, из которой вы деплоите раннер. Вот так – CI_CONCURRENT_ID: "$CI_CONCURRENT_ID". Делается это для того, чтобы в конфигурацию раннера в кубере попало такая строчка – name = "build-pvc-$CI_CONCURRENT_ID", а не строчка с уже подставленным значением, например name = "build-pvc-7"

4. В конфигурацию вашего CI добавить переменную:

GIT_CLONE_PATH: "$CI_BUILDS_DIR/$CI_PROJECT_PATH"

Эта переменная позволит клонировать репы по путям build/git/repo/path вместо дефолтного build/5/Ytdk54Kdf/git/repo/path. Не вдаваясь сильно в подробности, проблема с дефолтным путём в том, что при деплое нового раннера (например при изменении его настроек), этот путь меняется (Ytdk54Kdf – id раннера) и надо всё выкачивать заново. А ещё директории со старым id не удаляются – нужно чистить их руками.

Всё. У вас есть PVC. Вы прекрасны.

Красивые уведомления

В Контуре для рабочего общения мы используем Mattermost (кстати, есть эпичная статья, как мы туда переезжали). И, соответственно, там же находятся и каналы, в которые летят всякие алерты и оповещения.

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

Неочевидное поведение

workflow:rules

По умолчанию пайплайны создаются на каждый пуш, MR, и тд. Для того чтобы управлять созданием пайплайнов, можно описать правила, когда они должны создаваться, а когда не должны в секции workflow:rules. Особо аккуратно надо описывать правила, когда вы хотите по каким-то причинам не создавать пайплайн. 

Расскажу про ситуацию, которая произошла у нас. Написали следующее правило:

workflow:
 rules:
   - if:              # если
     changes:         # в диффах коммита
       - "**/*.md"    # есть файлы с раширением .md
     when: never      # никогда не создавай пайплайн
   - when: always     # иначе создавай

«Нет же никакого смысла запускать тесты на правки в документации. И список пайплайнов останется чище!» – думали мы.

В начале всё работало хорошо, но спустя некоторое время начали происходить странности: иногда не создавались пайплайны, даже если в коммите и близко не было markdown-файлов. Сразу никто эти проблемы не связал с rules, потому что прошло время с добавления этих правил до первых симптомов. В ходе расследования стало ясно, что пайплайны не создаются при ребейзе «старых» веток. На сколько именно должна быть старой ветка, чтобы пайплайн не создался, выяснить не удалось.

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

По итогу, перешерстив конфиг, нашли это самое правило, удалили, всё починилось.
Да, в документации есть абзац про то, что changes иногда работают неочевидным образом.

Кодировка файлов конфигурации

Если сохранить любой YAML-файла конфигурации в кодировке UTF-8 with BOM, то конфигурация не разваливается, как это бывает, когда например сделаешь очепятку в ключевом слове. А спокойно продолжает работать, но с интересными особенностями:

  • Частично пропали переменные окружения, которые заданы для конфигурации

  • Ещё более странно, чем раньше, работала директива changes, о которой я уже говорил

  • А из всех джоб, описанных в файле с битой кодировкой, отображалась только первая

Поменяли кодировку – всё заработало, как часы, включая директиву changes.

Отмена джобы (починилось)

Да, это поведение уже починили, но рассказать всё равно хочется :)

Джобы не хотели корректно завершаться при ручной отмене через UI или даже при срабатывании таймаута. Оказалось, что процессы запущенные в секции script не завершаются после команды на отмену, хотя джоба и помечается для гитлаба, как отменённая\зафейленная.

Почему это плохо:

  • Такие джобы ещё какое-то время висят в k8s как поды и занимают ресурсы

  • В after_script некоторых джоб находится логика освобождения ресурсов, которая не срабатывала

  • После after_script выполняется стандартный процесс выгрузки артефактов и кэша из джобы в хранилище, который тоже не срабатывал

Как чинили: написали bash код в after_script, который находил процесс основной секции script и прибивал его.

В одном из обновлении куберовского раннера эту проблему пофиксили. Точно не работало на версии 17.0, на последней 17.8 точно работает (по крайней мере, при последнем тестировании у меня всё отменялось корректно).

Утилиты в K8s

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

Все эти утилиты мы теперь запускаем прямо на kubernetes в виде cronjob.

Пара слов о том, как мы запускаем эти утилиты в кубере:

  • Деплоим через GitLab CI с использованием шаблонов helm charts, написанных в нашей компании. Это позволяет описывать только самые необходимые свойства в конфигурации приложений в файле values.yaml и избавиться от бойлерплейт кода. 

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

Что мы получили от переезда?

Снижение затрат. Нет лицензий, меньше инфраструктурных затрат. При расчете использовали прайс 2024 года и получилось, что если бы мы использовали весь 2024 год GitLab, мы бы заплатили на 40% меньше, чем при использовании TeamCity.

Не тратим время на поддержку виртуалок. Удалось выкинуть большую часть виртуальных машин, не тратим время на их поддержку. В том числе это помогло справляться с задачами без своего инженера-эксплуататора.

Динамическое масштабирование. Есть возможность простого горизонтального масштабирования. Запросили дополнительные мощности и изменили одну цифру (параллельность) в настройках раннера. Не нужно создавать и настраивать дополнительные виртуалки. Просто и быстро.

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

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

Кастомизируемость. GitLab – это ПО с открытым исходным кодом, то есть у нас есть все возможности произвольной настройки как UI, так и внутренних процессов платформы.

Коммитоцентричность. Крайне удобная особенность работы с GitLab CI. Легко понять общий статус коммита. В отличии от TeamCity, где тебе нужно в каждую конфигу залезть, посмотреть, а точно ли это запуск с нужного коммита.


Спасибо, что дочитали до конца! Оставляйте комментарии и делитесь идеями.

аа

Теги:
Хабы:
Всего голосов 6: ↑6 и ↓0+6
Комментарии5

Публикации

Информация

Сайт
tech.kontur.ru
Дата регистрации
Дата основания
Численность
свыше 10 000 человек
Местоположение
Россия
Представитель
Варя Домрачева