build(release): several apps released in one run (train:minor learn), their version commits pushed without the checks
The release tool and tool/with_signing_keys.sh take several apps, each with its version after a colon or none, and release them one after another, each as alone, stopping at the first that fails; the key script fetches every named app's key. The pre-push hook skips tool/check.dart when a push brings release commits alone: the release tool's chore(release) subject, an app's pubspec changed in its version line and its texts of what's new, nothing else (tool/release_push.dart). Signatures are checked as before, and CI still runs the checks. A release no longer costs a run of the checks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
551a29445f
commit
f06fc23023
2 changed files with 425 additions and 58 deletions
46
tool/src/release/release_push.dart
Normal file
46
tool/src/release/release_push.dart
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
/// Whether a push to main brings release commits alone, which the pre-push
|
||||
/// hook then lets through without the checks (tool/check.dart): a release
|
||||
/// commit changes an app's version and adds its texts of what's new, nothing
|
||||
/// the checks look at, as the release tool already holds when it asks CI
|
||||
/// about the newest commit that isn't one (decided 3 Nov 2026, so that
|
||||
/// releases in a row don't run the checks once each).
|
||||
library;
|
||||
|
||||
import 'version.dart';
|
||||
|
||||
final _subject = RegExp(r'^chore\(release\): [a-z]+-v\d+\.\d+\.\d+$');
|
||||
final _file = RegExp(
|
||||
'^apps/[a-z]+/halkyon_[a-z]+_flutter/'
|
||||
r'(pubspec\.yaml|assets/whats_new/\d+\.\d+\.\d+\.[a-z]{2}\.txt)$',
|
||||
);
|
||||
final _versionLine = RegExp(r'^[+-]version: \d+\.\d+\.\d+\+\d+$');
|
||||
|
||||
/// Why [commit] isn't a release commit, or null when it is one: its subject
|
||||
/// the release tool's, its files an app's pubspec and texts of what's new
|
||||
/// alone, and [pubspecLines], the lines its pubspecs' diff adds and removes
|
||||
/// (`+…`, `-…`), the version's alone.
|
||||
String? releaseCommitProblem(Commit commit, List<String> pubspecLines) {
|
||||
if (!_subject.hasMatch(commit.subject)) {
|
||||
return "${commit.subject}: not the release tool's subject.";
|
||||
}
|
||||
if (commit.files.isEmpty) {
|
||||
return '${commit.subject}: changes no file.';
|
||||
}
|
||||
for (final file in commit.files) {
|
||||
if (!_file.hasMatch(file)) {
|
||||
return '${commit.subject}: changes $file.';
|
||||
}
|
||||
}
|
||||
for (final line in pubspecLines) {
|
||||
if (!_versionLine.hasMatch(line)) {
|
||||
return '${commit.subject}: changes a pubspec beyond its version: $line';
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Whether [commits], each with its pubspecs' changed lines, are some, and
|
||||
/// every one of them a release commit.
|
||||
bool onlyReleaseCommits(List<(Commit, List<String>)> commits) =>
|
||||
commits.isNotEmpty &&
|
||||
commits.every((entry) => releaseCommitProblem(entry.$1, entry.$2) == null);
|
||||
59
tool/src/release/targets.dart
Normal file
59
tool/src/release/targets.dart
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
/// The apps one run of the release tool releases, as its command line names
|
||||
/// them: `learn`, `train minor` (as always), or several in a row,
|
||||
/// `train:minor learn`, released one after another in that order.
|
||||
library;
|
||||
|
||||
import 'version.dart';
|
||||
|
||||
/// An app to release, and the version asked for, if any.
|
||||
final class const ReleaseTarget(final String app, [final Bump? chosen]) {
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
other is ReleaseTarget && other.app == app && other.chosen == chosen;
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(app, chosen);
|
||||
|
||||
@override
|
||||
String toString() => chosen == null ? app : '$app:${chosen!.name}';
|
||||
}
|
||||
|
||||
/// The apps [words] name, of [apps], each with its version if asked for:
|
||||
/// after a colon (`train:minor`) or as the word after it (`train minor`).
|
||||
/// Throws a [FormatException] saying what is wrong otherwise.
|
||||
List<ReleaseTarget> parseTargets(List<String> words, Set<String> apps) {
|
||||
final bumps = Bump.values.asNameMap();
|
||||
final targets = <ReleaseTarget>[];
|
||||
for (final word in words) {
|
||||
if (bumps[word] case final bump?) {
|
||||
final last = targets.lastOrNull;
|
||||
if (last == null || last.chosen != null) {
|
||||
throw FormatException('$word: which app?');
|
||||
}
|
||||
targets[targets.length - 1] = ReleaseTarget(last.app, bump);
|
||||
continue;
|
||||
}
|
||||
final [app, ...rest] = word.split(':');
|
||||
if (!apps.contains(app)) {
|
||||
throw FormatException('No app "$app": one of ${apps.join(', ')}.');
|
||||
}
|
||||
if (targets.any((target) => target.app == app)) {
|
||||
throw FormatException('$app is named twice.');
|
||||
}
|
||||
switch (rest) {
|
||||
case []:
|
||||
targets.add(ReleaseTarget(app));
|
||||
case [final name] when bumps[name] != null:
|
||||
targets.add(ReleaseTarget(app, bumps[name]));
|
||||
default:
|
||||
throw FormatException(
|
||||
'$word: the version after the colon is none of major, minor, '
|
||||
'patch.',
|
||||
);
|
||||
}
|
||||
}
|
||||
if (targets.isEmpty) {
|
||||
throw const FormatException('No app named.');
|
||||
}
|
||||
return targets;
|
||||
}
|
||||
Loading…
Reference in a new issue