From 7b38b0ed11f29692e9a04a78d8cc4e3ca1067a1e Mon Sep 17 00:00:00 2001 From: Madhura Bhave Date: Tue, 1 Dec 2020 17:15:41 -0800 Subject: [PATCH] Add SDKMAN to pipeline Closes gh-11987 --- .../command/PublishToSdkmanCommand.java | 66 ++++++++ .../sdkman/SdkmanProperties.java | 49 ++++++ .../releasescripts/sdkman/SdkmanService.java | 141 ++++++++++++++++++ .../command/PublishToSdkmanCommandTests.java | 104 +++++++++++++ .../sdkman/SdkmanServiceTests.java | 90 +++++++++++ .../src/test/resources/application.yml | 3 + ci/pipeline.yml | 7 +- ci/scripts/promote.sh | 2 + ci/tasks/promote.yml | 3 + 9 files changed, 464 insertions(+), 1 deletion(-) create mode 100644 ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/command/PublishToSdkmanCommand.java create mode 100644 ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/sdkman/SdkmanProperties.java create mode 100644 ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/sdkman/SdkmanService.java create mode 100644 ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/command/PublishToSdkmanCommandTests.java create mode 100644 ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/sdkman/SdkmanServiceTests.java diff --git a/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/command/PublishToSdkmanCommand.java b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/command/PublishToSdkmanCommand.java new file mode 100644 index 0000000000..5118b59b39 --- /dev/null +++ b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/command/PublishToSdkmanCommand.java @@ -0,0 +1,66 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.spring.concourse.releasescripts.command; + +import java.util.List; + +import io.spring.concourse.releasescripts.ReleaseType; +import io.spring.concourse.releasescripts.sdkman.SdkmanService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.boot.ApplicationArguments; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; + +/** + * Command used to publish to SDKMAN. + * + * @author Madhura Bhave + */ +@Component +public class PublishToSdkmanCommand implements Command { + + private static final Logger logger = LoggerFactory.getLogger(PublishToSdkmanCommand.class); + + private final SdkmanService service; + + public PublishToSdkmanCommand(SdkmanService service) { + this.service = service; + } + + @Override + public void run(ApplicationArguments args) throws Exception { + logger.debug("Running 'push to SDKMAN' command"); + List nonOptionArgs = args.getNonOptionArgs(); + Assert.state(!nonOptionArgs.isEmpty(), "No command argument specified"); + Assert.state(nonOptionArgs.size() >= 3, "Release type or version not specified"); + String releaseType = nonOptionArgs.get(1); + ReleaseType type = ReleaseType.from(releaseType); + if (!ReleaseType.RELEASE.equals(type)) { + return; + } + String version = nonOptionArgs.get(2); + boolean makeDefault = false; + if (nonOptionArgs.size() == 4) { + String releaseBranch = nonOptionArgs.get(3); + makeDefault = ("master".equals(releaseBranch)); + } + this.service.publish(version, makeDefault); + } + +} diff --git a/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/sdkman/SdkmanProperties.java b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/sdkman/SdkmanProperties.java new file mode 100644 index 0000000000..575d3cf1b7 --- /dev/null +++ b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/sdkman/SdkmanProperties.java @@ -0,0 +1,49 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.spring.concourse.releasescripts.sdkman; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * {@link ConfigurationProperties @ConfigurationProperties} for SDKMAN. + * + * @author Madhura Bhave + */ +@ConfigurationProperties(prefix = "sdkman") +public class SdkmanProperties { + + private String consumerKey; + + private String consumerToken; + + public String getConsumerKey() { + return this.consumerKey; + } + + public void setConsumerKey(String consumerKey) { + this.consumerKey = consumerKey; + } + + public String getConsumerToken() { + return this.consumerToken; + } + + public void setConsumerToken(String consumerToken) { + this.consumerToken = consumerToken; + } + +} diff --git a/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/sdkman/SdkmanService.java b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/sdkman/SdkmanService.java new file mode 100644 index 0000000000..2f6823535c --- /dev/null +++ b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/sdkman/SdkmanService.java @@ -0,0 +1,141 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.spring.concourse.releasescripts.sdkman; + +import java.net.URI; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.http.MediaType; +import org.springframework.http.RequestEntity; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.client.RestTemplate; + +/** + * Central class for interacting with SDKMAN's API. + * + * @author Madhura Bhave + */ +@Component +public class SdkmanService { + + private static final Logger logger = LoggerFactory.getLogger(SdkmanService.class); + + private static final String SDKMAN_URL = "https://vendors.sdkman.io/"; + + private static final String DOWNLOAD_URL = "https://repo.spring.io/simple/libs-release-local/org/springframework/boot/spring-boot-cli/" + + "%s/spring-boot-cli-%s-bin.zip"; + + private static final String SPRING_BOOT = "springboot"; + + private final RestTemplate restTemplate; + + public SdkmanService(RestTemplateBuilder builder, SdkmanProperties properties) { + String consumerKey = properties.getConsumerKey(); + String consumerToken = properties.getConsumerToken(); + if (StringUtils.hasLength(consumerKey)) { + builder = builder.basicAuthentication(consumerKey, consumerToken); + } + this.restTemplate = builder.build(); + } + + public void publish(String version, boolean makeDefault) { + release(version); + if (makeDefault) { + makeDefault(version); + } + broadcast(version); + } + + private void broadcast(String version) { + BroadcastRequest broadcastRequest = new BroadcastRequest(version); + RequestEntity broadcastEntity = RequestEntity.post(URI.create(SDKMAN_URL + "announce/struct")) + .contentType(MediaType.APPLICATION_JSON).body(broadcastRequest); + this.restTemplate.exchange(broadcastEntity, String.class); + logger.debug("Broadcast complete"); + } + + private void makeDefault(String version) { + logger.debug("Making this version the default"); + Request request = new Request(version); + RequestEntity requestEntity = RequestEntity.post(URI.create(SDKMAN_URL + "default")) + .contentType(MediaType.APPLICATION_JSON).body(request); + this.restTemplate.exchange(requestEntity, String.class); + logger.debug("Make default complete"); + } + + private void release(String version) { + ReleaseRequest releaseRequest = new ReleaseRequest(version, String.format(DOWNLOAD_URL, version, version)); + RequestEntity releaseEntity = RequestEntity.post(URI.create(SDKMAN_URL + "release")) + .contentType(MediaType.APPLICATION_JSON).body(releaseRequest); + this.restTemplate.exchange(releaseEntity, String.class); + logger.debug("Release complete"); + } + + static class Request { + + private final String candidate = SPRING_BOOT; + + private final String version; + + Request(String version) { + this.version = version; + } + + public String getCandidate() { + return this.candidate; + } + + public String getVersion() { + return this.version; + } + + } + + static class ReleaseRequest extends Request { + + private final String url; + + ReleaseRequest(String version, String url) { + super(version); + this.url = url; + } + + public String getUrl() { + return this.url; + } + + } + + static class BroadcastRequest extends Request { + + private final String hashtag = SPRING_BOOT; + + BroadcastRequest(String version) { + super(version); + } + + public String getHashtag() { + return this.hashtag; + } + + } + +} diff --git a/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/command/PublishToSdkmanCommandTests.java b/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/command/PublishToSdkmanCommandTests.java new file mode 100644 index 0000000000..a528f9522b --- /dev/null +++ b/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/command/PublishToSdkmanCommandTests.java @@ -0,0 +1,104 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.spring.concourse.releasescripts.command; + +import io.spring.concourse.releasescripts.sdkman.SdkmanService; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import org.springframework.boot.DefaultApplicationArguments; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +/** + * Tests for {@link PublishToSdkmanCommand}. + * + * @author Madhura Bhave + */ +class PublishToSdkmanCommandTests { + + @Mock + private SdkmanService service; + + private PublishToSdkmanCommand command; + + @BeforeEach + void setup() { + MockitoAnnotations.initMocks(this); + this.command = new PublishToSdkmanCommand(this.service); + } + + @Test + void runWhenReleaseTypeNotSpecifiedShouldThrowException() throws Exception { + Assertions.assertThatIllegalStateException() + .isThrownBy(() -> this.command.run(new DefaultApplicationArguments("publishGradlePlugin"))); + } + + @Test + void runWhenVersionNotSpecifiedShouldThrowException() throws Exception { + Assertions.assertThatIllegalStateException() + .isThrownBy(() -> this.command.run(new DefaultApplicationArguments("publishGradlePlugin", "RELEASE"))); + } + + @Test + void runWhenReleaseTypeMilestoneShouldDoNothing() throws Exception { + this.command.run(new DefaultApplicationArguments("publishGradlePlugin", "M", "1.2.3")); + verifyNoInteractions(this.service); + } + + @Test + void runWhenReleaseTypeRCShouldDoNothing() throws Exception { + this.command.run(new DefaultApplicationArguments("publishGradlePlugin", "RC", "1.2.3")); + verifyNoInteractions(this.service); + } + + @Test + void runWhenBranchNotSpecifiedShouldCallServiceWithMakeDefaultFalse() throws Exception { + DefaultApplicationArguments args = new DefaultApplicationArguments("promote", "RELEASE", "1.2.3"); + testRun(args, false); + } + + @Test + void runWhenBranchNotMasterShouldCallServiceWithMakeDefaultFalse() throws Exception { + DefaultApplicationArguments args = new DefaultApplicationArguments("promote", "RELEASE", "1.2.3", "other"); + testRun(args, false); + } + + @Test + void runWhenReleaseTypeReleaseShouldCallService() throws Exception { + DefaultApplicationArguments args = new DefaultApplicationArguments("promote", "RELEASE", "1.2.3", "master"); + testRun(args, true); + } + + private void testRun(DefaultApplicationArguments args, boolean makeDefault) throws Exception { + ArgumentCaptor versionCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor makeDefaultCaptor = ArgumentCaptor.forClass(Boolean.class); + this.command.run(args); + verify(this.service).publish(versionCaptor.capture(), makeDefaultCaptor.capture()); + String version = versionCaptor.getValue(); + Boolean makeDefaultValue = makeDefaultCaptor.getValue(); + assertThat(version).isEqualTo("1.2.3"); + assertThat(makeDefaultValue).isEqualTo(makeDefault); + } + +} diff --git a/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/sdkman/SdkmanServiceTests.java b/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/sdkman/SdkmanServiceTests.java new file mode 100644 index 0000000000..ee4ff2bcfe --- /dev/null +++ b/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/sdkman/SdkmanServiceTests.java @@ -0,0 +1,90 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.spring.concourse.releasescripts.sdkman; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.autoconfigure.web.client.RestClientTest; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.test.web.client.MockRestServiceServer; +import org.springframework.util.Base64Utils; + +import static org.springframework.test.web.client.match.MockRestRequestMatchers.content; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.header; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.method; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; + +/** + * Tests for {@link SdkmanService}. + * + * @author Madhura Bhave + */ +@EnableConfigurationProperties(SdkmanProperties.class) +@RestClientTest(SdkmanService.class) +class SdkmanServiceTests { + + @Autowired + private SdkmanService service; + + @Autowired + private SdkmanProperties properties; + + @Autowired + private MockRestServiceServer server; + + @AfterEach + void tearDown() { + this.server.reset(); + } + + @Test + void publishWhenMakeDefaultTrue() throws Exception { + setupExpectation("https://vendors.sdkman.io/release", + "{\"candidate\": \"springboot\", \"version\": \"1.2.3\", \"url\": \"https://repo.spring.io/simple/libs-release-local/org/springframework/boot/spring-boot-cli/1.2.3/spring-boot-cli-1.2.3-bin.zip\"}"); + setupExpectation("https://vendors.sdkman.io/default", + "{\"candidate\": \"springboot\", \"version\": \"1.2.3\"}"); + setupExpectation("https://vendors.sdkman.io/announce/struct", + "{\"candidate\": \"springboot\", \"version\": \"1.2.3\", \"hashtag\": \"springboot\"}"); + this.service.publish("1.2.3", true); + this.server.verify(); + } + + @Test + void publishWhenMakeDefaultFalse() throws Exception { + setupExpectation("https://vendors.sdkman.io/release", + "{\"candidate\": \"springboot\", \"version\": \"1.2.3\", \"url\": \"https://repo.spring.io/simple/libs-release-local/org/springframework/boot/spring-boot-cli/1.2.3/spring-boot-cli-1.2.3-bin.zip\"}"); + setupExpectation("https://vendors.sdkman.io/announce/struct", + "{\"candidate\": \"springboot\", \"version\": \"1.2.3\", \"hashtag\": \"springboot\"}"); + this.service.publish("1.2.3", false); + this.server.verify(); + } + + private void setupExpectation(String url, String body) { + this.server.expect(requestTo(url)).andExpect(method(HttpMethod.POST)).andExpect(content().json(body)) + .andExpect(header("Authorization", + "Basic " + Base64Utils.encodeToString(String + .format("%s:%s", this.properties.getConsumerKey(), this.properties.getConsumerToken()) + .getBytes()))) + .andExpect(header("Content-Type", MediaType.APPLICATION_JSON.toString())).andRespond(withSuccess()); + } + +} diff --git a/ci/images/releasescripts/src/test/resources/application.yml b/ci/images/releasescripts/src/test/resources/application.yml index 88fe8a8f34..d045250905 100644 --- a/ci/images/releasescripts/src/test/resources/application.yml +++ b/ci/images/releasescripts/src/test/resources/application.yml @@ -9,3 +9,6 @@ bintray: sonatype: user-token: sonatype-user password-token: sonatype-password +sdkman: + consumer-key: sdkman-consumer-key + consumer-token: sdkman-consumer-token diff --git a/ci/pipeline.yml b/ci/pipeline.yml index dd0709f8d5..4e3ba22a09 100644 --- a/ci/pipeline.yml +++ b/ci/pipeline.yml @@ -4,7 +4,7 @@ anchors: username: ((github-username)) password: ((github-password)) branch: ((branch)) - docker-resource-source: &docker-resource-source + docker-resource-source: &docker-resource-source username: ((docker-hub-username)) password: ((docker-hub-password)) tag: ((milestone)) @@ -42,6 +42,9 @@ anchors: ARTIFACTORY_SERVER: ((artifactory-server)) ARTIFACTORY_USERNAME: ((artifactory-username)) ARTIFACTORY_PASSWORD: ((artifactory-password)) + sdkman-task-params: &sdkman-task-params + SDKMAN_CONSUMER_KEY: ((sdkman-consumer-key)) + SDKMAN_CONSUMER_TOKEN: ((sdkman-consumer-token)) artifactory-repo-put-params: &artifactory-repo-put-params repo: libs-snapshot-local folder: distribution-repository @@ -623,8 +626,10 @@ jobs: file: git-repo/ci/tasks/promote.yml params: RELEASE_TYPE: RELEASE + BRANCH: ((BRANCH)) <<: *artifactory-task-params <<: *bintray-task-params + <<: *sdkman-task-params - name: sync-to-maven-central serial: true plan: diff --git a/ci/scripts/promote.sh b/ci/scripts/promote.sh index cff0f3e86b..025720f60e 100755 --- a/ci/scripts/promote.sh +++ b/ci/scripts/promote.sh @@ -11,5 +11,7 @@ java -jar /spring-boot-release-scripts.jar distribute $RELEASE_TYPE $BUILD_INFO_ java -jar /spring-boot-release-scripts.jar publishGradlePlugin $RELEASE_TYPE $BUILD_INFO_LOCATION || { exit 1; } +java -jar /spring-boot-release-scripts.jar publishToSdkman $RELEASE_TYPE $version $BRANCH || {exit 1;} + echo "Promotion complete" echo $version > version/version diff --git a/ci/tasks/promote.yml b/ci/tasks/promote.yml index 68821a052c..03492c469a 100644 --- a/ci/tasks/promote.yml +++ b/ci/tasks/promote.yml @@ -14,5 +14,8 @@ params: BINTRAY_REPO: BINTRAY_USERNAME: BINTRAY_API_KEY: + SDKMAN_CONSUMER_KEY: + SDKMAN_CONSUMER_TOKEN: + BRANCH: run: path: git-repo/ci/scripts/promote.sh