Spaces:
Runtime error
Runtime error
| #!/usr/bin/env python3 | |
| """ | |
| Automated cache-busting version bump utility for TreeTrack. | |
| Updates: | |
| - static/sw.js: const VERSION = <timestamp> | |
| - static/index.html and static/map.html: | |
| * inline currentVersion = '<version>' | |
| * inline timestamp = '<timestamp>' | |
| * script tag query params (?v=<version>&t=<timestamp>) for tree-track-app.js and map.js | |
| Usage examples: | |
| python scripts/bump_cache.py --now # bump timestamp only | |
| python scripts/bump_cache.py --bump patch # bump version 1.2.3 -> 1.2.4 and timestamp | |
| python scripts/bump_cache.py --set-version 5.1.2 # set specific version and bump timestamp | |
| This script maintains state in version.json at repository root. | |
| """ | |
| import argparse | |
| import json | |
| import os | |
| import re | |
| import sys | |
| import time | |
| from pathlib import Path | |
| ROOT = Path(__file__).resolve().parents[1] | |
| VERSION_FILE = ROOT / 'version.json' | |
| FILES = { | |
| 'sw': ROOT / 'static' / 'sw.js', | |
| 'index': ROOT / 'static' / 'index.html', | |
| 'map': ROOT / 'static' / 'map.html', | |
| } | |
| SEMVER_RE = re.compile(r"^(\d+)\.(\d+)\.(\d+)$") | |
| def read_text(p: Path) -> str: | |
| try: | |
| return p.read_text(encoding='utf-8') | |
| except FileNotFoundError: | |
| print(f"Error: file not found: {p}", file=sys.stderr) | |
| sys.exit(1) | |
| def write_text(p: Path, content: str) -> None: | |
| p.write_text(content, encoding='utf-8', newline='\n') | |
| def bump_semver(version: str, which: str) -> str: | |
| m = SEMVER_RE.match(version) | |
| if not m: | |
| raise ValueError(f"Invalid semver: {version}") | |
| major, minor, patch = map(int, m.groups()) | |
| if which == 'major': | |
| major += 1 | |
| minor = 0 | |
| patch = 0 | |
| elif which == 'minor': | |
| minor += 1 | |
| patch = 0 | |
| elif which == 'patch': | |
| patch += 1 | |
| else: | |
| raise ValueError(f"Unknown bump type: {which}") | |
| return f"{major}.{minor}.{patch}" | |
| def load_version() -> dict: | |
| if VERSION_FILE.exists(): | |
| with VERSION_FILE.open('r', encoding='utf-8') as f: | |
| data = json.load(f) | |
| # minimal validation | |
| if 'version' not in data or 'timestamp' not in data: | |
| raise ValueError('version.json missing required fields') | |
| return data | |
| # default if not present | |
| return { | |
| 'version': '0.1.0', | |
| 'timestamp': int(time.time()), | |
| } | |
| def save_version(version: str, timestamp: int) -> None: | |
| with VERSION_FILE.open('w', encoding='utf-8') as f: | |
| json.dump({'version': version, 'timestamp': timestamp}, f, indent=2) | |
| f.write('\n') | |
| def update_sw_js(content: str, timestamp: int) -> str: | |
| # Replace: const VERSION = 1234567890; | |
| new_content, n = re.subn(r"(const\s+VERSION\s*=\s*)(\d+)(\s*;)", | |
| rf"\g<1>{timestamp}\g<3>", content) | |
| if n == 0: | |
| print('Warning: VERSION not updated in sw.js (pattern not found)') | |
| return new_content | |
| def update_html(content: str, version: str, timestamp: int, is_index: bool) -> str: | |
| updated = content | |
| # Update inline currentVersion = '<version>' | |
| updated, n_ver = re.subn(r"(currentVersion\s*=\s*')[^']*(')", rf"\g<1>{version}\g<2>", updated) | |
| if n_ver == 0: | |
| print('Warning: currentVersion not updated (pattern not found)') | |
| # Update inline timestamp = '<timestamp>' | |
| updated, n_ts = re.subn(r"(timestamp\s*=\s*')[^']*(')", rf"\g<1>{timestamp}\g<2>", updated) | |
| if n_ts == 0: | |
| print('Warning: timestamp not updated (pattern not found)') | |
| # Update script tag query params | |
| # For index.html: tree-track-app.js?v=X&t=Y | |
| # For map.html: /static/map.js?v=X&t=Y (we use same version for simplicity) | |
| if is_index: | |
| updated, n_tag = re.subn(r"(tree-track-app\.js\?v=)[^&']+(\&t=)[^'\"]+", | |
| rf"\g<1>{version}\g<2>{timestamp}", updated) | |
| if n_tag == 0: | |
| print('Warning: tree-track-app.js cache params not updated (pattern not found)') | |
| else: | |
| updated, n_tag = re.subn(r"(/static/map\.js\?v=)[^&']+(\&t=)[^'\"]+", | |
| rf"\g<1>{version}\g<2>{timestamp}", updated) | |
| if n_tag == 0: | |
| print('Warning: map.js cache params not updated (pattern not found)') | |
| return updated | |
| def main(): | |
| parser = argparse.ArgumentParser(description='Bump cache-busting version/timestamp across files') | |
| group = parser.add_mutually_exclusive_group() | |
| group.add_argument('--bump', choices=['major', 'minor', 'patch'], help='Semver bump type') | |
| group.add_argument('--set-version', help='Set explicit version (x.y.z)') | |
| parser.add_argument('--timestamp', type=int, help='Set explicit timestamp (epoch seconds)') | |
| parser.add_argument('--now', action='store_true', help='Use current time as timestamp') | |
| args = parser.parse_args() | |
| state = load_version() | |
| version = state['version'] | |
| timestamp = state['timestamp'] | |
| # Determine new version | |
| if args.set_version: | |
| if not SEMVER_RE.match(args.set_version): | |
| print('Error: --set-version must be in x.y.z format', file=sys.stderr) | |
| sys.exit(2) | |
| version = args.set_version | |
| elif args.bump: | |
| version = bump_semver(version, args.bump) | |
| # Determine new timestamp | |
| if args.timestamp is not None: | |
| timestamp = int(args.timestamp) | |
| elif args.now or args.set_version or args.bump: | |
| timestamp = int(time.time()) | |
| # Save version state | |
| save_version(version, timestamp) | |
| # Update files | |
| sw_path = FILES['sw'] | |
| index_path = FILES['index'] | |
| map_path = FILES['map'] | |
| sw_content = read_text(sw_path) | |
| index_content = read_text(index_path) | |
| map_content = read_text(map_path) | |
| sw_new = update_sw_js(sw_content, timestamp) | |
| index_new = update_html(index_content, version, timestamp, is_index=True) | |
| map_new = update_html(map_content, version, timestamp, is_index=False) | |
| if sw_new != sw_content: | |
| write_text(sw_path, sw_new) | |
| print(f"Updated {sw_path}") | |
| if index_new != index_content: | |
| write_text(index_path, index_new) | |
| print(f"Updated {index_path}") | |
| if map_new != map_content: | |
| write_text(map_path, map_new) | |
| print(f"Updated {map_path}") | |
| print(f"Done. version={version} timestamp={timestamp}") | |
| if __name__ == '__main__': | |
| main() | |