Skip to content

Commit f3caaab

Browse files
Merge pull request #56 from richardkoehler/chore/release-tooling-towncrier-docs
Release tooling: Towncrier + prepare-release workflow + docs
2 parents fad4c15 + 6521210 commit f3caaab

10 files changed

Lines changed: 493 additions & 21 deletions

File tree

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
name: CD - Prepare release PR
2+
3+
# Runs the same logic as ``scripts/release_prepare.py`` locally: bump versions,
4+
# assemble ``CHANGELOG.md`` with Towncrier, commit, push a branch, open a PR.
5+
# Tagging ``v*`` remains a deliberate maintainer step after merge (see docs/releasing.rst).
6+
7+
on:
8+
workflow_dispatch:
9+
inputs:
10+
version:
11+
description: "Version (X.Y.Z, no v prefix)"
12+
required: true
13+
type: string
14+
release_date:
15+
description: "Towncrier release date (YYYY-MM-DD); leave empty for today (UTC)"
16+
required: false
17+
type: string
18+
19+
permissions:
20+
contents: write
21+
pull-requests: write
22+
23+
jobs:
24+
open-pr:
25+
runs-on: ubuntu-latest
26+
steps:
27+
- name: Checkout
28+
uses: actions/checkout@v6
29+
with:
30+
ref: main
31+
fetch-depth: 0
32+
33+
- name: Set up uv
34+
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b
35+
with:
36+
python-version: "3.12"
37+
enable-cache: true
38+
39+
- name: Sync dev dependencies
40+
run: uv sync --locked --dev
41+
42+
- name: Compute branch name and release date
43+
id: meta
44+
shell: bash
45+
run: |
46+
VERSION="${{ github.event.inputs.version }}"
47+
echo "branch=chore/release-prep-${VERSION}" >> "$GITHUB_OUTPUT"
48+
D="${{ github.event.inputs.release_date }}"
49+
if [ -z "$D" ]; then
50+
D=$(date -u +"%Y-%m-%d")
51+
fi
52+
echo "date=${D}" >> "$GITHUB_OUTPUT"
53+
54+
- name: Configure git author
55+
run: |
56+
git config user.name "github-actions[bot]"
57+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
58+
59+
- name: Create release prep branch
60+
run: git checkout -b "${{ steps.meta.outputs.branch }}"
61+
62+
- name: Run release_prepare.py
63+
run: |
64+
uv run python scripts/release_prepare.py "${{ github.event.inputs.version }}" \
65+
--date "${{ steps.meta.outputs.date }}" \
66+
--commit \
67+
--any-branch
68+
69+
- name: Push branch
70+
run: git push -u origin "${{ steps.meta.outputs.branch }}"
71+
72+
- name: Open pull request
73+
env:
74+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
75+
VERSION: ${{ github.event.inputs.version }}
76+
RDATE: ${{ steps.meta.outputs.date }}
77+
BRANCH: ${{ steps.meta.outputs.branch }}
78+
run: |
79+
cat > "$RUNNER_TEMP/pr-body.md" <<EOF
80+
Automated release preparation for **${VERSION}** (Towncrier date: *${RDATE}*).
81+
82+
**Review**
83+
- \`src/dbs_annotator/__init__.py\` and \`[tool.briefcase].version\` in \`pyproject.toml\` match **${VERSION}**.
84+
- \`CHANGELOG.md\` for this version; removed \`newsfragments/*.md\` files were consumed by Towncrier.
85+
86+
**After merge** tag the merge commit and push \`v${VERSION}\`; see \`docs/releasing.rst\`. Pushing \`v*\` triggers \`release.yml\`.
87+
EOF
88+
gh pr create --base main --head "$BRANCH" \
89+
--title "chore(release): prepare v${VERSION}" \
90+
--body-file "$RUNNER_TEMP/pr-body.md"

CONTRIBUTING.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ We welcome contributions to the Clinical DBS Annotator! This document provides g
44

55
For detailed guidelines, please see the [Contributing Guide](docs/contributing.rst).
66

7+
**Releases (maintainers):** use the PR-based process and manual `v*` tag described in [docs/releasing.rst](docs/releasing.rst) (Sphinx: *Developer guide → Releasing*). The helper is `scripts/release_prepare.py`; GitHub Actions workflow **CD - Prepare release PR** runs the same steps.
8+
79
## Quick Start
810

911
1. Fork the repository

docs/conf.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@
3232
]
3333

3434
templates_path = ["_templates"]
35+
# Do not list ``_autosummary`` here: those ``.rst`` stubs are generated under
36+
# ``docs/_autosummary/`` during the same build and must be parsed. (Ignore the
37+
# directory in git via ``.gitignore``, not in Sphinx ``exclude_patterns``.)
3538
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
3639

3740
# Known, non-actionable warnings that would otherwise fail `-W` builds.

docs/contributing.rst

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Prerequisites
1818
To contribute to this project, you should have:
1919

2020
- **Python 3.12 or newer** (matches ``requires-python`` in ``pyproject.toml``;
21-
CI tests on 3.12, 3.13, and 3.14).
21+
CI uses 3.12 on Ubuntu, Windows, and macOS).
2222
- **uv** (https://github.com/astral-sh/uv) for dependency management and
2323
running tools.
2424
- **Git** installed and configured.
@@ -167,6 +167,33 @@ Optional: regenerate UI screenshot artifacts for docs (uploaded as CI artifact):
167167
QT_QPA_PLATFORM=offscreen DOCS_SCREENSHOT_DIR=docs/_build/screenshots \
168168
uv run pytest tests/docs/test_docs_screenshots.py -m docs_screenshot
169169
170+
Sphinx-related paths under ``docs/``:
171+
172+
- **``docs/_static/``** — **Keep in version control.** These are source assets (images,
173+
optional extra CSS/JS). Sphinx only *copies* them into ``docs/_build/html/_static/``
174+
when you build; the underscore is a Sphinx convention for “static inputs”, not
175+
garbage output.
176+
177+
- **``docs/_templates/``** (including ``_templates/autosummary/`` if present) —
178+
**Keep in version control** when the ``.html``/``.rst`` files there are **hand-written**
179+
Jinja templates that customize the HTML builder or autosummary. They are part of
180+
your documentation *source*, not generated API stubs.
181+
182+
- **``docs/_autosummary/``** at the **project root** (next to your ``.rst`` sources) —
183+
**Do not commit.** Those files are **generated stubs** produced by
184+
``sphinx.ext.autosummary``; they belong in ``.gitignore`` and are recreated by
185+
``sphinx-build`` (or your docs CI job).
186+
187+
- **``docs/_generated/``** — **Depends on your workflow.** If that directory holds
188+
output from a **script** (for example includes generated from Python constants),
189+
you can either (a) **commit** the files so a plain ``sphinx-build`` works everywhere,
190+
with CI checking they are up to date, or (b) **gitignore** them and always run the
191+
generator before Sphinx in CI and locally. Do **not** delete the folder if you still
192+
``.. include::`` it from an ``.rst`` file unless you switch fully to (b) and teach
193+
CI to regenerate it.
194+
195+
**Never commit** the HTML tree ``docs/_build/`` (already ignored).
196+
170197
Code Standards
171198
---------------
172199

@@ -324,8 +351,9 @@ Our review process:
324351
- ``uv audit`` for dependency vulnerabilities.
325352
- ``ruff`` (lint + format) and ``ty`` (type check).
326353
- ``pytest`` on Ubuntu, Windows, and macOS with a coverage floor.
327-
- Briefcase ZIP packaging smoke test on Windows.
328-
- Documentation build (Sphinx ``-W``) and link check.
354+
- Prose checks (codespell, doc8, interrogate) via pre-commit.
355+
- Briefcase smoke test on Linux (``create`` + ``build`` without packaging).
356+
- Documentation build (Sphinx ``-W``), TSV schema doc drift check, and link check.
329357

330358
2. **Peer review** by maintainers.
331359
3. **Discussion** of any required changes.
@@ -387,22 +415,29 @@ We use a simple trunk-based strategy:
387415
Release Process
388416
~~~~~~~~~~~~~~~
389417

390-
1. Update ``dbs_annotator.__version__`` in
391-
``src/dbs_annotator/__init__.py`` (Hatch reads this as the distribution
392-
version) and, if it changed, ``[tool.briefcase].version`` in
418+
Maintainers follow a **PR-based** flow: bump versions and assemble the changelog on a
419+
branch, merge to ``main``, then **tag** ``vX.Y.Z`` and push the tag to publish builds.
420+
421+
Concrete steps:
422+
423+
1. Update ``dbs_annotator.__version__`` in ``src/dbs_annotator/__init__.py`` (Hatch
424+
reads this as the distribution version) and ``[tool.briefcase].version`` in
393425
``pyproject.toml``. These two values must match.
394-
2. Build ``CHANGELOG.md`` from Towncrier fragments:
426+
2. Build ``CHANGELOG.md`` from Towncrier fragments (or use ``scripts/release_prepare.py`` /
427+
the **CD - Prepare release PR** workflow), for example:
395428

396429
.. code-block:: bash
397430
398431
uv run towncrier build --yes --version X.Y.Z --date YYYY-MM-DD
399-
3. Open a release PR, land it on ``main``, then push the matching
400-
``vX.Y.Z`` tag.
401-
4. The ``CD - Create GitHub Release`` workflow builds Python wheels/sdist and
402-
Briefcase installers (Windows MSI, macOS DMG, Linux system package), then
403-
creates the GitHub Release with artifacts attached.
432+
433+
3. Open a release PR, land it on ``main``, then push the matching ``vX.Y.Z`` tag.
434+
4. The **CD - Create GitHub Release** workflow builds Python wheels/sdist and Briefcase
435+
installers (Windows MSI, macOS DMG, Linux system package), then creates the GitHub
436+
Release with artifacts attached.
404437
5. Read the Docs publishes the matching versioned documentation from the tag.
405438

439+
See :doc:`releasing` for more detail (local script, workflow inputs, and tagging).
440+
406441
Continuous Integration
407442
~~~~~~~~~~~~~~~~~~~~~~
408443

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ Developed at the **Brain Modulation Lab, Massachusetts General Hospital**.
5151
:caption: Developer Guide
5252

5353
contributing
54+
releasing
5455
api
5556

5657
----

docs/releasing.rst

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
Releasing
2+
=========
3+
4+
This project uses a **pull request** to prepare each release, then a **manual Git tag**
5+
as the only step that starts publication (GitHub Actions builds installers and
6+
creates a GitHub Release when a ``v*`` tag is pushed).
7+
8+
Version sources
9+
---------------
10+
11+
Keep these in sync whenever you cut a release:
12+
13+
- ``__version__`` in ``src/dbs_annotator/__init__.py`` — Hatch reads this as the
14+
``dbs-annotator`` distribution version.
15+
- ``version`` under ``[tool.briefcase]`` in ``pyproject.toml`` (Briefcase requires a static string)
16+
17+
The automation below updates both, then runs `Towncrier`_ to fold Markdown fragments
18+
under ``newsfragments/`` into ``CHANGELOG.md`` (see ``[tool.towncrier]`` in
19+
``pyproject.toml``).
20+
21+
.. _Towncrier: https://towncrier.readthedocs.io/
22+
23+
Day-to-day: changelog fragments
24+
-------------------------------
25+
26+
For each PR that should appear in the release notes, add a fragment in ``newsfragments/``
27+
(usually in the same PR as the change). Types are ``added``, ``changed``, ``fixed``, and
28+
``docs`` — for example ``123.added.md`` or ``123.docs.md`` for PR number 123.
29+
30+
Create a stub interactively:
31+
32+
.. code-block:: bash
33+
34+
uv run towncrier create 123.added.md
35+
36+
CI may require a fragment when certain paths change unless the PR is labeled
37+
``skip-changelog`` or ``internal-only`` (see ``CONTRIBUTING.md``).
38+
39+
Option A — Prepare the release PR locally
40+
-----------------------------------------
41+
42+
1. Ensure ``main`` is up to date and you have a **clean** working tree (or pass
43+
``--allow-dirty`` only if you intend to include other edits — not recommended).
44+
45+
2. Create a branch (do **not** commit the release bump directly on ``main``):
46+
47+
.. code-block:: bash
48+
49+
git checkout main
50+
git pull
51+
git checkout -b chore/release-prep-X.Y.Z
52+
53+
3. Run the helper (omit ``--commit`` first if you want to inspect diffs only):
54+
55+
.. code-block:: bash
56+
57+
uv sync --dev
58+
uv run python scripts/release_prepare.py X.Y.Z --dry-run
59+
uv run python scripts/release_prepare.py X.Y.Z --commit
60+
61+
Use ``--date YYYY-MM-DD`` if the Towncrier release date should not be “today”.
62+
Use ``--skip-towncrier`` only in exceptional cases (changelog skipped).
63+
64+
4. Push the branch and open a pull request into ``main``. Wait for CI to pass, then
65+
merge.
66+
67+
Option B — Prepare the release PR from GitHub Actions
68+
-------------------------------------------------------
69+
70+
1. In GitHub: **Actions** → **CD - Prepare release PR** → **Run workflow**.
71+
72+
2. Enter **Version** (``X.Y.Z`` without a ``v`` prefix). Optionally set **Release date**
73+
(``YYYY-MM-DD``); otherwise UTC “today” is used.
74+
75+
3. The workflow creates branch ``chore/release-prep-X.Y.Z``, runs the same steps as
76+
``scripts/release_prepare.py``, pushes it, and opens a pull request.
77+
78+
4. Review and merge the PR when CI is green.
79+
80+
Publish: tag after merge (deliberate final step)
81+
------------------------------------------------
82+
83+
After the release-prep PR is **merged** into ``main``:
84+
85+
1. Update your local ``main`` and identify the **merge commit** (or use GitHub’s
86+
suggested SHA for the PR merge).
87+
88+
2. Create an **annotated** tag matching the version (``v`` prefix for the git tag
89+
only):
90+
91+
.. code-block:: bash
92+
93+
git checkout main
94+
git pull
95+
git tag -a vX.Y.Z -m "Release vX.Y.Z" <merge_commit_sha>
96+
git push origin vX.Y.Z
97+
98+
3. That push triggers ``.github/workflows/release.yml`` (tag pattern ``v*``), which
99+
builds Python wheels and Briefcase artifacts and, when appropriate, publishes a
100+
GitHub Release.
101+
102+
Do **not** push a ``v*`` tag until the release-prep PR is merged and you are satisfied
103+
with ``CHANGELOG.md`` and the version numbers on ``main``.
104+
105+
Manual workflow dispatch on ``release.yml`` can still build artifacts without a new
106+
tag; see that workflow’s inputs if you need a one-off build.
107+
108+
Troubleshooting
109+
---------------
110+
111+
- **“Working tree is not clean”** — stash or commit unrelated work, or use a fresh clone.
112+
- **Towncrier fails** — ensure there is at least one valid fragment for the release, or
113+
confirm ``CHANGELOG.md`` still contains the ``## [Unreleased]`` heading Towncrier
114+
uses as ``start_string`` in ``pyproject.toml``.
115+
- **Branch already exists** — delete the remote branch ``chore/release-prep-X.Y.Z`` or
116+
pick a new branch name before re-running the workflow.

newsfragments/55.docs.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Document PR-based releases (`scripts/release_prepare.py`, **CD - Prepare release PR** workflow), maintainer `docs/releasing.rst`, and Sphinx generated-dir guidance (`_autosummary`, `_generated`, `_static`, `_templates`).

pyproject.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,10 @@ dependencies = [
3333

3434
[dependency-groups]
3535
dev = [
36-
"pytest>=8.0.0",
37-
"pytest-qt>=4.4.0",
38-
"pytest-timeout>=2.3.0",
39-
"pytest-cov>=4.1.0",
36+
"pytest>=9.0.3",
37+
"pytest-qt>=4.5.0",
38+
"pytest-timeout>=2.4.0",
39+
"pytest-cov>=7.1.0",
4040
"ruff>=0.1.0",
4141
"pre-commit>=3.6",
4242
"pre-commit-uv>=4.2.1",

0 commit comments

Comments
 (0)