diff --git a/docs/src/main/asciidoc/images/sign-commits.png b/docs/src/main/asciidoc/images/sign-commits.png
new file mode 100644
index 00000000..b53a59a0
Binary files /dev/null and b/docs/src/main/asciidoc/images/sign-commits.png differ
diff --git a/docs/src/main/asciidoc/spring-cloud-release-process.adoc b/docs/src/main/asciidoc/spring-cloud-release-process.adoc
index db5dc57e..0def3251 100644
--- a/docs/src/main/asciidoc/spring-cloud-release-process.adoc
+++ b/docs/src/main/asciidoc/spring-cloud-release-process.adoc
@@ -111,6 +111,31 @@ image::https://raw.githubusercontent.com/spring-cloud/spring-cloud-release-tools
. Clear the Start_From field
. Click Rebuild
+=== Signing Commits and Tags
+
+The releaser can sign commits and tags when doing a release. Signing is enabled when the flag `releaser.git.signCommits`
+is set to `true`, by default it is set to `false`. When set to `true` you also need to set `releaser.git.signing-key-passphrase`
+to the passphrase for the key being used to sign commits. The key used to sign commits is configured in either global or
+git repo config properties. You can set the key to use by doing the following:
+
+```bash
+$ gpg --list-secret-keys
+$ git config --global user.signingkey
+```
+
+This will get you a list of ids of secret keys know by GPG. Select the id of the key you want to use to sign commits
+and then set that id in your git config:
+
+```bash
+$ git config [--global] user.signingkey [keyid]
+```
+
+The releaser (JGit) will use this key along with the passphrase you set to sign commits and tags.
+
+Signing commits/tags can be enabled/disabled in Jenkins by checking the following box during a release:
+
+image::images/sign-commits.png[]
+
=== Commercial Releases
See https://docs.google.com/document/d/10pk6b2Cy0OW9fzFKEHSRIys-2Z_rseqnu7CIYFXnJoM/edit#heading=h.slor8nyo3f1n[this document] from Trevor for more information on the requirement to create release bundles
diff --git a/releaser-core/pom.xml b/releaser-core/pom.xml
index bb86f048..7e1970d9 100644
--- a/releaser-core/pom.xml
+++ b/releaser-core/pom.xml
@@ -95,6 +95,11 @@
org.jfrog.artifactory.client
artifactory-java-client-services
+
+ org.eclipse.jgit
+ org.eclipse.jgit.gpg.bc
+ ${org.eclipse.jgit-version}
+
org.apache.groovy
groovy
diff --git a/releaser-core/src/main/java/releaser/internal/ReleaserProperties.java b/releaser-core/src/main/java/releaser/internal/ReleaserProperties.java
index aff6b89f..c68f90e8 100644
--- a/releaser-core/src/main/java/releaser/internal/ReleaserProperties.java
+++ b/releaser-core/src/main/java/releaser/internal/ReleaserProperties.java
@@ -615,6 +615,21 @@ public class ReleaserProperties implements Serializable {
*/
private boolean updateAllTestSamples = false;
+ /**
+ * If set to {@code true}, we will sign any commits we make. In order to do this
+ * GPG must contain the keys for the spring-builds user. This flag can be used to
+ * override signing preferences in the Git config. For example, JGit will sign
+ * commits/tags if signing is enabled the Git config (either repo config or global
+ * config). If you do not want to sign anything then you can set this flag to
+ * false and the releaser will not attempt to sign anything. The same is true if
+ * signing is set to false in your Git config but you would like the releaser to
+ * sign commits/tags, setting this flag to true would enable signing to take
+ * place.
+ */
+ private boolean signCommits = false;
+
+ private String signingKeyPassphrase;
+
/**
* Project to urls mapping. For each project will clone the test project and will
* update its versions.
@@ -877,6 +892,22 @@ public class ReleaserProperties implements Serializable {
this.cacheDirectory = cacheDirectory;
}
+ public boolean isSignCommits() {
+ return signCommits;
+ }
+
+ public void setSignCommits(boolean signCommits) {
+ this.signCommits = signCommits;
+ }
+
+ public String getSigningKeyPassphrase() {
+ return signingKeyPassphrase;
+ }
+
+ public void setSigningKeyPassphrase(String signingKeyPassphrase) {
+ this.signingKeyPassphrase = signingKeyPassphrase;
+ }
+
@Override
public String toString() {
return "Git{" + "releaseTrainBomUrl='" + this.releaseTrainBomUrl + '\'' + ", documentationUrl='"
@@ -888,7 +919,8 @@ public class ReleaserProperties implements Serializable {
+ ", cloneDestinationDir='" + this.cloneDestinationDir + '\'' + ", fetchVersionsFromGit="
+ this.fetchVersionsFromGit + ", numberOfCheckedMilestones=" + this.numberOfCheckedMilestones
+ ", updateSpringGuides=" + this.updateSpringGuides + ", updateSpringProject="
- + this.updateSpringProject + ", sampleUrlsSize=" + this.allTestSampleUrls.size() + '}';
+ + this.updateSpringProject + ", sampleUrlsSize=" + this.allTestSampleUrls.size() + ", signCommits="
+ + this.signCommits + '}';
}
private static String temporaryDirectory() {
diff --git a/releaser-core/src/main/java/releaser/internal/commercial/ReleaseBundleCreator.java b/releaser-core/src/main/java/releaser/internal/commercial/ReleaseBundleCreator.java
index ddc529f3..04e46d91 100644
--- a/releaser-core/src/main/java/releaser/internal/commercial/ReleaseBundleCreator.java
+++ b/releaser-core/src/main/java/releaser/internal/commercial/ReleaseBundleCreator.java
@@ -91,9 +91,10 @@ public class ReleaseBundleCreator {
log.info("Creating release bundle with JSON [{}]", json);
ArtifactoryRequest aqlRequest = new ArtifactoryRequestImpl().method(ArtifactoryRequest.Method.POST)
- .apiUrl("lifecycle/api/v2/release_bundle").addQueryParam("project", "spring").addQueryParam("async", "false")
- .addHeader("X-JFrog-Signing-Key-Name", "packagesKey").requestType(ArtifactoryRequest.ContentType.JSON)
- .responseType(ArtifactoryRequest.ContentType.JSON).requestBody(json);
+ .apiUrl("lifecycle/api/v2/release_bundle").addQueryParam("project", "spring")
+ .addQueryParam("async", "false").addHeader("X-JFrog-Signing-Key-Name", "packagesKey")
+ .requestType(ArtifactoryRequest.ContentType.JSON).responseType(ArtifactoryRequest.ContentType.JSON)
+ .requestBody(json);
return makeArtifactoryRequest(aqlRequest);
}
diff --git a/releaser-core/src/main/java/releaser/internal/git/GitRepo.java b/releaser-core/src/main/java/releaser/internal/git/GitRepo.java
index 49096bf2..50db2db2 100644
--- a/releaser-core/src/main/java/releaser/internal/git/GitRepo.java
+++ b/releaser-core/src/main/java/releaser/internal/git/GitRepo.java
@@ -25,6 +25,7 @@ import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
+import java.util.Set;
import java.util.stream.Stream;
import com.jcraft.jsch.IdentityRepository;
@@ -48,12 +49,16 @@ import org.eclipse.jgit.api.ResetCommand;
import org.eclipse.jgit.api.TransportConfigCallback;
import org.eclipse.jgit.api.errors.EmptyCommitException;
import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.errors.UnsupportedCredentialItem;
+import org.eclipse.jgit.lib.ConfigConstants;
import org.eclipse.jgit.lib.Constants;
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.FileBasedConfig;
+import org.eclipse.jgit.transport.CredentialItem;
import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.transport.FetchResult;
import org.eclipse.jgit.transport.RefSpec;
@@ -85,9 +90,17 @@ class GitRepo {
private final File basedir;
+ private boolean signCommits = false;
+
GitRepo(File basedir, ReleaserProperties properties) {
this.basedir = basedir;
this.gitFactory = new GitRepo.JGitFactory(properties);
+ this.signCommits = properties.getGit().isSignCommits();
+ if (this.signCommits) {
+ log.info("GitRepo signCommits enabled");
+ CredentialsProvider
+ .setDefault(new SigningCredentialsProvider(properties.getGit().getSigningKeyPassphrase()));
+ }
}
// for tests
@@ -173,13 +186,14 @@ class GitRepo {
void commit(String message) {
try (Git git = this.gitFactory.open(file(this.basedir))) {
git.add().addFilepattern(".").call();
- git.commit().setAllowEmpty(false).setMessage(message).call();
+ git.commit().setSign(this.signCommits).setAllowEmpty(false).setMessage(message).call();
printLog(git);
}
catch (EmptyCommitException e) {
log.info("There were no changes detected. Will not commit an empty commit");
}
catch (Exception e) {
+ e.printStackTrace();
throw new IllegalStateException(e);
}
}
@@ -334,7 +348,7 @@ class GitRepo {
*/
void tag(String tagName) {
try (Git git = this.gitFactory.open(file(this.basedir))) {
- git.tag().setName(tagName).call();
+ git.tag().setSigned(this.signCommits).setName(tagName).call();
}
catch (Exception e) {
throw new IllegalStateException(e);
@@ -384,7 +398,26 @@ class GitRepo {
}
void revert(String message) {
+ Boolean originalGpgSign = null;
try (Git git = this.gitFactory.open(file(this.basedir))) {
+ // The revert API has no way to disable signing if the global git config has
+ // it enabled
+ // In order to make sure revert commits are not signed when we disable signing
+ // we have to set
+ // signing to false in the repos git config
+ if (!this.signCommits) {
+ FileBasedConfig clonedConfig = new FileBasedConfig(new File(file(this.basedir), ".git/config"),
+ FS.DETECTED);
+ clonedConfig.load();
+ Set names = clonedConfig.getNames(ConfigConstants.CONFIG_COMMIT_SECTION);
+ if (names.contains(ConfigConstants.CONFIG_KEY_GPGSIGN)) {
+ originalGpgSign = clonedConfig.getBoolean(ConfigConstants.CONFIG_COMMIT_SECTION,
+ ConfigConstants.CONFIG_KEY_GPGSIGN, false);
+ }
+ clonedConfig.setBoolean(ConfigConstants.CONFIG_COMMIT_SECTION, null, ConfigConstants.CONFIG_KEY_GPGSIGN,
+ false);
+ clonedConfig.save();
+ }
RevCommit commit = git.log().setMaxCount(1).call().iterator().next();
String shortMessage = commit.getShortMessage();
String id = commit.getId().getName();
@@ -395,12 +428,28 @@ class GitRepo {
}
log.debug("The commit to be reverted is [{}]", commit);
git.revert().include(commit).call();
- git.commit().setAmend(true).setMessage(message).call();
+ git.commit().setSign(this.signCommits).setAmend(true).setMessage(message).call();
printLog(git);
}
catch (Exception e) {
throw new IllegalStateException(e);
}
+ finally {
+ if (originalGpgSign != null) {
+ try {
+ FileBasedConfig clonedConfig = new FileBasedConfig(new File(file(this.basedir), ".git/config"),
+ FS.DETECTED);
+ clonedConfig.load();
+ clonedConfig.setBoolean(ConfigConstants.CONFIG_COMMIT_SECTION, null,
+ ConfigConstants.CONFIG_KEY_GPGSIGN, originalGpgSign);
+ clonedConfig.save();
+ }
+ catch (Exception e) {
+ log.warn("Could not revert gpg signing configuration within cloned repo.", e);
+ }
+ }
+ }
+
}
String currentBranch() {
@@ -620,4 +669,51 @@ class GitRepo {
}
+ static class SigningCredentialsProvider extends CredentialsProvider {
+
+ private final String passphrase;
+
+ SigningCredentialsProvider(String passphrase) {
+ this.passphrase = passphrase;
+ }
+
+ @Override
+ public boolean isInteractive() {
+ return false;
+ }
+
+ @Override
+ public boolean supports(CredentialItem... items) {
+ for (CredentialItem i : items) {
+ if (i instanceof CredentialItem.Password) {
+ continue;
+ }
+ if (i instanceof CredentialItem.StringType) {
+ if (i.getPromptText().equals("Password: ")) {
+ continue;
+ }
+ }
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public boolean get(URIish uri, CredentialItem... items) throws UnsupportedCredentialItem {
+ for (CredentialItem item : items) {
+ if (item instanceof CredentialItem.Password) {
+ log.info("Password credential found, setting value.");
+ if (!StringUtils.hasText(passphrase)) {
+ log.warn("Password credential found, but passphrase is empty, did you forget to set "
+ + "releaser.git.signing-key-passphrase?");
+ return false;
+ }
+ ((CredentialItem.Password) item).setValue(passphrase.trim().toCharArray());
+ }
+ }
+ return true;
+ }
+
+ }
+
}
diff --git a/releaser-core/src/test/java/releaser/internal/git/GitRepoTests.java b/releaser-core/src/test/java/releaser/internal/git/GitRepoTests.java
index d285fbfd..fd3ae832 100644
--- a/releaser-core/src/test/java/releaser/internal/git/GitRepoTests.java
+++ b/releaser-core/src/test/java/releaser/internal/git/GitRepoTests.java
@@ -58,6 +58,7 @@ public class GitRepoTests {
this.springCloudReleaseProject = new File(
GitRepoTests.class.getResource("/projects/spring-cloud-release").toURI());
TestUtils.prepareLocalRepo();
+ // Use the constructor with signing disabled for most tests
this.gitRepo = new GitRepo(this.tmpFolder);
}
diff --git a/releaser-core/src/test/resources/mappings/commercial/create_porject_release_bundle.json b/releaser-core/src/test/resources/mappings/commercial/create_porject_release_bundle.json
index 8dd63fce..0da4351a 100644
--- a/releaser-core/src/test/resources/mappings/commercial/create_porject_release_bundle.json
+++ b/releaser-core/src/test/resources/mappings/commercial/create_porject_release_bundle.json
@@ -2,7 +2,7 @@
"id" : "0b389dc6-1b73-487d-8122-41f1c974647f",
"name" : "create_project_release_bundle_mapping",
"request" : {
- "url" : "/lifecycle/api/v2/release_bundle?project=spring",
+ "url" : "/lifecycle/api/v2/release_bundle?async=false&project=spring",
"method" : "POST",
"bodyPatterns" : [ {
"equalToJson" : "{\"release_bundle_version\":\"4.0.7\",\"release_bundle_name\":\"TNZ-spring-cloud-build-commercial\",\"source_type\":\"aql\",\"source\":{\"aql\":\"items.find({\\\"repo\\\":{\\\"$eq\\\":\\\"spring-enterprise-maven-prod-local\\\"},\\\"$or\\\":[{\\\"path\\\":{\\\"$match\\\":\\\"org/springframework/cloud/spring-cloud-build*/4.0.7\\\"}},{\\\"path\\\":{\\\"$match\\\":\\\"org/springframework/cloud/spring-cloud-starter-build*/4.0.7\\\"}},{\\\"path\\\":{\\\"$match\\\":\\\"org/springframework/cloud/spring-cloud-dependencies-parent*/4.0.7\\\"}}]}).sort({\\\"$asc\\\":[\\\"path\\\",\\\"name\\\"]})\"}}",
diff --git a/releaser-core/src/test/resources/mappings/commercial/create_release_train_source_bundle.json b/releaser-core/src/test/resources/mappings/commercial/create_release_train_source_bundle.json
index 397907f7..17dd02ce 100644
--- a/releaser-core/src/test/resources/mappings/commercial/create_release_train_source_bundle.json
+++ b/releaser-core/src/test/resources/mappings/commercial/create_release_train_source_bundle.json
@@ -2,7 +2,7 @@
"id" : "0b389dc6-1b73-487d-8122-41f1c974647f",
"name" : "create_release_train_source_bundle_mapping",
"request" : {
- "url" : "/lifecycle/api/v2/release_bundle?project=spring",
+ "url" : "/lifecycle/api/v2/release_bundle?async=false&project=spring",
"method" : "POST",
"bodyPatterns" : [ {
"equalToJson" : "{\"release_bundle_name\": \"TNZ-spring-cloud-commercial-release\",\"release_bundle_version\": \"2022.0.7\",\"skip_docker_manifest_resolution\": false,\"source_type\": \"release_bundles\",\"source\": {\"release_bundles\": [{\"project_key\": \"spring\",\"repository_key\": \"spring-release-bundles-v2\",\"release_bundle_name\": \"TNZ-spring-cloud-build-commercial\",\"release_bundle_version\": \"4.0.8\"},{\"project_key\": \"spring\",\"repository_key\": \"spring-release-bundles-v2\",\"release_bundle_name\": \"TNZ-spring-cloud-config-commercial\",\"release_bundle_version\": \"4.0.7\"},{\"project_key\": \"spring\",\"repository_key\": \"spring-release-bundles-v2\",\"release_bundle_name\": \"TNZ-spring-cloud-starter-commercial\",\"release_bundle_version\": \"2022.0.7\"},{\"project_key\": \"spring\",\"repository_key\": \"spring-release-bundles-v2\",\"release_bundle_name\": \"TNZ-spring-cloud-vault-commercial\",\"release_bundle_version\": \"4.0.7\"}]}}",