Что такое код выхода (exit code) — и почему CI бывает зелёным при упавших тестах

Вот история, которая случается с каждым. Ты добавил в CI сохранение лога тестов: npm test | tee test.log. Удобно. Через неделю выясняется: три теста давно падают, а CI всё это время горел зелёным.
Тесты честно упали. Лог честно записал ошибки. Сломалось то, чего ты не видел, — код выхода. Сейчас поймёшь, что это и как он потерялся.
Код выхода — это одно число в конце
Каждая программа, завершаясь, сообщает системе одно число от 0 до 255. Это и есть код выхода (exit code).
Правило простое:
- 0 — всё прошло успешно.
- Любое другое число — что-то пошло не так.
Это как посыльный, который возвращается не с длинным отчётом, а с одной табличкой. Ноль — «доставил». Не ноль — «проблема». Текст ошибки программа пишет отдельно, в вывод. А табличку читают другие программы.
Посмотреть код последней команды в терминале можно так:
npm test
echo $? # 0 — тесты прошли, 1 — упали
В PowerShell на Windows вместо $? — $LASTEXITCODE.
Кто на него смотрит
Ты в терминале обычно читаешь текст. А вот машины смотрят только на число:
&&в командах.npm test && npm run deployвыложит сайт, только если тесты вернули 0.- CI/CD. Шаг пайплайна зелёный при нуле и красный при любом другом коде. Текст «FAILED» в логе CI не волнует.
- Docker и сервер. Контейнер перезапускается или помечается упавшим по коду выхода.
- ИИ-агенты. Когда агент запускает команду, он смотрит на код. Ноль — идёт дальше, не ноль — читает ошибку и чинит.
В логе CI код тоже виден. В GitHub Actions упавший шаг заканчивается строкой Process completed with exit code 1. Если тесты красные в тексте, а такой строки нет и шаг зелёный — код где-то потерялся по дороге. Ищи его в самом скрипте шага.
Почему код потерялся в трубе
Вернёмся к началу. Знак | — это труба: вывод первой команды идёт на вход второй. Но у всей трубы один код выхода — последней команды.
npm test | tee test.log
echo $? # 0 — это код tee, а не тестов
tee успешно записал файл и вернул 0. Код тестов (1) просто выбросили. CI увидел ноль и обрадовался.
Починка — одна строка в начале скрипта:
set -o pipefail
npm test | tee test.log
echo $? # 1 — теперь труба честная
С pipefail труба возвращает ошибку, если упала любая её часть. Добавь это в каждый bash-скрипт, где есть |.
Тот же эффект дают ещё два приёма — оба часто пишет ИИ:
|| trueв конце команды. Означает «даже если упало, считай успехом». Иногда это нужно, но в шаге с тестами это выключатель всей проверки.try/catch, который только печатает ошибку. Скрипт поймал исключение, написал «Ошибка!» и спокойно завершился с нулём. Для CI он успешен.
Что значат частые коды
Сами числа — договорённость, но несколько встречаются постоянно:
- 1 — общая ошибка. Так завершается упавший Python или Node, если исключение никто не поймал.
- 127 — команда не найдена. Опечатка или программа не установлена.
- 126 — файл есть, но запустить его нельзя: нет прав на выполнение.
- 130 — ты нажал Ctrl+C. Считается так: 128 + номер сигнала (2).
- 137 — процесс убили принудительно: 128 + 9. В Docker это почти всегда значит одно — контейнеру не хватило памяти.
Видишь в логах exited with code 137? Не ищи баг в коде. Ищи, кто съел память: посмотри docker stats или подними лимит памяти контейнера.
Как задать свой код выхода?
В Python — sys.exit(2), в Node — process.exitCode = 2. Пригодится, если твой скрипт проверяет что-то для CI: нашёл проблему — верни не ноль, и пайплайн остановится.
Почему тесты падают в CI, а локально проходят?
Это уже другая история — про окружение, а не про коды. Разбор частых причин — в статье почему тесты проходят локально, но падают в CI.
Код выхода и текст ошибки — это одно и то же?
Нет. Текст программа пишет в отдельный поток для ошибок — stderr. Подробно об этом в статье что такое stdout и stderr. Код выхода — только число в самом конце.
Короткие уроки-истории, симулятор агента и ежедневная практика — в нашем мобильном приложении. Бесплатно.





