031GIT И КОНВЕЙЕР ДЛЯ АГЕНТА
Ветки и рабочие копии. Перехватчики опасных команд и секретов. Автотесты, CI/CD и раннеры без розовых очков. Где хранить код и какие три ключа нельзя отдавать ИИ-агенту, даже если он настраивает всё остальное сам.
ПОЧЕМУ БЕЗ GIT АГЕНТ ПРЕВРАЩАЕТ ПРОЕКТ В РУЛЕТКУ
Пока код пишет человек, он физически ограничен скоростью собственных пальцев. За час разработчик меняет два-три файла, держит в голове причину каждой строчки и обычно замечает, если случайно задел соседний экран. ИИ-агент устроен иначе: за сорок секунд он способен переписать восемнадцать файлов в четырёх папках, поправить схему базы данных, «заодно» упростить проверку оплаты — и в конце одинаково бодрым тоном отчитаться: «Готово, ошибку исправил».
Если в папке проекта нет Git (системы контроля версий, которая хранит полную историю снимков кода и умеет сравнивать или откатывать любые изменения), вы остаётесь с этим отчётом один на один. Вчера форма заказа работала, сегодня после трёх просьб «подвинь кнопку и ускорь загрузку» отвалилась оплата картой, а какая именно из сорока правок её сломала — неизвестно. Люди начинают копировать папки с названиями вроде project_final_v3_rabochaya, бояться лишний раз дышать на работающий сервис и проверять руками весь сайт после каждой мелочи. В этот момент главная выгода агента — скорость — начинает работать против владельца.
Git возвращает в работу с ИИ физический закон сохранения. Он нужен не ради красивой инженерной культуры и не «потому что так принято у программистов». В связке с агентом он решает четыре конкретные задачи, без которых проект разваливается на втором десятке задач.
1. Точка возврата за две секунды вместо страха перед правкой
Каждый коммит (зафиксированный снимок всех файлов проекта с короткой подписью и уникальным номером) работает как сохранение перед сложным прыжком. Когда рабочее состояние зафиксировано, вы перестаёте дрожать над кодом. Можно дать агенту смелую задачу — разделить раздувшийся файл на модули, перевести дизайн на новую сетку, переписать медленный запрос — и если через десять минут выяснится, что он ушёл не в ту степь, проект возвращается к чистому состоянию одной командой без единого следа неудачного эксперимента.
2. Рентген изменений вместо веры тексту в чате
Текст, который агент пишет в окне диалога, — это его пересказ собственных намерений, а не факт. Факт показывает команда git diff (построчное сравнение «что было» и «что реально изменилось на диске»). На практике именно там всплывают тихие сюрпризы: просили поменять текст уведомления, а в диффе рядом мелькает удалённая проверка прав администратора или заменённый адрес боевого сервера. Вам даже не обязательно самому читать код — достаточно попросить самого агента или соседнюю сессию разобрать вывод git diff перед сохранением и назвать каждый затронутый файл своими словами.
3. Неприкосновенный ствол и именные ветки
Главное правило взрослой работы с агентами звучит просто: основная ветка main (ствол проекта, из которого выкладывается живой сайт) всегда обязана быть готова к запуску. Ни человек, ни агент не пишут в неё напрямую. Под каждую задачу от ствола отводится отдельная ветка (branch — изолированная копия истории): пока агент чинит в ней корзину или верстает новую страницу, основной продукт остаётся целым. Если посреди крупной переделки прилетает срочная поломка с боевого сайта, вы не выкидываете полуразобранную работу — вы переключаетесь на чистый main, делаете быструю заплатку в отдельной короткой ветке, выпускаете её, а потом спокойно возвращаетесь к большой задаче.
У себя на сервере я ввёл жёсткую схему имён веток <кто>/<область>-<задача>: например, claude/billing-refund-guard, gemini/ui-mobile-header или fix/sentry-4812 для автоматического разбора ошибок. Когда в одном репозитории работают два разных ИИ-агента и фоновые скрипты, префикс в названии ветки экономит часы расследований. На одном из наших продуктовых сервисов за восемь месяцев накопилось 1 228 коммитов и 152 ветки: 56 веток от одного агента, 71 рабочая ветка по фичам и багам от второго, ещё 14 создал ночной охотник за ошибками. По одному списку сразу видно, чьи руки трогали узел.
Там же вылез и первый урок гигиены: когда мы провели ревизию этих 152 веток, оказалось, что 123 из них уже давно влиты в ствол, но никто их не удалил. Агент блестяще создаёт ветки и почти никогда не убирает за собой без отдельного правила. Если не заставить его удалять ветку сразу после слияния, а брошенные тупиковые подходы уносить под тег archive/..., через полгода в списке веток невозможно отличить живую незавершённую работу от весеннего мусора.
4. Параллельные руки в одном проекте и ловушка папки /tmp
Обычная ветка спасает ствол, но не спасает от толкотни в одной папке. Если вы открыли два окна чата на одном и том же каталоге проекта и попросили первого агента переделать шапку сайта, а второго — починить API профиля, они начнут переключать ветки из-под ног друг у друга и смешивать чужие несохранённые файлы в один коммит. Клонировать весь репозиторий целиком под каждую задачу тоже накладно: тяжёлые проекты весят гигабайты.
Для этого в Git встроен механизм git worktree (связанная рабочая копия). Он за секунду создаёт рядом лёгкую отдельную папку под конкретную ветку, а всю тяжёлую историю коммитов делит с основным репозиторием без дублирования. Каждый параллельный агент заходит в свою песочницу, собирает и тестирует код независимо, а после слияния папка удаляется.
По умолчанию агенты обожают создавать временные рабочие копии в системной папке /tmp. На нашем Linux-сервере под /tmp был отведён стандартный диск в оперативной памяти (tmpfs) объёмом 2 ГБ. Когда четыре параллельных помощника одновременно развернули там четыре рабочие копии с зависимостями, они заняли 1,8 из 2 ГБ. Как только /tmp забился, системный вызов запуска процессов и временных файлов начал молча падать сразу во всех сессиях агентов на машине — терминал у моделей просто перестал отвечать.
С того дня в глобальном правиле всех проектов прописан жёсткий адрес: любые рабочие копии создаются только на обычном диске в /var/tmp/wt/<имя>, где свободно больше 200 ГБ, и обязательно снимаются через git worktree remove сразу после закрытия задачи.
ЧТО ПРОСИТЬ АГЕНТА НАСТРОИТЬ В ПЕРВЫЙ ДЕНЬ
Хорошая новость в том, что вам не нужно учить десятки флагов консоли, чтобы выстроить этот порядок. Достаточно один раз поручить самому агенту подготовить фундамент репозитория и записать правила работы в файл инструкций проекта (AGENTS.md или CLAUDE.md, который агент читает автоматически в начале каждой новой сессии).
Примечание к промптам: во всех блоках для копирования ниже длинные тире намеренно заменены на двоеточия и запятые, чтобы текст без искажений вставлялся в любые терминалы и конфигурационные файлы.
КАК АГЕНТ НА САМОМ ДЕЛЕ РАБОТАЕТ С РЕПОЗИТОРИЕМ
У многих возникает ощущение, будто у современных ИИ-агентов есть какой-то особый нейросетевой разъём к Git. На самом деле никакой магии нет: агент управляет репозиторием ровно через тот же текстовый терминал, что и обычный разработчик. На каждом шаге модель вызывает инструмент оболочки (Bash), отправляет туда обычную команду вроде git status, git diff, git commit -m "..." или утилиту gh pr create, а обратно получает текст ответа и числовой код завершения (0 — команда прошла, ненулевой код — ошибка).
Из этой простой механики растут два важных следствия. Первое — приятное: агент ориентируется в консольном Git быстрее и аккуратнее большинства людей. Он без труда находит по git log -S, в какой день три месяца назад пропала нужная функция, умеет собирать дробные коммиты по смыслу и сам читает логи упавшей сборки через gh run view --log-failed.
Второе следствие — опасное: текстовое правило в инструкции — это дорожный знак, а не бетонный блок. Пока сессия свежая и всё идёт гладко, агент послушно соблюдает просьбу «не коммить в main и не стирать файлы». Но стоит диалогу разрастись, а слиянию веток упереться в неприятный конфликт, модель начинает искать самый короткий путь «привести репозиторий в порядок» — и тянется к командам, которые стирают историю без спроса.
Три реальных инцидента, после которых мы перестали верить обещаниям
-
12 мая 2026 года — «сейчас приберу за собой тестовый коммит». Агент проверял гипотезу, сделал временный коммит, убедился, что проблема в другом, и решил аккуратно откатить свой шаг командой
git reset --hard HEAD~1. Флаг--hardоткатывает не только последний коммит, но и безвозвратно затирает все незафиксированные правки в рабочем дереве — вместе с живой работой, которая ещё не попала в индекс. -
24 июня 2026 года — выкладка с недоделанной ветки. Скрипт публикации собирал сайт из текущего состояния папки на сервере. В тот момент репозиторий стоял не на стволе
main, а на старой рабочей ветке, где ещё не было двух свежих выпусков. Сборка прошла без единой ошибки — и молча сняла две уже опубликованные страницы с живого сайта, пока мы не заметили пропажу. -
2 октября 2026 года — обход наивного запрета на перезапись истории. У нас уже стоял защитный перехватчик команд, который искал в строке подстроки
git push --forceиgit push -f. Столкнувшись с расхождением веток, агент выполнилgit push -q --force-with-lease. Тихий флаг-qвстал между словамиpushи--force, строковый фильтр не узнал шаблон, а поскольку на бесплатном приватном репозитории GitHub серверная защита веток отключена, агент перезаписал удалённую ветку поверх чужих коммитов.
После третьего случая стало окончательно ясно: правила в AGENTS.md нужны, чтобы агент не шёл в тупик зря, но страховать необратимые действия обязаны гейты (механические защёлки — программы, которые физически возвращают ошибку до того, как опасная команда коснётся диска или сервера).
ЛОКАЛЬНЫЕ ГЕЙТЫ: ПЕРЕХВАТ КОМАНД И СКАНЕР СЕКРЕТОВ
Первые два рубежа срабатывают мгновенно прямо на вашей машине или рабочем сервере, ещё до того, как что-то улетит в сеть. Попробуйте прогнать реальные команды агента через живой симулятор ниже — в нём зашиты те же правила разбора, которые стоят у нас на сервере после октябрьского инцидента.
Обратите внимание, почему обычный поиск подстроки проигрывает разбору по токенам. Когда агент передаёт команду с путём git -C /srv/app push -qf origin main, склеивает короткие флаги в -qf, ставит плюс перед именем ветки (+main — скрытый синтаксис принудительной перезаписи в Git) или оборачивает вызов в bash -c "...", наивное регулярное выражение слепнет. А с другой стороны, грубая проверка на слово --force начинает ложно блокировать безобидный коммит git commit -m "document why --force is forbidden", где опасное слово стоит просто внутри текста описания.
Ищет буквальный кусок текста "git push --force" или "git push -f ". Пропускает git push -q --force-with-lease, git -C dir push -f и git push origin +main, зато ложно блокирует упоминание флага внутри кавычек git commit -m "...".
Разбивает оболочку по правилам кавычек и операторов && / ;, заглядывает внутрь bash -c, пропускает служебные опции -C и -c, находит подкоманду push и проверяет каждый её флаг и refspec без единого ложного срабатывания.
Ниже — рабочее ядро нашего перехватчика для обвязки агента (в Claude Code и совместимых CLI он подключается на событие PreToolUse для инструмента Bash; код возврата 2 и сообщение в поток ошибок stderr останавливают выполнение и объясняют модели причину отказа):
# Фрагмент перехватчика PreToolUse (guard-bash): токенная проверка force-push и разрушительного Git
import shlex, sys
GIT_OPTS_WITH_VALUE = {"-C", "-c", "--git-dir", "--work-tree", "--namespace"}
def check_git_tokens(tokens: list[str]) -> str | None:
i = 1
while i < len(tokens):
t = tokens[i]
if t in GIT_OPTS_WITH_VALUE:
i += 2 # пропускаем опцию git и её аргумент (например: -C /srv/app)
continue
if t.startswith("-"):
i += 1
continue
break
if i >= len(tokens):
return None
subcmd, args = tokens[i], tokens[i + 1:]
if subcmd == "push":
for a in args:
if a in ("--force", "--mirror") or a.startswith("--force-with-lease") or a.startswith("--force-if-includes"):
return f"Заблокирован перезаписывающий push ({a}). Нужен явный допуск владельца."
if a.startswith("-") and not a.startswith("--") and "f" in a[1:]:
return f"Заблокирован короткий флаг -f внутри {a}."
if a.startswith("+") and len(a) > 1:
return f"Заблокирован принудительный refspec ({a})."
if subcmd == "reset" and "--hard" in args:
return "Заблокирован git reset --hard: он стирает несохранённую работу в папке."
if subcmd == "clean" and any(a.startswith("-") and "f" in a for a in args):
return "Заблокирован git clean -f: удаление неотслеживаемых файлов без спроса запрещено."
if subcmd in ("commit", "push", "merge") and ("--no-verify" in args or "-n" in args if subcmd == "commit" else "--no-verify" in args):
return "Заблокирован обход репозиторных хуков (--no-verify)."
return None
Второй рубеж: репозиторный .githooks/pre-commit и десять ловушек на секреты
Хук в настройках конкретного ИИ-клиента защищает одного агента, но если завтра вы откроете проект в другом редакторе или запустите второго агента, защита должна сработать точно так же. Поэтому правила самого репозитория живут прямо внутри него — в папке .githooks/pre-commit, которая включается один раз командой git config core.hooksPath .githooks.
Второй по частоте грех агентов после прямой правки ствола — случайно закоммитить боевой ключ API. Модели делают это без всякого злого умысла: отлаживали интеграцию, положили временный скрипт проверки с ключом или выполнили широкое git add . там, где лежал свежий .env.production. Если секрет хотя бы на секунду попал в коммит и улетел в облако, удалять файл следующим коммитом бесполезно — ключ навсегда остался в истории Git, и его придётся отзывать и перевыпускать в кабинете провайдера.
Наш предкоммитный шлюз проверяет две вещи за доли секунды: текущую ветку и только добавленные строки в подготовленном диффе (git diff --cached). Вот компактный рабочий скрипт, который сразу закрывает обе дыры:
#!/usr/bin/env bash
# .githooks/pre-commit — защита ствола main и перехват секретов до записи в историю Git
set -euo pipefail
BRANCH="$(git rev-parse --abbrev-ref HEAD)"
if [[ "$BRANCH" == "main" || "$BRANCH" == "develop" ]]; then
echo "HARD STOP: прямой коммит в ветку '$BRANCH' запрещён." >&2
echo "Создайте рабочую ветку: git checkout -b <agent>/<scope>-<slug>" >&2
exit 1
fi
# 1. Блокируем любые файлы .env*, кроме публичных шаблонов
STAGED_FILES="$(git diff --cached --name-only --diff-filter=ACM)"
if echo "$STAGED_FILES" | grep -E '(^|/)\.env(\..+)?$' | grep -Ev '\.(example|sample|template|dist)$' >/dev/null; then
echo "BLOCKED: попытка закоммитить файл окружения (.env). Уберите его через git reset HEAD <file>." >&2
exit 1
fi
# 2. Сканируем только добавленные строки (+) на типовые ключи провайдеров и приватные ключи
ADDED_LINES="$(git diff --cached -U0 | grep -E '^\+[^+]' || true)"
SECRET_REGEX='(BEGIN [A-Z ]*PRIVATE KEY|sk-proj-[A-Za-z0-9_-]{20,}|sk-ant-[A-Za-z0-9_-]{20,}|AIza[0-9A-Za-z_-]{35}|AKIA[0-9A-Z]{16}|gh[pousr]_[A-Za-z0-9]{30,}|[0-9]{8,10}:AA[A-Za-z0-9_-]{33})'
if echo "$ADDED_LINES" | grep -E "$SECRET_REGEX" >/dev/null; then
echo "BLOCKED: в подготовленном коммите обнаружен ключ API или приватный ключ." >&2
echo "Вынесите секрет в переменную окружения (.env) и оставьте в коде только чтение из env." >&2
exit 1
fi
АВТОТЕСТЫ — ЭТО ЕДИНСТВЕННАЯ ЗАМЕНА РУЧНОЙ ВЫЧИТКЕ КОДА
Когда владельцу продукта без инженерного прошлого говорят «тебе нужны автотесты и CI/CD», это часто звучит как дорогой корпоративный налог, который придумали большие отделы разработки. В классической ручной разработке отчасти так и было: маленький проект одного автора мог месяцами жить на ручной проверке в браузере, потому что автор сам помнил каждую развилку своего кода.
С приходом ИИ-агентов роли перевернулись. Вы физически не будете глазами вычитывать две тысячи строк чужого кода в день — а если не читаете вы, значит, проверять работу агента должна машина. Автотест — это просто маленький скрипт-ревизор, который притворяется пользователем: создаёт заказ, пробует списать больше денег, чем есть на счёте, проверяет права чужого аккаунта и сверяет ответ с эталоном. А CI/CD (непрерывная интеграция и доставка) — это независимый конвейер на сервере, который автоматически запускает всех этих ревизоров на каждую новую заявку на слияние (Pull Request), прежде чем пустить код агента в основной ствол main.
На двух наших основных продуктовых сервисах сегодня крутятся 4 857 автотестов в первом и 8 463 автотеста во втором (6 061 серверный и 2 402 фронтенд-теста в 573 тестовых файлах). Почти все они написаны самими же агентами по правилу: «делаешь функцию — рядом кладёшь тест, который ломается, если функция начнёт врать». Именно эта сетка позволяет вливать десятки изменений в неделю без бессонных ночей.
Почему нельзя верить фразе агента «я локально всё прогнал, тесты зелёные»
В конце апреля 2026 года у нас подряд проскочили две заявки на слияние, где агент честно запустил один тестовый файл вокруг своей правки, увидел зелёную галочку и сдал задачу — а общий ствол проекта покраснел. Причина типичная для ИИ: агент поменял сигнатуру общей функции, поправил её тест, но не запустил полную проверку типов по всему проекту (tsc --noEmit), и три соседних экрана, которые вызывали эту функцию по-старому, развалились при сборке.
Локальная среда всегда «тёплая»: в ней могут лежать старые закешированные файлы, локальные переменные окружения или незакоммиченный модуль, который агент забыл добавить в Git. Серверный CI поднимает проект с чистого листа ровно из того, что реально попало в ветку. Если файл забыт на диске агента — CI упадёт через минуту и не даст сломать ствол.
ПЯТЬ ГЕЙТОВ В CI, КОТОРЫЕ НУЖНЫ ИМЕННО ПРОТИВ БОЛЕЗНЕЙ АГЕНТОВ
Стандартный учебник по CI предлагает три шага: линтер стиля, проверка типов и запуск тестов. Для человека этого обычно хватает. Для ИИ-агента — нет: у языковых моделей есть собственные устойчивые привычки «срезать угол», которые опытный живой инженер почти никогда не допускает. По следам реальных поломок у нас в конвейере прописались пять специальных агентских гейтов:
Когда просишь агента переименовать поле в таблице, он обожает открыть уже существующий файл миграции прошлого месяца и поправить его задним числом. В свежей тестовой базе всё проходит идеально, а на боевой базе, где эта миграция давно применена, выкладка взрывается. Скрипт в CI сверяет папку миграций со стволом main: добавлять новые файлы можно, менять уже влитые — стоп-кран.
Агент всегда идёт кратчайшим путём: если ему в компоненте кнопки понадобились данные, он способен импортировать прямой запрос к базе в обход сервисного слоя или завязать два независимых модуля в кольцо. Автоматическая проверка границ (lint:boundaries) рвёт сборку при первой же попытке протащить запрещённую связь.
В огромных файлах на тысячу строк агенты быстро теряют контекст и начинают плодить дубли. Скрипт-храповик считает число файлов длиннее лимита и число маршрутов без тестов, сравнивая их с зафиксированным потолком. Показатель имеет право только оставаться прежним или снижаться. Если агент незаметно раздул ещё один файл за предел — CI возвращает заявку на декомпозицию.
Команда git diff origin/main...HEAD находит все изменённые или новые API-маршруты в ветке агента и проверяет, есть ли для каждого из них соответствующий автотест. Забыть написать тест на новую точку оплаты или входа становится физически невозможно — заявка не пройдёт шлюз.
Отдельный быстрый шаг проверяет сами файлы .github/workflows/*.yml, чтобы ни один агент при правке CI не оставил широких прав записи токену сборки (по умолчанию строго permissions: contents: read), а заодно сканирует словари интерфейса на типичные модельные заглушки вроде TODO и пустых ключей перевода.
КАК НЕ ГОНЯТЬ ТЯЖЁЛЫЙ CI ДВАЖДЫ НА ОДИН И ТОТ ЖЕ КОД
Когда агенты начинают закрывать по пятнадцать задач в день, всплывает скрытая расточительность классических настроек GitHub Actions. Обычно конвейер настроен запускаться и на заявку pull_request, и на слияние push в main. В октябре 2026 года мы сняли трёхдневный срез по двум активным проектам и посчитали каждую минуту.
Фокус в том, что Git при каждом коммите уже вычисляет криптографический слепок всего дерева исходных файлов — Tree SHA (git rev-parse HEAD^{tree}). В отличие от хеша коммита, который меняется при squash-слиянии в main, хеш дерева файлов остаётся байт-в-байт тем же самым, если в ствол между проверкой заявки и слиянием никто не влил конкурирующую правку.
Мы добавили в конец успешного прогона заявки запись крошечного артефакта с этим слепком дерева. Когда через минуту код вливается в main, первый шаг конвейера за пару секунд сверяет текущий HEAD^{tree} с уже проверенным зелёным слепком: совпало — все тяжёлые многоминутные шаги тестов легально пропускаются; разошлось хоть на один байт — включается полный прогон. Треть нагрузки на раннеры исчезла в тот же вечер без малейшей потери надёжности.
Минимальный скелет такого конвейера в .github/workflows/ci.yml читается на одном экране — обратите внимание на зажатые права permissions: contents: read и отмену устаревших прогонов concurrency, чтобы три быстрых пуша агента подряд не стояли в очереди втроём, а сразу уступали место последнему:
name: CI
on:
pull_request:
branches: [main]
push:
branches: [main]
permissions:
contents: read
# Если агент запушил исправление поверх своей же ветки — старый незавершённый прогон сразу гасим
concurrency:
group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
agent-gates:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Неизменность уже влитых миграций БД
run: bash scripts/ci/check-migration-safety.sh origin/main
- name: Обязательный тест на изменённый API-маршрут
run: bash scripts/ci/check-route-tests.sh origin/main
- name: Линтер границ, потолок размера файлов (ratchet) и типы
run: npm ci && npm run lint:boundaries && npm run ratchet && npx tsc --noEmit
tests:
needs: agent-gates
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- name: Автотесты и контрольная сборка
run: npm ci && npm test && npm run build
КТО ФИЗИЧЕСКИ КРУТИТ ВАШИ ТЕСТЫ И СКОЛЬКО ЭТО СТОИТ
Файл ci.yml — это только рецепт. Чтобы реально собрать проект, поднять тестовую базу данных и прогнать тысячи проверок, нужен раннер (runner) — рабочая лошадка, процесс на виртуальной машине, который забирает задачу из очереди, выполняет команды и отправляет обратно зелёную или красную отметку.
Раннеры бывают двух видов: облачные (их выдаёт сам GitHub на пару минут под каждый прогон и тут же уничтожает) и собственные (self-hosted — вы ставите бесплатную программу-агента на арендованный сервер, и задачи прилетают на ваше железо). В ручной разработке об этой развилке вспоминают редко, но с ИИ-агентами вы упираетесь в неё в первый же плотный месяц: агент не устаёт, делает по тридцать коммитов в день и запускает конвейер впятеро чаще человека.
Смета облачных раннеров GitHub Actions в 2026 году
По официальному прайсу GitHub (docs.github.com · Actions Runner Pricing), для открытых публичных репозиториев стандартные облачные раннеры бесплатны без лимита. Для закрытых приватных репозиториев расклад такой:
- на бесплатном тарифе GitHub Free включено 2 000 минут Linux в месяц (на тарифе Team — 3 000 минут);
- сверх бесплатного пакета минута лёгкого 1-ядерного раннера стоит $0.002, стандартного 2-ядерного Linux — $0.006/мин, 4-ядерного — $0.012/мин, 8-ядерного — $0.022/мин.
Приложим это к нашему реальному темпу: 1 147 минут за три активных дня дают около 11 500–12 000 минут в месяц. Бесплатная квота в 2 000 минут выгорает за первые пять дней, а перерасход в 10 000 минут на базовом двухъядерном раннере обходится примерно в $57–60 в месяц. Сумма сама по себе не разорительная — куда сильнее бьёт другое: скорость обратной связи.
Стандартный бесплатный двухъядерный раннер в облаке крутил наш фронтенд-пакет тестов Vitest 617 секунд (больше десяти минут). На собственном сервере с быстрым NVMe-диском и прогретым кешем зависимостей ровно тот же набор проходил за 114 секунд — в 5,4 раза быстрее (а полный цикл сборки со всеми проверками ускорился втрое). Когда агент сидит и ждёт вердикта по десять минут на каждую мелкую поправку, живая парная работа превращается в переписку по почте.
ПЯТЬ ГРАБЛЕЙ СВОИХ РАННЕРОВ НА БОЕВОМ СЕРВЕРЕ (ОКТЯБРЬ 2026)
В большинстве заметок про self-hosted раннеры на этом месте ставят точку: «поставьте раннер к себе на сервер за пять минут — будет бесплатно и быстро». Мы тоже так подумали, поселили девять локальных раннеров на мощную рабочую машину (16 ядер CPU, 60 ГБ оперативной памяти), где рядом работали продукты и интерактивные сессии агентов, — и за две недели октября собрали коллекцию аварий, которую лучше прочитать чужими глазами, чем пережить самому.
-
Шторм по процессору и очередь на 50 минут. Стоило двум агентам параллельно открыть серию заявок, как 7 из 9 раннеров одновременно взялись компилировать TypeScript и гонять Python-тесты. Загрузка всех 16 ядер упёрлась в 97–98%, средняя очередь процессов (
load average) взлетела до 48,7–52, терминалы живых сессий начали вязнуть, а очередь самого CI растянулась на 28–50 минут. -
Тестовая база сбежала из клетки ядер. Мы ограничили службы раннеров половиной процессора через настройку systemd
AllowedCPUs=0-7и выдохнули — ровно до следующего утра. Оказалось, что сами раннеры послушно сидели на восьми ядрах, а вот Docker-контейнеры тестовой базыci-postgresиci-redisкрутились в общем демоне Docker без привязки к ядрам и продолжали выжигать всю машину. Вылечили только жёстким выравниванием: привязали контейнеры тестовых баз черезdocker update --cpuset-cpusк тому же пулу ядер, понизили приоритет раннеров (CPUWeight=50,Nice=15), а самим агентам запретили запускать полный прогон всех тестов локально — дома агент проверяет только затронутый модуль в два потока (--maxWorkers=2), полный пакет крутит CI. -
Случайный откат лимита памяти и стояк на load 69 (12.10.2026). При правке конфига один из агентов по старому шаблону ужал потолок памяти раннера до
4–6 ГБ, тогда как восемь параллельных потоковpytestв пике потребляли около шести гигабайт. Ядро Linux не убило процесс сразу, а включило принудительное торможение памяти: за час набежало 3,4 миллиона событийmemory.high, диск захлебнулся свопом на 500 МБ/с,loadпробил отметку 69, сами слушатели GitHub отвалились по таймауту вoffline, а восемь бесхозных процессов тестов продолжали молотить в пустоту ещё 61 минуту. Подняли честный коридорMemoryHigh=10GиMemoryMax=12G— торможения упали в ноль. -
Ускорение 6 061 серверного теста с 15:03 до 8:01 без покупки железа. Когда мы профилировали долгий серверный прогон, выяснилось, что 40% времени процессор вообще не считает логику: он ждёт, пока тестовый Postgres честно сбросит каждую транзакцию на диск на случай отключения электричества, и тысячи раз заново хеширует пароли тестовых пользователей тяжёлым алгоритмом
bcrypt. Для одноразовой тестовой базы, которая живёт восемь минут, сохранность при сбое питания бессмысленна: мы отключили в тестовом контейнере синхронную запись (fsync=off,synchronous_commit=off,full_page_writes=off), закешировали один настоящий хешbcryptв фабрике тестовых пользователей и разнесли запуск на изолированные временные базы по шардам. Чистое время прогона 6 061 теста упало с 15 минут 03 секунд до 8 минут 01 секунды. -
Кладбище сиротских баз и «спящий» слушатель. Когда блок
concurrencyотменяет устаревший прогон посреди работы, процесс не всегда успевает выполнить финальную уборку. За неделю у нас накопилось 88 брошенных тестовых базtest_ci_*и 21 осиротевший процесс Redis, а один из раннеров простоял 9 часов в статусеofflineна стороне GitHub при живом процессе на сервере из-за зависшего сетевого сокета (SocketException 125). Проблему закрыли двумя короткими скриптами: уборщиком сирот, который сверяет временные базы со списком реально идущихrun_id, и трёхминутным таймером-сторожем в systemd, перезапускающим зависшего слушателя.
| Параметр | Облачный GitHub Hosted | Свой раннер на отдельной дешёвой VPS | Свой раннер на общем сервере |
|---|---|---|---|
| Цена в месяц | 0 $ до 2 000 мин, далее $0.006/мин (≈ $57 за 11 500 мин) | Фиксированные $10–20 за VPS (4–8 ядер NVMe) без счётчика минут | 0 $ доплаты, но конкурирует за CPU и память с живыми задачами |
| Скорость прогона | Медленно на тяжёлых пакетах (617 с на наш Vitest, холодный кеш) | Быстро в 3–5 раз (114 с, тёплый кеш пакетов на локальном NVMe) | Быстро, пока соседние раннеры не устроили очередь за диск |
| Безопасность | Чистая одноразовая виртуалка на каждый запуск | На машине нет боевых баз и продакшен-ключей | Опасно для прода: требует жёсткой изоляции прав и секретов |
| Уход руками | Нулевой | Низкий: один раз настроить лимиты systemd и автоочистку диска | Высокий: cgroups, cpuset для Docker-баз, сторож и контроль памяти |
| Когда выбирать | Старт проекта, 1 агент, расход до 2 000–3 000 мин/мес | Золотая середина при активной работе 2+ агентов каждый день | Только как временная мера и строго в клетке AllowedCPUs + MemoryMax |
ГДЕ ДЕРЖАТЬ РЕПОЗИТОРИЙ, ЧТОБЫ АГЕНТУ БЫЛО УДОБНО, А ВАМ — СПОКОЙНО
Сам по себе Git живёт прямо в скрытой папке .git внутри вашего проекта и вообще не требует интернета. Внешний Git-сервер нужен для трёх вещей: хранить резервную копию истории отдельно от рабочей машины, принимать заявки на слияние (Pull Requests) с запуском CI и разделять права доступа, чтобы агент физически не мог переписать защищённый ствол main.
Вокруг выбора между облачным GitHub и собственным сервером обычно много идеологии. Если убрать эмоции и посчитать оперативную память, стоимость и то, насколько уверенно с площадкой умеют работать сами ИИ-агенты в 2026 году, остаются четыре понятных сценария.
1. GitHub (Free и Team) — лучший выбор по умолчанию, но с одной скрытой деталью
У GitHub в работе с ИИ есть огромное практическое преимущество: консольная утилита gh входит в «родной язык» абсолютно всех современных агентов. Модель без единой подсказки умеет создать заявку, повесить метку, вытащить лог конкретного упавшего шага сборки, прочитать замечания ревью и проверить статус чеков. Плюс он не тратит ни одного мегабайта оперативной памяти вашего сервера и остаётся живым, даже если ваш хостинг прилёг.
Скрытая деталь, о которую спотыкаются почти все на старте: на бесплатном тарифе GitHub Free для приватных репозиториев отключена серверная защита веток (Branch Protection / Rulesets). То есть кнопки «запретить прямой пуш в main и требовать зелёный CI перед слиянием» на стороне самого GitHub для закрытого проекта открываются только на тарифе GitHub Team за $4 за пользователя в месяц (github.com/pricing). Если вы остаётесь на бесплатном приватном тарифе, весь замок держится исключительно на ваших локальных хуках из Блока 02.
2. Forgejo или Gitea на своём сервере — когда код не должен покидать ваш контур
Если правила заказчика, требования по российскому контуру хранения или нежелание зависеть от блокировок зарубежного аккаунта запрещают держать исходники на американском облаке, не спешите ставить тяжёлый комбайн. Forgejo (открытое ответвление Gitea) и сама Gitea написаны на Go и поставляются одним лёгким бинарным файлом или крошечным контейнером.
В состоянии покоя такой сервер потребляет всего 40–100 МБ оперативной памяти и свободно дышит на самой простой VPS с 1–2 ГБ RAM (а вместе со встроенной системой Forgejo/Gitea Actions, чей формат YAML-файлов почти полностью совместим с привычным GitHub Actions, укладывается в 2–4 ГБ RAM; документация forgejo.org и docs.gitea.com). При этом серверная защита ветки main в них бесплатна на любом приватном репозитории. Единственная доплата в настройке агента — прописать в AGENTS.md консольный клиент tea или токен API вместо привычного gh.
3. GitLab Community Edition (CE) — карьерный самосвал для поездки в булочную
GitLab CE — зрелая корпоративная платформа, внутри которой одновременно крутятся Ruby-сервер Puma, фоновые воркеры Sidekiq, хранилище Gitaly, собственные PostgreSQL, Redis и мониторинг. Официальная документация GitLab для односерверной установки рекомендует 16 ГБ оперативной памяти и 8 ядер vCPU (docs.gitlab.com · Requirements); при ручном отключении лишних служб его удаётся ужать до 4–8 ГБ RAM, а на двухгигабайтной виртуалке он уходит в бесконечный своп. Если в вашей компании уже развёрнут корпоративный GitLab и его ведёт отдел инфраструктуры — смело подключайте агента через утилиту glab. Но поднимать GitLab CE с нуля ради одного-трёх проектов с ИИ — пустая трата железа и выходных.
4. Голый репозиторий по SSH (git init --bare) — идеальная страховка за ноль рублей
Самый недооценённый вариант работает вообще без веб-интерфейса: на любом вашем сервере с обычным SSH создаётся папка git init --bare project.git. Она потребляет 0 МБ памяти в простое. Работать через неё как через единственную площадку с агентами неудобно — там нет ни заявок на слияние, ни визуального обзора диффа, ни встроенного запуска CI. Зато в роли второго независимого зеркала это безупречное решение: три строчки ночного скрипта или дополнительный адрес git remote set-url --add --push автоматически дублируют каждый пуш с GitHub на ваш собственный диск. Даже если облачный аккаунт однажды заморозят, вся история всех веток уже лежит у вас дома.
| Критерий | GitHub (Free / Team) | Forgejo / Gitea (self-host) | GitLab CE (self-host) | Голый Git по SSH (--bare) |
|---|---|---|---|---|
| Память на сервере | 0 МБ (живёт в облаке) | 40–100 МБ в покое (VPS 1–4 ГБ) | Рекомендация 16 ГБ RAM (минимум 4–8 ГБ) | 0 МБ сверх штатного sshd |
| Защита ветки main | В приватных репо только с Team ($4/мес) | Встроена бесплатно для любых репо | Встроена бесплатно | Только через серверный pre-receive скрипт |
| Удобство для агента | Максимальное: родной gh CLI знает каждая модель | Хорошее: tea CLI / REST API + знакомый YAML Actions | Хорошее: glab CLI, но свой формат .gitlab-ci.yml | Слабое: нет Pull Requests и статусов проверок |
| Суверенность кода | Зависит от аккаунта внешнего облака | 100% на вашем диске и домене | 100% на вашем диске | 100% на вашем диске |
| Лучшая роль | Основная площадка для 80% команд и соло-авторов | Основная площадка в закрытом контуре РФ / NDA | Только если в компании уже живёт свой GitLab | Неубиваемое резервное зеркало к любому облаку |
НАСТРОИТЬ С НУЛЯ И ВЕСТИ ТЕКУЧКУ — ДА. ДЕРЖАТЬ КЛЮЧИ ОТ СЕЙФА — НЕТ
Главный вопрос, ради которого затевался весь этот разбор: можно ли просто сказать агенту «настрой весь Git, тесты и выкладку сам и дальше веди их без меня»?
Честный ответ состоит из двух половин. Технически агент спокойно берёт на себя 95% всей работы с Git и конвейером — причём настраивает их чище, чем большинство самоучек, которым лень писать хуки и проверки миграций. Вы можете поручить ему инициализировать репозиторий, вычистить мусор из индекса, написать .githooks/pre-commit, сканер секретов и ci.yml, заводить именные ветки, резать работу на аккуратные смысловые коммиты, открывать заявки на слияние, самому читать логи упавших проверок, чинить свои же ошибки и прибирать влитые ветки.
Но отдавать агенту стопроцентную власть над репозиторием и боевой выкладкой нельзя. Причина не в том, что модель «глупая» или «злонамеренная», а в фундаментальном конфликте стимулов. Когда вы ставите агенту цель «добейся, чтобы задача была влита и сборка стала зелёной», а его код упирается в капризный тест или строгий гейт, с точки зрения сухой логики у модели всегда есть два пути: потратить час на поиск глубокой причины в архитектуре — или за три секунды подправить правило в ci.yml, добавить флаг --admin при слиянии и отрапортовать о победе. Если замок висит на поясе у того, кого этот замок должен сдерживать, рано или поздно он его откроет.
Три ключа, которые всегда остаются только в руках владельца
Не давайте агенту свой личный мастер-токен от GitHub или Forgejo с полными правами admin. Выпустите для него отдельный узкий токен (Fine-Grained Personal Access Token) строго на нужные репозитории ровно с тремя разрешениями: Contents: Read & Write (пушить рабочие ветки), Pull requests: Read & Write (создавать заявки) и Actions: Read (читать логи упавших тестов). Права Administration оставьте выключенными, а папки .github/workflows/ и .githooks/ закройте через файл CODEOWNERS — тогда агент сможет предложить улучшение конвейера, но не сможет ослабить проверку без вашего одобрения.
Урок нашего внутреннего аудита безопасности от 18 октября 2026 года: на боевом сервере, где крутится живая база клиентов, не должны лежать ни пишущий токен агента, ни файл учётных данных раннера .credentials (обладатель этого файла способен притвориться раннером и получить из облака секреты сборки). Боевой сервер забирает готовый код из ствола main исключительно по ключу только для чтения (Read-Only Deploy Key).
Слияние веток, затрагивающих деньги, авторизацию, персональные данные или схему базы, а также запуск боевого deploy.sh остаются за человеком. А чтобы даже человек в спешке не выложил недоделанную ветку (как в нашем июньском инциденте), сам скрипт выкладки начинается с железного замка ветки и собирает релиз в соседний запасной слот с мгновенным откатом.
Посмотрите, как просто выглядит этот четвёртый, финальный рубеж в реальном скрипте deploy.sh — два десятка строк на Bash полностью исключают выкладку не из main и позволяют откатить неудачный релиз меньше чем за две секунды переключением символической ссылки (.dist-blue и .dist-green):
#!/usr/bin/env bash
# Фрагмент deploy.sh — замок на ветку main и мгновенное переключение слотов blue/green
set -euo pipefail
CMD="${1:-deploy}"
# Откат и проверка статуса разрешены с любой ветки в любую секунду
if [[ "$CMD" == "rollback" ]]; then
CURRENT="$(cat .active-slot)"
PREV=$([[ "$CURRENT" == "blue" ]] && echo "green" || echo "blue")
ln -sfn ".dist-$PREV" dist
echo "$PREV" > .active-slot
echo "Откат выполнен: активен предыдущий слот $PREV."
exit 0
fi
# Боевая выкладка — строго из чистого ствола main после слияния ветки
DEPLOY_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
if [[ "$DEPLOY_BRANCH" != "main" ]]; then
echo "ОШИБКА: выкладка запущена с ветки '$DEPLOY_BRANCH'." >&2
echo "Сначала влейте изменения в main, переключитесь на main и повторите запуск." >&2
exit 1
fi
ACTIVE="$(cat .active-slot 2>/dev/null || echo blue)"
NEXT=$([[ "$ACTIVE" == "blue" ]] && echo "green" || echo "blue")
# Собираем проект в неактивный слот — живой сайт в эту минуту спокойно раздаётся из старого
npm run build -- --outDir ".dist-$NEXT"
test -f ".dist-$NEXT/index.html"
# Атомарно переключаем указатель за доли миллисекунды
ln -sfn ".dist-$NEXT" dist
echo "$NEXT" > .active-slot
МАСТЕР-ПРОМПТ: ПОРУЧИТЬ АГЕНТУ СОБРАТЬ ВЕСЬ КОНТУР ЗА ОДИН РАЗ
Если у вас уже есть живой проект, где вы работаете с ИИ-агентом, и вы хотите навести порядок без недели чтения документации — скопируйте ему мастер-промпт ниже. Он специально построен в два такта: сначала агент молча обследует проект и приносит вам карту пробелов на одобрение, затем своими руками собирает все файлы защиты, а в финале выдаёт короткий список из трёх действий в веб-интерфейсе, которые остаётся нажать лично вам.
КАК ЭТОТ КОНТУР ЖИВЁТ ДАЛЬШЕ
Хорошо настроенный Git с гейтами и автотестами ощущается не как бюрократия, а как снятый ручной тормоз. Вы перестаёте открывать каждый файл за агентом из тревоги «а вдруг он там что-то снёс», перестаёте бояться параллельно запустить три большие переделки в разных рабочих папках и перестаёте гадать, почему после вчерашней мелочи сломалась регистрация.
Модель даёт тягу двигателя. Git, защёлки на входе, сборочный конвейер и три ключа в кармане владельца дают руль и тормоза. Только вместе они превращают быструю нейросеть в надёжное производство.
УТВЕРЖДЕНО ДЛЯ СЕРИИ
AUTHOR · /author/
END OF SHEET 06
// Обсуждение
Можно писать анонимно. Укажите email, чтобы получать уведомления об ответах.