mirror of
https://github.com/Akkudoktor-EOS/EOS.git
synced 2025-11-21 04:46:31 +00:00
chore: automate development version and release generation (#772)
Some checks failed
Bump Version / Bump Version Workflow (push) Has been cancelled
docker-build / platform-excludes (push) Has been cancelled
docker-build / build (push) Has been cancelled
docker-build / merge (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
Run Pytest on Pull Request / test (push) Has been cancelled
Some checks failed
Bump Version / Bump Version Workflow (push) Has been cancelled
docker-build / platform-excludes (push) Has been cancelled
docker-build / build (push) Has been cancelled
docker-build / merge (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
Run Pytest on Pull Request / test (push) Has been cancelled
This change introduces a GitHub Action to automate release creation, including proper tagging and automatic addition of a development marker to the version. A hash is also appended to development versions to make their state easier to distinguish. Tests and release documentation have been updated to reflect the revised release workflow. Several files now retrieve the current version dynamically. The test --full-run option has been rename to --finalize to make clear it is to do commit finalization testing. Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
This commit is contained in:
70
scripts/bump_dev_version.py
Normal file
70
scripts/bump_dev_version.py
Normal file
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Update VERSION_BASE in version.py after a release tag.
|
||||
|
||||
Behavior:
|
||||
- Read VERSION_BASE from version.py
|
||||
- Strip ANY existing "+dev" suffix
|
||||
- Append exactly one "+dev"
|
||||
- Write back the updated file
|
||||
|
||||
This ensures:
|
||||
0.2.0 --> 0.2.0+dev
|
||||
0.2.0+dev --> 0.2.0+dev
|
||||
0.2.0+dev+dev -> 0.2.0+dev
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
VERSION_FILE = ROOT / "src" / "akkudoktoreos" / "core" / "version.py"
|
||||
|
||||
|
||||
def bump_dev_version_file(file: Path) -> str:
|
||||
text = file.read_text(encoding="utf-8")
|
||||
|
||||
# Extract current version
|
||||
m = re.search(r'^VERSION_BASE\s*=\s*["\']([^"\']+)["\']',
|
||||
text, flags=re.MULTILINE)
|
||||
if not m:
|
||||
raise ValueError("VERSION_BASE not found")
|
||||
|
||||
base_version = m.group(1)
|
||||
|
||||
# Remove trailing +dev if present → ensure idempotency
|
||||
cleaned = re.sub(r'(\+dev)+$', '', base_version)
|
||||
|
||||
# Append +dev
|
||||
new_version = f"{cleaned}+dev"
|
||||
|
||||
# Replace inside file content
|
||||
new_text = re.sub(
|
||||
r'^VERSION_BASE\s*=\s*["\']([^"\']+)["\']',
|
||||
f'VERSION_BASE = "{new_version}"',
|
||||
text,
|
||||
flags=re.MULTILINE
|
||||
)
|
||||
|
||||
file.write_text(new_text, encoding="utf-8")
|
||||
|
||||
return new_version
|
||||
|
||||
|
||||
def main():
|
||||
# Use CLI argument or fallback default path
|
||||
version_file = Path(sys.argv[1]) if len(sys.argv) > 1 else VERSION_FILE
|
||||
|
||||
try:
|
||||
new_version = bump_dev_version_file(version_file)
|
||||
except Exception as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# MUST print to stdout
|
||||
print(new_version)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,170 +0,0 @@
|
||||
"""Update version strings in multiple project files only if the old version matches.
|
||||
|
||||
This script updates version information in:
|
||||
- pyproject.toml
|
||||
- src/akkudoktoreos/core/version.py
|
||||
- src/akkudoktoreos/data/default.config.json
|
||||
- Makefile
|
||||
|
||||
Supported version formats:
|
||||
- __version__ = "<version>"
|
||||
- version = "<version>"
|
||||
- "version": "<version>"
|
||||
- VERSION ?: <version>
|
||||
|
||||
It will:
|
||||
- Replace VERSION → NEW_VERSION if the old version is found.
|
||||
- Report which files were updated.
|
||||
- Report which files contained mismatched versions.
|
||||
- Report which files had no version.
|
||||
|
||||
Usage:
|
||||
python bump_version.py VERSION NEW_VERSION
|
||||
|
||||
Args:
|
||||
VERSION (str): Version expected before replacement.
|
||||
NEW_VERSION (str): Version to write.
|
||||
|
||||
"""
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import glob
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from typing import List, Tuple
|
||||
|
||||
# Patterns to match version strings
|
||||
VERSION_PATTERNS = [
|
||||
re.compile(r'(__version__\s*=\s*")(?P<ver>[^"]+)(")'),
|
||||
re.compile(r'(version\s*=\s*")(?P<ver>[^"]+)(")'),
|
||||
re.compile(r'("version"\s*:\s*")(?P<ver>[^"]+)(")'),
|
||||
re.compile(r'(VERSION\s*\?=\s*)(?P<ver>[^\s]+)'), # For Makefile: VERSION ?= 0.2.0
|
||||
]
|
||||
|
||||
# Default files to process
|
||||
DEFAULT_FILES = [
|
||||
"pyproject.toml",
|
||||
"src/akkudoktoreos/core/version.py",
|
||||
"src/akkudoktoreos/data/default.config.json",
|
||||
"Makefile",
|
||||
]
|
||||
|
||||
|
||||
def backup_file(file_path: str) -> str:
|
||||
"""Create a backup of the given file with a .bak suffix.
|
||||
|
||||
Args:
|
||||
file_path: Path to the file to backup.
|
||||
|
||||
Returns:
|
||||
Path to the backup file.
|
||||
"""
|
||||
backup_path = f"{file_path}.bak"
|
||||
shutil.copy2(file_path, backup_path)
|
||||
return backup_path
|
||||
|
||||
|
||||
def replace_version_in_file(
|
||||
file_path: Path, old_version: str, new_version: str, dry_run: bool = False
|
||||
) -> Tuple[bool, bool]:
|
||||
"""
|
||||
Replace old_version with new_version in the given file if it matches.
|
||||
|
||||
Args:
|
||||
file_path: Path to the file to modify.
|
||||
old_version: The old version to replace.
|
||||
new_version: The new version to set.
|
||||
dry_run: If True, don't actually modify files.
|
||||
|
||||
Returns:
|
||||
Tuple[bool, bool]: (file_would_be_updated, old_version_found)
|
||||
"""
|
||||
content = file_path.read_text()
|
||||
new_content = content
|
||||
old_version_found = False
|
||||
file_would_be_updated = False
|
||||
|
||||
for pattern in VERSION_PATTERNS:
|
||||
def repl(match):
|
||||
nonlocal old_version_found, file_would_be_updated
|
||||
ver = match.group("ver")
|
||||
if ver == old_version:
|
||||
old_version_found = True
|
||||
file_would_be_updated = True
|
||||
# Some patterns have 3 groups (like quotes)
|
||||
if len(match.groups()) == 3:
|
||||
return f"{match.group(1)}{new_version}{match.group(3)}"
|
||||
else:
|
||||
return f"{match.group(1)}{new_version}"
|
||||
return match.group(0)
|
||||
|
||||
new_content = pattern.sub(repl, new_content)
|
||||
|
||||
if file_would_be_updated:
|
||||
if dry_run:
|
||||
print(f"[DRY-RUN] Would update {file_path}")
|
||||
else:
|
||||
backup_path = file_path.with_suffix(file_path.suffix + ".bak")
|
||||
shutil.copy(file_path, backup_path)
|
||||
file_path.write_text(new_content)
|
||||
print(f"Updated {file_path} (backup saved to {backup_path})")
|
||||
elif not old_version_found:
|
||||
print(f"[SKIP] {file_path}: old version '{old_version}' not found")
|
||||
|
||||
return file_would_be_updated, old_version_found
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Bump version across project files.")
|
||||
parser.add_argument("old_version", help="Old version to replace")
|
||||
parser.add_argument("new_version", help="New version to set")
|
||||
parser.add_argument(
|
||||
"--dry-run", action="store_true", help="Show what would be changed without modifying files"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--glob", nargs="*", help="Optional glob patterns to include additional files"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
updated_files = []
|
||||
not_found_files = []
|
||||
|
||||
# Determine files to update
|
||||
files_to_update: List[Path] = [Path(f) for f in DEFAULT_FILES]
|
||||
if args.glob:
|
||||
for pattern in args.glob:
|
||||
files_to_update.extend(Path(".").glob(pattern))
|
||||
|
||||
files_to_update = list(dict.fromkeys(files_to_update)) # remove duplicates
|
||||
|
||||
any_updated = False
|
||||
for file_path in files_to_update:
|
||||
if file_path.exists() and file_path.is_file():
|
||||
updated, _ = replace_version_in_file(
|
||||
file_path, args.old_version, args.new_version, args.dry_run
|
||||
)
|
||||
any_updated |= updated
|
||||
if updated:
|
||||
updated_files.append(file_path)
|
||||
else:
|
||||
print(f"[SKIP] {file_path}: file does not exist")
|
||||
not_found_files.append(file_path)
|
||||
|
||||
print("\nSummary:")
|
||||
if updated_files:
|
||||
print(f"Updated files ({len(updated_files)}):")
|
||||
for f in updated_files:
|
||||
print(f" {f}")
|
||||
else:
|
||||
print("No files were updated.")
|
||||
|
||||
if not_found_files:
|
||||
print(f"Files where old version was not found ({len(not_found_files)}):")
|
||||
for f in not_found_files:
|
||||
print(f" {f}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
15
scripts/get_version.py
Normal file
15
scripts/get_version.py
Normal file
@@ -0,0 +1,15 @@
|
||||
#!.venv/bin/python
|
||||
"""Get version of EOS"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add the src directory to sys.path so Sphinx can import akkudoktoreos
|
||||
PROJECT_ROOT = Path(__file__).parent.parent
|
||||
SRC_DIR = PROJECT_ROOT / "src"
|
||||
sys.path.insert(0, str(SRC_DIR))
|
||||
|
||||
from akkudoktoreos.core.version import __version__
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(__version__)
|
||||
113
scripts/update_version.py
Normal file
113
scripts/update_version.py
Normal file
@@ -0,0 +1,113 @@
|
||||
#!.venv/bin/python
|
||||
"""General version replacement script.
|
||||
|
||||
Usage:
|
||||
python scripts/update_version.py <version> <file1> [file2 ...]
|
||||
"""
|
||||
|
||||
#!/usr/bin/env python3
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
# --- Patterns to match version strings ---
|
||||
VERSION_PATTERNS = [
|
||||
# Python: __version__ = "1.2.3"
|
||||
re.compile(
|
||||
r'(?<![A-Za-z0-9])(__version__\s*=\s*")'
|
||||
r'(?P<ver>\d+\.\d+\.\d+(?:\+[0-9A-Za-z\.]+)?)'
|
||||
r'(")'
|
||||
),
|
||||
|
||||
# Python: version = "1.2.3"
|
||||
re.compile(
|
||||
r'(?<![A-Za-z0-9])(version\s*=\s*")'
|
||||
r'(?P<ver>\d+\.\d+\.\d+(?:\+[0-9A-Za-z\.]+)?)'
|
||||
r'(")'
|
||||
),
|
||||
|
||||
# JSON: "version": "1.2.3"
|
||||
re.compile(
|
||||
r'(?<![A-Za-z0-9])("version"\s*:\s*")'
|
||||
r'(?P<ver>\d+\.\d+\.\d+(?:\+[0-9A-Za-z\.]+)?)'
|
||||
r'(")'
|
||||
),
|
||||
|
||||
# Makefile-style: VERSION ?= 1.2.3
|
||||
re.compile(
|
||||
r'(?<![A-Za-z0-9])(VERSION\s*\?=\s*)'
|
||||
r'(?P<ver>\d+\.\d+\.\d+(?:\+[0-9A-Za-z\.]+)?)'
|
||||
),
|
||||
|
||||
# YAML: version: "1.2.3"
|
||||
re.compile(
|
||||
r'(?m)^(version\s*:\s*["\']?)'
|
||||
r'(?P<ver>\d+\.\d+\.\d+(?:\+[0-9A-Za-z\.]+)?)'
|
||||
r'(["\']?)\s*$'
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def update_version_in_file(file_path: Path, new_version: str) -> bool:
|
||||
"""
|
||||
Replace version strings in a file based on VERSION_PATTERNS.
|
||||
Returns True if the file was updated.
|
||||
"""
|
||||
content = file_path.read_text()
|
||||
new_content = content
|
||||
file_would_be_updated = False
|
||||
|
||||
for pattern in VERSION_PATTERNS:
|
||||
def repl(match):
|
||||
nonlocal file_would_be_updated
|
||||
ver = match.group("ver")
|
||||
if ver != new_version:
|
||||
file_would_be_updated = True
|
||||
|
||||
# Three-group patterns (__version__, JSON, YAML)
|
||||
if len(match.groups()) == 3:
|
||||
return f"{match.group(1)}{new_version}{match.group(3)}"
|
||||
|
||||
# Two-group patterns (Makefile)
|
||||
return f"{match.group(1)}{new_version}"
|
||||
|
||||
return match.group(0)
|
||||
|
||||
new_content = pattern.sub(repl, new_content)
|
||||
|
||||
if file_would_be_updated:
|
||||
file_path.write_text(new_content)
|
||||
|
||||
return file_would_be_updated
|
||||
|
||||
|
||||
def main(version: str, files: List[str]):
|
||||
if not version:
|
||||
raise ValueError("No version provided")
|
||||
if not files:
|
||||
raise ValueError("No files provided")
|
||||
|
||||
updated_files = []
|
||||
for f in files:
|
||||
path = Path(f)
|
||||
if not path.exists():
|
||||
print(f"Warning: {path} does not exist, skipping")
|
||||
continue
|
||||
if update_version_in_file(path, version):
|
||||
updated_files.append(str(path))
|
||||
|
||||
if updated_files:
|
||||
print(f"Updated files: {', '.join(updated_files)}")
|
||||
else:
|
||||
print("No files updated.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 3:
|
||||
print("Usage: python update_version.py <version> <file1> [file2 ...]")
|
||||
sys.exit(1)
|
||||
|
||||
version_arg = sys.argv[1]
|
||||
files_arg = sys.argv[2:]
|
||||
main(version_arg, files_arg)
|
||||
Reference in New Issue
Block a user