diff --git a/README.adoc b/README.adoc index a4bf836..0198763 100644 --- a/README.adoc +++ b/README.adoc @@ -37,7 +37,7 @@ ghrnc: The `ghrnc.github-token` is optional but increases the rate limit of GitHub. If required, the endpoint of the GitHub API could be set with `ghrnc.base-url`. -If `sections` isn't set, the following default is used: +If `ghrnc.sections` isn't set, the following default is used: [source,yaml] ---- @@ -51,6 +51,20 @@ ghrnc: labels: ["documentation"] ---- +Optionally, a list of all contributors can be created. The corresponding configuration may look like this: + +[source,yaml] +---- +ghrnc: + contributors: + enabled: true + title: ":heart: Contributors" + message: "Thank you to all the contributors who worked on this release." + excludes: + - "core-developer-1" + - "core-developer-2" +---- + == Running Standalone Download the https://github.com/th-schwarz/GithubReleaseNotesCreator/releases[last release jar]. diff --git a/pom.xml b/pom.xml index 639437a..beaee39 100644 --- a/pom.xml +++ b/pom.xml @@ -16,7 +16,7 @@ 3.13.0 3.5.3 - 2.0-rc.3 + 2.0-rc.4 2.18.3 1.5.18 24.0.0 diff --git a/src/main/java/codes/thischwa/ghrnc/GithubService.java b/src/main/java/codes/thischwa/ghrnc/GithubService.java index fc23a07..aca5509 100644 --- a/src/main/java/codes/thischwa/ghrnc/GithubService.java +++ b/src/main/java/codes/thischwa/ghrnc/GithubService.java @@ -6,6 +6,7 @@ import java.util.List; import java.util.Map; import java.util.NoSuchElementException; +import java.util.Objects; import org.jetbrains.annotations.Nullable; import org.kohsuke.github.GHIssue; @@ -15,42 +16,51 @@ import org.kohsuke.github.GHRepository; import org.kohsuke.github.GitHub; import org.kohsuke.github.GitHubBuilder; -import org.kohsuke.github.PagedIterable; import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class GithubService { + private static final Logger LOG = LoggerFactory.getLogger(GithubService.class); - private static final Logger LOG = org.slf4j.LoggerFactory.getLogger(GithubService.class); private final GitHub github; - private final GHRepository repo; + private final GHRepository repository; - public GithubService(@Nullable String githubToken, String repo) throws IOException { - this(null, githubToken, repo); + public GithubService(@Nullable String githubToken, String repositoryName) throws IOException { + this(null, githubToken, repositoryName); } - public GithubService(@Nullable String baseUrl, @Nullable String githubToken, String repo) throws IOException { - assert repo != null; - GitHubBuilder builder = (githubToken == null || githubToken.isBlank()) ? new GitHubBuilder() : + public GithubService(@Nullable String baseUrl, @Nullable String githubToken, + String repositoryName) throws IOException { + Objects.requireNonNull(repositoryName, "Repository name must not be null"); + this.github = initGithubRepository(baseUrl, githubToken); + this.repository = github.getRepository(repositoryName); + LOG.debug("GitHub-Service initialized for {}", repositoryName); + } + + private GitHub initGithubRepository(@Nullable String baseUrl, @Nullable String githubToken) + throws IOException { + GitHubBuilder builder = isBlank(githubToken) ? new GitHubBuilder() : new GitHubBuilder().withOAuthToken(githubToken); - github = (baseUrl == null || baseUrl.isBlank()) ? builder.build() : builder.withEndpoint(baseUrl).build(); - this.repo = github.getRepository(repo); - LOG.debug("GitHub-Service initialized for {}", repo); + return isBlank(baseUrl) ? builder.build() : builder.withEndpoint(baseUrl).build(); + } + + private boolean isBlank(@Nullable String str) { + return (str == null || str.isBlank()); } - public GHMilestone findMilestone(String title) throws NoSuchElementException { - PagedIterable milestonePage = repo.listMilestones(GHIssueState.ALL); - for (GHMilestone mileStone : milestonePage) { - if (mileStone.getTitle().equals(title)) { - return mileStone; + public GHMilestone findMilestone(String title) { + for (GHMilestone milestone : repository.listMilestones(GHIssueState.ALL)) { + if (milestone.getTitle().equals(title)) { + return milestone; } } throw new NoSuchElementException("No such milestone: " + title); } public List getClosedIssuesForMilestone(GHMilestone milestone) throws IOException { - List ghIssues = repo.getIssues(GHIssueState.CLOSED, milestone); - LOG.info("Found {} closed issues for milestone {}", ghIssues.size(), milestone.getTitle()); - return ghIssues; + List closedIssues = repository.getIssues(GHIssueState.CLOSED, milestone); + LOG.info("Found {} closed issues for milestone {}", closedIssues.size(), milestone.getTitle()); + return closedIssues; } public Map> groupByLabel(List issues) { diff --git a/src/main/java/codes/thischwa/ghrnc/ReleaseNotesService.java b/src/main/java/codes/thischwa/ghrnc/ReleaseNotesService.java index 58f5db6..6e48a65 100644 --- a/src/main/java/codes/thischwa/ghrnc/ReleaseNotesService.java +++ b/src/main/java/codes/thischwa/ghrnc/ReleaseNotesService.java @@ -1,21 +1,22 @@ package codes.thischwa.ghrnc; import codes.thischwa.ghrnc.model.Conf; +import codes.thischwa.ghrnc.model.Ghrnc; import codes.thischwa.ghrnc.model.Section; import java.io.IOException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.NoSuchElementException; +import java.util.*; import org.kohsuke.github.GHIssue; import org.kohsuke.github.GHLabel; import org.kohsuke.github.GHMilestone; +import org.kohsuke.github.GHUser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class ReleaseNotesService { - + private static final Logger LOG = LoggerFactory.getLogger(ReleaseNotesService.class); private static final String NL = "\n"; + private static final String DELIMITER_CONTRIBUTORS = ", "; private final GithubService githubService; private final Conf config; @@ -27,50 +28,72 @@ public ReleaseNotesService(GithubService githubService, Conf config) { public String generateChangelog(String milestoneTitle) throws IOException, NoSuchElementException { - // Find the milestone GHMilestone milestone = githubService.findMilestone(milestoneTitle); - - // Get closed issues for the milestone List closedIssues = githubService.getClosedIssuesForMilestone(milestone); - - // Group issues by their labels Map> groupedIssues = groupBySection(closedIssues); - - // Generate markdown content return generateMarkdown(groupedIssues); } Map> groupBySection(List issues) { final Map> groupedIssues = new HashMap<>(); - for (GHIssue issue : issues) { for (String label : issue.getLabels().stream().map(GHLabel::getName).toList()) { for (Section section : config.ghrnc().sections()) { if (section.getLabels().contains(label)) { - String sectionTitle = section.getTitle(); - if (!groupedIssues.containsKey(sectionTitle)) { - groupedIssues.put(sectionTitle, new ArrayList<>()); - } - groupedIssues.get(sectionTitle).add(issue); + groupedIssues.computeIfAbsent(section.getTitle(), k -> new ArrayList<>()).add(issue); } } } } return groupedIssues; } + + private Set collectContributors(List closedIssues, Ghrnc ghrnc) { + Set contributors = new HashSet<>(); + if (!ghrnc.isContributorsEnabled()) { + return contributors; + } + for (GHIssue issue : closedIssues) { + GHUser contributor = issue.getUser(); + if (contributor != null && !contributor.getLogin().endsWith("[bot]") && + !ghrnc.contributors().excludes().contains(contributor.getLogin())) { + contributors.add(contributor); + LOG.debug("Contributor found: {}", contributor.getLogin()); + } + } + LOG.info("Found {} contributors in {} closed issues", contributors.size(), closedIssues.size()); + return contributors; + } + String generateMarkdown(Map> groupedIssues) { StringBuilder markdown = new StringBuilder(); + List usedIssues = new ArrayList<>(); config.ghrnc().sections().forEach(section -> { - List issues = groupedIssues.get(section.getTitle()); - if (issues != null && !issues.isEmpty()) { - markdown.append("## ").append(section.getTitle()).append(NL).append(NL); - for (GHIssue issue : issues) { - markdown.append("- ").append(issue.getTitle()).append(" [#").append(issue.getNumber()) - .append("](").append(issue.getHtmlUrl()).append(")").append(NL); - } - markdown.append(NL); + List issues = groupedIssues.get(section.getTitle()); + if (issues != null && !issues.isEmpty()) { + usedIssues.addAll(issues); + markdown.append("## ").append(section.getTitle()).append(NL).append(NL); + for (GHIssue issue : issues) { + markdown.append("- ").append(issue.getTitle()).append(" [#").append(issue.getNumber()) + .append("](").append(issue.getHtmlUrl()).append(")").append(NL); } + markdown.append(NL); + } }); + + Set contributors = collectContributors(usedIssues, config.ghrnc()); + if (config.ghrnc().isContributorsEnabled() && !contributors.isEmpty()) { + List contributorsSorted = new ArrayList<>(contributors); + contributorsSorted.sort(Comparator.comparing(u -> u.getLogin().toLowerCase(Locale.ROOT))); + markdown.append("## ").append(config.ghrnc().contributors().title()).append(NL).append(NL); + markdown.append(config.ghrnc().contributors().message()).append(NL).append(NL); + contributorsSorted.forEach( + contributor -> markdown.append("[@").append(contributor.getLogin()).append("](") + .append(contributor.getHtmlUrl()).append(")").append(DELIMITER_CONTRIBUTORS)); + if (markdown.toString().endsWith(DELIMITER_CONTRIBUTORS)) { + markdown.delete(markdown.length() - DELIMITER_CONTRIBUTORS.length(), markdown.length()); + } + } return markdown.toString().trim(); } } \ No newline at end of file diff --git a/src/main/java/codes/thischwa/ghrnc/model/Contributors.java b/src/main/java/codes/thischwa/ghrnc/model/Contributors.java new file mode 100644 index 0000000..7944df5 --- /dev/null +++ b/src/main/java/codes/thischwa/ghrnc/model/Contributors.java @@ -0,0 +1,6 @@ +package codes.thischwa.ghrnc.model; + +import java.util.List; + +public record Contributors(boolean enabled, String title, String message, List excludes) { +} diff --git a/src/main/java/codes/thischwa/ghrnc/model/Ghrnc.java b/src/main/java/codes/thischwa/ghrnc/model/Ghrnc.java index 331686f..5fb59fe 100644 --- a/src/main/java/codes/thischwa/ghrnc/model/Ghrnc.java +++ b/src/main/java/codes/thischwa/ghrnc/model/Ghrnc.java @@ -1,13 +1,21 @@ package codes.thischwa.ghrnc.model; import java.util.List; - import org.jetbrains.annotations.Nullable; -public record Ghrnc(@Nullable String baseUrl, String repo, String githubToken, - List
sections) { +public record Ghrnc( + @Nullable String baseUrl, + String repo, + String githubToken, + List
sections, + @Nullable Contributors contributors +) { public Ghrnc(Ghrnc ghrnc, List
sections) { - this(ghrnc.baseUrl(), ghrnc.repo(), ghrnc.githubToken(), sections); + this(ghrnc.baseUrl(), ghrnc.repo(), ghrnc.githubToken(), sections, ghrnc.contributors()); + } + + public boolean isContributorsEnabled() { + return contributors != null && contributors.enabled(); } } \ No newline at end of file diff --git a/src/test/java/codes/thischwa/ghrnc/ReleaseNotesServiceTest.java b/src/test/java/codes/thischwa/ghrnc/ReleaseNotesServiceTest.java index c08286e..7c7be72 100644 --- a/src/test/java/codes/thischwa/ghrnc/ReleaseNotesServiceTest.java +++ b/src/test/java/codes/thischwa/ghrnc/ReleaseNotesServiceTest.java @@ -1,5 +1,6 @@ package codes.thischwa.ghrnc; +import codes.thischwa.ghrnc.model.Ghrnc; import com.fasterxml.jackson.core.type.TypeReference; import java.io.BufferedReader; import java.io.IOException; @@ -10,13 +11,14 @@ import java.util.stream.Collectors; import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.kohsuke.github.GHIssue; public class ReleaseNotesServiceTest extends AbstractTest { @Test - void test() throws Exception { + void testOnline() throws Exception { GithubService service = new GithubService(GITHUB_TOKEN, REPO); ReleaseNotesService changelogService = new ReleaseNotesService(service, new YamlUtil().readInputStream(this.getClass().getResourceAsStream("/ghrnc.yml"))); @@ -26,16 +28,27 @@ void test() throws Exception { } @Test - void testSpring() throws Exception { + @Disabled + void testSpringOnline() throws Exception { + GithubService service = new GithubService(null, "spring-projects/spring-framework"); + ReleaseNotesService changelogService = new ReleaseNotesService(service, + new YamlUtil().readInputStream(this.getClass().getResourceAsStream("/spring-framework-contr.yml"))); + String changelog = changelogService.generateChangelog("6.2.5"); + String expected = readInputStreamToString(this.getClass().getResourceAsStream("/changelog-spring-contr.md")); + assertEquals(expected, changelog); + } + + @Test + void testSpringOffline() throws Exception { List issues = GithubApiYamlTestConfig.configureObjectMapper() .readValue(this.getClass().getResourceAsStream("/issues_spring-6.2.5.yml"), new TypeReference<>() { }); - ReleaseNotesService changelogService = new ReleaseNotesService(null, + ReleaseNotesService releaseNotesService = new ReleaseNotesService(null, new YamlUtil().readInputStream( this.getClass().getResourceAsStream("/spring-framework.yml"))); - Map> groupedIssues = changelogService.groupBySection(issues); - String actual = changelogService.generateMarkdown(groupedIssues); + Map> groupedIssues = releaseNotesService.groupBySection(issues); + String actual = releaseNotesService.generateMarkdown(groupedIssues); String expected = readInputStreamToString(this.getClass().getResourceAsStream("/changelog-spring.md")); assertEquals(expected, actual); diff --git a/src/test/java/codes/thischwa/ghrnc/YamlUtilTest.java b/src/test/java/codes/thischwa/ghrnc/YamlUtilTest.java index 9b523de..c2cf4ab 100644 --- a/src/test/java/codes/thischwa/ghrnc/YamlUtilTest.java +++ b/src/test/java/codes/thischwa/ghrnc/YamlUtilTest.java @@ -3,6 +3,7 @@ import codes.thischwa.ghrnc.model.Conf; import codes.thischwa.ghrnc.model.Ghrnc; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -35,8 +36,6 @@ void testMissingSections() { this.getClass().getResourceAsStream("/ghrnc_without-sections.yml")); assertNotNull(result); Ghrnc config = result.ghrnc(); - assertEquals("owner/project", config.repo()); - assertEquals("owner/project", config.repo()); assertEquals("ghp_abcdefghijklmnopqrstxyz0123456789bla", config.githubToken()); @@ -45,4 +44,19 @@ void testMissingSections() { assertEquals(":lady_beetle: Bug Fixes", config.sections().get(1).getTitle()); assertTrue(config.sections().get(1).getLabels().contains("bug")); } + + @Test + void testContributorsConfig() { + YamlUtil yamlUtil = new YamlUtil(); + Conf result = yamlUtil.readInputStream(this.getClass().getResourceAsStream("/ghrnc.yml")); + assertNotNull(result); + + Ghrnc config = result.ghrnc(); + assertNotNull(config.contributors()); + assertFalse(config.contributors().enabled()); + assertEquals("Contributors", config.contributors().title()); + assertEquals("Thank you to all the contributors who worked on this release.", config.contributors().message()); + assertTrue(config.contributors().excludes().contains("core-developer")); + } + } \ No newline at end of file diff --git a/src/test/resources/changelog-spring-contr.md b/src/test/resources/changelog-spring-contr.md new file mode 100644 index 0000000..fdcefa7 --- /dev/null +++ b/src/test/resources/changelog-spring-contr.md @@ -0,0 +1,31 @@ +## :star: New Features + +- Make dependencies on AssertJ and JUnit in `spring-core-test` optional [#34612](https://github.com/spring-projects/spring-framework/issues/34612) +- Suggest compilation with `-parameters` when `AspectJAdviceParameterNameDiscoverer` fails against ambiguity [#34609](https://github.com/spring-projects/spring-framework/issues/34609) +- SseBuilder in ServerResponse should allow empty comment [#34608](https://github.com/spring-projects/spring-framework/issues/34608) +- MockServerWebExchange does not allow setting the ApplicationContext on the base class [#34601](https://github.com/spring-projects/spring-framework/issues/34601) +- `FormHttpMessageConverter` should throw `HttpMessageNotReadableException` when the http form data is invalid [#34594](https://github.com/spring-projects/spring-framework/pull/34594) +- Provide a method to retrieve all singleton autowire candidates from the bean factory [#34591](https://github.com/spring-projects/spring-framework/issues/34591) + +## :lady_beetle: Bug Fixes + +- PathMatchingResourcePatternResolver regression for jar root scanning in 6.2.4 [#34607](https://github.com/spring-projects/spring-framework/issues/34607) +- AbstractReactiveTransactionManager throws IllegalStateException when rollback fails after commit attempt [#34595](https://github.com/spring-projects/spring-framework/issues/34595) +- Recursively boxing/unboxing nested inline value classes [#34592](https://github.com/spring-projects/spring-framework/pull/34592) + +## :notebook_with_decorative_cover: Documentation + +- `MvcUriComponentsBuilder` javadocs inaccurately reflects usage of forwarded headers [#34615](https://github.com/spring-projects/spring-framework/issues/34615) +- Fix formatting and update links to scripting libraries and HDIV [#34603](https://github.com/spring-projects/spring-framework/pull/34603) +- Remove dubious link to MockObjects Web site in reference manual [#34593](https://github.com/spring-projects/spring-framework/issues/34593) +- Fix `StringUtils#uriDecode` Javadoc [#34590](https://github.com/spring-projects/spring-framework/issues/34590) + +## :hammer: Dependency Upgrades + +- Upgrade to ASM 9.8 (for early Java 25 support) [#34600](https://github.com/spring-projects/spring-framework/issues/34600) + +## :heart: Contributors + +Thank you to all the contributors who worked on this release. + +[@dforrest-es](https://github.com/dforrest-es), [@dmitrysulman](https://github.com/dmitrysulman), [@dspasic](https://github.com/dspasic), [@dsyer](https://github.com/dsyer), [@Helmsdown](https://github.com/Helmsdown), [@jhoeller](https://github.com/jhoeller), [@kristofdepypere](https://github.com/kristofdepypere), [@ngocnhan-tran1996](https://github.com/ngocnhan-tran1996), [@rstoyanchev](https://github.com/rstoyanchev), [@sdeleuze](https://github.com/sdeleuze), [@SledgeHammer01](https://github.com/SledgeHammer01), [@xardael](https://github.com/xardael) \ No newline at end of file diff --git a/src/test/resources/ghrnc.yml b/src/test/resources/ghrnc.yml index 47d7b5f..972eb06 100644 --- a/src/test/resources/ghrnc.yml +++ b/src/test/resources/ghrnc.yml @@ -7,4 +7,9 @@ ghrnc: - title: "Bugs" labels: ["bug"] - title: "Improvements" - labels: ["improvement"] \ No newline at end of file + labels: ["improvement"] + contributors: + enabled: false + title: "Contributors" + message: "Thank you to all the contributors who worked on this release." + excludes: ["core-developer"] \ No newline at end of file diff --git a/src/test/resources/spring-framework-contr.yml b/src/test/resources/spring-framework-contr.yml new file mode 100644 index 0000000..63d04e4 --- /dev/null +++ b/src/test/resources/spring-framework-contr.yml @@ -0,0 +1,23 @@ +ghrnc: + repo: spring-projects/spring-framework + sections: + - title: ":star: New Features" + labels: + - "type: enhancement" + - title: ":lady_beetle: Bug Fixes" + labels: + - "type: bug" + - "type: regression" + - title: ":notebook_with_decorative_cover: Documentation" + labels: + - "type: documentation" + - title: ":hammer: Dependency Upgrades" + labels: + - "type: dependency-upgrade" + contributors: + enabled: true + title: ":heart: Contributors" + message: "Thank you to all the contributors who worked on this release." + excludes: + - "sbrannen" + - "wilkinsona" \ No newline at end of file diff --git a/src/test/resources/spring-framework.yml b/src/test/resources/spring-framework.yml index f79280f..7afb8b1 100644 --- a/src/test/resources/spring-framework.yml +++ b/src/test/resources/spring-framework.yml @@ -14,14 +14,16 @@ ghrnc: - title: ":hammer: Dependency Upgrades" labels: - "type: dependency-upgrade" -# contributors: -# exclude: -# names: -# - "bclozel" -# - "jhoeller" -# - "poutsma" -# - "rstoyanchev" -# - "sbrannen" -# - "sdeleuze" -# - "simonbasle" -# - "snicoll" \ No newline at end of file + contributors: + enabled: false + title: "Contributors" + message: ":heart: Thank you to all the contributors who worked on this release." + excludes: + - "bclozel" + - "jhoeller" + - "poutsma" + - "rstoyanchev" + - "sbrannen" + - "sdeleuze" + - "simonbasle" + - "snicoll" \ No newline at end of file