Files
mercadodevida/scripts/version.py

129 lines
4.4 KiB
Python
Executable File

#!/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())