Skip to content

Commit

Permalink
Add automated release note update to release workflow
Browse files Browse the repository at this point in the history
  • Loading branch information
Donnerbart committed Nov 11, 2024
1 parent f4b2c43 commit 1555e8f
Show file tree
Hide file tree
Showing 12 changed files with 396 additions and 1 deletion.
45 changes: 44 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,20 @@ jobs:
- name: Set up Helm
uses: azure/setup-helm@fe7b79cd5ee1e45176fcad797de68ecaf3ca4814 # v4

- name: Set up JDK 21
uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b # v4
with:
java-version: '21'
distribution: 'temurin'

- name: Set up Gradle
uses: gradle/actions/setup-gradle@d156388eb19639ec20ade50009f3d199ce1e2808 # v4
with:
gradle-home-cache-includes: |
caches
notifications
jdks
- name: Add dependency chart repos
run: |
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
Expand All @@ -34,7 +48,6 @@ jobs:
run: |
echo "${{ secrets.SIGNING_KEY }}" | gpg --dearmor > $HOME/secring.gpg
echo "${{ secrets.SIGNING_PASSWORD }}" > $HOME/passphrase
echo "CR_KEYRING=$HOME/secring.gpg" >> "$GITHUB_ENV"
echo "CR_PASSPHRASE_FILE=$HOME/passphrase" >> "$GITHUB_ENV"
Expand All @@ -50,3 +63,33 @@ jobs:
run: |
rm -f $HOME/secring.gpg
rm -f $HOME/passphrase
- name: Set latest release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# get a list of releases with names matching "hivemq-platform-<version>"
echo "Searching the latest hivemq-platform release"
releases=$(gh release list --repo hivemq/helm-charts --limit 10 | grep -oP '^hivemq-platform-\K[0-9]+\.[0-9]+\.[0-9]+')
latest_version=$(echo "$releases" | sort -V | tail -n 1)
if [ -z "$latest_version" ]; then
echo "No hivemq-platform releases found"
exit 1
fi
# mark the found release as latest
latest_release="hivemq-platform-$latest_version"
gh release edit "$latest_release" --repo hivemq/helm-charts --latest
echo "Marked $latest_release as the latest release"
- name: Update GitHub release notes
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
working-directory: helm-charts
run: |
helm repo add hivemq https://hivemq.github.io/helm-charts
helm repo update
helm search repo hivemq --versions -o json > charts.json
gh release list --repo "$REPO" --limit 10 --json name,tagName,publishedAt > releases.json
GH_PATH=$(which gh)
PWD=$(pwd)
./gradlew :github-release-note-updater:run --args " -g $GH_PATH -p $PWD"
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,7 @@ charts/**/charts/**.tgz

# Folders and files created when enabling debug mode in Helm
.debug

# temporary files for the github-release-note-updater
releases.json
charts.json
26 changes: 26 additions & 0 deletions github-release-note-updater/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
plugins {
application
}

group = "com.hivemq"

java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}

application {
mainClass = "com.hivemq.Main"
}

repositories {
mavenCentral()
}

dependencies {
compileOnly(libs.jetbrains.annotations)
implementation(libs.jackson.databind)
implementation(libs.java.semver)
implementation(libs.jcommander)
}
11 changes: 11 additions & 0 deletions github-release-note-updater/gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[versions]
jackson = "2.18.1"
java-semver = "0.10.2"
jcommander = "1.82"
jetbrains-annotations = "26.0.1"

[libraries]
jackson-databind = { module = "com.fasterxml.jackson.core:jackson-databind", version.ref = "jackson" }
java-semver = { module = "com.github.zafarkhaja:java-semver", version.ref = "java-semver" }
jcommander = { module = "com.beust:jcommander", version.ref = "jcommander" }
jetbrains-annotations = { module = "org.jetbrains:annotations", version.ref = "jetbrains-annotations" }
1 change: 1 addition & 0 deletions github-release-note-updater/settings.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
rootProject.name = "github-release-note-updater"
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.hivemq;

import com.beust.jcommander.Parameter;
import org.jetbrains.annotations.NotNull;

class Arguments {

@Parameter(names = {"--help", "-h"}, description = "Prints the usage.", help = true)
boolean help;

@Parameter(names = {"--github-cli", "-g"}, description = "Path to the GitHub CLI binary.", required = true)
@SuppressWarnings("NotNullFieldNotInitialized")
@NotNull String gitHubCliPath;

@Parameter(names = {"--path", "-p"}, description = "Path to input files.", required = true)
@SuppressWarnings("NotNullFieldNotInitialized")
@NotNull String path;
}
23 changes: 23 additions & 0 deletions github-release-note-updater/src/main/java/com/hivemq/Chart.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.hivemq;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.github.zafarkhaja.semver.Version;
import org.jetbrains.annotations.NotNull;

record Chart(String name, Version version, Version appVersion, String description) implements Comparable<Chart> {

@JsonCreator
Chart(
@JsonProperty(value = "name") final @NotNull String name,
@JsonProperty(value = "version") final @NotNull String version,
@JsonProperty(value = "app_version") final @NotNull String appVersion,
@JsonProperty(value = "description") final @NotNull String description) {
this(name.split("/")[1], Version.parse(version), Version.parse(appVersion), description);
}

@Override
public int compareTo(final @NotNull Chart o) {
return version.compareTo(o.version);
}
}
178 changes: 178 additions & 0 deletions github-release-note-updater/src/main/java/com/hivemq/Main.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
package com.hivemq;

import com.beust.jcommander.JCommander;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.zafarkhaja.semver.Version;
import org.jetbrains.annotations.NotNull;

import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Main {

private static final @NotNull String PLATFORM_RELEASE_NOTE_TEMPLATE = """
%s
[Updated to HiveMQ Platform %s](%s)
""";
private static final @NotNull String OPERATOR_RELEASE_NOTE_TEMPLATE = """
%s
[Updated to HiveMQ Platform Operator %s](%s)
""";

public static void main(final @NotNull String @NotNull [] args) throws Exception {
final var arguments = new Arguments();
final var jCommander = JCommander.newBuilder().addObject(arguments).build();
jCommander.parse(args);
if (arguments.help) {
jCommander.usage();
System.exit(0);
}

final var chartsPath = Path.of(arguments.path, "charts.json");
final var releasesPath = Path.of(arguments.path, "releases.json");
if (Files.notExists(chartsPath)) {
System.err.println("Charts path does not exist.");
System.exit(1);
}
if (Files.notExists(releasesPath)) {
System.err.println("Releases path does not exist.");
System.exit(1);
}

final var objectMapper = new ObjectMapper();
final var charts = Arrays.stream(objectMapper.readValue(Files.readString(chartsPath), Chart[].class)).toList();
final var releases =
Arrays.stream(objectMapper.readValue(Files.readString(releasesPath), Release[].class)).toList();

// sort the released Helm charts
final var platformOperatorCharts =
charts.stream().filter(chart -> chart.name().equals("hivemq-platform-operator")).sorted().toList();
final var platformCharts =
charts.stream().filter(chart -> chart.name().equals("hivemq-platform")).sorted().toList();
final var legacyOperatorCharts =
charts.stream().filter(chart -> chart.name().equals("hivemq-operator")).sorted().toList();
final var swarmCharts = charts.stream().filter(chart -> chart.name().equals("hivemq-swarm")).sorted().toList();

// prepare the release notes
final var releaseNotes = new HashMap<String, String>();
setPlatformReleaseNotes(releaseNotes, releases, platformOperatorCharts, platformCharts);
setPlatformReleaseNotes(releaseNotes, releases, platformOperatorCharts, legacyOperatorCharts);
setPlatformReleaseNotes(releaseNotes, releases, platformOperatorCharts, swarmCharts);
platformOperatorCharts.forEach(chart -> {
final var releaseTag = String.format("%s-%s", chart.name(), chart.version());
final var releaseNote = String.format(OPERATOR_RELEASE_NOTE_TEMPLATE,
chart.description(),
chart.appVersion(),
setPlatformOperatorReleaseUrl(chart.appVersion()));
releaseNotes.put(releaseTag, releaseNote);
});

// update the GitHub release notes
try (var executorService = Executors.newSingleThreadExecutor()) {
for (final var release : releases) {
final var releaseTag = release.tagName();
final var releaseNote = releaseNotes.get(releaseTag);
if (releaseNote == null) {
System.out.println("Skipping release " + releaseTag);
continue;
}
final var exitCode = execute(executorService,
arguments.gitHubCliPath,
"release",
"edit",
releaseTag,
"--repo",
"hivemq/helm-charts",
"--notes",
releaseNote);
System.out.println(releaseTag + " -> " + (exitCode == 0 ? "SUCCESS" : "FAILURE"));
}
}
}

private static void setPlatformReleaseNotes(
final @NotNull Map<String, String> releaseNotes,
final @NotNull List<Release> releases,
final @NotNull List<Chart> platformOperatorCharts,
final @NotNull List<Chart> charts) {
for (int i = 0; i < charts.size(); i++) {
final var chart = charts.get(i);
final var previousChart = i == 0 ? null : charts.get(i - 1);
final var wasChartUpdated = previousChart == null || !previousChart.appVersion().equals(chart.appVersion());
final var releaseTag = String.format("%s-%s", chart.name(), chart.version());
final var operatorReleaseOptional = getMatchingOperatorRelease(releases, releaseTag);
if (wasChartUpdated || operatorReleaseOptional.isEmpty()) {
// this release was triggered by a HiveMQ Platform release
final var releaseNote = String.format(PLATFORM_RELEASE_NOTE_TEMPLATE,
chart.description(),
chart.appVersion(),
getPlatformReleaseUrl(chart.appVersion()));
releaseNotes.put(releaseTag, releaseNote);
} else {
// this release was triggered by a HiveMQ Platform Operator release
final var operatorRelease = operatorReleaseOptional.get();
final var operatorChartVersion = Version.parse(operatorRelease.tagName()
.substring(operatorRelease.tagName().lastIndexOf('-') + 1));
final var operatorChart = platformOperatorCharts.stream()
.filter(c -> c.version().equals(operatorChartVersion))
.findFirst()
.orElseThrow();
final var releaseNote = String.format(OPERATOR_RELEASE_NOTE_TEMPLATE,
chart.description(),
operatorChart.appVersion(),
setPlatformOperatorReleaseUrl(operatorChart.appVersion()));
releaseNotes.put(releaseTag, releaseNote);
}
}
}

private static @NotNull Optional<Release> getMatchingOperatorRelease(
final @NotNull List<Release> releases, //
final @NotNull String releaseTag) {
return releases.stream()
.filter(release -> release.tagName().equals(releaseTag))
.findFirst()
.flatMap(value -> releases.stream()
.filter(release -> release.tagName().startsWith("hivemq-platform-operator-"))
.filter(release -> release.publishedAt().equals(value.publishedAt()))
.findFirst());
}

private static @NotNull String getPlatformReleaseUrl(final @NotNull Version version) {
if (version.patchVersion() != 0) {
return String.format("https://www.hivemq.com/changelog/hivemq-%s-%s-%s-released/",
version.majorVersion(),
version.minorVersion(),
version.patchVersion());
}
return String.format("https://www.hivemq.com/changelog/whats-new-in-hivemq-%s-%s/",
version.majorVersion(),
version.minorVersion());
}

private static @NotNull String setPlatformOperatorReleaseUrl(final @NotNull Version version) {
return String.format("https://www.hivemq.com/changelog/hivemq-platform-operator-%s-%s-%s-release/",
version.majorVersion(),
version.minorVersion(),
version.patchVersion());
}

private static int execute(final @NotNull ExecutorService executorService, final @NotNull String... command)
throws Exception {
final var process = new ProcessBuilder().command(command).start();
final var inputStreamGobbler = new StreamGobbler(process.getInputStream(), System.out::println);
final var errorStreamGobbler = new StreamGobbler(process.getErrorStream(), System.err::println);
executorService.submit(inputStreamGobbler);
executorService.submit(errorStreamGobbler);
return process.waitFor();
}
}
22 changes: 22 additions & 0 deletions github-release-note-updater/src/main/java/com/hivemq/Release.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.hivemq;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.jetbrains.annotations.NotNull;

import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;

record Release(String name, String tagName, ZonedDateTime publishedAt) {

private static final ZoneId ZONE_ID = ZoneId.systemDefault();

@JsonCreator
Release(
@JsonProperty(value = "name") final @NotNull String name,
@JsonProperty(value = "tagName") final @NotNull String tagName,
@JsonProperty(value = "publishedAt") final @NotNull String publishedAt) {
this(name, tagName, Instant.parse(publishedAt).atZone(ZONE_ID).toLocalDate().atStartOfDay(ZONE_ID));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.hivemq;

import org.jetbrains.annotations.NotNull;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.function.Consumer;

/**
* Consumes an {@link InputStream}.
*/
class StreamGobbler implements Runnable {

private final @NotNull InputStream inputStream;
private final @NotNull Consumer<String> consumer;

StreamGobbler(final @NotNull InputStream inputStream, final @NotNull Consumer<String> consumer) {
this.inputStream = inputStream;
this.consumer = consumer;
}

@Override
public void run() {
new BufferedReader(new InputStreamReader(inputStream)).lines().forEach(consumer);
}
}
Loading

0 comments on commit 1555e8f

Please sign in to comment.