Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
100 changes: 100 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
name: Release

# 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.
#
# 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}"
41 changes: 29 additions & 12 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -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.
32 changes: 32 additions & 0 deletions UPDATING.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
51 changes: 51 additions & 0 deletions spec/unit/pubsub/packaging_spec.rb
Original file line number Diff line number Diff line change
@@ -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
Loading