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 _needed(String package) { final needed = {}; 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 _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 _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 _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', ); } }); }); } }