#!/usr/bin/env python3 """Semantic version helper for MercadoDeVida. Source of truth: project/VERSION. Mirrors the version into package.json files that are part of project/. """ from __future__ import annotations import argparse import json import re import subprocess from pathlib import Path ROOT = Path(__file__).resolve().parents[1] VERSION_FILE = ROOT / 'project' / 'VERSION' PACKAGE_FILES = [ ROOT / 'project' / 'package.json', ROOT / 'project' / 'apps' / 'admin' / 'package.json', ROOT / 'project' / 'apps' / 'pos' / 'package.json', ROOT / 'project' / 'frontend' / 'package.json', ROOT / 'project' / 'storefront' / 'package.json', ] PACKAGE_LOCK_FILES = [ ROOT / 'project' / 'package-lock.json', ROOT / 'project' / 'apps' / 'admin' / 'package-lock.json', ROOT / 'project' / 'apps' / 'pos' / 'package-lock.json', ROOT / 'project' / 'frontend' / 'package-lock.json', ROOT / 'project' / 'storefront' / 'package-lock.json', ] SEMVER_RE = re.compile(r'^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:[-+][0-9A-Za-z.-]+)?$') def read_version() -> str: version = VERSION_FILE.read_text(encoding='utf-8').strip() if not SEMVER_RE.match(version): raise SystemExit(f'[FAIL] Invalid semver in {VERSION_FILE}: {version}') return version def write_version(version: str) -> None: if not SEMVER_RE.match(version): raise SystemExit(f'[FAIL] Invalid semver: {version}') VERSION_FILE.write_text(version + '\n', encoding='utf-8') for path in PACKAGE_FILES: if not path.exists(): continue data = json.loads(path.read_text(encoding='utf-8')) data['version'] = version path.write_text(json.dumps(data, indent=2, ensure_ascii=False) + '\n', encoding='utf-8') for path in PACKAGE_LOCK_FILES: if not path.exists(): continue data = json.loads(path.read_text(encoding='utf-8')) if 'version' in data: data['version'] = version root_package = data.get('packages', {}).get('') if isinstance(root_package, dict) and 'version' in root_package: root_package['version'] = version path.write_text(json.dumps(data, indent=2, ensure_ascii=False) + '\n', encoding='utf-8') def bump(version: str, part: str) -> str: major, minor, patch = [int(p) for p in version.split('-', 1)[0].split('+', 1)[0].split('.')] if part == 'major': return f'{major + 1}.0.0' if part == 'minor': return f'{major}.{minor + 1}.0' if part == 'patch': return f'{major}.{minor}.{patch + 1}' raise SystemExit(f'[FAIL] Unsupported bump part: {part}') def run(cmd: list[str]) -> None: subprocess.run(cmd, cwd=ROOT, check=True) def main() -> int: parser = argparse.ArgumentParser(description='Manage project semantic version') sub = parser.add_subparsers(dest='command', required=True) sub.add_parser('show', help='print current version') bump_parser = sub.add_parser('bump', help='bump semantic version') bump_parser.add_argument('part', choices=['major', 'minor', 'patch']) bump_parser.add_argument('--commit', action='store_true', help='create conventional release commit') bump_parser.add_argument('--tag', action='store_true', help='create git tag vX.Y.Z (requires --commit or clean index)') set_parser = sub.add_parser('set', help='set exact semantic version') set_parser.add_argument('version') args = parser.parse_args() current = read_version() if args.command == 'show': print(current) return 0 if args.command == 'set': write_version(args.version) print(f'[OK] Version set: {current} -> {args.version}') return 0 next_version = bump(current, args.part) write_version(next_version) print(f'[OK] Version bumped: {current} -> {next_version}') if args.commit: msg = f'chore(release): v{next_version}' run(['python3', 'scripts/validate_conventional_commit.py', '--message', msg]) run([ 'git', 'add', 'project/VERSION', *[str(p.relative_to(ROOT)) for p in PACKAGE_FILES if p.exists()], *[str(p.relative_to(ROOT)) for p in PACKAGE_LOCK_FILES if p.exists()], ]) run(['git', 'commit', '-m', msg]) if args.tag: run(['git', 'tag', f'v{next_version}']) print(f'[OK] Tagged v{next_version}') return 0 if __name__ == '__main__': raise SystemExit(main())