Полезная функциональщина. Грабим почту, трекеры задач и репозитории с Clojure
Я — тимлид в одной из команд разработки Positive Technologies Application Firewall. В один прекрасный момент размер команды превысил критическое значение, когда можно делать руками всю свою рутинную работу, и я начал писать автоматизированную систему, которая умела бы взаимодействовать с почтой, трекерами задач и системами контроля версий. Я назвал ее «автоматический тимлид».
Распределение задач — одна из рутинных процедур, которую легко автоматизировать, если в команде существует матрица компетенций: назначить исполнителя в зависимости от заголовка задачи или ее компонента можно, если уметь программно читать задачу с таск-трекера и обновлять некоторые поля в ней. Аналогичная история с автоматическим назначением по набору критериев ответственного проверяющего в пулл-реквест в системе контроля версий для рецензирования исходного кода.
В итоге, конечно, автоматический тимлид был разделен на бизнес-логику и общую библиотеку интеграций (я назвал ее Flower). О последней давай и поговорим.
Зачем оно вообще всем
Зачем мне понадобилось такое программное обеспечение, в целом понятно, но зачем же оно тебе?
Допустим, по счастливому стечению обстоятельств, у тебя есть учетные данные некоего пользователя Jira. Очень хочется на память сохранить пару тысяч задач этого пользователя (а заодно и всю его почту), чтобы развлекаться серыми осенними вечерами, почитывая занимательные комментарии. Почему бы не сделать это, написав три строчки кода?
Или ты менеджер проекта по разработке программных продуктов и решил устроить своим программистам полный KPI, замеряя производительность людей в условных пулл-реквестах на задачу. Собирать статистику с двух и более разных систем одновременно тоже можно без технических проблем.
Как вариант, ты желаешь перенести все задачи из GitLab и Jira на GitHub (потому что твой проект резко стал опенсорсным), а заодно настроить автоматическую пересылку сообщений из почты в Slack. И даже это делается не очень сложно!
Почему Clojure
Первый вопрос, который можно было бы мне задать, звучит примерно так: почему ты не выбрал Python, ведь ты с ним знаком десяток лет и он имеет все необходимые библиотеки интеграций? Не имеет. Имеет, конечно, но не все работает так гладко, как SDK на Java, созданный разработчиками систем, с которыми мы интегрируемся. А в некоторых случаях еще и не хватает многопоточности. Тут нужно заметить, что для первоначального прототипа я, конечно же, и выбрал Python, а точнее HyLang (Lisp на его стеке), но в итоге решил внимательнее присмотреться к JVM.
И второй очевидный вопрос — почему вообще Lisp? Потому что, насколько мне известно, лучший способ создать свой DSL, не изобретая новый синтаксис, — взять Lisp, проверенный временем язык с префиксной нотацией и веселыми скобочками (на самом деле это называется S-выражения), и, используя макросы, обогатить его до домена использования.
Совокупность этих двух факторов и побудила меня выбрать Clojure как язык программирования и платформу для интеграций с различными системами.
Show me the code
Чтобы начать писать на Clojure свой скрипт или даже целую систему, использующую Flower, необходимо для начала установить Leiningen (для Windows в некоторых случаях это может оказаться немного нетривиальной задачей, поэтому рекомендую взять любую *nix-систему). Если ты вдруг не знаешь Clojure, то рекомендую книгу Clojure for the Brave and True — идеальное пособие для освоения языка за пару вечеров.
Чтобы начать новый проект на базе Flower, можно воспользоваться шаблоном, набрав в терминале
lein new flower my-new-flower-app
При этом в файл project.clj в созданной директории проекта будет добавлена зависимость [flower "0.4.3"] — это метапакет, содержащий почти все необходимое. Для тестового приложения он нам вполне подойдет.
Давай теперь напишем наше приложение. В качестве подопытной системы контроля версий и трекера задач воспользуемся GitHub.
Для аутентификации нам понадобится добавить токен пользователя GitHub, чтобы иметь возможность делать изменения и не быть ограниченными рейтом запросов. Добавь сгенерированный для своей учетной записи токен вместо звездочек в файл .credentials.edn в домашней директории пользователя (~/.credentials.edn) в следующем формате:
<span class="pun">{:</span><span class="pln">token </span><span class="pun">{:</span><span class="pln">github </span><span class="str">"****************************************"</span><span class="pun">}}</span>
Теперь можно начать ставить эксперименты. Для этого в директории проекта запустим REPL из командной строки:
lein repl
Трекеры задач
Подключим необходимые модули и определим наш трекер задач:
(require '[flower.macros] '[flower.tracker.core]) (flower.macros/with-default-credentials ;; Можно без этого макроса, если трекер задач приемлет запросы без авторизации (def pt-github-tracker (flower.tracker.core/get-tracker "https://github.com/PositiveTechnologies/flower")))
Давай посмотрим, что теперь содержится в определении pt-github-tracker. Для наглядности я сделал pretty-вывод и убрал не очень важные сейчас поля.
my-new-flower-app.core=> pt-github-tracker #flower.tracker.github.tracker.GithubTracker{ :tracker-component #flower.tracker.core.TrackerComponent{ :auth {:github-token "****************************************"} :context {}} :tracker-name :github-github.com-flower :tracker-url "https://github.com/PositiveTechnologies" :tracker-project "flower"}
Функция get-tracker подставила авторизационные данные и развернула строковый адрес трекера задач в запись с общим протоколом flower.tracker.proto.TrackerProto. Если тип трекера не определился верно, то тип записи будет flower.tracker.default.tracker.DefaultTracker. Это поведение можно изменить, эксплицитно указав тип трекера с помощью макроса flower.tracker.core/with-tracker-type.
Получить все задачи из трекера можно следующей командой:
(def tasks (.get-tasks pt-github-tracker))
Самостоятельно можешь взглянуть, из каких полей состоит, например, первая задача в трекере после выборки, с помощью команды (first tasks), а затем давай сделаем полную выборку в формате JSON. Зависимость clojure.data.json приехала к нам вместе с метапакетом flower, поэтому весь оставшийся код будет выглядеть так:
(require '[clojure.data.json]) ;; Функция сериализации, которую мы используем для удаления ;; полей :tracker и разыменования дополнительных невыполненных ;; полей (например, комментарии) (defn serialize-task [task] (reduce-kv (fn [self k v] (if (= k :tracker) self (assoc self k (if (delay? v) @v v)))) {} task))
;; Печатаем результирующий JSON
(clojure.data.json/pprint {:tasks (map serialize-task tasks)})
Из интересных вещей, которые содержит в себе библиотека Flower при работе с трекерами задач, можно выделить еще изменение полей задачи. Делает��я это двумя строчками кода (и требует прав на запись):
;; Получаем первую задачу из выборки
(def first-task (first tasks)) ;; Обновляем поля заголовка и тегов (.upsert! (assoc first-task :task-title "New title" :task-tags ["sometag"]))
На этом месте, конечно, вывалится ошибка о невозможности что-либо записать в репозиторий, если не хватает на это прав: RequestException Must have admin rights to Repository. (403). Но если вдруг хватает, то для первой задачи из выборки будут фактически обновлены заголовок и теги, а результатом вернется обновленная запись.
Системы контроля версий
С системами контроля версий абсолютно аналогичная история:
(require '[flower.macros] '[flower.repository.core]) (flower.macros/with-default-credentials (def pt-github-repo (flower.repository.core/get-repository "https://github.com/PositiveTechnologies/flower")))
Знакомо, не правда ли? Во время разработки библиотеки я старался сделать так, чтобы определения для разных типов систем были похожи между собой и чтобы из названий функций было понятно их назначение.
Посмотреть, какие функции поддерживаются протоколами различных систем, можно здесь (однако идея записей в том, что обращаться к их полям можно и напрямую, минуя вызовы этих функций в протоколах; так что, если для какого-то поля не определена функция выборки, его можно выбрать из записи через функцию get).
Полистаем теперь пулл-реквесты из репозитория:
(def prs (.get-pull-requests pt-github-repo))
;; Получаем первый пулл-реквест из выборки
(def first-pr (first prs))
Для выбранного пулл-реквеста выведем на печать все комментарии и счетчики (например, количество комментариев LGTM):
(println (.get-comments first-pr)) (println (.get-counters first-pr))
Если вдруг у тебя есть права на запись в репозиторий, то можно смерджить пулл-реквест. Мерджить будем, только если один из комментариев содержит слово LGTM:
(when (> (get (.get-counters first-pr) :count-lgtms) 0) (.merge-pull-request! first-pr))
Системы обмена сообщениями
Пора программно почитать рабочую почту! Для этого нужно добавить в созданный раньше ~/.credentials.edn запись аккаунта Exchange. После добавления учетных данных файл может выглядеть, например, так (в примере поле :login содержит пользователя Exchange, :password — его пароль, :domain — домен Active Directory и :email — почтовый адрес пользователя):
{:token {:github "****************************************"} :account {:login "jdoe" :password "************************" :domain "EXAMPLE" :email "[email protected]"}}
Так как метапакет не включает в себя зависимости конкретных реализаций мессенджинговых систем, то каждую нужно указывать в project.clj эксплицитно. Для Exchange необходимо подключить пакет [flower/flower-integration-exchange "0.4.3"]. Итоговый файл проекта после этого будет выглядеть, например, так:
(defproject my-new-flower-app "0.1.0-SNAPSHOT" :description "FIXME: write description" :url "http://example.com/FIXME" :dependencies [[org.clojure/clojure "1.9.0"] [flower "0.4.3"] [flower/flower-integration-exchange "0.4.3"]] :main ^:skip-aot my-new-flower.core :target-path "target/%s" :profiles {:uberjar {:aot :all}})
Перезапустим REPL, закрыв его и повторно из директории проекта выполнив команду lein repl. После этого можно приступить к подключению библиотек и определению почтового ящика:
(require '[flower.macros] '[flower.messaging.core]) (flower.macros/with-default-credentials (flower.messaging.core/with-messaging-type :exchange (def msg-box (flower.messaging.core/get-messaging))))
Выберем первое сообщение из почтового ящика с загрузкой тела сообщения и отправим его же на другой почтовый ящик (фактически это не пересылка, а отправка нового сообщения с заменой получателя):
(def first-msg (first (.search-messages msg-box {:count 1 :load-body true}))) (.send-message! (assoc first-msg :msg-recipients ["[email protected]"]))
Но еще интереснее настроить автоматическую пересылку писем. Так как пакет clojure.core.async установлен как зависимость метапакета flower, то можно сразу же использовать его для асинхронной обработки входящих сообщений:
(require '[clojure.core.async :as async]) ;; Подпишемся на входящие сообщения (def ch (.subscribe msg-box {:load-body true})) ;; Создаем сопрограмму для вывода на печать ;; и автоматической пересылки сообщений (async/go-loop [] (when-let [msg (async/<! ch)] (println msg) (.send-message! (assoc msg :msg-recipients ["[email protected]"])) (recur)))
Теперь при запущенном приложении с подобным кодом все входящие сообщения будут отправляться на другой почтовый ящик сразу же, как только будут получены.
Заключительное слово
В статье я показал лишь небольшую часть примеров применения библиотеки интеграции. С этими знаниями уже можно писать небольшие сценарии ad hoc. А после погружения в Clojure можно писать и готовый программный продукт.
В моих планах — добавить в библиотеку поддержку большего числа сервисов. YouTrack, HipChat, Telegram и некоторые другие системы ждут своей очереди на добавление. Поскольку проект опенсорсный, я предлагаю тебе присоединиться любым доступным способом: даже простой фидбек в виде Issue на «Гитхабе» будет очень ценным для меня. А заведенный автоматизированно, с использованием самой библиотеки — вдвойне!