// The checks every push has to pass: the packages the lock names, 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). // // dart run tool/check.dart everything // dart run tool/check.dart --changed-since=origin/main // the tests of what changed since then, and of what // depends on it // // 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. import 'dart:io'; import 'package:args/args.dart'; import 'src/check_files.dart'; Future main(List 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=]', ); 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); 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. final filters = melosFilters(changedSince: changedSince); await _step('Tests', [ ('dart', ['run', 'melos', 'run', 'test:dart', ...filters]), ('dart', ['run', 'melos', 'run', 'test:flutter', ...filters]), ]); stdout.writeln('✓ All checks passed.'); } /// Runs [commands] one after another as the step [name], and ends the check /// at the first that fails, with its exit code. Future _step( String name, List<(String, List)> 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 _run( String executable, List 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 _output(String executable, List 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; }