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>
This commit is contained in:
parent
2ec58bfaee
commit
1ba532d6ef
1 changed files with 478 additions and 16 deletions
153
test/deploy/image_resolution_test.dart
Normal file
153
test/deploy/image_resolution_test.dart
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
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());
|
||||
});
|
||||
});
|
||||
}
|
||||
Loading…
Reference in a new issue