build(check): the images' resolution replayed as the deploy builds them, before any push
Only the deploy builds the images, on the server, after every check
has passed, so an image that pub refuses cost a push each time (twice
on 4 Nov 2026). A step of tool/check.dart, right after the packages,
replays each Dockerfile's build stage up to its `dart pub get` from
the working tree's files in an empty folder, with Dart alone at the
image's version (downloaded once into ~/.cache/halkyon-check/, its
SHA-256 checked), offline, in an environment holding nothing of the
machine's Dart or Flutter: `dart run` from Flutter sets DART_ROOT,
through which pub found Flutter and accepted what the image refuses.
Checked on both failed deploys' states, which fail it as they did,
and on the fix, which passes. --images-only runs it alone, in
seconds. The list of images, dartImages, moves into
tool/src/images.dart, and a test keeps every Dockerfile building with
Dart on it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-04 13:24:47 +01:00
|
|
|
// The checks every push has to pass: the packages the lock names, the images'
|
|
|
|
|
// resolution, the generated code, formatting, static analysis (infos are fatal
|
|
|
|
|
// too), and every package's tests. The pre-push hook (tool/pre-push) runs it, and so does CI on
|
2026-10-23 15:14:23 +02:00
|
|
|
// every push (.forgejo/workflows/ci.yaml).
|
build: Melos, a Dart check and every path for the single repository
- Melos 8.9 as the root's dev dependency, with scripts across the
workspace (analyze, test:dart, test:flutter, generate, check) and its
filters (--diff, --include-dependents); cli_util overridden to 0.5.2
for it, since Learn's flutter_launcher_icons asks for 0.4 and uses only
its logger, unchanged in 0.5.
- tool/check.dart in place of tool/check.sh: packages, formatting of our
own files, analysis, the tools' tests, then every package's tests
through Melos (--changed-since=<commit> for only what changed and what
depends on it). tool/pre-push is the hook: the signature check, then
the checks. CI generates every server's code and runs the same check.
- The images build from the root with a workspace narrowed to what they
need (tool/workspace_only.sh); compose, the deploy's migration test,
the snap, the release script, the editor's launch files and Learn's
asset tools (run from apps/learn) follow the new layout.
- The project's source is named as project-halkyon/halkyon (source link,
snap, F-Droid listing, release uploads); the AGPL packages carry the
licence's own text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 20:39:09 +02:00
|
|
|
//
|
|
|
|
|
// dart run tool/check.dart everything
|
|
|
|
|
// dart run tool/check.dart --changed-since=origin/main
|
2026-10-19 21:32:14 +02:00
|
|
|
// the tests of the packages changed since then (in
|
|
|
|
|
// the working tree too), and of what depends on them
|
build(check): the images' resolution replayed as the deploy builds them, before any push
Only the deploy builds the images, on the server, after every check
has passed, so an image that pub refuses cost a push each time (twice
on 4 Nov 2026). A step of tool/check.dart, right after the packages,
replays each Dockerfile's build stage up to its `dart pub get` from
the working tree's files in an empty folder, with Dart alone at the
image's version (downloaded once into ~/.cache/halkyon-check/, its
SHA-256 checked), offline, in an environment holding nothing of the
machine's Dart or Flutter: `dart run` from Flutter sets DART_ROOT,
through which pub found Flutter and accepted what the image refuses.
Checked on both failed deploys' states, which fail it as they did,
and on the fix, which passes. --images-only runs it alone, in
seconds. The list of images, dartImages, moves into
tool/src/images.dart, and a test keeps every Dockerfile building with
Dart on it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-04 13:24:47 +01:00
|
|
|
// dart run tool/check.dart --images-only
|
|
|
|
|
// the images' resolution alone, in seconds: after a
|
|
|
|
|
// change to a Dockerfile, the root pubspec or the lock
|
build: Melos, a Dart check and every path for the single repository
- Melos 8.9 as the root's dev dependency, with scripts across the
workspace (analyze, test:dart, test:flutter, generate, check) and its
filters (--diff, --include-dependents); cli_util overridden to 0.5.2
for it, since Learn's flutter_launcher_icons asks for 0.4 and uses only
its logger, unchanged in 0.5.
- tool/check.dart in place of tool/check.sh: packages, formatting of our
own files, analysis, the tools' tests, then every package's tests
through Melos (--changed-since=<commit> for only what changed and what
depends on it). tool/pre-push is the hook: the signature check, then
the checks. CI generates every server's code and runs the same check.
- The images build from the root with a workspace narrowed to what they
need (tool/workspace_only.sh); compose, the deploy's migration test,
the snap, the release script, the editor's launch files and Learn's
asset tools (run from apps/learn) follow the new layout.
- The project's source is named as project-halkyon/halkyon (source link,
snap, F-Droid listing, release uploads); the AGPL packages carry the
licence's own text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 20:39:09 +02:00
|
|
|
//
|
|
|
|
|
// The tests run through Melos' scripts (`test:dart`, `test:flutter`, the root
|
|
|
|
|
// pubspec), Melos itself a dependency of the root.
|
|
|
|
|
// It checks the working tree, so uncommitted changes count as well.
|
2026-10-19 21:32:14 +02:00
|
|
|
import 'dart:convert';
|
build: Melos, a Dart check and every path for the single repository
- Melos 8.9 as the root's dev dependency, with scripts across the
workspace (analyze, test:dart, test:flutter, generate, check) and its
filters (--diff, --include-dependents); cli_util overridden to 0.5.2
for it, since Learn's flutter_launcher_icons asks for 0.4 and uses only
its logger, unchanged in 0.5.
- tool/check.dart in place of tool/check.sh: packages, formatting of our
own files, analysis, the tools' tests, then every package's tests
through Melos (--changed-since=<commit> for only what changed and what
depends on it). tool/pre-push is the hook: the signature check, then
the checks. CI generates every server's code and runs the same check.
- The images build from the root with a workspace narrowed to what they
need (tool/workspace_only.sh); compose, the deploy's migration test,
the snap, the release script, the editor's launch files and Learn's
asset tools (run from apps/learn) follow the new layout.
- The project's source is named as project-halkyon/halkyon (source link,
snap, F-Droid listing, release uploads); the AGPL packages carry the
licence's own text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 20:39:09 +02:00
|
|
|
import 'dart:io';
|
|
|
|
|
|
build(check): the images' resolution replayed as the deploy builds them, before any push
Only the deploy builds the images, on the server, after every check
has passed, so an image that pub refuses cost a push each time (twice
on 4 Nov 2026). A step of tool/check.dart, right after the packages,
replays each Dockerfile's build stage up to its `dart pub get` from
the working tree's files in an empty folder, with Dart alone at the
image's version (downloaded once into ~/.cache/halkyon-check/, its
SHA-256 checked), offline, in an environment holding nothing of the
machine's Dart or Flutter: `dart run` from Flutter sets DART_ROOT,
through which pub found Flutter and accepted what the image refuses.
Checked on both failed deploys' states, which fail it as they did,
and on the fix, which passes. --images-only runs it alone, in
seconds. The list of images, dartImages, moves into
tool/src/images.dart, and a test keeps every Dockerfile building with
Dart on it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-04 13:24:47 +01:00
|
|
|
import 'package:archive/archive_io.dart';
|
build: Melos, a Dart check and every path for the single repository
- Melos 8.9 as the root's dev dependency, with scripts across the
workspace (analyze, test:dart, test:flutter, generate, check) and its
filters (--diff, --include-dependents); cli_util overridden to 0.5.2
for it, since Learn's flutter_launcher_icons asks for 0.4 and uses only
its logger, unchanged in 0.5.
- tool/check.dart in place of tool/check.sh: packages, formatting of our
own files, analysis, the tools' tests, then every package's tests
through Melos (--changed-since=<commit> for only what changed and what
depends on it). tool/pre-push is the hook: the signature check, then
the checks. CI generates every server's code and runs the same check.
- The images build from the root with a workspace narrowed to what they
need (tool/workspace_only.sh); compose, the deploy's migration test,
the snap, the release script, the editor's launch files and Learn's
asset tools (run from apps/learn) follow the new layout.
- The project's source is named as project-halkyon/halkyon (source link,
snap, F-Droid listing, release uploads); the AGPL packages carry the
licence's own text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 20:39:09 +02:00
|
|
|
import 'package:args/args.dart';
|
2026-10-23 15:14:23 +02:00
|
|
|
import 'package:crypto/crypto.dart';
|
build(check): the images' resolution replayed as the deploy builds them, before any push
Only the deploy builds the images, on the server, after every check
has passed, so an image that pub refuses cost a push each time (twice
on 4 Nov 2026). A step of tool/check.dart, right after the packages,
replays each Dockerfile's build stage up to its `dart pub get` from
the working tree's files in an empty folder, with Dart alone at the
image's version (downloaded once into ~/.cache/halkyon-check/, its
SHA-256 checked), offline, in an environment holding nothing of the
machine's Dart or Flutter: `dart run` from Flutter sets DART_ROOT,
through which pub found Flutter and accepted what the image refuses.
Checked on both failed deploys' states, which fail it as they did,
and on the fix, which passes. --images-only runs it alone, in
seconds. The list of images, dartImages, moves into
tool/src/images.dart, and a test keeps every Dockerfile building with
Dart on it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-04 13:24:47 +01:00
|
|
|
import 'package:http/http.dart' as http;
|
|
|
|
|
import 'package:path/path.dart' as p;
|
build: Melos, a Dart check and every path for the single repository
- Melos 8.9 as the root's dev dependency, with scripts across the
workspace (analyze, test:dart, test:flutter, generate, check) and its
filters (--diff, --include-dependents); cli_util overridden to 0.5.2
for it, since Learn's flutter_launcher_icons asks for 0.4 and uses only
its logger, unchanged in 0.5.
- tool/check.dart in place of tool/check.sh: packages, formatting of our
own files, analysis, the tools' tests, then every package's tests
through Melos (--changed-since=<commit> for only what changed and what
depends on it). tool/pre-push is the hook: the signature check, then
the checks. CI generates every server's code and runs the same check.
- The images build from the root with a workspace narrowed to what they
need (tool/workspace_only.sh); compose, the deploy's migration test,
the snap, the release script, the editor's launch files and Learn's
asset tools (run from apps/learn) follow the new layout.
- The project's source is named as project-halkyon/halkyon (source link,
snap, F-Droid listing, release uploads); the AGPL packages carry the
licence's own text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 20:39:09 +02:00
|
|
|
|
|
|
|
|
import 'src/check_files.dart';
|
build(check): the images' resolution replayed as the deploy builds them, before any push
Only the deploy builds the images, on the server, after every check
has passed, so an image that pub refuses cost a push each time (twice
on 4 Nov 2026). A step of tool/check.dart, right after the packages,
replays each Dockerfile's build stage up to its `dart pub get` from
the working tree's files in an empty folder, with Dart alone at the
image's version (downloaded once into ~/.cache/halkyon-check/, its
SHA-256 checked), offline, in an environment holding nothing of the
machine's Dart or Flutter: `dart run` from Flutter sets DART_ROOT,
through which pub found Flutter and accepted what the image refuses.
Checked on both failed deploys' states, which fail it as they did,
and on the fix, which passes. --images-only runs it alone, in
seconds. The list of images, dartImages, moves into
tool/src/images.dart, and a test keeps every Dockerfile building with
Dart on it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-04 13:24:47 +01:00
|
|
|
import 'src/images.dart';
|
build: Melos, a Dart check and every path for the single repository
- Melos 8.9 as the root's dev dependency, with scripts across the
workspace (analyze, test:dart, test:flutter, generate, check) and its
filters (--diff, --include-dependents); cli_util overridden to 0.5.2
for it, since Learn's flutter_launcher_icons asks for 0.4 and uses only
its logger, unchanged in 0.5.
- tool/check.dart in place of tool/check.sh: packages, formatting of our
own files, analysis, the tools' tests, then every package's tests
through Melos (--changed-since=<commit> for only what changed and what
depends on it). tool/pre-push is the hook: the signature check, then
the checks. CI generates every server's code and runs the same check.
- The images build from the root with a workspace narrowed to what they
need (tool/workspace_only.sh); compose, the deploy's migration test,
the snap, the release script, the editor's launch files and Learn's
asset tools (run from apps/learn) follow the new layout.
- The project's source is named as project-halkyon/halkyon (source link,
snap, F-Droid listing, release uploads); the AGPL packages carry the
licence's own text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 20:39:09 +02:00
|
|
|
|
|
|
|
|
Future<void> main(List<String> arguments) async {
|
|
|
|
|
final options =
|
|
|
|
|
(ArgParser()
|
|
|
|
|
..addOption(
|
|
|
|
|
'changed-since',
|
|
|
|
|
help:
|
|
|
|
|
'Test only the packages changed since this commit, and those '
|
|
|
|
|
'depending on them.',
|
|
|
|
|
)
|
build(check): the images' resolution replayed as the deploy builds them, before any push
Only the deploy builds the images, on the server, after every check
has passed, so an image that pub refuses cost a push each time (twice
on 4 Nov 2026). A step of tool/check.dart, right after the packages,
replays each Dockerfile's build stage up to its `dart pub get` from
the working tree's files in an empty folder, with Dart alone at the
image's version (downloaded once into ~/.cache/halkyon-check/, its
SHA-256 checked), offline, in an environment holding nothing of the
machine's Dart or Flutter: `dart run` from Flutter sets DART_ROOT,
through which pub found Flutter and accepted what the image refuses.
Checked on both failed deploys' states, which fail it as they did,
and on the fix, which passes. --images-only runs it alone, in
seconds. The list of images, dartImages, moves into
tool/src/images.dart, and a test keeps every Dockerfile building with
Dart on it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-04 13:24:47 +01:00
|
|
|
..addFlag(
|
|
|
|
|
'images-only',
|
|
|
|
|
negatable: false,
|
|
|
|
|
help: "Check only the images' resolution.",
|
|
|
|
|
)
|
build: Melos, a Dart check and every path for the single repository
- Melos 8.9 as the root's dev dependency, with scripts across the
workspace (analyze, test:dart, test:flutter, generate, check) and its
filters (--diff, --include-dependents); cli_util overridden to 0.5.2
for it, since Learn's flutter_launcher_icons asks for 0.4 and uses only
its logger, unchanged in 0.5.
- tool/check.dart in place of tool/check.sh: packages, formatting of our
own files, analysis, the tools' tests, then every package's tests
through Melos (--changed-since=<commit> for only what changed and what
depends on it). tool/pre-push is the hook: the signature check, then
the checks. CI generates every server's code and runs the same check.
- The images build from the root with a workspace narrowed to what they
need (tool/workspace_only.sh); compose, the deploy's migration test,
the snap, the release script, the editor's launch files and Learn's
asset tools (run from apps/learn) follow the new layout.
- The project's source is named as project-halkyon/halkyon (source link,
snap, F-Droid listing, release uploads); the AGPL packages carry the
licence's own text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 20:39:09 +02:00
|
|
|
..addFlag('help', abbr: 'h', negatable: false))
|
|
|
|
|
.parse(arguments);
|
|
|
|
|
if (options.flag('help')) {
|
|
|
|
|
stdout.writeln(
|
build(check): the images' resolution replayed as the deploy builds them, before any push
Only the deploy builds the images, on the server, after every check
has passed, so an image that pub refuses cost a push each time (twice
on 4 Nov 2026). A step of tool/check.dart, right after the packages,
replays each Dockerfile's build stage up to its `dart pub get` from
the working tree's files in an empty folder, with Dart alone at the
image's version (downloaded once into ~/.cache/halkyon-check/, its
SHA-256 checked), offline, in an environment holding nothing of the
machine's Dart or Flutter: `dart run` from Flutter sets DART_ROOT,
through which pub found Flutter and accepted what the image refuses.
Checked on both failed deploys' states, which fail it as they did,
and on the fix, which passes. --images-only runs it alone, in
seconds. The list of images, dartImages, moves into
tool/src/images.dart, and a test keeps every Dockerfile building with
Dart on it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-04 13:24:47 +01:00
|
|
|
'Usage: dart run tool/check.dart [--changed-since=<commit>] '
|
|
|
|
|
'[--images-only]',
|
build: Melos, a Dart check and every path for the single repository
- Melos 8.9 as the root's dev dependency, with scripts across the
workspace (analyze, test:dart, test:flutter, generate, check) and its
filters (--diff, --include-dependents); cli_util overridden to 0.5.2
for it, since Learn's flutter_launcher_icons asks for 0.4 and uses only
its logger, unchanged in 0.5.
- tool/check.dart in place of tool/check.sh: packages, formatting of our
own files, analysis, the tools' tests, then every package's tests
through Melos (--changed-since=<commit> for only what changed and what
depends on it). tool/pre-push is the hook: the signature check, then
the checks. CI generates every server's code and runs the same check.
- The images build from the root with a workspace narrowed to what they
need (tool/workspace_only.sh); compose, the deploy's migration test,
the snap, the release script, the editor's launch files and Learn's
asset tools (run from apps/learn) follow the new layout.
- The project's source is named as project-halkyon/halkyon (source link,
snap, F-Droid listing, release uploads); the AGPL packages carry the
licence's own text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 20:39:09 +02:00
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
final changedSince = options.option('changed-since');
|
|
|
|
|
|
|
|
|
|
Directory.current = (await _output('git', [
|
|
|
|
|
'rev-parse',
|
|
|
|
|
'--show-toplevel',
|
|
|
|
|
])).trim();
|
|
|
|
|
if ((await Process.run('git', ['diff', '--quiet', 'HEAD'])).exitCode != 0) {
|
|
|
|
|
stdout.writeln(
|
|
|
|
|
'Note: the working tree has uncommitted changes; they are checked too.',
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The packages the checked-out pubspecs and lock name, fetched first: after
|
|
|
|
|
// a merge or a switch of branch the workspace's resolution can still be the
|
|
|
|
|
// one from before, and the analysis then failed on packages it couldn't
|
|
|
|
|
// find (2026-10-13, 2026-10-17). The lock decides (--enforce-lockfile), so a
|
|
|
|
|
// check never changes it.
|
|
|
|
|
await _step('Packages', [
|
|
|
|
|
('flutter', ['pub', 'get', '--enforce-lockfile']),
|
|
|
|
|
], quiet: true);
|
|
|
|
|
|
build(check): the images' resolution replayed as the deploy builds them, before any push
Only the deploy builds the images, on the server, after every check
has passed, so an image that pub refuses cost a push each time (twice
on 4 Nov 2026). A step of tool/check.dart, right after the packages,
replays each Dockerfile's build stage up to its `dart pub get` from
the working tree's files in an empty folder, with Dart alone at the
image's version (downloaded once into ~/.cache/halkyon-check/, its
SHA-256 checked), offline, in an environment holding nothing of the
machine's Dart or Flutter: `dart run` from Flutter sets DART_ROOT,
through which pub found Flutter and accepted what the image refuses.
Checked on both failed deploys' states, which fail it as they did,
and on the fix, which passes. --images-only runs it alone, in
seconds. The list of images, dartImages, moves into
tool/src/images.dart, and a test keeps every Dockerfile building with
Dart on it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-04 13:24:47 +01:00
|
|
|
await _images();
|
|
|
|
|
if (options.flag('images-only')) {
|
|
|
|
|
stdout.writeln("✓ The images' resolution passed.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-23 15:14:23 +02:00
|
|
|
await _generatedCode();
|
|
|
|
|
|
build: Melos, a Dart check and every path for the single repository
- Melos 8.9 as the root's dev dependency, with scripts across the
workspace (analyze, test:dart, test:flutter, generate, check) and its
filters (--diff, --include-dependents); cli_util overridden to 0.5.2
for it, since Learn's flutter_launcher_icons asks for 0.4 and uses only
its logger, unchanged in 0.5.
- tool/check.dart in place of tool/check.sh: packages, formatting of our
own files, analysis, the tools' tests, then every package's tests
through Melos (--changed-since=<commit> for only what changed and what
depends on it). tool/pre-push is the hook: the signature check, then
the checks. CI generates every server's code and runs the same check.
- The images build from the root with a workspace narrowed to what they
need (tool/workspace_only.sh); compose, the deploy's migration test,
the snap, the release script, the editor's launch files and Learn's
asset tools (run from apps/learn) follow the new layout.
- The project's source is named as project-halkyon/halkyon (source link,
snap, F-Droid listing, release uploads); the AGPL packages carry the
licence's own text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 20:39:09 +02:00
|
|
|
final own = (await _output('git', [
|
|
|
|
|
'ls-files',
|
|
|
|
|
'*.dart',
|
|
|
|
|
])).split('\n').where(isOwnDartFile).toList();
|
|
|
|
|
await _step('Formatting', [
|
|
|
|
|
('dart', ['format', '--output=none', '--set-exit-if-changed', ...own]),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
await _step('Analysis', [
|
|
|
|
|
('dart', ['analyze', '--fatal-infos']),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
await _step('Tools', [
|
|
|
|
|
('dart', ['test', 'test/']),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// The sync tests need `serverpod start` running (`requires-server`); run
|
|
|
|
|
// them by hand with `flutter test -t requires-server` in Learn's app.
|
2026-10-19 21:32:14 +02:00
|
|
|
final touched = changedSince == null
|
|
|
|
|
? null
|
|
|
|
|
: await _packagesChangedSince(changedSince);
|
|
|
|
|
if (touched != null && touched.isEmpty) {
|
|
|
|
|
stdout
|
|
|
|
|
..writeln('▸ Tests')
|
|
|
|
|
..writeln('No package changed since $changedSince.');
|
|
|
|
|
} else {
|
|
|
|
|
final filters = melosFilters(touched: touched);
|
|
|
|
|
await _step('Tests', [
|
|
|
|
|
('dart', ['run', 'melos', 'run', 'test:dart', ...filters]),
|
|
|
|
|
('dart', ['run', 'melos', 'run', 'test:flutter', ...filters]),
|
|
|
|
|
]);
|
|
|
|
|
}
|
build: Melos, a Dart check and every path for the single repository
- Melos 8.9 as the root's dev dependency, with scripts across the
workspace (analyze, test:dart, test:flutter, generate, check) and its
filters (--diff, --include-dependents); cli_util overridden to 0.5.2
for it, since Learn's flutter_launcher_icons asks for 0.4 and uses only
its logger, unchanged in 0.5.
- tool/check.dart in place of tool/check.sh: packages, formatting of our
own files, analysis, the tools' tests, then every package's tests
through Melos (--changed-since=<commit> for only what changed and what
depends on it). tool/pre-push is the hook: the signature check, then
the checks. CI generates every server's code and runs the same check.
- The images build from the root with a workspace narrowed to what they
need (tool/workspace_only.sh); compose, the deploy's migration test,
the snap, the release script, the editor's launch files and Learn's
asset tools (run from apps/learn) follow the new layout.
- The project's source is named as project-halkyon/halkyon (source link,
snap, F-Droid listing, release uploads); the AGPL packages carry the
licence's own text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 20:39:09 +02:00
|
|
|
|
|
|
|
|
stdout.writeln('✓ All checks passed.');
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-23 15:14:23 +02:00
|
|
|
/// Fails the check unless the generated code (each server's
|
|
|
|
|
/// `lib/src/generated/` and test tools, its client package's
|
|
|
|
|
/// `lib/src/protocol/`) is what Serverpod's generator makes of the models and
|
|
|
|
|
/// endpoints now: it is committed, and a model or endpoint changed without
|
|
|
|
|
/// generating again failed only in CI before (23 Oct 2026). Every server
|
|
|
|
|
/// package, the apps' and the shared module, through Melos' `generate`
|
|
|
|
|
/// script, with the CLI at the version the lock holds Serverpod at, as CI
|
|
|
|
|
/// installs it. What it writes stays in the working tree, to be committed.
|
|
|
|
|
Future<void> _generatedCode() async {
|
|
|
|
|
stdout.writeln('▸ Generated code');
|
|
|
|
|
final wanted = lockedVersion(
|
|
|
|
|
File('pubspec.lock').readAsStringSync(),
|
|
|
|
|
'serverpod',
|
|
|
|
|
);
|
|
|
|
|
final ProcessResult cli;
|
|
|
|
|
try {
|
|
|
|
|
cli = await Process.run('serverpod', ['--no-analytics', 'version']);
|
|
|
|
|
} on ProcessException {
|
|
|
|
|
stderr.writeln(
|
|
|
|
|
"✗ The Serverpod CLI isn't on the PATH: "
|
|
|
|
|
'dart install serverpod_cli@$wanted',
|
|
|
|
|
);
|
|
|
|
|
exit(1);
|
|
|
|
|
}
|
|
|
|
|
final found = serverpodCliVersion('${cli.stdout}');
|
|
|
|
|
if (wanted == null || found != wanted) {
|
|
|
|
|
stderr.writeln(
|
|
|
|
|
'✗ The Serverpod CLI is at ${found ?? 'an unknown version'}, the lock '
|
|
|
|
|
'holds Serverpod at $wanted: dart install serverpod_cli@$wanted',
|
|
|
|
|
);
|
|
|
|
|
exit(1);
|
|
|
|
|
}
|
|
|
|
|
final before = await _uncommittedFiles();
|
|
|
|
|
final code = await _run('dart', [
|
|
|
|
|
'run',
|
|
|
|
|
'melos',
|
|
|
|
|
'run',
|
|
|
|
|
'generate',
|
|
|
|
|
], quiet: true);
|
|
|
|
|
if (code != 0) {
|
|
|
|
|
stderr.writeln('✗ Generated code failed (serverpod generate).');
|
|
|
|
|
exit(code);
|
|
|
|
|
}
|
|
|
|
|
final differing = differingFiles(before, await _uncommittedFiles());
|
|
|
|
|
if (differing.isNotEmpty) {
|
|
|
|
|
stderr
|
|
|
|
|
..writeln(
|
|
|
|
|
'✗ The generated code differs from what the generator makes of the '
|
|
|
|
|
'models and endpoints now; it has been written anew, commit it:',
|
|
|
|
|
)
|
|
|
|
|
..writeAll(differing.map((path) => ' $path\n'));
|
|
|
|
|
exit(1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
build(check): the images' resolution replayed as the deploy builds them, before any push
Only the deploy builds the images, on the server, after every check
has passed, so an image that pub refuses cost a push each time (twice
on 4 Nov 2026). A step of tool/check.dart, right after the packages,
replays each Dockerfile's build stage up to its `dart pub get` from
the working tree's files in an empty folder, with Dart alone at the
image's version (downloaded once into ~/.cache/halkyon-check/, its
SHA-256 checked), offline, in an environment holding nothing of the
machine's Dart or Flutter: `dart run` from Flutter sets DART_ROOT,
through which pub found Flutter and accepted what the image refuses.
Checked on both failed deploys' states, which fail it as they did,
and on the fix, which passes. --images-only runs it alone, in
seconds. The list of images, dartImages, moves into
tool/src/images.dart, and a test keeps every Dockerfile building with
Dart on it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-04 13:24:47 +01:00
|
|
|
/// Fails the check unless every image of [dartImages] resolves its packages
|
|
|
|
|
/// as its build stage does on the server: its steps up to `dart pub get`
|
|
|
|
|
/// replayed in an empty folder from the working tree's files, with a Dart
|
|
|
|
|
/// SDK of the image's version alone, without Flutter (pub refused the
|
|
|
|
|
/// overrides of Flutter plugins there and accepted them here, 4 Nov 2026),
|
|
|
|
|
/// offline from the pub cache the step before filled. Only the deploy builds
|
|
|
|
|
/// the images, after every check; a failure there cost a push each time.
|
|
|
|
|
Future<void> _images() async {
|
|
|
|
|
stdout.writeln('▸ Images');
|
|
|
|
|
final repository = Directory.current.path;
|
|
|
|
|
final tracked = (await _output('git', [
|
|
|
|
|
'ls-files',
|
|
|
|
|
'-z',
|
|
|
|
|
'--cached',
|
|
|
|
|
'--others',
|
|
|
|
|
'--exclude-standard',
|
|
|
|
|
])).split('\x00').where((path) => path.isNotEmpty).toList();
|
|
|
|
|
final work = await Directory.systemTemp.createTemp('halkyon-images-');
|
|
|
|
|
try {
|
|
|
|
|
for (final dockerfile in dartImages) {
|
|
|
|
|
final ImageResolution resolution;
|
|
|
|
|
try {
|
|
|
|
|
resolution = imageResolution(File(dockerfile).readAsStringSync());
|
|
|
|
|
} on FormatException catch (error) {
|
|
|
|
|
stderr.writeln('✗ $dockerfile: ${error.message}');
|
|
|
|
|
exit(1);
|
|
|
|
|
}
|
|
|
|
|
final sdk = await _dartSdk(resolution.dartVersion);
|
|
|
|
|
final app = Directory(p.join(work.path, p.dirname(dockerfile)));
|
|
|
|
|
var workdir = '/app';
|
|
|
|
|
final environment = _imageEnvironment(sdk);
|
|
|
|
|
for (final step in resolution.steps) {
|
|
|
|
|
final ok = switch (step) {
|
|
|
|
|
WorkdirStep(:final path) => () {
|
|
|
|
|
workdir = p.posix.normalize(p.posix.join(workdir, path));
|
|
|
|
|
return true;
|
|
|
|
|
}(),
|
|
|
|
|
CopyStep() => () {
|
|
|
|
|
for (final MapEntry(key: from, value: to) in copiedFiles(
|
|
|
|
|
step,
|
|
|
|
|
tracked,
|
|
|
|
|
workdir: workdir,
|
|
|
|
|
).entries) {
|
|
|
|
|
final source = File(p.join(repository, from));
|
|
|
|
|
if (source.existsSync()) {
|
|
|
|
|
final target = File(
|
|
|
|
|
p.join(app.path, p.relative(to, from: '/app')),
|
|
|
|
|
)..parent.createSync(recursive: true);
|
|
|
|
|
source.copySync(target.path);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}(),
|
|
|
|
|
RunStep(:final command) => await _runIn(
|
|
|
|
|
p.join(app.path, p.relative(workdir, from: '/app')),
|
|
|
|
|
command == 'dart pub get' ? 'dart pub get --offline' : command,
|
|
|
|
|
environment,
|
|
|
|
|
),
|
|
|
|
|
};
|
|
|
|
|
if (!ok) {
|
|
|
|
|
stderr.writeln(
|
|
|
|
|
"✗ Images failed: $dockerfile's build stage, as the deploy builds "
|
|
|
|
|
'it, fails here.',
|
|
|
|
|
);
|
|
|
|
|
exit(1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
stdout.writeln(' $dockerfile');
|
|
|
|
|
}
|
|
|
|
|
} finally {
|
|
|
|
|
work.deleteSync(recursive: true);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// What a build stage's commands see: little but the SDK on the PATH, as in
|
|
|
|
|
/// Dart's image. Nothing of the machine's Dart or Flutter passes: `dart run`
|
|
|
|
|
/// from Flutter sets `DART_ROOT` to Flutter's Dart, through which pub finds
|
|
|
|
|
/// Flutter and accepts what the image refuses (seen 4 Nov 2026), and
|
|
|
|
|
/// `FLUTTER_ROOT` would do the same. Only the home and the pub cache, the
|
|
|
|
|
/// temporary folder, the language and the proxies are kept.
|
|
|
|
|
Map<String, String> _imageEnvironment(String sdk) => {
|
|
|
|
|
for (final name in [
|
|
|
|
|
'HOME',
|
|
|
|
|
'PUB_CACHE',
|
|
|
|
|
'TMPDIR',
|
|
|
|
|
'LANG',
|
|
|
|
|
'http_proxy',
|
|
|
|
|
'https_proxy',
|
|
|
|
|
'no_proxy',
|
|
|
|
|
'HTTP_PROXY',
|
|
|
|
|
'HTTPS_PROXY',
|
|
|
|
|
'NO_PROXY',
|
|
|
|
|
])
|
|
|
|
|
name: ?Platform.environment[name],
|
|
|
|
|
'PATH': '${p.join(sdk, 'bin')}:/usr/local/bin:/usr/bin:/bin',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/// Runs [command] through `sh -c` in [directory] with [environment] alone,
|
|
|
|
|
/// its output shown only should it fail; whether it succeeded.
|
|
|
|
|
Future<bool> _runIn(
|
|
|
|
|
String directory,
|
|
|
|
|
String command,
|
|
|
|
|
Map<String, String> environment,
|
|
|
|
|
) async {
|
|
|
|
|
Directory(directory).createSync(recursive: true);
|
|
|
|
|
final result = await Process.run(
|
|
|
|
|
'sh',
|
|
|
|
|
['-c', command],
|
|
|
|
|
workingDirectory: directory,
|
|
|
|
|
environment: environment,
|
|
|
|
|
includeParentEnvironment: false,
|
|
|
|
|
);
|
|
|
|
|
if (result.exitCode != 0) {
|
|
|
|
|
stderr
|
|
|
|
|
..writeln(' \$ $command')
|
|
|
|
|
..write(result.stdout)
|
|
|
|
|
..write(result.stderr);
|
|
|
|
|
}
|
|
|
|
|
return result.exitCode == 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The Dart SDK at [version] alone, as Dart's images hold it: kept in the
|
|
|
|
|
/// user's cache (`~/.cache/halkyon-check/`), downloaded from Google's archive
|
|
|
|
|
/// the first time and checked against the archive's SHA-256. Its folder.
|
|
|
|
|
Future<String> _dartSdk(String version) async {
|
|
|
|
|
final home =
|
|
|
|
|
Platform.environment['XDG_CACHE_HOME'] ??
|
|
|
|
|
p.join(Platform.environment['HOME']!, '.cache');
|
|
|
|
|
final folder = Directory(p.join(home, 'halkyon-check', 'dart-$version'));
|
|
|
|
|
final sdk = p.join(folder.path, 'dart-sdk');
|
|
|
|
|
if (File(p.join(sdk, 'bin', 'dart')).existsSync()) {
|
|
|
|
|
return sdk;
|
|
|
|
|
}
|
|
|
|
|
final name = dartSdkArchive(Platform.operatingSystem, Platform.version);
|
|
|
|
|
final url =
|
|
|
|
|
'https://storage.googleapis.com/dart-archive/channels/stable/release/'
|
|
|
|
|
'$version/sdk/$name';
|
|
|
|
|
stdout.writeln(' Dart $version alone, downloaded once: $url');
|
|
|
|
|
final archive = await http.get(Uri.parse(url));
|
|
|
|
|
final sum = await http.get(Uri.parse('$url.sha256sum'));
|
|
|
|
|
if (archive.statusCode != 200 || sum.statusCode != 200) {
|
|
|
|
|
stderr.writeln('✗ Images failed: Dart $version not downloaded ($url).');
|
|
|
|
|
exit(1);
|
|
|
|
|
}
|
|
|
|
|
if ('${sha256.convert(archive.bodyBytes)}' != sum.body.split(' ').first) {
|
|
|
|
|
stderr.writeln("✗ Images failed: Dart $version doesn't match its SHA-256.");
|
|
|
|
|
exit(1);
|
|
|
|
|
}
|
|
|
|
|
final partial = Directory('${folder.path}.partial');
|
|
|
|
|
if (partial.existsSync()) {
|
|
|
|
|
partial.deleteSync(recursive: true);
|
|
|
|
|
}
|
|
|
|
|
await extractArchiveToDisk(
|
|
|
|
|
ZipDecoder().decodeBytes(archive.bodyBytes),
|
|
|
|
|
partial.path,
|
|
|
|
|
);
|
|
|
|
|
// The archive's modes aren't kept by every extraction; the SDK's
|
|
|
|
|
// executables are its bin folder's.
|
|
|
|
|
await _output('chmod', [
|
|
|
|
|
'-R',
|
|
|
|
|
'u+x',
|
|
|
|
|
p.join(partial.path, 'dart-sdk', 'bin'),
|
|
|
|
|
]);
|
|
|
|
|
partial.renameSync(folder.path);
|
|
|
|
|
return sdk;
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-23 15:14:23 +02:00
|
|
|
/// The working tree's files that differ from the committed ones, untracked
|
|
|
|
|
/// files not ignored among them, each with a digest of its content, or
|
|
|
|
|
/// [absentFile] where it was deleted.
|
|
|
|
|
Future<Map<String, String>> _uncommittedFiles() async {
|
|
|
|
|
final paths = (await _output('git', [
|
|
|
|
|
'ls-files',
|
|
|
|
|
'--modified',
|
|
|
|
|
'--deleted',
|
|
|
|
|
'--others',
|
|
|
|
|
'--exclude-standard',
|
|
|
|
|
'-z',
|
|
|
|
|
])).split('\x00').where((path) => path.isNotEmpty).toSet();
|
|
|
|
|
return {
|
|
|
|
|
for (final path in paths)
|
|
|
|
|
path: switch (File(path)) {
|
|
|
|
|
final file when file.existsSync() =>
|
|
|
|
|
'${sha256.convert(file.readAsBytesSync())}',
|
|
|
|
|
_ => absentFile,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-19 21:32:14 +02:00
|
|
|
/// The packages changed since [commit], in the working tree too (committed
|
|
|
|
|
/// or not, and files new to git), or null for every package
|
|
|
|
|
/// ([packagesTouched]).
|
|
|
|
|
Future<List<String>?> _packagesChangedSince(String commit) async {
|
|
|
|
|
final paths = [
|
|
|
|
|
...(await _output('git', ['diff', '--name-only', commit])).split('\n'),
|
|
|
|
|
...(await _output('git', [
|
|
|
|
|
'ls-files',
|
|
|
|
|
'--others',
|
|
|
|
|
'--exclude-standard',
|
|
|
|
|
])).split('\n'),
|
|
|
|
|
].where((path) => path.isNotEmpty);
|
|
|
|
|
final root = Directory.current.path;
|
|
|
|
|
final packages = {
|
|
|
|
|
for (final package in jsonDecode(
|
|
|
|
|
await _output('dart', ['run', 'melos', 'list', '--json']),
|
|
|
|
|
) as List<Object?>)
|
|
|
|
|
if (package case {'name': final String name, 'location': final String at})
|
|
|
|
|
name: at.substring(root.length + 1),
|
|
|
|
|
};
|
|
|
|
|
return packagesTouched(paths, packages);
|
|
|
|
|
}
|
|
|
|
|
|
build: Melos, a Dart check and every path for the single repository
- Melos 8.9 as the root's dev dependency, with scripts across the
workspace (analyze, test:dart, test:flutter, generate, check) and its
filters (--diff, --include-dependents); cli_util overridden to 0.5.2
for it, since Learn's flutter_launcher_icons asks for 0.4 and uses only
its logger, unchanged in 0.5.
- tool/check.dart in place of tool/check.sh: packages, formatting of our
own files, analysis, the tools' tests, then every package's tests
through Melos (--changed-since=<commit> for only what changed and what
depends on it). tool/pre-push is the hook: the signature check, then
the checks. CI generates every server's code and runs the same check.
- The images build from the root with a workspace narrowed to what they
need (tool/workspace_only.sh); compose, the deploy's migration test,
the snap, the release script, the editor's launch files and Learn's
asset tools (run from apps/learn) follow the new layout.
- The project's source is named as project-halkyon/halkyon (source link,
snap, F-Droid listing, release uploads); the AGPL packages carry the
licence's own text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 20:39:09 +02:00
|
|
|
/// Runs [commands] one after another as the step [name], and ends the check
|
|
|
|
|
/// at the first that fails, with its exit code.
|
|
|
|
|
Future<void> _step(
|
|
|
|
|
String name,
|
|
|
|
|
List<(String, List<String>)> commands, {
|
|
|
|
|
bool quiet = false,
|
|
|
|
|
}) async {
|
|
|
|
|
stdout.writeln('▸ $name');
|
|
|
|
|
for (final (executable, arguments) in commands) {
|
|
|
|
|
final code = await _run(executable, arguments, quiet: quiet);
|
|
|
|
|
if (code != 0) {
|
|
|
|
|
stderr.writeln('✗ $name failed ($executable ${arguments.first}).');
|
|
|
|
|
exit(code);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Runs [executable] and returns its exit code; its output is passed on as
|
|
|
|
|
/// it comes, or if [quiet] only should it fail.
|
|
|
|
|
Future<int> _run(
|
|
|
|
|
String executable,
|
|
|
|
|
List<String> arguments, {
|
|
|
|
|
bool quiet = false,
|
|
|
|
|
}) async {
|
|
|
|
|
if (quiet) {
|
|
|
|
|
final result = await Process.run(executable, arguments);
|
|
|
|
|
if (result.exitCode != 0) {
|
|
|
|
|
stderr
|
|
|
|
|
..write(result.stdout)
|
|
|
|
|
..write(result.stderr);
|
|
|
|
|
}
|
|
|
|
|
return result.exitCode;
|
|
|
|
|
}
|
|
|
|
|
final process = await Process.start(
|
|
|
|
|
executable,
|
|
|
|
|
arguments,
|
|
|
|
|
mode: ProcessStartMode.inheritStdio,
|
|
|
|
|
);
|
|
|
|
|
return await process.exitCode;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// What [executable] writes, failing the check if it fails.
|
|
|
|
|
Future<String> _output(String executable, List<String> arguments) async {
|
|
|
|
|
final result = await Process.run(executable, arguments);
|
|
|
|
|
if (result.exitCode != 0) {
|
|
|
|
|
stderr.write(result.stderr);
|
|
|
|
|
exit(result.exitCode);
|
|
|
|
|
}
|
|
|
|
|
return result.stdout as String;
|
|
|
|
|
}
|