From 1a082357d307d6976ba4499efe70c655554b3f12 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Wed, 7 Apr 2021 17:57:57 -0500 Subject: [PATCH] Add sagan(Create|Delete)Release Closes gh-9577 --- build.gradle | 6 + buildSrc/build.gradle | 6 + .../springframework/gradle/sagan/Release.java | 123 ++++++++++++++++++ .../gradle/sagan/SaganApi.java | 93 +++++++++++++ .../gradle/sagan/SaganCreateReleaseTask.java | 86 ++++++++++++ .../gradle/sagan/SaganDeleteReleaseTask.java | 62 +++++++++ .../gradle/sagan/SaganPlugin.java | 47 +++++++ .../convention/sagan/SaganApiTests.java | 85 ++++++++++++ 8 files changed, 508 insertions(+) create mode 100644 buildSrc/src/main/java/org/springframework/gradle/sagan/Release.java create mode 100644 buildSrc/src/main/java/org/springframework/gradle/sagan/SaganApi.java create mode 100644 buildSrc/src/main/java/org/springframework/gradle/sagan/SaganCreateReleaseTask.java create mode 100644 buildSrc/src/main/java/org/springframework/gradle/sagan/SaganDeleteReleaseTask.java create mode 100644 buildSrc/src/main/java/org/springframework/gradle/sagan/SaganPlugin.java create mode 100644 buildSrc/src/test/java/io/spring/gradle/convention/sagan/SaganApiTests.java diff --git a/build.gradle b/build.gradle index d7287fe39f..9718225ca6 100644 --- a/build.gradle +++ b/build.gradle @@ -16,6 +16,7 @@ apply plugin: 'locks' apply plugin: 'io.spring.convention.root' apply plugin: 'org.jetbrains.kotlin.jvm' apply plugin: 'org.springframework.security.update-dependencies' +apply plugin: 'org.springframework.security.sagan' group = 'org.springframework.security' description = 'Spring Security' @@ -28,6 +29,11 @@ repositories { mavenCentral() } +tasks.named("saganCreateRelease") { + referenceDocUrl = "https://docs.spring.io/spring-security/site/docs/{version}/reference/html5/" + apiDocUrl = "https://docs.spring.io/spring-security/site/docs/{version}/api/" +} + updateDependenciesSettings { gitHub { organization = "spring-projects" diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 3cc5c7c9e6..a1f3b75518 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -44,6 +44,10 @@ gradlePlugin { id = "org.springframework.security.update-dependencies" implementationClass = "org.springframework.security.convention.versions.UpdateDependenciesPlugin" } + sagan { + id = "org.springframework.security.sagan" + implementationClass = "org.springframework.gradle.sagan.SaganPlugin" + } } } @@ -54,6 +58,7 @@ configurations { } dependencies { + implementation 'com.google.code.gson:gson:2.8.6' implementation 'com.thaiopensource:trang:20091111' implementation 'net.sourceforge.saxon:saxon:9.1.0.8' implementation localGroovy() @@ -78,6 +83,7 @@ dependencies { testImplementation 'org.assertj:assertj-core:3.13.2' testImplementation 'org.mockito:mockito-core:3.0.0' testImplementation 'org.spockframework:spock-core:1.3-groovy-2.5' + testImplementation "com.squareup.okhttp3:mockwebserver:3.14.9" } diff --git a/buildSrc/src/main/java/org/springframework/gradle/sagan/Release.java b/buildSrc/src/main/java/org/springframework/gradle/sagan/Release.java new file mode 100644 index 0000000000..5e62c658e0 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/gradle/sagan/Release.java @@ -0,0 +1,123 @@ +/* + * Copyright 2019-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 org.springframework.gradle.sagan; + +import java.util.regex.Pattern; + +/** + * Domain object for creating a new release version. + */ +public class Release { + private String version; + + private ReleaseStatus status; + + private boolean current; + + private String referenceDocUrl; + + private String apiDocUrl; + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public ReleaseStatus getStatus() { + return status; + } + + public void setStatus(ReleaseStatus status) { + this.status = status; + } + + public boolean isCurrent() { + return current; + } + + public void setCurrent(boolean current) { + this.current = current; + } + + public String getReferenceDocUrl() { + return referenceDocUrl; + } + + public void setReferenceDocUrl(String referenceDocUrl) { + this.referenceDocUrl = referenceDocUrl; + } + + public String getApiDocUrl() { + return apiDocUrl; + } + + public void setApiDocUrl(String apiDocUrl) { + this.apiDocUrl = apiDocUrl; + } + + @Override + public String toString() { + return "Release{" + + "version='" + version + '\'' + + ", status=" + status + + ", current=" + current + + ", referenceDocUrl='" + referenceDocUrl + '\'' + + ", apiDocUrl='" + apiDocUrl + '\'' + + '}'; + } + + public enum ReleaseStatus { + /** + * Unstable version with limited support + */ + SNAPSHOT, + /** + * Pre-Release version meant to be tested by the community + */ + PRERELEASE, + /** + * Release Generally Available on public artifact repositories and enjoying full support from maintainers + */ + GENERAL_AVAILABILITY; + + private static final Pattern PRERELEASE_PATTERN = Pattern.compile("[A-Za-z0-9\\.\\-]+?(M|RC)\\d+"); + + private static final String SNAPSHOT_SUFFIX = "SNAPSHOT"; + + /** + * Parse the ReleaseStatus from a String + * @param version a project version + * @return the release status for this version + */ + public static ReleaseStatus parse(String version) { + if (version == null) { + throw new IllegalArgumentException("version cannot be null"); + } + if (version.endsWith(SNAPSHOT_SUFFIX)) { + return SNAPSHOT; + } + if (PRERELEASE_PATTERN.matcher(version).matches()) { + return PRERELEASE; + } + return GENERAL_AVAILABILITY; + } + } + +} diff --git a/buildSrc/src/main/java/org/springframework/gradle/sagan/SaganApi.java b/buildSrc/src/main/java/org/springframework/gradle/sagan/SaganApi.java new file mode 100644 index 0000000000..d40f296c20 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/gradle/sagan/SaganApi.java @@ -0,0 +1,93 @@ +/* + * Copyright 2019-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 org.springframework.gradle.sagan; + +import com.google.gson.Gson; +import okhttp3.*; + +import java.io.IOException; +import java.util.Base64; + +/** + * Implements necessary calls to the Sagan API See https://github.com/spring-io/sagan/blob/master/sagan-site/src/docs/asciidoc/index.adoc + */ +public class SaganApi { + private String baseUrl = "https://spring.io/api"; + + private OkHttpClient client; + private Gson gson = new Gson(); + + public SaganApi(String gitHubToken) { + this.client = new OkHttpClient.Builder() + .addInterceptor(new BasicInterceptor("not-used", gitHubToken)) + .build(); + } + + public void setBaseUrl(String baseUrl) { + this.baseUrl = baseUrl; + } + + public void createReleaseForProject(Release release, String projectName) { + String url = this.baseUrl + "/projects/" + projectName + "/releases"; + String releaseJsonString = gson.toJson(release); + RequestBody body = RequestBody.create(MediaType.parse("application/json"), releaseJsonString); + Request request = new Request.Builder() + .url(url) + .post(body) + .build(); + try { + Response response = this.client.newCall(request).execute(); + if (!response.isSuccessful()) { + throw new RuntimeException("Could not create release " + release + ". Got response " + response); + } + } catch (IOException fail) { + throw new RuntimeException("Could not create release " + release, fail); + } + } + + public void deleteReleaseForProject(String release, String projectName) { + String url = this.baseUrl + "/projects/" + projectName + "/releases/" + release; + Request request = new Request.Builder() + .url(url) + .delete() + .build(); + try { + Response response = this.client.newCall(request).execute(); + if (!response.isSuccessful()) { + throw new RuntimeException("Could not delete release " + release + ". Got response " + response); + } + } catch (IOException fail) { + throw new RuntimeException("Could not delete release " + release, fail); + } + } + + private static class BasicInterceptor implements Interceptor { + + private final String token; + + public BasicInterceptor(String username, String token) { + this.token = Base64.getEncoder().encodeToString((username + ":" + token).getBytes()); + } + + @Override + public okhttp3.Response intercept(Chain chain) throws IOException { + Request request = chain.request().newBuilder() + .addHeader("Authorization", "Basic " + this.token).build(); + return chain.proceed(request); + } + } +} diff --git a/buildSrc/src/main/java/org/springframework/gradle/sagan/SaganCreateReleaseTask.java b/buildSrc/src/main/java/org/springframework/gradle/sagan/SaganCreateReleaseTask.java new file mode 100644 index 0000000000..6592544b1f --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/gradle/sagan/SaganCreateReleaseTask.java @@ -0,0 +1,86 @@ +/* + * Copyright 2019-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 org.springframework.gradle.sagan; + +import org.gradle.api.DefaultTask; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.TaskAction; + +public class SaganCreateReleaseTask extends DefaultTask { + + @Input + private String gitHubAccessToken; + @Input + private String version; + @Input + private String apiDocUrl; + @Input + private String referenceDocUrl; + @Input + private String projectName; + + @TaskAction + public void saganCreateRelease() { + SaganApi sagan = new SaganApi(this.gitHubAccessToken); + Release release = new Release(); + release.setVersion(this.version); + release.setApiDocUrl(this.apiDocUrl); + release.setReferenceDocUrl(this.referenceDocUrl); + sagan.createReleaseForProject(release, this.projectName); + } + + public String getGitHubAccessToken() { + return gitHubAccessToken; + } + + public void setGitHubAccessToken(String gitHubAccessToken) { + this.gitHubAccessToken = gitHubAccessToken; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public String getApiDocUrl() { + return apiDocUrl; + } + + public void setApiDocUrl(String apiDocUrl) { + this.apiDocUrl = apiDocUrl; + } + + public String getReferenceDocUrl() { + return referenceDocUrl; + } + + public void setReferenceDocUrl(String referenceDocUrl) { + this.referenceDocUrl = referenceDocUrl; + } + + public String getProjectName() { + return projectName; + } + + public void setProjectName(String projectName) { + this.projectName = projectName; + } + +} diff --git a/buildSrc/src/main/java/org/springframework/gradle/sagan/SaganDeleteReleaseTask.java b/buildSrc/src/main/java/org/springframework/gradle/sagan/SaganDeleteReleaseTask.java new file mode 100644 index 0000000000..49a3885226 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/gradle/sagan/SaganDeleteReleaseTask.java @@ -0,0 +1,62 @@ +/* + * Copyright 2019-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 org.springframework.gradle.sagan; + +import org.gradle.api.DefaultTask; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.TaskAction; + +public class SaganDeleteReleaseTask extends DefaultTask { + + @Input + private String gitHubAccessToken; + @Input + private String version; + @Input + private String projectName; + + @TaskAction + public void saganCreateRelease() { + SaganApi sagan = new SaganApi(this.gitHubAccessToken); + sagan.deleteReleaseForProject(this.version, this.projectName); + } + + public String getGitHubAccessToken() { + return gitHubAccessToken; + } + + public void setGitHubAccessToken(String gitHubAccessToken) { + this.gitHubAccessToken = gitHubAccessToken; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public String getProjectName() { + return projectName; + } + + public void setProjectName(String projectName) { + this.projectName = projectName; + } + +} diff --git a/buildSrc/src/main/java/org/springframework/gradle/sagan/SaganPlugin.java b/buildSrc/src/main/java/org/springframework/gradle/sagan/SaganPlugin.java new file mode 100644 index 0000000000..388a2d15ba --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/gradle/sagan/SaganPlugin.java @@ -0,0 +1,47 @@ +/* + * Copyright 2019-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 org.springframework.gradle.sagan; + +import io.spring.gradle.convention.Utils; +import org.gradle.api.*; + +public class SaganPlugin implements Plugin { + @Override + public void apply(Project project) { + project.getTasks().register("saganCreateRelease", SaganCreateReleaseTask.class, new Action() { + @Override + public void execute(SaganCreateReleaseTask saganCreateVersion) { + saganCreateVersion.setGroup("Release"); + saganCreateVersion.setDescription("Creates a new version for the specified project on spring.io"); + saganCreateVersion.setVersion((String) project.findProperty("nextVersion")); + saganCreateVersion.setProjectName(Utils.getProjectName(project)); + saganCreateVersion.setGitHubAccessToken((String) project.findProperty("gitHubAccessToken")); + } + }); + project.getTasks().register("saganDeleteRelease", SaganDeleteReleaseTask.class, new Action() { + @Override + public void execute(SaganDeleteReleaseTask saganDeleteVersion) { + saganDeleteVersion.setGroup("Release"); + saganDeleteVersion.setDescription("Delete a version for the specified project on spring.io"); + saganDeleteVersion.setVersion((String) project.findProperty("previousVersion")); + saganDeleteVersion.setProjectName(Utils.getProjectName(project)); + saganDeleteVersion.setGitHubAccessToken((String) project.findProperty("gitHubAccessToken")); + } + }); + } + +} diff --git a/buildSrc/src/test/java/io/spring/gradle/convention/sagan/SaganApiTests.java b/buildSrc/src/test/java/io/spring/gradle/convention/sagan/SaganApiTests.java new file mode 100644 index 0000000000..cb35d2aea4 --- /dev/null +++ b/buildSrc/src/test/java/io/spring/gradle/convention/sagan/SaganApiTests.java @@ -0,0 +1,85 @@ +/* + * Copyright 2019-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.gradle.convention.sagan; + +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.gradle.sagan.Release; +import org.springframework.gradle.sagan.SaganApi; + +import java.nio.charset.Charset; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; + + +public class SaganApiTests { + private MockWebServer server; + + private SaganApi sagan; + + private String baseUrl; + + @Before + public void setup() throws Exception { + this.server = new MockWebServer(); + this.server.start(); + this.sagan = new SaganApi("mock-oauth-token"); + this.baseUrl = this.server.url("/api").toString(); + this.sagan.setBaseUrl(this.baseUrl); + } + + @After + public void cleanup() throws Exception { + this.server.shutdown(); + } + + @Test + public void createWhenValidThenNoException() throws Exception { + this.server.enqueue(new MockResponse()); + Release release = new Release(); + release.setVersion("5.6.0-SNAPSHOT"); + release.setApiDocUrl("https://docs.spring.io/spring-security/site/docs/{version}/api/"); + release.setReferenceDocUrl("https://docs.spring.io/spring-security/site/docs/{version}/reference/html5/"); + this.sagan.createReleaseForProject(release, "spring-security"); + RecordedRequest request = this.server.takeRequest(1, TimeUnit.SECONDS); + assertThat(request.getRequestUrl().toString()).isEqualTo(this.baseUrl + "/projects/spring-security/releases"); + assertThat(request.getMethod()).isEqualToIgnoringCase("post"); + assertThat(request.getHeaders().get("Authorization")).isEqualTo("Basic bm90LXVzZWQ6bW9jay1vYXV0aC10b2tlbg=="); + assertThat(request.getBody().readString(Charset.defaultCharset())).isEqualToIgnoringWhitespace("{\n" + + " \"version\":\"5.6.0-SNAPSHOT\",\n" + + " \"current\":false,\n" + + " \"referenceDocUrl\":\"https://docs.spring.io/spring-security/site/docs/{version}/reference/html5/\",\n" + + " \"apiDocUrl\":\"https://docs.spring.io/spring-security/site/docs/{version}/api/\"\n" + + "}"); + } + + @Test + public void deleteWhenValidThenNoException() throws Exception { + this.server.enqueue(new MockResponse()); + this.sagan.deleteReleaseForProject("5.6.0-SNAPSHOT", "spring-security"); + RecordedRequest request = this.server.takeRequest(1, TimeUnit.SECONDS); + assertThat(request.getRequestUrl().toString()).isEqualTo(this.baseUrl + "/projects/spring-security/releases/5.6.0-SNAPSHOT"); + assertThat(request.getMethod()).isEqualToIgnoringCase("delete"); + assertThat(request.getHeaders().get("Authorization")).isEqualTo("Basic bm90LXVzZWQ6bW9jay1vYXV0aC10b2tlbg=="); + assertThat(request.getBody().readString(Charset.defaultCharset())).isEmpty(); + } +}