mirror of
https://github.com/zama-ai/concrete.git
synced 2026-02-09 03:55:04 -05:00
chore: add changelog_helper to generate changelogs using semantic-release
- freeze semantic-release to specific version to keep internal APIs stable - add gitpyhton for changelog_helper - add a target to very easily create a changelog
This commit is contained in:
6
Makefile
6
Makefile
@@ -228,3 +228,9 @@ set_version:
|
||||
check_version_coherence:
|
||||
poetry run python ./script/make_utils/version_utils.py check-version
|
||||
.PHONY: check_version_coherence
|
||||
|
||||
changelog: check_version_coherence
|
||||
PROJECT_VER=($$(poetry version));\
|
||||
PROJECT_VER="$${PROJECT_VER[1]}";\
|
||||
poetry run python ./script/make_utils/changelog_helper.py > "CHANGELOG_$${PROJECT_VER}.md"
|
||||
.PHONY: changelog
|
||||
|
||||
8
poetry.lock
generated
8
poetry.lock
generated
@@ -595,7 +595,7 @@ qtconsole = "*"
|
||||
|
||||
[[package]]
|
||||
name = "jupyter-client"
|
||||
version = "7.0.5"
|
||||
version = "7.0.6"
|
||||
description = "Jupyter protocol implementation and client libraries"
|
||||
category = "dev"
|
||||
optional = false
|
||||
@@ -1901,7 +1901,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes
|
||||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = ">=3.8,<3.9"
|
||||
content-hash = "783e41a9b79babbea7b986de2d4b901e2472e202613952e9881ecf340ff69d18"
|
||||
content-hash = "b044859111d093c4bc499eed965c77062ebddab73f46f8b704edc801868696f5"
|
||||
|
||||
[metadata.files]
|
||||
alabaster = [
|
||||
@@ -2220,8 +2220,8 @@ jupyter = [
|
||||
{file = "jupyter-1.0.0.zip", hash = "sha256:3e1f86076bbb7c8c207829390305a2b1fe836d471ed54be66a3b8c41e7f46cc7"},
|
||||
]
|
||||
jupyter-client = [
|
||||
{file = "jupyter_client-7.0.5-py3-none-any.whl", hash = "sha256:124a6e6979c38999d9153b1c4d1808c4c820a45066d5ed1857a5b59c04ffccb3"},
|
||||
{file = "jupyter_client-7.0.5.tar.gz", hash = "sha256:382aca66dcaf96d7eaaa6c546d57cdf8b3b1cf5bc1f2704c58a1d8d244f1163d"},
|
||||
{file = "jupyter_client-7.0.6-py3-none-any.whl", hash = "sha256:074bdeb1ffaef4a3095468ee16313938cfdc48fc65ca95cc18980b956c2e5d79"},
|
||||
{file = "jupyter_client-7.0.6.tar.gz", hash = "sha256:8b6e06000eb9399775e0a55c52df6c1be4766666209c22f90c2691ded0e338dc"},
|
||||
]
|
||||
jupyter-console = [
|
||||
{file = "jupyter_console-6.4.0-py3-none-any.whl", hash = "sha256:7799c4ea951e0e96ba8260575423cb323ea5a03fcf5503560fa3e15748869e27"},
|
||||
|
||||
@@ -38,9 +38,10 @@ py-cpuinfo = "^8.0.0"
|
||||
python-dotenv = "^0.19.0"
|
||||
sphinx-copybutton = "^0.4.0"
|
||||
nbmake = "^0.9"
|
||||
python-semantic-release = "^7.19.2"
|
||||
python-semantic-release = "7.19.2"
|
||||
semver = "^2.13.0"
|
||||
tomlkit = "^0.7.0"
|
||||
GitPython = "^3.1.24"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
|
||||
248
script/make_utils/changelog_helper.py
Normal file
248
script/make_utils/changelog_helper.py
Normal file
@@ -0,0 +1,248 @@
|
||||
"""Tool to bypass the insane logic of semantic-release and generate changelogs we want"""
|
||||
|
||||
import argparse
|
||||
import subprocess
|
||||
import sys
|
||||
from collections import deque
|
||||
|
||||
from git.repo import Repo
|
||||
from semantic_release.changelog import markdown_changelog
|
||||
from semantic_release.errors import UnknownCommitMessageStyleError
|
||||
from semantic_release.settings import config, current_commit_parser
|
||||
from semantic_release.vcs_helpers import get_repository_owner_and_name
|
||||
from semver import VersionInfo
|
||||
|
||||
|
||||
def log_msg(*args, file=sys.stderr, **kwargs):
|
||||
"""Shortcut to print to sys.stderr."""
|
||||
print(*args, file=file, **kwargs)
|
||||
|
||||
|
||||
def strip_leading_v(version_str: str):
|
||||
"""Strip leading v of a version which is not SemVer compatible."""
|
||||
return version_str[1:] if version_str.startswith("v") else version_str
|
||||
|
||||
|
||||
def get_poetry_project_version() -> VersionInfo:
|
||||
"""Run poetry version and get the project version"""
|
||||
command = ["poetry", "version"]
|
||||
poetry_version_output = subprocess.check_output(command, text=True)
|
||||
return version_string_to_version_info(poetry_version_output.split(" ")[1])
|
||||
|
||||
|
||||
def raise_exception_or_print_warning(is_error: bool, message_body: str):
|
||||
"""Raise an exception if is_error is true else print a warning to stderr"""
|
||||
msg_start = "Error" if is_error else "Warning"
|
||||
msg = f"{msg_start}: {message_body}"
|
||||
if is_error:
|
||||
raise RuntimeError(msg)
|
||||
log_msg(msg)
|
||||
|
||||
|
||||
def version_string_to_version_info(version_string: str) -> VersionInfo:
|
||||
"""Convert git tag to VersionInfo."""
|
||||
return VersionInfo.parse(strip_leading_v(version_string))
|
||||
|
||||
|
||||
def generate_changelog(repo: Repo, from_commit_excluded: str, to_commit_included: str) -> dict:
|
||||
"""Recreate the functionality from semantic release with the from and to commits.
|
||||
|
||||
Args:
|
||||
repo (Repo): the gitpython Repo object representing your git repository
|
||||
from_commit_excluded (str): the commit after which we want to collect commit messages for
|
||||
the changelog
|
||||
to_commit_included (str): the last commit included in the collected commit messages for the
|
||||
changelog.
|
||||
|
||||
Returns:
|
||||
dict: the same formatted dict as the generate_changelog from semantic-release
|
||||
"""
|
||||
# Additional sections will be added as new types are encountered
|
||||
changes: dict = {"breaking": []}
|
||||
|
||||
rev = f"{from_commit_excluded}...{to_commit_included}"
|
||||
|
||||
for commit in repo.iter_commits(rev):
|
||||
hash_ = commit.hexsha
|
||||
commit_message = (
|
||||
commit.message.replace("\r\n", "\n")
|
||||
if isinstance(commit.message, str)
|
||||
else commit.message.replace(b"\r\n", b"\n")
|
||||
)
|
||||
try:
|
||||
message = current_commit_parser()(commit_message)
|
||||
if message.type not in changes:
|
||||
log_msg(f"Creating new changelog section for {message.type} ")
|
||||
changes[message.type] = []
|
||||
|
||||
# Capitalize the first letter of the message, leaving others as they were
|
||||
# (using str.capitalize() would make the other letters lowercase)
|
||||
formatted_message = message.descriptions[0][0].upper() + message.descriptions[0][1:]
|
||||
if config.get("changelog_capitalize") is False:
|
||||
formatted_message = message.descriptions[0]
|
||||
|
||||
# By default, feat(x): description shows up in changelog with the
|
||||
# scope bolded, like:
|
||||
#
|
||||
# * **x**: description
|
||||
if config.get("changelog_scope") and message.scope:
|
||||
formatted_message = f"**{message.scope}:** {formatted_message}"
|
||||
|
||||
changes[message.type].append((hash_, formatted_message))
|
||||
|
||||
if message.breaking_descriptions:
|
||||
# Copy breaking change descriptions into changelog
|
||||
for paragraph in message.breaking_descriptions:
|
||||
changes["breaking"].append((hash_, paragraph))
|
||||
elif message.bump == 3:
|
||||
# Major, but no breaking descriptions, use commit subject instead
|
||||
changes["breaking"].append((hash_, message.descriptions[0]))
|
||||
|
||||
except UnknownCommitMessageStyleError as err:
|
||||
log_msg(f"Ignoring UnknownCommitMessageStyleError: {err}")
|
||||
|
||||
return changes
|
||||
|
||||
|
||||
def main(args):
|
||||
"""Entry point"""
|
||||
|
||||
repo = Repo(args.repo_root)
|
||||
|
||||
sha1_to_tags = {tag.commit.hexsha: tag for tag in repo.tags}
|
||||
|
||||
to_commit = repo.commit(args.to_ref)
|
||||
log_msg(f"To commit: {to_commit}")
|
||||
|
||||
to_tag = sha1_to_tags.get(to_commit.hexsha, None)
|
||||
if to_tag is None:
|
||||
raise_exception_or_print_warning(
|
||||
is_error=args.to_ref_must_have_tag,
|
||||
message_body=f"to-ref {args.to_ref} has no tag associated to it",
|
||||
)
|
||||
|
||||
to_version = (
|
||||
get_poetry_project_version()
|
||||
if to_tag is None
|
||||
else version_string_to_version_info(to_tag.name)
|
||||
)
|
||||
log_msg(f"Project version {to_version} taken from tag: {to_tag is not None}")
|
||||
|
||||
from_commit = None
|
||||
if args.from_ref is None:
|
||||
tags_by_name = {strip_leading_v(tag.name): tag for tag in repo.tags}
|
||||
all_release_version_infos = {
|
||||
version_info: tags_by_name[tag_name]
|
||||
for tag_name in tags_by_name
|
||||
if VersionInfo.isvalid(tag_name)
|
||||
and (version_info := VersionInfo.parse(tag_name))
|
||||
and version_info.prerelease is None
|
||||
}
|
||||
log_msg(f"All release versions {all_release_version_infos}")
|
||||
|
||||
versions_before_project_version = [
|
||||
version_info for version_info in all_release_version_infos if version_info < to_version
|
||||
]
|
||||
if len(versions_before_project_version) > 0:
|
||||
highest_version_before_current_version = max(versions_before_project_version)
|
||||
highest_version_tag = all_release_version_infos[highest_version_before_current_version]
|
||||
from_commit = highest_version_tag.commit
|
||||
else:
|
||||
# No versions before, get the initial commit reachable from to_commit
|
||||
# from https://stackoverflow.com/a/48232574
|
||||
last_element_extractor = deque(repo.iter_commits(to_commit), 1)
|
||||
from_commit = last_element_extractor.pop()
|
||||
else:
|
||||
from_commit = repo.commit(args.from_ref)
|
||||
|
||||
log_msg(f"From commit: {from_commit}")
|
||||
ancestor_commit = repo.merge_base(to_commit, from_commit)
|
||||
assert len(ancestor_commit) == 1
|
||||
ancestor_commit = ancestor_commit[0]
|
||||
log_msg(f"Common ancestor: {ancestor_commit}")
|
||||
|
||||
if ancestor_commit != from_commit:
|
||||
do_not_change_from_ref = args.do_not_change_from_ref and args.from_ref is not None
|
||||
raise_exception_or_print_warning(
|
||||
is_error=do_not_change_from_ref,
|
||||
message_body=(
|
||||
f"the ancestor {ancestor_commit} for {from_commit} and {to_commit} "
|
||||
f"is not the same commit as the commit for '--from-ref' {from_commit}."
|
||||
),
|
||||
)
|
||||
|
||||
ancestor_tag = sha1_to_tags.get(ancestor_commit.hexsha, None)
|
||||
if ancestor_tag is None:
|
||||
raise_exception_or_print_warning(
|
||||
is_error=args.ancestor_must_have_tag,
|
||||
message_body=(
|
||||
f"the ancestor {ancestor_commit} for " f"{from_commit} and {to_commit} has no tag"
|
||||
),
|
||||
)
|
||||
|
||||
ancestor_version_str = (
|
||||
None if ancestor_tag is None else str(version_string_to_version_info(ancestor_tag.name))
|
||||
)
|
||||
|
||||
log_msg(
|
||||
f"Collecting commits from \n{ancestor_commit} "
|
||||
f"(tag: {ancestor_tag} - parsed version "
|
||||
f"{str(ancestor_version_str)}) to \n{to_commit} "
|
||||
f"(tag: {to_tag} - parsed version {str(to_version)})"
|
||||
)
|
||||
|
||||
log_dict = generate_changelog(repo, ancestor_commit.hexsha, to_commit.hexsha)
|
||||
|
||||
owner, name = get_repository_owner_and_name()
|
||||
md_changelog = markdown_changelog(
|
||||
owner,
|
||||
name,
|
||||
str(to_version),
|
||||
log_dict,
|
||||
header=True,
|
||||
previous_version=ancestor_version_str,
|
||||
)
|
||||
|
||||
print(md_changelog)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser("Changelog helper", allow_abbrev=False)
|
||||
|
||||
parser.add_argument("--repo-root", type=str, default=".", help="Path to the repo root")
|
||||
parser.add_argument(
|
||||
"--to-ref",
|
||||
type=str,
|
||||
help="Specify the git ref-like string (sha1, tag, HEAD~, etc.) that will mark the LAST "
|
||||
"included commit of the changelog. If this is not specified, the current project version "
|
||||
"will be used to create a changelog with the current commit as last commit.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--from-ref",
|
||||
type=str,
|
||||
help="Specify the git ref-like string (sha1, tag, HEAD~, etc.) that will mark the commit "
|
||||
"BEFORE the first included commit of the changelog. If this is not specified, the most "
|
||||
"recent actual release tag (no pre-releases) before the '--to-ref' argument will be used. "
|
||||
"If the tagged commit is not an ancestor of '--to-ref' then the most recent common ancestor"
|
||||
"(git merge-base) will be used unless '--do-not-change-from-ref' is specified.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ancestor-must-have-tag",
|
||||
action="store_true",
|
||||
help="Set if the used ancestor must have a tag associated to it.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--to-ref-must-have-tag",
|
||||
action="store_true",
|
||||
help="Set if '--to-ref' must have a tag associated to it.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--do-not-change-from-ref",
|
||||
action="store_true",
|
||||
help="Specify to prevent selecting a different '--from-ref' than the one specified in cli. "
|
||||
"Will raise an exception if '--from-ref' is not a suitable ancestor for '--to-ref' and "
|
||||
"would otherwise use the most recent common ancestor (git merge-base) as '--from-ref'.",
|
||||
)
|
||||
|
||||
cli_args = parser.parse_args()
|
||||
main(cli_args)
|
||||
@@ -13,9 +13,7 @@ from semver import VersionInfo
|
||||
|
||||
def strip_leading_v(version_str: str):
|
||||
"""Strip leading v of a version which is not SemVer compatible."""
|
||||
if version_str and version_str[0] == "v":
|
||||
return version_str[1:]
|
||||
return version_str
|
||||
return version_str[1:] if version_str.startswith("v") else version_str
|
||||
|
||||
|
||||
def islatest(args):
|
||||
|
||||
Reference in New Issue
Block a user