Как релизить библиотеку с открытым кодом в 2020 году

@

Выпуск библиотеки — это непростая задача, но с нужными инструментами это проходит намного легче. На конференции HolyJS Ундже Ли (Eunjae Lee) показал, как можно автоматизировать процесс релиза, как сделать его асинхронным и коллаборативным.

Ниже — видео и перевод этого доклада.

Далее повествование — от лица спикера.

Я работаю на компанию Algolia. Это сервис удаленно размещенного поиска, который дает разработчикам полный набор инструментов для создания поиска в их продуктах. В Algolia есть инфраструктура, движок, база данных и много других вещей на бэкенде. Помимо этого, есть различные REST API, которые используются для индексирования и поиска записей и прочего. Кроме REST API у нас есть API-клиенты. Мы предоставляем API-клиенты для большого количества разных языков, таких как PHP, Ruby, JavaScript, Python, Kotlin, .NET, Java, Golang, Scala, iOS, Android.

В общем, мы делаем многое для API-клиентов. И, помимо API-клиента на JavaScript, мы делаем нечто под названием Instant Search (мгновенный поиск). Это что-то наподобие компонента JavaScript. Вместо вызова поисковых методов API мы предоставляем компоненты, которые вы можете подключить. Например, строки поиска, клики, фильтры, пагинаторы.

В Instant Search мы поддерживаем InstantSearch.js для Vanilla JavaScript, у нас есть React InstantSearch, Vue InstantSearch, Angular InstantSearch и другие библиотеки. Моя команда управляет большим количеством библиотек с открытым кодом, и нам необходимо их релизить.

Скорее всего, большая часть из вас любит кодить. Кто-то из вас кодит после работы в стороннем проекте. Есть люди, которые не являются разработчиками, но кодят просто потому что любят это. И, думаю, много людей любят тестировать. Вы знаете TDD — Test Driven Development (разработка на основе тестов). Некоторые люди любят дебаггинг, потому что мы постоянно сталкиваемся с багами, и иногда очень сложно выяснить причину. Когда у нас получается и мы исправляем их, то радуемся. Но люди едва ли любят релизить, особенно библиотеки. Я ни разу не слышал, как кто-то говорит: «Я люблю релизить, пойду после работы выпущу что-нибудь».

Существует много CI/CD-сервисов, таких как Netlify, Vercel, Bamboo, Jenkins. Так что автоматическая сборка приложений достаточно хорошо автоматизирована, хоть и не без шероховатостей. В общем, со сборкой приложений дела обстоят более или менее неплохо.

Проблемы при релизе библиотек

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

Также мы постоянно боимся ошибиться. При выпуске библиотек есть шансы, что мы можем допустить ошибку.

Например, если использовать неверную версию Node.js, то это может привести к неожиданным эффектам при сборке. Или иногда я могу забыть выполнить git pull и опубликовать что-нибудь устаревшее. Да, это очень пугает. Однажды я запустил npm publish, но забыл поставить --tag beta. То есть должен был выпустить пакет как бету, а выпустил как стабильную версию. Ну и node_modules — источник головной боли. Переключение веток иногда приводит их в некорректное состояние, заметить которое очень тяжело.

Другая причина — вы заблокированы. Например, мне нужно запустить lint, unit test или e2e test и дождаться завершения. Вот скриншот моей библиотеки из CircleCI.

Полное тестирование занимает всего лишь 4 минуты 30 секунд, не так много. Но может быть и намного дольше, если проект большой и вы пишете много тестов.

Также мы заблокированы, потому что релизим из основной ветви. Когда я запускаю тесты или сборку, мне нужно дождаться их окончания. Я не могу работать в других ветках.

Семантическое версионирование и общепринятые коммиты

Я рассказал, почему мы не любим выпускать библиотеки, а сейчас расскажу, на что стоит обратить внимание при выпуске библиотек.

Если вы не слышали о semver, это означает «семантическое версионирование». Обычно версии выглядят так: v1.2.3.

  • 1 — мажорная версия
  • 2 — минорная версия
  • 3 — патч-версия

При выпуске новой версии нужно обновить правильный номер, чтобы не сломать приложения пользователей. Также многие авторы библиотек используют общепринятые коммиты (conventional commits), и вам нужно писать комментарии к коммитам особым образом. Вот пример комментария к коммитам.

fix: should use fallback for scoped slots with...

Еще один пример ниже.

fix(compiler): remove the warning for valid....

В этом случае compiler называется скоупом (область видимости).

Может быть feat.

feat(ssr): inheritAttrs support in SSR

Здесь ssr тоже является скоупом.

Если в комментарии к коммиту есть строка BREAKING CHANGE, это значит, что будет критическое изменение и мы должны поднять мажорную версию.

Итого:

  • Если комментарий к коммиту начинается с fix, chore, docs, refactor, test и прочего, то это патч-обновление.
  • Если комментарий к коммиту начинается с feat, то это минорное обновление.
  • Если в комментарии к коммиту содержится breaking change, это мажорное обновление.

Общепринятые коммиты используются во многих библиотеках и приложениях с открытым кодом: Vue.js, webpack, Angular, Nuxt.js, Gatsby, jest и других.

Когда вы написали сообщения к коммитам таким способом, то можете использовать conventional-changelog — инструмент для генерации ченджлогов. Ченджлог из примера ниже, скорее всего, был сделан с помощью conventional-changelog.

Рассмотрим еще один пример с комментариями к коммитам. Давайте скажем, что chore: release v0.15.0 — это последняя версия, и мы собираемся выпускать после нее.

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

Вы видите error, эта область видимости выделена жирным шрифтом. И, так как здесь есть коммит feat, то это минорное обновление. Версия изменилась с 0.15.0 до 0.16.0.

Когда мы выпускаем библиотеки, то, скорее всего, должны поставить --tag при выпуске alpha, beta, dev, canary, rc-версий. Вы иногда выпускаете что-то не как стабильную версию, а как alpha или beta, чтобы проверить что-нибудь на ограниченной группе.

Процесс выпуска и инструменты

Давайте вернемся к Instant Search. В Instant Search у нас есть файл scr/scripts/publish.js. Он занимается релизом Instant Search. Делает он следующее:

  • Проверяет, что в гите нет незакомиченных изменений. Если нет, то выбрасывает ошибку и выходит
  • Обновляет версию в package.json, используя общепринятый ченджлог
  • Обновляет ченджлог
  • Коммитит изменение
  • Запускает тесты
  • Выполняет задачи по сборке и подготовке проекта к релизу
  • Публикует его в репозиторий npm, иногда c --tag beta
  • После выпуска создается git tag
  • Пушит новый коммит и tag обратно в git-репозиторий

У нас был этот файл в репозитории Instant Search, значит, у нас есть такой же файл в React Instant Search, Vue Instant Search, Angular Instant Search. Проблема в том, что они немного разные. Мы вносили модификации в каждый из них вручную, и поддерживать это было сложно. И даже с этим скриптом я все еще делал ошибки. Например, в версии Node.js, папке node_modules, версиях Yarn.

np

Я начал искать другие варианты, чтобы еще немного автоматизировать процесс. Например, существует np от Sindre Sorhus. Описание гласит, что это «улучшенный npm publish», и это действительно так. Он проверяет незакоммиченные изменения, устанавливает зависимости, запускает тесты, ставит новую версию, публикует пакет и пушит теги.

Но когда я запускаю его, он спрашивает у меня новую версию. И я опять вынужден ждать, так что это тоже не выход. То есть np — это действительно лучше, чем просто npm publish, но его функционала недостаточно, чтобы покрыть наш скрипт.

semantic-release

Другой вариант — это semantic-release. Это полностью автоматизированный менеджер версий и издатель пакетов. В его документации показаны этапы:

  • проверка условий
  • получение последнего релиза
  • анализ коммитов
  • тестирование релиза
  • генерация ченджлога
  • создание git tag
  • подготовка к релизу
  • публикация
  • оповещения

Да, он действительно автоматизирует много всего. Но если вы взглянете в описание, там сказано: «полностью автоматизированный». Это значит, что у меня нет возможности вмешаться в процесс релиза. Например, я написал неправильный комментарий к коммиту, и набрал fix вместо feat. Тогда версия будет неправильной, и у меня не будет шанса это исправить, потому что процесс полностью автоматизирован.

Три части процесса выпуска

Нужен был баланс между чем-то полностью ручным и чем-то полностью автоматизированным. И я придумал совершенно иной процесс, он состоит из трех частей.

  • подготовка
  • проверка
  • запуск

Часть первая — подготовка. В этой части мы вычисляем следующую версию.

  • Мы можем выяснить, какая версия следующая, потому что использовали общепринятые коммиты.
  • Потом можем переключиться на промежуточную ветку, например, git checkout -b releases/v1.2.3.
  • В ней мы можем обновить версию в package.json и других файлах.
  • Можем обновить ченджлог
  • Коммитим изменение
  • Можем создать pull-запрос из этой ветки к основной.

Часть 2 — проверка. Мы можем изучить pull-запрос и результаты тестов.

Я использую CircleCI, вы можете запускать тесты на GitHub. Можно сделать дополнительные тесты, например, используя CodeSandbox CI или Pika CI.

Что такое CodeSandbox CI? Это сервис который можно интегрировать в вашу сборку, он умеет создавать отдельную песочницу на CodeSandbox, в который установлена свежесобранная версия библиотеки из текущего PR.

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

Помимо этого можно добавить больше коммитов к этому pull-запросу: например, вручную поменять ченджлог. Общепринятый ченджлог обновляется вместе с комментариями к коммитам, но иногда хочется поменять комментарии, добавить что-нибудь или изменить тип. Можно добавить больше коммитов, если хотите.

Затем можно сделать squash and merge. На GitHub есть кнопка Squash and merge. Добавится новый коммит, например, chore: release v1.2.3.

Часть 3 — релиз. CI прошел, тесты прошли, можно запускать скрипт релиза. Скрипт может быть наподобие npm run build, npm publish или npm publish --tag beta или alpha. После этого можно создать git tag и сделать пуш тега назад в ремоут.

Итого:

Первая часть: происходит на вашем компьютере.

  • Обновили версию
  • Обновили ченджлог
  • Выполнили коммит изменений

Вторая часть: происходит на GitHub.

  • Npm run test

Третья часть: происходит на CI/CD.

  • Npm run build
  • Npm publish
  • создали git tag и выполнили его пуш в ремоут.

В трех частях только первая происходит на моей машине, вторая — на GitHub, третья — на CI/CD. Я делаю меньше ошибок, так как я запускаю только первую часть, вторая и третья части — не моя или не ваша ответственность.

ship.js

А теперь я хочу представить наш собственный инструмент: ship.js. С помощью него можно автоматизировать части 1 и 3.

  • Часть 1. Вы просто запускаете команду shipjs prepare. Будут выполнены все задачи из первой части, также будет создан pull-запрос. Это делаете вы, но сама команда автоматизирована.
  • Часть 2. Можете просмотреть pull-запрос на GitHub самостоятельно или с вашими коллегами.
  • Часть 3. Когда коммит добавлен, CI/CD запустит команду shipjs trigger и сделает все остальное.

Выпуск ship.js с использованием ship.js

Я выпускаю ship.js, используя ship.js (да, это забавно). Вот как я это делаю. Недавно была версия 0.20.0-beta.3, которую я хочу выпустить как стабильную версию 0.20.0.

Здесь есть скрипт release, вызывающий shipjs prepare.

Я запущу yarn run release. Он вызывается из ремоута, принимает теги, пушит назад в ремоут и показывает текущую версию — 0.20.0-beta.3 и следующую версию — 0.20.0-beta.4.

Когда выпускаете бета-версии или альфа-версии, обычно увеличивается только последнее число, поэтому он предположил, что следующая версия будет beta.4. Но я хочу выпустить стабильную версию, поэтому просто введу No. Потом ввожу следующую версию.

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

Для краткости я проигнорирую это и просто сделаю мердж. Но не делайте это в своем продакшене.

Это добавит новый коммит в основную ветку, которая называется main. В данном случае: chore: release v0.20.0. И запускается shipjs_trigger.

Вернемся к коду и проверим конфигурацию CircleCI. Видим resease_if_needed, который вызывает shipjs_trigger только на главной ветке.

Он делает очень простые вещи: чекаут и некоторую работу с yarn и yarn shipjs trigger.

Также он запускает ship.js для релиза. Он устанавливает зависимости, и теперь вызван shipjs_trigger, который создает и публикует в репозиторий npm, создает git tag и отправляет тег в ремоут. Всё прошло успешно.

Откроем страницу пакета на npmjs.com и видим, что выпущена новая версия.

На вкладке релизов видим, что новые релизы на месте.

Интеграция ship.js в вашу библиотеку и ее релиз

Теперь покажу, как интегрировать ship.js в вашу библиотеку. Я создал библиотеку-болванку, которая называется eunjae-math-library. Сейчас ее версия — 0.1.0. Я только что создал эту библиотеку, но еще не выпустил ее на репозиторий npm. Файл версии гласит version: 0.1.0, но этой библиотеки на npm пока нет. И я хочу выпустить ее как 0.1.0.

В библиотеке есть простая функция.

Ее версия 0.1.0. Мы сохраним ее и выпустим как 0.1.0. Откроем документацию ship.js, секцию Getting Started. У нас есть команда npx shipjs setup, она помогает добавить ship.js в ваш проект. Она спросит вас о вашей основной ветке и конфигурации CI.

Выбираю CircleCI. Можете выбрать GitHub Actions или просто «Ничего».
Появится вопрос: «Запланировать выпуск через CircleCI?» Отвечаю «Нет».

Когда процесс закончится, ship.js установится как devDependency. Видим, что ship.js добавился.

Появляется новый скрипт release, который вызывает shipjs prepare.

Нам не нужно самому писать всю конфигурацию CI, джобы для сборки были созданы за нас. В ней один воркфлоу, он вызывает джоб shipjs_trigger, причем только на основной ветке. Он выполняет чекаут, установку yarn, управление его кэшем и запуск yarn shipjs trigger.

Коммитим это изменение и пушим. Идем в CircleCI и настраиваем проект.

Добавляем конфигурацию вручную и нажимаем Start Building.

Исполняется джоб shipjs_trigger. Он устанавливает некоторые зависимости. В редакторе вы увидите, что необходимо указать GitHub-токен как переменную среды. Дело в том, что я забыл сделать последний шаг. Откроем терминал, здесь сказано: «Необходимо закончить настройку на CircleCI».

И ниже есть ссылка на документацию. Здесь показано, как настроить CircleCI, что уже сделано командой установки ship.js. Нам нужно настроить два токена: npm-токен и GitHub-токен. Давайте начнем с npm-токена.

Заходим на сайт npm, идем в Auth Tokens, создаем токен для чтения и публикации.

Кстати, я аннулировал токен, так что не пытайтесь прочитать и использовать его. Возвращаемся в CircleCI, идем в Project Settings, Environment Variables. Вводим имя NPM_AUTH_TOKEN, вставляем значение.

Также необходимо добавить токен GitHub. Заходим на GitHub. Можем назвать release eunjae-math-library. Выбираем скоуп repo и генерируем токен.

Этого токена тоже уже нет.

В CircleCI добавляем GITHUB_TOKEN и вставляем его.

Теперь мы закончили с CircleCI. Можем добавить токен GitHub на нашу машину, чтобы поработать с ship.js. Добавляем GITHUB_TOKEN= и вставляем его. Нам нужно, чтобы этот файл был внутри gitignore, потому что мы не хотим коммитить этот токен в репозиторий. Мы обновляем этот файл gitignore.

Теперь yarn run release. Выпустим эту библиотеку первый раз.

Здесь сказано, что git tag 0.1.0 не существует.

Я только создал этот репозиторий, и эта версия была автоматически написана изначальной командой. Я еще не выпустил ее, у меня нет тега.

В вопросе «Из какого коммита вы хотите выпустить?» (From which commit do you want to release?) это мой первый релиз, поэтому я выберу последний вариант. Я выпущу ее как версию 0.1.0, то есть как текущую версию, так что выбираю initial commit.

«Эта следующая версия корректна?»

Нет, здесь сказано, что следующая версия будет 0.1.1. Но в данном случае мы выпускаем ее, как ту же версию, поэтому отвечаем «Нет» и указываем 0.1.0 вручную.

Есть ряд вещей, которые необходимо сделать, когда используете ship.js в первый раз, чтобы все настроить. Но после этого первого релиза во второй раз все будет намного проще.

Обновляется ченджлог, пушит в промежуточную ветку, создается pull-запрос.

Здесь мы видим некоторую основную информацию в описании этого pull-запроса.

При слиянии pull-запроса необходимо выбрать Squash and merge, и убедиться, что тайтл начинается c chore: release v0.1.0. Если нажмете See details, то увидите больше подробностей. Изначально здесь Create a merge commit.

Но нам это делать не нужно. Вместо того чтобы выбирать Squash and merge каждый раз, я могу настроить это для всего репозитория. Откроем настройки, прокрутим вниз и деактивируем два других варианта, чтобы в репозитории была активна была только кнопка Squash and merge.

Видим, что кнопка изменена и нельзя выбрать другие варианты.

Конечно, вы вольны выбирать, но я предпочитаю использовать Squash and merge для библиотек, это позволяет иметь чистую историю коммитов и удобные сообщения и ченджлоги.

Сейчас в ченджлоге немного всего, потому что мы еще ничего не сделали.

Обновим ченджлог. Добавим Initial Release и docs: update CHANGELOG.md.

Отправим, обновим — видим следующее изменение.

Необходимо сделать коммит — chore: release v0.1.0.

Видим в CircleCI, что новый коммит добавлен к основной ветке и выполняется shipjs_trigger. В нем запускается ship.js для релиза. Он публикует в репозиторий npm, создает git tag и отправляет тег в ремоут. Обновим GitHub и видим этот новый тег.

Если наберем в поиске eunjae-math-library на npm, то увидим, что новая версия была выпущена минуту назад, так что все работает хорошо.

Добавим новую функцию — вычитание. Это очень простая функция, которая производит вычитание двух чисел. Оставим к коммиту комментарий feat: add subtract function.

Я могу просто запустить yarn run release. Мы добавили новую фичу, это минорное обновление, так что следующая версия верна.

Обновление отсылается в ремоут и создает pull-запрос. В нем также видим, что версия изменилась верно. Еще в PR написана команда, которая будет вызвана для публикации в npm.

Теперь у нас есть ченджлог, потому что мы использовали общепринятые коммиты.

Версия обновлена. Перейдем по ссылке ниже.

Можно сравнить изменения с предыдущей версией.

Слияние этого pull-запроса запустит очередной релиз. Снова выполняется shipjs_trigger, устанавливаются зависимости, создается сборка, публикуется на npm, выполняется git tag. Обновим снова и увидим новый тег на GitHub и новую версию на npm.

Наконец, сделаем последнее изменение в этой библиотеке — chore: add log.

Создадим файл конфигурации ship.js (ship.config.js). В документации есть ряд типовых конфигураций. В данном случае я собираюсь сделать немного дополнительной работы в обновлении версии. Я скопирую и вставлю это из документации, оставлю только важные части.

Когда версия обновляется, создается или обновляется файл src/version.ts. Файл экспортирует версию как строку.

Вводим chore: update version file on release и выпускаем эту версию. Следующая версия 0.2.1 верна, а в новом pull-запросе изменена версия. Можно посмотреть изменения и увидеть, что создан новый файл.

Все работает верно. Теперь можно просто мерджить pull-запрос, и новая версия будет выпущена.

Также существует команда shipjs setup. Она помогает установить ship.js в библиотеку и дает выбор, использовать ли GitHub Actions. Я сфокусировался на CircleCI, потому что использую именно его. Но можно использовать GitHub Actions.

Также у нас есть референс для всех конфигураций. А если вы используете комментарии к коммитам, основанные на эмоджи, вместо общепринятых коммитов, я могу добавить новую конфигурацию к ship.js, чтобы вы могли использовать любую конвенцию, какую хотите. Так что это настраиваемый инструмент.

Заключение

Ship.js — не о инструменте, а о процессе. Вы можете внести вклад в ship.js. Или же «украдите» эту идею и сделайте подобное для других языков, ведь это всего лишь инструмент для улучшения процесса. Если сделаете — дайте мне знать.

Надеюсь, вы наслаждаетесь своим релизом даже по четвергам и пятницам.

Вы можете связаться со мной в твиттере: @eunjae_lee. Также у нас есть аккаунт для @ship_js, так что можете подписаться на ship.js.

Это доклад с июньской конференции, а теперь уже на носу следующая HolyJS: там будут Дэн Абрамов, бывший председатель TC39 Дэниэл Эренберг, основатель Smashing Magazine Виталий Фридман и другие интереснейшие спикеры. О том, какие доклады там будут, мы уже подробно писали на Хабре, а вся остальная информация и билеты есть на сайте.
Данные о правообладателе фото и видеоматериалов взяты с сайта «Хабрахабр», подробнее в Правилах сервиса
Анализ
×
Абрамов Дэн
Фридман Виталий
Apple iOS
Производитель:Apple
103
Google Android
Производитель:Google
133