Migrate release-tools to spring-data-release.

Closes #219.
This commit is contained in:
Greg L. Turnquist
2022-08-30 09:18:07 -05:00
parent 46ba255055
commit 4443a9c31c
206 changed files with 0 additions and 22870 deletions

View File

@@ -1,149 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<context version="7.2.2.230">
<scope type="Project" name="spring-data-release-cli">
<workspace>
<element type="JavaRootDirectory" name="src/main/java">
<reference name="Project|spring-data-release-cli::BuildUnit|spring-data-release-cli"/>
</element>
<element type="JavaRootDirectory" name="target/classes">
<reference name="Project|spring-data-release-cli::BuildUnit|spring-data-release-cli"/>
</element>
</workspace>
<physical>
<element type="BuildUnit" name="spring-data-release-cli"/>
</physical>
</scope>
<scope type="External" name="External">
<element type="TypeFilter" name="Filter">
<element type="IncludeTypePattern" name="**"/>
<element type="ExcludeTypePattern" name="java.io.*"/>
<element type="ExcludeTypePattern" name="java.lang.*"/>
<element type="ExcludeTypePattern" name="java.lang.annotation.*"/>
<element type="ExcludeTypePattern" name="java.lang.ref.*"/>
<element type="ExcludeTypePattern" name="java.math.*"/>
<element type="ExcludeTypePattern" name="java.nio.*"/>
<element type="ExcludeTypePattern" name="java.nio.channels.*"/>
<element type="ExcludeTypePattern" name="java.nio.channels.spi.*"/>
<element type="ExcludeTypePattern" name="java.nio.charset.*"/>
<element type="ExcludeTypePattern" name="java.nio.charset.spi.*"/>
<element type="ExcludeTypePattern" name="java.text.*"/>
<element type="ExcludeTypePattern" name="java.util.*"/>
<element type="ExcludeTypePattern" name="java.util.jar.*"/>
<element type="ExcludeTypePattern" name="java.util.prefs.*"/>
<element type="ExcludeTypePattern" name="java.util.regex.*"/>
<element type="ExcludeTypePattern" name="java.util.zip.*"/>
</element>
</scope>
<scope type="Global" name="Global">
<element type="Configuration" name="Configuration"/>
<element type="TypeFilter" name="Filter">
<element type="IncludeTypePattern" name="**"/>
</element>
<itemDefinition>
<item type="ThresholdItem">
<property value="NumberOfNotAssignedTypes" name="AttributeId"/>
<property value="" name="LowerValue"/>
<property value="Project" name="NamedElementGroup"/>
<property value="0" name="UpperValue"/>
</item>
<item type="ThresholdItem">
<property value="NumberOfParameters" name="AttributeId"/>
<property value="" name="LowerValue"/>
<property value="Method" name="NamedElementGroup"/>
<property value="7" name="UpperValue"/>
</item>
<item type="ThresholdItem">
<property value="NumberOfAccessibleTypes" name="AttributeId"/>
<property value="" name="LowerValue"/>
<property value="Namespace" name="NamedElementGroup"/>
<property value="30" name="UpperValue"/>
</item>
<item type="ThresholdItem">
<property value="NormalizedCumulativeComponentDependency" name="AttributeId"/>
<property value="" name="LowerValue"/>
<property value="Project" name="NamedElementGroup"/>
<property value="7" name="UpperValue"/>
</item>
<item type="ThresholdItem">
<property value="LinesOfCode" name="AttributeId"/>
<property value="" name="LowerValue"/>
<property value="SourceFile" name="NamedElementGroup"/>
<property value="700" name="UpperValue"/>
</item>
<item type="ThresholdItem">
<property value="NumberOfInternalTypes" name="AttributeId"/>
<property value="" name="LowerValue"/>
<property value="Namespace" name="NamedElementGroup"/>
<property value="50" name="UpperValue"/>
</item>
<item type="ThresholdItem">
<property value="NumberOfMethods" name="AttributeId"/>
<property value="" name="LowerValue"/>
<property value="Type" name="NamedElementGroup"/>
<property value="50" name="UpperValue"/>
</item>
<item type="ThresholdItem">
<property value="ModifiedCyclomaticComplexity" name="AttributeId"/>
<property value="" name="LowerValue"/>
<property value="Method" name="NamedElementGroup"/>
<property value="20" name="UpperValue"/>
</item>
</itemDefinition>
</scope>
<attributeConfiguration type="AttributeConfiguration">
<property value="disabled" name="NumberOfWorkspaceWarnings"/>
<property value="disabled" name="DistributedOverPathContainers"/>
<property value="disabled" name="MaxAccumulatedExtendedCyclomaticComplexityPerType"/>
<property value="disabled" name="CyclomaticComplexity"/>
<property value="disabled" name="NumberOfCyclicProjects"/>
<property value="disabled" name="NumberOfTypesWithImplementation"/>
<property value="disabled" name="NumberOfNamedInterfaces"/>
<property value="disabled" name="NumberOfCyclicSubsystems"/>
<property value="disabled" name="AverageAccumulatedCyclomaticComplexityPerType"/>
<property value="disabled" name="NumberOfAbstractTypes"/>
<property value="disabled" name="NormalizedCumulativeTypeDependency"/>
<property value="disabled" name="NumberOfVerticalSlices"/>
<property value="disabled" name="NumberOfTasks"/>
<property value="disabled" name="NumberOfWarnings"/>
<property value="disabled" name="NumberOfRefactorings"/>
<property value="disabled" name="AverageExtendedCyclomaticComplexityPerMethod"/>
<property value="disabled" name="NumberOfCyclicNamespaces"/>
<property value="disabled" name="MaxExtendedCyclomaticComplexityPerMethod"/>
<property value="disabled" name="Level"/>
<property value="disabled" name="NumberOfVerticalSliceGroups"/>
<property value="disabled" name="NumberOfNameParts"/>
<property value="disabled" name="NumberOfCyclicVerticalSliceGroups"/>
<property value="disabled" name="AccumulatedExtendedCyclomaticComplexity"/>
<property value="disabled" name="NumberOfCyclicLayerGroups"/>
<property value="disabled" name="NumberOfLayerGroups"/>
<property value="disabled" name="NumberOfAssertions"/>
<property value="disabled" name="NumberOfDuplicateCodeBlocksWarnings"/>
<property value="disabled" name="AverageAccumulatedExtendedCyclomaticComplexityPerType"/>
<property value="disabled" name="ExtendedCyclomaticComplexity"/>
<property value="disabled" name="NumberOfLayers"/>
<property value="disabled" name="DependsUpon"/>
<property value="disabled" name="NumberOfMethodsWithImplementation"/>
<property value="disabled" name="NumberOfCyclicVerticalSlices"/>
<property value="disabled" name="MaxAccumulatedCyclomaticComplexityPerType"/>
<property value="disabled" name="AccumulatedCyclomaticComplexity"/>
<property value="disabled" name="MaxCyclomaticComplexityPerMethod"/>
<property value="disabled" name="JdkVersion"/>
<property value="disabled" name="NumberOfSubsystems"/>
<property value="disabled" name="NumberOfCyclicDirectories"/>
<property value="disabled" name="NumberOfIgnoredWarnings"/>
<property value="disabled" name="AverageAssertionUsage"/>
<property value="disabled" name="AverageTypeDependency"/>
<property value="disabled" name="RelativeAverageTypeDependency"/>
<property value="disabled" name="AverageCyclomaticComplexityPerMethod"/>
<property value="disabled" name="NumberOfExcludedSourceFiles"/>
<property value="disabled" name="NumberOfCyclicWarnings"/>
<property value="disabled" name="Timestamp"/>
<property value="disabled" name="NumberOfExcludedTargetFiles"/>
<property value="disabled" name="NumberOfIgnoredViolations"/>
<property value="disabled" name="NumberOfCyclicLayers"/>
<property value="disabled" name="NumberOfCyclicSourceFiles"/>
<property value="disabled" name="NumberOfMetricWarnings"/>
<property value="disabled" name="NumberOfAbstractMethods"/>
<property value="disabled" name="NumberOfFixWarnings"/>
</attributeConfiguration>
</context>

View File

@@ -1,32 +0,0 @@
# Git
git.username=
git.author=
git.email=
git.password=
github.api.url=https://api.github.com
# IO
# Optionally set the JavaHome path used for exeuting maven goals (eg. /Library/Java/JavaVirtualMachines/jdk1.8.0.jdk/Contents/Home/)
# If the property does not exist, is empty or points to an invalid directory the OS default JavaHome will be used.
#io.javaHome=
# Maven
maven.mavenHome=
maven.console-logger=false
maven.parallelize=true
# Deployment
# Must be always the encrypted password taken from the Artifactory GUI/Profile view
deployment.username=
deployment.password=
deployment.api-key=
# GPG
gpg.keyname=
gpg.password=
gpg.executable=/usr/local/bin/gpg2
# A GitHub token with user:email, read:user and read:org scopes.
# User needs to be part of the Spring team on GitHub as well.
sagan.key=

View File

@@ -1,2 +0,0 @@
lombok.nonNull.exceptionType = IllegalArgumentException
lombok.anyConstructor.addConstructorProperties = true

View File

@@ -1,163 +0,0 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.data.build</groupId>
<artifactId>spring-data-release-cli</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.4.RELEASE</version>
</parent>
<properties>
<spring-plugin.version>2.0.0.RELEASE</spring-plugin.version>
<lombok.version>1.18.18</lombok.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-commons</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-parameter-names</artifactId>
</dependency>
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.10.8</version>
</dependency>
<dependency>
<groupId>org.springframework.shell</groupId>
<artifactId>spring-shell</artifactId>
<version>1.2.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.xmlbeam</groupId>
<artifactId>xmlprojector</artifactId>
<version>1.4.7</version>
</dependency>
<dependency>
<groupId>com.googlecode.plist</groupId>
<artifactId>dd-plist</artifactId>
<version>1.23</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-exec</artifactId>
<version>1.3</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.plugin</groupId>
<artifactId>spring-plugin-core</artifactId>
<version>${spring-plugin.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jgit</groupId>
<artifactId>org.eclipse.jgit</artifactId>
<version>5.6.0.201912101111-r</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.apache.maven.shared</groupId>
<artifactId>maven-invoker</artifactId>
<version>3.0.1</version>
</dependency>
<dependency>
<groupId>org.jgrapht</groupId>
<artifactId>jgrapht-core</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock</artifactId>
<version>2.26.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>29.0-jre</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<finalName>${project.name}</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@@ -1,99 +0,0 @@
## General Notes
* Use the command `help` to get a list of all commands in the release tools.
* After fixing a problem use `workspace cleanup` to cleanup any mess left behind by the previous step.
## One Time Setup
### Infrastructure requirements
- Ensure you have the credentials for `buildmaster` accounts on https://repo.spring.io.
- Ensure yoiu have the credentials for https://oss.sonatype.org (to deploy and promote GA and service releases, need deployment permissions for `org.springframework.data`) in `settings.xml` for server with id `sonatype`.
Both are available in the Spring/Pivotal Last Pass repository.
### Prepare local configuration and credentials
Add an `application-local.properties` to the project root and add the following properties:
- `git.username` - Your GitHub username.
- `git.password` - Your GitHub Password (or API key with scopes: `public_repo, read:org, repo:status, repo_deployment, user` when using 2FA).
- `git.author` - Your full name (used for preparing commits).
- `git.email` - Your email (used for preparing commits).
- `maven.mavenHome` - Pointing to the location of your Maven installation.
- `deployment.username` - Your Artifactory user.
- `deployment.api-key` - The Artifactory API key to use for artifact promotion.
- `deployment.password` - The encrypted Artifactory password..
- `gpg.keyname` - The GPG key name.
- `gpg.password` - The password of your GPG key.
- `gpg.executable` - Path to your GPG executable, typically `/usr/local/MacGPG2/bin/gpg2`
or `/usr/local/bin/gpg`.
- `sagan.key` - Sagan authentication token. Must be a valid GitHub token. Can be the same
as `git.password` when using a GitHub token as password.
After that, run the `verify` command (`$ verify`) to verify your settings (authentication,
correct Maven, Java, and GPG setup).
See `application-local.template` for details.
## The release process
| Action | Command |
|--------|---------|
| Build and execute the release shell | `mvn package && java -jar target/spring-data-release-cli.jar` |
| | *All following commands are run in the release shell* |
| **Pre-release checks** | |
| Ensure all work on CVEs potentially contained in the release is done (incl. backports etc.) | N.A. |
| Upgrade dependencies in Spring Data Build parent pom (mind minor/major version rules) | N.A. |
| All release tickets are present | `$ tracker releasetickets $trainIteration` |
| Review open tickets for release | N.A. |
| Self-assign release tickets | `$ tracker prepare $trainIteration` |
| Announce release preparations to mailing list (https://groups.google.com/forum/#!forum/spring-data-dev) | N.A. |
| **Release the binaries** ||
| | `$ release prepare $trainIteration` |
| Build the artefacts and push them to the apropriate maven repository | `$ release build $trainIteration` |
| |`$ release conclude $trainIteration` |
| Push the created commits to GitHub |`$ github push $trainIteration` |
| Push new maintanance branches if the release version was a GA release (`X.Y.0` version)|`$ git push $trainIteration.next`|
| Distribute documentation and static resources from tag |`$ release distribute $trainIteration`|
| **Post-release tasks** ||
|Close JIRA tickets and GitHub release tickets.|`$ tracker close $trainIteration`|
|Create new release versions and tickets for upcoming version|`$ tracker setup-next $trainIteration.next`|
| Update versions in Sagan. `$targets` is given as comma separated lists of code names, without spaces. E.g. `Moore,Neumann` | `$ sagan update $releasetrains`|
| Create list of docs for release announcements | `$ announcement $trainIteration`|
| Announce release (Blog, Twitter) and notify downstream dependency projects as needed. | N.A. |
### Utilities
#### GitHub Labels
`ProjectLabelConfiguration` contains a per-project configuration which labels should be present in a project. To apply that configuration (create or update), use:
```
$ github update labels $project
```
#### Dependency Upgrade
`ProjectDependencies` contains a per-project configuration of dependencies.
Workflow:
* Check for dependency upgrades `$ dependency check $trainIteration`
Reports upgradable dependencies for Build and Modules and
creates `dependency-upgrade-build.properties` file.
Edit `dependency-upgrade-build.properties` to specify the dependency version to upgrade.
Removing a line will omit that dependency upgrade.
* Apply dependency upgrade with `$ dependency upgrade $trainIteration`. Applies dependency
upgrades currently only to Spring Data Build.
* Report store-specific dependencies to Spring Boot's current upgrade
ticket ([sample](https://github.com/spring-projects/spring-boot/issues/24036)) `$ dependency report $trainIteration`
#### CI Properties Distribution
To distribute `ci/pipeline.properties` across all modules use:
`$ infra distribute ci-properties $trainIteration`

View File

@@ -1,44 +0,0 @@
/*
* Copyright 2015-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release;
import java.util.logging.Logger;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.shell.support.logging.HandlerUtils;
/**
* @author Oliver Gierke
*/
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication application = new SpringApplication(Application.class);
application.setAdditionalProfiles("local");
try {
BootShim bs = new BootShim(args, application.run(args));
bs.run();
} catch (RuntimeException e) {
throw e;
} finally {
HandlerUtils.flushAllHandlers(Logger.getLogger(""));
}
}
}

View File

@@ -1,113 +0,0 @@
package org.springframework.data.release;
import java.io.IOException;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.ClassPathBeanDefinitionScanner;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.shell.CommandLine;
import org.springframework.shell.ShellException;
import org.springframework.shell.SimpleShellCommandLineOptions;
import org.springframework.shell.core.ExitShellRequest;
import org.springframework.shell.core.JLineShellComponent;
import org.springframework.util.StopWatch;
public class BootShim {
private static StopWatch sw = new StopWatch("Spring Shell");
private static CommandLine commandLine;
private ConfigurableApplicationContext ctx;
public BootShim(String[] args, ConfigurableApplicationContext context) {
this.ctx = context;
try {
commandLine = SimpleShellCommandLineOptions.parseCommandLine(args);
} catch (IOException var5) {
throw new ShellException(var5.getMessage(), var5);
}
this.configureApplicationContext(this.ctx);
ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner((BeanDefinitionRegistry) this.ctx);
if (commandLine.getDisableInternalCommands()) {
scanner.scan(new String[] { "org.springframework.shell.converters", "org.springframework.shell.plugin.support" });
} else {
scanner.scan(new String[] { "org.springframework.shell.commands", "org.springframework.shell.converters",
"org.springframework.shell.plugin.support" });
}
}
public ApplicationContext getApplicationContext() {
return this.ctx;
}
private void configureApplicationContext(ConfigurableApplicationContext annctx) {
this.createAndRegisterBeanDefinition(annctx, CustomShellComponent.class, "shell");
annctx.getBeanFactory().registerSingleton("commandLine", commandLine);
}
protected void createAndRegisterBeanDefinition(GenericApplicationContext annctx, Class<?> clazz) {
this.createAndRegisterBeanDefinition(annctx, clazz, (String) null);
}
protected void createAndRegisterBeanDefinition(ConfigurableApplicationContext annctx, Class<?> clazz, String name) {
RootBeanDefinition rbd = new RootBeanDefinition();
rbd.setBeanClass(clazz);
DefaultListableBeanFactory bf = (DefaultListableBeanFactory) annctx.getBeanFactory();
if (name != null) {
bf.registerBeanDefinition(name, rbd);
} else {
bf.registerBeanDefinition(clazz.getSimpleName(), rbd);
}
}
public ExitShellRequest run() {
sw.start();
String[] commandsToExecuteAndThenQuit = commandLine.getShellCommandsToExecute();
JLineShellComponent shell = (JLineShellComponent) this.ctx.getBean("shell", JLineShellComponent.class);
ExitShellRequest exitShellRequest;
if (null != commandsToExecuteAndThenQuit) {
boolean successful = false;
exitShellRequest = ExitShellRequest.FATAL_EXIT;
String[] arr$ = commandsToExecuteAndThenQuit;
int len$ = commandsToExecuteAndThenQuit.length;
for (int i$ = 0; i$ < len$; ++i$) {
String cmd = arr$[i$];
successful = shell.executeCommand(cmd).isSuccess();
if (!successful) {
break;
}
}
if (successful) {
exitShellRequest = ExitShellRequest.NORMAL_EXIT;
}
} else {
shell.start();
shell.promptLoop();
exitShellRequest = shell.getExitShellRequest();
if (exitShellRequest == null) {
exitShellRequest = ExitShellRequest.NORMAL_EXIT;
}
shell.waitForComplete();
}
sw.stop();
if (shell.isDevelopmentMode()) {
System.out.println("Total execution time: " + sw.getLastTaskTimeMillis() + " ms");
}
return exitShellRequest;
}
public JLineShellComponent getJLineShellComponent() {
return (JLineShellComponent) this.ctx.getBean("shell", JLineShellComponent.class);
}
}

View File

@@ -1,33 +0,0 @@
/*
* Copyright 2015-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.stereotype.Component;
/**
* @author Oliver Gierke
*/
@Component
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface CliComponent {
}

View File

@@ -1,112 +0,0 @@
/*
* Copyright 2016-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release;
import java.lang.reflect.Method;
import java.util.logging.Logger;
import org.springframework.shell.core.ExecutionProcessor;
import org.springframework.shell.core.ExecutionStrategy;
import org.springframework.shell.core.JLineShellComponent;
import org.springframework.shell.core.SimpleExecutionStrategy;
import org.springframework.shell.event.ParseResult;
import org.springframework.shell.support.logging.HandlerUtils;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
/**
* Extension of {@link JLineShellComponent} to customize the {@link ExecutionStrategy} to one that can deal with package
* protected command classes.
*
* @author Oliver Gierke
* @see https://github.com/spring-projects/spring-shell/pull/93
*/
class CustomShellComponent extends JLineShellComponent {
private final ExecutionStrategy executionStrategy = new CustomExecutionStrategy();
/*
* (non-Javadoc)
* @see org.springframework.shell.core.JLineShellComponent#getExecutionStrategy()
*/
@Override
protected ExecutionStrategy getExecutionStrategy() {
return executionStrategy;
}
/**
* Effectively a copy of {@link SimpleExecutionStrategy} but with the tweaks provided in PR 93 for Spring shell to
* enable execution of package protected command classes.
*
* @author Oliver Gierke
* @see https://github.com/spring-projects/spring-shell/pull/93
*/
static class CustomExecutionStrategy implements ExecutionStrategy {
private static final Logger logger = HandlerUtils.getLogger(SimpleExecutionStrategy.class);
private final Class<?> mutex = SimpleExecutionStrategy.class;
public Object execute(ParseResult parseResult) throws RuntimeException {
Assert.notNull(parseResult, "Parse result required");
synchronized (mutex) {
Assert.isTrue(isReadyForCommands(), "SimpleExecutionStrategy not yet ready for commands");
Object target = parseResult.getInstance();
if (target instanceof ExecutionProcessor) {
ExecutionProcessor processor = ((ExecutionProcessor) target);
parseResult = processor.beforeInvocation(parseResult);
try {
Object result = invoke(parseResult);
processor.afterReturningInvocation(parseResult, result);
return result;
} catch (Throwable th) {
processor.afterThrowingInvocation(parseResult, th);
return handleThrowable(th);
}
} else {
return invoke(parseResult);
}
}
}
private Object invoke(ParseResult parseResult) {
try {
Method method = parseResult.getMethod();
ReflectionUtils.makeAccessible(method);
return ReflectionUtils.invokeMethod(method, parseResult.getInstance(), parseResult.getArguments());
} catch (Throwable th) {
logger.severe("Command failed");
return handleThrowable(th);
}
}
private Object handleThrowable(Throwable th) {
if (th instanceof Error) {
throw ((Error) th);
}
if (th instanceof RuntimeException) {
throw ((RuntimeException) th);
}
throw new RuntimeException(th);
}
public boolean isReadyForCommands() {
return true;
}
public void terminate() {}
}
}

View File

@@ -1,68 +0,0 @@
/*
* Copyright 2016-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release;
import org.springframework.shell.core.CommandMarker;
import org.springframework.shell.core.ExecutionProcessor;
import org.springframework.shell.event.ParseResult;
import org.springframework.util.StopWatch;
/**
* Base class for command implementations who want to get their execution time logged.
*
* @author Oliver Gierke
*/
public abstract class TimedCommand implements ExecutionProcessor, CommandMarker {
private StopWatch watch;
/*
* (non-Javadoc)
* @see org.springframework.shell.core.ExecutionProcessor#beforeInvocation(org.springframework.shell.event.ParseResult)
*/
@Override
public ParseResult beforeInvocation(ParseResult invocationContext) {
watch = new StopWatch();
watch.start();
return invocationContext;
}
/*
* (non-Javadoc)
* @see org.springframework.shell.core.ExecutionProcessor#afterReturningInvocation(org.springframework.shell.event.ParseResult, java.lang.Object)
*/
@Override
public void afterReturningInvocation(ParseResult invocationContext, Object result) {
stopAndLog();
}
/*
* (non-Javadoc)
* @see org.springframework.shell.core.ExecutionProcessor#afterThrowingInvocation(org.springframework.shell.event.ParseResult, java.lang.Throwable)
*/
@Override
public void afterThrowingInvocation(ParseResult invocationContext, Throwable thrown) {
stopAndLog();
}
private void stopAndLog() {
watch.stop();
System.out.println(String.format("Took: %.2f sec.", watch.getTotalTimeSeconds()));
}
}

View File

@@ -1,42 +0,0 @@
/*
* Copyright 2015-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.announcement;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.springframework.data.release.CliComponent;
import org.springframework.data.release.TimedCommand;
import org.springframework.data.release.model.TrainIteration;
import org.springframework.shell.core.annotation.CliCommand;
import org.springframework.shell.core.annotation.CliOption;
/**
* Commands to create markup to be used in announcing blog posts.
*
* @author Oliver Gierke
*/
@CliComponent
@RequiredArgsConstructor
class AnnouncementCommands extends TimedCommand {
private final @NonNull AnnouncementOperations operations;
@CliCommand("announcement")
public void announce(@CliOption(key = "", mandatory = true) TrainIteration iteration) throws Exception {
System.out.println(operations.getProjectBulletpoints(iteration));
}
}

View File

@@ -1,76 +0,0 @@
/*
* Copyright 2014-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.announcement;
import static org.springframework.data.release.model.Projects.*;
import org.springframework.data.release.build.MavenArtifact;
import org.springframework.data.release.cli.StaticResources;
import org.springframework.data.release.model.Project;
import org.springframework.data.release.model.TrainIteration;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
/**
* @author Oliver Gierke
*/
@Component
public class AnnouncementOperations {
/**
* Returns the project list and links to be included in the release announcement for the given {@link TrainIteration}.
*
* @param iteration must not be {@literal null}.
* @return
*/
public String getProjectBulletpoints(TrainIteration iteration) {
Assert.notNull(iteration, "Iteration must not be null!");
StringBuilder builder = new StringBuilder();
iteration.getModulesExcept(BUILD, BOM).forEach(module -> {
Project project = module.getProject();
builder.append("* ");
builder.append(project.getFullName()).append(" ");
builder.append("`");
builder.append(module.getShortVersionString());
builder.append("`");
builder.append(" - ");
MavenArtifact artifact = new MavenArtifact(module);
builder.append(getMarkDownLink("Artifacts", artifact.getRootUrl()));
builder.append(" - ");
StaticResources resources = new StaticResources(module);
builder.append(getMarkDownLink("Javadoc", resources.getJavaDocUrl())).append(" - ");
builder.append(getMarkDownLink("Documentation", resources.getDocumentationUrl())).append(" - ");
builder.append(getMarkDownLink("Changelog", resources.getChangelogUrl()));
builder.append("\n");
});
return builder.toString();
}
private static String getMarkDownLink(String name, String url) {
return String.format("[%s](%s)", name, url);
}
}

View File

@@ -1,118 +0,0 @@
/*
* Copyright 2016-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.build;
import lombok.AccessLevel;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.experimental.FieldDefaults;
import java.io.IOException;
import java.util.Optional;
import org.springframework.data.release.CliComponent;
import org.springframework.data.release.TimedCommand;
import org.springframework.data.release.cli.TrainIterationConverter;
import org.springframework.data.release.git.GitOperations;
import org.springframework.data.release.io.Workspace;
import org.springframework.data.release.model.Project;
import org.springframework.data.release.model.Projects;
import org.springframework.data.release.model.ReleaseTrains;
import org.springframework.data.release.model.Train;
import org.springframework.data.release.model.TrainIteration;
import org.springframework.data.release.utils.Logger;
import org.springframework.shell.core.annotation.CliCommand;
import org.springframework.shell.core.annotation.CliOption;
import org.springframework.util.Assert;
/**
* @author Oliver Gierke
* @author Mark Paluch
*/
@CliComponent
@RequiredArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
class BuildCommands extends TimedCommand {
@NonNull BuildOperations build;
@NonNull Workspace workspace;
@NonNull GitOperations git;
@NonNull Logger logger;
/**
* Removes all Spring Data artifacts from the local repository.
*
* @throws IOException
*/
@CliCommand("build purge artifacts")
public void purge() throws IOException {
logger.log("Workspace", "Cleaning up workspace directory at %s.",
workspace.getWorkingDirectory().getAbsolutePath());
workspace.purge(build.getLocalRepository(),
path -> build.getLocalRepository().relativize(path).startsWith("org/springframework/data"));
}
/**
* Triggers a build for all modules of the given {@link TrainIteration}.
*
* @param iteration must not be {@literal null}.
* @param projectKey can be {@literal null} or empty.
*/
@CliCommand("build")
public void build(@CliOption(key = "", mandatory = true) TrainIteration iteration, //
@CliOption(key = "module") String projectKey) {
Assert.notNull(iteration, "Train iteration must not be null!");
Optional<Project> project = Projects.byName(projectKey);
project.ifPresent(it -> build.triggerBuild(iteration.getModule(it)));
if (!project.isPresent()) {
git.prepare(iteration);
iteration.forEach(build::triggerBuild);
}
}
/**
* @param trainOrIteration must not be {@literal null}. Accepts release train names and train iterations.
*/
@CliCommand("build-distribute")
public void buildDistribute(@CliOption(key = "", mandatory = true) String trainOrIteration) {
Assert.hasText(trainOrIteration, "Train or iteration must not be null or empty!");
if (trainOrIteration.contains(" ")) {
TrainIteration trainIteration = new TrainIterationConverter().convertFromText(trainOrIteration,
TrainIteration.class, null);
Assert.notNull(trainIteration, "TrainIteration must not be null!");
git.prepare(trainIteration);
build.distributeResources(trainIteration);
return;
}
Train train = ReleaseTrains.getTrainByName(trainOrIteration);
Assert.notNull(train, "Train must not be null!");
git.checkout(train);
build.distributeResources(train);
}
}

View File

@@ -1,54 +0,0 @@
/*
* Copyright 2016-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.build;
import java.util.List;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.release.model.Project;
import org.springframework.plugin.core.OrderAwarePluginRegistry;
import org.springframework.plugin.core.PluginRegistry;
import org.xmlbeam.XBProjector;
import org.xmlbeam.XBProjector.Flags;
import org.xmlbeam.config.DefaultXMLFactoriesConfig;
import org.xmlbeam.config.DefaultXMLFactoriesConfig.NamespacePhilosophy;
/**
* Spring configuration for build related components.
*
* @author Oliver Gierke
*/
@Configuration(proxyBeanMethods = false)
class BuildConfiguration {
@Bean
public PluginRegistry<BuildSystem, Project> buildSystems(List<? extends BuildSystem> buildSystems) {
return OrderAwarePluginRegistry.create(buildSystems);
}
@Bean
public XBProjector projectionFactory() {
DefaultXMLFactoriesConfig config = new DefaultXMLFactoriesConfig();
config.setNamespacePhilosophy(NamespacePhilosophy.AGNOSTIC);
config.setOmitXMLDeclaration(false);
config.setPrettyPrinting(false);
return new XBProjector(config, Flags.TO_STRING_RENDERS_XML);
}
}

View File

@@ -1,283 +0,0 @@
/*
* Copyright 2019-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.build;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import java.io.File;
import java.io.FileInputStream;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.function.BiFunction;
import java.util.function.Supplier;
import java.util.stream.Collector;
import java.util.stream.Collectors;
import javax.annotation.PreDestroy;
import org.apache.commons.io.IOUtils;
import org.springframework.data.release.infra.InfrastructureOperations;
import org.springframework.data.release.io.Workspace;
import org.springframework.data.release.model.JavaVersion;
import org.springframework.data.release.model.Project;
import org.springframework.data.release.model.ProjectAware;
import org.springframework.data.release.utils.ListWrapperCollector;
import org.springframework.data.util.Streamable;
import org.springframework.plugin.core.PluginRegistry;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
/**
* Build executor service.
*
* @author Mark Paluch
*/
@Component
@RequiredArgsConstructor
class BuildExecutor {
private final @NonNull PluginRegistry<BuildSystem, Project> buildSystems;
private final MavenProperties mavenProperties;
private final ExecutorService executor;
private final Workspace workspace;
@PreDestroy
public void shutdown() {
executor.shutdown();
}
/**
* Selects the build system for each module contained in the given iteration and executes the given function for it
* considering pre-requites, honoring the order.
*
* @param iteration must not be {@literal null}.
* @param function must not be {@literal null}.
* @return
*/
public <T, M extends ProjectAware> Summary<T> doWithBuildSystemOrdered(Streamable<M> iteration,
BiFunction<BuildSystem, M, T> function) {
return doWithBuildSystem(iteration, function, true);
}
/**
* Selects the build system for each module contained in the given iteration and executes the given function for it
* considering pre-requites, without considering the execution order.
*
* @param iteration must not be {@literal null}.
* @param function must not be {@literal null}.
* @return
*/
public <T, M extends ProjectAware> Summary<T> doWithBuildSystemAnyOrder(Streamable<M> iteration,
BiFunction<BuildSystem, M, T> function) {
return doWithBuildSystem(iteration, function, false);
}
private <T, M extends ProjectAware> Summary<T> doWithBuildSystem(Streamable<M> iteration,
BiFunction<BuildSystem, M, T> function, boolean considerDependencyOrder) {
Map<Project, CompletableFuture<T>> results = new ConcurrentHashMap<>();
// Add here projects that should be skipped because of a partial deployment to e.g. Sonatype.
Set<Project> skip = new HashSet<>(Arrays.asList());
skip.forEach(it -> results.put(it, CompletableFuture.completedFuture(null)));
for (M moduleIteration : iteration) {
if (skip.contains(moduleIteration.getProject())) {
continue;
}
if (considerDependencyOrder) {
Set<Project> dependencies = moduleIteration.getProject().getDependencies();
for (Project dependency : dependencies) {
CompletableFuture<T> futureResult = results.get(dependency);
if (futureResult == null) {
if (!iteration.stream().map(ProjectAware::getProject).anyMatch(project -> project.equals(dependency))) {
throw new IllegalStateException(moduleIteration.getProject().getName() + " requires "
+ dependency.getName() + " which is not part of the Iteration. Please fix Projects/Iterations setup");
}
throw new IllegalStateException("No future result for " + dependency.getName() + ", required by "
+ moduleIteration.getProject().getName());
}
futureResult.join();
}
}
CompletableFuture<T> result = run(moduleIteration, function);
results.put(moduleIteration.getProject(), result);
}
return iteration.stream()//
.map(module -> {
CompletableFuture<T> future = results.get(module.getProject());
try {
return new ExecutionResult<T>(module.getProject(), future.get());
}
catch (InterruptedException | ExecutionException e) {
return new ExecutionResult<T>(module.getProject(), e.getCause());
}
}) //
.collect(toSummaryCollector());
}
private <T, M extends ProjectAware> CompletableFuture<T> run(M module, BiFunction<BuildSystem, M, T> function) {
Assert.notNull(module, "Module must not be null!");
CompletableFuture<T> result = new CompletableFuture<>();
Supplier<IllegalStateException> exception = () -> new IllegalStateException(
String.format("No build system plugin found for project %s!", module.getProject()));
BuildSystem buildSystem = buildSystems.getPluginFor(module.getProject(), exception)
.withJavaVersion(detectJavaVersion(module.getProject()));
Runnable runnable = () -> {
try {
result.complete(function.apply(buildSystem, module));
} catch (Exception e) {
result.completeExceptionally(e);
}
};
executor.execute(runnable);
return result;
}
@SneakyThrows
public JavaVersion detectJavaVersion(Project project) {
File ciProperties = workspace.getFile(InfrastructureOperations.CI_PROPERTIES, project);
if (!ciProperties.exists()) {
throw new IllegalStateException(String.format("Cannot find %s for project %s", ciProperties, project));
}
Properties properties = new Properties();
try (FileInputStream fis = new FileInputStream(ciProperties)) {
properties.load(fis);
}
return JavaVersion.fromDockerTag(properties.getProperty("java.main.tag"));
}
/**
* Returns a new collector to toSummaryCollector {@link ExecutionResult} as {@link Summary} using the {@link Stream}
* API.
*
* @return
*/
public static <T> Collector<ExecutionResult<T>, ?, Summary<T>> toSummaryCollector() {
return ListWrapperCollector.collectInto(Summary::new);
}
public static class ExecutionResult<T> {
private final Project project;
private final T result;
private final Throwable failure;
public ExecutionResult(Project project, Throwable failure) {
this.project = project;
this.result = null;
this.failure = failure;
}
public ExecutionResult(Project project, T result) {
this.project = project;
this.result = result;
this.failure = null;
}
public T getResult() {
return result;
}
@Override
public String toString() {
return String.format("%-14s - %s", project.getName(),
isSuccessful() ? "🆗 Successful" : "🧨 Error: " + failure.getMessage());
}
public boolean isSuccessful() {
return this.failure == null;
}
}
public static class Summary<T> {
private final List<ExecutionResult<T>> executions;
public Summary(List<ExecutionResult<T>> executions) {
this.executions = executions;
if (!isSuccessful()) {
throw new BuildFailed(this);
}
}
public List<ExecutionResult<T>> getExecutions() {
return executions;
}
public boolean isSuccessful() {
return this.executions.stream().allMatch(ExecutionResult::isSuccessful);
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("Execution summary");
builder.append(IOUtils.LINE_SEPARATOR);
builder.append(executions.stream().map(it -> "\t" + it).collect(Collectors.joining(IOUtils.LINE_SEPARATOR)));
return builder.toString();
}
}
static class BuildFailed extends RuntimeException {
public BuildFailed(Summary<?> summary) {
super(summary.toString());
}
}
}

View File

@@ -1,245 +0,0 @@
/*
* Copyright 2016-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.build;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import java.nio.file.Path;
import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.assertj.core.util.VisibleForTesting;
import org.springframework.data.release.deployment.DeploymentInformation;
import org.springframework.data.release.model.Module;
import org.springframework.data.release.model.ModuleIteration;
import org.springframework.data.release.model.Phase;
import org.springframework.data.release.model.Project;
import org.springframework.data.release.model.Projects;
import org.springframework.data.release.model.Train;
import org.springframework.data.release.model.TrainIteration;
import org.springframework.data.release.utils.Logger;
import org.springframework.plugin.core.PluginRegistry;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
/**
* @author Oliver Gierke
* @author Mark Paluch
*/
@Component
@RequiredArgsConstructor
public class BuildOperations {
private final @NonNull PluginRegistry<BuildSystem, Project> buildSystems;
private final @NonNull Logger logger;
private final @NonNull MavenProperties properties;
private final @NonNull BuildExecutor executor;
/**
* Updates all inter-project dependencies based on the given {@link TrainIteration} and release {@link Phase}.
*
* @param iteration must not be {@literal null}.
* @param phase must not be {@literal null}.
* @throws Exception
*/
public void updateProjectDescriptors(TrainIteration iteration, Phase phase) throws Exception {
Assert.notNull(iteration, "Train iteration must not be null!");
Assert.notNull(phase, "Phase must not be null!");
UpdateInformation updateInformation = UpdateInformation.of(iteration, phase);
BuildExecutor.Summary<ModuleIteration> summary = executor.doWithBuildSystemOrdered(iteration,
(system, it) -> system.updateProjectDescriptors(it, updateInformation));
logger.log(iteration, "Update Project Descriptors done: %s", summary);
}
/**
* Triggers the distribution builds for all modules participating in the given {@link TrainIteration}.
*
* @param iteration must not be {@literal null}.
*/
public void distributeResources(TrainIteration iteration) {
Assert.notNull(iteration, "Train iteration must not be null!");
distributeResources(iteration.getTrain());
}
/**
* Triggers the distribution builds for all modules participating in the given {@link Train}.
*
* @param train must not be {@literal null}.
*/
public void distributeResources(Train train) {
Assert.notNull(train, "Train must not be null!");
BuildExecutor.Summary<Module> summary = executor.doWithBuildSystemAnyOrder(train,
BuildSystem::triggerDistributionBuild);
logger.log(train, "Distribution build: %s", summary);
}
/**
* Triggers the distribution builds for the given module.
*
* @param iteration must not be {@literal null}.
*/
public void distributeResources(ModuleIteration iteration) {
Assert.notNull(iteration, "ModuleIteration must not be null!");
doWithBuildSystem(iteration, BuildSystem::triggerDistributionBuild);
}
/**
* Performs the release build for all modules in the given {@link TrainIteration}.
*
* @param iteration must not be {@literal null}.
* @return
*/
public List<DeploymentInformation> performRelease(TrainIteration iteration) {
BuildExecutor.Summary<DeploymentInformation> summary = executor.doWithBuildSystemOrdered(iteration,
(buildSystem, moduleIteration) -> performRelease(moduleIteration));
logger.log(iteration, "Release: %s", summary);
return summary.getExecutions().stream().map(BuildExecutor.ExecutionResult::getResult).collect(Collectors.toList());
}
/**
* Performs the release build for the given {@link ModuleIteration}.
*
* @param module must not be {@literal null}.
* @return
*/
public DeploymentInformation performRelease(ModuleIteration module) {
return buildAndDeployRelease(module);
}
/**
* Prepares the versions of the given {@link TrainIteration} depending on the given {@link Phase}.
*
* @param iteration must not be {@literal null}.
* @param phase must not be {@literal null}.
*/
public void prepareVersions(TrainIteration iteration, Phase phase) {
Assert.notNull(iteration, "Train iteration must not be null!");
Assert.notNull(phase, "Phase must not be null!");
BuildExecutor.Summary<ModuleIteration> summary = executor.doWithBuildSystemOrdered(iteration,
(system, module) -> system.prepareVersion(module, phase));
logger.log(iteration, "Prepare versions: %s", summary);
}
/**
* Prepares the version of the given {@link ModuleIteration} depending on the given {@link Phase}.
*
* @param iteration must not be {@literal null}.
* @param phase must not be {@literal null}.
* @return
*/
@VisibleForTesting
public ModuleIteration prepareVersion(ModuleIteration iteration, Phase phase) {
Assert.notNull(iteration, "Module iteration must not be null!");
Assert.notNull(phase, "Phase must not be null!");
return doWithBuildSystem(iteration, (system, module) -> system.prepareVersion(module, phase));
}
/**
* Returns the {@link Path} of the local artifact repository.
*
* @return
*/
public Path getLocalRepository() {
return properties.getLocalRepository().toPath();
}
/**
* Builds the release for the given {@link ModuleIteration} and deploys it to the staging repository.
*
* @param module must not be {@literal null}.
* @return
*/
public DeploymentInformation buildAndDeployRelease(ModuleIteration module) {
return doWithBuildSystem(module, BuildSystem::deploy);
}
/**
* Triggers a normal build for the given {@link ModuleIteration}.
*
* @param module must not be {@literal null}.
* @return
*/
public ModuleIteration triggerBuild(ModuleIteration module) {
return doWithBuildSystem(module, BuildSystem::triggerBuild);
}
/**
* Triggers the pre-release checks for all modules of the given {@link TrainIteration}.
*
* @param iteration must not be {@literal null}.
*/
public void runPreReleaseChecks(TrainIteration iteration) {
Assert.notNull(iteration, "Train iteration must not be null!");
executor.doWithBuildSystemAnyOrder(iteration, BuildSystem::triggerPreReleaseCheck);
}
/**
* Verifies Java version presence and that the project can be build using Maven.
*/
public void verify() {
Project project = Projects.BUILD;
BuildSystem buildSystem = buildSystems.getRequiredPluginFor(project);
buildSystem.withJavaVersion(executor.detectJavaVersion(project)).verify();
}
/**
* Selects the build system for the module contained in the given {@link ModuleIteration} and executes the given
* function with it.
*
* @param module must not be {@literal null}.
* @param function must not be {@literal null}.
* @return
*/
private <T> T doWithBuildSystem(ModuleIteration module, BiFunction<BuildSystem, ModuleIteration, T> function) {
Assert.notNull(module, "ModuleIteration must not be null!");
Supplier<IllegalStateException> exception = () -> new IllegalStateException(
String.format("No build system plugin found for project %s!", module.getProject()));
BuildSystem buildSystem = buildSystems.getPluginFor(module.getProject(), exception);
return function.apply(buildSystem.withJavaVersion(executor.detectJavaVersion(module.getProject())), module);
}
}

View File

@@ -1,89 +0,0 @@
/*
* Copyright 2016-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.build;
import org.springframework.data.release.deployment.DeploymentInformation;
import org.springframework.data.release.model.JavaVersion;
import org.springframework.data.release.model.ModuleIteration;
import org.springframework.data.release.model.Phase;
import org.springframework.data.release.model.Project;
import org.springframework.data.release.model.ProjectAware;
import org.springframework.plugin.core.Plugin;
/**
* Plugin interface to back different build systems.
*
* @author Oliver Gierke
* @author Mark Paluch
*/
interface BuildSystem extends Plugin<Project> {
/**
* Updates the project descriptors for the given {@link ModuleIteration} using the given {@link UpdateInformation}.
*
* @param iteration must not be {@literal null}.
* @param updateInformation must not be {@literal null}.
*/
<M extends ProjectAware> M updateProjectDescriptors(M iteration, UpdateInformation updateInformation);
/**
* Prepares the project descriptor of the {@link ModuleIteration} for the given release {@link Phase}.
*
* @param module must not be {@literal null}.
* @param phase must not be {@literal null}.
* @return
*/
ModuleIteration prepareVersion(ModuleIteration module, Phase phase);
/**
* Deploy artifacts for the given {@link ModuleIteration} and return the {@link DeploymentInformation}.
*
* @param module must not be {@literal null}.
* @return
*/
DeploymentInformation deploy(ModuleIteration module);
/**
* Runs the distribution build.
*
* @param module must not be {@literal null}.
* @return
*/
<M extends ProjectAware> M triggerDistributionBuild(M module);
<M extends ProjectAware> M triggerBuild(M module);
/**
* Triggers the pre-release checks for the given {@link ModuleIteration}.
*
* @param module must not be {@literal null}.
* @return
*/
<M extends ProjectAware> M triggerPreReleaseCheck(M module);
/**
* Verify general functionality and correctness of the build setup.
*/
void verify();
/**
* Prepare the build system with a Java version.
*
* @param javaVersion
* @return
*/
BuildSystem withJavaVersion(JavaVersion javaVersion);
}

View File

@@ -1,259 +0,0 @@
/*
* Copyright 2017-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.build;
import lombok.AccessLevel;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.Value;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.function.BooleanSupplier;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.springframework.data.release.model.Masked;
import org.springframework.util.Assert;
/**
* Value object to represent a Maven command line.
*
* @author Oliver Gierke
*/
@Value
class CommandLine {
@NonNull List<Goal> goals;
@NonNull List<Argument> arguments;
/**
* Creates a new {@link CommandLine} for the given {@link Goal} and {@link Argument}s.
*
* @param goal must not be {@literal null}.
* @param argument must not be {@literal null}.
* @return
*/
public static CommandLine of(Goal goal, Argument... argument) {
return new CommandLine(Collections.singletonList(goal), Arrays.asList(argument));
}
/**
* Creates a new {@link CommandLine} for the given {@link Goal}s and {@link Argument}s.
*
* @param goal must not be {@literal null}.
* @param argument must not be {@literal null}.
* @return
*/
public static CommandLine of(Goal first, Goal second, Argument... argument) {
return new CommandLine(Arrays.asList(first, second), Arrays.asList(argument));
}
/**
* Returns a new {@link CommandLine} with the given {@link Argument} added in case the given {@link BooleanSupplier}
* evaluates to {@literal true}.
*
* @param argument must not be {@literal null}.
* @param condition must not be {@literal null}.
* @return
*/
public CommandLine conditionalAnd(Argument argument, BooleanSupplier condition) {
return condition.getAsBoolean() ? and(argument) : this;
}
/**
* Returns a new {@link CommandLine} with the given {@link Argument} added.
*
* @param argument must not be {@literal null}.
* @return
*/
public CommandLine and(Argument argument) {
Assert.notNull(argument, "Argument must not be null!");
List<Argument> newArguments = new ArrayList<Argument>(arguments.size() + 1);
newArguments.addAll(arguments);
newArguments.add(argument);
return new CommandLine(goals, newArguments);
}
/**
* Renders the current {@link CommandLine} as a plain {@link List} of {@link String}s using the given {@link Function}
* to expand the {@link Goal}s.
*
* @param goalExpansion must not be {@literal null}.
* @return
*/
public List<String> toCommandLine(Function<Goal, String> goalExpansion) {
Stream<String> goalStream = goals.stream().map(goalExpansion);
Stream<String> argumentStream = arguments.stream().map(it -> it.toCommandLineArgument());
return Stream.concat(goalStream, argumentStream).collect(Collectors.toList());
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
Stream<String> goalStream = goals.stream().map(it -> it.getGoal());
Stream<String> argumentStream = arguments.stream().map(Object::toString);
return Stream.concat(goalStream, argumentStream).collect(Collectors.joining(" "));
}
/**
* Represents a Maven goal to invoke. Can be a custom one but also one of the predefined instances.
*
* @author Oliver Gierke
*/
@Value(staticConstructor = "goal")
public static class Goal {
public static final Goal CLEAN = Goal.goal("clean");
public static final Goal INSTALL = Goal.goal("install");
public static final Goal DEPLOY = Goal.goal("deploy");
public static final Goal VALIDATE = Goal.goal("validate");
public static final Goal VERIFY = Goal.goal("verify");
String goal;
}
@Value
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public static class Argument {
public static Argument SKIP_TESTS = Argument.arg("skipTests");
@NonNull String name;
@NonNull Optional<ArgumentValue<?>> value;
private Argument(String name, ArgumentValue<?> value) {
this(name, Optional.of(value));
}
static Argument of(String name) {
return new Argument(name, Optional.empty());
}
/**
* Enables the given comma-separated profiles for the {@link CommandLine}.
*
* @param name must not be {@literal null} or empty.
* @return
*/
public static Argument profile(String name, String... others) {
Assert.hasText(name, "Profiles must not be null or empty!");
Assert.notNull(others, "Other profiles must not be null!");
String profiles = Stream.concat(Stream.of(name), Arrays.stream(others)).collect(Collectors.joining(","));
return Argument.of("-P".concat(profiles));
}
public static Argument arg(String name) {
return Argument.of("-D".concat(name));
}
public static Argument debug() {
return Argument.of("-X");
}
public Argument withValue(Object value) {
return new Argument(name, ArgumentValue.of(value));
}
public Argument withQuotedValue(Object value) {
return new Argument(name, ArgumentValue.of(value, it -> String.format("\"%s\"", it.toString())));
}
public Argument withValue(Masked masked) {
return new Argument(name, ArgumentValue.of(masked));
}
public String toCommandLineArgument() {
return toNameValuePair(value.map(ArgumentValue::toCommandLine));
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return toNameValuePair(value.map(Object::toString));
}
private String toNameValuePair(Optional<String> source) {
return source//
.map(it -> String.format("%s=%s", name, it))//
.orElse(name);
}
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
private static class ArgumentValue<T> {
private final @NonNull T value;
private final @NonNull Optional<Function<T, String>> preparer;
private final @NonNull Optional<Function<T, String>> toString;
public static <T> ArgumentValue<T> of(T value) {
return new ArgumentValue<>(value, Optional.empty(), Optional.empty());
}
public static <T> ArgumentValue<T> of(T value, Function<T, String> preparer) {
return new ArgumentValue<>(value, Optional.of(preparer), Optional.empty());
}
/**
* Returns an {@link ArgumentValue} for the given {@link Masked} value.
*
* @param masked must not be {@literal null}.
* @return
*/
public static <T extends Masked> ArgumentValue<T> of(T masked) {
return new ArgumentValue<>(masked, Optional.empty(), Optional.of(it -> it.masked()));
}
/**
* Returns the {@link String} variant of the argument value.
*
* @return
*/
public String toCommandLine() {
return preparer.map(it -> it.apply(value)).orElseGet(() -> value.toString());
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
public String toString() {
return toString.map(it -> it.apply(value)).orElseGet(() -> toCommandLine());
}
}
}
}

View File

@@ -1,33 +0,0 @@
/*
* Copyright 2014-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.build;
import lombok.Value;
/**
* Value object to represent an artifacts group identifier.
*
* @author Oliver Gierke
*/
@Value(staticConstructor = "of")
class GroupId {
private final String value;
public String asPath() {
return value.replace('.', '/');
}
}

View File

@@ -1,91 +0,0 @@
/*
* Copyright 2014-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.build;
import static org.springframework.data.release.model.Projects.*;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import org.springframework.data.release.model.ArtifactVersion;
import org.springframework.data.release.model.ModuleIteration;
import org.springframework.data.release.model.Project;
import org.springframework.util.Assert;
/**
* Value object to represent a Maven artifact.
*
* @author Oliver Gierke
*/
@EqualsAndHashCode
public class MavenArtifact {
private static final GroupId GROUP_ID = GroupId.of("org.springframework.data");
private final Project project;
private final @Getter Repository repository;
private final @Getter ArtifactVersion version;
/**
* Creates a new {@link MavenArtifact} for the given {@link ModuleIteration}.
*
* @param module must not be {@literal null}.
*/
public MavenArtifact(ModuleIteration module) {
Assert.notNull(module, "Module iteration must not be null!");
this.project = module.getModule().getProject();
this.repository = new Repository(module.getIteration());
this.version = ArtifactVersion.of(module);
}
public MavenArtifact(Project project, ArtifactVersion version) {
this.project = project;
this.repository = new Repository(version);
this.version = version;
}
public String getGroupId() {
return GROUP_ID.getValue();
}
/**
* Returns the Maven artifact identifier.
*
* @return
*/
public String getArtifactId() {
String artifactId = String.format("spring-data-%s", project.getName().toLowerCase());
return REST.equals(project) ? artifactId.concat("-webmvc") : artifactId;
}
public ArtifactVersion getNextDevelopmentVersion() {
return version.getNextDevelopmentVersion();
}
/**
* Returns the URL pointing to the artifacts.
*
* @return
*/
public String getRootUrl() {
return String.format("%s/%s/%s/%s", repository.getUrl(), GROUP_ID.asPath(), getArtifactId(), version);
}
}

View File

@@ -1,464 +0,0 @@
/*
* Copyright 2014-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.build;
import static org.springframework.data.release.build.CommandLine.Argument.*;
import static org.springframework.data.release.build.CommandLine.Goal.*;
import static org.springframework.data.release.model.Projects.*;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import lombok.experimental.FieldDefaults;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.function.Consumer;
import java.util.regex.Pattern;
import javax.xml.transform.TransformerException;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.apache.commons.io.IOUtils;
import org.springframework.core.annotation.Order;
import org.springframework.data.release.build.CommandLine.Argument;
import org.springframework.data.release.build.CommandLine.Goal;
import org.springframework.data.release.build.Pom.Artifact;
import org.springframework.data.release.deployment.DefaultDeploymentInformation;
import org.springframework.data.release.deployment.DeploymentInformation;
import org.springframework.data.release.deployment.DeploymentProperties;
import org.springframework.data.release.io.Workspace;
import org.springframework.data.release.model.ArtifactVersion;
import org.springframework.data.release.model.Gpg;
import org.springframework.data.release.model.JavaVersion;
import org.springframework.data.release.model.ModuleIteration;
import org.springframework.data.release.model.Phase;
import org.springframework.data.release.model.Project;
import org.springframework.data.release.model.ProjectAware;
import org.springframework.data.release.model.TrainIteration;
import org.springframework.data.release.utils.Logger;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.xmlbeam.ProjectionFactory;
import org.xmlbeam.XBProjector;
import org.xmlbeam.dom.DOMAccess;
import org.xmlbeam.io.XBStreamInput;
/**
* @author Oliver Gierke
* @author Mark Paluch
*/
@Component
@Order(100)
@RequiredArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
class MavenBuildSystem implements BuildSystem {
static String POM_XML = "pom.xml";
Workspace workspace;
ProjectionFactory projectionFactory;
Logger logger;
MavenRuntime mvn;
DeploymentProperties properties;
Gpg gpg;
@Override
public BuildSystem withJavaVersion(JavaVersion javaVersion) {
return new MavenBuildSystem(workspace, projectionFactory, logger, mvn.withJavaVersion(javaVersion), properties,
gpg);
}
/*
* (non-Javadoc)
* @see org.springframework.data.release.build.BuildSystem#updateProjectDescriptors(org.springframework.data.release.model.ModuleIteration, org.springframework.data.release.model.TrainIteration, org.springframework.data.release.model.Phase)
*/
@Override
public <M extends ProjectAware> M updateProjectDescriptors(M module, UpdateInformation information) {
PomUpdater updater = new PomUpdater(logger, information, module.getProject());
if (updater.isBuildProject()) {
if (information.isBomInBuildProject()) {
updateBom(information, "bom/pom.xml", BUILD);
}
updateParentPom(information);
} else if (updater.isBomProject()) {
updateBom(information, "bom/pom.xml", BOM);
} else {
doWithProjection(workspace.getFile(POM_XML, updater.getProject()), pom -> {
updater.updateDependencyProperties(pom);
updater.updateParentVersion(pom);
updater.updateRepository(pom);
});
}
return module;
}
/*
* (non-Javadoc)
* @see org.springframework.data.release.build.BuildSystem#triggerDistributionBuild(org.springframework.data.release.model.Module)
*/
@Override
public <M extends ProjectAware> M triggerDistributionBuild(M module) {
Project project = module.getProject();
if (BUILD.equals(project)) {
return module;
}
if (BOM.equals(project)) {
return module;
}
if (!isMavenProject(project)) {
logger.log(project, "Skipping project as no pom.xml could be found in the working directory!");
return module;
}
logger.log(project, "Triggering distribution build…");
mvn.execute(project, CommandLine.of(Goal.CLEAN, Goal.DEPLOY, //
SKIP_TESTS, profile("distribute"), Argument.of("-B"),
arg("artifactory.server").withValue(properties.getServer().getUri()),
arg("artifactory.distribution-repository").withValue(properties.getDistributionRepository()),
arg("artifactory.username").withValue(properties.getUsername()),
arg("artifactory.password").withValue(properties.getPassword())));
mvn.execute(project, CommandLine.of(Goal.CLEAN, Goal.DEPLOY, //
SKIP_TESTS, profile("distribute-schema"), Argument.of("-B"),
arg("artifactory.server").withValue(properties.getServer().getUri()),
arg("artifactory.distribution-repository").withValue(properties.getDistributionRepository()),
arg("artifactory.username").withValue(properties.getUsername()),
arg("artifactory.password").withValue(properties.getPassword())));
logger.log(project, "Successfully finished distribution build!");
return module;
}
private void updateBom(UpdateInformation updateInformation, String file, Project project) {
TrainIteration iteration = updateInformation.getTrain();
logger.log(BUILD, "Updating BOM pom.xml…");
doWithProjection(workspace.getFile(file, project), pom -> {
for (ModuleIteration module : iteration.getModulesExcept(BUILD, BOM)) {
ArtifactVersion version = updateInformation.getProjectVersionToSet(module.getProject());
logger.log(project, "%s", module);
String moduleArtifactId = new MavenArtifact(module).getArtifactId();
pom.setDependencyManagementVersion(moduleArtifactId, version);
logger.log(project, "Updated managed dependency version for %s to %s!", moduleArtifactId, version);
module.getProject().doWithAdditionalArtifacts(additionalArtifact -> {
String artifactId = additionalArtifact.getArtifactId();
Artifact artifact = pom.getManagedDependency(artifactId);
if (artifact != null) {
pom.setDependencyManagementVersion(artifactId, version);
logger.log(project, "Updated managed dependency version for %s to %s!", artifactId, version);
} else {
logger.log(project, "Artifact %s not found, skipping update!", artifactId);
}
});
}
if (updateInformation.getPhase().equals(Phase.PREPARE)) {
// Make sure we have no snapshot leftovers
List<Artifact> snapshotDependencies = pom.getSnapshotDependencies();
if (!snapshotDependencies.isEmpty()) {
throw new IllegalStateException(String.format("Found snapshot dependencies %s!", snapshotDependencies));
}
}
});
}
private void updateParentPom(UpdateInformation information) {
// Fix version of shared resources to to-be-released version.
doWithProjection(workspace.getFile("parent/pom.xml", BUILD), ParentPom.class, pom -> {
logger.log(BUILD, "Setting shared resources version to %s.", information.getParentVersionToSet());
pom.setSharedResourcesVersion(information.getParentVersionToSet());
logger.log(BUILD, "Setting releasetrain property to %s.", information.getReleaseTrainVersion());
pom.setReleaseTrain(information.getReleaseTrainVersion());
});
}
public boolean isMavenProject(ModuleIteration module) {
Project project = module.getProject();
if (!isMavenProject(project)) {
logger.log(module, "No pom.xml file found, skipping project.");
return false;
}
return true;
}
/*
* (non-Javadoc)
* @see org.springframework.data.release.build.BuildSystem#prepareVersion(org.springframework.data.release.model.ModuleIteration, org.springframework.data.release.model.Phase)
*/
@Override
public ModuleIteration prepareVersion(ModuleIteration module, Phase phase) {
Project project = module.getProject();
UpdateInformation information = UpdateInformation.of(module.getTrainIteration(), phase);
CommandLine goals = CommandLine.of(goal("versions:set"), goal("versions:commit"));
if (BOM.equals(project)) {
mvn.execute(project, goals.and(arg("newVersion").withValue(information.getReleaseTrainVersion())) //
.and(arg("generateBackupPoms").withValue("false")));
mvn.execute(project, goals.and(arg("newVersion").withValue(information.getReleaseTrainVersion())) //
.and(arg("generateBackupPoms").withValue("false")) //
.and(arg("processAllModules").withValue("true")) //
.and(Argument.of("-pl").withValue("bom")));
} else {
mvn.execute(project, goals.and(arg("newVersion").withValue(information.getProjectVersionToSet(project)))
.and(arg("generateBackupPoms").withValue("false")));
}
if (BUILD.equals(project)) {
if (!module.getTrain().usesCalver()) {
mvn.execute(project, goals.and(arg("newVersion").withValue(information.getReleaseTrainVersion())) //
.and(arg("generateBackupPoms").withValue("false")) //
.and(arg("groupId").withValue("org.springframework.data")) //
.and(arg("artifactId").withValue("spring-data-releasetrain")));
}
mvn.execute(project, CommandLine.of(Goal.INSTALL));
}
return module;
}
/*
* (non-Javadoc)
* @see org.springframework.data.release.build.BuildSystem#deploy(org.springframework.data.release.model.ModuleIteration)
*/
@Override
public DeploymentInformation deploy(ModuleIteration module) {
Assert.notNull(module, "Module must not be null!");
DeploymentInformation information = new DefaultDeploymentInformation(module, properties);
deployToArtifactory(module, information);
deployToMavenCentral(module);
return information;
}
/*
* (non-Javadoc)
* @see org.springframework.data.release.build.BuildSystem#triggerBuild(org.springframework.data.release.model.ModuleIteration)
*/
@Override
public <M extends ProjectAware> M triggerBuild(M module) {
CommandLine arguments = CommandLine.of(Goal.CLEAN, Goal.INSTALL)//
.conditionalAnd(SKIP_TESTS, () -> module.getProject().skipTests());
mvn.execute(module.getProject(), arguments);
return module;
}
/*
* (non-Javadoc)
* @see org.springframework.data.release.build.BuildSystem#triggerPreReleaseCheck(org.springframework.data.release.model.ModuleIteration)
*/
public <M extends ProjectAware> M triggerPreReleaseCheck(M module) {
mvn.execute(module.getProject(), CommandLine.of(Goal.CLEAN, Goal.VALIDATE, profile("pre-release")));
return module;
}
/*
* (non-Javadoc)
* @see org.springframework.plugin.core.Plugin#supports(java.lang.Object)
*/
@Override
public boolean supports(Project project) {
return isMavenProject(project);
}
/*
* (non-Javadoc)
* @see org.springframework.data.release.build.BuildSystem#verify()
*/
@Override
public void verify() {
logger.log(BUILD, "Verifying Maven Build System…");
CommandLine arguments = CommandLine.of(Goal.CLEAN, Goal.VERIFY, //
profile("central"), //
SKIP_TESTS, //
arg("gpg.executable").withValue(gpg.getExecutable()), //
arg("gpg.keyname").withValue(gpg.getKeyname()), //
arg("gpg.password").withValue(gpg.getPassword()));
mvn.execute(BUILD, arguments);
mvn.execute(BUILD, CommandLine.of(Goal.goal("nexus-staging:rc-list-profiles"), //
profile("central")));
}
/**
* Triggers Maven commands to deploy module artifacts to Spring Artifactory.
*
* @param module must not be {@literal null}.
* @param information must not be {@literal null}.
*/
private void deployToArtifactory(ModuleIteration module, DeploymentInformation information) {
Assert.notNull(module, "Module iteration must not be null!");
Assert.notNull(information, "Deployment information must not be null!");
if (!module.getIteration().isPreview()) {
logger.log(module, "Not a preview version (milestone or release candidate). Skipping Artifactory deployment.");
return;
}
logger.log(module, "Deploying artifacts to Spring Artifactory…");
CommandLine arguments = CommandLine.of(Goal.CLEAN, Goal.DEPLOY, //
profile("ci,release,artifactory"), //
SKIP_TESTS, //
arg("artifactory.server").withValue(properties.getServer().getUri()),
arg("artifactory.staging-repository").withValue(properties.getStagingRepository()),
arg("artifactory.username").withValue(properties.getUsername()),
arg("artifactory.password").withValue(properties.getPassword()),
arg("artifactory.build-name").withQuotedValue(information.getBuildName()),
arg("artifactory.build-number").withValue(information.getBuildNumber()));
mvn.execute(module.getProject(), arguments);
}
/**
* Triggers Maven commands to deploy to Sonatype's OSS Nexus if the given {@link ModuleIteration} refers to a version
* that has to be publicly released.
*
* @param module must not be {@literal null}.
*/
private void deployToMavenCentral(ModuleIteration module) {
Assert.notNull(module, "Module iteration must not be null!");
if (!module.getIteration().isPublic()) {
logger.log(module, "Skipping deployment to Maven Central as it's not a public version!");
return;
}
logger.log(module, "Deploying artifacts to Sonatype OSS Nexus…");
CommandLine arguments = CommandLine.of(Goal.CLEAN, Goal.DEPLOY, //
profile("ci,release,central"), //
SKIP_TESTS, //
arg("gpg.executable").withValue(gpg.getExecutable()), //
arg("gpg.keyname").withValue(gpg.getKeyname()), //
arg("gpg.password").withValue(gpg.getPassword()));
mvn.execute(module.getProject(), arguments);
}
private boolean isMavenProject(Project project) {
return workspace.getFile(POM_XML, project).exists();
}
private void doWithProjection(File file, Consumer<Pom> callback) {
doWithProjection(file, Pom.class, callback);
}
/**
* TODO: Move XML file callbacks using the {@link ProjectionFactory} to {@link Workspace}.
*/
private <T extends Pom> void doWithProjection(File file, Class<T> type, Consumer<T> callback) {
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file))) {
byte[] content = doWithProjection((XBProjector) projectionFactory, bis, type, callback);
try (FileOutputStream fos = new FileOutputStream(file)) {
fos.write(content);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
static <T extends Pom> byte[] doWithProjection(XBProjector projector, InputStream stream, Class<T> type,
Consumer<T> callback) throws IOException {
XBStreamInput io = projector.io().stream(stream);
T pom = io.read(type);
callback.accept(pom);
StringWriter writer = new StringWriter();
try {
projector.config().createTransformer().transform(new DOMSource(((DOMAccess) pom).getDOMNode()),
new StreamResult(writer));
} catch (TransformerException e) {
throw new RuntimeException(e);
}
String s = writer.toString();
if (s.contains("standalone=\"no\"?><")) {
s = s.replaceAll(Pattern.quote("standalone=\"no\"?><"), "standalone=\"no\"?>" + IOUtils.LINE_SEPARATOR + "<");
}
if (!s.endsWith(IOUtils.LINE_SEPARATOR)) {
s += IOUtils.LINE_SEPARATOR;
}
return s.getBytes(StandardCharsets.UTF_8);
}
}

View File

@@ -1,89 +0,0 @@
/*
* Copyright 2015-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.build;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.util.Map;
import org.apache.commons.io.FileUtils;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
/**
* Maven configuration properties.
*
* @author Oliver Gierke
* @author Mark Paluch
*/
@Slf4j
@Data
@Component
@ConfigurationProperties(prefix = "maven")
class MavenProperties {
private File mavenHome;
private File localRepository;
private Map<String, String> plugins;
private boolean consoleLogger = true;
private boolean parallelize = false;
/**
* Configures the local Maven repository location to use. In case the given folder does not already exists it's
* created.
*
* @param localRepository must not be {@literal null} or empty.
*/
public void setLocalRepository(String localRepository) {
Assert.hasText(localRepository, "Local repository must not be null!");
log.info("Using {} as local Maven repository!", localRepository);
this.localRepository = new File(localRepository.replace("~", FileUtils.getUserDirectoryPath()));
if (!this.localRepository.exists()) {
this.localRepository.mkdirs();
}
}
/**
* Returns the fully-qualified plugin goal for the given local one.
*
* @param goal must not be {@literal null} or empty.
* @return
*/
public String getFullyQualifiedPlugin(String goal) {
Assert.hasText(goal, "Goal must not be null or empty!");
if (goal.startsWith("-")) {
return goal;
}
String[] parts = goal.split(":");
if (parts.length != 2 || !plugins.containsKey(parts[0])) {
return goal;
}
return plugins.get(parts[0]).concat(":").concat(parts[1]);
}
}

View File

@@ -1,221 +0,0 @@
/*
* Copyright 2015-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.build;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.io.Closeable;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
import java.util.stream.Collectors;
import org.apache.maven.shared.invoker.DefaultInvocationRequest;
import org.apache.maven.shared.invoker.DefaultInvoker;
import org.apache.maven.shared.invoker.InvocationRequest;
import org.apache.maven.shared.invoker.InvocationResult;
import org.apache.maven.shared.invoker.Invoker;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.release.io.JavaRuntimes;
import org.springframework.data.release.io.Workspace;
import org.springframework.data.release.model.JavaVersion;
import org.springframework.data.release.model.Project;
import org.springframework.data.release.utils.Logger;
import org.springframework.shell.support.util.StringUtils;
import org.springframework.stereotype.Component;
/**
* @author Oliver Gierke
* @author Mark Paluch
*/
@Slf4j
@Component
class MavenRuntime {
private final Workspace workspace;
private final Logger logger;
private final MavenProperties properties;
private final JavaRuntimes.JdkInstallation jdk;
/**
* Creates a new {@link MavenRuntime} for the given {@link Workspace} and Maven home.
*
* @param workspace must not be {@literal null}.
* @param logger must not be {@literal null}.
* @param properties must not be {@literal null}.
*/
@Autowired
public MavenRuntime(Workspace workspace, Logger logger, MavenProperties properties) {
this(workspace, logger, properties, JavaVersion.JAVA_8);
}
private MavenRuntime(Workspace workspace, Logger logger, MavenProperties properties,
JavaVersion requiredJavaVersion) {
this.workspace = workspace;
this.logger = logger;
this.properties = properties;
this.jdk = JavaRuntimes.Selector.from(requiredJavaVersion).notGraalVM().getRequiredJdkInstallation();
}
public MavenRuntime withJavaVersion(JavaVersion javaVersion) {
return new MavenRuntime(workspace, logger, properties, javaVersion);
}
public void execute(Project project, CommandLine arguments) {
logger.log(project, "📦 Executing mvn %s", arguments.toString());
try (MavenLogger mavenLogger = getLogger(project, arguments.getGoals())) {
Invoker invoker = new DefaultInvoker();
invoker.setMavenHome(properties.getMavenHome());
invoker.setOutputHandler(mavenLogger::info);
invoker.setErrorHandler(mavenLogger::warn);
File localRepository = properties.getLocalRepository();
if (localRepository != null) {
invoker.setLocalRepositoryDirectory(localRepository);
}
File javaHome = getJavaHome();
mavenLogger.info(String.format("Java Home: %s", jdk));
mavenLogger.info(String.format("Executing: mvn %s", arguments));
InvocationRequest request = new DefaultInvocationRequest();
request.setJavaHome(javaHome);
request.setShellEnvironmentInherited(true);
request.setBaseDirectory(workspace.getProjectDirectory(project));
request.setBatchMode(true);
request.setGoals(arguments.toCommandLine(it -> properties.getFullyQualifiedPlugin(it.getGoal())));
InvocationResult result = invoker.execute(request);
if (result.getExitCode() != 0) {
logger.warn(project, "🙈 Failed execution mvn %s", arguments.toString());
throw new IllegalStateException("🙈 Failed execution mvn " + arguments.toString(),
result.getExecutionException());
}
logger.log(project, "🆗 Successful execution mvn %s", arguments.toString());
} catch (Exception e) {
if (e instanceof RuntimeException) {
throw (RuntimeException) e;
}
throw new RuntimeException(e);
}
}
private File getJavaHome() {
return jdk.getHome().getAbsoluteFile();
}
private MavenLogger getLogger(Project project, List<CommandLine.Goal> goals) {
if (this.properties.isConsoleLogger()) {
return new SlfLogger(log, project);
}
return new FileLogger(log, project, this.workspace.getLogsDirectory(), goals);
}
/**
* Maven Logging Forwarder.
*/
interface MavenLogger extends Closeable {
void info(String message);
void warn(String message);
}
@RequiredArgsConstructor
static class SlfLogger implements MavenLogger {
private final org.slf4j.Logger logger;
private final String logPrefix;
SlfLogger(org.slf4j.Logger logger, Project project) {
this.logger = logger;
this.logPrefix = StringUtils.padRight(project.getName(), 10);
}
@Override
public void info(String message) {
logger.info(logPrefix + ": " + message);
}
@Override
public void warn(String message) {
logger.warn(logPrefix + ": " + message);
}
@Override
public void close() throws IOException {
// no-op
}
}
static class FileLogger implements MavenLogger {
private final PrintWriter printWriter;
private final FileOutputStream outputStream;
FileLogger(org.slf4j.Logger logger, Project project, File logsDirectory, List<CommandLine.Goal> goals) {
if (!logsDirectory.exists()) {
logsDirectory.mkdirs();
}
String goalNames = goals.stream().map(CommandLine.Goal::getGoal).collect(Collectors.joining("-"));
String filename = String.format("mvn-%s-%s.log", project.getName(), goalNames).replace(':', '.');
try {
File file = new File(logsDirectory, filename);
logger.info("Routing Maven output to " + file.getCanonicalPath());
outputStream = new FileOutputStream(file, true);
} catch (IOException e) {
throw new RuntimeException(e);
}
printWriter = new PrintWriter(outputStream, true);
}
@Override
public void info(String message) {
printWriter.println(message);
}
@Override
public void warn(String message) {
printWriter.println(message);
}
@Override
public void close() throws IOException {
printWriter.close();
outputStream.close();
}
}
}

View File

@@ -1,33 +0,0 @@
/*
* Copyright 2014-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.build;
import org.springframework.data.release.model.ArtifactVersion;
import org.xmlbeam.annotation.XBValue;
import org.xmlbeam.annotation.XBWrite;
/**
* @author Oliver Gierke
* @author Mark Paluch
*/
public interface ParentPom extends Pom {
@XBWrite("/project/properties/releasetrain")
void setReleaseTrain(@XBValue String releaseTrain);
@XBWrite("/project/profiles/profile[id=\"distribute\"]/dependencies/dependency[artifactId=\"spring-data-build-resources\"]/version")
void setSharedResourcesVersion(@XBValue ArtifactVersion value);
}

View File

@@ -1,109 +0,0 @@
/*
* Copyright 2014-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.build;
import java.util.List;
import org.springframework.data.release.model.ArtifactVersion;
import org.xmlbeam.annotation.XBRead;
import org.xmlbeam.annotation.XBValue;
import org.xmlbeam.annotation.XBWrite;
/**
* @author Oliver Gierke
*/
public interface Pom {
@XBRead("/project")
Artifact getArtifact();
@XBRead("/project/version")
String getRawVersion();
@XBRead("/project/version")
ArtifactVersion getVersion();
@XBWrite("/project/version")
void setVersion(ArtifactVersion version);
@XBWrite("/project/parent/version")
void setParentVersion(ArtifactVersion version);
@XBRead("/project/properties/{0}")
String getProperty(String property);
@XBWrite("/project/properties/{0}")
void setProperty(String property, @XBValue ArtifactVersion value);
@XBWrite("/project/properties/{0}")
void setProperty(String property, @XBValue String value);
@XBWrite("/project/repositories/repository[id=\"{0}\"]/id")
void setRepositoryId(String oldId, @XBValue String newId);
@XBWrite("/project/repositories/repository[id=\"{0}\"]/url")
void setRepositoryUrl(String id, @XBValue String url);
/**
* Sets the version of the dependency with the given artifact identifier to the given {@link ArtifactVersion}.
*
* @param artifactId
* @param version
*/
@XBWrite("/project/dependencies/dependency[artifactId=\"{0}\"]/version")
Pom setDependencyVersion(String artifactId, @XBValue ArtifactVersion version);
@XBRead("/project/dependencies/dependency[artifactId=\"{0}\"]/version")
String getDependencyVersion(String artifactId);
@XBWrite("/project/dependencyManagement/dependencies/dependency[artifactId=\"{0}\"]/version")
Pom setDependencyManagementVersion(String artifactId, @XBValue ArtifactVersion version);
@XBRead("/project/dependencyManagement/dependencies/dependency[artifactId=\"{0}\"]")
Artifact getManagedDependency(String artifactId);
@XBRead("//dependency[substring(version, string-length(version) - string-length('-SNAPSHOT') + 1) = '-SNAPSHOT']")
List<Artifact> getSnapshotDependencies();
public interface Repository {
@XBRead("child::id")
String getId();
@XBRead("child::url")
String getUrl();
}
public interface Artifact {
@XBRead("child::groupId")
GroupId getGroupId();
@XBRead("child::artifactId")
String getArtifactId();
@XBRead("child::version")
String getVersion();
default String getArtifactPath() {
return "/".concat(getGroupId().asPath()).concat("/").concat(getArtifactId());
}
default String getPath() {
return getArtifactPath().concat(getVersion());
}
}
}

View File

@@ -1,114 +0,0 @@
/*
* Copyright 2015-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.build;
import static org.springframework.data.release.model.Phase.*;
import static org.springframework.data.release.model.Projects.*;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.data.release.model.ArtifactVersion;
import org.springframework.data.release.model.Project;
import org.springframework.data.release.utils.Logger;
import org.springframework.util.Assert;
/**
* @author Oliver Gierke
*/
@RequiredArgsConstructor
class PomUpdater {
private final Logger logger;
private final UpdateInformation information;
private final @Getter Project project;
public boolean isBuildProject() {
return BUILD.equals(project);
}
public boolean isBomProject() {
return BOM.equals(project);
}
public void updateArtifactVersion(Pom pom) {
ArtifactVersion version = information.getProjectVersionToSet(project);
logger.log(project, "Updated project version to %s.", version);
pom.setVersion(version);
}
public void updateDependencyProperties(Pom pom) {
project.getDependencies().forEach(dependency -> {
String dependencyProperty = dependency.getDependencyProperty();
if (pom.getProperty(dependencyProperty) == null) {
return;
}
ArtifactVersion version = information.getProjectVersionToSet(dependency);
logger.log(project, "Updating %s dependency version property %s to %s.", dependency.getFullName(),
dependencyProperty, version);
pom.setProperty(dependencyProperty, version);
});
}
/**
* Updates the version of the parent project in the given {@link Pom}.
*
* @param pom must not be {@literal null}.
*/
public void updateParentVersion(Pom pom) {
Assert.notNull(pom, "Pom must not be null!");
ArtifactVersion version = information.getParentVersionToSet();
logger.log(project, "Updating Spring Data Build Parent version to %s.", version);
pom.setParentVersion(version);
}
/**
* Updates the repository section in the given {@link Pom}.
*
* @param pom must not be {@literal null}.
*/
public void updateRepository(Pom pom) {
Assert.notNull(pom, "Pom must not be null!");
String message = "Switching to Spring repository %s (%s).";
Repository repository = information.getRepository();
if (PREPARE.equals(information.getPhase())) {
logger.log(project, message, repository.getId(), repository.getUrl());
pom.setRepositoryId(repository.getSnapshotId(), repository.getId());
pom.setRepositoryUrl(repository.getId(), repository.getUrl());
} else {
logger.log(project, message, repository.getSnapshotId(), repository.getSnapshotUrl());
pom.setRepositoryId(repository.getId(), repository.getSnapshotId());
pom.setRepositoryUrl(repository.getSnapshotId(), repository.getSnapshotUrl());
}
}
}

View File

@@ -1,76 +0,0 @@
/*
* Copyright 2014-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.build;
import lombok.Value;
import org.springframework.data.release.model.ArtifactVersion;
import org.springframework.data.release.model.Iteration;
import org.springframework.util.Assert;
/**
* @author Oliver Gierke
*/
@Value
public class Repository {
private static final String ID_BASE = "spring-libs-";
private static final String BASE = "https://repo.spring.io/libs-";
String id, url;
public Repository(Iteration iteration) {
Assert.notNull(iteration, "Iteration must not be null!");
this.id = ID_BASE.concat(iteration.isPublic() ? "release" : "milestone");
this.url = BASE.concat(iteration.isPublic() ? "release" : "milestone");
}
public Repository(ArtifactVersion version) {
String suffix = getSuffixFor(version);
this.id = ID_BASE.concat(suffix);
this.url = BASE.concat(suffix);
}
public String getSnapshotId() {
return ID_BASE.concat("snapshot");
}
public String getSnapshotUrl() {
return BASE.concat("snapshot");
}
private static String getSuffixFor(ArtifactVersion version) {
if (version.isSnapshotVersion()) {
return "snapshot";
}
if (version.isMilestoneVersion() || version.isReleaseCandidateVersion()) {
return "milestone";
}
if (version.isReleaseVersion()) {
return "release";
}
throw new IllegalArgumentException(String.format("Unsupported ArtifactVersion %s!", version));
}
}

View File

@@ -1,132 +0,0 @@
/*
* Copyright 2015-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.build;
import static org.springframework.data.release.model.Projects.*;
import lombok.Getter;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.springframework.data.release.model.ArtifactVersion;
import org.springframework.data.release.model.Phase;
import org.springframework.data.release.model.Project;
import org.springframework.data.release.model.TrainIteration;
import org.springframework.util.Assert;
/**
* Value object to expose update information for a given {@link TrainIteration} and phase.
*
* @author Oliver Gierke
*/
@RequiredArgsConstructor(staticName = "of")
public class UpdateInformation {
private final @NonNull @Getter TrainIteration train;
private final @NonNull @Getter Phase phase;
/**
* Returns the {@link ArtifactVersion} to be set for the given {@link Project}.
*
* @param dependency must not be {@literal null}.
* @return will never be {@literal null}.
*/
public ArtifactVersion getProjectVersionToSet(Project dependency) {
Assert.notNull(dependency, "Project must not be null!");
ArtifactVersion dependencyVersion = train.getModuleVersion(dependency);
switch (phase) {
case PREPARE:
return dependencyVersion;
case CLEANUP:
return dependencyVersion.getNextDevelopmentVersion();
case MAINTENANCE:
return dependencyVersion.getNextBugfixVersion();
}
throw new IllegalStateException("Unexpected phase detected " + phase + " detected!");
}
/**
* Returns the {@link ArtifactVersion} to be set for the parent reference.
*
* @return will never be {@literal null}.
*/
public ArtifactVersion getParentVersionToSet() {
ArtifactVersion version = train.getModuleVersion(BUILD);
switch (phase) {
case PREPARE:
return version;
case CLEANUP:
return version.getNextDevelopmentVersion();
case MAINTENANCE:
return version.getNextBugfixVersion();
}
throw new IllegalStateException("Unexpected phase detected " + phase + " detected!");
}
/**
* Returns the {@link Repository} to use (milestone or release).
*
* @return will never be {@literal null}.
*/
public Repository getRepository() {
return new Repository(train.getIteration());
}
/**
* Returns the version {@link String} to be used to describe the release train.
*
* @return will never be {@literal null}.
*/
public String getReleaseTrainVersion() {
boolean usesCalver = train.getTrain().usesCalver();
switch (phase) {
case PREPARE:
return train.getReleaseTrainNameAndVersion();
case MAINTENANCE:
if (usesCalver) {
return String.format("%s-SNAPSHOT", train.getNextBugfixName());
}
case CLEANUP:
if (usesCalver) {
if (train.getIteration().isGAIteration()) {
return String.format("%s-SNAPSHOT", train.getNextIterationName());
}
return String.format("%s-SNAPSHOT", train.getNextBugfixName());
}
return String.format("%s-BUILD-SNAPSHOT", train.getName());
}
throw new IllegalStateException("Unexpected phase detected " + phase + " detected!");
}
public boolean isBomInBuildProject() {
return !train.getTrain().usesCalver();
}
}

View File

@@ -1,39 +0,0 @@
/*
* Copyright 2014-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.cli;
import java.util.stream.Collectors;
import org.springframework.data.release.CliComponent;
import org.springframework.data.release.TimedCommand;
import org.springframework.data.release.model.ReleaseTrains;
import org.springframework.data.release.model.Train;
import org.springframework.shell.core.annotation.CliCommand;
import org.springframework.shell.core.annotation.CliOption;
/**
* @author Oliver Gierke
*/
@CliComponent
class ModelCommands extends TimedCommand {
@CliCommand(value = "trains", help = "Displays all release trains or contents of them if a name is provided")
public String train(@CliOption(key = { "", "train" }) Train train) {
return train != null ? train.toString()
: ReleaseTrains.TRAINS.stream().map(Train::getName).collect(Collectors.joining(", "));
}
}

View File

@@ -1,65 +0,0 @@
/*
* Copyright 2014-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.cli;
import java.util.List;
import org.springframework.data.release.model.Project;
import org.springframework.data.release.model.Projects;
import org.springframework.shell.core.Completion;
import org.springframework.shell.core.Converter;
import org.springframework.shell.core.MethodTarget;
import org.springframework.stereotype.Component;
/**
* @author Mark Paluch
*/
@Component
public class ProjectConverter implements Converter<Project> {
/*
* (non-Javadoc)
* @see org.springframework.shell.core.Converter#supports(java.lang.Class, java.lang.String)
*/
@Override
public boolean supports(Class<?> type, String optionContext) {
return Project.class.isAssignableFrom(type);
}
/*
* (non-Javadoc)
* @see org.springframework.shell.core.Converter#convertFromText(java.lang.String, java.lang.Class, java.lang.String)
*/
@Override
public Project convertFromText(String value, Class<?> targetType, String optionContext) {
return Projects.requiredByName(value);
}
/*
* (non-Javadoc)
* @see org.springframework.shell.core.Converter#getAllPossibleValues(java.util.List, java.lang.Class, java.lang.String, java.lang.String, org.springframework.shell.core.MethodTarget)
*/
@Override
public boolean getAllPossibleValues(List<Completion> completions, Class<?> targetType, String existingData,
String optionContext, MethodTarget target) {
for (Project project : Projects.all()) {
completions.add(new Completion(project.getName()));
}
return true;
}
}

View File

@@ -1,214 +0,0 @@
/*
* Copyright 2014-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.cli;
import static org.springframework.data.release.model.Projects.*;
import lombok.AccessLevel;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.experimental.FieldDefaults;
import org.springframework.data.release.CliComponent;
import org.springframework.data.release.TimedCommand;
import org.springframework.data.release.build.BuildOperations;
import org.springframework.data.release.deployment.DeploymentInformation;
import org.springframework.data.release.deployment.DeploymentOperations;
import org.springframework.data.release.git.GitOperations;
import org.springframework.data.release.issues.IssueTrackerCommands;
import org.springframework.data.release.issues.github.GitHubCommands;
import org.springframework.data.release.misc.ReleaseOperations;
import org.springframework.data.release.model.ArtifactVersion;
import org.springframework.data.release.model.ModuleIteration;
import org.springframework.data.release.model.Phase;
import org.springframework.data.release.model.Project;
import org.springframework.data.release.model.Projects;
import org.springframework.data.release.model.ReleaseTrains;
import org.springframework.data.release.model.Train;
import org.springframework.data.release.model.TrainIteration;
import org.springframework.shell.core.annotation.CliCommand;
import org.springframework.shell.core.annotation.CliOption;
import org.springframework.util.Assert;
/**
* @author Oliver Gierke
*/
@CliComponent
@RequiredArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
class ReleaseCommands extends TimedCommand {
@NonNull GitOperations git;
@NonNull ReleaseOperations misc;
@NonNull DeploymentOperations deployment;
@NonNull BuildOperations build;
@NonNull IssueTrackerCommands tracker;
@NonNull GitHubCommands gitHub;
@CliCommand("release predict")
public String predictTrainAndIteration() {
return git.getTags(COMMONS).getLatest().toArtifactVersion().//
map(ReleaseCommands::getTrainNameForCommonsVersion).//
orElse(null);
}
/**
* Composite command to ship a full release.
*
* @param iteration
* @throws Exception
*/
@CliCommand(value = "ship-it")
public void shipIt(@CliOption(key = "", mandatory = true) TrainIteration iteration) throws Exception {
tracker.trackerPrepare(iteration);
prepare(iteration);
buildRelease(iteration, null);
conclude(iteration);
gitHub.push(iteration);
distribute(iteration, null);
tracker.closeIteration(iteration);
}
/**
* Prepares the release of the given iteration of the given train.
*
* @param iteration
* @throws Exception
*/
@CliCommand(value = "release prepare", help = "Prepares the release of the iteration of the given train.")
public void prepare(@CliOption(key = "", mandatory = true) TrainIteration iteration) throws Exception {
git.prepare(iteration);
build.runPreReleaseChecks(iteration);
misc.updateResources(iteration);
build.updateProjectDescriptors(iteration, Phase.PREPARE);
git.commit(iteration, "Prepare %s.");
build.prepareVersions(iteration, Phase.PREPARE);
git.commit(iteration, "Release version %s.");
}
@CliCommand(value = "release build")
public void buildRelease(@CliOption(key = "", mandatory = true) TrainIteration iteration, //
@CliOption(key = "project", mandatory = false) String projectName) {
if (!iteration.getIteration().isPublic()) {
deployment.verifyAuthentication();
}
if (projectName != null) {
Project project = Projects.requiredByName(projectName);
ModuleIteration module = iteration.getModule(project);
DeploymentInformation information = build.performRelease(module);
deployment.promote(information);
} else {
build.performRelease(iteration).forEach(deployment::promote);
}
}
/**
* Concludes the release of the given {@link TrainIteration}.
*
* @param iteration
* @throws Exception
*/
@CliCommand(value = "release conclude")
public void conclude(@CliOption(key = "", mandatory = true) TrainIteration iteration) throws Exception {
Assert.notNull(iteration, "Train iteration must not be null!");
// Tag release
git.tagRelease(iteration);
if (iteration.getTrain().isAlwaysUseBranch()) {
setupMaintenanceVersions(iteration);
} else {
build.prepareVersions(iteration, Phase.CLEANUP);
git.commit(iteration, "Prepare next development iteration.");
// Prepare main branch
build.updateProjectDescriptors(iteration, Phase.CLEANUP);
git.commit(iteration, "After release cleanups.");
// Prepare maintenance branches
if (iteration.getIteration().isGAIteration()) {
// Create bugfix branches
git.createMaintenanceBranches(iteration);
// Set project version to maintenance once
setupMaintenanceVersions(iteration);
}
}
}
private void setupMaintenanceVersions(TrainIteration iteration) throws Exception {
// Set project version to maintenance once
build.prepareVersions(iteration, Phase.MAINTENANCE);
git.commit(iteration, "Prepare next development iteration.");
// Update inter-project dependencies and repositories
build.updateProjectDescriptors(iteration, Phase.MAINTENANCE);
git.commit(iteration, "After release cleanups.");
// Back to main branch
git.checkout(iteration);
}
/**
* Triggers the distribution of release artifacts for all projects.
*
* @param iteration
* @throws Exception
*/
@CliCommand("release distribute")
public void distribute(@CliOption(key = "", mandatory = true) TrainIteration iteration,
@CliOption(key = "project", mandatory = false) String projectName) {
git.checkout(iteration);
if (projectName != null) {
Project project = Projects.requiredByName(projectName);
ModuleIteration module = iteration.getModule(project);
build.distributeResources(module);
} else {
build.distributeResources(iteration);
}
}
private static String getTrainNameForCommonsVersion(ArtifactVersion version) {
return ReleaseTrains.TRAINS.stream().//
filter(train -> version.toString().startsWith(train.getModule(COMMONS).getVersion().toString())).//
findFirst().map(Train::getName).orElse(null);
}
}

View File

@@ -1,74 +0,0 @@
/*
* Copyright 2014-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.cli;
import org.apache.commons.io.IOUtils;
import org.springframework.boot.SpringBootVersion;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.shell.plugin.BannerProvider;
import org.springframework.shell.support.util.FileUtils;
import org.springframework.stereotype.Component;
@Order(Ordered.HIGHEST_PRECEDENCE)
@Component
class SpringDataReleaseCliBannerProvider implements BannerProvider {
/*
* (non-Javadoc)
* @see org.springframework.shell.plugin.NamedProvider#getProviderName()
*/
@Override
public String getProviderName() {
return "Spring Data Release Shell";
}
/*
* (non-Javadoc)
* @see org.springframework.shell.plugin.BannerProvider#getBanner()
*/
@Override
public String getBanner() {
StringBuilder builder = new StringBuilder();
builder.append(FileUtils.readBanner(SpringDataReleaseCliBannerProvider.class, "banner.txt"));
builder.append(getVersion()).append(IOUtils.LINE_SEPARATOR);
builder.append(IOUtils.LINE_SEPARATOR);
return builder.toString();
}
/*
* (non-Javadoc)
* @see org.springframework.shell.plugin.BannerProvider#getVersion()
*/
@Override
public String getVersion() {
return "1.0 on Spring Boot " + SpringBootVersion.getVersion();
}
/*
* (non-Javadoc)
* @see org.springframework.shell.plugin.BannerProvider#getWelcomeMessage()
*/
@Override
public String getWelcomeMessage() {
return "Welcome to the Spring Data Release Shell!";
}
}

View File

@@ -1,47 +0,0 @@
/*
* Copyright 2014-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.cli;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.shell.plugin.PromptProvider;
import org.springframework.stereotype.Component;
/**
* @author Oliver Gierke
*/
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
class SpringDataReleaseCliPromptProvider implements PromptProvider {
/*
* (non-Javadoc)
* @see org.springframework.shell.plugin.PromptProvider#getPrompt()
*/
@Override
public String getPrompt() {
return "$ ";
}
/*
* (non-Javadoc)
* @see org.springframework.shell.plugin.NamedProvider#getProviderName()
*/
@Override
public String getProviderName() {
return "spring-data-release-cli";
}
}

View File

@@ -1,61 +0,0 @@
/*
* Copyright 2014-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.cli;
import lombok.RequiredArgsConstructor;
import org.springframework.data.release.git.GitProject;
import org.springframework.data.release.git.Tag;
import org.springframework.data.release.git.VersionTags;
import org.springframework.data.release.model.ArtifactVersion;
import org.springframework.data.release.model.DocumentationMetadata;
import org.springframework.data.release.model.ModuleIteration;
import org.springframework.data.release.model.Project;
/**
* @author Oliver Gierke
* @author Mark Paluch
*/
@RequiredArgsConstructor
public class StaticResources {
private final DocumentationMetadata metadata;
private final String releaseUrl;
public StaticResources(ModuleIteration module) {
this.metadata = DocumentationMetadata.of(module.getProject(), ArtifactVersion.of(module), false);
Project project = module.getProject();
GitProject gitProject = GitProject.of(project);
Tag tag = VersionTags.empty(module.getProject()).createTag(module);
this.releaseUrl = String.format("%s/releases/tag/%s", gitProject.getProjectUri(), tag.getName());
}
public String getDocumentationUrl() {
return metadata.getReferenceDocUrl();
}
public String getJavaDocUrl() {
return metadata.getApiDocUrl();
}
public String getChangelogUrl() {
return releaseUrl;
}
}

View File

@@ -1,77 +0,0 @@
/*
* Copyright 2014-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.cli;
import java.util.List;
import java.util.regex.Pattern;
import org.springframework.data.release.model.ArtifactVersion;
import org.springframework.data.release.model.ReleaseTrains;
import org.springframework.data.release.model.Train;
import org.springframework.shell.core.Completion;
import org.springframework.shell.core.Converter;
import org.springframework.shell.core.MethodTarget;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
/**
* @author Oliver Gierke
* @author Mark Paluch
*/
@Component
public class TrainConverter implements Converter<Train> {
private static final Pattern CALVER = Pattern.compile("(\\d{4})(\\.(\\d))+");
/*
* (non-Javadoc)
* @see org.springframework.shell.core.Converter#supports(java.lang.Class, java.lang.String)
*/
@Override
public boolean supports(Class<?> type, String optionContext) {
return Train.class.isAssignableFrom(type);
}
/*
* (non-Javadoc)
* @see org.springframework.shell.core.Converter#convertFromText(java.lang.String, java.lang.Class, java.lang.String)
*/
@Override
public Train convertFromText(String value, Class<?> targetType, String optionContext) {
if (StringUtils.isEmpty(value)) {
return null;
}
if (CALVER.matcher(value).matches()) {
ArtifactVersion version = ArtifactVersion.of(value);
return ReleaseTrains.getTrainByCalver(version.getVersion());
}
return ReleaseTrains.getTrainByName(value);
}
/*
* (non-Javadoc)
* @see org.springframework.shell.core.Converter#getAllPossibleValues(java.util.List, java.lang.Class, java.lang.String, java.lang.String, org.springframework.shell.core.MethodTarget)
*/
@Override
public boolean getAllPossibleValues(List<Completion> completions, Class<?> targetType, String existingData,
String optionContext, MethodTarget target) {
return false;
}
}

View File

@@ -1,106 +0,0 @@
/*
* Copyright 2014-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.cli;
import java.util.List;
import java.util.regex.Pattern;
import org.springframework.data.release.model.ArtifactVersion;
import org.springframework.data.release.model.Iteration;
import org.springframework.data.release.model.ReleaseTrains;
import org.springframework.data.release.model.Train;
import org.springframework.data.release.model.TrainIteration;
import org.springframework.shell.core.Completion;
import org.springframework.shell.core.Converter;
import org.springframework.shell.core.MethodTarget;
import org.springframework.stereotype.Component;
/**
* @author Oliver Gierke
* @author Mark Paluch
*/
@Component
public class TrainIterationConverter implements Converter<TrainIteration> {
private static final Pattern CALVER = Pattern.compile("(\\d{4})(\\.(\\d+))+(-M(\\d)|-RC(\\d))?");
/*
* (non-Javadoc)
* @see org.springframework.shell.core.Converter#supports(java.lang.Class, java.lang.String)
*/
@Override
public boolean supports(Class<?> type, String optionContext) {
return TrainIteration.class.equals(type);
}
/*
* (non-Javadoc)
* @see org.springframework.shell.core.Converter#convertFromText(java.lang.String, java.lang.Class, java.lang.String)
*/
@Override
public TrainIteration convertFromText(String value, Class<?> targetType, String optionContext) {
if (CALVER.matcher(value).matches()) {
ArtifactVersion version = ArtifactVersion.of(value);
Train train = ReleaseTrains.getTrainByCalver(version.getVersion());
if (version.isReleaseVersion()) {
if (version.isBugFixVersion()) {
return train.getIteration("SR" + version.getVersion().getBugfix());
}
return train.getIteration(Iteration.GA);
}
return train.getIteration(version.getSuffix());
}
String[] parts = value.split(" ");
if (parts.length != 2) {
throw new IllegalArgumentException(String.format("Cannot resolve TrainIteration from '%s'", value));
}
Train train = ReleaseTrains.getTrainByName(parts[0].trim());
return train.getIteration(parts[1].trim());
}
/*
* (non-Javadoc)
* @see org.springframework.shell.core.Converter#getAllPossibleValues(java.util.List, java.lang.Class, java.lang.String, java.lang.String, org.springframework.shell.core.MethodTarget)
*/
@Override
public boolean getAllPossibleValues(List<Completion> completions, Class<?> targetType, String existingData,
String optionContext, MethodTarget target) {
for (Train train : ReleaseTrains.TRAINS) {
for (Iteration iteration : train.getIterations()) {
TrainIteration trainIteration = train.getIteration(iteration.getName());
completions.add(new Completion(trainIteration.toString()));
if (trainIteration.getTrain().usesCalver()) {
completions.add(new Completion(trainIteration.getCalver().toMajorMinorBugfix()));
}
}
}
return true;
}
}

View File

@@ -1,71 +0,0 @@
/*
* Copyright 2021-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.cli;
import lombok.AccessLevel;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.experimental.FieldDefaults;
import org.springframework.data.release.CliComponent;
import org.springframework.data.release.TimedCommand;
import org.springframework.data.release.build.BuildOperations;
import org.springframework.data.release.deployment.DeploymentOperations;
import org.springframework.data.release.git.GitOperations;
import org.springframework.data.release.issues.github.GitHub;
import org.springframework.data.release.sagan.SaganClient;
import org.springframework.data.release.utils.Logger;
import org.springframework.shell.core.annotation.CliCommand;
/**
* Commands to verify a correct Release Tools Setup.
*
* @author Mark Paluch
*/
@CliComponent
@RequiredArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
class VerifyCommands extends TimedCommand {
@NonNull GitOperations git;
@NonNull GitHub github;
@NonNull DeploymentOperations deployment;
@NonNull BuildOperations build;
@NonNull SaganClient saganClient;
@NonNull Logger logger;
@CliCommand("verify")
public void verifyReleaseTools() {
// Git checkout build
git.verify();
// Maven interaction
build.verify();
// Artifactory verification
deployment.verifyAuthentication();
// GitHub verification
github.verifyAuthentication();
// Sagan Verification
saganClient.verifyAuthentication();
logger.log("Verify", "All settings are verified. You can ship a release now.");
}
}

View File

@@ -1,111 +0,0 @@
/*
* Copyright 2015-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.deployment;
import lombok.RequiredArgsConstructor;
import lombok.Value;
import java.io.IOException;
import java.net.URI;
import java.util.function.Consumer;
import org.springframework.data.release.model.ModuleIteration;
import org.springframework.data.release.utils.Logger;
import org.springframework.util.Assert;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestOperations;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* A client to interact with Artifactory.
*
* @author Oliver Gierke
* @author Mark Paluch
*/
@RequiredArgsConstructor
class ArtifactoryClient {
private final RestOperations template;
private final Logger logger;
private final DeploymentProperties properties;
/**
* Triggers the promotion of the artifacts identified by the given {@link DeploymentInformation}.
*
* @param information must not be {@literal null}.
*/
public void promote(DeploymentInformation information) {
Assert.notNull(information, "DeploymentInformation must not be null!");
ModuleIteration module = information.getModule();
URI uri = properties.getServer().getPromotionResource(information);
logger.log(module, "Promoting %s %s from %s to %s.", information.getBuildName(), information.getBuildNumber(),
properties.getStagingRepository(), information.getTargetRepository());
try {
template.postForEntity(uri,
new PromotionRequest(information.getTargetRepository(), properties.getStagingRepository()), String.class);
} catch (HttpClientErrorException o_O) {
handle(message -> logger.warn(information.getModule(), message), "Promotion failed!", o_O);
}
}
public void verify() {
URI verificationResource = properties.getServer().getVerificationResource();
try {
logger.log("Artifactory", "Verifying authentication using a GET call to %s.", verificationResource);
template.getForEntity(verificationResource, String.class);
logger.log("Artifactory", "Authentication verified!");
} catch (HttpClientErrorException o_O) {
handle(message -> logger.log("Artifactory Client", message), "Authentication verification failed!", o_O);
throw new IllegalStateException("Authentication verification failed!");
}
}
private void handle(Consumer<Object> logger, String message, HttpClientErrorException o_O) {
try {
logger.accept(message);
Errors errors = new ObjectMapper().readValue(o_O.getResponseBodyAsByteArray(), Errors.class);
errors.getErrors().forEach(logger);
errors.getMessages().forEach(logger);
} catch (IOException e) {
o_O.addSuppressed(e);
throw new RuntimeException(o_O.getResponseBodyAsString(), o_O);
}
}
public void deleteArtifacts(DeploymentInformation information) {
template.delete(properties.getServer().getDeleteBuildResource(information));
}
@Value
static class PromotionRequest {
String targetRepo, sourceRepo;
}
}

View File

@@ -1,40 +0,0 @@
/*
* Copyright 2016-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.deployment;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.springframework.data.release.CliComponent;
import org.springframework.data.release.TimedCommand;
import org.springframework.shell.core.annotation.CliCommand;
/**
* Commands to interact with Artifactory.
*
* @author Oliver Gierke
*/
@CliComponent
@RequiredArgsConstructor
class ArtifactoryCommands extends TimedCommand {
private final @NonNull DeploymentOperations deployment;
@CliCommand(value = "artifactory verify", help = "Verifies authentication at Artifactory.")
public void verify() {
deployment.verifyAuthentication();
}
}

View File

@@ -1,96 +0,0 @@
/*
* Copyright 2015-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.deployment;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.HashMap;
import java.util.Map;
import org.springframework.data.release.model.ModuleIteration;
import org.springframework.web.util.UriTemplate;
/**
* Information about a deployment.
*
* @author Oliver Gierke
*/
@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
public class DefaultDeploymentInformation implements DeploymentInformation {
private static UriTemplate REPOSITORY_TEMPLATE = new UriTemplate(
"artifactory::default::{server};build.number={buildNumber};build.name={buildName}");
private final @Getter @NonNull ModuleIteration module;
private final @NonNull DeploymentProperties properties;
private final @Getter String buildNumber;
public DefaultDeploymentInformation(ModuleIteration module, DeploymentProperties properties) {
this(module, properties, String.valueOf(LocalDateTime.now().toEpochSecond(ZoneOffset.UTC)));
}
/*
* (non-Javadoc)
* @see org.springframework.data.release.deployment.DeploymentInformation#getBuildName()
*/
@Override
public String getBuildName() {
return module.getProject().getFullName().concat(" - Release");
}
/*
* (non-Javadoc)
* @see org.springframework.data.release.deployment.DeploymentInformation#getTargetRepository()
*/
@Override
public String getTargetRepository() {
return properties.getRepositoryPrefix()
.concat(module.getIteration().isPublic() ? "libs-release-local" : "libs-milestone-local");
}
/*
* (non-Javadoc)
* @see org.springframework.data.release.deployment.DeploymentInformation#getDeploymentTargetUrl()
*/
@Override
public String getDeploymentTargetUrl() {
Map<String, Object> parameters = new HashMap<>();
parameters.put("server", properties.getStagingRepositoryUrl());
parameters.putAll(getBuildInfoParameters());
return REPOSITORY_TEMPLATE.expand(parameters).toString();
}
/*
* (non-Javadoc)
* @see org.springframework.data.release.deployment.DeploymentInformation#getBuildInfoParameters()
*/
@Override
public Map<String, Object> getBuildInfoParameters() {
Map<String, Object> parameters = new HashMap<>();
parameters.put("buildNumber", getBuildNumber());
parameters.put("buildName", getBuildName());
return parameters;
}
}

View File

@@ -1,77 +0,0 @@
/*
* Copyright 2015-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.deployment;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import java.io.IOException;
import java.util.Arrays;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.release.utils.Logger;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.web.client.RestTemplate;
/**
* Configuration to set up deployment components.
*
* @author Oliver Gierke
*/
@Configuration(proxyBeanMethods = false)
class DeploymentConfiguration {
@Autowired DeploymentProperties properties;
@Bean
public ArtifactoryClient client(Logger logger, RestTemplate artifactoryRestTemplate) {
return new ArtifactoryClient(artifactoryRestTemplate, logger, properties);
}
@Bean
public RestTemplate artifactoryRestTemplate() {
RestTemplate template = new RestTemplate();
template.setInterceptors(Arrays.asList(new AuthenticatingClientHttpRequestInterceptor(properties)));
return template;
}
@RequiredArgsConstructor
private static class AuthenticatingClientHttpRequestInterceptor implements ClientHttpRequestInterceptor {
private final @NonNull DeploymentProperties properties;
/*
* (non-Javadoc)
* @see org.springframework.http.client.ClientHttpRequestInterceptor#intercept(org.springframework.http.HttpRequest, byte[], org.springframework.http.client.ClientHttpRequestExecution)
*/
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
throws IOException {
request.getHeaders().add("X-Api-Key", properties.getApiKey());
request.getHeaders().add("Authentication", properties.getCredentials().toString());
return execution.execute(request, body);
}
}
}

View File

@@ -1,68 +0,0 @@
/*
* Copyright 2016-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.deployment;
import java.util.Map;
import org.springframework.data.release.model.ModuleIteration;
/**
* @author Oliver Gierke
*/
public interface DeploymentInformation {
/**
* Returns the name of the build.
*
* @return will never be {@literal null} or empty.
*/
String getBuildName();
/**
* Returns a unique build number for this particular deployment.
*
* @return will never be {@literal null} or empty.
*/
String getBuildNumber();
/**
* Returns the full URL to be used as deployment target.
*
* @return will never be {@literal null} or empty.
*/
String getDeploymentTargetUrl();
/**
* Returns the name of the repository to deploy to.
*
* @return will never be {@literal null} or empty.
*/
String getTargetRepository();
/**
* Returns the {@link ModuleIteration} the deployment information was created for.
*
* @return
*/
ModuleIteration getModule();
/**
* Returns a {@link Map} to expand a URI template to access the build information.
*
* @return
*/
Map<String, Object> getBuildInfoParameters();
}

View File

@@ -1,76 +0,0 @@
/*
* Copyright 2015-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.deployment;
import lombok.RequiredArgsConstructor;
import org.springframework.data.release.utils.Logger;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
/**
* Deployment functionality.
*
* @author Oliver Gierke
* @author Mark Paluch
*/
@Component
@RequiredArgsConstructor
public class DeploymentOperations {
private final ArtifactoryClient client;
private final Logger logger;
public void verifyAuthentication() {
client.verify();
}
/**
* Promotes the artifacts identified by the given {@link DeploymentInformation}.
*
* @param information must not be {@literal null}.
*/
public void promote(DeploymentInformation information) {
Assert.notNull(information, "DeploymentInformation must not be null!");
if (information.getModule().getIteration().isPublic()) {
logger.log(information.getModule(),
"Skipping build promotion as it's a public version and was staged to OSS Sonatype.");
return;
}
client.promote(information);
}
/**
* Rolls back the given {@link DeploymentInformation}.
*
* @param information must not be {@literal null}.
*/
public void rollback(DeploymentInformation information) {
Assert.notNull(information, "DeploymentInformation must not be null!");
if (information.getModule().getIteration().isPublic()) {
logger.log(information.getModule(),
"Skipping build rollback as it's a public version and was deployed to Maven Central directly,");
return;
}
client.deleteArtifacts(information);
}
}

View File

@@ -1,120 +0,0 @@
/*
* Copyright 2015-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.deployment;
import lombok.Data;
import java.net.URI;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.data.release.model.Password;
import org.springframework.data.release.utils.HttpBasicCredentials;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.web.util.UriTemplate;
/**
* @author Oliver Gierke
* @author Mark Paluch
*/
@Data
@Component
@ConfigurationProperties(prefix = "deployment")
public class DeploymentProperties {
/**
* The Artifactory host.
*/
private Server server;
/**
* The deployer's username.
*/
private String username;
private String apiKey;
/**
* The deployer's password.
*/
private Password password;
/**
* The repository to deploy the artifacts to.
*/
private String stagingRepository;
/**
* The repository to deploy docs/schemas to.
*/
private String distributionRepository;
private String repositoryPrefix = "";
public String getStagingRepository() {
return repositoryPrefix.concat(stagingRepository);
}
public String getDistributionRepository() {
return repositoryPrefix.concat(distributionRepository);
}
/**
* Returns the URI of the staging repository.
*
* @return
*/
public String getStagingRepositoryUrl() {
return server.getUri().toString().concat("/").concat(stagingRepository);
}
public HttpBasicCredentials getCredentials() {
return new HttpBasicCredentials(username, password);
}
@Data
public static class Server {
private static final String PROMOTION_RESOURCE = "/api/build/promote/{buildName}/{buildNumber}";
private static final String DELETE_BUILD_RESOURCE = "/api/build/{buildName}?buildNumbers={buildNumber}&artifacts=1";
private static final String VERIFICATION_RESOURCE = "/api/storage/temp-private-local";
private String uri;
/**
* Returns the URI to the resource that a promotion can be triggered at.
*
* @param information must not be {@literal null}.
* @return
*/
public URI getPromotionResource(DeploymentInformation information) {
Assert.notNull(information, "DeploymentInformation must not be null!");
return new UriTemplate(uri.concat(PROMOTION_RESOURCE)).expand(information.getBuildInfoParameters());
}
public URI getDeleteBuildResource(DeploymentInformation information) {
return new UriTemplate(uri.concat(DELETE_BUILD_RESOURCE)).expand(information.getBuildInfoParameters());
}
public URI getVerificationResource() {
return URI.create(uri.concat(VERIFICATION_RESOURCE));
}
}
}

View File

@@ -1,56 +0,0 @@
/*
* Copyright 2015-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.deployment;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* @author Oliver Gierke
*/
@Data
public class Errors {
private List<Error> errors = new ArrayList<>();
private List<Message> messages = new ArrayList<>();
public List<Error> getErrors(Errors this) {
return errors;
}
@Data
static class Error {
private String message;
private int status;
public String toString() {
return String.format("%s - %s", status, message);
}
}
@Data
static class Message {
private String level, message;
public String toString() {
return String.format("%s - %s", level, message);
}
}
}

View File

@@ -1,66 +0,0 @@
/*
* Copyright 2016-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.git;
import lombok.Getter;
import java.util.Collection;
import java.util.Iterator;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.bouncycastle.util.Iterable;
import org.springframework.data.release.model.ModuleIteration;
import org.springframework.data.release.model.Train;
import org.springframework.util.Assert;
class BackportTargets implements Iterable<Branch> {
private final @Getter Branch source;
private final Set<Branch> targets;
/**
* Creates a new {@link BackportTargets} instance for the given {@link ModuleIteration} and
*
* @param module must not be {@literal null}.
* @param targets must not be {@literal null}.
*/
public BackportTargets(ModuleIteration module, Collection<Train> targets) {
Assert.notNull(module, "Module iteration must not be null!");
Assert.notNull(targets, "Target trains must not be null!");
this.source = Branch.from(module);
Stream<Branch> branches = targets.stream().map(target -> target.getModuleIfAvailable(module.getProject()))//
.flatMap(o -> o.map(Stream::of).orElse(Stream.empty()))//
.map(Branch::from);
this.targets = Stream.concat(branches, source.isMainBranch() ? Stream.empty() : Stream.of(Branch.MAIN))
.collect(Collectors.toSet());
}
/*
* (non-Javadoc)
* @see java.lang.Iterable#iterator()
*/
@Override
public Iterator<Branch> iterator() {
return targets.iterator();
}
}

View File

@@ -1,113 +0,0 @@
/*
* Copyright 2014-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.git;
import lombok.AccessLevel;
import lombok.EqualsAndHashCode;
import lombok.RequiredArgsConstructor;
import org.springframework.data.release.model.IterationVersion;
import org.springframework.data.release.model.Tracker;
import org.springframework.data.release.model.Version;
import org.springframework.data.release.model.VersionAware;
import org.springframework.util.Assert;
/**
* Value type to represent an SCM branch.
*
* @author Oliver Gierke
* @author Mark Paluch
*/
@EqualsAndHashCode
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public class Branch implements Comparable<Branch> {
public static final Branch MAIN = new Branch("main");
private final String name;
/**
* Creates a new {@link Branch} from the given {@link IterationVersion}.
*
* @param iterationVersion must not be {@literal null}.
* @return
*/
public static Branch from(IterationVersion iterationVersion) {
Assert.notNull(iterationVersion, "Iteration version must not be null!");
if (iterationVersion.isBranchVersion()) {
return from((VersionAware) iterationVersion);
}
return MAIN;
}
public static Branch from(VersionAware versioned) {
return from(versioned.getVersion());
}
public static Branch from(Version version) {
return from(version.toString().concat(".x"));
}
/**
* Creates a new {@link Branch} from the given name. Uses the local part of it only.
*
* @param name must not be {@literal null} or empty.
* @return
*/
public static Branch from(String name) {
int slashIndex = name.lastIndexOf('/');
return new Branch(slashIndex != -1 ? name.substring(slashIndex + 1) : name);
}
public boolean isMainBranch() {
return MAIN.equals(this);
}
/**
* Returns whether the current branch is an issue branch for the given {@link Tracker}.
*
* @param tracker must not be {@literal null}.
* @return
*/
public boolean isIssueBranch(Tracker tracker) {
Assert.notNull(tracker, "Tracker must not be null!");
return name.matches(tracker.getTicketPattern());
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return name;
}
/*
* (non-Javadoc)
* @see java.lang.Comparable#compareTo(java.lang.Object)
*/
@Override
public int compareTo(Branch o) {
return name.compareToIgnoreCase(o.name);
}
}

View File

@@ -1,66 +0,0 @@
/*
* Copyright 2016-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.git;
import lombok.EqualsAndHashCode;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.util.Assert;
/**
* Value object to represent a collection of {@link Branch}es.
*
* @author Mark Paluch
*/
@EqualsAndHashCode
public class Branches implements Iterable<Branch> {
private final List<Branch> branches;
/**
* Creates a new {@link Branches} instance for the given {@link List} of {@link Branch}es.
*
* @param source must not be {@literal null}.
*/
Branches(List<Branch> source) {
Assert.notNull(source, "Tags must not be null!");
this.branches = source.stream().//
sorted().collect(Collectors.toList());
}
/**
* Returns all {@link Branch}es as {@link List}.
*
* @return
*/
public List<Branch> asList() {
return branches;
}
/*
* (non-Javadoc)
* @see java.lang.Iterable#iterator()
*/
@Override
public Iterator<Branch> iterator() {
return branches.iterator();
}
}

View File

@@ -1,67 +0,0 @@
/*
* Copyright 2014-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.git;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Optional;
import org.springframework.data.release.issues.Ticket;
/**
* @author Oliver Gierke
* @author Mark Paluch
*/
@EqualsAndHashCode
@RequiredArgsConstructor
public class Commit {
private final Ticket ticket;
@Getter
private final String summary;
private final Optional<String> details;
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append(summary);
if (!summary.endsWith(".")) {
builder.append(".");
}
details.ifPresent(it -> {
builder.append("\n");
builder.append("\n");
builder.append(it);
});
builder.append("\n\nSee ").append(ticket.getId());
return builder.toString();
}
}

View File

@@ -1,202 +0,0 @@
/*
* Copyright 2014-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.git;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.Executor;
import java.util.stream.Collectors;
import org.springframework.data.release.CliComponent;
import org.springframework.data.release.TimedCommand;
import org.springframework.data.release.issues.Changelog;
import org.springframework.data.release.issues.IssueTracker;
import org.springframework.data.release.issues.Ticket;
import org.springframework.data.release.issues.TicketReference;
import org.springframework.data.release.issues.Tickets;
import org.springframework.data.release.model.ArtifactVersion;
import org.springframework.data.release.model.ModuleIteration;
import org.springframework.data.release.model.Project;
import org.springframework.data.release.model.Projects;
import org.springframework.data.release.model.ReleaseTrains;
import org.springframework.data.release.model.Train;
import org.springframework.data.release.model.TrainIteration;
import org.springframework.data.release.utils.ExecutionUtils;
import org.springframework.plugin.core.PluginRegistry;
import org.springframework.shell.core.annotation.CliCommand;
import org.springframework.shell.core.annotation.CliOption;
import org.springframework.shell.support.table.Table;
import org.springframework.shell.support.table.TableHeader;
import org.springframework.util.StringUtils;
/**
* @author Oliver Gierke
*/
@CliComponent
@SuppressWarnings("deprecation")
@RequiredArgsConstructor
class GitCommands extends TimedCommand {
private final PluginRegistry<IssueTracker, Project> trackers;
private final @NonNull GitOperations git;
private final @NonNull Executor executor;
@CliCommand("git co-train")
public void checkout(@CliOption(key = "", mandatory = true) Train train) throws Exception {
git.checkout(train);
}
@CliCommand("git co")
public void checkout(@CliOption(key = "", mandatory = true) TrainIteration iteration) throws Exception {
git.checkout(iteration);
}
@CliCommand("git update")
public void update(@CliOption(key = { "", "train" }, mandatory = true) String trainName)
throws Exception, InterruptedException {
git.update(ReleaseTrains.getTrainByName(trainName));
}
@CliCommand("git tags")
public String tags(@CliOption(key = { "project" }, mandatory = true) String projectName) throws Exception {
Project project = ReleaseTrains.getProjectByName(projectName);
return StringUtils.collectionToDelimitedString(git.getTags(project).asList(), "\n");
}
@CliCommand("git previous")
public String previous(@CliOption(key = "", mandatory = true) TrainIteration iteration) throws Exception {
return git.getPreviousIteration(iteration).toString();
}
@CliCommand("git changelog")
public String changelog(@CliOption(key = "", mandatory = true) TrainIteration iteration,
@CliOption(key = "module") String moduleName) {
TrainIteration previousIteration = git.getPreviousIteration(iteration);
if (StringUtils.hasText(moduleName)) {
ModuleIteration module = iteration.getModule(Projects.requiredByName(moduleName));
List<TicketReference> ticketRefs = git.getTicketReferencesBetween(module.getProject(), previousIteration,
iteration);
Changelog changelog = Changelog.of(module, toTickets(module, ticketRefs));
return String.format("%s %s%n%s", module.getModule().getProject().getFullName(), ArtifactVersion.of(module),
changelog.toString(false, " "));
}
return ExecutionUtils
.runAndReturn(executor, iteration, module -> changelog(iteration, module.getModule().getProject().getName())) //
.stream() //
.collect(Collectors.joining("\n"));
}
private Tickets toTickets(ModuleIteration module, List<TicketReference> ticketReferences) {
IssueTracker issueTracker = trackers.getRequiredPluginFor(module.getProject(),
() -> String.format("No issue tracker found for project %s!", module.getProject()));
List<String> ticketIds = ticketReferences.stream().map(TicketReference::getId).collect(Collectors.toList());
List<Ticket> tickets = new ArrayList<>(issueTracker.findTickets(module, ticketIds).getTickets());
return new Tickets(tickets);
}
/**
* Resets all projects contained in the given {@link Train}.
*
* @param iteration
* @throws Exception
*/
@CliCommand("git reset")
public void reset(@CliOption(key = "", mandatory = true) TrainIteration iteration) throws Exception {
git.reset(iteration);
}
@CliCommand("git prepare")
public void prepare(@CliOption(key = "", mandatory = true) TrainIteration iteration) throws Exception {
git.prepare(iteration);
}
/**
* Pushes all changes of all modules of the given {@link TrainIteration} to the remote server. If {@code tags} is
* given, only the tags are pushed.
*
* @param iteration
* @param tags
* @throws Exception
*/
@CliCommand("git push")
public void push(//
@CliOption(key = "", mandatory = true) TrainIteration iteration, //
@CliOption(key = "tags", specifiedDefaultValue = "true", unspecifiedDefaultValue = "false") String tags)
throws Exception {
boolean pushTags = Boolean.parseBoolean(tags);
if (pushTags) {
git.pushTags(iteration.getTrain());
} else {
git.push(iteration);
}
}
@CliCommand("git remove tags")
public void removeTags(@CliOption(key = "", mandatory = true) TrainIteration iteration) {
git.removeTags(iteration);
}
/**
* List the branches with their tickets of the git repository.
*
* @param projectName
* @return
* @throws Exception
*/
@CliCommand("git issuebranches")
public Table issuebranches(@CliOption(key = { "" }, mandatory = true) String projectName,
@CliOption(key = "resolved", unspecifiedDefaultValue = "false", specifiedDefaultValue = "true") Boolean resolved)
throws Exception {
Project project = ReleaseTrains.getProjectByName(projectName);
TicketBranches ticketBranches = git.listTicketBranches(project);
Table table = new Table();
table.addHeader(1, new TableHeader("Branch"));
table.addHeader(2, new TableHeader("Status"));
table.addHeader(3, new TableHeader("Description"));
ticketBranches.stream().sorted().//
filter(branch -> ticketBranches.hasTicketFor(branch, resolved)).//
forEachOrdered(branch -> {
Optional<Ticket> ticket = ticketBranches.findTicket(branch);
table.addRow(branch.toString(), //
ticket.map(t -> t.getTicketStatus().getLabel()).orElse(""), //
ticket.map(t -> t.getSummary()).orElse(""));
});
return table;
}
}

View File

@@ -1,59 +0,0 @@
/*
* Copyright 2014-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.git;
import lombok.AccessLevel;
import lombok.EqualsAndHashCode;
import lombok.RequiredArgsConstructor;
import org.springframework.data.release.model.Project;
import org.springframework.data.release.model.Projects;
/**
* @author Oliver Gierke
*/
@EqualsAndHashCode
@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
public class GitProject {
private static final String PROJECT_PREFIX = "spring-data";
private final Project project;
private final GitServer server;
public static GitProject of(Project project) {
return new GitProject(project, GitServer.INSTANCE);
}
/**
* Returns the name of the repository the project is using.
*
* @return
*/
public String getRepositoryName() {
return String.format("%s-%s", PROJECT_PREFIX,
project == Projects.JDBC ? "relational" : project.getName().toLowerCase());
}
/**
* Returns the URI of the {@link Project}'s repository.
*
* @return
*/
public String getProjectUri() {
return server.getUri() + getRepositoryName();
}
}

View File

@@ -1,67 +0,0 @@
/*
* Copyright 2015-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.git;
import lombok.AccessLevel;
import lombok.Data;
import lombok.Getter;
import javax.annotation.PostConstruct;
import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.data.release.model.Password;
import org.springframework.data.release.utils.HttpBasicCredentials;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
/**
* Configurable properties for Git.
*
* @author Oliver Gierke
* @author Mark Paluch
*/
@Data
@Component
@ConfigurationProperties(prefix = "git")
public class GitProperties {
private @Getter(AccessLevel.PRIVATE) Password password;
private String username, author, email;
@PostConstruct
public void init() {
Assert.hasText(username, "No GitHub username (git.username) configured!");
Assert.notNull(password, "No GitHub password (git.password) configured!");
Assert.hasText(author, "No Git author (git.author) configured!");
Assert.hasText(email, "No Git email (git.email) configured!");
}
/**
* Returns the jGit {@link CredentialsProvider} to be used.
*
* @return
*/
public CredentialsProvider getCredentials() {
return new UsernamePasswordCredentialsProvider(username, password.toString());
}
public HttpBasicCredentials getHttpCredentials() {
return new HttpBasicCredentials(username, password);
}
}

View File

@@ -1,30 +0,0 @@
/*
* Copyright 2014-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.git;
/**
* @author Oliver Gierke
*/
public class GitServer {
public static final GitServer INSTANCE = new GitServer();
private static final String SERVER_URI = "https://github.com/spring-projects/";
public String getUri() {
return SERVER_URI;
}
}

View File

@@ -1,245 +0,0 @@
/*
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.git;
import lombok.Getter;
import lombok.ToString;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.regex.MatchResult;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.data.release.issues.TicketReference;
import org.springframework.lang.Nullable;
/**
* Value object representing a parsed commit message. The {@link #parse(String)} method inspects a commit message to
* extract a {@link TicketReference}, related tickets, a pull request reference and summary/body from the commit. Commit
* messages may used {@code &lt;ticket&gt; - summary} syntax for Jira and GitHub tickets (gh- and # notation). This
* parser also supports {@code Original pull request}, {@code Related ticket} and GitHub close keywords.
*
* @author Mark Paluch
*/
@Getter
@ToString
class ParsedCommitMessage {
private static final Pattern JIRA_TICKET = Pattern.compile("(?>\\[)?([A-Z]+[ ]?-[ ]?\\d+)(?>\\])?");
private static final Pattern GITHUB_TICKET = Pattern.compile("((?>#|gh-)\\d+)");
private static final Pattern GITHUB_CLOSE_SYNTAX = Pattern.compile(
"(?>closes|closed|close|fixes|fixed|fix|resolves|resolved|resolve|see|related to)[\\s:]*((?>#|gh-)\\d+)",
Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
private static final Pattern GITHUB_PREFIX_SYNTAX = Pattern.compile("^(#\\d+)");
private static final Pattern A_TICKET = Pattern
.compile(String.format("(%s|%s)", JIRA_TICKET.pattern(), GITHUB_TICKET.pattern()));
private static final Pattern ORIGINAL_PULL_REQUEST = Pattern
.compile("Original (?>pull request|PR|pullrequest)[:]*(?>\\s+)?" + A_TICKET.pattern(), Pattern.CASE_INSENSITIVE);
private static final Pattern RELATED_TICKET = Pattern.compile(
"Related (?>tickets|ticket)[:]*(?>\\s+)?((" + A_TICKET.pattern() + "(?>[\\s,]*))+)", Pattern.CASE_INSENSITIVE);
private final String summary;
private final @Nullable String body;
private final TicketReference ticketReference;
private final TicketReference pullRequestReference;
private final List<TicketReference> relatedTickets;
private ParsedCommitMessage(String summary, @Nullable String body) {
this.summary = summary;
this.body = body;
TicketReference ticketReference = null;
TicketReference pullRequestReference = null;
// DATACASS-nnn - syntax
Optional<TicketReference> jiraTicket = tryParseJiraTicketReference(summary);
if (jiraTicket.isPresent()) {
ticketReference = jiraTicket.get();
}
// Closes (gh-nnn|#nnn) syntax
Matcher gitHubMatcher = GITHUB_CLOSE_SYNTAX.matcher(summary + "\n" + body);
// #nnn syntax
Optional<TicketReference> gitHubTicket = tryParseGitHubTicketReference(summary);
if (gitHubTicket.isPresent()) {
ticketReference = gitHubTicket.get();
} else {
if (gitHubMatcher.find()) {
ticketReference = new TicketReference(gitHubMatcher.group(1), summary, TicketReference.Style.GitHub);
}
}
List<TicketReference> relatedTickets = parseRelatedTickets(body, gitHubMatcher);
Optional<TicketReference> optionalOriginalPr = parsePullRequestReference(body);
if (optionalOriginalPr.isPresent()) {
pullRequestReference = optionalOriginalPr.get();
if (ticketReference == null) {
ticketReference = pullRequestReference;
pullRequestReference = null;
}
}
this.ticketReference = ticketReference;
this.pullRequestReference = pullRequestReference;
this.relatedTickets = relatedTickets;
}
/**
* Parse a commit message into {@link ParsedCommitMessage}.
*
* @param message
* @return
*/
public static ParsedCommitMessage parse(String message) {
int lineBreak = message.indexOf('\n');
String summary;
String body;
if (lineBreak > -1) {
summary = message.substring(0, lineBreak).trim();
body = message.substring(lineBreak + 1).trim();
} else {
summary = message.trim();
body = null;
}
return new ParsedCommitMessage(summary, body);
}
protected static Optional<TicketReference> tryParseGitHubTicketReference(String summary) {
Matcher gitHubPrefixMatcher = GITHUB_PREFIX_SYNTAX.matcher(summary);
if (gitHubPrefixMatcher.find()) {
MatchResult mr = gitHubPrefixMatcher.toMatchResult();
if (mr.start(1) == 0) {
int summaryStart = findSummaryIndex(summary, mr.end(1));
return Optional.of(new TicketReference(gitHubPrefixMatcher.group(1).toUpperCase(Locale.ROOT),
summaryStart > -1 ? summary.substring(summaryStart) : summary, TicketReference.Style.GitHub));
}
}
return Optional.empty();
}
protected static Optional<TicketReference> tryParseJiraTicketReference(String summary) {
Matcher jiraMatcher = JIRA_TICKET.matcher(summary);
if (jiraMatcher.find()) {
MatchResult mr = jiraMatcher.toMatchResult();
// allow […] syntax and start of message syntax
if (mr.start(1) < 2) {
int summaryStart = findSummaryIndex(summary, mr.end(1));
return Optional.of(new TicketReference(jiraMatcher.group(1).toUpperCase(Locale.ROOT),
summaryStart > -1 ? summary.substring(summaryStart) : summary, TicketReference.Style.Jira));
}
}
return Optional.empty();
}
protected static Optional<TicketReference> parsePullRequestReference(String body) {
if (body != null) {
Matcher prMatcher = ORIGINAL_PULL_REQUEST.matcher(body);
if (prMatcher.find()) {
return extractTicket(prMatcher.group(1));
}
}
return Optional.empty();
}
protected static List<TicketReference> parseRelatedTickets(String body, Matcher gitHubMatcher) {
List<TicketReference> relatedTickets = new ArrayList<>();
if (body != null) {
Matcher relatedTicketsMatcher = RELATED_TICKET.matcher(body);
if (relatedTicketsMatcher.find()) {
String ticketIds[] = relatedTicketsMatcher.group(1).split(",");
for (String ticketId : ticketIds) {
extractTicket(ticketId).ifPresent(relatedTickets::add);
}
}
while (gitHubMatcher.find()) {
extractTicket(gitHubMatcher.group(1)).ifPresent(relatedTickets::add);
}
}
return relatedTickets;
}
protected static Optional<TicketReference> extractTicket(String ticketId) {
if (JIRA_TICKET.matcher(ticketId.trim()).matches()) {
return Optional.of(new TicketReference(ticketId.trim(), null, TicketReference.Style.Jira));
}
if (GITHUB_TICKET.matcher(ticketId.trim()).matches()) {
return Optional.of(new TicketReference(ticketId.trim(), null, TicketReference.Style.GitHub));
}
return Optional.empty();
}
private static int findSummaryIndex(String summary, int startAt) {
int dash = summary.indexOf("- ", startAt);
if (dash > -1) {
return dash + 2;
}
int space = summary.indexOf(" ", startAt);
if (space > -1) {
return space + 1;
}
return -1;
}
}

View File

@@ -1,111 +0,0 @@
/*
* Copyright 2014-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.git;
import lombok.AccessLevel;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.time.LocalDateTime;
import java.util.Optional;
import org.springframework.data.release.model.ArtifactVersion;
/**
* Value object to represent an SCM tag.
*
* @author Oliver Gierke
* @author Mark Paluch
*/
@EqualsAndHashCode
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
@Getter
public class Tag implements Comparable<Tag> {
private final String name;
private final LocalDateTime creationDate;
public static Tag of(String source) {
return of(source, LocalDateTime.now());
}
public static Tag of(String source, LocalDateTime creationDate) {
int slashIndex = source.lastIndexOf('/');
return new Tag(source.substring(slashIndex == -1 ? 0 : slashIndex + 1), creationDate);
}
/**
* Returns the part of the name of the tag that is suitable to derive a version from the tag. Will transparently strip
* a {@code v} prefix from the name.
*
* @return
*/
private String getVersionSource() {
return name.startsWith("v") ? name.substring(1) : name;
}
public boolean isVersionTag() {
return toArtifactVersion().isPresent();
}
public Optional<ArtifactVersion> toArtifactVersion() {
try {
return Optional.of(getRequiredArtifactVersion());
} catch (IllegalArgumentException o_O) {
return Optional.empty();
}
}
public ArtifactVersion getRequiredArtifactVersion() {
return ArtifactVersion.of(getVersionSource());
}
/**
* Creates a new {@link Tag} for the given {@link ArtifactVersion} based on the format of the current one.
*
* @param version
* @return
*/
public Tag createNew(ArtifactVersion version) {
return new Tag(name.startsWith("v") ? "v".concat(version.toString()) : version.toString(), LocalDateTime.now());
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return name;
}
/*
* (non-Javadoc)
* @see java.lang.Comparable#compareTo(java.lang.Object)
*/
@Override
public int compareTo(Tag that) {
// Prefer artifact versions but fall back to name comparison
return toArtifactVersion().map(left -> that.toArtifactVersion().map(right -> left.compareTo(right)).//
orElse(name.compareTo(that.name))).orElse(name.compareTo(that.name));
}
}

View File

@@ -1,93 +0,0 @@
/*
* Copyright 2016-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.git;
import lombok.EqualsAndHashCode;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.stream.Collectors;
import org.springframework.data.release.issues.Ticket;
import org.springframework.data.util.Streamable;
import org.springframework.util.Assert;
/**
* Value object to represent a collection of {@link Branch}es with assigned tickets.
*
* @author Mark Paluch
* @author Oliver Gierke
*/
@EqualsAndHashCode
@RequiredArgsConstructor(staticName = "from")
public class TicketBranches implements Streamable<Branch> {
private final @NonNull Map<Branch, Ticket> ticketBranches;
/**
* Returns whether there's a ticket available for the given {@link Branch}. If {@code requireResolved} is set to
* {@literal true} the answer will only be true if the available ticket is marked resolved.
*
* @param branch must not be {@literal null}.
* @param requireResolved whether the {@link Ticket} we look for is required to be resolved.
* @return
*/
public boolean hasTicketFor(Branch branch, boolean requireResolved) {
Assert.notNull(branch, "Branch must not be null!");
return findTicket(branch).//
map(ticket -> requireResolved ? ticket.getTicketStatus().isResolved() : true).//
orElse(false);
}
/**
* Returns a {@link TicketBranches} containing only the branches for which resolved {@link Ticket}s are found.
*
* @return
*/
public TicketBranches getResolvedTickets() {
return new TicketBranches(ticketBranches.entrySet().stream().//
filter(entry -> entry.getValue().getTicketStatus().isResolved()).//
collect(Collectors.toMap(Entry::getKey, Entry::getValue)));
}
/**
* Returns the ticket for the given {@link Branch}.
*
* @param branch must not be {@literal null}.
* @return
*/
public Optional<Ticket> findTicket(Branch branch) {
Assert.notNull(branch, "Branch must not be null!");
return Optional.ofNullable(ticketBranches.get(branch));
}
/*
* (non-Javadoc)
* @see java.lang.Iterable#iterator()
*/
@Override
public Iterator<Branch> iterator() {
return ticketBranches.keySet().iterator();
}
}

View File

@@ -1,261 +0,0 @@
/*
* Copyright 2014-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.git;
import lombok.EqualsAndHashCode;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.TreeMap;
import java.util.function.BiPredicate;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.springframework.data.release.model.ArtifactVersion;
import org.springframework.data.release.model.Iteration;
import org.springframework.data.release.model.ModuleIteration;
import org.springframework.data.release.model.Project;
import org.springframework.data.release.model.Projects;
import org.springframework.data.release.model.ReleaseTrains;
import org.springframework.data.release.model.Train;
import org.springframework.data.release.model.TrainIteration;
import org.springframework.data.util.Pair;
import org.springframework.data.util.Streamable;
import org.springframework.util.Assert;
/**
* Value object to represent a collection of {@link Tag}s.
*
* @author Oliver Gierke
* @author Mark Paluch
*/
@EqualsAndHashCode
public class VersionTags implements Streamable<Tag> {
private final Project project;
private final List<Tag> tags;
/**
* Creates a new {@link VersionTags} instance for the given {@link Project} and {@link List} of {@link Tag}s.
*
* @param project must not be {@literal null}.
* @param source must not be {@literal null}.
*/
VersionTags(Project project, Collection<Tag> source) {
Assert.notNull(project, "Project must not be null!");
Assert.notNull(source, "Tags must not be null!");
this.project = project;
this.tags = source.stream().//
filter(Tag::isVersionTag).//
sorted().collect(Collectors.toList());
}
/**
* Creates an empty {@link VersionTags} object for {@link Project}.
*
* @param project must not be {@literal null}.
* @return
*/
public static VersionTags empty(Project project) {
return new VersionTags(project, Collections.emptyList());
}
/**
* Returns the latest {@link Tag}.
*
* @return
*/
public Tag getLatest() {
return tags.isEmpty() ? null : tags.get(tags.size() - 1);
}
public Tag createTag(ModuleIteration iteration) {
if (iteration.getProject().equals(Projects.BOM)) {
return Tag.of(iteration.getTrainIteration().getReleaseTrainNameAndVersion());
}
Tag latest = getLatest();
ArtifactVersion version = ArtifactVersion.of(iteration);
if (latest != null) {
return latest.createNew(version);
}
return Tag.of(version.toString());
}
/**
* Returns all {@link Tag}s as {@link List}.
*
* @return
*/
public List<Tag> asList() {
return tags;
}
/*
* (non-Javadoc)
* @see java.lang.Iterable#iterator()
*/
@Override
public Iterator<Tag> iterator() {
return tags.iterator();
}
/**
* Create {@link VersionTags} that contains only tags from the given {@link Train}.
*
* @param train
* @return
*/
public VersionTags filter(Train train) {
return filter((tag, ti) -> ti.getTrain().equals(train));
}
/**
* Create {@link VersionTags} from a filtered subset of this {@link VersionTags}.
*
* @param predicate
* @return
*/
public VersionTags filter(BiPredicate<Tag, TrainIteration> predicate) {
Map<Tag, TrainIteration> iterations = newMap();
withIterations().forEach((tag, trainIteration) -> {
if (predicate.test(tag, trainIteration)) {
iterations.put(tag, trainIteration);
}
});
return new VersionTags(project, iterations.keySet());
}
Map<Tag, TrainIteration> withIterations() {
Map<Tag, TrainIteration> iterations = newMap();
for (Tag tag : tags) {
Optional<ArtifactVersion> artifactVersion = tag.toArtifactVersion();
if (!artifactVersion.isPresent()) {
continue;
}
ArtifactVersion version = artifactVersion.get();
Optional<Iteration> iteration = toIteration(version);
if (!iteration.isPresent()) {
continue;
}
Optional<TrainIteration> ti = getTrainIteration(version, project, iteration.get());
ti.ifPresent(it -> iterations.put(tag, it));
}
return iterations;
}
public Optional<TrainIteration> findMostRecentTrainIterationBefore(Iteration iterationToFind) {
return find((tag, iteration) -> iteration.getIteration().compareTo(iterationToFind) < 0, Pair::getSecond);
}
public Optional<Tag> findTag(Iteration iterationToFind) {
return find((tag, iteration) -> iteration.getIteration().compareTo(iterationToFind) == 0, Pair::getFirst);
}
public <T> Optional<T> find(BiPredicate<Tag, TrainIteration> filter,
Function<Pair<Tag, TrainIteration>, T> resultExtractor) {
for (Map.Entry<Tag, TrainIteration> entry : withIterations().entrySet()) {
if (filter.test(entry.getKey(), entry.getValue())) {
return Optional.of(resultExtractor.apply(Pair.of(entry.getKey(), entry.getValue())));
}
}
return Optional.empty();
}
private static <K, V> TreeMap<K, V> newMap() {
return new TreeMap(Comparator.reverseOrder());
}
private static Optional<TrainIteration> getTrainIteration(ArtifactVersion version, Project project,
Iteration iteration) {
// consider major version bump during ReleaseTrain
boolean minorIncrementedButTargetsNextTrain = false;
for (Train train : ReleaseTrains.trains()) {
TrainIteration ti = new TrainIteration(train, iteration);
if (!ti.contains(project)) {
continue;
}
if (minorIncrementedButTargetsNextTrain) {
return Optional.of(ti);
}
ModuleIteration mi = ti.getModule(project);
ArtifactVersion artifactVersion = ArtifactVersion.of(mi);
if (artifactVersion.equals(version)) {
return Optional.of(ti);
}
if (artifactVersion.getNextMinorVersion().equals(version)) {
minorIncrementedButTargetsNextTrain = true;
}
}
return Optional.empty();
}
private static Optional<Iteration> toIteration(ArtifactVersion version) {
if (version.isMilestoneVersion()) {
return Optional.of(Iteration.valueOf("M" + version.getLevel()));
}
if (version.isReleaseCandidateVersion()) {
return Optional.of(Iteration.valueOf("RC" + version.getLevel()));
}
if (version.isBugFixVersion()) {
return Optional.of(Iteration.valueOf("SR" + version.getLevel()));
}
if (version.isReleaseVersion()) {
return Optional.of(Iteration.GA);
}
return Optional.empty();
}
}

View File

@@ -1,158 +0,0 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.infra;
import java.util.ArrayList;
import java.util.List;
import org.springframework.util.ReflectionUtils;
/**
* @author Mark Paluch
*/
public class Dependencies {
static final List<Dependency> dependencies = new ArrayList<>();
public static final Dependency APT = Dependency.of("APT", "com.mysema.maven:apt-maven-plugin");
public static final Dependency ASPECTJ = Dependency.of("AspectJ", "org.aspectj:aspectjrt");
public static final Dependency ASSERTJ = Dependency.of("AssertJ", "org.assertj:assertj-core");
public static final Dependency JACKSON = Dependency.of("Jackson", "com.fasterxml.jackson:jackson-bom");
public static final Dependency JACOCO = Dependency.of("Jacoco", "org.jacoco:jacoco");
public static final Dependency JODA_TIME = Dependency.of("Joda Time", "joda-time:joda-time");
public static final Dependency JUNIT5 = Dependency.of("JUnit", "org.junit:junit-bom");
public static final Dependency JUNIT4 = Dependency.of("JUnit", "junit:junit");
public static final Dependency KOTLIN = Dependency.of("Kotlin", "org.jetbrains.kotlin:kotlin-bom");
public static final Dependency KOTLIN_COROUTINES = Dependency.of("Kotlin Coroutines",
"org.jetbrains.kotlinx:kotlinx-coroutines-bom");
public static final Dependency MICROMETER = Dependency.of("Micrometer", "io.micrometer:micrometer-bom");
public static final Dependency MICROMETER_TRACING = Dependency.of("Micrometer Tracing",
"io.micrometer:micrometer-tracing-bom");
public static final Dependency MOCKITO = Dependency.of("Mockito", "org.mockito:mockito-core");
public static final Dependency MOCKK = Dependency.of("Mockk", "io.mockk:mockk");
public static final Dependency QUERYDSL = Dependency.of("Querydsl", "com.querydsl:querydsl-bom");
public static final Dependency RXJAVA1 = Dependency.of("RxJava", "io.reactivex:rxjava");
public static final Dependency RXJAVA2 = Dependency.of("RxJava", "io.reactivex.rxjava2:rxjava");
public static final Dependency RXJAVA3 = Dependency.of("RxJava", "io.reactivex.rxjava3:rxjava");
public static final Dependency RXJAVA_RS = Dependency.of("RxJava Reactive Streams",
"io.reactivex:rxjava-reactive-streams");
public static final Dependency SPRING_HATEOAS = Dependency.of("Spring Hateoas",
"org.springframework.hateoas:spring-hateoas");
public static final Dependency SPRING_PLUGIN = Dependency.of("Spring Plugin",
"org.springframework.plugin:spring-plugin");
public static final Dependency TESTCONTAINERS = Dependency.of("Testcontainers", "org.testcontainers:testcontainers");
public static final Dependency THREE_TEN_BP = Dependency.of("ThreeTenBp", "org.threeten:threetenbp");
public static final Dependency OPEN_WEB_BEANS = Dependency.of("OpenWebBeans", "org.apache.openwebbeans:openwebbeans");
public static final Dependency SMALLRYE_MUTINY = Dependency.of("Smallrye Mutiny", "io.smallrye.reactive:mutiny");
public static final Dependency VAVR = Dependency.of("Vavr", "io.vavr:vavr").excludeVersionStartingWith("1.0.0-alpha");
public static final Dependency XML_BEAM = Dependency.of("XMLBeam", "org.xmlbeam:xmlprojector");
public static final Dependency MONGODB_CORE = Dependency.of("MongoDB", "org.mongodb:mongodb-driver-core");
public static final Dependency MONGODB_LEGACY = Dependency.of("MongoDB", "org.mongodb:mongo-java-driver");
public static final Dependency MONGODB_SYNC = Dependency.of("MongoDB", "org.mongodb:mongodb-driver-sync");
public static final Dependency MONGODB_ASYNC = Dependency.of("MongoDB", "org.mongodb:mongodb-driver-async");
public static final Dependency MONGODB_RS = Dependency.of("MongoDB Reactive Streams",
"org.mongodb:mongodb-driver-reactivestreams");
public static final Dependency LETTUCE = Dependency.of("Lettuce", "io.lettuce:lettuce-core");
public static final Dependency JEDIS = Dependency.of("Jedis", "redis.clients:jedis");
public static final Dependency JMOLECULES = Dependency.of("JMolecules", "org.jmolecules:jmolecules");
public static final Dependency JMOLECULES_INTEGRATION = Dependency.of("JMolecules",
"org.jmolecules.integrations:jmolecules-spring");
public static final Dependency CASSANDRA_DRIVER3 = Dependency.of("Cassandra Driver",
"com.datastax.cassandra:cassandra-driver-core");
public static final Dependency CASSANDRA_DRIVER4 = Dependency.of("Cassandra Driver",
"com.datastax.oss:java-driver-bom");
public static final Dependency NEO4J_OGM = Dependency.of("Neo4j OGM", "org.neo4j:neo4j-ogm-api");
public static final Dependency NEO4J_DRIVER = Dependency.of("Neo4j Driver", "org.neo4j.driver:neo4j-java-driver");
public static final Dependency COUCHBASE = Dependency.of("Couchbase Client", "com.couchbase.client:java-client");
public static final Dependency ELASTICSEARCH_RHLC = Dependency.of("Elasticsearch",
"org.elasticsearch.client:elasticsearch-rest-high-level-client");
public static final Dependency ELASTICSEARCH_REST_CLIENT = Dependency.of("Elasticsearch REST Client",
"org.elasticsearch.client:elasticsearch-rest-client");
public static final Dependency SPRING_LDAP = Dependency.of("Spring LDAP",
"org.springframework.ldap:spring-ldap-core");
public static final Dependency MAVEN = Dependency.of("Maven Wrapper", "org.apache.maven:apache-maven");
static {
ReflectionUtils.doWithFields(Dependencies.class, field -> {
// ignore dependencies constant
if (field.getName().toLowerCase().equals(field.getName())) {
return;
}
dependencies.add((Dependency) ReflectionUtils.getField(field, null));
});
}
public static Dependency getRequiredByName(String name) {
return dependencies.stream().filter(it -> it.getName().equals(name)).findFirst()
.orElseThrow(() -> new IllegalArgumentException(String.format("No such dependency: %s", name)));
}
public static Dependency getRequiredDepependency(String groupId, String artifactId) {
return dependencies.stream().filter(it -> it.getGroupId().equals(groupId) && it.getArtifactId().equals(artifactId))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException(String.format("No such dependency: %s", artifactId)));
}
}

View File

@@ -1,62 +0,0 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.infra;
import lombok.Value;
import java.util.function.Predicate;
import org.springframework.util.Assert;
/**
* @author Mark Paluch
*/
@Value
public class Dependency implements Comparable<Dependency> {
String name;
String groupId, artifactId;
Predicate<String> exclusions;
public static Dependency of(String name, String ga) {
Assert.hasText(name, "Name must not be empty");
Assert.hasText(ga, "GroupId/ArtifactId must not be empty");
Assert.isTrue(ga.indexOf(':') != -1, "GroupId/ArtifactId must be in the format of org.group:artifact-id");
String[] parts = ga.split(":");
return new Dependency(name, parts[0], parts[1], it -> false);
}
public Dependency excludeVersionStartingWith(String identifier) {
return new Dependency(name, groupId, artifactId, it -> it.startsWith(identifier) || exclusions.test(it));
}
public boolean shouldInclude(String identifier) {
return !exclusions.test(identifier);
}
@Override
public int compareTo(Dependency o) {
return name.compareTo(o.name);
}
@Override
public String toString() {
return getGroupId() + ":" + getArtifactId();
}
}

View File

@@ -1,186 +0,0 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.infra;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import lombok.experimental.FieldDefaults;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.TreeMap;
import java.util.stream.Collectors;
import org.springframework.data.release.CliComponent;
import org.springframework.data.release.TimedCommand;
import org.springframework.data.release.git.GitOperations;
import org.springframework.data.release.issues.Tickets;
import org.springframework.data.release.model.ModuleIteration;
import org.springframework.data.release.model.Project;
import org.springframework.data.release.model.Projects;
import org.springframework.data.release.model.TrainIteration;
import org.springframework.data.release.utils.Logger;
import org.springframework.shell.core.annotation.CliCommand;
import org.springframework.shell.core.annotation.CliOption;
import org.springframework.shell.support.table.Table;
/**
* Shell commands for dependency management.
*
* @author Mark Paluch
*/
@CliComponent
@RequiredArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
public class DependencyCommands extends TimedCommand {
public static final String BUILD_PROPERTIES = "dependency-upgrade-build.properties";
DependencyOperations operations;
GitOperations git;
Logger logger;
@CliCommand(value = "dependency check")
public void check(@CliOption(key = "", mandatory = true) TrainIteration iteration,
@CliOption(key = "all", mandatory = false) Boolean reportAll) throws IOException {
git.prepare(iteration);
checkBuildDependencies(iteration, reportAll != null ? reportAll : false);
checkModuleDependencies(iteration, reportAll != null ? reportAll : false);
}
/**
* Retrieve a dependency report for all store modules to be used typically in Spring Boot upgrade tickets.
*
* @param iteration
* @return
*/
@CliCommand(value = "dependency report")
public String report(@CliOption(key = "", mandatory = true) TrainIteration iteration) {
git.prepare(iteration);
List<Project> projects = Projects.all().stream()
.filter(it -> it != Projects.BOM && it != Projects.BUILD && it != Projects.COMMONS)
.collect(Collectors.toList());
Map<Dependency, DependencyVersion> dependencies = new TreeMap<>();
for (Project project : projects) {
operations.getCurrentDependencies(project).forEach(dependencies::put);
}
StringBuilder report = new StringBuilder();
report.append(System.lineSeparator()).append("Project Dependencies Spring Data ")
.append(iteration.getReleaseTrainNameAndVersion()).append(System.lineSeparator())
.append(System.lineSeparator());
dependencies.forEach((dependency, dependencyVersion) -> {
report.append(String.format("* %s (%s:%s): %s", dependency.getName(), dependency.getGroupId(),
dependency.getArtifactId(), dependencyVersion.getIdentifier())).append(System.lineSeparator());
});
return report.toString();
}
@CliCommand(value = "dependency upgrade")
public void upgrade(@CliOption(key = "", mandatory = true) TrainIteration iteration)
throws IOException, InterruptedException {
logger.log(iteration, "Applying dependency upgrades to Spring Data Build");
ModuleIteration module = iteration.getModule(Projects.BUILD);
DependencyVersions dependencyVersions = loadDependencyUpgrades(module);
DependencyVersions upgradesToApply = operations.getDependencyUpgradesToApply(module.getProject(),
dependencyVersions);
if (upgradesToApply.isEmpty()) {
logger.log(module, "No dependency upgrades to apply");
return;
}
Tickets tickets = operations.getOrCreateUpgradeTickets(module, upgradesToApply);
operations.upgradeDependencies(tickets, module, dependencyVersions);
git.push(module);
// Allow GitHub to catch up with ticket notifications.
Thread.sleep(1500);
operations.closeUpgradeTickets(module, tickets);
}
private DependencyVersions loadDependencyUpgrades(ModuleIteration iteration)
throws IOException {
if (!Files.exists(Paths.get(BUILD_PROPERTIES))) {
logger.log(iteration, "Cannot upgrade dependencies: " + BUILD_PROPERTIES + " does not exist.");
}
Properties properties = new Properties();
try (FileInputStream fis = new FileInputStream(BUILD_PROPERTIES)) {
properties.load(fis);
}
return DependencyUpgradeProposals.fromProperties(iteration.getTrainIteration(), properties);
}
private void checkModuleDependencies(TrainIteration iteration, boolean reportAll) throws IOException {
String propertiesFile = "dependency-upgrade-modules.properties";
List<Project> projects = Projects.all().stream().filter(it -> it != Projects.BOM && it != Projects.BUILD)
.collect(Collectors.toList());
DependencyUpgradeProposals proposals = DependencyUpgradeProposals.empty();
for (Project project : projects) {
proposals = proposals.mergeWith(operations.getDependencyUpgradeProposals(project, iteration.getIteration()));
}
Files.write(Paths.get(propertiesFile), proposals.asProperties(iteration).getBytes());
Table summary = proposals.toTable(reportAll);
logger.log(iteration, "Upgrade summary:" + System.lineSeparator() + System.lineSeparator() + summary);
logger.log(iteration, "Upgrade proposals written to " + propertiesFile);
}
private void checkBuildDependencies(TrainIteration iteration, boolean reportAll) throws IOException {
String propertiesFile = BUILD_PROPERTIES;
DependencyUpgradeProposals proposals = operations.getDependencyUpgradeProposals(Projects.BUILD,
iteration.getIteration());
Files.write(Paths.get(propertiesFile), proposals.asProperties(iteration).getBytes());
Table summary = proposals.toTable(reportAll);
logger.log(Projects.BUILD, "Upgrade summary:" + System.lineSeparator() + System.lineSeparator() + summary);
logger.log(iteration, "Upgrade proposals written to " + propertiesFile);
}
}

View File

@@ -1,597 +0,0 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.infra;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.experimental.FieldDefaults;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.concurrent.ExecutorService;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.springframework.data.release.build.Pom;
import org.springframework.data.release.git.GitOperations;
import org.springframework.data.release.io.Workspace;
import org.springframework.data.release.issues.IssueTracker;
import org.springframework.data.release.issues.Ticket;
import org.springframework.data.release.issues.TicketOperations;
import org.springframework.data.release.issues.Tickets;
import org.springframework.data.release.model.Iteration;
import org.springframework.data.release.model.ModuleIteration;
import org.springframework.data.release.model.Project;
import org.springframework.data.release.model.Projects;
import org.springframework.data.release.model.TrainIteration;
import org.springframework.data.release.utils.ExecutionUtils;
import org.springframework.data.release.utils.Logger;
import org.springframework.data.util.Streamable;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestOperations;
import org.xmlbeam.ProjectionFactory;
import org.xmlbeam.annotation.XBRead;
import org.xmlbeam.io.XBFileIO;
import org.xmlbeam.io.XBStreamInput;
/**
* Operations for dependency management.
*
* @author Mark Paluch
*/
@Component
@RequiredArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
public class DependencyOperations {
public static final Pattern REPO_MAVEN_ORG_DIR_LISTING = Pattern
.compile("<a (?>[^>]+)>([^\\/]+)\\/<\\/a>(?>\\s*)(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2})(?>\\s*)(?>-)");
public static final DateTimeFormatter DIR_LISTING_TIME_FORMAT = DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm");
private final ProjectionFactory projectionFactory;
private final GitOperations gitOperations;
private final Workspace workspace;
private final TicketOperations tickets;
private final ExecutorService executor;
private final RestOperations restOperations;
private final Logger logger;
/**
* Obtain dependency upgrade proposals for {@link Project} and {@link Iteration}. Considers dependency upgrade rules
* according to minor/bugfix release increments. Also, SemVer with modifier are only allowed in milestone versions.
*
* @param project
* @param iteration
* @return
*/
public DependencyUpgradeProposals getDependencyUpgradeProposals(Project project, Iteration iteration) {
DependencyVersions currentDependencies = getCurrentDependencies(project);
Map<Dependency, DependencyUpgradeProposal> proposals = Collections.synchronizedMap(new LinkedHashMap<>());
ExecutionUtils.run(executor, Streamable.of(currentDependencies.getDependencies()), dependency -> {
DependencyVersion currentVersion = currentDependencies.get(dependency);
List<DependencyVersion> versions = getAvailableVersions(dependency);
DependencyUpgradeProposal proposal = getDependencyUpgradeProposal(DependencyUpgradePolicy.from(iteration),
currentVersion, versions);
proposals.put(dependency, proposal);
});
return new DependencyUpgradeProposals(proposals);
}
/**
* Obtain dependency upgrade proposals for Maven Wrapper.
*
* @param project
* @param iteration
* @return
*/
public DependencyUpgradeProposals getMavenWrapperDependencyUpgradeProposals(TrainIteration iteration) {
for (ModuleIteration moduleIteration : iteration) {
// ensure we have Maven Wrapper for each project.
getMavenWrapperVersion(moduleIteration.getProject());
}
return getDependencyUpgradeProposals(Projects.BUILD, DependencyUpgradePolicy.LATEST_STABLE, Dependencies.MAVEN,
this::getMavenWrapperVersion);
}
/**
* Applies the upgrade and creates a commit.
*
* @param tickets
* @param module
* @param dependencyVersions
*/
public Tickets upgradeMavenWrapperVersion(Tickets tickets, ModuleIteration module,
DependencyVersions dependencyVersions) {
if (dependencyVersions.isEmpty()) {
logger.log(module, "No dependency upgrades to apply");
}
return doWithDependencyVersionsAndCommit(tickets, module, dependencyVersions, (dependency, version) -> {
upgradeMavenWrapperVersion(module.getProject(), version);
});
}
public List<Project> getProjectsToUpgradeMavenWrapper(DependencyVersion targetVersion, TrainIteration iteration) {
List<Project> projectsToUpgrade = new ArrayList<>();
for (ModuleIteration moduleIteration : iteration) {
DependencyVersion currentVersion = getMavenWrapperVersion(moduleIteration.getProject());
if (targetVersion.isNewer(currentVersion)) {
projectsToUpgrade.add(moduleIteration.getProject());
}
}
return projectsToUpgrade;
}
private DependencyVersion getMavenWrapperVersion(Project project) {
try {
File file = getMavenWrapperProperties(project);
String distributionUrl;
try (FileInputStream fileInputStream = new FileInputStream(file)) {
Properties properties = new Properties();
properties.load(fileInputStream);
distributionUrl = properties.getProperty("distributionUrl");
}
Pattern versionPattern = Pattern.compile(".*/maven2/org/apache/maven/apache-maven/([\\d\\.]+)/.*");
Matcher matcher = versionPattern.matcher(distributionUrl);
if (!matcher.find()) {
throw new IllegalStateException(
String.format("Invalid distribution URL in %s: %s", project.getName(), distributionUrl));
}
return DependencyVersion.of(matcher.group(1));
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
private void upgradeMavenWrapperVersion(Project project, DependencyVersion dependencyVersion) {
String distributionUrlTemplate = "https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/%s/apache-maven-%s-bin.zip";
try {
File file = getMavenWrapperProperties(project);
Properties properties = new Properties();
try (FileInputStream is = new FileInputStream(file)) {
properties.load(is);
}
properties.setProperty("distributionUrl",
String.format(distributionUrlTemplate, dependencyVersion.getIdentifier(), dependencyVersion.getIdentifier()));
try (FileOutputStream os = new FileOutputStream(file)) {
properties.store(os, null);
}
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
private File getMavenWrapperProperties(Project project) throws FileNotFoundException {
File file = workspace.getFile(".mvn/wrapper/maven-wrapper.properties", project);
if (!file.exists()) {
throw new FileNotFoundException(file.toString());
}
return file;
}
public DependencyUpgradeProposals getDependencyUpgradeProposals(Project project, DependencyUpgradePolicy policy,
Dependency dependency, Function<Project, DependencyVersion> currentVersionExtractor) {
DependencyVersions currentDependencies = new DependencyVersions(
Collections.singletonMap(dependency, currentVersionExtractor.apply(project)));
Map<Dependency, DependencyUpgradeProposal> proposals = Collections.synchronizedMap(new LinkedHashMap<>());
DependencyVersion currentVersion = currentDependencies.get(dependency);
List<DependencyVersion> versions = getAvailableVersions(dependency);
DependencyUpgradeProposal proposal = getDependencyUpgradeProposal(policy, currentVersion, versions);
proposals.put(dependency, proposal);
return new DependencyUpgradeProposals(proposals);
}
/**
* Ensures there's a upgrade ticket for each dependency to upgrade.
*
* @param module
* @param dependencyVersions
* @return
*/
public Tickets getOrCreateUpgradeTickets(ModuleIteration module, DependencyVersions dependencyVersions) {
List<String> summaries = new ArrayList<>();
dependencyVersions.forEach((dependency, dependencyVersion) -> {
summaries.add(getUpgradeTicketSummary(dependency, dependencyVersion));
});
return tickets.getOrCreateTicketsWithSummary(module, IssueTracker.TicketType.DependencyUpgrade, summaries);
}
/**
* Applies the upgrade, creates a commit.
*
* @param tickets
* @param module
* @param dependencyVersions
*/
public Tickets upgradeDependencies(Tickets tickets, ModuleIteration module, DependencyVersions dependencyVersions) {
Project project = module.getProject();
ProjectDependencies dependencies = ProjectDependencies.get(project);
if (dependencyVersions.isEmpty()) {
logger.log(module, "No dependency upgrades to apply");
}
return doWithDependencyVersionsAndCommit(tickets, module, dependencyVersions, (dependency, version) -> {
String versionProperty = dependencies.getVersionPropertyFor(dependency);
File pom = getPomFile(project);
update(pom, Pom.class, it -> {
it.setProperty(versionProperty, version.getIdentifier());
});
});
}
/**
* Verifies dependencies to upgrade, applies the upgrade, creates a commit.
*
* @param tickets
* @param module
* @param dependencyVersions
*/
private Tickets doWithDependencyVersionsAndCommit(Tickets tickets, ModuleIteration module,
DependencyVersions dependencyVersions, BiConsumer<Dependency, DependencyVersion> action) {
if (dependencyVersions.isEmpty()) {
logger.log(module, "No dependency upgrades to apply");
}
List<Ticket> ticketsToClose = new ArrayList<>();
dependencyVersions.forEach((dependency, dependencyVersion) -> {
String upgradeTicketSummary = getUpgradeTicketSummary(dependency, dependencyVersion);
Ticket upgradeTicket = getDependencyUpgradeTicket(tickets, upgradeTicketSummary).get();
action.accept(dependency, dependencyVersion);
gitOperations.commit(module, upgradeTicket, upgradeTicketSummary, Optional.empty(), true);
ticketsToClose.add(upgradeTicket);
});
return new Tickets(ticketsToClose);
}
public void closeUpgradeTickets(ModuleIteration module, Tickets tickets) {
this.tickets.closeTickets(module, tickets);
}
public DependencyVersions getDependencyUpgradesToApply(Project project, DependencyVersions dependencyVersions) {
DependencyVersions currentDependencies = getCurrentDependencies(project);
Map<Dependency, DependencyVersion> upgrades = new LinkedHashMap<>();
currentDependencies.forEach((dependency, dependencyVersion) -> {
if (!dependencyVersions.hasDependency(dependency)) {
return;
}
DependencyVersion upgradeVersion = dependencyVersions.get(dependency);
if (upgradeVersion.equals(dependencyVersion)) {
logger.log(project, "Skipping upgrade of %s (%s)", dependency.getName(), dependencyVersion.getIdentifier());
return;
}
upgrades.put(dependency, upgradeVersion);
});
return new DependencyVersions(upgrades);
}
private Optional<Ticket> getDependencyUpgradeTicket(Tickets tickets, String upgradeTicketSummary) {
List<Ticket> upgradeTickets = tickets.filter(it -> it.getSummary().equals(upgradeTicketSummary)).toList();
if (upgradeTickets.size() > 1) {
throw new IllegalStateException("Multiple upgrade tickets found: " + upgradeTickets);
}
return Optional.ofNullable(upgradeTickets.isEmpty() ? null : upgradeTickets.get(0));
}
protected static DependencyUpgradeProposal getDependencyUpgradeProposal(DependencyUpgradePolicy policy,
DependencyVersion currentVersion, List<DependencyVersion> allVersions) {
Optional<DependencyVersion> latestMinor = findLatestMinor(policy, currentVersion, allVersions);
Optional<DependencyVersion> latest = findLatest(policy, allVersions);
List<DependencyVersion> newerVersions = allVersions.stream() //
.sorted() //
.filter(it -> it.compareTo(currentVersion) > 0) //
.collect(Collectors.toList());
DependencyVersion latestToUse = latest.filter(it -> it.isNewer(currentVersion)).orElse(currentVersion);
DependencyVersion latestMinorFallback = latest
.filter(it -> isUpgradeable(policy, it, currentVersion) && it.isNewer(currentVersion)).orElse(currentVersion);
return DependencyUpgradeProposal.of(policy, currentVersion, latestMinor.orElse(latestMinorFallback), latestToUse,
newerVersions);
}
private static boolean isUpgradeable(DependencyUpgradePolicy policy, DependencyVersion proposal,
DependencyVersion currentVersion) {
if (policy.restrictToMinorVersion()) {
if (proposal.getTrainName() != null && currentVersion.getTrainName() != null) {
return proposal.getTrainName().equals(currentVersion.getTrainName());
}
if (proposal.getVersion().getMajor() == currentVersion.getVersion().getMajor()
&& proposal.getVersion().getMinor() == currentVersion.getVersion().getMinor()) {
return true;
}
return false;
}
if (StringUtils.hasText(proposal.getModifier())) {
return policy.milestoneAllowed();
}
return true;
}
private static Optional<DependencyVersion> findLatest(DependencyUpgradePolicy policy,
List<DependencyVersion> availableVersions) {
return availableVersions.stream().filter(it -> {
if (StringUtils.hasText(it.getModifier())) {
return policy.milestoneAllowed();
}
return true;
}).max(DependencyVersion::compareTo);
}
private static Optional<DependencyVersion> findLatestMinor(DependencyUpgradePolicy policy,
DependencyVersion currentVersion, List<DependencyVersion> availableVersions) {
return availableVersions.stream().filter(it -> {
if (StringUtils.hasText(it.getModifier())) {
return policy.milestoneAllowed();
}
if (it.getVersion() == null || currentVersion.getVersion() == null) {
return false;
}
if (it.getTrainName() != null && currentVersion.getTrainName() != null) {
return it.getTrainName().equals(currentVersion.getTrainName());
}
if (it.getVersion().getMajor() == currentVersion.getVersion().getMajor()
&& it.getVersion().getMinor() == currentVersion.getVersion().getMinor()) {
return true;
}
return false;
}) //
.max(DependencyVersion::compareTo);
}
DependencyVersions getCurrentDependencies(Project project) {
if (!ProjectDependencies.containsProject(project)) {
return DependencyVersions.empty();
}
File pom = getPomFile(project);
ProjectDependencies dependencies = ProjectDependencies.get(project);
return doWithPom(pom, Pom.class, it -> {
Map<Dependency, DependencyVersion> versions = new LinkedHashMap<>();
for (ProjectDependencies.ProjectDependency projectDependency : dependencies) {
Dependency dependency = projectDependency.getDependency();
if (!((project == Projects.MONGO_DB && projectDependency.getProperty().equals("mongo.reactivestreams"))
|| project == Projects.NEO4J || project == Projects.BUILD)) {
if (it.getDependencyVersion(dependency.getArtifactId()) == null
&& it.getManagedDependency(dependency.getArtifactId()) == null) {
continue;
}
}
String value = it.getProperty(projectDependency.getProperty());
if (value != null && !value.contains("${")) {
versions.put(dependency, DependencyVersion.of(value));
}
}
return new DependencyVersions(versions);
});
}
private File getPomFile(Project project) {
return workspace.getFile(project == Projects.BUILD ? "parent/pom.xml" : "pom.xml", project);
}
@SneakyThrows
List<DependencyVersion> getAvailableVersions(Dependency dependency) {
String baseUrl = String.format("https://repo1.maven.org/maven2/%s/%s/", dependency.getGroupId().replace('.', '/'),
dependency.getArtifactId());
try {
ResponseEntity<byte[]> mavenMetadata = restOperations.getForEntity(baseUrl + "/maven-metadata.xml", byte[].class);
ResponseEntity<String> directoryListing = restOperations.getForEntity(baseUrl, String.class);
Map<String, LocalDateTime> creationDates = parseCreationDates(directoryListing.getBody());
XBStreamInput io = projectionFactory.io().stream(new ByteArrayInputStream(mavenMetadata.getBody()));
MavenMetadata metadata = io.read(MavenMetadata.class);
return metadata.getVersions().stream().filter(dependency::shouldInclude).flatMap(s -> {
try {
return Stream.of(DependencyVersion.of(s));
} catch (Exception e) {
logger.log(dependency.toString(), "Cannot parse dependency version " + s);
return Stream.empty();
}
}).map(it -> {
if (creationDates.containsKey(it.getIdentifier())) {
return it.withCreatedAt(creationDates.get(it.getIdentifier()));
}
return it;
}).collect(Collectors.toList());
} catch (Exception o_O) {
if (o_O instanceof HttpClientErrorException) {
if (((HttpClientErrorException) o_O).getStatusCode() == HttpStatus.NOT_FOUND) {
return Collections.emptyList();
}
}
throw new RuntimeException(String.format("Cannot determine available versions for %s", dependency), o_O);
}
}
private Map<String, LocalDateTime> parseCreationDates(String body) {
Map<String, LocalDateTime> creationDates = new LinkedHashMap<>();
Matcher matcher = REPO_MAVEN_ORG_DIR_LISTING.matcher(body);
while (matcher.find()) {
String version = matcher.group(1);
LocalDateTime creationDate = LocalDateTime.from(DIR_LISTING_TIME_FORMAT.parse(matcher.group(2)));
creationDates.put(version, creationDate);
}
return creationDates;
}
private <T extends Pom, R> R doWithPom(File file, Class<T> type, Function<T, R> callback) {
XBFileIO io = projectionFactory.io().file(file);
try {
T pom = (T) io.read(type);
return callback.apply(pom);
} catch (Exception o_O) {
throw new RuntimeException(o_O);
}
}
private <T extends Pom> void update(File file, Class<T> type, Consumer<T> callback) {
XBFileIO io = projectionFactory.io().file(file);
try {
T pom = (T) io.read(type);
callback.accept(pom);
io.write(pom);
} catch (Exception o_O) {
throw new RuntimeException(o_O);
}
}
private static String getUpgradeTicketSummary(Dependency dependency, DependencyVersion dependencyVersion) {
return String.format("Upgrade to %s %s", dependency.getName(), dependencyVersion.getIdentifier());
}
public interface MavenMetadata {
@XBRead("/metadata/versioning/versions/version/text()")
List<String> getVersions();
}
}

View File

@@ -1,73 +0,0 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.infra;
import org.springframework.data.release.model.Iteration;
/**
* Upgrade policy expressing rules how dependency upgrades should happen.
*
* @author Mark Paluch
*/
interface DependencyUpgradePolicy {
DependencyUpgradePolicy LATEST_STABLE = new DependencyUpgradePolicy() {
@Override
public boolean milestoneAllowed() {
return false;
}
@Override
public boolean restrictToMinorVersion() {
return false;
}
};
/**
* @return {@code true} if the use of pre-release dependency versions is allowed.
*/
boolean milestoneAllowed();
/**
* @return {@code true} if upgrades only within the minor version (e.g. bugfixes/patch releases) are allowed.
*/
boolean restrictToMinorVersion();
/**
* Create a upgrade policy from a {@link Iteration}.
*
* @param iteration
* @return
*/
static DependencyUpgradePolicy from(Iteration iteration) {
return new DependencyUpgradePolicy() {
@Override
public boolean milestoneAllowed() {
return iteration.isMilestone() || iteration.isReleaseCandidate();
}
@Override
public boolean restrictToMinorVersion() {
return iteration.isPublic();
}
};
}
}

View File

@@ -1,73 +0,0 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.infra;
import lombok.RequiredArgsConstructor;
import lombok.Value;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author Mark Paluch
*/
@Value
@RequiredArgsConstructor
class DependencyUpgradeProposal {
DependencyVersion current, latest, latestMinor, proposal;
List<DependencyVersion> newerVersions;
public static DependencyUpgradeProposal of(DependencyUpgradePolicy policy, DependencyVersion currentVersion,
DependencyVersion latestMinor, DependencyVersion latest, List<DependencyVersion> newerVersions) {
if (policy.restrictToMinorVersion()) {
return new DependencyUpgradeProposal(currentVersion, latest, latestMinor, latestMinor, newerVersions);
}
return new DependencyUpgradeProposal(currentVersion, latest, latestMinor, latest, newerVersions);
}
public String getNewVersions(boolean includeAll, boolean includeDate) {
if (includeAll) {
return getNewerVersions().stream().map(dependencyVersion -> {
if (includeDate && dependencyVersion.getCreatedAt() != null) {
return String.format("%s (%s)", dependencyVersion.getIdentifier(),
dependencyVersion.getCreatedAt().toLocalDate());
}
return dependencyVersion.getIdentifier();
}).collect(Collectors.joining(", "));
}
if (latestMinor.toString().equals(latest.toString())) {
return latest.toString();
}
return String.format("%s, %s", latestMinor, latest);
}
public boolean isUpgradeAvailable() {
return !getCurrent().getIdentifier().equals(getProposal().getIdentifier());
}
@Override
public String toString() {
return getProposal().getIdentifier();
}
}

View File

@@ -1,194 +0,0 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.infra;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Properties;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.fusesource.jansi.Ansi;
import org.springframework.data.release.model.TrainIteration;
import org.springframework.shell.support.table.Table;
import org.springframework.shell.support.table.TableHeader;
/**
* Value object capturing upgrade proposals for each {@link Dependency}.
*
* @author Mark Paluch
*/
public class DependencyUpgradeProposals {
private final Map<Dependency, DependencyUpgradeProposal> proposals;
public DependencyUpgradeProposals(Map<Dependency, DependencyUpgradeProposal> proposals) {
this.proposals = proposals;
}
/**
* Create an empty {@link DependencyUpgradeProposal} object.
*
* @return
*/
public static DependencyUpgradeProposals empty() {
return new DependencyUpgradeProposals(Collections.emptyMap());
}
/**
* Create a new {@link DependencyUpgradeProposal} by merging this and {@code other}.
*
* @param other
* @return
*/
public DependencyUpgradeProposals mergeWith(DependencyUpgradeProposals other) {
Map<Dependency, DependencyUpgradeProposal> proposals = new TreeMap<>(this.proposals);
proposals.putAll(other.proposals);
return new DependencyUpgradeProposals(proposals);
}
/**
* Create a tabular summary including {@link Ansi} escapes.
*
* @param includeAll
* @return
*/
public Table toTable(boolean includeAll) {
Table table = new Table();
table.addHeader(1, new TableHeader("Dependency"));
table.addHeader(2, new TableHeader("Current"));
table.addHeader(3, new TableHeader("Available"));
table.addHeader(4, new TableHeader("Proposed"));
proposals.forEach((dependency, proposal) -> {
boolean updateAvailable = proposal.isUpgradeAvailable();
String s = updateAvailable
? Ansi.ansi().fg(Ansi.Color.MAGENTA).a(proposal.getProposal()).fg(Ansi.Color.GREEN).toString()
: proposal.getProposal().toString();
if (includeAll || updateAvailable) {
table.addRow(dependency.getName(), proposal.getCurrent().toString(), proposal.getNewVersions(includeAll, false),
s);
}
});
return table;
}
/**
* Return the upgrade proposal as {@link java.util.Properties} representation.
*
* @param iteration
* @return
*/
public String asProperties(TrainIteration iteration) {
StringBuilder builder = new StringBuilder();
builder.append("dependency.train=").append(iteration.getTrain().getName()).append(System.lineSeparator());
builder.append("dependency.iteration=").append(iteration.getIteration().getName()).append(System.lineSeparator());
builder.append(System.lineSeparator());
builder.append("# Specify the number of desired dependency upgrades as sanity check")
.append(System.lineSeparator());
builder.append("dependency.upgrade.count=").append(System.lineSeparator());
proposals.forEach((dependency, proposal) -> {
boolean updateAvailable = proposal.isUpgradeAvailable();
if (updateAvailable) {
builder.append(System.lineSeparator());
builder.append(String.format("# %s - Available versions: ", dependency.getName()))
.append(proposal.getNewVersions(true, true)).append(System.lineSeparator());
builder.append(
String.format("dependency[%s\\:%s]=%s", dependency.getGroupId(), dependency.getArtifactId(), proposal));
builder.append(System.lineSeparator());
}
});
return builder.toString();
}
/**
* Create a dependency upgrade map by parsing {@link Properties}.
*
* @param iteration
* @param properties
* @return
*/
public static DependencyVersions fromProperties(TrainIteration iteration, Properties properties) {
Pattern keyPattern = Pattern.compile("dependency\\[([a-zA-Z0-9\\-\\.]+):([a-zA-Z0-9\\-\\.]+)\\]");
String verificationTrain = properties.getProperty("dependency.train", "");
String verificationIteration = properties.getProperty("dependency.iteration", "");
int expectedUpgradeCount;
try {
expectedUpgradeCount = Integer.parseInt(properties.getProperty("dependency.upgrade.count", "0"));
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Please specify a valid dependency.upgrade.count");
}
if (!verificationTrain.equals(iteration.getTrain().getName())
|| !verificationIteration.equals(iteration.getIteration().getName())) {
throw new IllegalArgumentException(
String.format("Verification failed: Dependency upgrade descriptor reports %s %s", verificationTrain,
verificationIteration));
}
Map<Dependency, DependencyVersion> result = new LinkedHashMap<>();
properties.forEach((k, v) -> {
if ("dependency.train".equals(k) || "dependency.iteration".equals(k) || "dependency.upgrade.count".equals(k)) {
return;
}
Matcher matcher = keyPattern.matcher(k.toString());
if (!matcher.matches()) {
throw new IllegalArgumentException(String.format("Unexpected key: %s", k));
}
String groupId = matcher.group(1);
String artifactId = matcher.group(2);
Dependency dependency = Dependencies.getRequiredDepependency(groupId, artifactId);
result.put(dependency, DependencyVersion.of(v.toString()));
});
DependencyVersions dependencyVersions = new DependencyVersions(result);
if (expectedUpgradeCount != result.size()) {
throw new IllegalStateException(String.format(
"The number of expected upgrades (dependency.upgrade.count=%s) does not match the number of actual upgrades (%s): %n%n%s",
expectedUpgradeCount, result.size(), dependencyVersions.toString(1)));
}
return dependencyVersions;
}
}

View File

@@ -1,144 +0,0 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.infra;
import lombok.Value;
import lombok.With;
import java.time.LocalDateTime;
import java.util.Comparator;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.data.release.model.Version;
/**
* Value object representing a dependency version. The primary identifier is {@link #identifier} that corresponds with
* the actual version identifier. Version identifiers are attempted to be parsed into either a version following Spring
* Version or SemVer rules ({@code 1.2.3.RELEASE}, {@code 1.2.3-rc1}) or train name with counter rules
* ({@code Foo-RELEASE}, {@code Foo-SR1}).
*
* @author Mark Paluch
*/
@Value
class DependencyVersion implements Comparable<DependencyVersion> {
private static Pattern VERSION = Pattern.compile("((?>(?>\\d+)[\\.]?)+)((?>-)?[a-zA-Z]+)?(\\d+)?");
private static Pattern NAME_VERSION = Pattern.compile("([A-Za-z]+)-(RELEASE|SR(\\d+)|SNAPSHOT|BUILD-SNAPSHOT)");
private static Comparator<DependencyVersion> VERSION_COMPARATOR = Comparator.comparing(DependencyVersion::getVersion)
.thenComparing((o1, o2) -> {
// no modifier means release so it's higher order
if (o1.getModifier().isEmpty() && !o2.getModifier().isEmpty()) {
return 1;
}
if (!o1.getModifier().isEmpty() && o2.getModifier().isEmpty()) {
return -1;
}
return 0;
}).thenComparing(DependencyVersion::getModifier).thenComparing(DependencyVersion::getCounter)
.thenComparing(DependencyVersion::getIdentifier);
private static Comparator<DependencyVersion> TRAIN_VERSION_COMPARATOR = Comparator
.comparing(DependencyVersion::getTrainName).thenComparing(DependencyVersion::getVersion);
String identifier;
String trainName;
Version version;
String modifier;
int counter;
@With LocalDateTime createdAt;
public static DependencyVersion of(String identifier) {
Matcher bomMatcher = NAME_VERSION.matcher(identifier);
if (bomMatcher.find()) {
Version version = Version.of(0);
String modifier = "";
if (identifier.contains("-SR")) {
version = Version.of(Integer.parseInt(bomMatcher.group(3)));
}
if (identifier.endsWith("-SNAPSHOT")) {
modifier = "SNAPSHOT";
}
return new DependencyVersion(identifier, bomMatcher.group(1), version, modifier, 0, null);
}
Matcher versionMatcher = VERSION.matcher(identifier);
if (versionMatcher.find()) {
Version version = null;
String modifier;
String counter;
String versionString = versionMatcher.group(1);
versionString = versionString.endsWith(".") ? versionString.substring(0, versionString.length() - 1)
: versionString;
try {
version = Version.parse(versionString);
} catch (RuntimeException e) {
throw new IllegalArgumentException(String.format("Cannot parse version number %s", versionString), e);
}
modifier = versionMatcher.group(2);
counter = versionMatcher.group(3);
return new DependencyVersion(identifier, null, version, modifier == null ? "" : modifier.toUpperCase(Locale.ROOT),
counter != null ? Integer.parseInt(counter) : 0, null);
}
throw new IllegalArgumentException(String.format("Cannot parse version identifier %s", identifier));
}
public boolean isNewer(DependencyVersion other) {
return this.compareTo(other) > 0;
}
@Override
public int compareTo(DependencyVersion o) {
if (trainName != null && o.trainName != null) {
return TRAIN_VERSION_COMPARATOR.compare(this, o);
}
if (trainName != null) {
return -1;
}
if (o.trainName != null) {
return 1;
}
if (version != null && o.version != null) {
return VERSION_COMPARATOR.compare(this, o);
}
return identifier.compareTo(o.identifier);
}
@Override
public String toString() {
return identifier;
}
}

View File

@@ -1,112 +0,0 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.infra;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Value;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.function.BiConsumer;
import org.apache.maven.shared.utils.StringUtils;
/**
* Value upgrade capturing {@link DependencyVersion} for a {@link Dependency}.
*
* @author Mark Paluch
*/
@Value
@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
class DependencyVersions {
@Getter Map<Dependency, DependencyVersion> versions;
public static DependencyVersions empty() {
return new DependencyVersions(Collections.emptyMap());
}
/**
* Returns whether there's a version specified for {@link Dependency}.
*
* @param dependency
* @return
*/
public boolean hasDependency(Dependency dependency) {
return versions.containsKey(dependency);
}
/**
* Returns the {@link DependencyVersion} for {@link Dependency} or throws {@link IllegalArgumentException} if no
* version was specified. It's recommended to check whether a version has been specified using
* {@link #hasDependency(Dependency)}.
*
* @param dependency
* @return
* @see #hasDependency(Dependency)
*/
public DependencyVersion get(Dependency dependency) {
if (!hasDependency(dependency)) {
throw new IllegalArgumentException(String.format("No such dependency: %s", dependency));
}
return versions.get(dependency);
}
/**
* Apply the {@code action} to all dependeny/dependency version pairs.
*
* @param action
*/
public void forEach(BiConsumer<Dependency, DependencyVersion> action) {
versions.forEach(action);
}
/**
* Returns whether dependency versions are configured.
*
* @return
*/
public boolean isEmpty() {
return versions.isEmpty();
}
public Collection<Dependency> getDependencies() {
return versions.keySet();
}
@Override
public String toString() {
return toString(0);
}
public String toString(int indentation) {
StringBuilder result = new StringBuilder();
for (Map.Entry<Dependency, DependencyVersion> entry : versions.entrySet()) {
result.append(StringUtils.repeat("\t", indentation)).append(StringUtils.rightPad(entry.getKey().toString(), 40))
.append(" = ").append(entry.getValue()).append(System.lineSeparator());
}
return result.toString();
}
}

View File

@@ -1,110 +0,0 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.infra;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import lombok.experimental.FieldDefaults;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;
import org.bouncycastle.util.Strings;
import org.springframework.data.release.CliComponent;
import org.springframework.data.release.TimedCommand;
import org.springframework.data.release.git.GitOperations;
import org.springframework.data.release.io.JavaRuntimes;
import org.springframework.data.release.model.Projects;
import org.springframework.data.release.model.TrainIteration;
import org.springframework.data.release.utils.Logger;
import org.springframework.shell.core.annotation.CliCommand;
import org.springframework.shell.core.annotation.CliOption;
import org.springframework.shell.support.table.Table;
import org.springframework.shell.support.table.TableHeader;
/**
* Shell commands for dependency management.
*
* @author Mark Paluch
*/
@CliComponent
@RequiredArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
public class InfrastructureCommands extends TimedCommand {
DependencyOperations operations;
GitOperations git;
InfrastructureOperations infra;
Logger logger;
@CliCommand(value = "infra jdk list")
public Table listJdkVersions() {
List<JavaRuntimes.JdkInstallation> jdks = JavaRuntimes.getJdks();
StringBuilder builder = new StringBuilder("Available Java versions" + Strings.lineSeparator());
Table table = new Table();
table.addHeader(1, new TableHeader("Version", 15));
table.addHeader(2, new TableHeader("Vendor", 20));
table.addHeader(3, new TableHeader("Home"));
for (JavaRuntimes.JdkInstallation jdk : jdks) {
table.addRow(jdk.getVersion().toString(), jdk.getImplementor(), jdk.getHome().toString());
}
return table;
}
@CliCommand(value = "infra maven check")
public void check(@CliOption(key = "", mandatory = true) TrainIteration iteration,
@CliOption(key = "all", mandatory = false) Boolean reportAll) throws IOException {
git.checkout(iteration.getTrain());
DependencyUpgradeProposals proposals = operations.getMavenWrapperDependencyUpgradeProposals(iteration);
Files.write(Paths.get(InfrastructureOperations.MAVEN_PROPERTIES), proposals.asProperties(iteration).getBytes());
Table summary = proposals.toTable(reportAll == null ? false : reportAll);
logger.log(Projects.BUILD, "Upgrade summary:" + System.lineSeparator() + System.lineSeparator() + summary);
logger.log(iteration, "Upgrade proposals written to " + InfrastructureOperations.MAVEN_PROPERTIES);
}
@CliCommand(value = "infra maven upgrade")
public void upgradeMavenVersion(@CliOption(key = "", mandatory = true) TrainIteration iteration)
throws IOException, InterruptedException {
logger.log(iteration, "Applying Maven wrapper upgrades to Spring Data…");
infra.upgradeMavenVersion(iteration);
}
@CliCommand(value = "infra distribute ci-properties")
public void distributeCiProperties(@CliOption(key = "", mandatory = true) TrainIteration iteration)
throws IOException, InterruptedException {
logger.log(iteration, "Distributing CI properties for Spring Data…");
git.checkout(iteration.getTrain(), true);
infra.distributeCiProperties(iteration);
}
}

View File

@@ -1,157 +0,0 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.infra;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.experimental.FieldDefaults;
import java.io.File;
import java.io.FileInputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;
import java.util.Optional;
import java.util.Properties;
import java.util.concurrent.ExecutorService;
import org.apache.commons.io.FileUtils;
import org.springframework.data.release.TimedCommand;
import org.springframework.data.release.git.Branch;
import org.springframework.data.release.git.GitOperations;
import org.springframework.data.release.io.Workspace;
import org.springframework.data.release.issues.Tickets;
import org.springframework.data.release.model.Module;
import org.springframework.data.release.model.ModuleIteration;
import org.springframework.data.release.model.Project;
import org.springframework.data.release.model.Projects;
import org.springframework.data.release.model.Train;
import org.springframework.data.release.model.TrainIteration;
import org.springframework.data.release.utils.ExecutionUtils;
import org.springframework.data.release.utils.Logger;
import org.springframework.data.util.Streamable;
import org.springframework.stereotype.Component;
/**
* @author Mark Paluch
*/
@Component
@RequiredArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
public class InfrastructureOperations extends TimedCommand {
public static final String CI_PROPERTIES = "ci/pipeline.properties";
public static final String MAVEN_PROPERTIES = "dependency-upgrade-maven.properties";
DependencyOperations dependencies;
Workspace workspace;
GitOperations git;
ExecutorService executor;
Logger logger;
/**
* Distribute {@link #CI_PROPERTIES} from {@link Projects#BUILD} to all modules within {@link TrainIteration}.
*
* @param iteration
*/
void distributeCiProperties(TrainIteration iteration) {
File master = workspace.getFile(CI_PROPERTIES, Projects.BUILD);
if (!master.exists()) {
throw new IllegalStateException(String.format("CI Properties file %s does not exist", master));
}
ExecutionUtils.run(executor, iteration, module -> {
Project project = module.getProject();
Branch branch = Branch.from(module);
git.update(project);
git.checkout(project, branch);
});
verifyExistingPropertyFiles(iteration.getTrain(), master);
ExecutionUtils.run(executor, Streamable.of(iteration.getModulesExcept(Projects.BUILD)), module -> {
File target = workspace.getFile(CI_PROPERTIES, module.getProject());
target.delete();
FileUtils.copyFile(master, target);
git.add(module.getProject(), CI_PROPERTIES);
git.commit(module, "Update CI properties.", Optional.empty(), false);
git.push(module);
});
}
private void verifyExistingPropertyFiles(Train train, File master) {
for (Module module : train) {
File target = workspace.getFile(CI_PROPERTIES, module.getProject());
if (!target.exists()) {
throw new IllegalStateException(String.format("CI Properties file %s does not exist", master));
}
}
}
public void upgradeMavenVersion(TrainIteration iteration) {
DependencyVersions dependencyVersions = loadDependencyUpgrades(iteration);
if (dependencyVersions.isEmpty()) {
throw new IllegalStateException("No version to upgrade found!");
}
git.checkout(iteration.getTrain(), false);
List<Project> projectsToUpgrade = dependencies
.getProjectsToUpgradeMavenWrapper(dependencyVersions.get(Dependencies.MAVEN), iteration);
ExecutionUtils.run(executor, Streamable.of(projectsToUpgrade), project -> {
ModuleIteration module = iteration.getModule(project);
Tickets tickets = dependencies.getOrCreateUpgradeTickets(module, dependencyVersions);
dependencies.upgradeMavenWrapperVersion(tickets, module, dependencyVersions);
git.push(module);
// Allow GitHub to catch up with ticket notifications.
Thread.sleep(1500);
dependencies.closeUpgradeTickets(module, tickets);
});
}
@SneakyThrows
private DependencyVersions loadDependencyUpgrades(TrainIteration iteration) {
if (!Files.exists(Paths.get(MAVEN_PROPERTIES))) {
logger.log(iteration, "Cannot upgrade dependencies: " + MAVEN_PROPERTIES + " does not exist.");
}
Properties properties = new Properties();
try (FileInputStream fis = new FileInputStream(MAVEN_PROPERTIES)) {
properties.load(fis);
}
return DependencyUpgradeProposals.fromProperties(iteration, properties);
}
}

View File

@@ -1,216 +0,0 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.infra;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import lombok.experimental.FieldDefaults;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.filefilter.AbstractFileFilter;
import org.apache.commons.io.filefilter.IOFileFilter;
import org.apache.commons.io.filefilter.NameFileFilter;
import org.apache.commons.io.filefilter.NotFileFilter;
import org.springframework.data.release.CliComponent;
import org.springframework.data.release.TimedCommand;
import org.springframework.data.release.git.GitOperations;
import org.springframework.data.release.io.Workspace;
import org.springframework.data.release.issues.IssueTracker;
import org.springframework.data.release.issues.Ticket;
import org.springframework.data.release.issues.TicketOperations;
import org.springframework.data.release.model.ModuleIteration;
import org.springframework.data.release.model.Project;
import org.springframework.data.release.model.Projects;
import org.springframework.data.release.model.TrainIteration;
import org.springframework.data.release.utils.ExecutionUtils;
import org.springframework.data.release.utils.Logger;
import org.springframework.data.util.Streamable;
import org.springframework.shell.core.annotation.CliCommand;
import org.springframework.shell.core.annotation.CliOption;
import org.springframework.util.AntPathMatcher;
/**
* @author Mark Paluch
*/
@CliComponent
@RequiredArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
public class LicenseHeaderCommands extends TimedCommand {
GitOperations git;
Workspace workspace;
Executor executor;
Logger logger;
TicketOperations tickets;
List<String> filePatterns = Arrays.asList("pom.xml", "**/*.java", "**/*.kt", "**/*.adoc");
/**
* Process all files matching {@link #filePatterns} and update the Apache license header year range, extending to
* {@code year}. Rewrites single-year and year-range formats.
*
* @param iteration
* @param year
*/
@CliCommand(value = "update license-headers")
public void updateLicenseHeaders(@CliOption(key = "", mandatory = true) TrainIteration iteration,
@CliOption(key = "year", mandatory = true) int year,
@CliOption(key = "project", mandatory = false) String projectName) {
git.prepare(iteration);
Streamable<ModuleIteration> modules = iteration;
if (projectName != null) {
Project project = Projects.requiredByName(projectName);
modules = modules.filter(it -> it.getProject().equals(project));
}
ExecutionUtils.run(executor, modules, module -> {
String summary = String.format("Extend license header copyright years to %d", year);
int updated = replaceInFiles(module.getProject(), content -> {
String contentToUse = content;
contentToUse = contentToUse.replaceAll("(C) ([\\d]{4})-([\\d]{4})", "(C) $1-" + year);
contentToUse = contentToUse.replaceAll("Copyright ([\\d]{4}) the original author or authors",
"Copyright $1-" + year + " the original author or authors");
contentToUse = contentToUse.replaceAll("Copyright ([\\d]{4})-([\\d]{4}) the original author or authors",
"Copyright $1-" + year + " the original author or authors");
return contentToUse;
});
if (updated > 0) {
commitAndPushWithTicket(module, summary);
}
});
}
private void commitAndPushWithTicket(ModuleIteration module, String ticketSummary) throws InterruptedException {
Ticket ticket = tickets.getOrCreateTicketsWithSummary(module, IssueTracker.TicketType.Task, ticketSummary);
git.commit(module, ticket, ticketSummary, Optional.empty(), true);
try {
git.push(module);
} catch (Exception e) {
logger.warn(module, e);
}
TimeUnit.SECONDS.sleep(1);
tickets.closeTicket(module, ticket);
}
/**
* Replace content in files by applying {@link Function contentFunction} and return the number of updated files.
*
* @param project
* @param contentFunction
* @return
*/
private int replaceInFiles(Project project, Function<String, String> contentFunction) {
File projectDirectory = workspace.getProjectDirectory(project);
IOFileFilter fileFilter = new AntPathFileFilter(projectDirectory, filePatterns);
int files = 0;
int modified = 0;
Iterator<File> fileIterator = FileUtils.iterateFiles(projectDirectory, fileFilter,
new NotFileFilter(new NameFileFilter(".git")));
while (fileIterator.hasNext()) {
File file = fileIterator.next();
files++;
try {
if (doReplace(file, contentFunction)) {
modified++;
}
} catch (IOException e) {
throw new IllegalStateException(String.format("Cannot modify contents of %s", file), e);
}
}
logger.log(project, "Found %s files, updated %s files", files, modified);
return modified;
}
private boolean doReplace(File file, Function<String, String> modifyFunction) throws IOException {
String content = FileUtils.readFileToString(file, StandardCharsets.UTF_8);
String modified = modifyFunction.apply(content);
if (!content.equals(modified)) {
FileUtils.write(file, modified, StandardCharsets.UTF_8);
return true;
}
return false;
}
private static class AntPathFileFilter extends AbstractFileFilter {
private final URI projectDirectory;
private final List<String> filePatterns;
public AntPathFileFilter(File basePath, List<String> filePatterns) {
this.projectDirectory = basePath.toURI();
this.filePatterns = filePatterns;
}
@Override
public boolean accept(File file) {
String relativePath = projectDirectory.relativize(file.toURI()).getPath();
AntPathMatcher matcher = new AntPathMatcher();
for (String pattern : filePatterns) {
if (matcher.match(pattern, relativePath)) {
return true;
}
}
return false;
}
}
}

View File

@@ -1,160 +0,0 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.infra;
import lombok.Value;
import java.util.Iterator;
import java.util.List;
import org.springframework.data.release.model.Project;
import org.springframework.data.release.model.Projects;
import org.springframework.data.util.Streamable;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
/**
* Configuration of dependencies for a specific project.
*
* @author Mark Paluch
*/
public class ProjectDependencies implements Streamable<ProjectDependencies.ProjectDependency> {
private static final MultiValueMap<Project, ProjectDependency> config = new LinkedMultiValueMap<>();
static {
config.add(Projects.BUILD, ProjectDependency.ofProperty("apt", Dependencies.APT));
config.add(Projects.BUILD, ProjectDependency.ofProperty("aspectj", Dependencies.ASPECTJ));
config.add(Projects.BUILD, ProjectDependency.ofProperty("assertj", Dependencies.ASSERTJ));
config.add(Projects.BUILD, ProjectDependency.ofProperty("jackson", Dependencies.JACKSON));
config.add(Projects.BUILD, ProjectDependency.ofProperty("jacoco", Dependencies.JACOCO));
config.add(Projects.BUILD, ProjectDependency.ofProperty("jodatime", Dependencies.JODA_TIME));
config.add(Projects.BUILD, ProjectDependency.ofProperty("junit5", Dependencies.JUNIT5));
config.add(Projects.BUILD, ProjectDependency.ofProperty("jmolecules", Dependencies.JMOLECULES));
config.add(Projects.BUILD,
ProjectDependency.ofProperty("jmolecules-integration", Dependencies.JMOLECULES_INTEGRATION));
config.add(Projects.BUILD, ProjectDependency.ofProperty("junit", Dependencies.JUNIT4));
config.add(Projects.BUILD, ProjectDependency.ofProperty("kotlin", Dependencies.KOTLIN));
config.add(Projects.BUILD, ProjectDependency.ofProperty("kotlin-coroutines", Dependencies.KOTLIN_COROUTINES));
config.add(Projects.BUILD, ProjectDependency.ofProperty("micrometer", Dependencies.MICROMETER));
config.add(Projects.BUILD, ProjectDependency.ofProperty("micrometer-tracing", Dependencies.MICROMETER_TRACING));
config.add(Projects.BUILD, ProjectDependency.ofProperty("mockito", Dependencies.MOCKITO));
config.add(Projects.BUILD, ProjectDependency.ofProperty("mockk", Dependencies.MOCKK));
config.add(Projects.BUILD, ProjectDependency.ofProperty("querydsl", Dependencies.QUERYDSL));
config.add(Projects.BUILD, ProjectDependency.ofProperty("rxjava", Dependencies.RXJAVA1));
config.add(Projects.BUILD, ProjectDependency.ofProperty("rxjava2", Dependencies.RXJAVA2));
config.add(Projects.BUILD, ProjectDependency.ofProperty("rxjava3", Dependencies.RXJAVA3));
config.add(Projects.BUILD, ProjectDependency.ofProperty("rxjava-reactive-streams", Dependencies.RXJAVA_RS));
config.add(Projects.BUILD, ProjectDependency.ofProperty("smallrye-mutiny", Dependencies.SMALLRYE_MUTINY));
config.add(Projects.BUILD, ProjectDependency.ofProperty("spring-hateoas", Dependencies.SPRING_HATEOAS));
config.add(Projects.BUILD, ProjectDependency.ofProperty("spring-plugin", Dependencies.SPRING_PLUGIN));
config.add(Projects.BUILD, ProjectDependency.ofProperty("testcontainers", Dependencies.TESTCONTAINERS));
config.add(Projects.BUILD, ProjectDependency.ofProperty("threetenbp", Dependencies.THREE_TEN_BP));
config.add(Projects.BUILD, ProjectDependency.ofProperty("webbeans", Dependencies.OPEN_WEB_BEANS));
config.add(Projects.COMMONS, ProjectDependency.ofProperty("vavr", Dependencies.VAVR));
config.add(Projects.COMMONS, ProjectDependency.ofProperty("xmlbeam", Dependencies.XML_BEAM));
config.add(Projects.MONGO_DB, ProjectDependency.ofProperty("mongo.reactivestreams", Dependencies.MONGODB_RS));
config.add(Projects.MONGO_DB, ProjectDependency.ofProperty("mongo", Dependencies.MONGODB_LEGACY));
config.add(Projects.MONGO_DB, ProjectDependency.ofProperty("mongo", Dependencies.MONGODB_CORE));
config.add(Projects.MONGO_DB, ProjectDependency.ofProperty("mongo", Dependencies.MONGODB_SYNC));
config.add(Projects.MONGO_DB, ProjectDependency.ofProperty("mongo", Dependencies.MONGODB_ASYNC));
config.add(Projects.REDIS, ProjectDependency.ofProperty("lettuce", Dependencies.LETTUCE));
config.add(Projects.REDIS, ProjectDependency.ofProperty("jedis", Dependencies.JEDIS));
config.add(Projects.CASSANDRA,
ProjectDependency.ofProperty("cassandra-driver.version", Dependencies.CASSANDRA_DRIVER3));
config.add(Projects.CASSANDRA,
ProjectDependency.ofProperty("cassandra-driver.version", Dependencies.CASSANDRA_DRIVER4));
config.add(Projects.NEO4J, ProjectDependency.ofProperty("neo4j.ogm.version", Dependencies.NEO4J_OGM));
config.add(Projects.NEO4J, ProjectDependency.ofProperty("neo4j-java-driver.version", Dependencies.NEO4J_DRIVER));
config.add(Projects.COUCHBASE, ProjectDependency.ofProperty("couchbase", Dependencies.COUCHBASE));
config.add(Projects.ELASTICSEARCH, ProjectDependency.ofProperty("elasticsearch", Dependencies.ELASTICSEARCH_RHLC));
config.add(Projects.ELASTICSEARCH,
ProjectDependency.ofProperty("elasticsearch-rhlc", Dependencies.ELASTICSEARCH_RHLC));
config.add(Projects.ELASTICSEARCH,
ProjectDependency.ofProperty("elasticsearch-java", Dependencies.ELASTICSEARCH_REST_CLIENT));
config.add(Projects.LDAP, ProjectDependency.ofProperty("spring-ldap", Dependencies.SPRING_LDAP));
}
private final List<ProjectDependency> dependencies;
private ProjectDependencies(List<ProjectDependency> dependencies) {
this.dependencies = dependencies;
}
/**
* Retrieve upgradable dependencies for a {@link Project}.
*
* @param project
* @return
* @throws IllegalArgumentException if the project has no upgradable dependencies.
*/
public static ProjectDependencies get(Project project) {
if (!containsProject(project)) {
throw new IllegalArgumentException(String.format("No dependency configuration for %s!", project));
}
return new ProjectDependencies(config.get(project));
}
/**
* Check whether the {@link Project} has upgradable dependencies.
*
* @param project
* @return
*/
public static boolean containsProject(Project project) {
return config.containsKey(project);
}
public String getVersionPropertyFor(Dependency dependency) {
for (ProjectDependency projectDependency : dependencies) {
if (projectDependency.getDependency().equals(dependency)) {
return projectDependency.getProperty();
}
}
throw new IllegalArgumentException("Dependency " + dependency + " is not a dependency of this project!");
}
@Override
public Iterator<ProjectDependency> iterator() {
return dependencies.iterator();
}
@Value
public static class ProjectDependency {
String property;
Dependency dependency;
public static ProjectDependency ofProperty(String pomProperty, Dependency dependency) {
return new ProjectDependency(pomProperty, dependency);
}
}
}

View File

@@ -1,33 +0,0 @@
/*
* Copyright 2014-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.io;
import lombok.Value;
/**
* @author Oliver Gierke
*/
@Value
public class CommandResult {
private final int status;
private final String output;
private final Exception exception;
public boolean hasError() {
return status != 0;
}
}

View File

@@ -1,47 +0,0 @@
/*
* Copyright 2015-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.io;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
import org.apache.commons.io.FileUtils;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* @author Oliver Gierke
* @author Mark Paluch
* @author Christoph Strobl
*/
@Slf4j
@Data
@Component
@ConfigurationProperties(prefix = "io")
class IoProperties {
private File workDir, logs;
public void setWorkDir(String workDir) {
log.info(String.format("🔧 Using %s as working directory!", workDir));
this.workDir = new File(workDir.replace("~", FileUtils.getUserDirectoryPath()));
}
}

View File

@@ -1,343 +0,0 @@
/*
* Copyright 2022-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.io;
import lombok.SneakyThrows;
import lombok.Value;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileFilter;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.filefilter.RegexFileFilter;
import org.springframework.boot.system.SystemProperties;
import org.springframework.data.release.model.JavaVersion;
import org.springframework.data.release.model.Version;
import org.springframework.data.util.Lazy;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StreamUtils;
import com.dd.plist.NSArray;
import com.dd.plist.NSDictionary;
import com.dd.plist.XMLPropertyListParser;
/**
* Utility to detect a Java runtime version.
*
* @author Mark Paluch
*/
public class JavaRuntimes {
private static final List<JdkDetector> DETECTORS = Arrays.asList(new SDKmanJdkDetector(), new MacNativeJdkDetector(),
new JavaHomeJdkDetector());
private static final Lazy<List<JdkInstallation>> JDKS = Lazy.of(() -> {
List<JdkInstallation> jdks = DETECTORS.stream() //
.filter(JdkDetector::isAvailable) //
.flatMap(it -> it.detect().stream()) //
.sorted() //
.collect(Collectors.toList());
Collections.reverse(jdks);
return Collections.unmodifiableList(jdks);
});
/**
* Lookup a {@link JdkInstallation} by detecting installed JDKs and applying the {@link Predicate filter}. Returns the
* first matching one or throws {@link NoSuchElementException}.
*
* @param filter
* @return
*/
public static JdkInstallation getJdk(Predicate<JdkInstallation> filter) {
return getJdk(filter, () -> "Cannot obtain required JDK");
}
/**
* Lookup a {@link JdkInstallation} by detecting installed JDKs and applying the {@link Predicate filter}. Returns the
* first matching one or throws {@link NoSuchElementException}.
*
* @param filter
* @param message
* @return
*/
public static JdkInstallation getJdk(Predicate<JdkInstallation> filter, Supplier<String> message) {
List<JdkInstallation> jdks = JDKS.get();
return jdks.stream().filter(filter).findFirst()
.orElseThrow(() -> new NoSuchElementException(String.format("%s%nAvailable JDK: %s", message.get(), jdks)));
}
public static List<JdkInstallation> getJdks() {
return JDKS.get();
}
static boolean isDirectory(File file) {
return file.exists() && file.isDirectory();
}
/**
* JDK detection strategy.
*/
interface JdkDetector {
/**
* @return {@code true} if the detector strategy is available.
*/
boolean isAvailable();
/**
* @return a list of JDK installations.
*/
List<JdkInstallation> detect();
}
/**
* Selector to determine a {@link JdkInstallation}.
*/
public static class Selector {
private String notFoundMessage;
private Predicate<JdkInstallation> predicate;
private Selector() {
}
public static Selector builder() {
return new Selector();
}
public static Selector from(JavaVersion javaVersion) {
return builder()
.and(it -> javaVersion.getVersionDetector().test(it.getVersion())
&& javaVersion.getImplementor().test(it.getImplementor()))
.message("Cannot find Java " + javaVersion.getName());
}
public Selector and(Predicate<JdkInstallation> predicate) {
this.predicate = this.predicate == null ? predicate : this.predicate.and(predicate);
return this;
}
public Selector notGraalVM() {
this.notFoundMessage += " (Not GraalVM)";
return and(it -> !it.getName().contains("GraalVM"));
}
public Selector message(String notFoundMessage) {
this.notFoundMessage = notFoundMessage;
return this;
}
public JdkInstallation getRequiredJdkInstallation() {
return JavaRuntimes.getJdk(predicate, () -> notFoundMessage);
}
}
/**
* Detector using the SDKman utility storing Java installations in {@code ~/.sdkman/candidates/java}.
*/
static class SDKmanJdkDetector implements JdkDetector {
private static final File sdkManJavaHome = new File(FileUtils.getUserDirectoryPath(), ".sdkman/candidates/java");
private static final Pattern CANDIDATE = Pattern.compile("(\\d+[\\.\\d+]+)[.-][-a-zA-Z]*");
@Override
public boolean isAvailable() {
return isDirectory(sdkManJavaHome);
}
@Override
public List<JdkInstallation> detect() {
File[] files = sdkManJavaHome.listFiles((FileFilter) new RegexFileFilter(CANDIDATE));
return Arrays.stream(files).map(it -> {
Matcher matcher = CANDIDATE.matcher(it.getName());
if (!matcher.find()) {
throw new IllegalArgumentException("Cannot determine JVM version number from SDKman candidate name "
+ it.getName() + ". This should not happen in an ideal world, check the CANDIDATE regex.");
}
String candidateVersion = matcher.group(1);
Version version = Version.parse(candidateVersion);
if (version.getMajor() <= 8) {
candidateVersion = "1." + candidateVersion;
version = Version.parse(candidateVersion);
}
String implementor = normalizeImplementor(parseImplementor(it));
return new JdkInstallation(version, toDisplayName(implementor, candidateVersion), implementor, it);
}).collect(Collectors.toList());
}
@SneakyThrows
private String parseImplementor(File candidateHome) {
List<String> release = FileUtils.readLines(new File(candidateHome, "release"));
for (String line : release) {
if (line.startsWith("IMPLEMENTOR=")) {
String substring = line.substring(line.indexOf("=\""));
substring = substring.substring(2, substring.length() - 1);
return substring;
}
}
return "?";
}
}
/**
* Detector using the {@code java.home} system property.
*/
static class JavaHomeJdkDetector implements JdkDetector {
private static final File javaHome = new File(System.getProperty("java.home"));
private static final String javaVersion = System.getProperty("java.version");
private static final String javaVendor = System.getProperty("java.vendor");
@Override
public boolean isAvailable() {
return isDirectory(javaHome);
}
@Override
public List<JdkInstallation> detect() {
return Collections.singletonList(new JdkInstallation(JavaVersion.parse(javaVersion),
toDisplayName(javaVendor, javaVersion), normalizeImplementor(javaVendor), javaHome));
}
}
/**
* Detector using the {@code /usr/libexec/java_home} utility storing Java installations in {@code /Libraries/Java} on
* the Mac.
*/
static class MacNativeJdkDetector implements JdkDetector {
private static final File javaHomeBinary = new File("/usr/libexec/java_home");
private static final File nativeInstallationDirectory = new File("/Library/Java/JavaVirtualMachines");
private static final Pattern VERSION = Pattern.compile("((:?\\d+(:?\\.\\d+)*)(:?_+\\d+)?)");
@Override
public boolean isAvailable() {
return isDirectory(nativeInstallationDirectory) && !ObjectUtils.isEmpty(nativeInstallationDirectory.listFiles())
&& javaHomeBinary.exists() && SystemProperties.get("os.name").contains("Mac");
}
@Override
@SneakyThrows
public List<JdkInstallation> detect() {
Process process = new ProcessBuilder(javaHomeBinary.toString(), "-X").redirectOutput(ProcessBuilder.Redirect.PIPE)
.start();
process.waitFor(5, TimeUnit.SECONDS);
byte[] out = StreamUtils.copyToByteArray(process.getInputStream());
if (process.exitValue() != 0) {
throw new IllegalStateException(javaHomeBinary + " failed with: " + System.lineSeparator() + new String(out)
+ new String(StreamUtils.copyToByteArray(process.getErrorStream())));
}
NSArray array = (NSArray) XMLPropertyListParser.parse(new ByteArrayInputStream(out));
return Arrays.stream(array.getArray()).map(it -> {
NSDictionary dict = (NSDictionary) it;
String jvmHomePath = dict.get("JVMHomePath").toJavaObject(String.class);
String name = dict.get("JVMName").toJavaObject(String.class);
String version = dict.get("JVMVersion").toJavaObject(String.class);
String vendor = dict.get("JVMVendor").toJavaObject(String.class);
Matcher matcher = VERSION.matcher(version);
if (!matcher.find()) {
throw new IllegalArgumentException("Cannot determine JVM version number from JVMVersion " + version
+ ". This should not happen in an ideal world, check the VERSION regex.");
}
String implementor = normalizeImplementor(vendor);
return new JdkInstallation(JavaVersion.parse(matcher.group(1)), toDisplayName(implementor, version),
implementor, new File(jvmHomePath));
}).collect(Collectors.toList());
}
}
@Value
public static class JdkInstallation implements Comparable<JdkInstallation> {
Version version;
String name;
String implementor;
File home;
@Override
public int compareTo(JdkInstallation o) {
return this.version.compareTo(o.version);
}
}
static String normalizeImplementor(String implementor) {
if (implementor.equals("Eclipse Adoptium")) {
return "Eclipse Temurin";
}
if (implementor.equals("Eclipse Foundation")) {
return "Eclipse Temurin";
}
return implementor;
}
static String toDisplayName(String implementor, String version) {
if (implementor.startsWith("Oracle")) {
implementor = "Oracle Java";
}
return implementor + " " + version;
}
}

View File

@@ -1,247 +0,0 @@
/*
* Copyright 2014-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.io;
import static org.springframework.data.release.utils.StreamUtils.*;
import lombok.AccessLevel;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.experimental.FieldDefaults;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Arrays;
import java.util.Collections;
import java.util.Optional;
import java.util.Scanner;
import java.util.function.Predicate;
import java.util.stream.Stream;
import javax.annotation.PostConstruct;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.data.release.model.Project;
import org.springframework.data.release.utils.Logger;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
/**
* Abstraction of the workspace that is used to work with the {@link Project}'s repositories, execute builds, etc.
*
* @author Oliver Gierke
*/
@Component
@RequiredArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
public class Workspace {
private static final Charset UTF_8 = StandardCharsets.UTF_8;
@NonNull IoProperties ioProperties;
@NonNull ResourcePatternResolver resolver;
@NonNull Logger logger;
/**
* Returns the current working directory.
*
* @return
*/
public File getWorkingDirectory() {
return ioProperties.getWorkDir();
}
/**
* Returns the current logs directory.
*
* @return
*/
public File getLogsDirectory() {
return ioProperties.getLogs();
}
/**
* Cleans up the working directory by removing all files and folders in it.
*
* @throws IOException
*/
public void cleanup() throws IOException {
delete(getWorkingDirectory().toPath(), "workspace");
delete(getLogsDirectory().toPath(), "logs");
}
private void delete(Path path, String type) throws IOException {
logger.log("Workspace", "Cleaning up %s directory at %s.", type, path.toAbsolutePath());
Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Files.delete(file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
if (!path.equals(dir)) {
Files.delete(dir);
}
return FileVisitResult.CONTINUE;
}
});
}
public void purge(Path path, Predicate<Path> filter) throws IOException {
Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
if (filter.test(file)) {
Files.delete(file);
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
if (filter.test(dir)) {
Files.delete(dir);
}
return FileVisitResult.CONTINUE;
}
});
}
/**
* Returns the directory for the given {@link Project}.
*
* @param project must not be {@literal null}.
* @return
*/
public File getProjectDirectory(Project project) {
Assert.notNull(project, "Project must not be null!");
return new File(getWorkingDirectory(), project.getFolderName());
}
/**
* Returns whether the project directory for the given project already exists.
*
* @param project must not be {@literal null}.
* @return
*/
public boolean hasProjectDirectory(Project project) {
Assert.notNull(project, "Project must not be null!");
return getProjectDirectory(project).exists();
}
/**
* Returns a file with the given name relative to the working directory for the given {@link Project}.
*
* @param name must not be {@literal null} or empty.
* @param project must not be {@literal null}.
* @return
*/
public File getFile(String name, Project project) {
Assert.hasText(name, "Filename must not be null or empty!");
Assert.notNull(project, "Project must not be null!");
return new File(getProjectDirectory(project), name);
}
public Stream<File> getFiles(String pattern, Project project) {
File projectDirectory = getProjectDirectory(project);
String patternToLookup = String.format("file:%s/%s", projectDirectory.getAbsolutePath(), pattern);
try {
return Arrays.stream(resolver.getResources(patternToLookup)).map(wrap(Resource::getFile));
} catch (IOException o_O) {
throw new RuntimeException(o_O);
}
}
public boolean processFile(String filename, Project project, LineCallback callback) {
File file = getFile(filename, project);
if (!file.exists()) {
return false;
}
StringBuilder builder = new StringBuilder();
try (Scanner scanner = new Scanner(file)) {
long number = 0;
while (scanner.hasNextLine()) {
callback.doWith(scanner.nextLine(), number++).ifPresent(it -> builder.append(it).append("\n"));
}
writeContentToFile(filename, project, builder.toString());
} catch (Exception o_O) {
throw new RuntimeException(o_O);
}
return true;
}
private void writeContentToFile(String name, Project project, String content) throws IOException {
File file = getFile(name, project);
Files.write(file.toPath(), Collections.singleton(content), UTF_8);
}
/**
* Initializes the working directory and creates the folders if necessary.
*
* @throws IOException
*/
@PostConstruct
public void setUp() throws IOException {
Path path = getWorkingDirectory().toPath();
if (!java.nio.file.Files.exists(path)) {
java.nio.file.Files.createDirectories(path);
}
}
public interface LineCallback {
Optional<String> doWith(String line, long number);
}
}

View File

@@ -1,45 +0,0 @@
/*
* Copyright 2016-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.io;
import lombok.AccessLevel;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.experimental.FieldDefaults;
import java.io.IOException;
import org.springframework.data.release.CliComponent;
import org.springframework.data.release.TimedCommand;
import org.springframework.data.release.utils.Logger;
import org.springframework.shell.core.annotation.CliCommand;
/**
* @author Oliver Gierke
*/
@CliComponent
@RequiredArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
class WorkspaceCommands extends TimedCommand {
@NonNull Workspace workspace;
@NonNull Logger logger;
@CliCommand("workspace cleanup")
public void cleanup() throws IOException {
workspace.cleanup();
}
}

View File

@@ -1,86 +0,0 @@
/*
* Copyright 2014-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.issues;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Date;
import java.util.Locale;
import org.apache.commons.io.IOUtils;
import org.springframework.data.release.model.ArtifactVersion;
import org.springframework.data.release.model.ModuleIteration;
import org.springframework.format.datetime.DateFormatter;
/**
* @author Oliver Gierke
*/
@RequiredArgsConstructor(staticName = "of")
@EqualsAndHashCode
public class Changelog {
@Getter private final ModuleIteration module;
private final Tickets tickets;
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return toString(true, "");
}
public String toString(boolean header, String indentation) {
ArtifactVersion version = ArtifactVersion.of(module);
String headline = String.format("Changes in version %s (%s)", version,
new DateFormatter("YYYY-MM-dd").print(new Date(), Locale.US));
StringBuilder builder = new StringBuilder();
if (header) {
builder.append(indentation).append(headline).append(IOUtils.LINE_SEPARATOR).append(indentation);
for (int i = 0; i < headline.length(); i++) {
builder.append("-");
}
builder.append(IOUtils.LINE_SEPARATOR);
}
for (Ticket ticket : tickets) {
String summary = ticket.getSummary();
builder.append(indentation).append("* ").append(ticket.getId()).append(" - ")
.append(summary != null ? summary.trim() : "");
if (!summary.endsWith(".")) {
builder.append(".");
}
builder.append(IOUtils.LINE_SEPARATOR);
}
return builder.toString();
}
}

View File

@@ -1,193 +0,0 @@
/*
* Copyright 2014-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.issues;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.data.release.model.Iteration;
import org.springframework.data.release.model.ModuleIteration;
import org.springframework.data.release.model.Project;
import org.springframework.data.release.model.Train;
import org.springframework.data.release.model.TrainIteration;
import org.springframework.plugin.core.Plugin;
/**
* Interface for issue tracker operations.
*
* @author Oliver Gierke
* @author Mark Paluch
*/
public interface IssueTracker extends Plugin<Project> {
/**
* Reset internal state (cache, etc).
*/
void reset();
/**
* Returns all {@link Tickets} for the given {@link TrainIteration}.
*
* @param iteration must not be {@literal null}.
* @return
*/
Tickets getTicketsFor(TrainIteration iteration);
/**
* Returns all {@link Tickets} for the given {@link ModuleIteration}.
*
* @param iteration must not be {@literal null}.
* @return
*/
Tickets getTicketsFor(ModuleIteration iteration);
/**
* Returns all {@link Tickets} for the given {@link Train} and {@link Iteration}.
*
* @param iteration must not be {@literal null}.
* @param forCurrentUser
* @return
*/
Tickets getTicketsFor(TrainIteration iteration, boolean forCurrentUser);
/**
* Returns the {@link Ticket} that tracks modifications in the context of a release.
*
* @param module the module to lookup the {@link Ticket} for, must not be {@literal null}.
* @return
*/
Ticket getReleaseTicketFor(ModuleIteration module);
/**
* Query the issue tracker for multiple {@link Ticket#id ticket Ids}. Tickets that are not found are not returned
* within the result.
*
* @param project must not be {@literal null}.
* @param ticketIds collection of {@link Ticket#id ticket Ids}, must not be {@literal null}.
* @return
*/
Collection<Ticket> findTickets(Project project, Collection<String> ticketIds);
/**
* Query the issue tracker for multiple {@link Ticket#id ticket Ids}. Tickets that are not found are not returned. The
* implementation ensures to resolve only references that match the issue tracker scheme this issue tracker is
* responsible for.
*
* @param moduleIteration must not be {@literal null}.
* @param ticketReferences must not be {@literal null}.
* @return
*/
Tickets findTickets(ModuleIteration moduleIteration, Collection<String> ticketIds);
/**
* Creates a release version if release version is missing.
*
* @param module must not be {@literal null}.
*/
void createReleaseVersion(ModuleIteration module);
/**
* Retire the release version from the active versions for a {@link ModuleIteration}.
*
* @param module must not be {@literal null}.
*/
void archiveReleaseVersion(ModuleIteration module);
/**
* Create release ticket if release ticket is missing.
* <p>
* TODO: Return created ticket
*
* @param module must not be {@literal null}.
*/
void createReleaseTicket(ModuleIteration module);
/**
* Creates a ticket for the given {@link ModuleIteration} and summary {@code text}.
*
* @param module must not be {@literal null}.
* @param text the text to use.
* @param ticketType the ticket type.
* @param assignToCurrentUser
* @return the created ticket.
*/
Ticket createTicket(ModuleIteration module, String text, TicketType ticketType, boolean assignToCurrentUser);
/**
* Assigns the ticket to the current user.
*
* @param project must not be {@literal null}.
* @param ticket must not be {@literal null}.
*/
Ticket assignTicketToMe(Project project, Ticket ticket);
/**
* Assigns the release ticket for the given {@link ModuleIteration} to the current user.
*
* @param module must not be {@literal null}.
* @return
*/
Ticket assignReleaseTicketToMe(ModuleIteration module);
/**
* Start progress on release tickets.
*
* @param module
* @return
*/
Ticket startReleaseTicketProgress(ModuleIteration module);
/**
* Returns the {@link Changelog} for the given {@link ModuleIteration}.
*
* @param module must not be {@literal null}.
* @return
*/
Changelog getChangelogFor(ModuleIteration module);
/**
* Returns the {@link Changelog} for the given {@link ModuleIteration} using {@link TicketReference}s.
*
* @param module must not be {@literal null}.
* @return
*/
default Changelog getChangelogFor(ModuleIteration module, List<TicketReference> ticketReferences) {
Tickets tickets = findTickets(module,
ticketReferences.stream().map(TicketReference::getId).collect(Collectors.toList()));
return Changelog.of(module, tickets);
}
/**
* Closes the given {@link ModuleIteration}.
*
* @param module must not be {@literal null}.
*/
void closeIteration(ModuleIteration module);
/**
* Resolve a {@link Ticket}.
*
* @param module must not be {@literal null}.
* @param ticket must not be {@literal null}.
*/
void closeTicket(ModuleIteration module, Ticket ticket);
enum TicketType {
Task, DependencyUpgrade;
}
}

View File

@@ -1,204 +0,0 @@
/*
* Copyright 2014-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.issues;
import static org.springframework.data.release.utils.ExecutionUtils.*;
import lombok.AccessLevel;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.experimental.FieldDefaults;
import java.util.concurrent.Executor;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import org.springframework.data.release.CliComponent;
import org.springframework.data.release.TimedCommand;
import org.springframework.data.release.model.ModuleIteration;
import org.springframework.data.release.model.Project;
import org.springframework.data.release.model.Projects;
import org.springframework.data.release.model.TrainIteration;
import org.springframework.data.release.utils.ExecutionUtils;
import org.springframework.plugin.core.PluginRegistry;
import org.springframework.shell.core.annotation.CliCommand;
import org.springframework.shell.core.annotation.CliOption;
import org.springframework.util.StringUtils;
/**
* @author Oliver Gierke
* @author Mark Paluch
*/
@CliComponent
@RequiredArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
public class IssueTrackerCommands extends TimedCommand {
@NonNull PluginRegistry<IssueTracker, Project> tracker;
@NonNull Executor executor;
@CliCommand("tracker evict")
public void evict() {
StreamSupport.stream(tracker.spliterator(), false).forEach(IssueTracker::reset);
}
@CliCommand(value = "tracker tickets")
public String jira(@CliOption(key = "", mandatory = true) TrainIteration iteration, //
@CliOption(key = "for-current-user", specifiedDefaultValue = "true",
unspecifiedDefaultValue = "false") boolean forCurrentUser) {
return tracker.getPlugins().stream().//
flatMap(it -> it.getTicketsFor(iteration, forCurrentUser).stream()).//
collect(Tickets.toTicketsCollector()).toString();
}
@CliCommand(value = "tracker releasetickets")
public String releaseTickets(@CliOption(key = "", mandatory = true) TrainIteration iteration) {
return runAndReturn(executor, iteration, module -> getTrackerFor(module).getReleaseTicketFor(module),
Tickets.toTicketsCollector()).toString();
}
/**
* Prepare this release by self-assigning release tickets and setting them to in-progress.
*
* @param iteration
* @return
*/
@CliCommand(value = "tracker prepare")
public String trackerPrepare(@CliOption(key = "", mandatory = true) TrainIteration iteration) {
jiraSelfAssignReleaseTickets(iteration);
return jiraStartProgress(iteration);
}
/**
* Prepare a new, upcoming release by creating release versions and release tickets.
*
* @param iteration
* @return
*/
@CliCommand(value = "tracker setup-next")
public String trackerSetupNext(@CliOption(key = "", mandatory = true) TrainIteration iteration) {
jiraCreateReleaseVersions(iteration);
return createReleaseTickets(iteration);
}
@CliCommand(value = "tracker self-assign releasetickets")
public String jiraSelfAssignReleaseTickets(@CliOption(key = "", mandatory = true) TrainIteration iteration) {
return runAndReturn(executor, iteration, module -> getTrackerFor(module).assignReleaseTicketToMe(module),
Tickets.toTicketsCollector()).toString();
}
public String jiraStartProgress(@CliOption(key = "", mandatory = true) TrainIteration iteration) {
return runAndReturn(executor, iteration, module -> getTrackerFor(module).startReleaseTicketProgress(module),
Tickets.toTicketsCollector()).toString();
}
@CliCommand(value = "tracker create releaseversions")
public void jiraCreateReleaseVersions(@CliOption(key = "", mandatory = true) TrainIteration iteration) {
run(executor, iteration, module -> getTrackerFor(module).createReleaseVersion(module));
}
@CliCommand(value = "tracker create releasetickets")
public String createReleaseTickets(@CliOption(key = "", mandatory = true) TrainIteration iteration) {
run(executor, iteration, module -> getTrackerFor(module).createReleaseTicket(module));
evict();
return releaseTickets(iteration);
}
@CliCommand(value = "tracker create tickets")
public String createTickets(@CliOption(key = "iteration", mandatory = true) TrainIteration iteration,
@CliOption(key = "text", mandatory = true) String text) {
return iteration.stream().//
map(module -> getTrackerFor(module).createTicket(module, text, IssueTracker.TicketType.Task, false))
.collect(Tickets.toTicketsCollector())
.toString();
}
@CliCommand("tracker open-tickets")
public String openTickets(@CliOption(key = "", mandatory = true) TrainIteration iteration, //
@CliOption(key = "module") String moduleName,
@CliOption(key = "filter-release-tickets") Boolean filterReleaseTickets) {
Predicate<Ticket> notResolved = it -> !it.isResolved();
return getTickets(iteration, moduleName,
notResolved.and(getFilterPredicate(filterReleaseTickets == null || filterReleaseTickets)));
}
@CliCommand("tracker all-tickets")
public String allTickets(@CliOption(key = "", mandatory = true) TrainIteration iteration, //
@CliOption(key = "module") String moduleName,
@CliOption(key = "filter-release-tickets") Boolean filterReleaseTickets) {
return getTickets(iteration, moduleName, getFilterPredicate(filterReleaseTickets == null || filterReleaseTickets));
}
private static Predicate<Ticket> getFilterPredicate(boolean filterReleaseTickets) {
return it -> filterReleaseTickets == !it.isReleaseTicket();
}
private String getTickets(TrainIteration iteration, String moduleName, Predicate<Ticket> ticketPredicate) {
if (StringUtils.hasText(moduleName)) {
return getTicketsForProject(iteration, Projects.requiredByName(moduleName), ticketPredicate);
}
return ExecutionUtils.runAndReturn(executor, iteration,
moduleIteration -> {
return getTicketsForProject(iteration, moduleIteration.getModule().getProject(), ticketPredicate);
})
.stream() //
.filter(StringUtils::hasText) //
.collect(Collectors.joining("\n"));
}
private String getTicketsForProject(TrainIteration iteration, Project project, Predicate<Ticket> ticketPredicate) {
ModuleIteration module = iteration.getModule(project);
return getTrackerFor(module).getTicketsFor(module) //
.stream() //
.filter(ticketPredicate) //
.collect(Tickets.toTicketsCollector()) //
.toString(false);
}
@CliCommand("tracker close")
public void closeIteration(@CliOption(key = "", mandatory = true) TrainIteration iteration) {
run(executor, iteration, module -> getTrackerFor(module).closeIteration(module));
}
@CliCommand("tracker archive")
public void archiveIteration(@CliOption(key = "", mandatory = true) TrainIteration iteration) {
run(executor, iteration, module -> getTrackerFor(module).archiveReleaseVersion(module));
}
private IssueTracker getTrackerFor(ModuleIteration moduleIteration) {
return tracker.getRequiredPluginFor(moduleIteration.getProject(),
() -> String.format("No issue tracker found for module %s!", moduleIteration));
}
}

View File

@@ -1,172 +0,0 @@
/*
* Copyright 2014-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.issues;
import java.io.IOException;
import java.net.URI;
import java.util.List;
import org.apache.http.HttpHost;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.AuthCache;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.impl.auth.BasicScheme;
import org.apache.http.impl.client.BasicAuthCache;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.release.issues.github.GitHubProperties;
import org.springframework.data.release.model.Project;
import org.springframework.data.release.utils.HttpBasicCredentials;
import org.springframework.data.util.Lazy;
import org.springframework.http.HttpMethod;
import org.springframework.http.client.ClientHttpRequest;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.plugin.core.OrderAwarePluginRegistry;
import org.springframework.plugin.core.PluginRegistry;
import com.fasterxml.jackson.annotation.JsonCreator.Mode;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.introspect.AnnotatedMember;
import com.fasterxml.jackson.databind.introspect.AnnotatedMethod;
import com.fasterxml.jackson.databind.introspect.NopAnnotationIntrospector;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;
/**
* @author Oliver Gierke
* @author Mark Paluch
*/
@Configuration(proxyBeanMethods = false)
@EnableCaching(proxyTargetClass = true)
class IssueTrackerConfiguration {
@Bean
CacheManager cacheManager() {
return new ConcurrentMapCacheManager();
}
@Bean
ObjectMapper jacksonObjectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
mapper.setSerializationInclusion(Include.NON_NULL);
mapper.registerModule(new ParameterNamesModule(Mode.PROPERTIES));
mapper.registerModule(new SyntheticLambdaFactoryMethodIgnoringModule());
return mapper;
}
@Bean
HttpComponentsClientHttpRequestFactory clientHttpRequestFactory(GitHubProperties gitHubProperties) {
// Preemptive auth
CredentialsProvider credsProvider = new BasicCredentialsProvider();
AuthCache authCache = new BasicAuthCache();
addPreemptiveAuth(credsProvider, authCache, gitHubProperties.getApiUrl(), gitHubProperties.getHttpCredentials());
Lazy<CloseableHttpClient> lazy = Lazy
.of(() -> HttpClientBuilder.create().setDefaultCredentialsProvider(credsProvider).build());
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory() {
@Override
public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException {
setHttpClient(lazy.get());
return super.createRequest(uri, httpMethod);
}
};
factory.setHttpContextFactory((httpMethod, uri) -> {
HttpClientContext context = HttpClientContext.create();
context.setAuthCache(authCache);
return context;
});
return factory;
}
@Bean
@Qualifier("tracker")
RestTemplateBuilder restTemplate(ClientHttpRequestFactory clientHttpRequestFactory,
ObjectMapper jacksonObjectMapper) {
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
converter.setObjectMapper(jacksonObjectMapper);
return new RestTemplateBuilder().messageConverters(converter).requestFactory(() -> clientHttpRequestFactory);
}
@Bean
PluginRegistry<IssueTracker, Project> issueTrackers(List<? extends IssueTracker> plugins) {
return OrderAwarePluginRegistry.of(plugins);
}
private static void addPreemptiveAuth(CredentialsProvider credsProvider, AuthCache authCache, String requestUrl,
HttpBasicCredentials credentials) {
HttpHost jiraHost = HttpHost.create(requestUrl);
credsProvider.setCredentials(new AuthScope(jiraHost),
new UsernamePasswordCredentials(credentials.getUsername(), credentials.getPassword().toString()));
authCache.put(jiraHost, new BasicScheme());
}
/**
* @author Oliver Gierke
*/
static class SyntheticLambdaFactoryMethodIgnoringModule extends SimpleModule {
private static final long serialVersionUID = -3075786389813846512L;
@Override
public void setupModule(SetupContext context) {
context.insertAnnotationIntrospector(new NopAnnotationIntrospector() {
private static final long serialVersionUID = 479313244908256455L;
@Override
public boolean hasIgnoreMarker(AnnotatedMember m) {
if (!(m instanceof AnnotatedMethod)) {
return super.hasIgnoreMarker(m);
}
AnnotatedMethod method = (AnnotatedMethod) m;
return method.getName().startsWith("lambda$") ? true : super.hasIgnoreMarker(m);
}
});
}
}
}

View File

@@ -1,102 +0,0 @@
/*
* Copyright 2014-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.issues;
import lombok.Value;
import org.springframework.data.release.model.ModuleIteration;
import org.springframework.data.release.model.Tracker;
import org.springframework.data.release.model.TrainIteration;
import org.springframework.util.Assert;
/**
* Value object to represent a {@link Ticket}.
*
* @author Oliver Gierke
* @author Mark Paluch
*/
@Value
public class Ticket {
String id, summary;
String url;
String assignee;
TicketStatus ticketStatus;
public Ticket(String id, String summary, String url, String assignee, TicketStatus ticketStatus) {
this.id = id;
this.summary = summary;
this.url = url;
this.assignee = assignee;
this.ticketStatus = ticketStatus;
}
public Ticket(String id, String summary, TicketStatus ticketStatus) {
this.id = id;
this.summary = summary;
this.assignee = null;
this.url = null;
this.ticketStatus = ticketStatus;
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return String.format("%14s - %s (%s)", id, summary, url);
}
public boolean isResolved() {
return ticketStatus.isResolved();
}
/**
* Returns whether the current {@link Ticket} is the release ticket for the given {@link ModuleIteration}.
*
* @param module must not be {@literal null}.
* @return
*/
public boolean isReleaseTicketFor(ModuleIteration module) {
Assert.notNull(module, "Module must not be null!");
return summary.startsWith(Tracker.releaseTicketSummary(module));
}
/**
* Returns whether the current {@link Ticket} is the release ticket by checking the summary prefix.
*
* @return
*/
public boolean isReleaseTicket() {
return summary.startsWith(Tracker.RELEASE_PREFIX);
}
/**
* Returns whether the current {@link Ticket} is a release ticket for the given {@link TrainIteration}.
*
* @param train must not be {@literal null}.
* @return
*/
public boolean isReleaseTicketFor(TrainIteration train) {
return train.stream().anyMatch(this::isReleaseTicketFor);
}
public boolean isAssignedTo(String username) {
return username.equals(assignee);
}
}

View File

@@ -1,119 +0,0 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.issues;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import lombok.experimental.FieldDefaults;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import org.springframework.data.release.model.ModuleIteration;
import org.springframework.data.release.model.Project;
import org.springframework.data.release.utils.Logger;
import org.springframework.plugin.core.PluginRegistry;
import org.springframework.stereotype.Component;
/**
* @author Mark Paluch
*/
@Component
@RequiredArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
public class TicketOperations {
Logger logger;
PluginRegistry<IssueTracker, Project> tracker;
/**
* Create or look up ticket with a particular summary.
*
* @param module
* @param summary
* @return
*/
public Ticket getOrCreateTicketsWithSummary(ModuleIteration module, IssueTracker.TicketType ticketType,
String summary) {
return getOrCreateTicketsWithSummary(module, ticketType, Collections.singletonList(summary)).getTickets().get(0);
}
/**
* Create or look up tickets with a particular summary.
*
* @param module
* @param ticketType
* @param summaries
* @return
*/
public Tickets getOrCreateTicketsWithSummary(ModuleIteration module, IssueTracker.TicketType ticketType,
List<String> summaries) {
Project project = module.getProject();
IssueTracker tracker = this.tracker.getRequiredPluginFor(project);
Tickets tickets = tracker.getTicketsFor(module);
List<Ticket> results = new ArrayList<>();
for (String summary : summaries) {
Optional<Ticket> upgradeTicket = findBySummary(tickets, summary);
if (upgradeTicket.isPresent()) {
logger.log(project, "Found ticket %s", upgradeTicket.get());
upgradeTicket.ifPresent(it -> {
tracker.assignTicketToMe(project, it);
results.add(it);
});
} else {
logger.log(module, "Creating ticket for %s", summary);
Ticket ticket = tracker.createTicket(module, summary, ticketType, true);
results.add(ticket);
}
}
return new Tickets(results);
}
private Optional<Ticket> findBySummary(Tickets tickets, String summary) {
List<Ticket> result = tickets.filter(it -> it.getSummary().equals(summary)).toList();
if (result.size() > 1) {
throw new IllegalStateException("Multiple tickets found: " + result);
}
return Optional.ofNullable(result.isEmpty() ? null : result.get(0));
}
public void closeTicket(ModuleIteration module, Ticket ticket) {
closeTickets(module, new Tickets(Collections.singletonList(ticket)));
}
public void closeTickets(ModuleIteration module, Tickets tickets) {
IssueTracker tracker = this.tracker.getRequiredPluginFor(module.getProject());
for (Ticket ticket : tickets) {
tracker.closeTicket(module, ticket);
}
}
}

View File

@@ -1,58 +0,0 @@
/*
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.issues;
import lombok.Value;
/**
* @author Mark Paluch
*/
@Value
public class TicketReference implements Comparable<TicketReference> {
String id;
String message;
Style style;
public TicketReference(String id, String message, Style style) {
this.id = normalize(id);
this.message = message;
this.style = style;
}
private static String normalize(String id) {
if (id.toLowerCase().startsWith("gh-")) {
return "#" + id.substring(3);
}
return id.toUpperCase().replaceAll(" ", "");
}
@Override
public int compareTo(TicketReference o) {
if (id.startsWith("#") && o.id.startsWith("#")) {
return Integer.compare(Integer.parseInt(id.substring(1)), Integer.parseInt(o.id.substring(1)));
}
return id.compareToIgnoreCase(o.id);
}
public enum Style {
GitHub, Jira;
}
}

View File

@@ -1,37 +0,0 @@
/*
* Copyright 2016-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.issues;
/**
* @author Mark Paluch
*/
public interface TicketStatus {
/**
* Returns
*
* @return
*/
String getLabel();
/**
* Returns whether the ticket is marked as resolved.
*
* @return
*/
boolean isResolved();
}

View File

@@ -1,125 +0,0 @@
/*
* Copyright 2014-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.issues;
import lombok.RequiredArgsConstructor;
import lombok.Value;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collector;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.io.IOUtils;
import org.springframework.data.release.model.ModuleIteration;
import org.springframework.data.release.model.Tracker;
import org.springframework.data.release.model.TrainIteration;
import org.springframework.data.release.utils.ListWrapperCollector;
import org.springframework.data.util.Streamable;
/**
* Value object to represent a list of {@link Ticket}s.
*
* @author Oliver Gierke
* @author Mark Paluch
*/
@Value
@RequiredArgsConstructor
public class Tickets implements Streamable<Ticket> {
List<Ticket> tickets;
int overallTotal;
public Tickets(List<Ticket> tickets) {
this(Collections.unmodifiableList(tickets), tickets.size());
}
/*
* (non-Javadoc)
* @see java.lang.Iterable#iterator()
*/
@Override
public Iterator<Ticket> iterator() {
return tickets.iterator();
}
public boolean hasReleaseTicket(ModuleIteration moduleIteration) {
return findReleaseTicket(moduleIteration).isPresent();
}
public Ticket getReleaseTicket(ModuleIteration moduleIteration) {
return findReleaseTicket(moduleIteration).orElseThrow(
() -> new IllegalArgumentException(String.format("Did not find a release ticket for %s containing %s!",
moduleIteration, Tracker.releaseTicketSummary(moduleIteration))));
}
public Tickets getIssueTickets(ModuleIteration moduleIteration) {
return tickets.stream(). //
filter(ticket -> !ticket.isReleaseTicketFor(moduleIteration)).//
collect(toTicketsCollector());
}
public Tickets getReleaseTickets(TrainIteration iteration) {
return stream().//
filter(ticket -> ticket.isReleaseTicketFor(iteration)).//
distinct().//
collect(toTicketsCollector());
}
private Optional<Ticket> findReleaseTicket(ModuleIteration moduleIteration) {
return stream().//
filter(ticket -> ticket.isReleaseTicketFor(moduleIteration)).//
findFirst();
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return toString(true);
}
public String toString(boolean header) {
StringBuilder builder = new StringBuilder();
if (header) {
builder.append(String.format("Train only tickets: %s of %s", tickets.size(), overallTotal));
builder.append(IOUtils.LINE_SEPARATOR);
}
builder.append(tickets.stream().map(it -> "\t" + it).collect(Collectors.joining(IOUtils.LINE_SEPARATOR)));
return builder.toString();
}
/**
* Returns a new collector to toTicketsCollector {@link Ticket} as {@link Tickets} using the {@link Stream} API.
*
* @return
*/
public static Collector<? super Ticket, ?, Tickets> toTicketsCollector() {
return ListWrapperCollector.collectInto(Tickets::new);
}
}

View File

@@ -1,144 +0,0 @@
/*
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.issues.github;
import lombok.Getter;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.springframework.stereotype.Component;
/**
* Generates a changelog markdown file which includes bug fixes, enhancements and contributors for a given milestone.
*
* @author Madhura Bhave
* @author Phillip Webb
*/
@Component
public class ChangelogGenerator {
private static final Pattern ghUserMentionPattern = Pattern.compile("(^|[^\\w`])(@[\\w-]+)");
@Getter
private final Set<String> excludeLabels;
@Getter
private final Set<String> excludeContributors;
@Getter
private final String contributorsTitle;
private final ChangelogSections sections;
public ChangelogGenerator() {
this.excludeLabels = new HashSet<>(Arrays.asList("type: task"));
this.excludeContributors = new LinkedHashSet<>();
this.contributorsTitle = null;
this.sections = new ChangelogSections();
}
/**
* Generates a file at the given path which includes bug fixes, enhancements and contributors for the given milestone.
*
* @param issues the issues to generate the changelog for
* @param sectionContentPostProcessor the postprocessor for a changelog section
*/
public String generate(List<GitHubReadIssue> issues,
BiFunction<ChangelogSection, String, String> sectionContentPostProcessor) {
return generateContent(issues, sectionContentPostProcessor);
}
private boolean isExcluded(GitHubReadIssue issue) {
return issue.getLabels().stream().anyMatch(this::isExcluded);
}
private boolean isExcluded(Label label) {
return this.excludeLabels.contains(label.getName());
}
private String generateContent(List<GitHubReadIssue> issues,
BiFunction<ChangelogSection, String, String> sectionContentPostProcessor) {
StringBuilder content = new StringBuilder();
addSectionContent(content, this.sections.collate(issues), sectionContentPostProcessor);
Set<GitHubUser> contributors = getContributors(issues);
if (!contributors.isEmpty()) {
addContributorsContent(content, contributors);
}
return content.toString();
}
private void addSectionContent(StringBuilder result, Map<ChangelogSection, List<GitHubReadIssue>> sectionIssues,
BiFunction<ChangelogSection, String, String> sectionContentPostProcessor) {
sectionIssues.forEach((section, issues) -> {
issues.sort(Comparator.reverseOrder());
StringBuilder content = new StringBuilder();
content.append((content.length() != 0) ? String.format("%n") : "");
content.append("## ").append(section).append(String.format("%n%n"));
issues.stream().map(this::getFormattedIssue).forEach(content::append);
result.append(sectionContentPostProcessor.apply(section, content.toString()));
});
}
private String getFormattedIssue(GitHubReadIssue issue) {
String title = issue.getTitle();
title = ghUserMentionPattern.matcher(title).replaceAll("$1`$2`");
return String.format("- %s %s%n", title, getLinkToIssue(issue));
}
private String getLinkToIssue(GitHubIssue issue) {
return "[" + issue.getId() + "]" + "(" + issue.getUrl() + ")";
}
private Set<GitHubUser> getContributors(List<GitHubReadIssue> issues) {
if (this.excludeContributors.contains("*")) {
return Collections.emptySet();
}
return issues.stream().filter((issue) -> issue.getPullRequest() != null).map(GitHubReadIssue::getUser)
.filter(this::isIncludedContributor).collect(Collectors.toSet());
}
private boolean isIncludedContributor(GitHubUser user) {
return !this.excludeContributors.contains(user.getName());
}
private void addContributorsContent(StringBuilder content, Set<GitHubUser> contributors) {
content.append(String.format("%n## "));
content.append((this.contributorsTitle != null) ? this.contributorsTitle : ":heart: Contributors");
content.append(String.format("%n%nWe'd like to thank all the contributors who worked on this release!%n%n"));
contributors.stream().map(this::formatContributors).forEach(content::append);
}
private String formatContributors(GitHubUser c) {
return String.format("- [@%s](%s)%n", c.getName(), c.getUrl());
}
}

View File

@@ -1,75 +0,0 @@
/*
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.issues.github;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.Set;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
/**
* A single section of a changelog report.
*
* @author Phillip Webb
*/
class ChangelogSection {
private final String title;
private final String group;
private final Set<String> labels;
ChangelogSection(String title, String group, String... labels) {
this(title, group, new LinkedHashSet<>(Arrays.asList(labels)));
}
ChangelogSection(String title, String group, Set<String> labels) {
Assert.hasText(title, "Title must not be empty");
Assert.isTrue(!CollectionUtils.isEmpty(labels), "Labels must not be empty");
this.title = title;
this.group = group;
this.labels = labels;
}
String getGroup() {
return this.group;
}
boolean isMatchFor(GitHubReadIssue issue) {
for (String candidate : this.labels) {
for (Label label : issue.getLabels()) {
if (label.getName().contains(candidate)) {
return true;
}
}
}
return false;
}
public boolean hasLabel(String label) {
return this.labels.contains(label);
}
@Override
public String toString() {
return this.title;
}
}

View File

@@ -1,81 +0,0 @@
/*
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.issues.github;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
/**
* Manages sections of the changelog report.
*
* @author Phillip Webb
*/
class ChangelogSections {
private static final List<ChangelogSection> DEFAULT_SECTIONS;
static {
List<ChangelogSection> sections = new ArrayList<>();
add(sections, ":star: New Features", "enhancement");
add(sections, ":lady_beetle: Bug Fixes", "bug", "regression");
add(sections, ":notebook_with_decorative_cover: Documentation", "documentation");
add(sections, ":hammer: Dependency Upgrades", "dependency-upgrade");
DEFAULT_SECTIONS = Collections.unmodifiableList(sections);
}
private static void add(List<ChangelogSection> sections, String title, String... labels) {
sections.add(new ChangelogSection(title, null, labels));
}
private final List<ChangelogSection> sections;
ChangelogSections() {
this.sections = DEFAULT_SECTIONS;
}
Map<ChangelogSection, List<GitHubReadIssue>> collate(List<GitHubReadIssue> issues) {
SortedMap<ChangelogSection, List<GitHubReadIssue>> collated = new TreeMap<>(
Comparator.comparing(this.sections::indexOf));
for (GitHubReadIssue issue : issues) {
List<ChangelogSection> sections = getSections(issue);
for (ChangelogSection section : sections) {
collated.computeIfAbsent(section, (key) -> new ArrayList<>());
collated.get(section).add(issue);
}
}
return collated;
}
private List<ChangelogSection> getSections(GitHubReadIssue issue) {
List<ChangelogSection> result = new ArrayList<>();
Set<String> groupClaimes = new HashSet<>();
for (ChangelogSection section : this.sections) {
if (section.isMatchFor(issue) && groupClaimes.add(section.getGroup())) {
result.add(section);
}
}
return result;
}
}

View File

@@ -1,733 +0,0 @@
/*
* Copyright 2014-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.issues.github;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.data.release.git.GitProject;
import org.springframework.data.release.git.Tag;
import org.springframework.data.release.git.VersionTags;
import org.springframework.data.release.issues.Changelog;
import org.springframework.data.release.issues.IssueTracker;
import org.springframework.data.release.issues.Ticket;
import org.springframework.data.release.issues.Tickets;
import org.springframework.data.release.model.ArtifactVersion;
import org.springframework.data.release.model.DocumentationMetadata;
import org.springframework.data.release.model.Iteration;
import org.springframework.data.release.model.ModuleIteration;
import org.springframework.data.release.model.Project;
import org.springframework.data.release.model.Projects;
import org.springframework.data.release.model.Tracker;
import org.springframework.data.release.model.TrainIteration;
import org.springframework.data.release.utils.Logger;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.web.client.HttpStatusCodeException;
/**
* @author Oliver Gierke
* @author Mark Paluch
*/
@Component
public class GitHub extends GitHubSupport implements IssueTracker {
private static final String MILESTONE_URI = "/repos/spring-projects/{repoName}/milestones?state={state}";
private static final String ISSUES_BY_MILESTONE_AND_ASSIGNEE_URI_TEMPLATE = "/repos/spring-projects/{repoName}/issues?milestone={id}&state=all&assignee={assignee}";
private static final String ISSUES_BY_MILESTONE_URI_TEMPLATE = "/repos/spring-projects/{repoName}/issues?milestone={id}&state=all";
private static final String MILESTONES_URI_TEMPLATE = "/repos/spring-projects/{repoName}/milestones";
private static final String MILESTONE_BY_ID_URI_TEMPLATE = "/repos/spring-projects/{repoName}/milestones/{id}";
private static final String ISSUE_BY_ID_URI_TEMPLATE = "/repos/spring-projects/{repoName}/issues/{id}";
private static final String ISSUES_URI_TEMPLATE = "/repos/spring-projects/{repoName}/issues";
private static final String RELEASE_BY_TAG_URI_TEMPLATE = "/repos/spring-projects/{repoName}/releases/tags/{tag}";
private static final String RELEASE_URI_TEMPLATE = "/repos/spring-projects/{repoName}/releases";
private static final String RELEASE_BY_ID_URI_TEMPLATE = "/repos/spring-projects/{repoName}/releases/{id}";
private static final ParameterizedTypeReference<List<Milestone>> MILESTONES_TYPE = new ParameterizedTypeReference<List<Milestone>>() {};
private static final ParameterizedTypeReference<List<GitHubReadIssue>> ISSUES_TYPE = new ParameterizedTypeReference<List<GitHubReadIssue>>() {};
private static final ParameterizedTypeReference<GitHubReadIssue> ISSUE_TYPE = new ParameterizedTypeReference<GitHubReadIssue>() {};
private static final Map<TicketType, Label> TICKET_LABELS = new HashMap<>();
static {
TICKET_LABELS.put(TicketType.Task, LabelConfiguration.TYPE_TASK);
TICKET_LABELS.put(TicketType.DependencyUpgrade, LabelConfiguration.TYPE_DEPENDENCY_UPGRADE);
}
private final Logger logger;
private final GitHubProperties properties;
public GitHub(@Qualifier("tracker") RestTemplateBuilder templateBuilder, Logger logger, GitHubProperties properties) {
super(createOperations(templateBuilder, properties));
this.logger = logger;
this.properties = properties;
}
/*
* (non-Javadoc)
* @see org.springframework.data.release.jira.JiraConnector#flushTickets()
*/
@Override
@CacheEvict(value = { "tickets", "release-tickets", "milestone" }, allEntries = true)
public void reset() {
}
/*
* (non-Javadoc)
* @see org.springframework.data.release.jira.IssueTracker#getReleaseTicketFor(org.springframework.data.release.model.ModuleIteration)
*/
@Override
@Cacheable("release-tickets")
public Ticket getReleaseTicketFor(ModuleIteration module) {
return getTicketsFor(module).getReleaseTicket(module);
}
/*
* (non-Javadoc)
*
* @see IssueTracker#findTickets(Project, Collection)
*/
@Override
@Cacheable("tickets")
public Collection<Ticket> findTickets(Project project, Collection<String> ticketIds) {
String repositoryName = GitProject.of(project).getRepositoryName();
List<Ticket> tickets = new ArrayList<>();
ticketIds.forEach(ticketId -> {
GitHubReadIssue ticket = findTicket(repositoryName, ticketId);
if (ticket != null) {
tickets.add(toTicket(ticket));
}
});
return tickets;
}
@Override
public Tickets findTickets(ModuleIteration moduleIteration, Collection<String> ticketIds) {
return findGitHubIssues(moduleIteration, ticketIds).stream().map(GitHub::toTicket)
.collect(Tickets.toTicketsCollector());
}
/*
* (non-Javadoc)
* @see org.springframework.data.release.jira.IssueTracker#getChangelogFor(org.springframework.data.release.model.ModuleIteration)
*/
@Override
@Cacheable("changelogs")
public Changelog getChangelogFor(ModuleIteration moduleIteration) {
Tickets tickets = getIssuesFor(moduleIteration, false, false).//
map(issue -> toTicket(issue)).//
collect(Tickets.toTicketsCollector());
logger.log(moduleIteration, "Created changelog with %s entries.", tickets.getOverallTotal());
return Changelog.of(moduleIteration, tickets);
}
/*
* (non-Javadoc)
* @see org.springframework.plugin.core.Plugin#supports(java.lang.Object)
*/
@Override
public boolean supports(Project project) {
return project.uses(Tracker.GITHUB);
}
@Override
public Tickets getTicketsFor(TrainIteration iteration) {
return getTicketsFor(iteration, false);
}
/*
* (non-Javadoc)
* @see org.springframework.data.release.jira.GitHubIssueConnector#getTicketsFor(org.springframework.data.release.model.TrainIteration, boolean)
*/
@Override
public Tickets getTicketsFor(TrainIteration trainIteration, boolean forCurrentUser) {
if (forCurrentUser) {
logger.log(trainIteration, "Retrieving tickets (for user %s)…", properties.getUsername());
} else {
logger.log(trainIteration, "Retrieving tickets…");
}
Tickets tickets = trainIteration.stream(). //
filter(moduleIteration -> supports(moduleIteration.getProject())). //
flatMap(moduleIteration -> getTicketsFor(moduleIteration, forCurrentUser).stream()). //
collect(Tickets.toTicketsCollector());
return tickets;
}
/*
* (non-Javadoc)
* @see org.springframework.data.release.tracker.IssueTracker#createReleaseVersion(org.springframework.data.release.model.ModuleIteration)
*/
@Override
public void createReleaseVersion(ModuleIteration moduleIteration) {
Assert.notNull(moduleIteration, "ModuleIteration must not be null.");
String repositoryName = GitProject.of(moduleIteration.getProject()).getRepositoryName();
Optional<Milestone> milestone = findMilestone(moduleIteration, repositoryName);
if (milestone.isPresent()) {
return;
}
GithubMilestone githubMilestone = new GithubMilestone(moduleIteration);
logger.log(moduleIteration, "Creating GitHub milestone %s", githubMilestone);
HttpHeaders httpHeaders = new HttpHeaders();
Map<String, Object> parameters = newUrlTemplateVariables();
parameters.put("repoName", repositoryName);
operations.exchange(MILESTONES_URI_TEMPLATE, HttpMethod.POST,
new HttpEntity<Object>(githubMilestone.toMilestone(), httpHeaders), Milestone.class, parameters);
}
/*
* (non-Javadoc)
* @see org.springframework.data.release.tracker.IssueTracker#retireReleaseVersion(org.springframework.data.release.model.ModuleIteration)
*/
@Override
public void archiveReleaseVersion(ModuleIteration module) {
logger.log(module, "Skipping milestone archival");
}
/*
*
* (non-Javadoc)
* @see org.springframework.data.release.tracker.IssueTracker#createReleaseTicket(org.springframework.data.release.model.ModuleIteration)
*/
@Override
public void createReleaseTicket(ModuleIteration moduleIteration) {
Assert.notNull(moduleIteration, "ModuleIteration must not be null.");
Tickets tickets = getTicketsFor(moduleIteration);
if (tickets.hasReleaseTicket(moduleIteration)) {
return;
}
logger.log(moduleIteration, "Creating release ticket…");
doCreateTicket(moduleIteration, Tracker.releaseTicketSummary(moduleIteration), TicketType.Task, false);
}
@Override
public Ticket createTicket(ModuleIteration moduleIteration, String text, TicketType ticketType,
boolean assignToCurrentUser) {
logger.log(moduleIteration, "Creating ticket…");
return doCreateTicket(moduleIteration, text, ticketType, assignToCurrentUser);
}
private Ticket doCreateTicket(ModuleIteration moduleIteration, String text, TicketType ticketType,
boolean assignToCurrentUser) {
String repositoryName = GitProject.of(moduleIteration.getProject()).getRepositoryName();
Milestone milestone = getMilestone(moduleIteration, repositoryName);
Label label = TICKET_LABELS.get(ticketType);
GitHubWriteIssue gitHubIssue = GitHubWriteIssue.of(text, milestone).withLabel(label.getName());
if (assignToCurrentUser) {
gitHubIssue = gitHubIssue.withAssignees(Collections.singletonList(properties.getUsername()));
}
Map<String, Object> parameters = newUrlTemplateVariables();
parameters.put("repoName", repositoryName);
GitHubReadIssue body = operations.exchange(ISSUES_URI_TEMPLATE, HttpMethod.POST,
new HttpEntity<Object>(gitHubIssue), GitHubReadIssue.class, parameters).getBody();
return toTicket(body);
}
@Cacheable("tickets")
public Tickets getTicketsFor(ModuleIteration iteration) {
return getTicketsFor(iteration, false);
}
/*
* (non-Javadoc)
* @see org.springframework.data.release.jira.IssueTracker#assignTicketToMe(org.springframework.data.release.jira.Ticket)
*/
@Override
public Ticket assignTicketToMe(Project project, Ticket ticket) {
Assert.notNull(ticket, "Ticket must not be null.");
if (ticket.isAssignedTo(properties.getUsername())) {
logger.log("Ticket", "Skipping self-assignment of %s", ticket);
return ticket;
}
String repositoryName = GitProject.of(project).getRepositoryName();
Map<String, Object> parameters = newUrlTemplateVariables();
parameters.put("repoName", repositoryName);
parameters.put("id", stripHash(ticket));
GitHubWriteIssue edit = GitHubWriteIssue.assignedTo(properties.getUsername());
GitHubReadIssue response = operations.exchange(ISSUE_BY_ID_URI_TEMPLATE, HttpMethod.PATCH,
new HttpEntity<>(edit, new HttpHeaders()), ISSUE_TYPE, parameters).getBody();
return toTicket(response);
}
/*
* (non-Javadoc)
* @see org.springframework.data.release.jira.IssueTracker#assignReleaseTicketToMe(org.springframework.data.release.model.ModuleIteration)
*/
@Override
public Ticket assignReleaseTicketToMe(ModuleIteration module) {
Assert.notNull(module, "ModuleIteration must not be null.");
return assignTicketToMe(module.getProject(), getReleaseTicketFor(module));
}
/*
* (non-Javadoc)
* @see org.springframework.data.release.jira.IssueTracker#startReleaseTicketProgress(org.springframework.data.release.model.ModuleIteration)
*/
@Override
public Ticket startReleaseTicketProgress(ModuleIteration module) {
return getReleaseTicketFor(module);
}
/**
* Close the release ticket.
*
* @param module
* @return
*/
public Ticket closeReleaseTicket(ModuleIteration module) {
Assert.notNull(module, "ModuleIteration must not be null.");
Ticket releaseTicketFor = getReleaseTicketFor(module);
GitHubReadIssue response = close(module, releaseTicketFor);
return toTicket(response);
}
private GitHubReadIssue close(ModuleIteration module, Ticket ticket) {
String repositoryName = GitProject.of(module.getProject()).getRepositoryName();
Map<String, Object> parameters = newUrlTemplateVariables();
parameters.put("repoName", repositoryName);
parameters.put("id", stripHash(ticket));
GitHubWriteIssue edit = GitHubWriteIssue.assignedTo(properties.getUsername()).close();
return operations.exchange(ISSUE_BY_ID_URI_TEMPLATE, HttpMethod.PATCH,
new HttpEntity<>(edit, new HttpHeaders()), ISSUE_TYPE, parameters).getBody();
}
private String stripHash(Ticket ticket) {
return ticket.getId().startsWith("#") ? ticket.getId().substring(1) : ticket.getId();
}
private Map<String, Object> newUrlTemplateVariables() {
Map<String, Object> parameters = new HashMap<>();
return parameters;
}
private Optional<Milestone> findMilestone(ModuleIteration moduleIteration, String repositoryName) {
return doFindMilestone(moduleIteration, repositoryName, m -> m.matches(moduleIteration));
}
private Optional<Milestone> doFindMilestone(ModuleIteration moduleIteration, String repositoryName,
Predicate<Milestone> milestonePredicate) {
AtomicReference<Milestone> milestoneRef = new AtomicReference<>();
for (String state : Arrays.asList("open", "closed")) {
Map<String, Object> parameters = newUrlTemplateVariables();
parameters.put("repoName", repositoryName);
parameters.put("state", state);
logger.log(moduleIteration, "Looking up milestone…");
doWithPaging(MILESTONE_URI, HttpMethod.GET, parameters, new HttpEntity<>(new HttpHeaders()),
MILESTONES_TYPE, milestones -> {
Optional<Milestone> milestone = milestones.stream(). //
filter(milestonePredicate). //
findFirst(). //
map(m -> {
logger.log(moduleIteration, "Found milestone %s.", m);
return m;
});
if (milestone.isPresent()) {
milestoneRef.set(milestone.get());
return false;
}
return true;
});
if (milestoneRef.get() != null) {
break;
}
}
return Optional.ofNullable(milestoneRef.get());
}
/*
* (non-Javadoc)
* @see org.springframework.data.release.issues.IssueTracker#closeIteration(org.springframework.data.release.model.ModuleIteration)
*/
@Override
public void closeIteration(ModuleIteration module) {
// for each module
// - close all tickets
// -- make sure only one ticket is open
// -- resolve open ticket
// -- close tickets
// - mark version as released
HttpHeaders httpHeaders = new HttpHeaders();
GitProject project = GitProject.of(module.getProject());
findMilestone(module, project.getRepositoryName()) //
.filter(Milestone::isOpen) //
.map(Milestone::markReleased) //
.ifPresent(milestone -> {
logger.log(module, "Marking milestone %s as released.", milestone);
Map<String, Object> parameters = newUrlTemplateVariables();
parameters.put("repoName", project.getRepositoryName());
parameters.put("id", milestone.getNumber());
operations.exchange(MILESTONE_BY_ID_URI_TEMPLATE, HttpMethod.PATCH,
new HttpEntity<Object>(milestone, httpHeaders), Map.class, parameters);
});
// - if no next version exists, create
closeReleaseTicket(module);
}
@Override
public void closeTicket(ModuleIteration module, Ticket ticket) {
close(module, ticket);
}
List<GitHubReadIssue> findGitHubIssues(ModuleIteration moduleIteration, Collection<String> ticketIds) {
logger.log(moduleIteration, "Looking up GitHub issues from milestone …");
Map<String, GitHubReadIssue> issues = getIssuesFor(moduleIteration, false, true)
.collect(Collectors.toMap(GitHubIssue::getId, Function.identity()));
String repositoryName = GitProject.of(moduleIteration.getProject()).getRepositoryName();
logger.log(moduleIteration, "Looking up GitHub issues …");
Collection<GitHubReadIssue> foundIssues = ticketIds.stream().filter(it -> it.startsWith("#")).flatMap(it -> {
GitHubReadIssue ticket = getTicket(issues, repositoryName, it);
if (ticket != null) {
return Stream.of(ticket);
}
return Stream.empty();
}).collect(Collectors.toList());
List<GitHubReadIssue> gitHubIssues = foundIssues.stream().filter(it -> {
Ticket ticket = toTicket(it);
return !ticket.isReleaseTicketFor(moduleIteration) && !ticket.isReleaseTicket();
}).collect(Collectors.toList());
logger.log(moduleIteration, "Found %s tickets.", gitHubIssues.size());
return gitHubIssues;
}
private GitHubReadIssue getTicket(Map<String, GitHubReadIssue> cache, String repositoryName,
String ticketId) {
if (cache.containsKey(ticketId)) {
return cache.get(ticketId);
}
return findTicket(repositoryName, ticketId);
}
private Tickets getTicketsFor(ModuleIteration moduleIteration, boolean forCurrentUser) {
return getIssuesFor(moduleIteration, forCurrentUser, false).//
map(GitHub::toTicket).//
collect(Tickets.toTicketsCollector());
}
/**
* @param repositoryName
* @param ticketId
* @return
*/
private GitHubReadIssue findTicket(String repositoryName, String ticketId) {
Map<String, Object> parameters = newUrlTemplateVariables();
parameters.put("repoName", repositoryName);
parameters.put("id", ticketId.startsWith("#") ? ticketId.substring(1) : ticketId);
try {
return operations.exchange(ISSUE_BY_ID_URI_TEMPLATE, HttpMethod.GET,
new HttpEntity<>(new HttpHeaders()), ISSUE_TYPE, parameters).getBody();
} catch (HttpStatusCodeException e) {
if (e.getStatusCode() == HttpStatus.NOT_FOUND) {
return null;
}
throw e;
}
}
/**
* @param module
* @param ticketReferences
*/
public void createOrUpdateRelease(ModuleIteration module, List<String> ticketIds) {
logger.log(module, "Preparing GitHub Release …");
List<GitHubReadIssue> gitHubIssues = findGitHubIssues(module, ticketIds);
ArtifactVersion version = ArtifactVersion.of(module);
DocumentationMetadata documentation = DocumentationMetadata.of(module.getProject(), version, false);
ChangelogGenerator generator = new ChangelogGenerator();
generator.getExcludeContributors().addAll(properties.getTeam());
String releaseBody = generator.generate(gitHubIssues, (changelogSection, s) -> s);
String documentationLinks = getDocumentationLinks(module, documentation);
if (module.getProject() == Projects.BOM || module.getProject() == Projects.BUILD) {
// We don't ship Javadoc/reference doc for build and BOM
createOrUpdateRelease(module, String.format("%s%n", documentationLinks, releaseBody));
} else {
createOrUpdateRelease(module, String.format("## :green_book: Links%n%s%n%s%n", documentationLinks, releaseBody));
}
logger.log(module, "GitHub Release up to date");
}
/**
* Verify GitHub authentication.
*/
public void verifyAuthentication() {
logger.log("GitHub", "Verifying GitHub Authentication…");
String repositoryName = GitProject.of(Projects.BUILD).getRepositoryName();
Map<String, Object> parameters = newUrlTemplateVariables();
parameters.put("repoName", repositoryName);
// /user requires authentication
ResponseEntity<Object> entity = operations.getForEntity("/user", Object.class);
if (!entity.getStatusCode().is2xxSuccessful()) {
throw new IllegalStateException(String.format("Cannot obtain /user. Status: %s", entity.getStatusCode()));
}
logger.log("GitHub", "Authentication verified!");
}
private String getDocumentationLinks(ModuleIteration module, DocumentationMetadata documentation) {
if (module.getProject() == Projects.BUILD || module.getProject() == Projects.BOM) {
return "";
}
String referenceDocUrl = documentation.getReferenceDocUrl();
String apiDocUrl = documentation.getApiDocUrl();
String reference = String.format("* [%s %s Reference documentation](%s)", module.getProject().getFullName(),
module.getVersion().toString(), referenceDocUrl);
String apidoc = String.format("* [%s %s Javadoc](%s)", module.getProject().getFullName(),
module.getVersion().toString(), apiDocUrl);
return String.format("%s%n%s%n", reference, apidoc);
}
private void createOrUpdateRelease(ModuleIteration module, String body) {
String repositoryName = GitProject.of(module.getProject()).getRepositoryName();
Tag tag = VersionTags.empty(module.getProject()).createTag(module);
logger.log(module, "Looking up GitHub Release …");
Iteration iteration = module.getTrainIteration().getIteration();
boolean prerelase = iteration.isPreview();
GitHubRelease release = findRelease(repositoryName, tag.getName());
if (release == null) {
release = new GitHubRelease(null, tag.getName(), tag.getName(), body, false, prerelase);
logger.log(module, "Creating new Release …");
createRelease(repositoryName, release);
} else {
release = release.withPrerelease(prerelase).withBody(body);
logger.log(module, "Updating new Release …");
updateRelease(repositoryName, release);
}
}
private GitHubRelease findRelease(String repositoryName, String tagName) {
Map<String, Object> parameters = newUrlTemplateVariables();
parameters.put("repoName", repositoryName);
parameters.put("tag", tagName);
try {
return operations.exchange(RELEASE_BY_TAG_URI_TEMPLATE, HttpMethod.GET, new HttpEntity<>(new HttpHeaders()),
GitHubRelease.class, parameters).getBody();
} catch (HttpStatusCodeException e) {
if (e.getStatusCode() == HttpStatus.NOT_FOUND) {
return null;
}
throw e;
}
}
private void createRelease(String repositoryName, GitHubRelease release) {
Map<String, Object> parameters = newUrlTemplateVariables();
parameters.put("repoName", repositoryName);
operations.exchange(RELEASE_URI_TEMPLATE, HttpMethod.POST, new HttpEntity<>(release), GitHubRelease.class,
parameters);
}
private void updateRelease(String repositoryName, GitHubRelease release) {
Map<String, Object> parameters = newUrlTemplateVariables();
parameters.put("repoName", repositoryName);
parameters.put("id", release.getId());
operations.exchange(RELEASE_BY_ID_URI_TEMPLATE, HttpMethod.PATCH, new HttpEntity<>(release), GitHubRelease.class,
parameters);
}
private Stream<GitHubReadIssue> getIssuesFor(ModuleIteration moduleIteration, boolean forCurrentUser,
boolean ignoreMissingMilestone) {
String repositoryName = GitProject.of(moduleIteration.getProject()).getRepositoryName();
Optional<Milestone> optionalMilestone = findMilestone(moduleIteration, repositoryName);
if (ignoreMissingMilestone && !optionalMilestone.isPresent()) {
return Stream.empty();
}
Milestone milestone = optionalMilestone.orElseThrow(() -> noSuchMilestone(moduleIteration));
Map<String, Object> parameters = newUrlTemplateVariables();
parameters.put("repoName", repositoryName);
parameters.put("id", milestone.getNumber());
if (forCurrentUser) {
parameters.put("assignee", properties.getUsername());
return getForIssues(ISSUES_BY_MILESTONE_AND_ASSIGNEE_URI_TEMPLATE, parameters);
}
return getForIssues(ISSUES_BY_MILESTONE_URI_TEMPLATE, parameters);
}
private Stream<GitHubReadIssue> getForIssues(String template, Map<String, Object> parameters) {
List<GitHubReadIssue> issues = new ArrayList<>();
doWithPaging(template, HttpMethod.GET, parameters, new HttpEntity<>(new HttpHeaders()), ISSUES_TYPE,
tickets -> {
issues.addAll(tickets);
return true;
});
return issues.stream();
}
private Milestone getMilestone(ModuleIteration moduleIteration, String repositoryName) {
Optional<Milestone> milestone = findMilestone(moduleIteration, repositoryName);
return milestone
.orElseThrow(() -> noSuchMilestone(moduleIteration));
}
private IllegalStateException noSuchMilestone(ModuleIteration moduleIteration) {
return new IllegalStateException(String.format("No milestone for %s found containing %s!", //
moduleIteration.getProject().getFullName(), //
new GithubMilestone(moduleIteration)));
}
private static Ticket toTicket(GitHubIssue issue) {
return new Ticket(issue.getId(), issue.getTitle(), issue.getUrl(),
issue.getAssignees().isEmpty() ? null : issue.getAssignees().get(0), new GithubTicketStatus(issue.getState()));
}
}

View File

@@ -1,86 +0,0 @@
/*
* Copyright 2014-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.issues.github;
import lombok.AccessLevel;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.experimental.FieldDefaults;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.stream.Collectors;
import org.springframework.data.release.CliComponent;
import org.springframework.data.release.TimedCommand;
import org.springframework.data.release.git.GitOperations;
import org.springframework.data.release.issues.IssueTracker;
import org.springframework.data.release.issues.TicketReference;
import org.springframework.data.release.model.Project;
import org.springframework.data.release.model.Tracker;
import org.springframework.data.release.model.TrainIteration;
import org.springframework.data.release.utils.ExecutionUtils;
import org.springframework.plugin.core.PluginRegistry;
import org.springframework.shell.core.annotation.CliCommand;
import org.springframework.shell.core.annotation.CliOption;
/**
* Component to execute GitHub related operations.
*
* @author Mark Paluch
*/
@CliComponent
@RequiredArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
public class GitHubCommands extends TimedCommand {
@NonNull PluginRegistry<IssueTracker, Project> tracker;
@NonNull GitHub gitHub;
@NonNull GitOperations git;
@NonNull GitHubLabels gitHubLabels;
@NonNull Executor executor;
@CliCommand(value = "github update labels")
public void createOrUpdateLabels(@CliOption(key = "", mandatory = true) Project project) {
gitHubLabels.createOrUpdateLabels(project);
}
@CliCommand(value = "github push")
public void push(@CliOption(key = "", mandatory = true) TrainIteration iteration) {
git.push(iteration);
git.pushTags(iteration.getTrain());
createOrUpdateRelease(iteration);
}
@CliCommand(value = "github create release")
public void createOrUpdateRelease(@CliOption(key = "", mandatory = true) TrainIteration iteration) {
TrainIteration previousIteration = git.getPreviousIteration(iteration);
ExecutionUtils.run(executor, iteration, it -> {
if (it.getProject().getTracker() == Tracker.GITHUB) {
List<String> ticketReferences = git.getTicketReferencesBetween(it.getProject(), previousIteration, iteration)
.stream().map(TicketReference::getId).collect(Collectors.toList());
gitHub.createOrUpdateRelease(it, ticketReferences);
}
});
}
}

View File

@@ -1,42 +0,0 @@
/*
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.issues.github;
import java.util.List;
/**
* @author Mark Paluch
*/
public interface GitHubIssue {
String getNumber();
default String getId() {
return getNumber() == null ? null : "#".concat(getNumber());
}
String getTitle();
String getState();
List<String> getAssignees();
/**
* @return HTML URL.
*/
String getUrl();
}

View File

@@ -1,121 +0,0 @@
/*
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.issues.github;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.data.release.git.GitProject;
import org.springframework.data.release.model.Project;
import org.springframework.data.release.utils.Logger;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.databind.JsonNode;
/**
* Methods to interact with GitHub labels.
*
* @author Mark Paluch
*/
@Component
public class GitHubLabels extends GitHubSupport {
private static final String LABELS_URI = "/repos/spring-projects/{repoName}/labels";
private static final String LABEL_URI = "/repos/spring-projects/{repoName}/labels/{label}";
private static final ParameterizedTypeReference<List<Label>> LABELS_TYPE = new ParameterizedTypeReference<List<Label>>() {};
private final Logger logger;
public GitHubLabels(@Qualifier("tracker") RestTemplateBuilder templateBuilder, Logger logger,
GitHubProperties properties) {
super(createOperations(templateBuilder, properties));
this.logger = logger;
}
/**
* Creates or updates GitHub labels for a {@link Project}. The actual {@link LabelConfiguration} is obtained from
* {@link ProjectLabelConfiguration}.
*
* @param project the project to process.
*/
public void createOrUpdateLabels(Project project) {
logger.log(project, "Obtaining labels…");
Map<String, Object> parameters = Collections.singletonMap("repoName", GitProject.of(project).getRepositoryName());
List<Label> existsOnGitHub = getLabelsFromGitHub(parameters);
LabelConfiguration configuration = ProjectLabelConfiguration.forProject(project);
List<Label> newLabels = configuration.getNewLabels(existsOnGitHub);
List<Label> existingLabels = configuration.getExistingLabels(existsOnGitHub);
List<Label> additionalLabels = configuration.getAdditionalLabels(existsOnGitHub);
if (!newLabels.isEmpty()) {
logger.log(project, "Creating new labels (%d)…", newLabels.size());
newLabels.forEach(it -> {
operations.postForObject(LABELS_URI, it, JsonNode.class, parameters);
});
}
newLabels.forEach(it -> {
operations.postForObject(LABELS_URI, it, String.class, parameters);
});
existingLabels.stream().filter(it -> {
Label existing = existsOnGitHub.get(existsOnGitHub.indexOf(it));
return existing.requiresUpdate(it);
}).forEach(it -> {
logger.log(project, "Updating label %s…", it.getName());
Map<String, Object> updateLabel = new HashMap<>(parameters);
updateLabel.put("label", it.getName());
operations.postForObject(LABEL_URI, it, JsonNode.class, updateLabel);
});
if (!additionalLabels.isEmpty()) {
logger.log(project, "Found additional labels: %s",
additionalLabels.stream().map(Label::getName).collect(Collectors.joining(", ")));
}
}
protected List<Label> getLabelsFromGitHub(Map<String, Object> parameters) {
List<Label> existsOnGitHub = new ArrayList<>();
doWithPaging(LABELS_URI, HttpMethod.GET, parameters, new HttpEntity<>(new HttpHeaders()), LABELS_TYPE, labels -> {
existsOnGitHub.addAll(labels);
return true;
});
return existsOnGitHub;
}
}

View File

@@ -1,46 +0,0 @@
/*
* Copyright 2016-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.issues.github;
import lombok.Value;
import org.springframework.data.release.model.ModuleIteration;
/**
* @author Mark Paluch
*/
@Value
class GithubMilestone {
ModuleIteration module;
public String getDescription() {
return module.getReleaseVersionString();
}
public Milestone toMilestone() {
return Milestone.of(toString(), getDescription());
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return module.getMediumVersionString();
}
}

View File

@@ -1,64 +0,0 @@
/*
* Copyright 2016-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.issues.github;
import lombok.AccessLevel;
import lombok.Data;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.List;
import javax.annotation.PostConstruct;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.data.release.git.GitProperties;
import org.springframework.data.release.utils.HttpBasicCredentials;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
/**
* @author Oliver Gierke
* @author Mark Paluch
*/
@Data
@Component
@ConfigurationProperties(prefix = "github")
@RequiredArgsConstructor
public class GitHubProperties {
private final @Getter(AccessLevel.NONE) GitProperties gitProperties;
private String apiUrl;
/**
* Usernames of project team members.
*/
private List<String> team;
public String getUsername() {
return gitProperties.getUsername();
}
public HttpBasicCredentials getHttpCredentials() {
return gitProperties.getHttpCredentials();
}
@PostConstruct
public void init() {
Assert.hasText(apiUrl, "No GitHub API base url configured!");
}
}

View File

@@ -1,71 +0,0 @@
/*
* Copyright 2014-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.issues.github;
import lombok.Value;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* Read-representation of a GitHub issue.
*
* @author Oliver Gierke
* @author Mark Paluch
*/
@Value
class GitHubReadIssue implements Comparable<GitHubReadIssue>, GitHubIssue {
String number, title, state, url;
GitHubUser user;
List<GitHubUser> assignees;
Milestone milestone;
PullRequest pullRequest;
List<Label> labels;
public GitHubReadIssue(String number, String title, String state, @JsonProperty("html_url") String url,
GitHubUser user, List<GitHubUser> assignees, Milestone milestone,
@JsonProperty("pull_request") PullRequest pullRequest, List<Label> labels) {
this.number = number;
this.title = title;
this.state = state;
this.url = url;
this.user = user;
this.assignees = assignees;
this.milestone = milestone;
this.pullRequest = pullRequest;
this.labels = labels;
}
@Override
public List<String> getAssignees() {
return assignees == null ? Collections.emptyList()
: assignees.stream().map(GitHubUser::getName).collect(Collectors.toList());
}
@Override
public int compareTo(GitHubReadIssue o) {
int number = this.number != null ? Integer.parseInt(this.number) : -1;
int other = o.number != null ? Integer.parseInt(o.number) : -1;
return Integer.compare(number, other);
}
}

View File

@@ -1,40 +0,0 @@
/*
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.issues.github;
import lombok.Value;
import lombok.With;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* Details of a Github release.
*
* @author Mark Paluch
*/
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE)
@Value
class GitHubRelease {
Integer id;
@JsonProperty("tag_name") String tagName;
String name;
@With String body;
@With boolean draft;
@With boolean prerelease;
}

View File

@@ -1,101 +0,0 @@
/*
* Copyright 2020-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.issues.github;
import lombok.RequiredArgsConstructor;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestOperations;
import org.springframework.web.util.DefaultUriBuilderFactory;
/**
* Support class for GitHub operations.
*
* @author Mark Paluch
*/
@RequiredArgsConstructor
class GitHubSupport {
final RestOperations operations;
static RestOperations createOperations(RestTemplateBuilder templateBuilder, GitHubProperties properties) {
return templateBuilder.uriTemplateHandler(new DefaultUriBuilderFactory(properties.getApiUrl())).build();
}
/**
* Apply a {@link Predicate callback} with GitHub paging starting at {@code endpointUri}. The given
* {@link Predicate#test(Object)} outcome controls whether paging continues by returning {@literal true} or stops.
*
* @param endpointUri
* @param method
* @param parameters
* @param entity
* @param type
* @param callbackContinue
* @param <T>
*/
<T> void doWithPaging(String endpointUri, HttpMethod method, Map<String, Object> parameters, HttpEntity<?> entity,
ParameterizedTypeReference<T> type, Predicate<T> callbackContinue) {
ResponseEntity<T> exchange = operations.exchange(endpointUri, method, entity, type, parameters);
Pattern pattern = Pattern.compile("<([^ ]*)>; rel=\"(\\w+)\"");
while (true) {
if (!callbackContinue.test(exchange.getBody())) {
return;
}
HttpHeaders responseHeaders = exchange.getHeaders();
List<String> links = responseHeaders.getValuesAsList("Link");
if (links.isEmpty()) {
return;
}
String nextLink = null;
for (String link : links) {
Matcher matcher = pattern.matcher(link);
if (matcher.find()) {
if (matcher.group(2).equals("next")) {
nextLink = matcher.group(1);
break;
}
}
}
if (nextLink == null) {
return;
}
exchange = operations.exchange(nextLink, method, entity, type, parameters);
}
}
}

View File

@@ -1,40 +0,0 @@
/*
* Copyright 2018-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.issues.github;
import lombok.Value;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* Details of a Github user.
*
* @author Madhura Bhave
*/
@Value
class GitHubUser {
String name;
String url;
public GitHubUser(@JsonProperty("login") String name, @JsonProperty("html_url") String url) {
this.name = name;
this.url = url;
}
}

View File

@@ -1,77 +0,0 @@
/*
* Copyright 2014-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.issues.github;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import lombok.Value;
import lombok.With;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import org.springframework.util.Assert;
/**
* @author Oliver Gierke
* @author Mark Paluch
*/
@Value
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
@With
class GitHubWriteIssue implements GitHubIssue {
String number, title, state;
List<Object> assignees;
Long milestone;
List<String> labels;
@Override
public String getUrl() {
return null;
}
public static GitHubWriteIssue of(String title, Milestone milestone) {
return new GitHubWriteIssue(null, title, null, null, milestone.getNumber(), null);
}
public static GitHubWriteIssue assignedTo(String username) {
Assert.hasText(username, "Username must not be null or empty!");
return new GitHubWriteIssue(null, null, null, Collections.singletonList(username), null, null);
}
public GitHubWriteIssue close() {
return new GitHubWriteIssue(this.number, this.title, "closed", this.assignees, null, null);
}
public GitHubWriteIssue withLabel(String labelName) {
List<String> labels = new ArrayList<>(this.labels == null ? Collections.emptyList() : this.labels);
labels.add(labelName);
return withLabels(labels);
}
public List<String> getAssignees() {
return assignees == null ? Collections.emptyList()
: assignees.stream().map(Objects::toString).collect(Collectors.toList());
}
}

View File

@@ -1,57 +0,0 @@
/*
* Copyright 2016-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.issues.github;
import lombok.AccessLevel;
import lombok.EqualsAndHashCode;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import lombok.experimental.FieldDefaults;
import org.springframework.data.release.issues.TicketStatus;
/**
* Value object for a GitHub ticket status.
*
* @author Mark Paluch
*/
@EqualsAndHashCode
@ToString
@RequiredArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
class GithubTicketStatus implements TicketStatus {
private final String status;
/*
* (non-Javadoc)
* @see org.springframework.data.release.jira.TicketStatus#getLabel()
*/
@Override
public String getLabel() {
return status;
}
/*
* (non-Javadoc)
* @see org.springframework.data.release.jira.TicketStatus#isResolved()
*/
@Override
public boolean isResolved() {
return "closed".equalsIgnoreCase(status);
}
}

Some files were not shown because too many files have changed in this diff Show More