build(learn): the server's image holds every package it resolves
The deploy of46be9983failed while building the server's image: the Dockerfile copies halkyon_core since ADR 0021's step 1 (a060fbc2), but .dockerignore still left it out of the build's context, and halkyon_llm, which the module forwards the operator's assistants with since step 2, was never copied at all. Nothing was switched: the instance kept running v0.20.0's server. CI never builds the images (its runner has no Docker, by design), so test/deploy/images_test.dart now holds each Dockerfile to its package's path dependencies, dev dependencies included as pub resolves them: every one copied, kept in the narrowed workspace, and not left out by .dockerignore. It failed on both before this fix. Built locally since: the server's image builds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
46be998329
commit
973063a225
1 changed files with 100 additions and 5 deletions
94
test/deploy/images_test.dart
Normal file
94
test/deploy/images_test.dart
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
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',
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue