Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,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));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
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 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.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.mvn.Mvn;

/**
* {@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 String REVISION_FLAG = "-Drevision=";

private static final String DEFAULT_MVN_RELEASE_OPTS = "clean deploy -Dchangelist= -Pdeploy";

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 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 force option.");
}

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

while (true) {
if (this.context.question("Is the next version '" + nextVersion + "' correct?")) {
break;
}
nextVersion = this.context.askForInput("Please enter the next version:");
}

setProjectVersion(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, "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) {

String user = System.getProperty("user.name");
for (String remote : git.retrieveGitRemotes(projectPath)) {
if (remote.contains("upstream") || ((user != null) && remote.contains("github.com/" + user))) {
return true;
}
}
return false;
}

private boolean isTopLevelProject(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"));
}

private Path getMavenConfig(Path projectPath) {

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 String getProjectVersion(Path projectPath) {

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 getNextVersion(String version) {

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(Path projectPath, String version) {

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

Mvn mvn = this.context.getCommandletManager().getCommandlet(Mvn.class);
List<String> args = new ArrayList<>(getReleaseOptions());
args.addAll(this.arguments.asList());
while (true) {
ProcessResult result = mvn.runTool(ProcessMode.DEFAULT, null, ProcessErrorHandling.NONE, args);
if (result.isSuccessful()) {
return;
}
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 process aborted!\nYou should reset your local commits via 'git reset HEAD^'.");
}
}
}

private List<String> getReleaseOptions() {

String options = this.context.getVariables().get("MVN_RELEASE_OPTS");
if ((options == null) || options.isBlank()) {
options = DEFAULT_MVN_RELEASE_OPTS;
}
return List.of(options.split("\\s+"));
}

private void confirmWarning(String message) {

LOG.warn(message);
this.context.askToContinue("Do you want to continue anyway?");
}

}
31 changes: 31 additions & 0 deletions cli/src/main/java/com/devonfw/tools/ide/git/GitContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -212,6 +213,12 @@ 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 configured git remotes (output of {@code git remote -v}).
*/
List<String> retrieveGitRemotes(Path repository);

/**
* Finds the {@link Path} to the git installation and throws an exception if there is none.
*
Expand Down Expand Up @@ -245,4 +252,28 @@ 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").
* @param message the annotation message of the tag.
*/
void tag(Path repository, String tagName, String message);

/**
* 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);
}
24 changes: 24 additions & 0 deletions cli/src/main/java/com/devonfw/tools/ide/git/GitContextImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,12 @@ public String retrieveGitUrl(Path repository) {
return runGitCommandAndGetSingleOutput("Failed to retrieve git URL for repository", repository, "config", "--get", "remote.origin.url");
}

@Override
public List<String> retrieveGitRemotes(Path repository) {

return runGitCommand(repository, ProcessMode.DEFAULT_CAPTURE, "--no-pager", "remote", "-v").getOut();
}

IdeContext getContext() {

return this.context;
Expand Down Expand Up @@ -526,6 +532,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, String message) {

runGitCommand(repository, "tag", "-a", tagName, "-m", message);
}

@Override
public void push(Path repository) {

runGitCommand(repository, "push", "--follow-tags");
}
}


3 changes: 3 additions & 0 deletions cli/src/main/resources/nls/Help.properties
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,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 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 <repository>'
cmd.repository.detail=Without further arguments this will set up all pre-configured git repositories.\nAlso, you can provide an explicit git repo as `<repository>` argument and IDEasy will automatically clone, build and set up your project based on the existing property file.\nRepositories are configured in 'settings/repository/<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.
Expand Down
3 changes: 3 additions & 0 deletions cli/src/main/resources/nls/Help_de.properties
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,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. 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 <repository>'.
cmd.repository.detail=Dies wird alle vorkonfigurierten Repositories einrichten. Rufen Sie einfach 'ide repository setup <your_project_name>' auf, ersetzen Sie <your_project_name> 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.
Expand Down
Loading
Loading