"""Deploy the complete tested IKAR API build, preserving other containers.""" from pathlib import Path import hashlib import json import os import shutil import sqlite3 import subprocess import sys import tarfile import tempfile import time import urllib.request root = Path('/mnt/data/ikar_server') release = root / 'releases/20261007-komet' current = root / 'current' previous = current.resolve(strict=True) expected_sha = sys.argv[1].lower() if not os.path.ismount('/mnt/data') or previous != root / 'releases/20261007-video' or release.exists(): raise RuntimeError('Unexpected release or external disk state') def snapshot(): containers = subprocess.check_output(['docker', 'ps', '-q'], text=True).split() return {item['Name']: {'id': item['Id'], 'startedAt': item['State']['StartedAt']} for item in json.loads(subprocess.check_output(['docker', 'inspect', *containers], text=True)) if item['Name'] != '/ikar_server'} protected_before = snapshot() print('Creating database and attachment backup', flush=True) backup = Path(subprocess.check_output(['python3', str(previous / 'backup.py')], text=True).strip()) with tempfile.TemporaryDirectory() as temp: target = Path(temp) / 'ikar.db' with tarfile.open(backup) as archive: target.write_bytes(archive.extractfile('data/ikar.db').read()) with sqlite3.connect(target) as db: if db.execute('PRAGMA integrity_check').fetchone()[0] != 'ok': raise RuntimeError('Backup failed integrity verification') shutil.copytree(previous, release, ignore=shutil.ignore_patterns('publish', 'release.tar.gz')) with tarfile.open(root / 'server-fixes.tar.gz') as archive: archive.extractall(release, filter='data') actual_sha = hashlib.sha256((release / 'publish/Ikar.Server.dll').read_bytes()).hexdigest() if actual_sha != expected_sha: raise RuntimeError('Uploaded server differs from verified local build') compose = release / 'compose.yaml' text = compose.read_text() old_image = 'image: ikar_server:20261007-video' if text.count(old_image) != 1: raise RuntimeError('Unexpected API image in compose') compose.write_text(text.replace(old_image, 'image: ikar_server:20261007-komet', 1)) print('Building IKAR API image', flush=True) with (root / 'build-komet.log').open('w') as log: subprocess.run(['docker', 'build', '-f', 'Dockerfile.release', '-t', 'ikar_server:20261007-komet', '.'], cwd=release, stdout=log, stderr=subprocess.STDOUT, check=True) def update(path): subprocess.run(['docker', 'compose', '--project-directory', str(path), '--env-file', str(root / 'shared/turn.env'), 'up', '-d', '--no-deps', 'ikar_server'], check=True) try: print('Updating API container', flush=True) update(release) health = None for attempt in range(30): try: with urllib.request.urlopen('http://127.0.0.1:5099/health', timeout=3) as response: health = json.load(response) if health.get('status') == 'ok': break except Exception: pass time.sleep(1) if health is None or health.get('status') != 'ok': raise RuntimeError('New API failed health verification') running_sha = subprocess.check_output(['docker', 'exec', 'ikar_server', 'sha256sum', '/app/Ikar.Server.dll'], text=True).split()[0] if running_sha != expected_sha: raise RuntimeError('Running server differs from verified build') with sqlite3.connect(f'file:{root}/shared/data/ikar.db?mode=ro', uri=True) as db: integrity = db.execute('PRAGMA integrity_check').fetchone()[0] columns = {row[1] for row in db.execute('PRAGMA table_info(Users)')} if integrity != 'ok' or 'PublicLastSeenUnixMs' not in columns: raise RuntimeError('Database or presence schema verification failed') subprocess.run(['docker', 'exec', 'ikar_server', 'ffmpeg', '-version'], stdout=subprocess.DEVNULL, check=True) pending = root / 'current.next-komet' pending.symlink_to(release, target_is_directory=True) os.replace(pending, current) except Exception: print('Rolling back API to previous release', flush=True) update(previous) raise report = { 'passed': True, 'release': str(release), 'previousRelease': str(previous), 'backup': str(backup), 'backupIntegrity': 'ok', 'health': health, 'databaseIntegrity': integrity, 'publicPresenceColumn': True, 'serverSha256': running_sha, 'ffmpegAvailable': True, 'protectedServicesUnchanged': snapshot() == protected_before, 'protectedServices': protected_before, } (root / 'deployment-komet.json').write_text(json.dumps(report, indent=2)) print(json.dumps(report), flush=True)