Skip to content

Commit 5890790

Browse files
authored
docs(pypi): backlink to PyPI from README, docs, CHANGELOG, and future releases (#27)
- README badge row gains a PyPI version badge; install section text-links the package name to /project/yarlpattern/. Same in docs/index.md (a cross-domain backlink: chad-loder.github.io → pypi.org). - CHANGELOG header points at /project/yarlpattern/#history. - Custom PSR Jinja template under .semantic-release/templates/ emits a versioned PyPI deep-link on every new release heading: `## vX.Y.Z (date) — [Release](…) · [PyPI](…)`. Uses PSR's built-in `create_release_url` / `create_pypi_url` filters so the same template works across HVCS backends. - Only file deviating from PSR's bundled conventional/md/ templates is .components/versioned_changes.md.j2; the other eight are vendored verbatim because PSR uses templates/ as a binary switch (no merge with defaults). The heading format keeps the `# vX.Y.Z ` substring that PSR's update-mode dedup check requires, so CI retries are idempotent (verified against real git history). - Update mode preserves existing 0.1.0/0.2.0 entries verbatim; only releases cut from now on use the new format. Old end-of-file PSR stamp marker replaced with `<!-- version list -->` near the top, matching the v10 default insertion flag. Co-authored-by: chad-loder <26261238+chad-loder@users.noreply.github.com>
1 parent 5df9846 commit 5890790

14 files changed

Lines changed: 595 additions & 2 deletions
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# CHANGELOG
2+
3+
{% if ctx.changelog_mode == "update"
4+
%}{# # IMPORTANT: add insertion flag for next version update
5+
#}{{
6+
insertion_flag ~ "\n"
7+
8+
}}{% endif
9+
%}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{#
2+
This changelog template initializes a full changelog for the project,
3+
it follows the following logic:
4+
1. Header
5+
2. Any Unreleased Details (uncommon)
6+
3. all previous releases except the very first release
7+
4. the first release
8+
9+
#}{#
10+
# Header
11+
#}{% include "changelog_header.md.j2"
12+
-%}{#
13+
# Any Unreleased Details (uncommon)
14+
#}{% include "unreleased_changes.md.j2"
15+
-%}{#
16+
# Since this is initialization, we are generating all the previous
17+
# release notes per version. The very first release notes is specialized
18+
#}{% if releases | length > 0
19+
%}{% for release in releases
20+
%}{{ "\n"
21+
}}{% if loop.last and ctx.mask_initial_release
22+
%}{%- include "first_release.md.j2"
23+
-%}{% else
24+
%}{%- include "versioned_changes.md.j2"
25+
-%}{% endif
26+
%}{{ "\n"
27+
}}{% endfor
28+
%}{% endif
29+
%}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
{#
2+
This Update changelog template uses the following logic:
3+
4+
1. Read previous changelog file (ex. project_root/CHANGELOG.md)
5+
2. Split on insertion flag (ex. <!-- version list -->)
6+
3. Print top half of previous changelog
7+
3. New Changes (unreleased commits & newly released)
8+
4. Print bottom half of previous changelog
9+
10+
Note: if a previous file was not found, it does not write anything at the bottom
11+
but render does NOT fail
12+
13+
#}{% set prev_changelog_contents = prev_changelog_file | read_file | safe
14+
%}{% set changelog_parts = prev_changelog_contents.split(insertion_flag, maxsplit=1)
15+
%}{#
16+
#}{% if changelog_parts | length < 2
17+
%}{# # insertion flag was not found, check if the file was empty or did not exist
18+
#}{% if prev_changelog_contents | length > 0
19+
%}{# # File has content but no insertion flag, therefore, file will not be updated
20+
#}{{ changelog_parts[0]
21+
}}{% else
22+
%}{# # File was empty or did not exist, therefore, it will be created from scratch
23+
#}{% include "changelog_init.md.j2"
24+
%}{% endif
25+
%}{% else
26+
%}{#
27+
# Previous Changelog Header
28+
# - Depending if there is header content, then it will separate the insertion flag
29+
# with a newline from header content, otherwise it will just print the insertion flag
30+
#}{% set prev_changelog_top = changelog_parts[0] | trim
31+
%}{% if prev_changelog_top | length > 0
32+
%}{{
33+
"%s\n\n%s\n" | format(prev_changelog_top, insertion_flag | trim)
34+
35+
}}{% else
36+
%}{{
37+
"%s\n" | format(insertion_flag | trim)
38+
39+
}}{% endif
40+
%}{#
41+
# Any Unreleased Details (uncommon)
42+
#}{% include "unreleased_changes.md.j2"
43+
-%}{#
44+
#}{% if releases | length > 0
45+
%}{# # Latest Release Details
46+
#}{% set release = releases[0]
47+
%}{#
48+
#}{% if releases | length == 1 and ctx.mask_initial_release
49+
%}{# # First Release detected
50+
#}{{ "\n"
51+
}}{%- include "first_release.md.j2"
52+
-%}{{ "\n"
53+
}}{#
54+
#}{% elif "# " ~ release.version.as_semver_tag() ~ " " not in changelog_parts[1]
55+
%}{# # The release version is not already in the changelog so we add it
56+
#}{{ "\n"
57+
}}{%- include "versioned_changes.md.j2"
58+
-%}{{ "\n"
59+
}}{#
60+
#}{% endif
61+
%}{% endif
62+
%}{#
63+
# Previous Changelog Footer
64+
# - skips printing footer if empty, which happens when the insertion_flag
65+
# was at the end of the file (ignoring whitespace)
66+
#}{% set previous_changelog_bottom = changelog_parts[1] | trim
67+
%}{% if previous_changelog_bottom | length > 0
68+
%}{{ "\n%s\n" | format(previous_changelog_bottom)
69+
}}{% endif
70+
%}{% endif
71+
%}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
{% from 'macros.md.j2' import apply_alphabetical_ordering_by_brk_descriptions
2+
%}{% from 'macros.md.j2' import apply_alphabetical_ordering_by_descriptions
3+
%}{% from 'macros.md.j2' import apply_alphabetical_ordering_by_release_notices
4+
%}{% from 'macros.md.j2' import format_breaking_changes_description, format_commit_summary_line
5+
%}{% from 'macros.md.j2' import format_release_notice
6+
%}{#
7+
EXAMPLE:
8+
9+
### Features
10+
11+
- Add new feature ([#10](https://domain.com/namespace/repo/pull/10),
12+
[`abcdef0`](https://domain.com/namespace/repo/commit/HASH))
13+
14+
- **scope**: Add new feature ([`abcdef0`](https://domain.com/namespace/repo/commit/HASH))
15+
16+
### Bug Fixes
17+
18+
- Fix bug ([#11](https://domain.com/namespace/repo/pull/11),
19+
[`abcdef1`](https://domain.com/namespace/repo/commit/HASH))
20+
21+
### Breaking Changes
22+
23+
- With the change _____, the change causes ___ effect. Ultimately, this section
24+
it is a more detailed description of the breaking change. With an optional
25+
scope prefix like the commit messages above.
26+
27+
- **scope**: this breaking change has a scope to identify the part of the code that
28+
this breaking change applies to for better context.
29+
30+
### Additional Release Information
31+
32+
- This is a release note that provides additional information about the release
33+
that is not a breaking change or a feature/bug fix.
34+
35+
- **scope**: this release note has a scope to identify the part of the code that
36+
this release note applies to for better context.
37+
38+
#}{% set max_line_width = max_line_width | default(100)
39+
%}{% set hanging_indent = hanging_indent | default(2)
40+
%}{#
41+
#}{% for type_, commits in commit_objects if type_ != "unknown"
42+
%}{# PREPROCESS COMMITS (order by description & format description line)
43+
#}{% set ns = namespace(commits=commits)
44+
%}{% set _ = apply_alphabetical_ordering_by_descriptions(ns)
45+
%}{#
46+
#}{% set commit_descriptions = []
47+
%}{#
48+
#}{% for commit in ns.commits
49+
%}{# # Add reference links to the commit summary line
50+
#}{% set description = "- %s" | format(format_commit_summary_line(commit))
51+
%}{% set description = description | autofit_text_width(max_line_width, hanging_indent)
52+
%}{% set _ = commit_descriptions.append(description)
53+
%}{% endfor
54+
%}{#
55+
# # PRINT SECTION (header & commits)
56+
#}{% if commit_descriptions | length > 0
57+
%}{{ "\n"
58+
}}{{ "### %s\n" | format(type_ | title)
59+
}}{{ "\n"
60+
}}{{ "%s\n" | format(commit_descriptions | unique | join("\n\n"))
61+
}}{% endif
62+
%}{% endfor
63+
%}{#
64+
# Determine if there are any breaking change commits by filtering the list by breaking descriptions
65+
# commit_objects is a list of tuples [("Features", [ParsedCommit(), ...]), ("Bug Fixes", [ParsedCommit(), ...])]
66+
# HOW: Filter out breaking change commits that have no breaking descriptions
67+
# 1. Re-map the list to only the list of commits under the breaking category from the list of tuples
68+
# 2. Peel off the outer list to get a list of ParsedCommit objects
69+
# 3. Filter the list of ParsedCommits to only those with a breaking description
70+
#}{% set breaking_commits = commit_objects | map(attribute="1.0")
71+
%}{% set breaking_commits = breaking_commits | rejectattr("error", "defined") | selectattr("breaking_descriptions.0") | list
72+
%}{#
73+
#}{% if breaking_commits | length > 0
74+
%}{# PREPROCESS COMMITS
75+
#}{% set brk_ns = namespace(commits=breaking_commits)
76+
%}{% set _ = apply_alphabetical_ordering_by_brk_descriptions(brk_ns)
77+
%}{#
78+
#}{% set brking_descriptions = []
79+
%}{#
80+
#}{% for commit in brk_ns.commits
81+
%}{% set full_description = "- %s" | format(
82+
format_breaking_changes_description(commit).split("\n\n") | join("\n\n- ")
83+
)
84+
%}{% set _ = brking_descriptions.append(
85+
full_description | autofit_text_width(max_line_width, hanging_indent)
86+
)
87+
%}{% endfor
88+
%}{#
89+
# # PRINT BREAKING CHANGE DESCRIPTIONS (header & descriptions)
90+
#}{{ "\n"
91+
}}{{ "### Breaking Changes\n"
92+
}}{{
93+
"\n%s\n" | format(brking_descriptions | unique | join("\n\n"))
94+
}}{#
95+
#}{% endif
96+
%}{#
97+
# Determine if there are any commits with release notice information by filtering the list by release_notices
98+
# commit_objects is a list of tuples [("Features", [ParsedCommit(), ...]), ("Bug Fixes", [ParsedCommit(), ...])]
99+
# HOW: Filter out commits that have no release notices
100+
# 1. Re-map the list to only the list of commits from the list of tuples
101+
# 2. Peel off the outer list to get a list of ParsedCommit objects
102+
# 3. Filter the list of ParsedCommits to only those with a release notice
103+
#}{% set notice_commits = commit_objects | map(attribute="1.0")
104+
%}{% set notice_commits = notice_commits | rejectattr("error", "defined") | selectattr("release_notices.0") | list
105+
%}{#
106+
#}{% if notice_commits | length > 0
107+
%}{# PREPROCESS COMMITS
108+
#}{% set notice_ns = namespace(commits=notice_commits)
109+
%}{% set _ = apply_alphabetical_ordering_by_release_notices(notice_ns)
110+
%}{#
111+
#}{% set release_notices = []
112+
%}{#
113+
#}{% for commit in notice_ns.commits
114+
%}{% set full_description = "- %s" | format(
115+
format_release_notice(commit).split("\n\n") | join("\n\n- ")
116+
)
117+
%}{% set _ = release_notices.append(
118+
full_description | autofit_text_width(max_line_width, hanging_indent)
119+
)
120+
%}{% endfor
121+
%}{#
122+
# # PRINT RELEASE NOTICE INFORMATION (header & descriptions)
123+
#}{{ "\n"
124+
}}{{ "### Additional Release Information\n"
125+
}}{{
126+
"\n%s\n" | format(release_notices | unique | join("\n\n"))
127+
}}{#
128+
#}{% endif
129+
%}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{# EXAMPLE:
2+
3+
## vX.X.X (YYYY-MMM-DD)
4+
5+
_This release is published under the MIT License._ # Release Notes Only
6+
7+
- Initial Release
8+
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+
- Initial Release

0 commit comments

Comments
 (0)