halkyon/test/deploy/image_resolution_test.dart
Tizian 「ティツィアーン」 1ba532d6ef
All checks were successful
CI / check (push) Successful in 45m37s
CI / deploy (push) Successful in 5m23s
build(check): the images' resolution replayed as the deploy builds them, before any push
Only the deploy builds the images, on the server, after every check
has passed, so an image that pub refuses cost a push each time (twice
on 4 Nov 2026). A step of tool/check.dart, right after the packages,
replays each Dockerfile's build stage up to its `dart pub get` from
the working tree's files in an empty folder, with Dart alone at the
image's version (downloaded once into ~/.cache/halkyon-check/, its
SHA-256 checked), offline, in an environment holding nothing of the
machine's Dart or Flutter: `dart run` from Flutter sets DART_ROOT,
through which pub found Flutter and accepted what the image refuses.

Checked on both failed deploys' states, which fail it as they did,
and on the fix, which passes. --images-only runs it alone, in
seconds. The list of images, dartImages, moves into
tool/src/images.dart, and a test keeps every Dockerfile building with
Dart on it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-04 13:43:51 +01:00

153 lines
4.5 KiB
Dart

import 'dart:io';
import 'package:test/test.dart';
import '../../tool/src/images.dart';
/// tool/check.dart's replay of the images' resolution (4 Nov 2026: two
/// deploys failed on what pub refuses in Dart's image alone).
void main() {
group('a build stage read', () {
const dockerfile = r'''
# A comment.
FROM dart:3.13.4 AS build
WORKDIR /app
COPY pubspec.yaml pubspec.lock ./
COPY tool/workspace_only.sh tool/
RUN sh tool/workspace_only.sh apps/a \
packages/b
RUN dart pub get
WORKDIR /app/apps/a
RUN dart build cli --target bin/main.dart --output build
FROM ubuntu:24.04
COPY --from=build /app/apps/a/build /srv
''';
test('names its Dart and its steps up to and including dart pub get, '
'continued lines joined', () {
final resolution = imageResolution(dockerfile);
expect(resolution.dartVersion, '3.13.4');
expect(
[
for (final step in resolution.steps)
switch (step) {
CopyStep(:final sources, :final destination) =>
'COPY ${sources.join(' ')} -> $destination',
RunStep(:final command) => 'RUN $command',
WorkdirStep(:final path) => 'WORKDIR $path',
},
],
[
'WORKDIR /app',
'COPY pubspec.yaml pubspec.lock -> ./',
'COPY tool/workspace_only.sh -> tool/',
'RUN sh tool/workspace_only.sh apps/a packages/b',
'RUN dart pub get',
],
);
});
test('refuses a stage without Dart, without pub get, or with an '
'instruction it would replay wrongly', () {
expect(
() => imageResolution('FROM ubuntu:24.04\nRUN true\n'),
throwsFormatException,
);
expect(
() => imageResolution('FROM dart:3.13.4 AS build\nRUN true\n'),
throwsFormatException,
);
expect(
() => imageResolution(
'FROM dart:3.13.4 AS build\nENV A=b\nRUN dart pub get\n',
),
throwsFormatException,
);
});
});
group("COPY's files land as Docker puts them", () {
const files = [
'pubspec.yaml',
'pubspec.lock',
'tool/workspace_only.sh',
'tool/check.dart',
'apps/a/pubspec.yaml',
'apps/a/lib/a.dart',
'apps/ab/pubspec.yaml',
];
test('a folder: its content under the destination, nothing beside it', () {
expect(copiedFiles(const CopyStep(['apps/a'], 'apps/a'), files), {
'apps/a/pubspec.yaml': '/app/apps/a/pubspec.yaml',
'apps/a/lib/a.dart': '/app/apps/a/lib/a.dart',
});
});
test('files into a folder named with a slash, or several at once', () {
expect(
copiedFiles(const CopyStep(['tool/workspace_only.sh'], 'tool/'), files),
{'tool/workspace_only.sh': '/app/tool/workspace_only.sh'},
);
expect(
copiedFiles(
const CopyStep(['pubspec.yaml', 'pubspec.lock'], './'),
files,
),
{
'pubspec.yaml': '/app/pubspec.yaml',
'pubspec.lock': '/app/pubspec.lock',
},
);
});
test('a file as the destination named without a slash', () {
expect(
copiedFiles(
const CopyStep(['pubspec.yaml'], 'other.yaml'),
files,
workdir: '/app/x',
),
{'pubspec.yaml': '/app/x/other.yaml'},
);
});
});
test("the SDK's archive is this machine's", () {
expect(
dartSdkArchive('linux', '3.13.4 (stable) (Tue) on "linux_x64"'),
'dartsdk-linux-x64-release.zip',
);
expect(
dartSdkArchive('linux', '3.13.4 (stable) (Tue) on "linux_arm64"'),
'dartsdk-linux-arm64-release.zip',
);
});
group('the repository', () {
test('every image of the list is replayed up to its pub get', () {
for (final path in dartImages) {
final resolution = imageResolution(File(path).readAsStringSync());
expect(resolution.dartVersion, matches(RegExp(r'^\d+\.\d+\.\d+$')));
expect(resolution.steps.last, isA<RunStep>(), reason: path);
}
});
test('every Dockerfile building with Dart is on the list, so none '
'escapes the replay', () {
final dockerfiles = Process.runSync('git', [
'ls-files',
'*Dockerfile',
]).stdout.toString().split('\n').where((path) => path.isNotEmpty);
final dart = [
for (final path in dockerfiles)
if (File(path)
.readAsStringSync()
.contains(RegExp('^FROM dart:', multiLine: true)))
path,
];
expect(dart.toSet(), dartImages.toSet());
});
});
}