diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 88d12da0..648867f3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,139 +1,18 @@ name: CI +# pull_request only. A push trigger alongside it ran the whole suite twice on the same +# SHA, and the duplicate integration legs raced each other against the Stream app +# several SDK repos share. The trade is that a merge to master, and a direct push to it, +# now run nothing: release.yml runs this same workflow only when a release is pending, +# so the gate before a tag is covered but the routine post-merge signal is gone. on: - push: - branches: [ master, main ] pull_request: - branches: [ master, main ] - -concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.ref_name }} - cancel-in-progress: true + branches: [ master, main, '*.x' ] permissions: contents: read jobs: - unit: - name: Unit Tests & Code Quality - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: '3.1.0' - - - name: Install dependencies - run: bundle install --jobs 4 --retry 3 - - - name: Run unit tests - run: make test - - - name: Run code quality checks - run: | - make format-check - make lint - make security - - integration-chat: - name: Chat Integration Tests - runs-on: ubuntu-latest - environment: ci - if: github.event_name == 'push' || github.event_name == 'pull_request' - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: '3.1.0' - - - name: Install dependencies - run: bundle install --jobs 4 --retry 3 - - - name: Run chat integration tests - env: - STREAM_API_KEY: ${{ vars.STREAM_API_KEY }} - STREAM_API_SECRET: ${{ secrets.STREAM_API_SECRET }} - STREAM_BASE_URL: ${{ vars.STREAM_BASE_URL }} - run: make test-integration-chat - - integration-feed: - name: Feed Integration Tests - runs-on: ubuntu-latest - environment: ci - if: github.event_name == 'push' || github.event_name == 'pull_request' - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: '3.1.0' - - - name: Install dependencies - run: bundle install --jobs 4 --retry 3 - - - name: Run feed integration tests - env: - STREAM_API_KEY: ${{ vars.STREAM_API_KEY }} - STREAM_API_SECRET: ${{ secrets.STREAM_API_SECRET }} - STREAM_BASE_URL: ${{ vars.STREAM_BASE_URL }} - run: make test-integration-feed - - integration-video: - name: Video Integration Tests - runs-on: ubuntu-latest - environment: ci - if: github.event_name == 'push' || github.event_name == 'pull_request' - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: '3.1.0' - - - name: Install dependencies - run: bundle install --jobs 4 --retry 3 - - - name: Run video integration tests - env: - STREAM_API_KEY: ${{ vars.STREAM_VIDEO_API_KEY }} - STREAM_API_SECRET: ${{ secrets.STREAM_VIDEO_API_SECRET }} - STREAM_BASE_URL: ${{ vars.STREAM_VIDEO_BASE_URL }} - run: make test-integration-video - - integration-gcp-lb: - name: GCP load balancer keep-alive - runs-on: ubuntu-latest - environment: ci - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: '3.1.0' - - - name: Install dependencies - run: bundle install --jobs 4 --retry 3 - - - name: Run GCP keep-alive integration test - env: - STREAM_API_KEY: ${{ vars.STREAM_GCP_API_KEY || vars.STREAM_API_KEY }} - STREAM_API_SECRET: ${{ secrets.STREAM_GCP_API_SECRET || secrets.STREAM_API_SECRET }} - STREAM_BASE_URL: ${{ vars.STREAM_GCP_BASE_URL }} - run: make test-integration-gcp-lb + tests: + uses: ./.github/workflows/run_tests.yml + secrets: inherit diff --git a/.github/workflows/pr_title.yml b/.github/workflows/pr_title.yml new file mode 100644 index 00000000..923683d8 --- /dev/null +++ b/.github/workflows/pr_title.yml @@ -0,0 +1,17 @@ +name: Lint PR title + +on: + pull_request: + types: [opened, edited, reopened, synchronize] + +permissions: + pull-requests: read + +jobs: + pr_title: + name: 👮 Conventional PR title + runs-on: ubuntu-latest + steps: + - uses: amannn/action-semantic-pull-request@v6 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b316bba1..11d68749 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,217 +1,186 @@ name: Release on: + push: + branches: + - master + - '*.x' workflow_dispatch: inputs: - version_bump: - description: 'Version bump type for manual release' - required: true - default: 'patch' - type: choice - options: - - patch - - minor - - major - use_current_version: - description: 'Skip version bump and publish the version already set in lib/getstream_ruby/version.rb' + publish_tag: + description: 'Existing tag to (re)publish to RubyGems, e.g. v12.1.1. Leave empty for a normal release run.' required: false - default: false - type: boolean - pull_request: - types: [closed] - branches: - - main - - master + default: '' + +permissions: + contents: read concurrency: - group: release-${{ github.event.pull_request.base.ref || github.ref_name }} + group: release-${{ github.ref_name }} cancel-in-progress: false -permissions: - contents: write - jobs: - release: - name: 🚀 Release - if: github.event_name == 'workflow_dispatch' || github.event.pull_request.merged == true + # Reversible half: keep the Release PR current. Stands down only while a release is + # already pending, because until that one is tagged there is no release commit to stop + # the walk at and it would propose the same commits again in a second Release PR. + release-pr: + name: Release PR + needs: detect + if: needs.detect.outputs.pending != 'true' runs-on: ubuntu-latest - environment: ci + timeout-minutes: 5 + permissions: + contents: write + issues: write + pull-requests: write steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.ref || github.ref_name }} - - - name: Set up Ruby - uses: ruby/setup-ruby@v1 + - uses: googleapis/release-please-action@v4 with: - ruby-version: '3.1.0' - - - name: Install dependencies - run: bundle install --jobs 4 --retry 3 - - - name: Skip when PR is already released - id: already_released - run: | - if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - echo "value=false" >> "$GITHUB_OUTPUT" - else - # Idempotency is tracked on the release tag (annotated with the PR number - # in "Create release tag"), not on a default-branch commit: the version - # bump is no longer pushed to the protected default branch. - if git for-each-ref refs/tags --format='%(contents)' | grep -q "(pr #${{ github.event.pull_request.number }})"; then - echo "value=true" >> "$GITHUB_OUTPUT" - else - echo "value=false" >> "$GITHUB_OUTPUT" - fi - fi - - - name: Determine version bump (from PR metadata) - id: release_meta - if: github.event_name == 'pull_request' && steps.already_released.outputs.value != 'true' + config-file: release-please-config.json + manifest-file: .release-please-manifest.json + target-branch: ${{ github.ref_name }} + skip-github-release: true + + # Tagging, the GitHub Release and the gem push are irreversible, so the suite has to + # run before them, which means knowing a release is pending before the suite starts. + # The tag lands on the merged Release PR's merge commit while the suite runs on this + # workflow's own commit, so `ready` also requires those to be the same commit. They + # are, on the path that matters: the push of that merge. + detect: + name: Detect pending release + if: >- + (github.event_name == 'push' || inputs.publish_tag == '') && + (github.ref_name == 'master' || endsWith(github.ref_name, '.x')) + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: read + pull-requests: read + issues: read + outputs: + pending: ${{ steps.find.outputs.pending }} + ready: ${{ steps.find.outputs.ready }} + steps: + - name: Find a merged Release PR waiting to be tagged + id: find env: - PR_TITLE: ${{ github.event.pull_request.title }} - run: | - ruby scripts/release/bump_version.rb \ - --title "$PR_TITLE" \ - --output "$GITHUB_OUTPUT" - - - name: Determine version bump (manual) - id: release_meta_manual - if: github.event_name == 'workflow_dispatch' - run: | - ruby scripts/release/bump_version.rb \ - --manual-bump "${{ github.event.inputs.version_bump }}" \ - --use-current-version "${{ github.event.inputs.use_current_version }}" \ - --output "$GITHUB_OUTPUT" - - - name: Consolidate release metadata - id: release_meta_final - run: | - if [ "${{ steps.already_released.outputs.value }}" = "true" ]; then - echo "should_release=false" >> "$GITHUB_OUTPUT" - echo "bump=none" >> "$GITHUB_OUTPUT" - exit 0 - fi - if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - echo "should_release=${{ steps.release_meta_manual.outputs.should_release }}" >> "$GITHUB_OUTPUT" - echo "bump=${{ steps.release_meta_manual.outputs.bump }}" >> "$GITHUB_OUTPUT" - echo "previous_version=${{ steps.release_meta_manual.outputs.previous_version }}" >> "$GITHUB_OUTPUT" - echo "version=${{ steps.release_meta_manual.outputs.version }}" >> "$GITHUB_OUTPUT" - echo "tag=${{ steps.release_meta_manual.outputs.tag }}" >> "$GITHUB_OUTPUT" - else - echo "should_release=${{ steps.release_meta.outputs.should_release }}" >> "$GITHUB_OUTPUT" - echo "bump=${{ steps.release_meta.outputs.bump }}" >> "$GITHUB_OUTPUT" - echo "previous_version=${{ steps.release_meta.outputs.previous_version }}" >> "$GITHUB_OUTPUT" - echo "version=${{ steps.release_meta.outputs.version }}" >> "$GITHUB_OUTPUT" - echo "tag=${{ steps.release_meta.outputs.tag }}" >> "$GITHUB_OUTPUT" - fi - - - name: Stop when release is not required - if: steps.release_meta_final.outputs.should_release != 'true' + GH_TOKEN: ${{ github.token }} + BASE: ${{ github.ref_name }} + HEAD_SHA: ${{ github.sha }} run: | - if [ "${{ steps.already_released.outputs.value }}" = "true" ]; then - echo "PR #${{ github.event.pull_request.number }} is already released; skipping." - elif [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - echo "Manual release was not requested; skipping." + pending=false + ready=false + + # Query the label directly. Listing closed PRs and filtering client-side loses + # a release that has slipped past the first page, which reads as "nothing to + # release" and passes. Filter before picking, too: a hotfix branch can hold + # its own pending release, and taking the newest label match would drop this + # branch's release on every run until the other one clears. + nums="$(gh api "repos/${GITHUB_REPOSITORY}/issues" \ + -X GET -f state=closed -f labels='autorelease: pending' -f per_page=20 \ + --jq '.[] | select(.pull_request != null) | .number')" + + num="" + sha="" + for n in $nums; do + sha="$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${n}" \ + --jq 'select(.merged_at != null and .base.ref == env.BASE) | .merge_commit_sha // empty')" + if [ -n "$sha" ]; then + num="$n" + break + fi + done + + if [ -z "$sha" ]; then + echo "No pending release on ${BASE}." + elif [ "$sha" != "$HEAD_SHA" ]; then + # Reached when an earlier release run failed after the Release PR merged. + # Tagging $sha here would tag a tree this run never tested, and failing + # would redden every later push, so stand down and say why. + pending=true + echo "::warning::Release PR #${num} is still pending at ${sha}, which is not this run's commit ${HEAD_SHA}. Re-run the workflow run for ${sha} to finish that release." else - echo "No release type found in PR title/body; skipping." + pending=true + ready=true + echo "Pending release #${num} will be tagged at ${sha}." fi - - name: Run lint and security checks - if: steps.release_meta_final.outputs.should_release == 'true' - run: | - make format-check - make lint - make security - - - name: Run unit tests - if: steps.release_meta_final.outputs.should_release == 'true' - run: make test - - - name: Run chat integration tests - if: steps.release_meta_final.outputs.should_release == 'true' - env: - STREAM_API_KEY: ${{ vars.STREAM_API_KEY }} - STREAM_API_SECRET: ${{ secrets.STREAM_API_SECRET }} - STREAM_BASE_URL: ${{ vars.STREAM_BASE_URL }} - run: make test-integration-chat - - - name: Run feed integration tests - if: steps.release_meta_final.outputs.should_release == 'true' - env: - STREAM_API_KEY: ${{ vars.STREAM_API_KEY }} - STREAM_API_SECRET: ${{ secrets.STREAM_API_SECRET }} - STREAM_BASE_URL: ${{ vars.STREAM_BASE_URL }} - run: make test-integration-feed + { + echo "pending=${pending}" + echo "ready=${ready}" + } >> "$GITHUB_OUTPUT" - - name: Run video integration tests - if: steps.release_meta_final.outputs.should_release == 'true' - env: - STREAM_API_KEY: ${{ vars.STREAM_VIDEO_API_KEY }} - STREAM_API_SECRET: ${{ secrets.STREAM_VIDEO_API_SECRET }} - STREAM_BASE_URL: ${{ vars.STREAM_VIDEO_BASE_URL }} - run: make test-integration-video + tests: + name: Tests + needs: detect + if: needs.detect.outputs.ready == 'true' + uses: ./.github/workflows/run_tests.yml + secrets: inherit - # Commit the bump locally only, so the release tag points at a tree with the - # correct version. It is intentionally NOT pushed to the protected default - # branch (which rejects direct pushes). Versioning is driven by tags, not by - # version.rb on the default branch (see bump_version.rb: find_latest_semver_tag). - - name: Commit version files (local, for tagging only) - if: steps.release_meta_final.outputs.should_release == 'true' + # Irreversible half. + release: + name: 🚀 Tag and release + needs: [detect, tests] + if: needs.detect.outputs.ready == 'true' + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: write + issues: write + pull-requests: write + outputs: + release_created: ${{ steps.release.outputs.release_created }} + tag_name: ${{ steps.release.outputs.tag_name }} + steps: + - uses: googleapis/release-please-action@v4 + id: release + with: + config-file: release-please-config.json + manifest-file: .release-please-manifest.json + target-branch: ${{ github.ref_name }} + skip-github-pull-request: true + + # Chained rather than triggered on the release event, because a GitHub Release + # created with GITHUB_TOKEN starts no new workflow run. Also reachable on its own + # through workflow_dispatch with publish_tag, which is the recovery path once + # GitHub has retired the original run and "Re-run failed jobs" is gone. + publish: + name: 💎 Publish to RubyGems + needs: release + if: >- + !cancelled() && + (inputs.publish_tag != '' || needs.release.outputs.release_created == 'true') + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + steps: + - name: Resolve tag + id: target env: - VERSION: ${{ steps.release_meta_final.outputs.version }} + PUBLISH_TAG: ${{ inputs.publish_tag }} + RELEASE_TAG: ${{ needs.release.outputs.tag_name }} run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git add lib/getstream_ruby/version.rb - if git diff --cached --quiet; then - echo "No version changes to commit." - else - git commit -m "chore(release): v${VERSION}" - fi + tag="${PUBLISH_TAG:-$RELEASE_TAG}" + echo "tag=${tag}" >> "$GITHUB_OUTPUT" + echo "version=${tag#v}" >> "$GITHUB_OUTPUT" - - name: Create release tag - if: steps.release_meta_final.outputs.should_release == 'true' - env: - TAG: ${{ steps.release_meta_final.outputs.tag }} - PR: ${{ github.event.pull_request.number }} - run: | - if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then - echo "Tag ${TAG} already exists; skipping tag creation." - exit 0 - fi - # Annotate the tag with the trigger; the PR number is used by the - # "Skip when PR is already released" idempotency check. - if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - git tag -a "${TAG}" -m "chore(release): ${TAG} (manual)" - else - git tag -a "${TAG}" -m "chore(release): ${TAG} (pr #${PR})" - fi - git push origin "${TAG}" + - uses: actions/checkout@v4 + with: + ref: ${{ steps.target.outputs.tag }} + # gem build runs the gemspec, which is project code, and nothing here writes + # to the repository, so do not leave GITHUB_TOKEN in .git/config. + persist-credentials: false + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.1.0' - name: Build and publish gem - if: steps.release_meta_final.outputs.should_release == 'true' env: GEM_HOST_API_KEY: ${{ secrets.RUBYGEMS_API_KEY }} - VERSION: ${{ steps.release_meta_final.outputs.version }} + VERSION: ${{ steps.target.outputs.version }} run: | gem build getstream-ruby.gemspec - gem push "getstream-ruby-${VERSION}.gem" --key "$GEM_HOST_API_KEY" - - - name: Create release on GitHub - if: steps.release_meta_final.outputs.should_release == 'true' - uses: ncipollo/release-action@v1 - with: - tag: ${{ steps.release_meta_final.outputs.tag }} - token: ${{ secrets.GITHUB_TOKEN }} - skipIfReleaseExists: true - body: | - Release v${{ steps.release_meta_final.outputs.version }} - - - Bump type: `${{ steps.release_meta_final.outputs.bump }}` - - Previous: `${{ steps.release_meta_final.outputs.previous_version }}` - - Next: `${{ steps.release_meta_final.outputs.version }}` - - Trigger: `${{ github.event_name }}` - - Install with: `gem install getstream-ruby -v ${{ steps.release_meta_final.outputs.version }}` + gem push "getstream-ruby-${VERSION}.gem" diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml new file mode 100644 index 00000000..fcf5760b --- /dev/null +++ b/.github/workflows/run_tests.yml @@ -0,0 +1,146 @@ +name: _run-tests + +on: + workflow_call: + secrets: + STREAM_API_SECRET: + required: true + STREAM_VIDEO_API_SECRET: + required: true + STREAM_GCP_API_SECRET: + required: false + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + unit: + name: Unit Tests & Code Quality + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.1.0' + + - name: Install dependencies + run: bundle install --jobs 4 --retry 3 + + - name: Run unit tests + run: make test + + - name: Run code quality checks + run: | + make format-check + make lint + make security + + integration-chat: + name: Chat Integration Tests + runs-on: ubuntu-latest + timeout-minutes: 30 + environment: ci + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.1.0' + + - name: Install dependencies + run: bundle install --jobs 4 --retry 3 + + - name: Run chat integration tests + env: + STREAM_API_KEY: ${{ vars.STREAM_API_KEY }} + STREAM_API_SECRET: ${{ secrets.STREAM_API_SECRET }} + STREAM_BASE_URL: ${{ vars.STREAM_BASE_URL }} + run: make test-integration-chat + + integration-feed: + name: Feed Integration Tests + # Runs after chat rather than beside it: both blank and restore the app-global + # file_upload_config, so in parallel one suite's restore lands mid-assertion in + # the other. !cancelled() keeps the ordering without inheriting the implicit + # success(), so a red chat no longer hides whether feed passes. + needs: integration-chat + if: '!cancelled()' + runs-on: ubuntu-latest + timeout-minutes: 30 + environment: ci + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.1.0' + + - name: Install dependencies + run: bundle install --jobs 4 --retry 3 + + - name: Run feed integration tests + env: + STREAM_API_KEY: ${{ vars.STREAM_API_KEY }} + STREAM_API_SECRET: ${{ secrets.STREAM_API_SECRET }} + STREAM_BASE_URL: ${{ vars.STREAM_BASE_URL }} + run: make test-integration-feed + + integration-video: + name: Video Integration Tests + runs-on: ubuntu-latest + timeout-minutes: 30 + environment: ci + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.1.0' + + - name: Install dependencies + run: bundle install --jobs 4 --retry 3 + + - name: Run video integration tests + env: + STREAM_API_KEY: ${{ vars.STREAM_VIDEO_API_KEY }} + STREAM_API_SECRET: ${{ secrets.STREAM_VIDEO_API_SECRET }} + STREAM_BASE_URL: ${{ vars.STREAM_VIDEO_BASE_URL }} + run: make test-integration-video + + integration-gcp-lb: + name: GCP load balancer keep-alive + runs-on: ubuntu-latest + timeout-minutes: 30 + environment: ci + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.1.0' + + - name: Install dependencies + run: bundle install --jobs 4 --retry 3 + + - name: Run GCP keep-alive integration test + env: + STREAM_API_KEY: ${{ vars.STREAM_GCP_API_KEY || vars.STREAM_API_KEY }} + STREAM_API_SECRET: ${{ secrets.STREAM_GCP_API_SECRET || secrets.STREAM_API_SECRET }} + STREAM_BASE_URL: ${{ vars.STREAM_GCP_BASE_URL }} + run: make test-integration-gcp-lb diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 00000000..ca3a5114 --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "12.1.0" +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 71ecf64a..3b5688e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,28 @@ -## [Unreleased] +# Changelog + +This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +Entries from 12.1.1 on are generated by release-please from commit messages; earlier +ones were written by hand. + +## [12.1.0] - 2026-09-10 + +### Changed + +- Regenerated the OpenAPI client (FEEDS-1763.1). + +## [12.0.0] - 2026-09-02 + +### Changed + +- **Breaking:** regenerated the OpenAPI client (FEEDS-1830). + +## [11.1.0] - 2026-08-25 + +### Changed + +- Regenerated from the latest OpenAPI (CHA-4947). + +## [11.0.1] - 2026-08-24 ### Fixed @@ -8,6 +32,18 @@ `SSL_read: unexpected eof while reading` (`GetStreamRuby::TransportError`) on the next request. Override with `idle_timeout:` or `STREAM_IDLE_TIMEOUT`. +## [11.0.0] - 2026-08-17 + +### Changed + +- **Breaking:** regenerated from chat v235.17.1. + +## [10.1.0] - 2026-08-12 + +### Fixed + +- Name the HTTP status when the error body is not JSON (#76). + ## [10.0.0] - 2026-07-24 ### Added diff --git a/Makefile b/Makefile index 09330152..e9a4d4eb 100644 --- a/Makefile +++ b/Makefile @@ -72,16 +72,6 @@ console: ## Start IRB console with SDK loaded version: ## Show current version @ruby -e "require './lib/getstream_ruby/version'; puts GetStreamRuby::VERSION" -# Version management -patch: ## Bump patch version (0.0.1 -> 0.0.2) - @./scripts/version-bump.sh patch - -minor: ## Bump minor version (0.0.1 -> 0.1.0) - @./scripts/version-bump.sh minor - -major: ## Bump major version (0.0.1 -> 1.0.0) - @./scripts/version-bump.sh major - # Development helpers dev-setup: setup ## Complete development setup @echo "Development setup complete!" diff --git a/README.md b/README.md index f28dde7c..8f4d5b36 100644 --- a/README.md +++ b/README.md @@ -386,20 +386,47 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/getstr ## Release Process -Releases use two paths, both handled by `.github/workflows/release.yml`: - -- **Default**: automatic release when a PR is merged to `main`/`master`. The PR title drives the semver bump. -- **Fallback**: manual release via the `Release` workflow's `workflow_dispatch` (admin use). Select a `version_bump` (`patch`/`minor`/`major`). `use_current_version=true` skips the bump and publishes whatever is already in `lib/getstream_ruby/version.rb`. - -Automatic semver bump rules: - -- `feat:` -> minor -- `fix:` (or `bug:`) -> patch -- `feat!:` or `(scope)!:` (the `!` marker) -> major - -PRs with any other prefix do not trigger a release. - -The release pipeline runs lint (`make format-check && make lint && make security`), the unit suite (`make test`), and all three integration suites (chat, feed, video) on the merged commit before publishing to RubyGems. Each step is idempotent; a failed run can be re-dispatched from the Actions UI. +Releases are driven by [release-please](https://github.com/googleapis/release-please). + +- Merge PRs to `master` with conventional-commit titles, using **Squash and merge**. The + title becomes the commit subject and decides the next version: `feat:` is a minor, + `fix:` and `perf:` are a patch, `feat!:` or `(scope)!:` is a major. Other types + (`chore`, `ci`, `docs`, `test`, `refactor`) ship nothing. +- Squashing is a convention here, not yet enforced. The repo still has + `allow_merge_commit: true`, `allow_rebase_merge: true` and + `squash_merge_commit_title: COMMIT_OR_PR_TITLE`, and until someone with admin sets + those to `false`, `false` and `PR_TITLE` (as getstream-go has), two things silently + skip a release: a merge commit, whose subject is not conventional and whose body only + yields a plain `feat:`/`fix:` prefix, never `feat!:`; and a single-commit PR, which + squashes to that commit's subject rather than the PR title. `pr_title.yml` only checks + the PR title field, so it passes in both cases. +- release-please keeps a Release PR open with the version bump in + `lib/getstream_ruby/version.rb` and `CHANGELOG.md`. It is opened by + `github-actions[bot]`, so approve it and run its held checks like any other PR. Never + edit the version by hand. +- Merging the Release PR runs `make format-check`, `make lint`, `make security`, + `make test` and the four integration suites (chat, feed, video, GCP load balancer) on + that merge commit, which is the commit the tag will point at. Only if that is green does the workflow create the tag and the + GitHub Release and push the gem to RubyGems. The order matters: a tag, a GitHub + Release and a gem push cannot be withdrawn, a failed push can be retried. + +If the suite goes red after the Release PR merged, the release stays pending and every +later push to `master` logs a warning naming the commit to go back to, rather than +failing. Recovery in both that case and a failed gem push is "Re-run failed jobs" on the +run for that merge commit. Once GitHub has retired the run, dispatch `Release` from `master` +with `publish_tag` set to the tag (for example `v12.1.1`), which builds and pushes that +tag without touching release-please. + +To force a specific version, type `Release-As: X.Y.Z` in the commit message box of the +squash dialog when merging a PR; the PR description is not copied there. To hotfix while +`master` carries unreleased work, branch `N.x` from the last tag, cherry-pick the fix, +and merge the Release PR that release-please opens against that branch. + +`last-release-sha` in `release-please-config.json` is temporary. `v12.1.0` sits on a bump +commit the previous workflow created off-branch and never pushed, so release-please +cannot reach it by walking `master` and would otherwise treat the whole history as +unreleased. Delete the key once a release-please-created release exists on `master`; the +walk stops at that release commit before it reaches the pin. ## License diff --git a/lib/getstream_ruby/version.rb b/lib/getstream_ruby/version.rb index eea7c82e..5aafd1ed 100644 --- a/lib/getstream_ruby/version.rb +++ b/lib/getstream_ruby/version.rb @@ -2,6 +2,6 @@ module GetStreamRuby - VERSION = '8.0.2' + VERSION = '12.1.0' end diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 00000000..18ea2a6b --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "last-release-sha": "283aabc8a7d1886caf0bde9aa7a8ae77f8098a6f", + "packages": { + ".": { + "release-type": "ruby", + "package-name": "getstream-ruby", + "version-file": "lib/getstream_ruby/version.rb", + "include-component-in-tag": false + } + } +} diff --git a/scripts/release/bump_version.rb b/scripts/release/bump_version.rb deleted file mode 100755 index 7bf0829f..00000000 --- a/scripts/release/bump_version.rb +++ /dev/null @@ -1,143 +0,0 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -require 'optparse' - -def run_command(command) - `#{command}`.to_s.strip -end - -def find_latest_semver_tag - tags = run_command('git tag --list').split(/\R/) - versions = tags.map(&:strip).map { |tag| tag.sub(/^v/, '') }.grep(/^\d+\.\d+\.\d+$/) - return '0.0.0' if versions.empty? - - versions.max_by { |version| version.split('.').map(&:to_i) } -end - -def determine_bump_type(title) - # Breaking changes are signalled only by the `!` marker in the title - # (e.g. `feat!:`). Free-text body/title prose is not trusted: a PR that - # merely mentions "BREAKING CHANGE" must not force a major bump. - match = title.strip.match(/^([a-z]+)(\([^)]+\))?(!)?:/i) - return 'none' unless match - - type = match[1].downcase - return 'major' if match[3] == '!' - return 'minor' if type == 'feat' - return 'patch' if %w[fix bug].include?(type) - - 'none' -end - -def increment_version(version, bump) - major, minor, patch = version.split('.').map(&:to_i) - - case bump - when 'major' - "#{major + 1}.0.0" - when 'minor' - "#{major}.#{minor + 1}.0" - when 'patch' - "#{major}.#{minor}.#{patch + 1}" - else - version - end -end - -def read_version_file(path) - content = File.read(path) - match = content.match(/VERSION\s*=\s*'([^']+)'/) - raise "Could not find VERSION in #{path}" unless match - - match[1] -end - -def update_version_file(path, version) - content = File.read(path) - updated = content.sub(/VERSION = '[^']+'/, "VERSION = '#{version}'") - raise "Could not update VERSION in #{path}" if updated == content - - File.write(path, updated) -end - -def write_outputs(output_path, values) - lines = "#{values.map { |k, v| "#{k}=#{v}" }.join("\n")}\n" - if output_path.empty? - print(lines) - return - end - - File.open(output_path, 'a') { |file| file.write(lines) } -end - -def truthy?(value) - value.to_s.strip.downcase == 'true' -end - -options = { - title: '', - output: '', - manual_bump: '', - use_current_version: 'false', -} - -OptionParser.new do |opts| - - opts.on('--title TITLE') { |value| options[:title] = value } - opts.on('--output FILE') { |value| options[:output] = value } - opts.on('--manual-bump TYPE') { |value| options[:manual_bump] = value } - opts.on('--use-current-version VAL') { |value| options[:use_current_version] = value } - -end.parse! - -VERSION_FILE = 'lib/getstream_ruby/version.rb' - -manual = options[:manual_bump].to_s.strip.downcase - -unless manual.empty? - unless %w[major minor patch].include?(manual) - warn('manual-bump must be one of: major, minor, patch') - exit(1) - end - - previous_version = find_latest_semver_tag - next_version = if truthy?(options[:use_current_version]) - read_version_file(VERSION_FILE) - else - updated = increment_version(previous_version, manual) - update_version_file(VERSION_FILE, updated) - updated - end - - write_outputs(options[:output], { - 'should_release' => 'true', - 'bump' => manual, - 'previous_version' => previous_version, - 'version' => next_version, - 'tag' => "v#{next_version}", - }) - exit(0) -end - -bump = determine_bump_type(options[:title].to_s) -if bump == 'none' - write_outputs(options[:output], { - 'should_release' => 'false', - 'bump' => 'none', - }) - exit(0) -end - -current_version = find_latest_semver_tag -next_version = increment_version(current_version, bump) - -update_version_file(VERSION_FILE, next_version) - -write_outputs(options[:output], { - 'should_release' => 'true', - 'bump' => bump, - 'previous_version' => current_version, - 'version' => next_version, - 'tag' => "v#{next_version}", - }) diff --git a/scripts/version-bump.sh b/scripts/version-bump.sh deleted file mode 100755 index e9cb3bfd..00000000 --- a/scripts/version-bump.sh +++ /dev/null @@ -1,87 +0,0 @@ -#!/bin/bash - -# Version bump script for GetStream Ruby SDK -# Usage: ./scripts/version-bump.sh [major|minor|patch] [release_notes] - -set -e - -VERSION_TYPE="${1:-patch}" -RELEASE_NOTES="${2:-}" - -if [[ ! "$VERSION_TYPE" =~ ^(major|minor|patch)$ ]]; then - echo "Error: Version type must be major, minor, or patch" - echo "Usage: $0 [major|minor|patch] [release_notes]" - exit 1 -fi - -# Get current version -CURRENT_VERSION=$(ruby -r "./lib/getstream_ruby/version.rb" -e "puts GetStreamRuby::VERSION") -echo "Current version: $CURRENT_VERSION" - -# Parse version components -IFS='.' read -r major minor patch <<< "$CURRENT_VERSION" - -# Calculate new version -case "$VERSION_TYPE" in - "major") - NEW_VERSION="$((major + 1)).0.0" - ;; - "minor") - NEW_VERSION="$major.$((minor + 1)).0" - ;; - "patch") - NEW_VERSION="$major.$minor.$((patch + 1))" - ;; -esac - -echo "New version: $NEW_VERSION" - -# Update version.rb (gemspec loads version dynamically from here) -if [[ "$OSTYPE" == "darwin"* ]]; then - sed -i '' "s/VERSION = '[^']*'/VERSION = '$NEW_VERSION'/" "lib/getstream_ruby/version.rb" -else - sed -i "s/VERSION = '[^']*'/VERSION = '$NEW_VERSION'/" "lib/getstream_ruby/version.rb" -fi - -echo "Updated version files to $NEW_VERSION" - -# Update CHANGELOG -CHANGELOG_FILE="CHANGELOG.md" - -# Create CHANGELOG.md if it doesn't exist -if [ ! -f "$CHANGELOG_FILE" ]; then - cat > "$CHANGELOG_FILE" << 'EOF' -# Changelog - -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -EOF -fi - -# Add new version entry -TEMP_FILE=$(mktemp) -echo "## [$NEW_VERSION] - $(date +%Y-%m-%d)" >> "$TEMP_FILE" -echo "" >> "$TEMP_FILE" - -if [ -n "$RELEASE_NOTES" ]; then - echo "$RELEASE_NOTES" >> "$TEMP_FILE" -else - echo "### $VERSION_TYPE^2 changes" >> "$TEMP_FILE" - echo "- " >> "$TEMP_FILE" -fi - -echo "" >> "$TEMP_FILE" -cat "$CHANGELOG_FILE" >> "$TEMP_FILE" -mv "$TEMP_FILE" "$CHANGELOG_FILE" - -echo "Updated CHANGELOG.md" - -echo "✅ Version bump complete: $CURRENT_VERSION → $NEW_VERSION" -echo "Files updated:" -echo " - lib/getstream_ruby/version.rb" -echo " - CHANGELOG.md" -echo "" -echo "Note: getstream-ruby.gemspec loads version dynamically from version.rb"