Files
PortProtonQt/dev-scripts/bump_ver.py
2025-12-12 00:22:11 +05:00

283 lines
11 KiB
Python
Executable File

#!/usr/bin/env python3
import argparse
import re
import subprocess
from pathlib import Path
from datetime import date, datetime
# Base directory of the project
BASE_DIR = Path(__file__).parent.parent
# Specific project files
APPIMAGE_RECIPE = BASE_DIR / "build-aux" / "AppImageBuilder.yml"
ARCH_PKGBUILD = BASE_DIR / "build-aux" / "PKGBUILD"
FEDORA_SPEC = BASE_DIR / "build-aux" / "fedora.spec"
PYPROJECT = BASE_DIR / "pyproject.toml"
APP_PY = BASE_DIR / "portprotonqt" / "app.py"
GITEA_WORKFLOW = BASE_DIR / ".gitea" / "workflows" / "build.yml"
CHANGELOG = BASE_DIR / "CHANGELOG.md"
DEBIAN_CHANGELOG = BASE_DIR / "debian" / "changelog"
def bump_appimage(path: Path, old: str, new: str) -> bool:
"""
Update only the 'version' field under app_info in AppImageBuilder.yml
"""
if not path.exists():
return False
text = path.read_text(encoding='utf-8')
pattern = re.compile(r"(?m)^(\s*version:\s*)" + re.escape(old) + r"$")
new_text, count = pattern.subn(lambda m: m.group(1) + new, text)
if count:
path.write_text(new_text, encoding='utf-8')
return bool(count)
def bump_arch(path: Path, old: str, new: str) -> bool:
"""
Update pkgver in PKGBUILD
"""
if not path.exists():
return False
text = path.read_text(encoding='utf-8')
pattern = re.compile(r"(?m)^(pkgver=)" + re.escape(old) + r"$")
new_text, count = pattern.subn(lambda m: m.group(1) + new, text)
if count:
path.write_text(new_text, encoding='utf-8')
return bool(count)
def bump_fedora(path: Path, old: str, new: str) -> bool:
"""
Update only the '%global pypi_version' line in fedora.spec
"""
if not path.exists():
return False
text = path.read_text(encoding='utf-8')
pattern = re.compile(r"(?m)^(%global\s+pypi_version\s+)" + re.escape(old) + r"$")
new_text, count = pattern.subn(lambda m: m.group(1) + new, text)
if count:
path.write_text(new_text, encoding='utf-8')
return bool(count)
def bump_pyproject(path: Path, old: str, new: str) -> bool:
"""
Update version in pyproject.toml under [project]
"""
if not path.exists():
return False
text = path.read_text(encoding='utf-8')
pattern = re.compile(r"(?m)^(version\s*=\s*)\"" + re.escape(old) + r"\"$")
new_text, count = pattern.subn(lambda m: m.group(1) + f'"{new}"', text)
if count:
path.write_text(new_text, encoding='utf-8')
return bool(count)
def bump_app_py(path: Path, old: str, new: str) -> bool:
"""
Update __app_version__ in app.py
"""
if not path.exists():
return False
text = path.read_text(encoding='utf-8')
pattern = re.compile(r"(?m)^(\s*__app_version__\s*=\s*)\"" + re.escape(old) + r"\"$")
new_text, count = pattern.subn(lambda m: m.group(1) + f'"{new}"', text)
if count:
path.write_text(new_text, encoding='utf-8')
return bool(count)
def bump_workflow(path: Path, old: str, new: str) -> bool:
"""
Update VERSION in Gitea Actions workflow
"""
if not path.exists():
return False
text = path.read_text(encoding='utf-8')
pattern = re.compile(r"(?m)^(\s*VERSION:\s*)" + re.escape(old) + r"$")
new_text, count = pattern.subn(lambda m: m.group(1) + new, text)
if count:
path.write_text(new_text, encoding='utf-8')
return bool(count)
def bump_changelog(path: Path, old: str, new: str) -> bool:
"""
Update [Unreleased] to [new] - YYYY-MM-DD in CHANGELOG.md
"""
if not path.exists():
return False
text = path.read_text(encoding='utf-8')
pattern = re.compile(r"(?m)^##\s*\[Unreleased\]$")
current_date = date.today().strftime('%Y-%m-%d')
new_text, count = pattern.subn(f"## [{new}] - {current_date}", text)
if count:
path.write_text(new_text, encoding='utf-8')
return bool(count)
def bump_debian_changelog(path: Path, old: str, new: str) -> bool:
"""
Update debian/changelog with new version
"""
if not path.exists():
return False
# Extract changelog entries from CHANGELOG.md for this version
changelog_md_path = BASE_DIR / "CHANGELOG.md"
changelog_entries = []
changelog_date = None
if changelog_md_path.exists():
changelog_text = changelog_md_path.read_text(encoding='utf-8')
lines = changelog_text.splitlines()
# Find the section for the new version and extract the date
start_reading = False
end_reading = False
in_contributors_section = False
for line in lines:
if line.startswith(f"## [{new}]"):
# Extract date from line like "## [0.1.9] - 2025-12-08"
date_match = re.search(r'\[.+\] - (\d{4}-\d{2}-\d{2})', line)
if date_match:
changelog_date_str = date_match.group(1)
# Convert to the expected Debian format
date_obj = datetime.strptime(changelog_date_str, '%Y-%m-%d')
changelog_date = date_obj.strftime('%a, %d %b %Y') + " 00:00:00 +0000"
start_reading = True
in_contributors_section = False
continue
elif line.startswith("## [") and start_reading:
end_reading = True
break
elif line.strip().lower() == "### contributors":
# Start of contributors section - skip following lines until next section
in_contributors_section = True
continue
# Skip section headers and contributor sections
if start_reading and not end_reading and not in_contributors_section:
stripped_line = line.strip()
if stripped_line and not line.startswith("#") and not line.startswith("[") and not line.lower().startswith("###"):
# Check if this line is a list item with changes
if re.match(r'^\s*[*-]\s+', line):
# Remove markdown list formatting and add proper Debian format
clean_line = re.sub(r'^\s*[*-]\s+', ' * ', line.rstrip())
# Remove common markdown formatting like backticks
clean_line = re.sub(r'`([^`]+)`', r'"\1"', clean_line) # Replace `code` with "code"
changelog_entries.append(clean_line)
# Also include lines that are sub-items (indented changes)
elif line.startswith(" ") and re.match(r'^\s*[*-]\s+', line[4:]):
clean_line = re.sub(r'^\s*[*-]\s+', ' * ', line[4:].rstrip())
clean_line = " " + clean_line # Add extra indentation
# Remove common markdown formatting
clean_line = re.sub(r'`([^`]+)`', r'"\1"', clean_line)
changelog_entries.append(clean_line)
# If no specific entries found for this version, use generic message
if not changelog_entries:
changelog_entries = [" * New upstream release"]
# Use changelog date if available, otherwise use current time
current_time = changelog_date if changelog_date else datetime.now().strftime('%a, %d %b %Y %H:%M:%S +0000')
# Read the existing changelog to get maintainer info and other fields
text = path.read_text(encoding='utf-8')
# If the file is empty or doesn't contain proper maintainer info, use a default
lines = text.splitlines()
if not lines or not any(line.startswith(" -- ") for line in lines):
# Create a default changelog entry with proper format
package_name = "portprotonqt"
new_version_line = f"{package_name} ({new}-1) unstable; urgency=medium"
# Default maintainer info from the original file
default_maintainer = "Boris Yumankulov <boria138@altlinux.org>"
maintainer_line = f" -- {default_maintainer} {current_time}"
new_content = new_version_line + "\n\n" + "\n".join(changelog_entries) + "\n\n" + maintainer_line + "\n"
else:
# Extract the header template from the current first entry
header_parts = []
entry_end_index = 0
for i, line in enumerate(lines):
header_parts.append(line)
if line.startswith(" -- "):
entry_end_index = i + 1
break
# Construct new changelog entry
new_entry_lines = []
if header_parts:
# Parse the first line to extract package name (before the version)
first_line = header_parts[0]
# Extract package name by getting everything before the opening parenthesis
if '(' in first_line:
package_name = first_line.split('(')[0].strip()
new_version_line = f"{package_name} ({new}-1) unstable; urgency=medium"
else:
# Fallback: if no parentheses found, use a default format
new_version_line = f"portprotonqt ({new}-1) unstable; urgency=medium"
new_entry_lines.append(new_version_line)
# Add the changelog entries
new_entry_lines.extend(changelog_entries)
# Add the maintainer info and timestamp
for j in range(1, len(header_parts)):
if header_parts[j].startswith(" -- "):
# Extract the maintainer information (everything after "-- ")
maintainer_part = header_parts[j][4:] # Remove leading " -- "
# Extract only the name and email, ignore timestamp
maintainer_info = maintainer_part.split(' ')[0].strip()
new_entry_lines.append(f" -- {maintainer_info} {current_time}")
elif not header_parts[j].startswith(" *"): # Skip existing changes since we added new ones
new_entry_lines.append(header_parts[j])
# Reconstruct the file with new entry at the top followed by the rest
new_content = '\n'.join(new_entry_lines) + '\n' + '\n'.join(lines[entry_end_index:])
path.write_text(new_content, encoding='utf-8')
return True
def main():
parser = argparse.ArgumentParser(description='Bump project version in specific files')
parser.add_argument('old', help='Old version string')
parser.add_argument('new', help='New version string')
args = parser.parse_args()
old, new = args.old, args.new
tasks = [
(APPIMAGE_RECIPE, bump_appimage),
(ARCH_PKGBUILD, bump_arch),
(FEDORA_SPEC, bump_fedora),
(PYPROJECT, bump_pyproject),
(APP_PY, bump_app_py),
(GITEA_WORKFLOW, bump_workflow),
(CHANGELOG, bump_changelog),
(DEBIAN_CHANGELOG, bump_debian_changelog)
]
updated = []
for path, func in tasks:
if func(path, old, new):
updated.append(path.relative_to(BASE_DIR))
if updated:
print(f"Updated version from {old} to {new} in {len(updated)} files:")
for p in sorted(updated):
print(f" - {p}")
try:
subprocess.run(["uv", "lock"], check=True)
print("Regenerated uv.lock")
except subprocess.CalledProcessError as e:
print(f"Failed to regenerate uv.lock: {e}")
else:
print(f"No occurrences of version {old} found in specified files.")
if __name__ == '__main__':
main()