浏览代码

Add sagan(Create|Delete)Release

Closes gh-9577
Rob Winch 4 年之前
父节点
当前提交
1a082357d3

+ 6 - 0
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"

+ 6 - 0
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"
 }
 
 

+ 123 - 0
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;
+		}
+	}
+
+}

+ 93 - 0
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);
+		}
+	}
+}

+ 86 - 0
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;
+	}
+
+}

+ 62 - 0
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;
+	}
+
+}

+ 47 - 0
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<Project> {
+	@Override
+	public void apply(Project project) {
+		project.getTasks().register("saganCreateRelease", SaganCreateReleaseTask.class, new Action<SaganCreateReleaseTask>() {
+			@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<SaganDeleteReleaseTask>() {
+			@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"));
+			}
+		});
+	}
+
+}

+ 85 - 0
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();
+	}
+}