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:
foxyzandClaude Opus 5.5 committed 2026-10-18 02:11:37 +03:00
1 parent ec0849eb01
commit 638a30e16c
22 files changed
+1637 -58

No files matched your search

+28 -13
View File
@@ -3,7 +3,8 @@
// The Linux and Windows CI jobs both run this, possibly at the same time,
// so a lost create race falls back to the release the other job made.
// The manifest is written by tool/release.sh at release time (predictable
// asset URLs), so this never touches git history beyond reading notes.
// asset URLs). The release page's text is the version's CHANGELOG.md
// section (tool/changelog.dart).
// Usage: dart tool/release_publish.dart vX.Y.Z <archive>
// (run after tool/package_linux.sh or tool/package_windows.ps1)
// dart tool/release_publish.dart dev <archive> <manifest.json>
@@ -13,6 +14,8 @@
import 'dart:convert';
import 'dart:io';
import 'changelog.dart';
Future<void> main(List<String> args) async {
final dev = args.isNotEmpty && args[0] == 'dev';
if (args.length != (dev ? 3 : 2)) {
@@ -42,17 +45,7 @@ Future<void> main(List<String> args) async {
final token = _token();
final api = _apiBase(root);
final prev = _git(root, ['describe', '--tags', '--abbrev=0', '$tag^'],
allowFail: true);
final notes = prev.isEmpty
? 'Starsync $ver.'
: _git(root, [
'log',
'$prev..$tag',
'--format=- %s',
'--invert-grep',
'--grep=^Release v'
]).split('\n').take(20).join('\n');
final body = _changelog(root)?.section(ver)?.releaseBody(ver) ?? 'Starsync $ver.';
final http = HttpClient();
try {
@@ -63,7 +56,7 @@ Future<void> main(List<String> args) async {
rel = await c.post('$api/releases', {
'tag_name': tag,
'name': tag,
'body': 'Starsync $ver\n\n$notes',
'body': body,
'draft': false,
'prerelease': false,
});
@@ -130,6 +123,12 @@ Future<void> _publishDev(String root, List<File> files) async {
}) ??
_die('could not create or find release dev');
final id = rel['id'];
// The page says what the build has: Unreleased.
final ver = jsonDecode(files.last.readAsStringSync())['version'];
final next = _changelog(root)?.section(unreleased);
if (next != null && !next.isEmpty) {
await c.patch('$api/releases/$id', {'body': 'Latest test build; dev builds update from here.\n\n${next.releaseBody('$ver')}'});
}
final old = [for (final a in (rel['assets'] as List? ?? const [])) if (a is Map) a];
Future<void> upload(File f) {
final name = f.uri.pathSegments.last;
@@ -153,6 +152,16 @@ Future<void> _publishDev(String root, List<File> files) async {
}
}
Changelog? _changelog(String root) {
final f = File('$root/CHANGELOG.md');
try {
return Changelog.parse(f.readAsStringSync());
} on Object catch (e) {
stderr.writeln('warning: CHANGELOG.md not used: $e');
return null;
}
}
Never _die(String msg) {
stderr.writeln('error: $msg');
exit(1);
@@ -203,6 +212,12 @@ class _Api {
return jsonDecode(text) as Map<String, Object?>;
}
Future<void> patch(String url, Object json) async {
final (code, text) = await _send('PATCH', url,
body: utf8.encode(jsonEncode(json)), type: 'application/json');
_check('PATCH', url, code, text);
}
Future<void> delete(String url) async {
final (code, text) = await _send('DELETE', url);
_check('DELETE', url, code, text);