107 lines
4.0 KiB
Dart
107 lines
4.0 KiB
Dart
import 'dart:convert';
|
|
import 'dart:io';
|
|
import 'package:crypto/crypto.dart';
|
|
|
|
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 fileName = 'ikar-$version+$code.apk';
|
|
final reportSuffix = code == 1 ? '' : '-$version';
|
|
final base = Uri.parse('https://argus.kusoft.xyz');
|
|
final http = HttpClient()..connectionTimeout = const Duration(seconds: 15);
|
|
final local = File('artifacts/$fileName');
|
|
final downloaded = File('artifacts/ikar-$version+$code.argus.apk');
|
|
try {
|
|
Future<dynamic> getJson(String path) async {
|
|
final response = await (await http.getUrl(
|
|
base.resolve(path),
|
|
)).close().timeout(const Duration(seconds: 20));
|
|
if (response.statusCode != 200) {
|
|
throw StateError('HTTP ${response.statusCode} for $path');
|
|
}
|
|
return jsonDecode(
|
|
await utf8.decodeStream(response).timeout(const Duration(seconds: 20)),
|
|
);
|
|
}
|
|
|
|
final results = await Future.wait([
|
|
getJson('/api/apps/ikar/manifest?platform=android&channel=stable'),
|
|
getJson('/api/apps'),
|
|
getJson('/health'),
|
|
]);
|
|
final manifest = results[0] as Map<String, dynamic>;
|
|
final release = manifest['release'] as Map<String, dynamic>;
|
|
if (manifest['slug'] != 'ikar' ||
|
|
manifest['name'] != 'ИКАР' ||
|
|
release['version'] != version ||
|
|
release['androidVersionCode'] != code ||
|
|
release['platform'] != 'android' ||
|
|
release['channel'] != 'stable' ||
|
|
release['packageKind'] != 'apk' ||
|
|
release['originalFileName'] != fileName) {
|
|
throw StateError('Incorrect public application or release identity');
|
|
}
|
|
final catalog = results[1] as List;
|
|
final listed = catalog.singleWhere((app) => app['slug'] == 'ikar');
|
|
if (listed['name'] != 'ИКАР' ||
|
|
listed['latestRelease']['id'] != release['id'] ||
|
|
(results[2] as Map)['status'] != 'ok') {
|
|
throw StateError('Catalog or service health verification failed');
|
|
}
|
|
final uri = base.resolve(release['downloadPath'] as String);
|
|
if (uri.scheme != 'https' ||
|
|
uri.host != base.host ||
|
|
!uri.path.startsWith('/api/apps/ikar/releases/')) {
|
|
throw StateError('Unexpected public download address');
|
|
}
|
|
final response = await (await http.getUrl(
|
|
uri,
|
|
)).close().timeout(const Duration(seconds: 20));
|
|
if (response.statusCode != 200 ||
|
|
response.headers.contentType?.mimeType !=
|
|
'application/vnd.android.package-archive') {
|
|
throw StateError('APK download unavailable');
|
|
}
|
|
await response
|
|
.timeout(const Duration(seconds: 30))
|
|
.pipe(downloaded.openWrite());
|
|
final localDigest = (await sha256.bind(local.openRead()).first).toString();
|
|
final publicDigest = (await sha256.bind(downloaded.openRead()).first)
|
|
.toString();
|
|
final size = await downloaded.length();
|
|
if (publicDigest != localDigest ||
|
|
publicDigest != release['sha256'] ||
|
|
size != await local.length() ||
|
|
size != release['packageSizeBytes']) {
|
|
throw StateError('Public APK differs from the verified local artifact');
|
|
}
|
|
final report = {
|
|
'passed': true,
|
|
'slug': 'ikar',
|
|
'name': 'ИКАР',
|
|
'package': 'ru.ikar.app',
|
|
'version': version,
|
|
'androidVersionCode': code,
|
|
'releaseId': release['id'],
|
|
'sha256': publicDigest,
|
|
'bytes': size,
|
|
'tlsValidation': true,
|
|
'listedInCatalog': true,
|
|
'serviceHealth': 'ok',
|
|
'cardUrl': 'https://argus.kusoft.xyz/#app=ikar',
|
|
'downloadUrl': uri.toString(),
|
|
'timestamp': DateTime.now().toUtc().toIso8601String(),
|
|
};
|
|
const encoder = JsonEncoder.withIndent(' ');
|
|
await File(
|
|
'artifacts/argus-manifest$reportSuffix.json',
|
|
).writeAsString(encoder.convert(manifest));
|
|
await File(
|
|
'artifacts/argus-public-verification$reportSuffix.json',
|
|
).writeAsString(encoder.convert(report));
|
|
stdout.writeln(jsonEncode(report));
|
|
} finally {
|
|
http.close(force: true);
|
|
}
|
|
}
|