Артефакты: передача результатов между джобами

Учимся сохранять результаты джобы и передавать их дальше по пайплайну.

Артефакт — файлы, которые джоба сохраняет после выполнения; их можно скачать и автоматически передать следующим джобам.

Зачем нужны артефакты

С 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 сужает подтягивание до нужных джоб — это и про скорость, и про экономию сети.
Проверьте себя
1. Зачем нужны артефакты в GitLab CI?
AДля подсветки синтаксиса
BЧтобы передавать файлы (сборки, отчёты) между изолированными джобами
CЧтобы ускорить установку зависимостей
DЧтобы хранить секреты
2. Как сохранить отчёт о тестах, даже если джоба упала?
AНикак, при падении ничего не сохраняется
BУказать artifacts: when: always
CПоставить expire_in: 0
DИспользовать cache вместо artifacts