Files
TimeTracker/scripts/version-manager.py
T
Dries Peeters 4a8607f400 feat: Add major feature updates - integrations, services, and utilities
- Add Google Calendar integration with OAuth 2.0 support
- Implement integration service and workflow engine
- Add new routes: auth, clients, custom_reports, integrations, invoices, team_chat
- Add utility modules: config_manager, email, excel_export, file_upload, permissions_seed
- Add integration view template
- Add Docker permission fixes and enhanced start scripts
- Add migration management utilities and legacy schema migration
- Add validation and version management scripts
- Update setup.py version to 4.9.16

This release significantly expands the application's integration capabilities,
adds new business logic services, and improves infrastructure tooling.
2026-01-09 22:42:53 +01:00

339 lines
13 KiB
Python

#!/usr/bin/env python3
"""
Version Manager for TimeTracker
This script helps manage version tags for releases and builds
"""
import os
import sys
import subprocess
import argparse
from datetime import datetime
import re
import shlex
class VersionManager:
def __init__(self):
self.repo_path = os.getcwd()
def run_command(self, command, capture_output=True):
"""Run a command and return the result
Args:
command: Command string or list of command arguments
capture_output: Whether to capture output
"""
try:
# If command is a string, split it safely
if isinstance(command, str):
# For git commands with complex quoting, use shlex
try:
cmd_list = shlex.split(command)
except ValueError:
# Fallback to simple split
cmd_list = command.split()
else:
cmd_list = command
result = subprocess.run(
cmd_list,
capture_output=capture_output,
text=True,
cwd=self.repo_path
)
if result.returncode != 0:
print(f"Error running command: {' '.join(cmd_list)}")
if hasattr(result, 'stderr') and result.stderr:
print(f"Error: {result.stderr}")
return None
return result.stdout.strip() if capture_output and hasattr(result, 'stdout') else result
except Exception as e:
print(f"Exception running command: {e}")
return None
def get_current_branch(self):
"""Get the current git branch"""
return self.run_command(['git', 'branch', '--show-current'])
def get_latest_tag(self):
"""Get the latest git tag"""
result = self.run_command(['git', 'describe', '--tags', '--abbrev=0'])
if result:
return result
# If command fails (no tags), return 'none'
return 'none'
def get_commit_count(self):
"""Get the number of commits since the last tag"""
latest_tag = self.get_latest_tag()
if latest_tag == 'none':
return self.run_command(['git', 'rev-list', '--count', 'HEAD'])
else:
# Sanitize tag name to prevent command injection
safe_tag = re.sub(r'[^a-zA-Z0-9._/-]', '', latest_tag)
return self.run_command(['git', 'rev-list', '--count', f'{safe_tag}..HEAD'])
def get_commit_hash(self, short=True):
"""Get the current commit hash"""
if short:
return self.run_command(['git', 'rev-parse', '--short', 'HEAD'])
else:
return self.run_command(['git', 'rev-parse', 'HEAD'])
def validate_version_format(self, version):
"""Validate version format"""
# Allow various version formats
patterns = [
r'^v?\d+\.\d+\.\d+$', # v1.2.3 or 1.2.3
r'^v?\d+\.\d+$', # v1.2 or 1.2
r'^v?\d+$', # v1 or 1
r'^build-\d+$', # build-123
r'^rc\d+$', # rc1
r'^beta\d+$', # beta1
r'^alpha\d+$', # alpha1
r'^dev-\d+$', # dev-123
]
for pattern in patterns:
if re.match(pattern, version):
return True
return False
def suggest_next_version(self, current_version):
"""Suggest the next version based on current version"""
if current_version == 'none':
return 'v1.0.0'
# Remove 'v' prefix if present
clean_version = current_version.lstrip('v')
try:
parts = clean_version.split('.')
if len(parts) >= 3:
major, minor, patch = int(parts[0]), int(parts[1]), int(parts[2])
return f"v{major}.{minor}.{patch + 1}"
elif len(parts) == 2:
major, minor = int(parts[0]), int(parts[1])
return f"v{major}.{minor + 1}.0"
elif len(parts) == 1:
major = int(parts[0])
return f"v{major + 1}.0.0"
except ValueError:
pass
return f"{current_version}-next"
def create_tag(self, version, message=None, push=True):
"""Create a git tag"""
if not self.validate_version_format(version):
print(f"Error: Invalid version format '{version}'")
print("Valid formats: v1.2.3, 1.2.3, build-123, rc1, beta1, etc.")
return False
# Ensure version starts with 'v' for semantic versions
if re.match(r'^v?\d+\.\d+\.\d+$', version) and not version.startswith('v'):
version = f"v{version}"
# Generate message if not provided
if not message:
message = f"Release {version}"
print(f"Creating tag: {version}")
print(f"Message: {message}")
# Create the tag using list to avoid shell injection
# Version and message are already validated/sanitized
tag_cmd = ['git', 'tag', '-a', version, '-m', message]
if not self.run_command(tag_cmd, capture_output=False):
print("Failed to create tag")
return False
print(f"✓ Tag '{version}' created successfully")
# Push tag if requested
if push:
print("Pushing tag to remote...")
push_cmd = ['git', 'push', 'origin', version]
if not self.run_command(push_cmd, capture_output=False):
print("Failed to push tag to remote")
return False
print(f"✓ Tag '{version}' pushed to remote")
return True
def create_build_tag(self, build_number=None):
"""Create a build tag with build number"""
if not build_number:
build_number = self.get_commit_count()
branch = self.get_current_branch()
version = f"{branch}-build-{build_number}"
message = f"Build {build_number} from {branch} branch"
return self.create_tag(version, message, push=False)
def list_tags(self):
"""List all tags"""
tags = self.run_command(['git', 'tag', '--sort=-version:refname'])
if tags:
print("Available tags:")
for tag in tags.split('\n'):
if tag.strip():
print(f" {tag}")
else:
print("No tags found")
def show_tag_info(self, tag):
"""Show information about a specific tag"""
if not tag:
tag = self.get_latest_tag()
if tag == 'none':
print("No tags found")
return
# Sanitize tag name to prevent command injection
safe_tag = re.sub(r'[^a-zA-Z0-9._/-]', '', tag)
print(f"Tag: {safe_tag}")
print(f"Commit: {self.run_command(['git', 'rev-parse', safe_tag])}")
print(f"Date: {self.run_command(['git', 'log', '-1', '--format=%cd', safe_tag])}")
print(f"Message: {self.run_command(['git', 'log', '-1', '--format=%s', safe_tag])}")
# Show commits since this tag
commits_since = self.run_command(['git', 'log', '--oneline', f'{safe_tag}..HEAD'])
if commits_since:
print(f"\nCommits since {safe_tag}:")
for commit in commits_since.split('\n')[:10]: # Show last 10 commits
if commit.strip():
print(f" {commit}")
commit_lines = commits_since.split('\n')
if len(commit_lines) > 10:
print(f" ... and {len(commit_lines) - 10} more")
def show_status(self):
"""Show current version status"""
print("=== Version Status ===")
print(f"Current branch: {self.get_current_branch()}")
print(f"Latest tag: {self.get_latest_tag()}")
print(f"Commits since last tag: {self.get_commit_count()}")
print(f"Current commit: {self.get_commit_hash()}")
current_tag = self.get_latest_tag()
if current_tag != 'none':
next_version = self.suggest_next_version(current_tag)
print(f"Suggested next version: {next_version}")
print("=====================")
def main():
parser = argparse.ArgumentParser(description='Version Manager for TimeTracker')
parser.add_argument('action', choices=['tag', 'build', 'list', 'info', 'status', 'suggest', 'release', 'changelog'],
help='Action to perform')
parser.add_argument('--version', '-v', help='Version string (e.g., v1.2.3, build-123)')
parser.add_argument('--message', '-m', help='Tag message')
parser.add_argument('--build-number', '-b', type=int, help='Build number for build tags')
parser.add_argument('--no-push', action='store_true', help='Don\'t push tag to remote')
parser.add_argument('--tag', '-t', help='Tag to show info for (for info action)')
parser.add_argument('--pre-release', action='store_true', help='Mark as pre-release')
parser.add_argument('--changelog', action='store_true', help='Generate changelog')
parser.add_argument('--github-release', action='store_true', help='Create GitHub release')
args = parser.parse_args()
vm = VersionManager()
if args.action == 'tag':
if not args.version:
print("Error: Version required for tag action")
print("Use --version or -v to specify version")
sys.exit(1)
vm.create_tag(args.version, args.message, push=not args.no_push)
elif args.action == 'build':
vm.create_build_tag(args.build_number)
elif args.action == 'list':
vm.list_tags()
elif args.action == 'info':
vm.show_tag_info(args.tag)
elif args.action == 'status':
vm.show_status()
elif args.action == 'suggest':
current_tag = vm.get_latest_tag()
if current_tag != 'none':
next_version = vm.suggest_next_version(current_tag)
print(f"Current version: {current_tag}")
print(f"Suggested next version: {next_version}")
else:
print("No current version found")
print("Suggested first version: v1.0.0")
elif args.action == 'release':
if not args.version:
print("Error: Version required for release action")
print("Use --version or -v to specify version")
sys.exit(1)
print(f"🚀 Creating release {args.version}...")
# Create tag
if vm.create_tag(args.version, args.message, push=not args.no_push):
print(f"✅ Tag {args.version} created successfully")
# Generate changelog if requested
if args.changelog:
print("📋 Generating changelog...")
# Sanitize version before use
safe_version = re.sub(r'[^a-zA-Z0-9._/-]', '', args.version)
changelog_cmd = ['python', 'scripts/generate-changelog.py', safe_version]
if vm.run_command(changelog_cmd, capture_output=False):
print("✅ Changelog generated successfully")
else:
print("⚠️ Changelog generation failed")
# Create GitHub release if requested
if args.github_release:
print("🐙 Creating GitHub release...")
# Sanitize version before use
safe_version = re.sub(r'[^a-zA-Z0-9._/-]', '', args.version)
github_cmd = ['gh', 'release', 'create', safe_version]
if args.pre_release:
github_cmd.append('--prerelease')
if args.changelog and os.path.exists("CHANGELOG.md"):
github_cmd.extend(['--notes-file', 'CHANGELOG.md'])
elif args.message:
# Sanitize message to prevent command injection
safe_message = args.message.replace("'", "'\"'\"'")
github_cmd.extend(['--notes', safe_message])
if vm.run_command(github_cmd, capture_output=False):
print("✅ GitHub release created successfully")
else:
print("⚠️ GitHub release creation failed (make sure 'gh' CLI is installed and authenticated)")
else:
print("❌ Failed to create tag")
sys.exit(1)
elif args.action == 'changelog':
current_tag = vm.get_latest_tag()
version = args.version or vm.suggest_next_version(current_tag)
# Sanitize version before use
safe_version = re.sub(r'[^a-zA-Z0-9._/-]', '', version)
print(f"📋 Generating changelog for {safe_version}...")
changelog_cmd = ['python', 'scripts/generate-changelog.py', safe_version]
if vm.run_command(changelog_cmd, capture_output=False):
print("✅ Changelog generated successfully")
else:
print("❌ Changelog generation failed")
if __name__ == '__main__':
main()