From e57a6594dc3d42b4056be010b1478e400def3e90 Mon Sep 17 00:00:00 2001 From: David Venable Date: Wed, 4 Feb 2026 11:32:14 -0600 Subject: [PATCH] GitHub Action to prepare a branch for the next release. Signed-off-by: David Venable --- .github/workflows/release-prepare-branch.yml | 179 ++++++++++++++++++ build.gradle | 22 +++ buildSrc/build.gradle | 6 + .../release/PrintCurrentVersionTask.java | 24 +++ .../gradle/release/PrintNextVersionTask.java | 29 +++ .../gradle/release/UpdateVersionTask.java | 37 ++++ .../gradle/release/VersionCalculator.java | 60 ++++++ .../gradle/release/VersionUpdater.java | 38 ++++ .../gradle/release/VersionCalculatorTest.java | 71 +++++++ .../gradle/release/VersionUpdaterTest.java | 68 +++++++ 10 files changed, 534 insertions(+) create mode 100644 .github/workflows/release-prepare-branch.yml create mode 100644 buildSrc/src/main/java/org/opensearch/dataprepper/gradle/release/PrintCurrentVersionTask.java create mode 100644 buildSrc/src/main/java/org/opensearch/dataprepper/gradle/release/PrintNextVersionTask.java create mode 100644 buildSrc/src/main/java/org/opensearch/dataprepper/gradle/release/UpdateVersionTask.java create mode 100644 buildSrc/src/main/java/org/opensearch/dataprepper/gradle/release/VersionCalculator.java create mode 100644 buildSrc/src/main/java/org/opensearch/dataprepper/gradle/release/VersionUpdater.java create mode 100644 buildSrc/src/test/java/org/opensearch/dataprepper/gradle/release/VersionCalculatorTest.java create mode 100644 buildSrc/src/test/java/org/opensearch/dataprepper/gradle/release/VersionUpdaterTest.java diff --git a/.github/workflows/release-prepare-branch.yml b/.github/workflows/release-prepare-branch.yml new file mode 100644 index 0000000000..78660c245c --- /dev/null +++ b/.github/workflows/release-prepare-branch.yml @@ -0,0 +1,179 @@ +# +# Copyright OpenSearch Contributors +# SPDX-License-Identifier: Apache-2.0 +# +# The OpenSearch Contributors require contributions made to +# this file be licensed under the Apache-2.0 license or a +# compatible open source license. +# + +# This workflow automates the preparation of patch release branches. +# It validates the release branch, updates gradle.properties to the next version +# generates the THIRD-PARTY file, and creates a pull request with all changes. + +name: Prepare Release Branch + +on: + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +jobs: + prepare-release: + runs-on: ubuntu-latest + + steps: + - name: Checkout Data Prepper + uses: actions/checkout@v2 + + - name: Validate release branch + id: validate_branch + run: | + BRANCH_NAME=${GITHUB_REF#refs/heads/} + echo "Running on branch: $BRANCH_NAME" + + # Validate branch name matches {major}.{minor} pattern + if ! [[ $BRANCH_NAME =~ ^[0-9]+\.[0-9]+$ ]]; then + echo "::error::Invalid release branch name: $BRANCH_NAME" + echo "This workflow must run on a release branch with format {major}.{minor} (e.g., '2.6')" + echo "Current branch: $BRANCH_NAME" + echo "" + echo "To run this workflow:" + echo " 1. Switch to a release branch (e.g., 2.6)" + echo " 2. Navigate to Actions > Prepare Patch Release" + echo " 3. Click 'Run workflow' and select the release branch" + exit 1 + fi + + echo "✅ Valid release branch: $BRANCH_NAME" + echo "branch=$BRANCH_NAME" >> $GITHUB_OUTPUT + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + java-version: 21 + distribution: temurin + + - name: Update version to next release + id: version + run: | + ./gradlew updateToNextVersion + + RELEASE_VERSION=$(./gradlew -q printCurrentVersion) + echo "version=$RELEASE_VERSION" >> $GITHUB_OUTPUT + + - name: Generate THIRD-PARTY file + run: | + echo "Generating THIRD-PARTY file..." + + # Run the Gradle task to generate the THIRD-PARTY file + # Capture both stdout and stderr for better error reporting + if ! OUTPUT=$(./gradlew --no-daemon generateThirdPartyReport 2>&1); then + echo "::error::Failed to generate THIRD-PARTY file" + echo "Gradle output:" + echo "$OUTPUT" + exit 1 + fi + + # Verify the THIRD-PARTY file was created/updated + if [ ! -f THIRD-PARTY ]; then + echo "::error::THIRD-PARTY file not found after generation" + echo "The generateThirdPartyReport task completed but did not create the expected file" + exit 1 + fi + + echo "Successfully generated THIRD-PARTY file" + + - name: GitHub App token + id: github_app_token + uses: tibdex/github-app-token@v2 + with: + app_id: ${{ secrets.APP_ID }} + private_key: ${{ secrets.APP_PRIVATE_KEY }} + + - name: Create Pull Request + id: create_pr + uses: peter-evans/create-pull-request@v6 + with: + token: ${{ steps.github_app_token.outputs.token }} + add-paths: | + gradle.properties + THIRD-PARTY + commit-message: 'Prepare release ${{ steps.version.outputs.version }}' + signoff: true + branch: prepare-release-${{ steps.version.outputs.version }} + delete-branch: true + base: ${{ steps.validate_branch.outputs.branch }} + title: 'Prepare release ${{ steps.version.outputs.version }}' + body: | + ## Automated Release Preparation + + This PR prepares the release branch for version **${{ steps.version.outputs.version }}**. + + ### Automated Changes + - Updated `gradle.properties` version to `${{ steps.version.outputs.version }}` + - Generated `THIRD-PARTY` file + + ### Manual Steps Remaining + + Before merging this PR: + - [ ] Review the version number is correct + - [ ] Review the THIRD-PARTY file changes + + After merging this PR: + - [ ] Prepare release notes (see [release notes script](release/script/release-notes/README.md)) + - [ ] Run the [Release Artifacts workflow](https://github.com/opensearch-project/data-prepper/actions/workflows/release.yml) + - [ ] Approve the release issue (requires 2 maintainer approvals) + - [ ] Update the draft release with release notes + - [ ] Publish the release + - [ ] Close the milestone + + --- + + Branch: ${{ steps.validate_branch.outputs.branch }} + + Generated by the [Prepare Patch Release workflow](https://github.com/opensearch-project/data-prepper/actions/workflows/release-prepare-patch.yml) + + - name: Output PR URL + if: steps.create_pr.outputs.pull-request-url + run: | + echo "✅ Pull Request created successfully!" + echo "URL: ${{ steps.create_pr.outputs.pull-request-url }}" + echo "::notice::Pull Request created: ${{ steps.create_pr.outputs.pull-request-url }}" + + - name: Check PR creation + if: steps.create_pr.outcome == 'failure' + run: | + echo "::error::Failed to create pull request" + echo "This could be due to:" + echo " - Missing or invalid GitHub App credentials" + echo " - Insufficient permissions" + echo " - Network issues" + echo "Please check the logs above for more details" + exit 1 + + - name: Workflow summary + if: always() + run: | + echo "## Workflow Execution Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "${{ steps.validate_branch.outcome }}" == "success" ]; then + echo "✅ Branch validation: **${{ steps.validate_branch.outputs.branch }}**" >> $GITHUB_STEP_SUMMARY + else + echo "❌ Branch validation failed" >> $GITHUB_STEP_SUMMARY + fi + + if [ "${{ steps.version.outcome }}" == "success" ]; then + echo "✅ Version determination: **${{ steps.version.outputs.version }}**" >> $GITHUB_STEP_SUMMARY + else + echo "❌ Version determination failed" >> $GITHUB_STEP_SUMMARY + fi + + if [ "${{ steps.create_pr.outcome }}" == "success" ]; then + echo "✅ Pull request created: ${{ steps.create_pr.outputs.pull-request-url }}" >> $GITHUB_STEP_SUMMARY + else + echo "❌ Pull request creation failed" >> $GITHUB_STEP_SUMMARY + fi diff --git a/build.gradle b/build.gradle index 6fd3227fac..d610c8027d 100644 --- a/build.gradle +++ b/build.gradle @@ -1,8 +1,15 @@ /* * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. */ import com.github.jk1.license.render.TextReportRenderer +import org.opensearch.dataprepper.gradle.release.PrintCurrentVersionTask +import org.opensearch.dataprepper.gradle.release.PrintNextVersionTask +import org.opensearch.dataprepper.gradle.release.UpdateVersionTask buildscript { dependencies { @@ -328,6 +335,21 @@ task generateThirdPartyReport(type: Copy) { generateThirdPartyReport.dependsOn(generateLicenseReport) } +tasks.register('printNextVersion', PrintNextVersionTask) { + description = 'Prints the next release version based on the current version in gradle.properties' + group = 'release' +} + +tasks.register('printCurrentVersion', PrintCurrentVersionTask) { + description = 'Prints the current version from gradle.properties' + group = 'release' +} + +tasks.register('updateToNextVersion', UpdateVersionTask) { + description = 'Updates gradle.properties to the next release version' + group = 'release' +} + // Utility method to recursively collect tasks from all projects and subprojects def collectTasksRecursively(Project project, String taskName) { def tasks = [] diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index d54585f626..517689adf6 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -15,4 +15,10 @@ repositories { dependencies { implementation('me.champeau.jmh:me.champeau.jmh.gradle.plugin:0.7.2') + testImplementation('org.junit.jupiter:junit-jupiter:5.9.3') + testImplementation('org.hamcrest:hamcrest:2.2') +} + +test { + useJUnitPlatform() } diff --git a/buildSrc/src/main/java/org/opensearch/dataprepper/gradle/release/PrintCurrentVersionTask.java b/buildSrc/src/main/java/org/opensearch/dataprepper/gradle/release/PrintCurrentVersionTask.java new file mode 100644 index 0000000000..65a79baef2 --- /dev/null +++ b/buildSrc/src/main/java/org/opensearch/dataprepper/gradle/release/PrintCurrentVersionTask.java @@ -0,0 +1,24 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.dataprepper.gradle.release; + +import org.gradle.api.DefaultTask; +import org.gradle.api.tasks.TaskAction; + +/** + * Gradle task that prints the current version from gradle.properties. + */ +public class PrintCurrentVersionTask extends DefaultTask { + + @TaskAction + public void printCurrentVersion() { + System.out.println(getProject().getVersion().toString()); + } +} diff --git a/buildSrc/src/main/java/org/opensearch/dataprepper/gradle/release/PrintNextVersionTask.java b/buildSrc/src/main/java/org/opensearch/dataprepper/gradle/release/PrintNextVersionTask.java new file mode 100644 index 0000000000..1aea82cdc5 --- /dev/null +++ b/buildSrc/src/main/java/org/opensearch/dataprepper/gradle/release/PrintNextVersionTask.java @@ -0,0 +1,29 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.dataprepper.gradle.release; + +import org.gradle.api.DefaultTask; +import org.gradle.api.tasks.TaskAction; + +/** + * Gradle task that prints the next release version based on the current version in gradle.properties. + */ +public class PrintNextVersionTask extends DefaultTask { + + /** + * Executes the task to determine and print the next version. + */ + @TaskAction + public void printNextVersion() { + final String currentVersion = getProject().getVersion().toString(); + final String nextVersion = VersionCalculator.determineNextVersion(currentVersion); + System.out.println(nextVersion); + } +} diff --git a/buildSrc/src/main/java/org/opensearch/dataprepper/gradle/release/UpdateVersionTask.java b/buildSrc/src/main/java/org/opensearch/dataprepper/gradle/release/UpdateVersionTask.java new file mode 100644 index 0000000000..f9071387a8 --- /dev/null +++ b/buildSrc/src/main/java/org/opensearch/dataprepper/gradle/release/UpdateVersionTask.java @@ -0,0 +1,37 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.dataprepper.gradle.release; + +import org.gradle.api.DefaultTask; +import org.gradle.api.GradleException; +import org.gradle.api.tasks.TaskAction; + +import java.io.IOException; +import java.nio.file.Path; + +/** + * Gradle task that updates the version in gradle.properties to the next release version. + */ +public class UpdateVersionTask extends DefaultTask { + + @TaskAction + public void updateVersion() { + final String currentVersion = getProject().getVersion().toString(); + final String nextVersion = VersionCalculator.determineNextVersion(currentVersion); + final Path propertiesFile = getProject().getRootDir().toPath().resolve("gradle.properties"); + + try { + VersionUpdater.updateVersionInFile(propertiesFile, nextVersion); + getLogger().lifecycle("Updated version from {} to {}", currentVersion, nextVersion); + } catch (IOException e) { + throw new GradleException("Failed to update gradle.properties", e); + } + } +} diff --git a/buildSrc/src/main/java/org/opensearch/dataprepper/gradle/release/VersionCalculator.java b/buildSrc/src/main/java/org/opensearch/dataprepper/gradle/release/VersionCalculator.java new file mode 100644 index 0000000000..42553531ff --- /dev/null +++ b/buildSrc/src/main/java/org/opensearch/dataprepper/gradle/release/VersionCalculator.java @@ -0,0 +1,60 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.dataprepper.gradle.release; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Utility class for calculating the next release version from a current version string. + */ +class VersionCalculator { + private static final Pattern VERSION_PATTERN = Pattern.compile("^(\\d+)\\.(\\d+)\\.(\\d+)(-SNAPSHOT)?$"); + + /** + * Determines the next release version from the current version. + *

+ * If the current version ends with "-SNAPSHOT", the suffix is removed. + * If the current version does not end with "-SNAPSHOT", the patch version is incremented by 1. + * + * @param currentVersion The current version string (e.g., "2.6.2-SNAPSHOT" or "2.6.2") + * @return The next release version (e.g., "2.6.2" or "2.6.3") + * @throws IllegalArgumentException if version format is invalid + * @throws NullPointerException if currentVersion is null + */ + static String determineNextVersion(String currentVersion) { + if (currentVersion == null) { + throw new NullPointerException("currentVersion cannot be null"); + } + + Matcher matcher = VERSION_PATTERN.matcher(currentVersion); + if (!matcher.matches()) { + throw new IllegalArgumentException( + "Invalid version format: " + currentVersion + + ". Expected format: {major}.{minor}.{patch}[-SNAPSHOT]" + ); + } + + String major = matcher.group(1); + String minor = matcher.group(2); + String patch = matcher.group(3); + String snapshot = matcher.group(4); + + // If version has -SNAPSHOT suffix, remove it + if (snapshot != null) { + return major + "." + minor + "." + patch; + } + + // Otherwise, increment patch version + int patchNumber = Integer.parseInt(patch); + int nextPatch = patchNumber + 1; + return major + "." + minor + "." + nextPatch; + } +} diff --git a/buildSrc/src/main/java/org/opensearch/dataprepper/gradle/release/VersionUpdater.java b/buildSrc/src/main/java/org/opensearch/dataprepper/gradle/release/VersionUpdater.java new file mode 100644 index 0000000000..d5369b31f3 --- /dev/null +++ b/buildSrc/src/main/java/org/opensearch/dataprepper/gradle/release/VersionUpdater.java @@ -0,0 +1,38 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.dataprepper.gradle.release; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Utility class for updating version in gradle.properties files. + */ +class VersionUpdater { + + /** + * Updates the version line in a gradle.properties file. + * + * @param propertiesFile Path to the gradle.properties file + * @param newVersion The new version to set + * @throws IOException if file operations fail + */ + static void updateVersionInFile(final Path propertiesFile, final String newVersion) throws IOException { + final List lines = Files.readAllLines(propertiesFile); + final List updatedLines = lines.stream() + .map(line -> line.startsWith("version=") ? "version=" + newVersion : line) + .collect(Collectors.toList()); + + Files.write(propertiesFile, updatedLines); + } +} diff --git a/buildSrc/src/test/java/org/opensearch/dataprepper/gradle/release/VersionCalculatorTest.java b/buildSrc/src/test/java/org/opensearch/dataprepper/gradle/release/VersionCalculatorTest.java new file mode 100644 index 0000000000..adf4c59edf --- /dev/null +++ b/buildSrc/src/test/java/org/opensearch/dataprepper/gradle/release/VersionCalculatorTest.java @@ -0,0 +1,71 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.dataprepper.gradle.release; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class VersionCalculatorTest { + @ParameterizedTest + @CsvSource({ + "2.6.2-SNAPSHOT, 2.6.2", + "1.0.0-SNAPSHOT, 1.0.0", + "10.20.30-SNAPSHOT, 10.20.30", + "0.0.1-SNAPSHOT, 0.0.1" + }) + void determineNextVersion_with_SNAPSHOT_updates_to_same_version_without_SNAPSHOT(final String input, final String expected) { + final String result = VersionCalculator.determineNextVersion(input); + assertThat(result, equalTo(expected)); + } + + @ParameterizedTest + @CsvSource({ + "2.6.2, 2.6.3", + "1.0.0, 1.0.1", + "10.20.30, 10.20.31", + "0.0.0, 0.0.1", + "999.999.999, 999.999.1000" + }) + void determineNextVersion_with_non_SNAPSHOT_updates_to_next_patch_version(final String input, final String expected) { + final String result = VersionCalculator.determineNextVersion(input); + assertThat(result, equalTo(expected)); + } + + @ParameterizedTest + @ValueSource(strings = { + "invalid", + "1.2", + "1.2.3.4", + "1.2.3-BETA", + "a.b.c", + "1.2.x", + "", + "1.2.3-", + "-SNAPSHOT" + }) + void determineNextVersion_with_invalid_throws(String invalidVersion) { + assertThrows(IllegalArgumentException.class, () -> { + VersionCalculator.determineNextVersion(invalidVersion); + }); + } + + @Test + void determineNextVersion_with_null_throws() { + assertThrows(NullPointerException.class, () -> { + VersionCalculator.determineNextVersion(null); + }); + } +} diff --git a/buildSrc/src/test/java/org/opensearch/dataprepper/gradle/release/VersionUpdaterTest.java b/buildSrc/src/test/java/org/opensearch/dataprepper/gradle/release/VersionUpdaterTest.java new file mode 100644 index 0000000000..f1d6275b03 --- /dev/null +++ b/buildSrc/src/test/java/org/opensearch/dataprepper/gradle/release/VersionUpdaterTest.java @@ -0,0 +1,68 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.dataprepper.gradle.release; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; + +class VersionUpdaterTest { + + @TempDir + Path tempDir; + + private Path propertiesFile; + + @BeforeEach + void setUp() { + propertiesFile = tempDir.resolve("gradle.properties"); + } + + @Test + void updateVersionInFile_updates_version_line() throws IOException { + + Files.write(propertiesFile, List.of( + "# Comment", + "version=1.0.0-SNAPSHOT", + "org.gradle.jvmargs=-Xmx2048M" + )); + + VersionUpdater.updateVersionInFile(propertiesFile, "1.0.0"); + + final List lines = Files.readAllLines(propertiesFile); + assertThat(lines.get(0), equalTo("# Comment")); + assertThat(lines.get(1), equalTo("version=1.0.0")); + assertThat(lines.get(2), equalTo("org.gradle.jvmargs=-Xmx2048M")); + } + + @Test + void updateVersionInFile_preserves_other_lines() throws IOException { + Files.write(propertiesFile, List.of( + "someProperty=value", + "version=2.5.0", + "anotherProperty=anotherValue" + )); + + VersionUpdater.updateVersionInFile(propertiesFile, "2.6.0"); + + final List lines = Files.readAllLines(propertiesFile); + assertThat(lines.get(0), equalTo("someProperty=value")); + assertThat(lines.get(1), equalTo("version=2.6.0")); + assertThat(lines.get(2), equalTo("anotherProperty=anotherValue")); + } +}