Русский
Русский
English
Статистика
Реклама

Teamcity

Как мы в 2020 году изобретали процесс разработки, отладки и доставки в прод изменений базы данных

24.08.2020 14:14:04 | Автор: admin
На дворе 2020 год и фоновым шумом вы уже привыкли слышать: Кубернетес это ответ!, Микросервисы!, Сервис меш!, Сесурити полиси!. Все вокруг бегут в светлое будущее.

Подходы в том, что касается баз данных, в нашей компании более консервативны, чем в прикладных приложениях. Крутится база данных у нас не в кубернетесе, а на железе или в виртуалке. Для изменений базы данных процессинга платежных сервисов у нас есть устоявшийся процесс, который включает в себя множество автоматических проверок, большое ревью и релиз с участием DBA. Количество проверок и привлекаемых людей в этом случае негативно влияет на time-to-market. С другой стороны, он отлажен и позволяет надежно вносить изменения в продакшен, минимизируя вероятность что-то сломать. А если что-то сломалось, то нужные люди уже включены в процесс починки. Этот подход делает работу основного сервиса компании стабильнее.

Большинство новых реляционных баз данных для микросервисов мы заводим на PostgreSQL. Отлаженный процесс для Oracle хоть и надёжный, но несет с собой избыточную сложность для маленьких БД. Тащить тяжёлые процессы из прошлого в светлое будущее никто не хочет. Проработкой процесса для светлого будущего заранее никто не занялся. В итоге получили отсутствие стандарта и разножопицу.



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

Проблемы, которые мы решали


Нет единых стандартов версионирования


В лучшем случае это DDL SQL-файлы, которые лежат где-то в директории db в репозитории с микросервисом. Совсем плохо, если это просто текущее состояние БД, разное на тесте и на проде, и эталонных скриптов схемы БД нет.

В ходе отладки ушатываем тестовую базу


Я сейчас немного тестовую БД пошатаю, не пугайтесь там и пошел отлаживать на тестовой базе данных только что написанный код изменения схемы. Иногда долго, и всё это время тестовый контур не работает.

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

Методы DAO не покрываются тестами, не проверяются в CI


При разработке и отладке методы DAO вызываются через дергание за внешние ручки несколькими слоями выше. Это подвергает проверке целые сценарии бизнес-логики вместо конкретного взаимодействия микросервиса и базы данных.

Гарантии, что ничего не развалится в будущем, нет. Страдает качество и поддерживаемость микросервиса.

Неизоморфность сред


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

Объекты на тесте могут быть созданы из-под учетки разработчика или приложения. Гранты накидываются как попало, обычно grant all privileges. Гранты приложению выдаются по принципу вижу ошибку в логе даю грант. Часто при релизе забывают про гранты. Иногда после релиза смок-тестирование не покрывает всю новую функциональность и отсутствие гранта выстреливает не сразу.

Тяжелый и ломучий процесс наката в продакшен


Накат в прод сделали ручным, но по аналогии с процессом для Oracle, через согласование DBA, релиз-менеджеров и накат релиз-инженерами.

Это замедляет релиз. А в случае проблем увеличивает даунтайм, усложняя доступ разработчика к БД. Скрипты exec.sql и rollback.sql часто не проверялись на тесте, потому что стандарта патчсетирования для не-Oracle нет, а на тест катилось как попало.

Поэтому бывает такое, что в некритичные сервисы разработчики катят изменения без этого процесса вообще.

Как можно делать, чтобы было хорошо


Отладка на локальной БД в докер-контейнере


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

Вот вы же не лезете на тестовый сервер по ssh, чтобы писать и дебажить код приложения? Я считаю, что разрабатывать и отлаживать код базы данных на тестовом инстансе БД так же абсурдно. Есть исключения, бывает, что поднять локально базу данных очень сложно. Но обычно, если мы говорим о чем-то легковесном и не-легаси, то поднять локально базу и накатить на нее последовательно все миграции не составляет большого труда. Взамен вы получите стабильный инстанс под боком, который не ушатает другой разработчик, до которого не пропадут доступы и на котором вы имеете нужные для разработки права.

Приведу пример, насколько просто поднять локально БД:

Пишем двухстрочный Dockerfile:
FROM postgres:12.3ADD init.sql /docker-entrypoint-initdb.d/


В init.sql делаем чистую БД, которую рассчитываем получить и на тесте, и в проде. Она должна содержать:

  • Пользователя-владельца схемы и саму схему.
  • Пользователя приложения с грантом на использование схемы.
  • Требуемые EXTENSIONs


Пример init.sql
create role my_awesome_servicewith login password *** NOSUPERUSER inherit CREATEDB CREATEROLE NOREPLICATION;create tablespace my_awesome_service owner my_awesome_service location '/u01/postgres/my_awesome_service_data';create schema my_awesome_service authorization my_awesome_service;grant all on schema my_awesome_service to my_awesome_service;grant usage on schema my_awesome_service to my_awesome_service;alter role my_awesome_service set search_path to my_awesome_service,pg_catalog, public;create user my_awesome_service_app with LOGIN password *** NOSUPERUSER inherit NOREPLICATION;grant usage on schema my_awesome_service to my_awesome_service_app;create extension if not exists "uuid-ossp";



Для удобства можно добавить в Makefile таску db, которая (пере)запустит контейнер с базой и оттопырит порт для соединения:

db:    docker container rm -f my_awesome_service_db || true    docker build -t my_awesome_service_db docker/db/.    docker run -d --name my_awesome_service_db -p 5433:5432 my_awesome_service_db


Версионирование changesetов с помощью чего-то стандартного для индустрии


Тоже выглядит очевидно: нужно писать миграции и содержать их в системе контроля версий. Но очень часто я вижу голые sql-скрипты, без какой-либо обвязки. И это значит, что нет никакого контроля наката и отката, кем, что и когда было накачено. Нет даже гарантии, что ваши SQL-скрипты могут быть выполнены на тестовой и продовой БД, так как ее структура могла измениться.

В общем, нужен контроль. Системы миграции как раз про контроль.
Не будем вдаваться в сравнение разных систем версионирования схем БД. FlyWay vs Liquibase не тема этой статьи. Мы выбрали Liquibase.

Мы версионируем:

  • DDL-структуру объектов бд (create table).
  • DML-содержимое таблиц-справочников (insert, update).
  • DCL-гранты для УЗ Приложения (grant select, insert on ...).


Запуская и отлаживая микросервис на локальной БД, разработчик столкнется с необходимостью позаботиться о грантах. Единственный легальный способ для него завести DCL-скрипт в ченджсет. Это гарантирует нам, что гранты доедут до прода.

Пример sql-патчсета
0_ddl.sql:
create table my_awesome_service.ref_customer_type(    customer_type_code    varchar not null,    customer_type_description varchar not null,    constraint ref_customer_type_pk primary key (customer_type_code)); alter table my_awesome_service.ref_customer_type    add constraint customer_type_code_ck check ( (customer_type_code)::text = upper((customer_type_code)::text) );

1_dcl.sql:
grant select on all tables in schema my_awesome_service to ru_svc_qw_my_awesome_service_app;grant insert, update on my_awesome_service.some_entity to ru_svc_qw_my_awesome_service_app;

2_dml_refs.sql:
insert into my_awesome_service.ref_customer_type (customer_type_code, customer_type_description)values ('INDIVIDUAL', 'Физ. лицо');insert into my_awesome_service.ref_customer_type (customer_type_code, customer_type_description)values ('LEGAL_ENTITY', 'Юр. лицо');insert into my_awesome_service.ref_customer_type (customer_type_code, customer_type_description)values ('FOREIGN_AGENCY', 'Иностранное юр. лицо');

Fixtures. Данные для тестов или отладки идут отдельным ченжсетом с контекстом dev
3_dml_dev.sql:
insert into my_awesome_service.some_entity_state (state_type_code, state_data, some_entity_id)values ('BINDING_IN_PROGRESS', '{}', 1);

rollback.sql:
drop table my_awesome_service.ref_customer_type;


Пример changeset.yaml
databaseChangeLog: - changeSet:     id: 1     author: "mr.awesome"     changes:       - sqlFile:           path: db/changesets/001_init/0_ddl.sql       - sqlFile:           path: db/changesets/001_init/1_dcl.sql       - sqlFile:           path: db/changesets/001_init/2_dml_refs.sql     rollback:       sqlFile:         path: db/changesets/001_init/rollback.sql - changeSet:     id: 2     author: "mr.awesome"     context: dev     changes:       - sqlFile:           path: db/changesets/001_init/3_dml_dev.sql



Liquibase создает на БД таблицу databasechangelog, где отмечает накаченные ченджсеты.
Автоматически вычисляет, сколько ченджсетов нужно докатить до БД.

Есть maven и gradle plugin с возможностью сгенерировать из нескольких ченджсетов скрипт, который нужно докатить до БД.

Интеграция системы миграций БД в фазу запуска приложения


Здесь мог бы быть любой адаптер системы контроля миграций и фреймворка, на котором построено ваше приложение. Со многими фреймворками он идёт в комплекте с ORM. Например, Ruby-On-Rails, Yii2, Nest.JS.

Этот механизм нужен, чтобы катить миграции при старте контекста приложения.
Например:

  1. На тестовой БД патчсеты 001, 002, 003.
  2. Погромист наразрабатывал патчсеты 004, 005 и не деплоил приложение в тест.
  3. Деплоим в тест. Докатываются патчсеты 004, 005.


Если не накатываются приложение не стартует. Rolling update не убивает старые поды.
В нашем стеке JVM + Spring, и мы не используем ORM. Поэтому нам потребовалась интеграция Spring-Liquibase.

У нас в компании есть важное требование безопасности: пользователь приложения должен иметь ограниченный набор грантов и точно не должен иметь доступ уровня владельца схемы. С помощью Spring-Liquibase есть возможность катить миграции от имени пользователя-владельца схемы. При этом пул соединений прикладного уровня приложения не имеет доступа к DataSource'у Liquibase. Поэтому приложение не получит доступ из-под пользователя-владельца схемы.

Пример application-testing.yaml
spring:  liquibase:    enabled: true    database-change-log-lock-table: "databasechangeloglock"    database-change-log-table: "databasechangelog"    user: ${secret.liquibase.user:}    password: ${secret.liquibase.password:}    url: "jdbc:postgresql://my.test.db:5432/my_awesome_service?currentSchema=my_awesome_service"



DAO тесты на CI-этапе verify


В нашей компании есть такой CI-этап verify. На этом этапе происходит проверка изменений на соответствие внутренним стандартам качества. Для микросервисов это обычно прогон линтера для проверки кодстайла и на наличие багов, прогон unit-тестов и запуск приложения с поднятием контекста. Теперь на этапе verify можно проверить миграции БД и взаимодействие DAO-слоя приложения с БД.

Поднятие контейнера с БД и накат патчсетов увеличивает время старта Spring-контекста на 1,5-10 сек, в зависимости от мощности рабочей машины и количества патчсетов.

Это не совсем unit-тесты, это тесты интеграции DAO-слоя приложения с базой данных.
Называя БД частью микросервиса, мы говорим, что это тестирование интеграции двух частей одного микросервиса. Без внешних зависимостей. Таким образом эти тесты стабильны и могут выполняться на этапе verify. Они фиксируют контракт микросервиса и БД, обеспечивая уверенность при будущих доработках.

А еще это удобный способ отладки DAO. Вместо того, чтобы вызывать RestController, имитируя поведения пользователя в каком-то бизнес-сценарии, сразу вызываем DAO с нужными аргументами.

Пример DAO-теста
@Test@Transactional@Rollbackfun `create cheque positive flow`() {      jdbcTemplate.update(       "insert into my_awesome_service.some_entity(inn, registration_source_code)" +               "values (:inn, 'QIWICOM') returning some_entity_id",       MapSqlParameterSource().addValue("inn", "526317984689")   )   val insertedCheque = chequeDao.addCheque(cheque)   val resultCheque = jdbcTemplate.queryForObject(       "select cheque_id from my_awesome_service.cheque " +               "order by cheque_id desc limit 1", MapSqlParameterSource(), Long::class.java   )   Assert.assertTrue(insertedCheque.isRight())   Assert.assertEquals(insertedCheque, Right(resultCheque))}



Есть две сопутствующие задачи для прогона этих тестов в пайплайне на verify:

  1. На билдагенте может быть потенциально занят стандартный порт PostgreSQL 5432 или любой статичный. Мало ли, кто-то не потушил контейнер с базой после завершения тестов.
  2. Из этого вторая задача: нужно тушить контейнер после завершения тестов.

Эти две задачи решает библиотека TestContainers. Она использует существующий докер образ для поднятия контейнера с базой данных в состоянии init.sql.

Пример использования TestContainers
@TestConfigurationpublic class DatabaseConfiguration {   @Bean   GenericContainer postgreSQLContainer() {       GenericContainer container = new GenericContainer("my_awesome_service_db")               .withExposedPorts(5432);       container.start();       return container;   }   @Bean   @Primary   public DataSource onlineDbPoolDataSource(GenericContainer postgreSQLContainer) {       return DataSourceBuilder.create()               .driverClassName("org.postgresql.Driver")               .url("jdbc:postgresql://localhost:"                       + postgreSQLContainer.getMappedPort(5432)                       + "/postgres")               .username("my_awesome_service_app")               .password("my_awesome_service_app_pwd")               .build();   }       @Bean   @LiquibaseDataSource   public DataSource liquibaseDataSource(GenericContainer postgreSQLContainer) {       return DataSourceBuilder.create()               .driverClassName("org.postgresql.Driver")               .url("jdbc:postgresql://localhost:"                       + postgreSQLContainer.getMappedPort(5432)                       + "/postgres")               .username("my_awesome_service")               .password("my_awesome_service_app_pwd")               .build();   }



С разработкой и отладкой разобрались. Теперь нужно доставить изменения схемы БД в продакшен.

Kubernetes это ответ! А какой был ваш вопрос?


Итак, вам надо автоматизировать какой-то CI/CD-процесс. У нас есть обкатанный подход на тимсити. Казалось бы, где тут повод для еще одной статьи?

А повод есть. Кроме обкатанного подхода, есть и поднадоевшие проблемки большой компании.

  • Билдагентов тимсити на всех не хватает.
  • Лицензия стоит денег.
  • Настройки виртуалок билдагентов делаются по старинке, через репозитории с конфигами и puppet.
  • Доступы с билдагентов до целевых сетей пропиливать надо по старинке.
  • Логины-пароли для наката изменений на базу тоже хранятся по старинке.


И во всем этом по старинке проблема все бегут в светлое будущее, а поддержка легаси ну вы знаете. Работает и ладно. Не работает займемся потом. Когда-нибудь. Не сегодня.

Допустим, вы уже одной ногой по колено в светлом будущем и кубернетес-инфраструктура у вас уже есть. Есть даже возможность сгенерировать еще один микросервис, который сразу заведется в этой инфраструктуре, подхватит нужный конфиг и секреты, будет иметь нужные доступы, зарегистрируется в service mesh инфраструктуре. И всё это счастье может получить рядовой разработчик, без привлечения человека с ролью *OPS. Вспоминаем, что в кубернетесе есть тип ворклоада Job, как раз предназначенный для каких-то сервисных работ. Ну и погнали делать приложение на Kotlin+Spring-Liquibase, стараясь максимально переиспользовать существующую в компании инфраструктуру для микросервисов на JVM в кубере.

Переиспользуем следующие аспекты:

  • Генерация проекта.
  • Деплой.
  • Доставку конфигов и секретов.
  • Доступы.
  • Логирование и доставка логов в ELK.


Получаем такой пайплайн:


Кликабельно

Теперь мы имеем:


  • Версионирование ченджсетов.
  • Проверяем их на выполнимость update rollback.
  • Пишем тесты на DAO. Бывает даже следуем TDD: запускаем отладку DAO с помощью тестов. Тесты выполняются на свежеподнятой БД в TestContainers.
  • Запускаем локально БД в докере на стандартном порту. Проводим отладку, смотрим, что осталось в БД. При необходимости можем управлять локальной БД вручную.
  • Накатываем в тест и проводим авторелиз патчсетов стандартным пайплайном в teamcity, по аналогии с микросервисами. Пайплайн является дочерним для микросервиса, которому принадлежит БД.
  • Не храним креды от БД в тимсити. И не заботимся о доступах с виртуалок-билдагентов.


Знаю, что для многих это всё не откровение. Но раз уж вы дочитали, будем рады рассказу о вашем опыте в комментах =)
Подробнее..

Обновление процесса CICD год спустя

05.04.2021 02:07:14 | Автор: admin

Это четвертая и заключительная часть цикла об обновлении CI/CD процессов. Кстати, вот оглавление:
Часть 1: что есть, почему оно не нравится, планирование, немного bash. Я бы назвал эту часть околотехнической.
Часть 2: teamcity.
Часть 3: octopus deploy.
Часть 4: внезапно вполне себе техническая. Что произошло за прошедший год.

Сейчас у нас есть рабочая система доставки обновлений, которая прошла испытание временем. О настройке этой системы можно прочесть в первых трёх частях, а сейчас предлагаю вкратце вспомнить что там было.

Осуществлялся переезд CI/CD системы с CruiseControl.NET + git deploy на Teamcity + octopus. Будем честны, CD там и не пахло. Об этом, возможно, будет отдельная статья, но не в этом цикле.
С момента выхода первой статьи цикла прошло чуть больше года, с момента начала работы системы в проде - примерно полтора. Процесс разработки во время внедрения новой системы практически не прерывался. Было два раза, когда делали code freeze: один раз в момент перехода с mercurial репозитория в git (чтобы не потерять коммиты во время конвертации), и второй раз во время перехода билда production окружения с ccnet на teamcity (просто так, на всякий случай).
В результате мы получили систему которая способна наиболее оптимально (с минимальными время- и ресурсозатратами, а также с минимальными рисками) доставлять обновления во все существующие окружения.

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

Что произошло за этот год

  1. Мы практически полностью отказались от конфигураций вида Build + deploy. Теперь используем отдельно Build и отдельно Deploy. Последние всё также вызываются из teamcity, но это сделано исключительно для упрощения жизни всем менее причастным. На самом деле, для того чтобы обезопасить Octopus от вмешательства любопытных.

  2. Полностью перешли на semver. К сожалению, до момента внедрения девопс в проект, ни о каком semver речи не было. Картинка с этой версионностью уже мелькала в 3 части, останавливаться подробно не будем.

  3. Появился опыт настройки деплоя на сервере без каких-либо доступов, только с установленным octopus агентом. Не то, чтобы его не было, просто он был забыт как страшный сон за много лет. А тут пришлось вспоминать. Приятного мало, но на удивление, терпимо.

  4. Перешли с Visual studio (sln) раннера на .net msbuild ввиду окончания поддержки первого (Teamcity).

  5. Для Special Module (см часть 1) появился интересный вызов билда из деплоя с пробросом параметров через reverse.dep

  6. Появился какой-никакой роллбек.

  7. Переработали variable setы в octopus, используем tenant variables.

  8. Практически везде перешли от хранения connection string в репозитории на хранение в Octopus и подстановкой при деплое. К сожалению, раньше хранили именно в репозитории.

  9. Для деплоя особо важных модов добавили защиту от тестировщика (подробнее чуть позже).

  10. Выросли на 7 новых тенантов (клиентов).

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

Build-chain наоборот (пункт 4)

Для Special module, как и для любых других есть несколько окружений. Список всех пайплайнов для этого модуля в teamcity выглядит так:

Build конфигурация используется одна, с вот таким параметром ветки:

Также в данной конфигурации используются две prompted переменные типа Select: env.Environment и env.buildBranch. Выглядят они примерно одинаково, отличаются только Items. Для каждого env ставится в соответствие ветка репозитория.

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

В каждой Deploy конфигурации, есть зависимость от актуальности конфигурации build и параметры типа reverse.dep, которые при запуске Build устанавливают для него env.Environment и env.buildBranch. Например, для development это выглядит так:

Как всё это работает вместе: при нажатии кнопки deploy соответствующего окружения проверяется наличие изменений в репозитории. Если изменения есть - запускается конфигурация Build с установленными через reverse.dep переменными. По завершению билда, запускается ожидавший всё это время деплой новой версии пакета.

Rollback (пункт 6)

Rollback построен на следующем алгоритме:

  1. Определить номер текущего и предыдущего релизов в octopus для Core и Module.

  2. Откатить Core (задеплоить предыдущий релиз)

  3. Откатить Module.

Octopus хранит 3 предыдущих релиза так, на всякий случай. Rollback из teamcity работает только с предыдущим релизом. Откат на более давний релиз надо делать вручную, но такой необходимости ни разу не возникало. Так выглядят определение версий:

$packageRelease = ((%env.octoExe% list-deployments --server="%env.octoUrl%" --apikey="%env.octoApiKey%" --project="ProjectName.%env.modName%" --environment="%env.Environment%" --outputFormat=json) | ConvertFrom-Json).Version[0..1]$coreRelease = (((%env.octoExe% list-deployments --server="%env.octoUrl%" --apikey="%env.octoApiKey%" --project="%env.coreProjectName%" --environment="%env.Environment%" --outputFormat=json) | ConvertFrom-Json).Version | Get-Unique)[0..1]$OctopusPackageCurrentRelease = $packageRelease[0]$OctopusPackagePreviousRelease = $packageRelease[1]$corePreviousVersion = $OctopusPackagePreviousRelease | %{ $_.Split('-')[0]; }$coreEnv = $OctopusPackagePreviousRelease | %{ $_.Split('-')[1]; } |  %{ $_.Split('+')[0]; }$OctopusCoreCurrentRelease = $coreRelease[0]$OctopusCorePreviousRelease = "$corePreviousVersion-$coreEnv"Write-Host "##teamcity[setParameter name='OctopusPackageCurrentRelease' value='$OctopusPackageCurrentRelease']"Write-Host "##teamcity[setParameter name='OctopusPackagePreviousRelease' value='$OctopusPackagePreviousRelease']"Write-Host "##teamcity[setParameter name='OctopusCoreCurrentRelease' value='$OctopusCoreCurrentRelease']"Write-Host "##teamcity[setParameter name='OctopusCorePreviousRelease' value='$OctopusCorePreviousRelease']"

Откат является деплоем соответствующей версии, поэтому глобально ничем не отличается от шага Deploy.2 описанного в части 2. Меняется только Release Number. Вместо latest используется %OctopusCorePreviousRelease% и %OctopusPackagePreviousRelease% соответственно.

Переработка variable sets

Раньше все переменные тенантов хранились в конфигурациях проектов и разруливались расстановкой скоупов. Вот хороший пример из части 3:

При количестве тенантов больше 3 это оказывается неудобно. Поэтому, перешли к хранению переменных клиентов в предназначенном для этого месте - tenant variables - common variables.
Так списки переменных проектов стали чище, и там больше нет каши.

Защита от тестировщика (пункт 9)

В список задач тестировщика входит также деплой на некоторые окружения. Туда, куда деплои не попадают автоматически из за ограничений. Зачастую это выглядит как клик клик клик клик по кнопкам Run не задумываясь. Исключение составляет prod окружение, но это не точно. Пару раз были прецеденты деплоя модов, которые помечены как secure. Это особая категория модов, которыми пользуются особые люди. Они очень любят стабильность и все релизы у них планируются, а набор новых фич обсуждается. В общем, для этих модов пришлось добавить элементарную защиту в виде всплывающего Are you sure и требованием ввести ответ буквами.

Реализовано это с помощью prompted переменной и regexp.

Заключение

В данный момент я работаю над этим проектом по минимуму. Саппорт практически не требуется, всё работает практически без моего участия. Где-то есть continuous deployment, где-то пришлось ограничиться delivery. Там где надо нажимать кнопки вручную - справляются тестировщик и главный девелопер. Время добавления новых конфигураций (по факту нового клиента) вместе с проверкой работоспособности - час с чайком и без напряга. С CCNet такой результат показался бы фантастикой при условии отcутствия дичайшего оверхеда со стороны ресурсов сервера. Да и удобства никакого. Пропала проблема бесконечной нехватки места, так как на сервере не хранятся лишние копии одного и того же. И даже rollback показал себя с хорошей стороны, и на удивление работает.Всё работает классно шустро, и самое главное - стабильно и прогнозируемо.

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

Статья получилось вполне технической. На описание неприятных моментов и места то не осталось. Могу сказать, что все неприятные моменты связаны исключительно с впечатлением от windows, а не с проектом. Это всё таки был мой первый проект на оконном стеке. Впечатления непосредственно от проекта только самые лучшие. Хоть это и древнее легаси-зло, проект на самом деле очень крутой и интересный. И опыт поддержки и постановки таких проектов на новые рельсы без преувеличения можно назвать бесценным. В общем, в мире стало на один хороший проект с devops методологией больше.

Подробнее..

PVS-Studio и Continuous Integration TeamCity. Анализ проекта Open RollerCoaster Tycoon 2

20.07.2020 18:22:00 | Автор: admin

Один из самых актуальных сценариев использования анализатора PVS-Studio его интеграция с CI системами. И хотя анализ проекта PVS-Studio практически из-под любой continuous integration системы можно встроить всего в несколько команд, мы продолжаем делать этот процесс ещё удобнее. В PVS-Studio появилась поддержка преобразования вывода анализатора в формат для TeamCity TeamCity Inspections Type. Давайте посмотрим, как это работает.

Информация об используемом ПО


PVS-Studio статический анализатор С, С++, C# и Java кода, предназначенный для облегчения задачи поиска и исправления различного рода ошибок. Анализатор можно использовать в Windows, Linux и macOS. В данной статье мы будем активно использовать не только сам анализатор, но и некоторые утилиты из его дистрибутива.

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

PlogConverter утилита для конвертации отчёта анализатора в разные форматы.

Информация об исследуемом проекте


Давайте попробуем данную функциональность на практическом примере проанализируем проект OpenRCT2.

OpenRCT2 открытая реализация игры RollerCoaster Tycoon 2 (RCT2), расширяющая её новыми функциями и исправляющая ошибки. Игровой процесс вращается вокруг строительства и содержания парка развлечений, в котором находятся аттракционы, магазины и объекты. Игрок должен постараться получить прибыль и поддерживать хорошую репутацию парка, сохраняя при этом гостей счастливыми. OpenRCT2 позволяет играть как в сценарии, так и в песочнице. Сценарии требуют, чтобы игрок выполнил определенную задачу в установленное время, в то время как песочница позволяет игроку построить более гибкий парк без каких-либо ограничений или финансов.

Настройка


В целях экономии времени я, пожалуй, опущу процесс установки и начну с того момента, когда у меня на компьютере запущен сервер TeamCity. Нам нужно перейти: localhost:{указанный в процессе установки порт}(в моём случае, localhost:9090) и ввести данные для авторизации. После входа нас встретит:

image3.png

Нажмём на кнопку Create Project. Далее выберем Manually, заполним поля.

image5.png

После нажатия на кнопку Create, нас встречает окно с настройками.

image7.png

Нажмём Create build configuration.

image9.png

Заполняем поля, нажимаем Create. Мы видим окно с предложением выбора системы контроля версий. Так как исходники уже лежат локально, жмём Skip.

image11.png

Наконец, мы переходим к настройкам проекта.

image13.png

Добавим шаги сборки, для этого жмём: Build steps -> Add build step.

image15.png

Тут выберем:

  • Runner type -> Command Line
  • Run -> Custom Script

Так как мы будем проводить анализ во время компиляции проекта, сборка и анализ должны быть одним шагом, поэтому заполним поле Custom Script:

image17.png

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

Последнее, что нам нужно сделать, установить переменные окружения, которыми я обозначил некоторые пути для улучшения их читабельности. Для этого перейдём: Parameters -> Add new parameter и добавим три переменные:

image19.png

Остаётся нажать на кнопку Run в правом верхнем углу. Пока идёт сборка и анализ проекта расскажу вам о скрипте.

Непосредственно скрипт


Для начала нам нужно выкачать свежий дистрибутив PVS-Studio. Для этого мы используем пакетный менеджер Сhocolatey. Для тех, кто хочет узнать об этом поподробнее, есть соответствующая статья:

choco install pvs-studio -y

Далее запустим утилиту отслеживания сборки проекта CLMonitor.

%CLmon% monitor -attach

Потом произведём сборку проекта, в качестве переменной окружения MSB выступает путь к нужной мне для сборки версии MSBuild

%MSB% %ProjPath% /t:clean%MSB% %ProjPath% /t:rebuild /p:configuration=release%MSB% %ProjPath% /t:g2%MSB% %ProjPath% /t:PublishPortable

Введём логин и ключ лицензии PVS-Studio:

%PVS-Studio_cmd% credentials --username %PVS_Name% --serialNumber %PVS_Key%

После завершения сборки ещё раз запустим CLMonitor для генерации препроцессированных файлов и статического анализа:

%CLmon% analyze -l "c:\ptest.plog"

После воспользуемся ещё одной утилитой из нашего дистрибутива. PlogConverter преобразует отчёт из стандартного в специфичный для TeamCity формат. Благодаря этому мы сможем посмотреть его прямо в окне сборки.

%PlogConverter% "c:\ptest.plog" --renderTypes=TeamCity -o "C:\temp"

Последним действием выведем форматированный отчёт в stdout, где его подхватит парсер TeamCity.

type "C:\temp\ptest.plog_TeamCity.txt"

Полный код скрипта:

choco install pvs-studio -y%CLmon% monitor --attachset platform=x64%MSB% %ProjPath% /t:clean%MSB% %ProjPath% /t:rebuild /p:configuration=release%MSB% %ProjPath% /t:g2%MSB% %ProjPath% /t:PublishPortable%PVS-Studio_cmd% credentials --username %PVS_Name% --serialNumber %PVS_Key%%CLmon% analyze -l "c:\ptest.plog"%PlogConverter% "c:\ptest.plog" --renderTypes=TeamCity -o "C:\temp"type "C:\temp\ptest.plog_TeamCity.txt"

Тем временем, сборка и анализ проекта успешно завершились, мы можем перейти на вкладку Projects и убедиться в этом.

image21.png

Теперь кликнем на Inspections Total, чтоб перейти к просмотру отчёта анализатора:

image23.png

Предупреждения сгруппированы по номерам диагностических правил. Для осуществления навигации по коду нужно кликнуть на номер строки с предупреждением. Нажатие на знак вопроса в правом верхнем углу откроет вам новую вкладку с документацией. Также можно осуществить навигацию по коду, нажав на номер строки с предупреждением анализатора. Навигация с удалённого компьютера возможна при применении SourceTreeRoot маркера. Тот, кому интересен данный режим работы анализатора, может ознакомиться с соответствующим разделом документации.

Просмотр результатов работы анализатора


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

Предупреждение N1

V773 [CWE-401] The exception was thrown without releasing the 'result' pointer. A memory leak is possible. libopenrct2 ObjectFactory.cpp 443

Object* CreateObjectFromJson(....){  Object* result = nullptr;  ....  result = CreateObject(entry);  ....  if (readContext.WasError())  {    throw std::runtime_error("Object has errors");  }  ....}Object* CreateObject(const rct_object_entry& entry){  Object* result;  switch (entry.GetType())  {    case OBJECT_TYPE_RIDE:      result = new RideObject(entry);      break;    case OBJECT_TYPE_SMALL_SCENERY:      result = new SmallSceneryObject(entry);      break;    case OBJECT_TYPE_LARGE_SCENERY:      result = new LargeSceneryObject(entry);      break;    ....    default:      throw std::runtime_error("Invalid object type");  }  return result;}

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

Предупреждение N2

V501 There are identical sub-expressions '(1ULL << WIDX_MONTH_BOX)' to the left and to the right of the '|' operator. libopenrct2ui Cheats.cpp 487

static uint64_t window_cheats_page_enabled_widgets[] = {  MAIN_CHEAT_ENABLED_WIDGETS |  (1ULL << WIDX_NO_MONEY) |  (1ULL << WIDX_ADD_SET_MONEY_GROUP) |  (1ULL << WIDX_MONEY_SPINNER) |  (1ULL << WIDX_MONEY_SPINNER_INCREMENT) |  (1ULL << WIDX_MONEY_SPINNER_DECREMENT) |  (1ULL << WIDX_ADD_MONEY) |  (1ULL << WIDX_SET_MONEY) |  (1ULL << WIDX_CLEAR_LOAN) |  (1ULL << WIDX_DATE_SET) |  (1ULL << WIDX_MONTH_BOX) |  // <=  (1ULL << WIDX_MONTH_UP) |  (1ULL << WIDX_MONTH_DOWN) |  (1ULL << WIDX_YEAR_BOX) |  (1ULL << WIDX_YEAR_UP) |  (1ULL << WIDX_YEAR_DOWN) |  (1ULL << WIDX_DAY_BOX) |  (1ULL << WIDX_DAY_UP) |  (1ULL << WIDX_DAY_DOWN) |  (1ULL << WIDX_MONTH_BOX) |  // <=  (1ULL << WIDX_DATE_GROUP) |  (1ULL << WIDX_DATE_RESET),  ....};

Мало кто, кроме статического анализатора, смог бы пройти данный тест на внимательность. Данный пример копипасты хорош именно этим.

Предупреждения N3

V703 It is odd that the 'flags' field in derived class 'RCT12BannerElement' overwrites field in base class 'RCT12TileElementBase'. Check lines: RCT12.h:570, RCT12.h:259. libopenrct2 RCT12.h 570

struct RCT12SpriteBase{  ....  uint8_t flags;  ....};struct rct1_peep : RCT12SpriteBase{  ....  uint8_t flags;  ....};

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

Предупреждение N4

V793 It is odd that the result of the 'imageDirection / 8' statement is a part of the condition. Perhaps, this statement should have been compared with something else. libopenrct2 ObservationTower.cpp 38

void vehicle_visual_observation_tower(...., int32_t imageDirection, ....){  if ((imageDirection / 8) && (imageDirection / 8) != 3)  {    ....  }  ....}

Давайте разберёмся поподробнее. Выражение imageDirection / 8 будет false в том случае, если imageDirection находится в диапазоне от -7 до 7. Вторая часть: (imageDirection / 8) != 3 проверяет imageDirection на нахождение вне диапазона: от -31 до -24 и от 24 до 31 соответственно. Мне кажется довольно странным проверять числа на вхождение в определённый диапазон таким способом и, даже если в данном фрагменте кода нет ошибки, я бы рекомендовал переписать данные условия на более явные. Это существенно упростило бы жизнь людям, которые будут читать и поддерживать этот код.

Предупреждение N5

V587 An odd sequence of assignments of this kind: A = B; B = A;. Check lines: 1115, 1118. libopenrct2ui MouseInput.cpp 1118

void process_mouse_over(....){  ....  switch (window->widgets[widgetId].type)  {    case WWT_VIEWPORT:      ebx = 0;      edi = cursorId;                                 // <=      // Window event WE_UNKNOWN_0E was called here,      // but no windows actually implemented a handler and      // it's not known what it was for      cursorId = edi;                                 // <=      if ((ebx & 0xFF) != 0)      {        set_cursor(cursorId);        return;      }      break;      ....  }  ....}

Данный фрагмент кода, скорее всего, был получен путем декомпиляции. Затем, судя по оставленному комментарию, была удалена часть нерабочего кода. Однако осталась пара операций над cursorId, которые также не несут особого смысла.

Предупреждение N6

V1004 [CWE-476] The 'player' pointer was used unsafely after it was verified against nullptr. Check lines: 2085, 2094. libopenrct2 Network.cpp 2094

void Network::ProcessPlayerList(){  ....  auto* player = GetPlayerByID(pendingPlayer.Id);  if (player == nullptr)  {    // Add new player.    player = AddPlayer("", "");    if (player)                                          // <=    {      *player = pendingPlayer;       if (player->Flags & NETWORK_PLAYER_FLAG_ISSERVER)       {         _serverConnection->Player = player;       }    }    newPlayers.push_back(player->Id);                    // <=  }  ....}

Данный код поправить довольно просто, нужно или третий раз проверять player на нулевой указатель, либо внести его в тело условного оператора. Я бы предложил второй вариант:

void Network::ProcessPlayerList(){  ....  auto* player = GetPlayerByID(pendingPlayer.Id);  if (player == nullptr)  {    // Add new player.    player = AddPlayer("", "");    if (player)    {      *player = pendingPlayer;      if (player->Flags & NETWORK_PLAYER_FLAG_ISSERVER)      {        _serverConnection->Player = player;      }      newPlayers.push_back(player->Id);    }  }  ....}

Предупреждение N7

V547 [CWE-570] Expression 'name == nullptr' is always false. libopenrct2 ServerList.cpp 102

std::optional<ServerListEntry> ServerListEntry::FromJson(...){  auto name = json_object_get(server, "name");  .....  if (name == nullptr || version == nullptr)  {    ....  }  else  {    ....    entry.name = (name == nullptr ? "" : json_string_value(name));    ....  }  ....}

Можно одним махом избавиться от трудночитаемой строки кода и решить проблему с проверкой на nullptr. Предлагаю изменить код следующим образом:

std::optional<ServerListEntry> ServerListEntry::FromJson(...){  auto name = json_object_get(server, "name");  .....  if (name == nullptr || version == nullptr)  {    name = ""    ....  }  else  {    ....    entry.name = json_string_value(name);    ....  }  ....}

Предупреждение N8

V1048 [CWE-1164] The 'ColumnHeaderPressedCurrentState' variable was assigned the same value. libopenrct2ui CustomListView.cpp 510

void CustomListView::MouseUp(....){  ....  if (!ColumnHeaderPressedCurrentState)  {    ColumnHeaderPressed = std::nullopt;    ColumnHeaderPressedCurrentState = false;    Invalidate();  }}

Код выглядит довольно странно. Мне кажется, имела место быть опечатка либо в условии, либо при повторном присвоении переменной ColumnHeaderPressedCurrentState значения false.

Вывод


Как мы видим, интегрировать статический анализатор PVS-Studio в свой проект на TeamCity довольно просто. Для этого достаточно написать всего один маленький файл конфигурации. Проверка кода же позволит выявлять проблемы сразу после сборки, что поможет устранять их тогда, когда сложность и стоимость правок ещё малы.


Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Vladislav Stolyarov. PVS-Studio and Continuous Integration: TeamCity. Analysis of the Open RollerCoaster Tycoon 2 project.
Подробнее..

Что такое CI (Continuous Integration)

25.06.2020 16:23:50 | Автор: admin
CI (Continuous Integration) в дословном переводе непрерывная интеграция. Имеется в виду интеграция отдельных кусочков кода приложения между собой. Чем чаще мы собираем код воедино и проверяем:

  • Собирается ли он?
  • Проходят ли автотесты?

Тем лучше! CI позволяет делать такие проверки автоматически. Он используется в продвинутых командах разработки, которые пишут не только код, но и автотесты. Его спрашивают на собеседованиях хотя бы понимание того, что это такое. Да, даже у тестировщиков.

Поэтому я расскажу в статье о том, что это такое. Как CI устроен и чем он пригодится вашему проекту.



Содержание




Что такое CI


CI это сборка, деплой и тестирование приложения без участия человека. Сейчас объясню на примере.

Допустим, что у нас есть два разработчика Маша и Ваня. И тестировщица Катя.

Маша пишет код. Добавляет его в систему контроля версий (от англ. Version Control System, VCS). Это что-то типа дропбокса для кода место хранения, где сохраняются все изменения и в любой момент можно посмотреть кто, что и когда изменял.

Потом Ваня заканчивает свой кусок функционала. И тоже сохраняет код в VCS.



Но это просто исходный код набор файликов с расширением .java, или любым другим. Чтобы Катя могла протестировать изменения, нужно:

  1. Собрать билд из исходного кода
  2. Запустить его на тестовой машине

Сборка билда это когда мы из набора файликов исходного кода создаем один запускаемый файл:



Собрать билд можно вручную, но это лишний геморрой: нужно помнить, что в каком порядке запустить, какие файлики зависят друг от друга, не ошибиться в команде Обычно используют специальную программу. Для java это Ant, Maven или Gradle. С помощью сборщика вы один раз настраиваете процесс сборки, а потом запускаете одной командой. Пример запуска для Maven:

mvn clean install

Это полуавтоматизация все равно нужен человек, который введет команду и соберет билд ручками. Допустим, этим занимаются разработчики. Когда Катя просил билд на тестирование, Ваня обновляет версию из репозитория и собирает билд.



Но собрать билд получить приложение для тестирования. Его еще надо запустить! Этим занимается сервер приложения. Серверы бывают разные: wildfly, apache, jetty

Если это wildfly, то нужно:

  1. Подложить билд в директорию standalone/deployments
  2. Запустить сервер (предварительно один раз настроив службу)

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



А вот если убрать из этой схемы человека мы получим CI!



CI это приложение, которое позволяет автоматизировать весь процесс. Оно забирает изменения из репозитория с кодом. Само! Тут есть два варианта настройки:

  • CI опрашивает репозиторий Эй, ку-ку, у тебя есть изменения?? раз в N часов / минут, как настроите.

  • Репозиторий машет CI рукой при коммите: Эй, привет! А у меня обновление тут появилось! (это git hook или аналог в вашей VCS)


Когда CI получило изменения, оно запускает сборку билда и автотесты.

Если сборка провалилась (тесты упали, или не получилось собрать проект), система пишет элекронное письмо всем заинтересованным лицам:

  • Менеджеру проекта (чтобы знал, что делается!)
  • Разработчику, который внес изменения
  • Любому другому как настроите, так и будет.



Если сборка прошла успешно, CI разворачивает приложение на тестовой машине. И в итоге Катька может тестировать новую сборку!



Да, разумеется, один раз придется это все настроить рассказать серверу CI, откуда забирать изменения, какие автотесты запускать, как собирать проект, куда его потом билдить Но зато один раз настроил а дальше оно само!

Автотесты тоже придется писать самим, но чтож поделать =)



Если на пальцах, то система CI (Continuous Integration) это некая программа, которая следит за вашим Source Control, и при появлении там изменений автоматически стягивает их, билдит, гоняет автотесты (конечно, если их пишут).

В случае неудачи она дает об этом знать всем заинтересованным лицам, в первую очередь последнему коммитеру. (с) habr.com/ru/post/352282



Программы CI


Наиболее популярные Jenkins и TeamCity.

Но есть куча других вариаций CruiseControl, CruiseControl.Net, Atlassian Bamboo, Hudson, Microsoft Team Foundation Serve.


Как это выглядит


Давайте посмотрим, как это выглядит с точки зрения пользователя. Я покажу на примере системы TeamCity.

Когда я захожу в систему, я вижу все задачи. Задачи бывают разные:

  • Собрать билд
  • Прогнать автотесты
  • Развернуть приложение на тестовом стенде
  • Прогнать на этом стенде GUI тесты (или тесты Postman-a)
  • Оповестить всех заинтересованных по email о результатах сборки и тестирования

Задачи можно группировать. Вот, скажем, у нас есть проект CDI. Зайдя внутрь, я вижу задачи именно по этому проекту:



  • CDI Archetype и CDI Core это билды. Они проверяют, что приложение вообще собирается. Отрабатывают за пару минут и прогоняются на каждое изменение кода.
  • CDI Core with tests сборка проекта со всеми автотестами, которых, как видно на скрине, 4000+ штук. Тесты идут полчаса, но тоже прогоняются на каждый коммит.

Помимо автоматизированного запуска, я могу в любой момент пересобрать билд, нажав на кнопку Run:



Это нужно, чтобы:

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

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



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


Как CI устроен


Как и где CI собирает билд и прогоняет автотесты? Я расскажу на примере TeamCity, но другие системы работают примерно также.

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



Агент это простой компьютер. Железка или виртуальная машина, не суть. Но как этот комьютер понимает, что ему надо сделать?

В TeamCity есть сервер и клиент. Сервер это то самое приложение, в котором вы потом будете тыкать кнопочки и смотреть красивую картинку насколько все прошло успешно. Он устанавливается на одну машину.



А приложение-клиент устанавливается на машинах-агентах. И когда мы нажимаем кнопку Run на сервере:



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



Сервер отображает пользователю результат плюс рассылает email всем заинтересованным лицам.

При этом мы всегда видим, на каком конкретно агенте проходила сборка:



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

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

Мы собирали на нем проект, проводили автотесты все работало. А потом закупили вторую машинку и назвали Apollo. Вроде настроили также, как Буран, даже операционную систему одинаковую поставили CentOs 7.



Но запускаем сборку на Apollo падает. Причем падает странно, не хватает памяти или еще чего-то. Перезапускаем на Apollo снова падает. Запускаем на Буране проходит успешно!



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



Мы также можем для каждой сборки настроить список агентов, на которых она может выполняться. Зачем? Например, у нас на половине агентов линукс, а на половине винда. А сборку мы только под одну систему сделали. Или на винде вылезает какой-то плавающий баг, но исправлять его долго и дорого, а все клиенте на линуксе ну и зачем тогда?

А еще бывает, что агентов делят между проектами, чтобы не было драки этот проект использует Бурана и Аполло, а тот Чип и Дейла. Фишка ведь в том, что на одном агенте может выполняться только одно задание. Поэтому нет смысла покупать под агент крутую тачку, сразу кучу тестов там все равно не прогнать.

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



При этом TeamCity вы платите за количество лицензий на билд-агентов. Так что чем меньше их будет, тем лучше.

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

У нас есть два проекта Единый клиент и Фактор, которые взаимодействуют между собой. Тестировщик Единого клиента может не собирать Фактор локально. Он запускает сборку в TeamCity и скачивает готовый билд из артефактов!

Дальше уже разработчик выбирает, какую сборку он хочет запустить и нажимает Run. Что в этот момент происходит:

1. Сервер TeamCity проверяет по списку, на каких агентах эту сборку можно запускать. Потом он проверяет, кто из этих агентов в данный момент свободен:



Нашел свободного? Отдал ему задачку!



Если все агенты заняты, задача попадает в очередь. Очередь работает по принципу FIFO first in, first out. Кто первый встал того и тапки.



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



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

  • Робот? Значит, это просто плановая проверка, что ничего лишнего не разломалось. Такая может и подождать 5-10-30 минут, ничего страшного
  • Коллега? Ему эта сборка важна, раз не стал ждать планового запуска. Встаем в очередь, лезть вперед не стоит.

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

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

2. Агент выполняет задачу и возвращает серверу результат



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

Настоятельно рекомендуется настроить заранее количество сборок, которые CI будет хранить. Потому что если в артефактах лежат билды по 200+ мб и их будет много, то очередной запуск сборки упадет с ошибкой кончилось место на диске:



4. Сервер делает рассылку по email тут уж как настроите. Он может и позитивную рассылку делать сборка собралась успешно, а может присылать почту только в случае неудачи Ой-ей-ей, что-то пошло не так!.


Интеграция с VCS


Я говорила о разных вариантах настройки интеграции CI VCS:

  • CI опрашивает репозиторий Эй, ку-ку, у тебя есть изменения?? раз в N часов / минут, как настроите.

  • Репозиторий машет CI рукой при коммите: Эй, привет! А у меня обновление тут появилось! (это git hook или аналог в вашей VCS)



Но когда какой используется?

Лучше всего, конечно, чтобы система контроля версий оповещала сервер CI. И запускать весь цикл на каждое изменение: собрать, протестировать, задеплоить. Тогда любое изменение кода сразу попадет на тестовое окружение, которое будет максимально актуальным.



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



Но в реальной жизни такая схема редко применима. Только подумайте у вас ведь может быть много проектов, много разработчиков. Каждый что-то коммитит ну хотя бы раз в полчаса. И если на каждый коммит запускать 10 сборок по полчаса очереди в TeamCity никогда не разгребутся!

У нас у одного из продуктов есть core-модуль, а есть 15+ Заказчиков. В каждом свои автотесты. Сборка заказчика это core + особенности заказчика. То есть изменение в корневом проекте может повлиять на 15 разных сборок. Значит, их все надо запустить при коммите в core.

Когда у нас было 4 билд-агента, все-все-все сборки и тесты по этим заказчикам запускались в ночь на вторник. И к 10 утра в TeamCity еще была очередь на пару часов.

Другой вариант закупить много агентов. Но это цена за саму машину + за лицензию в TeamCity, что уже сильно дороже, да еще и каждый месяц платить.

Поэтому обычно делают как:

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



2. Остальные сборки проверяют, были ли изменения в VCS например, раз в 15 минут. Если были, тогда запускаем.



3. Долгие тесты (например, тесты производительности) раз в несколько дней ночью.





CI в тестировании


Если мы говорим о разработке своего приложения, то тестирование входит в стандартный цикл. Вы или ваши разработчики пишут автотесты, которые потом гоняет CI. Это могут быть unit, api, gui или нагрузочные тесты.



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

Вот, допустим, у вас есть API-тесты в Postman-е. Или GUI-тесты в Selenium. Можно ли настроить цикл CI для них?



Конечно, можно!

CI не ставит жестких рамок типа я работаю только в проектах с автотестами или я работаю только когда есть доступ к исходному коду. Он может смотреть в систему контроля версий, а может и не смотреть. Это необязательное условие!

Написали автотесты? Скажите серверу CI, как часто их запускать и наслаждайтесь результатом =)




Итого


CI непрерывная интеграция. Это когда ваше приложение постоянно проверяется: все ли с ним хорошо? Проходят ли тесты? Собирается ли сборка? Причем все проверки проводятся автоматически, без участия человека.

Особенно актуально для команд, где над кодом одного приложения трудятся несколько разработчиков. Как это бывает? По отдельности части программы работают, а вот вместе уже нет. CI позволяет очень быстро обнаружить такие проблемы. А чем быстрее найдешь тем дешевле исправить.



Отсюда и название постоянная проверка интеграции кусочков кода между собой.
Типичные задачи CI:

  • Проверить, было ли обновление в коде
  • Собрать билд
  • Прогнать автотесты
  • Развернуть приложение на тестовом стенде
  • Прогнать на этом стенде GUI тесты (или тесты Postman-a)
  • Оповестить всех заинтересованных по email о результатах сборки и тестирования




И все это автоматически, без вмешательства человека! То есть один раз настроили, а дальше оно само.

Если в проекте настроен CI, у вас будут постоянно актуальные тестовые стенды. И если в коде что-то сломается, вы узнаете об этом сразу, сервер CI пришлет письмо. А еще можно зайти в графический интерфейс и посмотреть все ли сборки успешные, а тесты зеленые? Оценить картину по проекту за минуту.

См также:
Continuous Integration для новичков

PS больше полезных статей ищите в моем блоге по метке полезное. А полезные видео на моем youtube-канале
Подробнее..

Как готовить Cake, используя только Frosting

26.12.2020 10:13:54 | Автор: admin

Итак, Cake. Многие слышали, многие хотели попробовать, но откладывали. Конечно, если ты все время работал на TeamCity или на Jenkins и продолжаешь, то зачем переизобретать то, что уже отлично работает? Люби свою жизнь и радуйся. Но вот, допустим, в твоей любимой жизни появился новый проект, новый дедлайн, минимум сторипойнтов до релиза, а опыта с новым сборщиком нет? Мне в этом случае и пригодился Cake.

Я сразу оговорюсь, что эта статья не подтолкнет сразу на использование Cake, как меня, и многих моих коллег не подтолкнули статьи, которые выходили ранее. По большей части потому что на него нет смысла переходить в проекте, который не приносит боль и который работает стабильно. Собираете в своем любимом Jenkins и все идет нормально. Но пусть после этой статьи в голове отложится, что Cake существует. Он в очередной раз никуда не делся, он умеет уже многое и работать с ним все проще. Гораздо проще, чем было раньше.

На что похож Cake? Наверное, любой разработчик, не погрязший в мире .Net, найдет свою аналогию: gradle, gulp, golang make. Make-системы не откровение в 2020 году. Это всегда было удобно, а значит нужно и правильно. Мир .Net долгое время был обделен такими средствами. Фактически был и есть до сих пор MSBuild, но у него есть очень-очень много недостатков. Основной - кто вообще умеет им пользоваться из рядовых разработчиков? И какова целесообразность его освоения? Какие-то базовые и нужные всем вещи явно проще делать на билд-сервере. Наверное, кому-то он и удобен, но я уверен, что значимая часть коммьюнити предпочтет MSBuild'у освоить новый билд-сервер. Один раз написать конфиг и забыть как страшный сон.

А что если бы существовала make-система с DSL на C#, автокомплитом и прочими фишками типизированного языка? Да, я про Cake. В частности сейчас пойдет разговор про библиотеку Cake.Frosting, являющуюся одним из раннеров make-системы.

Подробней про доступные раннеры можно прочитать тут: Cake Runners

С Frosting все привычно самодокументирующийся Api с которым почти сразу находишь общий язык. Методы расширения, загружаемые из Nuget на любой случай жизни, структура проекта, похожая на смесь тестов или бенчмарков и хоста Asp. Все решения угадываются сразу, все как дома.

Frosting от остальных раннеров Cake отличается тем, что существует не в виде тулза, а в виде отдельного проекта, который можно докинуть в solution и хранить вместе с ним в репозитории. Это невероятно упрощает работу с системой. Фактически стоит просто создать новый проект, подключить к нему зависимость Cake.Frosting, сконфигурировать Build-хост и можно запускать этот проект командой.

dotnet run

Чтобы нам стало еще проще, существует темплейт проекта. К нему в комплект даже идут шелл-скрипты для Mac OS, Linux и Windows, подгружающие SDK, если его нет в окружении. Через них стоит вызывать сборку вместо dotnet CLI, если в этом есть необходимость.

Тут можно почитать подробнее об этом: Frosting Bootstraping

После того, как проект создан, можно начинать конфигурировать процесс сборки. Из основных мест для нашего кода мы сразу обращаем внимание на прогрев и очистку системы перед сборкой это соответствующие методы класса Lifetime: Setup и TearDown. В них привычно делать уборку артефактов до и после сборки.

Вторая интересующая нас часть проекта папка Tasks. Тут хранятся все шаги сборки в виде классов -наследников от FrostingTask<Context>.

Задачи автоматически регистрируются в IoC контейнере, как мы привыкли в Asp. Более того, Frosting реализует точно такой же паттерн с DI через IServiceCollection, к которому мы все привыкли.

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

[Dependency(typeof(MyPreviousTask))]

Где MyPreviousTask это задача, которая должна завершиться ранее помеченной.


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

  1. Восстановление пакетов.

  2. Билд.

  3. Прогон unit-тестов.

  4. Publish.

  5. Поставка артефактов.

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

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

Единственный минус такого похода захламление IntelliSense окна чудовищным количеством методов, но когда это нас останавливало?

По случаю хотелось бы напомнить про относительно свежую фичу .Net core self-contained приложения. В этом способе публикации надо явно задать версию рантайма, в результате чего формируется не библиотека, исполняемая в контексте dotnet, а запускаемое приложение, содержащее рантайм, так сказать, в себе. Она может пригодиться при упаковке в образ без установленного рантайма, если по каким-то причинам установить последний нельзя. Нет никаких причин не делать этого в Cake.

Когда все готово, настроено и залито в репозиторий, мы делаем в TS или Jenkins всего одну команду

dotnet run ./Build/Build.csproj

Путь до проекта у вас будет свой (Ваш Кэп) и смотрим, как происходит медитативный процесс сборки. Frosting пишет события сборки в стандартный вывод, который читает билд-сервер, так что никакие данные не пропадут.

Конечно, это минимальный и самый простой сценарий использования системы. Билд-сервер может передать в dotnet аргументы при вызове билда Frosting. Например, чтобы в дальнейшем установить версию сборки или образа или тип рантайма или папки назначения для публикации.

В общем полный простор фантазии.

Мотивация

  1. Это удобно. Вы пишете на своем основном языке и не зависите от выразительности скриптов и настроек/плагинов билд-сервера;

  2. Это мобильно. Вы заливаете код в репозиторий и он универсально запускается на любом билд-сервере. И никакого вендор-лока.

  3. Это версионно. Код сборки хранится в репозитории. Вместе с самим релизом.

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

  5. Это легко. IntelliSense, автокомплит, разберется даже обленившийся senior.

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

Бонусом пример использования Cake.Frosting на github. Для затравки так сказать: Link

Ссылка на сайт проекта Cake

Подробнее..

Релиз мобильных приложений одной кнопкой

02.07.2020 18:23:23 | Автор: admin


Всем привет! Меня зовут Михаил Булгаков (нет, не родственник), я работаю релиз-инженером в Badoo. Пять лет назад я занялся автоматизацией релизов iOS-приложений, о чём подробно рассказывал в этой статье. А после взялся и за Android-приложения.

Сегодня я подведу некоторые итоги: расскажу, к чему мы пришли за это время. Long story short: любой причастный к процессу сотрудник может зарелизить хоть все наши приложения на обеих платформах в несколько кликов без головной боли, больших затрат времени, регистрации и СМС. Так, наш отдел релиз-инженеров за 2019 год сэкономил около 830 часов.

За подробностями добро пожаловать под кат!

Что стоит за мобильным релизом


Выпуск приложения в Badoo состоит из трёх этапов:

  1. Разработка.
  2. Подготовка витрины в магазине приложений: тексты, картинки всё то, что видит пользователь в App Store или Google Play.
  3. Релиз, которым занимается команда релиз-инжиниринга.

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

Большая часть времени уходит на подготовку витрины приложения в App Store или Google Play: необходимо залить красивые скриншоты, сделать завлекающее описание, оптимизированное для лучшей индексации, выбрать ключевые слова для поиска. От качества этой работы напрямую зависит популярность приложения, то есть по факту результат деятельности разработчиков, тестировщиков, дизайнеров, продакт-менеджеров, маркетологов всех причастных к созданию продукта.

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

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

Так как решение о готовности приложения к релизу в Badoo принимает команда QA-инженеров, им мы и решили предоставить право нажимать на красную кнопку запуска релиза. При этом мы хотели, чтобы она была доступна даже с мобильных устройств (с наглядным отображением прогресса скрипта).



Первые шаги на пути к автоматизации: загрузка метаданных


Как это работало в самом начале: для каждого релиза создавалась таблица в Google Sheets, в которую продакт-менеджер заливал выверенный мастер-текст на английском, после чего переводчики адаптировали его под конкретную страну, диалект и аудиторию, а затем релиз-инженер переносил всю информацию из этой таблицы в App Store или Google Play.

Первый шаг к автоматизации, который мы сделали, интегрировали перевод текстов в наш общий процесс переводов. Останавливаться на этом не буду это отдельная большая система, про которую можно прочитать в нашей недавней статье. Основной смысл в том, что переводчики не тратят время на таблички и работают с интерфейсом для удобной загрузки руками (читай: ctrl+c ctrl+v) переведённых вариантов в стор. Кроме того, присутствуют задатки версионирования и фундамент для Infrastructure-as-Code.

Одновременно с этим мы добавили выгрузку уже готовых переводов из базы данных и внедрение их в собирающийся IPA-файл (расширение файла iOS-приложения). Сборка приложения у нас происходит в TeamCity. Таким образом, каждая версия приложения всегда имела свежий перевод без ручного вмешательства в процесс сборки.

Какое-то время мы жили так, и в целом нас всё устраивало. Но количество приложений увеличивалось, а вместе с ним и время на подготовку каждого релиза.


Наша реальность по состоянию на 2015 год

В среднем на релиз одного приложения при наличии актуальной версии скриншотов уходило около полутора-двух часов работы релиз-инженера в случае с iOS и около получаса в случае с Android. Разница обусловлена тем, что iOS-приложения должны пройти так называемый Processing, который занимает некоторое время (отправить приложение на Review до успешного завершения Processing невозможно). Кроме того, App Store сам по себе по большинству операций в тот момент работал гораздо медленнее, чем Google Play.

Стало очевидно, что нам нужен дополнительный инструмент для доставки приложений в сторы. И как раз в тот момент на open-source-рынке начал набирать популярность продукт под названием Fastlane. Несмотря на то, что он тогда ещё был сыроватый, он уже мог решить огромный пласт наших проблем

Скажу о нём несколько слов, чтобы было понятнее, о чём пойдёт речь дальше.

Коротко о Fastlane


Сегодня Fastlane это продукт, который способен практически полностью автоматизировать все действия от момента окончания разработки до релиза приложения в App Store и Google Play. И речь не только о загрузке текстов, скриншотов и самого приложения здесь и управление сертификатами, и бета-тестирование, и подписывание кода, и многое другое.

Мы познакомились с Fastlane во времена его юности и нестабильности. Но сейчас это уверенно работающий и неотъемлемый компонент многих команд разработки мобильных приложений, которые сталкиваются с проблемой огромных временных затрат на доставку своих продуктов пользователям. Самое интересное в нём это возможности писать собственные плагины для основного компонента и пользоваться плагинами, написанными сообществом. Для такого специфического продукта это хорошее решение, которое (что важно) помогает не плодить лишние технологии в DevTools.

Доверие внушает и то, что основателя и главного разработчика Fastlane взяли на работу в Google: теперь компонент поддерживает не только комьюнити, но и Сам.

Со временем мы внедрили большинство предоставляемых Fastlane возможностей в системы сборки, подписания, заливки и т. д. наших приложений. И несказанно этому рады. Зачем изобретать колесо, да ещё и поддерживать его правильную форму, когда можно один раз написать унифицированный сценарий, который будет сам крутиться в CI/CD-системе?

Автоматизация iOS-релизов


По причине того, что Google Play более дружелюбен к разработчикам, на релиз Android-приложения уходило очень мало времени: без обновления текстов, видео и скриншотов пара минут. Отсюда и отсутствие необходимости в автоматизации. А вот с App Store проблема была очень даже осязаемой: слишком много времени уходило на отправку приложений на Review. Поэтому было решено начать автоматизацию именно с iOS.

Подобие своей системы автоматизации взаимодействия с App Store мы обдумывали (и даже сделали прототипы), но у нас не было ресурсов на допиливание и актуализацию. Также не было никакого мало-мальски адекватного API от Apple. Ну и последний гвоздь в гроб нашего кастомного решения вбили регулярные обновления App Store и его механизмов. В общем, мы решили попробовать Fastlane тогда ещё версии 2015 года.

Первым делом был написан механизм выгрузки переведённых текстов для приложений в нужную структуру как компонент нашей общей внутренней системы AIDA (Automated Interactive Deploy Assistant). Эта система своеобразный хаб, связующее звено между всеми системами, технологиями и компонентами, используемыми в Badoo. Работает она на самописной системе очередей, реализованной на Golang и MySQL. Поддерживает и совершенствует её в основном отдел Release Engineering. Подробнее о ней мы рассказывали в статье ещё в 2013 году, с тех пор многое изменилось. Обещаем рассказать про неё снова AIDA классная!

На следующем этапе выгруженные тексты скармливались Fastlane, который загружал их в App Store. После этого приходилось заходить в интерфейс App Store, вручную выбирать нужную загруженную версию и отправлять приложение на проверку, если к тому времени уже был завершён Processing.

Это сократило время подготовки релиза с пары часов до примерно 30 минут, из которых только полторы минуты надо было что-то делать руками! Остальное время ждать. Ждать окончания Processing. Механизм стал прорывом на тот момент как раз потому, что почти полностью избавил нас от ручной работы при подготовке AppStore к релизу. Под скрипт мы сделали репозиторий, к которому дали доступ людям, имеющим непосредственное отношение к релизам (проджект-менеджерам, релиз-инженерам).

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

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

  1. Нужно было идти в TeamCity за свежей сборкой, скачивать оттуда IPA-файл, загружать его в App Store через Application Manager.
  2. Потом идти в интерфейс с переводами в AIDA, смотреть, готовы ли все переводы, запускать скрипт, убеждаться, что он правильно сработал (всё-таки на тот момент Fastlane был ещё сыроват).
  3. После этого залезать в App Store и обновлять страницу с версией до того момента, пока не завершится Processing.
  4. И только после этого отправлять приложение на Review.

И так с каждым приложением. Напомню, на тот момент у нас их было восемь.

Следующим действием было решено перенести скрипт в нашу AIDA, заодно объединив и автоматизировав все шаги до момента отправки приложения: проверку на готовность переводов, сбор данных из TeamCity, оповещение, логирование и все остальные блага XXI века. Параллельно с этим мы начали загружать все собранные версии в TestFlight на этапе сборки.

TestFlight это приложение сторонних разработчиков, когда-то купленное Apple для тестирования готового приложения внешними тестировщиками практически в продакшен-окружении, то есть с push-оповещениями и вот этим всем.


AIDA молодец, будь как AIDA!

Всё это привело к сокращению времени с получаса до полутора минут на всё про всё: IPA-файл успевал пройти Processing ещё до того момента, когда команда QA-инженеров давала отмашку на запуск релиза. Тем не менее нам всё равно приходилось идти в App Store, выбирать нужную версию и отправлять её на Review.

Плюс, был нарисован простенький интерфейс: мы же все любим клац-клац.


Вот так, вкладка за вкладкой, Ctrl+C Ctrl+V...

Автоматизация Android-релизов


Далее встал вопрос об автоматизации релизов Android-приложений. Хотя этот процесс был значительно быстрее, но руками приходилось делать довольно много:

  1. Заходить в консоль Google Play, чтобы убедиться, что предыдущая версия раскатана на 100% пользователей или заморожена.
  2. Создавать новую версию релиза с обновлёнными текстами и скриншотами (при наличии).
  3. Загружать APK-файл (Android Package), загружать Mapping-файл.
  4. Идти в HockeyApp (использовался в то время для логирования крашей), загружать туда APK-файл и Mapping-файл.
  5. Идти в чат и отписываться о статусе релиза.

И так с каждым приложением.

Да, у Google Play есть свой API. Но зачем делать обёртку, следить за изменениями в протоколе, поддерживать её и плодить сущности без необходимости, если мы уже используем Fastlane для iOS-релизов? К тому же он комфортно существует на нашем сервере, варится в своём соку и вообще обновляется. А к тому времени он ещё и научился адекватно релизить Android-приложения. Звёзды сошлись!

Первым делом мы выпилили отовсюду всё старое, что было: отдельные скрипты, наброски автоматизации, старые обёртки для API это создавалось когда-то в качестве эксперимента и не представляло особой ценности. Сразу после этого мы добавили команду в AIDA, которая уже умела забирать что нужно из TeamCity, загружать что надо куда надо в HockeyApp, отправлять оповещения, логировать активность, и вообще она член команды.

Заливкой APK- и Mapping-файлов в Google Play занимался Fastlane. Надо сказать, что по проторенной тропе идти гораздо проще: реализовано это было достаточно быстро с минимальным количеством усилий.

На определённом этапе реализации автоматизации случился переход с APK-архивов на AAB (Android App Bundle). Опять же, нам повезло, что по горячим следам довольно быстро получилось всё поправить, но и развлечений добавилось в связи с этим переходом. Например, подгадил HockeyApp, который не умел использовать AAB-архивы в связи с подготовкой к самовыпиливанию. Так что для того чтобы комфортно продолжать его использовать, нужно было после сборки AAB разобрать собранный архив, доставать оттуда Mapping-файл, который полетит в HockeyApp, а из AAB нужно было отдельно собрать APK-файл и только потом загружать его в тот же HockeyApp. Звучит весело. При этом сам Google Play отлично раскладывает AAB, достаёт оттуда Mapping-файл и вставляет его куда нужно. Так что мы избавились от одного шага и добавили несколько, но от этого было никуда не деться.

Был написан интерфейс (опять же, по аналогии с iOS), который умел загружать новую версию, проверять релиз вдоль и поперёк, управлять текущим активным релизом (например, повышать rollout percentage). В таком виде мы отдали его ответственным за релизы членам команды Android QA, стали собирать фидбэк, исправлять недочёты, допиливать логику (и что там ещё бывает после релиза 1.0?).

Кстати, в дальнейшем автоматизация дала нам возможность заливать в Google Play бета-версии приложений автоматически по расписанию, что, в свою очередь, довольно сильно ускорило процесс автоматического и регрессионного тестирования.

Унификация флоу мобильных релизов


К моменту автоматизации Android-релизов Fastlane наконец-то научился отправлять версии iOS-приложений на ревью. А мы немного усовершенствовали систему проверки версий в AIDA.

Пришла пора отдать iOS-релизы на откуп команде QA-инженеров. Для этого мы решили нарисовать красивую формочку, которая бы полностью покрывала потребности, возникающие в процессе релиза iOS-приложений: давала бы возможность выбирать нужный билд в TeamCity по предопределённым параметрам, выбирать вариант загружаемых текстов, обновлять или нет опциональные поля (например, Promotional Text).

Сказано сделано. Формочка получилась очень симпатичная и полностью удовлетворяет все запросы. Более того, с её внедрением появилась возможность выбирать сразу все необходимые приложения со всеми требуемыми параметрами, так что и взаимодействие с интерфейсом свелось к минимуму. AIDA по команде присылает ссылку на build log, по которому можно отслеживать возникающие ошибки, убеждаться, что всё прошло хорошо, получать какую-то debug-информацию вроде версии загружаемого IPA-файла, версии релиза и т. д. Вот так красиво iOS-релизы и были переданы команде iOS QA.


Ну симпатично же?

Идея с формочкой понравилась нам настолько, что мы решили сделать аналогичную и для Android-релизов. Принимая во внимание то, что у нас есть приложение, полностью написанное на React Native, и что команда QA-инженеров этого приложения отвечает как за iOS-, так и за Android-релизы.

Уже используемый командой Android QA интерфейс был интегрирован с изменениями в аналогичную формочку, процесс был адаптирован под новые реалии всё было максимально приближено к процессам iOS-команды. Это дало стимул наконец набросать более-менее конечный вариант документации для обеих платформ (в процессе постоянных изменений делать это категорически не хотелось), а ещё отвязать процесс релиза от всех искусственных ограничений, которые сложились исторически и приводили к лишним телодвижениям в нестандартных ситуациях, требуя действий команды релиз-инженеров.

Вывод


Вот таким нескучным путём на протяжении примерно пяти лет (c момента, когда мы начали развивать мобильное направление, до сегодняшнего дня) мы полностью автоматизировали процессы сборки, тестирования и релиза, сделали их максимально эффективными и передали ответственность за релизы членам команды QA, которые и принимают решение о степени готовности приложения.

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

Здорово, что сейчас есть возможность автоматизации большинства рутинных действий, что инженеры могут писать код когда хотят, а любой код могут поддерживать и сторонние разработчики, не тратя драгоценное время на копание в сложных интерфейсах. Особенно трудно бывает разобраться в моментах типа Где надо поставить галочку?, когда на часах полночь, в офисе никого нет, а hotfix надо залить здесь и сейчас.

Пять лет. Почему так долго? Во-первых, мобильные релизы далеко не единственная зона ответственности нашей небольшой команды. Во-вторых, конечно же, требовалось время на развитие нового open-source-проекта Fastlane; наша система релизов развивалась вместе с ним.

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

Ваш безлимит как увеличить пропускную способность автомерджа

21.06.2021 14:12:41 | Автор: admin

Отыщи всему начало, и ты многое поймёшь (Козьма Прутков).

Меня зовут Руслан, я релиз-инженер в Badoo и Bumble. Недавно я столкнулся с необходимостью оптимизировать механизм автомерджа в мобильных проектах. Задача оказалась интересной, поэтому я решил поделиться её решением с вами. В статье я расскажу, как у нас раньше было реализовано автоматическое слияние веток Git и как потом мы увеличили пропускную способность автомерджа и сохранили надёжность процессов на прежнем высоком уровне.

Свой автомердж

Многие программисты ежедневно запускают git merge, разрешают конфликты и проверяют свои действия тестами. Кто-то автоматизирует сборки, чтобы они запускались автоматически на отдельном сервере. Но решать, какие ветки сливать, всё равно приходится человеку. Кто-то идёт дальше и добавляет автоматическое слияние изменений, получая систему непрерывной интеграции (Continuous Integration, или CI).

Например, GitHub предлагает полуручной режим, при котором пользователь с правом делать записи в репозиторий может поставить флажок Allow auto-merge (Разрешить автомердж). При соблюдении условий, заданных в настройках, ветка будет соединена с целевой веткой. Bitbucket поддерживает большую степень автоматизации, накладывая при этом существенные ограничения на модель ветвления, имена веток и на количество мерджей.

Такой автоматизации может быть достаточно для небольших проектов. Но с увеличением количества разработчиков и веток, ограничения, накладываемые сервисами, могут существенно повлиять на производительность CI. Например, раньше у нас была система мерджа, при которой основная ветка всегда находилась в стабильном состоянии благодаря последовательной стратегии слияний. Обязательным условием слияния была успешная сборка при наличии всех коммитов основной ветки в ветке разработчика. Работает эта стратегия надёжно, но у неё есть предел, определяемый временем сборки. И этого предела оказалось недостаточно. При времени сборки в 30 минут на обработку 100 слияний в день потребовалось бы более двух суток. Чтобы исключить ограничения подобного рода и получить максимальную свободу выбора стратегий мерджа и моделей ветвления, мы создали собственный автомердж.

Итак, у нас есть свой автомердж, который мы адаптируем под нужды каждой команды. Давайте рассмотрим реализацию одной из наиболее интересных схем, которую используют наши команды Android и iOS.

Термины

Main. Так я буду ссылаться на основную ветку репозитория Git. И коротко, и безопасно. =)

Сборка. Под этим будем иметь в виду сборку в TeamCity, ассоциированную с веткой Git и тикетом в трекере Jira. В ней выполняются как минимум статический анализ, компиляция и тестирование. Удачная сборка на последней ревизии ветки в сочетании со статусом тикета To Merge это однo из необходимых условий автомерджа.

Пример модели ветвления

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

На основе ветки main разработчик создаёт ветку с названием, включающим идентификатор тикета в трекере, например PRJ-k. По завершении работы над тикетом разработчик переводит его в статус Resolved. При помощи хуков, встроенных в трекер, мы запускаем для ветки тикета сборку. В определённый момент, когда изменения прошли ревью и необходимые проверки автотестами на разных уровнях, тикет получает статус To Merge, его забирает автоматика и отправляет в main.

Раз в неделю на основе main мы создаём ветку релиза release_x.y.z, запускаем на ней финальные сборки, при необходимости исправляем ошибки и наконец выкладываем результат сборки релиза в App Store или Google Play. Все фазы веток отражаются в статусах и дополнительных полях тикетов Jira. В общении с Jira помогает наш клиент REST API.

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

Первая версия: жадная стратегия

Сначала мы шли от простого и очевидного. Брали все тикеты, находящиеся в статусе To Merge, выбирали из них те, для которых есть успешные сборки, и отправляли их в main командой git merge, по одной.

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

Наличие в TeamCity актуальной успешной сборки мы проверяли при помощи метода REST API getAllBuilds примерно следующим образом (псевдокод):

haveFailed = False # Есть ли неудачные сборкиhaveActive = False # Есть ли активные сборки# Получаем сборки типа buildType для коммита commit ветки branchbuilds = teamCity.getAllBuilds(buildType, branch, commit)# Проверяем каждую сборкуfor build in builds:  # Проверяем каждую ревизию в сборке  for revision in build.revisions:    if revision.branch is branch and revision.commit is commit:      # Сборка актуальна      if build.isSuccessful:        # Сборка актуальна и успешна        return True      else if build.isRunning or build.isQueued        haveActive = True      else if build.isFailed:        haveFailed = Trueif haveFailed:  # Исключаем тикет из очереди, переоткрывая его  ticket = Jira.getTicket(branch.ticketKey)  ticket.reopen("Build Failed")  return Falseif not haveActiveBuilds:  # Нет ни активных, ни упавших, ни удачных сборок. Запускаем новую  TriggerBuild(buildType, branch)

Ревизии это коммиты, на основе которых TeamCity выполняет сборку. Они отображаются в виде 16-ричных последовательностей на вкладке Changes (Изменения) страницы сборки в веб-интерфейсе TeamCity. Благодаря ревизиям мы можем легко определить, требуется ли пересборка ветки тикета или тикет готов к слиянию.

Важно, что ревизию можно (а часто даже необходимо) указывать в запросе на добавление новой сборки в очередь в параметре lastChanges, потому что в противном случае TeamCity может выбрать устаревшую ревизию ветки при запуске сборки. Как будет показано ниже, указывать ревизию необходимо в тех случаях, если, например, логика вне TeamCity основана на поиске сборок на конкретных коммитах (наш случай).

Так как после перевода тикета в статус готовности (в нашем примере Resolved) соответствующая ветка, как правило, не меняется, то и сборка, ассоциированная с тикетом, чаще всего остаётся актуальной. Кроме того, сам факт нахождения тикета в статусе To Merge говорит о высокой вероятности того, что сборка не упала. Ведь при падении сборки мы сразу переоткрываем тикет.

На первый взгляд, дальнейшие действия кажутся очевидными: взять все готовые тикеты с актуальными сборками и соединять main с ними по одному. В первой версии автомерджа мы так и сделали.

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

Конфликты слияния

Если изменить одну и ту же строку кода в разных ветках и попытаться соединить их в main, то Git попросит разрешить конфликты слияния. Из двух вариантов нужно выбрать один и закоммитить изменения.

Это должно быть знакомо практически каждому пользователю системы контроля версий (VCS). Процессу CI, так же, как и любому пользователю VCS, нужно разрешать конфликты. Правда, делать это приходится немного вслепую, в условиях почти полного непонимания кодовой базы.

Если команда git merge завершилась с ошибкой и для всех файлов в списке git ls-files --unmerged заданы обработчики конфликтов, то для каждого такого файла мы выполняем парсинг содержимого по маркерам конфликтов <<<<<<<, ======= и >>>>>>>. Если конфликты вызваны только изменением версии приложения, то, например, выбираем последнюю версию между локальной и удалённой частями конфликта.

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

Решение следующее: нарушаем как минимум одно из необходимых условий слияния. Так как ветка ассоциирована с тикетом трекера, можно переоткрыть тикет, изменив его статус. Таким образом мы одновременно исключим тикет из автомерджа и оповестим об этом разработчика (ведь он подписан на изменения в тикете). На всякий случай мы отправляем ещё и сообщение в мессенджере.

Логические конфликты

А может ли случиться так, что, несмотря на успешность сборок пары веток в отдельности, после слияния их с main сборка на основной ветке упадёт? Практика показывает, что может. Например, если сумма a и b в каждой из двух веток не превышает 5, то это не гарантирует того, что совокупные изменения a и b в этих ветках не приведут к большей сумме.

Попробуем воспроизвести это на примере Bash-скрипта test.sh:

#!/bin/bashget_a() {    printf '%d\n' 1}get_b() {    printf '%d\n' 2}check_limit() {    local -i value="$1"    local -i limit="$2"    if (( value > limit )); then        printf >&2 '%d > %d%s\n' "$value" "$limit"        exit 1    fi}limit=5a=$(get_a)b=$(get_b)sum=$(( a + b ))check_limit "$a" "$limit"check_limit "$b" "$limit"check_limit "$sum" "$limit"printf 'OK\n'

Закоммитим его и создадим пару веток: a и b.
Пусть в первой ветке функция get_a() вернёт 3, а во второй get_b() вернёт 4:

diff --git a/test.sh b/test.shindex f118d07..39d3b53 100644--- a/test.sh+++ b/test.sh@@ -1,7 +1,7 @@ #!/bin/bash get_a() {-    printf '%d\n' 1+    printf '%d\n' 3 } get_b() {git diff main bdiff --git a/test.sh b/test.shindex f118d07..0bd80bb 100644--- a/test.sh+++ b/test.sh@@ -5,7 +5,7 @@ get_a() { }  get_b() {-    printf '%d\n' 2+    printf '%d\n' 4 }  check_limit() {

В обоих случаях сумма не превышает 5 и наш тест проходит успешно:

git checkout a && bash test.shSwitched to branch 'a'OKgit checkout b && bash test.shSwitched to branch 'b'OK

Но после слияния main с ветками тесты перестают проходить, несмотря на отсутствие явных конфликтов:

git merge a bFast-forwarding to: aTrying simple merge with bSimple merge did not work, trying automatic merge.Auto-merging test.shMerge made by the 'octopus' strategy. test.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-)bash test.sh7 > 5

Было бы проще, если бы вместо get_a() и get_b() использовались присваивания: a=1; b=2, заметит внимательный читатель и будет прав. Да, так было бы проще. Но, вероятно, именно поэтому встроенный алгоритм автомерджа Git успешно обнаружил бы конфликтную ситуацию (что не позволило бы продемонстрировать проблему логического конфликта):

git merge a Updating 4d4f90e..8b55df0Fast-forward test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-)git merge b Auto-merging test.shCONFLICT (content): Merge conflict in test.shRecorded preimage for 'test.sh'Automatic merge failed; fix conflicts and then commit the result.

Разумеется, на практике конфликты бывают менее явными. Например, разные ветки могут полагаться на API разных версий какой-нибудь библиотеки зависимости, притом что более новая версия не поддерживает обратной совместимости. Без глубоких знаний кодовой базы (читай: без разработчиков проекта) обойтись вряд ли получится. Но ведь CI как раз и нужен для решения таких проблем.

Конечно, от разрешения конфликта мы никуда не уйдём кто-то должен внести правки. Но чем раньше нам удастся обнаружить проблему, тем меньше людей будет привлечено к её решению. В идеале потребуется озадачить лишь разработчика одной из конфликтующих веток. Если таких веток две, то одна из них вполне может быть соединена с main.

Превентивные меры

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

Нужно синхронизировать ветки так, чтобы их совокупный вклад в main не приводил к падению сборки релиза. Ясно, что все готовые к слиянию ветки нужно так или иначе объединить и прогнать тесты по результату объединения. Путей решения много, давайте посмотрим, каким был наш путь.

Вторая версия: последовательная стратегия

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

Git, по идее, как раз и является средством синхронизации. Но порядок попадания веток в main и, наоборот, main в ветки определяем мы сами. Чтобы определить точно, какие из веток вызывают проблемы в main, можно попробовать отправлять их туда по одной. Тогда можно выстроить их в очередь, а порядок организовать на основе времени попадания тикета в статус To Merge в стиле первый пришёл первым обслужен.

С порядком определились. А как дальше соединять ветки? Допустим, мы сольём в main первый тикет из очереди. Так как main изменилась, она может конфликтовать с остальными тикетами в очереди. Поэтому перед тем как сливать следующий тикет, нужно удостовериться, что обновлённая main по-прежнему совместима с ним. Для этого достаточно слить main в тикет. Но так как после соединения main с веткой её состояние отличается от того, которое было в сборке, необходимо перезапустить сборку. Для сохранения порядка все остальные тикеты в очереди должны ждать завершения сборки и обработки впередистоящих тикетов. Примерно такие рассуждения привели нас к последовательной стратегии автомерджа.

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

Но есть у этой схемы существенный недостаток: пропускная способность автомерджа линейно зависит от времени сборки. При среднем времени сборки iOS-приложения в 25 минут мы можем рассчитывать на прохождение максимум 57 тикетов в сутки. В случае же с Android-приложением требуется примерно 45 минут, что ограничивает автомердж 32 тикетами в сутки, а это даже меньше количества Android-разработчиков в нашей компании.

На практике время ожидания тикета в статусе To Merge составляло в среднем 2 часа 40 минут со всплесками, доходящими до 10 часов! Необходимость оптимизации стала очевидной. Нужно было увеличить скорость слияний, сохранив при этом стабильность последовательной стратегии.

Финальная версия: сочетание последовательной и жадной стратегий

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

Давайте вспомним идею жадной стратегии: мы сливали все ветки готовых тикетов в main. Основной проблемой было отсутствие синхронизации между ветками. Решив её, мы получим быстрый и надёжный автомердж!

Раз нужно оценить общий вклад всех тикетов в статусе To Merge в main, то почему бы не слить все ветки в некоторую промежуточную ветку Main Candidate (MC) и не запустить сборку на ней? Если сборка окажется успешной, то можно смело сливать MC в main. В противном случае придётся исключать часть тикетов из MC и запускать сборку заново.

Как понять, какие тикеты исключить? Допустим, у нас n тикетов. На практике причиной падения сборки чаще всего является один тикет. Где он находится, мы не знаем все позиции от 1 до n являются равноценными. Поэтому для поиска проблемного тикета мы делим n пополам.

Так как место тикета в очереди определяется временем его попадания в статус To Merge, имеет смысл брать ту половину, в которой расположены тикеты с большим временем ожидания.

Следуя этому алгоритму, для k проблемных тикетов в худшем случае нам придётся выполнить O(k*log2(n)) сборок, прежде чем мы обработаем все проблемные тикеты и получим удачную сборку на оставшихся.

Вероятность благоприятного исхода велика. А ещё в то время, пока сборки на ветке MC падают, мы можем продолжать работу при помощи последовательного алгоритма!

Итак, у нас есть две автономные модели автомерджа: последовательная (назовём её Sequential Merge, или SM) и жадная (назовём её Greedy Merge, или GM). Чтобы получить пользу от обеих, нужно дать им возможность работать параллельно. А параллельные процессы требуют синхронизации, которой можно добиться либо средствами межпроцессного взаимодействия, либо неблокирующей синхронизацией, либо сочетанием этих двух методов. Во всяком случае, мне другие методы неизвестны.

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

Остаётся предотвратить все возможные случаи состояний гонки. Их много, но для понимания сути приведу несколько самых важных:

  1. SM-SM и GM-GM: между командами одного типа.

  2. SM-GM: между SM и GM в рамках одного репозитория.

Первая проблема легко решается при помощи мьютекса по токену, включающему в себя имя команды и название репозитория. Пример: lock_${command}_${repository}.

Поясню, в чём заключается сложность второго случая. Если SM и GM будут действовать несогласованно, то может случиться так, что SM соединит main с первым тикетом из очереди, а GM этого тикета не заметит, то есть соберёт все остальные тикеты без учёта первого. Например, если SM переведёт тикет в статус In Master, а GM будет всегда выбирать тикеты по статусу To Merge, то GM может никогда не обработать тикета, соединённого SM. При этом тот самый первый тикет может конфликтовать как минимум с одним из других.

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

Таким образом, мы получили своего рода неблокирующую синхронизацию.

Немного о TeamCity

В процессе реализации GM нам предстояло обработать много нюансов, которыми я не хочу перегружать статью. Но один из них заслуживает внимания. В ходе разработки я столкнулся с проблемой зацикливания команды GM: процесс постоянно пересобирал ветку MC и создавал новую сборку в TeamCity. Проблема оказалась в том, что TeamCity не успел скачать обновления репозитория, в которых была ветка MC, созданная процессом GM несколько секунд назад. К слову, интервал обновления репозитория в TeamCity у нас составляет примерно 30 секунд.

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

Кто-то посчитает решение очевидным, но я нашёл его не сразу. Оказывается, прикрепить ревизию к сборке при её добавлении в очередь можно при помощи параметра lastChanges метода addBuildToQueue:

<lastChanges>  <change    locator="version:{{revision}},buildType:(id:{{build_type}})"/></lastChanges>

В этом примере {{revision}} заменяется на 16-ричную последовательность коммита, а {{build_type}} на идентификатор конфигурации сборки. Но этого недостаточно, так как TeamCity, не имея информации о новом коммите, может отказать нам в запросе.

Для того чтобы новый коммит дошёл до TeamCity, нужно либо подождать примерно столько, сколько указано в настройках конфигурации корня VCS, либо попросить TeamCity проверить наличие изменений в репозитории (Pending Changes) при помощи метода requestPendingChangesCheck, а затем подождать, пока TeamCity скачает изменения, содержащие наш коммит. Проверка такого рода выполняется посредством метода getChange, где в changeLocator нужно передать как минимум сам коммит в качестве параметра локатора version. Кстати, на момент написания статьи (и кода) на странице ChangeLocator в официальной документации описание параметра version отсутствовало. Быть может, поэтому я не сразу узнал о его существовании и о том, что это 40-символьный 16-ричный хеш коммита.

Псевдокод:

teamCity.requestPendingChanges(buildType)attempt = 1while attempt <= 20:  response = teamCity.getChange(commit, buildType)  if response.commit == commit:    return True # Дождались  sleep(10)return False

О предельно высокой скорости слияний

У жадной стратегии есть недостаток на поиск ветки с ошибкой может потребоваться много времени. Например, 6 сборок для 20 тикетов у нас может занять около трёх часов. Можно ли устранить этот недостаток?

Допустим, в очереди находится 10 тикетов, среди которых только 6-й приводит к падению сборки.

Согласно жадной стратегии, мы пробуем собрать сразу все 10 тикетов, что приводит к падению сборки. Далее собираем левую половину (с 1 по 5) успешно, так как тикет с ошибкой остался в правой половине.

Если бы мы сразу запустили сборку на левой половине очереди, то не потеряли бы времени. А если бы проблемным оказался не 6-й тикет, а 4-й, то было бы выгодно запустить сборку на четверти длины всей очереди, то есть на тикетах с 1 по 3, например.

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

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

Примерно такой же алгоритм реализован в премиум-функции GitLab под названием Merge Trains. Перевода этого названия на русский язык я не нашёл, поэтому назову его Поезда слияний. Поезд представляет собой очередь запросов на слияние с основной веткой (merge requests). Для каждого такого запроса выполняется слияние изменений ветки самого запроса с изменениями всех запросов, расположенных перед ним (то есть запросов, добавленных в поезд ранее). Например, для трёх запросов на слияние A, B и С GitLab создаёт следующие сборки:

  1. Изменения из А, соединённые с основной веткой.

  2. Изменения из A и B, соединённые с основной веткой.

  3. Изменения из A, B и C, соединённые с основной веткой.

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

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

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

Но если преград человеческой мысли нет, то пределы аппаратных ресурсов видны достаточно отчётливо:

  1. Каждой сборке нужен свой агент в TeamCity.

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

  3. Сборки автомерджа мобильных приложений в main составляют лишь малую часть от общего количества сборок в TeamCity.

Взвесив все плюсы и минусы, мы решили пока остановиться на алгоритме SM + GM. При текущей скорости роста очереди тикетов алгоритм показывает хорошие результаты. Если в будущем заметим возможные проблемы с пропускной способностью, то, вероятно, пойдём в сторону Merge Trains и добавим пару параллельных сборок GM:

  1. Вся очередь.

  2. Левая половина очереди.

  3. Левая четверть очереди.

Что в итоге получилось

В результате применения комбинированной стратегии автомерджа нам удалось добиться следующего:

  • уменьшение среднего размера очереди в 2-3 раза;

  • уменьшение среднего времени ожидания в 4-5 раз;

  • мердж порядка 50 веток в день в каждом из упомянутых проектов;

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

Примеры графиков слияний за несколько дней:

Количество тикетов в очереди до и после внедрения нового алгоритма:

Среднее количество тикетов в очереди (AVG) уменьшилось в 2,5 раза (3,95/1,55).

Время ожидания тикетов в минутах:

Среднее время ожидания (AVG) уменьшилось в 4,4 раза (155,5/35,07).

Подробнее..

Нюансы использования TeamCity

20.12.2020 12:12:18 | Автор: admin

Картинка


Всем привет.


Статья написана в простом стиле "DevOps для домохозяек" от таких же домохозяек. В ней будет описано с какими неожиданностями можно столкнуться при настройке проекта в TeamCity. Также приведу рекомендации как эти проблемы можно обойти.


Нижеописанное основано на моём двухлетнем опыте настройке TeamCity сборок, чтению баг репортов и обмене мнений с коллегами по цеху. Не претендую на истину в последней инстанции, так как в работе в основном использовался подход SDD (Stackoverflow Driven Development).


Небольшая справка:


  • TeamCity CI (Continous Integration) инструмент. "Аналог" Gitlab CI, Github Actions с прицелом на возможность полной настройки автоматизации из графического интерфейса.
  • Проект (Project) агрегирующая сущность, в неё связываются несколько сборок. В TeamCity древовидная структура проект->подпроект->сборка с частичным наследованием настроек.
  • Сборка (Build) Атомарная сущность автоматизации. Примеры сборок "Запуск автотестов", "Установка конфигурации", "Сборка дистрибутива". Каждая сборка состоит из нескольких шагов.
  • Шаг (Build step) Описание "а что нужно делать" используя разные "runner type". Это может быть как простой Bash скрипт, так и запуск Docker контейнера.

Вкратце по проекту TeamCity с которым я работаю:


  • ~30 сборок, шаги сборок состоят из вызовов Bash, Ansible и Python.
  • Никаких сборок Android приложений, Web проектов, Docker, k8s и прочего. Просто заказ облачных серверов, подъем базы данных из дампа, установка программ и конфигурации.
  • Настройка ведётся из графического интерфейса, без Kotlin DSL (переход на него в планах).

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


1 Нельзя изменить параметры сборки при запуске по триггеру


Сборки можно запускать по триггеру (внешнему событию). Триггеры могут быть разные: была завершена другая сборка, обновилась git ветка, cron задача. При этом в сборке можно задать параметры по умолчанию.


Так вот: запуск по триггеру можно сделать только с параметрами по умолчанию. На эту проблему есть нерешённая задача 2008 года.


Опишу простой пример, на котором можно понять масштаб проблемы: у нас есть сборка по поднятию стенда и сборка по запуску автотестов на стенде. Вторая сборка получает на вход имя стенда и прогоняет автотесты. Первую сборку нужно запустить 1 раз, чтобы получить 1 стенд. Вторую сборку нужно запускать часто, чтобы понимать в каком состоянии стенд. Ииии такую простую штуку нельзя сделать.


Но мне возразят, что нужно использовать build chain (цепочку сборок). Окей, давайте посмотрим что там не так.


2 Build chain or not просто скопируй ещё сборки


В случае вышеописанного кейса (заказ стенда + прогон автотестов) мы настраиваем build chain. В таком случае если нам нужен стенд, то мы его получаем из сборки запуска автотестов. Интуитивно понятно, не правда ли?


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


Но как тогда брать имя стенда, на котором запускать автотесты? Если у нас две сборки отдельных, то мы указываем имя в параметрах. Если цепочка сборок то проще всего использовать артефакт. А теперь если мы желаем запускать и отдельно, и вместе, то нужен специальный огород, который будет это всё обрабатывать. Осталось только представить, как будет "интересно" настраивать такой огород, когда у нас появится две разные сборки на заказ стенда (заказ идёт в разных облаках, разные программы), а сборка с запуском автотестов будет одна.


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


3 Переопределение параметров зависимой сборки


Рассмотрим другой пример: у нас есть три зависимые последовательные сборки, которые связаны в build chain. Скажем заказ в облаке стенда, установка дампа базы данных и установка программ. Мы "интуитивно" запускаем сборку по установке программ и возникает вопрос: а как пробросить размер заказываемой машины в зависимой сборке? И тут нам на помощь приходит переопределение параметров.


Теперь у нас в одном диалоговом окне по запуску сборке возникает 3*N параметров, которые никак не отличаются друг от друга. Ещё стоит учитывать, что описание и параметры по умолчанию таких параметров не копируются, их нужно копировать отдельно. Особенно это "радует", когда эти описания и значения меняются. Их тогда нужно будет обновлять в N местах, если сборки можно вызывать из разных мест как в нашем случае. Например, нужен человеку только стенд с дампом базы данных, он тогда вправе заказать его со второй сборки. А там в параметрах версия дампа устаревшая, в отличие от последней сборки цепочки, и пойдёт разбор полётов на полдня почему дамп кривой.


И конечно тут не будет никакой валидации, что вы не ошиблись в имени переопределяемого параметра, всё в лучших традициях YAML Developer'ов.


4 Конфигурация сборки может быть только одна


Проблема не относится к Kotlin DSL (у меня нет опыта его использования, не могу сказать насколько эта проблема действительно решается). Если мы настраиваем сборки в графическом интерфейсе как настоящие домохозяйки, то сталкиваемся со следующей проблемой: а как плавно поменять настройки проекта, так чтобы это не коснулось пользователей?


Первый и самый простой вариант: объявить технологические работы и править "на горячую". Второй вариант: скопировать сборку в отдельное место и делать изменения в ней (наш вариант).


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


Рекомендация: не архивируйте/удаляйте старые сборки, а обновляете их после внесения изменений в основную ветку кода. Всегда найдутся люди, у которых ссылки на сборки хранятся где-то в закладках и если вы поменяется ID сборки, то все ссылки побьются.


5 TeamCity API


Я думаю многие здесь знакомы с "главными" 4 метриками DevOps. И TeamCity кажется идеальным местом, чтобы собрать всю информацию хотя бы о половине из них ("Deployment Frequency" и "Lead Time for Changes").


Иии вот нельзя в API быстро узнать процент упавших сборок, частоту запуска и причины падения. Да, там есть какие-то дашборды, но информация в них именно та, которая не нужна. То есть вот начали у нас падать все сборки на основной ветке, и мы можем только вручную "Assign investigation", либо придумывать как реализовывать такой инструмент в каждой сборке. Не очень удобно.


Однако, в чём хорош API так это в формировании build chain из-за вышеперечисленных недостатков с "нативным" способом. В таком случае можно сборки создавать для независимого запуска. А их связывание делать в отдельной сборке с запуском тупого Python скрипта. И много кто так обходит проблему.


6 При запуске Bash скрипта проверяется только результат последней команды


Есть у нас простой скрипт, написанный в интерфейсе:


./command_1.sh # always faills # always success

В таком случае этот шаг сборки будет всегда зелёный. Но если мы добавим мантру:


./command_1.sh # always failif [ $? -ne 0 ]; then  echo "##teamcity[buildProblem description='Build failed']"fils # always success

И тогда уже шаг будет красным, и сборка дальше не пойдёт (тут уже как выставлен "Execute step"). Иными словами, всегда нужно учитывать особенность работы Bash скриптов.


7 Работа с шифрованными параметрами сборки


В TeamCity можно добавить параметры-секреты, такие как пароли и API ключи. Чтобы такие данные не утекли, в логах сборки ищутся значения таких параметров и заменяются на символы *. И возникает следующий нюанс: а если нам эти параметры нужно записать в другой файл. Команда echo в таком случае не сработает фильтр перехватит. В итоге мы пришли к следующему варианту:


cat > constants.json <<- EOM{    "key": "%value%"}EOM

Задача, в которой это понадобилась, была следующая. Есть Python скрипт, который выполняет запросы к нескольким системам, и вся его конфигурация сохранена в JSON. Как к нему подать секреты? Можно через командную строку, но тогда у нас растекается конфигурация сразу по четырём местам: JSON, командная строка, значение по умолчанию в скрипте и ещё значение по умолчанию в параметре TeamCity. Поскольку скрипт должен быть тупым и одноразовым, то решили максимально упростить: всю конфигурацию втащить в JSON. JSON прям с вписанными именами параметров TeamCity сохраняем в репозиторий и копируем как есть в шаг сборки. В итоге мы сразу формируем нужный JSON и не разбираемся какой параметр, где нужен.


8 Скорость прогрузки графического интерфейса


Это будет очень субъективный пункт. Поскольку основная работа по настройке проводится в интерфейсе (для тех кто не пользуется связкой Kotlin DSL + TeamCity API), то производительность работы пропорционально скорости работы этого интерфейса. А он ооочень медленный. Я постарался в меру своих интеллектуальных способностей замерить скорость прогрузки и вот какие цифры получил (был использован браузер Firefox и инструмент Network).


  • Загрузка окно проекта со всеми сборками и вызов окна запуска сборки
    • load: 9.87 s
    • DOMContentLoaded: 4.92 s
    • Finish: 34.39 s
    • Size/transferred size of all requests: 10.69 MB / 2.42 MB
    • Requests: 345
  • Загрузка окна отдельной сборки и вызов окна запуска сборки
    • load: 4.59 s
    • DOMContentLoaded: 1.27 s
    • Finish: 27.42 s
    • Size/transferred size of all requests: 11.53 MB / 2.23 MB
    • Requests: 120

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


9 Информация по упавшей сборке


Это будет ещё один субъективный пункт. Для каждой сборки есть вкладка Overview. В ней в случае падения сборки указывается ошибка. И эта ошибка в 99% случаев определяется неправильно. В нашем случае (может у кого по-другому) ошибка определяется как "первое, что попало в stderr", хотя было бы логичнее сделать "последнее, что попало в stderr". В случае Ansible это всегда будет какой-нибудь "WARNING: Deprecation setting...". И в итоге это вызывает стабильный поток вопросов у людей слабо знакомых с TeamCity. Пусть лучше вообще там ничего не писалось.


10 Вечные проблемы с агентами


Здесь я бы хотел написать про все проблемы, которые связанные с агентами сборки (Build agents). Как известно в TeamCity есть master сервер, который выполняет роль планировщика, а сами сборки запускаются на отдельных серверах. И нужно уметь правильно с этими агентами работать (обслуживать), TeamCity тут мало что может сделать.


Первая проблема удаление агентов. У агентов чаще всего есть какой-то срок жизни, их лучше не делать долгоживущими, иначе потом появляется дрейф конфигурации. Например, кто-то поменял JAVA_HOME и понеслось. И вот это удаление может прилетать вообще неожиданно. Планировщик выдал агента для тяжеловесной ночной десятичасовой сборки. Но ночью чаще всего проводятся все работы по обслуживанию и какой-то сервис, не проверив что там с агентом просто его грохает. Мы так и не победили эту проблему (администрированием TeamCity занимается отдельная "команда").


Вторая проблема закончившееся место на агенте, либо неработающая базовая утилита. Пересекается с первым пунктом, но тут "веселее". Запускаем сборку, на самом первом шаге это всё падает и агент снова бодро готов браться за новую работу. Мы раздражённо запускаем сборку заново и по великому везению попадается тот самый агент и по новой. "Но у нас же есть великая настройка по выбору агента!" скажете вы и будете правы. Только один нюанс: а если у нас build chain? Тутуту руру тутуту, а оказывается, что выбрать агент можно только на текущую сборку, на зависимые может браться какой угодно и мы знаем какой будет выбран по закону подлости. Но это в случае, если вы не выставили при установке зависимой сборки "Run build on the same agent". Но вы же выставили, правда?


Третья проблема дрейф конфигурации на агентах. На агентах должно быть неизменяемое окружение (то, что не должно быть root прав, я думаю объяснять не надо). Кто-то поменял переменную окружения, поменял локальные пути до утилит и понеслось. Начинаешь после каждого такого случая перестраховываться и потом у тебя 90% сборки это подготовка агента к твоему print("Hello, World!").


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

Подробнее..
Категории: Системы сборки , Ci , Devops , Teamcity

XUnit тестирование в TeamCity

15.01.2021 14:04:54 | Автор: admin

Microsoft активно развивает свои проекты с открытым кодом, например, ASP.NET Core или MSBuild. Вместе с этим набирает популярность и тестовый фреймворк xUnit, используемый в них для модульного тестирования. В этой статье мы рассмотрим несколько способов запуска xUnit-тестов для непрерывной интеграции проекта средствами TeamCity.


Примеры конфигураций сборки можно найти на этом демо-сервере TeamCity, а исходный код лежит в этом репозитории: Lib это код тестируемого приложения, а Lib.Tests проект с тестами. Оба этих проекта нацелены на .NET версий net472 и netcoreapp2.1.


Для поддержки xUnit, в тестовом проекте задана NuGet-зависимость на соответствующий пакет xunit:


<PackageReference Include="xunit"/>


Этот мета-пакет не содержит бинарных файлов, а добавляет несколько зависимостей на NuGet-пакеты xunit.core, xunit.assert и xunit.analyzers. Это тестовое API xUnit. Каждый тестовый метод в xUnit помечается атрибутом [Fact] для обычных тестов или [Theory] для параметризованных тестов. Обычно, каждому тестируемому модулю соответствует свой тестовый класс с набором тестовых методов, проверяющих ту или иную логику. Каждому тестируемому проекту соответствует свой тестовый проект.


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


xUnit console runner


В простейшем случае, выполнить тесты можно утилитой xunit.console, из пакета xunit.runner.console. Формат запуска тестов типичен для подобных инструментов: набор тестовых сборок, фильтры для тестов, формат отчета и прочее. Кажущаяся простота использования этого подхода скрывает несколько нюансов:


  1. Где взять xunit.console на агенте TeamCity, чтобы потом использовать его для запуска тестов?
  2. Какую версию xunit.console выбрать? Пакет xunit.runner.console содержит набор исполняемых файлов для разных версий .NET.
  3. Как быть, если нужно выполнить тесты в нескольких сборках одного тестового проекта, созданных для разных версий .NET?
  4. Как настроить сбор статистики покрытия кода? Эта статистика, конечно, не может полностью отражать качество модульного тестирования, но она может быть полезной для обнаружения кода, непокрытого тестами.
  5. Какие параметры использовать для тестовой утилиты и для сбора статистики покрытия кода?
  6. Как передать результаты тестов и статистику покрытия в TeamCity?

Рассмотрим пример конфигурации сборки TeamCity, содержащей 5 шагов, в каждом из которых мы используем ранер .NET:


image


Первым шагом решаем вопрос (1): Где взять xunit.console?:


image


Этот шаг использует команду .NET, чтобы добавить зависимость на пакет xunit.runner.console в тестовый проект Lib.Tests. При восстановлении зависимости на шаге 2 утилита xunit.console появится на агенте TeamCity. Если есть несколько тестовых проектов, то зависимость можно будет добавить только в один. Но как определить точный путь к xunit.console после его загрузки? Если ничего не предпринять, пакет будет загружен в стандартную директорию кэша NuGet-пакетов:

  • в Windows: %userprofile%\.nuget\packages
  • на Mac/Linux: ~/.nuget/packages

Эти пути известны, но они зависят от операционной системы, от аккаунта, под которым запущен агент TeamCity, и от персональных настроек среды окружения для этого аккаунта. Условия могут меняться от агента к агенту. Чтобы быть уверенным, по какому пути найдется xunit.console, лучше задать переменную среды окружения NUGET_PACKAGES со значением %teamcity.build.checkoutDir%/packages. Эта переменная определяет, где появятся NuGet-пакеты после восстановления зависимостей на следующем шаге сборки. В этом примере она указывает на произвольную директорию packages, относительно корневой директории проекта. Вот как это выглядит на странице редактирования параметров:


image


Благодаря этой переменной окружения, путь к xunit.console больше не зависит от внешних факторов. Следующий шаг довольно прост. Он строит решение (solution), восстанавливая зависимости:


image


После его выполнения, в директорию packages добавятся NuGet-пакеты всех зависимостей, включая xunit.runner.console, а в директорию Lib.Tests/bin/Debug тестовые сборки, соответствующие целевым версиям .NET. И если версия тестовой сборки в директории Lib.Tests/bin/Debug/net472 уже готова для выполнения тестов, то директория Lib.Tests/bin/Debug/netcoreapp2.1 для .NET CoreApp 2.1 не содержит всех требуемых бинарных зависимостей. Вместо этого, в ней присутствуют _JSON-_файлы с описанием того, где найти эти бинарные зависимости. Шаг 3 собирает всё вместе для приложений .NET CoreApp 2.1:


image


После выполнения этого шага можно подвести промежуточный итог: тесты готовы к запуску. Тестовые сборки со всеми зависимостями находятся в директориях:


  • Lib.Tests/bin/Debug/net472
  • Lib.Tests/bin/Debug/netcoreapp2.1/publish

Необходимые для запуска тестов утилиты xunit.console соответственно находятся в:


  • packages/xunit.runner.console/**/net472/xunit.console.exe
  • packages/xunit.runner.console/**/netcoreapp1.0/xunit.console.dll

где ** версия пакета xunit.runner.console.


Вопросы (1) и (2) решены. Для решения вопроса (3) необходимо добавить два шага, выполняющих тесты для двух версии .NET. Потенциально, количество целевых версий тестовых проектов .NET может быть довольно большим, поэтому и шагов тестирования с похожим набором параметров тоже может быть много. Эту проблему можно решить, например, с помощью PowerShell-скрипта или TeamCity Kotlin DSL. С вопросами (4) и (5), в общем случае, приходится разбираться самостоятельно, но, использовав команду .NET, мы получим следующие преимущества:

  • статистику покрытия кода с передачей параметров, кроссплатформенностью и всеми отчетами
  • автоматический запуск xunit.console.dll и _xunit.console.exe _подходящим способом, в зависимости от выбранного окружения (ОС, Docker, и т.д.)

Следующие два шага выполняют тесты командой .NET:

image


image


Открытым остался последний вопрос (6): Как передать результаты тестов TeamCity?. xunit.console делает это самостоятельно, полагаясь на переменную среды окружения _TEAMCITY_PROJECTNAME, которую агент TeamCity автоматически добавляет ко всем порожденным процессам. xunit.console передает результаты тестов, используя TeamCity service messages.


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


Meta-Runners Power Pack


Пакет TeamCity мета-ранеров Power Pack содержит мета-ранер xUnit.net-dotCover, который упрощает запуск xUnit-тестов и сбор статистики покрытия кода. Пример конфигурации сборки с его использованием содержит всего два шага:


image


Здесь первый шаг идентичен шагу (2) из предыдущего подхода. Второй шаг, на основе мета-ранера, запускает тесты и выглядит внушительно:


image


Этот шаг получает xunit.console из того же NuGet-пакета xunit.runner.console и запускает тесты сборок только для полных версий .NET Framework (в нашем случае .NET Framework 4.72), попутно собирая статистику покрытия кода. Он заменяет 2 шага скачивания xunit.console и запуска тестов по сравнению с предыдущим подходом.


Недостатки мета-ранера xUnit.net-dotCover:


  • Не может запускать тесты в тестовых проектах, собранных для .NET Core и .NET 5+.
  • Пользовательский интерфейс для передачи параметров dotCover не очень нагляден.
  • Нужно самостоятельно выбирать версию xunit.console в поле Xunit Runner Executable.

Очевидно, что мета-ранер не подходит для нашего случая, но, тем не менее, является рабочим решением для тестовых проектов, нацеленных на полные версии .NET Framework.


dotnet test


.NET Runner с командой test является самым простым, надежным и мощным способом тестировать .NET код в TeamCity. Наша задача решается всего лишь одним простым шагом конфигурации:


image


Такой подход имеет следующие преимущества:


  • Он не зависит от фреймворков тестирования: xUnit, NUint и других. Можно использовать и несколько одновременно.


  • Тесты могут выполняться для всех тестовых сборок решения или нескольких решений, для одного или нескольких проектов.


  • Можно запускать тесты для определенной версии .NET или для набора версий в многоцелевых проектах с использованием элемента TargetFrameworks, включая Full .NET Framework, .NET Core и .NET 5+.


  • Поддерживается тестирование в Docker-контейнерах.


  • Кросс-платформенный сбор статистики покрытия кода.



Если тестовый проект создан в средах разработки Visual Studio или Rider или с использованием шаблонов из командной строки dotnet new, например, dotnet new xunit -o Lib.Tests, то ничего дополнительного делать не нужно. Если же тестовый проект создается в "блокноте", то, помимо зависимости xunit, дополнительно нужно добавить зависимость на пакет Microsoft.NET.Test.Sdk и на тестовый адаптер xunit.runner.visualstudio:


<PackageReference Include="Microsoft.NET.Test.Sdk"/>


<PackageReference Include="xunit.runner.visualstudio"/>


Пакет Microsoft.NET.Test.Sdk содержит набор свойств и скриптов MSBuild, которые делают проект тестовым, а тестовый адаптер отвечает за интеграцию определенного тестового фреймворка: в нашем случае xunit.runner.visualstudio, с Visual Studio Test Platform. Другие фреймворки также имеют свои адаптеры, например, NUnit NUnit3TestAdapter, а MSTest MSTest.TestAdapter.


Мы рекомендуем использовать именно этот подход для тестирования вместе с xUnit и другими тестовыми фреймворками.


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

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru