NPM version: версионирование, история изменений, деплой проекта

15.04.2020, 20:58 - 6 мин читать

Как я веду красивую историю изменений в своих проектах и деплою одной командой после коммита (нет, это не git hooks).

Если вы не пишете на JavaScript, пусть вас не смущает слово “NPM”, это подходит для любого языка.

npm version

Оглавление:

    npm version - классный механизм версионирования, встроенный в NPM.

    Версионирование

    Главная, казалось бы, цель команды: отмечать версии.

    Из коробки это значит, что вы можете написать npm version <version> и запустить цепочку задач по подготовке новой версии. Варианта собственно три:

    • npm version patch - 0.0.1 -> 0.0.2
    • npm version minor - 0.0.2 -> 0.1.0
    • npm version major - 0.1.0 -> 1.0.0

    Но можно и принудительно версию вписать, но только по semver.

    Что произойдёт:

    1. Выполнится scripts.preversion из package.json, если он есть
    2. В package.json изменится версия
    3. Выполнится scripts.version из package.json, если он есть
    4. Создастся коммит с package.json и версией в названии
    5. На этот коммит поставится тег версии
    6. Запустится 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 чисто под ангуларовский формат, я по историческим причинам его не использую.

    Пример CHANGELOG.md

    Красивые 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 что-то пошло не так

    При отладке этих процессов вы неизбежно где-то ошибётесь и окажетесь в ситуации, когда сценарий не прошёл полностью, а коммит версии уже есть.

    Тогда надо откатывать:

    1. Удалите тег: git tag v0.1.2 -d.
    2. Удалите коммит: git reset --hard HEAD~. Если кроме автоматических изменений в коммите было что-то ещё, аккуратнее тут, лучше тогда без --hard.
    3. Если коммит уже запушился и никто не успел заметить (аккуратнее в команде), удалите коммит оттуда: git push --force и тег отдельно удалите: git push origin :v0.1.2 (он не удалится по push).
    4. Руками удалите релиз с Github, сначала переведите его в draft, потом можно будет удалить.