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 the templates directory.

  • The templates/static folder is created at the top-level of the project, and the file templates/static/config.cfg is copied, not rendered to the new top-level static 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.

  • hvcs_type: str: the name of the VCS server type currently configured.

  • history: ReleaseHistory: a semantic_release.changelog.ReleaseHistory instance. (See ReleaseHistory)

  • filters: Tuple[Callable[..., Any], ...]: a tuple of filters for the template environment. These are added to the environment’s filters, and therefore there should be no need to access these from the context object inside the template.

The filters provided vary based on the VCS configured and available features:

  • create_server_url: Callable[[str, str | None, str | None, str | None], str]: when given a path, prepend the configured vcs server host and url scheme. Optionally you can provide, a auth string, a query string or a url fragment to be normalized into the resulting url. Parameter order is as described above respectively.

  • create_repo_url: Callable[[str, str | None, str | None], str]: when given a repository path, prepend the configured vcs server host, and repo namespace. Optionally you can provide, an additional query string and/or a url fragment to also put in the url. Parameter order is as described above respectively. This is similar to create_server_url but includes the repo namespace and owner automatically.

  • commit_hash_url: Callable[[str], str]: given a commit hash, return a URL to the commit in the remote.

  • compare_url: Callable[[str, str], str]: given a starting git reference and a ending git reference create a comparison url between the two references that can be opened on the remote

  • issue_url: Callable[[str | int], str]: given an issue number, return a URL to the issue on the remote vcs.

  • merge_request_url: Callable[[str | int], str]: given a merge request number, return a URL to the merge request in the remote. This is an alias to the pull_request_url but only available for the VCS that uses the merge request terminology.

  • pull_request_url: Callable[[str | int], str]: given a pull request number, return a URL to the pull request in the remote. For remote vcs’ that use merge request terminology, this filter is an alias to the merge_request_url filter function.

Availability of the documented filters can be found in the table below:

filter - hvcs_type

bitbucket

gitea

github

gitlab

create_server_url

create_repo_url

commit_hash_url

compare_url

issue_url

merge_request_url

pull_request_url

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 (Release): contains metadata about the content of the release, as parsed from commit logs

  • version (Version): contains metadata about the software version to be released and its git tag

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.