Skip to content

Commit bcf5503

Browse files
mstrathmanclaude
andcommitted
chore: the changelog has an owner at both moments
Entries are written with the change (a warning-level CodeRabbit pre-merge check nudges PRs that alter user-visible behavior), and release-time promotion is owned by make sync-version alongside every other version stamp: it turns [Unreleased] into a dated section, retargets the compare links, refuses an empty [Unreleased], and is idempotent. RELEASING.md documents both moments. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 108d5c8 commit bcf5503

3 files changed

Lines changed: 61 additions & 2 deletions

File tree

‎.coderabbit.yaml‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,17 @@ reviews:
114114
requirements: >-
115115
Conventional-commit style prefix (feat/fix/docs/bench/site/chore/
116116
refactor/test/ci) with a concise, specific subject.
117+
custom_checks:
118+
- name: "Changelog entry for user-visible changes"
119+
mode: "warning"
120+
instructions: >-
121+
If the PR changes user-visible behavior (SQL surface, option
122+
keys, error codes, defaults, output formats, packaging), it
123+
should add an entry under CHANGELOG.md's [Unreleased] section
124+
in the same PR. Refactors, docs, tests, benchmarks, and CI
125+
changes do not need one. Release-time promotion of
126+
[Unreleased] into a dated section is owned by make
127+
sync-version; PRs should never do that themselves.
117128
118129
chat:
119130
auto_reply: true

‎RELEASING.md‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ committing, and tagging.
66

77
## Cut a release
88

9+
0. Make sure `CHANGELOG.md`'s `[Unreleased]` section reflects the
10+
release. Entries are written with the changes themselves (a
11+
user-visible change adds its line in the same PR); this step is only
12+
a review that nothing is missing.
13+
914
1. Sync every manifest to the new version:
1015

1116
```sh
@@ -16,8 +21,12 @@ committing, and tagging.
1621
`bindings/python/pyproject.toml` (PEP 440 form, e.g. `0.0.1-alpha.5`
1722
becomes `0.0.1a5`), `bindings/python/sqlite_predict/__init__.py`
1823
(`__version__`), and `bindings/node/package.json` (including the
19-
per-platform `optionalDependencies` pins). The C header derives its
20-
version from `VERSION` at build time and needs no edit.
24+
per-platform `optionalDependencies` pins), and promotes
25+
`CHANGELOG.md`'s `[Unreleased]` section into a dated `[<version>]`
26+
section with updated compare links. It refuses to run if
27+
`[Unreleased]` is empty, so a release cannot ship without notes. The
28+
C header derives its version from `VERSION` at build time and needs
29+
no edit.
2130

2231
2. Commit the version bump.
2332

‎scripts/sync-version.mjs‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,43 @@ for (const d of deps) pkg.optionalDependencies[d] = version;
6565
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
6666
console.log(` bindings/node/package.json -> ${version} (+ ${deps.length} optionalDependencies)`);
6767

68+
/* Promote CHANGELOG's [Unreleased] into a dated section for this version.
69+
* Idempotent: if the section already exists, leave the file alone. The
70+
* changelog itself is written with the change; this owns only the
71+
* release-time promotion, like every other version stamp here. */
72+
const clPath = path.join(root, "CHANGELOG.md");
73+
let cl = fs.readFileSync(clPath, "utf8");
74+
if (cl.includes(`## [${version}]`)) {
75+
console.log(` CHANGELOG.md already has a [${version}] section`);
76+
} else {
77+
if (!/## \[Unreleased\]\n/.test(cl)) {
78+
console.error("CHANGELOG.md has no [Unreleased] section to promote");
79+
process.exit(1);
80+
}
81+
const unreleasedBody = cl
82+
.split("## [Unreleased]\n")[1]
83+
.split(/\n## \[/)[0]
84+
.trim();
85+
if (!unreleasedBody) {
86+
console.error(`CHANGELOG.md [Unreleased] is empty; write the release's
87+
entries before running sync-version`);
88+
process.exit(1);
89+
}
90+
const date = new Date().toISOString().slice(0, 10);
91+
cl = cl.replace("## [Unreleased]\n",
92+
`## [Unreleased]\n\n## [${version}] - ${date}\n`);
93+
/* retarget the compare links: Unreleased diffs from the new tag, and the
94+
* new tag diffs from the previous one (the old Unreleased base). */
95+
const linkRe = /^\[Unreleased\]: (.*)\/compare\/(v[^.]+[^ ]*)\.\.\.HEAD$/m;
96+
const lm = cl.match(linkRe);
97+
if (lm) {
98+
const [, repo, prevTag] = lm;
99+
cl = cl.replace(linkRe,
100+
`[Unreleased]: ${repo}/compare/v${version}...HEAD\n` +
101+
`[${version}]: ${repo}/compare/${prevTag}...v${version}`);
102+
}
103+
fs.writeFileSync(clPath, cl);
104+
console.log(` CHANGELOG.md -> [${version}] - ${date}`);
105+
}
106+
68107
console.log(`VERSION = ${version} (PyPI: ${pv})`);

0 commit comments

Comments
 (0)