Команда игнорировала линтеры и я написал свой нотификатор

Введение

Привет! Меня зовут Иван, и я автор проекта «Код на салфетке» — небольшой команды, в которой мы совмещаем написание обучающих статей, коммерческую разработку и open source.

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

Предпосылки

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

Это доставляло массу неудобств:

  • Приходилось чаще проверять статусы CI/CD;

  • Возникала лишняя ругань.

Так родилась идея найти «оповещатор» о статусе воркфлоу прямо в Telegram-чат. Через некоторое время после появления идеи — когда без такого инструмента уже нельзя было обходиться — появился Actions Telegram Notifier. Наш собственный Action для GitHub/Gitea Actions.

Поиск решения

Когда возникла потребность, я сразу начал искать готовые решения. В GitHub Actions Marketplace их действительно много. Но, внимательно изучив все варианты — выяснил, что ни одно из них полностью не соответствует нашим требованиям.

Я выделил такие требования к готовому решению:

  • Поддержка тем в супергруппах;

  • Информативность;

  • Кастомизируемость.

Сравнение с «конкурентами»:

  • Поддержка тем в супергруппах. В наших проектах мы используем Telegram-супергруппы, в которых создаём темы под разные проекты. Почти все готовые решения поддерживают отправку уведомлений только в обычные чаты (или личку).

    Пример организации супергруппы в Telegram:

    69413035c5b2198587f1e7120d8e86cd.png
  • Информативность. Оповещения должны быть информативными, но не перегруженными. Большинство готовых решений предлагают одну строку текста с описанием события — и всё.

    Пример встроенного в GitLab оповещения:

    a2918d5d518d3e73ed6949589c3eaf30.png

    Пример оповещения нашего Action:

    898adaa44a219bce8b12bc1cc2b35a3d.png
  • Кастомизируемость. Большая часть решений не позволяет управлять содержанием уведомления: ни убрать/добавить информацию, ни вставить свой текст. На скриншоте выше, например, строка "job: pre-commit linters" — это как раз пример пользовательского текста. При желании, можно вообще оставить только поле со статусом и кнопкой на коммит.

Кандидатов нет — пишем своё!

В итоге я оказался в классической ситуации: «ничего из готового не покрывает хотелки». Оставался один путь — писать свой Action.

Как писать свой экшен? Нужно создать специальный файл action.yml, в котором описываются параметры, способ запуска и прочие детали экшена.

b1ddebfb5b2cebfca3f78a8ae77d2358.png

На чём писать экшен? Тут тоже всё просто — на чём угодно! Можно на Python, JS или любом другом языке — главное, чтобы код запускался в среде раннера. Я выбрал Rust. На тот момент это было моё первое знакомство с языком, и без сложностей не обошлось.

Как запускать экшен? Если взглянуть на блок runs на скрине выше, есть два варианта:

  1. Непосредственно в среде раннера. Обычно так запускаются экшены на JS, которым достаточно Node.js, уже установленного в среде.

  2. С помощью Docker-образа. Это заранее собранный образ, указанный в репозитории экшена. Я выбрал этот вариант, поскольку Rust — компилируемый язык.

Разработка экшена

Разработка началась с изучения матчасти, а именно:

  • Как работают экшены?

  • Как их писать?

  • Какие данные о воркфлоу доступны внутри раннера?

  • С чем едят Rust?

  • И десятки других вопросов.

Часть из них я уже затронул в предыдущем блоке, но самый важный — про данные внутри раннера. Нужно было чётко понимать, что именно мне доступно для работы.

Поскольку используемая нами Gitea почти полностью совместима с GitHub, начал изучение с документации GitHub Actions. К счастью, у GitHub в этом плане всё отлично — документация подробная и понятная.

Первая версия

Первая версия была написана «на коленке» — быстро и без лишних заморочек.

Кому интересно, вот последний коммит той самой первой версии: ссылка на коммит

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

Логика была простая:

  1. Получаем данные из окружения и складываем в словарь;

  2. Формируем текст сообщения;

  3. Отправляем его в Telegram.

Весь экшен умещался примерно в 215 строк кода в одном main.rs.

В первой версии, в тексте оповещения я выделил следующие поля:

  • Workflow status — статус выполнения воркфлоу, получаемый из {{ job.status }};

  • Actor: инициатор запуска воркфлоу;

  • Repository: название репозитория в котором запустился воркфлоу;

  • Branch: название ветки в которой запустился воркфлоу;

  • Commit Message: сокрашённое до первой строки сообщение коммита;

  • Message и Footer: дополнительные текстовые поля.

Скриншот оповещения первой версии:

1bd6f46e4c27e3b13aced41684baed2c.png

Минусы первой версии

Любое отклонение в структуре данных могло «сломать» отправку. Всё потому, что я завязался не на реальные данные раннера, а на переменные окружения —, а они зависят от типа события. В итоге экшен нормально работал только при событии push.

Второй минус — неподдерживаемость. Двести строк кода в main.rs было тяжело расширить или адаптировать под другие события.

Ну и если говорить честно — это был далеко не лучший код. Сделаем скидку: это было моё первое знакомство с Rust =)

Вторая версия экшена

Спустя некоторое время, уже после того как я немного разобрался в Rust и написал ReBack (и статью про него), появилась новая потребность — добавить уведомления о Pull Request.

Собравшись с мыслями, я решил полностью переписать экшен.

Ссылка на актуальный репозиторий

При переработке проекта я учёл все прошлые недочёты и начал с самого важного — архитектуры. Теперь всё разбито на логические модули. Вместо словаря с данными используются структуры и энумы.

a6d669eaa73f3d75c987d35e192a053a.png

Часть параметров по-прежнему берётся из переменных окружения — например, токен бота или дополнительный текст. Но основные данные я теперь получаю из файла /github/workflow/event.json. В нём удобно и структурированно лежит вся необходимая информация. Более того, там есть данные, которых нет в стандартных переменных окружения.

61ec9c30545c1abbd6bf99981031bb48.png

Это открывает большие возможности — теперь при необходимости можно легко:

  • добавить новые поля в уведомления;

  • обрабатывать новые события.

Основной алгоритм работы остался прежним — собираем данные, формируем сообщение, отправляем в Telegram.

Во второй версии текст оповещения тоже претерпел некоторые изменения:

  • В поле Actor теперь ссылка на инициатора;

  • В поле Repository также теперь ссылка на репозиторий;

  • Для события Pull Request создаётся кнопка со ссылкой на PR;

  • Для события Pull Request отображается поле PR Title с заголовком PR’а и ссылкой на него.

Добавить ссылки позволило чтение данных из файла event.json.

Пример оповещения о пуше:

60c02eb0b7fa4ac678ff98c0ef1fc41c.png

Пример оповещения о Pull Request:

698d860bd8b95421424a8db34e4eeba3.png

Сборка образа экшена

Напомню: Rust — это компилируемый язык и с этим связанна одна особенность…

При запуске экшена в воркфлоу он начинал компилироваться прямо на раннере. Это тратит время и ресурсы впустую.

Чтобы решить эту проблему, я настроил сборку Docker-образа прямо в репозитории проекта.

abbcb749e20a6f56591ff41b9b6e3459.png

Теперь экшен использует уже собранный образ, а не компилируется каждый раз при запуске. Всё стало быстрее и стабильнее.

Вклад от сообщества

Неожиданно, ещё в первой версии кто-то открыл issue. Это был пользователь Kastov, который предложил улучшить текст уведомления. После того как я внёс это изменение, он прислал ещё и полноценный Pull Request с рядом улучшений.

А уже после выхода второй версии обратился ZAlexanderV. Он столкнулся с тем, что экшен поддерживал только события Push и Pull Request, а ему нужно было ещё и Workflow Dispatch.

Сложность добавления Workflow Dispatch заключалась в его кастомной природе. Я не могу заранее предусмотреть все возможные поля, которые туда передаются, — каждый проект настраивает это по-своему. Пришлось искать компромисс между универсальностью и поддержкой пользовательских параметров.

Пример оповещения на событие Workflow Dispatch:

1617784b7d05367133b852e7cb4937b7.png

Меня удивляло то, что этим проектом пользуются другие люди и когда я видел открытый issue, честно, даже немного нервничал. Мне было приятно, когда Kastov внёс некоторые изменения и интересно добавлять возможности для ZAlexanderV. Наверное, ради таких эмоций и стоит заниматься опенсорсом.

Пример использования

Использовать экшен очень просто — достаточно добавить в существующий workflow следующий шаг:

- name: Run Telegram Notify Action  
  uses: proDreams/actions-telegram-notifier@main  
  with:  
    token: ${{ secrets.TELEGRAM_BOT_TOKEN }}  
    chat_id: ${{ secrets.TELEGRAM_CHAT_ID }}  
    status: ${{ job.status }}  
    notify_fields: "actor,repository,workflow,repo,commit"  
    message: "Docker image successfully built and pushed to registry."  
    footer: "Workflow completed"

Обязательно добавьте в secrets два поля:

  • TELEGRAM_BOT_TOKEN — токен Telegram-бота

  • TELEGRAM_CHAT_ID — ID чата, куда будут приходить уведомления

Поле status оставляем как есть — оно нужно, чтобы в уведомлении отображался актуальный статус выполнения джоба.

Остальные параметры — опциональные. Вы можете кастомизировать их под свои задачи.

Подробное описание всех доступных полей — в README.md репозитория.

Что дало это команде?

У меня нет каких-то конкретных цифр, которые обычно пишут в таких постах — типа «Продуктивность выросла на 80%!!!».

Но могу точно сказать по ощущениям: мы стали быстрее реагировать на проблемы со сборкой или тестами. Раньше нужно было открывать репозиторий и следить за процессом вручную — это было неудобно и отнимало время.

Теперь всё иначе: сделал задачу, запушил изменения — и можно идти дальше. Когда воркфлоу завершится (или упадёт), в Telegram сразу приходит уведомление.

То же самое с Pull Request«ами: не нужно больше писать «я там PR открыл» — достаточно просто создать его, и все увидят уведомление в чате.

Заключение

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

С его помощью я:

  • глубже разобрался в механике GitHub/Gitea Actions;

  • прокачал навыки в Rust, особенно в работе с архитектурой и обработкой внешних данных;

  • на практике увидел, как важны хорошие уведомления для быстрой реакции и прозрачности рабочих процессов;

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

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

Обсудим, подумаем, возможно — добавим!

Подписывайтесь на наш Telegram‑канал «Код на салфетке», у нас много интересного как для новичков, так и профессионалов!

© Habrahabr.ru