Files
IKAR/tool/deploy_server_fixes.py
T
sevenhill 12b692f4a9
Tests / test (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
Fix avatar URLs, photo IDs and profile update notifications
2026-10-07 22:34:40 +03:00

110 lines
5.1 KiB
Python

"""Deploy the complete tested IKAR API build, preserving other containers."""
from pathlib import Path
import hashlib
import json
import os
import re
import shutil
import sqlite3
import subprocess
import sys
import tarfile
import tempfile
import time
import urllib.request
root = Path('/mnt/data/ikar_server')
release_name = sys.argv[2] if len(sys.argv) > 2 else '20261007-komet'
previous_name = sys.argv[3] if len(sys.argv) > 3 else '20261007-video'
archive_name = sys.argv[4] if len(sys.argv) > 4 else 'server-fixes.tar.gz'
if not all(re.fullmatch(r'[a-zA-Z0-9_.-]+', name) and name not in {'.', '..'}
for name in (release_name, previous_name, archive_name)):
raise ValueError('Invalid release or archive name')
release = root / 'releases' / release_name
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' / previous_name 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 / archive_name) 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 = f'image: ikar_server:{previous_name}'
if text.count(old_image) != 1:
raise RuntimeError('Unexpected API image in compose')
compose.write_text(text.replace(old_image, f'image: ikar_server:{release_name}', 1))
print('Building IKAR API image', flush=True)
with (root / f'build-{release_name}.log').open('w') as log:
subprocess.run(['docker', 'build', '-f', 'Dockerfile.release', '-t', f'ikar_server:{release_name}', '.'],
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 / f'current.next-{release_name}'
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 / f'deployment-{release_name}.json').write_text(json.dumps(report, indent=2))
print(json.dumps(report), flush=True)