28 октября

Поддерживаем разработку нескольких версий продукта в Git. Станислав Лукьянов (GridGain)

GitСистемы управления версиямиIT-стандартыУправление разработкойDevOps


Всем привет! Меня зовут Станислав Лукьянов. Я работаю в компании GridGain. Сегодня я хотел поговорить о том, как мы поддерживаем старые версии в Git.



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



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


Эта тема будет полезна:


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

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



Продукт GridGain – это распределенная СУБД и платформа для обработки данных. Она основана на продукте, который называется Apache Ignite, который open source. Все, что есть в Ignite, на слайде выделено красным. И GridGain добавляет вокруг какое-то количество интерпрайзных фич, которые тут по бокам.


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



Кто этим пользуется? Пользуются этим вот эти ребята. Тут много всяких компаний. В основном это финтех, банки и те, кто их обслуживают.


И все, что их объединяет, это то, что это все жесткий и кровавый enterprise. В чем проблема с enterprise?



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



Он хочет обновляться как можно реже и затягивать как можно меньше патчей.



Из-за этого нам приходится поддерживать много старых версий. Много – это сколько?


Вот такая у нас release model:



Тут я вывел версии, которые мы выпустили в наших активных ветках за последний год. Мы выпускаем minor-версии примерно раз в квартал. Поддерживаем их два года. Это означает, что мы поддерживаем порядка 8 minor-веток одновременно. В них мы выпускаем патчи. В среднем они выпускаются по одному в месяц на каждую minor-версию.


Если посмотреть на слайд, то получится, что у нас 5-10 версий выходят ежемесячно. Это довольно много.



В чем же проблема? В чем сложность, чтобы поддерживать все старые ветки?


Давайте пойдем наивно и попробуем сделать все нормально.



Есть у нас мастер, мы в него коммитим.



В какой-то момент захотели выпустить версию.



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



Выпустили так еще одну.



И стали готовиться к следующей.



Пока версия была в QA, произошло следующее.



Сначала мы запилили еще что-то, т. е. какой-то фикс или фичу, которая попала нам в мастер, но которую мы не хотим в версии 1.3. А потом QA нашел в 1.3 какой-то баг, который мы тоже пофиксили.



Вопрос: «Как теперь протащить B в 1.3?».


Самое очевидное решение – это переставить тэг 1.3.



Кто видит в этом проблему? Естественно, все понимают, что такое scope creep. Мы переставили тэг. И у нас в 1.3 еще попал коммит А. Очевидно, что решение плохое.


Что мы еще можем сделать?



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



Естественно, все знают в 2019-ом году, что мы хотим сделать branch под 1.3 и в отдельном branch проводить стабилизацию, т. е. там зафиксить B.


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



И как это сделать просто, и как дотащить B и туда, и туда, может быть, неочевидно.


Немножко усложним пример.



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



Выпустили следующую, отбранчевались от нулевой.



Потом приготовились выпускать вторую.



И снова та же ситуация.



Есть коммит А, который мы не хотим в 1.3.2. Но скажем, что мы хотим его когда-нибудь протащить в ветку 1.3, но не сейчас, т. е. не в 1.3.2.


Очевидное решение – это сразу сделать ветку 1.3.3. И туда все протащим, и все будет хорошо.



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



И теперь нам нужно ее протащить и в 1.3.2, и в 1.3.3.



И проблема здесь в том, что разработчик принимает очень много решений на основе того, что он знает о версиях, которые выпускаются. Он должен знать, что есть 1.3.2. Эта версия в QA и в нее надо тащить не все подряд. Есть 1.3.3., в которую надо тащить все подряд, что должно оказаться в ветке 1.3. Это довольно много информации.


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



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



Сформулируем проблемы, на которые мы посмотрели:


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


Из этого формулируем свои требования:


  1. Одновременные релизы не должны мешать друг другу и не должны мешать текущей разработке. Если нам надо закоммитить, то мы должны иметь возможность что-то закоммитить. Если нам надо выпускать релиз, вне зависимости оттого, что там еще мы выпускаем, мы имеем возможность забросить релиз в QA на стабилизацию.
  2. Мы недолжны терять консистентности между релизами. Если в старых релизах мы что-то пофиксили, то во всех новых это тоже должно быть пофиксено, потому что пользователь не должен видеть багов, которые для него уже один раз пофиксили.
  3. И все это должно масштабироваться:

  • В ширину – на большое количество версий, которые мы захотим поддерживать 2-3-4 года.
  • В глубину – часто мы хотим выпускать патчи для патчей. Мы выпускаем версии с тремя-четырьмя-пятью цифрами. У нас есть ветки, которые живут какое-то время, в которых у нас по пять цифр. И это бывает нужно.


Как построим разговор дальше?


  • Начнем с существующих подходов к работе с Git. Рассмотрим парочку популярных и посмотрим, как они работают с версиями.
  • Потом посмотрим на то, как мы строили собственный подход, какие есть развилки.
  • И расскажу о паре проблем, которые мы все еще решаем.


Начнем с Github Flow.



Кто знает, что такое Github Flow? Кто не знает, вы на самом деле знаете, просто не понимали, что пользовались Github Flow.


Это самый простой подход к работе к Git, который может быть.



У вас есть мастер. Когда вы хотите что-то пофиксить, вы делаете feature branch, либо bugfix branch.



Когда закончили, то через pull request при integration testing review вмерживаете все в мастер.



Требования, которые выделяет Github Flow – это: мастер должен быть постоянно стабилен и готов к deploy. Если у вас современный CI/CD, то, скорее всего, на каждый коммит в мастер, на каждый merge либо вы делаете deploy в production, либо принципиально можете сделать deploy в production, т. е. никакой стабилизации нет.



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



Как Github Flow справляется с требованиями, про которые мы говорили?


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

Github Flow нам про версии вообще ничего не говорит и каких-то инструментов, чтобы поддерживать старые версии нам не дает.


Понятно, что это основной способ, как мы все работаем с Git. Фиксы в feature branch и merge в мастер – с этого будем начинать. А снаружи будем накручивать все, что связано с выпуском версий.



Git Flow – еще один очень популярный процесс. Он поддерживает две главных ветки, помимо мастера есть еще ветка develop.


Мастер – это также постоянно стабильная штука, которую можно с любого места деплоить.


Develop – это нестабильная ветка, в которой аккумулируются изменения.


Feature branches создаются из develop.



Потом вмерживаются в него обратно.



Когда мы готовимся сделать релиз, мы делаем release branch, в нем проводим стабилизацию.



Когда стабилизация релиза закончилась, мы вмерживаем его в мастер и обратно в develop, т. е. делаем сразу два merge.



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


Кроме обычных релизов, есть еще понятие hotfix релизов.



Отличие от обычных в том, что branch делается не от develop, а от головы мастера. Делаем такой hotfix, а также делаем там стабилизацию. И после этого также вмерживаем в мастер и в develop.



Вот и весь процесс.



Как он справляется с нашими требованиями?


  1. Релизы не мешают изменениям, потому что мы используем release branches. Мы этому научились в самом начале.
  2. Сохраняется ли консистентность между ветками, которые мы поддерживаем? И да, и нет.

  • С одной стороны, мы вмерживаем релизные branches, hotfix branches обратно в develop и в мастер. И это хорошо. Это значит, что их изменения мы не потеряем, по крайней мере, в develop.
  • Но если у вас есть несколько одновременно живущих release branches или вы одновременно готовите hot fix и релиз, то процесс не дает какого-то инструмента, как вам их синхронизировать. Скажем, у вас есть hot fix, который вы готовите с каким-то критическим патчем и какой-то релиз. Hot fix по этому процессу в релиз вмержен не будет. Вам нужно что-то уже накручивать поверх Git Flow для того, чтобы этот коммит не потерять.

  1. А как поддерживать большое количество версий Git Flow нам ничего не говорит. Он предполагает, что мы поддерживаем один hot fix, один релиз и желательно не одновременно.


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



Я дальше буду использовать сквозной пример. У нас есть мастер и 5 версий, которые мы поддерживаем.


К нам пришел кастомер и сказал, что у него есть проблема в версии 1.3. В нее нам нужно что-то закоммитить.


Разберемся, как мы это будем делать. Какие у нас есть развилки?



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



То мы на самом деле хотим это закоммитить вот так сразу в мастер, в 1.5, в 1.4, 1.3.


В каком порядке будем делать эти коммиты? Здравый смысл подсказывает, что просто закоммитить их в случайном порядке – это плохая идея, а опыт подсказывает, что закоммитить все одновременно – это невозможно. У вас могут быть merge-конфликты, вам может быть понадобиться что-то переделать для старых или для новых версий, поэтому мы не можем сказать, что мы просто возьмем и сразу сделаем все и везде.


Тогда у нас остаются два логичных подхода: master-first и target-first.



Master-first или upstream first – начинаем с мастера, а потом по очереди промерживаем во все предыдущие ветки.


Ветки не пропускаем. Если хотим промержить в 1.3, то обязаны это сделать сначала в 1.4, а, чтобы сделать в 1.4, надо сделать в 1.5 и т. д.


Какие плюсы?


  • У нас нет моментов, когда мы нарушаем консистентность между релизами. Если у нас фикс есть в более старой ветке, значит мы до этого его уже сделали в более новой ветке. Такой ситуации, когда мы выпустили релиз кастомеру, отдали ему bugfix, а потом отдали ему какой-то релиз из более новой ветки и он этого bugfix не увидел, — не будет с этим подходом.


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



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



Другой противоположный подход – target-first.


Начинаем с фикса в 1.3 и поднимаемся вверх. И если нам вдруг надо, спускаемся вниз.


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


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


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


Вот это небольшое сравнение:



Понятно, что в чем хорошо один, в том плох другой. Они антиподы друг для друга.


Мы для себя выбрали master-first, потому что консистентность превыше всего. И мы готовы за нее заплатить иногда более долгим фиксом в той версии, где этот фикс нам нужен прямо сейчас.



Тот пример, который у нас уже был ранее, когда у нас есть какая-то версия 1.3.2, которая уже в QA и нам нужно было протащить фикс А в ветку 1.3, но 1.3.2 уже был в QA, поэтому мы создали dev-ветку 1.3.3 для того, чтобы аккумулировать изменения там.


И после этого у нас два активных релиза в одной и той же ветке: 1.3.2 и 1.3.3.


Вопрос: «Как разработчик принимает решение, что ему надо тащить?». Если он знает, что коммит нужен в какой-то версии в рамках ветки 1.3, то как он принимает решение, в какие именно релизы коммит протащить? Это достаточно сложно.



Решение, которое мы для себя придумали, это релиз мастер branches.


Первым для ветки 1.3 создается branch, который называется 1.3-master. Он играет для 1.3 такую же функцию, как мастер играет для всего проекта. Т. е. аккумулирует вообще все-все изменения.


В тот момент, когда нам нужно зарелизить что-то, мы делаем ветку от этого branch, от 1.3-master. Поэтому разработчик может видеть только мастер, протаскивать фиксы только туда. В нужный момент release engineers сделают ветку от этого 1.3-master. Ветки релизов в состоянии development, когда они еще не переданы в QA, нам в этом случае не нужны, потому что изменения могут накапливаться в 1.3-master.


А если нам нужно принести какой-то фикс во время QA, например, как здесь нам нужно было перенести фикс B в 1.3.2, то это делает release engineer, а не разработчик. Release engineer знает о scope релиза, за который он отвечает. У него есть доступ к этой ветке, он может сделать этот cherry-pick.



Следующая минорная, но все же развилка – это как нам отмечать версии? Что я имею в виду?



Есть вариант, когда мы на каждую версию создаем по branch. Нужны ли нам там стабилизационные коммиты, находит ли QA проблемы или не находит – все равно создаем по branch.



Часто предлагают вместо branches стараться использовать тэги, как это делает, например, Github Flow, когда мы просто на мастере ставим тэги. И только, если нам потребовалась стабилизация для какой-то версии, тогда мы уже делаем для нее branch.



Тут просто:


  • Тэги – более легковесные, они не засоряют список branches. Он действительно потихоньку распухает.


  • Branches – более универсальные, потому что, если заранее вы не знаете потребуется ли вам стабилизация или нет, то если потребуется, то вам придется тэг заменить на branch или как-то использовать их вместе. И это довольно трудоемко. Поэтому мы просто стали использовать branches. Тэги мы практически не используем в нашем процессе.




Последний момент о развилках. Как мы в мастер коммиты доносим? Мы уже поговорили обо всем, что мы делаем со старыми ветками, но как нам пофиксить мастер?


В чем тут сложность?



В мастер мы, наверное, хотим использовать стандартный процесс, практически Github Flow за тем исключением, что мы не требуем полной стабильности мастера.


Сделали bugfix-ветку, сделали там несколько коммитов. Вмержили в мастер.


Как нам после этого дотянуть несколько коммитов до предыдущих версий?



Первый вариант – это прямой cherry-pick. Если, кто не знает, cherry-pick – это команда, которая перетаскивает коммиты с одной ветки Git на другую.


Напрямую перетаскиваем все коммиты из bugfix release в старой ветке.


Какие здесь проблемы?


  • У нас могут быть какие-то изменения непосредственно в merge-коммите, которые были частью merge в мастер.


  • Во-вторых, мы таскаем целую кучу коммитов между ветками. Многие из них – это всевозможные work-in-progress, которые часто могут быть мусорными штуками.


  • В-третьих, у нас получается некоторая разница между тем, как фикс выглядит в мастере и в старых ветках, потому что в мастере – это отдельная ветка и merge-коммит, в старых ветках – это просто несколько фиксов прямо в релизных мастерах. И это тоже некоторая проблема, их потом сложнее сопоставлять друг с другом.




Другой вариант – cherry-pick -m. Что делает cherry-pick -m? В отличие от обычного cherry-pick он все коммиты схлопывает вместе. Вы натравливаете его на merge-коммит в мастере, говорите, что хотите взять коммиты из ветки bugfix. Он их все схлопывает вместе с изменениями, которые, возможно, происходили при merge. И таким образом вы перетаскиваете изменения в старые ветки.


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


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



Тот путь, который мы выбрали для себя – это merge --squash. Что делает merge --squash? Он вместо того, чтобы создавать merge-коммит в мастере, он создает squash-коммит, который примерно, как в cherry-pick -m, схлопывает все коммиты A, B, C из bugfix вместе и кладет их прямо в мастер. В мастере при этом никакого merge-коммита нет. Т. е. в мастере у нас лежит один коммит, соответствующий всему изменению, и мы его легко простым cherry-pick’ом одного коммита перетаскиваем на предыдущие ветки.


Это максимально удобная штука, если вам нужно часто делать cherry-pick bugfixes и фичей между ветками. И даже если это относится не к поддержке старых версий, а каким-то другим вашим процессам, то merge --squash спасает.



Поговорили обо всех частях процесса по отдельности, теперь посмотрим на то, как весь процесс выглядит вместе.


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



Мы можем не думать о release branches вообще, о конкретных релизах, мы можем думать только о мастер-branches, остальные мы вообще не видим.


И строго говоря, мы по процессу вообще запрещаем для всех, кроме release engineer, коммиты в релизной ветке.


Вот такую картину видят для себя разработчик:



Когда нужно сделать какой-то bugfix, он готовит себе bugfix-ветку. После интеграционного тестирования review и всего остального, вмерживает это все в мастер.



Потом делает cherry-pick по очереди в 1.5,



в 1.4,



в 1.3.



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


Откуда разработчик получает информацию о том, в какие из этих веток коммитить. В какой минорной версии нам нужен этот фикс – это обозначается прямо в Jira в fix version. И либо это делает тот, кто заводил тикет, либо это делает разработчик сам, исходя из своих знаний, какие фичи и в каких минорных релизах были добавлены. Если фиксят фичу, которая добавлена в 1.3, значит нужно промержить фикс вплоть до 1.3.


Как процесс видят те, кто занимаются выпуском релиза, как его видят release engineers?



Есть мастер, есть релизный master-1.3, от которого мы собираемся сделать релиз.



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


После этого, когда весь scope готов, мы делаем релизную ветку 1.3.2.



Так мы только что ее сделали, мы знаем, что все, что лежит в 1.3-master в этот релиз у нас попало.


Пока релиз тестируется, мастер и 1.3-master продолжают развиваться независимо.



Если нам нужна какая-то стабилизация в 1.3.2, например, мы нашли какой-то баг B, то мы проводим его по обычному процессу.



Разработчики пофиксят его в мастере, потом в 1.3-master.



И потом release engineer протащит его в 1.3.2.




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


  1. Мы говорили про то, что релизы не должны мешать друг другу и разработке.


  2. А также о том, что между релизами должна сохраняться консистентность и мы не хотим ею рисковать.


  3. И подход должен масштабироваться:



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


Кажется, что наш подход этому всему удовлетворяет:


  1. Одновременные релизы друг другу не мешают за счет того, что мы используем release branches и релиз master branches. Разработка всегда может вестись в master branches. И поэтому никакие работы с релизами не мешают разработчикам вносить новые изменения.


  2. Консистентность между релизами сохраняется за счет интеграции сверху вниз: master-first или upstream first, когда мы точно знаем, что промержили коммит во все более новые версии, если он есть в более старой.


  3. Масштабирование в ширину мы достигаем за счет того, что мы можем добавлять master branches. А масштабирование в глубину мы достигаем за счет того, что будем создавать такие branches, как 1.1.1-master и т. д. Они будут со своим релизным мастером относиться так же, как релизный мастер относится к обычному мастеру.




Какие проблемы у нас все еще остаются?



Первая проблема – это проблема с тем, как мы боремся с нарушениями процессов.


Естественно, ни один процесс не будет работать, если ему не следовать. Но нужно сказать, что этот принцип upstream first, которого мы придерживаемся, он более хрупкий. За счет того, что мы не вмерживаем старые релизные branches вверх, если мы в какой-то момент что-то куда-то забыли промержить, т. к. merges между ветками мы не делаем, то фикс может куда-то и не попасть. И это проблема.


Поэтому, если процесс нарушается, то начинается хаос. И решаем мы это через то, что пишем дополнительные инструменты. Сейчас у нас есть первая версия инструмента, который делает нам валидацию git log’а и Jira. Здесь нам очень помогает то, что мы делаем merge --squash и то, что у нас ровно один коммит под каждую Jira issue.


Скрипт может посмотреть на список коммитов на fix version, указанной в Jira и точно найти между ними соответствие 1 к 1. За счет этого можно легко поймать все проблемы, если мы что-то куда-то не промержили.



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


Представьте, что клиент использует версию 1.1.10 и захотел перейти на какую-то версию в ветке 1.2. На какую версию он может перейти? Может ли он перейти на 1.2.1?


На самом деле мы этого не знаем.


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


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



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


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


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


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


Можем сделать выводы. О чем сегодня поговорили?



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

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


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



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


  2. Поэтому мы тоже построили свой собственный велосипед. И решили им поделиться.



  • Наш подход основан на интеграции master-first или upstream first сверху вниз и использования release master branches, которые аккумулируют изменения, соответствующие поддерживаемым miner-веткам.



  1. И продолжаем его улучшать и дорабатывать.

  • Мы работаем над дополнительными инструментами такими, как: кросс-валидация Git и Jira.


  • А также работаем над инструментами, которые помогут нам понимать соотношение между нашими версиями.



Спасибо за внимание!



Здравствуйте! Спасибо за доклад! Не могли бы вы чуть более подробно рассказать про выбор мастер first? Получается, что баг приходит на конкретную версию, допустим, на 1.3. И если баг не понятен, то коммитить его будут тоже на версии 1.3. Соответственно, в 1.3 в target его можно легко починить и дальше следуя логическому процессу разработки эволюции решения перенести его во все последующие ветки.


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


Спасибо за хороший вопрос! Можно было бы сказать, что тут есть какой-то компонент того, что так исторически сложилось. Мы сначала пришли к тому, что у нас было upstream first. Это достаточно давно произошло. Кросс-валидация Git и Jira, т. е. контроль за merge у нас появился не так давно. Но он тоже зависит от upstream first, потому что, если мы только что промержили что-то в 1.3 и еще не промержили выше, то вот этот бот, который к нам ходит и присылает нотификации, он сразу начнет сыпать алертами, что мы что-то еще не промержили.


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


Добрый день! Спасибо за доклад! Какая политика при случае отката, если коммит не взлетел либо в конкретной версии, либо по всей ветке?


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


Если performance упал?


Если у нас была какая-то регрессия по performance, по чему-то еще, то мы, скорее всего, тоже не будем делать отката. Мы заведем отдельный regression issue и будем хендлить его таким образом.


Если у release engineer есть план, ему разработчик что-то замержил в ветку, это как согласовывается?


Мы на уровне правил проекта на Github лочим ветки соответствующим релизом.


Он замержил в мастер, правильно?


Да, это означает, что у нас регрессия появилась в куче релизных мастеров. И это означает, что мы заведем regression issue, какой-то блокер, который будет блокировать, возможно, релизы во всех мастерах. И мы постараемся быстро его пофиксить.


Это хороший вопрос. У нас, наверное, не так часто случаются такие проблемы.


Все frameworks идут по пути, когда все хорошо, но не всегда же это так. Ни один framework не задает ситуации, когда что-то сломалось.


Я согласен. Но здесь мы решаем проблему 99 % случаев. И 1 %, когда мы дропнули performance каким-то bugfix’ом в 2 раза и нашли это во время тестирования, а bugfix уже везде промержен, то мы в ручном режиме примем какое-то решение. Может быть, мы сделаем откат, но, скорее всего, мы очень быстро будем фиксить это по всем веткам.


Добрый день! Меня зовут Дмитрий, Информационная система «Кодекс». Вы говорите, что вы используете подход upstream first, но при этом, допустим, к вам пришел тикет на исправление версии 1.3. Соответственно, разработчик сначала должен найти и исправить этот баг в 1.3, а затем пойти сверху в 1.5, 1.4, возможно, 1.2? Или же он начинает с самой последней версии в 1.5 и смотрит есть ли этот баг в самой свежей версии?


У нас есть две инженерные команды. Одна, в которой я работаю, работает с кастомерами и воспроизводит их проблемы, и занимается частью bugfix. И есть команда, которая работает преимущественно с направленной вперед разработкой. Обычно команда, которая работает с кастомерами, получает от кастомера запрос. Кастомер не заводит баг репорт, он говорит, что у меня что-то не работает.


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


Поиск начинается с версии пользователя?


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


И еще вопрос. Мастер в вашем примере, который вы приводили, это master-1.6?


Он не станет master-1.6. Когда-нибудь master-1.6 от него отбранчуется. Но фактически да, фактически мастер – это релизная ветка для следующего релиза, который мы будем готовить.


И еще маленький вопрос. Версионирование там больше трех, больше вложенностей. И это специфика конкретно вашей разработки? Т. е. по факту находятся какие-то баги, вы их фиксите, но вряд ли получается так, что исправление каким-то кастомерам нужно, каким-то кастомерам не нужно, если это какой-то баг. Т. е. почему не выпускать 1.3, 1.3.1, 1.3.2, 1.3.3 и т. д?


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


В каком-то смысле, да. И это еще решит ту проблему, что branches копятся. Тогда по факту мы сможем удалять, условно, предпоследнюю ветку всегда в рамках минорных версий. Т. е. мы выпустили 1.3.3, мы можем удалить 1.3.1.


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


Почему нам нужно поддерживать старые ветки, почему кастомеры не переходят? Это всегда какая-то договоренность с кастомерами, по тому, насколько большой прыжок они готовы сделать. Если они сидели полтора года в production и у них все было хорошо, а потом они нашли какой-то один баг, и мы им говорим: «Отлично, вы теперь затяните все наши изменения и фичи за полтора года для того, чтобы его пофиксить», то они, скорее всего, будут не очень сильно этому рады. Даже если это захочет сделать сама команда разработки в каком-то банке, то их, скорее всего, развернут какие-то их operation. Они попросят change list для такого перехода. А в нем будет 1 000 изменений. И они скажут: «А можно нам патчик?».


Разве это не проблема версионирования? Получается, что у вас минорные версии на самом деле не минорные.


Почему? Мы выбираем и выпускаем минорную или мажорную версию просто по изменениям. Major ломает compatibility, а minor не ломает. Формально не ломает, по крайней мере. Но в рамках минорной или мажорной версии мы потом продолжаем независимо выпускать на них патчи, чтобы кастомеры, один раз выйдя в prod на минорной версии, было проще в рамках нее апгрейдиться.

Теги:Gitgit workflow
Хабы: Git Системы управления версиями IT-стандарты Управление разработкой DevOps
+16
3,1k 62
Комментарии 2
Похожие публикации
IT-Recruiter
22 декабря 202040 000 ₽OTUS
Product Manager IT-проектов
17 января 202160 000 ₽OTUS
Управление удаленными командами
21 декабря 202018 900 ₽Luxoft Training
Профессия iOS-разработчик
30 ноября 202075 000 ₽SkillFactory
Лучшие публикации за сутки