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\"}]}}",