build(release): several apps released in one run (train:minor learn), their version commits pushed without the checks
All checks were successful
CI / check (push) Successful in 41m7s
CI / deploy (push) Successful in 1m2s

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:
Tizian 「ティツィアーン」 2026-11-03 12:11:30 +01:00
commit f06fc23023
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
3 changed files with 425 additions and 58 deletions

View file

@ -0,0 +1,80 @@
import 'package:test/test.dart';
import '../../tool/src/release/release_push.dart';
import '../../tool/src/release/version.dart';
void main() {
Commit release({
String subject = 'chore(release): train-v0.2.0',
List<String> files = const [
'apps/train/halkyon_train_flutter/pubspec.yaml',
'apps/train/halkyon_train_flutter/assets/whats_new/0.2.0.en.txt',
'apps/train/halkyon_train_flutter/assets/whats_new/0.2.0.de.txt',
],
}) => Commit(hash: 'abc', subject: subject, body: 'Build 2.', files: files);
const versionOnly = ['-version: 0.1.0+1', '+version: 0.2.0+2'];
test('a release commit, the version and what is new alone, needs no '
'checks', () {
expect(releaseCommitProblem(release(), versionOnly), isNull);
expect(
releaseCommitProblem(
release(
subject: 'chore(release): todo-v0.1.1',
files: ['apps/todo/halkyon_todo_flutter/pubspec.yaml'],
),
versionOnly,
),
isNull,
);
});
test('anything else does: another subject, another file, or the pubspec '
'changed beyond its version', () {
expect(
releaseCommitProblem(release(subject: 'fix(train): a fix'), versionOnly),
isNotNull,
);
expect(
releaseCommitProblem(
release(subject: 'chore(release): train-v0.2.0 and more'),
versionOnly,
),
isNotNull,
);
for (final file in [
'apps/train/halkyon_train_flutter/lib/main.dart',
'pubspec.lock',
'apps/train/halkyon_train_flutter/assets/whats_new/notes.md',
'tool/pre-push',
]) {
expect(
releaseCommitProblem(
release(files: [...release().files, file]),
versionOnly,
),
isNotNull,
reason: file,
);
}
expect(releaseCommitProblem(release(files: []), versionOnly), isNotNull);
expect(
releaseCommitProblem(release(), [...versionOnly, '+ http: ^1.0.0']),
isNotNull,
);
});
test('a push needs no checks only when it brings commits, all of them '
'release commits', () {
expect(onlyReleaseCommits(const []), isFalse);
expect(onlyReleaseCommits([(release(), versionOnly)]), isTrue);
expect(
onlyReleaseCommits([
(release(), versionOnly),
(release(subject: 'feat(learn): more'), versionOnly),
]),
isFalse,
);
});
}

View file

@ -64,13 +64,29 @@ void main() {
test("fdroid runs the F-Droid repository's script alone, with the "
"index's key and its password only", () {
expect(script, contains(' fdroid) ;;'));
expect(script, contains(r'tool/fdroid_repo.sh "$@" || status=$?'));
final alone = script.substring(
script.indexOf(r'if [ "$app" = fdroid ]; then'),
script.indexOf(r'if [ "$1" = fdroid ]; then'),
script.indexOf(r'tool/fdroid_repo.sh "$@"'),
);
expect(alone, isNot(contains('app_item')));
expect(alone, isNot(contains('forgejo_token')));
});
test('several apps named, each gets its own key, under its own name and '
'variable, and the release tool every argument as given', () {
expect(script, contains(r'*) apps+=("${word%%:*}") ;;'));
expect(script, contains('-* | major | minor | patch) ;;'));
expect(
script,
contains(
r'fetch "$app_item" android-release.p12 "$keys/$app-android-release.p12"',
),
);
expect(
script,
contains(r'export "$app_variable=$keys/$app-android-release.p12"'),
);
expect(script, contains(r'dart run tool/release.dart "$@" || status=$?'));
});
}

View file

@ -0,0 +1,47 @@
import 'package:test/test.dart';
import '../../tool/src/release/targets.dart';
import '../../tool/src/release/version.dart';
void main() {
const apps = {'learn', 'todo', 'train'};
test('one app, as before, with or without the version asked for', () {
expect(parseTargets(['learn'], apps), [const ReleaseTarget('learn')]);
expect(parseTargets(['train', 'minor'], apps), [
const ReleaseTarget('train', Bump.minor),
]);
});
test('several apps, one after another in the order named, each with its '
'own version asked for after a colon or not at all', () {
expect(parseTargets(['train:minor', 'learn'], apps), [
const ReleaseTarget('train', Bump.minor),
const ReleaseTarget('learn'),
]);
expect(parseTargets(['learn', 'train', 'patch'], apps), [
const ReleaseTarget('learn'),
const ReleaseTarget('train', Bump.patch),
]);
});
test('refuses no app, an unknown one, one named twice, a version without '
'its app or asked for twice, and an unknown version', () {
for (final words in [
<String>[],
['meditate'],
['learn', 'learn:minor'],
['minor'],
['learn:minor', 'patch'],
['learn', 'minor', 'patch'],
['learn:huge'],
['learn:'],
]) {
expect(
() => parseTargets(words, apps),
throwsFormatException,
reason: '$words',
);
}
});
}