Skip to content

Commit 9b0ce73

Browse files
authored
release changelog automation (#824)
1 parent c6dbc41 commit 9b0ce73

File tree

5 files changed

+330
-19
lines changed

5 files changed

+330
-19
lines changed

.ci/ReleaseChangelog.java

Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
import java.io.IOException;
21+
import java.nio.charset.StandardCharsets;
22+
import java.nio.file.Files;
23+
import java.nio.file.Path;
24+
import java.nio.file.Paths;
25+
import java.time.LocalDate;
26+
import java.time.format.DateTimeFormatter;
27+
import java.util.ArrayList;
28+
import java.util.Comparator;
29+
import java.util.List;
30+
import java.util.Locale;
31+
import java.util.OptionalInt;
32+
import java.util.function.Predicate;
33+
import java.util.regex.Matcher;
34+
import java.util.regex.Pattern;
35+
36+
public class ReleaseChangelog {
37+
38+
public static void main(String[] args) throws IOException {
39+
if (args.length != 3) {
40+
System.out.println(
41+
"Expected exactly three arguments: <ChangelogFile> <ReleaseNotesPath> <VersionToRelease>");
42+
System.exit(-1);
43+
}
44+
Path nextChangelogFile = Paths.get(args[0]);
45+
Path releaseNotesDir = Paths.get(args[1]);
46+
Path releaseNotesFile = releaseNotesDir.resolve("index.md");
47+
Path deprecationsFile = releaseNotesDir.resolve("deprecations.md");
48+
Path breakingChangesFile = releaseNotesDir.resolve("breaking-changes.md");
49+
VersionNumber version = VersionNumber.parse(args[2].trim());
50+
51+
Lines nextChangelogLines = new Lines(
52+
Files.readAllLines(nextChangelogFile, StandardCharsets.UTF_8));
53+
Lines fixes = nextChangelogLines.cutLinesBetween("<!--FIXES-START-->", "<!--FIXES-END-->");
54+
Lines enhancements = nextChangelogLines.cutLinesBetween("<!--ENHANCEMENTS-START-->",
55+
"<!--ENHANCEMENTS-END-->");
56+
Lines deprecations = nextChangelogLines.cutLinesBetween("<!--DEPRECATIONS-START-->",
57+
"<!--DEPRECATIONS-END-->");
58+
Lines breakingChanges = nextChangelogLines.cutLinesBetween("<!--BREAKING-CHANGES-START-->",
59+
"<!--BREAKING-CHANGES-END-->");
60+
61+
Lines dependenciesNotes = nextChangelogLines.cutLinesBetween("<!--DEPENDENCIES-NOTES-START-->",
62+
"<!--DEPENDENCIES-NOTES-END-->");
63+
64+
var formatter = DateTimeFormatter.ofPattern("LLLL d, yyyy", Locale.ENGLISH);
65+
String releaseDateLine = "**Release date:** " + formatter.format(LocalDate.now());
66+
67+
Lines allReleaseNotes = new Lines(Files.readAllLines(releaseNotesFile, StandardCharsets.UTF_8));
68+
int insertBeforeLine = findHeadingOfPreviousVersion(allReleaseNotes, version);
69+
if (insertBeforeLine < 0) {
70+
insertBeforeLine = allReleaseNotes.lineCount();
71+
}
72+
allReleaseNotes.insert(
73+
generateReleaseNotes(version, releaseDateLine, enhancements, fixes, breakingChanges,
74+
dependenciesNotes),
75+
insertBeforeLine);
76+
77+
if (!deprecations.isEmpty()) {
78+
Lines allDeprecations = new Lines(
79+
Files.readAllLines(deprecationsFile, StandardCharsets.UTF_8));
80+
int insertDepsBeforeLine = findHeadingOfPreviousVersion(allDeprecations, version);
81+
if (insertDepsBeforeLine < 0) {
82+
// in case no previous version was listed
83+
insertDepsBeforeLine = allDeprecations.lineCount();
84+
}
85+
allDeprecations.insert(generateDeprecations(version, releaseDateLine, deprecations),
86+
insertDepsBeforeLine);
87+
Files.writeString(deprecationsFile, allDeprecations + "\n", StandardCharsets.UTF_8);
88+
}
89+
if (!breakingChanges.isEmpty()) {
90+
Lines allBreakingChanges = new Lines(
91+
Files.readAllLines(breakingChangesFile, StandardCharsets.UTF_8));
92+
int insertBcBeforeLine = findHeadingOfPreviousVersion(allBreakingChanges, version);
93+
if (insertBcBeforeLine < 0) {
94+
// in case no previous version was listed
95+
insertBcBeforeLine = allBreakingChanges.lineCount();
96+
}
97+
allBreakingChanges.insert(generateBreakingChanges(version, releaseDateLine, breakingChanges),
98+
insertBcBeforeLine);
99+
Files.writeString(breakingChangesFile, allBreakingChanges + "\n", StandardCharsets.UTF_8);
100+
}
101+
Files.writeString(releaseNotesFile, allReleaseNotes + "\n", StandardCharsets.UTF_8);
102+
Files.writeString(nextChangelogFile, nextChangelogLines + "\n", StandardCharsets.UTF_8);
103+
}
104+
105+
private static Lines generateReleaseNotes(VersionNumber version, String releaseDateLine,
106+
Lines enhancements, Lines fixes, Lines breaking, Lines dependenciesNotes) {
107+
Lines result = new Lines()
108+
.append("## " + version.dotStr() + " [edot-java-" + version.dashStr() + "-release-notes]")
109+
.append(releaseDateLine);
110+
if (!enhancements.isEmpty()) {
111+
result
112+
.append("")
113+
.append("### Features and enhancements [edot-java-" + version.dashStr()
114+
+ "-features-enhancements]")
115+
.append(enhancements);
116+
}
117+
if (!fixes.isEmpty()) {
118+
result
119+
.append("")
120+
.append("### Fixes [edot-java-" + version.dashStr() + "-fixes]")
121+
.append(fixes);
122+
}
123+
if (!breaking.isEmpty()) {
124+
result
125+
.append("")
126+
.append("### Breaking changes [edot-java-" + version.dashStr() + "-fixes]")
127+
.append(breaking);
128+
}
129+
if (!dependenciesNotes.isEmpty()) {
130+
result
131+
.append("")
132+
.append(dependenciesNotes);
133+
}
134+
result.append("");
135+
return result;
136+
}
137+
138+
private static Lines generateDeprecations(VersionNumber version, String releaseDateLine,
139+
Lines deprecations) {
140+
return new Lines()
141+
.append("## " + version.dotStr() + " [edot-java-" + version.dashStr() + "-deprecations]")
142+
.append(releaseDateLine)
143+
.append("")
144+
.append(deprecations)
145+
.append("");
146+
}
147+
148+
private static Lines generateBreakingChanges(VersionNumber version, String releaseDateLine,
149+
Lines breakingChanges) {
150+
return new Lines()
151+
.append("## " + version.dotStr() + " [" + version.dotStr() + "]")
152+
.append("")
153+
.append(releaseDateLine)
154+
.append("")
155+
.append(breakingChanges)
156+
.append("");
157+
}
158+
159+
static int findHeadingOfPreviousVersion(Lines lines, VersionNumber version) {
160+
Pattern headingPattern = Pattern.compile("## (\\d+\\.\\d+\\.\\d+) .*");
161+
Comparator<VersionNumber> comp = VersionNumber.comparator();
162+
int currentBestLineNo = -1;
163+
VersionNumber currentBestVersion = null;
164+
for (int i = 0; i < lines.lineCount(); i++) {
165+
Matcher matcher = headingPattern.matcher(lines.getLine(i));
166+
if (matcher.matches()) {
167+
VersionNumber headingForVersion = VersionNumber.parse(matcher.group(1));
168+
if (comp.compare(headingForVersion, version) < 0
169+
&& (currentBestVersion == null
170+
|| comp.compare(headingForVersion, currentBestVersion) > 0)) {
171+
currentBestLineNo = i;
172+
currentBestVersion = headingForVersion;
173+
}
174+
}
175+
}
176+
return currentBestLineNo;
177+
}
178+
179+
record VersionNumber(int major, int minor, int patch) {
180+
public static VersionNumber parse (String versionString){
181+
if (!versionString.matches("\\d+\\.\\d+\\.\\d+")) {
182+
throw new IllegalArgumentException(
183+
"Version must be in the format x.x.x but was not: " + versionString);
184+
}
185+
String[] parts = versionString.split("\\.");
186+
return new VersionNumber(Integer.parseInt(parts[0]), Integer.parseInt(parts[1]),
187+
Integer.parseInt(parts[2]));
188+
}
189+
190+
static Comparator<VersionNumber> comparator () {
191+
return Comparator
192+
.comparing(VersionNumber::major)
193+
.thenComparing(VersionNumber::minor)
194+
.thenComparing(VersionNumber::patch);
195+
}
196+
197+
String dashStr () {
198+
return major + "-" + minor + "-" + patch;
199+
}
200+
201+
String dotStr () {
202+
return major + "." + minor + "." + patch;
203+
}
204+
205+
}
206+
207+
static class Lines {
208+
209+
private final List<String> lines;
210+
211+
public Lines() {
212+
this.lines = new ArrayList<>();
213+
}
214+
215+
public Lines(List<String> lines) {
216+
this.lines = new ArrayList<>(lines);
217+
}
218+
219+
int lineCount() {
220+
return lines.size();
221+
}
222+
223+
boolean isEmpty() {
224+
return lines.isEmpty();
225+
}
226+
227+
Lines cutLinesBetween(String startLine, String endLine) {
228+
int start = findLine(l -> l.trim().equals(startLine), 0)
229+
.orElseThrow(
230+
() -> new IllegalStateException("Expected line '" + startLine + "' to exist"));
231+
int end = findLine(l -> l.trim().equals(endLine), start + 1)
232+
.orElseThrow(() -> new IllegalStateException(
233+
"Expected line '" + endLine + "' to exist after '" + startLine + "'"));
234+
Lines result = cut(start + 1, end).trim();
235+
236+
lines.add(start + 1, "");
237+
238+
return result;
239+
}
240+
241+
OptionalInt findLine(Predicate<String> condition, int startAt) {
242+
for (int i = startAt; i < lines.size(); i++) {
243+
if (condition.test(lines.get(i))) {
244+
return OptionalInt.of(i);
245+
}
246+
}
247+
return OptionalInt.empty();
248+
}
249+
250+
Lines cut(int startInclusive, int endExclusive) {
251+
List<String> cutLines = new ArrayList<>();
252+
for (int i = startInclusive; i < endExclusive; i++) {
253+
cutLines.add(lines.remove(startInclusive));
254+
}
255+
return new Lines(cutLines);
256+
}
257+
258+
void insert(Lines other, int insertAt) {
259+
this.lines.addAll(insertAt, other.lines);
260+
}
261+
262+
Lines append(String line) {
263+
lines.add(line);
264+
return this;
265+
}
266+
267+
Lines append(Lines toAppend) {
268+
lines.addAll(toAppend.lines);
269+
return this;
270+
}
271+
272+
/**
273+
* Trims lines consisting of only blanks at the top and bottom
274+
*/
275+
Lines trim() {
276+
while (!lines.isEmpty() && lines.get(0).matches("\\s*")) {
277+
lines.remove(0);
278+
}
279+
while (!lines.isEmpty() && lines.get(lines.size() - 1).matches("\\s*")) {
280+
lines.remove(lines.size() - 1);
281+
}
282+
return this;
283+
}
284+
285+
@Override
286+
public String toString() {
287+
return String.join("\n", lines);
288+
}
289+
290+
public String getLine(int number) {
291+
return lines.get(number);
292+
}
293+
}
294+
295+
}

.github/workflows/pre-post-release.yml

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,6 @@ on:
2626
description: 'pull-request body'
2727
type: string
2828
required: true
29-
changelog:
30-
description: 'The changelog to prepend to CHANGELOG.md without heading'
31-
type: string
32-
required: false
33-
default: ''
3429

3530
env:
3631
RELEASE_VERSION: ${{ inputs.version }}
@@ -100,21 +95,14 @@ jobs:
10095
with:
10196
command: ./gradlew -q setNextVersion
10297

103-
- name: Insert notes into cumulative changelog (post release)
104-
if: inputs.phase == 'post'
105-
run: |
106-
echo "# ${VERSION} - $(date +'%d/%m/%Y')" > tmpchangelog
107-
echo "${CHANGELOG}" >> tmpchangelog
108-
cat CHANGELOG.md >> tmpchangelog
109-
mv tmpchangelog CHANGELOG.md
110-
env:
111-
VERSION: ${{ inputs.version }}
112-
CHANGELOG: ${{ inputs.changelog }}
113-
114-
- name: Clear next release changelog (post release)
98+
- name: Generate documentation changelog (post release)
11599
if: inputs.phase == 'post'
116100
run: |
117-
echo '' > CHANGELOG.next-release.md
101+
echo '<!--DEPENDENCIES-NOTES-START-->' >> CHANGELOG.next-release.md
102+
echo -e "This release is based on the following upstream versions:\n\n" >> CHANGELOG.next-release.md
103+
./gradlew -q printUpstreamDependenciesMarkdown >> CHANGELOG.next-release.md
104+
echo '<!--DEPENDENCIES-NOTES-END-->' >> CHANGELOG.next-release.md
105+
java .ci/ReleaseChangelog.java CHANGELOG.next-release.md docs/release-notes ${{ env.RELEASE_VERSION }}
118106
119107
- name: Push the ${{ inputs.phase }} release branch
120108
run: |

.github/workflows/release-step-3.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,8 @@ jobs:
218218
command: ""
219219
- name: Print Release Notes
220220
id: print_release_notes
221+
# note: release notes here will be copied as-is from 'CHANGELOG.next-release.md'
222+
# the 'pre-post-release' workflow executed after this will reset contents of 'CHANGELOG.next-release.md'
221223
run: |
222224
echo 'notes<<RELNOTESEOF' >> $GITHUB_OUTPUT
223225
cat CHANGELOG.next-release.md >> $GITHUB_OUTPUT

CHANGELOG.next-release.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,29 @@
1+
This file contains all changes which are not released yet.
2+
<!--
3+
Note that the content between the marker comment lines (e.g. FIXES-START/END) will be automatically
4+
moved into the docs/release-notes markdown files on release (via the .ci/ReleaseChangelog.java script).
5+
Simply add the changes as bullet points into those sections, empty lines will be ignored. Example:
6+
7+
* Description of the change - [#1234](https://github.com/elastic/apm-agent-java/pull/1234)
8+
-->
9+
10+
# Fixes
11+
<!--FIXES-START-->
12+
13+
<!--FIXES-END-->
14+
# Features and enhancements
15+
<!--ENHANCEMENTS-START-->
116
* Add support for dynamic configuration options for 9.2 #818
217
* Switch upstream Opamp client #789
18+
19+
<!--ENHANCEMENTS-END-->
20+
# Deprecations
21+
<!--DEPRECATIONS-START-->
22+
23+
<!--DEPRECATIONS-END-->
24+
25+
# Breaking Changes
26+
<!--BREAKING-CHANGES-START-->
327
* Switch to upstream instrumentation of openai by default #763
28+
29+
<!--BREAKING-CHANGES-END-->

docs/release-notes/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ To check for security updates, go to [Security announcements for the Elastic sta
2727
% ### Fixes [edot-java-X.X.X-fixes]
2828
% *
2929

30-
# 1.5.0 [edot-java-1.5.0-release-notes]
30+
## 1.5.0 [edot-java-1.5.0-release-notes]
3131

3232
### Features and enhancements [edot-java-1.5.0-features-enhancements]
3333

0 commit comments

Comments
 (0)