Skip to content

Commit

Permalink
generate a changelog upon release (#17)
Browse files Browse the repository at this point in the history
* detect if CHANGELOG was altered
* tested locally
* fix changelog for v1.x branches
  • Loading branch information
2bndy5 authored Oct 5, 2024
1 parent 0030e74 commit 0bc3601
Show file tree
Hide file tree
Showing 4 changed files with 192 additions and 5 deletions.
5 changes: 4 additions & 1 deletion .github/workflows/bump_version_release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 }}
139 changes: 139 additions & 0 deletions .github/workflows/cliff.toml
Original file line number Diff line number Diff line change
@@ -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).
<!-- 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
{%- 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 = """
<!-- 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 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 = "<!-- 0 --> 💥 Breaking changes" },
{ field = "github.pr_labels", pattern = "breaking-change", group = "<!-- 0 --> 💥 Breaking changes" },
{ field = "github.pr_labels", pattern = "feature", group = "<!-- 1 --> 🚀 Added" },
{ field = "github.pr_labels", pattern = "enhancement", group = "<!-- 1 --> 🚀 Added" },
{ 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 --> 🛠️ Fixed" },
{ 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 = "documentation", group = "<!-- 8 --> 📝 Documentation" },
{ field = "github.pr_labels", pattern = "refactor", group = "<!-- 9 --> 🗨️ 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 = "<!-- 1 --> 🚀 Added" },
{ message = "^[s|S]upport", group = "<!-- 1 --> 🚀 Added" },
{ message = "^.*: support", group = "<!-- 1 --> 🚀 Added" },
{ message = "^.*: add", group = "<!-- 1 --> 🚀 Added" },
{ message = "^.*: deprecated", group = "<!-- 2 --> 🚫 Deprecated" },
{ message = "[d|D]eprecate", group = "<!-- 2 --> 🚫 Deprecated" },
{ message = "[t|T]ests", group = "<!-- 7 -->🚦 Tests"},
{ message = "[r|R]emove", group = "<!-- 3 --> 🗑️ Removed" },
{ message = "^.*: remove", group = "<!-- 3 --> 🗑️ Removed" },
{ message = "^.*: delete", group = "<!-- 3 --> 🗑️ Removed" },
{ message = "^[f|F]ix", group = "<!-- 4 --> 🛠️ Fixed" },
{ message = "^.*: fix", group = "<!-- 4 --> 🛠️ Fixed" },
{ message = "^.*: secure", group = "<!-- 5 --> 🔐 Security" },
{ message = "[s|S]ecure", group = "<!-- 5 --> 🔐 Security" },
{ message = "[s|S]ecurity", group = "<!-- 5 --> 🔐 Security" },
{ message = "^.*: security", group = "<!-- 5 --> 🔐 Security" },
{ message = "doc", group = "<!-- 8 --> 📝 Documentation" },
{ message = "docs", group = "<!-- 8 --> 📝 Documentation" },
{ message = "documentation", group = "<!-- 8 --> 📝 Documentation" },
{ message = "[r|R]efactor", group = "<!-- 9 --> 🗨️ Changed" },
{ field = "github.pr_labels", pattern = ".*", group = "<!-- 9 --> 🗨️ Changed" },
{ message = "^.*", group = "<!-- 9 --> 🗨️ 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"
51 changes: 47 additions & 4 deletions .github/workflows/increment_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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")

Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@

.github/workflows/ReleaseNotes.md

0 comments on commit 0bc3601

Please sign in to comment.