Initial implementation for signing commits and tags
Signed-off-by: Ryan Baxter <ryan.baxter@broadcom.com>
This commit is contained in:
BIN
docs/src/main/asciidoc/images/sign-commits.png
Normal file
BIN
docs/src/main/asciidoc/images/sign-commits.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
@@ -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
|
||||
|
||||
@@ -95,6 +95,11 @@
|
||||
<groupId>org.jfrog.artifactory.client</groupId>
|
||||
<artifactId>artifactory-java-client-services</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jgit</groupId>
|
||||
<artifactId>org.eclipse.jgit.gpg.bc</artifactId>
|
||||
<version>${org.eclipse.jgit-version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.groovy</groupId>
|
||||
<artifactId>groovy</artifactId>
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<String> 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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\\\"]})\"}}",
|
||||
|
||||
@@ -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\"}]}}",
|
||||
|
||||
Reference in New Issue
Block a user