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