From d24039ba17aac6129a4d877491095b24257edb49 Mon Sep 17 00:00:00 2001 From: umair Date: Tue, 1 Sep 2026 17:04:44 +0100 Subject: [PATCH 1/4] Add lockstep release workflow, update contributing and migration docs - release.yml publishes ably-pubsub-core and ably-pubsub-server at the same version via RubyGems trusted publishing (OIDC, no long-lived keys). A pre-flight fails before anything is pushed if the version input, either gem's version constant, or the server gem's exact core pin disagree. Partial releases fail reversibly: re-running with the same version skips already-published gems. - CONTRIBUTING.md documents the two-gem layout and the new release process; the manual rake release and the ably-ruby-rest step are gone (both legacy gems are maintenance-only per PDR-091b). - UPDATING.md gains the 1.x -> 2.0 migration section with the old-name -> new-name mapping table, marked draft pending PDR-091d. Co-Authored-By: Claude Fable 5 --- .github/workflows/release.yml | 101 ++++++++++++++++++++++++++++++++++ CONTRIBUTING.md | 41 ++++++++++---- UPDATING.md | 32 +++++++++++ 3 files changed, 162 insertions(+), 12 deletions(-) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..d7113e1f7 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,101 @@ +name: Release + +# Releases ably-pubsub-core and ably-pubsub-server in lockstep at the same version +# (PDR-091b). The pre-flight fails before anything is pushed if the version input, +# the two gems' version files, or the server->core exact pin disagree. +# +# Publishing uses RubyGems trusted publishing (OIDC): both gems must have a Trusted +# Publisher configured on rubygems.org pointing at this repository and this workflow +# file. NOTE: the binding is to the repo owner+name, so it must be reconfigured when +# the repo is renamed to ably-pubsub-ruby. +# +# A partial release fails reversibly: if the server push fails after the core push +# succeeded, re-running the workflow with the same version skips the already-published +# core gem and publishes the server gem. + +on: + workflow_dispatch: + inputs: + version: + description: "Version to release, e.g. 2.0.0 — must match Ably::VERSION, Ably::PubSub::Server::VERSION and the server gemspec's core pin" + required: true + +permissions: {} + +jobs: + release: + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2 + with: + persist-credentials: false + + - uses: ruby/setup-ruby@afeafc3d1ab54a631816aba4c914a0081c12ff2f # v1.310.0 + with: + ruby-version: '3.3' + bundler-cache: false + + - name: 'Pre-flight: versions and pin must agree (nothing is pushed if this fails)' + env: + RELEASE_VERSION: ${{ github.event.inputs.version }} + run: | + ruby <<'RUBY' + version = ENV.fetch('RELEASE_VERSION') + abort "Invalid version input: #{version.inspect}" unless version.match?(/\A\d+\.\d+\.\d+(\.[0-9A-Za-z]+)*\z/) + + require_relative 'core/lib/ably/version' + require_relative 'server/lib/ably/pubsub/server/version' + + errors = [] + errors << "core Ably::VERSION is #{Ably::VERSION}, expected #{version}" unless Ably::VERSION == version + errors << "server Ably::PubSub::Server::VERSION is #{Ably::PubSub::Server::VERSION}, expected #{version}" unless Ably::PubSub::Server::VERSION == version + + server_spec = Gem::Specification.load('server/ably-pubsub-server.gemspec') + core_dep = server_spec.dependencies.find { |d| d.name == 'ably-pubsub-core' } + errors << "server gemspec pins ably-pubsub-core '#{core_dep&.requirement}', expected '= #{version}'" unless core_dep&.requirement.to_s == "= #{version}" + + abort errors.join("\n") unless errors.empty? + puts "Pre-flight OK: releasing ably-pubsub-core and ably-pubsub-server at #{version}" + RUBY + + - name: Configure RubyGems credentials (trusted publishing) + uses: rubygems/configure-rubygems-credentials@dc5a8d8553e6ee01fc26761a49e99e733d17954a # v2.1.0 + + - name: Publish both gems in lockstep + env: + RELEASE_VERSION: ${{ github.event.inputs.version }} + run: | + set -euo pipefail + v="${RELEASE_VERSION}" + + published() { + curl -sf "https://rubygems.org/api/v2/rubygems/$1/versions/${v}.json" >/dev/null + } + + push_gem() { + local name="$1" dir="$2" + if published "${name}"; then + echo "${name} ${v} is already on RubyGems, skipping (safe re-run)" + return 0 + fi + (cd "${dir}" && gem build "${name}.gemspec") + gem push "${dir}/${name}-${v}.gem" + } + + push_gem ably-pubsub-core core + + # The server gem pins the core at this exact version, so wait until the + # core version is visible on RubyGems before publishing the server gem. + for i in $(seq 1 30); do + published ably-pubsub-core && break + echo "Waiting for ably-pubsub-core ${v} to appear on RubyGems (${i}/30)..." + sleep 10 + done + published ably-pubsub-core || { echo "ably-pubsub-core ${v} did not appear on RubyGems"; exit 1; } + + push_gem ably-pubsub-server server + + echo "Released ably-pubsub-core and ably-pubsub-server at ${v}" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index eac0af682..c086220de 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,31 +1,48 @@ # Contributing +This repository hosts two gems, released in lockstep at the same version: + +- [`core/`](./core) — `ably-pubsub-core`: the shared implementation. An internal package; only Ably packages depend on it. +- [`server/`](./server) — `ably-pubsub-server`: the public server-side package. Its factory functions (`Ably::PubSub::Server.create_http_client` / `.create_realtime_client`) are the only recommended entry points. + +## Development + 1. Fork it 2. When pulling to local, make sure to also pull the `ably-common` repo (`git submodule init && git submodule update`) 3. Create your feature branch (`git checkout -b my-new-feature`) 4. Commit your changes (`git commit -am 'Add some feature'`) -5. Ensure you have added suitable tests and the test suite is passing(`bundle exec rspec`) +5. Ensure you have added suitable tests and the test suite is passing (`bundle exec rspec`) — the root `Gemfile` wires both gems up as path dependencies, so a single `bundle install` at the root covers everything 6. Push to the branch (`git push origin my-new-feature`) 7. Create a new Pull Request ---- - ## Release process -This library uses [semantic versioning](http://semver.org/). For each release, the following needs to be done: +This library uses [semantic versioning](http://semver.org/). `ably-pubsub-core` and `ably-pubsub-server` always release together at the same version: the release workflow refuses to publish them independently. + +For each release, the following needs to be done: -1. Create a branch for the release, named like `release/1.2.3` (where `1.2.3` is the new version number) -2. Update the version number in [version.rb](./lib/ably/version.rb) and commit the change. +1. Create a branch for the release, named like `release/2.0.1` (where `2.0.1` is the new version number) +2. Update the version number in **all three places**, which must agree (the release workflow's pre-flight enforces this): + - `Ably::VERSION` in [core/lib/ably/version.rb](./core/lib/ably/version.rb) + - `Ably::PubSub::Server::VERSION` in [server/lib/ably/pubsub/server/version.rb](./server/lib/ably/pubsub/server/version.rb) + - the exact-version `ably-pubsub-core` pin in [server/ably-pubsub-server.gemspec](./server/ably-pubsub-server.gemspec) (derived from the version constant, so it normally follows automatically) 3. Run [`github_changelog_generator`](https://github.com/github-changelog-generator/github-changelog-generator) to automate the update of the [CHANGELOG](./CHANGELOG.md). This may require some manual intervention, both in terms of how the command is run and how the change log file is modified. Your mileage may vary: - - The command you will need to run will look something like this: `github_changelog_generator -u ably -p ably-ruby --since-tag v1.2.3 --output delta.md --token $GITHUB_TOKEN_WITH_REPO_ACCESS`. Generate token [here](https://github.com/settings/tokens/new?description=GitHub%20Changelog%20Generator%20token). + - The command you will need to run will look something like this: `github_changelog_generator -u ably -p ably-ruby --since-tag v2.0.0 --output delta.md --token $GITHUB_TOKEN_WITH_REPO_ACCESS`. Generate token [here](https://github.com/settings/tokens/new?description=GitHub%20Changelog%20Generator%20token). - Using the command above, `--output delta.md` writes changes made after `--since-tag` to a new file - The contents of that new file (`delta.md`) then need to be manually inserted at the top of the `CHANGELOG.md`, changing the "Unreleased" heading and linking with the current version numbers - Also ensure that the "Full Changelog" link points to the new version tag instead of the `HEAD` 4. Commit this change: `git add CHANGELOG.md && git commit -m "Update change log."` 5. Ideally, run `rake doc:spec` to generate a new [spec file](./SPEC.md). Then commit these changes. 6. Make a PR against `main`. Once the PR is approved, merge it into `main`. -7. Add a tag to the new `main` head commit and push to origin such as `git tag v1.0.3 && git push origin v1.0.3`. -8. Visit [https://github.com/ably/ably-ruby/tags](https://github.com/ably/ably-ruby/tags) and `Add release notes` for the release including links to the changelog entry. -9. Run `rake release` to publish the gem to [Rubygems](https://rubygems.org/gems/ably). -10. Release the [REST-only library `ably-ruby-rest`](https://github.com/ably/ably-ruby-rest#release-process). -11. Create the entry on the [Ably Changelog](https://changelog.ably.com/) (via [headwayapp](https://headwayapp.co/)). +7. Add a tag to the new `main` head commit and push to origin such as `git tag v2.0.1 && git push origin v2.0.1`. +8. Visit [the tags page](https://github.com/ably/ably-ruby/tags) and `Add release notes` for the release including links to the changelog entry. +9. Run the [Release workflow](./.github/workflows/release.yml) (Actions → Release → Run workflow) with the version number. It publishes `ably-pubsub-core` and then `ably-pubsub-server` to RubyGems via trusted publishing — no local credentials involved. A failed run is safe to re-run with the same version: already-published gems are skipped. +10. Create the entry on the [Ably Changelog](https://changelog.ably.com/) (via [headwayapp](https://headwayapp.co/)). + +### Trusted publishing + +The workflow authenticates to RubyGems with [trusted publishing](https://guides.rubygems.org/trusted-publishing/) (GitHub OIDC): both gems have a Trusted Publisher configured on rubygems.org bound to this repository and `.github/workflows/release.yml`. There are no long-lived RubyGems API keys anywhere. If the repository is renamed, both bindings must be reconfigured on rubygems.org or publishing fails. + +### The legacy `ably` gem + +The `ably` gem is in its maintenance window (security and critical fixes only, released from the maintenance branch) and is **not** released from `main`. The `ably-rest` gem (from the `ably-ruby-rest` repo) is likewise in maintenance and no longer part of this release process. diff --git a/UPDATING.md b/UPDATING.md index db9ad6f76..fabb77aff 100644 --- a/UPDATING.md +++ b/UPDATING.md @@ -1,5 +1,37 @@ # Upgrade / Migration Guide +## Version 1.x (`ably` gem) to 2.0.0 (`ably-pubsub-server` gem) + +> **Status: draft.** The final public API naming is still under review; this section will be finalized before the 2.0.0 GA release. + +Version 2.0.0 splits the SDK into new packages. The `ably` gem is superseded: it receives security and critical-bug fixes only for one year from the 2.0.0 release date, and is then end-of-life. Under MAU-based pricing the platform must classify every connection as device- or server-side; the new packages declare this automatically, while the old constructors cannot — once MAU pricing is live, they raise on MAU-enabled accounts. + +Ruby is a server-side SDK, so there is a single new public gem, `ably-pubsub-server`, whose factory functions are the only recommended entry points. (It is built on `ably-pubsub-core`, an internal gem you should never depend on directly.) The objects the factories return are the same clients as today — channels, presence, history, auth and error handling are unchanged. For most applications the migration is confined to the Gemfile, the `require`, and the constructor call. + +### Mapping + +| 1.x (`ably`) | 2.0 (`ably-pubsub-server`) | +| --- | --- | +| `gem 'ably'` | `gem 'ably-pubsub-server'` | +| `gem 'ably-rest'` (from `ably-ruby-rest`) | `gem 'ably-pubsub-server'` | +| `require 'ably'` | `require 'ably/pubsub/server'` | +| `Ably::Rest::Client.new(options)` | `Ably::PubSub::Server.create_http_client(options)` | +| `Ably::Realtime::Client.new(options)` | `Ably::PubSub::Server.create_realtime_client(options)` | + +### Example + +```ruby +# 1.x +require 'ably' +client = Ably::Rest::Client.new(key: ENV['ABLY_API_KEY']) + +# 2.0 +require 'ably/pubsub/server' +client = Ably::PubSub::Server.create_http_client(key: ENV['ABLY_API_KEY']) +``` + +Both factories accept everything the old constructors accepted: an options `Hash`, an API key `String`, or a token `String`. + ## Version 1.1.8 to 1.2.0 ### Notable Changes From 79cde4c7b863a81fa38d2149cde23a31ab33417d Mon Sep 17 00:00:00 2001 From: umair Date: Tue, 1 Sep 2026 17:13:14 +0100 Subject: [PATCH 2/4] Add release dry-run job to CI Runs the same version-consistency assertions as release.yml's pre-flight and builds both gems on every PR, so a version-file or gemspec regression surfaces immediately instead of at release time. Publishes nothing. Co-Authored-By: Claude Fable 5 --- .github/workflows/check.yml | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index c10d05282..28959a303 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -56,6 +56,43 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} flag-name: ruby-${{ matrix.ruby }}-${{ matrix.protocol }}-${{ matrix.type }} parallel: true + # Dry run of the release pipeline: the same consistency assertions release.yml's + # pre-flight makes, plus a build of both gems — so a version-file or gemspec + # regression surfaces on every PR instead of at release time. Publishes nothing. + release-dry-run: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2 + with: + persist-credentials: false + - uses: ruby/setup-ruby@afeafc3d1ab54a631816aba4c914a0081c12ff2f # v1.310.0 + with: + ruby-version: '3.3' + bundler-cache: false + - name: 'Versions and pin must agree (mirrors release.yml pre-flight)' + run: | + ruby <<'RUBY' + require_relative 'core/lib/ably/version' + require_relative 'server/lib/ably/pubsub/server/version' + + errors = [] + errors << "core Ably::VERSION (#{Ably::VERSION}) != server Ably::PubSub::Server::VERSION (#{Ably::PubSub::Server::VERSION})" unless Ably::VERSION == Ably::PubSub::Server::VERSION + + server_spec = Gem::Specification.load('server/ably-pubsub-server.gemspec') + core_dep = server_spec.dependencies.find { |d| d.name == 'ably-pubsub-core' } + errors << "server gemspec pins ably-pubsub-core '#{core_dep&.requirement}', expected '= #{Ably::VERSION}'" unless core_dep&.requirement.to_s == "= #{Ably::VERSION}" + + abort errors.join("\n") unless errors.empty? + puts "Version consistency OK: #{Ably::VERSION}" + RUBY + - name: Build both gems + run: | + set -euo pipefail + (cd core && gem build ably-pubsub-core.gemspec) + (cd server && gem build ably-pubsub-server.gemspec) + finish: needs: check runs-on: ubuntu-latest From f18bcbe5698e5d24b7489d45484cfe989863d60f Mon Sep 17 00:00:00 2001 From: umair Date: Wed, 2 Sep 2026 13:17:52 +0100 Subject: [PATCH 3/4] Trim release workflow header comment Co-Authored-By: Claude Fable 5 --- .github/workflows/release.yml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d7113e1f7..bff4a22ea 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,13 +1,12 @@ name: Release -# Releases ably-pubsub-core and ably-pubsub-server in lockstep at the same version -# (PDR-091b). The pre-flight fails before anything is pushed if the version input, -# the two gems' version files, or the server->core exact pin disagree. +# Releases ably-pubsub-core and ably-pubsub-server in lockstep at the same version. +# The pre-flight fails before anything is pushed if the version input, the two gems' +# version files, or the server->core exact pin disagree. # # Publishing uses RubyGems trusted publishing (OIDC): both gems must have a Trusted # Publisher configured on rubygems.org pointing at this repository and this workflow -# file. NOTE: the binding is to the repo owner+name, so it must be reconfigured when -# the repo is renamed to ably-pubsub-ruby. +# file. # # A partial release fails reversibly: if the server push fails after the core push # succeeded, re-running the workflow with the same version skips the already-published From 97f823f00ff1db40da4a54848b74badcf79d913d Mon Sep 17 00:00:00 2001 From: umair Date: Wed, 2 Sep 2026 13:38:15 +0100 Subject: [PATCH 4/4] Add gem-packaging specs for the core/server split Asserts the two gemspecs' built file lists directly: no load-path file ships in both gems, core does not ship the server subtree or the ably-common submodule, server ships only its own subtree under lib, and the versions and server->core pin agree. The release pre-flight checks version agreement, but nothing else asserts gem contents, so a packaging mistake would otherwise surface only after publish. Adapted from the packaging specs in #452. Co-Authored-By: Claude Fable 5 --- spec/unit/pubsub/packaging_spec.rb | 51 ++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 spec/unit/pubsub/packaging_spec.rb diff --git a/spec/unit/pubsub/packaging_spec.rb b/spec/unit/pubsub/packaging_spec.rb new file mode 100644 index 000000000..7eb1675bc --- /dev/null +++ b/spec/unit/pubsub/packaging_spec.rb @@ -0,0 +1,51 @@ +# encoding: utf-8 +require 'spec_helper' + +# The Ably namespace is assembled at install time from two gems: ably-pubsub-core ships the +# implementation under lib/ably, and ably-pubsub-server ships only the lib/ably/pubsub/server +# subtree on top of it. That only holds together if each gem ships exactly its own subtree — +# a file shipped by both would be resolved from whichever gem comes first on the load path, +# hiding the other's copy. The release pre-flight checks version agreement but nothing else +# asserts the gems' file lists, so a packaging mistake would otherwise surface only after +# publish. These specs load the gemspecs and check the built file lists directly. +describe 'Pub/Sub gem packaging' do + repo_root = File.expand_path('../../..', __dir__) + + gemspec_for = lambda do |gem_name, dir| + path = File.join(repo_root, dir, "#{gem_name}.gemspec") + Gem::Specification.load(path) || raise("could not load #{path}") + end + + core_spec = gemspec_for.call('ably-pubsub-core', 'core') + server_spec = gemspec_for.call('ably-pubsub-server', 'server') + + it 'ships no load-path file in both gems' do + core_lib = core_spec.files.grep(%r{\Alib/}) + server_lib = server_spec.files.grep(%r{\Alib/}) + expect(core_lib & server_lib).to be_empty + end + + it 'core does not ship the server subtree' do + expect(core_spec.files.grep(%r{\Alib/ably/pubsub(/|\.rb\z)})).to be_empty + end + + it 'server ships only the lib/ably/pubsub/server subtree under lib' do + lib_files = server_spec.files.grep(%r{\Alib/}) + expect(lib_files).to_not be_empty + expect(lib_files).to all(match(%r{\Alib/ably/pubsub/server(/|\.rb\z)})) + end + + it 'core does not ship the ably-common submodule' do + expect(core_spec.files.grep(%r{\Alib/submodules/})).to be_empty + end + + it 'releases both gems at one version (lockstep)' do + expect(server_spec.version).to eql(core_spec.version) + end + + it 'server pins core at exactly the shared version' do + core_dep = server_spec.dependencies.find { |dep| dep.name == 'ably-pubsub-core' } + expect(core_dep).to_not be_nil + expect(core_dep.requirement.to_s).to eql("= #{core_spec.version}") + end +end