Added automatic SC-Guides issue creation; fixes gh-69

This commit is contained in:
Marcin Grzejszczak
2018-02-04 21:53:49 +01:00
parent 6cfc436eaa
commit 06f7fa7160
8 changed files with 265 additions and 1 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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