javascript

Как мы во фронтенд-команде ПСБ прокачали релизный цикл npm-пакетов

  • среда, 8 июля 2026 г. в 00:00:12
https://habr.com/ru/companies/psb/articles/1055888/

Привет, Хабр! Меня зовут Оля Борисова, я фронтенд-разработчик в ПСБ. Работала как в продуктовых, так и в платформенной команде. Сегодня хочу поделиться нашим опытом разработки внутренних пакетов, их релизным процессом, расскажу какие проблемы у нас были, а также постараюсь дать советы тем, кто еще в начале пути.

На момент написания статьи у нас около 300 репозиториев, большинство из которых представляет собой внутренние библиотеки, утилиты, конфиги и микрофронты. 

Так было не всегда, когда-то у нас был один проект и все что с ним связано (компоненты, хелперы, конфиги и прочее) хранилось внутри него. Это работало пока не появилась надобность переиспользования наработок и лучших практик в других приложениях и направлениях. Так мы пришли к тому, что нам необходимы внутренние пакеты и механизмы их организации и менеджмента.

В начале был пакет, и имя его было 0.1.0

Мы создали свой первый пакет, выставили ему версию 0.1.0 и начали его развивать. Оказалось, что у разработчиков различается видение версионирования, кто-то видит опечатку в одной букве в экспортируемой функции, исправляет её и поднимает патч-версию, кто-то в ходе рефакторинга удаляет старую реализацию, заменяя её новой. Автор изменений чувствует себя прекрасно, он с легкостью обновляет версию, вносит правки в приложении. Для него это ожидаемые изменения. Другие же разработчики, указавшие версию в виде 0.1.0 могут столкнуться с сюрпризом в виде сломанного билда не в самый подходящий момент (например, разработчик использовал старое окружение, не обновлял node_modules, тогда проблема возникнет на CI).

Проблему можно решить указанием фиксированной версии, но мы хотим получать улучшения и фиксы без постоянного ручного обновления, поэтому лучшим вариантом для нас стало введение обязательности соблюдения правил и прозрачности версионирования на уровне всех проектов. Благо, до нас уже придумали семантическое версионирование (Semver).

Как мы начали использовать Semver и migration guide

Если вкратце, то согласно Semver версия представляет набор из трех параметров MAJOR.MINOR.PATCH, изменение значения каждого из которых четко регламентировано: 

· PATCH (x.y.Z): Обратно совместимые исправления багов.

· MINOR (x.Y.z): Новая функциональность, обратно совместимая, без нарушения публичного API.

· MAJOR (X.y.z): Критические изменения, ломающие обратную совместимость. Требуют особого внимания всей команды.

Начали использовать. Вышло здорово, разработчики стали понимать чего ожидать от новой версии, с патчами и минорами проблем не возникало. Но мы стали замечать, что коллеги не всегда оперативно обновляются до мажорных версий, порой нам приходили запросы на поддержку нескольких версий одновременно.  Собрав обратную связь мы поняли, что, несмотря на наличие CHANGELOG, каждому разработчику не всегда очевидно сразу, какие изменения необходимо внести, чтобы элементарно починить билд, также непонятно, что стоит подсветить тестировщику на регрессе. Все это приходится определять путем проб и ошибок, обращаясь к исходному коду библиотеки, нельзя никак переиспользовать этот опыт между проектами и разработчиками. Масштаб перехода часто пугает и разработчика и команду, затмевая профит. 

Решение данной проблемы оказалось достаточно простым - сопровождение мажорных изменений обязательным migration guide. Мы заносим перечень breaking changes с примерами кода для их исправления. 

Разработчик может сразу оценить изменения, запланировать подготовительные работы в своем проекте, многие ошибки решаются простым копированием примеров кода из migration guide. Также введение migration guide упрощает добавление автоматизированных скриптов миграции.

Публикация пакетов

Отлично, мы создали пакет, договорились о правилах версионирования, теперь нужно определиться с тем как этот пакет будет попадать к конечному пользователю, встает вопрос публикации: куда публиковать и кто должен это делать. 

Для хранения пакетов используются registry. Их можно поделить на:

  • публичные (доступные всем в интернете, например https://registry.npmjs.org/ );

  • приватные registry (корпоративные, в контуре компании, либо облачные решения, например Verdaccio, Nexus, Github Packages);

  • гибридный вариант (при запросе проверяет наличие локально, а если не находит, то подгружает из публичного реестра).

Наш выбор пал на приватный registry (используем Sonatype Nexus). Для разработчика это выглядит как единоразовый вызов команды 

npm config set @myco:registry=http://reg.example.com,

вся дальнейшая работа происходит как со стандартным реестром. 

С реестром определились: теперь каждый разработчик может вызвать npm publish и опубликовать пакет со своего рабочего ноутбука. Но также нам необходимо было избежать риска компрометации рабочей машины, исключить банальный человеческий фактор и минимизировать ситуации, когда у разных разработчиков разное окружение, предустановленные версии пакетов, отличия в версиях node.js и npm. 

Оптимальным решением стал вынос публикации на CI. Раннер является изолированным окружением, с фиксированными версиями node.js и npm, токен хранится в зашифрованных секретах. Помимо этого выполнение на CI позволяет автоматизировать рутинные проверки  и гарантирует их выполнение.

Выходим на CI/CD

В общем виде сейчас наш процесс на CI/CD выглядит следующим образом:

  1. Срабатывает триггер, им может быть пуш в ветку или создание Pull/Merge Request.

  2. Запуск тестов (unit, e2e, скриншот-тесты для библиотек с компонентами).

  3. Анализ кода, проверка на уязвимости , использование статических анализаторов (eslint, stylelint).

  4. Проверка/генерация необходимых артефактов (changelog, migration guide).

  5. Запуск релиза и публикация при необходимости.

В пятом пункте я указала, что релиз запускается при необходимости, как же определяется эта необходимость? Здесь все достаточно просто, мы проверяем наличие пре-релизной версии в поле “version” в package.json-е. Но откуда она там берется? Есть две стратегии:

  1. Поднятие версии разработчиком напрямую (разработчик запускает команду, которая предлагает ему выбрать тип вносимых изменений: мажор, минор или патч и обновляет версию согласно выбранному типу);

  2. Автоматическое поднятие версии на основании коммитов разработчиков, для этого используется conventional commits, коммиты в определенном формате.

Мы используем оба варианта, но в разных условиях. Ручное поднятие предпочтительнее, когда необходим полный контроль версионирования со стороны разработчика выполняющего изменения, у нас используется для критически важных пакетов, там где один коммит = одна версия. Из недостатков можно выделить необходимость дополнительный действий от разработчика. Автоматизированный вариант снимает ответственность в принятии решения с разработчика, позволяет генерировать CHANGELOG, но требует от разработчика более осмысленного именования коммитов, мы этот режим используем в микрофронтах, где релизный процесс отличается от релиза обычных пакетов, и в релизную версию попадает сразу несколько коммитов. 

Как мы пришли к концепции пре-релизов 

Отдельно стоит отметить процесс интеграционного тестирования изменений. Но мы хотели исключить ситуацию, когда после выпуска версии, разработчик приходит с фиксами проблем, которые воспроизводятся только во время интеграции. Почему так вообще получается? Мы разрабатываем и проверяем свои изменения в идеальной изолированной среде, компоненты смотрим в сторибуке без каких-либо внешних воздействий, что не всегда соответствует действительности. Например, бывают конфликты с глобальными стилями, либо возникают ошибки во время функционального тестирования командой QA.  

Невозможно все предусмотреть, но хотелось бы дать возможность разработчику проверить свои изменения до публикации конечной версии. Для этого мы внедрили концепцию пре-релизов (Prerelease Tags). Когда разработчик создает Pull Request, CI автоматически собирает пакет и публикует его в registry с тегом, соответствующим текущей ветке @.../ui-kit@0.12.3-my-awesome-feature-branch.0, после влития создается дополнительная джоба, которая публикует уже конечную релизную версию. Таким образом, владелец зависимого приложения может установить пре-релизную версию, чтобы проверить, как его приложение будет работать с новым кодом до релиза новой версии пакета.

Описанный мною процесс является стандартом внутри нашего центра компетенций и покрывает большинство наших базовых потребностей, но не всегда идеален. Давайте представим, что мы разрабатываем утилиту генерации контрактов на основании swagger-схемы. Разделим процесс генерации на 3 шага:

  1. Построение AST на основании схемы;

  2. Валидация схемы;

  3. Генерация контрактов.

Также мы хотим, чтобы бэкенд-разработчики понимали, по каким правилам мы валидируем схему и могли выявлять ошибки еще до передачи схемы frontend-разработчику. Отсюда возникает идея разделить нашу утилиту на три пакета согласно описанным шагам, так как нам нужно построение AST и для валидации и для генерации, а также мы хотим иметь возможность использовать валидацию без генерации.

Мы можем положить каждый пакет в отдельный репозиторий, но очевидно, что между ними есть некоторая связность: внесение правок в пакет построения AST приведет к необходимости обновления его версии в зависимостях и валидатора и генератора, изменение правил валидации также потребует обновление версии в генераторе.

В процессе разработки каждое такое изменение будет сопровождаться локальной сборкой, сборкой на CI, публикацией в registry, обновлением версии в каждом из репозиториев. Такой подход значительно увеличивает время разработки и добавляет лишние промежуточные действия.

А если монорепозиторий?

Можно поработать над ускорением CI и автоматизацией рутины, но есть и другой вариант— монорепозиторий. Монорепозиторий — это способ организации репозитория, когда физически в одном репозитории находится несколько проектов.  

Давайте представим, что мы перенесли несколько проектов в один репозиторий. Допустим у нас есть пакеты A, B, С. Их иерархия выглядит следующим образом.

Как мы можем их совместно использовать? Мы можем прописать прямые импорты, для красоты даже можно добавить typescript path aliases.

Но это всего лишь визуально улучшит восприятия кода. К тому же, если вы соберете проект и попытаетесь запустить его, то он упадет с ошибкой, потому что компилятор typescript не заменяет такой путь на фактический,  для этого потребуются дополнительные инструменты или этапы сборки. 

Правильным решением будет использование workspaces. Это встроенная функция большинства пакетных менеджеров. Для её настройки достаточно указать в конфиге:

Теперь, когда вы вызовете npm install в корне проекта, то npm автоматически создаст симлинки на внутренние пакеты и поместит их в node_modules корневого проекта. 

Решение проблемы зависимостей

Однако, большинство пакетов необходимо собрать перед их использованием. Возникает проблема определения порядка сборки пакетов и их зависимостей - построение графа зависимостей и оркестрация задач. Простым решением может стать ручное указание порядка сборки необходимых зависимостей, но тогда каждый раз при добавлении новой зависимости необходимо будет править скрипт и следить за очередностью. Другой вариант указание - typescript project references

Project References — это механизм самого TypeScript, который позволяет разбить большую программу на более мелкие, взаимозависимые проекты, которые компилируются отдельно, но знают друг о друге. Для настройки необходимо:Теперь, когда мы вызовем tsc –build, typescript автоматически определит порядок сборки зависимостей. Также имеется возможность собирать только изменившиеся пакеты и встроенное кеширование результатов. Из недостатков данного решения можно выделить необходимость ручного обновления references как и в предыдущем варианте, а также работу только там где сборка представляет собой простую компиляцию ts -> js и не будет работать определение для определения порядка, если сборка происходит через другие инструменты (Babel, Webpack, esbuild, Rollup). 

Сравнение инструментов управления монорепозиториями

Для решения рассмотренных выше проблем существуют готовые инструменты управления монорепозиториями. Самыми популярными из них являются Turborepo, Nx, Lerna, давайте сравним их и рассмотрим на что стоит обратить внимание при выборе.

Turborepo — быстрый оркестратор задач от Vercel, фокусируется на кешировании.

Плюсы: 

  • Простой конфиг (turbo.json), вы описываете конвейер (pipeline) задач, а turbo run build сам разрулит порядок зависимостей (^build) и распараллелит процессы;

  • Скорость работы;

  • Фокус на кеширование, есть поддержка облачного кеширования.

Минусы: 

  • Нет управления версиями и публикацией (только задачи, кеш);

  • Не содержит встроенных линтеров для архитектуры и генераторов кода;

Когда выбрать:

  • Хотите попробовать монорепозиторий и необходим быстрый старт;

  • Вам нужна только скорость и кеширование;

  • Вы используете Next.js или стек Vercel.

  • Публикацию делаете отдельно (Changesets, Lerna).

Nx — больше, чем просто утилита для управления монорепозиторием, это полноценная система сборки с плагинами, генераторами и умным кешированием. Не просто запускает задачи, но и анализирует ваш код. Он строит граф проекта, анализируя import в TypeScript-файлах, а не только зависимости в package.json.

Плюсы: 

  • Отличная поддержка Angular, React, Node;

  • Встроенные линтеры;

  • Автообновление зависимостей, продвинутое построение графа проекта c учетом импортов.

Минусы: 

  • Высокий порог вхождения.

Когда выбрать:

  • Нужны генераторы кода и строгие архитектурные правила из коробки;

  • Вы используете Angular, Nx является стандартом.

Lerna — раньше Lerna была самостоятельным движком и самым популярным инструментом. В 2022 году команда Nrwl (создатели Nx) взяла шефство над Lerna, и теперь она просто обертка для обновление версий, в то время как тяжелая работа по кешированию и сборке делегируется Nx.

Плюсы: 

  • Удобная обертка для обновления версий и публикации в npm.

Минусы:

  • По факту делает все тоже самое, что и workspaces - создает симлинки.

  • Нет собственных механизмов кеширования и оркестрации.

Когда выбрать:

  • Необходимо выстроить только release-процессы для npm-пакетов.

Мы сейчас находимся на начальном этапе внедрения монорепозитория. Так как у нас уже есть своя выстроенная система версионирования и релизов пакетов, а также потребность в гибкой настройке сквозного или независимого версионирования внутри репозитория, мы решили выбрать самое простое нативное решение - npm workspaces и доработка собственного паблишера пакетов. Выше мы рассмотрели ряд недостатков этого варианта, поэтому после отладки процесса мы будем смотреть в сторону либо готовых решений, либо дальнейшего развития своего внутреннего решения. 

Монорепозитории: плюсы и минусы

Мы для себя выделили следующие плюсы и минусы работы с монорепозиториями.

Плюсы:

  • Ускорение разработки за счет сокращения времени ожидания сборки  и публикации пактов на CI;

  • Упрощение менеджмента общих зависимостей, так как есть возможность выноса их в корень без дублирования для каждого пакета и приложения;

  • Единые стандарты и конфигурации для всех приложений внутри репозитория;

  • Упрощение внесения кросс-проектных изменений;

  • Ускорение сборки при корректной настройке;

  • Возможность синхронизации версионирования путем введения сквозной версии;

  • Автоматическое обновление зависимостей внутри монорепозитория.

Минусы:

  • Невозможность разграничения доступа, разработчик получает полный доступ ко всем проектам внутри репозитория;

  • Разрастание истории Git, замедление работы систем контроля версий;

  • Необходимость дополнительной настройки, а также дополнительного времени на погружение при переходе в существующих проектах;

  • Замедление LSP в IDE при неправильной настройке;

  • Замедление сборки при неправильной настройке.

Итог

В заключение хочу оставить пару советов, которые помогут улучшить ваши процессы:

  1. Семантическое версионирование должно быть строгим стандартом для разработки;

  2. Введите changelog и migration guide

  3. Все что можно автоматизировать — автоматизируйте;

  4. Используйте стратегию пре-релизных версий;

  5. Попробуйте монорепозитории.

И пара полезных ссылочек напоследок: