Skip to content

Commit 05ca05a

Browse files
authored
Adopt Towncrier changelog fragments (#3609)
1 parent 238c2f6 commit 05ca05a

18 files changed

Lines changed: 577 additions & 143 deletions

.claude/skills/release-audit/SKILL.md

Lines changed: 68 additions & 22 deletions
Large diffs are not rendered by default.

.claude/skills/release-audit/references/classification-rules.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ These are noted here so Claude can pattern-match when reading commits, but the s
6262

6363
- `.github/**`, `.pre-commit-config.yaml`, `uv.lock`, `.python-version` — infrastructure, not user-facing.
6464
- `asv.conf.json`, `asv/**`, root-level `_bench_*.py` — benchmark harness.
65-
- `docs/**`, root-level `*.md`, `CHANGELOG.md` — documentation.
65+
- `docs/**`, root-level `*.md`, `CHANGELOG.md`, `changelog/**` — documentation.
6666
- `newton/examples/**` — example scripts. New files here are user-facing (a new example is a release-notable addition). Changes to existing examples are typically not release-notable unless they change the example's registered name or behavior.
6767
- `newton/_src/**` other than the solver / sim / math / geometry paths above — internal Python implementation.
6868
- `pyproject.toml`, `uv.lock`, and files matched by `project.license-files` — dependency and license-audit inputs. New external dependency names, direct requirement scope changes, new resolved package names, and notice-file changes belong in "Dependency & License Audit". Version bumps are not release-notable on their own; dependency changes may also belong in "Behavioral & Support Changes" if a user-visible pin moves (e.g., `mujoco-warp ~=3.7.0`).

.claude/skills/release-audit/references/language-review-examples.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ For entry "Add support for Gaussian splats (GH-NNNN)", fetch commits tagged `GH-
5656

5757
Don't flag if:
5858
- Commits touch `newton/_src/geometry/**` or `newton/_src/sim/builder.py` for a geometry-addition entry → topic matches.
59-
- Commits touch `docs/**` and the entry is in the Documentation section → topic matches.
59+
- Commits touch `docs/**` and the `Added` or `Changed` entry describes that documentation → topic matches.
6060
- Commits touch `newton/_src/solvers/**` for a solver-capability entry → topic matches.
6161

6262
**Tier-2 heuristic (only if `gh` CLI is installed + authenticated):**
@@ -114,7 +114,10 @@ Before flagging, cross-check against existing sibling symbols in the same module
114114

115115
**Err on "mention, don't block"**: flagging should raise a question for human review, not gate the report. The audit appendix shows flagged entries and a one-line reason; a human decides.
116116

117-
**Don't auto-rewrite**: Claude flags the entry, never modifies it. The release manager updates CHANGELOG.md manually.
117+
**Don't auto-rewrite**: Claude flags the entry, never modifies it. Before the
118+
Towncrier build, the release manager updates pending fragments during changelog
119+
maintenance; after release, corrections to dated history require explicit
120+
maintainer approval.
118121

119122
**Prefer false positives over false negatives**: a flag that turns out to be fine costs a 5-second eyeball. A missed wrong-ref or jargon-leak ships to users.
120123

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2026 The Newton Developers
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
"""Safely remove a temporary Newton release-audit report."""
5+
6+
from __future__ import annotations
7+
8+
import argparse
9+
import re
10+
import tempfile
11+
from pathlib import Path
12+
13+
_REPORT_NAME = re.compile(r"newton-[A-Za-z0-9][A-Za-z0-9.+-]*-(?:prerelease|rc|retrospective)-report\.md")
14+
15+
16+
def cleanup_report(path: Path, *, temporary_directory: Path | None = None) -> None:
17+
"""Remove an allowed report directly beneath the temporary directory."""
18+
temporary_root = (temporary_directory or Path(tempfile.gettempdir())).resolve()
19+
if path.is_symlink():
20+
raise ValueError("report path must not be a symlink")
21+
candidate = path.resolve()
22+
if candidate.parent != temporary_root:
23+
raise ValueError(f"report must be directly beneath {temporary_root}")
24+
if _REPORT_NAME.fullmatch(candidate.name) is None:
25+
raise ValueError(f"not an allowed Newton report filename: {candidate.name}")
26+
path.unlink(missing_ok=True)
27+
28+
29+
def main() -> None:
30+
"""Run the report cleanup command."""
31+
parser = argparse.ArgumentParser(description=__doc__)
32+
parser.add_argument("path", type=Path)
33+
args = parser.parse_args()
34+
try:
35+
cleanup_report(args.path)
36+
except ValueError as error:
37+
parser.error(str(error))
38+
39+
40+
if __name__ == "__main__":
41+
main()
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2026 The Newton Developers
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
"""Test safe release-audit report cleanup."""
5+
6+
from __future__ import annotations
7+
8+
import tempfile
9+
import unittest
10+
from pathlib import Path
11+
12+
from cleanup_report import cleanup_report
13+
14+
15+
class CleanupReportTest(unittest.TestCase):
16+
def test_removes_allowed_report(self):
17+
"""Remove an allowed report from the temporary root."""
18+
with tempfile.TemporaryDirectory() as directory:
19+
temporary_root = Path(directory)
20+
report = temporary_root / "newton-1.5.0-prerelease-report.md"
21+
report.write_text("report\n", encoding="utf-8")
22+
23+
cleanup_report(report, temporary_directory=temporary_root)
24+
25+
self.assertFalse(report.exists())
26+
27+
def test_rejects_unexpected_filename(self):
28+
"""Reject a file that is not a Newton release-audit report."""
29+
with tempfile.TemporaryDirectory() as directory:
30+
temporary_root = Path(directory)
31+
report = temporary_root / "unrelated.md"
32+
report.write_text("keep\n", encoding="utf-8")
33+
34+
with self.assertRaisesRegex(ValueError, "not an allowed"):
35+
cleanup_report(report, temporary_directory=temporary_root)
36+
37+
self.assertTrue(report.exists())
38+
39+
def test_rejects_nested_path(self):
40+
"""Reject an allowed filename outside the temporary root."""
41+
with tempfile.TemporaryDirectory() as directory:
42+
temporary_root = Path(directory)
43+
nested = temporary_root / "nested"
44+
nested.mkdir()
45+
report = nested / "newton-1.5.0-rc-report.md"
46+
report.write_text("keep\n", encoding="utf-8")
47+
48+
with self.assertRaisesRegex(ValueError, "directly beneath"):
49+
cleanup_report(report, temporary_directory=temporary_root)
50+
51+
self.assertTrue(report.exists())
52+
53+
def test_rejects_symlink(self):
54+
"""Reject a report symlink without deleting its target."""
55+
with tempfile.TemporaryDirectory() as directory:
56+
temporary_root = Path(directory)
57+
target = temporary_root / "newton-1.5.0-rc-report.md"
58+
target.write_text("keep\n", encoding="utf-8")
59+
report = temporary_root / "newton-1.5.0-prerelease-report.md"
60+
try:
61+
report.symlink_to(target)
62+
except OSError as error:
63+
self.skipTest(f"symlinks are unavailable: {error}")
64+
65+
with self.assertRaisesRegex(ValueError, "symlink"):
66+
cleanup_report(report, temporary_directory=temporary_root)
67+
68+
self.assertTrue(report.is_symlink())
69+
self.assertEqual(target.read_text(encoding="utf-8"), "keep\n")
70+
71+
72+
if __name__ == "__main__":
73+
unittest.main()
Lines changed: 103 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,103 +1,116 @@
11
---
22
name: release-changelog
3-
description: Use when editing, auditing, or preparing Newton CHANGELOG.md for a release, especially to make upgrade-impact information actionable for developers.
3+
description: Use when auditing Newton changelog fragments, building a dated release changelog, or synchronizing a release build back to main.
44
---
55

66
# Newton Release Changelog
77

8-
Maintain `CHANGELOG.md` as the detailed upgrade source of truth. Release notes
9-
and release announcements carry the high-level summary; the changelog should
10-
preserve specific breaking changes, removals, deprecations, behavior/default
11-
changes, dependency constraints, and migration guidance.
12-
13-
## Workflow
14-
15-
1. Protect released history first. Diff `CHANGELOG.md` from the latest stable
16-
tag and inspect every hunk under a dated version header. Move late PR entries
17-
accidentally added to a released section into the current `[Unreleased]`
18-
section. Change released history only with explicit maintainer approval.
19-
2. Identify the release ref and comparison base. For final releases, use the
20-
final tag or release branch. For RC prep, use the latest RC tag as temporary
21-
ground truth and verify against the previous released tag.
22-
3. Read the current `CHANGELOG.md` section being edited, the release audit if
23-
one exists, and PRs behind unclear entries. Do not rely only on commit
24-
subjects for migration guidance.
25-
4. Check completeness from the previous GA or micro release through the release
26-
ref, including RC fixes. Compare the range with the release audit and add
27-
missed user-visible changes.
28-
5. Preserve information. Rephrase, split, merge, and regroup entries only when
29-
the facts remain intact. Ask before deleting information, omitting a
30-
questionable entry, or downgrading a user-visible change to silence.
31-
6. Use the existing Keep-a-Changelog categories (`Added`, `Changed`,
32-
`Deprecated`, `Removed`, `Fixed`). Keep migration and retesting guidance in
33-
the affected entries; release notes carry the summary.
34-
7. Within each category, group entries by the current release's user-facing
35-
feature areas or migration themes when this improves readability.
36-
8. Remove exact and semantic duplicates within the release, not only identical
37-
wording. If a feature and a fix for that feature both landed during the same
38-
release cycle, consolidate the entries around the final user-visible
39-
behavior instead of recording it once as `Added` and again as `Fixed`.
40-
9. Audit category boundaries before finalizing. Keep `Added` for new public
41-
APIs, options, features, examples, and docs; move existing-API behavior
42-
changes, new warnings, default changes, and importer/solver semantics into
8+
Pending user-facing changes live in Towncrier fragments under `changelog/`.
9+
`CHANGELOG.md` is generated only on a release branch. Shipped sections are
10+
immutable; the assembled section for the pending release remains a rolling
11+
document until tagging. Follow `changelog/README.md` as the command and format
12+
authority.
13+
14+
## Audit pending changes
15+
16+
1. Identify the release ref and comparison base. Audit `release-X.Y` once it
17+
exists; otherwise audit the intended main ref.
18+
2. Protect released history. Diff `CHANGELOG.md` from the latest stable tag and
19+
require explicit maintainer approval for edits to dated sections.
20+
3. Render a non-mutating preview, which also validates Towncrier's renderable
21+
fragment filenames:
22+
```bash
23+
uvx --from towncrier==25.8.0 towncrier build --draft \
24+
--version X.Y.Z --date YYYY-MM-DD
25+
```
26+
4. Compare the preview with the release audit and commit range from the previous
27+
release. Inspect `.skip` reasons separately.
28+
5. Preserve information. Rephrase, split, merge, or recategorize fragments only
29+
when the facts remain intact. Ask before deleting information or downgrading
30+
a user-visible change.
31+
6. Use only `Added`, `Changed`, `Deprecated`, `Removed`, and `Fixed`, in that
32+
order. Keep migration and retesting guidance in affected entries.
33+
7. Remove exact and semantic duplicates. When a feature and its fix both land
34+
in one cycle, describe the final user-visible behavior once.
35+
8. Keep `Added` for new public APIs, options, features, examples, and docs. Put
36+
existing-API behavior, warning, default, importer, and solver changes in
4337
`Changed`, even when they expand support.
44-
10. Add same-repository PR references as compact `(#NNNN)` references
45-
selectively, not mechanically. Prioritize high-importance entries:
46-
breaking/default-changing behavior, public API additions that affect
47-
migration, deprecations, removals, and major support fixes. Do not add PR
48-
refs to every routine docs, example, cleanup, or minor fix entry.
49-
11. Before adding a PR reference, verify that the PR actually introduced the
50-
change being cited. Prefer local history such as `git log --oneline` and
51-
`git show --name-only <commit>`; skip ambiguous references rather than
52-
guessing.
53-
12. For each breaking, removed, deprecated, or default-changing entry, include
54-
migration guidance or a clear action: replacement symbol, opt-out flag,
55-
compatibility setting, or what to re-test.
56-
13. Avoid directing users to private/internal APIs as migration targets. If a
57-
public alias is deprecated because storage is becoming internal, say to avoid
58-
depending on that data directly rather than pointing at underscore-prefixed
59-
members.
60-
14. Separate internal cleanup from public API removals. If an internal symbol is
61-
mentioned for completeness, label it as internal and do not imply users must
62-
migrate unless it was public.
63-
15. Verify restored APIs against the final/RC tag before classifying removals.
64-
For example, if a public symbol was removed during development but restored
65-
before the release tag, do not list it as removed.
66-
16. When moving entries between release sections, make sure the information is
67-
not duplicated under an older released version and the historical section
68-
still reflects what actually shipped there.
69-
17. Perform a second editorial pass after regrouping. Re-read the source entries
70-
and the final diff to catch user-relevant behavior, limitations, opt-in
71-
conditions, changed defaults, compatibility details, or migration actions
72-
lost during condensation.
73-
74-
## Post-release reconciliation
75-
76-
Merge a release branch's finalized changelog back to `main` through a dedicated
77-
feature branch and changelog-only PR:
78-
79-
1. Fetch the canonical remote and create the feature branch from the latest
80-
`upstream/main`, not from the release branch.
81-
2. Use the final tag as the source of truth.
82-
3. Keep `## [Unreleased]` first and preserve all post-cut entries not shipped
83-
in the release. Do not replace the whole file with the release-branch copy.
84-
4. Insert the finalized release section immediately below `[Unreleased]` and
85-
keep shipped entries only in that dated section. Resolve semantic overlap so
86-
the same user-facing change is not recorded twice.
87-
5. Verify that only `CHANGELOG.md` changes, the dated section matches the final
88-
tag, and older released sections remain unchanged.
38+
9. Give every breaking, removed, deprecated, or default-changing entry a
39+
concrete action. Never direct users to `newton._src`.
40+
10. A numeric fragment identifier is a GitHub issue number. Towncrier renders
41+
its issue link automatically; do not rewrite it as a pull request number.
8942

90-
## Checks
43+
## Assemble the release during RC stabilization
44+
45+
After the initial release scope has been audited on `release-X.Y`, assemble the
46+
current fragments early enough for maintainer review:
47+
48+
```bash
49+
uvx --from towncrier==25.8.0 towncrier build --draft \
50+
--version X.Y.Z --date YYYY-MM-DD
51+
uvx --from towncrier==25.8.0 towncrier build --yes \
52+
--version X.Y.Z --date YYYY-MM-DD
53+
git rm --ignore-unmatch "changelog/*.skip"
54+
git add -A CHANGELOG.md changelog
55+
```
56+
57+
Review and approve the draft before running the mutating command. Towncrier
58+
inserts the dated section below `[Unreleased]` and deletes rendered fragments.
59+
It ignores `.skip` files, so remove those explicitly. Review the staged diff in
60+
a changelog-only pull request labeled `release-management`.
61+
62+
After assembly, apply the audit rules above to the dated section: verify
63+
completeness, grouping, deduplication, wording, categories, and migration
64+
guidance. Keep editorial cleanup in the changelog-management commits that will
65+
later be synchronized to `main`.
9166

92-
Run targeted searches before finishing:
67+
The first Towncrier release requires one migration audit. The insertion marker
68+
sits above the legacy `[Unreleased]` entries so they remain under the first
69+
generated release title. Merge duplicate category headings without dropping or
70+
duplicating an entry. Later releases need no special handling.
71+
72+
Treat the assembled section as a rolling document. For every later cherry-pick
73+
before tagging:
74+
75+
1. Validate the new fragments and render them with `towncrier build --draft`.
76+
2. Fold the previewed entries into the existing dated section without creating
77+
a second release heading.
78+
3. Delete exactly the consumed `.md` and `.skip` fragments, then stage
79+
`CHANGELOG.md` and `changelog/`.
80+
4. Rerun the changelog cleanup and `release-audit` checks, and merge the update
81+
as another changelog-only `release-management` pull request.
82+
83+
Final GA preparation verifies the completed section and confirms that no
84+
release-branch fragments remain. Do not postpone the full cleanup until GA.
85+
86+
## Synchronize to main
87+
88+
After tagging:
89+
90+
1. Create a changelog-only branch from current `main`.
91+
2. Cherry-pick, in order, every changelog-management commit from `release-X.Y`:
92+
the initial Towncrier build, editorial cleanup, and all later cherry-pick
93+
additions.
94+
3. Confirm fragments deleted by those commits disappear while fragments added to
95+
`main` after the branch cut remain under `changelog/`.
96+
4. Confirm the dated section matches the release tag and older history is
97+
unchanged.
98+
5. Open a changelog-only pull request labeled `release-management`.
99+
100+
Do not replace the whole file with the release-branch copy. The commits'
101+
path-level deletions are what preserve main-only fragments.
102+
103+
## Checks
93104

94105
```bash
95-
git diff v<latest-release> -- CHANGELOG.md
96-
git log --oneline <previous-release>..<release-ref>
97-
rg -n "removed|removal|deprecated|will be removed|in favor of|use .* instead|renam|replac|default|breaking" CHANGELOG.md
98-
git diff -- CHANGELOG.md
106+
uvx --from towncrier==25.8.0 towncrier build --draft \
107+
--version X.Y.Z --date YYYY-MM-DD
108+
git diff v<latest-release> -- CHANGELOG.md changelog
109+
git diff --cached --name-status -- CHANGELOG.md changelog
110+
rg -ni "removed|deprecated|in favor of|use .* instead|renam|replac|default|breaking" \
111+
CHANGELOG.md changelog
99112
```
100113

101-
Confirm that no new hunk lands in a released section, then check for missing or
102-
duplicate entries, accidental deletions, stale removal targets, and missing
103-
migration guidance or PR references.
114+
Confirm that `[Unreleased]` is empty after the first migration, no dated history
115+
changed, released entries appear exactly once, and post-cut main fragments
116+
survive synchronization.

.claude/skills/release-notes/SKILL.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,18 @@ what users should know. Do not reproduce the full changelog; link to it.
2323
- RCs: state that this is a release candidate and what needs validation.
2424
If drafting final-release text from an RC tag or release branch, do not
2525
mention the RC; use the RC only as the temporary source of truth.
26-
2. Read the matching `CHANGELOG.md` section from the release tag or release
27-
branch. Do not rely on `main` unless the release is actually cut from `main`.
26+
2. Choose changelog source material from the authoritative release ref:
27+
- After the Towncrier build or tagging, read the matching dated
28+
`CHANGELOG.md` section from the release tag or release branch.
29+
- Before the build, check out the release branch, validate its pending
30+
fragments, and render a non-mutating preview:
31+
```bash
32+
uvx --from towncrier==25.8.0 towncrier build --draft \
33+
--version X.Y.Z --date YYYY-MM-DD
34+
```
35+
Draft from the preview and legacy `[Unreleased]` entries during the first
36+
Towncrier transition. Do not run a mutating build merely to draft notes.
37+
Do not rely on `main` unless the release is actually cut from `main`.
2838
3. Determine the previous release tag:
2939
- Patch release `X.Y.Z`, `Z > 0`: use the highest earlier `vX.Y.<Z'>` tag.
3040
- Feature release `X.Y.0`: use the highest `vX.<Y-1>.*` tag. If `Y == 0`,

0 commit comments

Comments
 (0)