Changelog Templates#
Warning
If you have an existing changelog in the location you have configured with the changelog_file setting, or if you have a template inside your template directory which will render to the location of an existing file, Python Semantic Release will overwrite the contents of this file.
Please make sure to refer to Migrating an Existing Changelog.
Python Semantic Release can write a changelog for your project. By default, it uses an in-built template; once rendered this will be written to the location you configure with the changelog_file setting.
However, Python Semantic Release is also capable of rendering an entire directory tree of templates during the changelog generation process. This directory is specified using the template directory setting.
Python Semantic Release uses Jinja as its template engine, so you should refer to the Template Designer Documentation for guidance on how to customize the appearance of the files which are rendered during the release process. If you would like to customize the template environment itself, then certain options are available to you via changelog environment configuration.
Changelogs are rendered during the semantic-release version and semantic-release changelog commands. You can disable changelog generation entirely during the semantic-release version command by providing the –no-changelog command-line option.
The changelog template is re-rendered on each release.
Template Rendering#
Directory Structure:#
If you don’t want to set up your own custom changelog template, you can have Python
Semantic Release use its in-built template. If you would like to customize the
appearance of the changelog, or to render additional files, then you will need to
create a directory within your repository and set the template_dir
setting to the name of this directory. The default name is "templates"
.
Note
It is strongly recommended that you use a dedicated top-level folder for the template directory.
When the templates are rendered, files within the tree are output to the location within your repository that has the same relative path to the root as the relative path of the template to the templates directory.
Templates are identified by giving a .j2
extension to the template file. Any such
templates have the .j2
extension removed from the target file. Therefore, to render
an output file foo.csv
, you should create a template called foo.csv.j2
within
your template directory.
Note
A file within your template directory which does not end in .j2
will not
be treated as a template; it will be copied to its target location without being
rendered by the template engine.
Files within the template directory are excluded from the rendering process if the
file begins with a "."
or if any of the folders containing this file begin with
a "."
.
Directory Structure (Example)#
Suppose a project sets template_dir to
"templates"
and has the following structure:
example-project
├── src
│ └── example_project
│ └── __init__.py
└── templates
├── CHANGELOG.md.j2
├── .components
│ └── authors.md.j2
├── .macros.j2
├── src
│ └── example_project
│ └── data
│ └── data.json.j2
└── static
└── config.cfg
After running a release with Python Semantic Release, the directory structure of the project will now look like this:
example-project
├── CHANGELOG.md
├── src
│ └── example_project
│ ├── data
│ │ └── data.json
│ └── __init__.py
├── static
│ └── config.cfg
└── templates
├── CHANGELOG.md.j2
├── .components
│ └── authors.md.j2
├── .macros.j2
├── src
│ └── example_project
│ └── data
│ └── data.json.j2
└── static
└── config.cfg
Note that:
There is no top-level
.macros
file created, because this file is excluded from the rendering process.There is no top-level
.components
directory created, because this folder and all files and folders contained within it are excluded from the rendering process.To render data files into the
src/
folder, the path to which the template should be rendered has to be created within thetemplates
directory.The
templates/static
folder is created at the top-level of the project, and the filetemplates/static/config.cfg
is copied, not rendered to the new top-levelstatic
folder.
You may wish to leverage this behaviour to modularise your changelog template, to define macros in a separate file, or to reference static data which you would like to avoid duplicating between your template environment and the remainder of your project.
Template Context:#
Alongside the rendering of a directory tree, Python Semantic Release makes information about the history of the project available within the templating environment in order for it to be used to generate Changelogs and other such documents.
The history of the project is made available via the global variable context
. In
Python terms, context
is a dataclass with the following attributes:
repo_name: str
: the name of the current repository parsed from the Git url.repo_owner: str
: the owner of the current repository parsed from the Git url.history: ReleaseHistory
: asemantic_release.changelog.ReleaseHistory
instance. (See ReleaseHistory)filters: Tuple[Callable[..., Any], ...]
: a tuple of filters for the template environment. These are added to the environment’sfilters
, and therefore there should be no need to access these from thecontext
object inside the template.
Currently, two filters are defined:
pull_request_url: Callable[[str], str]
: given a pull request number, return a URL to the pull request in the remote.commit_hash_url: Callable[[str], str]
: given a commit hash, return a URL to the commit in the remote.
See also
ReleaseHistory
#
A ReleaseHistory
instance has two attributes: released
and unreleased
.
The unreleased
attribute is of type Dict[str, List[ParseResult]]
. Each commit
in the current branch’s commit history since the last release on this branch is grouped
by the type
attribute of the ParsedCommit
returned by the commit parser,
or if the parser returned a ParseError
then the result is grouped under the
"unknown"
key.
For this reason, every element of ReleaseHistory.unreleased["unknown"]
is a
ParseError
, and every element of every other value in ReleaseHistory.unreleased
is of type ParsedCommit
.
Typically, commit types will be "feature"
, "fix"
, "breaking"
, though the
specific types are determined by the parser. For example, the
semantic_release.commit_parser.EmojiCommitParser
uses a textual
representation of the emoji corresponding to the most significant change introduced
in a commit (e.g. ":boom:"
) as the different commit types. As a template author,
you are free to customise how these are presented in the rendered template.
Note
If you are using a custom commit parser following the guide at
Writing your own parser, your custom implementations of
semantic_release.ParseResult
, semantic_release.ParseError
and semantic_release.ParsedCommit
will be used in place of the built-in
types.
The released
attribute is of type Dict[Version, Release]
. The keys of this
dictionary correspond to each version released within this branch’s history, and
are of type semantic_release.Version
. You can use the as_tag()
method to
render these as the Git tag that they correspond to inside your template.
A Release
object has an elements
attribute, which has the same
structure as the unreleased
attribute of a ReleaseHistory
; that is,
elements
is of type Dict[str, List[ParseResult]]
, where every element
of elements["unknown"]
is a ParseError
, and elements of every other
value correspond to the type
attribute of the ParsedCommit
returned
by the commit parser.
The commits represented within each ReleaseHistory.released[version].elements
grouping are the commits which were made between version
and the release
corresponding to the previous version.
That is, given two releases Version(1, 0, 0)
and Version(1, 1, 0)
,
ReleaseHistory.released[Version(1, 0, 0)].elements
contains only commits
made after the release of Version(1, 0, 0)
up to and including the release
of Version(1, 1, 0)
.
To maintain a consistent order of subsections in the changelog headed by the commit type, it’s recommended to use Jinja’s dictsort filter.
Each Release
object also has the following attributes:
tagger: git.Actor
: The tagger who tagged the release.committer: git.Actor
: The committer who made the release commit.tagged_date: datetime
: The date and time at which the release was tagged.
Customizing VCS Release Notes#
The same template rendering mechanism generates the release notes when creating VCS releases:
the in-built template is used by default
create a file named
.release_notes.md.j2
inside the project’s template_dir to customize the release notes
Release Notes Context#
All of the changelog’s template context is exposed to the Jinja template when rendering the release notes.
Additionally, the following two globals are available to the template:
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):
## 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 %}
Changelog Template Example#
Below is an example template that can be used to render a Changelog:
# CHANGELOG
{% if context.history.unreleased | length > 0 %}
{# UNRELEASED #}
## Unreleased
{% for type_, commits in context.history.unreleased | dictsort %}
### {{ type_ | capitalize }}
{% for commit in commits %}{% if type_ != "unknown" %}
* {{ commit.commit.message.rstrip() }} ([`{{ commit.commit.hexsha[:7] }}`]({{ commit.commit.hexsha | commit_hash_url }}))
{% else %}
* {{ commit.commit.message.rstrip() }} ([`{{ commit.commit.hexsha[:7] }}`]({{ commit.commit.hexsha | commit_hash_url }}))
{% endif %}{% endfor %}{% endfor %}
{% endif %}
{# RELEASED #}
{% for version, release in context.history.released.items() %}
## {{ version.as_tag() }} ({{ release.tagged_date.strftime("%Y-%m-%d") }})
{% for type_, commits in release["elements"] | dictsort %}
### {{ type_ | capitalize }}
{% for commit in commits %}{% if type_ != "unknown" %}
* {{ commit.commit.message.rstrip() }} ([`{{ commit.commit.hexsha[:7] }}`]({{ commit.commit.hexsha | commit_hash_url }}))
{% else %}
* {{ commit.commit.message.rstrip() }} ([`{{ commit.commit.hexsha[:7] }}`]({{ commit.commit.hexsha | commit_hash_url }}))
{% endif %}{% endfor %}{% endfor %}{% endfor %}
Migrating an Existing Changelog#
If you have an existing changelog that you would like to preserve, it’s recommended
that you add the contents of this file to your changelog template - either directly
or via Jinja’s include
tag. If you would like only the history from your next release onwards to be rendered
into the changelog in addition to the existing changelog, you can add an if statement based upon the versions in
the keys of context.released
.