Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
9ccb91b
chore: fix RPCError::Code mispelling
chris-olszewski Apr 1, 2026
aa71656
Add sig/ to gemspec to ship RBS types with the gem
chris-olszewski Apr 1, 2026
25d30dd
Add tapioca baseline for RBI drift detection
chris-olszewski Apr 1, 2026
c040ac3
Add enriched RBI types for Sorbet users
chris-olszewski Apr 1, 2026
eb4559d
Fix RBI issues found during Sorbet validation
chris-olszewski Apr 2, 2026
1ca1584
Fix Workflow::Mutex/Queue/SizedQueue missing parent classes in RBI
chris-olszewski Apr 2, 2026
8c65984
Add Api::Common::V1::Payload/Payloads stubs to RBI
chris-olszewski Apr 2, 2026
4fb7d81
Add CI Sorbet type check for enriched RBI
chris-olszewski Apr 2, 2026
e98bd98
Document RBI maintenance workflow for SDK developers
chris-olszewski Apr 2, 2026
7f4b103
fixup rbi
chris-olszewski Apr 2, 2026
237ea7b
strip comments
chris-olszewski Apr 2, 2026
7c40bc8
feat: add sorbet runtime check to tests
chris-olszewski Apr 6, 2026
5405fc8
fixup type errors
chris-olszewski Apr 6, 2026
aa2da00
test: run sorbet runtime typecheck in ci
chris-olszewski Apr 6, 2026
6d15ca9
chore: fix interceptor type errors
chris-olszewski Apr 6, 2026
c91948d
fix(test): fix activity info test to avoid type error
chris-olszewski Apr 6, 2026
13a022a
attempt to remove non-determism in type checking
chris-olszewski Apr 7, 2026
d8b78ad
self review
chris-olszewski Apr 7, 2026
03329d9
include sig globally, support anon blocks
chris-olszewski Apr 8, 2026
f5d98ba
chore: add test to limit amount of untyped uses
chris-olszewski Apr 8, 2026
33e7e8a
first pass at removing untyped
chris-olszewski Apr 8, 2026
7c94020
type envconfig
chris-olszewski Apr 8, 2026
ea6ae84
split out grpc service rbi
chris-olszewski Apr 9, 2026
3f29a2d
fail on uninstrumented methods
chris-olszewski Apr 9, 2026
76e1c77
increase coverage
chris-olszewski Apr 9, 2026
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
10 changes: 10 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,18 @@ jobs:
TEMPORAL_CLOUD_OPS_TEST_API_KEY: ${{ secrets.TEMPORAL_CLIENT_CLOUD_API_KEY }}
TEMPORAL_CLOUD_OPS_TEST_API_VERSION: 2024-05-13-00

# Enable Sorbet runtime type checking to verify RBI accuracy
TEMPORAL_SORBET_RUNTIME_CHECK: "1"

run: bundle exec rake TESTOPTS="--verbose"

- name: Check RBI types with Sorbet
working-directory: ./temporalio
run: |
cd extra/sorbet_check
bundle install --quiet
bundle exec srb tc

- name: Deploy docs
# Only deploy on main merge, not in PRs
if: ${{ github.ref == 'refs/heads/main' && matrix.docsTarget }}
Expand Down
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1411,6 +1411,36 @@ Now can run `steep`:

bundle exec rake steep

### Maintaining Type Signatures

The SDK ships two sets of type signatures:

* **RBS** (`sig/`) -- Source-of-truth types validated by Steep in CI. Update these whenever a public API changes.
* **RBI** (`rbi/temporalio.rbi`) -- Enriched Sorbet types derived from the RBS signatures. Must be updated manually
when the RBS changes (see below).

The RBI is validated in CI two ways:

* **Static** -- `srb tc` checks that `extra/sorbet_check/check_types.rb` typechecks against the RBI. This catches
inconsistencies within the RBI itself (missing classes, wrong param types, etc.).
* **Runtime** -- The test suite runs with `TEMPORAL_SORBET_RUNTIME_CHECK=1`, which applies every RBI signature to the
real implementation at runtime via `SigApplicator`. This catches drift between the RBI and actual code (e.g. a
renamed parameter, a changed return type, or a missing method).

**When adding or changing a public method:**

1. Update the RBS file in `sig/` as usual. Verify with `bundle exec rake steep`.
2. Update `rbi/temporalio.rbi` to match.
3. If the new method is important for users, add a usage example to `extra/sorbet_check/check_types.rb`.
4. Run the Sorbet check: `cd extra/sorbet_check && bundle exec srb tc`.
5. Run tests with runtime type checking enabled `TEMPORAL_SORBET_RUNTIME_CHECK=1 bundle exec rake test`

**What NOT to include in the RBI:**
* `Temporalio::Internal::*` types (excluded entirely)
* Methods prefixed with `_` (internal use only)
* `Temporalio::Api::*` protobuf types (except `Api::Common::V1::Payload` and `Payloads` which users reference in
Copy link
Copy Markdown

@jazev-stripe jazev-stripe Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible to include the RBI types for these as well? At Stripe we maintain https://github.com/sorbet/protoc-gen-rbi, which I'd recommend. Tapioca looks like it also has its own support: https://github.com/Shopify/tapioca/blob/main/manual/compiler_protobuf.md, though I'm not sure how it compares.

To give some motivation, this is the most common source of needing to do T.unsafe(...) in our integration with sdk-ruby at Stripe today (which is still in its early stages). We have code that looks like this:

workflow_service_client = T.let(
  temporal_client.workflow_service,
  Temporalio::Client::Connection::WorkflowService
)
namespaces_result = workflow_service_client.list_namespaces(
  T.unsafe(Temporalio::Api::WorkflowService::V1::ListNamespacesRequest).new(
    page_size: page_size,
    next_page_token: next_page_token
  )
)

The existing types being added in this PR would let us remove the T.let(...) cast on the first statement [0], but we'd need to keep the T.unsafe(cls).new(...) on the second statement unless there were RBI stubs for the Protobuf types.

It's understandable if the SigApplicator system wouldn't work for these (since I think the methods might not actually exist on the classes?

[0]: (we could remove that today, but we'd be calling a method on a T.untyped)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If sdk-ruby weren't able to publish the RBI types for the protobufs, I think we'd strongly consider setting up our own additional RBI generation to cover the protobuf classes, at Stripe.

I think it's still globally preferable if this is provided by the upstream, though (since all users will get them).

custom codecs)

### Proto Generation

Run:
Expand Down
12 changes: 12 additions & 0 deletions temporalio/.rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,24 @@ Style/Documentation:
Enabled: true
Exclude:
- lib/temporalio/internal/**/*
- extra/sorbet_check/**/*

# We want methods to be documented
Style/DocumentationMethod:
Enabled: true
Exclude:
- lib/temporalio/internal/**/*
- extra/sorbet_check/**/*

# Sorbet check file uses extend T::Sig at top level
Style/MixinUsage:
Exclude:
- extra/sorbet_check/**/*

# Sorbet check file defines multiple stub classes in one file
Style/OneClassPerFile:
Exclude:
- extra/sorbet_check/**/*

# Ok to have global vars in tests
Style/GlobalVars:
Expand Down
2 changes: 2 additions & 0 deletions temporalio/Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@ group :development do
gem 'opentelemetry-sdk'
gem 'rake'
gem 'rake-compiler'
gem 'rbi'
gem 'rbs', '~> 3.10'
gem 'rb_sys', '~> 0.9'
gem 'rdoc'
gem 'rubocop'
gem 'sorbet-runtime'
gem 'sqlite3'
gem 'steep', '~> 1.10'
gem 'yard'
Expand Down
1 change: 1 addition & 0 deletions temporalio/Steepfile
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ end
target :test do
signature 'sig', 'test/sig'
check 'test'
ignore 'test/support'
library 'uri', 'objspace'
configure_code_diagnostics do |hash|
hash.update(common_diagnostics)
Expand Down
27 changes: 27 additions & 0 deletions temporalio/extra/proto_gen.rb
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,33 @@ def #{rpc}: (
end
TEXT
end

# Open file to generate RBI code
File.open("rbi/temporalio/client/connection/#{file_name}.rbi", 'w') do |file|
file.puts <<~TEXT
# typed: false
# frozen_string_literal: true

# Generated code. DO NOT EDIT!

class Temporalio::Client::Connection::#{class_name} < ::Temporalio::Client::Connection::Service
extend T::Sig

sig { params(connection: Temporalio::Client::Connection).void }
def initialize(connection); end
TEXT

desc.each do |method|
rpc = method.name.gsub(/([A-Z])/, '_\1').downcase.delete_prefix('_')
file.puts <<-TEXT

sig { params(request: #{method.input_type.msgclass}, rpc_options: T.nilable(Temporalio::Client::RPCOptions)).returns(#{method.output_type.msgclass}) }
def #{rpc}(request, rpc_options: T.unsafe(nil)); end
TEXT
end

file.puts 'end'
end
end

def generate_rust_client_file
Expand Down
8 changes: 8 additions & 0 deletions temporalio/extra/sorbet_check/Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# frozen_string_literal: true

source 'https://rubygems.org'

gem 'temporalio', path: '../..'

gem 'sorbet', group: :development
gem 'sorbet-runtime'
199 changes: 199 additions & 0 deletions temporalio/extra/sorbet_check/check_types.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
# typed: true
# frozen_string_literal: true

# This file validates the enriched RBI types by exercising key SDK API patterns.
# It is checked by `srb tc` in CI — it is never executed at runtime.

require 'sorbet-runtime'
require 'temporalio/activity'
require 'temporalio/client'
require 'temporalio/converters'
require 'temporalio/converters/payload_codec'
require 'temporalio/worker'
require 'temporalio/workflow'

extend T::Sig

# --- Activity definitions ---

class MyActivity < Temporalio::Activity::Definition
extend T::Sig

sig { params(name: String).returns(String) }
def execute(name)
ctx = Temporalio::Activity::Context.current
ctx.logger.info("Running activity for #{name}")
ctx.heartbeat('progress')
"Hello, #{name}!"
end
end

# --- Workflow definitions ---

class MyWorkflow < Temporalio::Workflow::Definition
extend T::Sig

T::Sig::WithoutRuntime.sig { returns(Temporalio::Workflow::Definition::Query) }
def self.my_query = T.unsafe(nil)

T::Sig::WithoutRuntime.sig { returns(Temporalio::Workflow::Definition::Signal) }
def self.my_signal = T.unsafe(nil)

T::Sig::WithoutRuntime.sig { returns(Temporalio::Workflow::Definition::Update) }
def self.my_update = T.unsafe(nil)

sig { void }
def initialize
@value = T.let(nil, T.nilable(String))
@done = T.let(false, T::Boolean)
end

sig { params(name: String).returns(String) }
def execute(name)
# Execute activity
result = T.cast(
Temporalio::Workflow.execute_activity(MyActivity, name, start_to_close_timeout: 300),
String
)

# Workflow primitives
Temporalio::Workflow.logger.info("Activity result: #{result}")
_info = Temporalio::Workflow.info
_now = Temporalio::Workflow.now

# Wait with timeout
begin
Temporalio::Workflow.timeout(10) do
Temporalio::Workflow.wait_condition { @done }
end
rescue Timeout::Error
# expected
end

result
end

workflow_query
sig { returns(T.nilable(String)) }
def my_query
@value
end

workflow_signal
sig { params(value: String).void }
def my_signal(value)
@value = value
end

workflow_update
sig { params(value: String).returns(String) }
def my_update(value)
old = @value
@value = value
old || ''
end

workflow_update_validator(:my_update)
sig { params(value: String).void }
def validate_my_update(value)
raise 'empty' if value.empty?
end
end

# --- Client usage ---

sig { void }
def check_client_types
client = Temporalio::Client.connect('localhost:7233', 'default')

# Start workflow
handle = client.start_workflow(
MyWorkflow, 'world',
id: 'test-id', task_queue: 'test-queue'
)

# Query, signal, update
handle.query(MyWorkflow.my_query)
handle.signal(MyWorkflow.my_signal, 'value')
handle.execute_update(MyWorkflow.my_update, 'value')

# Result
_result = handle.result
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For some expressions like this (and in various other places in this file), could we add T.assert_type!(...) calls? https://sorbet.org/docs/type-assertions#tassert_type

The runtime typechecking that assertion performs wouldn't run, but the static typechecking would

end

# --- Worker usage ---

sig { void }
def check_worker_types
client = Temporalio::Client.connect('localhost:7233', 'default')

worker = Temporalio::Worker.new(
client: client,
task_queue: 'test-queue',
activities: [MyActivity],
workflows: [MyWorkflow]
)

worker.run(shutdown_signals: ['SIGINT'])
end

# --- Converter/codec usage ---

class MyCodec < Temporalio::Converters::PayloadCodec
extend T::Sig

sig { params(payloads: T::Enumerable[T.untyped]).returns(T::Array[T.untyped]) }
def encode(payloads)
payloads.map { |p| p }
end

sig { params(payloads: T::Enumerable[T.untyped]).returns(T::Array[T.untyped]) }
def decode(payloads)
payloads.map { |p| p }
end
end

sig { void }
def check_converter_types
codec = MyCodec.new
converter = Temporalio::Converters::DataConverter.new(payload_codec: codec)
Temporalio::Client.connect('localhost:7233', 'default', data_converter: converter)
end

# --- Error types ---

sig { void }
def check_error_types
raise Temporalio::Error::ApplicationError.new('test', non_retryable: true)
rescue Temporalio::Error::ApplicationError => e
_msg = e.message
_non_retryable = e.non_retryable
end

# --- Cancellation ---

sig { void }
def check_cancellation_types
cancellation = Temporalio::Cancellation.new
_canceled = cancellation.canceled?
_reason = cancellation.canceled_reason
cancellation.check!
end

# --- Search attributes ---

sig { void }
def check_search_attributes_types
key = Temporalio::SearchAttributes::Key.new('my-key', Temporalio::SearchAttributes::IndexedValueType::TEXT)
update = Temporalio::SearchAttributes::Update.new(key, 'value')
_k = update.key
_v = update.value
end

# --- Workflow mutex ---

sig { void }
def check_workflow_mutex_types
mutex = Temporalio::Workflow::Mutex.new
mutex.synchronize { 'value' }
end
2 changes: 2 additions & 0 deletions temporalio/extra/sorbet_check/sorbet/config
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
--dir
.
Loading
Loading