"""Deploy static assets only. Abort on drift; keep a private rollback snapshot.""" import argparse import hashlib import json import os from pathlib import Path import shutil import time parser = argparse.ArgumentParser() parser.add_argument('--apply', action='store_true') args = parser.parse_args() package = Path(__file__).resolve().parent root = Path('/home/agent/services/license') manifest = json.loads((package / 'manifest.json').read_text()) sha = lambda path: hashlib.sha256(path.read_bytes()).hexdigest() if path.exists() else None for entry in manifest: relative = Path(entry['path']) if relative.is_absolute() or '..' in relative.parts or relative.parts[0] != 'public': raise SystemExit('Unsafe manifest path') target = root / relative if target.is_symlink() or sha(target) != entry['before']: raise SystemExit('Runtime drift: ' + str(relative)) if sha(package / relative) != entry['after']: raise SystemExit('Release checksum mismatch: ' + str(relative)) if not args.apply: print(json.dumps({'preflight': 'ok', 'files': len(manifest)})) raise SystemExit(0) backup = Path('/srv/agent/forecast-completion-20261006') / ('portal-backup-' + str(time.time_ns())) backup.mkdir(mode=0o700) for entry in manifest: source = root / entry['path'] if source.exists(): target = backup / entry['path'] target.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(source, target) shutil.copyfile(package / 'manifest.json', backup / 'manifest.json') written = [] try: # New imported dependencies first; the HTML entrypoint is switched last. order = sorted(manifest, key=lambda e: (e['path'].endswith('index.html'), e['path'].endswith('app.js'))) for entry in order: target = root / entry['path'] if sha(target) != entry['before']: raise RuntimeError('Concurrent change: ' + entry['path']) temp = target.with_name(target.name + '.forecast-new') with temp.open('xb') as stream: stream.write((package / entry['path']).read_bytes()) os.chmod(temp, target.stat().st_mode & 0o777 if target.exists() else 0o644) os.replace(temp, target) written.append(entry) if any(sha(root / entry['path']) != entry['after'] for entry in manifest): raise RuntimeError('Post-deployment checksum mismatch') except Exception: for entry in reversed(written): target = root / entry['path'] if sha(target) != entry['after']: continue if entry['before'] is not None: shutil.copy2(backup / entry['path'], target) raise print(json.dumps({'deployed': True, 'files': len(written), 'backup': str(backup)}))