Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 30 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,32 @@ jobs:
"`no-docs`/`internal-only` label with reviewer approval."
);

newsfragment-naming:
name: News fragment naming (ID + KAC type)
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 0

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Validate all newsfragments
run: |
python scripts/validate_newsfragments.py

- name: On PR, changed fragments must match this PR number
if: github.event_name == 'pull_request'
run: |
python scripts/validate_newsfragments.py \
--pr "${{ github.event.number }}" \
--diff "${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}"

towncrier-fragment-gate:
name: Changelog fragment gate (Towncrier)
runs-on: ubuntu-latest
Expand All @@ -126,8 +152,10 @@ jobs:
fragment:
- 'newsfragments/*.added.md'
- 'newsfragments/*.changed.md'
- 'newsfragments/*.deprecated.md'
- 'newsfragments/*.removed.md'
- 'newsfragments/*.fixed.md'
- 'newsfragments/*.docs.md'
- 'newsfragments/*.security.md'
changelog:
- 'CHANGELOG.md'

Expand Down Expand Up @@ -172,7 +200,7 @@ jobs:

core.setFailed(
"This PR changes code/workflows without a changelog fragment. " +
"Add newsfragments/<PR>.(added|changed|fixed|docs).md, update CHANGELOG.md, " +
"Add newsfragments/<PR>.(added|changed|deprecated|removed|fixed|security).md, update CHANGELOG.md, " +
"or apply `skip-changelog`/`internal-only` with reviewer approval."
);

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ jobs:
run: |
set -euo pipefail
shopt -s nullglob
fragments=(newsfragments/*.added.md newsfragments/*.changed.md newsfragments/*.fixed.md newsfragments/*.docs.md)
fragments=(newsfragments/*.added.md newsfragments/*.changed.md newsfragments/*.deprecated.md newsfragments/*.removed.md newsfragments/*.fixed.md newsfragments/*.security.md)
if [ "${#fragments[@]}" -gt 0 ]; then
echo "::error::Unreleased Towncrier fragments remain. Build CHANGELOG.md before tagging:" >&2
printf ' - %s\n' "${fragments[@]}" >&2
Expand Down
14 changes: 11 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [0.4.0a1] - 2026-04-22

#### docs
- Deduplicate TSV schema docs by generating tables from code constants, add CI/pre-commit drift checks, and extend documentation automation with docs-touch/changelog gates, Release Drafter, and Read the Docs tag triggers. (#51)- Improve documentation automation and TSV schema generation. (#52)- 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`). (#55)#### changed
- Ship ``0.4.0a1`` alpha, space-free data/install paths (``WyssCenter`` / ``DBSAnnotator``), optional GitHub release update checks, upgrade-safe user config locations, and ``release_prepare.py --bump`` for prerelease and semver bumps. (#62)- Update checker compares all published GitHub releases for the highest applicable newer semver (including pre-releases), handles repos with no releases quietly, surfaces pre-release guidance with support links, and lets users turn off automatic update checks from Help or the notification. (#63)- Consolidate branding under `icons/logosimple/` for Briefcase and Qt, add `scripts/build_app_icons.py` to generate platform icon sizes from a single source PNG, and document Linux `linux system` icon requirements. (#66)### Added
### Added

- Initial changelog scaffold following Keep a Changelog 1.1.0.

### Changed

- Deduplicate TSV schema docs by generating tables from code constants, add CI/pre-commit drift checks, and extend documentation automation with docs-touch/changelog gates, Release Drafter, and Read the Docs tag triggers. ([#51](https://github.com/Brain-Modulation-Lab/App_ClinicalDBSAnnot/pull/51))
- Improve documentation automation and TSV schema generation. ([#52](https://github.com/Brain-Modulation-Lab/App_ClinicalDBSAnnot/pull/52))
- 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`). ([#55](https://github.com/Brain-Modulation-Lab/App_ClinicalDBSAnnot/pull/55))
- Ship ``0.4.0a1`` alpha, space-free data/install paths (``WyssCenter`` / ``DBSAnnotator``), optional GitHub release update checks, upgrade-safe user config locations, and ``release_prepare.py --bump`` for prerelease and semver bumps. ([#62](https://github.com/Brain-Modulation-Lab/App_ClinicalDBSAnnot/pull/62))
- Update checker compares all published GitHub releases for the highest applicable newer semver (including pre-releases), handles repos with no releases quietly, surfaces pre-release guidance with support links, and lets users turn off automatic update checks from Help or the notification. ([#63](https://github.com/Brain-Modulation-Lab/App_ClinicalDBSAnnot/pull/63))
- Consolidate branding under `icons/logosimple/` for Briefcase and Qt, add `scripts/build_app_icons.py` to generate platform icon sizes from a single source PNG, and document Linux `linux system` icon requirements. ([#66](https://github.com/Brain-Modulation-Lab/App_ClinicalDBSAnnot/pull/66))
12 changes: 7 additions & 5 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,13 @@ For detailed guidelines, please see the [Contributing Guide](docs/contributing.r

We use Towncrier for changelog automation.

- Add one fragment per PR in `newsfragments/`:
- `newsfragments/<PR>.added.md`
- `newsfragments/<PR>.changed.md`
- `newsfragments/<PR>.fixed.md`
- `newsfragments/<PR>.docs.md`
- Add one fragment per PR in `newsfragments/` (types from [Keep a Changelog 1.1.0](https://keepachangelog.com/en/1.1.0/)):
- `newsfragments/<PR>.added.md` — new features
- `newsfragments/<PR>.changed.md` — changes in existing functionality (including doc updates)
- `newsfragments/<PR>.deprecated.md` — soon-to-be removed
- `newsfragments/<PR>.removed.md` — removed features
- `newsfragments/<PR>.fixed.md` — bug fixes
- `newsfragments/<PR>.security.md` — vulnerability fixes
- Use one short sentence suitable for changelog bullet points.
- PRs without a fragment may fail CI unless explicitly labeled `skip-changelog` or `internal-only`.

Expand Down
12 changes: 7 additions & 5 deletions docs/contributing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -298,9 +298,11 @@ Before Submitting
~~~~~~~~~~~~~~~~~

1. **Update documentation** in ``docs/`` when user-visible behaviour changes.
2. **Add a Towncrier fragment** under ``newsfragments/`` (for example
``newsfragments/<PR>.added.md`` or ``newsfragments/<PR>.fixed.md``).
``CHANGELOG.md`` is assembled from fragments at release time.
2. **Add a Towncrier fragment** under ``newsfragments/``. Types follow *Types of
changes* in `Keep a Changelog <https://keepachangelog.com/en/1.1.0/>`__ (``added``,
``changed``, ``deprecated``, ``removed``, ``fixed``, ``security``); for example
``newsfragments/<PR>.changed.md`` for a doc-only update. ``CHANGELOG.md`` is
assembled from fragments at release time.
3. **Add or update tests** for new functionality.
4. **Run the local quality gate**: ``ruff format``, ``ruff check``,
``ty check``, ``pytest``.
Expand Down Expand Up @@ -336,8 +338,8 @@ When creating a pull request, please include:
- Code follows the project style (ruff, ty clean).
- Tests added or updated and passing locally.
- Documentation updated (``docs/`` and/or ``README.md``).
- A ``newsfragments/<PR>.(added|changed|fixed|docs).md`` entry was added,
or the PR is explicitly labeled ``skip-changelog`` / ``internal-only``.
- A ``newsfragments/<PR>.(added|changed|deprecated|removed|fixed|security).md`` entry
was added, or the PR is explicitly labeled ``skip-changelog`` / ``internal-only``.
- ``uv.lock`` regenerated if dependencies changed.

Review Process
Expand Down
7 changes: 5 additions & 2 deletions docs/releasing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,12 @@ under ``newsfragments/`` into ``CHANGELOG.md`` (see ``[tool.towncrier]`` in
Day-to-day: changelog fragments
-------------------------------

.. _Keep a Changelog: https://keepachangelog.com/en/1.1.0/

For each PR that should appear in the release notes, add a fragment in ``newsfragments/``
(usually in the same PR as the change). Types are ``added``, ``changed``, ``fixed``, and
``docs`` — for example ``123.added.md`` or ``123.docs.md`` for PR number 123.
(usually in the same PR as the change). Types match *Types of changes* in
`Keep a Changelog`_: ``added``, ``changed``, ``deprecated``, ``removed``, ``fixed``,
``security`` (for example ``123.added.md`` or ``123.changed.md`` for PR number 123).

Create a stub interactively:

Expand Down
18 changes: 14 additions & 4 deletions newsfragments/README.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,30 @@
# Towncrier fragments

This project uses [Towncrier](https://towncrier.readthedocs.io/) to build
`CHANGELOG.md` from per-PR fragments.
`CHANGELOG.md` from per-PR fragments, using the
[Types of changes](https://keepachangelog.com/en/1.1.0/) from *Keep a Changelog* 1.1.0:
**Added**, **Changed**, **Deprecated**, **Removed**, **Fixed**, **Security**.

## How to add an entry

For a PR number `123`, add exactly one fragment file:
For a PR number `123`, add exactly one fragment file (choose the type that matches the
change; documentation-only updates usually use `changed`):

- `newsfragments/123.added.md`
- `newsfragments/123.changed.md`
- `newsfragments/123.deprecated.md`
- `newsfragments/123.removed.md`
- `newsfragments/123.fixed.md`
- `newsfragments/123.docs.md`
- `newsfragments/123.security.md`

The file content should be one short bullet-ready sentence, for example:

`Improve docs build reproducibility by generating schema tables from code constants.`
`Fix memory leak in session export for empty scales.`

**Filename rules (enforced in CI):** `newsfragments/<id>.<type>.md` where `id` is
the issue/PR number and `type` is one of the six names above. On pull requests, any
changed fragment in the diff must use **this** PR’s number as `id` (e.g. PR 66 →
`66.changed.md`). See `scripts/validate_newsfragments.py`.

## When a fragment is not needed

Expand Down
30 changes: 25 additions & 5 deletions newsfragments/template.md.jinja
Original file line number Diff line number Diff line change
@@ -1,13 +1,33 @@
{% for section, _categories in sections.items() %}
{% if section %}
### {{ section }}
{# H2 from towncrie title_format (e.g. ## [0.4.0] - date). Subsections: ### per #}
{# https://keepachangelog.com/en/1.1.0/ — use ### so MyST does not jump H2 -> H4 #}
{% for _section, categories in sections.items() %}
{% if _section and _section|trim %}
### {{ _section }}

{% endif %}
{% for category, val in _categories.items() %}
{% for category, val in categories.items() %}
{% if category %}
#### {{ category }}
{% set _cat = category | lower | trim %}
{% if _cat == "added" %}
### Added
{% elif _cat == "changed" %}
### Changed
{% elif _cat == "deprecated" %}
### Deprecated
{% elif _cat == "removed" %}
### Removed
{% elif _cat == "fixed" %}
### Fixed
{% elif _cat == "security" %}
### Security
{% else %}
### {{ category | title }}
{% endif %}

{% endif %}
{% for text, values in val.items() %}
- {{ text }}{% if values %} ({{ values|join(", ") }}){% endif %}

{% endfor %}
{% endfor %}
{% endfor %}
18 changes: 15 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -151,9 +151,11 @@ filename = "CHANGELOG.md"
template = "newsfragments/template.md.jinja"
start_string = "## [Unreleased]"
title_format = "## [{version}] - {project_date}"
issue_format = "#{issue}"
# Render each fragment's PR/issue as a Markdown link in CHANGELOG (GitHub + MyST)
issue_format = "[#{issue}](https://github.com/Brain-Modulation-Lab/App_ClinicalDBSAnnot/pull/{issue})"
underlines = ["", "", ""]

# Order matches https://keepachangelog.com/en/1.1.0/ (Types of changes)
[[tool.towncrier.type]]
directory = "added"
name = "Added"
Expand All @@ -164,14 +166,24 @@ directory = "changed"
name = "Changed"
showcontent = true

[[tool.towncrier.type]]
directory = "deprecated"
name = "Deprecated"
showcontent = true

[[tool.towncrier.type]]
directory = "removed"
name = "Removed"
showcontent = true

[[tool.towncrier.type]]
directory = "fixed"
name = "Fixed"
showcontent = true

[[tool.towncrier.type]]
directory = "docs"
name = "Documentation"
directory = "security"
name = "Security"
showcontent = true

[tool.briefcase]
Expand Down
133 changes: 133 additions & 0 deletions scripts/validate_newsfragments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
#!/usr/bin/env python3
"""Validate Towncrier news fragment filenames in ``newsfragments/``.

* Every ``*.md`` file (except ``README.md``) must be named::

<id>.(added|changed|deprecated|removed|fixed|security).md

where ``<id>`` is the GitHub issue/PR number (digits only, no leading zeros
required by the regex, but use the real PR number in practice).

* With ``--pr N`` and ``--diff`` ``BASE_SHA..HEAD_SHA``, every fragment path
changed between those commits must be ``N.<category>.md`` (PR-scoped
filenames in CI).
"""

from __future__ import annotations

import argparse
import re
import subprocess
import sys
from pathlib import Path

REPO_ROOT = Path(__file__).resolve().parents[1]
NEWS = REPO_ROOT / "newsfragments"
SKIP = frozenset({"README.md"})
NAME_RE = re.compile(
r"^([1-9]\d*)\.(added|changed|deprecated|removed|fixed|security)\.md$",
re.IGNORECASE,
)
CATEGORIES = frozenset(
("added", "changed", "deprecated", "removed", "fixed", "security")
)


def _discovered_fragments() -> list[Path]:
if not NEWS.is_dir():
return []
return sorted(
p
for p in NEWS.iterdir()
if p.is_file() and p.suffix == ".md" and p.name not in SKIP
)


def _validate_basename(name: str) -> str | None:
"""Return an error string, or None if valid."""
m = NAME_RE.match(name)
if not m:
return (
f"Invalid fragment name {name!r}. "
f"Expected: <id>.<{'|'.join(sorted(CATEGORIES))}>.md"
)
if m.group(2).lower() not in CATEGORIES:
return f"Invalid category in {name!r}"
return None


def _git_names_from_diff(sha_a: str, sha_b: str) -> set[str]:
r = subprocess.run(
["git", "diff", "--name-only", sha_a, sha_b],
cwd=REPO_ROOT,
check=False,
capture_output=True,
text=True,
)
if r.returncode != 0:
return set()
return {line.strip() for line in r.stdout.splitlines() if line.strip()}


def main() -> int:
p = argparse.ArgumentParser(description=__doc__)
p.add_argument(
"--pr",
type=int,
help="With --diff, require that each changed fragment uses this id prefix.",
)
p.add_argument(
"--diff",
metavar="BASE..HEAD",
help="Two SHAs as BASE..HEAD; changed fragment paths are checked with --pr.",
)
args = p.parse_args()
err = 0
for path in _discovered_fragments():
msg = _validate_basename(path.name)
if msg:
print(f"ERROR: {path.relative_to(REPO_ROOT)}: {msg}", file=sys.stderr)
err = 1
if err:
return 1
if args.pr is not None and args.diff:
parts = args.diff.split("..", 1)
if len(parts) != 2 or not all(parts):
print(
f"ERROR: --diff must be BASE..HEAD (got {args.diff!r})",
file=sys.stderr,
)
return 1
base_sha, head_sha = parts[0].strip(), parts[1].strip()
want = str(args.pr)
for rel in _git_names_from_diff(base_sha, head_sha):
if not rel.startswith("newsfragments/") or not rel.endswith(".md"):
continue
name = Path(rel).name
if name in SKIP:
continue
mpath = REPO_ROOT / rel
if not mpath.is_file():
continue
msg = _validate_basename(name)
if msg:
print(f"ERROR: {rel}: {msg}", file=sys.stderr)
err = 1
continue
m = NAME_RE.match(name)
assert m # already validated
if m.group(1) != want:
print(
f"ERROR: {rel} must be {want}.<category>.md for PR #{want} "
f"(filename starts with {m.group(1)!r}, not {want!r}).",
file=sys.stderr,
)
err = 1
elif args.pr is not None and not args.diff:
print("ERROR: --pr requires --diff", file=sys.stderr)
return 1
return err


if __name__ == "__main__":
raise SystemExit(main())
Loading