Перевод неустаревающей статьи Мартина Фаулера про Непрерывную Интеграцию. Несмотря на то, что статья написана в 2006 году и упоминаемые в ней инструменты уже устарели, описание самой практики остаётся актуальной и по сей день.
Непрерывная интеграция ― это практика разработки программного обеспечения, в которой участники команды часто выполняют интеграцию своих изменений. Как правило, каждый участник выполняет интеграцию как минимум раз в день, и в итоге достигается такой режим работы, при котором интеграция выполняется несколько раз в день. Каждая интеграция проверяется путем автоматической сборки (включающей тестирование), что позволяет находить ошибки интеграции как можно скорее. Многие команды убедились, что такой подход существенно снижает количество интеграционных проблем и позволяет быстрее разрабатывать целостный программный продукт. Эта статья представляет собой обзор непрерывной интеграции, описывающий эту технику и то, как она используется в настоящее время.
Я четко помню свое первое впечатление о работе над большим проектом по разработке программного обеспечения. Я проходил летнюю стажировку в крупной английской компании, производящей электронику. Мой менеджер, по совместительству руководитель группы контроля качества, ввел меня в курс дела, после чего мы пришли на огромный склад, загроможденный коробками и наводящий уныние. Мне сказали, что этот проект разрабатывался в течение двух лет, а сейчас выполняется его интеграция, причем эта интеграция длится уже несколько месяцев. Мой гид сказал мне, что никто на самом деле не знает, как скоро она закончится. Таким образом, первый вывод, который я сделал о процессах разработки, был таким: интеграция ―долгий и непредсказуемый процесс.
Но это необязательно должно быть так. В большинстве проектов, над которыми работают мои коллеги в ThoughtWorks и многие другие разработчики по всему миру, интеграция не считается проблемой. Работа каждого отдельного разработчика лишь на несколько часов опережает общую копию проекта и может быть интегрирована в нее в считанные минуты. Любые ошибки интеграции быстро находятся и быстро исправляются.
Чтобы достичь такой организации работы, вам не нужны дорогостоящие и сложные инструменты. Ее секрет лежит в простой практике, когда каждый участник команды часто (как правило, ежедневно) интегрирует свои изменения в репозиторий хранения исходного кода.
Наша первая статья о непрерывной интеграции описывает наш опыт использования этой практики в одном из проектов ThoughtWorks в 2000 году, который Мэтт помог собрать воедино.
Когда я описывал эту практику разным людям, я обычно встречал две реакции: «Это не будет работать (здесь)» или «Вряд ли это что-то изменит». Но когда люди пробовали эту методику, они понимали, что она гораздо проще, чем кажется, и, кроме того, она существенно влияет на процесс разработки. Поэтому третья реакция была такой: «Да, мы это делаем ― как же мы жили без этого раньше?»
Термин «Непрерывная интеграция» впервые использовал Кент Бек в своей книге Экстремальное программирование, называя им одну из двенадцати оригинальных методик процесса разработки. Когда я начал работать в ThoughtWorks, то взялся продвигать эту методику в своем проекте. Мэттью Фоммель помог мне перейти от неясных призывов к конкретным действиям, и в итоге мы увидели, как наш проект перешел от редких и сложных интеграций к простой процедуре, которую я описал выше. Мы с Мэттью описали наш опыт в первой версии этой статьи, которая стала одной из самых популярных статей на моем сайте.
Хотя непрерывная интеграция ― практика, не требующая специальных инструментов, мы обнаружили, что в ней полезно использовать сервер непрерывной интеграции. Самый известный из подобных серверов ― это CruiseControl, решение с открытым исходным кодом, созданное несколькими сотрудниками ThoughtWorks и теперь поддерживаемое большим сообществом. С тех пор появилось еще несколько серверов непрерывной интеграции (CI-серверов), как с открытым кодом, так и коммерческих, в частности, Cruise, созданный в ThoughtWorks Studios.
Сборка фичи при непрерывной интеграции
Наиболее простой для меня способ объяснить вам, что такое непрерывная интеграция и как она работает, на примере разработки небольшой фичи. Давайте предположим, что мне нужно что-то доработать в программном обеспечении, неважно, что именно, но будем считать, что фича небольшая и может быть разработана за несколько часов. (Позже мы рассмотрим более долговременные задачи и связанные с ними проблемы).
Я начинаю с получения актуальной копии интегрированного исходного кода продукта на мой компьютер. Для этого я использую систему управления исходным кодом, получая рабочую копию из основной ветки.
Предыдущий абзац будет понятен тем, кто использует системы управления исходным кодом, но остальным читателям может показаться непонятным набором слов. Поэтому давайте я кратко объясню, что имею в виду. Система управления исходным кодом хранит весь код по проекту в репозитории. Текущее состояние системы обычно называют «основной веткой». Каждый разработчик в любой момент может получить управляемую копию основной ветки на свою машину. Копия на машине разработчика называется «рабочей копией». (На самом деле, большую часть времени вы занимаетесь тем, что обновляете свою рабочую копию из основной ветки. По сути, это то же самое).
Затем я беру свою рабочую копию и вношу в нее все изменения, необходимые для решения моей задачи. В частности, изменяю рабочий код и добавляю или изменяю автоматизированные тесты. Непрерывная интеграция подразумевает использование большого количества тестов, автоматически встроенных в продукт: в таких случаях говорят о самотестируемом коде. Для этого часто используется популярный тестовый фреймворк XUnit.
Когда я завершил работу (а также на некоторых этапах работы, пока она еще не завершена), я запускаю автоматическую сборку продукта на своей рабочей машине. Это означает что код из моей рабочей копии, компилируется и линкуется в исполняемый файл, а затем запускает автоматизированные тесты. И только если сборка и тестирование прошли без ошибок, полученный билд считается работоспособным.
Имея работоспособную сборку, я могу подумать о том, чтобы выполнить коммит моих изменений в репозиторий. На этом этапе следует иметь в виду, что другие люди могли внести изменения в основную ветку (и, скорее всего, они их уже внесли), прежде чем я получил возможность выполнить свой коммит. Поэтому сначала я обновляю свою рабочую копию, добавив их изменения, а затем снова выполняю сборку. Если их изменения вступают в конфликт с моими, я получу ошибку либо во время компиляции, либо во время тестирования. В этом случае я должен исправить эту ошибку и повторять свои действия до тех пор, пока не смогу собрать рабочую копию, которая будет корректно синхронизирована с основной веткой.
Как только я получу сборку рабочей копии, которая корректно синхронизируется с основной веткой, я могу выполнить коммит. После этого основная ветка обновляется в репозитории.
Однако после коммита моя работа не завершена. В этот момент мы снова выполняем сборку, теперь уже на сервере интеграции, на кодовой базе основной ветки. И только если эта сборка проходит успешно, можно сказать, что мои изменения завершены. Всегда есть вероятность, что я что-то упустил, работая на своем компьютере, или что репозиторий не был корректно обновлен. Только когда добавленные мной изменения успешно собираются на сервере интеграции, моя работа считается завершенной. Эту интеграционную сборку я могу выполнить вручную или автоматически при помощи Cruise.
Если между изменениями, выполненными двумя разработчиками, возникает конфликт, он обычно проявляется в тот момент, когда второй разработчик собирает свою обновленную рабочую копию. Если же конфликт не проявился на этом этапе, он проявится во время интеграционной сборки. В любом случае ошибка находится быстро. На этом этапе самая важная задача ― исправить ошибку и добиться, чтобы сборка вновь начала работать корректно. В окружении непрерывной интеграции у вас не должно быть неудачных интеграционных сборок, которые долго не исправляются. В хорошей команде бывает по несколько рабочих сборок в день. Неудачные сборки случаются время от времени, но исправляются они очень быстро.
В результате у вас есть стабильный программный продукт, который корректно работает и содержит мало ошибок. Каждый участник команды начинает разработку с этой стабильной базовой копии, и никогда не удаляется от нее настолько, чтобы интеграция с ней заняла много времени. На поиск ошибок тратится значительно меньше времени, потому что они проявляются быстро.
Описанная выше история кратко рассказывает о том, что такое непрерывная интеграция (Continious integration, CI), и как она работает в повседневной жизни. Чтобы понять, как обеспечить безотказную работу этого процесса, этого описания, очевидно, недостаточно. Далее я расскажу о ключевых практиках, повышающих эффективность непрерывной интеграции.
В проектах разработки программного обеспечения обычно используется большое количество файлов, которые надо объединить, чтобы собрать продукт. Отслеживать эти файлы ― самая трудоемкая задача, особенно, если задействовано несколько человек. Поэтому неудивительно, что за последние годы команды разработки программного обеспечения создали ряд инструментов для управления файлами. Эти инструменты называют, например, инструментами управления исходным кодом, системами контроля версий или репозиториями. Они стали неотъемлемой частью большинства проектов разработки. Печально и удивительно то, что они пока еще не стали частью всех проектов. Иногда (хоть и редко) я встречаюсь с проектами, которые не используют такие системы, а вместо них используют запутанную комбинацию локальных и общих дисков.
Поэтому, в качестве простого первого шага, убедитесь, что у вас есть подходящая система управления исходным кодом. Цена таких систем ― не проблема, так как многие из них имеют открытый исходный код. В настоящее время наилучшим репозиторием с открытым исходным кодом можно назвать Subversion. (Прежняя популярная система с открытым исходным кодом CVS по-прежнему широко распространена, и она лучше, чем ничего. Но Subversion ― все же наилучший выбор в наши дни). Любопытно, что многие коммерческие инструменты управления исходным кодом, по словам разработчиков, нравятся им меньше, чем Subversion. Единственный инструмент, за который, по мнению многих моих собеседников, стоит заплатить — это Perforce.
Начиная использовать систему управления исходным кодом, убедитесь, что все хорошо знают, что исходный код надо получать именно из нее. Никто не должен спрашивать: «Где находится файл foo-whiffle?» Все должны хорошо ориентироваться в репозитории.
Хотя многие команды используют репозитории, я часто вижу, что они помещают в них не всё. И это распространенная ошибка. Если вы используете репозиторий, вы будете помещать в него код. Но в нем также должно быть и все необходимое для успешной сборки: тестовые скрипты, property-файлы, схема базы данных, установочные скрипты и сторонние библиотеки. Я знаю некоторые проекты, в которых компиляторы также помещались в репозитории (что было особенно актуально во время использования первых компиляторов C++, которые иногда вели себя непредсказуемо). Главное правило: вы должны быть в состоянии включиться в работу по проекту с «чистым» компьютером, извлечь на него рабочую копию и полностью собрать систему. На «чистом» компьютере должен быть установлен лишь минимум программных продуктов ― как правило, больших по размеру, сложных в установке и стабильных в работе. Например, операционная система, среда разработки на Java или основная система баз данных.
Вы должны помещать все необходимое для сборки в систему управления исходным кодом, однако вы можете хранить в них также и другие данные, с которыми вы обычно работаете. Хорошая практика ― помещать туда конфигурации IDE, так как это простой способ помочь другим разработчикам установить IDE с такими же настройками.
Одной из фич систем управления версиями можно назвать то, что они позволяют вам создавать новые ветки и управлять различными потоками разработки. Это полезная и даже необходимая фича, но зачастую она используется избыточно, создавая лишние проблемы. Сведите использование веток к минимуму. В частности, вам нужна основная ветка ― единственная ветка проекта, который находится в разработке. Фактически, большую часть времени все разработчики будут работать c этой основной веткой. (Имеет смысл создавать ветки для исправлений ошибок, предварительных релизов продукта и временных экспериментов).
Если обобщить, вам необходимо хранить в системе управления исходным кодом все, что нужно для того чтобы собрать всё, но ничего из того, что вы собираете. Некоторые разработчики хранят в системе управления исходным кодом продукты сборки, но я считаю это плохой практикой и признаком более глубокой проблемы ― как правило, неспособности надежным способом пересобрать продукт.
Превращение исходного кода в работающую систему может быть сложным процессом, который включает в себя компиляцию, копирование файлов, загрузку схем в базы данных и так далее. Однако, как и большая часть задач в этой части разработки программного обеспечения, этот процесс можно автоматизировать, а значит, его нужно автоматизировать. Просить людей набирать странные команды или щелкать мышкой по диалоговым окнам ― напрасная трата времени и создание благодатной почвы для возникновения ошибок.
Общая черта описанных систем ― наличие автоматизированных сред для сборки. В Unix десятилетиями использовалась утилита make, Java-сообщество разработало Ant, сообщество .NET ранее использовало Nant, а сейчас ― MSBuild. Убедитесь, что вы можете собрать и запустить свою систему с помощью этих инструментов, всего одной командой.
Распространенной ошибкой также можно назвать ситуацию, когда в автоматическую сборку включается не все. Сборка должна включать в себя получение схемы базы данных из репозитория и ее создание в среде выполнения. Я несколько переработаю основное правило, о котором писал ранее: вы должны взять «чистый» компьютер, загрузить на него исходный код из репозитория, запустить одну команду и получить на своем компьютере работающую систему.
Существует множество скриптов сборки на любой вкус, и они бывают специфичны для той или иной платформы или сообщества, хотя это и необязательно. Хотя в большинстве наших Java-проектов используется Ant, в некоторых из них используется Ruby (надо отметить, что система Ruby Rake ― очень хороший инструмент для сборки). Сборка ранних версий проекта Microsoft COM была очень успешно автоматизирована с помощью Ant.
Большая сборка часто занимает много времени, и вы не должны выполнять все ее шаги каждый раз, когда внесли небольшое изменение. Поэтому хороший инструмент сборки в ходе процесса анализирует, что именно необходимо изменить. Обычно это делается путем проверки дат исходных и объектных файлов и компиляции только в том случае, если исходные файлы изменены позже. В этом случае усложняются зависимости: если изменяется один файл, то все файлы, зависимые от него, также могут нуждаться в повторной сборке. Некоторые компиляторы могут отслеживать подобные ситуации и управлять ими, а некоторые ― нет.
Вам может понадобиться собирать разные типы файлов, в зависимости от того, что вы хотите получить. Вы можете собрать систему с тестовым кодом или без него, а также с различными наборами тестов. Некоторые компоненты могут быть собраны отдельно. Скрипт сборки должен позволять вам собирать различные типы файлов для различных целей.
Многие используют IDE (интегрированные среды разработки), а в большинстве IDE предусмотрен тот или иной встроенный инструмент управления сборкой. Однако подобные инструменты всегда являются собственностью IDE и часто бывают нестабильными. Более того, для их работы необходима IDE. Для пользователей IDE может быть вполне естественно устанавливать файлы своего проекта и использовать их для индивидуальной разработки. Однако необходимо также иметь основную сборку, которая может использоваться на сервере и запускаться с помощью других скриптов. В частности, для Java-проектов характерна ситуация, когда разработчики выполняют сборку в своих IDE, но для основной сборки используется Ant, чтобы гарантировать, что сборку можно запустить на сервере разработки.
Традиционно, сборка включает в себя компиляцию, линкование и все прочие дополнительные действия, необходимые для получения исполняемой программы. В результате программу можно запустить, но это еще не означает, что она будет делать то, что нужно. Современные статически типизированные языки могут отлавливать много ошибок, но большая часть ошибок все равно проскальзывает в программу.
Хороший способ отловить ошибки быстро и эффективно ― включить автоматизированные тесты в процесс сборки. Тестирование, конечно, не бывает безупречным, но все же оно помогает отловить многие ошибки, а значит, оно полезно. В частности, развитие экстремального программирования (XP) и разработки через тестирование (TDD) внесло большой вклад в популяризацию самотестируемого кода и, в результате, многие успели оценить преимущества этой техники.
Те, кто регулярно читают мои статьи, знают, что я убежденный приверженец как TDD, так и XP, однако я хотел подчеркнуть, что ни один из этих подходов не требуется для того, чтобы оценить преимущества самотестируемого кода. Как правило, при каждом из этих подходов тесты пишутся еще до того, как будет написан код, к которому они будут применяться. В этом случае тесты нужны скорее для исследования дизайна системы, чем для поиска ошибок. Это очень хорошая практика, но она необязательна для целей непрерывной интеграции, где к самотестируемому коду предъявляются не такие строгие требования. (И все же мой любимый способ написания самотестируемого кода ― подход TDD.)
Для создания самотестируемого кода вам необходим набор автоматизированных тестов, которые могут проверять большую часть кода на наличие ошибок. Эти тесты должны запускаться с помощью простой команды и быть самопроверяющими. Результат запуска тестов должен показывать, какие из них окончились неудачей. Для сборки с самотестируемым кодом неудачное завершение теста должно приводить к неудачной сборке.
Благодаря развитию TDD в последние годы популярным стало использование инструментов семейства XUnit с открытым исходным кодом, которые идеально подходят для тестирования такого типа. Инструменты XUnit очень хорошо себя зарекомендовали в ThoughtWorks, и я всегда и всем предлагаю их использовать. С помощью этих инструментов, впервые примененных Кентом Беком, можно легко создать полностью самотестируемую среду.
Безусловно, инструменты XUnit ― отправная точка для того, чтобы сделать код самотестируемым. Обратите также внимание на другие инструменты, предназначенные больше для сквозного тестирования. Их сейчас довольно много, и к ним относятся FIT, Selenium, Sahi, Watir, FITnesse и множество других, которые я не буду перечислять здесь.
Конечно, не стоит рассчитывать, что тесты найдут абсолютно все ошибки. Как говорится, тестирование не доказывает отсутствие ошибок. Но идеального тестирования и не требуется, чтобы получить отдачу от самотестируемой сборки. Неидеальные тесты, запускаемые часто ― это гораздо лучше, чем идеальные тесты, которые никогда не будут написаны.
Интеграция — это, в первую очередь, коммуникация. Интеграция позволяет разработчикам сообщать другим разработчикам о своих изменениях. Частая коммуникация позволяет людям быстро узнавать о разрабатываемых изменениях.
Одно из предварительных условий для коммита разработчика в основную ветку ― корректно собранный код. Это, конечно, включает в себя прогон тестов на сборке. Как и при любом коммите, разработчик сначала обновляет свою рабочую копию до основной ветки, разрешает все конфликты с основной веткой, а затем выполняет сборку на своем компьютере. Если сборка прошла успешно, можно выполнять коммит в основную ветку.
Если это делается часто, может возникнуть конфликт между изменениями двух разработчиков. Чтобы исправить проблему быстро, главное ― быстро ее обнаружить. Если разработчики выполняют коммиты каждые несколько часов, конфликт может быть обнаружен через несколько часов после возникновения. В этот момент его легко исправить, так как изменений за это время произошло еще немного. Конфликты, которые не были обнаружены в течение нескольких недель, очень трудно исправлять.
Тот факт, что вы выполняете сборку сразу после обновления своей рабочей копии, означает, что вы обнаруживаете конфликты компиляции, а также буквальные конфликты. С самотестируемой сборкой вы также можете найти конфликты в исполнении кода. Такие конфликты особенно трудно обнаружить, особенно, если они находятся в коде в течение долгого времени. Так как между коммитами проходит лишь несколько часов изменений, то мест, в которых может скрываться проблема, не так много. Кроме того, поскольку изменений было немного, вы можете выполнить diff-отладку, чтобы быстрее найти ошибку.
Мое основное правило ― каждый разработчик должен ежедневно выполнять коммит в репозиторий. На практике, часто оказывается полезнее, если разработчики выполняют коммиты еще чаще. Чем чаще вы выполняете коммиты, тем меньше остается мест для обнаружения конфликтов, и тем быстрее вы можете их исправить.
Частые коммиты стимулируют разработчиков разбивать свою работу на небольшие фрагменты по несколько часов. Это помогает отслеживать прогресс и создает ощущение прогресса. Некоторые разработчики на первых этапах не чувствуют уверенности в том, что могут создать что-то значимое за несколько часов, но в этом случае им может помочь наставничество и практика.
С ежедневными коммитами в команде появляются частые и протестированные сборки. Стоит отметить, что при этом основная ветка остается в стабильном состоянии. На практике, однако, проблемы все еще случаются. Одна из причин ― в дисциплине, разработчики не выполняют обновление и сборку перед коммитом. Другая причина ― разное окружение на компьютерах разработчиков.
В итоге вы должны убедиться, что регулярные сборки выполняются на интеграционном сервере, и только если эти интеграционные сборки проходят успешно, коммит считается выполненным. Поскольку разработчики отвечают за свои коммиты, они сами должны отслеживать сборку в основной ветке и исправлять ошибки в случае их возникновения. Отсюда можно сделать вывод, что вы не должны уходить домой, пока в основной ветке не пройдет успешная сборка, включающая все коммиты, добавленные вами за день.
Чтобы это гарантировать, есть два основных способа: использование ручной сборки и сервер непрерывной интеграции.
Ручную сборку описать легче. В сущности, это похоже на локальную сборку, которую разработчик выполняет перед коммитом в репозиторий. Разработчик заходит на интеграционный сервер, проверяет head основной ветки (которая содержит его последний коммит) и запускает интеграционную сборку. Далее он следит за ее прогрессом, и, в случае ее успешного прохождения, считает, что коммит выполнен успешно. (Смотрите также описаниеэтого процесса в статье Джима Шора.)
Сервер непрерывной интеграции выступает в роли монитора репозитория. Каждый раз по завершении коммита сервер автоматически получает исходный код на интеграционный компьютер, запускает сборку и уведомляет автора коммита о ее результате. Коммит не считается завершенным успешно, пока его автор не получит уведомление (обычно, по электронной почте).
В ThoughtWorks мы очень активно используем серверы непрерывной интеграции ― более того, здесь мы изначально разрабатывали CruiseControl и CruiseControl.NET, широко используемые CI-серверы с открытым исходным кодом. После этого мы также создали коммерческий CI-сервер Cruise. Мы используем CI-серверы практически во всех проектах и очень довольны результатами.
Но не все предпочитают использовать CI-серверы. Джим Шор приводит хорошо аргументированное объяснение, почему он предпочитает ручной подход. Я согласен с ним в том смысле, что CI ― это гораздо больше, чем просто установка определенного программного обеспечения. Необходимо придерживаться всех описанных здесь практик, чтобы непрерывная интеграция была эффективной. В то же время многие команды, использующие подход CI, считают CI-сервер полезным инструментом.
Во многих организациях сборки выполняются регулярно по расписанию, например, по ночам. Это не то же самое, что непрерывная сборка, и этого недостаточно для непрерывной интеграции. Главный смысл непрерывной интеграции в том, чтобы выявлять проблемы как можно быстрее. Ночные сборки означают, что проблемы остаются незамеченными в течение дня, пока кто-нибудь не обнаружит их. А поскольку они находятся в системе так долго, то уходит много времени для их поиска и исправления.
Ключевой смысл непрерывной сборки заключается в том, что если сборка основной ветки завершилась ошибкой, эту ошибку необходимо исправить сразу. Главное при работе с CI ― то, что вы всегда ведете разработку на известной и стабильной базе. Сама по себе неудачная сборка основной ветки ― не слишком большая проблема, хотя она может свидетельствовать о том, что разработчики недостаточно заботятся об обновлении и сборке на своих компьютерах прежде, чем сделать коммит. Но если сборка основной ветки завершилась ошибкой, важно исправить эту ошибку как можно скорее.
Я помню фразу Кента Бека: «Ни одна задача не может быть приоритетнее, чем исправление сборки». Это не значит, что все участники команды должны немедленно прекратить свою текущую работу, чтобы заняться исправлением сборки. Как правило, достаточно пары человек, чтобы устранить проблему. Это означает сознательное присвоение наивысшего приоритета и срочности задаче исправления сборки.
Зачастую самый быстрый способ исправить сборку ― откатить последний коммит из основной ветки, вернув систему к последнему состоянию, о котором известно, что сборка в нем была успешной. Конечно, команде не стоит пытаться выполнять отладку в сломанной основной ветке. Даже если причина поломки сразу известна, откатитесь к основной ветке и запустите отладку проблемы на рабочей станции, на которой велась разработка.
Чтобы по возможности избежать нарушений сборки основной ветки, вы можете попробовать использовать pending head.
Когда команды вводят CI, им часто бывает очень сложно во всем разобраться. На ранних стадиях участникам команды может быть трудно выработать постоянную привычку работать со сборками основной ветки, особенно, если они работают на существующей базе кода. Терпение и постоянное применение этой методики может помочь преодолеть эти трудности, так что не падайте духом.