Publish IKAR Android 1.0.4 and verify release artifacts
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

This commit is contained in:
sevenhill
2026-10-07 22:46:55 +03:00
parent 12b692f4a9
commit 2fcd23a4cf
3 changed files with 120 additions and 15 deletions
+15 -5
View File
@@ -2,9 +2,9 @@
<img src="assets/ikar_icon.png" width="96" alt="Иконка ИКАР"> <img src="assets/ikar_icon.png" width="96" alt="Иконка ИКАР">
Самостоятельный Android-мессенджер с сервером `ikar_server`. Имя приложения — **ИКАР**, пакет Android — `ru.ikar.app`, версия исходников — `1.0.4+5` (не опубликована), вариант сборки — `ikar`. Опубликованный выпуск — `1.0.3+4`. Самостоятельный Android-мессенджер с сервером `ikar_server`. Имя приложения — **ИКАР**, пакет Android — `ru.ikar.app`, версия исходников и опубликованного выпуска — `1.0.4+5`, вариант сборки — `ikar`.
Клиент находится в `C:\Repos\IKAR`, сервер — в `ikar_server/`. Иконка перенесена из прежнего Икара. Исходный каталог `C:\Repos\Qlyra` не изменён; использованный клиентский код и библиотеки сохраняют свои авторские сведения и лицензии. Клиент находится в `C:\Repos\IKAR`, сервер — в `ikar_server/`. Основная иконка содержит рисунок Икара без надписей. Исходный каталог `C:\Repos\Qlyra` не изменён; использованный клиентский код и библиотеки сохраняют свои авторские сведения и лицензии.
## Возможности ## Возможности
@@ -18,6 +18,16 @@
Сквозное шифрование исходного клиента сохранено в коде как отдельная функция. Обычные сообщения сервер принимает и хранит в читаемом виде; HTTPS обеспечивает шифрование транспорта. Проверки не доказывают работу всех режимов сквозного шифрования и звонков с большим количеством участников. Сквозное шифрование исходного клиента сохранено в коде как отдельная функция. Обычные сообщения сервер принимает и хранит в читаемом виде; HTTPS обеспечивает шифрование транспорта. Проверки не доказывают работу всех режимов сквозного шифрования и звонков с большим количеством участников.
## Выпуск 1.0.4
В [Аргусе](https://argus.kusoft.xyz/#app=ikar) опубликована [ИКАР 1.0.4, сборка 5](https://argus.kusoft.xyz/api/apps/ikar/releases/24f6ba10-f133-443e-9635-e38e54ce7852/download). Выпуск включает новую главную иконку, собственную тему, изменения жестов и видеоредактора, действия с расшифрованным текстом, работающую с сервером невидимость и меню расшифровки голосовых сообщений. Серверное исправление аватарок уже развёрнуто.
APK `artifacts/ikar-1.0.4+5.apk` подписан прежним ключом ИКАР: обновление сохраняет совместимость подписи с 1.0.3. Пакет `ru.ikar.app`, версия `1.0.4`, `versionCode=5`; размер — 88 773 085 байт, SHA-256 — `56ed77c0965cafc7bb71daee6af51e9418c93aab92b07acda9263134eb41e410`. В итоговом APK проверены свежие Flutter-библиотеки всех трёх ABI, наличие нативных зависимостей, актуальные изображения и пиксели адаптивной иконки для всех плотностей, нулевой inset основного рисунка.
Публичные каталог, манифест и скачанный из Аргуса APK совпадают по версии, размеру и SHA-256; TLS и здоровье сервиса проверены. Обновление через сервер ИКАР также выдаёт новый манифест и тот же APK. Перед публикацией создана согласованная копия базы Аргуса; рабочая база и копия целостны, остальные карточки и работающие сервисы сохранены. Предыдущие выпуски оставлены. Проверки исходников описаны ниже; на физический телефон новый APK не устанавливался — подключённых ADB-устройств нет.
Отчёты: `artifacts/apk-build-1.0.4.log`, `artifacts/apk-verification-1.0.4.json`, `artifacts/argus-publication-1.0.4.json`, `artifacts/argus-public-verification-1.0.4.json`, `artifacts/release-verification-1.0.4.json`. Проверка локального APK: `python tool/verify_android_apk.py --apk artifacts/ikar-1.0.4+5.apk --previous artifacts/ikar-1.0.3+4.argus.apk --version 1.0.4 --code 5`; проверки публикации: `dart run tool/verify_argus_release.dart 1.0.4 5`, `dart run tool/release_smoke.dart 1.0.4 5`.
## Исправление установки аватарки ## Исправление установки аватарки
Фото загружалось на сервер, но профиль и список фотографий возвращали относительный путь изображения и не передавали `photoId`. Клиент не мог открыть фото и корректно обновить кэш. Теперь эти ответы содержат полный HTTPS-адрес и стабильный ID фотографии; замена меняет URL и ID, удаление очищает оба значения. Push обновления профиля использует ожидаемую клиентом структуру `payload.profile`. Фото загружалось на сервер, но профиль и список фотографий возвращали относительный путь изображения и не передавали `photoId`. Клиент не мог открыть фото и корректно обновить кэш. Теперь эти ответы содержат полный HTTPS-адрес и стабильный ID фотографии; замена меняет URL и ID, удаление очищает оба значения. Push обновления профиля использует ожидаемую клиентом структуру `payload.profile`.
@@ -44,7 +54,7 @@
Главная иконка содержит только рисунок Икара без надписей. Размер рисунка — около 90% диаметра круглой области; адаптивный слой учитывает расширение Android с 72 до 108 dp. Исходный рисунок — `assets/ikar_mark.png`, основные ресурсы — `assets/ikar_icon.png` и `assets/ikar.png`. Пересоздание: `dart run tool/make_ikar_icons.dart`, затем `dart run flutter_launcher_icons`. Ресурсы основной иконки обновлены для Android, iOS, Windows и macOS; альтернативная иконка сохранена. Главная иконка содержит только рисунок Икара без надписей. Размер рисунка — около 90% диаметра круглой области; адаптивный слой учитывает расширение Android с 72 до 108 dp. Исходный рисунок — `assets/ikar_mark.png`, основные ресурсы — `assets/ikar_icon.png` и `assets/ikar.png`. Пересоздание: `dart run tool/make_ikar_icons.dart`, затем `dart run flutter_launcher_icons`. Ресурсы основной иконки обновлены для Android, iOS, Windows и macOS; альтернативная иконка сохранена.
Новая иконка и клиентские изменения требуют нового APK; текущий опубликованный выпуск их не содержит. Выпуск APK и проверка этих клиентских изменений на телефоне в этой задаче не выполнялись. Новая иконка и клиентские изменения включены в опубликованный APK 1.0.4. Проверка этих клиентских изменений на физическом телефоне пока не выполнялась.
## Размещение на Pi ## Размещение на Pi
@@ -86,13 +96,13 @@ dotnet test ikar_server/src/Ikar.Server.Tests/Ikar.Server.Tests.csproj
`tool/deploy_ikar.ps1` упаковывает опубликованный `linux-arm64` сервер и обновляет только новый Compose-проект на Pi. Модель Vosk в `ikar_server/asr/model.zip` проверяется по SHA-256 при сборке контейнера. Скрипты в `tool/` для проверок используют временные тестовые аккаунты и удаляют их по завершении. `tool/deploy_ikar.ps1` упаковывает опубликованный `linux-arm64` сервер и обновляет только новый Compose-проект на Pi. Модель Vosk в `ikar_server/asr/model.zip` проверяется по SHA-256 при сборке контейнера. Скрипты в `tool/` для проверок используют временные тестовые аккаунты и удаляют их по завершении.
## Меню расшифровки голосовых сообщений 1.0.4 (не опубликовано) ## Меню расшифровки голосовых сообщений 1.0.4
Расшифровка сохранена и перенесена из маленькой кнопки «Т» в меню голосового сообщения. Нажатие на облако открывает меню с пунктом «Расшифровка голосового сообщения»: нажимается вся строка, её высота не меньше 48 логических пикселей и увеличивается при переносе текста или увеличенном шрифте. У обычных сообщений, файлов, видео, удалённых и ещё не отправленных записей этого пункта нет. Пересланные записи используют идентификаторы исходного голосового сообщения. Расшифровка сохранена и перенесена из маленькой кнопки «Т» в меню голосового сообщения. Нажатие на облако открывает меню с пунктом «Расшифровка голосового сообщения»: нажимается вся строка, её высота не меньше 48 логических пикселей и увеличивается при переносе текста или увеличенном шрифте. У обычных сообщений, файлов, видео, удалённых и ещё не отправленных записей этого пункта нет. Пересланные записи используют идентификаторы исходного голосового сообщения.
Под записью отображается состояние распознавания, затем готовый текст; через меню его можно скрыть и показать снова без повторного запроса. Одновременные нажатия не создают повторные запросы. Ошибку можно повторить; результат, уже полученный через push, не затирается поздним ответом о постановке в очередь или сетевой ошибкой. Под записью отображается состояние распознавания, затем готовый текст; через меню его можно скрыть и показать снова без повторного запроса. Одновременные нажатия не создают повторные запросы. Ошибку можно повторить; результат, уже полученный через push, не затирается поздним ответом о постановке в очередь или сетевой ошибкой.
Целевые тесты: `artifacts/voice-menu-tests.log`. Анализ изменённых UI-файлов и новых тестов: `artifacts/voice-menu-analyze-final.log`. Живая проверка тестовой записи подтвердила загрузку, запрос получателя, завершение распознавания через push, сохранение текста для обоих участников и запрет доступа постороннему: `artifacts/voice-transcription-live-verification.json`. Тестовые аккаунты удалены. Новая версия не публиковалась и не устанавливалась на телефон по указанию пользователя; нового APK нет. Целевые тесты: `artifacts/voice-menu-tests.log`. Анализ изменённых UI-файлов и новых тестов: `artifacts/voice-menu-analyze-final.log`. Живая проверка тестовой записи подтвердила загрузку, запрос получателя, завершение распознавания через push, сохранение текста для обоих участников и запрет доступа постороннему: `artifacts/voice-transcription-live-verification.json`. Тестовые аккаунты удалены. Меню включено в опубликованный APK 1.0.4; на физический телефон этот выпуск пока не устанавливался.
## Исправления видео 1.0.3 ## Исправления видео 1.0.3
+18 -10
View File
@@ -1,45 +1,53 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:crypto/crypto.dart'; import 'package:crypto/crypto.dart';
import '../lib/core/transport/ikar_network.dart'; import 'package:ikar/core/transport/ikar_network.dart';
Future<void> main() async { Future<void> main(List<String> args) async {
final version = args.isEmpty ? '1.0.0' : args[0];
final code = args.length < 2 ? 1 : int.parse(args[1]);
final reportSuffix = code == 1 ? '' : '-$version';
final http = IkarNetwork.client(); final http = IkarNetwork.client();
try { try {
final base = Uri.parse('https://ikar.kusoft.xyz'); final base = Uri.parse('https://ikar.kusoft.xyz');
final response = await (await http.getUrl( final response = await (await http.getUrl(
base.resolve('/api/apps/ikar/manifest'), base.resolve('/api/apps/ikar/manifest'),
)).close(); )).close();
if (response.statusCode != 200) if (response.statusCode != 200) {
throw StateError('Release manifest unavailable'); throw StateError('Release manifest unavailable');
}
final manifest = jsonDecode(await utf8.decodeStream(response)); final manifest = jsonDecode(await utf8.decodeStream(response));
if (manifest['app']['packageName'] != 'ru.ikar.app' || if (manifest['app']['packageName'] != 'ru.ikar.app' ||
manifest['release']['version'] != '1.0.0+1') manifest['release']['version'] != '$version+$code') {
throw StateError('Incorrect release identity'); throw StateError('Incorrect release identity');
}
final expected = (await File( final expected = (await File(
'artifacts/ikar-1.0.0+1.apk.sha256', 'artifacts/ikar-$version+$code.apk.sha256',
).readAsString()).split(' ').first; ).readAsString()).split(' ').first;
if (manifest['release']['sha256'] != expected) if (manifest['release']['sha256'] != expected) {
throw StateError('Manifest checksum mismatch'); throw StateError('Manifest checksum mismatch');
}
final download = await (await http.getUrl( final download = await (await http.getUrl(
base.resolve(manifest['release']['downloadPath']), base.resolve(manifest['release']['downloadPath']),
)).close(); )).close();
if (download.statusCode != 200) if (download.statusCode != 200) {
throw StateError('APK download unavailable'); throw StateError('APK download unavailable');
}
final digest = await sha256.bind(download).first; final digest = await sha256.bind(download).first;
if (digest.toString() != expected) if (digest.toString() != expected) {
throw StateError('Downloaded APK checksum mismatch'); throw StateError('Downloaded APK checksum mismatch');
}
final report = { final report = {
'passed': true, 'passed': true,
'package': 'ru.ikar.app', 'package': 'ru.ikar.app',
'version': '1.0.0+1', 'version': '$version+$code',
'sha256': expected, 'sha256': expected,
'tlsValidation': true, 'tlsValidation': true,
'bytes': manifest['release']['packageSizeBytes'], 'bytes': manifest['release']['packageSizeBytes'],
'timestamp': DateTime.now().toUtc().toIso8601String(), 'timestamp': DateTime.now().toUtc().toIso8601String(),
}; };
await File( await File(
'artifacts/release-verification.json', 'artifacts/release-verification$reportSuffix.json',
).writeAsString(const JsonEncoder.withIndent(' ').convert(report)); ).writeAsString(const JsonEncoder.withIndent(' ').convert(report));
stdout.writeln(jsonEncode(report)); stdout.writeln(jsonEncode(report));
} finally { } finally {
+87
View File
@@ -0,0 +1,87 @@
"""Verify APK identity, upgrade signature, fresh Flutter libraries and icon assets."""
import argparse
import hashlib
import io
import json
from pathlib import Path
import re
import subprocess
import zipfile
from PIL import Image
def sha(data):
return hashlib.sha256(data).hexdigest()
def verify(args):
apk = Path(args.apk)
previous = Path(args.previous)
sdk = Path(args.build_tools)
metadata = subprocess.check_output([str(sdk / 'aapt.exe'), 'dump', 'badging', str(apk)], text=True, encoding='utf-8')
identity = re.search(r"package: name='([^']+)' versionCode='([^']+)' versionName='([^']+)'", metadata)
if identity is None or identity.groups() != ('ru.ikar.app', str(args.code), args.version):
raise RuntimeError('Incorrect Android package or version')
def certificate(path):
output = subprocess.check_output([str(sdk / 'apksigner.bat'), 'verify', '--verbose', '--print-certs', str(path)], text=True, encoding='utf-8')
values = re.findall(r'Signer #\d+ certificate SHA-256 digest: ([0-9a-f]+)', output)
if len(values) != 1 or 'Verified using v2 scheme (APK Signature Scheme v2): true' not in output:
raise RuntimeError('APK signature verification failed')
return values[0]
signer = certificate(apk)
if signer != certificate(previous):
raise RuntimeError('Signing certificate differs from the previously published APK')
latest_source = max(path.stat().st_mtime for path in Path('lib').rglob('*.dart'))
libraries = []
assets = []
with zipfile.ZipFile(apk) as package:
icon_path = re.search(r"application-icon-160:'([^']+)'", metadata).group(1)
icon_tree = subprocess.check_output([str(sdk / 'aapt.exe'), 'dump', 'xmltree', str(apk), icon_path], text=True, encoding='utf-8')
inset = re.search(r'android:inset\([^)]*\)=\(type 0x6\)0x([0-9a-f]+)', icon_tree)
if inset is None or int(inset[1], 16) >> 8 != 0:
raise RuntimeError('Adaptive launcher icon still has the old inset')
resources = subprocess.check_output([str(sdk / 'aapt.exe'), 'dump', '--values', 'resources', str(apk)], text=True, encoding='utf-8')
paths = re.findall(r'[^\n]*ru\.ikar\.app:drawable/ic_launcher_foreground: t=0x03[^\n]*\n\s*\(string8\) "([^"]+)"', resources)
expected_icons = {}
for path in Path('android/app/src/main/res').glob('drawable-*/ic_launcher_foreground.png'):
with Image.open(path) as image:
expected_icons[image.size] = image.convert('RGBA').tobytes()
if len(paths) != len(expected_icons):
raise RuntimeError('Missing launcher foreground densities')
for path in paths:
with Image.open(io.BytesIO(package.read(path))) as image:
if image.convert('RGBA').tobytes() != expected_icons.get(image.size):
raise RuntimeError('Packaged launcher icon differs from the current drawing')
for abi in ('armeabi-v7a', 'arm64-v8a', 'x86_64'):
generated = Path(f'build/app/intermediates/flutter/ikarRelease/jniLibs/{abi}/libapp.so')
stripped = Path(f'build/app/intermediates/stripped_native_libs/ikarRelease/stripIkarReleaseDebugSymbols/out/lib/{abi}/libapp.so')
actual = sha(package.read(f'lib/{abi}/libapp.so'))
if actual != sha(stripped.read_bytes()) or generated.stat().st_mtime < latest_source:
raise RuntimeError(f'Stale Flutter library for {abi}')
for name in ('libflutter.so', 'libfjs.so', 'libkomet_crypto.so', 'libkomet_tlottie.so'):
if not package.read(f'lib/{abi}/{name}'):
raise RuntimeError(f'Missing native dependency: {abi}/{name}')
libraries.append({'abi': abi, 'apkSha256': actual, 'matchesStripped': True, 'generatedAfterLastSourceChange': True})
for name in ('ikar.png', 'ikar_icon.png'):
expected = Path('assets') / name
if sha(package.read(f'assets/flutter_assets/assets/{name}')) != sha(expected.read_bytes()):
raise RuntimeError(f'Stale icon asset: {name}')
assets.append(name)
report = {'passed': True, 'package': 'ru.ikar.app', 'version': args.version,
'androidVersionCode': args.code, 'sha256': sha(apk.read_bytes()), 'bytes': apk.stat().st_size,
'certificateSha256': signer, 'upgradeSignerMatches': True, 'libraries': libraries, 'currentIconAssets': assets,
'adaptiveLauncherMatchesCurrentDrawing': True, 'adaptiveLauncherInsetZero': True}
Path(f'artifacts/apk-verification-{args.version}.json').write_text(json.dumps(report, indent=2), encoding='utf-8')
print(json.dumps(report))
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('--apk', required=True)
parser.add_argument('--previous', required=True)
parser.add_argument('--version', required=True)
parser.add_argument('--code', required=True, type=int)
parser.add_argument('--build-tools', default='C:/Tools/AndroidSdk/build-tools/36.0.0')
verify(parser.parse_args())