build(release): what's new drafted from the commits' Whats-New trailers, other apps' commits left out
The drafts came from the commits' subjects, which are written for developers: each of the releases of 4 Nov 2026 (Todo 0.2.0, Train 0.3.0, Learn 0.24.0) listed every commit since the last, Meditate's among them where it touched a shared package, more than twice F-Droid's 500 characters, the German all still to translate, and each had to be written again whole. A commit people would notice now says so in its message's trailers, `Whats-New: <sentence>` once a change and `Whats-New-De: <Satz>` in the same order, and the draft is those lines alone, the features' first, a missing translation marked TRANSLATE:. Only where no commit of the release carries one does it fall back to the subjects. A commit whose scope names apps (feat(todo,meditate): ...) counts for those alone, any other for every app it touches. Over a running next.<language>.txt per app, which parallel branches conflict on at every merge, and over an assistant drafting at release time (a key, the commits sent to a provider, a different text each run). ADR 0030, D61. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
3310ffa1cd
commit
4239a1de24
2 changed files with 211 additions and 31 deletions
|
|
@ -699,7 +699,15 @@ final class Releaser({
|
|||
List<Commit> commits, {
|
||||
required bool first,
|
||||
}) async {
|
||||
final drafts = draftWhatsNew(commits, first: first);
|
||||
final drafts = draftWhatsNew(
|
||||
commits,
|
||||
first: first,
|
||||
app: app.id,
|
||||
apps: {
|
||||
for (final entry in Directory(p.join(root.path, 'apps')).listSync())
|
||||
if (entry is Directory) p.basename(entry.path),
|
||||
},
|
||||
);
|
||||
final files = {
|
||||
for (final language in whatsNewLanguages)
|
||||
language: File(p.join(root.path, whatsNewFile(app, version, language))),
|
||||
|
|
|
|||
|
|
@ -26,34 +26,110 @@ String whatsNewFile(AppRelease app, Version version, String language) =>
|
|||
/// The files' names in the app's folder: `<version>.<language>.txt`.
|
||||
final whatsNewName = RegExp(r'^(\d+\.\d+\.\d+)\.([a-z]{2,3})\.txt$');
|
||||
|
||||
/// The drafts of what's new, by language, from the features and fixes among
|
||||
/// [commits] (oldest first), the features first: each commit's description
|
||||
/// without its type, scope and closing references (`(ADR 0030, D60)`),
|
||||
/// starting with a capital and ending with a full stop. English as the
|
||||
/// commits say it, every other language the same lines marked with
|
||||
/// [translateMark]. [first]: the app's first release.
|
||||
Map<String, String> draftWhatsNew(List<Commit> commits, {required bool first}) {
|
||||
List<String> changes(String types) => [
|
||||
/// The trailer of a commit's message that says, in English, what it changes
|
||||
/// for the people using the app: `Whats-New: <sentence>`, once a change;
|
||||
/// each other language's is `Whats-New-<Language>:` (`Whats-New-De:`), its
|
||||
/// lines in the same order. Written as the commit is (decided 4 Nov 2026), so
|
||||
/// that a release finds its texts ready rather than drafted from subjects
|
||||
/// written for developers.
|
||||
const whatsNewTrailer = 'Whats-New';
|
||||
|
||||
/// The drafts of what's new in [app], by language, from [commits] (oldest
|
||||
/// first), those that count for [app]: a commit whose scope names apps
|
||||
/// ([apps], the ids of every app, `feat(todo,meditate): …`) counts for those
|
||||
/// alone, any other for every app it touches.
|
||||
///
|
||||
/// Where any of them carries [whatsNewTrailer] lines, those lines, in each
|
||||
/// language, the features' first; a line without its translation is the
|
||||
/// English marked with [translateMark]. Else, as before the trailers, the
|
||||
/// features and fixes among them, the features first: each commit's
|
||||
/// description without its type, scope and closing references
|
||||
/// (`(ADR 0030, D60)`), starting with a capital and ending with a full stop,
|
||||
/// every other language the same lines marked. [first]: the app's first
|
||||
/// release.
|
||||
Map<String, String> draftWhatsNew(
|
||||
List<Commit> commits, {
|
||||
required bool first,
|
||||
required String app,
|
||||
required Set<String> apps,
|
||||
}) {
|
||||
final counting = [
|
||||
for (final commit in commits)
|
||||
if (RegExp('^($types)(\\([^)]*\\))?!?: (.*)').firstMatch(commit.subject)
|
||||
case final match?)
|
||||
_sentence(match.group(3)!),
|
||||
if (_countsFor(commit, app, apps)) commit,
|
||||
];
|
||||
final lines = first
|
||||
? ['The first release.']
|
||||
: [...changes('feat'), ...changes('fix|perf')];
|
||||
final ordered = [
|
||||
...counting.where((commit) => _hasType(commit, 'feat')),
|
||||
...counting.where((commit) => !_hasType(commit, 'feat')),
|
||||
];
|
||||
final written = [for (final commit in ordered) ..._trailerLines(commit)];
|
||||
final List<Map<String, String>> lines;
|
||||
if (first) {
|
||||
lines = [
|
||||
{whatsNewLanguages.first: 'The first release.'},
|
||||
];
|
||||
} else if (written.isNotEmpty) {
|
||||
lines = written;
|
||||
} else {
|
||||
lines = [
|
||||
for (final types in ['feat', 'fix|perf'])
|
||||
for (final commit in counting)
|
||||
if (RegExp('^($types)(\\([^)]*\\))?!?: (.*)')
|
||||
.firstMatch(commit.subject)
|
||||
case final match?)
|
||||
{whatsNewLanguages.first: _sentence(match.group(3)!)},
|
||||
];
|
||||
}
|
||||
return {
|
||||
for (final language in whatsNewLanguages)
|
||||
language: [
|
||||
for (final line in lines)
|
||||
if (language == whatsNewLanguages.first)
|
||||
'- $line\n'
|
||||
if (line[language] case final text?)
|
||||
'- $text\n'
|
||||
else
|
||||
'- $translateMark $line\n',
|
||||
'- $translateMark ${line[whatsNewLanguages.first]}\n',
|
||||
].join(),
|
||||
};
|
||||
}
|
||||
|
||||
/// Whether [commit] counts for [app]: its scope names no app of [apps], or
|
||||
/// names [app].
|
||||
bool _countsFor(Commit commit, String app, Set<String> apps) {
|
||||
final scope = RegExp(r'^[a-z]+\(([^)]*)\)').firstMatch(commit.subject)?[1];
|
||||
final named = {
|
||||
for (final part in scope?.split(',') ?? <String>[])
|
||||
if (apps.contains(part.trim())) part.trim(),
|
||||
};
|
||||
return named.isEmpty || named.contains(app);
|
||||
}
|
||||
|
||||
bool _hasType(Commit commit, String type) =>
|
||||
RegExp('^$type(\\([^)]*\\))?!?:').hasMatch(commit.subject);
|
||||
|
||||
/// The [whatsNewTrailer] lines of [commit]'s message, each with its
|
||||
/// translations by language where given, in the order written.
|
||||
List<Map<String, String>> _trailerLines(Commit commit) {
|
||||
final byLanguage = <String, List<String>>{};
|
||||
for (final line in commit.body.split('\n')) {
|
||||
final match = RegExp(
|
||||
'^$whatsNewTrailer(?:-([A-Za-z]{2,3}))?:\\s*(.+?)\\s*\$',
|
||||
).firstMatch(line);
|
||||
if (match == null) {
|
||||
continue;
|
||||
}
|
||||
final language = match[1]?.toLowerCase() ?? whatsNewLanguages.first;
|
||||
(byLanguage[language] ??= []).add(match[2]!);
|
||||
}
|
||||
final english = byLanguage[whatsNewLanguages.first] ?? const [];
|
||||
return [
|
||||
for (final (index, text) in english.indexed)
|
||||
{
|
||||
for (final MapEntry(key: language, value: texts) in byLanguage.entries)
|
||||
if (index < texts.length) language: texts[index],
|
||||
whatsNewLanguages.first: text,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
String _sentence(String description) {
|
||||
final text = description
|
||||
.replaceFirst(RegExp(r'\s*\((ADR|D\d)[^()]*\)\s*$'), '')
|
||||
|
|
|
|||
Loading…
Reference in a new issue