@mikitasazan/notify
v1.16.0
Published
Единая типизированная отправка Telegram-уведомлений (форум-темы, маршрутизация, ретраи) для всех проектов
Readme
@mikitasazan/notify
Единая типизированная отправка Telegram-уведомлений: форум на проект,
внутри вкладки «⚙️ Ops» (роботы) и «💬 Dev» (люди), один бот, семь типов
событий. Заменяет 18 разных имён переменных и 4+ независимых реализации
jq | curl, что были в playhub, arvent, game-publisher до 26.07.2026.
Почему форум на проект, а не один общий форум с темой на проект. Второе пробовали первым — не работает: Telegram не умеет закрывать отдельную тему от участника, кто в группе, видит все вкладки. Значит сотрудников в общий форум не пустить, им нужен свой чат — и проект начинает жить в двух местах сразу («тема у владельца» + «канал у команды»). Форум на проект убирает дублирование: сотрудник добавляется в форум своего проекта, чужих не видит, схема одна для всех.
Идея в одном абзаце
notify() принимает объект (NotifyEvent), а не строку — «своё» сообщение
написать нельзя. Один рендерер на тип события гарантирует одинаковый каркас:
эмодзи Заголовок · проект, затем ключ: значение, затем ссылка. Маршрут
«куда слать» вычисляется из --project и severity события — добавить новый
проект значит дописать одну строку в src/routes.ts, больше нигде ничего
заводить не нужно.
Ноль рантайм-зависимостей. Composite-action и консьюмеры не собирают ничего:
Node 22.18+ / 24+ исполняет .ts напрямую (проверено на macOS и на проде
playhub, Node 22.22.2), в GitHub Actions ставить нечего. Релиз пакета в npm
собирается в dist (prepare → tsc) — поэтому npm-консьюмеры получают
скомпилированный JS и типы.
Установка
npm install @mikitasazan/notifyСекрет один — OPS_BOT_TOKEN. Взять из vault (vault get
notify.OPS_BOT_TOKEN) или из GitHub secrets репозитория.
Из TypeScript
import { notify } from '@mikitasazan/notify';
await notify({
type: 'report',
project: 'playhub',
title: 'Сводка за день',
period: '26 июля',
lines: [['Игр в каталоге', 1284], ['Ошибок PM2', 3]]
});Из bash (сервер, cron, ноутбук)
notify job --project playhub --job "Импорт игр" --status ok --stat "добавлено=5"
trap 'notify job --project playhub --job "Импорт игр" --status fail --note "лог: $LOG"' ERR
notify report --project playhub --json < payload.json # весь объект со stdinКод возврата всегда 0 — уведомление не имеет права уронить вызвавший его процесс. Все ошибки — в stderr.
Из GitHub Actions
- uses: mikitasazan/[email protected] # версию пинит propagate
if: always()
with: { event: deploy, project: playhub, status: '${{ job.status }}' }
env: { OPS_BOT_TOKEN: '${{ secrets.OPS_BOT_TOKEN }}' }status: ${{ job.status }} (success/failure) автоматически маппится в
ok/fail — не нужно двух шагов с if: success()/if: failure().
Типы событий
| Тип | Когда | Обязательные поля |
|---|---|---|
| deploy | выкатка кода | project, status |
| job | регулярная задача (импорт, бэкап) | project, job, status |
| report | сводка с цифрами | project, title, lines |
| ci | итог CI на master | project, status |
| pr | событие пул-реквеста | project, action, number, title |
| issue | событие задачи | project, action, number, title |
| incident | что-то встало и ждёт тебя прямо сейчас: приложение, сейф, рабочая сессия на маке | project, title |
| session | УСТАРЕЛ, это incident: карточка выходит с тегом #incident, имя собирается из action — Claude session is burning the limit | project, action |
| heartbeat_miss | УСТАРЕЛ, молчание — это job --status silent | project, job |
Скобка у слова типа говорит одним словом, чем кончилось: Deploy (OK),
CI (Fail), Job (Off), Job (Silent), Issue (Assigned), PR (Merged).
Она стоит там, где у типа исходов больше одного. У incident состояние одно,
поэтому его скобка называет МЕСТО, где горит: Incident (Vault),
Incident (Session) — слово из --scope, без него имя проекта. У report
исхода нет вовсе, и скобку занимает день: Report (2026-08-23 / 2026-08-22).
У job есть ещё два необязательных флага: --via — где задача крутилась
(mac, vps, actions), это идёт строкой Via: Mac первой под второй
строкой; и --took — сколько прогон занял, твоими же словами (4m 12s),
строкой Took: под причиной. Не передал — строки нет.
Отдельного вида «файл» нет: --path применим к ЛЮБОМУ событию, и тогда
карточка едет подписью к вложению (подпись у Telegram ограничена 1024 знаками,
а не 4000). Слово file осталось псевдонимом и собирает report с вложением —
старый отправитель не замолкает.
Полные сигнатуры — src/events.ts.
Ключ задачи. Первая строка каждой карточки — три тега: #тип #ключ
#итог.
По ним дневной разборщик сверяет «это 🔴 уже закрыто более поздней карточкой
той же задачи?» без сравнения человеческих формулировок. Явный --key
побеждает; без него ключ выводится из заголовка и меняется вместе с ним —
регулярный отправитель передаёт --key явно. В stdout CLI ключ не попадает.
Свободного HTML в пакете нет. Дверь sendReport() удалена 25.08.2026:
она отдавала произвольную разметку и ставила карточке два тега вместо трёх —
единственный вид, который нельзя было отфильтровать по итогу. Последний её
отправитель перешёл на типизированный report ещё 25.08; тип события задаёт
каждый знак карточки, других путей нет.
Неизвестный проект не роняет вызвавший крон (код возврата 0), но больше и
не исчезает молча: в mac-config Ops уходит красная карточка «notify: событие
потеряно». До 18.08.2026 опечатка в --project терялась без следа неделями.
--action у pr: opened, approved, changes_requested, merged,
closed. У issue: opened, assigned, closed. Сырые имена GitHub, которые
означают то же самое, сводятся к ним: reopened, ready_for_review и
review_requested — это всё opened. Неизвестное действие — ошибка разбора, а
не молчаливая подмена: иначе «запрошены правки» приехали бы как «открыт».
Из GitHub в «⚙️ Ops» уходит не всё, что там происходит. Общий workflow
.github/workflows/ops-notify.yml отбрасывает ready_for_review и
review_requested ещё на входе: оба сообщают про PR, который уже объявлен
открытым, и открытие одного PR давало три карточки подряд. Правило владельца от 27.07.2026 — одна новость, одна
карточка: открыт → вердикт ревью (👍 / 📝) → закрыт или смёржен. Белый список
живёт в общем workflow, а не в подписках проектов, потому что подписка обязана
лежать в вызывающем репозитории и в четырёх проектах расходится сама собой;
политика же должна раскатываться одним тегом.
Новый проект
- В Telegram: создать группу, открыть её → «Изменить» → включить «Темы»,
добавить
@mikita_ops_botадминистратором с правом «Управление темами». Это единственный ручной шаг — Telegram разрешает создавать группы только живому аккаунту, ботом это не сделать. notify setup <chat_id> maphub— заведёт вкладки «⚙️ Ops» и «💬 Dev» и напечатает готовую строку.- Вставить строку в
src/routes.ts(и значение в типProjectвsrc/events.ts), выпустить пакет. - В репозиторий проекта — трёхстрочный
.github/workflows/notify.yml, вызывающийmikitasazan/notify/.github/workflows/ops-notify.yml@v1: CI, деплой, PR и задачи начинают приходить сами. - Закрепить в «⚙️ Ops» легенду значков (пример — в любом существующем
форуме) и, если задача проекта бежит с мака, — добавить её плист под
обёртку
run-scheduled.sh(репозиторий mac-config).
Весь путь — минут десять, единственный ручной шаг — п. 1.
Появились сотрудники на проекте — просто добавь их в форум этого проекта. Ничего не мигрируется и не дублируется: они видят Ops и Dev своего проекта и не видят остальных.
Эволюция схемы событий
Версии схемы нет и не будет. Сообщение живёт секунду и читается глазами — версионировать нечего. Вместо этого одно правило: новое поле у существующего типа — только опциональное; обязательные поля не добавляются никогда, только новый тип события. Тогда старый вызывающий код и новый пакет всегда совместимы.
Тег сменился 18.08.2026 у шести отправителей
Коммит 85564fc в mac-config дал явный --key шести местам вызова, которые
раньше отправляли карточку БЕЗ ключа — тег брался из --job/--title
автоматически (slug(), см. src/render.ts). Без этой таблицы старую
красную карточку в Telegram не найти поиском по новому тегу — она там под
старым.
| Отправитель | Старый тег (авто из русского названия) | Новый тег (из --key) |
|---|---|---|
| arvent-eval-report.sh, отчёт по эвалу | #eval_качество_ответов_бота | #arvent_eval |
| arvent-eval-report.sh, файл диалогов | нет — шёл мимо пакета, сырым sendDocument Bot API | #arvent_eval_dialogues |
| daily-digest.sh, дайджест задач (job) | #дайджест_задач | #daily_digest |
| daily-digest.sh, дайджест задач (report) | #дайджест_задач (то же слово, другой тип-тег: #report вместо #job) | #daily_digest |
| daily-digest.sh, отключённые Actions | #github_actions_выключены | #actions_off |
| daily-digest.sh, Config doctor | #config_doctor | #config_doctor — не изменился: slug() приводит и старое, и новое к одной строке |
Чего в пакете нет и почему
- Очереди, брокера, демона — десятки сообщений в день, ретрай в памяти процесса; доставка уведомления не стоит инфраструктуры, которую саму надо мониторить.
- Редактирования уже отправленных сообщений — требует хранить
message_id, то есть состояние и базу. - Автосоздания темы при первой отправке — сбой мог бы наплодить дублей;
создание — явная команда
notify setup. - Любых зависимостей, сборки, MarkdownV2, мультиязычности, троттла (троттл —
забота вызывающего кода, он есть в
arvent/web/src/server/alert.tsи там и остаётся: работает только внутри долгоживущего процесса, а CLI стартует заново на каждый вызов).
Разработка
npm run typecheck # tsc --noEmit
npm test # node --test src/render.test.tsRelease
Один ритуал для всех общих пакетов (канон — скилл package-ops):
release patch|minor|major # из этого каталогаТесты → бамп + тег → публикация из клона тега → точная версия у всех
потребителей с прогоном их собственных проверок, включая пин uses: в их
workflow. release и propagate живут в mac-config/home/bin/ и доступны из
PATH. Карты потребителей нет: propagate находит их поиском по репозиториям.
