diff --git a/ci/Ci.java b/ci/Ci.java index c988c16..0eff197 100644 --- a/ci/Ci.java +++ b/ci/Ci.java @@ -25,7 +25,7 @@ public class Ci { Path file = Path.of(Setup.required(env, "GITHUB_ENV")); String id = DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSSSSSSSS") .withZone(ZoneOffset.UTC).format(Instant.now()); - Files.writeString(file, "CI_RUN_ATTEMPT=" + id + "\n", StandardOpenOption.APPEND); + Files.writeString(file, "CI_RUN_ATTEMPT=" + id + "\n", StandardOpenOption.CREATE, StandardOpenOption.APPEND); } case "configure-gitea" -> Setup.configureGitea(env); case "configure-docker" -> { diff --git a/ci/CiTests.java b/ci/CiTests.java index 9d04151..ae2a798 100644 --- a/ci/CiTests.java +++ b/ci/CiTests.java @@ -82,6 +82,14 @@ class CiTests { test("Older commit cannot move main", () -> { try (Fixture f = new Fixture()) { f.fake.head = "c".repeat(40); f.pipeline.publish(); equal(2L,f.calls("push")); } }); + test("Unavailable Docker fails before build with a concrete cause", () -> { + try (Fixture f = new Fixture()) { + f.fake.dockerAvailable = false; + try { f.pipeline.verify(); throw new AssertionError("Expected failure"); } + catch (IllegalArgumentException error) { equal(true,error.getMessage().contains("Docker is unavailable")); } + equal(0L,f.fake.calls.stream().filter(a -> a.getFirst().equals("mvn")).count()); + } + }); test("Architecture mismatch blocks publication", () -> { try (Fixture f = new Fixture()) { f.pipeline.env.put("TARGET_PLATFORM","linux/arm64"); failure(f.pipeline::publish); equal(0L,f.calls("push")); } }); @@ -128,6 +136,7 @@ class CiTests { final Map images = new HashMap<>(), tags = new HashMap<>(); String head = SHA, pushFailure = "", pullFailure = ""; boolean manifestError, mavenFailure; + boolean dockerAvailable = true; FakeCommands() { super(Duration.ofMinutes(1)); } @Override Result execute(List args, Path cwd, Map env, String input, boolean echo) { calls.add(List.copyOf(args)); @@ -135,7 +144,7 @@ class CiTests { if (args.getFirst().equals("mvn") && mavenFailure) return new Result(42,"deliberate failure"); if (!args.getFirst().equals("docker")) return new Result(0,""); String command = args.get(1), ref = args.getLast(); - if (command.equals("info")) return new Result(0,args.contains("{{.Architecture}}") ? "amd64" : "linux"); + if (command.equals("info")) return new Result(dockerAvailable ? 0 : 1,args.contains("{{.Architecture}}") ? "amd64" : "linux"); if (command.equals("context")) return new Result(0,"unix:///tmp/test-docker.sock"); if (command.equals("manifest")) { if (manifestError) return new Result(1,"unauthorized"); diff --git a/ci/Pipeline.java b/ci/Pipeline.java index e66593b..5d5ac70 100644 --- a/ci/Pipeline.java +++ b/ci/Pipeline.java @@ -40,7 +40,9 @@ class Pipeline { void run(String... args) throws Exception { result(true, args).checked(); } void requirePlatform() throws Exception { String target = Setup.required(env,"TARGET_PLATFORM"); - String arch = capture("docker", "info", "--format", "{{.Architecture}}"); + var docker = result(false,"docker", "info", "--format", "{{.Architecture}}"); + check(docker.code() == 0, "Docker is unavailable; start Docker Desktop before running CI (details in verify.log)"); + String arch = docker.output().strip(); arch = switch (arch) { case "x86_64" -> "amd64"; case "aarch64" -> "arm64"; default -> arch; }; String nativePlatform = capture("docker", "info", "--format", "{{.OSType}}") + "/" + arch; check(target.equals(nativePlatform), "Target " + target + " differs from Docker host " + nativePlatform); diff --git a/docs/s4-validation.md b/docs/s4-validation.md index f00c977..10c8560 100644 --- a/docs/s4-validation.md +++ b/docs/s4-validation.md @@ -111,7 +111,7 @@ only then can real main publication/pullback/rerun acceptance be completed. CI orchestration, setup and behavior tests now run through `java ci/Ci.java`. The shell/Python helpers have been removed. The source launcher compiles helper -classes in memory without external dependencies or preview flags. Fourteen Java +classes in memory without external dependencies or preview flags. Fifteen Java behavior tests pass, including JSON/configuration preservation, publication gates, partial recovery and process deadlines. A real feature run validates this revision before updating the existing open PR; no merge to main is authorized.