Initial implementation for signing commits and tags

Signed-off-by: Ryan Baxter <ryan.baxter@broadcom.com>
This commit is contained in:
Ryan Baxter
2025-05-21 10:52:32 -04:00
parent f68a482d40
commit d9c20f2cfd
9 changed files with 169 additions and 9 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -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

View File

@@ -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>

View File

@@ -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() {

View File

@@ -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);
}

View File

@@ -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;
}
}
}

View File

@@ -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);
}

View File

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

View File

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