-
Notifications
You must be signed in to change notification settings - Fork 23
release changelog automation #824
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
3efbc8c
e87353a
1ed7604
b64d3ce
6ceea87
5cef3be
30d1c6b
b768c2e
7302496
efca93f
4e1add2
e5c9a9c
be6bf58
cc2cbb2
bfe05f1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,295 @@ | ||
| /* | ||
| * Licensed to Elasticsearch B.V. under one or more contributor | ||
| * license agreements. See the NOTICE file distributed with | ||
| * this work for additional information regarding copyright | ||
| * ownership. Elasticsearch B.V. licenses this file to you under | ||
| * the Apache License, Version 2.0 (the "License"); you may | ||
| * not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, | ||
| * software distributed under the License is distributed on an | ||
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
| * KIND, either express or implied. See the License for the | ||
| * specific language governing permissions and limitations | ||
| * under the License. | ||
| */ | ||
|
|
||
| import java.io.IOException; | ||
| import java.nio.charset.StandardCharsets; | ||
| import java.nio.file.Files; | ||
| import java.nio.file.Path; | ||
| import java.nio.file.Paths; | ||
| import java.time.LocalDate; | ||
| import java.time.format.DateTimeFormatter; | ||
| import java.util.ArrayList; | ||
| import java.util.Comparator; | ||
| import java.util.List; | ||
| import java.util.Locale; | ||
| import java.util.OptionalInt; | ||
| import java.util.function.Predicate; | ||
| import java.util.regex.Matcher; | ||
| import java.util.regex.Pattern; | ||
|
|
||
| public class ReleaseChangelog { | ||
|
|
||
| public static void main(String[] args) throws IOException { | ||
| if (args.length != 3) { | ||
| System.out.println( | ||
| "Expected exactly three arguments: <ChangelogFile> <ReleaseNotesPath> <VersionToRelease>"); | ||
| System.exit(-1); | ||
| } | ||
| Path nextChangelogFile = Paths.get(args[0]); | ||
| Path releaseNotesDir = Paths.get(args[1]); | ||
| Path releaseNotesFile = releaseNotesDir.resolve("index.md"); | ||
| Path deprecationsFile = releaseNotesDir.resolve("deprecations.md"); | ||
| Path breakingChangesFile = releaseNotesDir.resolve("breaking-changes.md"); | ||
| VersionNumber version = VersionNumber.parse(args[2].trim()); | ||
|
|
||
| Lines nextChangelogLines = new Lines( | ||
| Files.readAllLines(nextChangelogFile, StandardCharsets.UTF_8)); | ||
| Lines fixes = nextChangelogLines.cutLinesBetween("<!--FIXES-START-->", "<!--FIXES-END-->"); | ||
| Lines enhancements = nextChangelogLines.cutLinesBetween("<!--ENHANCEMENTS-START-->", | ||
| "<!--ENHANCEMENTS-END-->"); | ||
| Lines deprecations = nextChangelogLines.cutLinesBetween("<!--DEPRECATIONS-START-->", | ||
| "<!--DEPRECATIONS-END-->"); | ||
| Lines breakingChanges = nextChangelogLines.cutLinesBetween("<!--BREAKING-CHANGES-START-->", | ||
| "<!--BREAKING-CHANGES-END-->"); | ||
|
|
||
| Lines dependenciesNotes = nextChangelogLines.cutLinesBetween("<!--DEPENDENCIES-NOTES-START-->", | ||
| "<!--DEPENDENCIES-NOTES-END-->"); | ||
|
Comment on lines
+61
to
+62
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [for reviewer] this section does not exists in the next release changelog template, we have to append it manually at runtime. This is due to the fact that we can't easily invoke the gradle task from here, doing everything from gradle in the future would simplify this. |
||
|
|
||
| var formatter = DateTimeFormatter.ofPattern("LLLL d, yyyy", Locale.ENGLISH); | ||
| String releaseDateLine = "**Release date:** " + formatter.format(LocalDate.now()); | ||
|
|
||
| Lines allReleaseNotes = new Lines(Files.readAllLines(releaseNotesFile, StandardCharsets.UTF_8)); | ||
| int insertBeforeLine = findHeadingOfPreviousVersion(allReleaseNotes, version); | ||
| if (insertBeforeLine < 0) { | ||
| insertBeforeLine = allReleaseNotes.lineCount(); | ||
| } | ||
| allReleaseNotes.insert( | ||
| generateReleaseNotes(version, releaseDateLine, enhancements, fixes, breakingChanges, | ||
| dependenciesNotes), | ||
| insertBeforeLine); | ||
|
|
||
| if (!deprecations.isEmpty()) { | ||
| Lines allDeprecations = new Lines( | ||
| Files.readAllLines(deprecationsFile, StandardCharsets.UTF_8)); | ||
| int insertDepsBeforeLine = findHeadingOfPreviousVersion(allDeprecations, version); | ||
| if (insertDepsBeforeLine < 0) { | ||
| // in case no previous version was listed | ||
| insertDepsBeforeLine = allDeprecations.lineCount(); | ||
| } | ||
| allDeprecations.insert(generateDeprecations(version, releaseDateLine, deprecations), | ||
| insertDepsBeforeLine); | ||
| Files.writeString(deprecationsFile, allDeprecations + "\n", StandardCharsets.UTF_8); | ||
| } | ||
| if (!breakingChanges.isEmpty()) { | ||
| Lines allBreakingChanges = new Lines( | ||
| Files.readAllLines(breakingChangesFile, StandardCharsets.UTF_8)); | ||
| int insertBcBeforeLine = findHeadingOfPreviousVersion(allBreakingChanges, version); | ||
| if (insertBcBeforeLine < 0) { | ||
| // in case no previous version was listed | ||
| insertBcBeforeLine = allBreakingChanges.lineCount(); | ||
| } | ||
| allBreakingChanges.insert(generateBreakingChanges(version, releaseDateLine, breakingChanges), | ||
| insertBcBeforeLine); | ||
| Files.writeString(breakingChangesFile, allBreakingChanges + "\n", StandardCharsets.UTF_8); | ||
| } | ||
| Files.writeString(releaseNotesFile, allReleaseNotes + "\n", StandardCharsets.UTF_8); | ||
| Files.writeString(nextChangelogFile, nextChangelogLines + "\n", StandardCharsets.UTF_8); | ||
| } | ||
|
|
||
| private static Lines generateReleaseNotes(VersionNumber version, String releaseDateLine, | ||
| Lines enhancements, Lines fixes, Lines breaking, Lines dependenciesNotes) { | ||
| Lines result = new Lines() | ||
| .append("## " + version.dotStr() + " [edot-java-" + version.dashStr() + "-release-notes]") | ||
| .append(releaseDateLine); | ||
| if (!enhancements.isEmpty()) { | ||
| result | ||
| .append("") | ||
| .append("### Features and enhancements [edot-java-" + version.dashStr() | ||
| + "-features-enhancements]") | ||
| .append(enhancements); | ||
| } | ||
| if (!fixes.isEmpty()) { | ||
| result | ||
| .append("") | ||
| .append("### Fixes [edot-java-" + version.dashStr() + "-fixes]") | ||
| .append(fixes); | ||
| } | ||
| if (!breaking.isEmpty()) { | ||
| result | ||
| .append("") | ||
| .append("### Breaking changes [edot-java-" + version.dashStr() + "-fixes]") | ||
| .append(breaking); | ||
| } | ||
| if (!dependenciesNotes.isEmpty()) { | ||
| result | ||
| .append("") | ||
| .append(dependenciesNotes); | ||
| } | ||
| result.append(""); | ||
| return result; | ||
| } | ||
|
|
||
| private static Lines generateDeprecations(VersionNumber version, String releaseDateLine, | ||
| Lines deprecations) { | ||
| return new Lines() | ||
| .append("## " + version.dotStr() + " [edot-java-" + version.dashStr() + "-deprecations]") | ||
| .append(releaseDateLine) | ||
| .append("") | ||
| .append(deprecations) | ||
| .append(""); | ||
| } | ||
|
|
||
| private static Lines generateBreakingChanges(VersionNumber version, String releaseDateLine, | ||
| Lines breakingChanges) { | ||
| return new Lines() | ||
| .append("## " + version.dotStr() + " [" + version.dotStr() + "]") | ||
| .append("") | ||
| .append(releaseDateLine) | ||
| .append("") | ||
| .append(breakingChanges) | ||
| .append(""); | ||
| } | ||
|
|
||
| static int findHeadingOfPreviousVersion(Lines lines, VersionNumber version) { | ||
| Pattern headingPattern = Pattern.compile("## (\\d+\\.\\d+\\.\\d+) .*"); | ||
| Comparator<VersionNumber> comp = VersionNumber.comparator(); | ||
| int currentBestLineNo = -1; | ||
| VersionNumber currentBestVersion = null; | ||
| for (int i = 0; i < lines.lineCount(); i++) { | ||
| Matcher matcher = headingPattern.matcher(lines.getLine(i)); | ||
| if (matcher.matches()) { | ||
| VersionNumber headingForVersion = VersionNumber.parse(matcher.group(1)); | ||
| if (comp.compare(headingForVersion, version) < 0 | ||
| && (currentBestVersion == null | ||
| || comp.compare(headingForVersion, currentBestVersion) > 0)) { | ||
| currentBestLineNo = i; | ||
| currentBestVersion = headingForVersion; | ||
| } | ||
| } | ||
| } | ||
| return currentBestLineNo; | ||
| } | ||
|
|
||
| record VersionNumber(int major, int minor, int patch) { | ||
| public static VersionNumber parse (String versionString){ | ||
| if (!versionString.matches("\\d+\\.\\d+\\.\\d+")) { | ||
| throw new IllegalArgumentException( | ||
| "Version must be in the format x.x.x but was not: " + versionString); | ||
| } | ||
| String[] parts = versionString.split("\\."); | ||
| return new VersionNumber(Integer.parseInt(parts[0]), Integer.parseInt(parts[1]), | ||
| Integer.parseInt(parts[2])); | ||
| } | ||
|
|
||
| static Comparator<VersionNumber> comparator () { | ||
| return Comparator | ||
| .comparing(VersionNumber::major) | ||
| .thenComparing(VersionNumber::minor) | ||
| .thenComparing(VersionNumber::patch); | ||
| } | ||
|
|
||
| String dashStr () { | ||
| return major + "-" + minor + "-" + patch; | ||
| } | ||
|
|
||
| String dotStr () { | ||
| return major + "." + minor + "." + patch; | ||
| } | ||
|
|
||
| } | ||
|
|
||
| static class Lines { | ||
|
|
||
| private final List<String> lines; | ||
|
|
||
| public Lines() { | ||
| this.lines = new ArrayList<>(); | ||
| } | ||
|
|
||
| public Lines(List<String> lines) { | ||
| this.lines = new ArrayList<>(lines); | ||
| } | ||
|
|
||
| int lineCount() { | ||
| return lines.size(); | ||
| } | ||
|
|
||
| boolean isEmpty() { | ||
| return lines.isEmpty(); | ||
| } | ||
|
|
||
| Lines cutLinesBetween(String startLine, String endLine) { | ||
| int start = findLine(l -> l.trim().equals(startLine), 0) | ||
| .orElseThrow( | ||
| () -> new IllegalStateException("Expected line '" + startLine + "' to exist")); | ||
| int end = findLine(l -> l.trim().equals(endLine), start + 1) | ||
| .orElseThrow(() -> new IllegalStateException( | ||
| "Expected line '" + endLine + "' to exist after '" + startLine + "'")); | ||
| Lines result = cut(start + 1, end).trim(); | ||
|
|
||
| lines.add(start + 1, ""); | ||
|
|
||
| return result; | ||
| } | ||
|
|
||
| OptionalInt findLine(Predicate<String> condition, int startAt) { | ||
| for (int i = startAt; i < lines.size(); i++) { | ||
| if (condition.test(lines.get(i))) { | ||
| return OptionalInt.of(i); | ||
| } | ||
| } | ||
| return OptionalInt.empty(); | ||
| } | ||
|
|
||
| Lines cut(int startInclusive, int endExclusive) { | ||
| List<String> cutLines = new ArrayList<>(); | ||
| for (int i = startInclusive; i < endExclusive; i++) { | ||
| cutLines.add(lines.remove(startInclusive)); | ||
| } | ||
| return new Lines(cutLines); | ||
| } | ||
|
|
||
| void insert(Lines other, int insertAt) { | ||
| this.lines.addAll(insertAt, other.lines); | ||
| } | ||
|
|
||
| Lines append(String line) { | ||
| lines.add(line); | ||
| return this; | ||
| } | ||
|
|
||
| Lines append(Lines toAppend) { | ||
| lines.addAll(toAppend.lines); | ||
| return this; | ||
| } | ||
|
|
||
| /** | ||
| * Trims lines consisting of only blanks at the top and bottom | ||
| */ | ||
| Lines trim() { | ||
| while (!lines.isEmpty() && lines.get(0).matches("\\s*")) { | ||
| lines.remove(0); | ||
| } | ||
| while (!lines.isEmpty() && lines.get(lines.size() - 1).matches("\\s*")) { | ||
| lines.remove(lines.size() - 1); | ||
| } | ||
| return this; | ||
| } | ||
|
|
||
| @Override | ||
| public String toString() { | ||
| return String.join("\n", lines); | ||
| } | ||
|
|
||
| public String getLine(int number) { | ||
| return lines.get(number); | ||
| } | ||
| } | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -26,11 +26,6 @@ on: | |
| description: 'pull-request body' | ||
| type: string | ||
| required: true | ||
| changelog: | ||
| description: 'The changelog to prepend to CHANGELOG.md without heading' | ||
| type: string | ||
| required: false | ||
| default: '' | ||
|
|
||
| env: | ||
| RELEASE_VERSION: ${{ inputs.version }} | ||
|
|
@@ -100,21 +95,14 @@ jobs: | |
| with: | ||
| command: ./gradlew -q setNextVersion | ||
|
|
||
| - name: Insert notes into cumulative changelog (post release) | ||
| if: inputs.phase == 'post' | ||
| run: | | ||
| echo "# ${VERSION} - $(date +'%d/%m/%Y')" > tmpchangelog | ||
| echo "${CHANGELOG}" >> tmpchangelog | ||
| cat CHANGELOG.md >> tmpchangelog | ||
| mv tmpchangelog CHANGELOG.md | ||
| env: | ||
| VERSION: ${{ inputs.version }} | ||
| CHANGELOG: ${{ inputs.changelog }} | ||
|
|
||
| - name: Clear next release changelog (post release) | ||
| - name: Generate documentation changelog (post release) | ||
| if: inputs.phase == 'post' | ||
| run: | | ||
| echo '' > CHANGELOG.next-release.md | ||
| echo '<!--DEPENDENCIES-NOTES-START-->' >> CHANGELOG.next-release.md | ||
| echo -e "This release is based on the following upstream versions:\n\n" >> CHANGELOG.next-release.md | ||
| ./gradlew -q printUpstreamDependenciesMarkdown >> CHANGELOG.next-release.md | ||
| echo '<!--DEPENDENCIES-NOTES-END-->' >> CHANGELOG.next-release.md | ||
|
Comment on lines
+101
to
+104
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what's the reason those headers/bottom are not also generated by
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| java .ci/ReleaseChangelog.java CHANGELOG.next-release.md docs/release-notes ${{ env.RELEASE_VERSION }} | ||
|
|
||
| - name: Push the ${{ inputs.phase }} release branch | ||
| run: | | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -218,6 +218,8 @@ jobs: | |
| command: "" | ||
| - name: Print Release Notes | ||
| id: print_release_notes | ||
| # note: release notes here will be copied as-is from 'CHANGELOG.next-release.md' | ||
| # the 'pre-post-release' workflow executed after this will reset contents of 'CHANGELOG.next-release.md' | ||
|
Comment on lines
+221
to
+222
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [for reviewer] here the github release changelog needs to be in pure markdown, so we can't append our custom markdown from breaking changes. |
||
| run: | | ||
| echo 'notes<<RELNOTESEOF' >> $GITHUB_OUTPUT | ||
| cat CHANGELOG.next-release.md >> $GITHUB_OUTPUT | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,29 @@ | ||
| This file contains all changes which are not released yet. | ||
| <!-- | ||
| Note that the content between the marker comment lines (e.g. FIXES-START/END) will be automatically | ||
| moved into the docs/release-notes markdown files on release (via the .ci/ReleaseChangelog.java script). | ||
| Simply add the changes as bullet points into those sections, empty lines will be ignored. Example: | ||
|
|
||
| * Description of the change - [#1234](https://github.com/elastic/apm-agent-java/pull/1234) | ||
| --> | ||
|
|
||
| # Fixes | ||
| <!--FIXES-START--> | ||
|
|
||
| <!--FIXES-END--> | ||
| # Features and enhancements | ||
| <!--ENHANCEMENTS-START--> | ||
| * Add support for dynamic configuration options for 9.2 #818 | ||
| * Switch upstream Opamp client #789 | ||
|
|
||
| <!--ENHANCEMENTS-END--> | ||
| # Deprecations | ||
| <!--DEPRECATIONS-START--> | ||
|
|
||
| <!--DEPRECATIONS-END--> | ||
|
|
||
| # Breaking Changes | ||
| <!--BREAKING-CHANGES-START--> | ||
| * Switch to upstream instrumentation of openai by default #763 | ||
|
|
||
| <!--BREAKING-CHANGES-END--> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -27,7 +27,7 @@ To check for security updates, go to [Security announcements for the Elastic sta | |
| % ### Fixes [edot-java-X.X.X-fixes] | ||
| % * | ||
|
|
||
| # 1.5.0 [edot-java-1.5.0-release-notes] | ||
| ## 1.5.0 [edot-java-1.5.0-release-notes] | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [for reviewer] this made the changelog generation not properly detect the previous version. |
||
|
|
||
| ### Features and enhancements [edot-java-1.5.0-features-enhancements] | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[for reviewer] almost copied as-is from apm-agent-java with minor tweaks to handle the lack of previous version.