build(train): Train on the desktop as a snap, to the Store's edge channel with each release (ADR 0030, D62)

Built as Todo's in a clone of its own, from apps/train/snap/, for train.halkyon.eu; its
listing says the desktop shows, plans and keeps the statistics, and records nothing.
The snap tests now hold every snap built in a clone of its own.

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 11:48:19 +01:00
commit 551a29445f
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -92,7 +92,6 @@ void main() {
reason: path,
);
}
expect(trainRelease.snap, isNull);
expect(
trainRelease.android.certificateFile,
isNot(learnRelease.android.certificateFile),
@ -153,68 +152,76 @@ void main() {
);
});
group("Todo's snap, built in a clone of its own with its folder in place of "
"the root's snap/", () {
final snap = todoRelease.snap!;
final recipe = File('${snap.directory}/snapcraft.yaml').readAsStringSync();
test("is not the root's, and is built outside the repository", () {
test(
"Learn's snap is the root's; Todo's and Train's are built in clones of "
'their own, outside the repository, and packed without --output, as '
"snapcraft takes the clone's own path for a file outside the project",
() {
expect(learnRelease.snap!.atRoot, isTrue);
expect(snap.atRoot, isFalse);
expect(
snap.projectPath('/home/someone'),
'/home/someone/.cache/halkyon-release/snap/halkyon-todo',
);
});
test('is packed without --output, into its clone, as snapcraft takes the '
"clone's own path for a file outside the project; Learn's into the "
"release's folder", () {
expect(snap.packOutput(root: '/repo', out: 'build/x'), isNull);
expect(
learnRelease.snap!.packOutput(root: '/repo', out: 'build/x'),
'/repo/build/x',
);
});
for (final (app, name) in [
(todoRelease, 'halkyon-todo'),
(trainRelease, 'halkyon-train'),
]) {
final snap = app.snap!;
expect(snap.atRoot, isFalse, reason: app.id);
expect(
snap.projectPath('/home/someone'),
'/home/someone/.cache/halkyon-release/snap/$name',
);
expect(snap.packOutput(root: '/repo', out: 'build/x'), isNull);
expect(snap.storeChannel, 'edge', reason: app.id);
}
},
);
test('names the snap and the part the release tool takes', () {
expect(recipe, contains('name: ${snap.name}\n'));
expect(recipe, contains('\n ${snap.part}:\n after: [flutter]\n'));
expect(recipe, contains('adopt-info: ${snap.part} '));
});
for (final app in [todoRelease, trainRelease]) {
group("${app.name}'s snap, built in a clone of its own with its folder in "
"place of the root's snap/", () {
final snap = app.snap!;
final recipe = File('${snap.directory}/snapcraft.yaml')
.readAsStringSync();
test('builds Todo for its instance', () {
expect(recipe, contains('cd ${todoRelease.flutterPackage}\n'));
expect(
recipe,
contains('--dart-define=SERVER_URL=${todoRelease.serverUrl}\n'),
);
});
test('names the snap and the part the release tool takes', () {
expect(recipe, contains('name: ${snap.name}\n'));
expect(recipe, contains('\n ${snap.part}:\n after: [flutter]\n'));
expect(recipe, contains('adopt-info: ${snap.part} '));
});
test('has the files it names, as they will stand in the clone', () {
// In the clone, the folder is snap/, the root's snap/local/ beside it.
for (final match in RegExp(
r'(?<![\w/$])snap/([\w./-]+)',
).allMatches(recipe)) {
final inClone = match[1]!;
final path = inClone.startsWith('local')
? 'snap/$inClone'
: '${snap.directory}/$inClone';
if (inClone == 'snapcraft.yaml') {
continue;
test('builds the app for its instance', () {
expect(recipe, contains('cd ${app.flutterPackage}\n'));
expect(recipe, contains('--dart-define=SERVER_URL=${app.serverUrl}\n'));
});
test('has the files it names, as they will stand in the clone', () {
// In the clone, the folder is snap/, the root's snap/local/ beside
// it.
for (final match in RegExp(
r'(?<![\w/$])snap/([\w./-]+)',
).allMatches(recipe)) {
final inClone = match[1]!;
final path = inClone.startsWith('local')
? 'snap/$inClone'
: '${snap.directory}/$inClone';
if (inClone == 'snapcraft.yaml') {
continue;
}
expect(
FileSystemEntity.typeSync(path),
isNot(FileSystemEntityType.notFound),
reason: match[0],
);
}
expect(
FileSystemEntity.typeSync(path),
isNot(FileSystemEntityType.notFound),
reason: match[0],
File('${snap.directory}/gui/${snap.name}.desktop').readAsStringSync(),
contains('Exec=${snap.name}\n'),
);
}
expect(
File('${snap.directory}/gui/${snap.name}.desktop').readAsStringSync(),
contains('Exec=${snap.name}\n'),
);
});
});
});
}
test("every snap's recipe builds with the Flutter CI pins", () {
final pinned = RegExp(