forked from iarv/mc-webui
feat: Add dynamic Git-based versioning system
- Add app/version.py module generating version from Git metadata - Format: YYYY.MM.DD+<commit_hash> (e.g., 2025.01.18+576c8ca9) - Add +dirty suffix for uncommitted changes (ignores .env, technotes/) - Add /api/version endpoint for monitoring - Display version in hamburger menu - Add freeze mechanism for Docker builds Deploy command updated: git push && ssh ... "cd ~/mc-webui && git pull && python -m app.version freeze && docker compose up -d --build" Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -11,6 +11,8 @@
|
||||
# Python
|
||||
# ============================================
|
||||
__pycache__/
|
||||
# Auto-generated version file (created during Docker build)
|
||||
app/version_frozen.py
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
|
||||
@@ -16,6 +16,8 @@ COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copy application code
|
||||
# Note: Run 'python -m app.version freeze' before build to include version info
|
||||
# The version_frozen.py file will be copied automatically if it exists
|
||||
COPY app/ ./app/
|
||||
|
||||
# Expose Flask port
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
mc-webui - Flask application package
|
||||
"""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
Version is managed dynamically via app/version.py based on Git metadata.
|
||||
"""
|
||||
|
||||
@@ -12,6 +12,7 @@ from flask_socketio import SocketIO, emit
|
||||
from app.config import config, runtime_config
|
||||
from app.routes.views import views_bp
|
||||
from app.routes.api import api_bp
|
||||
from app.version import VERSION_STRING
|
||||
from app.archiver.manager import schedule_daily_archiving
|
||||
from app.meshcore.cli import fetch_device_name_from_bridge
|
||||
|
||||
@@ -43,6 +44,11 @@ def create_app():
|
||||
app.config['DEBUG'] = config.FLASK_DEBUG
|
||||
app.config['SECRET_KEY'] = 'mc-webui-secret-key-change-in-production'
|
||||
|
||||
# Inject version into all templates
|
||||
@app.context_processor
|
||||
def inject_version():
|
||||
return {'version': VERSION_STRING}
|
||||
|
||||
# Register blueprints
|
||||
app.register_blueprint(views_bp)
|
||||
app.register_blueprint(api_bp)
|
||||
|
||||
@@ -1910,6 +1910,27 @@ def get_read_status_api():
|
||||
}), 500
|
||||
|
||||
|
||||
@api_bp.route('/version', methods=['GET'])
|
||||
def get_version():
|
||||
"""
|
||||
Get application version.
|
||||
|
||||
Returns:
|
||||
JSON with version info:
|
||||
{
|
||||
"success": true,
|
||||
"version": "2025.01.18+576c8ca9",
|
||||
"docker_tag": "2025.01.18-576c8ca9"
|
||||
}
|
||||
"""
|
||||
from app.version import VERSION_STRING, DOCKER_TAG
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'version': VERSION_STRING,
|
||||
'docker_tag': DOCKER_TAG
|
||||
}), 200
|
||||
|
||||
|
||||
@api_bp.route('/read_status/mark_read', methods=['POST'])
|
||||
def mark_read_api():
|
||||
"""
|
||||
|
||||
@@ -58,6 +58,9 @@
|
||||
<h5 class="offcanvas-title"><i class="bi bi-menu-button-wide"></i> Menu</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="offcanvas"></button>
|
||||
</div>
|
||||
<div class="px-3 pb-2 text-muted small border-bottom">
|
||||
<i class="bi bi-tag"></i> {{ version }}
|
||||
</div>
|
||||
<div class="offcanvas-body">
|
||||
<div class="list-group list-group-flush">
|
||||
<button class="list-group-item list-group-item-action d-flex align-items-center gap-3" id="refreshBtn">
|
||||
|
||||
70
app/version.py
Normal file
70
app/version.py
Normal file
@@ -0,0 +1,70 @@
|
||||
"""
|
||||
Git-based version management for mc-webui.
|
||||
Format: YYYY.MM.DD+<short_hash> (e.g., 2025.01.18+576c8ca9)
|
||||
"""
|
||||
import subprocess
|
||||
import shlex
|
||||
import os
|
||||
|
||||
VERSION_STRING = "0.0.0+unknown"
|
||||
DOCKER_TAG = "0.0.0-unknown"
|
||||
|
||||
|
||||
def subprocess_run(args):
|
||||
"""Execute subprocess and return stripped stdout."""
|
||||
if not isinstance(args, (list, tuple)):
|
||||
args = shlex.split(args)
|
||||
proc = subprocess.run(
|
||||
args,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
env={"PATH": os.environ.get("PATH", ""), "LC_ALL": "C"}
|
||||
)
|
||||
return proc.stdout.strip()
|
||||
|
||||
|
||||
def get_git_version():
|
||||
"""Get version from git commit date and hash."""
|
||||
# Get date (YYYY.MM.DD) and short hash
|
||||
git_version = subprocess_run(
|
||||
r"git show -s --date=format:%Y.%m.%d --format=%cd+%h"
|
||||
)
|
||||
# Keep full ISO format (with leading zeros)
|
||||
docker_tag = git_version.replace("+", "-")
|
||||
|
||||
# Check for uncommitted changes (ignore .env and technotes/)
|
||||
try:
|
||||
subprocess_run("git diff --quiet -- . :!*.env :!.env :!technotes/")
|
||||
except subprocess.CalledProcessError as e:
|
||||
if e.returncode == 1:
|
||||
git_version += "+dirty"
|
||||
|
||||
return git_version, docker_tag
|
||||
|
||||
|
||||
# Load version: frozen file takes priority, then git, then fallback
|
||||
try:
|
||||
from app.version_frozen import VERSION_STRING, DOCKER_TAG
|
||||
except ImportError:
|
||||
try:
|
||||
VERSION_STRING, DOCKER_TAG = get_git_version()
|
||||
except Exception:
|
||||
pass # Keep defaults
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
if len(sys.argv) >= 2 and sys.argv[1] == "freeze":
|
||||
VERSION_STRING, DOCKER_TAG = get_git_version()
|
||||
code = f'''"""Frozen version - auto-generated, do not edit."""
|
||||
VERSION_STRING = "{VERSION_STRING}"
|
||||
DOCKER_TAG = "{DOCKER_TAG}"
|
||||
'''
|
||||
path = os.path.join(os.path.dirname(__file__), "version_frozen.py")
|
||||
with open(path, "w", encoding="utf8") as f:
|
||||
f.write(code)
|
||||
print(f"Version frozen: {VERSION_STRING}")
|
||||
else:
|
||||
print(f'VERSION_STRING="{VERSION_STRING}"')
|
||||
print(f'DOCKER_TAG="{DOCKER_TAG}"')
|
||||
Reference in New Issue
Block a user