Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
singlestore-labs
GitHub Repository: singlestore-labs/singlestoredb-python
Path: blob/main/resources/create_release.py
798 views
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 pyproject.toml
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
import time
29
from pathlib import Path
30
31
32
def status(message: str) -> None:
33
"""Show status messages to indicate progress."""
34
print(f'📋 {message}', file=sys.stderr)
35
36
37
def step(step_num: int, total_steps: int, message: str) -> None:
38
"""Show a numbered step with progress."""
39
print(f'📍 Step {step_num}/{total_steps}: {message}', file=sys.stderr)
40
41
42
def get_version_from_pyproject() -> str:
43
"""Extract the current version from pyproject.toml."""
44
pyproject_path = Path(__file__).parent.parent / 'pyproject.toml'
45
46
if not pyproject_path.exists():
47
raise FileNotFoundError(f'Could not find pyproject.toml at {pyproject_path}')
48
49
with open(pyproject_path, 'r') as f:
50
content = f.read()
51
52
match = re.search(r'^version\s*=\s*["\'](.+)["\']$', content, re.MULTILINE)
53
if not match:
54
raise ValueError('Could not find version in pyproject.toml')
55
56
return match.group(1).strip()
57
58
59
def extract_release_notes(version: str) -> str:
60
"""Extract release notes for the specified version from whatsnew.rst."""
61
whatsnew_path = Path(__file__).parent.parent / 'docs' / 'src' / 'whatsnew.rst'
62
63
if not whatsnew_path.exists():
64
raise FileNotFoundError(f'Could not find whatsnew.rst at {whatsnew_path}')
65
66
with open(whatsnew_path, 'r') as f:
67
content = f.read()
68
69
# Look for the version section
70
version_pattern = rf'^v{re.escape(version)}\s*-\s*.*$'
71
version_match = re.search(version_pattern, content, re.MULTILINE)
72
73
if not version_match:
74
raise ValueError(f'Could not find release notes for version {version} in whatsnew.rst')
75
76
# Find the start of the version section
77
start_pos = version_match.end()
78
79
# Find the separator line (dashes)
80
lines = content[start_pos:].split('\n')
81
notes_start = None
82
83
for i, line in enumerate(lines):
84
if line.strip() and all(c == '-' for c in line.strip()):
85
notes_start = i + 1
86
break
87
88
if notes_start is None:
89
raise ValueError(f'Could not find release notes separator for version {version}')
90
91
# Extract notes until the next version section
92
notes_lines: list[str] = []
93
for line in lines[notes_start:]:
94
# Stop at the next version (starts with 'v' followed by a number)
95
if re.match(r'^v\d+\.\d+\.\d+', line.strip()):
96
break
97
# Stop at empty line followed by version line
98
if line.strip() == '' and notes_lines:
99
# Check if next non-empty line is a version
100
remaining_lines = lines[notes_start + len(notes_lines) + 1:]
101
for next_line in remaining_lines:
102
if next_line.strip():
103
if re.match(r'^v\d+\.\d+\.\d+', next_line.strip()):
104
break
105
break
106
else:
107
continue
108
notes_lines.append(line)
109
110
# Clean up the notes
111
# Remove trailing empty lines
112
while notes_lines and not notes_lines[-1].strip():
113
notes_lines.pop()
114
115
if not notes_lines:
116
raise ValueError(f'No release notes found for version {version}')
117
118
return '\n'.join(notes_lines)
119
120
121
def create_release(version: str, notes: str, dry_run: bool = False) -> None:
122
"""Create a GitHub release using gh CLI."""
123
tag = f'v{version}'
124
title = f'SingleStoreDB v{version}'
125
126
# Create temporary file for release notes
127
with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f:
128
f.write(notes)
129
notes_file = f.name
130
131
try:
132
# Construct the gh release create command
133
cmd = [
134
'gh', 'release', 'create', tag,
135
'--title', title,
136
'--notes-file', notes_file,
137
]
138
139
if dry_run:
140
status('🔍 DRY RUN - Preview mode')
141
print('=' * 50, file=sys.stderr)
142
print(f'Command: {" ".join(cmd)}', file=sys.stderr)
143
print(f'Tag: {tag}', file=sys.stderr)
144
print(f'Title: {title}', file=sys.stderr)
145
print(f'Notes file: {notes_file}', file=sys.stderr)
146
print('=' * 50, file=sys.stderr)
147
print('Release notes content:', file=sys.stderr)
148
print(notes, file=sys.stderr)
149
print('=' * 50, file=sys.stderr)
150
return
151
152
status(f'🚀 Creating GitHub release for v{version}...')
153
status(f' 📎 Tag: {tag}')
154
status(f' 📝 Title: {title}')
155
156
start_time = time.time()
157
158
# Execute the command
159
result = subprocess.run(cmd, capture_output=True, text=True)
160
161
elapsed = time.time() - start_time
162
163
if result.returncode != 0:
164
print(f'❌ Error creating GitHub release (after {elapsed:.1f}s):', file=sys.stderr)
165
print(f' STDOUT: {result.stdout}', file=sys.stderr)
166
print(f' STDERR: {result.stderr}', file=sys.stderr)
167
sys.exit(1)
168
169
status(f'✅ GitHub release created successfully in {elapsed:.1f}s!')
170
if result.stdout.strip():
171
status(f' 🔗 {result.stdout.strip()}')
172
173
finally:
174
# Clean up temporary file
175
try:
176
os.unlink(notes_file)
177
except OSError:
178
pass
179
180
181
def check_prerequisites() -> None:
182
"""Check that required tools are available."""
183
status('🔍 Checking prerequisites...')
184
185
# Check if gh CLI is available
186
try:
187
result = subprocess.run(['gh', '--version'], capture_output=True, text=True, check=True)
188
version = result.stdout.strip().split()[2]
189
status(f' ✓ GitHub CLI found: v{version}')
190
except (subprocess.CalledProcessError, FileNotFoundError):
191
print('❌ GitHub CLI (gh) is not installed or not in PATH', file=sys.stderr)
192
print(' Please install it from https://cli.github.com/', file=sys.stderr)
193
sys.exit(1)
194
195
# Check if we're in a git repository
196
try:
197
subprocess.run(['git', 'rev-parse', '--git-dir'], capture_output=True, check=True)
198
status(' ✓ Git repository detected')
199
except subprocess.CalledProcessError:
200
print('❌ Not in a git repository', file=sys.stderr)
201
sys.exit(1)
202
203
# Check if we're authenticated with GitHub
204
try:
205
result = subprocess.run(['gh', 'auth', 'status'], capture_output=True, text=True)
206
if result.returncode != 0:
207
print('❌ Not authenticated with GitHub', file=sys.stderr)
208
print(' Please run: gh auth login', file=sys.stderr)
209
sys.exit(1)
210
else:
211
# Extract username from output
212
lines = result.stderr.split('\n')
213
username = 'unknown'
214
for line in lines:
215
if 'Logged in to github.com as' in line:
216
username = line.split()[-1]
217
break
218
status(f' ✓ GitHub authenticated as {username}')
219
except subprocess.CalledProcessError:
220
print('❌ Could not check GitHub authentication status', file=sys.stderr)
221
sys.exit(1)
222
223
status('✅ All prerequisites satisfied')
224
225
226
def main() -> None:
227
parser = argparse.ArgumentParser(
228
description='Create GitHub release for SingleStoreDB Python SDK',
229
formatter_class=argparse.RawDescriptionHelpFormatter,
230
epilog='''Examples:
231
%(prog)s # Use current version from pyproject.toml
232
%(prog)s --version 1.15.6 # Use specific version
233
%(prog)s --dry-run # Preview without executing''',
234
)
235
236
parser.add_argument(
237
'--version',
238
help='Version to release (default: extract from pyproject.toml)',
239
)
240
241
parser.add_argument(
242
'--dry-run',
243
action='store_true',
244
help='Show what would be done without executing',
245
)
246
247
args = parser.parse_args()
248
249
try:
250
total_start_time = time.time()
251
252
print('🚀 Starting GitHub release creation', file=sys.stderr)
253
print('=' * 50, file=sys.stderr)
254
255
# Step 1: Check prerequisites (unless dry run)
256
if not args.dry_run:
257
step(1, 4, 'Checking prerequisites')
258
check_prerequisites()
259
else:
260
step(1, 4, 'Skipping prerequisites check (dry-run)')
261
262
# Step 2: Get version
263
step(2, 4, 'Determining version')
264
start_time = time.time()
265
266
if args.version:
267
version = args.version
268
status(f'Using specified version: {version}')
269
else:
270
version = get_version_from_pyproject()
271
status(f'Extracted from pyproject.toml: {version}')
272
273
elapsed = time.time() - start_time
274
status(f'✅ Version determined in {elapsed:.1f}s')
275
276
# Step 3: Extract release notes
277
step(3, 4, 'Extracting release notes')
278
start_time = time.time()
279
280
status(f'📄 Reading release notes for v{version}...')
281
notes = extract_release_notes(version)
282
lines_count = len(notes.split('\n'))
283
284
elapsed = time.time() - start_time
285
status(f'✅ Extracted {lines_count} lines of release notes in {elapsed:.1f}s')
286
287
# Step 4: Create the release
288
step(4, 4, 'Creating GitHub release')
289
create_release(version, notes, dry_run=args.dry_run)
290
291
total_elapsed = time.time() - total_start_time
292
print('=' * 50, file=sys.stderr)
293
if args.dry_run:
294
print(f'🔍 Dry run completed in {total_elapsed:.1f}s', file=sys.stderr)
295
else:
296
print(f'🎉 GitHub release created successfully in {total_elapsed:.1f}s!', file=sys.stderr)
297
print(f'🏷️ Version: v{version}', file=sys.stderr)
298
299
except Exception as e:
300
print(f'❌ Error: {e}', file=sys.stderr)
301
sys.exit(1)
302
303
304
if __name__ == '__main__':
305
main()
306
307