diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index c10d0528..28959a30 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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 196dd8ce..97868587 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-pubsub-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-pubsub-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-pubsub-ruby/tags](https://github.com/ably/ably-pubsub-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-pubsub-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/INTRO.md b/INTRO.md index 582a0a85..d4ec4790 100644 --- a/INTRO.md +++ b/INTRO.md @@ -1,5 +1,10 @@ # Ably `Ruby` Client Library SDK API Reference +> **Using the `ably` gem?** This is the API reference for the `ably-pubsub-server` gem, which replaces it. +> The API reference for the maintenance-only `ably` gem remains available at +> [sdk.ably.com/builds/ably/ably-ruby/main/docs](https://sdk.ably.com/builds/ably/ably-ruby/main/docs/) +> until its end of life. + The `Ruby` Client Library SDK supports a realtime and a REST interface. The realtime interface enables a client to maintain a persistent connection to Ably and publish, subscribe and be present on channels. diff --git a/UPDATING.md b/UPDATING.md index db9ad6f7..fabb77af 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 diff --git a/spec/unit/pubsub/packaging_spec.rb b/spec/unit/pubsub/packaging_spec.rb new file mode 100644 index 00000000..7eb1675b --- /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