Общеизвестно, что каждый программный продукт в конечном итоге обретает номер поставляемой версии. Изначально это может быть цифра в README файле, на борде в JIRA либо просто в голове у тимлида или ПМа. Но в какой-то момент становится понятно, что нужно формализовать процесс назначения версии релизу, отобразить номер в приложении, интегрировать версионность в CI, аналитику и другие места.
С появлением технологии PWA, версионность в вебе обрела еще больший смысл, ведь теперь самая последняя версия приложения доступна пользователю не в момент загрузки страницы, а только через определенное время после обновления файлов в фоновом режиме. Поэтому важно следить за номерами версий у пользователей, чтобы знать в какой из них возникла проблема и сколько обновилось до последней.
Ниже рассмотрим способы добавления версионности веб-проекту, используя готовые решения, и напишем свой универсальный скрипт, удовлетворяющий всем требованиям.
Git и версионность
Для наглядности, давайте взглянем на диаграмму одного из самых популярных подходов Git Flow:
Как видим, нумерация версий происходит в ветке, код которого попадает на прод (разумеется, можно добавлять версии для любых окружений), при этом формат версии обычно задается X.Y.Z (как правило, согласно спецификации семантической версионности, где X - мажорная версия, Y - минорная, Z - патч).
Очевидно, что версии в первую очередь должны быть привязаны к коммитам (после которых и собираются релизные сборки), чтобы хранилась наглядная история релизов и легко было откатываться до предыдущего при необходимости. Удобнее всего это реализовать с помощью тегирования коммитов (git tag). Давайте рассмотрим npm пакеты, помогающие решить эту задачу.
Готовые npm-решения
Основная проблема в том, что все существующие npm пакеты предназначены больше для версионности и публикации вашего проекта в npm непосредственно, хотя никто не мешает воспользоваться функционалом инкрементирования версии сборки и пуша тегов в гит (будь то ручной запуск команды, либо из CI-скрипта).
Примеры:
После установки пакета, выполняем команду np, выбираем какую цифру релиза нужно увеличить. Правда, придется выполнять команду добавив флаги --no-publish --no-tests --no-release-draft
Плюсы:
Обновление версии с помощью одной команды
Минусы:
Нет возможности контролировать, куда записывается номер версии (только в package.json)
Нет возможности добавить постфикс версиям (например, 1.3.1-dev, 0.1.1-alpha)
Не все git сервисы разрешают пушить тег с новой версией и измененный package.json в репозиторий прямо из CI-скрипта после окончания сборки.
Пакет популярнее предыдущего, больше конфигураций, интерактивный режим настройки. Принцип выполнения команды такой же:
release-it minor --no-npm.publish
Для режима CI нужно добавить флаг --ci
Плюсы:
Генерация changelog из коробки (+Conventional Changelog plugin)
CLI
Минусы:
Чтобы хранить номер версии в своем файле нужен отдельный плагин (тем не менее, там нет поддержки .js расширения, поэтому проще использовать пакет replace-in-file) - для отображения версии сборки в самом приложении
Нет возможности добавить постфикс к версии (по крайней мере я не нашел)
Наиболее популярный из представленных примеров. Полностью автоматизирует процесс версионности, убирая человеческий фактор по инкременту версий. Однако, для этого необходимо следовать Commit Message Conventions (хороший повод начать именовать коммиты более организованно).
Инкремент версий происходит по следующей логике: если в названии коммита находится слово fix - это считается как Patch Release (обновляется третья цифра); если в названии коммита присутствует feat (feature) - Minor Release (вторая цифра); perf (performance) или breaking change - Major Release (первая цифра).
Вдобавок, на основе коммитов генерируется changelog.
Минусы:
Не обнаружил возможность хранить версию в отдельном .js файле для последующего отображения в приложении
Возможность контролировать процесс обновления мажорных и минорных версий вручную.
Как видим, готовых решений достаточно, однако, давайте все же попробуем написать свое, адаптировав его под свои нужды.
Что нам нужно?
Код из git-веток попадающий на окружение (dev, staging, prod) должен быть пронумерован и хранить тип окружения (к примеру, 1.0.1-dev)
Каждый пуш в ветку (master, integration, release) увеличивает патч-версию
Обновление мажорной и минорной версии происходит вручную после каждого релиза / спринта. Какую версию менять решаем сами на основе запланированных задач и потенциальных изменений
Версия сборки доступна в JS, для того чтобы была возможность ее отображать в самом приложении, использовать для аналитики, передавать в системы репорта ошибок и т.п.
package.json не должен меняться во время CI (т.е версию приложения не храним в этом файле) во избежание потенциальных мерж конфликтов (к примеру, когда одновременно вмерживается несколько реквестов и сборки собираются одновременно, в нашей команде такое случается достаточно часто).
При релизе патч-версия (z) начинается с 1, оставляя только номер релиза (x.y). Например: версии на деве 1.4.1-dev, 1.4.2-dev, 1.4.3-dev, а в релиз пойдет 1.4.1. Если же подливаем hotfix в тот же релиз, то версия будет 1.4.2.
Данные пункты являются субъективными и легко могут быть изменены под ваши требования. Ниже рассмотрим JS-реализацию данной логики.
Реализация своей системы версионности
Предварительно создадим 2 файла, первый version.txt (в корне проекта) для хранения мажорной и минорной версии релиза (которые мы вручную меняем, как указано выше). В файле будет хранится только 2 числа версии, разделенные точкой вида: 2.13
Создадим второй файл app-version.js (путь - src/environment, т.к. там лежат подобные файлы в Angular проекте, вы же можете выбрать любой удобный путь), который будет меняться CI-скриптом перед сборкой, при этом в самом репозитории файл всегда статичен. Содержимое выглядит так:
exports.APP_VERSION = '{VERSION}'; // DO NOT TOUCH
Это позволит получить доступ к версии прямо во время выполнения javascript / typescript кода приложения и использовать по назначению:
import { APP_VERSION } from 'src/environments/app-version';
Логика определения и назначения версии будет следующая:
получить текущую мажорную и минорную версии (x.y) из файла version.txt
вывести список всех git-тегов данного релиза x.y.*
обнаружить патч версию (z) последнего тега релиза
добавить новый тег вида x.y.(z+1)
при необходимости добавить постфикс окружения (x.y.z-dev)
Приступим к скрипту. Нам нужен пакет npm shelljs - для того, чтобы мы могли работать с системными командами (git, файловая система) прямо из js-файла без нужды писать shell-скрипты. Создадим update-version.js:
const shell = require('shelljs');
if (!shell.which('git')) {
shell.echo('This script requires git');
shell.exit(1);
}
const postfixArg = process.argv[2]; // переданные аргументы во время запуска скрипта начинаются со второго индекса, ссылка: https://nodejs.org/docs/latest/api/process.html#process_process_argv
const tagPostfix = postfixArg ? `-${postfixArg}` : ''; // пример: x.y.z-dev, если нет аргумента - x.y.z
// ПОИСК ПОСЛЕДНЕГО ТЕГА ДЛЯ ТЕКУЩЕГО РЕЛИЗА
const releaseNum = shell.head('version.txt'); // читаем содержимое файла чтобы получить версию (x.y) текущего релиза
const tagsTemplateToSearch = `${releaseNum}.*${tagPostfix}`; // 'x.y.*-postfix' шаблон для поиска предыдущих тегов для текущего релиза
const releasedTags = shell
.exec(`git tag -l ${tagsTemplateToSearch} --sort=-v:refname`)
.split('\n')
.filter(Boolean);
const lastReleaseTag = releasedTags.length > 0 ? releasedTags[0] : '';
shell.echo(`The last tag for ${releaseNum}: ${lastReleaseTag || '-'}`);
// ПОЛУЧЕНИЕ НОВОЙ ВЕРСИИ
let patchVersion = 1;
if (lastReleaseTag) {
// если для данного релиза уже были теги, получить версию последнего патча и увеличить
const lastReleaseVersion = lastReleaseTag.split('-')[0]; // получаем 'x.y.z' из 'x.y.z-postfix'
patchVersion = +lastReleaseVersion.split('.').pop() + 1; // получаем патч версию z из x.y.z и увеличиваем на 1
}
const newVersionTag = `${releaseNum}.${patchVersion}${tagPostfix}`;
shell.echo(`New version tag: ${newVersionTag}`);
// СОХРАНЯЕМ ВЕРСИЮ СБОРКИ В app-version.js
shell.cd('src/environments');
shell.sed('-i', '{VERSION}', newVersionTag, 'app-version.js');
shell.cd('../../');
// КОММИТИМ НОВЫЙ ТЕГ
shell.exec(`git tag ${newVersionTag}`);
Скрипт генерации новой версии приложения на основе предыдущих версий из git готов. Далее можно запускать сборку проекта, зная, что app-version.js с новой версией попадет в проект и будет доступен в JS.
Остался лишь последний шаг - git push нового тега после успешной сборки в git-репозиторий.
Создаем еще один файл push-new-version-tag.js:
const shell = require('shelljs');
const version = require('./src/environments/app-version');
if (!shell.which('git')) {
shell.echo('The script requires git');
shell.exit(1);
}
shell.echo(`Tag to push: ${version.APP_VERSION}`);
// shell.exec(`git push --force origin ${version.APP_VERSION} -o ci.skip`); GitLab CI не позволяет пуш в репозиторий, поэтому делаем POST запрос с флагом --silent и приватным ключом переданным как аргумент из скрипта
const branchName = process.argv[2];
const gitlabToken = process.argv[3];
shell.exec(
`curl -X POST --silent --show-error --fail "https://gitlab.com/api/v4/projects/gitlab_project_id/repository/tags?tag_name=${version.APP_VERSION}&ref=${branchName}&private_token=${gitlabToken}"`
);
Готово. Остается добавить запуск этих команд в ваш CI скрипт.
А вот как выглядит наш для GitLab (проект на Angular):
stages:
- build
- deploy
build_dev:
image: 'node:latest'
stage: build
script:
- npm install
- node ./update-version.js dev
- npm run build -- --configuration=dev
- node ./push-new-version-tag.js integration $GITLAB_TOKEN_CI
artifacts:
paths:
- dist/uxcel
only:
- integration
build_prod:
image: 'node:latest'
stage: build
script:
- npm install
- node ./update-version.js
- npm run build -- --configuration=production
- node ./push-new-version-tag.js master $GITLAB_TOKEN_CI
artifacts:
paths:
- dist/uxcel
only:
- master
Таким образом, мы добавили версионность в веб-приложение для разных окружений (дев-версии сборок именуются вида x.y.z-dev, а прод-версии без постфикса - x.y.z). Как видите, реализация получилась несложной и адаптируемой. К примеру, если вы уже используете готовое решение, но у вас нет возможности получить номер версии в JS, можно добавить скрипт, который будет вычитывать последний тег и записывать его в js-файл. К слову, для меня был большим открытием npm-пакет shelljs, заметно упрощающий написание логики вместо shell-скриптов.
Как мы используем номер версии в Uxcel?
Наше приложение является PWA, поэтому нам важно следить за номерами версий наших пользователей: версии отправляются в google analytics, в систему мониторинга ошибок sentry, в API запросы (которые могут обрабатываться по-разному в зависимости от версии) и, само собой, версия отображается в самом приложении. Помимо этого, номер версии может использоваться для отображения Release Notes или для показа обучающего окна нового функционала сразу после авто-обновления приложения.
Спасибо за внимание, надеюсь, статья была полезной для вас! Буду рад услышать ваше мнение, делитесь своими способами версионности веб-приложений.
mailoman
npm version?
ian_phobos Автор
Вы про хранение версии в package.json? Если да, то все перечисленные пакеты из коробки это делают. Но, как мне кажется, это наиболее актуально для тех проектов которые публикуются в npm. Для обычных проектов, на мой взгляд, удобнее хранить версию в виде тегов в git и в каждую сборку передавать локально, не храня версию в файле в git'е.