diff --git a/.github/workflows/bump_version_release.yaml b/.github/workflows/bump_version_release.yaml index 73b1e51..2d954e5 100644 --- a/.github/workflows/bump_version_release.yaml +++ b/.github/workflows/bump_version_release.yaml @@ -53,6 +53,9 @@ jobs: # here we need v3.10+ python-version: 3.x + - name: Install git-cliff + run: pip install git-cliff + - name: increment version working-directory: ${{ inputs.repo }} id: inc-ver @@ -77,6 +80,6 @@ jobs: run: >- gh release create v${{ steps.inc-ver.outputs.new-version }} - --generate-notes + --notes-file ${{ steps.inc-ver.outputs.release-notes }} --repo nRF24/${{ inputs.repo }} --target ${{ inputs.branch }} diff --git a/.github/workflows/cliff.toml b/.github/workflows/cliff.toml new file mode 100644 index 0000000..f9ddb8d --- /dev/null +++ b/.github/workflows/cliff.toml @@ -0,0 +1,139 @@ +# 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). +\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 +{%- endif -%} + +{% 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 %}\n +""" +# template for the changelog footer +footer = """ + +""" +# 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 issue numbers from commits + { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "" }, +] +# regex for parsing and grouping commits +commit_parsers = [ + { field = "github.pr_labels", pattern = "breaking", group = " 💥 Breaking changes" }, + { field = "github.pr_labels", pattern = "breaking-change", group = " 💥 Breaking changes" }, + { field = "github.pr_labels", pattern = "feature", group = " 🚀 Added" }, + { field = "github.pr_labels", pattern = "enhancement", group = " 🚀 Added" }, + { field = "github.pr_labels", pattern = "deprecated", group = " 🚫 Deprecated" }, + { field = "github.pr_labels", pattern = "removed", group = " 🗑️ Removed" }, + { field = "github.pr_labels", pattern = "bug", group = " 🛠️ Fixed" }, + { field = "github.pr_labels", pattern = "security", group = " 🔐 Security" }, + { field = "github.pr_labels", pattern = "dependencies", group = " 📦 Dependency updates" }, + { field = "github.pr_labels", pattern = "test", group = "🚦 Tests"}, + { field = "github.pr_labels", pattern = "tests", group = "🚦 Tests"}, + { field = "github.pr_labels", pattern = "documentation", group = " 📝 Documentation" }, + { field = "github.pr_labels", pattern = "refactor", group = " 🗨️ Changed" }, + { 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 }, + # The order of parsers matters. Put rules for PR labels first to prioritize PR labels. + { message = "^[a|A]dd", group = " 🚀 Added" }, + { message = "^[s|S]upport", group = " 🚀 Added" }, + { message = "^.*: support", group = " 🚀 Added" }, + { message = "^.*: add", group = " 🚀 Added" }, + { message = "^.*: deprecated", group = " 🚫 Deprecated" }, + { message = "[d|D]eprecate", group = " 🚫 Deprecated" }, + { message = "[t|T]ests", group = "🚦 Tests"}, + { message = "[r|R]emove", group = " 🗑️ Removed" }, + { message = "^.*: remove", group = " 🗑️ Removed" }, + { message = "^.*: delete", group = " 🗑️ Removed" }, + { message = "^[f|F]ix", group = " 🛠️ Fixed" }, + { message = "^.*: fix", group = " 🛠️ Fixed" }, + { message = "^.*: secure", group = " 🔐 Security" }, + { message = "[s|S]ecure", group = " 🔐 Security" }, + { message = "[s|S]ecurity", group = " 🔐 Security" }, + { message = "^.*: security", group = " 🔐 Security" }, + { message = "doc", group = " 📝 Documentation" }, + { message = "docs", group = " 📝 Documentation" }, + { message = "documentation", group = " 📝 Documentation" }, + { message = "[r|R]efactor", group = " 🗨️ Changed" }, + { field = "github.pr_labels", pattern = ".*", group = " 🗨️ Changed" }, + { message = "^.*", group = " 🗨️ Changed" }, +] +# filter out the commits that are not matched by commit parsers +filter_commits = true +# sort the tags topologically +topo_order = false +# sort the commits inside sections by oldest/newest order +sort_commits = "oldest" diff --git a/.github/workflows/increment_version.py b/.github/workflows/increment_version.py index 62d205c..ac1686d 100644 --- a/.github/workflows/increment_version.py +++ b/.github/workflows/increment_version.py @@ -10,12 +10,15 @@ import subprocess from typing import cast, Tuple, List, Sequence, Dict import sys +from difflib import unified_diff VERSION_TUPLE = Tuple[int, int, int] COMPONENTS = ["major", "minor", "patch"] +GIT_CLIFF_CONFIG = Path(__file__).parent / "cliff.toml" +RELEASE_NOTES = GIT_CLIFF_CONFIG.with_name("ReleaseNotes.md") -def get_version() -> VERSION_TUPLE: +def get_version() -> Tuple[VERSION_TUPLE, str]: """get current latest tag and parse into a 3-tuple""" # get list of all tags result = subprocess.run( @@ -70,7 +73,7 @@ def get_version() -> VERSION_TUPLE: print("treating branch", repr(branch), "as latest stable branch") ver_tag = tags[0] print("Current version:", ".".join([str(x) for x in ver_tag])) - return ver_tag + return ver_tag, branch def increment_version(version: VERSION_TUPLE, bump: str = "patch") -> VERSION_TUPLE: @@ -84,6 +87,40 @@ def increment_version(version: VERSION_TUPLE, bump: str = "patch") -> VERSION_TU return tuple(new_ver) +def get_changelog( + tag: str, first_commit: str, full: bool = False, branch: str = "main" +): + """Gets the changelog for this release. + If ``full`` is true, then this returns a flag to describe if + anything was changed in the CHANGELOG.md""" + old = "" + changelog = Path("CHANGELOG.md") + if full and changelog.exists(): + old = changelog.read_text(encoding="utf-8") + output = changelog + args = ["git-cliff", "--config", str(GIT_CLIFF_CONFIG), "--tag", tag] + if not full: + args.append("--unreleased") + output = str(RELEASE_NOTES) + if branch == "v1.x": + args.extend(["--ignore-tags", "[v|V]?2\\..*"]) + subprocess.run( + args + ["--output", output], env={"FIRST_COMMIT": first_commit}, check=True + ) + if full: + new = changelog.read_text(encoding="utf-8") + changes = list(unified_diff(old, new)) + return len(changes) != 0 + return False + + +def get_first_commit() -> str: + result = subprocess.run( + ["git", "rev-list", "--max-parents=0", "HEAD"], check=True, capture_output=True + ) + return result.stdout.decode("utf-8").strip() + + def update_metadata_files(version: str) -> bool: """update the library metadata files with the new specified ``version``.""" made_changes = False @@ -142,17 +179,23 @@ def main() -> int: ) args = parser.parse_args(namespace=Args()) - version = increment_version(version=get_version(), bump=args.bump) + version, branch = get_version() + version = increment_version(version=version, bump=args.bump) ver_str = ".".join([str(x) for x in version]) + first_commit = get_first_commit() + # generate release notes and save them to a file + get_changelog(ver_str, first_commit, full=False, branch=branch) + # generate complete changelog + made_changes = get_changelog(ver_str, first_commit, full=True, branch=branch) print("New version:", ver_str) - made_changes = False if args.update_metadata: made_changes = update_metadata_files(ver_str) print("Metadata file(s) updated:", made_changes) if "GITHUB_OUTPUT" in environ: # create an output variables for use in CI workflow with open(environ["GITHUB_OUTPUT"], mode="a") as gh_out: + gh_out.write(f"release-notes={RELEASE_NOTES}\n") gh_out.write(f"new-version={ver_str}\n") gh_out.write(f"made-changes={str(made_changes).lower()}\n") diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b248363 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ + +.github/workflows/ReleaseNotes.md