CI/CD для фронтенда: конфиг, который можно забрать себе
Порядок шагов, кэш зависимостей, multi-stage Docker и превью-стенд на каждый PR. Плюс грабли с переменными окружения, на которых обжигаются все.
Фронтенд долго считался местом, где CI не нужен - там же просто билд. Потом выяснилось, что ручная сборка с ноутбука это источник половины инцидентов: не та ветка, не те переменные, забыл прогнать тесты, а на машине лежал закешированный node_modules полугодовой давности.
Разбираю пайплайн, который я ставлю на новых проектах.
Порядок: сначала дешёвое
Что падает часто и проверяется быстро, идёт первым. Разработчик должен узнать об ошибке через минуту, а не через десять.
stages: [check, test, build, deploy]
install:
stage: check
script: pnpm install --frozen-lockfile
cache:
key:
files: [pnpm-lock.yaml]
paths: [.pnpm-store]
lint:
stage: check
script:
- pnpm lint
- pnpm typecheck # vue-tsc --noEmit
test:
stage: test
script: pnpm test --coverage
build:
stage: build
script: pnpm build
artifacts:
paths: [dist]
expire_in: 1 week
--frozen-lockfile обязателен. Без него CI может поставить версии, отличные от локальных, и получится классическое «у меня работает».
Кэш вешаем на хэш lock-файла: поменялся - переустановка, не поменялся - достаём из кэша. На среднем проекте это разница между тремя минутами и двадцатью секундами. Для pnpm кэшируем store, а не node_modules - он общий и переиспользуется между ветками.
Docker: собираем в одном образе, отдаём другой
В финальный образ не должно попасть ни исходников, ни node_modules:
FROM node:22-alpine AS build
WORKDIR /app
RUN corepack enable
COPY pnpm-lock.yaml package.json ./
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
Сотни мегабайт против полусотни. Это не про эстетику: меньше образ - быстрее выкатка и меньше поверхность атаки.
Порядок команд тут важнее, чем кажется. Сначала копируем package.json и lock, ставим зависимости, и только потом весь код - тогда Docker кэширует слой с зависимостями и не пересобирает его из-за правки в одном компоненте.
Конфиг nginx для SPA - две строчки, которые забывают все:
location / {
try_files $uri $uri/ /index.html; # без этого F5 на /users даёт 404
}
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable"; # у файлов хэш в имени
}
А вот index.html кэшировать нельзя, иначе после деплоя люди продолжат получать старую версию приложения и будут писать в поддержку, что ничего не изменилось.
Один артефакт на все окружения
Частая ошибка - собирать отдельный образ на dev, stage и prod. Тогда «проверено на stage» ничего не значит: в прод едет другая сборка.
Правильно - собрать один раз, а конфиг подставить при запуске:
#!/bin/sh
cat > /usr/share/nginx/html/config.js <<EOF
window.__APP_CONFIG__ = { apiUrl: "${API_URL}", sentryDsn: "${SENTRY_DSN}" };
EOF
exec nginx -g 'daemon off;'
И раз уж речь про переменные: ничего секретного в VITE_*. Всё, что попало в бандл, доступно любому через DevTools. На фронте не бывает секретных ключей, бывают публичные - это стоит повторять новым людям в команде примерно каждый месяц.
Превью-стенд на каждый PR
Самое окупаемое, что можно добавить к фронтовому пайплайну. Каждый пул-реквест получает свой URL:
review:
stage: deploy
script: deploy-to "pr-$CI_MERGE_REQUEST_IID"
environment:
name: review/$CI_MERGE_REQUEST_IID
url: https://pr-$CI_MERGE_REQUEST_IID.stage.example.com
on_stop: stop_review
rules:
- if: $CI_MERGE_REQUEST_IID
Дизайнер и продакт смотрят фичу до мержа, тестировщик не ждёт свободного стенда, обсуждение идёт по ссылке, а не по скриншотам в переписке. У нас это сократило цикл «сделал - показал - поправил» с дней до часов, и я до сих пор считаю это лучшим вложением времени в инфраструктуру фронта.
Только обязательно с автоудалением после мержа, иначе через полгода в кластере будет сотня мёртвых окружений и удивлённый счёт за облако.
Что ещё стоит добавить
Аудит зависимостей (pnpm audit, Renovate) - но не блокирующим шагом, иначе команда научится его игнорировать, и он станет бесполезным.
Проверку размера бандла: падать, если вырос больше чем на N процентов. Без неё бандл растёт незаметно, по двадцать килобайт за спринт, а через год все удивляются.
Lighthouse CI на пару ключевых страниц - регрессии производительности видно сразу.
Хороший пайплайн - тот, о котором не думают. Быстрый, падает по делу, выкатывает один и тот же проверенный артефакт. Всё остальное - синтаксис конкретной CI-системы.
Ссылки
- Multi-stage сборки в Docker и кэш слоёв
- Переменные окружения в Vite - почему
VITE_виден всем - Review Apps в GitLab
- Cache-Control на MDN
- pnpm в CI