Simplify release DX: local manifest, optional hash, slimmer publish

release.sh writes updates/manifest.json at release time (predictable
asset URL) — no CI commit-back, no push-token problem, no detached
checkout dance. release_publish.sh is create-release + upload only.
Manifest sha256 now optional; updater skips verification with a note
when absent (HTTPS + gzip CRC cover the realistic cases). Fixed the
repo-URL sed keeping a .git suffix (caught by a worktree dry run).
Updater tests: 8 (sha-less manifest flow added).
This commit is contained in:
foxyz committed 2026-09-13 03:03:48 +03:00
1 parent afb4a9e051
commit 32848aaf71
9 files changed
+111 -71

No files matched your search

+12 -9
View File
@@ -24,34 +24,32 @@ class UpdateManifest {
final String version;
final int protocol;
final String url;
final String sha256;
final String? sha256;
final String notes;
const UpdateManifest(
{required this.version,
required this.protocol,
required this.url,
required this.sha256,
this.sha256,
required this.notes});
factory UpdateManifest.fromJson(Map<String, Object?> json) {
final version = json['version'];
final protocol = json['protocol'];
final url = json['url'];
final sha = json['sha256'];
if (version is! String ||
version.isEmpty ||
protocol is! int ||
url is! String ||
url.isEmpty ||
sha is! String ||
sha.isEmpty) {
throw const FormatException('update manifest missing version/protocol/url/sha256');
url.isEmpty) {
throw const FormatException('update manifest missing version/protocol/url');
}
final sha = json['sha256'];
return UpdateManifest(
version: version,
protocol: protocol,
url: url,
sha256: sha,
sha256: sha is String && sha.isNotEmpty ? sha : null,
notes: (json['notes'] as String?) ?? '');
}
}
@@ -220,7 +218,12 @@ Future<int> runUpgrade(
final file = File('${tmp.path}/update.tar.gz');
stdout.writeln('downloading ${manifest.url} ...');
await downloadFile(manifest.url, file, http: http);
await verifySha256(file, manifest.sha256);
if (manifest.sha256 != null) {
await verifySha256(file, manifest.sha256!);
} else {
stdout.writeln('no checksum in manifest — skipping verification '
'(HTTPS transit + gzip CRC still apply).');
}
stdout.writeln('verified, applying over ${root.path} ...');
await applyTarball(file, root);
} catch (e) {
+38 -2
View File
@@ -16,15 +16,20 @@ void main() {
expect(isNewerVersion('0.2.0', '0.1.9'), isFalse);
});
test('manifest rejects missing fields', () {
test('manifest rejects missing fields, sha256 is optional', () {
expect(() => UpdateManifest.fromJson({}), throwsFormatException);
expect(
() => UpdateManifest.fromJson({
'version': '0.2.0',
'protocol': 4,
'url': 'https://x/y.tar.gz',
}),
throwsFormatException);
final noSha = UpdateManifest.fromJson({
'version': '0.2.0',
'protocol': 4,
'url': 'https://x/y.tar.gz',
});
expect(noSha.sha256, isNull);
});
test('findInstallRoot honors bundle layout', () async {
@@ -162,6 +167,37 @@ void main() {
'binary-v1');
});
test('manifest without sha256 still applies', () async {
await server.close(force: true);
server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
server.listen((req) async {
if (req.uri.path == '/manifest.json') {
req.response
..statusCode = 200
..write(jsonEncode({
'version': '0.2.0',
'protocol': 4,
'url':
'http://127.0.0.1:${server.port}/watch-together-0.2.0.tar.gz',
'notes': '',
}));
} else {
req.response
..statusCode = 200
..add(tarball);
}
await req.response.close();
});
final root = await fakeRoot();
final rc = await runUpgrade(
currentVersion: '0.1.0',
manifestUrl: manifestUrl(),
installRoot: root);
expect(rc, 0);
expect(await File('${root.path}/watch_together').readAsString(),
'binary-v2');
});
test('unknown layout asks for manual update', () async {
final rc = await runUpgrade(
currentVersion: '0.1.0',