-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathrelease.py
More file actions
343 lines (262 loc) · 9.4 KB
/
release.py
File metadata and controls
343 lines (262 loc) · 9.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
#!/usr/bin/env python3
"""
Automatic CHANGELOG.md Generator
Generates changelog from git commits with GitHub links
"""
import re
import subprocess
import sys
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Tuple
# GitHub repository (update this if needed)
GITHUB_REPO = "SSujitX/clawdbot-ui"
# Emoji mappings for commit types
TYPE_EMOJIS = {
"feat": "✨",
"fix": "🐛",
"docs": "📝",
"style": "💄",
"refactor": "♻️",
"perf": "⚡",
"test": "✅",
"build": "👷",
"ci": "💚",
"chore": "🔧",
"revert": "⏪",
}
# Type labels for changelog
TYPE_LABELS = {
"feat": "✨ Features",
"fix": "🐛 Bug Fixes",
"docs": "📝 Documentation",
"style": "💄 Styles",
"refactor": "♻️ Code Refactoring",
"perf": "⚡ Performance Improvements",
"test": "✅ Tests",
"build": "👷 Build System",
"ci": "💚 Continuous Integration",
"chore": "🔧 Chores",
"revert": "⏪ Reverts",
}
def run_command(cmd: List[str]) -> str:
"""Run a shell command and return output."""
try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
check=True,
)
return result.stdout.strip()
except subprocess.CalledProcessError as e:
print(f"Error running command: {' '.join(cmd)}")
print(f"Error: {e.stderr}")
sys.exit(1)
def get_latest_tag() -> str:
"""Get the latest git tag."""
try:
return run_command(["git", "describe", "--tags", "--abbrev=0"])
except:
# No tags yet
return None
def get_commits_since_tag(tag: str = None) -> List[str]:
"""Get all commits since the given tag (or all commits if no tag)."""
if tag:
cmd = ["git", "log", f"{tag}..HEAD", "--pretty=format:%H|%s|%b"]
else:
cmd = ["git", "log", "--pretty=format:%H|%s|%b"]
output = run_command(cmd)
if not output:
return []
commits = []
current_commit = []
for line in output.split('\n'):
if '|' in line and len(line.split('|')) >= 2:
if current_commit:
commits.append('|'.join(current_commit))
current_commit = [line]
else:
if current_commit:
current_commit.append(line)
if current_commit:
commits.append('|'.join(current_commit))
return commits
def parse_commit(commit_str: str) -> Dict:
"""Parse a commit string into structured data."""
parts = commit_str.split('|', 2)
if len(parts) < 2:
return None
commit_hash = parts[0]
subject = parts[1]
body = parts[2] if len(parts) > 2 else ""
# Parse conventional commit format: type(scope): description
match = re.match(r'^(\w+)(?:\(([^)]+)\))?: (.+)$', subject)
if not match:
# Not a conventional commit, treat as chore
return {
"hash": commit_hash,
"type": "chore",
"scope": None,
"description": subject,
"body": body,
"breaking": False,
}
commit_type = match.group(1)
scope = match.group(2)
description = match.group(3)
# Check for breaking changes
breaking = "BREAKING CHANGE" in body or subject.startswith(f"{commit_type}!")
# Extract issue numbers from description and body
issues = re.findall(r'#(\d+)', subject + body)
return {
"hash": commit_hash,
"type": commit_type,
"scope": scope,
"description": description,
"body": body,
"breaking": breaking,
"issues": issues,
}
def group_commits_by_type(commits: List[Dict]) -> Dict[str, List[Dict]]:
"""Group commits by their type."""
grouped = {}
for commit in commits:
if not commit:
continue
commit_type = commit["type"]
if commit_type not in grouped:
grouped[commit_type] = []
grouped[commit_type].append(commit)
return grouped
def format_commit_for_changelog(commit: Dict) -> str:
"""Format a single commit for the changelog."""
short_hash = commit["hash"][:7]
scope_part = f"**{commit['scope']}**: " if commit['scope'] else ""
# Format description
description = commit["description"]
# Add issue links
for issue in commit.get("issues", []):
issue_link = f"[#{issue}](https://github.com/{GITHUB_REPO}/issues/{issue})"
description = description.replace(f"#{issue}", issue_link)
# Commit link
commit_link = f"[{short_hash}](https://github.com/{GITHUB_REPO}/commit/{commit['hash']})"
# Format: * scope: description (commit_link)
return f"* {scope_part}{description} ({commit_link})"
def get_next_version(current_version: str, commits: List[Dict]) -> str:
"""Determine next version based on commits."""
# Parse current version
if not current_version:
return "1.0.0"
# Remove 'v' prefix if present
version = current_version.lstrip('v')
parts = version.split('.')
if len(parts) != 3:
return "1.0.0"
major, minor, patch = map(int, parts)
# Check for breaking changes
has_breaking = any(c.get("breaking", False) for c in commits if c)
has_feat = any(c.get("type") == "feat" for c in commits if c)
if has_breaking:
major += 1
minor = 0
patch = 0
elif has_feat:
minor += 1
patch = 0
else:
patch += 1
return f"{major}.{minor}.{patch}"
def generate_changelog_entry(version: str, commits: List[Dict], previous_version: str = None) -> str:
"""Generate a changelog entry for the given version."""
today = datetime.now().strftime("%Y-%m-%d")
# Build compare link
if previous_version:
compare_link = f"https://github.com/{GITHUB_REPO}/compare/{previous_version}...v{version}"
header = f"## [{version}]({compare_link}) ({today})"
else:
header = f"## v{version} ({today})"
lines = [header, ""]
# Group commits by type
grouped = group_commits_by_type(commits)
# Add sections in order
for commit_type in ["feat", "fix", "docs", "style", "refactor", "perf", "test", "build", "ci", "chore", "revert"]:
if commit_type not in grouped:
continue
type_commits = grouped[commit_type]
if not type_commits:
continue
# Add section header
section_label = TYPE_LABELS.get(commit_type, commit_type.capitalize())
lines.append(f"### {section_label}")
lines.append("")
# Add commits
for commit in type_commits:
lines.append(format_commit_for_changelog(commit))
lines.append("")
return "\n".join(lines)
def get_version_from_pyproject() -> str:
"""Get version from pyproject.toml."""
try:
import tomllib
except ImportError:
import tomli as tomllib
pyproject_path = Path("pyproject.toml")
if not pyproject_path.exists():
print("❌ pyproject.toml not found!")
sys.exit(1)
with open(pyproject_path, 'rb') as f:
data = tomllib.load(f)
version = data.get("project", {}).get("version")
if not version:
print("❌ Version not found in pyproject.toml!")
sys.exit(1)
return version
def update_changelog(version: str, entry: str):
"""Replace CHANGELOG.md with only the current version."""
changelog_path = Path("CHANGELOG.md")
# Simple structure: header + current version only
content = [
"# Changelog",
"",
"All notable changes to ClawdBot Control Panel will be documented in this file.",
"",
]
content.extend(entry.split('\n'))
# Write complete file
with open(changelog_path, 'w', encoding='utf-8') as f:
f.write('\n'.join(content))
print(f"✅ Updated CHANGELOG.md with version {version} (new commits only)")
def main():
"""Main function."""
print("🔍 Analyzing git commits...")
# Get version from pyproject.toml
version = get_version_from_pyproject()
print(f"📦 Version from pyproject.toml: {version}")
# Get latest tag
latest_tag = get_latest_tag()
print(f"📌 Latest tag: {latest_tag or 'None (first release)'}")
# Get commits since last tag
commit_strings = get_commits_since_tag(latest_tag)
if not commit_strings:
print("⚠️ No new commits since last tag")
print(f"\n💡 Tip: Make some commits first, then run this script")
return
print(f"📝 Found {len(commit_strings)} new commits")
# Parse commits
commits = [parse_commit(c) for c in commit_strings]
commits = [c for c in commits if c] # Filter None
# Generate changelog entry for this version only
entry = generate_changelog_entry(version, commits, latest_tag)
# Update CHANGELOG.md with just this version
update_changelog(version, entry)
print(f"\n✨ Done! CHANGELOG.md updated with v{version}")
print(f"\nNext steps:")
print(f"1. Review CHANGELOG.md")
print(f"2. Update version in pyproject.toml if needed")
print(f"3. git add CHANGELOG.md pyproject.toml")
print(f"4. git commit -m 'chore: release v{version}'")
print(f"5. git push")
print(f"6. Draft release will be auto-created!")
if __name__ == "__main__":
main()