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) {