Skip to content
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

Add validation for branch protection #386

Merged
merged 5 commits into from
Nov 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 27 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,8 @@ jobs:
steps:
- name: Checkout HiveMQ Helm Charts
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4

- name: Fetch history
run: git fetch --prune --unshallow
with:
fetch-depth: 0

- name: Configure Git
run: |
Expand All @@ -26,6 +25,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 +47,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 +62,14 @@ jobs:
run: |
rm -f $HOME/secring.gpg
rm -f $HOME/passphrase

- name: Set latest release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: bash ./release/set-latest-release.sh

- name: Update GitHub release notes
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
working-directory: helm-charts
run: bash ./release/update-github-release-notes.sh 10
8 changes: 8 additions & 0 deletions .github/workflows/verify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ on:
branches:
- "**"
workflow_dispatch:
concurrency:
group: verify-${{ github.ref }}
cancel-in-progress: true
jobs:
verify:
runs-on: ubuntu-latest
Expand Down Expand Up @@ -60,3 +63,8 @@ jobs:
helm kubeconform --config .github/configs/kubeconform.yml charts/hivemq-platform-operator -r hivemq-platform-operator-release -n hivemq-platform-operator-namespace
helm kubeconform --config .github/configs/kubeconform.yml charts/hivemq-platform -r hivemq-platform-release -n hivemq-platform-namespace --schema-location hivemqplatform.hivemq-v1.json
helm kubeconform --config .github/configs/kubeconform.yml charts/hivemq-swarm -r hivemq-swarm-release -n hivemq-swarm-namespace

- name: Validate branch protection
env:
GH_TOKEN: ${{ secrets.JENKINS_GITHUB_TOKEN }}
run: bash ./release/validate-branch-protection.sh ${{ github.base_ref }}
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.release.GitHubReleaseNotesUpdater"
}

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.release;

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;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.hivemq.release;

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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
package com.hivemq.release;

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;
import java.util.function.Function;
import java.util.regex.Pattern;

public class GitHubReleaseNotesUpdater {

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)
""";

private static final @NotNull String PLATFORM_MAINTENANCE_RELEASE_URL =
"https://www.hivemq.com/changelog/hivemq-%s-%s-%s-released/";
private static final @NotNull String PLATFORM_FEATURE_RELEASE_URL =
"https://www.hivemq.com/changelog/whats-new-in-hivemq-%s-%s/";
private static final @NotNull String OPERATOR_RELEASE_URL =
"https://www.hivemq.com/changelog/hivemq-platform-operator-%s-%s-%s-release/";

private static final @NotNull Pattern PLATFORM_RELEASE_PATTERN =
Pattern.compile("^hivemq-platform-(\\d+\\.\\d+\\.\\d+)$");
private static final @NotNull Pattern OPERATOR_RELEASE_PATTERN =
Pattern.compile("^hivemq-platform-operator-(\\d+\\.\\d+\\.\\d+)$");

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.printf("Charts path '%s' does not exist%n", chartsPath);
System.exit(1);
}
if (Files.notExists(releasesPath)) {
System.err.printf("Releases path '%s' does not exist%n", releasesPath);
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>();
setReleaseNotes(releaseNotes,
releases,
OPERATOR_RELEASE_PATTERN,
platformCharts,
PLATFORM_RELEASE_NOTE_TEMPLATE,
GitHubReleaseNotesUpdater::getPlatformReleaseUrl,
platformOperatorCharts,
OPERATOR_RELEASE_NOTE_TEMPLATE,
GitHubReleaseNotesUpdater::getOperatorReleaseUrl);
setReleaseNotes(releaseNotes,
releases,
OPERATOR_RELEASE_PATTERN,
legacyOperatorCharts,
PLATFORM_RELEASE_NOTE_TEMPLATE,
GitHubReleaseNotesUpdater::getPlatformReleaseUrl,
platformOperatorCharts,
OPERATOR_RELEASE_NOTE_TEMPLATE,
GitHubReleaseNotesUpdater::getOperatorReleaseUrl);
setReleaseNotes(releaseNotes,
releases,
OPERATOR_RELEASE_PATTERN,
swarmCharts,
PLATFORM_RELEASE_NOTE_TEMPLATE,
GitHubReleaseNotesUpdater::getPlatformReleaseUrl,
platformOperatorCharts,
OPERATOR_RELEASE_NOTE_TEMPLATE,
GitHubReleaseNotesUpdater::getOperatorReleaseUrl);
setReleaseNotes(releaseNotes,
releases,
PLATFORM_RELEASE_PATTERN,
platformOperatorCharts,
OPERATOR_RELEASE_NOTE_TEMPLATE,
GitHubReleaseNotesUpdater::getOperatorReleaseUrl,
platformCharts,
PLATFORM_RELEASE_NOTE_TEMPLATE,
GitHubReleaseNotesUpdater::getPlatformReleaseUrl);

// update the GitHub release notes
try (var executorService = Executors.newSingleThreadExecutor()) {
var success = true;
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);
if (exitCode != 0) {
success = false;
}
}
if (!success) {
System.err.println("Not all release notes were updated successfully!");
System.exit(1);
}
}
}

private static void setReleaseNotes(
final @NotNull Map<String, String> releaseNotes,
final @NotNull List<Release> releases,
final @NotNull Pattern matchingReleasePattern,
final @NotNull List<Chart> charts,
final @NotNull String releaseNoteTemplate,
final @NotNull Function<Version, String> releaseUrlFunction,
final @NotNull List<Chart> otherCharts,
final @NotNull String otherReleaseNoteTemplate,
final @NotNull Function<Version, String> otherReleaseUrlFunction) {
for (int i = 0; i < charts.size(); i++) {
// we get the previous chart and check if the appVersion has changed in the current chart
// (if so we generate a release note for this chart, otherwise for the matching otherChart)
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 otherReleaseOptional = getMatchingRelease(releases, releaseTag, matchingReleasePattern);
if (wasChartUpdated || otherReleaseOptional.isEmpty()) {
// this release was triggered by an update of this chart
final var releaseNote = String.format(releaseNoteTemplate,
chart.description(),
chart.appVersion(),
releaseUrlFunction.apply(chart.appVersion()));
releaseNotes.put(releaseTag, releaseNote);
} else {
// this release was triggered by an update of the other chart
final var otherRelease = otherReleaseOptional.get();
final var otherChartVersion =
Version.parse(otherRelease.tagName().substring(otherRelease.tagName().lastIndexOf('-') + 1));
final var otherChart = otherCharts.stream()
.filter(c -> c.version().equals(otherChartVersion))
.findFirst()
.orElseThrow();
final var releaseNote = String.format(otherReleaseNoteTemplate,
chart.description(),
otherChart.appVersion(),
otherReleaseUrlFunction.apply(otherChart.appVersion()));
releaseNotes.put(releaseTag, releaseNote);
}
}
}

private static @NotNull Optional<Release> getMatchingRelease(
final @NotNull List<Release> releases,
final @NotNull String releaseTag,
final @NotNull Pattern matchingReleasePattern) {
return releases.stream()
.filter(release -> release.tagName().equals(releaseTag))
.findFirst()
.flatMap(value -> releases.stream()
.filter(release -> matchingReleasePattern.matcher(release.tagName()).matches())
.filter(release -> release.publishedAt().equals(value.publishedAt()))
.findFirst());
}

private static @NotNull String getPlatformReleaseUrl(final @NotNull Version version) {
if (version.patchVersion() != 0) {
return String.format(PLATFORM_MAINTENANCE_RELEASE_URL,
version.majorVersion(),
version.minorVersion(),
version.patchVersion());
}
return String.format(PLATFORM_FEATURE_RELEASE_URL, version.majorVersion(), version.minorVersion());
}

private static @NotNull String getOperatorReleaseUrl(final @NotNull Version version) {
return String.format(OPERATOR_RELEASE_URL,
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();
}
}
Loading
Loading