Skip to content

Commit 86923b0

Browse files
committed
Improved handling of dependency updates
Changesets can now have the type "dependency" and be grouped along with all other dependency updates in a separate list from any code changes.
1 parent 3e5ef1c commit 86923b0

File tree

85 files changed

+1331
-22
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

85 files changed

+1331
-22
lines changed

.changeset/legal-parents-scream.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"changesets": minor
3+
---
4+
5+
Add support for handling dependency updates separately from other changesets, so that they can be presented in a more organised way.
6+
7+
This is utilized by setting the update type to `dependency` in place of major/minor/patch.

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,24 @@ users recognize the ways of working and feel at home.
88

99
This is it, at the moment. Stay tuned for more docs later on, thanks!
1010

11+
12+
13+
## Dependency updates
14+
Due to the way automated dependency update bots like Dependabot and Renovate work, there is often a large influx of automated changesets that are not easy to merge into the normal changelog. They can also be the source of an unwanted amount of noise in the changelog.
15+
16+
To help with this, we have added a feature to mark changesets as dependency update using the update type "dependency". This will make the changeset appear in a separate section in the changelog, and will be added as a single list of updates in the end of the released version.
17+
18+
Dependencies that has been updated to new versions multiple times between releases will have each of the updates listed in the changelog.
19+
20+
```
21+
---
22+
"changesets-java": dependency
23+
---
24+
25+
- ch.qos.logback:logback-core: 1.5.12
26+
- com.google.errorprone:error_prone_annotations: 2.34.0
27+
```
28+
1129
# Release Maven Plugin Integration
1230

1331
To delegate versioning to the Release Maven Plugin, you can use the `ChangesetsVersionPolicy` together with the `useReleasePluginIntegration` flag:

changesets-java/src/main/java/se/fortnox/changesets/ChangelogAggregator.java

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import static java.util.stream.Collectors.toList;
2020
import static org.slf4j.LoggerFactory.getLogger;
2121
import static se.fortnox.changesets.ChangesetWriter.CHANGESET_DIR;
22+
import static se.fortnox.changesets.Level.DEPENDENCY;
2223
import static se.fortnox.changesets.Level.MAJOR;
2324
import static se.fortnox.changesets.Level.MINOR;
2425
import static se.fortnox.changesets.Level.PATCH;
@@ -27,9 +28,15 @@ public class ChangelogAggregator {
2728
private static final Logger LOG = getLogger(ChangelogAggregator.class);
2829
public static final String CHANGELOG_FILE = "CHANGELOG.md";
2930
private final Path baseDir;
31+
private final DependencyUpdatesParser dependencyUpdatesParser;
3032

3133
public ChangelogAggregator(Path baseDir) {
34+
this(baseDir, new DependencyUpdatesParser());
35+
}
36+
37+
public ChangelogAggregator(Path baseDir, DependencyUpdatesParser dependencyUpdatesParser) {
3238
this.baseDir = baseDir;
39+
this.dependencyUpdatesParser = dependencyUpdatesParser;
3340
}
3441

3542
/**
@@ -38,23 +45,26 @@ public ChangelogAggregator(Path baseDir) {
3845
*
3946
* @param packageName The package name to get changesets for
4047
* @param version The version number of the merged changes
48+
* @return
4149
*/
42-
public void mergeChangesetsToChangelog(String packageName, String version) {
50+
public Path mergeChangesetsToChangelog(String packageName, String version) {
4351
Path changesetsDir = this.baseDir.resolve(CHANGESET_DIR);
4452

4553
ChangesetLocator changesetLocator = new ChangesetLocator(this.baseDir);
4654
List<Changeset> changesets = changesetLocator.getChangesets(packageName);
4755
if (changesets.isEmpty()) {
4856
LOG.info("No changesets found in {}", this.baseDir);
49-
return;
57+
return changesetsDir;
5058
}
5159

5260
String changelog = generateChangelog(packageName, version, changesets);
5361

62+
Path changelogFile;
5463
try {
55-
writeChangelog(changelog);
64+
changelogFile = writeChangelog(changelog);
5665
} catch (ChangelogException exception) {
5766
LOG.error("Failed to update changelog at {}", changesetsDir, exception);
67+
return changesetsDir;
5868
}
5969

6070
changesets.forEach(changeset -> {
@@ -65,26 +75,25 @@ public void mergeChangesetsToChangelog(String packageName, String version) {
6575
LOG.error("Failed to delete {}", file, e);
6676
}
6777
});
78+
return changelogFile;
6879
}
6980

70-
private static String generateChangelog(String packageName, String version, List<Changeset> changesets) {
81+
private String generateChangelog(String packageName, String version, List<Changeset> changesets) {
7182
String changes = changesets
7283
.stream()
7384
.collect(groupingBy(Changeset::level, mapping(Changeset::message, toList())))
7485
.entrySet()
7586
.stream()
7687
.sorted(sortChangesets())
7788
.map(entry -> {
78-
String level = entry.getKey().getPresentationString();
79-
String levelChanges = entry.getValue().stream()
80-
.map(ChangelogAggregator::formatChangeAsBulletPoint)
81-
.sorted()
82-
.collect(Collectors.joining("\n"));
89+
Level level = entry.getKey();
90+
String levelString = level.getPresentationString();
91+
String levelChanges = formatChangeset(level, entry.getValue());
8392

8493
return """
85-
### %s Changes
94+
### %s
8695
87-
%s""".formatted(level, levelChanges);
96+
%s""".formatted(levelString, levelChanges);
8897
})
8998
.collect(Collectors.joining("\n\n"));
9099

@@ -99,6 +108,23 @@ private static String generateChangelog(String packageName, String version, List
99108
return MarkdownFormatter.format(markdown);
100109
}
101110

111+
private String formatChangeset(Level level, List<String> changes) {
112+
if (level == DEPENDENCY) {
113+
// Extract all dependencies from each dependency change and put them into a single list
114+
return changes.stream()
115+
.flatMap(change -> dependencyUpdatesParser.parseDependencyChangeset(change).stream())
116+
.map(ChangelogAggregator::formatChangeAsBulletPoint)
117+
.distinct()
118+
.sorted()
119+
.collect(Collectors.joining("\n"));
120+
}
121+
122+
return changes.stream()
123+
.map(ChangelogAggregator::formatChangeAsBulletPoint)
124+
.sorted()
125+
.collect(Collectors.joining("\n"));
126+
}
127+
102128
private static String formatChangeAsBulletPoint(String change) {
103129
// Add the change as a bullet point, with leading dash and each subsequent line indented with two spaces
104130
String firstLinePrefix = "- ";
@@ -118,7 +144,7 @@ private static String formatChangeAsBulletPoint(String change) {
118144
}
119145

120146
private static Comparator<Map.Entry<Level, List<String>>> sortChangesets() {
121-
List<Level> levelOrder = List.of(MAJOR, MINOR, PATCH);
147+
List<Level> levelOrder = List.of(MAJOR, MINOR, PATCH, DEPENDENCY);
122148

123149
return (o1, o2) -> {
124150
// Sort levels in the order specified in levelOrder
@@ -135,9 +161,10 @@ private static Comparator<Map.Entry<Level, List<String>>> sortChangesets() {
135161
* If the file already exists, trim the first header before prepending it with the new changelog entries.
136162
*
137163
* @param changelog The new changelog content
164+
* @return
138165
* @throws ChangelogException Thrown if file operations on the existing or new file are unsuccessful
139166
*/
140-
private void writeChangelog(String changelog) throws ChangelogException {
167+
private Path writeChangelog(String changelog) throws ChangelogException {
141168
Path changelogFile = this.baseDir.resolve(CHANGELOG_FILE);
142169

143170
if (Files.exists(changelogFile)) {
@@ -146,6 +173,7 @@ private void writeChangelog(String changelog) throws ChangelogException {
146173

147174
try {
148175
Files.writeString(changelogFile, changelog, TRUNCATE_EXISTING, CREATE);
176+
return changelogFile;
149177

150178
} catch (IOException e) {
151179
throw new ChangelogException("Failed to write " + changelogFile, e);

changesets-java/src/main/java/se/fortnox/changesets/ChangesetParser.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ public static List<Changeset> parseFile(File file) {
3636

3737
// Translate to enum
3838
Level level = switch (levelString) {
39+
case "dependency" -> Level.DEPENDENCY;
3940
case "patch" -> Level.PATCH;
4041
case "minor" -> Level.MINOR;
4142
case "major" -> Level.MAJOR;
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package se.fortnox.changesets;
2+
3+
import com.vladsch.flexmark.ast.Paragraph;
4+
import com.vladsch.flexmark.parser.Parser;
5+
import com.vladsch.flexmark.util.ast.Document;
6+
import com.vladsch.flexmark.util.ast.Node;
7+
import com.vladsch.flexmark.util.collection.iteration.ReversiblePeekingIterable;
8+
import org.slf4j.Logger;
9+
10+
import java.util.List;
11+
import java.util.stream.StreamSupport;
12+
13+
import static org.slf4j.LoggerFactory.getLogger;
14+
15+
public class DependencyUpdatesParser {
16+
private static final Logger LOG = getLogger(DependencyUpdatesParser.class);
17+
18+
public List<String> parseDependencyChangeset(String change) {
19+
Parser parser = Parser.builder().build();
20+
Document parsed = parser.parse(change);
21+
22+
if (!parsed.hasChildren()) {
23+
return List.of();
24+
}
25+
26+
String nodeName = parsed.getFirstChild().getNodeName();
27+
if (!nodeName.equals("BulletList")) {
28+
LOG.warn("Unexpected node type {}", nodeName);
29+
System.out.println();
30+
return List.of();
31+
}
32+
33+
ReversiblePeekingIterable<Node> dependencyNodes = parsed.getFirstChild().getChildren();
34+
35+
return StreamSupport.stream(dependencyNodes.spliterator(), false)
36+
.map(node -> {
37+
Paragraph paragraph = (Paragraph)node.getChildOfType(Paragraph.class);
38+
return paragraph.getChars().toString().trim();
39+
})
40+
.distinct()
41+
.toList();
42+
}
43+
}

changesets-java/src/main/java/se/fortnox/changesets/Level.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
package se.fortnox.changesets;
22

33
public enum Level {
4-
MAJOR("major", "Major"),
5-
MINOR("minor", "Minor"),
6-
PATCH("patch", "Patch");
4+
MAJOR("major", "Major Changes"),
5+
MINOR("minor", "Minor Changes"),
6+
PATCH("patch", "Patch Changes"),
7+
DEPENDENCY("dependency", "Dependency Updates");
78

89
private final String textValue;
910
private final String presentationString;

changesets-java/src/main/java/se/fortnox/changesets/MarkdownFormatter.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,14 @@ public class MarkdownFormatter {
2020
public static String format(String markdown) {
2121
MutableDataSet formatOptions = new MutableDataSet();
2222
// Clean up whitespaces in different ways
23-
formatOptions.set(FORMAT_FLAGS, LineAppendable.F_FORMAT_ALL);
23+
// Do not use F_TRIM_TRAILING_WHITESPACE as it is required for line breaks in bullet lists
24+
int format = LineAppendable.F_CONVERT_TABS |
25+
// LineAppendable.F_COLLAPSE_WHITESPACE;
26+
// LineAppendable.F_TRIM_TRAILING_WHITESPACE |
27+
LineAppendable.F_TRIM_LEADING_WHITESPACE |
28+
LineAppendable.F_TRIM_LEADING_EOL
29+
;
30+
formatOptions.set(FORMAT_FLAGS, format);
2431

2532
// Limit line lengths
2633
formatOptions.set(RIGHT_MARGIN, 120);

changesets-java/src/test/java/se/fortnox/changesets/ChangelogAggregatorTest.java

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,4 +256,48 @@ void shouldFormatMarkdown(@TempDir Path tempDir) throws FileAlreadyExistsExcepti
256256
257257
""");
258258
}
259+
260+
@Test
261+
void shouldAggregateDependencyUpdates(@TempDir Path tempDir) throws FileAlreadyExistsException {
262+
ChangelogAggregator changelog = new ChangelogAggregator(tempDir);
263+
264+
ChangesetWriter changesetWriter = new ChangesetWriter(tempDir);
265+
changesetWriter.writeChangeset(PACKAGE_NAME, Level.DEPENDENCY, "- Some dependency\n- Another dependency");
266+
changesetWriter.writeChangeset(PACKAGE_NAME, Level.DEPENDENCY, "- Third dependency");
267+
changesetWriter.writeChangeset(PACKAGE_NAME, Level.DEPENDENCY, " - Differently indented");
268+
changesetWriter.writeChangeset(PACKAGE_NAME, Level.DEPENDENCY, "- Fourth dependency\n - Fifth dependency");
269+
changesetWriter.writeChangeset(PACKAGE_NAME, Level.DEPENDENCY, "- Multiline dependency \n that should be kept as a single item");
270+
changesetWriter.writeChangeset(PACKAGE_NAME, Level.DEPENDENCY, "- Multi \nline");
271+
272+
assertThat(tempDir.resolve(ChangesetWriter.CHANGESET_DIR))
273+
.exists()
274+
.isDirectory()
275+
.isDirectoryContaining(path -> path.toFile().getName().endsWith(".md"));
276+
277+
changelog.mergeChangesetsToChangelog(PACKAGE_NAME, "1.0.0");
278+
279+
assertThat(tempDir.resolve(CHANGELOG_FILE))
280+
.exists()
281+
.isRegularFile()
282+
.content()
283+
.isEqualTo("""
284+
# my-package
285+
286+
## 1.0.0
287+
288+
### Dependency Updates
289+
290+
- Another dependency
291+
- Differently indented
292+
- Fifth dependency
293+
- Fourth dependency
294+
- Multi\s\s
295+
line
296+
- Multiline dependency\s\s
297+
that should be kept as a single item
298+
- Some dependency
299+
- Third dependency
300+
301+
""");
302+
}
259303
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package se.fortnox.changesets;
2+
3+
import org.junit.jupiter.api.Test;
4+
import org.junit.jupiter.api.io.TempDir;
5+
6+
import java.io.IOException;
7+
import java.nio.file.Files;
8+
import java.nio.file.Path;
9+
10+
import static org.assertj.core.api.Assertions.assertThat;
11+
12+
class CompareTest {
13+
@Test
14+
void shouldGenerateChangelogForReactiveWizard(@TempDir Path tempDir) throws IOException {
15+
Path changesetsPath = Path.of("src/test/resources/changesets/reactive-wizard/.changeset");
16+
17+
Path changesetsTarget = tempDir.resolve(".changeset");
18+
changesetsTarget.toFile().mkdir();
19+
20+
String[] files = changesetsPath.toFile().list();
21+
for (String file : files) {
22+
Files.copy(changesetsPath.resolve(file), changesetsTarget.resolve(file));
23+
}
24+
25+
assertThat(changesetsTarget.toFile().list())
26+
.isNotEmpty()
27+
.containsExactly(changesetsPath.toFile().list());
28+
29+
ChangelogAggregator changelog = new ChangelogAggregator(tempDir);
30+
Path changelogFile = changelog.mergeChangesetsToChangelog("reactivewizard-parent", "26.0.0");
31+
32+
String actualChangelogText = Files.readString(changelogFile);
33+
String expectedChangelogText = Files.readString(changesetsPath.resolve("../CHANGELOG-expected.md"));
34+
35+
assertThat(actualChangelogText)
36+
.isEqualTo(expectedChangelogText);
37+
38+
}
39+
@Test
40+
void shouldGenerateChangelogForReactiveWizardWithDependencyTypes(@TempDir Path tempDir) throws IOException {
41+
Path changesetsPath = Path.of("src/test/resources/changesets/reactive-wizard-with-dependencies/.changeset");
42+
43+
Path changesetsTarget = tempDir.resolve(".changeset");
44+
changesetsTarget.toFile().mkdir();
45+
46+
String[] files = changesetsPath.toFile().list();
47+
for (String file : files) {
48+
Files.copy(changesetsPath.resolve(file), changesetsTarget.resolve(file));
49+
}
50+
51+
assertThat(changesetsTarget.toFile().list())
52+
.isNotEmpty()
53+
.containsExactly(changesetsPath.toFile().list());
54+
55+
ChangelogAggregator changelog = new ChangelogAggregator(tempDir);
56+
Path changelogFile = changelog.mergeChangesetsToChangelog("reactivewizard-parent", "26.0.0");
57+
58+
String actualChangelogText = Files.readString(changelogFile);
59+
String expectedChangelogText = Files.readString(changesetsPath.resolve("../CHANGELOG-expected.md"));
60+
61+
assertThat(actualChangelogText)
62+
.isEqualTo(expectedChangelogText);
63+
64+
}
65+
}

0 commit comments

Comments
 (0)