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
103 lines
4.6 KiB
Python
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)
|