Skip to content

Commit d4c753e

Browse files
committed
👷 ci(release): wire up towncrier-driven release pipeline
The news fragments under docs/changelog/ were never consumed because the project shipped only the PyPI publish workflow without the towncrier config, template, rolled-up changelog, or release script that drives them. Tags were being cut by hand and the urls.Changelog link pointed at GitHub auto-generated notes, leaving the .rst fragments as dead weight. Port the full pipeline from tox-dev/tox so a release is a single workflow_dispatch click: prepare-release.yaml computes the next version (auto-detects major/minor/patch from breaking/feature fragment counts), then tox -e release runs tasks/release.py which calls towncrier build, commits the assembled docs/changelog.rst, tags, force-pushes main, and opens the GitHub release. The existing tag-push release.yaml handles PyPI publishing unchanged. docs/changelog.rst is backfilled for 1.0.0 through 1.3.0 from git history so the rendered changelog is a single source of truth rather than splitting history between Sphinx and GitHub Releases. The 65.feature.rst fragment is dropped because it already shipped in 1.3.0 via PR #71 and would otherwise double-count on the next release. The sphinxcontrib-towncrier extension renders unreleased fragments as a draft section in the docs.
1 parent 68f7dc1 commit d4c753e

9 files changed

Lines changed: 472 additions & 4 deletions

File tree

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
name: Prepare Release
2+
on:
3+
workflow_dispatch:
4+
inputs:
5+
bump:
6+
description: "Version bump type"
7+
required: true
8+
type: choice
9+
options:
10+
- auto
11+
- major
12+
- minor
13+
- patch
14+
default: auto
15+
16+
permissions:
17+
contents: read
18+
19+
env:
20+
FORCE_COLOR: 1
21+
22+
jobs:
23+
prepare-release:
24+
runs-on: ubuntu-24.04
25+
environment: release-auth
26+
permissions:
27+
contents: write
28+
steps:
29+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
30+
with:
31+
fetch-depth: 0
32+
token: ${{ secrets.RELEASE_PAT }}
33+
persist-credentials: true
34+
35+
- name: Install the latest version of uv
36+
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
37+
with:
38+
enable-cache: true
39+
cache-dependency-glob: "pyproject.toml"
40+
github-token: ${{ secrets.GITHUB_TOKEN }}
41+
42+
- name: Configure git
43+
run: |
44+
git config user.name "${GITHUB_ACTOR}"
45+
git config user.email "${GITHUB_ACTOR}@users.noreply.github.com"
46+
47+
- name: Calculate next version
48+
id: version
49+
env:
50+
BUMP: ${{ inputs.bump }}
51+
run: |
52+
current=$(git describe --tags --abbrev=0 2>/dev/null || echo "0.0.0")
53+
echo "Current version: $current"
54+
55+
IFS='.' read -r major minor patch <<< "$current"
56+
57+
bump_type="$BUMP"
58+
if [ "$bump_type" = "auto" ]; then
59+
feature_count=$(find docs/changelog -name "*.feature.rst" 2>/dev/null | wc -l)
60+
breaking_count=$(find docs/changelog -name "*.breaking.rst" 2>/dev/null | wc -l)
61+
if [ "$breaking_count" -gt 0 ]; then
62+
bump_type="major"
63+
echo "Auto-detected: major bump (found $breaking_count breaking changelog(s))"
64+
elif [ "$feature_count" -gt 0 ]; then
65+
bump_type="minor"
66+
echo "Auto-detected: minor bump (found $feature_count feature changelog(s))"
67+
else
68+
bump_type="patch"
69+
echo "Auto-detected: patch bump (no feature changelogs)"
70+
fi
71+
fi
72+
73+
case "$bump_type" in
74+
major)
75+
next="$((major + 1)).0.0"
76+
;;
77+
minor)
78+
next="$major.$((minor + 1)).0"
79+
;;
80+
patch)
81+
next="$major.$minor.$((patch + 1))"
82+
;;
83+
esac
84+
85+
echo "Next version: $next"
86+
echo "version=$next" >> $GITHUB_OUTPUT
87+
88+
- name: Install tox
89+
run: uv tool install --python-preference only-managed --python 3.14 tox@latest --with tox-uv
90+
91+
- name: Run release process
92+
run: tox run -e release -- ${STEPS_VERSION_OUTPUTS_VERSION}
93+
env:
94+
GH_TOKEN: ${{ secrets.RELEASE_PAT }}
95+
STEPS_VERSION_OUTPUTS_VERSION: ${{ steps.version.outputs.version }}
96+
97+
- name: Display completion message
98+
run: echo "Release ${STEPS_VERSION_OUTPUTS_VERSION} prepared and pushed successfully!"
99+
env:
100+
STEPS_VERSION_OUTPUTS_VERSION: ${{ steps.version.outputs.version }}

‎docs/changelog.rst‎

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
#################
2+
Release History
3+
#################
4+
5+
.. towncrier-draft-entries::
6+
7+
.. towncrier release notes start
8+
9+
********************
10+
v1.3.0 (2026-05-05)
11+
********************
12+
13+
Features - 1.3.0
14+
================
15+
16+
- Add :func:`~python_discovery.iter_interpreters` for enumerating every discovered interpreter, with PATH and
17+
UV-install support for non-CPython implementations listed in :data:`~python_discovery.KNOWN_IMPLEMENTATIONS`
18+
(:pull:`71`)
19+
20+
********************
21+
v1.2.2 (2026-04-06)
22+
********************
23+
24+
Features - 1.2.2
25+
================
26+
27+
- Export ``normalize_isa`` and deprecate ``KNOWN_ARCHITECTURES`` (:pull:`62`)
28+
29+
********************
30+
v1.2.1 (2026-03-26)
31+
********************
32+
33+
Features - 1.2.1
34+
================
35+
36+
- Expose ``KNOWN_ARCHITECTURES`` as public API (:pull:`56`)
37+
38+
Contributor-facing changes - 1.2.1
39+
==================================
40+
41+
- Add zizmor security auditing for workflows (:pull:`55`)
42+
43+
********************
44+
v1.2.0 (2026-03-18)
45+
********************
46+
47+
Features - 1.2.0
48+
================
49+
50+
- Increase interpreter query timeout to 15s, with an override (:pull:`53`)
51+
52+
********************
53+
v1.1.3 (2026-03-10)
54+
********************
55+
56+
Bug fixes - 1.1.3
57+
=================
58+
59+
- Add ``loongarch64`` to known ISAs (:pull:`50`)
60+
61+
********************
62+
v1.1.2 (2026-03-09)
63+
********************
64+
65+
Bug fixes - 1.1.2
66+
=================
67+
68+
- Match prerelease versions against ``major.minor`` specs (:pull:`48`)
69+
- Drain pipes after killing a timed-out interpreter probe (:pull:`49`)
70+
71+
Improved documentation - 1.1.2
72+
==============================
73+
74+
- Add package description and usage to the README (:pull:`46`)
75+
76+
********************
77+
v1.1.1 (2026-03-06)
78+
********************
79+
80+
Bug fixes - 1.1.1
81+
=================
82+
83+
- Add a timeout to interpreter probing (:pull:`42`)
84+
- Add ``i686`` to known ISAs (:pull:`43`)
85+
86+
Contributor-facing changes - 1.1.1
87+
==================================
88+
89+
- Add a security policy and workflow permissions hardening (:pull:`33`)
90+
91+
********************
92+
v1.1.0 (2026-02-26)
93+
********************
94+
95+
Features - 1.1.0
96+
================
97+
98+
- Add a ``predicate`` parameter to :func:`~python_discovery.get_interpreter` (:pull:`31`)
99+
100+
Improved documentation - 1.1.0
101+
==============================
102+
103+
- Fix the ReadTheDocs build (:pull:`29`)
104+
- Add ``:param:`` descriptions to all public APIs (:pull:`32`)
105+
106+
********************
107+
v1.0.0 (2026-02-25)
108+
********************
109+
110+
Features - 1.0.0
111+
================
112+
113+
- Initial release as a standalone package, extracted from ``virtualenv`` (:pull:`28`)

‎docs/changelog/65.feature.rst‎

Lines changed: 0 additions & 3 deletions
This file was deleted.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{% set top_underline = "*" %}
2+
{% set underline = "=" %}
3+
{% if versiondata.name %}
4+
{{ top_underline * ((versiondata.version + versiondata.date)|length + 5)}}
5+
v{{ versiondata.version }} ({{ versiondata.date }})
6+
{{ top_underline * ((versiondata.version + versiondata.date)|length + 5)}}
7+
{% else %}
8+
{{ top_underline * ((versiondata.version + versiondata.date)|length + 4)}}
9+
{{ versiondata.version }} ({{ versiondata.date }})
10+
{{ top_underline * ((versiondata.version + versiondata.date)|length + 4)}}
11+
{% endif %}
12+
13+
{% for section, _ in sections.items() %}
14+
{% if sections[section] %}
15+
{% for category, val in definitions.items() if category in sections[section]%}
16+
{{ definitions[category]['name'] }} - {{ versiondata.version }}
17+
{{ underline * ((definitions[category]['name'] + versiondata.version)|length + 3)}}
18+
{% if definitions[category]['showcontent'] %}
19+
{% for text, values in sections[section][category].items() %}
20+
- {{ text }} ({{ values|join(', ') }})
21+
{% endfor %}
22+
23+
{% else %}
24+
- {{ sections[section][category]['']|join(', ') }}
25+
26+
{% endif %}
27+
{% if sections[section][category]|length == 0 %}
28+
No significant changes.
29+
30+
{% else %}
31+
{% endif %}
32+
{% endfor %}
33+
{% else %}
34+
No significant changes.
35+
36+
37+
{% endif %}
38+
{% endfor %}

‎docs/conf.py‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import annotations
44

55
from datetime import datetime, timezone
6+
from pathlib import Path
67

78
from python_discovery import __version__
89

@@ -19,8 +20,13 @@
1920
"sphinx.ext.intersphinx",
2021
"sphinx_autodoc_typehints",
2122
"sphinxcontrib.mermaid",
23+
"sphinxcontrib.towncrier.ext",
2224
]
2325

26+
towncrier_draft_autoversion_mode = "draft"
27+
towncrier_draft_include_empty = True
28+
towncrier_draft_working_directory = Path(__file__).parent.parent
29+
2430
extlinks = {
2531
"issue": ("https://github.com/tox-dev/python-discovery/issues/%s", "#%s"),
2632
"pull": ("https://github.com/tox-dev/python-discovery/pull/%s", "PR #%s"),
@@ -33,7 +39,7 @@
3339

3440
templates_path = []
3541
source_suffix = ".rst"
36-
exclude_patterns = ["_build", "changelog/*.rst"]
42+
exclude_patterns = ["_build", "changelog/*"]
3743

3844
main_doc = "index"
3945
pygments_style = "default"

‎docs/index.rst‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,9 @@ repeated lookups are fast.
4747
:hidden:
4848

4949
explanation
50+
51+
.. toctree::
52+
:caption: Project
53+
:hidden:
54+
55+
changelog

‎pyproject.toml‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ optional-dependencies.docs = [
5050
"sphinx>=9.1",
5151
"sphinx-autodoc-typehints>=3.6.3",
5252
"sphinxcontrib-mermaid>=2",
53+
"sphinxcontrib-towncrier>=0.4",
54+
"towncrier>=25.8",
5355
]
5456
optional-dependencies.testing = [
5557
"covdefaults>=2.3",
@@ -64,6 +66,14 @@ urls.Homepage = "https://github.com/tox-dev/python-discovery"
6466
urls.Source = "https://github.com/tox-dev/python-discovery"
6567
urls.Tracker = "https://github.com/tox-dev/python-discovery/issues"
6668

69+
[dependency-groups]
70+
release = [
71+
"gitpython>=3.1.46",
72+
"packaging>=26",
73+
"pre-commit>=4.5.1",
74+
"towncrier>=25.8",
75+
]
76+
6777
[tool.hatch]
6878
version.source = "vcs"
6979

@@ -98,6 +108,12 @@ lint.per-file-ignores."src/python_discovery/_py_info.py" = [
98108
lint.per-file-ignores."src/python_discovery/_windows/_pep514.py" = [
99109
"PTH", # os.path.exists is monkeypatched in tests; pathlib.Path.exists bypasses the mock
100110
]
111+
lint.per-file-ignores."tasks/**/*.py" = [
112+
"D", # release helper scripts, not API
113+
"INP001", # no __init__.py in tasks directory
114+
"S404", # subprocess import (release tooling)
115+
"S603", # `subprocess` call: argv built from trusted inputs (version, gh CLI)
116+
]
101117
lint.per-file-ignores."tests/**/*.py" = [
102118
"D", # don't care about documentation in tests
103119
"FBT", # don't care about booleans as positional arguments in tests
@@ -180,3 +196,21 @@ report.partial_branches = [
180196
report.show_missing = true
181197
html.show_contexts = true
182198
html.skip_covered = false
199+
200+
[tool.towncrier]
201+
name = "python-discovery"
202+
filename = "docs/changelog.rst"
203+
directory = "docs/changelog"
204+
title_format = false
205+
issue_format = ":issue:`{issue}`"
206+
template = "docs/changelog/template.jinja2"
207+
type = [
208+
{ directory = "breaking", name = "Backward incompatible changes", showcontent = true },
209+
{ directory = "deprecation", name = "Deprecations (removal in next major release)", showcontent = true },
210+
{ directory = "feature", name = "Features", showcontent = true },
211+
{ directory = "bugfix", name = "Bug fixes", showcontent = true },
212+
{ directory = "doc", name = "Improved documentation", showcontent = true },
213+
{ directory = "packaging", name = "Packaging updates and notes for downstreams", showcontent = true },
214+
{ directory = "contrib", name = "Contributor-facing changes", showcontent = true },
215+
{ directory = "misc", name = "Miscellaneous internal changes", showcontent = true },
216+
]

0 commit comments

Comments
 (0)