Таск-раннер nest на rust: как я заменил громоздкий makefile и bash-скрипты

Хватит страдать с Makefile и бесконечными bash-скриптами: как родился свой таск-раннер на Rust

Иногда просто хочется писать фичи. Но вместо этого день уходит на изучение того, почему в очередной раз не сработал `make deploy`, куда пропал аргумент в bash-функции и какой именно `deploy.sh` нужно запускать на этом проекте. Файлы с задачами разрастаются до монструозных размеров, а любой правке в Makefile начинает предшествовать короткий сеанс дыхательных упражнений.

В какой‑то момент я поймал себя на том, что трачу больше времени на «оркестрацию» задач, чем на саму разработку. И понял: так больше нельзя. Хотелось инструмента, который помогает, а не наказывает за каждую лишнюю пробел/табуляцию.

Так появился Nest — свой таск‑раннер, написанный на Rust. Идея была простой: сделать утилиту, которая заменит громоздкий Makefile, груду shell-скриптов и при этом будет выглядеть так же аккуратно и логично, как хорошо спроектированный код.

Почему Make больше не спасает

Make — легенда. Ему почти полвека, он пережил смену поколений ОС и языков. Но исторически он создавался под одну задачу: собирать C-проекты, отслеживать зависимости и пересобирать только то, что изменилось.

Мы же в 2026 году используем его для всего подряд:

- запуск Docker-контейнеров;
- выкатывание релизов на staging/production;
- прогоны линтеров и форматтеров с десятком флагов;
- миграции, сиды, ресеты баз данных;
- локальные dev-сервера с хот-релоадом.

Make это умеет… формально. Но это выглядит как бесконечный список одноуровневых целей со странным синтаксисом, где малейший неверный отступ ломает всё. Табы вместо пробелов, магические переменные, сложные выражения с `$(shell ...)`, которые никто не хочет трогать.

В итоге у нас либо один огромный Makefile, в который страшно заглядывать, либо хаотичный зоопарк `build.sh`, `deploy.sh`, `migrate.sh` и прочих скриптов. Каждый из них когда‑то «быстро накинули под задачу», и с тех пор все боятся их редактировать.

Just оказался не конечной остановкой

Разумеется, идея «нормального Make-подобного таск-раннера» приходила в голову не мне одному. Есть, например, Just. Он удобнее:

- не требует табов;
- адекватно работает с аргументами;
- лучше документирован;
- его синтаксис проще и человечнее.

Некоторое время я жил с Just и был доволен. Но довольно быстро столкнулся с тем же эффектом: Justfile начал пухнуть.

Главная проблема и Make, и Just — это плоская структура. Все команды лежат в одном уровне:

- `db-reset`,
- `test-frontend`,
- `deploy-production`,
- `deploy-staging`,
- `lint-backend`,
- `lint-frontend`.

Через месяц весь этот список выглядит как длинный чек-лист в супермаркет: всё в кучу, логики и иерархии почти нет.

Хотелось чего‑то более естественного:
чтобы можно было писать:

- `nest db reset`;
- `nest dev start`;
- `nest build frontend`;
- `nest deploy staging`.

То есть обращаться к задачам так же, как к модулям и функциям в коде: по логичной иерархии, а не по случайным названиям с дефисами.

Каким должен быть нормальный таск‑раннер

Сформировалось несколько чётких требований к инструменту, которым я сам буду пользоваться ежедневно:

1. Читабельный DSL без криптографии
Минимум символов вроде `$()`, `${VAR}`, `&&` и прочего шума. Конфиг должен читаться как текст, а не как результат компрессии bash-скрипта.

2. Иерархия и вложенность
Команды должны группироваться по смыслу. `dev.start`, `dev.stop`, `db.migrate`, `db.reset`, `deploy.staging`, `deploy.production` — и всё это вызывалось бы через `nest dev start` и т. п.

3. Типизированные параметры
Если задача ждёт число — это должно быть числом, а не строкой, которую надо парсить в shell. Если булево — то булево, а не `yes/no` или `0/1` в случайной интерпретации.

4. Умное поведение из коробки
- подхват переменных окружения, в том числе из `.env`;
- шаблонизация значений;
- автогенерация help по структуре файла;
- предсказуемое выполнение и понятные ошибки.

5. Независимость от окружения
Один бинарник без внешних зависимостей. Положил в проект или поставил в систему — и всё, он работает. Никакого Node.js, Python, npm-пакетов и прочей инфраструктуры «по кругу».

6. Производительность и безопасность
Отсюда выбор Rust:
- быстрота старта и выполнения;
- статическая линковка (через musl) — удобно таскать бинарь куда угодно;
- жёсткая типизация и безопасность по памяти.

Так родился Nest

Nest — это таск-раннер с собственным DSL, основанным на отступах. Вдохновлён идеями Python и YAML: структура задаётся пробелами, без лишних скобок и «магии» синтаксиса.

Конфигурация представляет собой дерево пространств имён (namespaces) и задач.

Примерно вместо этого:

```make
db-reset:
docker compose exec db dropdb ...
docker compose exec db createdb ...
docker compose exec db migrate ...
```

хочется писать нечто вроде:

```text
db:
reset:
run: |
docker compose exec db dropdb ...
docker compose exec db createdb ...
docker compose exec db migrate ...
```

А в терминале — запускать это как:

```bash
nest db reset
```

Вложенность и пространства имён

Именно иерархия — одна из ключевых фич Nest.

Вместо:

- `test-frontend`
- `test-backend`
- `test-e2e`

можно иметь:

```text
test:
frontend:
run: npm test -- --watch=false
backend:
run: cargo test
e2e:
run: playwright test
```

И запускать:

```bash
nest test frontend
nest test backend
nest test e2e
```

Автокомплит для оболочек (bash, zsh, fish) генерируется автоматически на основе структуры конфигурации: вы начинаете набирать `nest test` — и сразу видите все подкоманды. Это убирает ту самую «памятку в голове»: как же называлась команда — `test-fe`, `frontend-test` или ещё как-нибудь.

Работа со списками и циклами

Типичный кейс — монолитный проект с несколькими сервисами: `api`, `worker`, `frontend`, `scheduler`. Часто нужно прогнать одну и ту же задачу по всем сервисам: запустить, перезапустить, собрать, проверить.

В классическом Make это превращается в пляски с `foreach`, подстановками и внешними shell-конструкциями. В Nest предусмотрены нативные механизмы для работы с коллекциями и циклами: можно описать список сервисов и одну задачу, которая пробегается по ним.

Структурно это выглядит как декларация:
«для каждого сервиса сделай вот такой запуск».

В результате конфиг остаётся компактным, а логика — явной, без череды почти одинаковых целей `start-api`, `start-worker`, `start-frontend` и так далее.

Сложные сценарии деплоя с проверками

Реальный деплой — это почти никогда не одна команда. Обычно это:

1. Проверка, что мы на правильной ветке.
2. Подтверждение от человека: точно ли деплоим.
3. Сборка артефактов.
4. Бэкап базы и/или данных.
5. Заливка на сервер.
6. Прогон миграций.
7. Отправка уведомления об успехе или обвале.

В башевых скриптах это превращается в длинную портянку с `if`, `read`, `||` и ручной обработкой кодов возврата. Любая мелочь, добавленная через пару месяцев, легко ломает сценарий в неожиданном месте.

В Nest подобные вещи описываются как последовательность шагов, где явно видно:

- что выполняется;
- в каком порядке;
- какие проверки происходят между шагами;
- что делать, если какой-то шаг упал.

Fallback: автоматическая обработка ошибок

Один из самых приятных моментов — встроенные fallback-сценарии.

Частая практика в shell:

```bash
backup || ./alert_failure.sh
```

Каждую команду приходится «страховать» ручной проверкой. В Nest можно задать правило:
если этап `backup` завершился неуспешно — выполнить задачу `alert_failure`.

То есть обработка ошибок становится частью декларативной конфигурации, а не россыпью `|| ...` и `set -e` по всему скрипту. Это и проще читать, и легче поддерживать, и меньше риск случайно забыть обработать неудачный сценарий.

Глобальные переменные и функции

В реальных проектах огромное количество команд использует одни и те же значения:

- путь к репозиторию;
- адрес сервера;
- имя Docker-образа;
- версия приложения;
- имя базы и пользователя;
- общие флаги для линтеров, тестов, билдов.

В Nest есть глобальные переменные и переиспользуемые функции, которые позволяют написать всё это один раз и применять в разных задачах.

Благодаря этому конфиг остаётся DRY:

- меняете имя docker-образа в одном месте — и обновляется весь пайплайн;
- переиспользуете один и тот же шаг сборки в нескольких задачах;
- общий набор аргументов для команд не приходится повторять в каждой строке.

Модульность через include

Когда Nest-конфиг дорастает до десятков и сотен задач, держать его в одном файле — мучительно. Да и организационно это неудобно: разным командам нужны разные группы задач.

В Nest есть механизм разбиения конфигурации на модули и подключения их через `@include`.

Это позволяет, например:

- выделить `db.nest` для всего, что касается базы;
- `deploy.nest` — для сценариев выката;
- `dev.nest` — для локальной разработки;
- `ci.nest` — для прогонов в CI/CD.

Все они собираются в единое дерево команд, но логически и физически разделены. Командам проще договориться: каждый отвечает за «свой» кусок инфраструктуры, не мешая остальным.

Что происходит под капотом

Nest не просто тупо прогоняет строки. Под капотом он:

1. Парсит собственный DSL и строит из него абстрактное дерево команд.
2. На основе этого дерева формирует граф зависимостей: какие задачи требуют запуска других и в каком порядке.
3. Шаблонизирует переменные вида `{{variable}}`, подставляя значения из:
- окружения;
- `.env` файлов;
- локальных и глобальных объявлений внутри Nest-конфига.
4. Исполняет команды, контролируя успех/ошибки и при необходимости вызывая fallback-сценарии.

Rust даёт возможность сделать это очень быстро и предсказуемо: даже большой конфиг обрабатывается практически моментально, а ошибки в описании задач детектируются на стадии парсинга, с понятными сообщениями.

Почему не npm scripts, Gulp, Bash и прочие

В роли таск‑раннеров часто выступают совсем другие инструменты:

- npm scripts
Отлично подходят для маленьких JS-проектов, где всё крутится вокруг Node. Но как только нужно описать сложную оркестрацию — начинается ад из `&&`, `||`, `cross-env`, кавычек и экранирования. Плюс они жёстко привязаны к Node-миру.

- Gulp/Grunt
Мощные, но снова JS-only и требуют наличие вокруг стека Node.js, dependency management и прочих радостей. Заводить это ради того, чтобы один бинарник запускал docker-команду и пару скриптов — избыточно.

- Bash-скрипты
Максимально универсальны, но:
- слабая читаемость на больших объёмах;
- отсутствие типов;
- легко допустить неочевидную ошибку;
- бедный встроенный help;
- высокая зависимость от среды (разные версии bash/sh, разные утилиты).

Nest решает именно эту задачу: быть компактным, читаемым и в меру «умным» слоем над реальными командами, который не привязан к конкретному языку, экосистеме или фреймворку.

Расширение для редактора

Работать с DSL вслепую неудобно, поэтому у Nest есть полноценное расширение для VS Code:

- подсветка синтаксиса;
- базовая навигация по структуре;
- сниппеты для часто используемых конструкций;
- помощь при написании новых задач.

Это делает конфиг не «магическим текстом», а полноценным рабочим артефактом проекта, с которым комфортно взаимодействовать ежедневно.

Где Nest особенно полезен

Хотя таск‑раннер в целом универсален, есть несколько сценариев, где он особенно заходит:

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

2. Команды с разными языками и стеками
Когда в одном проекте живут Rust-сервисы, Node-приложения, Python-скрипты и, например, Terraform — нужен единый слой оркестрации, не привязанный к конкретному языку.

3. Инфраструктурные пайплайны
Миграции БД, подготовка окружений, синхронизация данных, бэкапы, деплои. Всё это удобно собирать в иерархию задач с fallback-сценариями.

4. Developer Experience
Новому разработчику достаточно открыть один конфиг, посмотреть список доступных команд и их структуру, а дальше использовать автокомплит и help. Порог входа в проект сильно снижается.

Практические советы по миграции с Make/Just на Nest

Если вы задались целью избавиться от огромного Makefile или разросшегося Justfile, полезно двигаться по шагам:

1. Выделите группы задач
Разбейте все цели на логические блоки: `dev`, `db`, `deploy`, `test`, `lint` и т. д.

2. Сначала перенесите только верхний слой
Не пытайтесь переписать весь пайплайн за один день. Начните с пары ключевых сценариев: запуск dev-сервера, базовый деплой, прогон тестов.

3. Подумайте об именовании и иерархии
Сделайте структуру такой, чтобы она была очевидна даже человеку, который впервые видит проект. Это окупится уже через пару недель.

4. Выделяйте общие части в функции и переменные
Как только увидели дублирование — выносите. Nest как раз для этого и придуман.

5. Оставьте старые скрипты как fallback
На время перехода можно держать и Makefile, и Nest-конфиг, постепенно перенося команды. Как только новая конфигурация покрывает подавляющее большинство задач — старое можно смело выкидывать.

Итог: живое MVP, которое уже можно использовать

Nest начинался как личный эксперимент «на выходные», чтобы перестать воевать с Makefile и bash-скриптами. По факту из него вырос полноценный инструмент:

- с читаемым DSL на отступах;
- с иерархией команд и неймспейсами;
- с типизированными параметрами;
- с шаблонизацией и работой с окружением;
- с fallback-сценариями при ошибках;
- с модульностью через include;
- с поддержкой автокомплита и синтаксической подсветкой.

Формально это всё ещё MVP, но уже вполне боеспособное: его можно брать и использовать в реальных проектах, постепенно выдавливая из них хрупкие Makefile и ветхие bash-скрипты.

Если вы устали открывать очередной `deploy.sh` с мыслью «лучше бы я этого не видел», возможно, вам тоже пора завести свой аккуратный таск‑раннер — или хотя бы попробовать Nest и посмотреть, как себя чувствует проект, когда инфраструктурные команды превращаются из хаотичного списка в стройное дерево задач.

3
2
Прокрутить вверх