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
78 changes: 78 additions & 0 deletions .github/scripts/changelog-draft.sh
Original file line number Diff line number Diff line change
@@ -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"
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ All three of test, fmt --check and clippy must pass before a commit
`capture <name>`, `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).
Expand Down
54 changes: 54 additions & 0 deletions cliff.toml
Original file line number Diff line number Diff line change
@@ -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 = "<!-- 1 -->Changed" },
{ message = "^(Add|Introduce|Support)", group = "<!-- 0 -->Added" },
{ message = "^Fix", group = "<!-- 2 -->Fixed" },
{ message = "^(Drop|Remove|Delete)", group = "<!-- 3 -->Removed" },
{ message = ".*", group = "<!-- 1 -->Changed" },
]
15 changes: 10 additions & 5 deletions docs/RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '''
Expand Down