Я покажу вам, насколько легко может быть автоматизация процесса выпуска и увеличения версии.

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

Я думаю, что одна из тем, которая не освещена должным образом в статьях по ML, - это автоматизация процесса выпуска и увеличение версии проекта. Я не хочу обсуждать возможные варианты выпуска в этом посте, это не об этом. Здесь мы обсудим, как настроить процесс выпуска для вашего инструмента CI / CD (конвейеры Bitbucket, конвейеры Gitlab и т. Д.). Я думаю, можно с уверенностью сказать, что в большинстве случаев выпуск - это файл или набор файлов, которые будут каким-то образом интегрирован в существующее клиентское решение (независимо от того, является ли это jar-файл с приложением загрузки Spring внутри, набор скриптов python, которые используются для обучения модели и прогнозов, или образ докера, в котором выполняется основное приложение). Формулировка проблемы: мы хотим управлять версиями продукта и иметь возможность вернуться к конкретной версии, чтобы иметь возможность тестировать и диагностировать проблемы, возникшие в производственной среде.

Управление версиями

Для начала давайте определим, что означает число в строке версии.

Определение из семантического управления версиями (https://semver.org/)

Учитывая номер версии MAJOR.MINOR.PATCH-pre_release-label, увеличьте:

  1. ОСНОВНАЯ версия при внесении несовместимых изменений API
  2. Версия MINOR при добавлении функций обратно совместимым способом.
  3. Версия PATCH при исправлении ошибок с обратной совместимостью.

Pre-release-label - это метка-заменитель версии, которая сохраняется перед выпуском.

Это оптимальное определение, но, конечно, вы можете выбрать свою собственную схему управления версиями. Итак, согласно этому определению, нам нужно определить три задачи:

  1. Выпустить и увеличить основную версию
  2. Выпустить и увеличить дополнительную версию
  3. Выпустить и увеличить версию патча

Следующим шагом является определение того, как выпуск будет представлен в истории GIT, за исключением тега git. Для этого существует несколько методов, наиболее популярными из которых являются отдельная ветвь выпуска или отдельная фиксация. В этом посте мы будем обсуждать отдельный коммит, потому что его гораздо проще автоматизировать (слияние не требуется).

Отдельная фиксация

Начнем с теории. Отдельная фиксация - это фиксация, которая не принадлежит ни к какой ветке и может быть указана только напрямую. Как сделать его доступным для поиска в истории git, не зная хеша коммита? Вам просто нужно присвоить ему тег git. Затем вы можете фильтровать историю git по тегам в любом пользовательском интерфейсе VCS.

VERSION файл

Как-то вам нужно сохранить актуальную строку версии. Самый простой способ - сохранить его в текстовом файле в корне репозитория. Политики файла VERSION:

  1. Содержимое файла нельзя изменить вручную
  2. Его нельзя изменить в функциональной ветви (лучше всего создать этап конвейера, который сравнивает его с мастером)
  3. Файл изменяется только после выпуска и автоматически фиксируется непосредственно в основной ветке.

Настройка конвейеров CI / CD

В файл конвейеров CI / CD вы должны добавить три настраиваемых / ручных шага / задания, которые будут выполнять выпуск и соответствующее приращение. И, при желании, добавьте шаг / задание, которое выполняется только в ветвях функций и проверяет, был ли файл VERSION изменен по сравнению с мастером.

Сценарий Bash, чтобы проверить, изменился ли файл VERSION относительно master:

git diff --exit-code VERSION

А теперь самое интересное:

  1. Нам нужно создать скрипт, который управляет файлом VERSION с функциями: получить версию, получить версию с заполнителем, увеличить версию major / minor / patch
  2. Мы должны определить политику ограничения ветки выпуска (выпуск может быть выполнен только в основной ветке) и создать отдельный поток отправки фиксации.
  3. Определите шаг выпуска CI / CD (здесь мы определим его для конвейеров Bitbucket и конвейеров Gitlab)

Ненавижу писать сценарии на bash. Я использую bash только для определения высокоуровневых шагов и запуска из него сценариев python, потому что я твердо убежден, что сценарии bash имеют уродливую нотацию, не читаются менее опытными пользователями Linux и не обслуживаются в долгосрочной перспективе. fire библиотека может помочь вам легко написать хорошие сценарии Python, запускаемые через CLI. Посмотрите мой предыдущий пост:



VERSION скрипт управления файлами (обсуждать нечего, только операции записи файлов):

#!/usr/bin/env python
import os
import re

import fire

pre_release_placeholder = 'SNAPSHOT'
version_filepath = os.path.join('.', 'VERSION')
version_pattern = re.compile(fr'^\d+.\d+.\d+(-{pre_release_placeholder})?$')


def get(with_pre_release_placeholder: bool = False):
    with open(version_filepath, 'r') as version_file:
        version_lines = version_file.readlines()
        assert len(version_lines) == 1, 'Version file is malformed'
        version = version_lines[0]
        assert version_pattern.match(version), 'Version string is malformed'
        if with_pre_release_placeholder:
            return version
        else:
            return version.replace(f'-{pre_release_placeholder}', '')


def write_version_file(major: int, minor: int, patch: int):
    version = f'{major}.{minor}.{patch}-{pre_release_placeholder}'
    with open(version_filepath, 'w') as version_file:
        version_file.write(version)


def inc_patch():
    version = get()
    major, minor, patch = version.split('.')
    write_version_file(major, minor, int(patch) + 1)


def inc_minor():
    version = get()
    major, minor, patch = version.split('.')
    write_version_file(major, int(minor) + 1, patch)


def inc_major():
    version = get()
    major, minor, patch = version.split('.')
    write_version_file(int(major) + 1, minor, patch)


if __name__ == "__main__":
    fire.Fire({
        'get': get,
        'inc-patch': inc_patch,
        'inc-minor': inc_minor,
        'inc-major': inc_major
    })

Теперь займемся разработкой сценария выпуска. Во-первых, мы должны проверить, запущен ли сценарий выпуска в правой ветке:

# Bitbucket
commit=${BITBUCKET_COMMIT:-$(git rev-parse HEAD)}
# Gitlab
commit=${CI_COMMIT_SHA:-$(git rev-parse HEAD)}
# Define ALLOWED_RELEASE_BRANCH in VCS pipelines secret variables.
branch=${ALLOWED_RELEASE_BRANCH:-master}

if ! git branch -a --contains "${commit}" | grep -e "^[* ]*remotes/origin/${branch}\$"
then
  echo -e "###\n### Not on ${branch}. Only ${branch} commits can be released.\n###"
  exit 1
else
  echo -e "###\n### Releasing of ${commit} on ${branch}\n###"
fi

Затем вам нужно написать кое-что о выпуске, например: опубликовать образ докера или скопировать его в корзину S3 или что-то еще, что вам нужно. Не забудьте сделать это с правильным файлом ВЕРСИИ:

version=$(./scripts/version.py get)
version_file=VERSION
echo ${version} > ${version_file}

Затем мы должны отправить отсоединенный коммит с измененным на предыдущем шаге файлом VERSION:

# Define some cool release pusher to ditinguish from amnual commits :)
git config user.name "Elon Musk"
git config user.email "[email protected]"

echo "Pushing detached tag of new version"
git add ${version_file}
git commit -m "Release version ${version}"
git tag  -a ${version} -m "Release version ${version} tag"
git push origin ${version}

Вернитесь в основную ветку и увеличьте версию с предварительным заполнителем:

echo "Pushing new version to ${branch}"
git fetch origin "${branch}:${branch}" || git pull
git checkout "${branch}"
# Parameter of the script
release_type=${1}
./scripts/version.py inc-${release_type}

next_working_version=$(./scripts/version.py get --with-pre-release-placeholder)
git add ${version_file}
git commit -m "Incrementing working version to ${next_working_version} after ${version} release."
git push origin ${branch}

Вот и все. Мы создали чистый скрипт выпуска. Теперь давайте разработаем фрагменты шагов конвейера Bitbucket и Gitlab.

Bitbucket:

image:
  name: python:3.7.6-slim-buster

pipelines:
  default:
    - step:
        - name: Test version file not changed
        - script:
            - git diff --exit-code VERSION

  custom:
    release-inc-patch:
      - step:
          caches:
            - pip
          name: Release current version and increment patch version
          script:
            - apt-get update && apt-get install -y git
            - pip install fire==0.2.1
            - ./scripts/release.sh patch

    release-inc-minor:
      - step:
          caches:
            - pip
          name: Release current version and increment minor version
          script:
            - apt-get update && apt-get install -y git
            - pip install fire==0.2.1
            - ./scripts/release.sh minor
    release-inc-major:
      - step:
          caches:
            - pip
          name: Release current version and increment major version
          script:
            - apt-get update && apt-get install -y git
            - pip install fire==0.2.1
            - ./scripts/release.sh major

Выполнение настраиваемых шагов конвейера: https://confluence.atlassian.com/bitbucket/run-pipelines-manually-861242583.html

Gitlab:

test-version-changed:
  stage: test
  name: Test version file not changed
  script:
    - git diff --exit-code VERSION

release-inc-patch:
  stage: deploy
  image: python:3.7.6-slim-buster
  caches:
    - pip
  name: Release current version and increment patch version
  script:
    - apt-get update && apt-get install -y git
    - pip install fire==0.2.1
    - ./scripts/release.sh patch
  when: manual
  only:
    - master

release-inc-minor:
  stage: deploy
  image: python:3.7.6-slim-buster
  caches:
    - pip
  name: Release current version and increment minor version
  script:
    - apt-get update && apt-get install -y git
    - pip install fire==0.2.1
    - ./scripts/release.sh minor
  when: manual
  only:
    - master

release-inc-major:
  stage: deploy
  image: python:3.7.6-slim-buster
  caches:
    - pip
  name: Release current version and increment major version
  script:
    - apt-get update && apt-get install -y git
    - pip install fire==0.2.1
    - ./scripts/release.sh major
  when: manual
  only:
    - master

Запускайте ручные задания: https://forum.gitlab.com/t/gitlab-ci-run-pipeline-manually/13797

Вот и все! Все исходники вы можете найти на моем GitHub: https://github.com/mbalatsko/release-version-increment

Надеюсь, эта статья будет полезной и информативной. Жду ваших отзывов!