From 06f7fa7160a846da890555fdb7d369db3fbc1867 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Sun, 4 Feb 2018 21:53:49 +0100 Subject: [PATCH] Added automatic SC-Guides issue creation; fixes gh-69 --- README.adoc | 1 + .../asciidoc/spring-cloud-release-tools.adoc | 1 + .../cloud/release/internal/Releaser.java | 12 ++ .../release/internal/git/GithubIssues.java | 118 ++++++++++++++++++ .../internal/git/ProjectGitHandler.java | 7 ++ .../internal/git/GithubIssuesTests.java | 106 ++++++++++++++++ .../cloud/release/internal/spring/Tasks.java | 9 +- .../internal/spring/AcceptanceTests.java | 12 ++ 8 files changed, 265 insertions(+), 1 deletion(-) create mode 100644 spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/git/GithubIssues.java create mode 100644 spring-cloud-release-tools-core/src/test/java/org/springframework/cloud/release/internal/git/GithubIssuesTests.java diff --git a/README.adoc b/README.adoc index a6a0f60a..d74cd966 100644 --- a/README.adoc +++ b/README.adoc @@ -41,6 +41,7 @@ why this tool makes it easy to automate the release / dependency update process - Generates a tweet template under `target/tweet.txt` (ONLY FOR NON-SNAPSHOT VERSIONS) - Generates a release notes template under `target/notes.md` (ONLY FOR NON-SNAPSHOT VERSIONS) - Updates project information in Sagan (http://spring.io) (ONLY FOR SNAPSHOT / RELEASE VERSIONS) +- For `GA`/ `SR` release will create an issue in Spring Guides under https://github.com/spring-guides/getting-started-guides/issues/ IMPORTANT: Starting with version that does Sagan integration, you MUST pass the OAuth token, otherwise the application will fail to start diff --git a/docs/src/main/asciidoc/spring-cloud-release-tools.adoc b/docs/src/main/asciidoc/spring-cloud-release-tools.adoc index cdd8a2a8..3cbe3bbb 100644 --- a/docs/src/main/asciidoc/spring-cloud-release-tools.adoc +++ b/docs/src/main/asciidoc/spring-cloud-release-tools.adoc @@ -31,6 +31,7 @@ why this tool makes it easy to automate the release / dependency update process - Generates a tweet template under `target/tweet.txt` (ONLY FOR NON-SNAPSHOT VERSIONS) - Generates a release notes template under `target/notes.md` (ONLY FOR NON-SNAPSHOT VERSIONS) - Updates project information in Sagan (http://spring.io) (ONLY FOR SNAPSHOT / RELEASE VERSIONS) +- For `GA`/ `SR` release will create an issue in Spring Guides under https://github.com/spring-guides/getting-started-guides/issues/ IMPORTANT: Starting with version that does Sagan integration, you MUST pass the OAuth token, otherwise the application will fail to start diff --git a/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/Releaser.java b/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/Releaser.java index cf3c3ae8..39a16b49 100644 --- a/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/Releaser.java +++ b/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/Releaser.java @@ -1,7 +1,9 @@ package org.springframework.cloud.release.internal; import java.io.File; +import java.io.IOException; import java.lang.invoke.MethodHandles; +import java.nio.file.Files; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -132,6 +134,16 @@ public class Releaser { } File blog = this.templateGenerator.blog(projects); log.info("\nSuccessfully created blog template at location [{}]", blog); + + } + + public void updateSpringGuides(ProjectVersion releaseVersion, Projects projects) { + if (!(releaseVersion.isRelease() || releaseVersion.isServiceRelease())) { + log.info("\nWon't updated Spring Guides for a non Release / Service Release version"); + return; + } + this.projectGitHandler.createIssueInSpringGuides(projects, releaseVersion); + log.info("\nSuccessfully updated Spring Guides issues"); } public void createTweet(ProjectVersion releaseVersion) { diff --git a/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/git/GithubIssues.java b/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/git/GithubIssues.java new file mode 100644 index 00000000..d15e7e47 --- /dev/null +++ b/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/git/GithubIssues.java @@ -0,0 +1,118 @@ +package org.springframework.cloud.release.internal.git; + +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.net.URL; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +import com.jcabi.github.Coordinates; +import com.jcabi.github.Github; +import com.jcabi.github.Issue; +import com.jcabi.github.Milestone; +import com.jcabi.github.Repo; +import com.jcabi.github.RtGithub; +import com.jcabi.http.wire.RetryWire; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.cloud.release.internal.ReleaserProperties; +import org.springframework.cloud.release.internal.pom.ProjectVersion; +import org.springframework.cloud.release.internal.pom.Projects; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * @author Marcin Grzejszczak + */ +class GithubIssues { + + private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + private static final String GITHUB_ISSUE_TITLE = "Spring Cloud Release took place"; + + private final Github github; + private final ReleaserProperties properties; + + GithubIssues(ReleaserProperties properties) { + this.github = new RtGithub(new RtGithub( + properties.getGit().getOauthToken()).entry().through(RetryWire.class)); + this.properties = properties; + } + + GithubIssues(Github github, ReleaserProperties properties) { + this.github = github; + this.properties = properties; + } + + void fileIssue(Projects projects, ProjectVersion version) { + Assert.hasText(this.properties.getGit().getOauthToken(), + "You have to pass Github OAuth token for milestone closing to be operational"); + // do this only for RELEASE & SR + String releaseVersion = parsedVersion(); + if (!(version.isRelease() || version.isServiceRelease())) { + log.info("Guide issue creation will occur only for Release or Service Release versions. Your version is [{}]", releaseVersion); + return; + } + Repo springGuides = this.github.repos() + .get(new Coordinates.Simple("spring-guides", "getting-started-guides")); + String issueTitle = StringUtils.capitalize(releaseVersion) + " " + GITHUB_ISSUE_TITLE; + // check if the issue is not already there + boolean issueAlreadyFiled = issueAlreadyFiled(springGuides, issueTitle); + if (issueAlreadyFiled) { + log.info("Issue already filed, will not do that again"); + return; + } + try { + int number = springGuides.issues().create(issueTitle, issueText(projects)) + .number(); + log.info("Successfully created an issue with title [{}] in Spring Guides under: https://github.com/spring-guides/getting-started-guides/issues/" + number, issueTitle); + } + catch (IOException e) { + log.error("Exception occurred while trying to create the issue in guides", e); + } + } + + private String parsedVersion() { + String version = this.properties.getPom().getBranch(); + if (version.startsWith("v")) { + return version.substring(1); + } + return version; + } + + private String issueText(Projects projects) { + StringBuilder builder = new StringBuilder() + .append("Spring Cloud [") + .append(parsedVersion()) + .append("] Released with the following projects:") + .append("\n\n"); + projects.forEach(project -> builder + .append(project.projectName).append(" : ") + .append("`").append(project.version).append("`") + .append("\n")); + return builder.toString(); + } + + private boolean issueAlreadyFiled(Repo springGuides, String issueTitle) { + Map map = new HashMap<>(); + map.put("state", "open"); + int counter = 0; + int maxIssues = 10; + for (Issue issue : springGuides.issues().iterate(map)) { + if (counter >= maxIssues) { + return false; + } + Issue.Smart smartIssue = new Issue.Smart(issue); + try { + if (issueTitle.equals(smartIssue.title())) { + return true; + } + } + catch (IOException e) { + return false; + } + counter = counter + 1; + } + return false; + } +} diff --git a/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/git/ProjectGitHandler.java b/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/git/ProjectGitHandler.java index 7683711f..c1135060 100644 --- a/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/git/ProjectGitHandler.java +++ b/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/git/ProjectGitHandler.java @@ -9,6 +9,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cloud.release.internal.ReleaserProperties; import org.springframework.cloud.release.internal.pom.ProjectVersion; +import org.springframework.cloud.release.internal.pom.Projects; /** * Contains business logic around Git & Github operations @@ -26,10 +27,12 @@ public class ProjectGitHandler { private final ReleaserProperties properties; private final GithubMilestones githubMilestones; + private final GithubIssues githubIssues; public ProjectGitHandler(ReleaserProperties properties) { this.properties = properties; this.githubMilestones = new GithubMilestones(properties); + this.githubIssues = new GithubIssues(properties); } public void commitAndTagIfApplicable(File project, ProjectVersion version) { @@ -90,6 +93,10 @@ public class ProjectGitHandler { this.githubMilestones.closeMilestone(releaseVersion); } + public void createIssueInSpringGuides(Projects projects, ProjectVersion version) { + this.githubIssues.fileIssue(projects, version); + } + public String milestoneUrl(ProjectVersion releaseVersion) { return this.githubMilestones.milestoneUrl(releaseVersion); } diff --git a/spring-cloud-release-tools-core/src/test/java/org/springframework/cloud/release/internal/git/GithubIssuesTests.java b/spring-cloud-release-tools-core/src/test/java/org/springframework/cloud/release/internal/git/GithubIssuesTests.java new file mode 100644 index 00000000..ac1e5cd9 --- /dev/null +++ b/spring-cloud-release-tools-core/src/test/java/org/springframework/cloud/release/internal/git/GithubIssuesTests.java @@ -0,0 +1,106 @@ +package org.springframework.cloud.release.internal.git; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.Collections; +import javax.json.Json; + +import com.jcabi.github.Coordinates; +import com.jcabi.github.Issue; +import com.jcabi.github.Milestone; +import com.jcabi.github.Repo; +import com.jcabi.github.mock.MkGithub; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.springframework.boot.test.rule.OutputCapture; +import org.springframework.cloud.release.internal.ReleaserProperties; +import org.springframework.cloud.release.internal.pom.ProjectVersion; +import org.springframework.cloud.release.internal.pom.Projects; + +import static org.assertj.core.api.BDDAssertions.then; +import static org.assertj.core.api.BDDAssertions.thenThrownBy; + +/** + * @author Marcin Grzejszczak + */ +public class GithubIssuesTests { + + MkGithub github; + Repo repo; + @Rule public TemporaryFolder folder = new TemporaryFolder(); + @Rule public OutputCapture capture = new OutputCapture(); + + @Before + public void setup() throws URISyntaxException, IOException { + this.github = new MkGithub("spring-guides"); + this.repo = createGettingStartedGuides(this.github); + } + + @Test + public void should_not_do_anything_for_non_release_train_version() throws IOException { + GithubIssues issues = new GithubIssues(this.github, withToken()); + + issues.fileIssue(new Projects( + new ProjectVersion("foo", "1.0.0.BUILD-SNAPSHOT") + ), new ProjectVersion("sc-release", "Edgware.BUILD-SNAPSHOT")); + + then(this.capture.toString()).contains("Guide issue creation will occur only"); + } + + @Test + public void should_file_an_issue_for_release_version() throws IOException { + GithubIssues issues = new GithubIssues(this.github, withToken()); + + issues.fileIssue(new Projects( + new ProjectVersion("foo", "1.0.0.RELEASE"), + new ProjectVersion("bar", "2.0.0.RELEASE"), + new ProjectVersion("baz", "3.0.0.RELEASE") + ), new ProjectVersion("sc-release", "Edgware.RELEASE")); + + then(this.capture.toString()).doesNotContain("Guide issue creation will occur only"); + Issue issue = this.github.repos() + .get(new Coordinates.Simple("spring-guides", "getting-started-guides")) + .issues().get(1); + then(issue.exists()).isTrue(); + Issue.Smart smartIssue = new Issue.Smart(issue); + then(smartIssue.title()).isEqualTo("Edgware.RELEASE Spring Cloud Release took place"); + then(smartIssue.body()) + .contains("Spring Cloud [Edgware.RELEASE]") + .contains("foo : `1.0.0.RELEASE`") + .contains("bar : `2.0.0.RELEASE`") + .contains("baz : `3.0.0.RELEASE`"); + } + + @Test + public void should_throw_exception_when_no_token_was_passed() { + GithubIssues issues = new GithubIssues(new ReleaserProperties()); + + thenThrownBy(() -> issues.fileIssue(new Projects(Collections.emptySet()), + nonGaSleuthProject())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("You have to pass Github OAuth token for milestone closing to be operational"); + } + + private Repo createGettingStartedGuides(MkGithub github) throws IOException { + return github.repos().create( + Json.createObjectBuilder().add( + "name", + "getting-started-guides" + ).build() + ); + } + + private ProjectVersion nonGaSleuthProject() { + return new ProjectVersion("spring-cloud-sleuth", "0.2.0.BUILD-SNAPSHOT"); + } + + ReleaserProperties withToken() { + ReleaserProperties properties = new ReleaserProperties(); + properties.getGit().setOauthToken("foo"); + properties.getPom().setBranch("vEdgware.RELEASE"); + return properties; + } +} \ No newline at end of file diff --git a/spring-cloud-release-tools-spring/src/main/java/org/springframework/cloud/release/internal/spring/Tasks.java b/spring-cloud-release-tools-spring/src/main/java/org/springframework/cloud/release/internal/spring/Tasks.java index ca26142a..6b17dbbc 100644 --- a/spring-cloud-release-tools-spring/src/main/java/org/springframework/cloud/release/internal/spring/Tasks.java +++ b/spring-cloud-release-tools-spring/src/main/java/org/springframework/cloud/release/internal/spring/Tasks.java @@ -52,6 +52,12 @@ class Tasks { args.releaser.createTweet(args.versionFromScRelease); args.releaser.createReleaseNotes(args.versionFromScRelease, args.projects); }); + static Task UPDATE_GUIDES = task("updateGuides", "ug", + "UPDATE GUIDES", + "Updating Spring Guides", + args -> { + args.releaser.updateSpringGuides(args.versionFromScRelease, args.projects); + }); static Task UPDATE_SAGAN = task("updateSagan", "g", "UPDATE SAGAN", "Updating Sagan with release info", @@ -69,7 +75,8 @@ class Tasks { Tasks.PUSH, Tasks.CLOSE_MILESTONE, Tasks.CREATE_TEMPLATES, - Tasks.UPDATE_SAGAN + Tasks.UPDATE_SAGAN, + Tasks.UPDATE_GUIDES ).collect(Collectors.toList()); static Task RELEASE = Tasks.task("release", "r", diff --git a/spring-cloud-release-tools-spring/src/test/java/org/springframework/cloud/release/internal/spring/AcceptanceTests.java b/spring-cloud-release-tools-spring/src/test/java/org/springframework/cloud/release/internal/spring/AcceptanceTests.java index 5e222745..1b597276 100644 --- a/spring-cloud-release-tools-spring/src/test/java/org/springframework/cloud/release/internal/spring/AcceptanceTests.java +++ b/spring-cloud-release-tools-spring/src/test/java/org/springframework/cloud/release/internal/spring/AcceptanceTests.java @@ -25,6 +25,7 @@ import org.springframework.cloud.release.internal.git.ProjectGitHandler; import org.springframework.cloud.release.internal.gradle.GradleUpdater; import org.springframework.cloud.release.internal.pom.ProjectPomUpdater; import org.springframework.cloud.release.internal.pom.ProjectVersion; +import org.springframework.cloud.release.internal.pom.Projects; import org.springframework.cloud.release.internal.pom.TestPomReader; import org.springframework.cloud.release.internal.pom.TestUtils; import org.springframework.cloud.release.internal.project.ProjectBuilder; @@ -104,6 +105,7 @@ public class AcceptanceTests { BDDMockito.then(this.saganClient).should(BDDMockito.times(2)).updateRelease(BDDMockito.eq("spring-cloud-consul"), BDDMockito.anyList()); BDDMockito.then(this.saganClient).should().deleteRelease("spring-cloud-consul", "1.1.2.BUILD-SNAPSHOT"); + then(this.gitHandler.issueCreatedInSpringGuides).isTrue(); } // issue #74 @@ -139,6 +141,7 @@ public class AcceptanceTests { BDDMockito.anyList()); BDDMockito.then(this.saganClient).should() .deleteRelease("spring-cloud-build", "1.2.2.BUILD-SNAPSHOT"); + then(this.gitHandler.issueCreatedInSpringGuides).isTrue(); } @Test @@ -178,6 +181,8 @@ public class AcceptanceTests { .contains("- Spring Cloud Bus `1.3.0.M1` ([issues](http://foo.bar.com/1.3.0.M1))"); BDDMockito.then(this.saganClient).should().updateRelease(BDDMockito.eq("spring-cloud-consul"), BDDMockito.anyList()); + // we update guides only for SR / RELEASE + then(this.gitHandler.issueCreatedInSpringGuides).isFalse(); } @Test @@ -209,6 +214,7 @@ public class AcceptanceTests { .contains("- Spring Cloud Bus `1.3.0.M1` ([issues](http://foo.bar.com/1.3.0.M1)"); BDDMockito.then(this.saganClient).should(BDDMockito.never()).updateRelease( BDDMockito.anyString(), BDDMockito.anyList()); + then(this.gitHandler.issueCreatedInSpringGuides).isFalse(); } private Iterable listOfCommits(File project) throws GitAPIException { @@ -349,6 +355,7 @@ public class AcceptanceTests { class TestProjectGitHandler extends ProjectGitHandler { boolean closedMilestones = false; + boolean issueCreatedInSpringGuides = false; final String expectedVersion; final String projectName; @@ -365,6 +372,11 @@ public class AcceptanceTests { this.closedMilestones = true; } + @Override public void createIssueInSpringGuides(Projects projects, + ProjectVersion version) { + this.issueCreatedInSpringGuides = true; + } + @Override public String milestoneUrl(ProjectVersion releaseVersion) { return "http://foo.bar.com/" + releaseVersion.toString(); }