diff --git a/.env.example b/.env.example index c476cf1..d9c8978 100644 --- a/.env.example +++ b/.env.example @@ -23,3 +23,4 @@ ICON="chart-671.png" LOG_LEVEL="DEBUG" LOG_PATH="./logs" LOG_CLEAR="True" +BACKUP_PATH="./thechart-backups" diff --git a/Makefile b/Makefile index f099bf8..d1e9d59 100644 --- a/Makefile +++ b/Makefile @@ -136,10 +136,19 @@ shell: ## Open a shell in the local environment requirements: ## Export the requirements to a file @echo "Exporting requirements to requirements.txt..." poetry export --without-hashes -f requirements.txt -o requirements.txt + +update-version: ## Update version in pyproject.toml from .env file and sync uv.lock + @echo "Updating version in pyproject.toml from .env..." + @$(PYTHON) scripts/update_version.py + +update-version-only: ## Update version in pyproject.toml from .env file (skip uv.lock) + @echo "Updating version in pyproject.toml from .env (skipping uv.lock)..." + @$(PYTHON) scripts/update_version.py --skip-uv-lock + commit-emergency: ## Emergency commit (bypasses pre-commit hooks) - USE SPARINGLY @echo "⚠️ WARNING: Emergency commit bypasses all pre-commit checks!" @echo "This should only be used in true emergencies." @read -p "Enter commit message: " msg; \ git add . && git commit --no-verify -m "$$msg" @echo "✅ Emergency commit completed. Please run tests manually when possible." -.PHONY: install clean reinstall check-env build attach deploy run start stop test lint format shell requirements commit-emergency help +.PHONY: install clean reinstall check-env build attach deploy run start stop test lint format shell requirements update-version update-version-only commit-emergency help diff --git a/docker-build.sh b/docker-build.sh index c26e77d..86bd07a 100755 --- a/docker-build.sh +++ b/docker-build.sh @@ -1,20 +1,27 @@ #!/usr/bin/bash CONTAINER_ENGINE="docker" # podman | docker -VERSION="v1.7.5" REGISTRY="gitea-http.taildb3494.ts.net/will/thechart" +# Source .env file to load environment variables +if [ -f .env ]; then + source .env +fi + +# Set APP_VERSION from .env VERSION, with fallback +export APP_VERSION=${VERSION} + if [ "$CONTAINER_ENGINE" == "podman" ]; then buildah build \ - -t $REGISTRY:$VERSION \ + -t $REGISTRY:$APP_VERSION \ --platform linux/amd64 \ --no-cache . else DOCKER_BUILDKIT=1 \ docker buildx build \ --platform linux/amd64 \ - -t $REGISTRY:$VERSION \ + -t $REGISTRY:$APP_VERSION \ --no-cache \ --push . fi diff --git a/docs/VERSION_MANAGEMENT.md b/docs/VERSION_MANAGEMENT.md new file mode 100644 index 0000000..7a98905 --- /dev/null +++ b/docs/VERSION_MANAGEMENT.md @@ -0,0 +1,77 @@ +# Version Management + +This project uses automatic version synchronization between the `.env` file and `pyproject.toml`. + +## Overview + +The version is maintained in the `.env` file as the single source of truth, and automatically synchronized to `pyproject.toml` using the provided script. + +## Files Involved + +- **`.env`**: Contains `VERSION="x.y.z"` - the authoritative version source +- **`pyproject.toml`**: Contains `version = "x.y.z"` in the `[project]` section +- **`uv.lock`**: Lock file updated automatically to reflect version changes +- **`scripts/update_version.py`**: Python script that reads from `.env` and updates both files + +## Usage + +### Manual Update + +```bash +# Update pyproject.toml version from .env (and sync uv.lock) +python scripts/update_version.py + +# Or use the Makefile target +make update-version + +# Skip uv.lock update if needed +python scripts/update_version.py --skip-uv-lock +make update-version-only +``` + +### Automatic Update + +The script can be integrated into your development workflow in several ways: + +1. **Before builds**: Run `make update-version` before building +2. **In CI/CD**: Add the script to your deployment pipeline +3. **As a pre-commit hook**: Add to `.pre-commit-config.yaml` (optional) + +### Workflow + +1. **Update the version**: Edit the `VERSION` variable in `.env` +2. **Synchronize**: Run `make update-version` or `python scripts/update_version.py` +3. **Verify**: All files now have the same version (`.env`, `pyproject.toml`, `uv.lock`) +4. **Commit**: All files can be committed together + +## Examples + +```bash +# Change version in .env +echo 'VERSION="1.14.0"' > .env # (update just the VERSION line) + +# Sync to pyproject.toml and uv.lock +make update-version + +# Result: All files now have version 1.14.0 +``` + +## Script Features + +- **Comprehensive updates**: Updates both `pyproject.toml` and `uv.lock` automatically +- **Precise targeting**: Only updates the `version` field in the `[project]` section +- **Safe operation**: Leaves other version fields untouched (`minversion`, `target-version`, etc.) +- **Flexible options**: Can skip `uv.lock` update with `--skip-uv-lock` flag +- **Error handling**: Validates file existence, uv installation, and command success +- **Safety checks**: Shows current vs new version before changing +- **Idempotent**: Safe to run multiple times +- **Minimal dependencies**: Only uses Python standard library + uv +- **Clear output**: Shows exactly what changed + +## Integration + +The script is designed to be: +- **Fast**: Minimal overhead for CI/CD pipelines +- **Reliable**: Robust error handling and validation +- **Flexible**: Can be called from Make, CI, or manually +- **Maintainable**: Clear code with type hints and documentation diff --git a/run-container.sh b/run-container.sh index a0ab073..d3b3593 100755 --- a/run-container.sh +++ b/run-container.sh @@ -6,6 +6,14 @@ if [ ! -f .env ]; then touch .env fi +# Source .env file to load environment variables +if [ -f .env ]; then + source .env +fi + +# Set APP_VERSION from .env VERSION, with fallback +export APP_VERSION=${VERSION} + # Allow local X server connections xhost +local: @@ -22,10 +30,11 @@ if command -v hostname >/dev/null 2>&1; then fi export SRC_PATH=$(pwd) -export IMAGE="thechart:latest" +export IMAGE="thechart:$APP_VERSION" export XAUTHORITY=$HOME/.Xauthority echo "Building and running the container..." +echo "Using APP_VERSION=$APP_VERSION" echo "Using DISPLAY=$DISPLAY" echo "Using SRC_PATH=$SRC_PATH" echo "Using XAUTHORITY=$XAUTHORITY" diff --git a/scripts/test_update_version.py b/scripts/test_update_version.py new file mode 100644 index 0000000..1106303 --- /dev/null +++ b/scripts/test_update_version.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +""" +Test script to verify update_version.py only updates the project version. + +This script creates a test pyproject.toml with multiple version fields +and verifies that only the [project] section version is updated. +""" + +import os +import sys +import tempfile +from pathlib import Path + +# Add scripts directory to path so we can import update_version +sys.path.insert(0, str(Path(__file__).parent.parent / "scripts")) + +from update_version import update_pyproject_version + + +def test_selective_version_update(): + """Test that only the project version is updated, not other version fields.""" + + test_content = """[project] +name = "test" +version = "1.0.0" +description = "Test project" + +[tool.pytest.ini_options] +minversion = "8.0" + +[tool.ruff] +target-version = "py313" + +[other] +version = "2.0.0" +some_version = "3.0.0" +""" + + expected_content = """[project] +name = "test" +version = "1.5.0" +description = "Test project" + +[tool.pytest.ini_options] +minversion = "8.0" + +[tool.ruff] +target-version = "py313" + +[other] +version = "2.0.0" +some_version = "3.0.0" +""" + + # Create temporary file + with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f: + f.write(test_content) + temp_path = Path(f.name) + + try: + # Update the version + result = update_pyproject_version(temp_path, "1.5.0") + + # Check that update was successful + assert result, "Version update should succeed" + + # Read the updated content + with open(temp_path, encoding="utf-8") as f: + updated_content = f.read() + + # Verify the content matches expectations + assert updated_content == expected_content, ( + f"Content doesn't match expectations.\n" + f"Expected:\n{expected_content}\n" + f"Got:\n{updated_content}" + ) + + print("✅ Test passed: Only [project] version was updated") + print(" - Project version: 1.0.0 → 1.5.0") + print(" - minversion: 8.0 (unchanged)") + print(" - target-version: py313 (unchanged)") + print(" - Other versions: unchanged") + + finally: + # Clean up + os.unlink(temp_path) + + +if __name__ == "__main__": + test_selective_version_update() diff --git a/scripts/update_version.py b/scripts/update_version.py new file mode 100755 index 0000000..9ce4995 --- /dev/null +++ b/scripts/update_version.py @@ -0,0 +1,230 @@ +#!/usr/bin/env python3 +""" +Script to update the version in pyproject.toml from the .env file. + +This script reads the VERSION variable from .env and updates the version +field in pyproject.toml to keep them synchronized. +""" + +import argparse +import re +import subprocess +import sys +from pathlib import Path + + +def read_version_from_env(env_path: Path) -> str | None: + """ + Read the VERSION variable from the .env file. + + Args: + env_path: Path to the .env file + + Returns: + The version string or None if not found + """ + try: + with open(env_path, encoding="utf-8") as f: + content = f.read() + + # Look for VERSION="x.y.z" pattern + match = re.search(r'VERSION\s*=\s*["\']([^"\']+)["\']', content) + if match: + return match.group(1) + else: + print("ERROR: VERSION not found in .env file") + return None + + except FileNotFoundError: + print(f"ERROR: .env file not found at {env_path}") + return None + except Exception as e: + print(f"ERROR: Failed to read .env file: {e}") + return None + + +def update_pyproject_version(pyproject_path: Path, new_version: str) -> bool: + """ + Update the version in pyproject.toml. + + Args: + pyproject_path: Path to the pyproject.toml file + new_version: The new version string + + Returns: + True if successful, False otherwise + """ + try: + with open(pyproject_path, encoding="utf-8") as f: + content = f.read() + + # Split content into lines for more precise matching + lines = content.split("\n") + in_project_section = False + version_line_index = None + current_version = None + + # Find the version line specifically in the [project] section + for i, line in enumerate(lines): + line_stripped = line.strip() + + # Check if we're entering the [project] section + if line_stripped == "[project]": + in_project_section = True + continue + + # Check if we're leaving the [project] section (entering a new section) + if ( + in_project_section + and line_stripped.startswith("[") + and line_stripped != "[project]" + ): + in_project_section = False + continue + + # Look for version = "x.y.z" only within [project] section + if in_project_section and line_stripped.startswith("version"): + version_pattern = r'^version\s*=\s*["\']([^"\']+)["\']' + version_match = re.match(version_pattern, line_stripped) + if version_match: + current_version = version_match.group(1) + version_line_index = i + break + + if current_version is None or version_line_index is None: + print( + "ERROR: version field not found in [project] section of pyproject.toml" + ) + return False + + if current_version == new_version: + print(f"Version is already up to date: {current_version}") + return True + + # Replace only the specific version line in the [project] section + old_line = lines[version_line_index] + new_line = re.sub( + r'^(\s*version\s*=\s*["\'])([^"\']+)(["\'])(.*)$', + f"\\g<1>{new_version}\\g<3>\\g<4>", + old_line, + ) + lines[version_line_index] = new_line + + # Reconstruct the content + new_content = "\n".join(lines) + + # Write back to file + with open(pyproject_path, "w", encoding="utf-8") as f: + f.write(new_content) + + print(f"Updated version from {current_version} to {new_version}") + return True + + except FileNotFoundError: + print(f"ERROR: pyproject.toml file not found at {pyproject_path}") + return False + except Exception as e: + print(f"ERROR: Failed to update pyproject.toml: {e}") + return False + + +def update_uv_lock(project_root: Path) -> bool: + """ + Update uv.lock file to reflect changes in pyproject.toml. + + Args: + project_root: Path to the project root directory + + Returns: + True if successful, False otherwise + """ + try: + print("Updating uv.lock file...") + + # Run uv lock to update the lock file + result = subprocess.run( + ["uv", "lock"], + cwd=project_root, + capture_output=True, + text=True, + timeout=60, # 60 second timeout + ) + + if result.returncode == 0: + print("Successfully updated uv.lock") + return True + else: + print(f"ERROR: Failed to update uv.lock: {result.stderr}") + return False + + except subprocess.TimeoutExpired: + print("ERROR: uv lock command timed out after 60 seconds") + return False + except FileNotFoundError: + print( + "ERROR: 'uv' command not found. Please ensure uv is installed and in PATH" + ) + return False + except Exception as e: + print(f"ERROR: Failed to run uv lock: {e}") + return False + + +def main() -> int: + """ + Main function to update version from .env to pyproject.toml. + + Returns: + Exit code: 0 for success, 1 for failure + """ + parser = argparse.ArgumentParser( + description="Update version in pyproject.toml from .env file" + ) + parser.add_argument( + "--skip-uv-lock", + action="store_true", + help="Skip updating uv.lock file after version update", + ) + args = parser.parse_args() + + # Get the project root directory (assuming script is in scripts/ folder) + script_dir = Path(__file__).parent + project_root = script_dir.parent + + env_path = project_root / ".env" + pyproject_path = project_root / "pyproject.toml" + + print(f"Reading version from: {env_path}") + print(f"Updating version in: {pyproject_path}") + + # Read version from .env + version = read_version_from_env(env_path) + if not version: + return 1 + + print(f"Found version in .env: {version}") + + # Update pyproject.toml + pyproject_updated = update_pyproject_version(pyproject_path, version) + if not pyproject_updated: + return 1 + + print("Version update completed successfully!") + + # Update uv.lock unless explicitly skipped + if args.skip_uv_lock: + print("Skipping uv.lock update (--skip-uv-lock specified)") + return 0 + + # Update uv.lock to reflect the changes + if update_uv_lock(project_root): + print("All updates completed successfully!") + return 0 + else: + print("⚠️ Version updated but uv.lock update failed") + print(" Please run 'uv lock' manually to update the lock file") + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/uv.lock b/uv.lock index ac113e6..6d557c0 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.13" [[package]]