Skip to content

Commit 8c9a36b

Browse files
authored
Feature1 add a list of contributers (#6)
* issue #1 -Add contributors section support in YAML configuration and release notes generation Implemented a new "Contributors" section in the YAML configuration, allowing inclusion of contributor acknowledgments in generated release notes. Updated core logic, added tests, and extended related examples to support this feature. Upgraded `github-api` dependency to v2.0-rc.4. * issue #1 - Add contributors section support in YAML configuration and release notes generation Implemented a new "Contributors" section in the YAML configuration, allowing inclusion of contributor acknowledgments in generated release notes. Updated core logic, added tests, and extended related examples to support this feature. Upgraded `github-api` dependency to v2.0-rc.4. * issue #1 - Update README with contributors section configuration example Added an example YAML configuration demonstrating how to enable and customize a contributors section in release notes. * fix 1 Refactor `ReleaseNotesService` and `GithubService` for readability and efficiency Simplified imports, reduced verbosity, introduced reusable methods (e.g., `collectContributors`, `initGithubRepository`), and improved string handling with constants. Enhanced logging and parameter validation for better maintainability and functionality. * [maven-release-plugin] prepare release v0.1.0-beta.4 * [maven-release-plugin] prepare for next development iteration * Revert "[maven-release-plugin] prepare for next development iteration" This reverts commit 48ac855. * Revert "[maven-release-plugin] prepare release v0.1.0-beta.4" This reverts commit 2b170b1.
1 parent 6a2901b commit 8c9a36b

12 files changed

Lines changed: 220 additions & 71 deletions

File tree

README.adoc

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ ghrnc:
3737

3838
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`.
3939

40-
If `sections` isn't set, the following default is used:
40+
If `ghrnc.sections` isn't set, the following default is used:
4141

4242
[source,yaml]
4343
----
@@ -51,6 +51,20 @@ ghrnc:
5151
labels: ["documentation"]
5252
----
5353

54+
Optionally, a list of all contributors can be created. The corresponding configuration may look like this:
55+
56+
[source,yaml]
57+
----
58+
ghrnc:
59+
contributors:
60+
enabled: true
61+
title: ":heart: Contributors"
62+
message: "Thank you to all the contributors who worked on this release."
63+
excludes:
64+
- "core-developer-1"
65+
- "core-developer-2"
66+
----
67+
5468
== Running Standalone
5569

5670
Download the https://github.com/th-schwarz/GithubReleaseNotesCreator/releases[last release jar].

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
<compiler-plugin.version>3.13.0</compiler-plugin.version>
1717
<shade-plugin.version>3.5.3</shade-plugin.version>
1818

19-
<github-api.version>2.0-rc.3</github-api.version>
19+
<github-api.version>2.0-rc.4</github-api.version>
2020
<jackson.version>2.18.3</jackson.version>
2121
<logback.version>1.5.18</logback.version>
2222
<jetbrains-annotations.version>24.0.0</jetbrains-annotations.version>

src/main/java/codes/thischwa/ghrnc/GithubService.java

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import java.util.List;
77
import java.util.Map;
88
import java.util.NoSuchElementException;
9+
import java.util.Objects;
910

1011
import org.jetbrains.annotations.Nullable;
1112
import org.kohsuke.github.GHIssue;
@@ -15,42 +16,51 @@
1516
import org.kohsuke.github.GHRepository;
1617
import org.kohsuke.github.GitHub;
1718
import org.kohsuke.github.GitHubBuilder;
18-
import org.kohsuke.github.PagedIterable;
1919
import org.slf4j.Logger;
20+
import org.slf4j.LoggerFactory;
2021

2122
public class GithubService {
23+
private static final Logger LOG = LoggerFactory.getLogger(GithubService.class);
2224

23-
private static final Logger LOG = org.slf4j.LoggerFactory.getLogger(GithubService.class);
2425
private final GitHub github;
25-
private final GHRepository repo;
26+
private final GHRepository repository;
2627

27-
public GithubService(@Nullable String githubToken, String repo) throws IOException {
28-
this(null, githubToken, repo);
28+
public GithubService(@Nullable String githubToken, String repositoryName) throws IOException {
29+
this(null, githubToken, repositoryName);
2930
}
3031

31-
public GithubService(@Nullable String baseUrl, @Nullable String githubToken, String repo) throws IOException {
32-
assert repo != null;
33-
GitHubBuilder builder = (githubToken == null || githubToken.isBlank()) ? new GitHubBuilder() :
32+
public GithubService(@Nullable String baseUrl, @Nullable String githubToken,
33+
String repositoryName) throws IOException {
34+
Objects.requireNonNull(repositoryName, "Repository name must not be null");
35+
this.github = initGithubRepository(baseUrl, githubToken);
36+
this.repository = github.getRepository(repositoryName);
37+
LOG.debug("GitHub-Service initialized for {}", repositoryName);
38+
}
39+
40+
private GitHub initGithubRepository(@Nullable String baseUrl, @Nullable String githubToken)
41+
throws IOException {
42+
GitHubBuilder builder = isBlank(githubToken) ? new GitHubBuilder() :
3443
new GitHubBuilder().withOAuthToken(githubToken);
35-
github = (baseUrl == null || baseUrl.isBlank()) ? builder.build() : builder.withEndpoint(baseUrl).build();
36-
this.repo = github.getRepository(repo);
37-
LOG.debug("GitHub-Service initialized for {}", repo);
44+
return isBlank(baseUrl) ? builder.build() : builder.withEndpoint(baseUrl).build();
45+
}
46+
47+
private boolean isBlank(@Nullable String str) {
48+
return (str == null || str.isBlank());
3849
}
3950

40-
public GHMilestone findMilestone(String title) throws NoSuchElementException {
41-
PagedIterable<GHMilestone> milestonePage = repo.listMilestones(GHIssueState.ALL);
42-
for (GHMilestone mileStone : milestonePage) {
43-
if (mileStone.getTitle().equals(title)) {
44-
return mileStone;
51+
public GHMilestone findMilestone(String title) {
52+
for (GHMilestone milestone : repository.listMilestones(GHIssueState.ALL)) {
53+
if (milestone.getTitle().equals(title)) {
54+
return milestone;
4555
}
4656
}
4757
throw new NoSuchElementException("No such milestone: " + title);
4858
}
4959

5060
public List<GHIssue> getClosedIssuesForMilestone(GHMilestone milestone) throws IOException {
51-
List<GHIssue> ghIssues = repo.getIssues(GHIssueState.CLOSED, milestone);
52-
LOG.info("Found {} closed issues for milestone {}", ghIssues.size(), milestone.getTitle());
53-
return ghIssues;
61+
List<GHIssue> closedIssues = repository.getIssues(GHIssueState.CLOSED, milestone);
62+
LOG.info("Found {} closed issues for milestone {}", closedIssues.size(), milestone.getTitle());
63+
return closedIssues;
5464
}
5565

5666
public Map<GHLabel, List<GHIssue>> groupByLabel(List<GHIssue> issues) {
Lines changed: 50 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,22 @@
11
package codes.thischwa.ghrnc;
22

33
import codes.thischwa.ghrnc.model.Conf;
4+
import codes.thischwa.ghrnc.model.Ghrnc;
45
import codes.thischwa.ghrnc.model.Section;
56
import java.io.IOException;
6-
import java.util.ArrayList;
7-
import java.util.HashMap;
8-
import java.util.List;
9-
import java.util.Map;
10-
import java.util.NoSuchElementException;
7+
import java.util.*;
118

129
import org.kohsuke.github.GHIssue;
1310
import org.kohsuke.github.GHLabel;
1411
import org.kohsuke.github.GHMilestone;
12+
import org.kohsuke.github.GHUser;
13+
import org.slf4j.Logger;
14+
import org.slf4j.LoggerFactory;
1515

1616
public class ReleaseNotesService {
17-
17+
private static final Logger LOG = LoggerFactory.getLogger(ReleaseNotesService.class);
1818
private static final String NL = "\n";
19+
private static final String DELIMITER_CONTRIBUTORS = ", ";
1920

2021
private final GithubService githubService;
2122
private final Conf config;
@@ -27,50 +28,72 @@ public ReleaseNotesService(GithubService githubService, Conf config) {
2728

2829
public String generateChangelog(String milestoneTitle)
2930
throws IOException, NoSuchElementException {
30-
// Find the milestone
3131
GHMilestone milestone = githubService.findMilestone(milestoneTitle);
32-
33-
// Get closed issues for the milestone
3432
List<GHIssue> closedIssues = githubService.getClosedIssuesForMilestone(milestone);
35-
36-
// Group issues by their labels
3733
Map<String, List<GHIssue>> groupedIssues = groupBySection(closedIssues);
38-
39-
// Generate markdown content
4034
return generateMarkdown(groupedIssues);
4135
}
4236

4337
Map<String, List<GHIssue>> groupBySection(List<GHIssue> issues) {
4438
final Map<String, List<GHIssue>> groupedIssues = new HashMap<>();
45-
4639
for (GHIssue issue : issues) {
4740
for (String label : issue.getLabels().stream().map(GHLabel::getName).toList()) {
4841
for (Section section : config.ghrnc().sections()) {
4942
if (section.getLabels().contains(label)) {
50-
String sectionTitle = section.getTitle();
51-
if (!groupedIssues.containsKey(sectionTitle)) {
52-
groupedIssues.put(sectionTitle, new ArrayList<>());
53-
}
54-
groupedIssues.get(sectionTitle).add(issue);
43+
groupedIssues.computeIfAbsent(section.getTitle(), k -> new ArrayList<>()).add(issue);
5544
}
5645
}
5746
}
5847
}
5948
return groupedIssues;
6049
}
50+
51+
private Set<GHUser> collectContributors(List<GHIssue> closedIssues, Ghrnc ghrnc) {
52+
Set<GHUser> contributors = new HashSet<>();
53+
if (!ghrnc.isContributorsEnabled()) {
54+
return contributors;
55+
}
56+
for (GHIssue issue : closedIssues) {
57+
GHUser contributor = issue.getUser();
58+
if (contributor != null && !contributor.getLogin().endsWith("[bot]") &&
59+
!ghrnc.contributors().excludes().contains(contributor.getLogin())) {
60+
contributors.add(contributor);
61+
LOG.debug("Contributor found: {}", contributor.getLogin());
62+
}
63+
}
64+
LOG.info("Found {} contributors in {} closed issues", contributors.size(), closedIssues.size());
65+
return contributors;
66+
}
67+
6168
String generateMarkdown(Map<String, List<GHIssue>> groupedIssues) {
6269
StringBuilder markdown = new StringBuilder();
70+
List<GHIssue> usedIssues = new ArrayList<>();
6371
config.ghrnc().sections().forEach(section -> {
64-
List<GHIssue> issues = groupedIssues.get(section.getTitle());
65-
if (issues != null && !issues.isEmpty()) {
66-
markdown.append("## ").append(section.getTitle()).append(NL).append(NL);
67-
for (GHIssue issue : issues) {
68-
markdown.append("- ").append(issue.getTitle()).append(" [#").append(issue.getNumber())
69-
.append("](").append(issue.getHtmlUrl()).append(")").append(NL);
70-
}
71-
markdown.append(NL);
72+
List<GHIssue> issues = groupedIssues.get(section.getTitle());
73+
if (issues != null && !issues.isEmpty()) {
74+
usedIssues.addAll(issues);
75+
markdown.append("## ").append(section.getTitle()).append(NL).append(NL);
76+
for (GHIssue issue : issues) {
77+
markdown.append("- ").append(issue.getTitle()).append(" [#").append(issue.getNumber())
78+
.append("](").append(issue.getHtmlUrl()).append(")").append(NL);
7279
}
80+
markdown.append(NL);
81+
}
7382
});
83+
84+
Set<GHUser> contributors = collectContributors(usedIssues, config.ghrnc());
85+
if (config.ghrnc().isContributorsEnabled() && !contributors.isEmpty()) {
86+
List<GHUser> contributorsSorted = new ArrayList<>(contributors);
87+
contributorsSorted.sort(Comparator.comparing(u -> u.getLogin().toLowerCase(Locale.ROOT)));
88+
markdown.append("## ").append(config.ghrnc().contributors().title()).append(NL).append(NL);
89+
markdown.append(config.ghrnc().contributors().message()).append(NL).append(NL);
90+
contributorsSorted.forEach(
91+
contributor -> markdown.append("[@").append(contributor.getLogin()).append("](")
92+
.append(contributor.getHtmlUrl()).append(")").append(DELIMITER_CONTRIBUTORS));
93+
if (markdown.toString().endsWith(DELIMITER_CONTRIBUTORS)) {
94+
markdown.delete(markdown.length() - DELIMITER_CONTRIBUTORS.length(), markdown.length());
95+
}
96+
}
7497
return markdown.toString().trim();
7598
}
7699
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package codes.thischwa.ghrnc.model;
2+
3+
import java.util.List;
4+
5+
public record Contributors(boolean enabled, String title, String message, List<String> excludes) {
6+
}
Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
11
package codes.thischwa.ghrnc.model;
22

33
import java.util.List;
4-
54
import org.jetbrains.annotations.Nullable;
65

7-
public record Ghrnc(@Nullable String baseUrl, String repo, String githubToken,
8-
List<Section> sections) {
6+
public record Ghrnc(
7+
@Nullable String baseUrl,
8+
String repo,
9+
String githubToken,
10+
List<Section> sections,
11+
@Nullable Contributors contributors
12+
) {
913

1014
public Ghrnc(Ghrnc ghrnc, List<Section> sections) {
11-
this(ghrnc.baseUrl(), ghrnc.repo(), ghrnc.githubToken(), sections);
15+
this(ghrnc.baseUrl(), ghrnc.repo(), ghrnc.githubToken(), sections, ghrnc.contributors());
16+
}
17+
18+
public boolean isContributorsEnabled() {
19+
return contributors != null && contributors.enabled();
1220
}
1321
}

src/test/java/codes/thischwa/ghrnc/ReleaseNotesServiceTest.java

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package codes.thischwa.ghrnc;
22

3+
import codes.thischwa.ghrnc.model.Ghrnc;
34
import com.fasterxml.jackson.core.type.TypeReference;
45
import java.io.BufferedReader;
56
import java.io.IOException;
@@ -10,13 +11,14 @@
1011
import java.util.stream.Collectors;
1112
import static org.junit.jupiter.api.Assertions.assertEquals;
1213

14+
import org.junit.jupiter.api.Disabled;
1315
import org.junit.jupiter.api.Test;
1416
import org.kohsuke.github.GHIssue;
1517

1618
public class ReleaseNotesServiceTest extends AbstractTest {
1719

1820
@Test
19-
void test() throws Exception {
21+
void testOnline() throws Exception {
2022
GithubService service = new GithubService(GITHUB_TOKEN, REPO);
2123
ReleaseNotesService changelogService = new ReleaseNotesService(service,
2224
new YamlUtil().readInputStream(this.getClass().getResourceAsStream("/ghrnc.yml")));
@@ -26,16 +28,27 @@ void test() throws Exception {
2628
}
2729

2830
@Test
29-
void testSpring() throws Exception {
31+
@Disabled
32+
void testSpringOnline() throws Exception {
33+
GithubService service = new GithubService(null, "spring-projects/spring-framework");
34+
ReleaseNotesService changelogService = new ReleaseNotesService(service,
35+
new YamlUtil().readInputStream(this.getClass().getResourceAsStream("/spring-framework-contr.yml")));
36+
String changelog = changelogService.generateChangelog("6.2.5");
37+
String expected = readInputStreamToString(this.getClass().getResourceAsStream("/changelog-spring-contr.md"));
38+
assertEquals(expected, changelog);
39+
}
40+
41+
@Test
42+
void testSpringOffline() throws Exception {
3043
List<GHIssue> issues = GithubApiYamlTestConfig.configureObjectMapper()
3144
.readValue(this.getClass().getResourceAsStream("/issues_spring-6.2.5.yml"),
3245
new TypeReference<>() {
3346
});
34-
ReleaseNotesService changelogService = new ReleaseNotesService(null,
47+
ReleaseNotesService releaseNotesService = new ReleaseNotesService(null,
3548
new YamlUtil().readInputStream(
3649
this.getClass().getResourceAsStream("/spring-framework.yml")));
37-
Map<String, List<GHIssue>> groupedIssues = changelogService.groupBySection(issues);
38-
String actual = changelogService.generateMarkdown(groupedIssues);
50+
Map<String, List<GHIssue>> groupedIssues = releaseNotesService.groupBySection(issues);
51+
String actual = releaseNotesService.generateMarkdown(groupedIssues);
3952
String expected =
4053
readInputStreamToString(this.getClass().getResourceAsStream("/changelog-spring.md"));
4154
assertEquals(expected, actual);

src/test/java/codes/thischwa/ghrnc/YamlUtilTest.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import codes.thischwa.ghrnc.model.Conf;
44
import codes.thischwa.ghrnc.model.Ghrnc;
55
import static org.junit.jupiter.api.Assertions.assertEquals;
6+
import static org.junit.jupiter.api.Assertions.assertFalse;
67
import static org.junit.jupiter.api.Assertions.assertNotNull;
78
import static org.junit.jupiter.api.Assertions.assertTrue;
89

@@ -35,8 +36,6 @@ void testMissingSections() {
3536
this.getClass().getResourceAsStream("/ghrnc_without-sections.yml"));
3637
assertNotNull(result);
3738
Ghrnc config = result.ghrnc();
38-
assertEquals("owner/project", config.repo());
39-
4039
assertEquals("owner/project", config.repo());
4140
assertEquals("ghp_abcdefghijklmnopqrstxyz0123456789bla", config.githubToken());
4241

@@ -45,4 +44,19 @@ void testMissingSections() {
4544
assertEquals(":lady_beetle: Bug Fixes", config.sections().get(1).getTitle());
4645
assertTrue(config.sections().get(1).getLabels().contains("bug"));
4746
}
47+
48+
@Test
49+
void testContributorsConfig() {
50+
YamlUtil yamlUtil = new YamlUtil();
51+
Conf result = yamlUtil.readInputStream(this.getClass().getResourceAsStream("/ghrnc.yml"));
52+
assertNotNull(result);
53+
54+
Ghrnc config = result.ghrnc();
55+
assertNotNull(config.contributors());
56+
assertFalse(config.contributors().enabled());
57+
assertEquals("Contributors", config.contributors().title());
58+
assertEquals("Thank you to all the contributors who worked on this release.", config.contributors().message());
59+
assertTrue(config.contributors().excludes().contains("core-developer"));
60+
}
61+
4862
}

0 commit comments

Comments
 (0)