Настоящая сборка: свой образ и npm в пайплайне
Меняем echo на реальный сборщик: образ node, npm ci, esbuild и dist в артефактах. Разбираемся, откуда берётся окружение job.
Что не так с предыдущей сборкой
До сих пор «сборка» была командой cp. Настоящий проект собирают
инструментом: бандлером, компилятором, генератором статики. Инструмент надо
где-то взять, и вот тут появляется ключевое свойство job, которое мы пока
не трогали.
Берём материалы урока:
git pull --no-rebase upstream main
cp -r lesson-07-build/src lesson-07-build/package.json lesson-07-build/package-lock.json .
cp lesson-07-build/pipeline.yml .gitlab-ci.yml
image: окружение задаётся в конфиге
Каждый job выполняется в контейнере, и образ выбираете вы:
build:
stage: build
image: node:22-alpine
script:
- npm ci
- npm run build
Без image раннер берёт свой образ по умолчанию, и там может не оказаться
ни Node, ни Python, ни нужной версии. Явный образ решает вопрос
«у меня работает, а на CI нет»: версия инструмента записана в репозитории
рядом с кодом.
node:22-alpine вместо node:22 берут ради размера, alpine-вариант
качается в разы быстрее. Тег 22 фиксирует мажорную версию. Как выглядит
вариант без тега и почему так не делают, разберём в уроке про антипаттерны.
npm ci, а не npm install
- npm ci
- npm run build
npm ci ставит зависимости строго по package-lock.json и падает, если
lock разошёлся с package.json. npm install в такой ситуации молча
поправит lock и поедет дальше, а вы получите на CI не те версии, что
локально.
Правило простое: на машине разработчика npm install, в пайплайне
npm ci.
Сам билд описан в package.json:
"scripts": {
"build": "mkdir -p dist && esbuild src/app.js --bundle --minify --outfile=dist/app.js && cp src/index.html style.css dist/"
}
Пайплайн не знает про esbuild ничего. Он вызывает npm run build, а чем
именно собирать, решает проект. Это важно: инструмент можно поменять, не
трогая CI.
Результат в артефакты
artifacts:
paths:
- dist
Собранное лежит в dist, а Pages ждёт папку public, поэтому стадия
деплоя просто переименовывает:
pages:
stage: deploy
script:
- mv dist public
artifacts:
paths:
- public
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
Обратите внимание: в pages нет ни image: node, ни установки
зависимостей. Node нужен только на сборке, деплою хватает дефолтного
образа и одной команды mv. Каждый job тянет ровно своё окружение.
Сколько это занимает
Полный прогон с установкой зависимостей и сборкой укладывается примерно в 30 секунд, из них сама установка меньше секунды: пакетов в проекте два.
На реальном фронтенде с сотнями зависимостей установка станет самой долгой частью пайплайна. Что с этим делать, в следующем уроке.
Если сборка падает
npm ci can only install packages when your package.json and package-lock.json or npm-shrinkwrap.json are in sync — lock устарел.
Локально npm install, закоммитить обновлённый lock.
sh: esbuild: not found — зависимости не установились или установились в
другом job. Каждый job начинается с чистого контейнера, поэтому npm ci
нужен в том же job, где идёт сборка.
npm ERR! code EACCES в самописном образе — сборка запускается от
пользователя без прав на папку. В официальном node этой проблемы нет.
Что важно запомнить
image в job задаёт окружение: версия инструмента живёт в репозитории, а
не в голове того, кто настраивал раннер.
npm ci в CI обязателен, он ставит ровно то, что в lock-файле, и падает
при расхождении.
Что именно собирается, описывает проект в package.json; пайплайн только
дёргает npm run build.
Каждому job нужен свой образ и свои шаги: деплою Node не требуется.
Дальше ускоряем установку зависимостей кешем и разбираемся, чем кеш отличается от артефактов.
Прошли материал до конца? Отметьте урок пройденным.
Прогресс сохранён в этом браузере
Почистишь историю или откроешь с телефона, и отметки пропадут. Оставь почту, они переедут в аккаунт. Пароль придумывать не нужно.
Готово. теперь в аккаунте: прогресс виден с любого устройства. Мой прогресс