94 lines
3 KiB
Dart
94 lines
3 KiB
Dart
|
|
import 'dart:io';
|
||
|
|
|
||
|
|
import 'package:path/path.dart' as p;
|
||
|
|
import 'package:test/test.dart';
|
||
|
|
import 'package:yaml/yaml.dart';
|
||
|
|
|
||
|
|
/// The images CI's checks never build: the deploy does, on the server, and
|
||
|
|
/// a package missing from one failed it there (26 Oct 2026, `halkyon_core`
|
||
|
|
/// kept out by `.dockerignore`, `halkyon_llm` never copied).
|
||
|
|
const _dockerfiles = [
|
||
|
|
'apps/learn/halkyon_learn_server/Dockerfile',
|
||
|
|
'apps/learn/halkyon_learn_curator/Dockerfile',
|
||
|
|
];
|
||
|
|
|
||
|
|
/// The packages [package] takes by path, and theirs, with itself: pub
|
||
|
|
/// resolves every workspace member's dev dependencies too, so an image
|
||
|
|
/// needs those as well.
|
||
|
|
Set<String> _needed(String package) {
|
||
|
|
final needed = <String>{};
|
||
|
|
final waiting = [package];
|
||
|
|
while (waiting.isNotEmpty) {
|
||
|
|
final each = waiting.removeLast();
|
||
|
|
if (!needed.add(each)) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
final pubspec =
|
||
|
|
loadYaml(File('$each/pubspec.yaml').readAsStringSync()) as YamlMap;
|
||
|
|
for (final section in ['dependencies', 'dev_dependencies']) {
|
||
|
|
final dependencies = pubspec[section] as YamlMap? ?? YamlMap();
|
||
|
|
for (final dependency in dependencies.values) {
|
||
|
|
if (dependency case {'path': final String path}) {
|
||
|
|
waiting.add(p.normalize(p.join(each, path)));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return needed;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// The folders the image's build stage copies in as they are named.
|
||
|
|
Set<String> _copied(String dockerfile) => {
|
||
|
|
for (final match in RegExp(
|
||
|
|
r'^COPY ((?:apps|packages)/\S+) \1$',
|
||
|
|
multiLine: true,
|
||
|
|
).allMatches(dockerfile))
|
||
|
|
match[1]!,
|
||
|
|
};
|
||
|
|
|
||
|
|
/// The members `tool/workspace_only.sh` keeps in the image's workspace.
|
||
|
|
List<String> _members(String dockerfile) => RegExp(
|
||
|
|
r'^RUN sh tool/workspace_only\.sh (.+)$',
|
||
|
|
multiLine: true,
|
||
|
|
).firstMatch(dockerfile)![1]!.split(' ');
|
||
|
|
|
||
|
|
/// The lines of `.dockerignore` that name a folder, without the slash.
|
||
|
|
Set<String> _ignored() => {
|
||
|
|
for (final line in File('.dockerignore').readAsLinesSync())
|
||
|
|
if (line.trim() case final folder
|
||
|
|
when folder.isNotEmpty && !folder.startsWith('#'))
|
||
|
|
folder.replaceFirst(RegExp(r'/$'), ''),
|
||
|
|
};
|
||
|
|
|
||
|
|
void main() {
|
||
|
|
for (final path in _dockerfiles) {
|
||
|
|
group(path, () {
|
||
|
|
final dockerfile = File(path).readAsStringSync();
|
||
|
|
final package = _members(dockerfile).first;
|
||
|
|
|
||
|
|
test('builds the package it lies in', () {
|
||
|
|
expect(package, p.dirname(path));
|
||
|
|
});
|
||
|
|
|
||
|
|
test('copies every package the build resolves, and keeps each in the '
|
||
|
|
'workspace', () {
|
||
|
|
final needed = _needed(package);
|
||
|
|
expect(_copied(dockerfile), needed);
|
||
|
|
expect(_members(dockerfile).toSet(), needed);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("keeps none of them out of the build's context", () {
|
||
|
|
final ignored = _ignored();
|
||
|
|
for (final each in _needed(package)) {
|
||
|
|
expect(
|
||
|
|
ignored.where(
|
||
|
|
(folder) => each == folder || p.isWithin(folder, each),
|
||
|
|
),
|
||
|
|
isEmpty,
|
||
|
|
reason: '.dockerignore leaves $each out',
|
||
|
|
);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|