Path: blob/main/resources/create_release.py
798 views
#!/usr/bin/env python31"""2Script to create GitHub releases for SingleStoreDB Python SDK.34This script automatically:51. Extracts the current version from setup.cfg62. Finds the matching release notes from docs/src/whatsnew.rst73. Creates a temporary notes file84. Creates a GitHub release using gh CLI95. Cleans up temporary files1011Usage:12python create_release.py [--version VERSION] [--dry-run]1314Examples:15python create_release.py # Use current version from pyproject.toml16python create_release.py --version 1.15.6 # Use specific version17python create_release.py --dry-run # Preview without executing18"""19from __future__ import annotations2021import argparse22import os23import re24import subprocess25import sys26import tempfile27import time28from pathlib import Path293031def status(message: str) -> None:32"""Show status messages to indicate progress."""33print(f'📋 {message}', file=sys.stderr)343536def step(step_num: int, total_steps: int, message: str) -> None:37"""Show a numbered step with progress."""38print(f'📍 Step {step_num}/{total_steps}: {message}', file=sys.stderr)394041def get_version_from_pyproject() -> str:42"""Extract the current version from pyproject.toml."""43pyproject_path = Path(__file__).parent.parent / 'pyproject.toml'4445if not pyproject_path.exists():46raise FileNotFoundError(f'Could not find pyproject.toml at {pyproject_path}')4748with open(pyproject_path, 'r') as f:49content = f.read()5051match = re.search(r'^version\s*=\s*["\'](.+)["\']$', content, re.MULTILINE)52if not match:53raise ValueError('Could not find version in pyproject.toml')5455return match.group(1).strip()565758def extract_release_notes(version: str) -> str:59"""Extract release notes for the specified version from whatsnew.rst."""60whatsnew_path = Path(__file__).parent.parent / 'docs' / 'src' / 'whatsnew.rst'6162if not whatsnew_path.exists():63raise FileNotFoundError(f'Could not find whatsnew.rst at {whatsnew_path}')6465with open(whatsnew_path, 'r') as f:66content = f.read()6768# Look for the version section69version_pattern = rf'^v{re.escape(version)}\s*-\s*.*$'70version_match = re.search(version_pattern, content, re.MULTILINE)7172if not version_match:73raise ValueError(f'Could not find release notes for version {version} in whatsnew.rst')7475# Find the start of the version section76start_pos = version_match.end()7778# Find the separator line (dashes)79lines = content[start_pos:].split('\n')80notes_start = None8182for i, line in enumerate(lines):83if line.strip() and all(c == '-' for c in line.strip()):84notes_start = i + 185break8687if notes_start is None:88raise ValueError(f'Could not find release notes separator for version {version}')8990# Extract notes until the next version section91notes_lines: list[str] = []92for line in lines[notes_start:]:93# Stop at the next version (starts with 'v' followed by a number)94if re.match(r'^v\d+\.\d+\.\d+', line.strip()):95break96# Stop at empty line followed by version line97if line.strip() == '' and notes_lines:98# Check if next non-empty line is a version99remaining_lines = lines[notes_start + len(notes_lines) + 1:]100for next_line in remaining_lines:101if next_line.strip():102if re.match(r'^v\d+\.\d+\.\d+', next_line.strip()):103break104break105else:106continue107notes_lines.append(line)108109# Clean up the notes110# Remove trailing empty lines111while notes_lines and not notes_lines[-1].strip():112notes_lines.pop()113114if not notes_lines:115raise ValueError(f'No release notes found for version {version}')116117return '\n'.join(notes_lines)118119120def create_release(version: str, notes: str, dry_run: bool = False) -> None:121"""Create a GitHub release using gh CLI."""122tag = f'v{version}'123title = f'SingleStoreDB v{version}'124125# Create temporary file for release notes126with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f:127f.write(notes)128notes_file = f.name129130try:131# Construct the gh release create command132cmd = [133'gh', 'release', 'create', tag,134'--title', title,135'--notes-file', notes_file,136]137138if dry_run:139status('🔍 DRY RUN - Preview mode')140print('=' * 50, file=sys.stderr)141print(f'Command: {" ".join(cmd)}', file=sys.stderr)142print(f'Tag: {tag}', file=sys.stderr)143print(f'Title: {title}', file=sys.stderr)144print(f'Notes file: {notes_file}', file=sys.stderr)145print('=' * 50, file=sys.stderr)146print('Release notes content:', file=sys.stderr)147print(notes, file=sys.stderr)148print('=' * 50, file=sys.stderr)149return150151status(f'🚀 Creating GitHub release for v{version}...')152status(f' 📎 Tag: {tag}')153status(f' 📝 Title: {title}')154155start_time = time.time()156157# Execute the command158result = subprocess.run(cmd, capture_output=True, text=True)159160elapsed = time.time() - start_time161162if result.returncode != 0:163print(f'❌ Error creating GitHub release (after {elapsed:.1f}s):', file=sys.stderr)164print(f' STDOUT: {result.stdout}', file=sys.stderr)165print(f' STDERR: {result.stderr}', file=sys.stderr)166sys.exit(1)167168status(f'✅ GitHub release created successfully in {elapsed:.1f}s!')169if result.stdout.strip():170status(f' 🔗 {result.stdout.strip()}')171172finally:173# Clean up temporary file174try:175os.unlink(notes_file)176except OSError:177pass178179180def check_prerequisites() -> None:181"""Check that required tools are available."""182status('🔍 Checking prerequisites...')183184# Check if gh CLI is available185try:186result = subprocess.run(['gh', '--version'], capture_output=True, text=True, check=True)187version = result.stdout.strip().split()[2]188status(f' ✓ GitHub CLI found: v{version}')189except (subprocess.CalledProcessError, FileNotFoundError):190print('❌ GitHub CLI (gh) is not installed or not in PATH', file=sys.stderr)191print(' Please install it from https://cli.github.com/', file=sys.stderr)192sys.exit(1)193194# Check if we're in a git repository195try:196subprocess.run(['git', 'rev-parse', '--git-dir'], capture_output=True, check=True)197status(' ✓ Git repository detected')198except subprocess.CalledProcessError:199print('❌ Not in a git repository', file=sys.stderr)200sys.exit(1)201202# Check if we're authenticated with GitHub203try:204result = subprocess.run(['gh', 'auth', 'status'], capture_output=True, text=True)205if result.returncode != 0:206print('❌ Not authenticated with GitHub', file=sys.stderr)207print(' Please run: gh auth login', file=sys.stderr)208sys.exit(1)209else:210# Extract username from output211lines = result.stderr.split('\n')212username = 'unknown'213for line in lines:214if 'Logged in to github.com as' in line:215username = line.split()[-1]216break217status(f' ✓ GitHub authenticated as {username}')218except subprocess.CalledProcessError:219print('❌ Could not check GitHub authentication status', file=sys.stderr)220sys.exit(1)221222status('✅ All prerequisites satisfied')223224225def main() -> None:226parser = argparse.ArgumentParser(227description='Create GitHub release for SingleStoreDB Python SDK',228formatter_class=argparse.RawDescriptionHelpFormatter,229epilog='''Examples:230%(prog)s # Use current version from pyproject.toml231%(prog)s --version 1.15.6 # Use specific version232%(prog)s --dry-run # Preview without executing''',233)234235parser.add_argument(236'--version',237help='Version to release (default: extract from pyproject.toml)',238)239240parser.add_argument(241'--dry-run',242action='store_true',243help='Show what would be done without executing',244)245246args = parser.parse_args()247248try:249total_start_time = time.time()250251print('🚀 Starting GitHub release creation', file=sys.stderr)252print('=' * 50, file=sys.stderr)253254# Step 1: Check prerequisites (unless dry run)255if not args.dry_run:256step(1, 4, 'Checking prerequisites')257check_prerequisites()258else:259step(1, 4, 'Skipping prerequisites check (dry-run)')260261# Step 2: Get version262step(2, 4, 'Determining version')263start_time = time.time()264265if args.version:266version = args.version267status(f'Using specified version: {version}')268else:269version = get_version_from_pyproject()270status(f'Extracted from pyproject.toml: {version}')271272elapsed = time.time() - start_time273status(f'✅ Version determined in {elapsed:.1f}s')274275# Step 3: Extract release notes276step(3, 4, 'Extracting release notes')277start_time = time.time()278279status(f'📄 Reading release notes for v{version}...')280notes = extract_release_notes(version)281lines_count = len(notes.split('\n'))282283elapsed = time.time() - start_time284status(f'✅ Extracted {lines_count} lines of release notes in {elapsed:.1f}s')285286# Step 4: Create the release287step(4, 4, 'Creating GitHub release')288create_release(version, notes, dry_run=args.dry_run)289290total_elapsed = time.time() - total_start_time291print('=' * 50, file=sys.stderr)292if args.dry_run:293print(f'🔍 Dry run completed in {total_elapsed:.1f}s', file=sys.stderr)294else:295print(f'🎉 GitHub release created successfully in {total_elapsed:.1f}s!', file=sys.stderr)296print(f'🏷️ Version: v{version}', file=sys.stderr)297298except Exception as e:299print(f'❌ Error: {e}', file=sys.stderr)300sys.exit(1)301302303if __name__ == '__main__':304main()305306307