Прямой ответ
У каждого проекта со временем накапливается набор ритуальных команд: установить зависимости, поднять стек, прогнать тесты, собрать, отформатировать, задеплоить. Хранить их в головах и README — значит обречать новичков (и себя через месяц) на археологию. Таск-раннер решает это декларативно: Makefile — стандарт де-факто, есть в каждой Unix-системе; just — современная альтернатива с человеческим синтаксисом, аргументами и кросс-платформенностью. Правило простое: любая команда длиннее двух слов или используемая дважды становится целью make <цель> / just <цель>, а README вместо простыни команд содержит одну строку — «список задач: make help».
Makefile: минимум, который работает
.PHONY: help install test up down fmt
help: ## показать список задач
@grep -E '^[a-zA-Z_-]+:.*?## ' $(MAKEFILE_LIST) | awk -F':.*?## ' '{printf " %-15s %s\n", $$1, $$2}'
install: ## установить зависимости
uv sync
up: ## поднять локальный стек
docker compose up -d db redis
test: ## тесты + линтеры
ruff check . && pytest -q
fmt: ## форматирование
ruff format .
Ключевые детали, которые отличают рабочий Makefile от наспех написанного: .PHONY объявляет цели, не являющиеся файлами (без него цель test «перестанет работать», если в проекте появится файл с именем test); комментарий ## после цели плюс цель help дают само-документирующийся список задач; tab в начале команд обязателен (классическая ошибка — пробелы вместо tab). Make приходит из мира сборки C, но как таск-раннер для любых языков он универсален: цель — просто именованная последовательность команд.
Just: тот же смысл, современный синтаксис
justfile решает исторические шероховатости Make:
# список задач
default: (help)
help:
@just --list
test *ARGS:
pytest -q {{ARGS}}
serve port='8000':
uvicorn app.main:app --port {{port}}
Преимущества перед Make: аргументы целей — первоклассная возможность (just test tests/api -x, just serve 9000) без танцев с переменными; нет tab-ловушек; just --list показывает задачи из коробки; одна и та же рецептура работает на Windows/macOS/Linux (just выбирает shell по настройке). Цена — отдельная установка бинарника (brew install just, cargo install just, winget) против «make уже есть везде».
Что выносить в цели, а что нет
В таск-раннер выносят стабильные точки входа: install, dev/up, test, lint, fmt, build, migrate, seed (тестовые данные), clean. Не выносят: одноразовые команды (проще выполнить), секреты и параметры окружений (их место в .env по правилам статьи про секреты), и гигантские многострочные сценарии — когда команда разрослась, она переезжает в отдельный скрипт, а цель его вызывает. Связка с другими инструментами естественная: цель up вызывает docker compose (стек из статьи про Compose), цель lint дублирует проверку из git-хуков (см. pre-commit) — хуки ловят на коммите, make даёт ручной прогон.
Идемпотентность и порядок
Хорошая цель безопасна при повторном запуске: install доустанавливает недостающее, up не падает на уже поднятом стеке, clean не требует существования артефактов. Вторая привычка — зависимости целей: test: install (make выполнит install перед test), в just — test: (install). Это превращает onboarding в одну команду: новичок клонирует репозиторий и выполняет make (первая цель) — окружение собирается, тесты проходят, проект живой. Команды, которые нельзя сделать идемпотентными (миграции с данными, деплой), помечайте в help предупреждением.
Командная дисциплина
Таск-раннер окупается, когда он обязателен: CI вызывает те же цели (make test в workflow), что и разработчик локально — расхождение «локально прогнал по-другому» исчезает. Новые задачи добавляются вместе с кодом, который их требует; мёртвые цели удаляются (проверка — make help и честный взгляд). Для монорепозиториев работает паттерн корневого Makefile с делегацией (make -C services/api test) или just-модулей по пакетам. И финальный аргумент: таск-раннер — это исполняемая документация; в отличие от README она не устаревает молча, потому что её запускают каждый день.
Переменные, окружение и переносимость
Таск-раннер часто становится местом, где концентрируются знания об окружении — и здесь важна дисциплина. Значения по умолчанию объявляйте в самом файле с возможностью переопределения: в Make это PORT ?= 8000 (переменная окружения победит), в just — аргументы со значениями по умолчанию. Секреты не объявляйте никогда: команды читают их из окружения или .env, который подгружается явно. Переносимость команд проверяйте простым вопросом: «запустится ли эта цель на машине коллеги без правок?» — если внутри зашит абсолютный путь или флаг конкретной ОС, цель либо параметризуется, либо документируется как платформенная.
Отдельная практика для больших проектов — группировка: префиксы целей (db-up, db-seed, api-run, api-test) читаются лучше плоского списка из тридцати имён; в just ту же роль играют комментарии-секции в выводе --list. И держите в голове границу с CI: цели таск-раннера должны быть теми же командами, которые дёргает пайплайн, — тогда локальный прогон честно предсказывает результат в CI, а расхождения между средами сводятся к переменным окружения, а не к разным командам.
FAQ
Make или just для нового проекта? В чисто Unix-команде — Make (ноль установок). При Windows-разработчиках в команде, активных аргументах и желании читаемого синтаксиса — just. Оба подхода совместимы: иногда justfile зовёт make-цели легаси.
Почему моя цель не запускается / запускается странно?
Почти всегда одна из трёх причин: пробелы вместо tab (Make), цель совпадает с именем файла без .PHONY, или команда падает на первом же шаге — make останавливает выполнение цели при ненулевом коде, это фича.
Стоит ли дублировать команды в README?
Нет: README ссылается на make help/just --list. Дубль расходится с реальностью в первую же неделю.
Можно ли таск-раннером деплоить? Да, если деплой идемпотентен и не содержит секретов в тексте целей; типичная схема — цель вызывает скрипт деплоя с проверками и подтверждением, как в статье про скрипты автоматизации.
Нужен ли таск-раннер, если проект маленький?
Если команд больше пяти — да: цель make test переживёт смену стека, а память о «той самой команде с флагами» — нет. Стоимость — один файл на двадцать строк.
Почему команда выполняется дважды / не выполняется при повторном make? Скорее всего, цель совпала с именем файла: make считает её «сделанной». Объявите цели в .PHONY — это лечит оба симптома.
Источники: справочник GNU Make, документация just (ссылки в шапке).