diff --git a/.github/workflows/initiate_release.yml b/.github/workflows/initiate_release.yml deleted file mode 100644 index f78e7ea8..00000000 --- a/.github/workflows/initiate_release.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: Create release PR - -on: - workflow_dispatch: - inputs: - version: - description: "The new version number. Example: 1.40.1" - required: true - type: string - pattern: "^[0-9]+\\.[0-9]+\\.[0-9]+$" - -jobs: - init_release: - name: 🚀 Create release PR - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 # gives the changelog generator access to all previous commits - - - name: Update CHANGELOG.md, build.gradle and push release branch - env: - VERSION: ${{ github.event.inputs.version }} - run: | - npx --yes standard-version@9.3.2 --release-as "$VERSION" --skip.tag --skip.commit --tag-prefix= - echo "version=$VERSION" > gradle.properties - git config --global user.name 'github-actions' - git config --global user.email 'release@getstream.io' - git checkout -q -b "release-$VERSION" - git add CHANGELOG.md gradle.properties - git commit -am "chore(release): $VERSION" - git push -q -u origin "release-$VERSION" - - - name: Open pull request - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - gh pr create \ - -t "Release ${{ github.event.inputs.version }}" \ - -b "# :rocket: ${{ github.event.inputs.version }} - Make sure to use squash & merge when merging! - Once this is merged, another job will kick off automatically and publish the package. - # :memo: Changelog - $(cat CHANGELOG.md)" diff --git a/.github/workflows/manual-release.yml b/.github/workflows/manual-release.yml new file mode 100644 index 00000000..1191899a --- /dev/null +++ b/.github/workflows/manual-release.yml @@ -0,0 +1,80 @@ +name: Manual Release + +on: + workflow_dispatch: + inputs: + version: + description: "Release version (example: 7.2.0)" + required: true + type: string + +permissions: + contents: write + +jobs: + manual-release: + name: 🚀 Manual Release + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: main + + - name: Setup JDK 17 + uses: actions/setup-java@v5.1.0 + with: + distribution: "corretto" + java-version: "17" + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Update version file + env: + VERSION: ${{ github.event.inputs.version }} + run: | + sed -i "s/^version=.*/version=${VERSION}/" gradle.properties + + - name: Commit version update + env: + VERSION: ${{ github.event.inputs.version }} + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add gradle.properties + if git diff --cached --quiet; then + echo "No version changes to commit." + else + git commit -m "chore(release): v${VERSION} (manual)" + git push origin HEAD:main + fi + + - name: Publish to MavenCentral + env: + STREAM_API_KEY: ${{ vars.STREAM_API_KEY }} + STREAM_API_SECRET: ${{ secrets.STREAM_API_SECRET }} + GPG_KEY_CONTENTS: ${{ secrets.GPG_KEY_CONTENTS }} + OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }} + OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} + SIGNING_KEY_ID: ${{ secrets.SIGNING_KEY_ID }} + SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} + SIGNING_SECRET_KEY_RING_FILE: ${{ secrets.SIGNING_SECRET_KEY_RING_FILE }} + SONATYPE_STAGING_PROFILE_ID: ${{ secrets.SONATYPE_STAGING_PROFILE_ID }} + run: | + ./gradlew -Pversion=${{ github.event.inputs.version }} publishToSonatype --no-daemon --max-workers 1 closeAndReleaseSonatypeStagingRepository + + - name: Create and push tag + env: + VERSION: ${{ github.event.inputs.version }} + run: | + git tag "v${VERSION}" + git push origin "v${VERSION}" + + - name: Create release on GitHub + uses: ncipollo/release-action@v1 + with: + tag: v${{ github.event.inputs.version }} + token: ${{ secrets.GITHUB_TOKEN }} + body: | + Manual release v${{ github.event.inputs.version }}. diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml deleted file mode 100644 index 0763ef73..00000000 --- a/.github/workflows/prerelease.yml +++ /dev/null @@ -1,52 +0,0 @@ -name: Pre-release - -on: - release: - types: [prereleased] - -jobs: - Prerelease: - name: 🚀 Pre-release - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - - name: Resolve version - run: | - TAG="${{ github.event.release.tag_name }}" - if [ -n "$TAG" ]; then - echo "VERSION=$TAG" >> $GITHUB_ENV - else - PROP=$(grep '^version=' gradle.properties | cut -d'=' -f2) - if [ -n "$PROP" ]; then - echo "VERSION=$PROP" >> $GITHUB_ENV - else - echo "❌ Could not determine version from tag or gradle.properties" && exit 1 - fi - fi - - - name: Setup JDK 17 - uses: actions/setup-java@v5.1.0 - with: - distribution: 'corretto' - java-version: '17' - - - name: Setup Gradle - uses: gradle/actions/setup-gradle@v4 - - - name: Publish to MavenCentral - run: | - ./gradlew -Pversion=$VERSION publishToSonatype --no-daemon --max-workers 1 closeAndReleaseSonatypeStagingRepository - env: - STREAM_API_KEY: ${{ vars.STREAM_API_KEY }} - STREAM_API_SECRET: ${{ secrets.STREAM_API_SECRET }} - GPG_KEY_CONTENTS: ${{ secrets.GPG_KEY_CONTENTS }} - OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }} - OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} - SIGNING_KEY_ID: ${{ secrets.SIGNING_KEY_ID }} - SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} - SIGNING_SECRET_KEY_RING_FILE: ${{ secrets.SIGNING_SECRET_KEY_RING_FILE }} - SONATYPE_STAGING_PROFILE_ID: ${{ secrets.SONATYPE_STAGING_PROFILE_ID }} - diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6d763e1c..5b1bdcd5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,36 +6,91 @@ on: branches: - main +concurrency: + group: release-${{ github.event.pull_request.base.ref }} + cancel-in-progress: true + +permissions: + contents: write + jobs: - Release: + release: name: 🚀 Release - if: github.event.pull_request.merged && startsWith(github.head_ref, 'release-') + if: github.event.pull_request.merged == true runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 + ref: ${{ github.event.pull_request.base.ref }} - - uses: actions/github-script@v6 - with: - script: | - // Getting the release version from the PR source branch - // Source branch looks like this: release-1.0.0 - const version = context.payload.pull_request.head.ref.replace(/^release-/, '') - core.exportVariable('VERSION', version) + - name: Skip when PR is already released + id: already_released + run: | + if git log --oneline --grep="(pr #${{ github.event.pull_request.number }})" -n 1 | grep -q "chore(release):"; then + echo "value=true" >> "$GITHUB_OUTPUT" + else + echo "value=false" >> "$GITHUB_OUTPUT" + fi + + - name: Determine and apply version bump + id: release_meta + if: steps.already_released.outputs.value != 'true' + env: + PR_TITLE: ${{ github.event.pull_request.title }} + PR_BODY: ${{ github.event.pull_request.body }} + run: | + PR_BODY_FILE=$(mktemp) + printf '%s' "$PR_BODY" > "$PR_BODY_FILE" + bash scripts/release/bump_version.sh \ + --title "$PR_TITLE" \ + --body-file "$PR_BODY_FILE" \ + --output "$GITHUB_OUTPUT" + + - name: Stop when PR does not require release + if: steps.already_released.outputs.value == 'true' || steps.release_meta.outputs.should_release != 'true' + run: | + if [ "${{ steps.already_released.outputs.value }}" = "true" ]; then + echo "PR #${{ github.event.pull_request.number }} is already released; skipping." + exit 0 + fi + echo "No release type found in PR title; skipping." + exit 0 - name: Setup JDK 17 + if: steps.already_released.outputs.value != 'true' && steps.release_meta.outputs.should_release == 'true' uses: actions/setup-java@v5.1.0 with: distribution: 'corretto' java-version: '17' - name: Setup Gradle + if: steps.already_released.outputs.value != 'true' && steps.release_meta.outputs.should_release == 'true' uses: gradle/actions/setup-gradle@v4 + - name: Commit version files + if: steps.already_released.outputs.value != 'true' && steps.release_meta.outputs.should_release == 'true' + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add gradle.properties + if git diff --cached --quiet; then + echo "No version changes to commit." + exit 0 + fi + git commit -m "chore(release): v${{ steps.release_meta.outputs.version }} (pr #${{ github.event.pull_request.number }})" + git push origin "HEAD:${{ github.event.pull_request.base.ref }}" + + - name: Create release tag + if: steps.already_released.outputs.value != 'true' && steps.release_meta.outputs.should_release == 'true' + run: | + git tag "${{ steps.release_meta.outputs.tag }}" + git push origin "${{ steps.release_meta.outputs.tag }}" + - name: Publish to MavenCentral + if: steps.already_released.outputs.value != 'true' && steps.release_meta.outputs.should_release == 'true' run: | - ./gradlew publishToSonatype --no-daemon --max-workers 1 closeAndReleaseSonatypeStagingRepository + ./gradlew -Pversion=${{ steps.release_meta.outputs.version }} publishToSonatype --no-daemon --max-workers 1 closeAndReleaseSonatypeStagingRepository env: STREAM_API_KEY: ${{ vars.STREAM_API_KEY }} STREAM_API_SECRET: ${{ secrets.STREAM_API_SECRET }} @@ -48,7 +103,14 @@ jobs: SONATYPE_STAGING_PROFILE_ID: ${{ secrets.SONATYPE_STAGING_PROFILE_ID }} - name: Create release on GitHub + if: steps.already_released.outputs.value != 'true' && steps.release_meta.outputs.should_release == 'true' uses: ncipollo/release-action@v1 with: - tag: ${{ env.VERSION }} + tag: ${{ steps.release_meta.outputs.tag }} token: ${{ secrets.GITHUB_TOKEN }} + body: | + Release v${{ steps.release_meta.outputs.version }} + + - Bump type: `${{ steps.release_meta.outputs.bump }}` + - Previous: `${{ steps.release_meta.outputs.previous_version }}` + - Next: `${{ steps.release_meta.outputs.version }}` diff --git a/README.md b/README.md index e66343a9..ece88599 100644 --- a/README.md +++ b/README.md @@ -127,3 +127,18 @@ To regenerate the Java source from OpenAPI, just run the `./generate.sh` script ## Contributing Contributions are welcome! Please read the [contributing guidelines](CONTRIBUTING.md) to get started. + +## Release Process + +Releases use two paths: + +- Default: automatic release when a PR is merged to `main`. +- Fallback: manual release using `.github/workflows/manual-release.yml` (admin use only). + +Automatic semver bump rules are based on merged PR title/body: + +- `feat:` -> minor +- `fix:` (or `bug:`) -> patch +- `feat!:` or `BREAKING CHANGE` in PR body -> major + +PRs with other prefixes do not trigger a release. diff --git a/scripts/release/bump_version.sh b/scripts/release/bump_version.sh new file mode 100644 index 00000000..ed7b250e --- /dev/null +++ b/scripts/release/bump_version.sh @@ -0,0 +1,117 @@ +#!/usr/bin/env bash + +set -euo pipefail + +title="" +body="" +body_file="" +output="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --title) + title="${2:-}" + shift 2 + ;; + --body) + body="${2:-}" + shift 2 + ;; + --body-file) + body_file="${2:-}" + shift 2 + ;; + --output) + output="${2:-}" + shift 2 + ;; + *) + echo "unknown argument: $1" >&2 + exit 1 + ;; + esac +done + +if [[ -n "${body_file}" ]]; then + body="$(<"${body_file}")" +fi + +determine_bump() { + local pr_title="$1" + local pr_body="$2" + + if [[ "${pr_body}" =~ BREAKING[[:space:]-]CHANGE ]]; then + echo "major" + return + fi + + if ! echo "${pr_title}" | grep -Eq '^([a-zA-Z]+)(\([^)]+\))?(!)?:'; then + echo "none" + return + fi + + if echo "${pr_title}" | grep -Eq '^([a-zA-Z]+)(\([^)]+\))?!:'; then + echo "major" + return + fi + + local type + type="$(echo "${pr_title}" | sed -E 's/^([a-zA-Z]+).*/\1/' | tr '[:upper:]' '[:lower:]')" + if [[ "${type}" == "feat" ]]; then + echo "minor" + return + fi + if [[ "${type}" == "fix" || "${type}" == "bug" ]]; then + echo "patch" + return + fi + + echo "none" +} + +bump="$(determine_bump "${title}" "${body}")" + +write_output() { + local key="$1" + local value="$2" + if [[ -n "${output}" ]]; then + echo "${key}=${value}" >> "${output}" + else + echo "${key}=${value}" + fi +} + +if [[ "${bump}" == "none" ]]; then + write_output "should_release" "false" + write_output "bump" "none" + exit 0 +fi + +latest_version="$(git tag --list | sed 's/^v//' | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -n 1)" +if [[ -z "${latest_version}" ]]; then + latest_version="$(grep '^version=' gradle.properties | cut -d'=' -f2)" +fi +if [[ -z "${latest_version}" ]]; then + latest_version="0.0.0" +fi + +IFS='.' read -r major minor patch <<< "${latest_version}" +case "${bump}" in + major) + next_version="$((major + 1)).0.0" + ;; + minor) + next_version="${major}.$((minor + 1)).0" + ;; + patch) + next_version="${major}.${minor}.$((patch + 1))" + ;; +esac + +sed -i "s/^version=.*/version=${next_version}/" gradle.properties + +write_output "should_release" "true" +write_output "bump" "${bump}" +write_output "previous_version" "${latest_version}" +write_output "version" "${next_version}" +write_output "tag" "v${next_version}"