В очередной раз душа поэта не выдержала и написала велосипед. Просто не нашел готового.

Как выглядит хороший релиз?

  • заходим в pipeline основной ветки main или ветки горячего релиза release/*
  • запускаем там задачу Release
  • в Gitea/Gitlab/Github создается релиз (в их терминах) с описанием изменений и тег
  • по факту создания тега начинается релизный пайплайн (сегодня не про него совсем)

Для этого нам нужно 2 вещи (остальное достаточно тривиально):

  • вычислить следующую версию на базе предыдущей, а из сообщений комитов понять какую из 3х цифр увеличивать на 1
  • сформировать release notes на базе сообщений комитов.

Предполагается использовать такой формат сообщений комитов (я это делаю в описании MR, а не отдельных комитов, за исключением горячих исправлений):

- my mega feature
- fix: small fix for other feature

Some description about feature.

Чем это отличается от https://www.conventionalcommits.org/en/v1.0.0/ ? Наличием нескольких строк заголовка. Хорошо, когда можно на каждый MR создать отдельное тестовое окружение для QA и других разработчиков. Тогда правило 1 MR - 1 изменение (фича или баг) хорошо. Когда же этого нет, то приходится комбинировать в одном MR несколько исправлений или новую фичу и несколько исправлений. Для этих случаев предлагаемый формат как раз подходит.

Как получить предыдущую (ака текущую) версию релиза (к ней будем прибавлять)?

git describe --tags --abbrev=0 --always > currentVersion.txt

Как получить текущую версию (ее можно использовать, если деплоить на тестовое окружение)?

git describe --tags --always > currentVersion.txt

Для парсинга же сообщений комитов пришлось написать Python-скрипт. Из хорошего: это только 1 файл, без зависимостей от библиотек. Так что легко добавить в любой Docker-образ. Для удобства сделал образ с ним stepin/git-parse-commits:1.0.0:

git-parse-commits version
git-parse-commits --tag-prefix "v" --tag releaseVersion > nextVersion.txt
git-parse-commits --tag-prefix "v" releaseNotes > releaseNotes.md

Справка по утиле (больше подробностей в репозитарии):

git-parse-commits -h

usage: git-parse-commits [-h] [-j] [-t [TAG_PREFIX]] [-s [SCOPE]] [-i [INITIAL_REVISION]] [-l [LAST_REVISION]] [--tag]
                         {version,currentVersion,lastReleaseVersion,releaseVersion,releaseNotes} ...

Provides next release version and release notes from git commit messages.

positional arguments:
  {version,currentVersion,lastReleaseVersion,releaseVersion,releaseNotes}
    version             Prints version of this tool
    currentVersion      Prints current version (useful for non-release builds)
    lastReleaseVersion  Prints version of last release
    releaseVersion      Prints version of next release from git commit messages
    releaseNotes        Prints release notes from git commit messages

options:
  -h, --help            show this help message and exit
  -j, --json            Output in json format
  -t [TAG_PREFIX], --tag-prefix [TAG_PREFIX]
                        prefix for tags (optional)
  -s [SCOPE], --scope [SCOPE]
                        scope to filter release note items
  -i [INITIAL_REVISION], --initial-revision [INITIAL_REVISION]
                        start range from next revision
  -l [LAST_REVISION], --last-revision [LAST_REVISION]
                        stop on this revision
  --tag                 add tag prefix to version (only if tag prefix is defined)

Скрипт даже подходит для mono-репозитариев (изменения по компонентам):

git-parse-commits version
git describe --tags --abbrev=0 --match 'component1-*' > currentVersion.txt
git-parse-commits --tag --tag-prefix "component1-" --scope component1 releaseVersion > nextVersion.txt
git-parse-commits --tag-prefix "component1-" --scope component1 releaseNotes > releaseNotes.md

Соответственно, если изменений нет, то и releaseNotes.md пустой (и можно не релизить компонент).

Ну, и как это может выглядеть в pipeline на примере Gitlab:

create_changelog:
  stage: "build"
  image:
      name: "stepin/git-parse-commits:latest"
      entrypoint: [""]
  variables:
      GIT_DEPTH: "0"
  script:
  - git-parse-commits version
  - CURRENT_VERSION="$(git-parse-commits currentVersion)"
  - RELEASE_VERSION="$(git-parse-commits --tag-prefix 'v' releaseVersion)"
  - echo "RELEASE_VERSION=$RELEASE_VERSION\nCURRENT_VERSION=$CURRENT_VERSION" > relNotes.env
  - git-parse-commits --tag-prefix 'v' releaseNotes > releaseNotes.md
  artifacts:
      reports:
          dotenv: relNotes.env
      paths:
      - releaseNotes.md
      expire_in: 1 day
  rules:
  - if: $CI_MERGE_REQUEST_IID
  - if: $CI_COMMIT_REF_NAME == "main" && $CI_PIPELINE_SOURCE != "schedule"
  - if: $CI_COMMIT_REF_NAME == "release/*" && $CI_PIPELINE_SOURCE != "schedule"

release:
  stage: "release"
  image:
      name: "registry.gitlab.com/gitlab-org/release-cli:latest"
      entrypoint: [""]
  script:
  - echo "Release $RELEASE_VERSION"
  release:
      tag_name: "$RELEASE_VERSION"
      tag_message: "Release $RELEASE_VERSION"
      description: "releaseNotes.md"
  assets:
    links:
    - name: "Container Image $CI_COMMIT_TAG"
      url: "https://$CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG:$CI_COMMIT_SHA"
  needs:
  - "create_changelog"
  rules:
  - if: $CI_COMMIT_REF_NAME == "main" && $CI_PIPELINE_SOURCE != "schedule"
    when: manual
    allow_failure: true
  - if: $CI_COMMIT_REF_NAME == "release/*" && $CI_PIPELINE_SOURCE != "schedule"
    when: manual
    allow_failure: true

(CURRENT_VERSION может использоваться для нерелизной сборки проекта – будет уникальная версия)

Отмечу, что этот автоматизм не обязателен: можно в UI сделать релиз руками. Как и исправить его описание руками.

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

Код: https://github.com/stepin/git-parse-commits