From b3b5939750eb269553234fefdd491d0c3cb4c194 Mon Sep 17 00:00:00 2001 From: Rodrigo Dlugokenski Date: Sat, 24 Oct 2026 09:25:23 -0300 Subject: [PATCH] Draft the changelog section with git-cliff mise run changelog drafts CHANGELOG.md's section for Cargo.toml's version from the commits since the last tag that touch src/, Cargo.toml or Cargo.lock. Commits are grouped by first word, dependency bumps collapse into one line, hand-written [Unreleased] entries are merged in, and the link references follow. The release checklist edits the draft. Co-Authored-By: Claude Opus 5.5 --- .github/scripts/changelog-draft.sh | 78 ++++++++++++++++++++++++++++++ CLAUDE.md | 3 +- cliff.toml | 54 +++++++++++++++++++++ docs/RELEASE.md | 15 ++++-- mise.toml | 7 +++ 5 files changed, 151 insertions(+), 6 deletions(-) create mode 100755 .github/scripts/changelog-draft.sh create mode 100644 cliff.toml diff --git a/.github/scripts/changelog-draft.sh b/.github/scripts/changelog-draft.sh new file mode 100755 index 0000000..994c045 --- /dev/null +++ b/.github/scripts/changelog-draft.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +# Drafts CHANGELOG.md's section for VERSION with git-cliff (cliff.toml) from +# the commits since the last tag that touch the shipped code, and moves what +# is under [Unreleased] into it, merged by ### heading. Updates the link +# references at the end. The result is a draft to edit before committing. +set -euo pipefail + +version=${1:?usage: changelog-draft.sh VERSION} +root=$(cd "$(dirname "$0")/../.." && pwd) +changelog=$root/CHANGELOG.md +repo=https://github.com/rdlu/plugii + +if grep -q "^## \[$version\]" "$changelog"; then + echo "CHANGELOG.md already has a $version section" >&2 + exit 1 +fi +previous=$(git -C "$root" describe --tags --abbrev=0 --match 'v[0-9]*') + +draft=$(cd "$root" && git cliff --unreleased --tag "v$version" \ + --include-path 'src/**' --include-path Cargo.toml --include-path Cargo.lock \ + 2>/dev/null) + +# Keep a Changelog's heading order. Entries under a heading appear in the +# order read: the hand-written [Unreleased] ones first, then the draft's. +merged=$(printf '%s\n' "$draft" | awk -v changelog="$changelog" ' + function add(heading, line) { + if (!(heading in body)) { body[heading] = "" } + body[heading] = body[heading] line "\n" + } + BEGIN { + while ((getline line < changelog) > 0) { + if (line ~ /^## /) { inside = (line == "## [Unreleased]"); continue } + if (line ~ /^\[[^]]*\]: /) inside = 0 + if (!inside) continue + if (line ~ /^### /) { heading = substr(line, 5); continue } + if (line != "" && heading != "") add(heading, line) + } + } + /^## / { title = $0; next } + /^### / { heading = substr($0, 5); next } + # A hand-written "Updated dependencies, …" line wins over the generic one. + $0 == "- Updated dependencies." && body["Changed"] ~ /(^|\n)- Updated dependencies/ { next } + $0 != "" && heading != "" { add(heading, $0) } + END { + print title + n = split("Added Changed Deprecated Removed Fixed Security", order, " ") + for (i = 1; i <= n; i++) { + if (order[i] in body) { + printf "\n### %s\n\n%s", order[i], body[order[i]] + delete body[order[i]] + } + } + for (h in body) printf "\n### %s\n\n%s", h, body[h] + } +') + +if [ "$(printf '%s\n' "$merged" | wc -l)" -le 1 ]; then + echo "nothing to release since $previous: no user-facing commits and an empty [Unreleased]" >&2 + exit 1 +fi + +tmp=$(mktemp) +awk -v merged="$merged" -v version="$version" -v previous="$previous" -v repo="$repo" ' + $0 == "## [Unreleased]" { print; print ""; print merged; print ""; skip = 1; next } + skip && /^## / { skip = 0 } + skip && /^\[[^]]*\]: / { skip = 0 } + skip { next } + /^\[Unreleased\]: / { + print "[Unreleased]: " repo "/compare/v" version "...HEAD" + print "[" version "]: " repo "/releases/tag/v" version + next + } + { print } +' "$changelog" >"$tmp" +mv "$tmp" "$changelog" + +echo "drafted the $version section in CHANGELOG.md from the commits since $previous:" +echo "reword it for users before committing" diff --git a/CLAUDE.md b/CLAUDE.md index 1b40f24..842e3e7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -19,7 +19,8 @@ All three of test, fmt --check and clippy must pass before a commit `capture `, `outdated`, `update`, `watch`, `install`, `push`, and for the docs site in `site/` (Hugo + Hextra): `docs` (dev server on http://127.0.0.1:1313/) and `docs-build` (`site/public/`, gitignored), and `demos` (re-records -`site/demos/` with VHS). Releases (`docs/RELEASE.md`): `release-check` +`site/demos/` with VHS). Releases (`docs/RELEASE.md`): `changelog` +(drafts the next CHANGELOG section with git-cliff), `release-check` (crate contents, `cargo publish --dry-run`, PKGBUILD checks), `release-tag` (pushes main and the `vX.Y.Z` tag), and for `packaging/aur/`: `aur-srcinfo` and `aur-test` (makepkg + namcap from a `git archive` of HEAD). diff --git a/cliff.toml b/cliff.toml new file mode 100644 index 0000000..146c938 --- /dev/null +++ b/cliff.toml @@ -0,0 +1,54 @@ +# git-cliff: drafts CHANGELOG.md's next section from the commits since the +# last tag (mise run changelog). The draft is a starting point to edit in the +# release PR, not the final notes. +# +# Commits here are plain imperative sentences, so they're grouped by their +# first word. The task only passes commits that touch the shipped code +# (src/, Cargo.toml, Cargo.lock): docs site, CI and packaging work stay out. + +[changelog] +header = "" +# One section, Keep a Changelog style. Dependency bumps (Dependabot's +# chore(deps) PRs and the like) collapse into one "Updated dependencies" line. +body = """ +{% set dep_bump = "^(chore\\(deps\\)|Update dependencies|Bump )" -%} +{% if version -%} +## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} +{%- else -%} +## [Unreleased] +{%- endif %} +{% for group, commits in commits | group_by(attribute="group") %} +### {{ group | striptags | trim }} +{% set_global deps = false -%} +{% for commit in commits -%} +{% if commit.message is matching(dep_bump) -%} +{% set_global deps = true -%} +{% else %} +- {{ commit.message | split(pat="\n") | first | trim }} +{%- endif -%} +{% endfor -%} +{% if deps %} +- Updated dependencies. +{%- endif %} +{% endfor %} +""" +trim = true +footer = "" + +[git] +conventional_commits = false +filter_unconventional = false +filter_commits = false +tag_pattern = "v[0-9].*" +sort_commits = "oldest" +# The HTML comments only order the groups; striptags drops them. +commit_parsers = [ + { message = "^Merge ", skip = true }, + { message = "^Release ", skip = true }, + { message = "^(Satisfy clippy|Refactor)", skip = true }, + { message = "^(chore\\(deps\\)|Update dependencies|Bump )", group = "Changed" }, + { message = "^(Add|Introduce|Support)", group = "Added" }, + { message = "^Fix", group = "Fixed" }, + { message = "^(Drop|Remove|Delete)", group = "Removed" }, + { message = ".*", group = "Changed" }, +] diff --git a/docs/RELEASE.md b/docs/RELEASE.md index 91a378d..423dfeb 100644 --- a/docs/RELEASE.md +++ b/docs/RELEASE.md @@ -25,11 +25,16 @@ steps are at the end, ready but not part of a release yet. 1. **Bump the version** in Cargo.toml (`version = "X.Y.Z"`), then `cargo check` so Cargo.lock follows. -2. **Update CHANGELOG.md**: move what's under `[Unreleased]` into a - `## [X.Y.Z] - YYYY-MM-DD` section, dated today (a section still saying - `unreleased` stops `release-tag`), and add its link at the bottom: - `[X.Y.Z]: https://github.com/rdlu/plugii/releases/tag/vX.Y.Z`, with - `[Unreleased]` comparing from `vX.Y.Z`. +2. **Draft the CHANGELOG.md section:** `mise run changelog`. It runs + git-cliff (`cliff.toml`; `pacman -S git-cliff`) over the commits since + the last tag that touch `src/`, `Cargo.toml` or `Cargo.lock`, groups + them by first word (Add → Added, Fix → Fixed, Drop/Remove → Removed, + the rest → Changed), collapses dependency bumps into one line, and + merges in whatever was written under `[Unreleased]` by hand. It dates + the section today and updates the link references at the bottom. + Then edit it: commit subjects are for developers, so reword them for + users, drop what nobody would notice, and add a `### Security` entry + naming the advisory for a security fix. 3. **Check:** ```sh diff --git a/mise.toml b/mise.toml index c55a7c0..a16f3a8 100644 --- a/mise.toml +++ b/mise.toml @@ -90,6 +90,13 @@ run = "site/demos/build.sh" # Releases: docs/RELEASE.md is the checklist these serve. +[tasks.changelog] +description = "Draft CHANGELOG.md's section for Cargo.toml's version from the commits since the last tag (git-cliff)" +run = ''' +version=$(sed -n '/^\[package\]/,/^\[/s/^version = "\(.*\)"$/\1/p' Cargo.toml) +.github/scripts/changelog-draft.sh "$version" +''' + [tasks.release-check] description = "Release dry runs: version and CHANGELOG, crate contents, cargo publish --dry-run, PKGBUILDs" run = '''