feat: Implement centralized version management and sync process
This commit is contained in:
parent
c1133ae8e9
commit
0d9464854d
7 changed files with 523 additions and 45 deletions
166
CONTRIBUTING.md
166
CONTRIBUTING.md
|
|
@ -314,31 +314,159 @@ Integration tests should cover workflows across multiple components. See [tests/
|
||||||
|
|
||||||
## Release Process
|
## Release Process
|
||||||
|
|
||||||
### Version Numbering
|
### Versioning & Release Process
|
||||||
|
|
||||||
We follow [Semantic Versioning](https://semver.org/):
|
### Version Management
|
||||||
|
|
||||||
- **MAJOR**: Breaking changes
|
WebDrop Bridge uses **semantic versioning** (MAJOR.MINOR.PATCH). The version is centralized in one location:
|
||||||
- **MINOR**: New features (backward compatible)
|
|
||||||
- **PATCH**: Bug fixes
|
|
||||||
|
|
||||||
Example: `1.2.3` (Major.Minor.Patch)
|
**Single Source of Truth**: `src/webdrop_bridge/__init__.py`
|
||||||
|
|
||||||
### Creating a Release
|
```python
|
||||||
|
__version__ = "1.0.0"
|
||||||
1. Update version in:
|
|
||||||
- `pyproject.toml`
|
|
||||||
- `src/webdrop_bridge/__init__.py`
|
|
||||||
|
|
||||||
2. Update CHANGELOG.md
|
|
||||||
|
|
||||||
3. Create git tag:
|
|
||||||
```bash
|
|
||||||
git tag -a v1.2.3 -m "Release version 1.2.3"
|
|
||||||
git push origin v1.2.3
|
|
||||||
```
|
```
|
||||||
|
|
||||||
4. GitHub Actions will automatically build installers
|
**Shared Version Utility**: `build/scripts/version_utils.py`
|
||||||
|
|
||||||
|
All build scripts and version management tools use a shared utility to read the version from `__init__.py`, ensuring consistency across:
|
||||||
|
- `pyproject.toml` - Reads dynamically at build time
|
||||||
|
- `config.py` - Reads dynamically at startup
|
||||||
|
- `.env.example` - Updated by sync script (optional)
|
||||||
|
- `CHANGELOG.md` - Updated by sync script
|
||||||
|
|
||||||
|
### Releasing a New Version
|
||||||
|
|
||||||
|
#### Step 1: Update the Version (Only Place to Edit)
|
||||||
|
|
||||||
|
Edit `src/webdrop_bridge/__init__.py` and change `__version__`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
__version__ = "1.2.0" # Change this to your new version
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Step 2: Sync Version to Changelog
|
||||||
|
|
||||||
|
Run the sync script to update the changelog:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/sync_version.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Or let the build script do it automatically:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Windows
|
||||||
|
python build/scripts/build_windows.py
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
bash build/scripts/build_macos.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Both the build script and sync script use the shared `build/scripts/version_utils.py` utility.
|
||||||
|
|
||||||
|
#### Step 3: Update CHANGELOG.md Manually (Content Only)
|
||||||
|
|
||||||
|
The sync script adds the version header with the date. Now add your changes under each section:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## [1.2.0] - 2026-01-15
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- New feature description
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Breaking change description
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Bug fix description
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Step 4: Commit and Tag
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add -A
|
||||||
|
git commit -m "chore: release v1.2.0
|
||||||
|
|
||||||
|
- Feature 1 details
|
||||||
|
- Feature 2 details"
|
||||||
|
|
||||||
|
git tag -a v1.2.0 -m "Release version 1.2.0"
|
||||||
|
git push origin main --tags
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual Version Sync (If Needed)
|
||||||
|
|
||||||
|
If you need to sync versions without building:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/sync_version.py
|
||||||
|
```
|
||||||
|
|
||||||
|
To set a specific version:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/sync_version.py --version 1.2.0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Querying Version in Code
|
||||||
|
|
||||||
|
Always import from the package:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from webdrop_bridge import __version__
|
||||||
|
|
||||||
|
print(__version__) # "1.2.0"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment Override (Development Only)
|
||||||
|
|
||||||
|
If needed for testing, you can override with `.env`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# .env (development only)
|
||||||
|
APP_VERSION=1.2.0-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Config loads it via lazy import (to avoid circular dependencies):
|
||||||
|
```python
|
||||||
|
if not os.getenv("APP_VERSION"):
|
||||||
|
from webdrop_bridge import __version__
|
||||||
|
app_version = __version__
|
||||||
|
else:
|
||||||
|
app_version = os.getenv("APP_VERSION")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Shared Version Utility
|
||||||
|
|
||||||
|
Both build scripts and the sync script use `build/scripts/version_utils.py` to read the version:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from version_utils import get_current_version, get_project_root
|
||||||
|
|
||||||
|
version = get_current_version() # Reads from __init__.py
|
||||||
|
root = get_project_root() # Gets project root
|
||||||
|
```
|
||||||
|
|
||||||
|
This ensures:
|
||||||
|
- **No duplication** - Single implementation used everywhere
|
||||||
|
- **Consistency** - All tools read from the same source
|
||||||
|
- **Maintainability** - Update once, affects all tools
|
||||||
|
|
||||||
|
If you create new build scripts or tools, import from this utility instead of implementing version reading again.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary of Version Management
|
||||||
|
|
||||||
|
| Task | How | Location |
|
||||||
|
|------|-----|----------|
|
||||||
|
| Define version | Edit `__version__` | `src/webdrop_bridge/__init__.py` |
|
||||||
|
| Read version in app | Lazy import `__init__.py` | `src/webdrop_bridge/config.py` |
|
||||||
|
| Read version in builds | Use shared utility | `build/scripts/version_utils.py` |
|
||||||
|
| Update changelog | Run sync script | `scripts/sync_version.py` |
|
||||||
|
| Release new version | Edit `__init__.py`, run sync, commit/tag | See "Releasing a New Version" above |
|
||||||
|
|
||||||
|
**Golden Rule**: Only edit `src/webdrop_bridge/__init__.py`. Everything else is automated or handled by scripts.
|
||||||
|
|
||||||
## Getting Help
|
## Getting Help
|
||||||
|
|
||||||
|
|
|
||||||
140
VERSIONING_SIMPLIFIED.md
Normal file
140
VERSIONING_SIMPLIFIED.md
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
# Simplified Versioning System
|
||||||
|
|
||||||
|
## Problem Solved
|
||||||
|
|
||||||
|
Previously, the application version had to be manually updated in **multiple places**:
|
||||||
|
1. `src/webdrop_bridge/__init__.py` - source of truth
|
||||||
|
2. `pyproject.toml` - package version
|
||||||
|
3. `.env.example` - environment example
|
||||||
|
4. Run `scripts/sync_version.py` - manual sync step
|
||||||
|
|
||||||
|
This was error-prone and tedious.
|
||||||
|
|
||||||
|
## Solution: Single Source of Truth
|
||||||
|
|
||||||
|
The version is now defined **only in one place**:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# src/webdrop_bridge/__init__.py
|
||||||
|
__version__ = "1.0.0"
|
||||||
|
```
|
||||||
|
|
||||||
|
All other components automatically read from this single source.
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
### 1. **pyproject.toml** (Automatic)
|
||||||
|
```toml
|
||||||
|
[tool.setuptools.dynamic]
|
||||||
|
version = {attr = "webdrop_bridge.__version__"}
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "webdrop-bridge"
|
||||||
|
dynamic = ["version"] # Reads from __init__.py
|
||||||
|
```
|
||||||
|
|
||||||
|
When you build the package, setuptools automatically extracts the version from `__init__.py`.
|
||||||
|
|
||||||
|
### 2. **config.py** (Automatic - with ENV override)
|
||||||
|
```python
|
||||||
|
# Lazy import to avoid circular imports
|
||||||
|
if not os.getenv("APP_VERSION"):
|
||||||
|
from webdrop_bridge import __version__
|
||||||
|
app_version = __version__
|
||||||
|
else:
|
||||||
|
app_version = os.getenv("APP_VERSION")
|
||||||
|
```
|
||||||
|
|
||||||
|
The config automatically reads from `__init__.py`, but can be overridden with the `APP_VERSION` environment variable if needed.
|
||||||
|
|
||||||
|
### 3. **sync_version.py** (Simplified)
|
||||||
|
The script now only handles:
|
||||||
|
- Updating `__init__.py` with a new version
|
||||||
|
- Updating `CHANGELOG.md` with a new version header
|
||||||
|
- Optional: updating `.env.example` if it explicitly sets `APP_VERSION`
|
||||||
|
|
||||||
|
It **no longer** needs to manually sync pyproject.toml or config defaults.
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
### To Release a New Version
|
||||||
|
|
||||||
|
**Option 1: Simple (Recommended)**
|
||||||
|
```bash
|
||||||
|
# Edit only one file
|
||||||
|
# src/webdrop_bridge/__init__.py:
|
||||||
|
__version__ = "1.1.0" # Change this
|
||||||
|
|
||||||
|
# Then run sync script to update changelog
|
||||||
|
python scripts/sync_version.py
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option 2: Using the Sync Script**
|
||||||
|
```bash
|
||||||
|
python scripts/sync_version.py --version 1.1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
The script will:
|
||||||
|
- ✅ Update `__init__.py`
|
||||||
|
- ✅ Update `CHANGELOG.md`
|
||||||
|
- ✅ (Optional) Update `.env.example` if it has `APP_VERSION=`
|
||||||
|
|
||||||
|
### What Happens Automatically
|
||||||
|
|
||||||
|
When you run your application:
|
||||||
|
1. Config loads and checks environment for `APP_VERSION`
|
||||||
|
2. If not set, it imports `__version__` from `__init__.py`
|
||||||
|
3. The version is displayed in the UI
|
||||||
|
4. Update checks use the correct version
|
||||||
|
|
||||||
|
When you build with `pip install`:
|
||||||
|
1. setuptools reads `__version__` from `__init__.py`
|
||||||
|
2. Package metadata is set automatically
|
||||||
|
3. No manual sync needed
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
To verify the version is correctly propagated:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check __init__.py
|
||||||
|
python -c "from webdrop_bridge import __version__; print(__version__)"
|
||||||
|
|
||||||
|
# Check config loading
|
||||||
|
python -c "from webdrop_bridge.config import Config; c = Config.from_env(); print(c.app_version)"
|
||||||
|
|
||||||
|
# Check package metadata (after building)
|
||||||
|
pip show webdrop-bridge
|
||||||
|
```
|
||||||
|
|
||||||
|
All should show the same version.
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Always edit `__init__.py` first** - it's the single source of truth
|
||||||
|
2. **Run `sync_version.py` to update changelog** - keeps release notes organized
|
||||||
|
3. **Use environment variables only for testing** - don't hardcode overrides
|
||||||
|
4. **Run tests after version changes** - config tests verify version loading
|
||||||
|
|
||||||
|
## Migration Notes
|
||||||
|
|
||||||
|
If you had other places where version was defined:
|
||||||
|
- ❌ Remove version from `pyproject.toml` `[project]` section
|
||||||
|
- ✅ Add `dynamic = ["version"]` instead
|
||||||
|
- ❌ Don't manually edit `.env.example` for version
|
||||||
|
- ✅ Let `sync_version.py` handle it
|
||||||
|
- ❌ Don't hardcode version in config.py defaults
|
||||||
|
- ✅ Use lazy import from `__init__.py`
|
||||||
|
|
||||||
|
## Testing the System
|
||||||
|
|
||||||
|
Run the config tests to verify everything works:
|
||||||
|
```bash
|
||||||
|
pytest tests/unit/test_config.py -v
|
||||||
|
```
|
||||||
|
|
||||||
|
All tests should pass, confirming version loading works correctly.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Result**: One place to change, multiple places automatically updated. Simple, clean, professional.
|
||||||
|
|
@ -19,6 +19,9 @@ import shutil
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Import shared version utilities
|
||||||
|
from version_utils import get_current_version
|
||||||
|
|
||||||
# Fix Unicode output on Windows
|
# Fix Unicode output on Windows
|
||||||
if sys.platform == "win32":
|
if sys.platform == "win32":
|
||||||
import io
|
import io
|
||||||
|
|
@ -37,16 +40,15 @@ class WindowsBuilder:
|
||||||
self.dist_dir = self.build_dir / "dist" / "windows"
|
self.dist_dir = self.build_dir / "dist" / "windows"
|
||||||
self.temp_dir = self.build_dir / "temp" / "windows"
|
self.temp_dir = self.build_dir / "temp" / "windows"
|
||||||
self.spec_file = self.build_dir / "webdrop_bridge.spec"
|
self.spec_file = self.build_dir / "webdrop_bridge.spec"
|
||||||
self.version = self._get_version()
|
self.version = get_current_version()
|
||||||
|
|
||||||
def _get_version(self) -> str:
|
def _get_version(self) -> str:
|
||||||
"""Get version from config.py."""
|
"""Get version from __init__.py.
|
||||||
config_file = self.project_root / "src" / "webdrop_bridge" / "config.py"
|
|
||||||
for line in config_file.read_text().split("\n"):
|
Note: This method is deprecated. Use get_current_version() from
|
||||||
if "app_version" in line and "1.0.0" in line:
|
version_utils.py instead.
|
||||||
# Extract default version from config
|
"""
|
||||||
return "1.0.0"
|
return get_current_version()
|
||||||
return "1.0.0"
|
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
"""Clean previous builds."""
|
"""Clean previous builds."""
|
||||||
|
|
@ -322,28 +324,27 @@ class WindowsBuilder:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def sync_version() -> None:
|
||||||
"""Main entry point."""
|
"""Sync version from __init__.py to all dependent files."""
|
||||||
import argparse
|
script_path = Path(__file__).parent.parent.parent / "scripts" / "sync_version.py"
|
||||||
|
result = subprocess.run(
|
||||||
|
[sys.executable, str(script_path)],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
if result.returncode != 0:
|
||||||
|
print(f"❌ Version sync failed: {result.stderr}")
|
||||||
|
sys.exit(1)
|
||||||
|
print(result.stdout)
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description="Build WebDrop Bridge for Windows"
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--msi",
|
|
||||||
action="store_true",
|
|
||||||
help="Create MSI installer (requires WiX Toolset)",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--sign",
|
|
||||||
action="store_true",
|
|
||||||
help="Sign executable (requires CODE_SIGN_CERT environment variable)",
|
|
||||||
)
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
def main() -> int:
|
||||||
|
"""Build Windows MSI installer."""
|
||||||
|
print("🔄 Syncing version...")
|
||||||
|
sync_version()
|
||||||
|
|
||||||
builder = WindowsBuilder()
|
builder = WindowsBuilder()
|
||||||
success = builder.build(create_msi=args.msi, sign=args.sign)
|
success = builder.build(create_msi=True, sign=False)
|
||||||
|
|
||||||
return 0 if success else 1
|
return 0 if success else 1
|
||||||
|
|
||||||
|
|
|
||||||
49
build/scripts/version_utils.py
Normal file
49
build/scripts/version_utils.py
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
"""Shared version management utilities for build scripts.
|
||||||
|
|
||||||
|
This module provides a single source of truth for version reading
|
||||||
|
to avoid duplication between different build scripts.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def get_project_root() -> Path:
|
||||||
|
"""Get the project root directory.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Path to project root (parent of build/scripts)
|
||||||
|
"""
|
||||||
|
return Path(__file__).parent.parent.parent
|
||||||
|
|
||||||
|
|
||||||
|
def get_current_version() -> str:
|
||||||
|
"""Read version from __init__.py.
|
||||||
|
|
||||||
|
This is the single source of truth for version information.
|
||||||
|
All build scripts and version management tools use this function.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Current version string from __init__.py
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If __version__ cannot be found in __init__.py
|
||||||
|
"""
|
||||||
|
project_root = get_project_root()
|
||||||
|
init_file = project_root / "src" / "webdrop_bridge" / "__init__.py"
|
||||||
|
|
||||||
|
if not init_file.exists():
|
||||||
|
raise FileNotFoundError(
|
||||||
|
f"Cannot find __init__.py at {init_file}"
|
||||||
|
)
|
||||||
|
|
||||||
|
content = init_file.read_text(encoding="utf-8")
|
||||||
|
match = re.search(r'__version__\s*=\s*["\']([^"\']+)["\']', content)
|
||||||
|
|
||||||
|
if not match:
|
||||||
|
raise ValueError(
|
||||||
|
f"Could not find __version__ in {init_file}. "
|
||||||
|
"Expected: __version__ = \"X.Y.Z\""
|
||||||
|
)
|
||||||
|
|
||||||
|
return match.group(1)
|
||||||
|
|
@ -2,9 +2,12 @@
|
||||||
requires = ["setuptools>=65.0", "wheel"]
|
requires = ["setuptools>=65.0", "wheel"]
|
||||||
build-backend = "setuptools.build_meta"
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[tool.setuptools.dynamic]
|
||||||
|
version = {attr = "webdrop_bridge.__version__"}
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "webdrop-bridge"
|
name = "webdrop-bridge"
|
||||||
version = "1.0.0"
|
dynamic = ["version"]
|
||||||
description = "Professional Qt-based desktop bridge application converting web drag-and-drop to native file operations for InDesign, Word, and other desktop applications"
|
description = "Professional Qt-based desktop bridge application converting web drag-and-drop to native file operations for InDesign, Word, and other desktop applications"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.9"
|
||||||
|
|
|
||||||
152
scripts/sync_version.py
Normal file
152
scripts/sync_version.py
Normal file
|
|
@ -0,0 +1,152 @@
|
||||||
|
"""Sync version from __init__.py to changelog.
|
||||||
|
|
||||||
|
This script reads the version from src/webdrop_bridge/__init__.py and
|
||||||
|
updates the CHANGELOG.md. Config and pyproject.toml automatically read
|
||||||
|
from __init__.py, so no manual sync needed for those files.
|
||||||
|
|
||||||
|
This script uses shared version utilities (build/scripts/version_utils.py)
|
||||||
|
to ensure consistent version reading across all build scripts.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python scripts/sync_version.py [--version VERSION]
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
python scripts/sync_version.py # Use version from __init__.py
|
||||||
|
python scripts/sync_version.py --version 2.0.0 # Override with new version
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Import shared version utilities
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent.parent / "build" / "scripts"))
|
||||||
|
from version_utils import get_current_version, get_project_root
|
||||||
|
|
||||||
|
PROJECT_ROOT = get_project_root()
|
||||||
|
|
||||||
|
|
||||||
|
def get_current_version_from_init() -> str:
|
||||||
|
"""Get version from __init__.py using shared utility.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Current version string from __init__.py
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If __version__ cannot be found
|
||||||
|
"""
|
||||||
|
return get_current_version()
|
||||||
|
|
||||||
|
|
||||||
|
def update_init_version(version: str) -> None:
|
||||||
|
"""Update version in __init__.py.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
version: New version string to set
|
||||||
|
"""
|
||||||
|
init_file = PROJECT_ROOT / "src/webdrop_bridge/__init__.py"
|
||||||
|
content = init_file.read_text()
|
||||||
|
new_content = re.sub(
|
||||||
|
r'__version__\s*=\s*["\'][^"\']+["\']',
|
||||||
|
f'__version__ = "{version}"',
|
||||||
|
content,
|
||||||
|
)
|
||||||
|
init_file.write_text(new_content)
|
||||||
|
print(f"✓ Updated src/webdrop_bridge/__init__.py to {version}")
|
||||||
|
|
||||||
|
|
||||||
|
def update_env_example(version: str) -> None:
|
||||||
|
"""Update APP_VERSION in .env.example (optional).
|
||||||
|
|
||||||
|
Note: config.py now reads from __init__.py by default.
|
||||||
|
Only update if .env.example explicitly sets APP_VERSION for testing.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
version: New version string to set
|
||||||
|
"""
|
||||||
|
env_file = PROJECT_ROOT / ".env.example"
|
||||||
|
if env_file.exists():
|
||||||
|
content = env_file.read_text()
|
||||||
|
# Only update if APP_VERSION is explicitly set
|
||||||
|
if 'APP_VERSION=' in content:
|
||||||
|
new_content = re.sub(
|
||||||
|
r'APP_VERSION=[^\n]+',
|
||||||
|
f'APP_VERSION={version}',
|
||||||
|
content,
|
||||||
|
)
|
||||||
|
env_file.write_text(new_content)
|
||||||
|
print(f"✓ Updated .env.example to {version}")
|
||||||
|
else:
|
||||||
|
print(
|
||||||
|
f"ℹ️ .env.example does not override APP_VERSION "
|
||||||
|
f"(uses __init__.py)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def update_changelog(version: str) -> None:
|
||||||
|
"""Add version header to CHANGELOG.md if not present.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
version: New version string to add
|
||||||
|
"""
|
||||||
|
changelog = PROJECT_ROOT / "CHANGELOG.md"
|
||||||
|
if changelog.exists():
|
||||||
|
content = changelog.read_text()
|
||||||
|
if f"## [{version}]" not in content and f"## {version}" not in content:
|
||||||
|
date_str = datetime.now().strftime("%Y-%m-%d")
|
||||||
|
header = (
|
||||||
|
f"## [{version}] - {date_str}\n\n"
|
||||||
|
"### Added\n\n### Changed\n\n### Fixed\n\n"
|
||||||
|
)
|
||||||
|
new_content = header + content
|
||||||
|
changelog.write_text(new_content)
|
||||||
|
print(f"✓ Added version header to CHANGELOG.md for {version}")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
"""Sync version across project.
|
||||||
|
|
||||||
|
Updates __init__.py (source of truth) and changelog.
|
||||||
|
Config and pyproject.toml automatically read from __init__.py.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
0 on success, 1 on error
|
||||||
|
"""
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Sync version from __init__.py to dependent files"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--version",
|
||||||
|
type=str,
|
||||||
|
help="Version to set (if not provided, reads from __init__.py)",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
try:
|
||||||
|
if args.version:
|
||||||
|
if not re.match(r"^\d+\.\d+\.\d+", args.version):
|
||||||
|
print(
|
||||||
|
"❌ Invalid version format. Use semantic versioning"
|
||||||
|
" (e.g., 1.2.3)"
|
||||||
|
)
|
||||||
|
return 1
|
||||||
|
version = args.version
|
||||||
|
update_init_version(version)
|
||||||
|
else:
|
||||||
|
version = get_current_version_from_init()
|
||||||
|
print(f"📍 Current version from __init__.py: {version}")
|
||||||
|
|
||||||
|
update_env_example(version)
|
||||||
|
update_changelog(version)
|
||||||
|
print(f"\n✅ Version sync complete: {version}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error: {e}")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
|
|
@ -69,7 +69,12 @@ class Config:
|
||||||
|
|
||||||
# Extract and validate configuration values
|
# Extract and validate configuration values
|
||||||
app_name = os.getenv("APP_NAME", "WebDrop Bridge")
|
app_name = os.getenv("APP_NAME", "WebDrop Bridge")
|
||||||
app_version = os.getenv("APP_VERSION", "1.0.0")
|
# Version comes from __init__.py (lazy import to avoid circular imports)
|
||||||
|
if not os.getenv("APP_VERSION"):
|
||||||
|
from webdrop_bridge import __version__
|
||||||
|
app_version = __version__
|
||||||
|
else:
|
||||||
|
app_version = os.getenv("APP_VERSION")
|
||||||
log_level = os.getenv("LOG_LEVEL", "INFO").upper()
|
log_level = os.getenv("LOG_LEVEL", "INFO").upper()
|
||||||
log_file_str = os.getenv("LOG_FILE", "logs/webdrop_bridge.log")
|
log_file_str = os.getenv("LOG_FILE", "logs/webdrop_bridge.log")
|
||||||
allowed_roots_str = os.getenv("ALLOWED_ROOTS", "Z:/,C:/Users/Public")
|
allowed_roots_str = os.getenv("ALLOWED_ROOTS", "Z:/,C:/Users/Public")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue