Почему номера версий важнее, чем вы думаете

Если вы когда-нибудь задумывались, почему некоторые разработчики покрываются мурашами при виде перехода версии с 1.2.3 на 1.2.4 или почему другие празднуют, как будто выиграли в лотерею, когда им удаётся увеличить основную версию, вы вот-вот узнаете секретный язык версионирования программного обеспечения. Это не магия — это Семантическое версионирование, и это, возможно, самая недооценённая практика в современной разработке программного обеспечения.

Представьте себе ситуацию. Вы поддерживаете критически важную библиотеку, от которой зависят сотни приложений. Вы исправили небольшую ошибку и выпустили версию 2.0.5. Через три дня ваш канал в Slack взрывается. Разработчики сообщают, что их приложения полностью вышли из строя после обновления. Ваше тщательно продуманное исправление ошибки? Оно было связано с непредвиденным критическим изменением, о котором вы не сообщили, и никто — абсолютно никто — этого не ожидал. Теперь вы пишете письма с извинениями вместо того, чтобы писать код.

Этот кошмарный сценарий как раз то, что предотвращает Семантическое версионирование. Это стандартизированный способ сообщить, что содержится в вашем релизе, только через номера версий.

Понимание языка версий

Семантическое версионирование следует обманчиво простой формуле: MAJOR.MINOR.PATCH.

Подумайте об этом так:

  • Версия MAJOR (первое число) сигнализирует о том, что вы внесли несовместимые изменения API. Пользователям нужно позаботиться об обновлении — это может нарушить работу их приложений.
  • Версия MINOR (среднее число) означает, что вы добавили новые функции, но всё по-прежнему работает так же, как и раньше. Приятные дополнения, без сюрпризов.
  • Версия PATCH (последнее число) указывает на исправления ошибок и небольшие настройки, которые пользователи могут применить, не теряя сна.

Вот где это становится прекрасным: когда вы увеличиваете одно число, числа справа сбрасываются до нуля. Так версия 1.5.2 становится 2.0.0 при наличии критических изменений, а не 1.5.3 или какое-либо произвольное число, придуманное вами в пятницу вечером.

Правила увеличения в действии

Позвольте мне показать вам, как это работает:

Начальная версия: 1.2.3
Выпущена исправленная версия       → 1.2.4 (увеличение PATCH)
Выпущена новая функция            → 1.3.0 (увеличение MINOR, PATCH сбрасывается)
Произведено критическое изменение → 2.0.0 (увеличение MAJOR, MINOR и PATCH сбрасываются)

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

Почему это важно: Ад зависимостей реален

Вот практический кошмар, который решает Семантическое версионирование. Представьте, что ваш проект зависит от трёх библиотек:

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

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

С Семантическим версионированием ответ встроен в сам номер версии. Если LibraryA переходит с 2.1.0 на 2.1.1, вы знаете, что это безопасно. Если он переходит на 2.2.0, это всё ещё безопасно (новая функция, обратная совместимость). Если он переходит на 3.0.0? Время читать журнал изменений и планировать миграцию.

Реализация в реальном мире: конвейер коммитов и релизов

Давайте перейдём к практике. Большинство команд автоматизируют увеличение версий, используя инструменты Семантического версионирования. Самый популярный подход включает автоматическое инструментирование, такое как semantic-release, которое читает ваши сообщения коммитов и определяет, какое увеличение версии необходимо.

Вот как обычно работает этот процесс:

graph LR A["Писать код"] --> B["Коммитить по конвенции"] B --> C["Пушить в репозиторий"] C --> D["CI/CD пайплайн запускается"] D --> E{"Анализировать коммиты"} E -->|Только исправления| F["Увеличить PATCH"] E -->|Новые функции| G["Увеличить MINOR"] E -->|Критические изменения| H["Увеличить MAJOR"] F --> I["Создать релиз"] G --> I H --> I I --> J["Опубликовать пакет"]

Магия происходит в сообщениях коммитов. Вам нужно следовать формату обычных коммитов, чтобы это работало. Самое распространённое соглашение — спецификация Conventional Commits:

<type>(<scope>): <subject>
<body>
<footer>

Три типа, которые имеют значение

Коммиты feat приводят к увеличению MINOR версии (новая функция):

feat(auth): добавить поддержку двухфакторной аутентификации
Добавить поддержку аутентификации по SMS для повышения безопасности учётной записи.
Пользователи теперь могут включить 2FA в настройках своей учётной записи.

Коммиты fix приводят к увеличению PATCH версии (исправление ошибки):

fix(api): решить проблему тайм-аута на пользовательском эндпоинте
Пользовательский эндпоинт зависал при высокой нагрузке из-за неэффективного
запроса к базе данных. Оптимизирован запрос для использования индексированных полей.

BREAKING CHANGE в футере запускает увеличение MAJOR версии (несовместимые изменения):

feat(api)!: redesign authentication endpoint
Полный редизайн потока аутентификации. Старый эндпоинт /auth/token
удален в пользу новой реализации OAuth 2.0.
BREAKING CHANGE: эндпоинт /auth/token был удален.
Перейдите на /oauth/token.

Настройка автоматизации: пошаговое руководство

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

Шаг 1: Установка Semantic Release

Сначала добавьте необходимые зависимости в ваш проект Node.js:

npm install --save-dev semantic-release @semantic-release/npm @semantic-release/git @semantic-release/github @semantic-release/changelog

Шаг 2: Настройка Semantic Release

Создайте файл .releaserc.json в корне вашего проекта:

{
  "branches": [
    "main",
    {
      "name": "develop",
      "prerelease": true
    }
  ],
  "plugins": [
    "@semantic-release/commit-analyzer",
    "@semantic-release/release-notes-generator",
    "@semantic-release/changelog",
    "@semantic-release/npm",
    [
      "@semantic-release/git",
      {
        "assets": [
          "package.json",
          "package-lock.json",
          "CHANGELOG.md"
        ],
        "message": "chore(release): ${nextRelease.version}\n\n${nextRelease.notes}"
      }
    ],
    "@semantic-release/github"
  ]
}

Шаг 3: Создание рабочего процесса GitHub Actions

Создайте .github/workflows/release.yml:

name: Semantic Release
on:
  push:
    branches:
      - main
      - develop
jobs:
  release:
    runs-on: ubuntu-latest
    permissions:
      contents: write
      issues: write
      pull-requests: write
      packages: write
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: actions/setup-node@v4
        with:
          node-version: '18'
          registry-url: 'https://registry.npmjs.org'
      - run: npm ci
      - run: npm run build
      - run: npm test
      - name: Release
        run: npx semantic-release
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

Шаг 4: Проверка сообщений коммитов

Прежде чем semantic-release сможет выполнить свою работу, ваши коммиты должны быть в правильном формате. Используйте commitlint, чтобы обеспечить это:

npm install --save-dev @commitlint/cli @commitlint/config-conventional husky

Создайте commitlint.config.js:

module.exports = {
  extends: ['@commitlint/config-conventional'],
  rules: {
    'type-enum': [
      2,
      'always',
      [
        'build',
        'chore',
        'ci',
        'docs',
        'feat',
        'fix',
        'perf',
        'refactor',
        'revert',