From d16d3dee49ddf84902d1846a47dfaa3f5118655b Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Wed, 6 Sep 2017 13:13:19 +0200 Subject: [PATCH] Added updating of release notes with this change a template with links to closed milestones will be created fixes #10 --- README.adoc | 3 + .../asciidoc/spring-cloud-release-tools.adoc | 3 + .../cloud/release/internal/Releaser.java | 27 ++- .../release/internal/ReleaserProperties.java | 13 ++ .../internal/git/GithubMilestones.java | 160 ++++++++++++++++++ .../release/internal/git/MilestoneCloser.java | 93 ---------- ...GitUpdater.java => ProjectGitHandler.java} | 16 +- .../internal/pom/ProjectPomUpdater.java | 6 +- .../template/BlogTemplateGenerator.java | 20 +-- .../ReleaseNotesTemplateGenerator.java | 111 ++++++++++++ .../internal/template/TemplateGenerator.java | 20 ++- .../src/main/resources/templates/notes.hbs | 6 + .../cloud/release/internal/ReleaserTests.java | 12 +- ...rTests.java => GithubMilestonesTests.java} | 115 ++++++++++--- ...Tests.java => ProjectGitHandlerTests.java} | 6 +- .../template/TemplateGeneratorTests.java | 96 ++++++----- .../spring/ReleaserConfiguration.java | 5 +- .../internal/spring/SpringReleaser.java | 1 + .../internal/spring/AcceptanceTests.java | 53 ++++-- 19 files changed, 560 insertions(+), 206 deletions(-) create mode 100644 spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/git/GithubMilestones.java delete mode 100644 spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/git/MilestoneCloser.java rename spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/git/{ProjectGitUpdater.java => ProjectGitHandler.java} (86%) create mode 100644 spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/template/ReleaseNotesTemplateGenerator.java create mode 100644 spring-cloud-release-tools-core/src/main/resources/templates/notes.hbs rename spring-cloud-release-tools-core/src/test/java/org/springframework/cloud/release/internal/git/{MilestoneCloserTests.java => GithubMilestonesTests.java} (54%) rename spring-cloud-release-tools-core/src/test/java/org/springframework/cloud/release/internal/git/{ProjectGitUpdaterTests.java => ProjectGitHandlerTests.java} (95%) diff --git a/README.adoc b/README.adoc index 67ac211e..f4f7f553 100644 --- a/README.adoc +++ b/README.adoc @@ -39,6 +39,7 @@ why this tool makes it easy to automate the release / dependency update process - Generates an email template under `target/email.txt` (ONLY FOR NON-SNAPSHOT VERSIONS) - Generates a blog template under `target/blog.md` (ONLY FOR NON-SNAPSHOT VERSIONS) - 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) === How to run it @@ -100,6 +101,8 @@ the `spring-cloud-cli.version` to `1.0.0.RELEASE` regardless of what was set in - `releaser.git.clone-destination-dir` - Where should the Spring Cloud Release repo get cloned to. If null defaults to a temporary directory - `releaser.git.spring-cloud-release-git-url` - URL to Spring Cloud Release Git repository. Defaults to `https://github.com/spring-cloud/spring-cloud-release` - `releaser.git.oauth-token` - GitHub OAuth token to be used to interact with GitHub repo. +- `releaser.git.number-of-checked-milestones` - In order not to iterate endlessly over milestones we introduce a threshold of milestones that +we will go through to find the matching milestone. Defaults to `10` - `releaser.maven.build-command` - Command to be executed to build the project. Defaults to `./mvnw clean install -Pdocs` - `releaser.maven.deploy-command` - Command to be executed to deploy a built project". Defaults to `./mvnw deploy -DskipTests -Pfast` - `releaser.maven.publish-docs-commands` - Command to be executed to deploy a built project. If present `{{version}}` will be replaced by the proper version. diff --git a/docs/src/main/asciidoc/spring-cloud-release-tools.adoc b/docs/src/main/asciidoc/spring-cloud-release-tools.adoc index 98e33973..fb30bae4 100644 --- a/docs/src/main/asciidoc/spring-cloud-release-tools.adoc +++ b/docs/src/main/asciidoc/spring-cloud-release-tools.adoc @@ -29,6 +29,7 @@ why this tool makes it easy to automate the release / dependency update process - Generates an email template under `target/email.txt` (ONLY FOR NON-SNAPSHOT VERSIONS) - Generates a blog template under `target/blog.md` (ONLY FOR NON-SNAPSHOT VERSIONS) - 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) === How to run it @@ -90,6 +91,8 @@ the `spring-cloud-cli.version` to `1.0.0.RELEASE` regardless of what was set in - `releaser.git.clone-destination-dir` - Where should the Spring Cloud Release repo get cloned to. If null defaults to a temporary directory - `releaser.git.spring-cloud-release-git-url` - URL to Spring Cloud Release Git repository. Defaults to `https://github.com/spring-cloud/spring-cloud-release` - `releaser.git.oauth-token` - GitHub OAuth token to be used to interact with GitHub repo. +- `releaser.git.number-of-checked-milestones` - In order not to iterate endlessly over milestones we introduce a threshold of milestones that +we will go through to find the matching milestone. Defaults to `10` - `releaser.maven.build-command` - Command to be executed to build the project. Defaults to `./mvnw clean install -Pdocs` - `releaser.maven.deploy-command` - Command to be executed to deploy a built project". Defaults to `./mvnw deploy -DskipTests -Pfast` - `releaser.maven.publish-docs-commands` - Command to be executed to deploy a built project. If present `{{version}}` will be replaced by the proper version. 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 4a273a33..3bde6cb3 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 @@ -7,7 +7,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cloud.release.internal.gradle.GradleUpdater; import org.springframework.cloud.release.internal.template.TemplateGenerator; -import org.springframework.cloud.release.internal.git.ProjectGitUpdater; +import org.springframework.cloud.release.internal.git.ProjectGitHandler; import org.springframework.cloud.release.internal.pom.ProjectPomUpdater; import org.springframework.cloud.release.internal.pom.ProjectVersion; import org.springframework.cloud.release.internal.pom.Projects; @@ -21,16 +21,16 @@ public class Releaser { private final ProjectPomUpdater projectPomUpdater; private final ProjectBuilder projectBuilder; - private final ProjectGitUpdater projectGitUpdater; + private final ProjectGitHandler projectGitHandler; private final TemplateGenerator templateGenerator; private final GradleUpdater gradleUpdater; public Releaser(ProjectPomUpdater projectPomUpdater, ProjectBuilder projectBuilder, - ProjectGitUpdater projectGitUpdater, TemplateGenerator templateGenerator, + ProjectGitHandler projectGitHandler, TemplateGenerator templateGenerator, GradleUpdater gradleUpdater) { this.projectPomUpdater = projectPomUpdater; this.projectBuilder = projectBuilder; - this.projectGitUpdater = projectGitUpdater; + this.projectGitHandler = projectGitHandler; this.templateGenerator = templateGenerator; this.gradleUpdater = gradleUpdater; } @@ -53,7 +53,7 @@ public class Releaser { } public void commitAndPushTags(File project, ProjectVersion changedVersion) { - this.projectGitUpdater.commitAndTagIfApplicable(project, changedVersion); + this.projectGitHandler.commitAndTagIfApplicable(project, changedVersion); log.info("\nCommit was made and tag was pushed successfully"); } @@ -72,12 +72,12 @@ public class Releaser { log.info("\nWon't rollback a snapshot version"); return; } - this.projectGitUpdater.revertChangesIfApplicable(project, scReleaseVersion); + this.projectGitHandler.revertChangesIfApplicable(project, scReleaseVersion); ProjectVersion originalVersion = originalVersion(project); log.info("Original project version is [{}]", originalVersion); if ((scReleaseVersion.isRelease() || scReleaseVersion.isServiceRelease()) && originalVersion.isSnapshot()) { this.projectBuilder.bumpVersions(originalVersion.bumpedVersion()); - this.projectGitUpdater.commitAfterBumpingVersions(project, originalVersion); + this.projectGitHandler.commitAfterBumpingVersions(project, originalVersion); log.info("\nSuccessfully reverted the commit and bumped snapshot versions"); } else { log.info("\nSuccessfully reverted the commit and came back to snapshot versions"); @@ -89,7 +89,7 @@ public class Releaser { } public void pushCurrentBranch(File project) { - this.projectGitUpdater.pushCurrentBranch(project); + this.projectGitHandler.pushCurrentBranch(project); log.info("\nSuccessfully pushed current branch"); } @@ -98,7 +98,7 @@ public class Releaser { log.info("\nWon't close a milestone for a SNAPSHOT version"); return; } - this.projectGitUpdater.closeMilestone(releaseVersion); + this.projectGitHandler.closeMilestone(releaseVersion); log.info("\nSuccessfully closed milestone"); } @@ -128,4 +128,13 @@ public class Releaser { File blog = this.templateGenerator.tweet(); log.info("\nSuccessfully created tweet template at location [{}]", blog); } + + public void createReleaseNotes(ProjectVersion releaseVersion, Projects projects) { + if (releaseVersion.isSnapshot()) { + log.info("\nWon't create release notes for a SNAPSHOT version"); + return; + } + File output = this.templateGenerator.releaseNotes(projects); + log.info("\nSuccessfully created release notes at location [{}]", output); + } } diff --git a/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/ReleaserProperties.java b/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/ReleaserProperties.java index 9ae5a21d..7ff7a693 100644 --- a/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/ReleaserProperties.java +++ b/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/ReleaserProperties.java @@ -62,6 +62,12 @@ public class ReleaserProperties { */ private String oauthToken; + /** + * In order not to iterate endlessly over milestones we introduce a threshold of milestones + * that we will go through to find the matching milestone + */ + private Integer numberOfCheckedMilestones = 10; + public String getSpringCloudReleaseGitUrl() { return this.springCloudReleaseGitUrl; } @@ -86,6 +92,13 @@ public class ReleaserProperties { this.oauthToken = oauthToken; } + public Integer getNumberOfCheckedMilestones() { + return this.numberOfCheckedMilestones; + } + + public void setNumberOfCheckedMilestones(Integer numberOfCheckedMilestones) { + this.numberOfCheckedMilestones = numberOfCheckedMilestones; + } } public static class Pom { diff --git a/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/git/GithubMilestones.java b/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/git/GithubMilestones.java new file mode 100644 index 00000000..f9d5c874 --- /dev/null +++ b/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/git/GithubMilestones.java @@ -0,0 +1,160 @@ +package org.springframework.cloud.release.internal.git; + +import com.jcabi.github.Coordinates; +import com.jcabi.github.Github; +import com.jcabi.github.Milestone; +import com.jcabi.github.RtGithub; +import com.jcabi.http.wire.RetryWire; + +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 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.util.Assert; + +/** + * @author Marcin Grzejszczak + */ +class GithubMilestones { + + private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + private final Github github; + private final ReleaserProperties properties; + + GithubMilestones(ReleaserProperties properties) { + this.github = new RtGithub(new RtGithub( + properties.getGit().getOauthToken()).entry().through(RetryWire.class)); + this.properties = properties; + } + + GithubMilestones(Github github, ReleaserProperties properties) { + this.github = github; + this.properties = properties; + } + + void closeMilestone(ProjectVersion version) { + Assert.hasText(this.properties.getGit().getOauthToken(), + "You have to pass Github OAuth token for milestone closing to be operational"); + String tagVersion = version.version; + Milestone.Smart foundMilestone = matchingMilestone(tagVersion, openMilestones(version)); + if (foundMilestone != null) { + try { + log.info("Found a matching milestone - closing it"); + foundMilestone.close(); + log.info("Closed the [{}] milestone", tagVersion); + } + catch (IOException e) { + log.error("Exception occurred while trying to retrieve the milestone", e); + } + } else { + log.warn("No matching milestone was found"); + } + } + + Milestone.Smart matchingMilestone(String tagVersion, + Iterable milestones) { + log.debug("Successfully received list of milestones [{}]", milestones); + log.info("Will try to match against tag version [{}]", tagVersion); + try { + int counter = 0; + for (Milestone milestone : milestones) { + if (counter++ >= this.properties.getGit().getNumberOfCheckedMilestones()) { + log.warn("No matching milestones were found within the provided threshold [{}] of checked milestones", + this.properties.getGit().getNumberOfCheckedMilestones()); + return null; + } + Milestone.Smart smartMilestone = new Milestone.Smart(milestone); + String title = milestoneTitle(smartMilestone); + if (tagVersion.equals(title) || numericVersion(tagVersion) + .equals(title)) { + log.info("Found a matching milestone [{}]", smartMilestone.number()); + return smartMilestone; + } + } + } catch (AssertionError | IOException e) { + log.error("Exception occurred while trying to retrieve the milestone", e); + return null; + } + log.warn("No matching milestones were found"); + return null; + } + + String milestoneUrl(ProjectVersion version) { + Assert.hasText(this.properties.getGit().getOauthToken(), + "You have to pass Github OAuth token for milestone closing to be operational"); + String tagVersion = version.version; + Milestone.Smart foundMilestone = matchingMilestone(tagVersion, closedMilestones(version)); + if (foundMilestone != null) { + try { + URL url = foundMilestoneUrl(foundMilestone); + log.info("Found a matching milestone with issues URL [{}]", url); + return url.toString(); + } + catch (IOException e) { + throw new IllegalStateException(e); + } + catch (Exception e) { + log.error("Exception occurred while trying to find milestone", e); + return ""; + } + } + return ""; + } + + private String numericVersion(String version) { + return version.contains("RELEASE") ? + version.substring(0, version.lastIndexOf(".")) : ""; + } + + String milestoneTitle(Milestone.Smart milestone) throws IOException { + return milestone.title(); + } + + URL foundMilestoneUrl(Milestone.Smart milestone) throws IOException { + return milestone.url(); + } + + private Iterable getMilestones(ProjectVersion version, + Map map) { + try { + return this.github.repos() + .get(new Coordinates.Simple(org(), version.projectName)) + .milestones().iterate(map); + } catch (AssertionError e) { + log.error("Exception occurred while trying to fetch milestones", e); + return new ArrayList<>(); + } + } + + private Iterable openMilestones(ProjectVersion version) { + return getMilestones(version, openMilestones()); + } + + private Iterable closedMilestones(ProjectVersion version) { + return getMilestones(version, closedMilestones()); + } + + String org() { + return "spring-cloud"; + } + + private Map openMilestones() { + Map params = new HashMap<>(); + params.put("state", "open"); + return params; + } + + private Map closedMilestones() { + Map params = new HashMap<>(); + params.put("state", "closed"); + return params; + } +} diff --git a/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/git/MilestoneCloser.java b/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/git/MilestoneCloser.java deleted file mode 100644 index 7b3824ff..00000000 --- a/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/git/MilestoneCloser.java +++ /dev/null @@ -1,93 +0,0 @@ -package org.springframework.cloud.release.internal.git; - -import java.io.IOException; -import java.lang.invoke.MethodHandles; -import java.util.HashMap; -import java.util.Map; - -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.util.Assert; - -import com.jcabi.github.Coordinates; -import com.jcabi.github.Github; -import com.jcabi.github.Milestone; -import com.jcabi.github.RtGithub; -import com.jcabi.http.wire.RetryWire; - -/** - * @author Marcin Grzejszczak - */ -class MilestoneCloser { - - private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - - private final Github github; - private final ReleaserProperties properties; - - MilestoneCloser(ReleaserProperties properties) { - this.github = new RtGithub(new RtGithub( - properties.getGit().getOauthToken()).entry().through(RetryWire.class)); - this.properties = properties; - } - - MilestoneCloser(Github github, ReleaserProperties properties) { - this.github = github; - this.properties = properties; - } - - void closeMilestone(ProjectVersion version) { - Assert.hasText(this.properties.getGit().getOauthToken(), - "You have to pass Github OAuth token for milestone closing to be operational"); - Iterable milestones = milestones(version); - log.info("Successfully received list of milestones"); - String tagVersion = version.version; - log.info("Will try to match against tag version [{}]", tagVersion); - boolean matchingMilestone = false; - for (Milestone milestone : milestones) { - Milestone.Smart smartMilestone = new Milestone.Smart(milestone); - try { - String title = milestoneTitle(smartMilestone); - if (tagVersion.equals(title) || numericVersion(tagVersion).equals(title)) { - log.info("Found a matching milestone - closing it"); - smartMilestone.close(); - matchingMilestone = true; - log.info("Closed the [{}] milestone", tagVersion); - } - } - catch (IOException e) { - throw new IllegalStateException(e); - } - } - if (!matchingMilestone) { - log.warn("No matching milestone was found"); - } - } - - private String numericVersion(String version) { - return version.contains("RELEASE") ? - version.substring(0, version.lastIndexOf(".")) : ""; - } - - String milestoneTitle(Milestone.Smart milestone) throws IOException { - return milestone.title(); - } - - private Iterable milestones(ProjectVersion version) { - return this.github.repos() - .get(new Coordinates.Simple(org(), version.projectName)) - .milestones().iterate(openMilestones()); - } - - String org() { - return "spring-cloud"; - } - - private Map openMilestones() { - Map params = new HashMap<>(); - params.put("state", "open"); - return params; - } -} diff --git a/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/git/ProjectGitUpdater.java b/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/git/ProjectGitHandler.java similarity index 86% rename from spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/git/ProjectGitUpdater.java rename to spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/git/ProjectGitHandler.java index bf9c6cd5..4252b8ff 100644 --- a/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/git/ProjectGitUpdater.java +++ b/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/git/ProjectGitHandler.java @@ -11,11 +11,11 @@ import org.springframework.cloud.release.internal.ReleaserProperties; import org.springframework.cloud.release.internal.pom.ProjectVersion; /** - * Contains business logic around Git operations + * Contains business logic around Git & Github operations * * @author Marcin Grzejszczak */ -public class ProjectGitUpdater { +public class ProjectGitHandler { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); @@ -25,11 +25,11 @@ public class ProjectGitUpdater { private static final String POST_RELEASE_BUMP_MSG = "Bumping versions to %s after release"; private final ReleaserProperties properties; - private final MilestoneCloser milestoneCloser; + private final GithubMilestones githubMilestones; - public ProjectGitUpdater(ReleaserProperties properties) { + public ProjectGitHandler(ReleaserProperties properties) { this.properties = properties; - this.milestoneCloser = new MilestoneCloser(properties); + this.githubMilestones = new GithubMilestones(properties); } public void commitAndTagIfApplicable(File project, ProjectVersion version) { @@ -87,7 +87,11 @@ public class ProjectGitUpdater { } public void closeMilestone(ProjectVersion releaseVersion) { - this.milestoneCloser.closeMilestone(releaseVersion); + this.githubMilestones.closeMilestone(releaseVersion); + } + + public String milestoneUrl(ProjectVersion releaseVersion) { + return this.githubMilestones.milestoneUrl(releaseVersion); } GitRepo gitRepo(File workingDir) { diff --git a/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/pom/ProjectPomUpdater.java b/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/pom/ProjectPomUpdater.java index 14515d9e..9bf1ed46 100644 --- a/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/pom/ProjectPomUpdater.java +++ b/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/pom/ProjectPomUpdater.java @@ -30,7 +30,7 @@ import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cloud.release.internal.ReleaserProperties; -import org.springframework.cloud.release.internal.git.ProjectGitUpdater; +import org.springframework.cloud.release.internal.git.ProjectGitHandler; /** * @author Marcin Grzejszczak @@ -40,12 +40,12 @@ public class ProjectPomUpdater { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); private final ReleaserProperties properties; - private final ProjectGitUpdater gitRepo; + private final ProjectGitHandler gitRepo; private final PomUpdater pomUpdater = new PomUpdater(); public ProjectPomUpdater(ReleaserProperties properties) { this.properties = properties; - this.gitRepo = new ProjectGitUpdater(properties); + this.gitRepo = new ProjectGitHandler(properties); } /** diff --git a/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/template/BlogTemplateGenerator.java b/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/template/BlogTemplateGenerator.java index 22d30e60..d5949bff 100644 --- a/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/template/BlogTemplateGenerator.java +++ b/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/template/BlogTemplateGenerator.java @@ -1,5 +1,8 @@ package org.springframework.cloud.release.internal.template; +import com.github.jknack.handlebars.Template; +import com.google.common.collect.ImmutableMap; + import java.io.File; import java.io.IOException; import java.nio.file.Files; @@ -13,9 +16,6 @@ import java.util.stream.Collectors; import org.springframework.cloud.release.internal.pom.Projects; import org.springframework.util.StringUtils; -import com.github.jknack.handlebars.Template; -import com.google.common.collect.ImmutableMap; - /** * @author Marcin Grzejszczak */ @@ -68,13 +68,13 @@ class BlogTemplateGenerator { } } - private Set fromProjects() { + private Set fromProjects() { return this.projects.stream().map(projectVersion -> { String name = projectVersion.projectName; String version = projectVersion.version; String convertedName = Arrays.stream(name.split("-")).map( StringUtils::capitalize).collect(Collectors.joining(" ")); - return new Tuple(convertedName, version); + return new BlogTuple(convertedName, version); }).collect(Collectors.toSet()); } @@ -111,11 +111,11 @@ class BlogTemplateGenerator { } } -class Tuple { +class BlogTuple { private final String name; private final String version; - Tuple(String name, String version) { + BlogTuple(String name, String version) { this.name = name; this.version = version; } @@ -133,10 +133,10 @@ class Tuple { return true; if (o == null || getClass() != o.getClass()) return false; - Tuple tuple = (Tuple) o; - if (name != null ? !name.equals(tuple.name) : tuple.name != null) + BlogTuple blogTuple = (BlogTuple) o; + if (name != null ? !name.equals(blogTuple.name) : blogTuple.name != null) return false; - return version != null ? version.equals(tuple.version) : tuple.version == null; + return version != null ? version.equals(blogTuple.version) : blogTuple.version == null; } @Override public int hashCode() { diff --git a/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/template/ReleaseNotesTemplateGenerator.java b/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/template/ReleaseNotesTemplateGenerator.java new file mode 100644 index 00000000..6d54dfb1 --- /dev/null +++ b/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/template/ReleaseNotesTemplateGenerator.java @@ -0,0 +1,111 @@ +package org.springframework.cloud.release.internal.template; + +import com.github.jknack.handlebars.Template; +import com.google.common.collect.ImmutableMap; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.Arrays; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import org.springframework.cloud.release.internal.git.ProjectGitHandler; +import org.springframework.cloud.release.internal.pom.Projects; +import org.springframework.util.StringUtils; + +/** + * @author Marcin Grzejszczak + */ +class ReleaseNotesTemplateGenerator { + + private final Template template; + private final String releaseVersion; + private final File blogOutput; + private final Projects projects; + private final ProjectGitHandler handler; + + ReleaseNotesTemplateGenerator(Template template, String releaseVersion, + File blogOutput, Projects projects, ProjectGitHandler handler) { + this.template = template; + this.releaseVersion = releaseVersion; + this.blogOutput = blogOutput; + this.projects = projects; + this.handler = handler; + } + + File releseNotes() { + try { + Map map = ImmutableMap.builder() + .put("date", LocalDate.now().format(DateTimeFormatter.ISO_DATE)) + .put("releaseVersion", this.releaseVersion) + .put("projects", fromProjects()) + .build(); + String blog = this.template.apply(map); + Files.write(this.blogOutput.toPath(), blog.getBytes()); + return this.blogOutput; + } + catch (IOException e) { + throw new IllegalStateException(e); + } + } + + private Set fromProjects() { + return this.projects.stream().filter(projectVersion -> + !projectVersion.projectName.toLowerCase().contains("boot") + ).map(projectVersion -> { + String name = projectVersion.projectName; + String version = projectVersion.version; + String closedMilestoneUrl = this.handler.milestoneUrl(projectVersion); + String convertedName = Arrays.stream(name.split("-")) + .map(StringUtils::capitalize).collect(Collectors.joining(" ")); + return new Notes(convertedName, version, closedMilestoneUrl); + }).collect(Collectors.toSet()); + } +} + +class Notes { + private final String name; + private final String version; + private final String closedMilestoneUrl; + + Notes(String name, String version, String closedMilestoneUrl) { + this.name = name; + this.version = version; + this.closedMilestoneUrl = closedMilestoneUrl; + } + + public String getName() { + return name; + } + + public String getVersion() { + return version; + } + + public String getClosedMilestoneUrl() { + return closedMilestoneUrl; + } + + @Override public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + Notes notes = (Notes) o; + if (name != null ? !name.equals(notes.name) : notes.name != null) + return false; + return version != null ? + version.equals(notes.version) : + notes.version == null; + } + + @Override public int hashCode() { + int result = name != null ? name.hashCode() : 0; + result = 31 * result + (version != null ? version.hashCode() : 0); + return result; + } +} diff --git a/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/template/TemplateGenerator.java b/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/template/TemplateGenerator.java index 5162bbc8..9475f151 100644 --- a/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/template/TemplateGenerator.java +++ b/spring-cloud-release-tools-core/src/main/java/org/springframework/cloud/release/internal/template/TemplateGenerator.java @@ -9,6 +9,7 @@ import java.io.File; import java.io.IOException; import org.springframework.cloud.release.internal.ReleaserProperties; +import org.springframework.cloud.release.internal.git.ProjectGitHandler; import org.springframework.cloud.release.internal.pom.Projects; /** @@ -19,23 +20,30 @@ public class TemplateGenerator { private static final String EMAIL_TEMPLATE = "email"; private static final String BLOG_TEMPLATE = "blog"; private static final String TWITTER_TEMPLATE = "tweet"; + private static final String RELEASE_NOTES_TEMPLATE = "notes"; private final File emailOutput; private final File blogOutput; private final File tweetOutput; + private final File releaseNotesOutput; private final ReleaserProperties props; + private final ProjectGitHandler handler; - public TemplateGenerator(ReleaserProperties props) { + public TemplateGenerator(ReleaserProperties props, ProjectGitHandler handler) { this.props = props; + this.handler = handler; this.emailOutput = new File("target/email.txt"); this.blogOutput = new File("target/blog.md"); this.tweetOutput = new File("target/tweet.txt"); + this.releaseNotesOutput = new File("target/notes.md"); } - TemplateGenerator(ReleaserProperties props, File output) { + TemplateGenerator(ReleaserProperties props, File output, ProjectGitHandler handler) { this.props = props; this.emailOutput = output; this.blogOutput = output; this.tweetOutput = output; + this.releaseNotesOutput = output; + this.handler = handler; } public File email() { @@ -75,6 +83,14 @@ public class TemplateGenerator { return new TwitterTemplateGenerator(template, releaseVersion, output).tweet(); } + public File releaseNotes(Projects projects) { + File output = file(this.releaseNotesOutput); + String releaseVersion = parsedVersion(); + Template template = template(RELEASE_NOTES_TEMPLATE); + return new ReleaseNotesTemplateGenerator(template, releaseVersion, + output, projects, this.handler).releseNotes(); + } + private String parsedVersion() { String version = this.props.getPom().getBranch(); if (version.startsWith("v")) { diff --git a/spring-cloud-release-tools-core/src/main/resources/templates/notes.hbs b/spring-cloud-release-tools-core/src/main/resources/templates/notes.hbs new file mode 100644 index 00000000..3304b909 --- /dev/null +++ b/spring-cloud-release-tools-core/src/main/resources/templates/notes.hbs @@ -0,0 +1,6 @@ +# {{ releaseVersion }} + +{{ date }} + +{{#each projects}} - {{name}} `{{version}}` ([issues]({{ closedMilestoneUrl }})) +{{/each}} \ No newline at end of file diff --git a/spring-cloud-release-tools-core/src/test/java/org/springframework/cloud/release/internal/ReleaserTests.java b/spring-cloud-release-tools-core/src/test/java/org/springframework/cloud/release/internal/ReleaserTests.java index adcc783d..7983c87b 100644 --- a/spring-cloud-release-tools-core/src/test/java/org/springframework/cloud/release/internal/ReleaserTests.java +++ b/spring-cloud-release-tools-core/src/test/java/org/springframework/cloud/release/internal/ReleaserTests.java @@ -10,7 +10,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; -import org.springframework.cloud.release.internal.git.ProjectGitUpdater; +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; @@ -30,7 +30,7 @@ public class ReleaserTests { @Mock ProjectPomUpdater projectPomUpdater; @Mock ProjectBuilder projectBuilder; - @Mock ProjectGitUpdater projectGitUpdater; + @Mock ProjectGitHandler projectGitHandler; @Mock TemplateGenerator templateGenerator; @Mock GradleUpdater gradleUpdater; File pom; @@ -43,7 +43,7 @@ public class ReleaserTests { Releaser releaser(Supplier originalVersionSupplier) { return new Releaser(this.projectPomUpdater, this.projectBuilder, - this.projectGitUpdater, this.templateGenerator, this.gradleUpdater) { + this.projectGitHandler, this.templateGenerator, this.gradleUpdater) { @Override ProjectVersion originalVersion(File project) { return originalVersionSupplier.get(); } @@ -52,7 +52,7 @@ public class ReleaserTests { Releaser releaser() { return new Releaser(this.projectPomUpdater, this.projectBuilder, - this.projectGitUpdater, this.templateGenerator, this.gradleUpdater); + this.projectGitHandler, this.templateGenerator, this.gradleUpdater); } @Test @@ -100,7 +100,7 @@ public class ReleaserTests { public void should_not_close_milestone_for_snapshots() throws Exception { releaser().closeMilestone(new ProjectVersion("original", "1.0.0.BUILD-SNAPSHOT")); - then(this.projectGitUpdater).should(never()).closeMilestone(any(ProjectVersion.class)); + then(this.projectGitHandler).should(never()).closeMilestone(any(ProjectVersion.class)); } @Test @@ -108,7 +108,7 @@ public class ReleaserTests { releaser(() -> new ProjectVersion("original", "1.0.0.BUILD-SNAPSHOT")) .rollbackReleaseVersion(null, new ProjectVersion("original", "1.0.0.BUILD-SNAPSHOT")); - then(this.projectGitUpdater).should(never()).revertChangesIfApplicable(any(File.class), any(ProjectVersion.class)); + then(this.projectGitHandler).should(never()).revertChangesIfApplicable(any(File.class), any(ProjectVersion.class)); } } \ No newline at end of file diff --git a/spring-cloud-release-tools-core/src/test/java/org/springframework/cloud/release/internal/git/MilestoneCloserTests.java b/spring-cloud-release-tools-core/src/test/java/org/springframework/cloud/release/internal/git/GithubMilestonesTests.java similarity index 54% rename from spring-cloud-release-tools-core/src/test/java/org/springframework/cloud/release/internal/git/MilestoneCloserTests.java rename to spring-cloud-release-tools-core/src/test/java/org/springframework/cloud/release/internal/git/GithubMilestonesTests.java index 1b8e67a9..2d19d2ba 100644 --- a/spring-cloud-release-tools-core/src/test/java/org/springframework/cloud/release/internal/git/MilestoneCloserTests.java +++ b/spring-cloud-release-tools-core/src/test/java/org/springframework/cloud/release/internal/git/GithubMilestonesTests.java @@ -1,30 +1,33 @@ package org.springframework.cloud.release.internal.git; +import com.jcabi.github.Milestone; +import com.jcabi.github.Repo; +import com.jcabi.github.mock.MkGithub; + import java.io.IOException; import java.net.URISyntaxException; +import java.net.URL; import javax.json.Json; 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 com.jcabi.github.Milestone; -import com.jcabi.github.Repo; -import com.jcabi.github.mock.MkGithub; - import static org.assertj.core.api.BDDAssertions.then; import static org.assertj.core.api.BDDAssertions.thenThrownBy; /** * @author Marcin Grzejszczak */ -public class MilestoneCloserTests { +public class GithubMilestonesTests { MkGithub github; Repo repo; + @Rule public TemporaryFolder folder = new TemporaryFolder(); @Rule public OutputCapture capture = new OutputCapture(); @Before @@ -35,7 +38,7 @@ public class MilestoneCloserTests { @Test public void should_close_milestone_if_there_is_one() throws IOException { - MilestoneCloser closer = new MilestoneCloser(this.github, withToken()) { + GithubMilestones milestones = new GithubMilestones(this.github, withToken()) { @Override String org() { return repo.coordinates().user(); } @@ -47,14 +50,14 @@ public class MilestoneCloserTests { }; repo.milestones().create("0.2.0.BUILD-SNAPSHOT"); - closer.closeMilestone(nonGaSleuthProject()); + milestones.closeMilestone(nonGaSleuthProject()); then(this.capture.toString()).doesNotContain("No matching milestone was found"); } @Test public void should_close_milestone_when_the_milestone_contains_numeric_version_only_and_version_is_ga() throws IOException { - MilestoneCloser closer = new MilestoneCloser(this.github, withToken()) { + GithubMilestones milestones = new GithubMilestones(this.github, withToken()) { @Override String org() { return repo.coordinates().user(); } @@ -66,7 +69,7 @@ public class MilestoneCloserTests { }; repo.milestones().create("0.2.0"); - closer.closeMilestone(gaSleuthProject()); + milestones.closeMilestone(gaSleuthProject()); then(this.capture.toString()).doesNotContain("No matching milestone was found"); } @@ -77,7 +80,7 @@ public class MilestoneCloserTests { @Test public void should_not_close_milestone_when_the_milestone_contains_numeric_version_only() throws IOException { - MilestoneCloser closer = new MilestoneCloser(this.github, withToken()) { + GithubMilestones milestones = new GithubMilestones(this.github, withToken()) { @Override String org() { return repo.coordinates().user(); } @@ -89,18 +92,84 @@ public class MilestoneCloserTests { }; repo.milestones().create("0.2.0"); - closer.closeMilestone(nonGaSleuthProject()); + milestones.closeMilestone(nonGaSleuthProject()); then(this.capture.toString()).contains("No matching milestone was found"); } + @Test + public void should_fetch_url_of_a_closed_matching_milestone() throws IOException { + GithubMilestones milestones = new GithubMilestones(this.github, withToken()) { + @Override String org() { + return repo.coordinates().user(); + } + + @Override String milestoneTitle(Milestone.Smart milestone) + throws IOException { + return "0.2.0.RELEASE"; + } + + @Override URL foundMilestoneUrl(Milestone.Smart milestone) + throws IOException { + return new URL("http://foo.com/bar"); + } + }; + repo.milestones().create("0.2.0.RELEASE"); + + String url = milestones.milestoneUrl(gaSleuthProject()); + + then(url).isEqualTo("http://foo.com/bar"); + } + + @Test + public void should_return_null_if_no_matching_milestone_was_found() throws IOException { + GithubMilestones milestones = new GithubMilestones(this.github, withToken()) { + @Override String org() { + return repo.coordinates().user(); + } + + @Override String milestoneTitle(Milestone.Smart milestone) + throws IOException { + return "0.9.0.RELEASE"; + } + + @Override URL foundMilestoneUrl(Milestone.Smart milestone) + throws IOException { + return new URL("http://foo.com/bar"); + } + }; + + String url = milestones.milestoneUrl(gaSleuthProject()); + + then(url).isEmpty(); + } + + @Test + public void should_return_null_if_no_matching_milestone_was_found_within_threshold() throws IOException { + GithubMilestones milestones = new GithubMilestones(this.github, withThreshold()) { + @Override String org() { + return repo.coordinates().user(); + } + + @Override String milestoneTitle(Milestone.Smart milestone) + throws IOException { + return "0.2.0"; + } + }; + repo.milestones().create("0.2.0"); + + milestones.closeMilestone(gaSleuthProject()); + + then(this.capture.toString()).contains("No matching milestones were found within the provided threshold [0]"); + } + private ProjectVersion nonGaSleuthProject() { return new ProjectVersion("spring-cloud-sleuth", "0.2.0.BUILD-SNAPSHOT"); } @Test public void should_throw_exception_when_there_is_no_matching_milestone() throws IOException { - MilestoneCloser closer = new MilestoneCloser(this.github, withToken()) { + GithubMilestones milestones = new GithubMilestones(this.github, withToken()) { @Override String org() { return repo.coordinates().user(); } @@ -112,13 +181,13 @@ public class MilestoneCloserTests { }; repo.milestones().create("v0.2.0.BUILD-SNAPSHOT"); - closer.closeMilestone(nonGaSleuthProject()); + milestones.closeMilestone(nonGaSleuthProject()); then(this.capture.toString()).contains("No matching milestone was found"); } @Test - public void should_throw_exception_when_io_problems_occurred() throws IOException { - MilestoneCloser closer = new MilestoneCloser(this.github, withToken()) { + public void should_print_that_no_milestones_were_found_when_io_problems_occurred() throws IOException { + GithubMilestones milestones = new GithubMilestones(this.github, withToken()) { @Override String org() { return repo.coordinates().user(); } @@ -130,9 +199,9 @@ public class MilestoneCloserTests { }; repo.milestones().create("v0.2.0.BUILD-SNAPSHOT"); - thenThrownBy(() -> closer.closeMilestone(nonGaSleuthProject())) - .isInstanceOf(IllegalStateException.class) - .hasMessageContaining("foo"); + milestones.closeMilestone(nonGaSleuthProject()); + + then(this.capture.toString()).contains("No matching milestone was found"); } private Repo createSleuthRepo(MkGithub github) throws IOException { @@ -146,9 +215,9 @@ public class MilestoneCloserTests { @Test public void should_throw_exception_when_no_token_was_passed() { - MilestoneCloser closer = new MilestoneCloser(new ReleaserProperties()); + GithubMilestones milestones = new GithubMilestones(new ReleaserProperties()); - thenThrownBy(() -> closer.closeMilestone(nonGaSleuthProject())) + thenThrownBy(() -> milestones.closeMilestone(nonGaSleuthProject())) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("You have to pass Github OAuth token for milestone closing to be operational"); } @@ -158,4 +227,10 @@ public class MilestoneCloserTests { properties.getGit().setOauthToken("foo"); return properties; } + + ReleaserProperties withThreshold() { + ReleaserProperties properties = withToken(); + properties.getGit().setNumberOfCheckedMilestones(0); + return properties; + } } \ No newline at end of file diff --git a/spring-cloud-release-tools-core/src/test/java/org/springframework/cloud/release/internal/git/ProjectGitUpdaterTests.java b/spring-cloud-release-tools-core/src/test/java/org/springframework/cloud/release/internal/git/ProjectGitHandlerTests.java similarity index 95% rename from spring-cloud-release-tools-core/src/test/java/org/springframework/cloud/release/internal/git/ProjectGitUpdaterTests.java rename to spring-cloud-release-tools-core/src/test/java/org/springframework/cloud/release/internal/git/ProjectGitHandlerTests.java index 930f1a01..144d7854 100644 --- a/spring-cloud-release-tools-core/src/test/java/org/springframework/cloud/release/internal/git/ProjectGitUpdaterTests.java +++ b/spring-cloud-release-tools-core/src/test/java/org/springframework/cloud/release/internal/git/ProjectGitHandlerTests.java @@ -19,12 +19,12 @@ import static org.mockito.Mockito.never; * @author Marcin Grzejszczak */ @RunWith(MockitoJUnitRunner.class) -public class ProjectGitUpdaterTests { +public class ProjectGitHandlerTests { @Mock GitRepo gitRepo; - ProjectGitUpdater updater = new ProjectGitUpdater(new ReleaserProperties()) { + ProjectGitHandler updater = new ProjectGitHandler(new ReleaserProperties()) { @Override GitRepo gitRepo(File workingDir) { - return ProjectGitUpdaterTests.this.gitRepo; + return ProjectGitHandlerTests.this.gitRepo; } }; File file = new File(""); diff --git a/spring-cloud-release-tools-core/src/test/java/org/springframework/cloud/release/internal/template/TemplateGeneratorTests.java b/spring-cloud-release-tools-core/src/test/java/org/springframework/cloud/release/internal/template/TemplateGeneratorTests.java index 916219f9..dc6474bb 100644 --- a/spring-cloud-release-tools-core/src/test/java/org/springframework/cloud/release/internal/template/TemplateGeneratorTests.java +++ b/spring-cloud-release-tools-core/src/test/java/org/springframework/cloud/release/internal/template/TemplateGeneratorTests.java @@ -7,6 +7,7 @@ import java.util.HashSet; import org.junit.Test; import org.springframework.cloud.release.internal.ReleaserProperties; +import org.springframework.cloud.release.internal.git.ProjectGitHandler; import org.springframework.cloud.release.internal.pom.ProjectVersion; import org.springframework.cloud.release.internal.pom.Projects; @@ -17,52 +18,51 @@ import static org.assertj.core.api.BDDAssertions.then; */ public class TemplateGeneratorTests { + ReleaserProperties props = new ReleaserProperties(); + ProjectGitHandler handler = new ProjectGitHandler(this.props); + @Test public void should_generate_email_from_template_for_tag_with_v_prefix() { - ReleaserProperties props = new ReleaserProperties(); - props.getPom().setBranch("vDalston.RELEASE"); + this.props.getPom().setBranch("vDalston.RELEASE"); - File generatedMail = new TemplateGenerator(props).email(); + File generatedMail = new TemplateGenerator(this.props, this.handler).email(); then(generatedMail).hasContent(expectedEmail()); } @Test public void should_generate_email_from_template_when_output_folder_is_missing() { - ReleaserProperties props = new ReleaserProperties(); - props.getPom().setBranch("vDalston.RELEASE"); + this.props.getPom().setBranch("vDalston.RELEASE"); - File generatedMail = new TemplateGenerator(props, new File("target/foo/bar/baz/template.txt")).email(); + File generatedMail = new TemplateGenerator(this.props, new File("target/foo/bar/baz/template.txt"), + handler).email(); then(generatedMail).hasContent(expectedEmail()); } @Test public void should_generate_email_from_template_for_tag_without_v_prefix() { - ReleaserProperties props = new ReleaserProperties(); - props.getPom().setBranch("Dalston.RELEASE"); + this.props.getPom().setBranch("Dalston.RELEASE"); - File generatedMail = new TemplateGenerator(props).email(); + File generatedMail = new TemplateGenerator(this.props, this.handler).email(); then(generatedMail).hasContent(expectedEmail()); } @Test public void should_generate_tweet_from_template_for_tag_with_v_prefix() { - ReleaserProperties props = new ReleaserProperties(); - props.getPom().setBranch("vDalston.RELEASE"); + this.props.getPom().setBranch("vDalston.RELEASE"); - File generatedTweet = new TemplateGenerator(props).tweet(); + File generatedTweet = new TemplateGenerator(this.props, this.handler).tweet(); then(generatedTweet).hasContent(expectedTweet()); } @Test public void should_generate_tweet_from_template_for_tag_without_v_prefix() { - ReleaserProperties props = new ReleaserProperties(); - props.getPom().setBranch("Dalston.RELEASE"); + this.props.getPom().setBranch("Dalston.RELEASE"); - File generatedTweet = new TemplateGenerator(props).tweet(); + File generatedTweet = new TemplateGenerator(this.props, this.handler).tweet(); then(generatedTweet).hasContent(expectedTweet()); } @@ -70,8 +70,7 @@ public class TemplateGeneratorTests { @Test public void should_generate_blog_from_template_for_tag_with_v_prefix_release() throws IOException { - ReleaserProperties props = new ReleaserProperties(); - props.getPom().setBranch("vDalston.RELEASE"); + this.props.getPom().setBranch("vDalston.RELEASE"); Projects projects = new Projects( new HashSet() {{ add(new ProjectVersion("spring-cloud-sleuth", "1.0.0.RELEASE")); @@ -79,7 +78,7 @@ public class TemplateGeneratorTests { }} ); - File generatedBlog = new TemplateGenerator(props).blog(projects); + File generatedBlog = new TemplateGenerator(this.props, this.handler).blog(projects); then(content(generatedBlog)) .contains("General Availability (RELEASE) of the [Spring Cloud Dalston]") @@ -93,8 +92,7 @@ public class TemplateGeneratorTests { @Test public void should_generate_blog_from_template_for_tag_without_v_prefix_release() throws IOException { - ReleaserProperties props = new ReleaserProperties(); - props.getPom().setBranch("Dalston.RELEASE"); + this.props.getPom().setBranch("Dalston.RELEASE"); Projects projects = new Projects( new HashSet() {{ add(new ProjectVersion("spring-cloud-sleuth", "1.0.0.RELEASE")); @@ -102,7 +100,7 @@ public class TemplateGeneratorTests { }} ); - File generatedBlog = new TemplateGenerator(props).blog(projects); + File generatedBlog = new TemplateGenerator(this.props, this.handler).blog(projects); then(content(generatedBlog)) .contains("General Availability (RELEASE) of the [Spring Cloud Dalston]") @@ -116,8 +114,7 @@ public class TemplateGeneratorTests { @Test public void should_generate_sr_blog_from_template_for_tag_with_v_prefix_release() throws IOException { - ReleaserProperties props = new ReleaserProperties(); - props.getPom().setBranch("vDalston.SR1"); + this.props.getPom().setBranch("vDalston.SR1"); Projects projects = new Projects( new HashSet() {{ add(new ProjectVersion("spring-cloud-sleuth", "1.0.0.RELEASE")); @@ -125,7 +122,7 @@ public class TemplateGeneratorTests { }} ); - File generatedBlog = new TemplateGenerator(props).blog(projects); + File generatedBlog = new TemplateGenerator(this.props, this.handler).blog(projects); then(content(generatedBlog)) .contains("Service Release 1 (SR1) of the [Spring Cloud Dalston]") @@ -139,8 +136,7 @@ public class TemplateGeneratorTests { @Test public void should_generate_sr_blog_from_template_for_tag_without_v_prefix_release() throws IOException { - ReleaserProperties props = new ReleaserProperties(); - props.getPom().setBranch("Dalston.SR1"); + this.props.getPom().setBranch("Dalston.SR1"); Projects projects = new Projects( new HashSet() {{ add(new ProjectVersion("spring-cloud-sleuth", "1.0.0.RELEASE")); @@ -148,7 +144,7 @@ public class TemplateGeneratorTests { }} ); - File generatedBlog = new TemplateGenerator(props).blog(projects); + File generatedBlog = new TemplateGenerator(this.props, this.handler).blog(projects); then(content(generatedBlog)) .contains("Service Release 1 (SR1) of the [Spring Cloud Dalston]") @@ -162,8 +158,7 @@ public class TemplateGeneratorTests { @Test public void should_generate_milestone_blog_from_template_for_tag_with_v_prefix_release() throws IOException { - ReleaserProperties props = new ReleaserProperties(); - props.getPom().setBranch("vDalston.M1"); + this.props.getPom().setBranch("vDalston.M1"); Projects projects = new Projects( new HashSet() {{ add(new ProjectVersion("spring-cloud-sleuth", "1.0.0.M1")); @@ -171,7 +166,7 @@ public class TemplateGeneratorTests { }} ); - File generatedBlog = new TemplateGenerator(props).blog(projects); + File generatedBlog = new TemplateGenerator(this.props, this.handler).blog(projects); then(content(generatedBlog)) .contains("Milestone 1 (M1) of the [Spring Cloud Dalston]") @@ -187,8 +182,7 @@ public class TemplateGeneratorTests { @Test public void should_generate_milestone_blog_from_template_for_tag_without_v_prefix_release() throws IOException { - ReleaserProperties props = new ReleaserProperties(); - props.getPom().setBranch("Dalston.M1"); + this.props.getPom().setBranch("Dalston.M1"); Projects projects = new Projects( new HashSet() {{ add(new ProjectVersion("spring-cloud-sleuth", "1.0.0.M1")); @@ -196,7 +190,7 @@ public class TemplateGeneratorTests { }} ); - File generatedBlog = new TemplateGenerator(props).blog(projects); + File generatedBlog = new TemplateGenerator(this.props, this.handler).blog(projects); then(content(generatedBlog)) .contains("Milestone 1 (M1) of the [Spring Cloud Dalston]") @@ -212,8 +206,7 @@ public class TemplateGeneratorTests { @Test public void should_generate_rc_blog_from_template_for_tag_with_v_prefix_release() throws IOException { - ReleaserProperties props = new ReleaserProperties(); - props.getPom().setBranch("vDalston.RC1"); + this.props.getPom().setBranch("vDalston.RC1"); Projects projects = new Projects( new HashSet() {{ add(new ProjectVersion("spring-cloud-sleuth", "1.0.0.RC1")); @@ -221,7 +214,7 @@ public class TemplateGeneratorTests { }} ); - File generatedBlog = new TemplateGenerator(props).blog(projects); + File generatedBlog = new TemplateGenerator(this.props, this.handler).blog(projects); then(content(generatedBlog)) .contains("Release Candidate 1 (RC1) of the [Spring Cloud Dalston]") @@ -237,8 +230,7 @@ public class TemplateGeneratorTests { @Test public void should_generate_rc_blog_from_template_for_tag_without_v_prefix_release() throws IOException { - ReleaserProperties props = new ReleaserProperties(); - props.getPom().setBranch("Dalston.RC1"); + this.props.getPom().setBranch("Dalston.RC1"); Projects projects = new Projects( new HashSet() {{ add(new ProjectVersion("spring-cloud-sleuth", "1.0.0.RC1")); @@ -246,7 +238,7 @@ public class TemplateGeneratorTests { }} ); - File generatedBlog = new TemplateGenerator(props).blog(projects); + File generatedBlog = new TemplateGenerator(this.props, this.handler).blog(projects); then(content(generatedBlog)) .contains("Release Candidate 1 (RC1) of the [Spring Cloud Dalston]") @@ -259,6 +251,32 @@ public class TemplateGeneratorTests { .contains("mavenBom 'org.springframework.cloud:spring-cloud-dependencies:Dalston.RC1'"); } + @Test + public void should_generate_release_notes_template_when_url_exists() + throws IOException { + ProjectGitHandler handler = new ProjectGitHandler(this.props) { + @Override public String milestoneUrl(ProjectVersion releaseVersion) { + return "http://foo.bar.com"; + } + }; + this.props.getPom().setBranch("Dalston.RC1"); + Projects projects = new Projects( + new HashSet() {{ + add(new ProjectVersion("spring-cloud-sleuth", "1.0.0.RC1")); + add(new ProjectVersion("spring-cloud-consul", "1.0.1.RC1")); + add(new ProjectVersion("spring-boot-dependencies", "1.0.1.RC1")); + }} + ); + + File generatedOutput = new TemplateGenerator(this.props, handler).releaseNotes(projects); + + then(content(generatedOutput)) + .contains("# Dalston.RC1") + .contains("Spring Cloud Sleuth `1.0.0.RC1` ([issues](http://foo.bar.com))") + .contains("Spring Cloud Consul `1.0.1.RC1` ([issues](http://foo.bar.com))") + .doesNotContain("Boot"); + } + private String content(File file) throws IOException { return new String(Files.readAllBytes(file.toPath())); } diff --git a/spring-cloud-release-tools-spring/src/main/java/org/springframework/cloud/release/internal/spring/ReleaserConfiguration.java b/spring-cloud-release-tools-spring/src/main/java/org/springframework/cloud/release/internal/spring/ReleaserConfiguration.java index 3aa38bfa..cfd50d70 100644 --- a/spring-cloud-release-tools-spring/src/main/java/org/springframework/cloud/release/internal/spring/ReleaserConfiguration.java +++ b/spring-cloud-release-tools-spring/src/main/java/org/springframework/cloud/release/internal/spring/ReleaserConfiguration.java @@ -21,7 +21,7 @@ import org.springframework.cloud.release.internal.ReleaserProperties; import org.springframework.cloud.release.internal.gradle.GradleUpdater; import org.springframework.cloud.release.internal.template.TemplateGenerator; import org.springframework.cloud.release.internal.project.ProjectBuilder; -import org.springframework.cloud.release.internal.git.ProjectGitUpdater; +import org.springframework.cloud.release.internal.git.ProjectGitHandler; import org.springframework.cloud.release.internal.pom.ProjectPomUpdater; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -32,8 +32,9 @@ class ReleaserConfiguration { @Bean SpringReleaser releaser(ReleaserProperties properties) { ProjectPomUpdater pomUpdater = new ProjectPomUpdater(properties); + ProjectGitHandler handler = new ProjectGitHandler(properties); return new SpringReleaser(new Releaser(pomUpdater, new ProjectBuilder(properties, pomUpdater), - new ProjectGitUpdater(properties), new TemplateGenerator(properties), + handler, new TemplateGenerator(properties, handler), new GradleUpdater(properties)), properties); } } diff --git a/spring-cloud-release-tools-spring/src/main/java/org/springframework/cloud/release/internal/spring/SpringReleaser.java b/spring-cloud-release-tools-spring/src/main/java/org/springframework/cloud/release/internal/spring/SpringReleaser.java index fe812048..a830bb8b 100644 --- a/spring-cloud-release-tools-spring/src/main/java/org/springframework/cloud/release/internal/spring/SpringReleaser.java +++ b/spring-cloud-release-tools-spring/src/main/java/org/springframework/cloud/release/internal/spring/SpringReleaser.java @@ -63,6 +63,7 @@ public class SpringReleaser { args.releaser.createEmail(args.versionFromScRelease); args.releaser.createBlog(args.versionFromScRelease, args.projects); args.releaser.createTweet(args.versionFromScRelease); + args.releaser.createReleaseNotes(args.versionFromScRelease, args.projects); }) ).collect(Collectors.toList()); 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 df89fe8e..cf6786d4 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 @@ -17,7 +17,7 @@ import org.junit.rules.TemporaryFolder; import org.springframework.cloud.release.internal.Releaser; import org.springframework.cloud.release.internal.ReleaserProperties; import org.springframework.cloud.release.internal.git.GitTestUtils; -import org.springframework.cloud.release.internal.git.ProjectGitUpdater; +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; @@ -38,7 +38,7 @@ public class AcceptanceTests { TestPomReader testPomReader = new TestPomReader(); File springCloudConsulProject; File temporaryFolder; - TestProjectGitUpdater gitUpdater; + TestProjectGitHandler gitHandler; @Before public void setup() throws Exception { @@ -80,7 +80,7 @@ public class AcceptanceTests { commitIsPresent(iterator, "Update SNAPSHOT to 1.1.2.RELEASE"); pomVersionIsEqualTo(project, "1.2.1.BUILD-SNAPSHOT"); pomParentVersionIsEqualTo(project, "1.2.1.BUILD-SNAPSHOT"); - then(this.gitUpdater.executed).isTrue(); + then(this.gitHandler.closedMilestones).isTrue(); then(emailTemplate()).exists(); then(emailTemplateContents()) .contains("Spring Cloud Camden.SR5 available") @@ -88,6 +88,11 @@ public class AcceptanceTests { then(blogTemplate()).exists(); then(blogTemplateContents()) .contains("I am pleased to announce that the Service Release 5 (SR5)"); + then(releaseNotesTemplate()).exists(); + then(releaseNotesTemplateContents()) + .contains("Camden.SR5") + .contains("- Spring Cloud Config `1.2.2.RELEASE` ([issues](http://foo.bar.com/1.2.2.RELEASE))") + .contains("- Spring Cloud Aws `1.1.3.RELEASE` ([issues](http://foo.bar.com/1.1.3.RELEASE))"); } @Test @@ -109,7 +114,7 @@ public class AcceptanceTests { commitIsPresent(iterator, "Update SNAPSHOT to 1.2.0.RC1"); pomVersionIsEqualTo(project, "1.2.0.BUILD-SNAPSHOT"); pomParentVersionIsEqualTo(project, "1.2.0.BUILD-SNAPSHOT"); - then(this.gitUpdater.executed).isTrue(); + then(this.gitHandler.closedMilestones).isTrue(); then(emailTemplate()).exists(); then(emailTemplateContents()) .contains("Spring Cloud Dalston.RC1 available") @@ -120,6 +125,11 @@ public class AcceptanceTests { then(tweetTemplate()).exists(); then(tweetTemplateContents()) .contains("The Dalston.RC1 version of @springcloud has been released!"); + then(releaseNotesTemplate()).exists(); + then(releaseNotesTemplateContents()) + .contains("Dalston.RC1") + .contains("- Spring Cloud Build `1.3.1.RELEASE` ([issues](http://foo.bar.com/1.3.1.RELEASE))") + .contains("- Spring Cloud Bus `1.3.0.M1` ([issues](http://foo.bar.com/1.3.0.M1))"); } @Test @@ -133,7 +143,7 @@ public class AcceptanceTests { releaser.release(); - then(this.gitUpdater.executed).isFalse(); + then(this.gitHandler.closedMilestones).isFalse(); then(emailTemplate()).exists(); then(emailTemplateContents()) .contains("Spring Cloud Dalston.RC1 available") @@ -144,6 +154,11 @@ public class AcceptanceTests { then(tweetTemplate()).exists(); then(tweetTemplateContents()) .contains("The Dalston.RC1 version of @springcloud has been released!"); + then(releaseNotesTemplate()).exists(); + then(releaseNotesTemplateContents()) + .contains("Dalston.RC1") + .contains("- Spring Cloud Build `1.3.1.RELEASE` ([issues](http://foo.bar.com/1.3.1.RELEASE))") + .contains("- Spring Cloud Bus `1.3.0.M1` ([issues](http://foo.bar.com/1.3.0.M1)"); } private Iterable listOfCommits(File project) throws GitAPIException { @@ -197,6 +212,10 @@ public class AcceptanceTests { return new File("target/tweet.txt"); } + private File releaseNotesTemplate() throws URISyntaxException { + return new File("target/notes.md"); + } + private String blogTemplateContents() throws URISyntaxException, IOException { return new String(Files.readAllBytes(blogTemplate().toPath())); } @@ -205,6 +224,10 @@ public class AcceptanceTests { return new String(Files.readAllBytes(tweetTemplate().toPath())); } + private String releaseNotesTemplateContents() throws URISyntaxException, IOException { + return new String(Files.readAllBytes(releaseNotesTemplate().toPath())); + } + private SpringReleaser releaser(File projectFile, String branch, String expectedVersion) throws Exception { ReleaserProperties properties = releaserProperties(projectFile, branch); return releaserWithFullDeployment(expectedVersion, properties); @@ -238,13 +261,13 @@ public class AcceptanceTests { private Releaser defaultReleaser(String expectedVersion, ReleaserProperties properties) throws Exception { ProjectPomUpdater pomUpdater = new ProjectPomUpdater(properties); ProjectBuilder projectBuilder = new ProjectBuilder(properties, pomUpdater); - TestProjectGitUpdater gitUpdater = new TestProjectGitUpdater(properties, + TestProjectGitHandler handler = new TestProjectGitHandler(properties, expectedVersion); - TemplateGenerator templateGenerator = new TemplateGenerator(properties); + TemplateGenerator templateGenerator = new TemplateGenerator(properties, handler); GradleUpdater gradleUpdater = new GradleUpdater(properties); - Releaser releaser = new Releaser(pomUpdater, projectBuilder, gitUpdater, + Releaser releaser = new Releaser(pomUpdater, projectBuilder, handler, templateGenerator, gradleUpdater); - this.gitUpdater = gitUpdater; + this.gitHandler = handler; return releaser; } @@ -265,12 +288,12 @@ public class AcceptanceTests { return releaserProperties; } - class TestProjectGitUpdater extends ProjectGitUpdater { + class TestProjectGitHandler extends ProjectGitHandler { - boolean executed = false; + boolean closedMilestones = false; final String expectedVersion; - public TestProjectGitUpdater(ReleaserProperties properties, + public TestProjectGitHandler(ReleaserProperties properties, String expectedVersion) { super(properties); this.expectedVersion = expectedVersion; @@ -279,7 +302,11 @@ public class AcceptanceTests { @Override public void closeMilestone(ProjectVersion releaseVersion) { then(releaseVersion.projectName).isEqualTo("spring-cloud-consul"); then(releaseVersion.version).isEqualTo(this.expectedVersion); - this.executed = true; + this.closedMilestones = true; + } + + @Override public String milestoneUrl(ProjectVersion releaseVersion) { + return "http://foo.bar.com/" + releaseVersion.toString(); } }