# git-cliff ~ configuration file # https://git-cliff.org/docs/configuration # Documentation: # Set a script step like this in the pipeline where git cliff is used: # wget --output-document cliff.toml https://raw.githubusercontent.com/mendersoftware/mendertesting/master/utils/cliff.toml [changelog] # template for the changelog header header = """---""" # template for the changelog body # https://keats.github.io/tera/docs/#introduction body = """ {% if version %}## {{ version | replace(from="v", to="") }} - {{ timestamp | date(format="%Y-%m-%d") }}{% else %}## [unreleased]{% endif %} {# Highlights are curated manually at release prep (grammar doc section 4.2). Insert a "## Highlights" block above this line in the release PR. #} {%- macro entry(commit) -%} {%- set_global c_line = commit.message | upper_first -%} {%- set_global c_render_body = false -%} {%- set_global c_ticket = "" -%} {%- for f in commit.footers -%} {# Exact-match keywords only. "All" is a legacy synonym for "Commit". Miscased values (e.g. "commit") are silently ignored per the grammar -> subject only, no body. #} {%- if f.token == "Changelog" and (f.value == "Commit" or f.value == "All") -%}{%- set_global c_render_body = true -%}{%- endif -%} {# A Changelog value that is not a (case-insensitive) known keyword is a free-text sentence override. #} {%- set kw = f.value | lower -%} {%- if f.token == "Changelog" and kw != "commit" and kw != "none" and kw != "title" and kw != "all" -%}{%- set_global c_line = f.value -%}{%- endif -%} {%- if f.token == "Ticket" and f.value != "None" -%}{%- set_global c_ticket = f.value -%}{%- endif -%} {%- endfor -%} {{ c_line }}{% if c_ticket %} ([{{ c_ticket }}](https://northerntech.atlassian.net/browse/{{ c_ticket }})){% endif %} {%- if commit.body and c_render_body %} {% for l in commit.body | split(pat="\n") %}{% if "(cherry picked from commit" not in l %} {{ l }} {% endif %}{% endfor %}{%- endif -%} {%- endmacro entry -%} {# Entries that would render identically (same scope, subject and tickets — e.g. recurring dependabot bumps) are listed once. Distinct commits sharing a subject still render, since their scope or ticket differs. #} {%- macro list(commits) -%} {%- set_global seen = [] -%} {%- for commit in commits | unique(attribute="id") %} {%- set_global tks = "" -%} {%- for f in commit.footers %}{% if f.token == "Ticket" %}{% set_global tks = tks ~ f.value %}{% endif %}{% endfor -%} {%- set sc = commit.scope | default(value="") -%} {%- set key = sc ~ "|" ~ commit.message ~ "|" ~ tks -%} {%- if key not in seen %} {%- set_global seen = seen | concat(with=key) %} - {% if commit.scope %}*({{ commit.scope }})* {% endif %}{{ self::entry(commit=commit) }} {%- endif -%} {%- endfor -%} {%- endmacro list -%} {# 1. Breaking changes #} {%- set breaking = commits | filter(attribute="breaking", value=true) -%} {%- if breaking | length > 0 %} ### Breaking changes {% for commit in breaking %} - {% if commit.scope %}*({{ commit.scope }})* {% endif %}{{ self::entry(commit=commit) }} {%- if commit.breaking_description and commit.breaking_description != commit.message %} {% raw %} {% endraw %}- **BREAKING**: {{ commit.breaking_description }} {%- endif -%} {% endfor -%} {% endif -%} {# 2. Deprecations #} {%- set_global has_dep = false -%} {%- for commit in commits %}{% for f in commit.footers %}{% if f.token == "Deprecation" %}{% set_global has_dep = true %}{% endif %}{% endfor %}{% endfor -%} {%- if has_dep %} ### Deprecations {% for commit in commits %}{% for f in commit.footers %}{% if f.token == "Deprecation" %} - {{ f.value }} {%- endif %}{% endfor %}{% endfor -%} {% endif -%} {# 3. Ordered type groups (breaking excluded above; Deprecations group rendered above) #} {%- for group, gc in commits | filter(attribute="breaking", value=false) | group_by(attribute="group") %} {%- set name = group | split(pat="-->") | last -%} {%- if name != "Deprecations" %} ### {{ name }} {{ self::list(commits=gc) }} {%- endif -%} {%- endfor -%} {# 4. All tickets resolved appendix #} {%- set_global tickets = [] -%} {%- for commit in commits %}{% for f in commit.footers %}{% if f.token == "Ticket" and f.value != "None" %}{% set_global tickets = tickets | concat(with=f.value) %}{% endif %}{% endfor %}{% endfor -%} {%- if tickets | length > 0 %} --- ### All tickets resolved in this release | Ticket | |---| {% for t in tickets | unique %}| [{{ t }}](https://northerntech.atlassian.net/browse/{{ t }}) | {% endfor -%} {% endif -%} {%- if not commits %} No Changelog found. {% endif %} {%- raw %} {% endraw -%} """ # template for the changelog footer footer = """---""" # remove the leading and trailing whitespace from the templates trim = true [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 parsing and grouping commits commit_parsers = [ # NOTE: git-cliff evaluates parsers first-match-wins and combines a single parser's # fields with OR. Order therefore matters: type rules come first so a feat/fix that # merely mentions a CVE, or carries a Deprecation trailer, keeps its own type group. { message = "^[^:]+\\(internal\\):", skip = true }, # Skip "internal" scope { message = "(?m)^Changelog: None$", skip = true }, # exact footer line only; a sentence starting with "None" is a valid override # Type groups { message = "^feat", group = "New features" }, { message = "^fix", group = "Bug fixes" }, { message = "^perf", group = "Improvements" }, { message = "^refactor", group = "Improvements" }, # CVE-flagged commits that fell through the type rules (i.e. dependency bumps and # chores) route to Security. This keeps CVE-Security scoped to bumps/chores per the # grammar, not to any commit that happens to mention a CVE. { message = "CVE-\\d{4}-\\d+", group = "Security" }, { body = "CVE-\\d{4}-\\d+", group = "Security" }, { footer = "CVE-\\d{4}-\\d+", group = "Security" }, # Non-CVE dependency bumps route to Dependency updates (any deps* scope, plus scopeless bump) { message = "^chore: bump", group = "Dependency updates" }, { message = "^chore\\(deps", group = "Dependency updates" }, # Deprecation-only commits (e.g. a plain chore) are kept out of the skip bucket so the # template can surface their Deprecation trailer. feat/fix carrying one already matched above. { footer = "^Deprecation:", group = "Deprecations" }, # Everything else (docs, style, test, ci, non-bump chore) is skipped { message = "^.*", skip = true }, ] link_parsers = [ { pattern = "MEN-(\\d+)", href = "https://northerntech.atlassian.net/browse/MEN-$1" }, { pattern = "SEC-(\\d+)", href = "https://northerntech.atlassian.net/browse/SEC-$1" }, { pattern = "QA-(\\d+)", href = "https://northerntech.atlassian.net/browse/QA-$1" }, { pattern = "ME-(\\d+)", href = "https://northerntech.atlassian.net/browse/ME-$1" }, ] # 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 = "" # regex for ignoring tags # Use ignore instead of skip so that a commit with both an rc and a final tag is still processed ignore_tags = "beta|alpha|rc" # sort the tags topologically topo_order = false # sort the commits inside sections by oldest/newest order sort_commits = "oldest" [bump] features_always_bump_minor = true breaking_always_bump_major = true