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 a948ed1c..0362698c 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 195199e7..24ff0b5c 100644 --- a/releaser-core/src/main/java/releaser/internal/ReleaserProperties.java +++ b/releaser-core/src/main/java/releaser/internal/ReleaserProperties.java @@ -706,6 +706,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. @@ -968,6 +983,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='" @@ -979,7 +1010,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/git/GitRepo.java b/releaser-core/src/main/java/releaser/internal/git/GitRepo.java index ba6b08a0..77216d70 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); } } @@ -345,7 +359,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); @@ -395,7 +409,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(); @@ -406,13 +439,29 @@ 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) { log.warn("Exception occurred while reverting the commit", 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() { @@ -632,4 +681,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 c1cc1f48..f9cd8999 100644 --- a/releaser-core/src/test/java/releaser/internal/git/GitRepoTests.java +++ b/releaser-core/src/test/java/releaser/internal/git/GitRepoTests.java @@ -57,6 +57,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); }