Статья является вольным переводом вот этого материала: krisjordan.com/essays/setting-up-push-to-deploy-with-git. Любителям длинных и заумных первоисточников можно сразу читать оригинал.
Когда перед нами ставится задача при изменении кодбейса, например, в Github-репозитории выполнить пересборку/перезапуск какого-нибудь приложения на каком-то нашем окружении, то первое, что приходит на ум в качестве возможного триггера такой пересборки, это предоставляемый тем же гитхабом механизм веб-хуков: при наступлении какого-либо события с нашим удаленным репозиторием (т.к. появление нового коммита в какой-нибудь его отслеживаемой ветке) гитхаб задействует соответствующий веб-хук и «дернет» указанный в его настройках сервис, который и запустит процесс пересборки/перезапуска нашего приложения. Это стандартный широкоиспользуемый и простой механизм для таких случаев, все так делают, и все такое…
Но что, если наше приложение живет на хосте, доступ к которому по каким-то причинам гитхабу не положен? Например, хост находится в закрытой сети или за NAT'ом и недоступен из интернета?
В этом случае можно воспользоваться механизмом локальных хуков самого Git, о котором (механизме), как выясняется, мало кто знает даже из тех, кто использует Git уже довольно продолжительное время.
Когда мы создаем локальный репозиторий (инициализируем пустой, клонируем удаленный, ...), то в папке .git, что в его корне, или в самом корне, если это bare-репозиторий, присутствует папка hooks. После инициализации репозитория в этой папке сохраняются шаблоны хуков для различных событий, таких как, например: post-merge, post-receive, post-update и др.
Полное описание поддерживаемых событий для хуков можно найти, например, тут: git-scm.com/book/ru/v1/%D0%9D%D0%B0%D1%81%D1%82%D1%80%D0%BE%D0%B9%D0%BA%D0%B0-Git-%D0%9F%D0%B5%D1%80%D0%B5%D1%85%D0%B2%D0%B0%D1%82%D1%87%D0%B8%D0%BA%D0%B8-%D0%B2-Git
Мы воспользуемся этим механизмом и реализуем простенькую push-to-deploy схему для нашего горемычного приложения.
Нам понадобится два локальных репозитория. Создадим их, например, по указанным путям:
1. /opt/repo-dev/example-repo/
2. /opt/repo-remote.git/
Первый репозиторий — это клон нашего удаленного репозитория example-repo на гитхабе.
Второй — это bare репозиторий, копия первого, который будет служить нам исключительно для обработки события post-update при появлении обновлений в удаленном репозитории. Итак, как же мы это реализуем?
Схема очень проста (предположим, мы отслеживаем ветку test, а приложение наше это node.js, управляемый менеджером pm2):
1. Периодически обновляем первый локальный репозиторий до состояния, полностью соотвествующего состоянию удаленного репозитория.
2. Из первого локального репозитория обновляем второй.
3. Как только HEAD у нас переместился — появился новый коммит — во втором репозитории будет задействован хук post-update, который выполняется при появлении любых изменений в репозитории, и который и выполнит необходимые действия по ребилду и рестарту приложения.
Для этого мы делаем следующее:
1. В первом локальном репозитории добавляем remote — второй локальный репозиторий:
cd /opt/repo-dev/example-repo/ && git remote add prod /opt/repo-remote.git
Теперь мы можем выполнять git push из первого локального репозитория во второй.
2. В папке /opt/repo-remote.git/hooks/ создаем файл post-update и делаем его исполняемым:
touch /opt/repo-remote.git/hooks/post-update && chmod +x /opt/repo-remote.git/hooks/post-update
Это обычный шел-скрипт, но согласно внутренней конвенции Git без расширения .sh!
Давайте добавим в него несколько команд:
#!/bin/bash
cd /opt/repo-remote.git
/usr/bin/git --work-tree=/opt/repo/example-repo/ checkout -f origin/test
cd /opt/repo/example-repo/
/usr/bin/npm install
/usr/local/bin/pm2 restart all
Что делает скрипт? Сначала просто выгружает working tree нашего bare репозитория в папку с нашим работающим приложением, а затем пересобирает зависимости и рестартует сервисы pm2. Как видите, никакой магии.
3. Настраиваем cron, который каждые n минут будет обновлять первый репозиторий из удаленного:
git fetch origin && git reset --hard -f origin/test
Т.о. теперь наш хост будет являться регулярным инициатором проверки удаленного репозитория — а не появились ли там обновления?
Прошу заметить, что обновляем локальный репозиторий мы не путем git pull, а путем git reset --hard. Делается это для того, чтобы исключить необходимость мерджей при определенном содержимом очередного коммита — мы делаем локальный репозиторий полной копией удаленного.
4. Сразу после синхронизации первого локального репозитория с удаленным мы делаем пуш всех изменений в наш псевдо-удаленный второй локальный репозиторий:
git push prod test
Вот и все. Как только наш псевдо-удаленный локальный репозиторий получает ненулевые изменения, Git дергает свой хук post-update, который выполняет соответствующий скрипт. И мы получаем простенькую рабочую схему push-to-deploy, которую при желании можно и дальше усовершенствовать в соответствии с нашими потребностями.
«Зачем городить такую неудобоваримую монструозную схему?!» — спросите вы. Я сначала задавался этим же вопросом, но оказалось, что с существующим перечнем хуков Git'а только так мы сможем вызвать необходимую обработку при любом обновлении нашей ветки в удаленном репозитории. Нужный нам хук post-update предназначен для выполнения на remote репозитории (а часть хуков предназначена для выполнения на локальном репозитории). И мы таким вот не очень изящным способом это псевдо-удаленный репозиторий и сэмулировали. Возможно в скором времени появится еще какой-нибудь более удобный хук, выполняющийся локально, и схему можно будет упростить. Но пока так.
В заключение хочу дать несколько советов тем, кто решит это реализовать у себя и, столкнувшись с проблемами неработающего хука или каких-то его частей, будет яростно проклинать меня, Git, автора оригинальной статьи и всех остальных по списку:
1. Помните про специфику работы cron — программы в нем по умолчанию запускаются совсем не в том окружении, которое вы, вероятно, ожидаете.
2. Проверяйте версии ваших утилит (npm, node etc) при вызове их из скриптов и по cron — они могут быть не такими, как при ручном запуске из-за различия путей к исполняемым файлам в переменных окружения, например. А запуск других их версий может приводить к непрогнозируемым результатам.
3. Потратьте 20 минут на просмотр очередной серии Simpsons и возвращайтесь к экспериментам с новыми силами и хорошим настроением
Буду рад любым замечаниям и уточнениям по существу.
Хорошего всем дня!
Автор: akrymets