Skip to content

Commit

Permalink
feat: Allow user customization of release notes template (#736)
Browse files Browse the repository at this point in the history
Signed-off-by: Bryant Finney <bryant.finney@outlook.com>
  • Loading branch information
bryant-finney committed Oct 23, 2023
1 parent 3284258 commit 94a1311
Show file tree
Hide file tree
Showing 18 changed files with 1,042 additions and 656 deletions.
1,134 changes: 567 additions & 567 deletions CHANGELOG.md

Large diffs are not rendered by default.

50 changes: 50 additions & 0 deletions docs/changelog_templates.rst
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,56 @@ Each ``Release`` object also has the following attributes:

.. _dataclass: https://docs.python.org/3/library/dataclasses.html

.. _changelog-templates-customizing-vcs-release-notes:

Customizing VCS Release Notes
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

The same :ref:`template rendering <changelog-templates-template-rendering>` mechanism
generates the release notes when :ref:`creating VCS releases <index-creating-vcs-releases>`:

* the `in-built template`_ is used by default
* create a file named ``.release_notes.md.j2`` inside the project's
:ref:`template_dir <config-changelog-template-dir>` to customize the release notes

.. _changelog-templates-customizing-vcs-release-notes-release-notes-context:

Release Notes Context
"""""""""""""""""""""

All of the changelog's
:ref:`template context <changelog-templates-template-rendering-template-context>` is
exposed to the `Jinja`_ template when rendering the release notes.

Additionally, the following two globals are available to the template:

* ``release`` (:class:`Release <semantic_release.changelog.release_history.Release>`):
contains metadata about the content of the release, as parsed from commit logs
* ``version`` (:class:`Version <semantic_release.version.version.Version>`): contains
metadata about the software version to be released and its ``git`` tag

.. _in-built template: https://github.com/python-semantic-release/python-semantic-release/blob/master/semantic_release/data/templates/release_notes.md.j2

.. _changelog-templates-release-notes-template-example:

Release Notes Template Example
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Below is an example template that can be used to render release notes (it's similar to
GitHub's `automatically generated release notes`_):

.. code-block::
## What's Changed
{% for type_, commits in release["elements"] | dictsort %}
### {{ type_ | capitalize }}
{%- if type_ != "unknown" %}
{% for commit in commits %}
* {{ commit.descriptions[0] }} by {{commit.commit.author.name}} in [`{{ commit.short_hash }}`]({{ commit.hexsha | commit_hash_url }})
{%- endfor %}{% endif %}{% endfor %}
.. _Automatically generated release notes: https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes

.. _changelog-templates-template-rendering-example:

Changelog Template Example
Expand Down
2 changes: 2 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,8 @@ environment variable.

.. seealso::
- :ref:`Changelog <config-changelog>` - customize your project's changelog.
- :ref:`Customizing VCS Release Notes <changelog-templates-customizing-vcs-release-notes>` - customize
the VCS release notes.
- :ref:`upload_to_vcs_release <config-publish-upload-to-vcs-release>` -
enable/disable uploading artefacts to VCS releases
- :ref:`version --vcs-release/--no-vcs-release <cmd-version-option-vcs-release>` - enable/disable VCS release
Expand Down
5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# Ref: https://packaging.python.org/en/latest/specifications/declaring-project-metadata/
# and https://setuptools.pypa.io/en/latest/userguide/pyproject_config.html

[build-system]
requires = ["setuptools>=61.0.0", "wheel"]
build-backend = "setuptools.build_meta"
Expand Down Expand Up @@ -77,13 +76,17 @@ addopts = [
"-ra",
"--cache-clear",
"--cov=semantic_release",
"--cov-context=test",
"--cov-report",
"html:coverage-html",
"--cov-report",
"term",
]
python_files = ["tests/test_*.py", "tests/**/test_*.py"]

[tool.coverage.html]
show_contexts = true

[tool.coverage.run]
omit = ["*/tests/*"]

Expand Down
6 changes: 4 additions & 2 deletions semantic_release/changelog/template.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

# pylint: disable=too-many-arguments,too-many-locals
def environment(
template_dir: str = ".",
template_dir: Path | str = ".",
block_start_string: str = "{%",
block_end_string: str = "%}",
variable_start_string: str = "{{",
Expand Down Expand Up @@ -75,7 +75,9 @@ def environment(

# pylint: disable=redefined-outer-name
def recursive_render(
template_dir: str, environment: Environment, _root_dir: str = "."
template_dir: Path,
environment: Environment,
_root_dir: str | os.PathLike[str] = ".",
) -> list[str]:
rendered_paths: list[str] = []
for root, file in (
Expand Down
57 changes: 38 additions & 19 deletions semantic_release/cli/commands/changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,23 @@

import logging
import os
from typing import TYPE_CHECKING

import click

from semantic_release.changelog import ReleaseHistory, recursive_render
from semantic_release.changelog.context import make_changelog_context
from semantic_release.cli.common import (
get_release_notes_template,
render_default_changelog_file,
render_release_notes,
)
from semantic_release.cli.util import noop_report

if TYPE_CHECKING:
from semantic_release.cli.config import RuntimeContext
from semantic_release.version import Version

log = logging.getLogger(__name__)


Expand All @@ -26,12 +32,12 @@
"--post-to-release-tag",
"release_tag",
default=None,
help="Post the generated changelog to the remote VCS's release for this tag",
help="Post the generated release notes to the remote VCS's release for this tag",
)
@click.pass_context
def changelog(ctx: click.Context, release_tag: str | None = None) -> None:
"""Generate and optionally publish a changelog for your project"""
runtime = ctx.obj
runtime: RuntimeContext = ctx.obj
repo = runtime.repo
parser = runtime.commit_parser
translator = runtime.version_translator
Expand Down Expand Up @@ -59,24 +65,27 @@ def changelog(ctx: click.Context, release_tag: str | None = None) -> None:
"would have written your changelog to "
+ str(changelog_file.relative_to(repo.working_dir))
)
ctx.exit(0)
else:
changelog_text = render_default_changelog_file(env)
changelog_file.write_text(changelog_text, encoding="utf-8")

changelog_text = render_default_changelog_file(env)
with open(str(changelog_file), "w+", encoding="utf-8") as f:
f.write(changelog_text)
else:
if runtime.global_cli_options.noop:
noop_report(
f"would have recursively rendered the template directory "
f"{template_dir!r} relative to {repo.working_dir!r}"
)
ctx.exit(0)
recursive_render(template_dir, environment=env, _root_dir=repo.working_dir)
else:
recursive_render(template_dir, environment=env, _root_dir=repo.working_dir)

if release_tag:
if runtime.global_cli_options.noop:
noop_report(
f"would have posted changelog to the release for tag {release_tag}"
)

if release_tag and runtime.global_cli_options.noop:
noop_report(f"would have posted changelog to the release for tag {release_tag}")
elif release_tag:
version = translator.from_tag(release_tag)
# note: the following check ensures 'version is not None', but mypy can't follow
version: Version = translator.from_tag(release_tag) # type: ignore[assignment]
if not version:
ctx.fail(
f"Tag {release_tag!r} doesn't match tag format "
Expand All @@ -88,13 +97,23 @@ def changelog(ctx: click.Context, release_tag: str | None = None) -> None:
except KeyError:
ctx.fail(f"tag {release_tag} not in release history")

template = get_release_notes_template(template_dir)
release_notes = render_release_notes(
template_environment=env, version=version, release=release
release_notes_template=template,
template_environment=env,
version=version,
release=release,
)
try:
hvcs_client.create_or_update_release(
release_tag, release_notes, prerelease=version.is_prerelease

if runtime.global_cli_options.noop:
noop_report(
"would have posted the following release notes:\n" + release_notes
)
except Exception as e:
log.exception(e)
ctx.fail(str(e))
else:
try:
hvcs_client.create_or_update_release(
release_tag, release_notes, prerelease=version.is_prerelease
)
except Exception as e:
log.exception(e)
ctx.fail(str(e))
77 changes: 43 additions & 34 deletions semantic_release/cli/commands/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
import subprocess
from contextlib import nullcontext
from datetime import datetime
from typing import TYPE_CHECKING, ContextManager
from typing import TYPE_CHECKING, ContextManager, Iterable

import click
import shellingham # type: ignore[import]

from semantic_release.changelog import ReleaseHistory, environment, recursive_render
from semantic_release.changelog.context import make_changelog_context
from semantic_release.cli.common import (
get_release_notes_template,
render_default_changelog_file,
render_release_notes,
)
Expand All @@ -27,6 +28,7 @@
if TYPE_CHECKING:
from git import Repo

from semantic_release.cli.config import RuntimeContext
from semantic_release.version import VersionTranslator
from semantic_release.version.declaration import VersionDeclarationABC

Expand Down Expand Up @@ -60,7 +62,7 @@ def version_from_forced_level(

def apply_version_to_source_files(
repo: Repo,
version_declarations: list[VersionDeclarationABC],
version_declarations: Iterable[VersionDeclarationABC],
version: Version,
noop: bool = False,
) -> list[str]:
Expand Down Expand Up @@ -207,7 +209,7 @@ def version( # noqa: C901
* Push the new tag and commit to the remote for the repository
* Create a release (if supported) in the remote VCS for this tag
"""
runtime = ctx.obj
runtime: RuntimeContext = ctx.obj
repo = runtime.repo
parser = runtime.commit_parser
translator = runtime.version_translator
Expand Down Expand Up @@ -383,7 +385,20 @@ def version( # noqa: C901

updated_paths: list[str] = []
if update_changelog:
if not os.path.exists(template_dir):
if template_dir.is_dir():
if opts.noop:
noop_report(
f"would have recursively rendered the template directory "
f"{template_dir!r} relative to {repo.working_dir!r}. "
"Paths which would be modified by this operation cannot be "
"determined in no-op mode."
)
else:
updated_paths = recursive_render(
template_dir, environment=env, _root_dir=repo.working_dir
)

else:
log.info(
"Path %r not found, using default changelog template", template_dir
)
Expand All @@ -394,22 +409,9 @@ def version( # noqa: C901
)
else:
changelog_text = render_default_changelog_file(env)
with open(str(changelog_file), "w+", encoding="utf-8") as f:
f.write(changelog_text)
changelog_file.write_text(changelog_text, encoding="utf-8")

updated_paths = [str(changelog_file.relative_to(repo.working_dir))]
else:
if opts.noop:
noop_report(
f"would have recursively rendered the template directory "
f"{template_dir!r} relative to {repo.working_dir!r}. "
"Paths which would be modified by this operation cannot be "
"determined in no-op mode."
)
else:
updated_paths = recursive_render(
template_dir, environment=env, _root_dir=repo.working_dir
)

if commit_changes and opts.noop:
noop_report(
Expand Down Expand Up @@ -516,34 +518,41 @@ def custom_git_environment() -> ContextManager[None]:

gha_output.released = True

if make_vcs_release and opts.noop:
noop_report(
f"would have created a release for the tag {new_version.as_tag()!r}"
)
noop_report(f"would have uploaded the following assets: {runtime.assets}")
elif make_vcs_release:
if make_vcs_release:
if opts.noop:
noop_report(
f"would have created a release for the tag {new_version.as_tag()!r}"
)

release = rh.released[new_version]
# Use a new, non-configurable environment for release notes -
# not user-configurable at the moment
release_note_environment = environment(template_dir=runtime.template_dir)
changelog_context.bind_to_environment(release_note_environment)

template = get_release_notes_template(template_dir)
release_notes = render_release_notes(
release_notes_template=template,
template_environment=release_note_environment,
version=new_version,
release=release,
)
try:
release_id = hvcs_client.create_or_update_release(
tag=new_version.as_tag(),
release_notes=release_notes,
prerelease=new_version.is_prerelease,
if opts.noop:
noop_report(
"would have created the following release notes: \n" + release_notes
)
except Exception as e:
log.exception(e)
ctx.fail(str(e))
if not release_id:
log.warning("release_id not identified, cannot upload assets")
noop_report(f"would have uploaded the following assets: {runtime.assets}")
else:
try:
release_id = hvcs_client.create_or_update_release(
tag=new_version.as_tag(),
release_notes=release_notes,
prerelease=new_version.is_prerelease,
)
except Exception as e:
log.exception(e)
ctx.fail(str(e))

for asset in assets:
log.info("Uploading asset %s", asset)
try:
Expand Down

0 comments on commit 94a1311

Please sign in to comment.