Как я веду красивую историю изменений в своих проектах и деплою одной командой после коммита (нет, это не git hooks).
Если вы не пишете на JavaScript, пусть вас не смущает слово “NPM”, это подходит для любого языка.
npm version
- классный механизм версионирования, встроенный в NPM.
Версионирование
Главная, казалось бы, цель команды: отмечать версии.
Из коробки это значит, что вы можете написать npm version <version>
и запустить цепочку задач по подготовке новой версии. Варианта собственно три:
npm version patch
- 0.0.1 -> 0.0.2npm version minor
- 0.0.2 -> 0.1.0npm version major
- 0.1.0 -> 1.0.0
Но можно и принудительно версию вписать, но только по semver.
Что произойдёт:
- Выполнится
scripts.preversion
из package.json, если он есть - В package.json изменится версия
- Выполнится
scripts.version
из package.json, если он есть - Создастся коммит с package.json и версией в названии
- На этот коммит поставится тег версии
- Запустится
scripts.postversion
из package.json, если он есть
Это уже немало:
- Не надо высчитывать версию, просто выбираете
patch
,minor
илиmajor
в зависимости от содержания изменений - Не надо ставить тег
- Вы не забудете изменить номер версии в package.json
- В
postversion
вы можете вписать дальшейшие шаги (релиз, деплой). По-хорошему надо релизить после прохождения CI, но кого это волнует
Команда не сработает, если Git грязный, но можно обойти это с --force
, я часто обхожу.
Минус в том, что создаются коммиты на каждую версию, кого-то это может раздражать, меня наоборот раздражает, что просматривая список коммитов на Github не видно версий, коммиты версий исправляют это.
Как делать теги 1.2.3 вместо v1.2.3 (убрать v
из тега версии):
Добавьте файл .npmrc
, добавьте в него пустой tag-version-prefix:
tag-version-prefix=""
Дальше - интереснее.
Автоматический Changelog
CHANGELOG.md генерится утилитой conventional-changelog.
Об этом я уже писал 4 года назад. Там я описывал свои поиски, здесь опишу, к чему я пришёл.
С тех пор я все коммиты называю по Angular Conventions. Не пугайтесь “Angular” в названии, никакой привязки там нет.
В 2 словах о соглашениях:
Пишите feat: новая фича
или fix: исправлен баг такой-то
. Этого уже достаточно, чтобы генерить красивые истории изменений.
Подробнее, насколько я пользуюсь:
Сообщение имеет структуру:
type(scope): subject
body
footer
Я обычно ограничиваюсь:
type: subject
body
Типы, в порядке, как я использую:
feat
- новый функционалfix
- исправления ошибокchore
- правки скриптов деплоя и т.п.refactor
- правки кода без изменения функциональностиdocs
- правки текстовstyle
- правки отступовtest
- добавление тестов
В changelog попадают только feat
и fix
.
В футере принято перечислять ссылки на связанные задачи.
Если в футер добавить BREAKING CHANGE: что-то ломающее обратную совместимость
, то это также попадёт в changelog. По semver принято менять мажорную версию, если случился BREAKING CHANGE, но я не придерживаюсь строго.
В package.json
:
{
"scripts": {
"version": "npm run changelog && git add CHANGELOG.md",
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0"
}
}
CHANGELOG.md при таком конфиге попадёт в коммит версии, что удобно, да и коммиты версий становятся не такими уж бесполезными.
Можно поставить standard-changelog
чисто под ангуларовский формат, я по историческим причинам его не использую.
Красивые releases на Github
conventional-github-releaser использует тот же механизм извлечения описания из коммитов, но пишет не в файл, а в релиз на Github.
В package.json
:
{
"scripts": {
"postversion": "git push && npm run release",
"release": "conventional-github-releaser -p angular"
}
}
Вы можете не хотеть автоматом запускать release
при версионировании, тогда думаю вы знаете, что делать.
Не помню, обязательно ли пушить перед запуском conventional-github-releaser
, скорее да, чем нет, я всегда пушу.
Подмена версий в коде
Что если нужно поменять версию не только в package.json
? Тогда нужно писать скрипты.
У меня случаи несложные, такие:
Поменять версию в README.md
Отсюда (это кстати проект на Go)
Универсальный вариант на JavaScript из 2 файлов: первый меняет в README.md версию на токен на этапе preversion
(на нём в package.json ещё старая версия), второй на этапе version
меняет токен на новую версию. Регулярка получается универсальной, не надо подгонять под каждый файл.
version-replace-pre.js:
// заменяет старую версию в README.md на строку "{{version}}"
const fs = require('fs');
const packageJson = require('../package.json');
const str = fs.readFileSync('README.md', 'utf8');
const regex = new RegExp(packageJson.version.replace(/\./g, '\\.'), 'g');
const replaced = str.replace(regex, '{{version}}');
fs.writeFileSync('README.md', replaced, 'utf8');
version-replace.js:
// заменяет строку "{{version}}" в README.md на новую версию
const fs = require('fs');
const packageJson = require('../package.json');
const str = fs.readFileSync('README.md', 'utf8');
const replaced = str.replace(/\{\{version\}\}/g, packageJson.version);
fs.writeFileSync('README.md', replaced, 'utf8');
package.json:
{
"scripts": {
"preversion": "node scripts/version-replace-pre.js",
"replace-version": "node scripts/version-replace.js",
"version": "npm run replace-version && npm run changelog && git add CHANGELOG.md README.md"
}
}
Поменять версию в docker-compose.yml
#!/bin/bash
set -eu
version="$(cat package.json | grep '"version": "[0-9]' | cut -d':' -f2 | cut -d'"' -f2)"
echo "$version"
sed -i 's/shop-list:.*/shop-list:v'"${version}"'/g' docker-compose.yml
Поменять версию в userscript
#!/bin/bash
set -eu
version="$(cat package.json | grep '"version": "[0-9]' | cut -d':' -f2 | cut -d'"' -f2)"
echo "$version"
sed -i 's/@version.*/@version '"${version}"'/g' planfixfix.user.js
sed -i
на MacOS из коробки не работает, чтобы исправить, поставьте brew install gnu-sed
и добавьте alias sed=gsed
(это неточно, пишу по памяти).
Скрипт надо добавить в секцию scripts.version
вашего package.json
, добавить изменённый файл в Git, чтобы он попал в коммит версии:
{
"scripts": {
"version": "bash scripts/version-update.sh && git add changed-file.js",
}
}
Публикация в NPM
Это нужно только js проектам и то далеко не всем. Тут всё просто: добавьте npm publish
в секцию scripts.release
вашего package.json
.
В package.json
:
{
"scripts": {
"postversion": "npm run release",
"release": "npm publish"
}
}
Деплой куда угодно
В секцию release
можно вписать любой скрипт, который будет деплоить.
Например, в одном моём проекте по npm version
генерится статическое приложение и деплоится на Github Pages скриптом deploy.sh
.
В package.json
:
{
"scripts": {
"postversion": "npm run release",
"release": "npm run deploy",
"deploy": "bash scripts/deploy.sh"
}
}
Не только JavaScript
Это неочевидно, но npm version
можно использовать для проектов на любом языке, единственное условие: наличие package.json
.
Я, например, использовал такой способ для ведения CHANGELOG.md для некоторых сайтов на PHP.
Пример package.json
Типичный пример для моих новых проектов:
{
"name": "packagename",
"version": "0.0.1",
"description": "Description",
"scripts": {
"version": "npm run changelog && git add CHANGELOG.md",
"postversion": "git push && npm run release",
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0",
"release": "conventional-github-releaser -p angular"
},
"author": "Stanislav Popov",
"license": "ISC"
}
Установка пакетов
Чтобы всё работало, нужно скачать эти утилиты:
npm install -g conventional-changelog-cli conventional-github-releaser
Не вижу смысла добавлять эти пакеты в зависимости, хотя раньше так делал.
Если в процессе npm version что-то пошло не так
При отладке этих процессов вы неизбежно где-то ошибётесь и окажетесь в ситуации, когда сценарий не прошёл полностью, а коммит версии уже есть.
Тогда надо откатывать:
- Удалите тег:
git tag v0.1.2 -d
. - Удалите коммит:
git reset --hard HEAD~
. Если кроме автоматических изменений в коммите было что-то ещё, аккуратнее тут, лучше тогда без--hard
. - Если коммит уже запушился и никто не успел заметить (аккуратнее в команде), удалите коммит оттуда:
git push --force
и тег отдельно удалите:git push origin :v0.1.2
(он не удалится по push). - Руками удалите релиз с Github, сначала переведите его в draft, потом можно будет удалить.