Skip to content

Commit 8e05422

Browse files
feat(changelog): add git-cliff config + reusable workflow (#206)
## Summary - Adds `templates/cliff.toml` — canonical git-cliff config for the estate (Keep-a-Changelog format, conventional-commit parsers, security bucket). - Adds `.github/workflows/changelog-reusable.yml` — `workflow_call` reusable with four modes (commit-back / pr-back / release-only / check-only). - Closes **Item 3** of the 2026-05-26 estate tech-debt audit follow-up (#197 surfaced 180/279 repos with no CHANGELOG.md — 65% gap). ## Caller adoption (one line) ```yaml jobs: changelog: uses: hyperpolymath/standards/.github/workflows/changelog-reusable.yml@main permissions: contents: write pull-requests: write ``` ## Modes | Mode | Use when | |---|---| | `commit-back` (default) | Repo allows direct pushes to main. | | `pr-back` | Branch protection blocks direct main pushes. | | `release-only` | CHANGELOG only ever updates on release tag; attached as artifact. | | `check-only` | CI gate — fail PR if author forgot to regenerate CHANGELOG.md. | ## Companion - standards#197 — documentation debt audit (drives this PR). - standards#201 — licence-consistency CI check (same governance-reusable pattern). - standards#203 — trusted-base reduction policy. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 7c2b815 commit 8e05422

2 files changed

Lines changed: 262 additions & 0 deletions

File tree

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
# SPDX-License-Identifier: MPL-2.0
2+
# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath)
3+
#
4+
# changelog-reusable.yml — Generate CHANGELOG.md from conventional commits.
5+
#
6+
# Closes Item 3 of the 2026-05-26 estate tech-debt audit follow-up: 65% of
7+
# estate repos (180/279) had no CHANGELOG.md. This reusable wires up git-cliff
8+
# with the canonical config at `hyperpolymath/standards/templates/cliff.toml`.
9+
#
10+
# Caller example (auto-update CHANGELOG.md on every push to main):
11+
# jobs:
12+
# changelog:
13+
# uses: hyperpolymath/standards/.github/workflows/changelog-reusable.yml@main
14+
# permissions:
15+
# contents: write
16+
# pull-requests: write
17+
#
18+
# Modes (controlled by the `mode` input):
19+
# `commit-back` - default. On push to main, regenerate CHANGELOG.md and
20+
# commit it back to the same branch (uses
21+
# `GITHUB_TOKEN`). Skip if no change.
22+
# `pr-back` - On push to main, open a PR with the regenerated
23+
# CHANGELOG.md (good for repos with branch protection
24+
# that disallows direct pushes to main).
25+
# `release-only` - Only generate on a `release` event; attach as an artifact
26+
# to the release. Does not modify the repo state.
27+
# `check-only` - Render the changelog but do NOT commit. Fail the job if
28+
# the on-disk CHANGELOG.md disagrees with the regenerated
29+
# output. Use as a `pull_request` gate.
30+
31+
on:
32+
workflow_call:
33+
inputs:
34+
mode:
35+
description: 'commit-back | pr-back | release-only | check-only'
36+
required: false
37+
type: string
38+
default: 'commit-back'
39+
runs-on:
40+
description: 'Runner label'
41+
required: false
42+
type: string
43+
default: 'ubuntu-latest'
44+
git-cliff-version:
45+
description: 'Version of git-cliff to install (semver, no leading v)'
46+
required: false
47+
type: string
48+
default: '2.6.1'
49+
50+
permissions:
51+
contents: read
52+
53+
jobs:
54+
generate:
55+
name: Generate CHANGELOG.md
56+
runs-on: ${{ inputs.runs-on }}
57+
permissions:
58+
contents: write
59+
pull-requests: write
60+
steps:
61+
- name: Checkout caller repository (full history)
62+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
63+
with:
64+
repository: ${{ github.repository }}
65+
ref: ${{ github.ref }}
66+
fetch-depth: 0
67+
path: caller
68+
69+
- name: Checkout standards (for canonical cliff.toml)
70+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
71+
with:
72+
repository: hyperpolymath/standards
73+
ref: main
74+
path: standards
75+
76+
- name: Install git-cliff
77+
run: |
78+
set -euo pipefail
79+
version="${{ inputs.git-cliff-version }}"
80+
asset="git-cliff-${version}-x86_64-unknown-linux-gnu.tar.gz"
81+
url="https://github.com/orhun/git-cliff/releases/download/v${version}/${asset}"
82+
tmp="$(mktemp -d)"
83+
curl -fsSL "$url" -o "$tmp/${asset}"
84+
tar -C "$tmp" -xzf "$tmp/${asset}"
85+
# Move binary onto PATH (the tarball extracts to a versioned dir).
86+
install -m 0755 "$tmp"/git-cliff-*/git-cliff /usr/local/bin/git-cliff
87+
git-cliff --version
88+
89+
- name: Pick the cliff.toml to use
90+
id: cfg
91+
run: |
92+
set -euo pipefail
93+
if [ -f caller/cliff.toml ]; then
94+
echo "Using caller's own cliff.toml"
95+
echo "path=caller/cliff.toml" >> "$GITHUB_OUTPUT"
96+
else
97+
echo "Using canonical cliff.toml from standards"
98+
echo "path=standards/templates/cliff.toml" >> "$GITHUB_OUTPUT"
99+
fi
100+
101+
- name: Generate CHANGELOG.md
102+
working-directory: caller
103+
run: |
104+
set -euo pipefail
105+
git-cliff \
106+
--config "../${{ steps.cfg.outputs.path }}" \
107+
--output CHANGELOG.md.new
108+
wc -l CHANGELOG.md.new
109+
echo "::group::CHANGELOG preview (first 40 lines)"
110+
head -40 CHANGELOG.md.new
111+
echo "::endgroup::"
112+
113+
- name: Mode = check-only — verify no drift
114+
if: ${{ inputs.mode == 'check-only' }}
115+
working-directory: caller
116+
run: |
117+
set -euo pipefail
118+
if [ ! -f CHANGELOG.md ]; then
119+
echo "ERROR: caller has no CHANGELOG.md; check-only mode requires one to exist."
120+
echo "Adopt mode=commit-back to seed it, then switch to check-only."
121+
exit 1
122+
fi
123+
if ! diff -q CHANGELOG.md CHANGELOG.md.new >/dev/null; then
124+
echo "ERROR: CHANGELOG.md is out of date relative to commit history."
125+
echo "Run git-cliff locally or switch to mode=commit-back."
126+
diff -u CHANGELOG.md CHANGELOG.md.new | head -60 || true
127+
exit 1
128+
fi
129+
echo "CHANGELOG.md is up to date."
130+
131+
- name: Mode = commit-back — commit regenerated CHANGELOG.md
132+
if: ${{ inputs.mode == 'commit-back' && github.event_name == 'push' }}
133+
working-directory: caller
134+
run: |
135+
set -euo pipefail
136+
if [ -f CHANGELOG.md ] && diff -q CHANGELOG.md CHANGELOG.md.new >/dev/null; then
137+
echo "No CHANGELOG changes; skipping commit."
138+
exit 0
139+
fi
140+
mv CHANGELOG.md.new CHANGELOG.md
141+
git config user.email "github-actions[bot]@users.noreply.github.com"
142+
git config user.name "github-actions[bot]"
143+
git add CHANGELOG.md
144+
if git diff --cached --quiet; then
145+
echo "Nothing to commit after move (race condition)."
146+
exit 0
147+
fi
148+
git commit -m "chore(changelog): regenerate from conventional commits
149+
150+
Auto-generated by hyperpolymath/standards changelog-reusable.yml.
151+
See standards/templates/cliff.toml for the canonical config.
152+
153+
Closes part of the 2026-05-26 CHANGELOG gap (standards#197 audit)."
154+
git push origin HEAD:${{ github.ref_name }}
155+
156+
- name: Mode = pr-back — open PR with regenerated CHANGELOG.md
157+
if: ${{ inputs.mode == 'pr-back' && github.event_name == 'push' }}
158+
working-directory: caller
159+
env:
160+
GH_TOKEN: ${{ github.token }}
161+
run: |
162+
set -euo pipefail
163+
if [ -f CHANGELOG.md ] && diff -q CHANGELOG.md CHANGELOG.md.new >/dev/null; then
164+
echo "No CHANGELOG changes; skipping PR."
165+
exit 0
166+
fi
167+
mv CHANGELOG.md.new CHANGELOG.md
168+
branch="bot/changelog-$(date +%Y%m%d-%H%M%S)"
169+
git config user.email "github-actions[bot]@users.noreply.github.com"
170+
git config user.name "github-actions[bot]"
171+
git checkout -b "$branch"
172+
git add CHANGELOG.md
173+
git commit -m "chore(changelog): regenerate from conventional commits"
174+
git push -u origin "$branch"
175+
gh pr create \
176+
--title "chore(changelog): regenerate from conventional commits" \
177+
--body "Auto-generated by hyperpolymath/standards changelog-reusable.yml.
178+
179+
Closes part of the 2026-05-26 CHANGELOG gap (standards#197 audit)." \
180+
--base "${{ github.ref_name }}" \
181+
--head "$branch"
182+
183+
- name: Mode = release-only — attach to release
184+
if: ${{ inputs.mode == 'release-only' && github.event_name == 'release' }}
185+
env:
186+
GH_TOKEN: ${{ github.token }}
187+
run: |
188+
set -euo pipefail
189+
tag="${{ github.event.release.tag_name }}"
190+
gh release upload "$tag" caller/CHANGELOG.md.new \
191+
--clobber \
192+
--repo "${{ github.repository }}"
193+
echo "Attached CHANGELOG.md to release $tag"

templates/cliff.toml

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# SPDX-License-Identifier: MPL-2.0
2+
# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath)
3+
#
4+
# Canonical git-cliff config for the hyperpolymath estate.
5+
#
6+
# Place a copy at `cliff.toml` in any repo and run `git cliff -o CHANGELOG.md`
7+
# locally, OR consume the `changelog-reusable.yml` workflow which uses this
8+
# config automatically.
9+
#
10+
# Closes the 65% CHANGELOG gap surfaced by the 2026-05-26 documentation-debt
11+
# audit (standards#197).
12+
13+
[changelog]
14+
header = """
15+
# Changelog
16+
17+
All notable changes to this project are documented here.
18+
19+
This file is generated by [git-cliff](https://git-cliff.org) from conventional
20+
commits. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
21+
and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
22+
23+
"""
24+
body = """
25+
{% if version -%}
26+
## [{{ version | trim_start_matches(pat="v") }}] — {{ timestamp | date(format="%Y-%m-%d") }}
27+
{% else -%}
28+
## [Unreleased]
29+
{% endif -%}
30+
31+
{% for group, commits in commits | group_by(attribute="group") %}
32+
### {{ group | upper_first }}
33+
34+
{% for commit in commits -%}
35+
- {{ commit.message | upper_first | trim }}{% if commit.breaking %} **[BREAKING]**{% endif %} ({{ commit.id | truncate(length=7, end="") }})
36+
{% endfor %}
37+
{% endfor %}
38+
"""
39+
footer = """
40+
<!-- generated by git-cliff using hyperpolymath/standards/templates/cliff.toml -->
41+
"""
42+
trim = true
43+
44+
[git]
45+
conventional_commits = true
46+
filter_unconventional = false
47+
split_commits = false
48+
commit_parsers = [
49+
{ message = "^feat", group = "Added" },
50+
{ message = "^add", group = "Added" },
51+
{ message = "^fix", group = "Fixed" },
52+
{ message = "^perf", group = "Performance" },
53+
{ message = "^refactor", group = "Changed" },
54+
{ message = "^docs", group = "Documentation" },
55+
{ message = "^test", group = "Tests" },
56+
{ message = "^ci", group = "CI" },
57+
{ message = "^chore", group = "Chores" },
58+
{ message = "^style", skip = true },
59+
{ message = "^revert", group = "Reverted" },
60+
{ body = ".*security",group = "Security" },
61+
# Fallback: any commit not matching above falls into "Other".
62+
{ message = ".*", group = "Other" },
63+
]
64+
filter_commits = false
65+
tag_pattern = "v[0-9]+\\.[0-9]+\\.[0-9]+"
66+
skip_tags = "v0.0.0-test"
67+
ignore_tags = ""
68+
topo_order = false
69+
sort_commits = "newest"

0 commit comments

Comments
 (0)