forked from Foxyz/sync_replay_project
Changelog and What's new: CHANGELOG.md cut into each release, shown in the update band, a What's new card and once after updating
CHANGELOG.md keeps an Unreleased section (user-facing changes add a line in the same commit); tool/release.sh cuts it into the version, the manifest gets a structured "changes" field next to the one-line "notes" older apps read, and the Gitea release text is that section. The app's update band shows the top items, What's new opens a card (Added / Changed / Fixed), and the first start after an update shows it once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ec0849eb01
commit
638a30e16c
22 files changed
+1637
-58
No files matched your search
@@ -78,18 +78,50 @@ const _stagingDir = '.update-staging';
|
||||
const _backupDir = '.update-backup';
|
||||
const _downloadFile = '.update-download';
|
||||
|
||||
/// What a release changed, in the words of CHANGELOG.md: the manifest's
|
||||
/// `changes` ({"added": [...], "changed": [...], "fixed": [...]}).
|
||||
/// Manifests from before it have only `notes`.
|
||||
class ReleaseChanges {
|
||||
final List<String> added;
|
||||
final List<String> changed;
|
||||
final List<String> fixed;
|
||||
const ReleaseChanges({this.added = const [], this.changed = const [], this.fixed = const []});
|
||||
|
||||
/// Null for anything that is not a map, or one with no items; entries
|
||||
/// that are not strings (or blank) are dropped, unknown keys ignored.
|
||||
static ReleaseChanges? fromJson(Object? json) {
|
||||
if (json is! Map) return null;
|
||||
List<String> list(String key) => List.unmodifiable([
|
||||
if (json[key] case final List l)
|
||||
for (final item in l)
|
||||
if (item is String && item.trim().isNotEmpty) item.trim(),
|
||||
]);
|
||||
final c = ReleaseChanges(added: list('added'), changed: list('changed'), fixed: list('fixed'));
|
||||
return c.isEmpty ? null : c;
|
||||
}
|
||||
|
||||
Map<String, Object?> toJson() => {'added': added, 'changed': changed, 'fixed': fixed};
|
||||
|
||||
bool get isEmpty => added.isEmpty && changed.isEmpty && fixed.isEmpty;
|
||||
int get length => added.length + changed.length + fixed.length;
|
||||
}
|
||||
|
||||
class UpdateManifest {
|
||||
final String version;
|
||||
final int protocol;
|
||||
final String url;
|
||||
final String? sha256;
|
||||
/// One line for every app (1.2.x and 1.3.x show only this).
|
||||
final String notes;
|
||||
/// Null in manifests from before the changelog (show [notes] then).
|
||||
final ReleaseChanges? changes;
|
||||
const UpdateManifest(
|
||||
{required this.version,
|
||||
required this.protocol,
|
||||
required this.url,
|
||||
this.sha256,
|
||||
required this.notes});
|
||||
required this.notes,
|
||||
this.changes});
|
||||
|
||||
// `assets` maps platform → archive URL. Top-level `url` is the Linux
|
||||
// tarball, kept for builds from before `assets` existed (<= 0.2.1).
|
||||
@@ -115,7 +147,8 @@ class UpdateManifest {
|
||||
protocol: protocol,
|
||||
url: url,
|
||||
sha256: sha is String && sha.isNotEmpty ? sha : null,
|
||||
notes: (json['notes'] as String?) ?? '');
|
||||
notes: json['notes'] is String ? json['notes'] as String : '',
|
||||
changes: ReleaseChanges.fromJson(json['changes']));
|
||||
}
|
||||
|
||||
// Newer wire protocol: this build can't join servers running it.
|
||||
|
||||
@@ -190,6 +190,47 @@ void main() {
|
||||
isTrue);
|
||||
});
|
||||
|
||||
test('manifest changes: structured lists, notes kept, old manifests without', () {
|
||||
final base = {
|
||||
'version': '1.3.3',
|
||||
'protocol': RelayConstants.protoVersion,
|
||||
'url': 'https://x/y.tar.gz',
|
||||
'notes': 'One line for older apps.',
|
||||
};
|
||||
final old = UpdateManifest.fromJson(base);
|
||||
expect(old.changes, isNull);
|
||||
expect(old.notes, 'One line for older apps.');
|
||||
|
||||
final m = UpdateManifest.fromJson({
|
||||
...base,
|
||||
'changes': {
|
||||
'added': ['Danmaku style notifications', ' ', 7],
|
||||
'fixed': ['Various bug fixes'],
|
||||
'later': ['unknown kinds are ignored'],
|
||||
},
|
||||
});
|
||||
expect(m.notes, 'One line for older apps.');
|
||||
expect(m.changes!.added, ['Danmaku style notifications']);
|
||||
expect(m.changes!.changed, isEmpty);
|
||||
expect(m.changes!.fixed, ['Various bug fixes']);
|
||||
expect(m.changes!.length, 2);
|
||||
expect(ReleaseChanges.fromJson(m.changes!.toJson())!.toJson(), m.changes!.toJson());
|
||||
|
||||
// Junk or empty `changes` (and a non-string `notes`) do not break the manifest.
|
||||
for (final junk in [null, 'text', [], {}, {'added': 'no list'}]) {
|
||||
expect(UpdateManifest.fromJson({...base, 'changes': junk}).changes, isNull);
|
||||
}
|
||||
expect(UpdateManifest.fromJson({...base, 'notes': 3}).notes, '');
|
||||
});
|
||||
|
||||
test('the repo manifest parses, and keeps a one-line string notes for 1.2.x/1.3.x', () {
|
||||
final json = jsonDecode(File('../../updates/manifest.json').readAsStringSync()) as Map<String, Object?>;
|
||||
// Older apps read `(json['notes'] as String?)`: it must stay a string.
|
||||
expect(json['notes'], isA<String>());
|
||||
expect((json['notes'] as String).contains('\n'), isFalse);
|
||||
expect(UpdateManifest.fromJson(json, platform: 'linux-x64').version, json['version']);
|
||||
});
|
||||
|
||||
test('manifest picks the asset for this platform', () {
|
||||
final json = {
|
||||
'version': '0.2.2',
|
||||
|
||||
Reference in new issue
Block a user