frontend-ci-cd.md — vim

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-системы.

Ссылки