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
3 changed files with 100 additions and 5 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
# The Docker build context is the repository root. Nothing secret, local or bulky
|
# The Docker build context is the repository root. Nothing secret, local or bulky
|
||||||
# goes into it: Learn's server's and curator's Dockerfiles copy their whole package
|
# goes into it: Learn's server's and curator's Dockerfiles copy their whole package
|
||||||
# folders (and the server the module it builds on, with the module's client, which the
|
# folders (and the server every package it resolves, which test/deploy/images_test.dart
|
||||||
# module's tests depend on).
|
# holds to its pubspecs, so none of them may be left out here).
|
||||||
|
|
||||||
# Secrets and local state
|
# Secrets and local state
|
||||||
**/config/passwords.yaml
|
**/config/passwords.yaml
|
||||||
|
|
@ -27,7 +27,6 @@ apps/learn/halkyon_learn_client/
|
||||||
apps/learn/tool/
|
apps/learn/tool/
|
||||||
apps/todo/
|
apps/todo/
|
||||||
packages/halkyon_app/
|
packages/halkyon_app/
|
||||||
packages/halkyon_core/
|
|
||||||
packages/halkyon_interop/
|
packages/halkyon_interop/
|
||||||
packages/halkyon_keys/
|
packages/halkyon_keys/
|
||||||
packages/halkyon_ui/
|
packages/halkyon_ui/
|
||||||
|
|
|
||||||
|
|
@ -17,12 +17,14 @@ COPY apps/learn/halkyon_learn_server apps/learn/halkyon_learn_server
|
||||||
COPY packages/halkyon_base_server packages/halkyon_base_server
|
COPY packages/halkyon_base_server packages/halkyon_base_server
|
||||||
COPY packages/halkyon_base_client packages/halkyon_base_client
|
COPY packages/halkyon_base_client packages/halkyon_base_client
|
||||||
COPY packages/halkyon_core packages/halkyon_core
|
COPY packages/halkyon_core packages/halkyon_core
|
||||||
|
COPY packages/halkyon_llm packages/halkyon_llm
|
||||||
COPY packages/halkyon_oidc_server packages/halkyon_oidc_server
|
COPY packages/halkyon_oidc_server packages/halkyon_oidc_server
|
||||||
COPY packages/halkyon_oidc_client packages/halkyon_oidc_client
|
COPY packages/halkyon_oidc_client packages/halkyon_oidc_client
|
||||||
|
|
||||||
# The workspace root with only the server and the modules it builds on (halkyon_base,
|
# The workspace root with only the server and the modules it builds on (halkyon_base,
|
||||||
# and the OpenID Connect module halkyon_base signs in with, ADR 0023), with
|
# and the OpenID Connect module halkyon_base signs in with, ADR 0023), with
|
||||||
# halkyon_core, whose BIP-39 list halkyon_base takes a matching code from: neither the
|
# halkyon_core, whose BIP-39 list halkyon_base takes a matching code from, and
|
||||||
|
# halkyon_llm, which it forwards the operator's assistants with: neither the
|
||||||
# other apps nor Learn's client package are needed to build the server, and the Flutter
|
# other apps nor Learn's client package are needed to build the server, and the Flutter
|
||||||
# packages would need Flutter. The base module's client comes along only because the
|
# packages would need Flutter. The base module's client comes along only because the
|
||||||
# module's tests depend on it (SRP's two halves, held to each other), and pub resolves
|
# module's tests depend on it (SRP's two halves, held to each other), and pub resolves
|
||||||
|
|
@ -30,7 +32,7 @@ COPY packages/halkyon_oidc_client packages/halkyon_oidc_client
|
||||||
# client depends on it; the server never imports either. Everything else stays as the
|
# client depends on it; the server never imports either. Everything else stays as the
|
||||||
# repository has it, above all the dependency overrides, which bring the server our
|
# repository has it, above all the dependency overrides, which bring the server our
|
||||||
# fork of the offline sync package (fetched with the git the Dart image has).
|
# fork of the offline sync package (fetched with the git the Dart image has).
|
||||||
RUN sh tool/workspace_only.sh apps/learn/halkyon_learn_server packages/halkyon_base_server packages/halkyon_base_client packages/halkyon_core packages/halkyon_oidc_server packages/halkyon_oidc_client
|
RUN sh tool/workspace_only.sh apps/learn/halkyon_learn_server packages/halkyon_base_server packages/halkyon_base_client packages/halkyon_core packages/halkyon_llm packages/halkyon_oidc_server packages/halkyon_oidc_client
|
||||||
RUN dart pub get
|
RUN dart pub get
|
||||||
WORKDIR /app/apps/learn/halkyon_learn_server
|
WORKDIR /app/apps/learn/halkyon_learn_server
|
||||||
RUN dart build cli --target bin/main.dart --output build
|
RUN dart build cli --target bin/main.dart --output build
|
||||||
|
|
|
||||||
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