Артефакты: передача результатов между джобами
Учимся сохранять результаты джобы и передавать их дальше по пайплайну.
Артефакт — файлы, которые джоба сохраняет после выполнения; их можно скачать и автоматически передать следующим джобам.
Зачем нужны артефакты
С docker-executor каждая джоба — чистый контейнер, который удаляется после завершения. Если джоба build собрала бинарник, а джоба deploy должна его выложить — нужен мост. Этим мостом и служат артефакты: build объявляет их, GitLab складывает на сервер, а зависимые джобы получают эти файлы в свою рабочую папку.
Чтобы прочувствовать проблему, представьте конвейер без артефактов. Джоба build запускается в контейнере, скачивает зависимости, компилирует приложение и кладёт результат в dist/. Контейнер выполнил последнюю строку script — и тут же уничтожается вместе со всей файловой системой. Следующая джоба deploy стартует в совершенно новом контейнере, который ничего не знает о предыдущем: для него dist/ просто не существует. Без явного механизма передачи результат сборки исчезает в момент завершения джобы. Именно эту пропасть между изолированными контейнерами и закрывают артефакты — они материализуют результат работы джобы в виде файлов, которые GitLab берёт на себя сохранить и доставить туда, где они понадобятся.
Важно понимать, что артефакт — это не «общая папка» и не сетевой диск. Это снимок указанных файлов, сделанный в конкретный момент: сразу после успешного (или, при настройке, любого) завершения script. GitLab архивирует их и хранит как самостоятельную сущность, привязанную к конкретной джобе конкретного пайплайна. Поэтому артефакты переживают смерть контейнера, доступны для скачивания через веб-интерфейс и API спустя дни после запуска, и могут быть подтянуты в любую зависимую джобу. Эта модель «джоба производит файлы → платформа хранит → потребители забирают» лежит в основе передачи данных между стадиями.
build:
stage: build
script:
- npm ci
- npm run build
artifacts:
paths:
- dist/
expire_in: 1 week
deploy:
stage: deploy
script:
- ls dist/ # папка уже здесь — пришла из артефактовЗдесь джоба build сохраняет каталог dist/. Любая последующая джоба той же или более поздней стадии получит dist/ автоматически.
GitLab CI против GitHub Actions: разный подход к передаче файлов
Если вы знакомы с GitHub Actions, разница в философии бросается в глаза. В Actions передача файлов между джобами — явная двухсторонняя операция: одна джоба вызывает actions/upload-artifact, другая — actions/download-artifact с тем же именем, и без этих шагов ничего не передастся. В GitLab CI всё устроено декларативно и асимметрично: продюсер описывает блок artifacts, а потребителю не нужно ничего «скачивать» вручную — раннер сам распакует артефакты предыдущих стадий в рабочий каталог ещё до первой строки script. Меньше церемоний, но и меньше контроля: в GitLab по умолчанию подтягиваются артефакты всех джоб предыдущих стадий, тогда как в Actions вы всегда называете конкретный артефакт. Понимание этого «магического» автоматического подтягивания экономит часы отладки, когда файл вдруг оказывается на месте сам собой или, наоборот, тянется лишнее.
expire_in: не копить вечно
Артефакты занимают место. Ключ expire_in задаёт срок жизни: 1 week, 30 days, 1 hour. По истечении GitLab удалит их. Артефакты последнего успешного пайплайна по умолчанию можно сохранять дольше (настройка проекта), но в целом задавать срок — хорошая гигиена.
Дисциплина с expire_in — это не мелочь, а вопрос здоровья всей инсталляции. Пайплайны запускаются на каждый push и каждый Merge Request, и без срока хранения это превращается в неудержимый рост: гигабайты сборок и временных файлов копятся месяцами, забивают хранилище, замедляют резервное копирование и в худшем случае останавливают весь GitLab, когда место заканчивается. Поэтому опытные команды относятся к expire_in как к обязательному полю: короткий срок (часы или день) для промежуточных сборок, нужных лишь внутри пайплайна, и более длинный (недели) — только для артефактов, которые действительно могут понадобиться постфактум, например релизных бинарников.
when: сохранять даже при падении
По умолчанию артефакты сохраняются только при успехе джобы. Но логи и отчёты о падении часто нужны именно когда джоба упала. Тогда указывают artifacts: when: always (или on_failure):
test:
script:
- pytest --junitxml=report.xml
artifacts:
when: always
paths:
- report.xmlЛогика поведения по умолчанию вполне разумна: если джоба упала, её результат, скорее всего, бракованный, и хранить его незачем. Но тесты — особый случай. Когда pytest завершается с ненулевым кодом, сама джоба считается упавшей, и без when: always отчёт report.xml — тот самый файл, который объясняет, что именно сломалось, — будет выброшен ровно в тот момент, когда он нужнее всего. Получается парадокс: чем важнее диагностика, тем вероятнее, что вы её потеряете при настройках по умолчанию. Поэтому для любой джобы, чей смысл — производить отчёт о проблемах (тесты, линтеры, сканеры), правило простое: ставьте when: always. Значение on_failure используют реже — для тяжёлых дампов и трейсов, которые имеет смысл сохранять только когда что-то пошло не так, чтобы не раздувать хранилище на удачных запусках.
Отчёты (reports)
Особый вид артефактов — artifacts: reports:. GitLab умеет разбирать стандартизированные форматы: junit для результатов тестов, coverage_report для покрытия, отчёты сканеров безопасности. Такие отчёты показываются прямо в Merge Request: упавшие тесты, изменение покрытия, новые уязвимости — всё рядом с diff. Это сильная сторона интегрированной платформы.
Ключевое отличие reports от обычных paths в том, что GitLab не просто хранит файл, а понимает его содержимое. Загрузив junit-отчёт, платформа разбирает XML, сравнивает список тестов с предыдущим запуском и показывает в Merge Request виджет: «упало 2 новых теста, починилось 3, всего 145». Ревьюер видит это, не открывая логи и не скачивая ничего, — прямо на странице ревью. То же с покрытием: GitLab сопоставляет процент с базовой веткой и предупреждает, если новый код снижает покрытие. Это превращает CI из «прогонщика скриптов» в инструмент, который активно помогает принимать решение о слиянии. В GitHub Actions сопоставимый результат обычно достигается сторонними actions и ботами, оставляющими комментарии в PR; в GitLab разбор отчётов встроен в саму платформу, потому что CI/CD и ревью живут в одном продукте, а не склеены интеграциями.
Как работает под капотом
После script раннер собирает указанные в paths файлы в архив и загружает на сервер GitLab, привязывая к джобе. Когда запускается зависимая джоба, раннер до выполнения её команд скачивает и распаковывает артефакты нужных предыдущих джоб в рабочий каталог. С needs подтягиваются только артефакты перечисленных джоб; без needs — всех джоб предыдущих стадий.
Разберём этот цикл подробнее, потому что именно здесь прячется большинство сюрпризов. Раннер собирает архив строго относительно рабочего каталога джобы (обычно это склонированный репозиторий): пути в paths трактуются как относительные к этому корню, и всё, что лежит вне него, в архив не попадёт. Архив уходит на сервер по HTTP, поэтому крупные артефакты — это реальный сетевой трафик и время как на выгрузку у продюсера, так и на загрузку у каждого потребителя. Теперь становится понятен смысл needs не только как инструмента ускорения пайплайна: с явным needs: ["build"] джоба deploy подтянет артефакты только из build и ничего лишнего, тогда как без needs раннер честно скачает и распакует артефакты всех джоб всех предыдущих стадий. На большом пайплайне с десятком джоб в стадии это превращается в заметные минуты, потраченные впустую на распаковку файлов, которые джобе не нужны. Поэтому связка «узкие paths + точечные needs + разумный expire_in» — это не три отдельные настройки, а единая стратегия управления стоимостью артефактов: по диску, по сети и по времени пайплайна.
Частые ошибки
- Указать в
pathsпуть вне рабочей директории — артефакты собираются только из рабочего каталога джобы. - Не поставить
when: alwaysи удивляться, что отчёт о падении не сохранился. - Складывать в артефакты гигабайты
node_modules— для зависимостей есть кеш, не артефакты (следующий урок). - Забыть
expire_inи постепенно забить хранилище мусором — пока место не закончится в самый неподходящий момент. - Полагаться на автоматическое подтягивание артефактов всех предыдущих стадий вместо явного
needs— пайплайн раздувается и замедляется незаметно.
Итоги
- Артефакты переносят файлы между джобами, преодолевая изоляцию контейнеров.
expire_inограничивает срок хранения;when: alwaysсохраняет даже при падении.reports(junit, coverage) интегрируются в Merge Request.- В GitLab потребитель получает артефакты автоматически (декларативно), в отличие от явных upload/download в GitHub Actions.
needsсужает подтягивание до нужных джоб — это и про скорость, и про экономию сети.