2026-10-23 15:14:23 +02:00
|
|
|
// The checks every push has to pass: the packages the lock names, 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
|
|
|
|
|
// 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: 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';
|
|
|
|
|
|
|
|
|
|
import 'package:args/args.dart';
|
2026-10-23 15:14:23 +02:00
|
|
|
import 'package:crypto/crypto.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 'src/check_files.dart';
|
|
|
|
|
|
|
|
|
|
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.',
|
|
|
|
|
)
|
|
|
|
|
..addFlag('help', abbr: 'h', negatable: false))
|
|
|
|
|
.parse(arguments);
|
|
|
|
|
if (options.flag('help')) {
|
|
|
|
|
stdout.writeln(
|
|
|
|
|
'Usage: dart run tool/check.dart [--changed-since=<commit>]',
|
|
|
|
|
);
|
|
|
|
|
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);
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 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;
|
|
|
|
|
}
|