Skip to content

Commit bffa6c7

Browse files
committed
Create changelog.yml
1 parent e8869ce commit bffa6c7

File tree

1 file changed

+218
-0
lines changed

1 file changed

+218
-0
lines changed

.github/workflows/changelog.yml

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
name: Update Changelog
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
- master
8+
workflow_dispatch:
9+
10+
jobs:
11+
update-changelog:
12+
runs-on: ubuntu-latest
13+
14+
steps:
15+
- name: Checkout Repository
16+
uses: actions/checkout@v4
17+
with:
18+
fetch-depth: 0
19+
token: ${{ secrets.GITHUB_TOKEN }}
20+
21+
- name: Setup Python
22+
uses: actions/setup-python@v4
23+
with:
24+
python-version: '3.x'
25+
26+
- name: Generate Changelog
27+
run: |
28+
python3 << 'EOF'
29+
import subprocess
30+
import sys
31+
from datetime import datetime
32+
from collections import defaultdict
33+
34+
def run_git_log(args):
35+
try:
36+
result = subprocess.run(['git'] + args, capture_output=True, text=True, check=True)
37+
# FIX #1: Split by a null character instead of a newline to handle multi-line bodies.
38+
return result.stdout.strip().split('\x00') if result.stdout.strip() else []
39+
except subprocess.CalledProcessError as e:
40+
print(f"Error running git: {e}")
41+
sys.exit(1)
42+
43+
def get_commits_by_tag():
44+
try:
45+
latest_tag = subprocess.run(
46+
['git', 'describe', '--tags', '--abbrev=0'],
47+
capture_output=True, text=True, check=True
48+
).stdout.strip()
49+
50+
tag_date = subprocess.run(
51+
['git', 'log', '-1', '--format=%ad', '--date=short', latest_tag],
52+
capture_output=True, text=True, check=True
53+
).stdout.strip()
54+
55+
tagged = run_git_log([
56+
'log', latest_tag,
57+
# FIX #2: Add the %x00 null character as a safe delimiter for commits.
58+
'--pretty=format:%s|||%b|||%an|||%ad|||%H%x00',
59+
'--date=short',
60+
'--invert-grep',
61+
'--grep=docs: update changelog',
62+
'--grep=changelog.yml',
63+
'--grep=\\[skip ci\\]'
64+
])
65+
66+
unreleased = run_git_log([
67+
'log', f'{latest_tag}..HEAD',
68+
# FIX #3: Add the delimiter here as well.
69+
'--pretty=format:%s|||%b|||%an|||%ad|||%H%x00',
70+
'--date=short',
71+
'--invert-grep',
72+
'--grep=docs: update changelog',
73+
'--grep=changelog.yml',
74+
'--grep=\\[skip ci\\]'
75+
])
76+
77+
return {
78+
'tagged': {latest_tag: {'commits': tagged, 'date': tag_date}},
79+
'unreleased': unreleased
80+
}
81+
82+
except subprocess.CalledProcessError:
83+
all_commits = run_git_log([
84+
'log',
85+
# FIX #4: And add the delimiter here for the fallback case.
86+
'--pretty=format:%s|||%b|||%an|||%ad|||%H%x00',
87+
'--date=short',
88+
'--invert-grep',
89+
'--grep=docs: update changelog',
90+
'--grep=changelog.yml',
91+
'--grep=\\[skip ci\\]'
92+
])
93+
return {
94+
'tagged': {},
95+
'unreleased': all_commits
96+
}
97+
98+
def categorize_commit(subject, body):
99+
text = (subject + ' ' + body).lower()
100+
if any(x in text for x in ['security', 'vulnerability', 'cve', 'exploit']):
101+
return 'security'
102+
if any(x in text for x in ['breaking change', 'breaking:', 'break:']):
103+
return 'breaking'
104+
if any(x in text for x in ['deprecat', 'obsolete', 'phase out']):
105+
return 'deprecated'
106+
if any(x in text for x in ['remove', 'delete', 'drop', 'eliminate']):
107+
return 'removed'
108+
# Correction: Check for 'added' keywords before 'fixed' keywords.
109+
if any(x in text for x in ['add', 'new', 'create', 'implement', 'feat', 'feature']):
110+
return 'added'
111+
if any(x in text for x in ['fix', 'resolve', 'correct', 'patch', 'bug', 'issue']):
112+
return 'fixed'
113+
return 'changed'
114+
115+
def format_commit_entry(commit):
116+
entry = f"- **{commit['subject']}** ({commit['date']} – {commit['author']})"
117+
body = commit['body'].replace('\\n', '\n')
118+
if body.strip():
119+
lines = [line.strip() for line in body.splitlines() if line.strip()]
120+
for line in lines:
121+
entry += f"\n {line}"
122+
return entry + "\n"
123+
124+
def parse_commits(lines):
125+
commits = []
126+
for line in lines:
127+
if not line: continue
128+
parts = line.split('|||')
129+
if len(parts) >= 5:
130+
subject, body, author, date, hash_id = map(str.strip, parts)
131+
commits.append({
132+
'subject': subject,
133+
'body': body,
134+
'author': author,
135+
'date': date,
136+
'hash': hash_id[:7]
137+
})
138+
return commits
139+
140+
def build_changelog(commits_by_version):
141+
sections = [
142+
('security', 'Security'),
143+
('breaking', 'Breaking Changes'),
144+
('deprecated', 'Deprecated'),
145+
('added', 'Added'),
146+
('changed', 'Changed'),
147+
('fixed', 'Fixed'),
148+
('removed', 'Removed')
149+
]
150+
151+
lines = [
152+
"# Changelog",
153+
"",
154+
"All notable changes to this project will be documented in this file.",
155+
"",
156+
"The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),",
157+
"and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).",
158+
""
159+
]
160+
161+
unreleased = parse_commits(commits_by_version['unreleased'])
162+
if unreleased:
163+
lines.append("## [Unreleased]")
164+
lines.append("")
165+
categorized = defaultdict(list)
166+
for commit in unreleased:
167+
cat = categorize_commit(commit['subject'], commit['body'])
168+
categorized[cat].append(commit)
169+
170+
for key, label in sections:
171+
if categorized[key]:
172+
lines.append(f"### {label}")
173+
for commit in categorized[key]:
174+
lines.append(format_commit_entry(commit))
175+
lines.append("")
176+
else:
177+
lines.append("## [Unreleased]\n")
178+
lines.append("_No unreleased changes._\n")
179+
180+
for tag, info in commits_by_version['tagged'].items():
181+
commits = parse_commits(info['commits'])
182+
lines.append(f"## [{tag}] - {info['date']}\n")
183+
categorized = defaultdict(list)
184+
for commit in commits:
185+
cat = categorize_commit(commit['subject'], commit['body'])
186+
categorized[cat].append(commit)
187+
188+
for key, label in sections:
189+
if categorized[key]:
190+
lines.append(f"### {label}")
191+
for commit in categorized[key]:
192+
lines.append(format_commit_entry(commit))
193+
lines.append("")
194+
195+
return "\n".join(lines)
196+
197+
try:
198+
commit_data = get_commits_by_tag()
199+
changelog = build_changelog(commit_data)
200+
with open("CHANGELOG.md", "w", encoding="utf-8") as f:
201+
f.write(changelog)
202+
print("Changelog generated.")
203+
except Exception as e:
204+
print(f"Error generating changelog: {e}")
205+
sys.exit(1)
206+
EOF
207+
208+
- name: Commit Updated Changelog
209+
run: |
210+
git config --local user.email "action@github.com"
211+
git config --local user.name "GitHub Action"
212+
git add CHANGELOG.md
213+
if git diff --staged --quiet; then
214+
echo "No changes to commit"
215+
else
216+
git commit -m "docs: update changelog [skip ci]"
217+
git push
218+
fi

0 commit comments

Comments
 (0)