From fe3a0626a9d360998f93757d2307a30a40275ea7 Mon Sep 17 00:00:00 2001 From: Laert Llaveshi Date: Tue, 11 Aug 2026 07:59:43 +0200 Subject: [PATCH 01/12] #2178: Initial implementation --- .../tools/ide/commandlet/BuildCommandlet.java | 38 ++++++++++++------- .../ide/commandlet/ReleaseCommandlet.java | 26 ++++++++----- 2 files changed, 41 insertions(+), 23 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/BuildCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/BuildCommandlet.java index 99c7c131de..46a082e3b6 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/BuildCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/BuildCommandlet.java @@ -50,25 +50,37 @@ protected void doRun() { throw new CliException("Missing current working directory!"); } - List args = this.arguments.asList(); - LocalToolCommandlet commandlet = null; - for (Class toolClass : BUILD_TOOLS) { - LocalToolCommandlet toolCommandlet = this.context.getCommandletManager().getCommandlet(toolClass); - Path buildDescriptor = toolCommandlet.findBuildDescriptor(buildPath); - if (buildDescriptor != null) { - commandlet = toolCommandlet; - if (args.isEmpty()) { - String variableName = commandlet.getName().toUpperCase(Locale.ROOT) + "_BUILD_OPTS"; - args = getDefaultToolOptions(variableName); - } - } - } + LocalToolCommandlet commandlet = findBuildCommandlet(this.context, buildPath); if (commandlet == null) { throw new CliException("Could not find build descriptor - no pom.xml, build.gradle, or package.json found!"); } + List args = this.arguments.asList(); + if (args.isEmpty()) { + String variableName = commandlet.getName().toUpperCase(Locale.ROOT) + "_BUILD_OPTS"; + args = getDefaultToolOptions(variableName); + } commandlet.runTool(args); } + /** + * Detects the applicable build tool for the given {@code buildPath} by {@link LocalToolCommandlet#findBuildDescriptor(Path) querying} the available build + * commandlets (in order of priority) for a matching build descriptor (e.g. {@code pom.xml}, {@code build.gradle} or {@code package.json}). + * + * @param context the {@link IdeContext}. + * @param buildPath the {@link Path} to the directory to build. + * @return the applicable build {@link LocalToolCommandlet} or {@code null} if no build descriptor was found. + */ + static LocalToolCommandlet findBuildCommandlet(IdeContext context, Path buildPath) { + + for (Class toolClass : BUILD_TOOLS) { + LocalToolCommandlet toolCommandlet = context.getCommandletManager().getCommandlet(toolClass); + if (toolCommandlet.findBuildDescriptor(buildPath) != null) { + return toolCommandlet; + } + } + return null; + } + private List getDefaultToolOptions(String buildOptionName) { String[] defaultToolOptions = this.context.getVariables().get(buildOptionName).split(" "); diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/ReleaseCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/ReleaseCommandlet.java index 141992e9fa..9f94c0f9be 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/ReleaseCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/ReleaseCommandlet.java @@ -1,6 +1,5 @@ package com.devonfw.tools.ide.commandlet; -import java.nio.file.Files; import java.nio.file.Path; import org.slf4j.Logger; @@ -11,7 +10,8 @@ import com.devonfw.tools.ide.git.GitContext; import com.devonfw.tools.ide.process.ProcessResult; import com.devonfw.tools.ide.property.StringProperty; -import com.devonfw.tools.ide.tool.mvn.Mvn; +import com.devonfw.tools.ide.tool.BuildTool; +import com.devonfw.tools.ide.tool.LocalToolCommandlet; import com.devonfw.tools.ide.version.VersionIdentifier; /** @@ -41,15 +41,20 @@ protected void doRun() { Path projectPath = this.context.getCwd(); GitContext git = this.context.getGitContext(); - Mvn buildTool = this.context.getCommandletManager().getCommandlet(Mvn.class); + + LocalToolCommandlet commandlet = BuildCommandlet.findBuildCommandlet(this.context, projectPath); + if (!(commandlet instanceof BuildTool buildTool)) { + throw new CliException("Could not find a supported build tool to release the project in " + projectPath + "."); + } if (git.hasUntrackedFiles(projectPath)) { throw new CliException("Your local git repository has uncommitted changes. Please use 'git stash' and rerun on clean repo."); } if (warnIfFork(git, projectPath)) { - confirmWarning("You seem to work on a fork. Releases should be done on the original repository!\nWe strongly recommend to abort and rerun on original repository."); + confirmWarning("You seem to work on a fork. Releases should be done on the original repository!\n" + + "We strongly recommend to abort and rerun on original repository."); } - if (!this.context.isForceMode() && !isTopLevelProject(projectPath)) { + if (!this.context.isForceMode() && !isTopLevelProject(commandlet, projectPath)) { throw new CliException("Release has to be performed from the top-level project or using force option."); } @@ -94,14 +99,15 @@ private boolean warnIfFork(GitContext git, Path projectPath) { return false; } - private boolean isTopLevelProject(Path projectPath) { + private boolean isTopLevelProject(LocalToolCommandlet buildTool, Path projectPath) { - // returns false in case there's no pom.xml present or if parent directory has a pom.xml - return Files.exists(projectPath.resolve("pom.xml")) - && !Files.exists(projectPath.getParent().resolve("pom.xml")); + // top-level if a build descriptor is present here but not in the parent directory + Path parent = projectPath.getParent(); + return (buildTool.findBuildDescriptor(projectPath) != null) + && ((parent == null) || (buildTool.findBuildDescriptor(parent) == null)); } - private void buildAndDeploy(Mvn buildTool) { + private void buildAndDeploy(BuildTool buildTool) { while (true) { ProcessResult result = buildTool.buildAndDeploy(this.arguments.asList()); From 793a9ab908aaf505ff18fb378424caed9c308ee6 Mon Sep 17 00:00:00 2001 From: Laert Llaveshi Date: Tue, 11 Aug 2026 08:29:03 +0200 Subject: [PATCH 02/12] #2178: Add tests --- .../ide/commandlet/ReleaseCommandlet.java | 3 +-- .../ide/commandlet/BuildCommandletTest.java | 24 +++++++++++++++++++ .../ide/commandlet/ReleaseCommandletTest.java | 15 ++++++++++++ .../workspaces/main/gradle/build.gradle | 0 4 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 cli/src/test/resources/ide-projects/release/project/workspaces/main/gradle/build.gradle diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/ReleaseCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/ReleaseCommandlet.java index 9f94c0f9be..b73dff13d8 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/ReleaseCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/ReleaseCommandlet.java @@ -51,8 +51,7 @@ protected void doRun() { throw new CliException("Your local git repository has uncommitted changes. Please use 'git stash' and rerun on clean repo."); } if (warnIfFork(git, projectPath)) { - confirmWarning("You seem to work on a fork. Releases should be done on the original repository!\n" - + "We strongly recommend to abort and rerun on original repository."); + confirmWarning("You seem to work on a fork. Releases should be done on the original repository!\nWe strongly recommend to abort and rerun on original repository."); } if (!this.context.isForceMode() && !isTopLevelProject(commandlet, projectPath)) { throw new CliException("Release has to be performed from the top-level project or using force option."); diff --git a/cli/src/test/java/com/devonfw/tools/ide/commandlet/BuildCommandletTest.java b/cli/src/test/java/com/devonfw/tools/ide/commandlet/BuildCommandletTest.java index 9bda384fac..a817ca98b0 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/commandlet/BuildCommandletTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/commandlet/BuildCommandletTest.java @@ -2,6 +2,8 @@ import static org.junit.jupiter.api.Assertions.assertThrows; +import java.nio.file.Path; + import org.junit.jupiter.api.Test; import com.devonfw.tools.ide.cli.CliException; @@ -11,6 +13,10 @@ import com.devonfw.tools.ide.log.IdeLogLevel; import com.devonfw.tools.ide.os.SystemInfo; import com.devonfw.tools.ide.os.SystemInfoMock; +import com.devonfw.tools.ide.tool.gradle.Gradle; +import com.devonfw.tools.ide.tool.mvn.Mvn; +import com.devonfw.tools.ide.tool.npm.Npm; +import com.devonfw.tools.ide.tool.yarn.Yarn; import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; import com.github.tomakehurst.wiremock.junit5.WireMockTest; @@ -118,4 +124,22 @@ void testBuildWithNoBuildFile() { context.setCwd(context.getWorkspacePath().resolve("empty"), context.getWorkspacePath().toString(), context.getIdeHome()); assertThrows(CliException.class, buildCommandlet::run); } + + /** + * Tests {@link BuildCommandlet#findBuildCommandlet(com.devonfw.tools.ide.context.IdeContext, Path)} detecting the applicable build tool by its build + * descriptor and preferring {@link Yarn} over {@link Npm} when a {@code yarn.lock} is present. + */ + @Test + void testFindBuildCommandlet() { + + IdeTestContext context = newContext(PROJECT_BUILD); + Path workspace = context.getWorkspacePath(); + + assertThat(BuildCommandlet.findBuildCommandlet(context, workspace.resolve("mvn"))).isInstanceOf(Mvn.class); + assertThat(BuildCommandlet.findBuildCommandlet(context, workspace.resolve("gradle"))).isInstanceOf(Gradle.class); + assertThat(BuildCommandlet.findBuildCommandlet(context, workspace.resolve("npm"))).isInstanceOf(Npm.class); + // both npm and yarn match package.json, but yarn.lock is present so yarn must take precedence over npm + assertThat(BuildCommandlet.findBuildCommandlet(context, workspace.resolve("yarn"))).isInstanceOf(Yarn.class); + assertThat(BuildCommandlet.findBuildCommandlet(context, workspace.resolve("empty"))).isNull(); + } } diff --git a/cli/src/test/java/com/devonfw/tools/ide/commandlet/ReleaseCommandletTest.java b/cli/src/test/java/com/devonfw/tools/ide/commandlet/ReleaseCommandletTest.java index a17e2db7e0..6cc5f39c77 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/commandlet/ReleaseCommandletTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/commandlet/ReleaseCommandletTest.java @@ -93,4 +93,19 @@ void testReleaseWithoutBuildDescriptor() { assertThrows(CliException.class, releaseCommandlet::run); } + + /** + * Tests that the release fails gracefully if a build descriptor is found but its build tool does not support releasing (does not implement + * {@link com.devonfw.tools.ide.tool.BuildTool}), e.g. a gradle project (only maven currently supports releasing). + */ + @Test + void testReleaseWithUnsupportedBuildToolThrowsException() { + + IdeTestContext context = newReleaseContext(false); + context.setCwd(context.getWorkspacePath().resolve("gradle"), context.getWorkspacePath().toString(), context.getIdeHome()); + ReleaseCommandlet releaseCommandlet = context.getCommandletManager().getCommandlet(ReleaseCommandlet.class); + + CliException exception = assertThrows(CliException.class, releaseCommandlet::run); + assertThat(exception).hasMessageContaining("Could not find a supported build tool"); + } } diff --git a/cli/src/test/resources/ide-projects/release/project/workspaces/main/gradle/build.gradle b/cli/src/test/resources/ide-projects/release/project/workspaces/main/gradle/build.gradle new file mode 100644 index 0000000000..e69de29bb2 From 8b9e5a75ce08b9983b194944cae8cd2de88a9272 Mon Sep 17 00:00:00 2001 From: Laert Llaveshi Date: Tue, 11 Aug 2026 08:30:20 +0200 Subject: [PATCH 03/12] #2178: Add CHANGELOG.adoc --- CHANGELOG.adoc | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 9b633d56ba..28dfdd91b0 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -45,6 +45,7 @@ The full list of changes for this release can be found in https://github.com/dev Release with new features and bugfixes: +* https://github.com/devonfw/IDEasy/issues/2178[#2178]: Make ReleaseCommandlet independent of specific build commandlet * https://github.com/devonfw/IDEasy/issues/2197[#2197]: Fix broken python integration * https://github.com/devonfw/IDEasy/issues/2131[#2131]: Improve `ide upgrade --mode=` auto-completion * https://github.com/devonfw/IDEasy/issues/1870[#1870]: Add generic get-version implementation for global tools under windows From f6dd0196e9a292414864ffa38f8971147adaea51 Mon Sep 17 00:00:00 2001 From: Laert Llaveshi Date: Tue, 11 Aug 2026 08:38:38 +0200 Subject: [PATCH 04/12] #2178: Added line break due to CI build fail --- .../com/devonfw/tools/ide/commandlet/ReleaseCommandlet.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/ReleaseCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/ReleaseCommandlet.java index b73dff13d8..9f94c0f9be 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/ReleaseCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/ReleaseCommandlet.java @@ -51,7 +51,8 @@ protected void doRun() { throw new CliException("Your local git repository has uncommitted changes. Please use 'git stash' and rerun on clean repo."); } if (warnIfFork(git, projectPath)) { - confirmWarning("You seem to work on a fork. Releases should be done on the original repository!\nWe strongly recommend to abort and rerun on original repository."); + confirmWarning("You seem to work on a fork. Releases should be done on the original repository!\n" + + "We strongly recommend to abort and rerun on original repository."); } if (!this.context.isForceMode() && !isTopLevelProject(commandlet, projectPath)) { throw new CliException("Release has to be performed from the top-level project or using force option."); From 3be890e7efdf3cd3e2132ad098a21617e2fac317 Mon Sep 17 00:00:00 2001 From: Laert Llaveshi Date: Mon, 17 Aug 2026 09:19:17 +0200 Subject: [PATCH 05/12] #2178: Improve error messages --- .../com/devonfw/tools/ide/commandlet/ReleaseCommandlet.java | 5 ++++- .../devonfw/tools/ide/commandlet/ReleaseCommandletTest.java | 5 +++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/ReleaseCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/ReleaseCommandlet.java index 9f94c0f9be..e4c5b89104 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/ReleaseCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/ReleaseCommandlet.java @@ -43,8 +43,11 @@ protected void doRun() { GitContext git = this.context.getGitContext(); LocalToolCommandlet commandlet = BuildCommandlet.findBuildCommandlet(this.context, projectPath); + if (commandlet == null) { + throw new CliException("Could not find a build descriptor in " + projectPath + ". There is nothing to release here."); + } if (!(commandlet instanceof BuildTool buildTool)) { - throw new CliException("Could not find a supported build tool to release the project in " + projectPath + "."); + throw new CliException("The build tool " + commandlet.getName() + " detected in " + projectPath + " does not support releasing."); } if (git.hasUntrackedFiles(projectPath)) { diff --git a/cli/src/test/java/com/devonfw/tools/ide/commandlet/ReleaseCommandletTest.java b/cli/src/test/java/com/devonfw/tools/ide/commandlet/ReleaseCommandletTest.java index 6cc5f39c77..4905037d8a 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/commandlet/ReleaseCommandletTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/commandlet/ReleaseCommandletTest.java @@ -91,7 +91,8 @@ void testReleaseWithoutBuildDescriptor() { context.setCwd(context.getWorkspacePath().resolve("empty"), context.getWorkspacePath().toString(), context.getIdeHome()); ReleaseCommandlet releaseCommandlet = context.getCommandletManager().getCommandlet(ReleaseCommandlet.class); - assertThrows(CliException.class, releaseCommandlet::run); + CliException exception = assertThrows(CliException.class, releaseCommandlet::run); + assertThat(exception).hasMessageContaining("Could not find a build descriptor"); } /** @@ -106,6 +107,6 @@ void testReleaseWithUnsupportedBuildToolThrowsException() { ReleaseCommandlet releaseCommandlet = context.getCommandletManager().getCommandlet(ReleaseCommandlet.class); CliException exception = assertThrows(CliException.class, releaseCommandlet::run); - assertThat(exception).hasMessageContaining("Could not find a supported build tool"); + assertThat(exception).hasMessageContaining("gradle").hasMessageContaining("does not support releasing"); } } From 7db6236cfa4178a3311c340337a10c543c612c4c Mon Sep 17 00:00:00 2001 From: Laert Llaveshi Date: Thu, 3 Sep 2026 10:10:01 +0200 Subject: [PATCH 06/12] #2178: Update CHANGELOG.adoc --- CHANGELOG.adoc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 28dfdd91b0..ebc2986ae2 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -6,8 +6,10 @@ This file documents all notable changes to https://github.com/devonfw/IDEasy[IDE Release with new features and bugfixes: +* https://github.com/devonfw/IDEasy/issues/2178[#2178]: Make ReleaseCommandlet independent of specific build commandlet and fix `ide build` using npm instead of yarn * https://github.com/devonfw/IDEasy/issues/989[#989]: Allow expressions in template variable definitions + The full list of changes for this release can be found in https://github.com/devonfw/IDEasy/milestone/50?closed=1[milestone 2026.09.002]. == 2026.09.001 @@ -45,7 +47,6 @@ The full list of changes for this release can be found in https://github.com/dev Release with new features and bugfixes: -* https://github.com/devonfw/IDEasy/issues/2178[#2178]: Make ReleaseCommandlet independent of specific build commandlet * https://github.com/devonfw/IDEasy/issues/2197[#2197]: Fix broken python integration * https://github.com/devonfw/IDEasy/issues/2131[#2131]: Improve `ide upgrade --mode=` auto-completion * https://github.com/devonfw/IDEasy/issues/1870[#1870]: Add generic get-version implementation for global tools under windows From 5325462027939f676ba4fcb54808f4ac10c626b1 Mon Sep 17 00:00:00 2001 From: Laert Llaveshi Date: Thu, 3 Sep 2026 08:52:27 +0200 Subject: [PATCH 07/12] #2178: Add test for build tool priority in polyglot projects --- .../com/devonfw/tools/ide/commandlet/BuildCommandletTest.java | 2 ++ .../build/project/workspaces/main/mvn-and-npm/package.json | 0 .../build/project/workspaces/main/mvn-and-npm/pom.xml | 0 3 files changed, 2 insertions(+) create mode 100644 cli/src/test/resources/ide-projects/build/project/workspaces/main/mvn-and-npm/package.json create mode 100644 cli/src/test/resources/ide-projects/build/project/workspaces/main/mvn-and-npm/pom.xml diff --git a/cli/src/test/java/com/devonfw/tools/ide/commandlet/BuildCommandletTest.java b/cli/src/test/java/com/devonfw/tools/ide/commandlet/BuildCommandletTest.java index a817ca98b0..20c5ad8eb6 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/commandlet/BuildCommandletTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/commandlet/BuildCommandletTest.java @@ -140,6 +140,8 @@ void testFindBuildCommandlet() { assertThat(BuildCommandlet.findBuildCommandlet(context, workspace.resolve("npm"))).isInstanceOf(Npm.class); // both npm and yarn match package.json, but yarn.lock is present so yarn must take precedence over npm assertThat(BuildCommandlet.findBuildCommandlet(context, workspace.resolve("yarn"))).isInstanceOf(Yarn.class); + // a polyglot project must be built by the highest-priority tool, not the last one that matches + assertThat(BuildCommandlet.findBuildCommandlet(context, workspace.resolve("mvn-and-npm"))).isInstanceOf(Mvn.class); assertThat(BuildCommandlet.findBuildCommandlet(context, workspace.resolve("empty"))).isNull(); } } diff --git a/cli/src/test/resources/ide-projects/build/project/workspaces/main/mvn-and-npm/package.json b/cli/src/test/resources/ide-projects/build/project/workspaces/main/mvn-and-npm/package.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cli/src/test/resources/ide-projects/build/project/workspaces/main/mvn-and-npm/pom.xml b/cli/src/test/resources/ide-projects/build/project/workspaces/main/mvn-and-npm/pom.xml new file mode 100644 index 0000000000..e69de29bb2 From 0e51ecc94ce80f0064e4a797bda87d6d872f06e3 Mon Sep 17 00:00:00 2001 From: Laert Llaveshi Date: Thu, 3 Sep 2026 08:55:40 +0200 Subject: [PATCH 08/12] #2178: Rename buildTool to BuildCommandlet --- .../com/devonfw/tools/ide/commandlet/ReleaseCommandlet.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/ReleaseCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/ReleaseCommandlet.java index e4c5b89104..33504895c4 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/ReleaseCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/ReleaseCommandlet.java @@ -102,12 +102,12 @@ private boolean warnIfFork(GitContext git, Path projectPath) { return false; } - private boolean isTopLevelProject(LocalToolCommandlet buildTool, Path projectPath) { + private boolean isTopLevelProject(LocalToolCommandlet buildCommandlet, Path projectPath) { // top-level if a build descriptor is present here but not in the parent directory Path parent = projectPath.getParent(); - return (buildTool.findBuildDescriptor(projectPath) != null) - && ((parent == null) || (buildTool.findBuildDescriptor(parent) == null)); + return (buildCommandlet.findBuildDescriptor(projectPath) != null) + && ((parent == null) || (buildCommandlet.findBuildDescriptor(parent) == null)); } private void buildAndDeploy(BuildTool buildTool) { From dc44b9a4d690601f03ccfebf83111f60446fc321 Mon Sep 17 00:00:00 2001 From: Laert Llaveshi Date: Thu, 3 Sep 2026 09:03:15 +0200 Subject: [PATCH 09/12] #2178: Remove redundant build descriptor check in isTopLevelProject --- .../com/devonfw/tools/ide/commandlet/ReleaseCommandlet.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/ReleaseCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/ReleaseCommandlet.java index 33504895c4..fb2c48f359 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/ReleaseCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/ReleaseCommandlet.java @@ -104,10 +104,9 @@ private boolean warnIfFork(GitContext git, Path projectPath) { private boolean isTopLevelProject(LocalToolCommandlet buildCommandlet, Path projectPath) { - // top-level if a build descriptor is present here but not in the parent directory + // top-level if the build descriptor found here is not also present in the parent directory Path parent = projectPath.getParent(); - return (buildCommandlet.findBuildDescriptor(projectPath) != null) - && ((parent == null) || (buildCommandlet.findBuildDescriptor(parent) == null)); + return (parent == null) || (buildCommandlet.findBuildDescriptor(parent) == null); } private void buildAndDeploy(BuildTool buildTool) { From 7f17064794b42b6b5118d4c8df2f2e83f830b45d Mon Sep 17 00:00:00 2001 From: Laert Llaveshi Date: Thu, 3 Sep 2026 09:11:16 +0200 Subject: [PATCH 10/12] #2178: Return null from findBuildCommandlet for null path --- .../java/com/devonfw/tools/ide/commandlet/BuildCommandlet.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/BuildCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/BuildCommandlet.java index 46a082e3b6..705741c6f2 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/BuildCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/BuildCommandlet.java @@ -72,6 +72,9 @@ protected void doRun() { */ static LocalToolCommandlet findBuildCommandlet(IdeContext context, Path buildPath) { + if (buildPath == null) { + return null; + } for (Class toolClass : BUILD_TOOLS) { LocalToolCommandlet toolCommandlet = context.getCommandletManager().getCommandlet(toolClass); if (toolCommandlet.findBuildDescriptor(buildPath) != null) { From 632af8e18838773ca2c08ab2f120f6b5bc3d4fdb Mon Sep 17 00:00:00 2001 From: Laert Llaveshi Date: Thu, 3 Sep 2026 09:49:34 +0200 Subject: [PATCH 11/12] #2178: Move build tool detection to CommandletManager --- .../tools/ide/commandlet/BuildCommandlet.java | 30 +------------------ .../ide/commandlet/CommandletManager.java | 10 +++++++ .../ide/commandlet/CommandletManagerImpl.java | 20 +++++++++++++ .../ide/commandlet/ReleaseCommandlet.java | 2 +- .../ide/commandlet/BuildCommandletTest.java | 16 +++++----- 5 files changed, 40 insertions(+), 38 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/BuildCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/BuildCommandlet.java index 705741c6f2..981bfd2173 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/BuildCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/BuildCommandlet.java @@ -8,18 +8,12 @@ import com.devonfw.tools.ide.context.IdeContext; import com.devonfw.tools.ide.property.StringProperty; import com.devonfw.tools.ide.tool.LocalToolCommandlet; -import com.devonfw.tools.ide.tool.gradle.Gradle; -import com.devonfw.tools.ide.tool.mvn.Mvn; -import com.devonfw.tools.ide.tool.npm.Npm; -import com.devonfw.tools.ide.tool.yarn.Yarn; /** * Build tool {@link Commandlet} for automatically detecting build configuration files and running the respective tool. */ public class BuildCommandlet extends Commandlet { - private static final List> BUILD_TOOLS = List.of(Mvn.class, Gradle.class, Yarn.class, Npm.class); - /** The explicit build options to use (if empty use defaults). */ public final StringProperty arguments; @@ -50,7 +44,7 @@ protected void doRun() { throw new CliException("Missing current working directory!"); } - LocalToolCommandlet commandlet = findBuildCommandlet(this.context, buildPath); + LocalToolCommandlet commandlet = this.context.getCommandletManager().findBuildTool(buildPath); if (commandlet == null) { throw new CliException("Could not find build descriptor - no pom.xml, build.gradle, or package.json found!"); } @@ -62,28 +56,6 @@ protected void doRun() { commandlet.runTool(args); } - /** - * Detects the applicable build tool for the given {@code buildPath} by {@link LocalToolCommandlet#findBuildDescriptor(Path) querying} the available build - * commandlets (in order of priority) for a matching build descriptor (e.g. {@code pom.xml}, {@code build.gradle} or {@code package.json}). - * - * @param context the {@link IdeContext}. - * @param buildPath the {@link Path} to the directory to build. - * @return the applicable build {@link LocalToolCommandlet} or {@code null} if no build descriptor was found. - */ - static LocalToolCommandlet findBuildCommandlet(IdeContext context, Path buildPath) { - - if (buildPath == null) { - return null; - } - for (Class toolClass : BUILD_TOOLS) { - LocalToolCommandlet toolCommandlet = context.getCommandletManager().getCommandlet(toolClass); - if (toolCommandlet.findBuildDescriptor(buildPath) != null) { - return toolCommandlet; - } - } - return null; - } - private List getDefaultToolOptions(String buildOptionName) { String[] defaultToolOptions = this.context.getVariables().get(buildOptionName).split(" "); diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManager.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManager.java index 92105a33ea..dd6cc327f8 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManager.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManager.java @@ -1,5 +1,6 @@ package com.devonfw.tools.ide.commandlet; +import java.nio.file.Path; import java.util.Collection; import java.util.Iterator; @@ -106,4 +107,13 @@ default LocalToolCommandlet getRequiredLocalToolCommandlet(String name) { */ Iterator findCommandlet(CliArguments arguments, CompletionCandidateCollector collector); + /** + * Detects the applicable build tool for the given {@code buildPath} by {@link LocalToolCommandlet#findBuildDescriptor(Path) querying} the registered build + * commandlets (in order of priority) for a matching build descriptor (e.g. {@code pom.xml}, {@code build.gradle} or {@code package.json}). + * + * @param buildPath the {@link Path} to the directory to build. + * @return the applicable build {@link LocalToolCommandlet} or {@code null} if no build descriptor was found or {@code buildPath} was {@code null}. + */ + LocalToolCommandlet findBuildTool(Path buildPath); + } diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManagerImpl.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManagerImpl.java index f98a63decd..c42d462bb8 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManagerImpl.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManagerImpl.java @@ -1,5 +1,6 @@ package com.devonfw.tools.ide.commandlet; +import java.nio.file.Path; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -19,6 +20,7 @@ import com.devonfw.tools.ide.git.repository.RepositoryCommandlet; import com.devonfw.tools.ide.property.KeywordProperty; import com.devonfw.tools.ide.property.Property; +import com.devonfw.tools.ide.tool.LocalToolCommandlet; import com.devonfw.tools.ide.tool.androidstudio.AndroidStudio; import com.devonfw.tools.ide.tool.aws.Aws; import com.devonfw.tools.ide.tool.az.Azure; @@ -82,6 +84,9 @@ public class CommandletManagerImpl implements CommandletManager { private static final Logger LOG = LoggerFactory.getLogger(CommandletManagerImpl.class); + /** The build commandlets in order of priority - the first one with a matching build descriptor wins. */ + private static final List> BUILD_TOOLS = List.of(Mvn.class, Gradle.class, Yarn.class, Npm.class); + private final IdeContext context; private final Map, Commandlet> commandletTypeMap; @@ -279,6 +284,21 @@ public Iterator findCommandlet(CliArguments arguments, CompletionCan return new CommandletFinder(commandlet, arguments.copy(), collector); } + @Override + public LocalToolCommandlet findBuildTool(Path buildPath) { + + if (buildPath == null) { + return null; + } + for (Class toolClass : BUILD_TOOLS) { + LocalToolCommandlet toolCommandlet = getCommandlet(toolClass); + if (toolCommandlet.findBuildDescriptor(buildPath) != null) { + return toolCommandlet; + } + } + return null; + } + private final class CommandletFinder implements Iterator { private final Commandlet firstCandidate; diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/ReleaseCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/ReleaseCommandlet.java index fb2c48f359..2c14eff032 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/ReleaseCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/ReleaseCommandlet.java @@ -42,7 +42,7 @@ protected void doRun() { Path projectPath = this.context.getCwd(); GitContext git = this.context.getGitContext(); - LocalToolCommandlet commandlet = BuildCommandlet.findBuildCommandlet(this.context, projectPath); + LocalToolCommandlet commandlet = this.context.getCommandletManager().findBuildTool(projectPath); if (commandlet == null) { throw new CliException("Could not find a build descriptor in " + projectPath + ". There is nothing to release here."); } diff --git a/cli/src/test/java/com/devonfw/tools/ide/commandlet/BuildCommandletTest.java b/cli/src/test/java/com/devonfw/tools/ide/commandlet/BuildCommandletTest.java index 20c5ad8eb6..a6c5ae4962 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/commandlet/BuildCommandletTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/commandlet/BuildCommandletTest.java @@ -126,22 +126,22 @@ void testBuildWithNoBuildFile() { } /** - * Tests {@link BuildCommandlet#findBuildCommandlet(com.devonfw.tools.ide.context.IdeContext, Path)} detecting the applicable build tool by its build + * Tests {@link CommandletManager#findBuildTool(Path)} detecting the applicable build tool by its build * descriptor and preferring {@link Yarn} over {@link Npm} when a {@code yarn.lock} is present. */ @Test - void testFindBuildCommandlet() { + void testFindBuildTool() { IdeTestContext context = newContext(PROJECT_BUILD); Path workspace = context.getWorkspacePath(); - assertThat(BuildCommandlet.findBuildCommandlet(context, workspace.resolve("mvn"))).isInstanceOf(Mvn.class); - assertThat(BuildCommandlet.findBuildCommandlet(context, workspace.resolve("gradle"))).isInstanceOf(Gradle.class); - assertThat(BuildCommandlet.findBuildCommandlet(context, workspace.resolve("npm"))).isInstanceOf(Npm.class); + assertThat(context.getCommandletManager().findBuildTool(workspace.resolve("mvn"))).isInstanceOf(Mvn.class); + assertThat(context.getCommandletManager().findBuildTool(workspace.resolve("gradle"))).isInstanceOf(Gradle.class); + assertThat(context.getCommandletManager().findBuildTool(workspace.resolve("npm"))).isInstanceOf(Npm.class); // both npm and yarn match package.json, but yarn.lock is present so yarn must take precedence over npm - assertThat(BuildCommandlet.findBuildCommandlet(context, workspace.resolve("yarn"))).isInstanceOf(Yarn.class); + assertThat(context.getCommandletManager().findBuildTool(workspace.resolve("yarn"))).isInstanceOf(Yarn.class); // a polyglot project must be built by the highest-priority tool, not the last one that matches - assertThat(BuildCommandlet.findBuildCommandlet(context, workspace.resolve("mvn-and-npm"))).isInstanceOf(Mvn.class); - assertThat(BuildCommandlet.findBuildCommandlet(context, workspace.resolve("empty"))).isNull(); + assertThat(context.getCommandletManager().findBuildTool(workspace.resolve("mvn-and-npm"))).isInstanceOf(Mvn.class); + assertThat(context.getCommandletManager().findBuildTool(workspace.resolve("empty"))).isNull(); } } From df1ad010a685dac2a28be16baf01d8155e453ed2 Mon Sep 17 00:00:00 2001 From: Laert Llaveshi Date: Thu, 3 Sep 2026 10:12:56 +0200 Subject: [PATCH 12/12] #2178: Improve exception message --- CHANGELOG.adoc | 1 - .../java/com/devonfw/tools/ide/commandlet/BuildCommandlet.java | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index ebc2986ae2..35e56d7f07 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -9,7 +9,6 @@ Release with new features and bugfixes: * https://github.com/devonfw/IDEasy/issues/2178[#2178]: Make ReleaseCommandlet independent of specific build commandlet and fix `ide build` using npm instead of yarn * https://github.com/devonfw/IDEasy/issues/989[#989]: Allow expressions in template variable definitions - The full list of changes for this release can be found in https://github.com/devonfw/IDEasy/milestone/50?closed=1[milestone 2026.09.002]. == 2026.09.001 diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/BuildCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/BuildCommandlet.java index 981bfd2173..f47cf8f088 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/BuildCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/BuildCommandlet.java @@ -46,7 +46,7 @@ protected void doRun() { LocalToolCommandlet commandlet = this.context.getCommandletManager().findBuildTool(buildPath); if (commandlet == null) { - throw new CliException("Could not find build descriptor - no pom.xml, build.gradle, or package.json found!"); + throw new CliException("Could not find a build descriptor in " + buildPath + " - no supported build tool detected."); } List args = this.arguments.asList(); if (args.isEmpty()) {