Skip to content

Commit 41172c1

Browse files
authored
feat(release-notes): add license information to default release notes template (#1167)
Adds a one-line notice to the version release notes about the current license the version is released under at that time. Resolves: #228 * test(release-notes): update unit tests to include license statement * test(fixtures): add release notes generator fixture from repo definition * test(cmd-version): add test to evaluate release notes generation w/ license statement * test(cmd-changelog): add test to evaluate release notes generation w/ license statement * docs(changelog-templates): add details about license specification in the release notes * refactor(changelog-templates): simplify markdown template processing * refactor(changelog-templates): simplify reStructuredText template processing * chore(release-notes): add license information to each version release for psr
1 parent cd14e92 commit 41172c1

File tree

21 files changed

+921
-77
lines changed

21 files changed

+921
-77
lines changed

config/release-templates/.components/versioned_changes.md.j2

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,19 @@
22
33
## vX.X.X (YYYY-MMM-DD)
44
5+
_This release is published under the MIT License._ # Release Notes Only
6+
57
{{ change_sections }}
68
79
#}{{
810
"## %s (%s)\n" | format(
911
release.version.as_semver_tag(),
1012
release.tagged_date.strftime("%Y-%m-%d")
1113
)
12-
}}{#
14+
}}{% if license_name is defined and license_name
15+
%}{{ "\n_This release is published under the %s License._\n" | format(license_name)
16+
}}{% endif
17+
%}{#
1318
#}{% set commit_objects = release["elements"]
1419
%}{% include "changes.md.j2"
1520
-%}

config/release-templates/.release_notes.md.j2

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
44
## v1.0.0 (2020-01-01)
55
6+
_This release is published under the MIT License._
7+
68
### ✨ Features
79
810
- Add new feature ([PR#10](https://domain.com/namespace/repo/pull/10), [`abcdef0`](https://domain.com/namespace/repo/commit/HASH))
@@ -43,6 +45,7 @@
4345
#}{# # Set line width to 1000 to avoid wrapping as GitHub will handle it
4446
#}{% set max_line_width = max_line_width | default(1000)
4547
%}{% set hanging_indent = hanging_indent | default(2)
48+
%}{% set license_name = license_name | default("", True)
4649
%}{% set releases = context.history.released.values() | list
4750
%}{% set curr_release_index = releases.index(release)
4851
%}{#

docs/changelog_templates.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,10 +286,16 @@ The default template provided by PSR will respect the
286286
will also add a comparison link to the previous release if one exists without
287287
customization.
288288

289+
As of ``${NEW_RELEASE_TAG}``, the default release notes will also include a statement to
290+
declare which license the project was released under. PSR determines which license
291+
to declare based on the value of ``project.license-expression`` in the ``pyproject.toml``
292+
file as defined in the `PEP 639`_ specification.
293+
289294
.. seealso::
290295
- To personalize your release notes, see the
291296
:ref:`changelog-templates-custom_release_notes` section.
292297

298+
.. _PEP 639: https://peps.python.org/pep-0639/
293299

294300
.. _changelog-templates-template-rendering:
295301

src/semantic_release/changelog/context.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ class ReleaseNotesContext:
2929
version: Version
3030
release: Release
3131
mask_initial_release: bool
32+
license_name: str
3233
filters: tuple[Callable[..., Any], ...] = ()
3334

3435
def bind_to_environment(self, env: Environment) -> Environment:

src/semantic_release/cli/changelog_writer.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@ def generate_release_notes(
230230
history: ReleaseHistory,
231231
style: str,
232232
mask_initial_release: bool,
233+
license_name: str = "",
233234
) -> str:
234235
users_tpl_file = template_dir / DEFAULT_RELEASE_NOTES_TPL_FILE
235236

@@ -256,6 +257,7 @@ def generate_release_notes(
256257
version=release["version"],
257258
release=release,
258259
mask_initial_release=mask_initial_release,
260+
license_name=license_name,
259261
filters=(
260262
*hvcs_client.get_changelog_context_filters(),
261263
create_pypi_url,

src/semantic_release/cli/commands/changelog.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
from __future__ import annotations
22

33
import logging
4+
from contextlib import suppress
5+
from pathlib import Path
46
from typing import TYPE_CHECKING
57

68
import click
7-
from git import Repo
9+
import tomlkit
10+
from git import GitCommandError, Repo
811

912
from semantic_release.changelog.release_history import ReleaseHistory
1013
from semantic_release.cli.changelog_writer import (
@@ -21,6 +24,43 @@
2124
log = logging.getLogger(__name__)
2225

2326

27+
def get_license_name_for_release(tag_name: str, project_root: Path) -> str:
28+
# Retrieve the license name at the time of the specific release tag
29+
project_metadata: dict[str, str] = {}
30+
curr_dir = Path.cwd().resolve()
31+
allowed_directories = [
32+
dir_path
33+
for dir_path in [curr_dir, *curr_dir.parents]
34+
if str(project_root) in str(dir_path)
35+
]
36+
for allowed_dir in allowed_directories:
37+
proj_toml = allowed_dir.joinpath("pyproject.toml")
38+
with Repo(project_root) as git_repo, suppress(GitCommandError):
39+
toml_contents = git_repo.git.show(
40+
f"{tag_name}:{proj_toml.relative_to(project_root)}"
41+
)
42+
config_toml = tomlkit.parse(toml_contents)
43+
project_metadata = config_toml.unwrap().get("project", project_metadata)
44+
break
45+
46+
license_cfg = project_metadata.get(
47+
"license-expression",
48+
project_metadata.get(
49+
"license",
50+
"",
51+
),
52+
)
53+
54+
if not isinstance(license_cfg, (str, dict)) or license_cfg is None:
55+
return ""
56+
57+
return (
58+
license_cfg.get("text", "") # type: ignore[attr-defined]
59+
if isinstance(license_cfg, dict)
60+
else license_cfg or ""
61+
)
62+
63+
2464
def post_release_notes(
2565
release_tag: str,
2666
release_notes: str,
@@ -120,6 +160,10 @@ def changelog(cli_ctx: CliContextObj, release_tag: str | None) -> None:
120160
release_history,
121161
style=runtime.changelog_style,
122162
mask_initial_release=runtime.changelog_mask_initial_release,
163+
license_name=get_license_name_for_release(
164+
tag_name=release_tag,
165+
project_root=runtime.repo_dir,
166+
),
123167
)
124168

125169
try:

src/semantic_release/cli/commands/version.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -699,13 +699,31 @@ def version( # noqa: C901
699699
log.info("Remote does not support releases. Skipping release creation...")
700700
return
701701

702+
license_cfg = runtime.project_metadata.get(
703+
"license-expression",
704+
runtime.project_metadata.get(
705+
"license",
706+
"",
707+
),
708+
)
709+
710+
if not isinstance(license_cfg, (str, dict)) or license_cfg is None:
711+
license_cfg = ""
712+
713+
license_name = (
714+
license_cfg.get("text", "")
715+
if isinstance(license_cfg, dict)
716+
else license_cfg or ""
717+
)
718+
702719
release_notes = generate_release_notes(
703720
hvcs_client,
704-
release_history.released[new_version],
705-
runtime.template_dir,
721+
release=release_history.released[new_version],
722+
template_dir=runtime.template_dir,
706723
history=release_history,
707724
style=runtime.changelog_style,
708725
mask_initial_release=runtime.changelog_mask_initial_release,
726+
license_name=license_name,
709727
)
710728

711729
exception: Exception | None = None

src/semantic_release/cli/config.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
)
1616
from typing import Any, ClassVar, Dict, List, Literal, Optional, Tuple, Type, Union
1717

18+
# typing_extensions is for Python 3.8, 3.9, 3.10 compatibility
19+
import tomlkit
1820
from git import Actor, InvalidGitRepositoryError
1921
from git.repo.base import Repo
2022
from jinja2 import Environment
@@ -26,8 +28,6 @@
2628
field_validator,
2729
model_validator,
2830
)
29-
30-
# typing_extensions is for Python 3.8, 3.9, 3.10 compatibility
3131
from typing_extensions import Annotated, Self
3232
from urllib3.util.url import parse_url
3333

@@ -523,6 +523,7 @@ def _recursive_getattr(obj: Any, path: str) -> Any:
523523
class RuntimeContext:
524524
_mask_attrs_: ClassVar[List[str]] = ["hvcs_client.token"]
525525

526+
project_metadata: dict[str, Any]
526527
repo_dir: Path
527528
commit_parser: CommitParser[ParseResult, ParserOptions]
528529
version_translator: VersionTranslator
@@ -599,6 +600,21 @@ def from_raw_config( # noqa: C901
599600
# credentials masking for logging
600601
masker = MaskingFilter(_use_named_masks=raw.logging_use_named_masks)
601602

603+
# TODO: move to config if we change how the generated config is constructed
604+
# Retrieve project metadata from pyproject.toml
605+
project_metadata: dict[str, str] = {}
606+
curr_dir = Path.cwd().resolve()
607+
allowed_directories = [
608+
dir_path
609+
for dir_path in [curr_dir, *curr_dir.parents]
610+
if str(raw.repo_dir) in str(dir_path)
611+
]
612+
for allowed_dir in allowed_directories:
613+
if (proj_toml := allowed_dir.joinpath("pyproject.toml")).exists():
614+
config_toml = tomlkit.parse(proj_toml.read_text())
615+
project_metadata = config_toml.unwrap().get("project", project_metadata)
616+
break
617+
602618
# Retrieve details from repository
603619
with Repo(str(raw.repo_dir)) as git_repo:
604620
try:
@@ -825,6 +841,7 @@ def from_raw_config( # noqa: C901
825841
# )
826842

827843
self = cls(
844+
project_metadata=project_metadata,
828845
repo_dir=raw.repo_dir,
829846
commit_parser=commit_parser,
830847
version_translator=version_translator,

src/semantic_release/data/templates/angular/md/.components/changes.md.j2

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ EXAMPLE:
4141
#}{% for type_, commits in commit_objects if type_ != "unknown"
4242
%}{# PREPROCESS COMMITS (order by description & format description line)
4343
#}{% set ns = namespace(commits=commits)
44-
%}{{ apply_alphabetical_ordering_by_descriptions(ns) | default("", true)
45-
}}{#
44+
%}{% set _ = apply_alphabetical_ordering_by_descriptions(ns)
45+
%}{#
4646
#}{% set commit_descriptions = []
4747
%}{#
4848
#}{% for commit in ns.commits
@@ -57,8 +57,8 @@ EXAMPLE:
5757
)
5858
%}{% endif
5959
%}{% set description = description | autofit_text_width(max_line_width, hanging_indent)
60-
%}{{ commit_descriptions.append(description) | default("", true)
61-
}}{% endfor
60+
%}{% set _ = commit_descriptions.append(description)
61+
%}{% endfor
6262
%}{#
6363
# # PRINT SECTION (header & commits)
6464
#}{% if commit_descriptions | length > 0
@@ -81,18 +81,18 @@ EXAMPLE:
8181
#}{% if breaking_commits | length > 0
8282
%}{# PREPROCESS COMMITS
8383
#}{% set brk_ns = namespace(commits=breaking_commits)
84-
%}{{ apply_alphabetical_ordering_by_brk_descriptions(brk_ns) | default("", true)
85-
}}{#
84+
%}{% set _ = apply_alphabetical_ordering_by_brk_descriptions(brk_ns)
85+
%}{#
8686
#}{% set brking_descriptions = []
8787
%}{#
8888
#}{% for commit in brk_ns.commits
8989
%}{% set full_description = "- %s" | format(
9090
format_breaking_changes_description(commit).split("\n\n") | join("\n\n- ")
9191
)
92-
%}{{ brking_descriptions.append(
92+
%}{% set _ = brking_descriptions.append(
9393
full_description | autofit_text_width(max_line_width, hanging_indent)
94-
) | default("", true)
95-
}}{% endfor
94+
)
95+
%}{% endfor
9696
%}{#
9797
# # PRINT BREAKING CHANGE DESCRIPTIONS (header & descriptions)
9898
#}{{ "\n"
@@ -114,18 +114,18 @@ EXAMPLE:
114114
#}{% if notice_commits | length > 0
115115
%}{# PREPROCESS COMMITS
116116
#}{% set notice_ns = namespace(commits=notice_commits)
117-
%}{{ apply_alphabetical_ordering_by_release_notices(notice_ns) | default("", true)
118-
}}{#
117+
%}{% set _ = apply_alphabetical_ordering_by_release_notices(notice_ns)
118+
%}{#
119119
#}{% set release_notices = []
120120
%}{#
121121
#}{% for commit in notice_ns.commits
122122
%}{% set full_description = "- %s" | format(
123123
format_release_notice(commit).split("\n\n") | join("\n\n- ")
124124
)
125-
%}{{ release_notices.append(
125+
%}{% set _ = release_notices.append(
126126
full_description | autofit_text_width(max_line_width, hanging_indent)
127-
) | default("", true)
128-
}}{% endfor
127+
)
128+
%}{% endfor
129129
%}{#
130130
# # PRINT RELEASE NOTICE INFORMATION (header & descriptions)
131131
#}{{ "\n"
Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
1-
{#
1+
{# EXAMPLE:
22
33
## vX.X.X (YYYY-MMM-DD)
44
5+
_This release is published under the MIT License._ # Release Notes Only
6+
7+
- Initial Release
8+
59
#}{{
6-
"## %s (%s)" | format(
10+
"## %s (%s)\n" | format(
711
release.version.as_semver_tag(),
812
release.tagged_date.strftime("%Y-%m-%d")
9-
)}}
10-
13+
)
14+
}}{% if license_name is defined and license_name
15+
%}{{ "\n_This release is published under the %s License._\n" | format(license_name)
16+
}}{% endif
17+
%}
1118
- Initial Release
Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
1-
{#
1+
{# EXAMPLE:
22
33
## vX.X.X (YYYY-MMM-DD)
44
5-
#}{{
5+
_This release is published under the MIT License._ # Release Notes Only
66
7-
"## %s (%s)\n" | format(
8-
release.version.as_semver_tag(),
9-
release.tagged_date.strftime("%Y-%m-%d")
10-
)
7+
{{ change_sections }}
118
12-
}}{% set commit_objects = release["elements"] | dictsort
9+
#}{{
10+
"## %s (%s)\n" | format(
11+
release.version.as_semver_tag(),
12+
release.tagged_date.strftime("%Y-%m-%d")
13+
)
14+
}}{% if license_name is defined and license_name
15+
%}{{ "\n_This release is published under the %s License._\n" | format(license_name)
16+
}}{% endif
17+
%}{#
18+
#}{% set commit_objects = release["elements"] | dictsort
1319
%}{% include "changes.md.j2"
1420
-%}

0 commit comments

Comments
 (0)