Files
IKAR/tool/deploy_server_fixes.py
T
sevenhill 5423355d83
Tests / test (push) Canceled after 0s
Build Android (FCM) / build-android-fcm (push) Canceled after 0s
Build Android / build-android (push) Canceled after 0s
Build iOS / build-ios (push) Canceled after 0s
Build Linux / build-linux (push) Canceled after 0s
Build macOS / build-macos (push) Canceled after 0s
Build Windows / build-windows (push) Canceled after 0s
Native crypto / native-crypto (push) Canceled after 0s
Release (main) / android (oneme) (push) Canceled after 0s
Release (main) / android (qlyra) (push) Canceled after 0s
Release (main) / windows (push) Canceled after 0s
Release (main) / linux (push) Canceled after 0s
Release (main) / macos (push) Canceled after 0s
Release (main) / ios (push) Canceled after 0s
Release (main) / release (push) Canceled after 0s
Add verified server rollout and live ghost-mode checks
2026-10-07 22:22:04 +03:00

103 lines
4.6 KiB
Python

"""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)