63 lines
2.7 KiB
Python
63 lines
2.7 KiB
Python
"""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)}))
|