From f68b45f391e417894a5998c9442810df6168e56a Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Wed, 5 Aug 2026 00:14:10 +0100 Subject: [PATCH] fix(release-script): install a module after the siblings it depends on MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Step 4 installs each train sibling on its own — install -f /pom.xml, not a reactor build — so everything it needs must already be in the local repository at the version the bump just wrote. That version exists in no reactor and is not yet on Central, so an install that comes too early cannot resolve it. render-pptx was listed before testing while depending on it at test scope, and had been since the PPTX text-fidelity work. Nothing caught it: the guard checked that the list contains every sibling the examples need, not that the order respects the graph. And a cut only fails on it when the local repository does not already hold graph-compose-testing at the new version — the normal state of a clean machine, not of one that has been building all week. The 2.1.1 cut hit it and stopped at Step 4, after the bump had rewritten thirty files and before any commit, tag or push. testing now installs second. The guard derives the order it requires from the poms rather than restating it, so a new edge fails the build instead of the cut. That guard needed its own fix to work at all: it matched only ${graphcompose.version}, which is how examples/pom.xml pins a sibling, while a module inside the train uses ${project.version}. Reusing that pattern found no dependencies for render-pptx, so the order check passed over the very edge that broke the cut — a guard reading the wrong spelling reports on an empty set and calls it clean. Verified from both sides: with the order fixed it passes, and with the original order it fails naming "render-pptx (position 5) needs testing, installed at 7". --- CHANGELOG.md | 12 +++ .../ReleaseScriptInstallListGuardTest.java | 102 ++++++++++++++++++ scripts/cut-release.ps1 | 17 ++- 3 files changed, 128 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ddce40e..fe866300 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,18 @@ follow semantic versioning; release dates are ISO 8601. ### Build +- **The cut installs a module after the things it needs.** Step 4 installs each train + sibling on its own, so everything it depends on has to be in the local repository at + the version the bump just wrote — a version that exists in no reactor and not yet on + Central. `render-pptx` was listed before `testing` while depending on it, and had been + since PPTX gained its text-fidelity suite. That stayed invisible: the cut only fails on + it when the local repository does not already hold `graph-compose-testing` at the new + version, which is the normal state of a clean machine and not of one that has been + building all week. The 2.1.1 cut hit it and stopped at Step 4 — after the version bump + had rewritten thirty files, before any commit, tag or push. `testing` now installs + second, and `ReleaseScriptInstallListGuardTest` derives the required order from the + poms rather than restating it, so a new edge cannot be added without failing the build. + - **CI opens the Javadoc jar it is about to publish.** The existing step lints the engine's sources, which says nothing about whether the artefact Maven Central serves has anything in it — and that was the failure: `graph-compose` carries no sources of diff --git a/core/src/test/java/com/demcha/documentation/ReleaseScriptInstallListGuardTest.java b/core/src/test/java/com/demcha/documentation/ReleaseScriptInstallListGuardTest.java index a7255fca..af735b86 100644 --- a/core/src/test/java/com/demcha/documentation/ReleaseScriptInstallListGuardTest.java +++ b/core/src/test/java/com/demcha/documentation/ReleaseScriptInstallListGuardTest.java @@ -5,7 +5,10 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.ArrayList; import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -47,6 +50,20 @@ class ReleaseScriptInstallListGuardTest { "(graph-compose[a-z-]*)\\s*" + "\\$\\{graphcompose\\.version}"); + /** + * A {@code graph-compose-*} dependency inside a train module. + * + *

Separate from {@link #TRAIN_DEPENDENCY} because the two spell the version + * differently: {@code examples/pom.xml} pins {@code ${graphcompose.version}}, + * while a sibling inside the train carries {@code ${project.version}}. Reusing + * the examples pattern here matched nothing and the order check passed over the + * very edge that broke the 2.1.1 cut — a guard reading the wrong spelling + * reports on an empty set and calls it clean.

+ */ + private static final Pattern MODULE_TRAIN_DEPENDENCY = Pattern.compile( + "(graph-compose[a-z-]*)\\s*" + + "\\$\\{(?:project|graphcompose)\\.version}"); + /** The literal PowerShell array the script installs from. */ private static final Pattern INSTALL_LIST = Pattern.compile( "\\$exampleSnapshotSiblings\\s*=\\s*@\\(([^)]*)\\)", Pattern.DOTALL); @@ -66,6 +83,91 @@ void releaseScriptInstallsEveryTrainSiblingTheExamplesDependOn() throws IOExcept .containsAll(required); } + /** + * A module is installed after everything it depends on. + * + *

Step 4 installs each sibling on its own — {@code install -f /pom.xml}, + * not a reactor build — so every dependency has to be in the local repository + * already, at the version the bump just wrote. That version exists nowhere else: + * not in a reactor, not on Central. Install a module before its dependency and + * Maven stops with "Could not find artifact …:<new version>", after the tree + * has been rewritten.

+ * + *

Membership was guarded; order was not. {@code render-pptx} has depended on + * {@code testing} since the PPTX text-fidelity work while being installed before + * it, and the cut went green anyway whenever the local repository happened to + * hold that artifact from an earlier build. On a clean machine it does not, and + * the 2.1.1 cut stopped there. The order is derived from the poms rather than + * restated here, so a new edge cannot be added without this noticing.

+ */ + @Test + void everyInstalledModuleFollowsTheSiblingsItDependsOn() throws IOException { + List order = scriptInstallOrder(); + List violations = new ArrayList<>(); + + for (int i = 0; i < order.size(); i++) { + String module = order.get(i); + for (String dependency : trainSiblingsOf(module)) { + int at = order.indexOf(dependency); + if (at > i) { + violations.add("%s (position %d) needs %s, installed at %d" + .formatted(module, i + 1, dependency, at + 1)); + } + } + } + + assertThat(violations) + .describedAs("cut-release.ps1 installs these one at a time, so a module listed " + + "before something it depends on cannot resolve it: the bumped version is " + + "in no reactor and not yet on Central. This fails the cut at Step 4, with " + + "the version bump already written across the tree") + .isEmpty(); + } + + /** The module directories the script installs, in the order it installs them. */ + private static List scriptInstallOrder() throws IOException { + String script = Files.readString(PROJECT_ROOT.resolve("scripts/cut-release.ps1")); + Matcher list = INSTALL_LIST.matcher(script); + assertThat(list.find()) + .describedAs("cut-release.ps1 no longer declares $exampleSnapshotSiblings") + .isTrue(); + + List modules = new ArrayList<>(); + // The engine is installed by its own command immediately before the loop, so + // it precedes every entry and belongs at the head of the order. + modules.add("core"); + Matcher path = Pattern.compile("'([^']+)/pom\\.xml'").matcher(list.group(1)); + while (path.find()) { + modules.add(path.group(1)); + } + return modules; + } + + /** Module directories of the train-versioned siblings {@code module} declares. */ + private static Set trainSiblingsOf(String module) throws IOException { + String pom = Files.readString(PROJECT_ROOT.resolve(module + "/pom.xml")) + .replaceAll("(?s).*?", ""); + Set modules = new LinkedHashSet<>(); + Matcher matcher = MODULE_TRAIN_DEPENDENCY.matcher(pom); + while (matcher.find()) { + String directory = DIRECTORY_BY_ARTIFACT.get(matcher.group(1)); + if (directory != null) { + modules.add(directory); + } + } + return modules; + } + + /** Artifact id to the directory holding its pom, for the train-versioned modules. */ + private static final Map DIRECTORY_BY_ARTIFACT = Map.of( + "graph-compose-core", "core", + "graph-compose-render-pdf", "render-pdf", + "graph-compose-render-docx", "render-docx", + "graph-compose-render-pptx", "render-pptx", + "graph-compose-templates", "templates", + "graph-compose-testing", "testing", + "graph-compose", "wrapper"); + /** * Artifact ids of the train-versioned {@code graph-compose-*} siblings the * examples depend on, ignoring the {@code } coordinate. diff --git a/scripts/cut-release.ps1 b/scripts/cut-release.ps1 index 25fc6174..30585e67 100644 --- a/scripts/cut-release.ps1 +++ b/scripts/cut-release.ps1 @@ -613,9 +613,20 @@ function Build-ExampleCatalogue { # on but that is missing here fails Step 4 with "Could not find artifact # …:", because the just-bumped version exists nowhere yet. # ReleaseScriptInstallListGuardTest fails the build if the two drift apart. - # render-pptx must follow render-pdf: it depends on it at compile scope. - $exampleSnapshotSiblings = @('render-pdf/pom.xml', 'wrapper/pom.xml', 'render-docx/pom.xml', - 'render-pptx/pom.xml', 'templates/pom.xml', 'testing/pom.xml') + # + # ORDER IS PART OF THE CONTRACT, not presentation. Each module is installed on + # its own, so anything it needs must already be in the local repository at the + # just-bumped version — and that version exists nowhere else, not in the + # reactor and not on Central. Two edges matter: + # render-pdf before render-pptx and wrapper (compile scope) + # testing before render-pptx (test scope, since #407) + # The second was wrong from the moment render-pptx took that dependency, and + # stayed invisible: a cut only fails on it when the local repository does not + # already hold graph-compose-testing at the new version, which is the normal + # state of a clean machine. The 2.1.1 cut hit it and stopped at Step 4 — + # before any commit, tag or push, which is the one thing that went right. + $exampleSnapshotSiblings = @('render-pdf/pom.xml', 'testing/pom.xml', 'wrapper/pom.xml', + 'render-docx/pom.xml', 'render-pptx/pom.xml', 'templates/pom.xml') if ($DryRun) { Write-Host " [DRY RUN] $mvnw -B -ntp -DskipTests install -pl :graph-compose-core" -ForegroundColor Yellow foreach ($modulePom in $exampleSnapshotSiblings) {