Skip to content

Commit d115204

Browse files
committed
Add / update release utilities
1 parent db9e758 commit d115204

File tree

2 files changed

+249
-5
lines changed

2 files changed

+249
-5
lines changed

resources/bump_version.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -148,23 +148,23 @@ def summarize_changes(git_log: str) -> str:
148148
summary = []
149149

150150
if features:
151-
summary.append('**New Features:**')
151+
# summary.append('**New Features:**')
152152
for feat in features[:5]: # Limit to 5 most recent
153153
summary.append(f'* {feat}')
154154
if len(features) > 5:
155155
summary.append(f'* ...and {len(features) - 5} more features')
156-
summary.append('')
156+
# summary.append('')
157157

158158
if fixes:
159-
summary.append('**Bug Fixes:**')
159+
# summary.append('**Bug Fixes:**')
160160
for fix in fixes[:5]: # Limit to 5 most recent
161161
summary.append(f'* {fix}')
162162
if len(fixes) > 5:
163163
summary.append(f'* ...and {len(fixes) - 5} more fixes')
164-
summary.append('')
164+
# summary.append('')
165165

166166
if other:
167-
summary.append('**Other Changes:**')
167+
# summary.append('**Other Changes:**')
168168
for change in other[:3]: # Limit to 3 most recent
169169
summary.append(f'* {change}')
170170
if len(other) > 3:

resources/create_release.py

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Script to create GitHub releases for SingleStoreDB Python SDK.
4+
5+
This script automatically:
6+
1. Extracts the current version from setup.cfg
7+
2. Finds the matching release notes from docs/src/whatsnew.rst
8+
3. Creates a temporary notes file
9+
4. Creates a GitHub release using gh CLI
10+
5. Cleans up temporary files
11+
12+
Usage:
13+
python create_release.py [--version VERSION] [--dry-run]
14+
15+
Examples:
16+
python create_release.py # Use current version from setup.cfg
17+
python create_release.py --version 1.15.6 # Use specific version
18+
python create_release.py --dry-run # Preview without executing
19+
"""
20+
from __future__ import annotations
21+
22+
import argparse
23+
import os
24+
import re
25+
import subprocess
26+
import sys
27+
import tempfile
28+
from pathlib import Path
29+
30+
31+
def get_version_from_setup_cfg() -> str:
32+
"""Extract the current version from setup.cfg."""
33+
setup_cfg_path = Path(__file__).parent.parent / 'setup.cfg'
34+
35+
if not setup_cfg_path.exists():
36+
raise FileNotFoundError(f'Could not find setup.cfg at {setup_cfg_path}')
37+
38+
with open(setup_cfg_path, 'r') as f:
39+
content = f.read()
40+
41+
match = re.search(r'^version\s*=\s*(.+)$', content, re.MULTILINE)
42+
if not match:
43+
raise ValueError('Could not find version in setup.cfg')
44+
45+
return match.group(1).strip()
46+
47+
48+
def extract_release_notes(version: str) -> str:
49+
"""Extract release notes for the specified version from whatsnew.rst."""
50+
whatsnew_path = Path(__file__).parent.parent / 'docs' / 'src' / 'whatsnew.rst'
51+
52+
if not whatsnew_path.exists():
53+
raise FileNotFoundError(f'Could not find whatsnew.rst at {whatsnew_path}')
54+
55+
with open(whatsnew_path, 'r') as f:
56+
content = f.read()
57+
58+
# Look for the version section
59+
version_pattern = rf'^v{re.escape(version)}\s*-\s*.*$'
60+
version_match = re.search(version_pattern, content, re.MULTILINE)
61+
62+
if not version_match:
63+
raise ValueError(f'Could not find release notes for version {version} in whatsnew.rst')
64+
65+
# Find the start of the version section
66+
start_pos = version_match.end()
67+
68+
# Find the separator line (dashes)
69+
lines = content[start_pos:].split('\n')
70+
notes_start = None
71+
72+
for i, line in enumerate(lines):
73+
if line.strip() and all(c == '-' for c in line.strip()):
74+
notes_start = i + 1
75+
break
76+
77+
if notes_start is None:
78+
raise ValueError(f'Could not find release notes separator for version {version}')
79+
80+
# Extract notes until the next version section
81+
notes_lines: list[str] = []
82+
for line in lines[notes_start:]:
83+
# Stop at the next version (starts with 'v' followed by a number)
84+
if re.match(r'^v\d+\.\d+\.\d+', line.strip()):
85+
break
86+
# Stop at empty line followed by version line
87+
if line.strip() == '' and notes_lines:
88+
# Check if next non-empty line is a version
89+
remaining_lines = lines[notes_start + len(notes_lines) + 1:]
90+
for next_line in remaining_lines:
91+
if next_line.strip():
92+
if re.match(r'^v\d+\.\d+\.\d+', next_line.strip()):
93+
break
94+
break
95+
else:
96+
continue
97+
notes_lines.append(line)
98+
99+
# Clean up the notes
100+
# Remove trailing empty lines
101+
while notes_lines and not notes_lines[-1].strip():
102+
notes_lines.pop()
103+
104+
if not notes_lines:
105+
raise ValueError(f'No release notes found for version {version}')
106+
107+
return '\n'.join(notes_lines)
108+
109+
110+
def create_release(version: str, notes: str, dry_run: bool = False) -> None:
111+
"""Create a GitHub release using gh CLI."""
112+
# Create temporary file for release notes
113+
with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f:
114+
f.write(notes)
115+
notes_file = f.name
116+
117+
try:
118+
# Construct the gh release create command
119+
tag = f'v{version}'
120+
title = f'SingleStoreDB v{version}'
121+
122+
cmd = [
123+
'gh', 'release', 'create', tag,
124+
'--title', title,
125+
'--notes-file', notes_file,
126+
]
127+
128+
if dry_run:
129+
print('DRY RUN: Would execute the following command:')
130+
print(' '.join(cmd))
131+
print(f'\nRelease notes file content:')
132+
print(f"{'='*50}")
133+
print(notes)
134+
print(f"{'='*50}")
135+
return
136+
137+
print(f'Creating GitHub release for version {version}...')
138+
print(f'Tag: {tag}')
139+
print(f'Title: {title}')
140+
print(f'Notes file: {notes_file}')
141+
142+
# Execute the command
143+
result = subprocess.run(cmd, capture_output=True, text=True)
144+
145+
if result.returncode != 0:
146+
print('Error creating GitHub release:')
147+
print(f'STDOUT: {result.stdout}')
148+
print(f'STDERR: {result.stderr}')
149+
sys.exit(1)
150+
151+
print('✅ GitHub release created successfully!')
152+
print(f'Output: {result.stdout}')
153+
154+
finally:
155+
# Clean up temporary file
156+
try:
157+
os.unlink(notes_file)
158+
except OSError:
159+
pass
160+
161+
162+
def check_prerequisites() -> None:
163+
"""Check that required tools are available."""
164+
# Check if gh CLI is available
165+
try:
166+
result = subprocess.run(['gh', '--version'], capture_output=True, text=True, check=True)
167+
print(f'GitHub CLI found: {result.stdout.strip().split()[2]}')
168+
except (subprocess.CalledProcessError, FileNotFoundError):
169+
print('Error: GitHub CLI (gh) is not installed or not in PATH')
170+
print('Please install it from https://cli.github.com/')
171+
sys.exit(1)
172+
173+
# Check if we're in a git repository
174+
try:
175+
subprocess.run(['git', 'rev-parse', '--git-dir'], capture_output=True, check=True)
176+
except subprocess.CalledProcessError:
177+
print('Error: Not in a git repository')
178+
sys.exit(1)
179+
180+
# Check if we're authenticated with GitHub
181+
try:
182+
result = subprocess.run(['gh', 'auth', 'status'], capture_output=True, text=True)
183+
if result.returncode != 0:
184+
print('Error: Not authenticated with GitHub')
185+
print('Please run: gh auth login')
186+
sys.exit(1)
187+
except subprocess.CalledProcessError:
188+
print('Error: Could not check GitHub authentication status')
189+
sys.exit(1)
190+
191+
192+
def main() -> None:
193+
parser = argparse.ArgumentParser(
194+
description='Create GitHub release for SingleStoreDB Python SDK',
195+
formatter_class=argparse.RawDescriptionHelpFormatter,
196+
epilog='''Examples:
197+
%(prog)s # Use current version from setup.cfg
198+
%(prog)s --version 1.15.6 # Use specific version
199+
%(prog)s --dry-run # Preview without executing''',
200+
)
201+
202+
parser.add_argument(
203+
'--version',
204+
help='Version to release (default: extract from setup.cfg)',
205+
)
206+
207+
parser.add_argument(
208+
'--dry-run',
209+
action='store_true',
210+
help='Show what would be done without executing',
211+
)
212+
213+
args = parser.parse_args()
214+
215+
try:
216+
# Check prerequisites
217+
if not args.dry_run:
218+
check_prerequisites()
219+
220+
# Get version
221+
if args.version:
222+
version = args.version
223+
print(f'Using specified version: {version}')
224+
else:
225+
version = get_version_from_setup_cfg()
226+
print(f'Extracted version from setup.cfg: {version}')
227+
228+
# Extract release notes
229+
print(f'Extracting release notes for version {version}...')
230+
notes = extract_release_notes(version)
231+
232+
# Create the release
233+
create_release(version, notes, dry_run=args.dry_run)
234+
235+
if not args.dry_run:
236+
print(f'\n✅ Successfully created GitHub release for v{version}')
237+
238+
except Exception as e:
239+
print(f'Error: {e}')
240+
sys.exit(1)
241+
242+
243+
if __name__ == '__main__':
244+
main()

0 commit comments

Comments
 (0)