fix(tool): the check limited to changes sees uncommitted ones too
Melos' --diff compares commits alone (<commit>...HEAD), so a check with --changed-since before committing tested nothing and passed. The check now works out the packages touched itself, from git diff against the commit and the files new to git, each file counted in its innermost package, and hands them to Melos as --scope with --include-dependents; a changed root pubspec, lock or lint rule means every package. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
e65f61bdd1
commit
bf20134819
3 changed files with 126 additions and 16 deletions
|
|
@ -46,11 +46,54 @@ void main() {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Melos' filters take only what changed since a commit, and what depends "
|
group('what changed since a commit', () {
|
||||||
'on it, when asked', () {
|
const packages = {
|
||||||
|
'halkyon_app': 'packages/halkyon_app',
|
||||||
|
'halkyon_ui': 'packages/halkyon_ui',
|
||||||
|
'halkyon_ui_example': 'packages/halkyon_ui/example',
|
||||||
|
'halkyon_learn_flutter': 'apps/learn/halkyon_learn_flutter',
|
||||||
|
};
|
||||||
|
|
||||||
|
test('touches the packages its files are in, the innermost one', () {
|
||||||
|
expect(
|
||||||
|
packagesTouched([
|
||||||
|
'packages/halkyon_app/lib/src/app/app_frame.dart',
|
||||||
|
'packages/halkyon_app/test/app/instance_gate_test.dart',
|
||||||
|
'packages/halkyon_ui/example/lib/main.dart',
|
||||||
|
], packages),
|
||||||
|
['halkyon_app', 'halkyon_ui_example'],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("files of no package (docs, tools, deploy) touch none: the tools' "
|
||||||
|
'own tests run anyway', () {
|
||||||
|
expect(
|
||||||
|
packagesTouched([
|
||||||
|
'docs/adr/0015-one-repository.md',
|
||||||
|
'tool/check.dart',
|
||||||
|
'deploy/compose.yaml',
|
||||||
|
], packages),
|
||||||
|
isEmpty,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the root's pubspec, lock or lint rules touch every package", () {
|
||||||
|
for (final path in [
|
||||||
|
'pubspec.yaml',
|
||||||
|
'pubspec.lock',
|
||||||
|
'analysis_options.yaml',
|
||||||
|
]) {
|
||||||
|
expect(packagesTouched([path], packages), isNull, reason: path);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Melos' filters take every package, or those touched and what "
|
||||||
|
'depends on them', () {
|
||||||
expect(melosFilters(), isEmpty);
|
expect(melosFilters(), isEmpty);
|
||||||
expect(melosFilters(changedSince: 'origin/main'), [
|
expect(melosFilters(touched: ['halkyon_app', 'halkyon_ui']), [
|
||||||
'--diff=origin/main',
|
'--scope=halkyon_app',
|
||||||
|
'--scope=halkyon_ui',
|
||||||
'--include-dependents',
|
'--include-dependents',
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,13 @@
|
||||||
//
|
//
|
||||||
// dart run tool/check.dart everything
|
// dart run tool/check.dart everything
|
||||||
// dart run tool/check.dart --changed-since=origin/main
|
// dart run tool/check.dart --changed-since=origin/main
|
||||||
// the tests of what changed since then, and of what
|
// the tests of the packages changed since then (in
|
||||||
// depends on it
|
// the working tree too), and of what depends on them
|
||||||
//
|
//
|
||||||
// The tests run through Melos' scripts (`test:dart`, `test:flutter`, the root
|
// The tests run through Melos' scripts (`test:dart`, `test:flutter`, the root
|
||||||
// pubspec), Melos itself a dependency of the root.
|
// pubspec), Melos itself a dependency of the root.
|
||||||
// It checks the working tree, so uncommitted changes count as well.
|
// It checks the working tree, so uncommitted changes count as well.
|
||||||
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:args/args.dart';
|
import 'package:args/args.dart';
|
||||||
|
|
@ -73,15 +74,47 @@ Future<void> main(List<String> arguments) async {
|
||||||
|
|
||||||
// The sync tests need `serverpod start` running (`requires-server`); run
|
// The sync tests need `serverpod start` running (`requires-server`); run
|
||||||
// them by hand with `flutter test -t requires-server` in Learn's app.
|
// them by hand with `flutter test -t requires-server` in Learn's app.
|
||||||
final filters = melosFilters(changedSince: changedSince);
|
final touched = changedSince == null
|
||||||
await _step('Tests', [
|
? null
|
||||||
('dart', ['run', 'melos', 'run', 'test:dart', ...filters]),
|
: await _packagesChangedSince(changedSince);
|
||||||
('dart', ['run', 'melos', 'run', 'test:flutter', ...filters]),
|
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]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
stdout.writeln('✓ All checks passed.');
|
stdout.writeln('✓ All checks passed.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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);
|
||||||
|
}
|
||||||
|
|
||||||
/// Runs [commands] one after another as the step [name], and ends the check
|
/// Runs [commands] one after another as the step [name], and ends the check
|
||||||
/// at the first that fails, with its exit code.
|
/// at the first that fails, with its exit code.
|
||||||
Future<void> _step(
|
Future<void> _step(
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,43 @@ final _generated = RegExp(
|
||||||
bool isOwnDartFile(String path) =>
|
bool isOwnDartFile(String path) =>
|
||||||
path.endsWith('.dart') && !_generated.hasMatch(path);
|
path.endsWith('.dart') && !_generated.hasMatch(path);
|
||||||
|
|
||||||
/// The filters handed to Melos for the tests: every package, or with
|
/// The root's files every package's resolution or analysis depends on.
|
||||||
/// [changedSince] only the packages changed since that commit and those
|
const _rootFiles = {'pubspec.yaml', 'pubspec.lock', 'analysis_options.yaml'};
|
||||||
/// depending on them.
|
|
||||||
List<String> melosFilters({String? changedSince}) => [
|
/// The workspace's packages that [paths] (changed files, relative to the
|
||||||
if (changedSince != null) ...['--diff=$changedSince', '--include-dependents'],
|
/// repository's root) are in, each file counted in the innermost package
|
||||||
|
/// holding it ([packages]: name to its folder, relative to the root); null
|
||||||
|
/// for every package, should a root file all of them depend on have changed.
|
||||||
|
/// Files in no package (docs, tools, deploy) touch none.
|
||||||
|
List<String>? packagesTouched(
|
||||||
|
Iterable<String> paths,
|
||||||
|
Map<String, String> packages,
|
||||||
|
) {
|
||||||
|
final touched = <String>{};
|
||||||
|
for (final path in paths) {
|
||||||
|
if (_rootFiles.contains(path)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String? innermost;
|
||||||
|
for (final MapEntry(key: name, value: folder) in packages.entries) {
|
||||||
|
if (path.startsWith('$folder/') &&
|
||||||
|
(innermost == null || folder.length > packages[innermost]!.length)) {
|
||||||
|
innermost = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (innermost != null) {
|
||||||
|
touched.add(innermost);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return touched.toList()..sort();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The filters handed to Melos for the tests: none for every package, or
|
||||||
|
/// the packages [touched] and those depending on them. (Melos' own `--diff`
|
||||||
|
/// compares commits alone, so a check before committing would see nothing.)
|
||||||
|
List<String> melosFilters({List<String>? touched}) => [
|
||||||
|
if (touched != null) ...[
|
||||||
|
for (final name in touched) '--scope=$name',
|
||||||
|
'--include-dependents',
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue