diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 72a02bb52c2be573baee3f2c4f01e9c6680183a3..a008562c6120dc1b2cfc65c630285dc5e68f73aa 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -2,6 +2,8 @@ image: golang:latest
 
 stages:
   - test
+  - pre-release
+  - release
 
 run_tests:
   rules:
@@ -27,3 +29,91 @@ lint:
       codequality: gl-code-quality-report.json
     paths:
       - gl-code-quality-report.json
+
+# ----- prepare release -----
+
+# Расчет тега и формирование Changelog
+get_release_info:
+  stage: pre-release
+  image:
+    name: orhunp/git-cliff:latest
+    entrypoint: [ "" ]
+  rules:
+    - if: '$CI_PIPELINE_SOURCE == "pipeline" && $PREPARE_RELEASE == "true"' # Запуск при инициации из perxis
+    - if: $CI_PIPELINE_SOURCE == "web"                                      # или при ручном запуске из GUI
+      when: manual                    
+  variables:
+    GIT_STRATEGY: clone
+    GIT_DEPTH: 0
+  script:
+    - echo "RELEASE_VERSION=$(git-cliff --bumped-version)" >> vars.env                # Расчет новой версии релиза через git-cliff
+    - echo "$(git-cliff --unreleased | sed '1,6d' | sed '$d')" > current_changelog.md # удалить "лишние" строки для Changelog
+  artifacts:
+    reports:
+      dotenv: vars.env # Use artifacts:reports:dotenv to expose the variables to other jobs
+    paths:
+      - current_changelog.md
+
+# Получаем последнюю версию релиза
+get_current_version:
+  stage: pre-release
+  image: bitnami/git:latest
+  rules:
+    - if: '$CI_PIPELINE_SOURCE == "pipeline" && $PREPARE_RELEASE == "true"'
+    - if: $CI_PIPELINE_SOURCE == "web"
+  needs:
+    - job: get_release_info
+  variables:
+    GIT_STRATEGY: clone
+    GIT_DEPTH: 0
+  script:
+    - echo CURRENT_VERSION=$(git describe --tags --abbrev=0) >> vars.env
+  artifacts:
+    reports:
+      dotenv: vars.env
+
+# Релиз и запись тега в артефакт для использования в perxis
+prepare_release:
+  stage: pre-release
+  image: registry.gitlab.com/gitlab-org/release-cli:latest
+  rules:
+    - if: '$CI_PIPELINE_SOURCE == "pipeline" && $PREPARE_RELEASE == "true"'
+    - if: $CI_PIPELINE_SOURCE == "web"
+  needs:
+    - job: get_release_info
+      artifacts: true
+    - job: get_current_version
+      artifacts: true
+  script:
+    - echo "PERXIS_GO_RELEASE_VERSION=$RELEASE_VERSION" >> vars.env
+    - echo "PERXIS_GO_CURRENT_VERSION=$CURRENT_VERSION" >> vars.env
+    - |
+      # Если новая версия совпадает со старой, значит изменений не было, и выпускать новый релиз не нужно
+      NEEDS_RELEASE=false
+      if [ "$RELEASE_VERSION" != "$CURRENT_VERSION" ]; then
+        NEEDS_RELEASE=true
+      fi
+      
+      echo "PERXIS_GO_NEEDS_RELEASE=$NEEDS_RELEASE" >> vars.env
+  artifacts:
+    when: always
+    paths:
+      - vars.env
+      - current_changelog.md
+    expire_in: 1 week
+
+# ----- release -----
+
+release:
+  stage: release
+  image: registry.gitlab.com/gitlab-org/release-cli:latest
+  rules:
+    - if: $CI_PIPELINE_SOURCE == "pipeline" && $NEEDS_RELEASE == "true"
+    - if: $CI_PIPELINE_SOURCE == "web"
+      when: manual
+  script:
+    - echo "Start release $VERSION"
+  release:
+    name: 'Release $VERSION'
+    description: '$VERSION'
+    tag_name: '$VERSION'
diff --git a/cliff.toml b/cliff.toml
new file mode 100644
index 0000000000000000000000000000000000000000..78b82d95b808fd8b3a85e9013870059b8bb46b13
--- /dev/null
+++ b/cliff.toml
@@ -0,0 +1,107 @@
+# git-cliff ~ default configuration file
+# https://git-cliff.org/docs/configuration
+#
+# Lines starting with "#" are comments.
+# Configuration options are organized into tables and keys.
+# See documentation for more information on available options.
+[bump]
+features_always_bump_minor = true
+breaking_always_bump_major = true
+
+[changelog]
+# changelog header
+header = """
+# Changelog\n
+All notable changes to this project will be documented in this file.\n
+"""
+# template for the changelog body
+# https://keats.github.io/tera/docs/#introduction
+body = """
+{% if version %}\
+    ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
+{% else %}\
+    ## [unreleased]
+{% endif %}\
+{% for group, commits in commits | group_by(attribute="group") %}
+    ### {{ group | striptags | trim | upper_first }}
+    {% for commit in commits %}
+        - {% if commit.scope %}*({{ commit.scope }})* {% endif %}\
+            {% if commit.breaking %}[**breaking**] {% endif %}\
+            {{ commit.message | upper_first }} \
+        {% if commit.links %}\
+       ({% for link in commit.links %}\
+          [{{ link.text }}]({{ link.href }})\
+          {% if not loop.last %}, {% endif %}\
+        {% endfor %})\
+        {% endif %}\
+        -([{{ commit.id | truncate(length=7, end="") }}]($REPO/-/commit/{{ commit.id }}))\
+    {% endfor %}
+{% endfor %}\n
+"""
+# template for the changelog footer
+footer = """
+<!-- generated by git-cliff -->
+"""
+# remove the leading and trailings
+trim = true
+# postprocessors
+postprocessors = [
+    { pattern = '\$REPO', replace = "https://git.perx.ru/perxis/perxis-go" },
+]
+
+[git]
+# parse the commits based on https://www.conventionalcommits.org
+conventional_commits = true
+# filter out the commits that are not conventional
+filter_unconventional = true
+# process each line of a commit as an individual commit
+split_commits = false
+# regex for preprocessing the commit messages
+commit_preprocessors = [
+    { pattern = 'Feat:', replace = "feat:"},
+    { pattern = 'WIP:', replace = ""},
+    { pattern = 'wip:', replace = ""}
+    # Replace issue numbers
+    #{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](<REPO>/issues/${2}))"},
+    # Check spelling of the commit with https://github.com/crate-ci/typos
+    # If the spelling is incorrect, it will be automatically fixed.
+    #{ pattern = '.*', replace_command = 'typos --write-changes -' },
+]
+# regex for parsing and grouping commits
+commit_parsers = [
+    { message = "^feat", group = "<!-- 0 -->рџљЂ Features" },
+    { message = "^fix", group = "<!-- 1 -->рџђ› Bug Fixes" },
+    { message = "^doc", group = "<!-- 3 -->рџ“љ Documentation" },
+    { message = "^perf", group = "<!-- 4 -->вљЎ Performance" },
+    { message = "^refactor", group = "<!-- 2 -->рџљњ Refactor", skip = true },
+    { message = "^style", group = "<!-- 5 -->рџЋЁ Styling" },
+    { message = "^test", group = "<!-- 6 -->рџ§Є Testing" },
+    { message = "^chore\\(release\\): prepare for", skip = true },
+    { message = "^chore\\(deps.*\\)", skip = true },
+    { message = "^chore\\(pr\\)", skip = true },
+    { message = "^chore\\(pull\\)", skip = true },
+    { message = "^chore|^ci", group = "<!-- 7 -->вљ™пёЏ Miscellaneous Tasks" },
+    { body = ".*security", group = "<!-- 8 -->рџ›ЎпёЏ Security" },
+    { message = "^revert", group = "<!-- 9 -->в—ЂпёЏ Revert" },
+    { message = "^irefac", skip = true },
+]
+# protect breaking changes from being skipped due to matching a skipping commit_parser
+protect_breaking_commits = false
+# filter out the commits that are not matched by commit parsers
+filter_commits = false
+# regex for matching git tags
+# tag_pattern = "v[0-9].*"
+# regex for skipping tags
+skip_tags = "v0.([0-9]\\.|1[0-8]).*"
+# regex for ignoring tags
+# ignore_tags = ""
+# sort the tags topologically
+topo_order = false
+# sort the commits inside sections by oldest/newest order
+sort_commits = "oldest"
+# limit the number of commits included in the changelog.
+# limit_commits = 42
+link_parsers = [
+    { pattern = "#(PRXS-(\\d+))", href = "https://tracker.yandex.ru/$1"},
+    { pattern = "RFC(\\d+)", text = "ietf-rfc$1", href = "https://datatracker.ietf.org/doc/html/rfc$1"},
+]
\ No newline at end of file