Skip to content

Commit 9fdebad

Browse files
committed
feat: enhance changelog generation with optional summary input and improved commit parsing
1 parent b84bf72 commit 9fdebad

File tree

2 files changed

+102
-13
lines changed

2 files changed

+102
-13
lines changed

.github/workflows/ci-cd.yml

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ on:
66
pull_request:
77
branches: [ main ]
88
workflow_dispatch:
9+
inputs:
10+
changelog_summary:
11+
description: "Plain-English summary for CHANGELOG (optional)."
12+
required: false
13+
default: ""
914

1015
permissions:
1116
contents: write
@@ -90,7 +95,14 @@ jobs:
9095
- name: Generate Changelog Entry
9196
# We run the script directly.
9297
# It appends to CHANGELOG.md in the runner.
93-
run: python3 scripts/generate_changelog.py
98+
env:
99+
CHANGELOG_SUMMARY: ${{ inputs.changelog_summary }}
100+
run: |
101+
if [ -n "${CHANGELOG_SUMMARY}" ]; then
102+
python3 scripts/generate_changelog.py --use-last-tag --summary "${CHANGELOG_SUMMARY}"
103+
else
104+
python3 scripts/generate_changelog.py --use-last-tag
105+
fi
94106
95107
- name: Commit and Push Changelog
96108
run: |
@@ -109,4 +121,4 @@ jobs:
109121
uses: actions/upload-artifact@v4
110122
with:
111123
name: changelog-update
112-
path: CHANGELOG.md
124+
path: CHANGELOG.md

scripts/generate_changelog.py

Lines changed: 88 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import argparse
12
import subprocess
23
import sys
34
from datetime import datetime
@@ -11,11 +12,37 @@ def get_git_log(range_str="HEAD~1..HEAD"):
1112
try:
1213
cmd = ["git", "log", range_str, "--pretty=format:%h|%an|%ad|%s%n%b", "--date=short"]
1314
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
14-
return result.stdout.strip().split('\n\n') # Split by double newline to separate commits
15+
output = result.stdout.strip()
16+
if not output:
17+
return []
18+
# Split by double newline to separate commits
19+
return output.split("\n\n")
1520
except subprocess.CalledProcessError:
1621
print(f"Error reading git log for range {range_str}")
1722
return []
1823

24+
def get_latest_tag():
25+
"""Return the most recent git tag reachable from HEAD."""
26+
try:
27+
cmd = ["git", "describe", "--tags", "--abbrev=0"]
28+
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
29+
tag = result.stdout.strip()
30+
return tag or None
31+
except subprocess.CalledProcessError:
32+
return None
33+
34+
def compute_confidence(commits, known_types):
35+
"""Compute confidence based on conventional commit parsing coverage."""
36+
if not commits:
37+
return "low"
38+
known = sum(1 for c in commits if c["type"].split("(")[0] in known_types)
39+
ratio = known / len(commits)
40+
if ratio == 1.0:
41+
return "high"
42+
if ratio >= 0.6:
43+
return "medium"
44+
return "low"
45+
1946
def parse_commit(commit_text):
2047
"""Parse a single commit block."""
2148
lines = commit_text.strip().split('\n')
@@ -47,7 +74,7 @@ def parse_commit(commit_text):
4774
"desc": desc
4875
}
4976

50-
def generate_entry(commits):
77+
def generate_entry(commits, range_str, origin="generated", summary=None):
5178
if not commits:
5279
return ""
5380

@@ -66,9 +93,16 @@ def generate_entry(commits):
6693
# We will put placeholders.
6794

6895
now = datetime.now().strftime("%Y-%m-%d")
96+
known_types = {"feat", "fix", "refactor", "perf", "test", "docs", "chore"}
97+
confidence = compute_confidence(commits, known_types)
6998
output = []
7099
output.append(f"## [Unreleased] - {now}")
71100
output.append("")
101+
output.append(f"**Origin:** {origin.title()}")
102+
output.append(f"**Range:** `{range_str}`")
103+
output.append(f"**Commit Count:** {len(commits)}")
104+
output.append(f"**Confidence:** {confidence}")
105+
output.append("")
72106
# Determine intent from the most common type or the first commit
73107
focus = "Routine Maintenance"
74108
if 'feat' in groups:
@@ -79,7 +113,10 @@ def generate_entry(commits):
79113
output.append(f"**Focus:** {focus}")
80114
output.append("")
81115
output.append("### 🧠 Temporal Context & Intent")
82-
output.append("> *Auto-generated: Add context about why these changes were made.*")
116+
if summary:
117+
output.append(f"> {summary}")
118+
else:
119+
output.append("> *Auto-generated: Add context about why these changes were made.*")
83120
output.append("")
84121
output.append("### 🏗️ Architectural Impact")
85122
output.append("> *Auto-generated: Describe high-level architectural shifts.*")
@@ -133,18 +170,58 @@ def generate_entry(commits):
133170

134171
def main():
135172
changelog_path = Path("CHANGELOG.md")
136-
137-
# Allow range to be passed as argument
138-
range_str = "HEAD~5..HEAD"
139-
if len(sys.argv) > 1:
140-
range_str = sys.argv[1]
141173

142-
# In a real CI, we might compare against the last tag.
143-
commits_raw = get_git_log(range_str)
174+
parser = argparse.ArgumentParser(description="Generate a changelog entry from git history.")
175+
parser.add_argument(
176+
"--range",
177+
dest="range_str",
178+
default=None,
179+
help="Git commit range (e.g., v2.1.0..HEAD).",
180+
)
181+
parser.add_argument(
182+
"--use-last-tag",
183+
action="store_true",
184+
help="Use the latest tag as the start of the range (falls back if none).",
185+
)
186+
parser.add_argument(
187+
"--origin",
188+
default="generated",
189+
help="Origin label for the entry (generated or curated).",
190+
)
191+
parser.add_argument(
192+
"--summary",
193+
default=None,
194+
help="Plain-English summary of the changes to include in the entry.",
195+
)
196+
parser.add_argument(
197+
"--summary-file",
198+
default=None,
199+
help="Path to a file containing the plain-English summary.",
200+
)
201+
args = parser.parse_args()
202+
203+
if args.range_str:
204+
range_str = args.range_str
205+
elif args.use_last_tag:
206+
tag = get_latest_tag()
207+
range_str = f"{tag}..HEAD" if tag else "HEAD~5..HEAD"
208+
else:
209+
range_str = "HEAD~5..HEAD"
210+
211+
# In a real CI, we might compare against the last tag.
212+
commits_raw = get_git_log(range_str)
144213
parsed_commits = [parse_commit(c) for c in commits_raw if c]
145214
parsed_commits = [c for c in parsed_commits if c] # filter Nones
146215

147-
new_entry = generate_entry(parsed_commits)
216+
summary = args.summary
217+
if args.summary_file:
218+
try:
219+
summary = Path(args.summary_file).read_text(encoding="utf-8").strip()
220+
except OSError as exc:
221+
print(f"Failed to read summary file: {exc}")
222+
sys.exit(1)
223+
224+
new_entry = generate_entry(parsed_commits, range_str, origin=args.origin, summary=summary)
148225

149226
if not new_entry:
150227
print("No commits found to generate changelog.")

0 commit comments

Comments
 (0)