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
5 changed files with 211 additions and 31 deletions
|
|
@ -26,7 +26,7 @@ Halkyon's apps and the packages they share, in one repository and one pub worksp
|
|||
- **Explain every migration's SQL.**
|
||||
- **Before keeping a framework default, name the alternatives** and recommend one, especially where a choice becomes expensive once users or data exist.
|
||||
- Every decision carries its reason (in `NEXT.md` or an ADR under `docs/adr/`).
|
||||
- **Commits** are GPG-signed and carry a DCO `Signed-off-by` trailer (`git commit -s`; a local `prepare-commit-msg` hook adds it automatically), and **keep their type prefix** (`feat`, `fix`, `perf`, `refactor`, `docs`, `build`, `chore`, a `!` or a `BREAKING CHANGE:` line for what breaks): releases work out their versions from them (Learn's `AGENTS.md`, *Releases*).
|
||||
- **Commits** are GPG-signed and carry a DCO `Signed-off-by` trailer (`git commit -s`; a local `prepare-commit-msg` hook adds it automatically), and **keep their type prefix** (`feat`, `fix`, `perf`, `refactor`, `docs`, `build`, `chore`, a `!` or a `BREAKING CHANGE:` line for what breaks): releases work out their versions from them (Learn's `AGENTS.md`, *Releases*). A commit whose change people using an app would notice says so for them in its trailers, `Whats-New: <one sentence>` and `Whats-New-De: <its German>`, once a change, short (a release's lines share F-Droid's 500 characters) and in the app's own names for its screens and buttons; the release drafts its *What's new* from them alone (Train's ADR 0030, D61, 4 Nov 2026). A commit scoped to apps (`feat(todo,meditate):`) counts for those alone, a shared package's for every app it touches; a refactor, a build change or a test carries none.
|
||||
- **Servers are named in prose by the domain they serve** (learn.halkyon.eu's server, the halkyon.app server), which says what a reader needs; the SSH aliases and the CI runner's label keep the servers' own names (`sahaquiel-deploy`, `ramiel-fdroid`, `sahaquiel:host`).
|
||||
- **Pushes** pass the checks first, through a local `pre-push` hook (in a fresh clone: `ln -s ../../tool/pre-push .git/hooks/pre-push`): `tool/pre-push` first refuses a push to `main` that brings a commit not signed by a key in `deploy/allowed-signers` (`tool/check_signatures.sh`, the deploy's own rule), so it fails on the laptop rather than after CI, then runs `tool/check.dart`, but for a push of release commits alone (the release tool's `chore(release):` commits changing an app's version line and its texts of what's new, nothing else: `tool/release_push.dart`, decided 3 Nov 2026, so that releases in a row don't run the checks once each; CI still runs them). CI runs the checks again, and a green `main` deploys learn.halkyon.eu (see *CI and deploys*), so an unsigned commit on `main` blocks every deploy after it.
|
||||
- **Lints**: LeanCode's strict set (`leancode_lint`), configured once in the root `analysis_options.yaml`, which also enables its analyzer plugin. Plugins can only be enabled at the root of a pub workspace and only reach packages whose options include that file: Learn's, Todo's and Train's servers and apps, Learn's curator and every shared package include it; the generated client packages keep their own, but for the module's, which includes it for its SRP half and leaves `lib/src/protocol/` out. Every `// ignore:` gets a comment saying why (`document_ignores`). Each server package switches off the four rules for Dart v3.13's constructor syntax (`use_primary_constructors`, `use_declaring_parameters`, `empty_container_bodies`, `unnecessary_type_name_in_constructor`): Serverpod's code generator (checked with v4.0.3) can't parse that syntax in endpoint files yet.
|
||||
|
|
@ -83,7 +83,7 @@ Checklist after doing changes, in this order:
|
|||
- Check `serverpod` MCP `tail_server_logs` and `tail_flutter_logs` for any issues.
|
||||
|
||||
- **Melos** (decided 2026-10-19, ADR 0015) runs scripts across the workspace's packages: a dev dependency of the root (`dart run melos …`, its version in the lock), its scripts under `melos:` in the root `pubspec.yaml`: `analyze`; `test:dart` (the packages without Flutter, one at a time, as each server's tests start a PostgreSQL of their own on the same port); `test:flutter` (`flutter test -x requires-server`, one package at a time); `generate` (Serverpod's code generation in every package with a `config/generator.yaml`, the CLI on the PATH); `check` (`tool/check.dart`). Every script takes Melos' filters: `dart run melos run test:flutter --scope=halkyon_app`, or `--diff=origin/main --include-dependents` for what changed and what depends on it; `dart run melos list` lists the packages. The root overrides `cli_util` to 0.5.2 for it: Learn's `flutter_launcher_icons` (0.14.4, its latest) asks for ^0.4.1 and uses only the logger, which 0.5 keeps; drop the override once that package allows 0.5.
|
||||
- **Tools are Dart where they hold logic** (decided 2026-10-19): conditions, parsing, parallel work, anything worth a test, as `tool/check.dart` (its file selection in `tool/src/check_files.dart`, tested in the root's `test/`). **Bash for glue**, short and linear (`tool/pre-push`, `tool/check_signatures.sh`, `tool/workspace_only.sh`, which narrows the root pubspec's workspace to the packages an image builds), **and for everything run on the production host**, which has no Dart SDK and gets none (`deploy/redeploy.sh`, `deploy/backup.sh`). **Python only where a library exists only there** (the apps' icons with librsvg and Pillow, `tool/app_icons.py`, its Anki fixtures with Anki itself); Learn's data scripts and `packages/halkyon_app/tool/subdivision_names.py` need nothing but Python's standard library and may move to Dart when they next change. **Releases come from `tool/release.dart`** (decided 2026-10-19, ADR 0015), shared by the apps, each configured in Dart beside it (`apps/<id>/release.dart`, typed and commented, registered in `tool/src/release/apps.dart`): `dart run tool/release.dart learn [major | minor | patch] [--dry-run]`, or several apps one after another (`train:minor learn`, decided 3 Nov 2026: each as alone, the run stopping at the first that fails; `tool/src/release/targets.dart`), on a developer's laptop, never in CI, through `tool/with_signing_keys.sh learn …` (Bash glue, fetching every named app's key): the keystores are kept in Proton Pass alone (vault *Infra · Signing*, each a note's attachment, the password its hidden field), and it downloads the app's and the F-Droid index's into a folder in memory (`$XDG_RUNTIME_DIR`), hands their paths and, but for a dry run, their passwords (each item's hidden field *Password*, in the environment as `<keystore variable>_PASSWORD`, so the tool asks for neither) and the Forgejo token (a login in the vault *Infra*, `HALKYON_FORGEJO_TOKEN`) to the release tool and removes the files however the run ends (`test/release/signing_keys_test.dart` holds it to every app released). Tags are per app (`learn-v0.19.0`; an app's earlier tags, Learn's `v…`, count as its history), and only commits touching the app decide its version: its folder, the shared packages its packages use by path (transitively), the root pubspec and lock, its release files (key pins, F-Droid listing, snap recipe) and where its packages were before a move. Each release's *What's new* (Train's ADR 0030, D61), one file a language in the app's `assets/whats_new/` (`<version>.<language>.txt`, English and German), is drafted by it from the commits after *Go on?* and edited in `$VISUAL`/`$EDITOR` until it fits (no `TRANSLATE:` left, F-Droid's 500 characters), committed with the version, and is the app's sheet after the update, the Forgejo notes and F-Droid's changelog. `--dry-run` reads everything a release would (the version, the commits that count and what each touches, the steps, the notes it would publish), changes nothing, asks for no secret, and lists what a release would refuse. Its pure logic is in `tool/src/release/`, tested in the root's `test/release/`. `tool/fdroid_repo.sh` stays Bash (glue for fdroidserver); `tool/with_signing_keys.sh fdroid [APK]` runs it alone with the index's key, e.g. when a release's upload failed after its release was made; a release colliding with the entry Forgejo makes of the pushed tag itself asks again, a few seconds later. Build 21 (`v0.18.0`) was the last release of `tool/release.sh`, the Bash script it replaced, removed since.
|
||||
- **Tools are Dart where they hold logic** (decided 2026-10-19): conditions, parsing, parallel work, anything worth a test, as `tool/check.dart` (its file selection in `tool/src/check_files.dart`, tested in the root's `test/`). **Bash for glue**, short and linear (`tool/pre-push`, `tool/check_signatures.sh`, `tool/workspace_only.sh`, which narrows the root pubspec's workspace to the packages an image builds), **and for everything run on the production host**, which has no Dart SDK and gets none (`deploy/redeploy.sh`, `deploy/backup.sh`). **Python only where a library exists only there** (the apps' icons with librsvg and Pillow, `tool/app_icons.py`, its Anki fixtures with Anki itself); Learn's data scripts and `packages/halkyon_app/tool/subdivision_names.py` need nothing but Python's standard library and may move to Dart when they next change. **Releases come from `tool/release.dart`** (decided 2026-10-19, ADR 0015), shared by the apps, each configured in Dart beside it (`apps/<id>/release.dart`, typed and commented, registered in `tool/src/release/apps.dart`): `dart run tool/release.dart learn [major | minor | patch] [--dry-run]`, or several apps one after another (`train:minor learn`, decided 3 Nov 2026: each as alone, the run stopping at the first that fails; `tool/src/release/targets.dart`), on a developer's laptop, never in CI, through `tool/with_signing_keys.sh learn …` (Bash glue, fetching every named app's key): the keystores are kept in Proton Pass alone (vault *Infra · Signing*, each a note's attachment, the password its hidden field), and it downloads the app's and the F-Droid index's into a folder in memory (`$XDG_RUNTIME_DIR`), hands their paths and, but for a dry run, their passwords (each item's hidden field *Password*, in the environment as `<keystore variable>_PASSWORD`, so the tool asks for neither) and the Forgejo token (a login in the vault *Infra*, `HALKYON_FORGEJO_TOKEN`) to the release tool and removes the files however the run ends (`test/release/signing_keys_test.dart` holds it to every app released). Tags are per app (`learn-v0.19.0`; an app's earlier tags, Learn's `v…`, count as its history), and only commits touching the app decide its version: its folder, the shared packages its packages use by path (transitively), the root pubspec and lock, its release files (key pins, F-Droid listing, snap recipe) and where its packages were before a move. Each release's *What's new* (Train's ADR 0030, D61), one file a language in the app's `assets/whats_new/` (`<version>.<language>.txt`, English and German), is drafted by it from the commits after *Go on?* (their `Whats-New:` and `Whats-New-De:` trailers, below) and edited in `$VISUAL`/`$EDITOR` until it fits (no `TRANSLATE:` left, F-Droid's 500 characters), committed with the version, and is the app's sheet after the update, the Forgejo notes and F-Droid's changelog. `--dry-run` reads everything a release would (the version, the commits that count and what each touches, the steps, the notes it would publish), changes nothing, asks for no secret, and lists what a release would refuse. Its pure logic is in `tool/src/release/`, tested in the root's `test/release/`. `tool/fdroid_repo.sh` stays Bash (glue for fdroidserver); `tool/with_signing_keys.sh fdroid [APK]` runs it alone with the index's key, e.g. when a release's upload failed after its release was made; a release colliding with the entry Forgejo makes of the pushed tag itself asks again, a few seconds later. Build 21 (`v0.18.0`) was the last release of `tool/release.sh`, the Bash script it replaced, removed since.
|
||||
|
||||
## CI and deploys
|
||||
|
||||
|
|
|
|||
|
|
@ -1561,6 +1561,19 @@ Strava. What was found, 27 Oct:
|
|||
commit with the version (the first release's alone, where the pubspec keeps its
|
||||
version). A run stopped there keeps them for the next, the only untracked files
|
||||
a release starts with. The dry run prints them, or the drafts.
|
||||
- **Written with the commit** (the developer's, 4 Nov 2026, on the agent's
|
||||
recommendation, after three releases' drafts had to be rewritten whole: the
|
||||
subjects are written for developers, listed other apps' commits touching a
|
||||
shared package, and left German to translate): a commit people would notice
|
||||
carries what it changes for them as 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 has one does it fall back to the subjects. A
|
||||
commit whose scope names apps (`feat(todo,meditate):`) counts for those alone,
|
||||
any other (a shared package's, `feat(app):`) for every app it touches. Over a
|
||||
running `next.<language>.txt` per app, which parallel branches would conflict on
|
||||
at every merge, and over an assistant drafting at release time, which needs a
|
||||
key, sends the commits to a provider and writes differently each run.
|
||||
- **The same text everywhere**: the English as the Forgejo notes' *What's new*, in
|
||||
place of the commits' list, and each language as F-Droid's changelog of the build
|
||||
(`metadata/<app id>/<locale>/changelogs/<build>.txt`, `en-US` and `de`), written
|
||||
|
|
|
|||
|
|
@ -11,16 +11,21 @@ import 'fixtures.dart';
|
|||
void main() {
|
||||
test('the drafts list the features, then the fixes, without types, scopes '
|
||||
'and closing references, as sentences; German marked to translate', () {
|
||||
final drafts = draftWhatsNew(const [
|
||||
Commit(hash: 'a', subject: 'fix(sync): no lost rating'),
|
||||
Commit(hash: 'b', subject: 'docs: why'),
|
||||
Commit(
|
||||
hash: 'c',
|
||||
subject: 'feat(train): tabs folding into the title (ADR 0030, D60)',
|
||||
),
|
||||
Commit(hash: 'd', subject: 'perf!: faster library.'),
|
||||
Commit(hash: 'e', subject: 'chore(release): notes-v0.18.0'),
|
||||
], first: false);
|
||||
final drafts = draftWhatsNew(
|
||||
const [
|
||||
Commit(hash: 'a', subject: 'fix(sync): no lost rating'),
|
||||
Commit(hash: 'b', subject: 'docs: why'),
|
||||
Commit(
|
||||
hash: 'c',
|
||||
subject: 'feat(train): tabs folding into the title (ADR 0030, D60)',
|
||||
),
|
||||
Commit(hash: 'd', subject: 'perf!: faster library.'),
|
||||
Commit(hash: 'e', subject: 'chore(release): notes-v0.18.0'),
|
||||
],
|
||||
first: false,
|
||||
app: 'notes',
|
||||
apps: _apps,
|
||||
);
|
||||
|
||||
expect(drafts['en'], '''
|
||||
- Tabs folding into the title.
|
||||
|
|
@ -36,11 +41,87 @@ void main() {
|
|||
|
||||
test('the first release says so', () {
|
||||
expect(
|
||||
draftWhatsNew(const [], first: true)['en'],
|
||||
draftWhatsNew(const [], first: true, app: 'notes', apps: _apps)['en'],
|
||||
'- The first release.\n',
|
||||
);
|
||||
});
|
||||
|
||||
test("commits scoped to other apps don't count; a shared package's do", () {
|
||||
final drafts = draftWhatsNew(
|
||||
const [
|
||||
Commit(hash: 'a', subject: 'feat(diary): the first diary'),
|
||||
Commit(hash: 'b', subject: 'feat(notes,diary): a lock on both'),
|
||||
Commit(hash: 'c', subject: 'feat(app): a profile everywhere'),
|
||||
Commit(hash: 'd', subject: 'fix(deploy): the image builds'),
|
||||
],
|
||||
first: false,
|
||||
app: 'notes',
|
||||
apps: _apps,
|
||||
);
|
||||
|
||||
expect(drafts['en'], '''
|
||||
- A lock on both.
|
||||
- A profile everywhere.
|
||||
- The image builds.
|
||||
''');
|
||||
});
|
||||
|
||||
group('the trailers', () {
|
||||
const commits = [
|
||||
Commit(
|
||||
hash: 'a',
|
||||
subject: 'fix(notes): the sync no longer loses a rating',
|
||||
body:
|
||||
'Why it did.\n\n'
|
||||
'Whats-New: Ratings are kept when syncing.\n'
|
||||
'Whats-New-De: Bewertungen bleiben beim Synchronisieren.\n'
|
||||
'Signed-off-by: A <a@example.org>\n',
|
||||
),
|
||||
Commit(hash: 'b', subject: 'feat(notes): a refactor nobody sees'),
|
||||
Commit(
|
||||
hash: 'c',
|
||||
subject: 'feat(app): tabs folding into the title (ADR 0030, D60)',
|
||||
body:
|
||||
'Whats-New: Tabs fold into the title.\n'
|
||||
'Whats-New: Search: in every list.\n'
|
||||
'Whats-New-De: Tabs falten sich in den Titel.\n',
|
||||
),
|
||||
Commit(
|
||||
hash: 'd',
|
||||
subject: 'feat(diary): a diary',
|
||||
body: 'Whats-New: A diary.\nWhats-New-De: Ein Tagebuch.\n',
|
||||
),
|
||||
];
|
||||
|
||||
test("are what's new where a commit has them: the features' first, "
|
||||
'commits without them left out, those of other apps too', () {
|
||||
final drafts = draftWhatsNew(
|
||||
commits,
|
||||
first: false,
|
||||
app: 'notes',
|
||||
apps: _apps,
|
||||
);
|
||||
|
||||
expect(drafts['en'], '''
|
||||
- Tabs fold into the title.
|
||||
- Search: in every list.
|
||||
- Ratings are kept when syncing.
|
||||
''');
|
||||
expect(drafts['de'], '''
|
||||
- Tabs falten sich in den Titel.
|
||||
- TRANSLATE: Search: in every list.
|
||||
- Bewertungen bleiben beim Synchronisieren.
|
||||
''');
|
||||
});
|
||||
|
||||
test('are left alone in the first release', () {
|
||||
expect(
|
||||
draftWhatsNew(commits, first: true, app: 'notes', apps: _apps)['de'],
|
||||
'- TRANSLATE: The first release.\n',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test("a text's changes are its lines starting with '- '", () {
|
||||
expect(whatsNewChanges('- One\n\n - Two \n'), ['One', 'Two']);
|
||||
expect(whatsNewList('- One\n\n - Two \n'), '- One\n- Two');
|
||||
|
|
@ -110,3 +191,5 @@ void main() {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
const _apps = {'notes', 'diary'};
|
||||
|
|
|
|||
|
|
@ -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