From a1ed3b04f6d6e833286f12aa06506b30532dae4e Mon Sep 17 00:00:00 2001 From: Laert Llaveshi Date: Mon, 15 Jun 2026 10:25:59 +0200 Subject: [PATCH 1/9] #1594: Preliminary simple implementation --- .../ide/commandlet/CommandletManagerImpl.java | 1 + .../ide/commandlet/ReleaseCommandlet.java | 109 ++++++++++++++++++ .../com/devonfw/tools/ide/git/GitContext.java | 23 ++++ .../devonfw/tools/ide/git/GitContextImpl.java | 18 +++ .../devonfw/tools/ide/git/GitContextMock.java | 15 +++ 5 files changed, 166 insertions(+) create mode 100644 cli/src/main/java/com/devonfw/tools/ide/commandlet/ReleaseCommandlet.java 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 19c53c7975..e36e30e65e 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 @@ -119,6 +119,7 @@ public CommandletManagerImpl(IdeContext context) { add(new UpgradeSettingsCommandlet(context)); add(new CreateCommandlet(context)); add(new BuildCommandlet(context)); + add(new ReleaseCommandlet(context)); add(new InstallPluginCommandlet(context)); add(new UninstallPluginCommandlet(context)); add(new UpgradeCommandlet(context)); 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 new file mode 100644 index 0000000000..899f5775df --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/ReleaseCommandlet.java @@ -0,0 +1,109 @@ +package com.devonfw.tools.ide.commandlet; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.devonfw.tools.ide.cli.CliException; +import com.devonfw.tools.ide.context.IdeContext; +import com.devonfw.tools.ide.git.GitContext; +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; + +/** + * {@link Commandlet} to perform a release of the current project. See #1594. + */ +public class ReleaseCommandlet extends Commandlet { + + private static final Logger LOG = LoggerFactory.getLogger(ReleaseCommandlet.class); + + private static final List> BUILD_TOOLS = List.of(Mvn.class, Gradle.class, Yarn.class, Npm.class); + + public final StringProperty arguments; + + public ReleaseCommandlet(IdeContext context) { + + super(context); + addKeyword(getName()); + this.arguments = add(new StringProperty("", false, true, "args")); + } + + @Override + public String getName() { + + return "release"; + } + + @Override + protected void doRun() { + + Path projectPath = this.context.getCwd(); + GitContext git = this.context.getGitContext(); + + if (git.hasUntrackedFiles(projectPath)) { + throw new CliException("Your project has uncommitted or untracked changes. Please commit or revert them before releasing."); + } + + String releaseVersion = this.context.askForInput("Enter the release version to publish (e.g. 1.5.0):"); + String nextVersion = getNextSnapshotVersion(releaseVersion); + this.context.askToContinue("About to release version {} and then continue development on {}. Proceed?", releaseVersion, nextVersion); + + setProjectVersion(projectPath, releaseVersion); + git.commit(projectPath, "release/" + releaseVersion); + git.tag(projectPath, "release/" + releaseVersion); + + buildAndDeploy(projectPath); + + setProjectVersion(projectPath, nextVersion); + git.commit(projectPath, "set next development version " + nextVersion); + git.push(projectPath); + + LOG.info("Successfully released version {}.", releaseVersion); + } + + private String getNextSnapshotVersion(String releaseVersion) { + + int lastDot = releaseVersion.lastIndexOf('.'); + String prefix = releaseVersion.substring(0, lastDot + 1); + int lastNumber = Integer.parseInt(releaseVersion.substring(lastDot + 1)); + return prefix + (lastNumber + 1) + "-SNAPSHOT"; + } + + private void setProjectVersion(Path projectPath, String version) { + + Mvn mvn = getCommandlet(Mvn.class); + mvn.runTool(List.of("versions:set", "-DnewVersion=" + version, "-DgenerateBackupPoms=false")); + } + + private void buildAndDeploy(Path projectPath) { + + for (Class toolClass : BUILD_TOOLS) { + LocalToolCommandlet tool = getCommandlet(toolClass); + if (tool.findBuildDescriptor(projectPath) != null) { + String releaseOptsVariable = tool.getName().toUpperCase(Locale.ROOT) + "_RELEASE_OPTS"; + List args = getReleaseOptions(releaseOptsVariable); + args.addAll(this.arguments.asList()); + tool.runTool(args); + return; + } + } + throw new CliException("Could not find a build descriptor - no pom.xml, build.gradle, or package.json found!"); + } + + private List getReleaseOptions(String releaseOptsVariable) { + + String value = this.context.getVariables().get(releaseOptsVariable); + if ((value == null) || value.isBlank()) { + return new ArrayList<>(); + } + return new ArrayList<>(List.of(value.split(" "))); + } +} diff --git a/cli/src/main/java/com/devonfw/tools/ide/git/GitContext.java b/cli/src/main/java/com/devonfw/tools/ide/git/GitContext.java index 00f0bb469b..48b495c88d 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/git/GitContext.java +++ b/cli/src/main/java/com/devonfw/tools/ide/git/GitContext.java @@ -245,4 +245,27 @@ default void reset(Path repository, String branch) { * @param trackedCommitIdPath the path to the file where the commit Id will be written. */ void saveCurrentCommitId(Path repository, Path trackedCommitIdPath); + + /** + * Commits all currently changed files (tracked) in the given repository. + * + * @param repository the {@link Path} to the git repository. + * @param message the commit message. + */ + void commit(Path repository, String message); + + /** + * Creates an annotated git tag in the given repository. + * + * @param repository the {@link Path} to the git repository. + * @param tagName the name of the tag to create (e.g. "release/1.5.0"). + */ + void tag(Path repository, String tagName); + + /** + * Pushes the local commits and tags of the given repository to the remote repository. + * + * @param repository the {@link Path} to the git repository. + */ + void push(Path repository); } diff --git a/cli/src/main/java/com/devonfw/tools/ide/git/GitContextImpl.java b/cli/src/main/java/com/devonfw/tools/ide/git/GitContextImpl.java index f950241845..745525546a 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/git/GitContextImpl.java +++ b/cli/src/main/java/com/devonfw/tools/ide/git/GitContextImpl.java @@ -526,6 +526,24 @@ public void saveCurrentCommitId(Path repository, Path trackedCommitIdPath) { protected String determineCurrentCommitId(Path repository) { return runGitCommandAndGetSingleOutput("Failed to get current commit id.", repository, "rev-parse", FILE_HEAD); } + + @Override + public void commit(Path repository, String message) { + + runGitCommand(repository, "commit", "-a", "-m", message); + } + + @Override + public void tag(Path repository, String tagName) { + + runGitCommand(repository, "tag", "-a", tagName, "-m", tagName); + } + + @Override + public void push(Path repository) { + + runGitCommand(repository, "push", "--follow-tags"); + } } diff --git a/cli/src/test/java/com/devonfw/tools/ide/git/GitContextMock.java b/cli/src/test/java/com/devonfw/tools/ide/git/GitContextMock.java index 9c4892976d..9a9c207464 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/git/GitContextMock.java +++ b/cli/src/test/java/com/devonfw/tools/ide/git/GitContextMock.java @@ -116,4 +116,19 @@ public String determineRemote(Path repository) { public void saveCurrentCommitId(Path repository, Path trackedCommitIdPath) { } + + @Override + public void commit(Path repository, String message) { + + } + + @Override + public void tag(Path repository, String tagName) { + + } + + @Override + public void push(Path repository) { + + } } From 5c96cf819ca211e71f13f70f39ce5e9bcbb80c63 Mon Sep 17 00:00:00 2001 From: Laert Llaveshi Date: Tue, 16 Jun 2026 12:38:25 +0200 Subject: [PATCH 2/9] Rebase: resolve CHANGELOG.adoc conflict. --- CHANGELOG.adoc | 1 + cli/src/main/resources/nls/Help.properties | 3 + cli/src/main/resources/nls/Help_de.properties | 3 + .../ide/commandlet/ReleaseCommandletTest.java | 90 +++++++++++++++++++ .../tools/ide/git/GitContextImplMock.java | 15 ++++ .../_ide/urls/java/java/17.0.10_7/urls | 1 + .../release/_ide/urls/mvn/mvn/3.9.6/urls | 1 + .../_ide/urls/mvn/mvn/dependencies.json | 14 +++ .../release/project/settings/ide.properties | 3 + .../release/project/settings/readme | 1 + .../project/workspaces/main/empty/.gitkeep | 0 .../project/workspaces/main/mvn/pom.xml | 0 .../repository/java/java/default/bin/java | 2 + .../repository/java/java/default/bin/java.cmd | 2 + .../repository/mvn/mvn/default/bin/mvn | 2 + 15 files changed, 138 insertions(+) create mode 100644 cli/src/test/java/com/devonfw/tools/ide/commandlet/ReleaseCommandletTest.java create mode 100644 cli/src/test/resources/ide-projects/release/_ide/urls/java/java/17.0.10_7/urls create mode 100644 cli/src/test/resources/ide-projects/release/_ide/urls/mvn/mvn/3.9.6/urls create mode 100644 cli/src/test/resources/ide-projects/release/_ide/urls/mvn/mvn/dependencies.json create mode 100644 cli/src/test/resources/ide-projects/release/project/settings/ide.properties create mode 100644 cli/src/test/resources/ide-projects/release/project/settings/readme create mode 100644 cli/src/test/resources/ide-projects/release/project/workspaces/main/empty/.gitkeep create mode 100644 cli/src/test/resources/ide-projects/release/project/workspaces/main/mvn/pom.xml create mode 100644 cli/src/test/resources/ide-projects/release/repository/java/java/default/bin/java create mode 100644 cli/src/test/resources/ide-projects/release/repository/java/java/default/bin/java.cmd create mode 100644 cli/src/test/resources/ide-projects/release/repository/mvn/mvn/default/bin/mvn diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index b95c6b8a9f..2ff6f06e50 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -16,6 +16,7 @@ Release with new features and bugfixes: * https://github.com/devonfw/IDEasy/issues/797[#797]: Fix VSCode install on macOS * https://github.com/devonfw/IDEasy/issues/1956[#1956]: Merging MAVEN_ARGS buggy * https://github.com/devonfw/IDEasy/issues/1978[#1978]: Enhanced the quality status documentation +* https://github.com/devonfw/IDEasy/issues/1594[#1594]: Add release commandlet * https://github.com/devonfw/IDEasy/issues/1946[#1946]: Add Spyder Python IDE * https://github.com/devonfw/IDEasy/issues/1846[#1846]: Fixed macOS Gatekeeper workaround * https://github.com/devonfw/IDEasy/issues/1906[#1906]: Fixed `ide uninstall` failing on macOS diff --git a/cli/src/main/resources/nls/Help.properties b/cli/src/main/resources/nls/Help.properties index c197f8cb0b..093a576378 100644 --- a/cli/src/main/resources/nls/Help.properties +++ b/cli/src/main/resources/nls/Help.properties @@ -111,6 +111,9 @@ cmd.python=Tool commandlet for Python. cmd.python.detail=Python is an object-oriented programming language, comparable to Perl, Ruby, Scheme, or Java. Detailed documentation can be found at https://www.python.org/doc/ cmd.quarkus=Tool commandlet for Quarkus (framework for cloud-native apps). cmd.quarkus.detail=Quarkus is a Kubernetes-native Java framework for building cloud-native applications. Detailed documentation can be found at https://quarkus.io/ +cmd.release=Performs a release of the project in your current working directory. +cmd.release.detail=The `release` commandlet automates a release of the project in your current working directory. It first ensures there are no uncommitted or untracked changes, then asks for the release version. It will set the release version, commit and tag it via git, build and deploy the project, then set the next development (SNAPSHOT) version, commit it and push the changes. Build and deploy options can be configured per build-system via the variable `_RELEASE_OPTS` (e.g. `MVN_RELEASE_OPTS`). You may also supply additional arguments as `ide release «args»` that will be passed to the detected build command. +cmd.release.val.args=Additional arguments to pass to the build and deploy command. cmd.repository=Set up pre-configured git repositories using 'ide repository setup ' cmd.repository.detail=Without further arguments this will set up all pre-configured git repositories.\nAlso, you can provide an explicit git repo as `` argument and IDEasy will automatically clone, build and set up your project based on the existing property file.\nRepositories are configured in 'settings/repository/.properties' and can therefore be shared with your project team for automatic or optional setup. cmd.repository.val.repository=The name of the properties file of the pre-configured git repository to set up, omit to set up all active repositories. diff --git a/cli/src/main/resources/nls/Help_de.properties b/cli/src/main/resources/nls/Help_de.properties index ffe1bd8459..89042bb56f 100644 --- a/cli/src/main/resources/nls/Help_de.properties +++ b/cli/src/main/resources/nls/Help_de.properties @@ -111,6 +111,9 @@ cmd.python=Werkzeug Kommando für Python. cmd.python.detail=Python ist eine objektorientierte Programmiersprache, vergleichbar mit Perl, Ruby, Scheme oder Java. Detaillierte Dokumentation ist zu finden unter https://www.python.org/doc/ cmd.quarkus=Werkzeug Kommando für Quarkus (Framework für Cloud-native Anwendungen). cmd.quarkus.detail=Quarkus ist ein Kubernetes-native Java-Framework zur Entwicklung von Cloud-native Anwendungen. Detaillierte Dokumentation ist zu finden unter https://quarkus.io/ +cmd.release=Führt ein Release des Projekts in Ihrem aktuellen Arbeitsverzeichnis durch. +cmd.release.detail=Der `release`-Befehl automatisiert ein Release des Projekts in Ihrem aktuellen Arbeitsverzeichnis. Zunächst wird sichergestellt, dass keine uncommitteten oder ungetrackten Änderungen vorliegen, anschließend wird nach der Release-Version gefragt. Er setzt die Release-Version, committet und taggt sie via git, baut und veröffentlicht das Projekt, setzt anschließend die nächste Entwicklungsversion (SNAPSHOT), committet diese und pusht die Änderungen. Build- und Deploy-Optionen können pro Build-System über die Variable `_RELEASE_OPTS` (z. B. `MVN_RELEASE_OPTS`) konfiguriert werden. Sie können auch zusätzliche Argumente wie `ide release «args»` angeben, die an den erkannten Build-Befehl weitergeleitet werden. +cmd.release.val.args=Zusätzliche Argumente, die an den Build- und Deploy-Befehl übergeben werden. cmd.repository=Richtet das vorkonfigurierte Git Repository ein mittels 'ide repository setup '. cmd.repository.detail=Dies wird alle vorkonfigurierten Repositories einrichten. Rufen Sie einfach 'ide repository setup ' auf, ersetzen Sie durch den Namen Ihrer Projektkonfigurationsdatei, die sich in 'settings/repository/your_project_name' befindet und IDEasy wird Ihr Projekt basierend auf der vorhandenen Eigenschaftsdatei automatisch klonen, bauen und einrichten.\nWenn Sie den Projektnamen weglassen, werden alle im Repository-Verzeichnis gefundenen Projekte vorkonfiguriert. cmd.repository.val.repository=Der Name der Properties-Datei des vorkonfigurierten Git Repositories zum Einrichten. Falls nicht angegeben, werden alle aktiven Projekte eingerichtet. 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 new file mode 100644 index 0000000000..1615834d4c --- /dev/null +++ b/cli/src/test/java/com/devonfw/tools/ide/commandlet/ReleaseCommandletTest.java @@ -0,0 +1,90 @@ +package com.devonfw.tools.ide.commandlet; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +import com.devonfw.tools.ide.cli.CliAbortException; +import com.devonfw.tools.ide.cli.CliException; +import com.devonfw.tools.ide.context.AbstractIdeContextTest; +import com.devonfw.tools.ide.context.IdeTestContext; +import com.devonfw.tools.ide.git.GitContextImplMock; +import com.devonfw.tools.ide.log.IdeLogEntry; + +/** + * Test of {@link ReleaseCommandlet}. + */ +class ReleaseCommandletTest extends AbstractIdeContextTest { + + private static final String PROJECT_RELEASE = "release"; + + private IdeTestContext newReleaseContext(boolean untrackedFiles) { + + IdeTestContext context = newContext(PROJECT_RELEASE); + GitContextImplMock gitContext = new GitContextImplMock(context, context.getWorkspacePath().resolve("mvn")); + gitContext.setSimulateUntrackedFiles(untrackedFiles); + context.setGitContext(gitContext); + context.setCwd(context.getWorkspacePath().resolve("mvn"), context.getWorkspacePath().toString(), context.getIdeHome()); + return context; + } + + @Test + void testReleaseWithUntrackedFilesThrowsException() { + + IdeTestContext context = newReleaseContext(true); + ReleaseCommandlet releaseCommandlet = context.getCommandletManager().getCommandlet(ReleaseCommandlet.class); + + assertThrows(CliException.class, releaseCommandlet::run); + } + + @Test + void testRelease() { + + IdeTestContext context = newReleaseContext(false); + context.setAnswers("1.0.0", "yes"); + ReleaseCommandlet releaseCommandlet = context.getCommandletManager().getCommandlet(ReleaseCommandlet.class); + + releaseCommandlet.run(); + + assertThat(context).log().hasEntries( + IdeLogEntry.ofInfo("mvn versions:set -DnewVersion=1.0.0 -DgenerateBackupPoms=false"), + IdeLogEntry.ofInfo("mvn clean deploy"), + IdeLogEntry.ofInfo("mvn versions:set -DnewVersion=1.0.1-SNAPSHOT -DgenerateBackupPoms=false"), + IdeLogEntry.ofInfo("Successfully released version 1.0.0.")); + } + + @Test + void testReleaseWithAdditionalArguments() { + + IdeTestContext context = newReleaseContext(false); + context.setAnswers("1.0.0", "yes"); + ReleaseCommandlet releaseCommandlet = context.getCommandletManager().getCommandlet(ReleaseCommandlet.class); + releaseCommandlet.arguments.addValue("-DskipTests"); + + releaseCommandlet.run(); + + assertThat(context).log().hasEntries( + IdeLogEntry.ofInfo("mvn clean deploy -DskipTests")); + } + + @Test + void testReleaseAbortedByUser() { + + IdeTestContext context = newReleaseContext(false); + context.setAnswers("1.0.0", "no"); + ReleaseCommandlet releaseCommandlet = context.getCommandletManager().getCommandlet(ReleaseCommandlet.class); + + assertThrows(CliAbortException.class, releaseCommandlet::run); + } + + @Test + void testReleaseWithoutBuildDescriptor() { + + IdeTestContext context = newReleaseContext(false); + context.setAnswers("1.0.0", "yes"); + context.setCwd(context.getWorkspacePath().resolve("empty"), context.getWorkspacePath().toString(), context.getIdeHome()); + ReleaseCommandlet releaseCommandlet = context.getCommandletManager().getCommandlet(ReleaseCommandlet.class); + + assertThrows(CliException.class, releaseCommandlet::run); + } +} diff --git a/cli/src/test/java/com/devonfw/tools/ide/git/GitContextImplMock.java b/cli/src/test/java/com/devonfw/tools/ide/git/GitContextImplMock.java index 64e5c614b3..ef0fc82bda 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/git/GitContextImplMock.java +++ b/cli/src/test/java/com/devonfw/tools/ide/git/GitContextImplMock.java @@ -216,4 +216,19 @@ private String findStashRefByMessageInList(String needle) { } return null; } + + @Override + public void commit(Path repository, String message) { + LOG.debug("Mock commit with message: {}", message); + } + + @Override + public void tag(Path repository, String tagName) { + LOG.debug("Mock tag: {}", tagName); + } + + @Override + public void push(Path repository) { + LOG.debug("Mock push"); + } } diff --git a/cli/src/test/resources/ide-projects/release/_ide/urls/java/java/17.0.10_7/urls b/cli/src/test/resources/ide-projects/release/_ide/urls/java/java/17.0.10_7/urls new file mode 100644 index 0000000000..62e6853f29 --- /dev/null +++ b/cli/src/test/resources/ide-projects/release/_ide/urls/java/java/17.0.10_7/urls @@ -0,0 +1 @@ +${testbaseurl}/download/java/java/17.0.10_7/java-17.0.10_7.tgz diff --git a/cli/src/test/resources/ide-projects/release/_ide/urls/mvn/mvn/3.9.6/urls b/cli/src/test/resources/ide-projects/release/_ide/urls/mvn/mvn/3.9.6/urls new file mode 100644 index 0000000000..69685961cf --- /dev/null +++ b/cli/src/test/resources/ide-projects/release/_ide/urls/mvn/mvn/3.9.6/urls @@ -0,0 +1 @@ +${testbaseurl}/download/mvn/mvn/3.9.6/mvn-3.9.6.tgz diff --git a/cli/src/test/resources/ide-projects/release/_ide/urls/mvn/mvn/dependencies.json b/cli/src/test/resources/ide-projects/release/_ide/urls/mvn/mvn/dependencies.json new file mode 100644 index 0000000000..cc895cfb18 --- /dev/null +++ b/cli/src/test/resources/ide-projects/release/_ide/urls/mvn/mvn/dependencies.json @@ -0,0 +1,14 @@ +{ + "[4.0,)": [ + { + "tool": "java", + "versionRange": "[17,)" + } + ], + "[3.0,4.0)": [ + { + "tool": "java", + "versionRange": "[8,)" + } + ] +} \ No newline at end of file diff --git a/cli/src/test/resources/ide-projects/release/project/settings/ide.properties b/cli/src/test/resources/ide-projects/release/project/settings/ide.properties new file mode 100644 index 0000000000..11a7893719 --- /dev/null +++ b/cli/src/test/resources/ide-projects/release/project/settings/ide.properties @@ -0,0 +1,3 @@ +JAVA_VERSION=17.0.10_7 +MAVEN_VERSION=3.9.6 +MVN_RELEASE_OPTS=clean deploy diff --git a/cli/src/test/resources/ide-projects/release/project/settings/readme b/cli/src/test/resources/ide-projects/release/project/settings/readme new file mode 100644 index 0000000000..e937d913f0 --- /dev/null +++ b/cli/src/test/resources/ide-projects/release/project/settings/readme @@ -0,0 +1 @@ +This folder represents the IDE settings (see https://github.com/devonfw/ide-settings) diff --git a/cli/src/test/resources/ide-projects/release/project/workspaces/main/empty/.gitkeep b/cli/src/test/resources/ide-projects/release/project/workspaces/main/empty/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cli/src/test/resources/ide-projects/release/project/workspaces/main/mvn/pom.xml b/cli/src/test/resources/ide-projects/release/project/workspaces/main/mvn/pom.xml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cli/src/test/resources/ide-projects/release/repository/java/java/default/bin/java b/cli/src/test/resources/ide-projects/release/repository/java/java/default/bin/java new file mode 100644 index 0000000000..bb96ab46f2 --- /dev/null +++ b/cli/src/test/resources/ide-projects/release/repository/java/java/default/bin/java @@ -0,0 +1,2 @@ +#!/bin/bash +echo java $* diff --git a/cli/src/test/resources/ide-projects/release/repository/java/java/default/bin/java.cmd b/cli/src/test/resources/ide-projects/release/repository/java/java/default/bin/java.cmd new file mode 100644 index 0000000000..bb96ab46f2 --- /dev/null +++ b/cli/src/test/resources/ide-projects/release/repository/java/java/default/bin/java.cmd @@ -0,0 +1,2 @@ +#!/bin/bash +echo java $* diff --git a/cli/src/test/resources/ide-projects/release/repository/mvn/mvn/default/bin/mvn b/cli/src/test/resources/ide-projects/release/repository/mvn/mvn/default/bin/mvn new file mode 100644 index 0000000000..6ff9465148 --- /dev/null +++ b/cli/src/test/resources/ide-projects/release/repository/mvn/mvn/default/bin/mvn @@ -0,0 +1,2 @@ +#!/bin/bash +echo "mvn $*" \ No newline at end of file From d998c2003567487f5863ee4e37fec6fc3e99b719 Mon Sep 17 00:00:00 2001 From: Laert Llaveshi Date: Tue, 16 Jun 2026 15:47:54 +0200 Subject: [PATCH 3/9] #1594: Test fix --- .../ide-projects/release/project/settings/ide.properties | 1 + 1 file changed, 1 insertion(+) diff --git a/cli/src/test/resources/ide-projects/release/project/settings/ide.properties b/cli/src/test/resources/ide-projects/release/project/settings/ide.properties index 11a7893719..d817a07839 100644 --- a/cli/src/test/resources/ide-projects/release/project/settings/ide.properties +++ b/cli/src/test/resources/ide-projects/release/project/settings/ide.properties @@ -1,3 +1,4 @@ JAVA_VERSION=17.0.10_7 MAVEN_VERSION=3.9.6 MVN_RELEASE_OPTS=clean deploy +CVE_MIN_SEVERITY=11 From e9beb7d8eb962d7a7c7dfeb90789ec46b6060df4 Mon Sep 17 00:00:00 2001 From: Laert Llaveshi Date: Wed, 17 Jun 2026 08:12:32 +0200 Subject: [PATCH 4/9] #1594: More elaborate implementation and close to original bash --- .../ide/commandlet/ReleaseCommandlet.java | 195 +++++++++++++++--- .../com/devonfw/tools/ide/git/GitContext.java | 13 ++ .../devonfw/tools/ide/git/GitContextImpl.java | 12 ++ cli/src/main/resources/nls/Help.properties | 2 +- cli/src/main/resources/nls/Help_de.properties | 2 +- .../ide/commandlet/ReleaseCommandletTest.java | 13 +- .../tools/ide/git/GitContextImplMock.java | 10 + .../devonfw/tools/ide/git/GitContextMock.java | 12 ++ .../repository/mvn/mvn/default/bin/mvn | 6 +- 9 files changed, 231 insertions(+), 34 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 899f5775df..190479eb35 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 @@ -11,6 +11,9 @@ import com.devonfw.tools.ide.cli.CliException; import com.devonfw.tools.ide.context.IdeContext; import com.devonfw.tools.ide.git.GitContext; +import com.devonfw.tools.ide.process.ProcessErrorHandling; +import com.devonfw.tools.ide.process.ProcessMode; +import com.devonfw.tools.ide.process.ProcessResult; import com.devonfw.tools.ide.property.StringProperty; import com.devonfw.tools.ide.tool.LocalToolCommandlet; import com.devonfw.tools.ide.tool.gradle.Gradle; @@ -19,7 +22,8 @@ import com.devonfw.tools.ide.tool.yarn.Yarn; /** - * {@link Commandlet} to perform a release of the current project. See #1594. + * {@link Commandlet} to build and deploy a release of the current project. See + * #1594. */ public class ReleaseCommandlet extends Commandlet { @@ -27,6 +31,8 @@ public class ReleaseCommandlet extends Commandlet { private static final List> BUILD_TOOLS = List.of(Mvn.class, Gradle.class, Yarn.class, Npm.class); + private static final String DEFAULT_MVN_RELEASE_OPTS = "clean deploy -Dchangelist= -Pdeploy"; + public final StringProperty arguments; public ReleaseCommandlet(IdeContext context) { @@ -49,61 +55,198 @@ protected void doRun() { GitContext git = this.context.getGitContext(); if (git.hasUntrackedFiles(projectPath)) { - throw new CliException("Your project has uncommitted or untracked changes. Please commit or revert them before releasing."); + throw new CliException("Your local git repository has uncommitted changes. Please use 'git stash' and rerun on a clean repository."); + } + warnIfFork(git, projectPath); + if (!this.context.isForceMode() && !isTopLevelProject(projectPath)) { + throw new CliException("Release has to be performed from the top-level project or using the force option (-f)."); } - String releaseVersion = this.context.askForInput("Enter the release version to publish (e.g. 1.5.0):"); - String nextVersion = getNextSnapshotVersion(releaseVersion); - this.context.askToContinue("About to release version {} and then continue development on {}. Proceed?", releaseVersion, nextVersion); + String currentVersion = getProjectVersion(); + LOG.info("Current version of your project is {}.", currentVersion); + boolean devVersion = isDevVersion(currentVersion); + String releaseVersion; + String nextVersion; + if (devVersion) { + LOG.info("Current version is {} so we assume the version is not tracked in your version control.", currentVersion); + LOG.info("Existing tags are:"); + for (String tag : git.retrieveGitTags(projectPath)) { + LOG.info(tag); + } + releaseVersion = this.context.askForInput("Please enter the release version:"); + nextVersion = currentVersion; + } else { + releaseVersion = currentVersion.replace("-SNAPSHOT", ""); + nextVersion = getNextVersion(releaseVersion); + } + if (currentVersion.equals(releaseVersion + "-SNAPSHOT")) { + nextVersion = nextVersion + "-SNAPSHOT"; + } + if (currentVersion.equals(releaseVersion)) { + confirmWarning("Current version is not a SNAPSHOT version!"); + } + nextVersion = confirmNextVersion(devVersion, currentVersion, releaseVersion, nextVersion); - setProjectVersion(projectPath, releaseVersion); - git.commit(projectPath, "release/" + releaseVersion); + if (!devVersion) { + setProjectVersion(releaseVersion); + git.commit(projectPath, "set release version to " + releaseVersion); + } git.tag(projectPath, "release/" + releaseVersion); - buildAndDeploy(projectPath); + buildAndDeploy(projectPath, devVersion ? releaseVersion : null); - setProjectVersion(projectPath, nextVersion); - git.commit(projectPath, "set next development version " + nextVersion); - git.push(projectPath); + if (devVersion) { + LOG.info("Local tag needs to be pushed now. You now have the chance to review these changes manually before they are pushed."); + } else { + setProjectVersion(nextVersion); + git.commit(projectPath, "set next version to " + nextVersion); + LOG.info("Local commits and tag need to be pushed now. You now have the chance to review these changes manually before they are pushed."); + } + this.context.askToContinue("Do you want to push the changes and the tag now?"); + git.push(projectPath); LOG.info("Successfully released version {}.", releaseVersion); } - private String getNextSnapshotVersion(String releaseVersion) { + private String confirmNextVersion(boolean devVersion, String currentVersion, String releaseVersion, String nextVersion) { + + while (true) { + LOG.info("Current version: {}", currentVersion); + LOG.info("Release version: {}", releaseVersion); + LOG.info("Next version: {}", nextVersion); + if (!nextVersion.endsWith("-SNAPSHOT")) { + LOG.warn("Next version is not a SNAPSHOT version!"); + } + if (devVersion) { + return nextVersion; + } + if (this.context.question("Is the next version correct?")) { + return nextVersion; + } + nextVersion = this.context.askForInput("Please enter the next version:"); + } + } + + private void warnIfFork(GitContext git, Path projectPath) { + + String user = System.getProperty("user.name"); + for (String remote : git.retrieveGitRemotes(projectPath)) { + if (remote.contains("upstream") || ((user != null) && remote.contains("github.com/" + user))) { + confirmWarning("You seem to work on a fork. Releases should be done on the original repository! We strongly recommend to abort and rerun on the original repository."); + return; + } + } + } + + private boolean isTopLevelProject(Path projectPath) { + + Path parent = projectPath.getParent(); + if (parent == null) { + return true; + } + for (Class toolClass : BUILD_TOOLS) { + if (getCommandlet(toolClass).findBuildDescriptor(parent) != null) { + return false; + } + } + return true; + } + + private void confirmWarning(String message) { + + LOG.warn(message); + this.context.askToContinue("Do you want to continue anyway?"); + } - int lastDot = releaseVersion.lastIndexOf('.'); - String prefix = releaseVersion.substring(0, lastDot + 1); - int lastNumber = Integer.parseInt(releaseVersion.substring(lastDot + 1)); - return prefix + (lastNumber + 1) + "-SNAPSHOT"; + private boolean isDevVersion(String version) { + + return "dev-SNAPSHOT".equals(version) || "0-SNAPSHOT".equals(version); + } + + private String getProjectVersion() { + + Mvn mvn = getCommandlet(Mvn.class); + ProcessResult result = mvn.runTool(ProcessMode.DEFAULT_CAPTURE, null, ProcessErrorHandling.NONE, + List.of("help:evaluate", "-Dexpression=project.version", "-q", "-DforceStdout")); + List out = result.getOut(); + if (!result.isSuccessful() || out.isEmpty()) { + throw new CliException("Failed to determine the current version of your project. You need to run 'mvn install' before."); + } + return out.get(out.size() - 1).trim(); } - private void setProjectVersion(Path projectPath, String version) { + private void setProjectVersion(String version) { Mvn mvn = getCommandlet(Mvn.class); mvn.runTool(List.of("versions:set", "-DnewVersion=" + version, "-DgenerateBackupPoms=false")); } - private void buildAndDeploy(Path projectPath) { + private void buildAndDeploy(Path projectPath, String revision) { + LocalToolCommandlet tool = null; for (Class toolClass : BUILD_TOOLS) { - LocalToolCommandlet tool = getCommandlet(toolClass); - if (tool.findBuildDescriptor(projectPath) != null) { - String releaseOptsVariable = tool.getName().toUpperCase(Locale.ROOT) + "_RELEASE_OPTS"; - List args = getReleaseOptions(releaseOptsVariable); - args.addAll(this.arguments.asList()); + LocalToolCommandlet candidate = getCommandlet(toolClass); + if (candidate.findBuildDescriptor(projectPath) != null) { + tool = candidate; + break; + } + } + if (tool == null) { + throw new CliException("Could not find a build descriptor - no pom.xml, build.gradle, or package.json found!"); + } + String releaseOptsVariable = tool.getName().toUpperCase(Locale.ROOT) + "_RELEASE_OPTS"; + while (true) { + List args = new ArrayList<>(); + if (revision != null) { + args.add("-Drevision=" + revision); + } + args.addAll(getReleaseOptions(releaseOptsVariable, tool)); + args.addAll(this.arguments.asList()); + try { tool.runTool(args); return; + } catch (CliException e) { + LOG.error("Release build failed!"); + if (!this.context.question("Do you want to retry the build (e.g. in case of a temporary network error)?")) { + throw new CliException("Release build failed and the process was aborted! You should reset your local commits via 'git reset HEAD^'."); + } } } - throw new CliException("Could not find a build descriptor - no pom.xml, build.gradle, or package.json found!"); } - private List getReleaseOptions(String releaseOptsVariable) { + private List getReleaseOptions(String releaseOptsVariable, LocalToolCommandlet tool) { String value = this.context.getVariables().get(releaseOptsVariable); if ((value == null) || value.isBlank()) { - return new ArrayList<>(); + if (tool instanceof Mvn) { + value = DEFAULT_MVN_RELEASE_OPTS; + } else { + return new ArrayList<>(); + } } return new ArrayList<>(List.of(value.split(" "))); } + + private String getNextVersion(String version) { + + int end = version.length(); + while ((end > 0) && !Character.isDigit(version.charAt(end - 1))) { + end--; + } + int start = end; + while ((start > 0) && Character.isDigit(version.charAt(start - 1))) { + start--; + } + if (start == end) { + return version; + } + String prefix = version.substring(0, start); + String number = version.substring(start, end); + String suffix = version.substring(end); + String next = String.valueOf(Long.parseLong(number) + 1); + if (number.length() > next.length()) { + next = number.substring(0, number.length() - next.length()) + next; + } + return prefix + next + suffix; + } } diff --git a/cli/src/main/java/com/devonfw/tools/ide/git/GitContext.java b/cli/src/main/java/com/devonfw/tools/ide/git/GitContext.java index 48b495c88d..6ec2a7fa45 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/git/GitContext.java +++ b/cli/src/main/java/com/devonfw/tools/ide/git/GitContext.java @@ -2,6 +2,7 @@ import java.nio.file.Files; import java.nio.file.Path; +import java.util.List; import com.devonfw.tools.ide.cli.CliException; import com.devonfw.tools.ide.cli.CliOfflineException; @@ -212,6 +213,18 @@ default void reset(Path repository, String branch) { */ String retrieveGitUrl(Path repository); + /** + * @param repository the {@link Path} to the git repository. + * @return the {@link List} of git tags of the given repository. + */ + List retrieveGitTags(Path repository); + + /** + * @param repository the {@link Path} to the git repository. + * @return the {@link List} of configured git remotes (output of {@code git remote -v}). + */ + List retrieveGitRemotes(Path repository); + /** * Finds the {@link Path} to the git installation and throws an exception if there is none. * diff --git a/cli/src/main/java/com/devonfw/tools/ide/git/GitContextImpl.java b/cli/src/main/java/com/devonfw/tools/ide/git/GitContextImpl.java index 745525546a..86dc0806dc 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/git/GitContextImpl.java +++ b/cli/src/main/java/com/devonfw/tools/ide/git/GitContextImpl.java @@ -320,6 +320,18 @@ public String retrieveGitUrl(Path repository) { return runGitCommandAndGetSingleOutput("Failed to retrieve git URL for repository", repository, "config", "--get", "remote.origin.url"); } + @Override + public List retrieveGitTags(Path repository) { + + return runGitCommand(repository, ProcessMode.DEFAULT_CAPTURE, "--no-pager", "tag", "--list").getOut(); + } + + @Override + public List retrieveGitRemotes(Path repository) { + + return runGitCommand(repository, ProcessMode.DEFAULT_CAPTURE, "--no-pager", "remote", "-v").getOut(); + } + IdeContext getContext() { return this.context; diff --git a/cli/src/main/resources/nls/Help.properties b/cli/src/main/resources/nls/Help.properties index 093a576378..bd96bf186c 100644 --- a/cli/src/main/resources/nls/Help.properties +++ b/cli/src/main/resources/nls/Help.properties @@ -112,7 +112,7 @@ cmd.python.detail=Python is an object-oriented programming language, comparable cmd.quarkus=Tool commandlet for Quarkus (framework for cloud-native apps). cmd.quarkus.detail=Quarkus is a Kubernetes-native Java framework for building cloud-native applications. Detailed documentation can be found at https://quarkus.io/ cmd.release=Performs a release of the project in your current working directory. -cmd.release.detail=The `release` commandlet automates a release of the project in your current working directory. It first ensures there are no uncommitted or untracked changes, then asks for the release version. It will set the release version, commit and tag it via git, build and deploy the project, then set the next development (SNAPSHOT) version, commit it and push the changes. Build and deploy options can be configured per build-system via the variable `_RELEASE_OPTS` (e.g. `MVN_RELEASE_OPTS`). You may also supply additional arguments as `ide release «args»` that will be passed to the detected build command. +cmd.release.detail=The `release` commandlet automates a release of the project in your current working directory. It ensures there are no uncommitted changes, warns if you work on a fork, determines the current version of your project and derives the release version and the next development (SNAPSHOT) version for you to confirm. It then sets the release version, commits and tags it via git (as `release/«version»`), builds and deploys the project, sets the next development version, commits, and (after your confirmation) pushes the changes and the tag. Build and deploy options can be configured per build-system via the variable `_RELEASE_OPTS` (e.g. `MVN_RELEASE_OPTS`). You may also supply additional arguments as `ide release «args»` that will be passed to the detected build command. cmd.release.val.args=Additional arguments to pass to the build and deploy command. cmd.repository=Set up pre-configured git repositories using 'ide repository setup ' cmd.repository.detail=Without further arguments this will set up all pre-configured git repositories.\nAlso, you can provide an explicit git repo as `` argument and IDEasy will automatically clone, build and set up your project based on the existing property file.\nRepositories are configured in 'settings/repository/.properties' and can therefore be shared with your project team for automatic or optional setup. diff --git a/cli/src/main/resources/nls/Help_de.properties b/cli/src/main/resources/nls/Help_de.properties index 89042bb56f..aebab617dd 100644 --- a/cli/src/main/resources/nls/Help_de.properties +++ b/cli/src/main/resources/nls/Help_de.properties @@ -112,7 +112,7 @@ cmd.python.detail=Python ist eine objektorientierte Programmiersprache, vergleic cmd.quarkus=Werkzeug Kommando für Quarkus (Framework für Cloud-native Anwendungen). cmd.quarkus.detail=Quarkus ist ein Kubernetes-native Java-Framework zur Entwicklung von Cloud-native Anwendungen. Detaillierte Dokumentation ist zu finden unter https://quarkus.io/ cmd.release=Führt ein Release des Projekts in Ihrem aktuellen Arbeitsverzeichnis durch. -cmd.release.detail=Der `release`-Befehl automatisiert ein Release des Projekts in Ihrem aktuellen Arbeitsverzeichnis. Zunächst wird sichergestellt, dass keine uncommitteten oder ungetrackten Änderungen vorliegen, anschließend wird nach der Release-Version gefragt. Er setzt die Release-Version, committet und taggt sie via git, baut und veröffentlicht das Projekt, setzt anschließend die nächste Entwicklungsversion (SNAPSHOT), committet diese und pusht die Änderungen. Build- und Deploy-Optionen können pro Build-System über die Variable `_RELEASE_OPTS` (z. B. `MVN_RELEASE_OPTS`) konfiguriert werden. Sie können auch zusätzliche Argumente wie `ide release «args»` angeben, die an den erkannten Build-Befehl weitergeleitet werden. +cmd.release.detail=Der `release`-Befehl automatisiert ein Release des Projekts in Ihrem aktuellen Arbeitsverzeichnis. Er stellt sicher, dass keine uncommitteten Änderungen vorliegen, warnt wenn Sie auf einem Fork arbeiten, ermittelt die aktuelle Version Ihres Projekts und leitet daraus die Release-Version sowie die nächste Entwicklungsversion (SNAPSHOT) zur Bestätigung ab. Anschließend setzt er die Release-Version, committet und taggt sie via git (als `release/«version»`), baut und veröffentlicht das Projekt, setzt die nächste Entwicklungsversion, committet und pusht nach Ihrer Bestätigung die Änderungen und den Tag. Build- und Deploy-Optionen können pro Build-System über die Variable `_RELEASE_OPTS` (z. B. `MVN_RELEASE_OPTS`) konfiguriert werden. Sie können auch zusätzliche Argumente wie `ide release «args»` angeben, die an den erkannten Build-Befehl weitergeleitet werden. cmd.release.val.args=Zusätzliche Argumente, die an den Build- und Deploy-Befehl übergeben werden. cmd.repository=Richtet das vorkonfigurierte Git Repository ein mittels 'ide repository setup '. cmd.repository.detail=Dies wird alle vorkonfigurierten Repositories einrichten. Rufen Sie einfach 'ide repository setup ' auf, ersetzen Sie durch den Namen Ihrer Projektkonfigurationsdatei, die sich in 'settings/repository/your_project_name' befindet und IDEasy wird Ihr Projekt basierend auf der vorhandenen Eigenschaftsdatei automatisch klonen, bauen und einrichten.\nWenn Sie den Projektnamen weglassen, werden alle im Repository-Verzeichnis gefundenen Projekte vorkonfiguriert. 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 1615834d4c..a2bebeb402 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 @@ -12,7 +12,8 @@ import com.devonfw.tools.ide.log.IdeLogEntry; /** - * Test of {@link ReleaseCommandlet}. + * Test of {@link ReleaseCommandlet}. The mocked {@code mvn} returns {@code 1.0.0-SNAPSHOT} for {@code help:evaluate} (see the test fixture), so the release + * version is {@code 1.0.0} and the next version is {@code 1.0.1-SNAPSHOT}. */ class ReleaseCommandletTest extends AbstractIdeContextTest { @@ -41,7 +42,8 @@ void testReleaseWithUntrackedFilesThrowsException() { void testRelease() { IdeTestContext context = newReleaseContext(false); - context.setAnswers("1.0.0", "yes"); + // answer 1: "Is the next version correct?", answer 2: "Do you want to push...?" + context.setAnswers("yes", "yes"); ReleaseCommandlet releaseCommandlet = context.getCommandletManager().getCommandlet(ReleaseCommandlet.class); releaseCommandlet.run(); @@ -57,7 +59,7 @@ void testRelease() { void testReleaseWithAdditionalArguments() { IdeTestContext context = newReleaseContext(false); - context.setAnswers("1.0.0", "yes"); + context.setAnswers("yes", "yes"); ReleaseCommandlet releaseCommandlet = context.getCommandletManager().getCommandlet(ReleaseCommandlet.class); releaseCommandlet.arguments.addValue("-DskipTests"); @@ -71,7 +73,8 @@ void testReleaseWithAdditionalArguments() { void testReleaseAbortedByUser() { IdeTestContext context = newReleaseContext(false); - context.setAnswers("1.0.0", "no"); + // answer 1: next version correct -> yes, answer 2: push -> no (aborts) + context.setAnswers("yes", "no"); ReleaseCommandlet releaseCommandlet = context.getCommandletManager().getCommandlet(ReleaseCommandlet.class); assertThrows(CliAbortException.class, releaseCommandlet::run); @@ -81,7 +84,7 @@ void testReleaseAbortedByUser() { void testReleaseWithoutBuildDescriptor() { IdeTestContext context = newReleaseContext(false); - context.setAnswers("1.0.0", "yes"); + context.setAnswers("yes", "yes"); context.setCwd(context.getWorkspacePath().resolve("empty"), context.getWorkspacePath().toString(), context.getIdeHome()); ReleaseCommandlet releaseCommandlet = context.getCommandletManager().getCommandlet(ReleaseCommandlet.class); diff --git a/cli/src/test/java/com/devonfw/tools/ide/git/GitContextImplMock.java b/cli/src/test/java/com/devonfw/tools/ide/git/GitContextImplMock.java index ef0fc82bda..bb2f44d358 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/git/GitContextImplMock.java +++ b/cli/src/test/java/com/devonfw/tools/ide/git/GitContextImplMock.java @@ -231,4 +231,14 @@ public void tag(Path repository, String tagName) { public void push(Path repository) { LOG.debug("Mock push"); } + + @Override + public List retrieveGitTags(Path repository) { + return List.of(); + } + + @Override + public List retrieveGitRemotes(Path repository) { + return List.of(); + } } diff --git a/cli/src/test/java/com/devonfw/tools/ide/git/GitContextMock.java b/cli/src/test/java/com/devonfw/tools/ide/git/GitContextMock.java index 9a9c207464..ac8ee3de15 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/git/GitContextMock.java +++ b/cli/src/test/java/com/devonfw/tools/ide/git/GitContextMock.java @@ -65,6 +65,18 @@ public String retrieveGitUrl(Path repository) { return MOCKED_URL_VALUE; } + @Override + public java.util.List retrieveGitTags(Path repository) { + + return java.util.Collections.emptyList(); + } + + @Override + public java.util.List retrieveGitRemotes(Path repository) { + + return java.util.Collections.emptyList(); + } + @Override public Path findGitRequired() { return Path.of("git"); diff --git a/cli/src/test/resources/ide-projects/release/repository/mvn/mvn/default/bin/mvn b/cli/src/test/resources/ide-projects/release/repository/mvn/mvn/default/bin/mvn index 6ff9465148..ee016d9304 100644 --- a/cli/src/test/resources/ide-projects/release/repository/mvn/mvn/default/bin/mvn +++ b/cli/src/test/resources/ide-projects/release/repository/mvn/mvn/default/bin/mvn @@ -1,2 +1,6 @@ #!/bin/bash -echo "mvn $*" \ No newline at end of file +if [ "$1" = "help:evaluate" ]; then + echo "1.0.0-SNAPSHOT" +else + echo "mvn $*" +fi From 8c487251d3ccd21e7fa80da2fab2dc4388e8bf90 Mon Sep 17 00:00:00 2001 From: Laert Llaveshi Date: Thu, 18 Jun 2026 10:23:54 +0200 Subject: [PATCH 5/9] #1594: Code cleanup and bug fixes --- .../ide/commandlet/ReleaseCommandlet.java | 231 ++++++------------ 1 file changed, 81 insertions(+), 150 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 190479eb35..53b2247598 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,9 +1,10 @@ package com.devonfw.tools.ide.commandlet; +import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; -import java.util.Locale; +import java.util.concurrent.locks.ReentrantLock; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -15,21 +16,16 @@ import com.devonfw.tools.ide.process.ProcessMode; import com.devonfw.tools.ide.process.ProcessResult; 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; /** - * {@link Commandlet} to build and deploy a release of the current project. See - * #1594. + * {@link Commandlet} to build and deploy a release of the current project. */ public class ReleaseCommandlet extends Commandlet { private static final Logger LOG = LoggerFactory.getLogger(ReleaseCommandlet.class); - private static final List> BUILD_TOOLS = List.of(Mvn.class, Gradle.class, Yarn.class, Npm.class); + private static final String REVISION_FLAG = "-Drevision="; private static final String DEFAULT_MVN_RELEASE_OPTS = "clean deploy -Dchangelist= -Pdeploy"; @@ -57,196 +53,131 @@ protected void doRun() { if (git.hasUntrackedFiles(projectPath)) { throw new CliException("Your local git repository has uncommitted changes. Please use 'git stash' and rerun on a clean repository."); } - warnIfFork(git, projectPath); + 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."); + } if (!this.context.isForceMode() && !isTopLevelProject(projectPath)) { throw new CliException("Release has to be performed from the top-level project or using the force option (-f)."); } - String currentVersion = getProjectVersion(); - LOG.info("Current version of your project is {}.", currentVersion); - boolean devVersion = isDevVersion(currentVersion); - String releaseVersion; - String nextVersion; - if (devVersion) { - LOG.info("Current version is {} so we assume the version is not tracked in your version control.", currentVersion); - LOG.info("Existing tags are:"); - for (String tag : git.retrieveGitTags(projectPath)) { - LOG.info(tag); - } - releaseVersion = this.context.askForInput("Please enter the release version:"); - nextVersion = currentVersion; - } else { - releaseVersion = currentVersion.replace("-SNAPSHOT", ""); - nextVersion = getNextVersion(releaseVersion); - } - if (currentVersion.equals(releaseVersion + "-SNAPSHOT")) { - nextVersion = nextVersion + "-SNAPSHOT"; - } - if (currentVersion.equals(releaseVersion)) { - confirmWarning("Current version is not a SNAPSHOT version!"); - } - nextVersion = confirmNextVersion(devVersion, currentVersion, releaseVersion, nextVersion); + String currentVersion = getProjectVersion(projectPath); + String releaseVersion = currentVersion.replace("-SNAPSHOT", ""); + String nextVersion = getNextVersion(releaseVersion) + "-SNAPSHOT"; + + LOG.info("Current version: {}", currentVersion); + LOG.info("Release version: {}", releaseVersion); + LOG.info("Next version: {}", nextVersion); - if (!devVersion) { - setProjectVersion(releaseVersion); - git.commit(projectPath, "set release version to " + releaseVersion); + while (true) { + if (this.context.question("Is the next version '" + nextVersion + "' correct?")) { + break; + } + nextVersion = this.context.askForInput("Please enter the next version:"); } - git.tag(projectPath, "release/" + releaseVersion); - buildAndDeploy(projectPath, devVersion ? releaseVersion : null); + setProjectVersion(projectPath, releaseVersion); + git.commit(projectPath, "#release: set release version " + releaseVersion); + git.tag(projectPath, releaseVersion); - if (devVersion) { - LOG.info("Local tag needs to be pushed now. You now have the chance to review these changes manually before they are pushed."); - } else { - setProjectVersion(nextVersion); - git.commit(projectPath, "set next version to " + nextVersion); - LOG.info("Local commits and tag need to be pushed now. You now have the chance to review these changes manually before they are pushed."); - } + buildAndDeploy(); - this.context.askToContinue("Do you want to push the changes and the tag now?"); + setProjectVersion(projectPath, nextVersion); + git.commit(projectPath, "#release: set next snapshot version " + nextVersion); git.push(projectPath); - LOG.info("Successfully released version {}.", releaseVersion); - } - private String confirmNextVersion(boolean devVersion, String currentVersion, String releaseVersion, String nextVersion) { - - while (true) { - LOG.info("Current version: {}", currentVersion); - LOG.info("Release version: {}", releaseVersion); - LOG.info("Next version: {}", nextVersion); - if (!nextVersion.endsWith("-SNAPSHOT")) { - LOG.warn("Next version is not a SNAPSHOT version!"); - } - if (devVersion) { - return nextVersion; - } - if (this.context.question("Is the next version correct?")) { - return nextVersion; - } - nextVersion = this.context.askForInput("Please enter the next version:"); - } } - private void warnIfFork(GitContext git, Path projectPath) { + private boolean warnIfFork(GitContext git, Path projectPath) { String user = System.getProperty("user.name"); for (String remote : git.retrieveGitRemotes(projectPath)) { if (remote.contains("upstream") || ((user != null) && remote.contains("github.com/" + user))) { - confirmWarning("You seem to work on a fork. Releases should be done on the original repository! We strongly recommend to abort and rerun on the original repository."); - return; + return true; } } + return false; } private boolean isTopLevelProject(Path projectPath) { - Path parent = projectPath.getParent(); - if (parent == null) { - return true; - } - for (Class toolClass : BUILD_TOOLS) { - if (getCommandlet(toolClass).findBuildDescriptor(parent) != null) { - return false; - } - } - return true; + // 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")); } - private void confirmWarning(String message) { + private Path getMavenConfig(Path projectPath) { - LOG.warn(message); - this.context.askToContinue("Do you want to continue anyway?"); + Path mavenConfig = projectPath.resolve(".mvn").resolve("maven.config"); + if (!Files.exists(mavenConfig)) { + throw new CliException("Could not find the maven configuration at " + mavenConfig); + } + return mavenConfig; } - private boolean isDevVersion(String version) { + private String getProjectVersion(Path projectPath) { - return "dev-SNAPSHOT".equals(version) || "0-SNAPSHOT".equals(version); + Path mavenConfig = getMavenConfig(projectPath); + String content = this.context.getFileAccess().readFileContent(mavenConfig); + for (String token : content.split("\\s+")) { + if (token.startsWith(REVISION_FLAG)) { + return token.substring(REVISION_FLAG.length()); + } + } + throw new CliException("Could not find '" + REVISION_FLAG + "' in " + mavenConfig); } - private String getProjectVersion() { + private String getNextVersion(String version) { - Mvn mvn = getCommandlet(Mvn.class); - ProcessResult result = mvn.runTool(ProcessMode.DEFAULT_CAPTURE, null, ProcessErrorHandling.NONE, - List.of("help:evaluate", "-Dexpression=project.version", "-q", "-DforceStdout")); - List out = result.getOut(); - if (!result.isSuccessful() || out.isEmpty()) { - throw new CliException("Failed to determine the current version of your project. You need to run 'mvn install' before."); - } - return out.get(out.size() - 1).trim(); + int lastDot = version.lastIndexOf('.'); + String prefix = version.substring(0, lastDot + 1); + String lastSegment = version.substring(lastDot + 1); + String incrementedSegment = String.valueOf(Long.parseLong(lastSegment) + 1); + incrementedSegment = "0".repeat(lastSegment.length() - incrementedSegment.length()) + incrementedSegment; + return prefix + incrementedSegment; } - private void setProjectVersion(String version) { + private void setProjectVersion(Path projectPath, String version) { - Mvn mvn = getCommandlet(Mvn.class); - mvn.runTool(List.of("versions:set", "-DnewVersion=" + version, "-DgenerateBackupPoms=false")); + Path mavenConfig = getMavenConfig(projectPath); + String content = this.context.getFileAccess().readFileContent(mavenConfig); + if (!content.contains(REVISION_FLAG)) { + throw new CliException("Could not find '" + REVISION_FLAG + "' in " + mavenConfig); + } + content = content.replaceAll(REVISION_FLAG + "\\S+", REVISION_FLAG + version); + this.context.getFileAccess().writeFileContent(content, mavenConfig); } - private void buildAndDeploy(Path projectPath, String revision) { + private void buildAndDeploy() { - LocalToolCommandlet tool = null; - for (Class toolClass : BUILD_TOOLS) { - LocalToolCommandlet candidate = getCommandlet(toolClass); - if (candidate.findBuildDescriptor(projectPath) != null) { - tool = candidate; - break; - } - } - if (tool == null) { - throw new CliException("Could not find a build descriptor - no pom.xml, build.gradle, or package.json found!"); - } - String releaseOptsVariable = tool.getName().toUpperCase(Locale.ROOT) + "_RELEASE_OPTS"; + Mvn mvn = this.context.getCommandletManager().getCommandlet(Mvn.class); + List args = new ArrayList<>(getReleaseOptions()); + args.addAll(this.arguments.asList()); while (true) { - List args = new ArrayList<>(); - if (revision != null) { - args.add("-Drevision=" + revision); - } - args.addAll(getReleaseOptions(releaseOptsVariable, tool)); - args.addAll(this.arguments.asList()); - try { - tool.runTool(args); + ProcessResult result = mvn.runTool(ProcessMode.DEFAULT, null, ProcessErrorHandling.NONE, args); + if (result.isSuccessful()) { return; - } catch (CliException e) { - LOG.error("Release build failed!"); - if (!this.context.question("Do you want to retry the build (e.g. in case of a temporary network error)?")) { - throw new CliException("Release build failed and the process was aborted! You should reset your local commits via 'git reset HEAD^'."); - } + } + LOG.error("Release build failed!"); + if (!this.context.question("Do you want to retry the release build?")) { + throw new CliException("Release build failed and was aborted! The release commit and tag were already created locally - " + + "undo them via 'git reset --hard HEAD~1' and 'git tag -d ' before retrying."); } } } - private List getReleaseOptions(String releaseOptsVariable, LocalToolCommandlet tool) { + private List getReleaseOptions() { - String value = this.context.getVariables().get(releaseOptsVariable); - if ((value == null) || value.isBlank()) { - if (tool instanceof Mvn) { - value = DEFAULT_MVN_RELEASE_OPTS; - } else { - return new ArrayList<>(); - } + String options = this.context.getVariables().get("MVN_RELEASE_OPTS"); + if ((options == null) || options.isBlank()) { + options = DEFAULT_MVN_RELEASE_OPTS; } - return new ArrayList<>(List.of(value.split(" "))); + return List.of(options.split("\\s+")); } - private String getNextVersion(String version) { + private void confirmWarning(String message) { - int end = version.length(); - while ((end > 0) && !Character.isDigit(version.charAt(end - 1))) { - end--; - } - int start = end; - while ((start > 0) && Character.isDigit(version.charAt(start - 1))) { - start--; - } - if (start == end) { - return version; - } - String prefix = version.substring(0, start); - String number = version.substring(start, end); - String suffix = version.substring(end); - String next = String.valueOf(Long.parseLong(number) + 1); - if (number.length() > next.length()) { - next = number.substring(0, number.length() - next.length()) + next; - } - return prefix + next + suffix; + LOG.warn(message); + this.context.askToContinue("Do you want to continue anyway?"); } + } From d9c69117d7f072f3f2177eef2cdac8a1f9dc7aad Mon Sep 17 00:00:00 2001 From: Laert Llaveshi Date: Thu, 25 Jun 2026 09:18:59 +0200 Subject: [PATCH 6/9] #1594: Code cleanup and bug fixes --- .../ide/commandlet/ReleaseCommandlet.java | 19 ++++++++++--------- .../com/devonfw/tools/ide/git/GitContext.java | 9 ++------- .../devonfw/tools/ide/git/GitContextImpl.java | 10 ++-------- cli/src/main/resources/nls/Help.properties | 2 +- cli/src/main/resources/nls/Help_de.properties | 2 +- .../tools/ide/git/GitContextImplMock.java | 7 +------ .../devonfw/tools/ide/git/GitContextMock.java | 8 +------- 7 files changed, 18 insertions(+), 39 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 53b2247598..2112962fb7 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 @@ -4,7 +4,6 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.locks.ReentrantLock; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -51,13 +50,13 @@ protected void doRun() { GitContext git = this.context.getGitContext(); if (git.hasUntrackedFiles(projectPath)) { - throw new CliException("Your local git repository has uncommitted changes. Please use 'git stash' and rerun on a clean repository."); + 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."); } if (!this.context.isForceMode() && !isTopLevelProject(projectPath)) { - throw new CliException("Release has to be performed from the top-level project or using the force option (-f)."); + throw new CliException("Release has to be performed from the top-level project or using force option."); } String currentVersion = getProjectVersion(projectPath); @@ -76,15 +75,18 @@ protected void doRun() { } setProjectVersion(projectPath, releaseVersion); - git.commit(projectPath, "#release: set release version " + releaseVersion); - git.tag(projectPath, releaseVersion); + git.commit(projectPath, "set release version to " + releaseVersion); + git.tag(projectPath, "release/" + releaseVersion, "tagged version " + releaseVersion); buildAndDeploy(); setProjectVersion(projectPath, nextVersion); - git.commit(projectPath, "#release: set next snapshot version " + nextVersion); + git.commit(projectPath, "set next version to " + nextVersion); + LOG.info("Local commits and tag need to be pushed now.\nYou now have the chance to review these changes manually before they are pushed."); + this.context.askToContinue("Do you want to continue?"); git.push(projectPath); + LOG.info("Successfully released version {}.", releaseVersion); } private boolean warnIfFork(GitContext git, Path projectPath) { @@ -158,9 +160,8 @@ private void buildAndDeploy() { return; } LOG.error("Release build failed!"); - if (!this.context.question("Do you want to retry the release build?")) { - throw new CliException("Release build failed and was aborted! The release commit and tag were already created locally - " - + "undo them via 'git reset --hard HEAD~1' and 'git tag -d ' before retrying."); + if (!this.context.question("Do you want to retry the build (e.g. in case of a temporary network error)?")) { + throw new CliException("Release build failed and process aborted!\nYou should reset your local commits via 'git reset HEAD^'."); } } } diff --git a/cli/src/main/java/com/devonfw/tools/ide/git/GitContext.java b/cli/src/main/java/com/devonfw/tools/ide/git/GitContext.java index 6ec2a7fa45..b6a3a7aa94 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/git/GitContext.java +++ b/cli/src/main/java/com/devonfw/tools/ide/git/GitContext.java @@ -213,12 +213,6 @@ default void reset(Path repository, String branch) { */ String retrieveGitUrl(Path repository); - /** - * @param repository the {@link Path} to the git repository. - * @return the {@link List} of git tags of the given repository. - */ - List retrieveGitTags(Path repository); - /** * @param repository the {@link Path} to the git repository. * @return the {@link List} of configured git remotes (output of {@code git remote -v}). @@ -272,8 +266,9 @@ default void reset(Path repository, String branch) { * * @param repository the {@link Path} to the git repository. * @param tagName the name of the tag to create (e.g. "release/1.5.0"). + * @param message the annotation message of the tag. */ - void tag(Path repository, String tagName); + void tag(Path repository, String tagName, String message); /** * Pushes the local commits and tags of the given repository to the remote repository. diff --git a/cli/src/main/java/com/devonfw/tools/ide/git/GitContextImpl.java b/cli/src/main/java/com/devonfw/tools/ide/git/GitContextImpl.java index 86dc0806dc..0c365aca50 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/git/GitContextImpl.java +++ b/cli/src/main/java/com/devonfw/tools/ide/git/GitContextImpl.java @@ -320,12 +320,6 @@ public String retrieveGitUrl(Path repository) { return runGitCommandAndGetSingleOutput("Failed to retrieve git URL for repository", repository, "config", "--get", "remote.origin.url"); } - @Override - public List retrieveGitTags(Path repository) { - - return runGitCommand(repository, ProcessMode.DEFAULT_CAPTURE, "--no-pager", "tag", "--list").getOut(); - } - @Override public List retrieveGitRemotes(Path repository) { @@ -546,9 +540,9 @@ public void commit(Path repository, String message) { } @Override - public void tag(Path repository, String tagName) { + public void tag(Path repository, String tagName, String message) { - runGitCommand(repository, "tag", "-a", tagName, "-m", tagName); + runGitCommand(repository, "tag", "-a", tagName, "-m", message); } @Override diff --git a/cli/src/main/resources/nls/Help.properties b/cli/src/main/resources/nls/Help.properties index bd96bf186c..e0b78c7111 100644 --- a/cli/src/main/resources/nls/Help.properties +++ b/cli/src/main/resources/nls/Help.properties @@ -112,7 +112,7 @@ cmd.python.detail=Python is an object-oriented programming language, comparable cmd.quarkus=Tool commandlet for Quarkus (framework for cloud-native apps). cmd.quarkus.detail=Quarkus is a Kubernetes-native Java framework for building cloud-native applications. Detailed documentation can be found at https://quarkus.io/ cmd.release=Performs a release of the project in your current working directory. -cmd.release.detail=The `release` commandlet automates a release of the project in your current working directory. It ensures there are no uncommitted changes, warns if you work on a fork, determines the current version of your project and derives the release version and the next development (SNAPSHOT) version for you to confirm. It then sets the release version, commits and tags it via git (as `release/«version»`), builds and deploys the project, sets the next development version, commits, and (after your confirmation) pushes the changes and the tag. Build and deploy options can be configured per build-system via the variable `_RELEASE_OPTS` (e.g. `MVN_RELEASE_OPTS`). You may also supply additional arguments as `ide release «args»` that will be passed to the detected build command. +cmd.release.detail=The `release` commandlet automates a release of the project in your current working directory. It ensures there are no uncommitted changes, warns if you work on a fork, determines the current version of your project and derives the release version and the next development (SNAPSHOT) version for you to confirm. It then sets the release version, commits and tags it via git (as `release/«version»`), builds and deploys the project, sets the next development version, commits, and (after your confirmation) pushes the changes and the tag. Build and deploy options can be configured via the variable `MVN_RELEASE_OPTS` (default `clean deploy -Dchangelist= -Pdeploy`). You may also supply additional arguments as `ide release «args»` that will be passed to the maven build command. cmd.release.val.args=Additional arguments to pass to the build and deploy command. cmd.repository=Set up pre-configured git repositories using 'ide repository setup ' cmd.repository.detail=Without further arguments this will set up all pre-configured git repositories.\nAlso, you can provide an explicit git repo as `` argument and IDEasy will automatically clone, build and set up your project based on the existing property file.\nRepositories are configured in 'settings/repository/.properties' and can therefore be shared with your project team for automatic or optional setup. diff --git a/cli/src/main/resources/nls/Help_de.properties b/cli/src/main/resources/nls/Help_de.properties index aebab617dd..d22bf66400 100644 --- a/cli/src/main/resources/nls/Help_de.properties +++ b/cli/src/main/resources/nls/Help_de.properties @@ -112,7 +112,7 @@ cmd.python.detail=Python ist eine objektorientierte Programmiersprache, vergleic cmd.quarkus=Werkzeug Kommando für Quarkus (Framework für Cloud-native Anwendungen). cmd.quarkus.detail=Quarkus ist ein Kubernetes-native Java-Framework zur Entwicklung von Cloud-native Anwendungen. Detaillierte Dokumentation ist zu finden unter https://quarkus.io/ cmd.release=Führt ein Release des Projekts in Ihrem aktuellen Arbeitsverzeichnis durch. -cmd.release.detail=Der `release`-Befehl automatisiert ein Release des Projekts in Ihrem aktuellen Arbeitsverzeichnis. Er stellt sicher, dass keine uncommitteten Änderungen vorliegen, warnt wenn Sie auf einem Fork arbeiten, ermittelt die aktuelle Version Ihres Projekts und leitet daraus die Release-Version sowie die nächste Entwicklungsversion (SNAPSHOT) zur Bestätigung ab. Anschließend setzt er die Release-Version, committet und taggt sie via git (als `release/«version»`), baut und veröffentlicht das Projekt, setzt die nächste Entwicklungsversion, committet und pusht nach Ihrer Bestätigung die Änderungen und den Tag. Build- und Deploy-Optionen können pro Build-System über die Variable `_RELEASE_OPTS` (z. B. `MVN_RELEASE_OPTS`) konfiguriert werden. Sie können auch zusätzliche Argumente wie `ide release «args»` angeben, die an den erkannten Build-Befehl weitergeleitet werden. +cmd.release.detail=Der `release`-Befehl automatisiert ein Release des Projekts in Ihrem aktuellen Arbeitsverzeichnis. Er stellt sicher, dass keine uncommitteten Änderungen vorliegen, warnt wenn Sie auf einem Fork arbeiten, ermittelt die aktuelle Version Ihres Projekts und leitet daraus die Release-Version sowie die nächste Entwicklungsversion (SNAPSHOT) zur Bestätigung ab. Anschließend setzt er die Release-Version, committet und taggt sie via git (als `release/«version»`), baut und veröffentlicht das Projekt, setzt die nächste Entwicklungsversion, committet und pusht nach Ihrer Bestätigung die Änderungen und den Tag. Build- und Deploy-Optionen können über die Variable `MVN_RELEASE_OPTS` (Standard `clean deploy -Dchangelist= -Pdeploy`) konfiguriert werden. Sie können auch zusätzliche Argumente wie `ide release «args»` angeben, die an den Maven-Build-Befehl weitergeleitet werden. cmd.release.val.args=Zusätzliche Argumente, die an den Build- und Deploy-Befehl übergeben werden. cmd.repository=Richtet das vorkonfigurierte Git Repository ein mittels 'ide repository setup '. cmd.repository.detail=Dies wird alle vorkonfigurierten Repositories einrichten. Rufen Sie einfach 'ide repository setup ' auf, ersetzen Sie durch den Namen Ihrer Projektkonfigurationsdatei, die sich in 'settings/repository/your_project_name' befindet und IDEasy wird Ihr Projekt basierend auf der vorhandenen Eigenschaftsdatei automatisch klonen, bauen und einrichten.\nWenn Sie den Projektnamen weglassen, werden alle im Repository-Verzeichnis gefundenen Projekte vorkonfiguriert. diff --git a/cli/src/test/java/com/devonfw/tools/ide/git/GitContextImplMock.java b/cli/src/test/java/com/devonfw/tools/ide/git/GitContextImplMock.java index bb2f44d358..4f527c86e7 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/git/GitContextImplMock.java +++ b/cli/src/test/java/com/devonfw/tools/ide/git/GitContextImplMock.java @@ -223,7 +223,7 @@ public void commit(Path repository, String message) { } @Override - public void tag(Path repository, String tagName) { + public void tag(Path repository, String tagName, String message) { LOG.debug("Mock tag: {}", tagName); } @@ -232,11 +232,6 @@ public void push(Path repository) { LOG.debug("Mock push"); } - @Override - public List retrieveGitTags(Path repository) { - return List.of(); - } - @Override public List retrieveGitRemotes(Path repository) { return List.of(); diff --git a/cli/src/test/java/com/devonfw/tools/ide/git/GitContextMock.java b/cli/src/test/java/com/devonfw/tools/ide/git/GitContextMock.java index ac8ee3de15..0e90d95a3a 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/git/GitContextMock.java +++ b/cli/src/test/java/com/devonfw/tools/ide/git/GitContextMock.java @@ -65,12 +65,6 @@ public String retrieveGitUrl(Path repository) { return MOCKED_URL_VALUE; } - @Override - public java.util.List retrieveGitTags(Path repository) { - - return java.util.Collections.emptyList(); - } - @Override public java.util.List retrieveGitRemotes(Path repository) { @@ -135,7 +129,7 @@ public void commit(Path repository, String message) { } @Override - public void tag(Path repository, String tagName) { + public void tag(Path repository, String tagName, String message) { } From cc97ae8e5245f0297dfd1505ff74acf84e7fbd66 Mon Sep 17 00:00:00 2001 From: Laert Llaveshi Date: Thu, 25 Jun 2026 09:40:26 +0200 Subject: [PATCH 7/9] #1594: Fix tests --- .../tools/ide/commandlet/ReleaseCommandletTest.java | 11 +++++++---- .../release/repository/mvn/mvn/default/bin/mvn | 6 +----- 2 files changed, 8 insertions(+), 9 deletions(-) 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 a2bebeb402..a17e2db7e0 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 @@ -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.CliAbortException; @@ -12,8 +14,8 @@ import com.devonfw.tools.ide.log.IdeLogEntry; /** - * Test of {@link ReleaseCommandlet}. The mocked {@code mvn} returns {@code 1.0.0-SNAPSHOT} for {@code help:evaluate} (see the test fixture), so the release - * version is {@code 1.0.0} and the next version is {@code 1.0.1-SNAPSHOT}. + * Test of {@link ReleaseCommandlet}. The test fixture's {@code .mvn/maven.config} declares {@code -Drevision=1.0.0-SNAPSHOT}, so the release version is + * {@code 1.0.0} and the next version is {@code 1.0.1-SNAPSHOT}. */ class ReleaseCommandletTest extends AbstractIdeContextTest { @@ -49,10 +51,11 @@ void testRelease() { releaseCommandlet.run(); assertThat(context).log().hasEntries( - IdeLogEntry.ofInfo("mvn versions:set -DnewVersion=1.0.0 -DgenerateBackupPoms=false"), IdeLogEntry.ofInfo("mvn clean deploy"), - IdeLogEntry.ofInfo("mvn versions:set -DnewVersion=1.0.1-SNAPSHOT -DgenerateBackupPoms=false"), IdeLogEntry.ofInfo("Successfully released version 1.0.0.")); + // the maven.config must be left at the next development version + Path mavenConfig = context.getWorkspacePath().resolve("mvn").resolve(".mvn").resolve("maven.config"); + assertThat(context.getFileAccess().readFileContent(mavenConfig)).contains("-Drevision=1.0.1-SNAPSHOT"); } @Test diff --git a/cli/src/test/resources/ide-projects/release/repository/mvn/mvn/default/bin/mvn b/cli/src/test/resources/ide-projects/release/repository/mvn/mvn/default/bin/mvn index ee016d9304..1b4a9986e2 100644 --- a/cli/src/test/resources/ide-projects/release/repository/mvn/mvn/default/bin/mvn +++ b/cli/src/test/resources/ide-projects/release/repository/mvn/mvn/default/bin/mvn @@ -1,6 +1,2 @@ #!/bin/bash -if [ "$1" = "help:evaluate" ]; then - echo "1.0.0-SNAPSHOT" -else - echo "mvn $*" -fi +echo "mvn $*" From 015b25de056514a1e061fcebc8ebd8ffebe411a3 Mon Sep 17 00:00:00 2001 From: Laert Llaveshi Date: Thu, 25 Jun 2026 09:49:26 +0200 Subject: [PATCH 8/9] #1594: Add missing fixture --- .../release/project/workspaces/main/mvn/.mvn/maven.config | 1 + 1 file changed, 1 insertion(+) create mode 100644 cli/src/test/resources/ide-projects/release/project/workspaces/main/mvn/.mvn/maven.config diff --git a/cli/src/test/resources/ide-projects/release/project/workspaces/main/mvn/.mvn/maven.config b/cli/src/test/resources/ide-projects/release/project/workspaces/main/mvn/.mvn/maven.config new file mode 100644 index 0000000000..009da89161 --- /dev/null +++ b/cli/src/test/resources/ide-projects/release/project/workspaces/main/mvn/.mvn/maven.config @@ -0,0 +1 @@ +-Drevision=1.0.0-SNAPSHOT From 32dd6feb94c89bc39ec444b6fd94f3806aef6c37 Mon Sep 17 00:00:00 2001 From: Laert Llaveshi Date: Thu, 25 Jun 2026 10:07:03 +0200 Subject: [PATCH 9/9] #1594: Fix executable bit on release fixture mocks --- .../ide-projects/release/repository/java/java/default/bin/java | 0 .../ide-projects/release/repository/mvn/mvn/default/bin/mvn | 0 2 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 cli/src/test/resources/ide-projects/release/repository/java/java/default/bin/java mode change 100644 => 100755 cli/src/test/resources/ide-projects/release/repository/mvn/mvn/default/bin/mvn diff --git a/cli/src/test/resources/ide-projects/release/repository/java/java/default/bin/java b/cli/src/test/resources/ide-projects/release/repository/java/java/default/bin/java old mode 100644 new mode 100755 diff --git a/cli/src/test/resources/ide-projects/release/repository/mvn/mvn/default/bin/mvn b/cli/src/test/resources/ide-projects/release/repository/mvn/mvn/default/bin/mvn old mode 100644 new mode 100755