Skip to content

Commit 1f963ee

Browse files
yibeichanclaude
andcommitted
feat: implement hybrid release approach with automated changelog
- Add git-cliff configuration (.cliff.toml) for conventional commit parsing - Integrate git-cliff into release workflow for automated changelog generation - Update CONTRIBUTING.md to document hybrid approach and benefits - Fix dual push strategy to prevent race conditions - Add proper workflow permissions for protected branch compatibility - Add error handling for git-cliff operations Addresses Gemini's feedback about automating manual changelog updates while maintaining intentional release control appropriate for cookiecutter templates. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 2541a03 commit 1f963ee

File tree

3 files changed

+155
-27
lines changed

3 files changed

+155
-27
lines changed

.cliff.toml

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# git-cliff configuration for reproschema-protocol-cookiecutter
2+
# https://git-cliff.org/docs/configuration
3+
4+
[changelog]
5+
# Changelog header
6+
header = """
7+
# Changelog
8+
9+
All notable changes to this project will be documented in this file.
10+
11+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
12+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
13+
14+
"""
15+
# Template for the changelog body
16+
body = """
17+
{% if version %}\
18+
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
19+
{% else %}\
20+
## [Unreleased]
21+
{% endif %}\
22+
23+
{% for group, commits in commits | group_by(attribute="group") %}
24+
### {{ group | upper_first }}
25+
{% for commit in commits %}
26+
- {{ commit.message | upper_first | trim }}{% if commit.breaking %} (**BREAKING**){% endif %}\
27+
{% endfor %}
28+
{% endfor %}\n
29+
"""
30+
# Remove the leading and trailing whitespace from the template
31+
trim = true
32+
# Changelog footer
33+
footer = """
34+
<!-- Links -->
35+
{% for release in releases -%}
36+
{% if release.version -%}
37+
{% if release.previous and release.previous.version -%}
38+
[{{ release.version | trim_start_matches(pat="v") }}]: https://github.com/ReproNim/reproschema-protocol-cookiecutter/compare/{{ release.previous.version }}...{{ release.version }}
39+
{% else -%}
40+
[{{ release.version | trim_start_matches(pat="v") }}]: https://github.com/ReproNim/reproschema-protocol-cookiecutter/releases/tag/{{ release.version }}
41+
{% endif -%}
42+
{% else -%}
43+
{% if releases | length > 1 -%}
44+
[Unreleased]: https://github.com/ReproNim/reproschema-protocol-cookiecutter/compare/{{ releases | nth(n=1) | get(key="version") }}...HEAD
45+
{% else -%}
46+
[Unreleased]: https://github.com/ReproNim/reproschema-protocol-cookiecutter/compare/HEAD
47+
{% endif -%}
48+
{% endif -%}
49+
{% endfor %}
50+
"""
51+
52+
[git]
53+
# Parse the commits based on conventional commits
54+
conventional_commits = true
55+
# Filter out the commits that are not conventional
56+
filter_unconventional = false
57+
# Process each line of a commit as an individual commit
58+
split_commits = false
59+
# Regex for preprocessing the commit messages
60+
commit_preprocessors = [
61+
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/ReproNim/reproschema-protocol-cookiecutter/pull/${2}))" },
62+
]
63+
# Regex for parsing and grouping commits
64+
commit_parsers = [
65+
{ message = "^feat", group = "Added" },
66+
{ message = "^fix", group = "Fixed" },
67+
{ message = "^doc", group = "Documentation" },
68+
{ message = "^docs", group = "Documentation" },
69+
{ message = "^perf", group = "Performance" },
70+
{ message = "^refactor", group = "Refactored" },
71+
{ message = "^style", group = "Styling" },
72+
{ message = "^test", group = "Testing" },
73+
{ message = "^chore\\(release\\):", skip = true },
74+
{ message = "^chore", group = "Miscellaneous" },
75+
{ message = "^build", group = "Build" },
76+
{ message = "^ci", group = "CI" },
77+
{ body = ".*security", group = "Security" },
78+
]
79+
# Filter commits by regex
80+
protect_breaking_commits = false
81+
# Glob pattern for matching git tags
82+
tag_pattern = "v[0-9]*"
83+
# Regex for skipping tags
84+
skip_tags = "v0.1.0-beta.1"
85+
# Regex for ignoring tags
86+
ignore_tags = ""
87+
# Sort the tags chronologically
88+
date_order = false
89+
# Sort the commits inside sections by oldest/newest order
90+
sort_commits = "oldest"

.github/workflows/release.yml

Lines changed: 47 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ on:
1818

1919
permissions:
2020
contents: write
21+
pull-requests: write # For protected branch compatibility
22+
metadata: read
2123

2224
jobs:
2325
release:
@@ -97,38 +99,59 @@ with open('cookiecutter.json', 'w') as f:
9799
f.write('\n')
98100
"
99101
100-
- name: Commit version updates
102+
- name: Install git-cliff
103+
uses: kenji-miyake/setup-git-cliff@v2
104+
105+
- name: Generate changelog with git-cliff
106+
id: changelog
107+
run: |
108+
# Get the previous tag
109+
PREV_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
110+
111+
if [ -z "$PREV_TAG" ]; then
112+
echo "No previous tag found, this is the first release"
113+
# Generate changelog for all commits
114+
git cliff --tag v${{ inputs.version }} --output CHANGELOG_NEW.md || { echo "Error: Failed to generate changelog"; exit 1; }
115+
else
116+
echo "Generating changelog from $PREV_TAG to v${{ inputs.version }}"
117+
# Generate changelog for commits since last tag
118+
git cliff --tag v${{ inputs.version }} --unreleased --output CHANGELOG_NEW.md || { echo "Error: Failed to generate changelog"; exit 1; }
119+
fi
120+
121+
# Extract just the content for this release (without header/footer)
122+
echo "changelog<<EOF" >> $GITHUB_OUTPUT
123+
cat CHANGELOG_NEW.md >> $GITHUB_OUTPUT
124+
echo "" >> $GITHUB_OUTPUT
125+
echo "EOF" >> $GITHUB_OUTPUT
126+
127+
# Generate the complete updated changelog
128+
git cliff --tag v${{ inputs.version }} --output CHANGELOG.md || { echo "Error: Failed to update CHANGELOG.md"; exit 1; }
129+
130+
- name: Commit version updates and changelog
101131
run: |
102132
git config user.name "github-actions[bot]"
103133
git config user.email "github-actions[bot]@users.noreply.github.com"
134+
135+
# Stage version updates
104136
git add pyproject.toml cookiecutter.json
105137
git commit -m "chore: bump version to ${{ inputs.version }}"
138+
139+
# Stage and commit changelog (amend if possible, otherwise separate commit)
140+
git add CHANGELOG.md
141+
if git diff --cached --quiet; then
142+
echo "No changelog changes to commit"
143+
else
144+
git commit --amend --no-edit || git commit -m "docs: update CHANGELOG.md for v${{ inputs.version }}"
145+
fi
146+
147+
# Single push to main
106148
git push origin main
107149
108150
- name: Create and push tag
109151
run: |
110152
git tag -a "v${{ inputs.version }}" -m "Release v${{ inputs.version }}"
111153
git push origin "v${{ inputs.version }}"
112154
113-
- name: Generate release notes
114-
id: release_notes
115-
run: |
116-
# Get the previous tag
117-
PREV_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")
118-
119-
if [ -z "$PREV_TAG" ]; then
120-
echo "No previous tag found, this is the first release"
121-
COMPARE_FROM=$(git rev-list --max-parents=0 HEAD)
122-
else
123-
COMPARE_FROM=$PREV_TAG
124-
fi
125-
126-
# Generate commit list
127-
echo "commits<<EOF" >> $GITHUB_OUTPUT
128-
git log --pretty=format:"- %s (%h)" $COMPARE_FROM..HEAD >> $GITHUB_OUTPUT
129-
echo "" >> $GITHUB_OUTPUT
130-
echo "EOF" >> $GITHUB_OUTPUT
131-
132155
- name: Create Release
133156
uses: softprops/action-gh-release@v2
134157
with:
@@ -156,4 +179,7 @@ with open('cookiecutter.json', 'w') as f:
156179
157180
### What's Changed
158181
159-
Auto-generated release notes are below.
182+
${{ steps.changelog.outputs.changelog }}
183+
184+
---
185+
*Changelog automatically generated from conventional commits using git-cliff*

CONTRIBUTING.md

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ feat: add support for custom activity types
9595

9696
## Release Process
9797

98-
Releases are managed by maintainers using the GitHub Actions release workflow:
98+
Releases are managed by maintainers using a hybrid approach: manual release workflow with automated changelog generation.
9999

100100
### Creating a Release
101101

@@ -105,21 +105,33 @@ Releases are managed by maintainers using the GitHub Actions release workflow:
105105
4. Select release type (patch/minor/major)
106106
5. The workflow will:
107107
- Update version in pyproject.toml and cookiecutter.json
108+
- **Automatically generate and update CHANGELOG.md using git-cliff**
108109
- Create a git tag
109-
- Generate release notes
110+
- Generate comprehensive release notes from conventional commits
110111
- Create a GitHub release
111112

113+
### Automated Changelog
114+
115+
The project uses [git-cliff](https://git-cliff.org/) to automatically generate changelog entries from conventional commits. This eliminates manual changelog maintenance and ensures consistency.
116+
117+
- Changelog is generated from commit messages following the Conventional Commits specification
118+
- The `.cliff.toml` configuration file defines how commits are grouped and formatted
119+
- CHANGELOG.md is automatically updated during the release process
120+
- No manual post-release changelog updates needed!
121+
112122
### Version Guidelines
113123

114124
- **Major** (X.0.0): Breaking changes to template structure or output
115125
- **Minor** (1.X.0): New features, activities, or capabilities
116126
- **Patch** (1.0.X): Bug fixes, documentation updates
117127

118-
### Post-Release
128+
### Why This Approach?
119129

120-
After a release, update CHANGELOG.md:
121-
1. Move items from "Unreleased" to the new version section
122-
2. Add comparison link at the bottom
130+
We use a hybrid approach (manual release + automated changelog) because:
131+
- Cookiecutter templates have different release needs than packages
132+
- Manual releases provide intentional version control
133+
- Automated changelog prevents human error and ensures consistency
134+
- Simpler than full release automation tools while addressing key pain points
123135

124136
## Schema Updates
125137

0 commit comments

Comments
 (0)