diff --git a/release-tools/application-local.template b/release-tools/application-local.template index 065ebdf..277a68c 100644 --- a/release-tools/application-local.template +++ b/release-tools/application-local.template @@ -20,9 +20,9 @@ jira.password= jira.url=https://jira.spring.io # GPG -deployment.gpg.keyname= -deployment.gpg.password= -# deployment.gpg.executable=/usr/local/bin/gpg2 +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. diff --git a/release-tools/pom.xml b/release-tools/pom.xml index 30eef71..d93dc5f 100644 --- a/release-tools/pom.xml +++ b/release-tools/pom.xml @@ -96,7 +96,7 @@ org.eclipse.jgit org.eclipse.jgit - 4.2.0.201601211800-r + 5.7.0.202003110725-r diff --git a/release-tools/readme.md b/release-tools/readme.md index f12270b..4bd1ebf 100644 --- a/release-tools/readme.md +++ b/release-tools/readme.md @@ -16,6 +16,10 @@ Add an `application-local.properties` to the project root and add the following - `maven.mavenHome` - Pointing to the location of your Maven installation. - `deployment.api-key` - The API key to use for artifact promotion. - `deployment.password` - The password of the deployment user (buildmaster). +- `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`. + See `application-local.template` for details. diff --git a/release-tools/src/main/java/org/springframework/data/release/build/MavenBuildSystem.java b/release-tools/src/main/java/org/springframework/data/release/build/MavenBuildSystem.java index 09367fc..11d020d 100644 --- a/release-tools/src/main/java/org/springframework/data/release/build/MavenBuildSystem.java +++ b/release-tools/src/main/java/org/springframework/data/release/build/MavenBuildSystem.java @@ -34,9 +34,9 @@ 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.deployment.DeploymentProperties.Gpg; 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.ModuleIteration; import org.springframework.data.release.model.Phase; import org.springframework.data.release.model.Project; @@ -66,6 +66,7 @@ class MavenBuildSystem implements BuildSystem { Logger logger; MavenRuntime mvn; DeploymentProperties properties; + Gpg gpg; /* * (non-Javadoc) @@ -331,7 +332,7 @@ class MavenBuildSystem implements BuildSystem { logger.log(module, "Deploying artifacts to Sonatype OSS Nexus…"); - Gpg gpg = properties.getGpg(); + Gpg gpg = this.gpg.isGpgAvailable() ? this.gpg : properties.getGpg(); CommandLine arguments = CommandLine.of(Goal.DEPLOY, // profile("ci,release,central"), // diff --git a/release-tools/src/main/java/org/springframework/data/release/deployment/DeploymentProperties.java b/release-tools/src/main/java/org/springframework/data/release/deployment/DeploymentProperties.java index 6b5f54a..0492cb4 100644 --- a/release-tools/src/main/java/org/springframework/data/release/deployment/DeploymentProperties.java +++ b/release-tools/src/main/java/org/springframework/data/release/deployment/DeploymentProperties.java @@ -20,6 +20,8 @@ import lombok.Data; import java.net.URI; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.DeprecatedConfigurationProperty; +import org.springframework.data.release.model.Gpg; import org.springframework.data.release.model.Password; import org.springframework.data.release.utils.HttpBasicCredentials; import org.springframework.stereotype.Component; @@ -74,6 +76,11 @@ public class DeploymentProperties { return repositoryPrefix.concat(distributionRepository); } + @DeprecatedConfigurationProperty(reason = "Moved to gpg.", replacement = "gpg") + public Gpg getGpg() { + return gpg; + } + /** * Returns the URI of the staging repository. * @@ -119,9 +126,4 @@ public class DeploymentProperties { } } - @Data - public static class Gpg { - private String keyname, executable; - private Password password; - } } diff --git a/release-tools/src/main/java/org/springframework/data/release/git/GitOperations.java b/release-tools/src/main/java/org/springframework/data/release/git/GitOperations.java index 6b821f9..0f66d36 100644 --- a/release-tools/src/main/java/org/springframework/data/release/git/GitOperations.java +++ b/release-tools/src/main/java/org/springframework/data/release/git/GitOperations.java @@ -21,8 +21,10 @@ import lombok.experimental.FieldDefaults; import java.io.File; import java.io.IOException; +import java.util.Arrays; import java.util.Collection; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.concurrent.atomic.AtomicBoolean; @@ -32,22 +34,31 @@ import java.util.stream.Stream; import org.eclipse.jgit.api.CheckoutCommand; import org.eclipse.jgit.api.CherryPickResult; import org.eclipse.jgit.api.CherryPickResult.CherryPickStatus; +import org.eclipse.jgit.api.CommitCommand; import org.eclipse.jgit.api.CreateBranchCommand.SetupUpstreamMode; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.ResetCommand.ResetType; import org.eclipse.jgit.api.errors.RefNotFoundException; +import org.eclipse.jgit.errors.UnsupportedCredentialItem; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.storage.file.FileRepositoryBuilder; +import org.eclipse.jgit.transport.CredentialItem; +import org.eclipse.jgit.transport.CredentialItem.CharArrayType; +import org.eclipse.jgit.transport.CredentialItem.InformationalMessage; +import org.eclipse.jgit.transport.CredentialsProvider; import org.eclipse.jgit.transport.RefSpec; import org.eclipse.jgit.transport.TagOpt; +import org.eclipse.jgit.transport.URIish; + 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.model.ArtifactVersion; +import org.springframework.data.release.model.Gpg; import org.springframework.data.release.model.Iteration; import org.springframework.data.release.model.ModuleIteration; import org.springframework.data.release.model.Project; @@ -63,6 +74,7 @@ import org.springframework.util.Assert; * Component to execute Git related operations. * * @author Oliver Gierke + * @author Mark Paluch */ @Component @RequiredArgsConstructor @@ -78,6 +90,7 @@ public class GitOperations { Logger logger; PluginRegistry issueTracker; GitProperties gitProperties; + Gpg gpg; /** * Returns the {@link GitProject} for the given {@link Project}. @@ -93,7 +106,6 @@ public class GitOperations { * Resets the repositories for all modules of the given {@link Train}. * * @param train must not be {@literal null}. - * @throws Exception */ public void reset(TrainIteration train) { @@ -437,16 +449,25 @@ public class GitOperations { String author = gitProperties.getAuthor(); String email = gitProperties.getEmail(); - logger.log(module, "git commit -m \"%s\" --author=\"%s <%s>\"", commit, author, email); + logger.log(module, "git commit -m \"%s\" %s --author=\"%s <%s>\"", commit, + gpg.isGpgAvailable() ? "-S" + gpg.getKeyname() : "", author, email); doWithGit(project, git -> { - git.commit()// + CommitCommand commitCommand = git.commit()// .setMessage(commit.toString())// .setAuthor(author, email)// .setCommitter(author, email)// - .setAll(true)// - .call(); + .setAll(true); + + if (gpg.isGpgAvailable()) { + commitCommand.setSign(true).setSigningKey(gpg.getKeyname()) + .setCredentialsProvider(new GpgPassphraseProvider(gpg)); + } else { + commitCommand.setSign(false); + } + + commitCommand.call(); }); } @@ -783,4 +804,54 @@ public class GitOperations { private static interface VoidGitCallback { void doWithGit(Git git) throws Exception; } + + /** + * {@link CredentialsProvider} for GPG Keys used with JGit Commit Signing. + */ + private static class GpgPassphraseProvider extends CredentialsProvider { + + private final Gpg gpg; + + private GpgPassphraseProvider(Gpg gpg) { + this.gpg = gpg; + } + + @Override + public boolean isInteractive() { + return false; + } + + @Override + public boolean supports(CredentialItem... items) { + + boolean matchesKey = matchesKey(items); + boolean hasSettableCharArray = Arrays.stream(items).anyMatch(CharArrayType.class::isInstance); + + return matchesKey && hasSettableCharArray; + } + + private boolean matchesKey(CredentialItem[] items) { + return Arrays.stream(items).filter(InformationalMessage.class::isInstance) // + .map(CredentialItem::getPromptText) // + .map(it -> it.toLowerCase(Locale.US)) // + .anyMatch(it -> it.contains(gpg.getKeyname().toLowerCase(Locale.US))); + } + + @Override + public boolean get(URIish uri, CredentialItem... items) throws UnsupportedCredentialItem { + + if (!matchesKey(items)) { + return false; + } + + for (CredentialItem item : items) { + if (item instanceof CharArrayType) { + ((CharArrayType) item).setValueNoCopy(gpg.getPassword().toString().toCharArray()); + + return true; + } + } + return false; + } + } } diff --git a/release-tools/src/main/java/org/springframework/data/release/model/Gpg.java b/release-tools/src/main/java/org/springframework/data/release/model/Gpg.java new file mode 100644 index 0000000..3d7bad1 --- /dev/null +++ b/release-tools/src/main/java/org/springframework/data/release/model/Gpg.java @@ -0,0 +1,38 @@ +/* + * Copyright 2019 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 + * + * http://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.model; + +import lombok.Data; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +/** + * @author Oliver Gierke + * @author Mark Paluch + */ +@Data +@Component +@ConfigurationProperties(prefix = "gpg") +public class Gpg { + private String keyname, executable; + private Password password; + + public boolean isGpgAvailable() { + return this.password != null && StringUtils.hasText(keyname); + } +} diff --git a/release-tools/src/main/resources/application.properties b/release-tools/src/main/resources/application.properties index fa3b02e..f4b5380 100644 --- a/release-tools/src/main/resources/application.properties +++ b/release-tools/src/main/resources/application.properties @@ -1,6 +1,7 @@ spring.main.banner-mode=off io.work-dir=~/temp/spring-data-shell/workspace io.logs=logs + # Maven setup maven.local-repository=~/temp/spring-data-shell/repository maven.plugins.versions=org.codehaus.mojo:versions-maven-plugin:2.2 @@ -10,12 +11,15 @@ deployment.staging-repository=libs-staging-local deployment.distribution-repository=temp-private-local deployment.username=buildmaster #deployment.password <- local + # GPG setup -deployment.gpg.executable=/usr/local/bin/gpg2 -# deployment.gpg.keyname -# deployment.gpg.password +gpg.executable=/usr/local/bin/gpg2 +# gpg.keyname +# gpg.password + # JIRA jira.api-url=https://jira.spring.io + # GitHub github.api-url=https://api.github.com logging.pattern.console=%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(%5p) %clr(%-20.20logger{19}){cyan}%clr(:){faint} %m%n%wEx