Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 169 additions & 0 deletions .github/cliff.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
# git-cliff ~ configuration file
# https://git-cliff.org/docs/configuration

[changelog]
# template for the changelog header
header = """
# Changelog\n
All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
<!-- markdownlint-disable MD024 -->\n
"""
# template for the changelog body
# https://keats.github.io/tera/docs/#introduction
body = """
{%- macro remote_url() -%}
https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}
{%- endmacro -%}
{%- set init_commit = get_env(name="FIRST_COMMIT", default="") -%}
{%- set this_version = "Unreleased" -%}

{% if version -%}
{%- set this_version = version | trim_start_matches(pat="v") -%}
## [{{ this_version }}] - {{ timestamp | date(format="%Y-%m-%d") }}
{%- if message %}

> {{ message }}
{%- endif %}
{% else -%}
## [Unreleased]
{% endif -%}

{% for group, commits in commits | group_by(attribute="group") %}
### {{ group | upper_first }}
{% for commit in commits | unique(attribute="message") %}
- {{ commit.message | split(pat="\n") | first | upper_first | trim }}\
{% if commit.remote.username %} by \\@{{ commit.remote.username }}{%- endif -%}
{% if commit.remote.pr_number %} in \
[#{{ commit.remote.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.remote.pr_number }})
{%- else %} in \
[`{{ commit.id | truncate(length=7, end="") }}`]({{ self::remote_url() }}/commit/{{commit.id }})
{%- endif -%}
{% endfor %}
{% endfor -%}

{% set first_commit = previous.version -%}
{%- set last_commit = "HEAD" -%}
{% if version -%}
{%- set last_commit = version -%}
{%- if not previous.version -%}
{%- set first_commit = init_commit -%}
{%- endif -%}
{%- endif %}
[{{ this_version }}]: {{ self::remote_url() }}/compare/{{ first_commit }}...{{ last_commit }}

Full commit diff: [`{% if previous.version -%}
{{ first_commit }}
{%- else -%}
{{ init_commit | truncate(length=7, end="") }}
{%- endif %}...{{ last_commit }}`][{{ this_version }}]
{% if github.contributors | filter(attribute="is_first_time", value=true) | length != 0 %}
## New Contributors
{% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %}
- @{{ contributor.username }} made their first contribution
{%- if contributor.pr_number %} in \
[#{{ contributor.pr_number }}]({{ self::remote_url() }}/pull/{{ contributor.pr_number }}) \
{%- endif %}
{%- endfor %}
{% endif %}

"""
# template for the changelog footer
footer = """
<!-- generated by git-cliff -->
"""
# remove the leading and trailing whitespace from the templates
trim = true
# The file path for output. This can be overridden with `--output` CLI arg
# output = "CHANGELOG.md"

[git]
# parse the commits based on https://www.conventionalcommits.org
conventional_commits = true
# filter out the commits that are not conventional
filter_unconventional = false
# regex for preprocessing the commit messages
commit_preprocessors = [
# Remove the auto-appended PR numbers from commit titles.
# For example, `some PR title (#42)` becomes `some PR title`
# NOTE: git-cliff will append a link to the commit or PR in the commit title.
# For example, `some PR title (#42)` gets displayed as
# `some PR title by <user> in <link to #42>`
{ pattern = '(?m)(\s?\(#[0-9]+\))$', replace = "" },
]
# regex for parsing and grouping commits
commit_parsers = [
# The order of parsers matters. Put rules for PR labels first to prioritize PR labels.
{ field = "github.pr_labels", pattern = "breaking", group = "<!-- 0 --> 💥 Breaking changes" },
{ field = "github.pr_labels", pattern = "breaking-change", group = "<!-- 0 --> 💥 Breaking changes" },
{ field = "github.pr_labels", pattern = "feature", group = "<!-- 1 --> 🚀 New features and improvements" },
{ field = "github.pr_labels", pattern = "enhancement", group = "<!-- 1 --> 🚀 New features and improvements" },
{ field = "github.pr_labels", pattern = "deprecated", group = "<!-- 2 --> ⚠️ Deprecated" },
{ field = "github.pr_labels", pattern = "removed", group = "<!-- 3 --> 🚨 Removed" },
{ field = "github.pr_labels", pattern = "bug", group = "<!-- 4 --> 🐛 Bug fixes" },
{ field = "github.pr_labels", pattern = "bugfix", group = "<!-- 4 --> 🐛 Bug fixes" },
{ field = "github.pr_labels", pattern = "fix", group = "<!-- 4 --> 🐛 Bug fixes" },
{ field = "github.pr_labels", pattern = "regression", group = "<!-- 4 --> 🐛 Bug fixes" },
{ field = "github.pr_labels", pattern = "security", group = "<!-- 5 --> 🔐 Security" },
{ field = "github.pr_labels", pattern = "dependencies", group = "<!-- 6 --> 📦 Dependency updates" },
{ field = "github.pr_labels", pattern = "test", group = "<!-- 7 --> 🚦 Tests"},
{ field = "github.pr_labels", pattern = "tests", group = "<!-- 7 --> 🚦 Tests"},
{ field = "github.pr_labels", pattern = "localization", group = "<!-- 8 --> 🌐 Localization and translation"},
{ field = "github.pr_labels", pattern = "documentation", group = "<!-- 9 --> 📝 Documentation" },
{ field = "github.pr_labels", pattern = "chore", group = "<!-- 10 --> 👻 Maintenance" },
{ field = "github.pr_labels", pattern = "maintenance", group = "<!-- 10 --> 👻 Maintenance" },
{ field = "github.pr_labels", pattern = "internal", group = "<!-- 10 --> 👻 Maintenance" },
{ field = "github.pr_labels", pattern = "developer", group = "<!-- 11 --> 👷 Changes for developers" },
{ field = "github.pr_labels", pattern = "refactor", group = "<!-- 12 --> ✍ Other changes" },
{ field = "github.pr_labels", pattern = "skip-changelog", skip = true },
{ field = "github.pr_labels", pattern = "no-changelog", skip = true },
{ field = "github.pr_labels", pattern = "invalid", skip = true },
# Here are rules that apply to conventional commits
{ field = "group", pattern = "add", group = "<!-- 1 --> 🚀 New features and improvements" },
{ field = "group", pattern = "feat", group = "<!-- 1 --> 🚀 New features and improvements" },
{ field = "group", pattern = "fix", group = "<!-- 4 --> 🐛 Bug fixes" },
{ field = "group", pattern = "perf", group = "<!-- 13 --> ⚡ Performance" },
{ field = "group", pattern = "build", group = "<!-- 6 --> 📦 Dependency updates" },
{ field = "group", pattern = "test", group = "<!-- 7 --> 🚦 Tests" },
{ field = "group", pattern = "docs", group = "<!-- 9 --> 📝 Documentation" },
{ field = "group", pattern = "chore", group = "<!-- 10 --> 👻 Maintenance" },
{ field = "group", pattern = "style", group = "<!-- 10 --> 👻 Maintenance" },
{ field = "breaking", pattern = true, group = "<!-- 0 --> 💥 Breaking changes" },
{ field = "group", pattern = "remove", group = "<!-- 3 --> 🚨 Removed" },
{ field = "group", pattern = "deprecate", group = "<!-- 2 --> ⚠️ Deprecated" },
{ field = "group", pattern = "delete", group = "<!-- 3 --> 🚨 Removed" },
{ field = "group", pattern = "security", group = "<!-- 5 --> 🔐 Security" },
{ field = "group", pattern = "refactor", group = "<!-- 12 --> ✍ Other changes" },
# Here are rules that apply to unconventional commits
{ message = "(?i)^add", group = "<!-- 1 --> 🚀 New features and improvements" },
{ message = "(?i)^support", group = "<!-- 1 --> 🚀 New features and improvements" },
{ message = "^.*: support", group = "<!-- 1 --> 🚀 New features and improvements" },
{ message = "^.*: add", group = "<!-- 1 --> 🚀 New features and improvements" },
{ message = "^.*: deprecated", group = "<!-- 2 --> ⚠️ Deprecated" },
{ message = "(?i)deprecate", group = "<!-- 2 --> ⚠️ Deprecated" },
{ message = "(?i)tests", group = "<!-- 7 --> 🚦 Tests"},
{ message = "(?i)remove", group = "<!-- 3 --> 🚨 Removed" },
{ message = "^.*: remove", group = "<!-- 3 --> 🚨 Removed" },
{ message = "^.*: delete", group = "<!-- 3 --> 🚨 Removed" },
{ message = "(?i)^fix", group = "<!-- 4 --> 🐛 Bug fixes" },
{ message = "^.*: fix", group = "<!-- 4 --> 🐛 Bug fixes" },
{ message = "^.*: secure", group = "<!-- 5 --> 🔐 Security" },
{ message = "(?i)secure", group = "<!-- 5 --> 🔐 Security" },
{ message = "(?i)security", group = "<!-- 5 --> 🔐 Security" },
{ message = "^.*: security", group = "<!-- 5 --> 🔐 Security" },
{ message = "doc", group = "<!-- 9 --> 📝 Documentation" },
{ message = "docs", group = "<!-- 9 --> 📝 Documentation" },
{ message = "documentation", group = "<!-- 9 --> 📝 Documentation" },
# group any un-matched commits into the "Other changes" category
{ message = "(?i)refactor", group = "<!-- 12 --> ✍ Other changes" },
{ field = "github.pr_labels", pattern = ".*", group = "<!-- 12 --> ✍ Other changes" },
{ message = ".*", group = "<!-- 12 --> ✍ Other changes" },
]
# filter out the commits that are not matched by commit parsers
filter_commits = false
# sort the tags topologically
topo_order = false
# sort the commits inside sections by oldest/newest order
sort_commits = "oldest"
8 changes: 8 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,11 @@ updates:
actions:
patterns:
- "*"
- package-ecosystem: pip
directory: .github/
Comment on lines +18 to +19
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just for updating the pinned version of git-cliff. (see .github/requirements.txt)

schedule:
interval: "monthly"
groups:
pip:
patterns:
- "*"
25 changes: 25 additions & 0 deletions .github/labeler.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# The config for actions/labeler.
# See https://github.com/actions/labeler for more details.

documentation:
- all:
- changed-files:
- any-glob-to-any-file:
- "**/*.md"
- "**/*.rst"
- all-globs-to-all-files:
- "!CHANGELOG.md"
- any:
- head-branch:
- "^docs?"
- "docs?"
bug:
- head-branch:
- "^fix"
- "fix"
enhancement:
- head-branch:
- "^feature"
- "feature"
- "^feat"
- "feat"
1 change: 1 addition & 0 deletions .github/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
git-cliff==2.10.0
77 changes: 77 additions & 0 deletions .github/workflows/labeler.yml
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested this workflow on cpp-linter/cpp-linter#156.

I verified that the title will affect the PR labels.
I had to exclude CHANGELOG.md file from adding the documentation label.
Other exceptions can be added in the .github/labeler.yml config file.
Currently, it is designed to resemble our old release-drafter config about auto-labeling.

Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
name: Label PR

on:
workflow_call:

jobs:
update-pr-labels:
# Only execute this job when the PR is not from a fork. If the PR is from a fork,
# then the needed permission (pull-requests: write) is not granted.
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name }}
permissions:
# needed to read info like git refs and changed files
contents: read
# needed to make changes to a PR's labels
pull-requests: write
runs-on: ubuntu-latest
steps:
- name: Checkout cpp-linter/.github (org) repo
# needed for .github/labeler.yml config (org-specific)
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
repository: cpp-linter/.github
path: org-repo
- uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1
id: labeler
# This step doesn't require a `git checkout`.
# The above checkout step only pulls in the org config.
with:
# If the config file does not exist, then this action will default to
# trying to read the config in project repo
configuration-path: org-repo/.github/labeler.yml
# allow removing labels to keep them in sync with file changes
sync-labels: true

# The rest of this simply adds labels based on the PR title.
# This isn't really necessary due to how git-cliff parses commits.
# This can be removed once actions/labeler resolves the feature request:
# https://github.com/actions/labeler/issues/809
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

release-drafter autoleber already supports branch, title, and body
https://github.com/release-drafter/release-drafter?tab=readme-ov-file#autolabeler

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes but it also requires write permission when it shouldn't need it. This workflow fixes that problem by separating the auto-label behavior (for PR only) from drafting release behavior (for main branch only)

Copy link
Contributor

@shenxianpeng shenxianpeng Sep 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes but it also requires write permission when it shouldn't need it

It needs both write permission to create a GitHub release and add label to PR. release-drafter/release-drafter#1187

contents: write  # for release-drafter to create a github release
pull-requests: write  # for release-drafter to add label to PR

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It shouldn't create a draft release for PR events. PR events have no bearing on releases until the changes are merged to main branch.

- name: Setup Nu shell
# because using bash to manipulate arrays is nonsensical
uses: hustcer/setup-nu@985d59ec83ae3e3418f9d36471cda38b9d8b9879 # v3.20
with:
version: ${{ vars.NUSHELL_VERSION || '*' }}
- name: Label from PR title
shell: nu {0}
env:
GH_REPO: ${{ github.repository }}
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
# includes all existing and added labels from previous step
ALL_LABELS: ${{ steps.labeler.outputs.all-labels }}
run: |-
let title = (
^gh pr view $env.PR_NUMBER --repo $env.GH_REPO --json "title" -q ".title"
)
print $"Analyzing title: ($title)"
let labels = $env.ALL_LABELS | split row ","
mut new_labels = []
if (
($title | str contains --ignore-case "fix")
and not ("bug" in $labels)
) {
$new_labels = $new_labels | append "bug"
}
if (
($title | str contains --ignore-case "feat")
and not ("enhancement" in $labels)
) {
$new_labels = $new_labels | append "enhancement"
}
if ($new_labels | is-not-empty) {
print $"Adding the following labels: ($new_labels | str join ' ')"
gh pr edit $env.PR_NUMBER --add-label ($new_labels | str join ",")
} else {
print "No new labels added based on the PR title"
}
Loading