Отправка уведомлений на почту при деплое проекта

В нашем проекте при каждом деплое разработчики, тестеры и ещё пара людей получают замечательные письма:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Subject: Наш проект версии v1.1.1 обновлён на сервере 'testing'

user1 выложил следующие обновления на сервер 'testing':

Коммиты по задачам:
http://jira.local/browse/PROJECT-1234

Полный список коммитов с предыдущего обновления:
4392a53 Thu Aug 18 17:50:32 2011 +0700 user1 / [PROJECT-1234] сделал полезное
f2fcfe2 Thu Aug 18 17:37:53 2011 +0700 user1 / сделал страшное
cb1fcbe Wed Aug 17 15:18:10 2011 +0700 user2 / зарефакторил

Изменения по файлам:
 file1                 |    4 ++--
 file2                 |    8 ++++----
 file3                 |    8 ++++----
 3 files changed, 10 insertions(+), 10 deletions(-)

Такое решение помогло нам избавиться от вопросов тестеров “Ну что, выложили уже исправление бага XXX?”, “Что нового на тестовом сервере?”. Так же - все члены команды, отдел внедрения и руководство в курсе, что происходит с кодом на серверах. Для работы используется git, capistrano (+ multistage), php, bash (+ некоторые консольные утилитки). Если интересно - заходим под кат.

Алгоритм работы

  • Обновляем код на сервере testing (cap testing deploy)
  • После deploy:restart срабатывает хук, создающий тег в репозитории. Тег формируется на основе версии проекта (хранится в конфиге, в репозитории), названии staging-сервера и названии релиза
  • В репозитории срабатывает хук. Если пришёл не тег - игнорируем, если же тег:
    • Распиливаем его на компоненты: версия, сервер
    • Определяем предыдущий тег на этом же сервере
    • Если тега нет - значит эта первая установка и генерировать различия не стоит, там может быть несколько тысяч коммитов
    • Если же есть тег - генерируем список различий, выдёргиваем из них список задач; составляем список изменённых файлов
    • Отправка сгенерированного письма

Создание тега

Про настройку capistrano и capistrano-multistage уже где только не написано, поэтому я только расскажу, как у нас добавляется тег.

Будем считать, что у нас в корне репозитория есть файл configs.ini, который содержит ключ runtime.version. За основу был взят gist#381852.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
namespace :deploy do

  after "deploy:restart", "deploy:git:push_deploy_tag"
  namespace :git do
    desc "Place release tag into Git and push it to server."
    task :push_deploy_tag do
      user = `git config --get user.name`.strip
      email = `git config --get user.email`.strip
      version = `git cat-file -p #{real_revision}:configs.ini | fgrep runtime.version | awk -F '[ =]+' '{print $2}'`.strip
      puts `git tag v#{version}-#{stage}-#{release_name} #{real_revision} -m "Deployed by #{user} <#{email}>"`
      puts `git push --tags`
    end
  end
end

Что здесь происходит:

  • Извлекаем данные текущего пользователя (имя и почту) из конфига гита
  • Берём файл configs.ini из устанавливаемой ревизии и выдёргиваем версию
  • Создаём аннотированный тэг. В аннотации указываем, кто и когда задеплоил
  • Публикуем теги

Обрабатываем обновление репозитория

Хуку pre-receive на вход (stdin) подётся 3 значения: предыдущая и текущая ревизии, refname. Читаем входящие параметры и убеждаемся, что пришёл тег:

1
2
3
4
5
6
7
8
9
while read oldrev newrev refname
do
    rev_type=$(git cat-file -t $newrev 2>/dev/null)

    case "$refname","$rev_type" in
        refs/tags/*,tag)
        ;;
    esac
done

Выделяем название тега, разбиваем его на части, ищем предыдущий тег для этого сервера:

1
2
3
4
5
tag=${refname##refs/tags/}

version=`echo $tag | cut -d- -f1`
server=`echo $tag | cut -d- -f2`
prevtag=$(git describe --tags --abbrev=0 --match="*-$server-*" $newrev^ 2>/dev/null)

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

Начнём формировать тело письма. Сперва - определим кто осмелился задеплоить:

1
2
3
4
5
eval $(git for-each-ref --shell --format='
          tagger=%(taggername)
          tagged=%(taggerdate)' $refname
        )
echo "$tagger выложил следующие обновления на сервер '$server':" > msg

Теперь разберём коммиты по задачам. Последние в Jira именуются по маске <алиас проекта>-<id задачи>, все разработчики в обязательном порядке указывают алиас задачи (uppercase) в коммите. Если задача крупнее чем на 30 минут и требует более 1 коммита - создаётся ветка, по алиасу задачи, и тогда в коммитах эту самую задачу мы уже не упоминаем. Итого, чтобы достать список задач нам нужно выполнить не сложную обработку регуляркой:

1
2
3
4
5
6
7
8
9
10
11
git log $rev_range --abbrev-commit --pretty="format:%s" > tmpfile
php >tickets <<END
<?php
\$f = file_get_contents("$tmp");
if (preg_match_all("#([A-Z.]+-\d+)#", \$f, \$matches)) {
    \$matches[1] = array_unique(\$matches[1]);
    foreach (\$matches[1] as \$match) {
        echo '$JIRA_HOST/browse/', \$match, PHP_EOL;
    }
}
END

Если в итоге файл tmpfile не пуст - добавляем его к телу письма. Дальше идёт информация, которая интересует только разработчиков проекта: списки коммитов и изменённых файлов:

1
2
3
4
5
echo "Полный список коммитов с предыдущего обновления:" >> msg
git log $rev_range --no-merges --abbrev-commit --pretty="format:%h %ad %an / %s" >> msg

echo -e "\n\nИзменения по файлам:" >>msg
git diff --stat=140,110 $rev_range >>msg

Ну и, наконец, тупая отправка письма:

1
cat msg | mail -s "$subject" $MAIL_TO

Все файлы можно взять на гитхабе: https://github.com/zvirusz/git-deploy-notify

P.S. Если кто-нибудь поможет переписать кусок кода, выдёргивающий имена задач, на perl/bash - я буду очень рад.

Comments