Skip to content

Commit b5f5d79

Browse files
authored
Create changelog.yml
1 parent 375450b commit b5f5d79

File tree

1 file changed

+219
-0
lines changed

1 file changed

+219
-0
lines changed

.github/workflows/changelog.yml

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

0 commit comments

Comments
 (0)