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:
parent
c8d94d0ceb
commit
551a29445f
9 changed files with 231 additions and 53 deletions
|
|
@ -15,7 +15,7 @@ Halkyon's apps and the packages they share, in one repository and one pub worksp
|
||||||
- `halkyon_keys` (pure Dart on libsodium, ADR 0017): an account's keys on the device, the password's Argon2id and SRP's x, the key pairs and their wrapping, the recovery phrase, data keys, content sealed and signed, row IDs keyed by the account (`keyedRowId`). SRP-6a's two halves are the module's (`halkyon_base_server`'s `base.dart`, `halkyon_base_client`'s `srp.dart`), as are TOTP and the recovery codes. `sodium`'s build hook compiles libsodium, so every machine that builds or tests needs a C compiler and `make`.
|
- `halkyon_keys` (pure Dart on libsodium, ADR 0017): an account's keys on the device, the password's Argon2id and SRP's x, the key pairs and their wrapping, the recovery phrase, data keys, content sealed and signed, row IDs keyed by the account (`keyedRowId`). SRP-6a's two halves are the module's (`halkyon_base_server`'s `base.dart`, `halkyon_base_client`'s `srp.dart`), as are TOTP and the recovery codes. `sodium`'s build hook compiles libsodium, so every machine that builds or tests needs a C compiler and `make`.
|
||||||
- `halkyon_llm` (pure Dart, Halkyon Learn's ADR 0021, D15 and D46): language models' APIs streamed over plain HTTP, no SDKs: Anthropic's Messages API, OpenAI's chat completions (each known host's fields, D45) and its Responses API, behind one interface (`LlmClient`), with `FakeLlmClient` for tests; tested on replies written from the providers' documentation and on those `tool/record_llm.dart` records with a key (D36). The module forwards the operator's assistants with it, and the apps call a person's own provider. BSD-3-Clause as `halkyon_oidc`, `publish_to: none`.
|
- `halkyon_llm` (pure Dart, Halkyon Learn's ADR 0021, D15 and D46): language models' APIs streamed over plain HTTP, no SDKs: Anthropic's Messages API, OpenAI's chat completions (each known host's fields, D45) and its Responses API, behind one interface (`LlmClient`), with `FakeLlmClient` for tests; tested on replies written from the providers' documentation and on those `tool/record_llm.dart` records with a key (D36). The module forwards the operator's assistants with it, and the apps call a person's own provider. BSD-3-Clause as `halkyon_oidc`, `publish_to: none`.
|
||||||
- `halkyon_oidc_server`, `halkyon_oidc_client` (ADR 0023): signing in with OpenID Connect, with a SimpleLogin preset, as a Serverpod module that halkyon_base builds on; `halkyon_oidc_flutter`, the device's half: the provider's page opened, the way back caught (the app's scheme, or the loopback on Linux and Windows, `LoopbackWebAuth`) and its state checked. Nothing of Halkyon's is in it, so that others can use it with Serverpod: under BSD-3-Clause, Serverpod's own licence, where the other packages are AGPL; `publish_to: none` until it is published to pub.dev, which is asked again first.
|
- `halkyon_oidc_server`, `halkyon_oidc_client` (ADR 0023): signing in with OpenID Connect, with a SimpleLogin preset, as a Serverpod module that halkyon_base builds on; `halkyon_oidc_flutter`, the device's half: the provider's page opened, the way back caught (the app's scheme, or the loopback on Linux and Windows, `LoopbackWebAuth`) and its state checked. Nothing of Halkyon's is in it, so that others can use it with Serverpod: under BSD-3-Clause, Serverpod's own licence, where the other packages are AGPL; `publish_to: none` until it is published to pub.dev, which is asked again first.
|
||||||
- **`deploy/`**: the instance deploy at the root, learn.halkyon.eu's today (Learn's `AGENTS.md`, *CI and deploys*), where other services' compose files join Learn's (Train's `compose.train.yaml`, Todo's `compose.todo.yaml`, each switched on in `.env`). **`snap/`**: Learn's snap, built from the root, which its build needs whole; Todo's is `apps/todo/snap/`, which the release tool builds in a clone of the repository with it in place of `snap/` (Todo's `AGENTS.md`, *Releases*). **`docs/adr/`**: the decision records of every app and package, one numbering. **`tool/`**: the repository's tools (below).
|
- **`deploy/`**: the instance deploy at the root, learn.halkyon.eu's today (Learn's `AGENTS.md`, *CI and deploys*), where other services' compose files join Learn's (Train's `compose.train.yaml`, Todo's `compose.todo.yaml`, each switched on in `.env`). **`snap/`**: Learn's snap, built from the root, which its build needs whole; Todo's and Train's are `apps/todo/snap/` and `apps/train/snap/`, which the release tool builds each in a clone of the repository with it in place of `snap/` (Todo's `AGENTS.md`, *Releases*). **`docs/adr/`**: the decision records of every app and package, one numbering. **`tool/`**: the repository's tools (below).
|
||||||
- **One workspace, one lock**: the root `pubspec.yaml` lists every package, `pubspec.lock` holds one version of each dependency for all of them, and its `dependency_overrides` hold for every app (the offline sync fork, Learn's `AGENTS.md`; `cli_util`, below). **Serverpod's version moves as one**: every package pins Serverpod's packages to the same version (4.0.4), the module's, Todo's and Train's too, together with the CLI version CI generates code with (`SERVERPOD_VERSION` in `.forgejo/workflows/ci.yaml`) and the sync packages Learn's server depends on.
|
- **One workspace, one lock**: the root `pubspec.yaml` lists every package, `pubspec.lock` holds one version of each dependency for all of them, and its `dependency_overrides` hold for every app (the offline sync fork, Learn's `AGENTS.md`; `cli_util`, below). **Serverpod's version moves as one**: every package pins Serverpod's packages to the same version (4.0.4), the module's, Todo's and Train's too, together with the CLI version CI generates code with (`SERVERPOD_VERSION` in `.forgejo/workflows/ci.yaml`) and the sync packages Learn's server depends on.
|
||||||
|
|
||||||
## How work is done here
|
## How work is done here
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -32,4 +32,13 @@ const trainRelease = AppRelease(
|
||||||
indexCertificateFile: 'tool/release/fdroid-index-certificate.sha256',
|
indexCertificateFile: 'tool/release/fdroid-index-certificate.sha256',
|
||||||
repositoryUrl: 'https://halkyon.app/fdroid/repo',
|
repositoryUrl: 'https://halkyon.app/fdroid/repo',
|
||||||
),
|
),
|
||||||
|
// The desktop's Train (ADR 0030, D62), built in a clone of its own as
|
||||||
|
// Todo's (`SnapRelease.projectPath`), the root's snap/ being Learn's; to the
|
||||||
|
// Store's edge channel, its name registered on 3 Oct 2026.
|
||||||
|
snap: SnapRelease(
|
||||||
|
name: 'halkyon-train',
|
||||||
|
part: 'halkyon-train',
|
||||||
|
directory: 'apps/train/snap',
|
||||||
|
storeChannel: 'edge',
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
9
apps/train/snap/gui/halkyon-train.desktop
Normal file
9
apps/train/snap/gui/halkyon-train.desktop
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Name=Halkyon Train
|
||||||
|
Comment=Your workouts, routes and training plans, sealed and synced
|
||||||
|
Exec=halkyon-train
|
||||||
|
Icon=${SNAP}/meta/gui/halkyon-train.png
|
||||||
|
Terminal=false
|
||||||
|
Categories=Utility;
|
||||||
|
StartupWMClass=app.halkyon.train
|
||||||
BIN
apps/train/snap/gui/halkyon-train.png
Normal file
BIN
apps/train/snap/gui/halkyon-train.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
144
apps/train/snap/snapcraft.yaml
Normal file
144
apps/train/snap/snapcraft.yaml
Normal file
|
|
@ -0,0 +1,144 @@
|
||||||
|
# Halkyon Train's Linux app as a Snap (decided 3 Nov 2026, ADR 0030, D62), built in LXD
|
||||||
|
# by the release tool (`dart run tool/release.dart train`) as Todo's is: snapcraft
|
||||||
|
# builds one recipe a folder, from its snap/snapcraft.yaml, and the root's snap/ is
|
||||||
|
# Learn's, so the tool builds this one in a clone of the repository at the release's
|
||||||
|
# tag with this folder in place of the clone's snap/ (`SnapRelease.projectPath`), but
|
||||||
|
# for snap/local/, which the recipes share. By hand, the same: in such a clone,
|
||||||
|
# `snapcraft pack`.
|
||||||
|
#
|
||||||
|
# It builds what is committed (the source is the clone's HEAD, the tag), never a
|
||||||
|
# working tree with its build caches, and always for train.halkyon.eu. Flutter is
|
||||||
|
# pinned like the local and CI one (FLUTTER_VERSION in .forgejo/workflows/ci.yaml),
|
||||||
|
# spelled out as in Learn's recipe, which says why.
|
||||||
|
name: halkyon-train
|
||||||
|
title: Halkyon Train
|
||||||
|
base: core24
|
||||||
|
adopt-info: halkyon-train # the version, from the app's pubspec.yaml
|
||||||
|
summary: Your workouts, routes and training plans, sealed and synced
|
||||||
|
description: |
|
||||||
|
Halkyon Train on the desktop: your runs, walks, hikes, rides and rows with
|
||||||
|
their maps, splits, heart rate and power, the routes you plan on the map,
|
||||||
|
turn by turn, your strength plans with the equipment at each place and the
|
||||||
|
plates to load, and the statistics of your weeks and months.
|
||||||
|
|
||||||
|
Recording, the Polar Loop and Health Connect are the phone's (Halkyon Train
|
||||||
|
for Android); what the phone records shows here, synced.
|
||||||
|
|
||||||
|
Everything is sealed on your device with your account's keys before it
|
||||||
|
reaches the instance, which can't read it. You sign in with SimpleLogin, by
|
||||||
|
Proton, or with an email address, a password and an authenticator app. It
|
||||||
|
connects to train.halkyon.eu, the project's instance, or to any other:
|
||||||
|
everything an instance needs is in the repository, to run your own. The app
|
||||||
|
is AGPL-3.0 and the server SSPL-1.0, so all of its code is public.
|
||||||
|
|
||||||
|
Questions and reports: support@halkyon.app
|
||||||
|
license: AGPL-3.0-or-later
|
||||||
|
source-code: https://code.halkyon.app/project-halkyon/halkyon
|
||||||
|
# The project's, not an instance's: the app works with any instance.
|
||||||
|
website: https://halkyon.app/train
|
||||||
|
# No contact or issues, as Learn's (its recipe says why): the address is in the
|
||||||
|
# description.
|
||||||
|
# The app's icon with rounded corners, made by the repository's tool/app_icons.py; also
|
||||||
|
# the desktop file's.
|
||||||
|
icon: snap/gui/halkyon-train.png
|
||||||
|
grade: stable
|
||||||
|
confinement: strict
|
||||||
|
platforms:
|
||||||
|
amd64:
|
||||||
|
|
||||||
|
apps:
|
||||||
|
halkyon-train:
|
||||||
|
command: bin/halkyon_train_flutter
|
||||||
|
# How the session is kept: see snap/local/secret-backend (Learn's, kept in the
|
||||||
|
# clone).
|
||||||
|
command-chain: [bin/secret-backend]
|
||||||
|
# GTK, WebKitGTK (the sign-in's page) and libsecret come from the GNOME runtime;
|
||||||
|
# files (GPX, TCX and FIT in and out) and the browser go through the desktop's
|
||||||
|
# portals.
|
||||||
|
extensions: [gnome]
|
||||||
|
plugs:
|
||||||
|
- network
|
||||||
|
# SimpleLogin's sign-in comes back on a loopback address the app listens on.
|
||||||
|
- network-bind
|
||||||
|
# Never connected by itself; whoever connects it by hand gets the keyring.
|
||||||
|
- password-manager-service
|
||||||
|
# As Learn's: the sync's connection ended at once when the network goes;
|
||||||
|
# not connected until the Store grants it.
|
||||||
|
- network-manager-observe
|
||||||
|
|
||||||
|
parts:
|
||||||
|
flutter:
|
||||||
|
plugin: nil
|
||||||
|
source: https://github.com/flutter/flutter.git
|
||||||
|
source-tag: '3.47.8'
|
||||||
|
source-depth: 1
|
||||||
|
build-packages:
|
||||||
|
- clang
|
||||||
|
- cmake
|
||||||
|
- curl
|
||||||
|
- git
|
||||||
|
- libgtk-3-dev
|
||||||
|
- lld
|
||||||
|
- llvm-18
|
||||||
|
- ninja-build
|
||||||
|
- pkg-config
|
||||||
|
- unzip
|
||||||
|
- xz-utils
|
||||||
|
- zip
|
||||||
|
override-build: |
|
||||||
|
mkdir -p "$CRAFT_PART_INSTALL/usr/libexec" "$CRAFT_PART_INSTALL/usr/bin"
|
||||||
|
cp -r "$CRAFT_PART_SRC" "$CRAFT_PART_INSTALL/usr/libexec/flutter"
|
||||||
|
ln -s ../libexec/flutter/bin/flutter "$CRAFT_PART_INSTALL/usr/bin/flutter"
|
||||||
|
ln -s ../libexec/flutter/bin/dart "$CRAFT_PART_INSTALL/usr/bin/dart"
|
||||||
|
"$CRAFT_PART_INSTALL/usr/bin/flutter" --disable-analytics
|
||||||
|
"$CRAFT_PART_INSTALL/usr/bin/flutter" precache --linux
|
||||||
|
# Needed to build, not in the Snap.
|
||||||
|
override-prime: ''
|
||||||
|
|
||||||
|
halkyon-train:
|
||||||
|
after: [flutter]
|
||||||
|
plugin: nil
|
||||||
|
source: .
|
||||||
|
source-type: git
|
||||||
|
# sodium's build hook compiles libsodium with its configure script and make
|
||||||
|
# (ADR 0017), with the Flutter part's clang.
|
||||||
|
build-packages:
|
||||||
|
- make
|
||||||
|
override-build: |
|
||||||
|
set -eu
|
||||||
|
export PATH="$CRAFT_STAGE/usr/bin:$PATH"
|
||||||
|
cd apps/train/halkyon_train_flutter
|
||||||
|
craftctl set version="$(sed -n 's/^version: *\([^+]*\).*/\1/p' pubspec.yaml)"
|
||||||
|
flutter pub get --enforce-lockfile
|
||||||
|
flutter build linux --release \
|
||||||
|
--dart-define=SERVER_URL=https://train.halkyon.eu/
|
||||||
|
mkdir -p "$CRAFT_PART_INSTALL/bin"
|
||||||
|
cp -r build/linux/x64/release/bundle/. "$CRAFT_PART_INSTALL/bin/"
|
||||||
|
|
||||||
|
secret-backend:
|
||||||
|
plugin: dump
|
||||||
|
source: snap/local
|
||||||
|
organize:
|
||||||
|
secret-backend: bin/secret-backend
|
||||||
|
|
||||||
|
# As Learn's: every file the GNOME runtime or the base has at the same path
|
||||||
|
# goes, so that nothing staged hides the runtime's own (Learn's recipe says
|
||||||
|
# what that broke there). Train stages no package today; the part keeps a
|
||||||
|
# later one from doing so unnoticed.
|
||||||
|
cleanup:
|
||||||
|
after: [halkyon-train, secret-backend]
|
||||||
|
plugin: nil
|
||||||
|
build-snaps: [core24, gnome-46-2404]
|
||||||
|
override-prime: |
|
||||||
|
set -eu
|
||||||
|
for snap in core24 gnome-46-2404; do
|
||||||
|
cd "/snap/$snap/current/usr"
|
||||||
|
find . \( -type f -o -type l \) -print0 |
|
||||||
|
while IFS= read -r -d '' path; do
|
||||||
|
case "$path" in */blas/* | */lapack/*) continue ;; esac
|
||||||
|
target="$CRAFT_PRIME/usr/$path"
|
||||||
|
if [ -L "$target" ] || [ -f "$target" ]; then
|
||||||
|
rm -f "$target"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
@ -1571,6 +1571,14 @@ Strava. What was found, 27 Oct:
|
||||||
folder), newest first; over the newest alone. Nothing on a fresh install; a device
|
folder), newest first; over the newest alone. Nothing on a fresh install; a device
|
||||||
used before (an instance chosen) is shown the running version's on its first start
|
used before (an instance chosen) is shown the running version's on its first start
|
||||||
with this. In the app's language, else English.
|
with this. In the app's language, else English.
|
||||||
|
- **D62 Train on the desktop as a snap** (the developer's, 3 Nov 2026, on the agent's
|
||||||
|
recommendation): to the Snap Store's edge channel with each release, as Learn's and
|
||||||
|
Todo's, built as Todo's in a clone of its own (`apps/train/snap/`, Todo's ADR 0033,
|
||||||
|
step 5), for train.halkyon.eu. D58 had released the APK alone, before the release
|
||||||
|
tool could build a snap from an app's own folder. The desktop records nothing and
|
||||||
|
connects to no device (*Consequences*); it shows the workouts with their maps, plans
|
||||||
|
routes and strength sessions, and keeps the statistics, with what the phones
|
||||||
|
recorded coming through the server. Its listing says so.
|
||||||
|
|
||||||
## Consequences
|
## Consequences
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,6 @@ void main() {
|
||||||
reason: path,
|
reason: path,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
expect(trainRelease.snap, isNull);
|
|
||||||
expect(
|
expect(
|
||||||
trainRelease.android.certificateFile,
|
trainRelease.android.certificateFile,
|
||||||
isNot(learnRelease.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 "
|
test(
|
||||||
"the root's snap/", () {
|
"Learn's snap is the root's; Todo's and Train's are built in clones of "
|
||||||
final snap = todoRelease.snap!;
|
'their own, outside the repository, and packed without --output, as '
|
||||||
final recipe = File('${snap.directory}/snapcraft.yaml').readAsStringSync();
|
"snapcraft takes the clone's own path for a file outside the project",
|
||||||
|
() {
|
||||||
test("is not the root's, and is built outside the repository", () {
|
|
||||||
expect(learnRelease.snap!.atRoot, isTrue);
|
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(
|
expect(
|
||||||
learnRelease.snap!.packOutput(root: '/repo', out: 'build/x'),
|
learnRelease.snap!.packOutput(root: '/repo', out: 'build/x'),
|
||||||
'/repo/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', () {
|
for (final app in [todoRelease, trainRelease]) {
|
||||||
expect(recipe, contains('name: ${snap.name}\n'));
|
group("${app.name}'s snap, built in a clone of its own with its folder in "
|
||||||
expect(recipe, contains('\n ${snap.part}:\n after: [flutter]\n'));
|
"place of the root's snap/", () {
|
||||||
expect(recipe, contains('adopt-info: ${snap.part} '));
|
final snap = app.snap!;
|
||||||
});
|
final recipe = File('${snap.directory}/snapcraft.yaml')
|
||||||
|
.readAsStringSync();
|
||||||
|
|
||||||
test('builds Todo for its instance', () {
|
test('names the snap and the part the release tool takes', () {
|
||||||
expect(recipe, contains('cd ${todoRelease.flutterPackage}\n'));
|
expect(recipe, contains('name: ${snap.name}\n'));
|
||||||
expect(
|
expect(recipe, contains('\n ${snap.part}:\n after: [flutter]\n'));
|
||||||
recipe,
|
expect(recipe, contains('adopt-info: ${snap.part} '));
|
||||||
contains('--dart-define=SERVER_URL=${todoRelease.serverUrl}\n'),
|
});
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('has the files it names, as they will stand in the clone', () {
|
test('builds the app for its instance', () {
|
||||||
// In the clone, the folder is snap/, the root's snap/local/ beside it.
|
expect(recipe, contains('cd ${app.flutterPackage}\n'));
|
||||||
for (final match in RegExp(
|
expect(recipe, contains('--dart-define=SERVER_URL=${app.serverUrl}\n'));
|
||||||
r'(?<![\w/$])snap/([\w./-]+)',
|
});
|
||||||
).allMatches(recipe)) {
|
|
||||||
final inClone = match[1]!;
|
test('has the files it names, as they will stand in the clone', () {
|
||||||
final path = inClone.startsWith('local')
|
// In the clone, the folder is snap/, the root's snap/local/ beside
|
||||||
? 'snap/$inClone'
|
// it.
|
||||||
: '${snap.directory}/$inClone';
|
for (final match in RegExp(
|
||||||
if (inClone == 'snapcraft.yaml') {
|
r'(?<![\w/$])snap/([\w./-]+)',
|
||||||
continue;
|
).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(
|
expect(
|
||||||
FileSystemEntity.typeSync(path),
|
File('${snap.directory}/gui/${snap.name}.desktop').readAsStringSync(),
|
||||||
isNot(FileSystemEntityType.notFound),
|
contains('Exec=${snap.name}\n'),
|
||||||
reason: match[0],
|
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
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", () {
|
test("every snap's recipe builds with the Flutter CI pins", () {
|
||||||
final pinned = RegExp(
|
final pinned = RegExp(
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,7 @@ APPS = {
|
||||||
"halkyon_train_icon.png",
|
"halkyon_train_icon.png",
|
||||||
"dumbbell",
|
"dumbbell",
|
||||||
(COBALT, VIOLET, STRAWBERRY, CARROT),
|
(COBALT, VIOLET, STRAWBERRY, CARROT),
|
||||||
|
snap="apps/train/snap/gui/halkyon-train.png",
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue