Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: upgrade psr configuration #812

6 changes: 3 additions & 3 deletions project/.github/workflows/ci.yml.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -116,14 +116,14 @@ jobs:

# Do a dry run of PSR
- name: Test release
uses: python-semantic-release/python-semantic-release@v9.8.1
uses: python-semantic-release/python-semantic-release@v9.12.0
if: github.ref_name != 'main'
with:
root_options: --noop

# On main branch: actual PSR + upload to PyPI & GitHub
- name: Release
uses: python-semantic-release/python-semantic-release@v9.8.1
uses: python-semantic-release/python-semantic-release@v9.12.0
id: release
if: github.ref_name == 'main'
with:
Expand All @@ -140,7 +140,7 @@ jobs:
if: steps.release.outputs.released == 'true'

- name: Publish package distributions to GitHub Releases
uses: python-semantic-release/publish-action@v9.8.1
uses: python-semantic-release/publish-action@v9.12.0
if: steps.release.outputs.released == 'true'
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
Expand Down
14 changes: 11 additions & 3 deletions project/pyproject.toml.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -171,10 +171,18 @@ version_variables = [
build_command = "pip install poetry && poetry build"

[tool.semantic_release.changelog]
insertion_flag = "# CHANGELOG"
mode = "update"
template_dir = "templates"
exclude_commit_patterns = [
"chore.*",
"ci.*",
"Merge pull request .*",
'''chore(?:\([^)]*?\))?: .+''',
'''ci(?:\([^)]*?\))?: .+''',
'''refactor(?:\([^)]*?\))?: .+''',
'''style(?:\([^)]*?\))?: .+''',
'''test(?:\([^)]*?\))?: .+''',
'''build\((?!deps\): .+)''',
'''Merged? .*''',
'''Initial [Cc]ommit.*''', # codespell:ignore
]

[tool.semantic_release.changelog.environment]
Expand Down
5 changes: 5 additions & 0 deletions project/templates/.components/changelog_header.md.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{# Insertion flag is the Changelog header so we keep this clear
#}{% if ctx.changelog_mode == "update"
%}{{ insertion_flag ~ "\n"
}}{% endif
%}
33 changes: 33 additions & 0 deletions project/templates/.components/changelog_init.md.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{#
This changelog template initializes a full changelog for the project,
it follows the following logic:
1. Header
2. Any Unreleased Details (uncommon)
3. all previous releases except the very first release
4. the first release

#}{#
# Header
#}{% include "changelog_header.md.j2"
-%}{#
# Any Unreleased Details (uncommon)
#}{% include "unreleased_changes.md.j2"
-%}{#
# Since this is initialization, we are generating all the previous
# release notes per version. The very first release notes is specialized
#}{% if releases | length - 1 > 0
%}{% for release in releases[:-1]
%}{{ "\n"
}}{% include "versioned_changes.md.j2"
-%}{{ "\n"
}}{% endfor
%}{% endif
%}{#
# First Release
#}{% if releases | length > 0
%}{% set release = releases[-1]
%}{{ "\n"
}}{% include "first_release.md.j2"
-%}{{ "\n"
}}{% endif
%}
69 changes: 69 additions & 0 deletions project/templates/.components/changelog_update.md.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
{#
This Update changelog template uses the following logic:

1. Read previous changelog file (ex. project_root/CHANGELOG.md)
2. Split on insertion flag (ex. <!-- version list -->)
3. Print top half of previous changelog
3. New Changes (unreleased commits & newly released)
4. Print bottom half of previous changelog

Note: if a previous file was not found, it does not write anything at the bottom
but render does NOT fail

#}{% set prev_changelog_contents = prev_changelog_file | read_file | safe
%}{% set changelog_parts = prev_changelog_contents.split(insertion_flag, maxsplit=1)
%}{#
#}{% if changelog_parts | length < 2
%}{# # insertion flag was not found, check if the file was empty or did not exist
#}{% if prev_changelog_contents | length > 0
%}{# # File has content but no insertion flag, therefore, file will not be updated
#}{{ changelog_parts[0]
}}{% else
%}{# # File was empty or did not exist, therefore, it will be created from scratch
#}{% include "changelog_init.md.j2"
%}{% endif
%}{% else
%}{#
# Previous Changelog Header
# - Depending if there is header content, then it will separate the insertion flag
# with a newline from header content, otherwise it will just print the insertion flag
#}{% set prev_changelog_top = changelog_parts[0] | trim
%}{% if prev_changelog_top | length > 0
%}{{
"%s\n\n%s\n" | format(prev_changelog_top, insertion_flag | trim)

}}{% else
%}{{
"%s\n" | format(insertion_flag | trim)

}}{% endif
%}{#
# Any Unreleased Details (uncommon)
#}{% include "unreleased_changes.md.j2"
-%}{#
#}{% if releases | length == 1
%}{# # First Release detected
#}{{ "\n"
}}{% include "first_release.md.j2"
%}{{ "\n"
}}{% else
%}{# # Latest Release Details
#}{% for release in new_releases
%}{# # Check if the release version is already in the changelog and if not, add it
#}{% if "# " ~ release.version.as_semver_tag() ~ " " not in changelog_parts[1]
%}{{ "\n"
}}{%- include "versioned_changes.md.j2"
-%}{{ "\n"
}}{% endif
%}{% endfor
%}{% endif
%}{#
# Previous Changelog Footer
# - skips printing footer if empty, which happens when the insertion_flag
# was at the end of the file (ignoring whitespace)
#}{% set previous_changelog_bottom = changelog_parts[1] | trim
%}{% if previous_changelog_bottom | length > 0
%}{{ "\n%s\n" | format(previous_changelog_bottom)
}}{% endif
%}{% endif
%}
72 changes: 72 additions & 0 deletions project/templates/.components/changes.md.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
{% from 'macros.md.j2' import apply_alphabetical_ordering_by_descriptions
%}{% from 'macros.md.j2' import apply_alphabetical_ordering_by_brk_descriptions
%}{% from 'macros.md.j2' import format_breaking_changes_description
%}{% from 'macros.md.j2' import format_commit_summary_line
%}{#
EXAMPLE:

### Features

- Add new feature ([#10](https://domain.com/namespace/repo/pull/10), [`abcdef0`](https://domain.com/namespace/repo/commit/HASH))

- **scope**: Add new feature ([`abcdef0`](https://domain.com/namespace/repo/commit/HASH))

### Fixes

- Fix bug (#11, [`abcdef1`](https://domain.com/namespace/repo/commit/HASH))

### BREAKING CHANGES

- With the change _____, the change causes ___ effect. Ultimately, this section
it is a more detailed description of the breaking change. With an optional
scope prefix like the commit messages above.

- **scope**: this breaking change has a scope to identify the part of the code that
this breaking change applies to for better context.

#}{% set max_line_width = 100
%}{% set hanging_indent = 2
%}{#
#}{% for type_, commits in commit_objects if type_ != "unknown"
%}{{
"\n### %s\n" | format(type_ | title)

}}{% set ns = namespace(commits=commits)
%}{{ apply_alphabetical_ordering_by_descriptions(ns) | default("", true)
}}{#
#}{% for commit in ns.commits
%}{% set commit_line = "- %s" | format(format_commit_summary_line(commit, hvcs_type))
%}{{ "\n%s\n" | format(
commit_line | autofit_text_width(max_line_width, hanging_indent)
)
}}{% endfor
%}{% endfor
%}{#
# # Determine if there are any breaking change commits by seeing if the type exists
# # commit_objects is a list of tuples [("breaking", [ParsedCommit(), ...]), ("Features", [ParsedCommit(), ...])]
#}{% if "breaking" in commit_objects | map(attribute="0") | list
%}{# # Filter out breaking change commits that have no breaking descriptions
# 1. first filter the list for only tuples of type breaking
# 2. Re-map the list to only the list of commits under the breaking category from the list of tuples
# 3. Peel off the outer list to get a list of ParsedCommit objects
# 4. Filter the list of ParsedCommits to only those with a breaking description
#}{% set breaking_commits = commit_objects | selectattr("0", "equalto", "breaking") | map(attribute="1.0")
%}{% set breaking_commits = breaking_commits | selectattr("breaking_descriptions.0") | list
%}{% if breaking_commits | length > 0
%}{{
"\n### BREAKING CHANGES\n"
}}{#
#}{% set brk_ns = namespace(commits=breaking_commits)
%}{{ apply_alphabetical_ordering_by_brk_descriptions(brk_ns) | default("", true)
}}{#
#}{% for commit in brk_ns.commits
%}{% set full_description = "- %s" | format(
format_breaking_changes_description(commit).split("\n\n") | join("\n\n- ")
)
%}{{ "\n%s\n" | format(
full_description | autofit_text_width(max_line_width, hanging_indent)
)
}}{% endfor
%}{% endif
%}{% endif
%}
14 changes: 14 additions & 0 deletions project/templates/.components/first_release.md.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{#

## vX.X.X (YYYY-MMM-DD)

#}{{

"## %s (%s)" | format(
release.version.as_semver_tag(),
release.tagged_date.strftime("%Y-%m-%d")
)

}}

- Initial Release
153 changes: 153 additions & 0 deletions project/templates/.components/macros.md.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
{#
MACRO: format the breaking changes description by:
- Capitalizing the description
- Adding an optional scope prefix
#}{% macro format_breaking_changes_description(commit)
%}{% set ns = namespace(full_description="")
%}{#
#}{% if commit.error is undefined
%}{% for paragraph in commit.breaking_descriptions
%}{% if paragraph | trim | length > 0
%}{% set paragraph_text = [
paragraph.split(" ", maxsplit=1)[0] | capitalize,
paragraph.split(" ", maxsplit=1)[1]
] | join(" ") | trim | safe
%}{% set ns.full_description = [ns.full_description, paragraph_text] | join("\n\n")
%}{% endif
%}{% endfor
%}{#
#}{% set ns.full_description = ns.full_description | trim
%}{#
#}{% if commit.scope
%}{% set ns.full_description = "**%s**: %s" | format(commit.scope, ns.full_description)
%}{% endif
%}{% endif
%}{#
#}{{ ns.full_description
}}{% endmacro
%}


{#
MACRO: apply smart ordering of commits objects based on alphabetized breaking changes and then scopes
- Commits are sorted based on the commit type and the commit message
- Commits are grouped by the commit type
- parameter: ns (namespace) object with a commits list
- returns None but modifies the ns.commits list in place
#}{% macro apply_alphabetical_ordering_by_brk_descriptions(ns)
%}{% set ordered_commits = []
%}{#
# # Eliminate any ParseError commits from input set
#}{% set filtered_commits = ns.commits | rejectattr("error", "defined") | list
%}{#
# # grab all commits with no scope and sort alphabetically by the first line of the commit message
#}{% for commit in filtered_commits | rejectattr("scope") | sort(attribute='breaking_descriptions.0')
%}{{ ordered_commits.append(commit) | default("", true)
}}{% endfor
%}{#
# # grab all commits with a scope and sort alphabetically by the scope and then the first line of the commit message
#}{% for commit in filtered_commits | selectattr("scope") | sort(attribute='scope,breaking_descriptions.0')
%}{{ ordered_commits.append(commit) | default("", true)
}}{% endfor
%}{#
# # Return the ordered commits
#}{% set ns.commits = ordered_commits
%}{% endmacro
%}


{#
MACRO: Retrieve the PR identifier based on the HVCS type
#}{% macro get_pr_identifier(hvcs_type)
%}{# # Determine the PR identifier based on the HVCS type
#}{% if hvcs_type == "gitlab"
%}{% set pr_identifier = "!"
%}{% elif hvcs_type in ["bitbucket", "gitea", "github"]
%}{% set pr_identifier = "#"
%}{% else
%}{% set pr_identifier = ""
%}{% endif
%}{{ pr_identifier | default("", true)
}}{% endmacro
%}


{#
MACRO: commit message links or PR/MR links of commit
#}{% macro commit_msg_links(commit, hvcs_type)
%}{% if commit.error is undefined
%}{% set commit_hash_link = "[`%s`](%s)" | format(
commit.short_hash, commit.hexsha | commit_hash_url
)
%}{#
#}{% set summary_line = commit.descriptions[0] | safe
%}{% set summary_line = [
summary_line.split(" ", maxsplit=1)[0] | capitalize,
summary_line.split(" ", maxsplit=1)[1]
] | join(" ")
%}{#
# # Determine the PR identifier based on the HVCS type
#}{% set pr_identifier = get_pr_identifier(hvcs_type) | default("", true)
%}{#
#}{% if pr_identifier != "" and summary_line.split("(" ~ pr_identifier) | length > 1
%}{# # Replace PR references with a link to the PR
#}{% set pr_num = summary_line.split("(" ~ pr_identifier, maxsplit=1)[1].split(")")[0]
%}{% set pr_link = "[%s](%s)" | format(pr_identifier ~ pr_num, pr_num | pull_request_url)
%}{% set replacement = "(%s, %s)" | format(pr_link, commit_hash_link)
%}{% set search_text = "(%s)" | format(pr_identifier ~ pr_num)
%}{% set summary_line = summary_line | replace(search_text, replacement)
%}{#
# DEFAULT: No PR identifier found, so just append commit hash as url to the commit summary_line
#}{% else
%}{% set summary_line = "%s (%s)" | format(summary_line, commit_hash_link)
%}{% endif
%}{#
# Return the modified summary_line
#}{{ summary_line
}}{% endif
%}{% endmacro
%}


{#
MACRO: format the first line of the commit message with optional scope
- PR/MR links are added if found in the first line description
- commit hash links are always added
#}{% macro format_commit_summary_line(commit, hvcs_type)
%}{% if commit.error is undefined
%}{% set summary_line = commit_msg_links(commit, hvcs_type)
%}{% if commit.scope
%}{% set summary_line = "**%s**: %s" | format(commit.scope, summary_line)
%}{% endif
%}{{ summary_line
}}{% endif
%}{% endmacro
%}


{#
MACRO: apply smart ordering of commits objects based on alphabetized summaries and then scopes
- Commits are sorted based on the commit type and the commit message
- Commits are grouped by the commit type
- parameter: ns (namespace) object with a commits list
- returns None but modifies the ns.commits list in place
#}{% macro apply_alphabetical_ordering_by_descriptions(ns)
%}{% set ordered_commits = []
%}{#
# # Eliminate any ParseError commits from input set
#}{% set filtered_commits = ns.commits | rejectattr("error", "defined") | list
%}{#
# # grab all commits with no scope and sort alphabetically by the first line of the commit message
#}{% for commit in filtered_commits | rejectattr("scope") | sort(attribute='descriptions.0')
%}{{ ordered_commits.append(commit) | default("", true)
}}{% endfor
%}{#
# # grab all commits with a scope and sort alphabetically by the scope and then the first line of the commit message
#}{% for commit in filtered_commits | selectattr("scope") | sort(attribute='scope,descriptions.0')
%}{{ ordered_commits.append(commit) | default("", true)
}}{% endfor
%}{#
# # Return the ordered commits
#}{% set ns.commits = ordered_commits
%}{% endmacro
%}
Loading