From 1ba30694a40e68c69f8b07395f79784cc42f5bdd Mon Sep 17 00:00:00 2001 From: Aditya Agarwal Date: Fri, 1 May 2026 17:26:30 +0200 Subject: [PATCH 1/4] fix(ci): adopt PR-title release automation Replace release-branch based publishing with merged-PR title semantic releases, add version bump scripting, and enforce conventional PR titles in CI. Co-authored-by: Cursor --- .github/workflows/ci.yml | 11 ++- .github/workflows/initiate_release.yml | 54 ------------ .github/workflows/release.yml | 82 +++++++++++++++--- scripts/release/bump_version.sh | 114 +++++++++++++++++++++++++ 4 files changed, 195 insertions(+), 66 deletions(-) delete mode 100644 .github/workflows/initiate_release.yml create mode 100644 scripts/release/bump_version.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a9ae731..762e18d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,10 +9,19 @@ on: - main concurrency: - group: ${{ github.workflow }}-${{ github.head_ref }} + group: ${{ github.workflow }}-${{ github.head_ref || github.ref_name }} cancel-in-progress: true jobs: + check-pr-title: + name: Validate PR title + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + steps: + - uses: aslafy-z/conventional-pr-title-action@v3 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + test-build: name: 👷 Test & Build ${{ matrix.goVer }}${{ matrix.label }} environment: feeds-enabled-shard diff --git a/.github/workflows/initiate_release.yml b/.github/workflows/initiate_release.yml deleted file mode 100644 index b254009..0000000 --- a/.github/workflows/initiate_release.yml +++ /dev/null @@ -1,54 +0,0 @@ -name: Create release PR - -on: - workflow_dispatch: - inputs: - version: - description: "The new version number following semantic versioning convention. Example: v1.40.1" - required: true - type: string - pattern: "^v[0-9]+\\.[0-9]+\\.[0-9]+$" - -jobs: - init_release: - name: 🚀 Create release PR - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 # gives the changelog generator access to all previous commits - - - name: Update CHANGELOG.md and version.go, then push release branch - env: - VERSION: ${{ github.event.inputs.version }} - run: | - # Generate the changelog without creating a tag or commit - npx --yes standard-version@9.3.2 --release-as "$VERSION" --skip.tag --skip.commit --tag-prefix=v - - # Update version in version.go - sed -i "s/\(versionName = \)\"[^\"]*\"/\1\"$VERSION\"/" version.go - - # Stage the new or updated files - git add CHANGELOG.md version.go - - # Set up Git configuration - git config --global user.name 'github-actions' - git config --global user.email 'release@getstream.io' - - # Create a release branch with the version and push it - git checkout -q -b "release-$VERSION" - 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/release.yml b/.github/workflows/release.yml index 05b51b8..aecb529 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,25 +6,85 @@ 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@v4 - - - 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.split('-')[1] - core.exportVariable('VERSION', version) + fetch-depth: 0 + ref: ${{ github.event.pull_request.base.ref }} + + - 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: 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 version.go + 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: 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 }} - generateReleaseNotes: true + 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/scripts/release/bump_version.sh b/scripts/release/bump_version.sh new file mode 100644 index 0000000..a878518 --- /dev/null +++ b/scripts/release/bump_version.sh @@ -0,0 +1,114 @@ +#!/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="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|versionName = \".*\"|versionName = \"v${next_version}\"|" version.go + +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}" From e2ddb560cb7ccf8b67c5e430d4ed3d674685b343 Mon Sep 17 00:00:00 2001 From: Aditya Agarwal Date: Fri, 1 May 2026 17:27:44 +0200 Subject: [PATCH 2/4] chore(ci): simplify workflow changes Remove the extra PR-title validation job to keep CI minimal while retaining the release automation updates. Co-authored-by: Cursor --- .github/workflows/ci.yml | 9 --------- 1 file changed, 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 762e18d..f9b6c1f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,15 +13,6 @@ concurrency: cancel-in-progress: true jobs: - check-pr-title: - name: Validate PR title - if: github.event_name == 'pull_request' - runs-on: ubuntu-latest - steps: - - uses: aslafy-z/conventional-pr-title-action@v3 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - test-build: name: 👷 Test & Build ${{ matrix.goVer }}${{ matrix.label }} environment: feeds-enabled-shard From ba799cd3a7b879585c6931c5bffc8385bf5fb453 Mon Sep 17 00:00:00 2001 From: Aditya Agarwal Date: Fri, 1 May 2026 17:37:50 +0200 Subject: [PATCH 3/4] chore(release): add manual fallback workflow Add an admin-triggered manual release workflow with explicit version input as a break-glass path. Co-authored-by: Cursor --- .github/workflows/manual-release.yml | 68 ++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 .github/workflows/manual-release.yml diff --git a/.github/workflows/manual-release.yml b/.github/workflows/manual-release.yml new file mode 100644 index 0000000..c10cf23 --- /dev/null +++ b/.github/workflows/manual-release.yml @@ -0,0 +1,68 @@ +name: Manual Release + +on: + workflow_dispatch: + inputs: + version: + description: "Release version (example: 4.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: Set up Go + uses: actions/setup-go@v5 + with: + go-version: "1.23" + + - name: Update version file + env: + VERSION: ${{ github.event.inputs.version }} + run: | + sed -i "s|versionName = \".*\"|versionName = \"v${VERSION}\"|" version.go + + - 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 version.go + 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: Run tests + env: + STREAM_BASE_URL: ${{ vars.STREAM_BASE_URL }} + STREAM_API_KEY: ${{ vars.STREAM_API_KEY }} + STREAM_API_SECRET: ${{ secrets.STREAM_API_SECRET }} + run: go test -short -v ./... + + - 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 }} + generateReleaseNotes: true From ebc913bb9931b48d6f739ba311b8676bc3fc2249 Mon Sep 17 00:00:00 2001 From: Aditya Agarwal Date: Fri, 1 May 2026 17:40:23 +0200 Subject: [PATCH 4/4] docs(release): document auto and manual release paths Add a concise release process section describing PR-title semver rules and the manual fallback workflow. Co-authored-by: Cursor --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index f2e7aa0..b1042b5 100644 --- a/README.md +++ b/README.md @@ -63,3 +63,18 @@ We've recently closed a [$38 million Series B funding round](https://techcrunch. Our APIs are used by more than a billion end-users, and you'll have a chance to make a huge impact on the product within a team of the strongest engineers all over the world. Check out our current openings and apply via [Stream's website](https://getstream.io/team/#jobs). + +## 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.