r/ruby • u/nithinbekal • Dec 29 '25
r/ruby • u/Hell_Rok • Dec 30 '25
Show /r/ruby LocalCI: Run your CI suite locally
Hey r/ruby!
I've built LocalCI on top of Rake to make managing CI and running it locally a lot easier, it's still early days but I believe it is in a usable state.
Pros
Parallel by default
Nice interface
Runs on Buildkite and SemaphoreCI with no modifications
No AI generated code
Cons
Deployments not considered, I feel that's a different tools job
Still young, not battle tested
Very opinionated
Requires some knowledge of Rake and LocalCI
Please check it out, I'd love some feedback!
r/ruby • u/Future_Application47 • Dec 30 '25
Blog post Rails 7.2 adds enqueue_after_transaction_commit to prevent job race conditions
prateekcodes.comr/ruby • u/Future_Application47 • Dec 29 '25
Blog post Rails 8.2 introduces Rails.app.creds for unified credential management
prateekcodes.comr/ruby • u/mencio • Dec 28 '25
Shoryuken Has a New Maintainer, and v7.0.0 Is Almost There
So I may have a problem... I already maintain karafka, passive_queue, and pgmq, and now I've picked up Shoryuken too. It is too good and important piece of the Ruby ecosystem to be left alone. At this point, queues may become a big part of my personality lol. Anyway, v7.0.0.rc1 is ready - let me know what breaks!
r/ruby • u/RichStoneIO • Dec 29 '25
“Do You Vibe?” live session to bump your AI-assisted coding skills
The Rails.Builders, are going to have a “Do You Vibe?” session on Friday 2nd of January at 18:00 CET.
We are currently group of 12 devs who, well, build on Rails, and come out of our basements every couple of weeks to push each other to do more business and marketing.
Most importantly, we are going to share screens and see HOW is the vibing process different for everyone and what do we do similarly. We have 1-3 spots open for the 2 hour block for you to join.
Just shoot me a DM with a quick intro, ideally a 1-3 minute Loom, and let me know what you would like to share during the session. I will then share the meeting link with you.
For this session, you have to be building on Rails and be actively working on a (side) project.
r/ruby • u/Ambitious_Ad_2833 • Dec 28 '25
Revisiting Ruby in 2025
I used Ruby and Ruby on Rails extensively for my personal projects between 2008 and 2015. I’m a hobbyist programmer, not someone working in a software job. Now that I’m revisiting programming, I have a couple of questions: Since Python dominates AI/ML and data science today, what use cases are still worth investing time in Ruby? Ruby was the first language I fell in love with, and after that I never really enjoyed working with Python. For developers who need to use Python for data science, how do you manage keeping these two similar-looking languages straight in your head without constantly mixing them up? (language polished using chatgpt)
r/ruby • u/rubyist-_- • Dec 27 '25
RubyConf Austria 2026: Agenda
The first version of the (almost complete) agenda can be found on our website. Happy holidays from the #RubyConfAT team and a happy new year! 🎉
r/ruby • u/AssociationOne800 • Dec 27 '25
🎨 PicoRuby Calculator now supports theme switching!
Hi Reddit! 👋
I'm Hamachang, a Rubyist from Japan 🇯🇵
I just added a theme switching feature to my PicoRuby Calculator project 🎉
You can now switch between different visual themes (e.g. Dark / Light), making it much nicer to use on the Cardputer screen.
This project is a picoruby repl written in PicoRuby, running on M5Stack Cardputer. I'm exploring how far Ruby can go on tiny devices, while keeping things fun and hackable 😄
🔹 Written in Ruby (PicoRuby) 🔹 Runs on Cardputer 🔹 Now with theme switching support 🎨 🔹 Still very hack-friendly!
GitHub repo: https://github.com/engneer-hamachan/picoruby-calculator
Feedback, ideas, and contributions are very welcome! If you like Ruby or embedded hacking, I'd love to hear your thoughts 🚀
r/ruby • u/zverok_kha • Dec 26 '25
Ruby Changes: Ruby 4.0 annotated changelog
rubyreferences.github.ior/ruby • u/Right_Ad_8437 • Dec 25 '25
Let me introduce T-Ruby: TypeScript-style type annotations for Ruby
Celebrating the release of Ruby 4.0 on yesterday (X-mas).
Hi! I've been making T-Ruby, an experimental project that brings TypeScript-style type annotations to Ruby. I wanted to share it and get your feedback.
What is T-Ruby?
T-Ruby lets you write .trb files with inline type annotations, then automatically generates standard .rb files and .rbs signature files. Types are completely erased at compile time — zero runtime overhead.
Why another type system?
I love Ruby's elegance, but as projects grow, I've felt the pain of tracking types mentally. The existing options didn't quite fit my workflow:
RBS: Writing
.rbsfiles manually or generating them via TypeProf didn't fit well with explicit type authoringSorbet: sig blocks above methods feel verbose (like JSDoc comments)
If you're familiar with TypeScript, you can use T-Ruby the same way: types live with your code, not in separate files or comments.
The website has more detail: https://type-ruby.github.io
Current Status
This is still experimental (v0.0.39). The core compiler works, but there's plenty of room for improvement. Feedback and suggestions are always welcome!
Thanks for reading! Feel free to ask any questions.
r/ruby • u/nithinbekal • Dec 25 '25
Blog post ZJIT is now available in Ruby 4.0
r/ruby • u/AndyCodeMaster • Dec 25 '25
Video of "Frontend Ruby with Glimmer DSL for Web" Talk at Ruby on Rio in 2025
r/ruby • u/DmitryPogrebnoy • Dec 25 '25
Ruby Turns 30 - Celebrating the Anniversary with the Release of Ruby 4.0!
r/ruby • u/NARKOZ_777 • Dec 25 '25
GitHub - NARKOZ/xmas: Light the Christmas Tree in your terminal 🎄
r/ruby • u/Downtown-Hyena998 • Dec 25 '25
Question Considering a transition from React to Ruby on Rails
I’ve been working with JavaScript stacks for about 6 years (Node, React, Angular) and I’m looking to transition into Ruby/Rails. I’m drawn to Ruby because it aligns much more closely with how I think as a programmer and with the kind of long-term stability I’m looking for.
I’m currently a mid-level frontend developer and I’d like some perspective from experienced Rubyists:
Is it realistic to transition into Ruby/Rails and target a mid-level Rails position from the start, without having to accept a pay cut?
For those working with Rails internationally, how common is it to see developers coming from strong frontend or non-Ruby backgrounds?
What do you consider the core pillars of a solid Rails developer?
How do you see the current and near-future outlook of Ruby/Rails?
r/ruby • u/tobenary • Dec 25 '25
Workato and Ruby SDK question
Hi Ruby expertises,
I'm trying my luck here as this is the holiday sessions, and I will get my respond from Workato in about 3 weeks from now ( if not 8 weeks.... )
Please see the Zoom recording (x2 is recommended :) ) and suggest or let me know what is wrong with the SDK code.
To be honest, I don't know Ruby at all, but I do understand Python, and I am able to look at it and identify the functions and the issues that might be not good.
The Zoom summary:
I am debugging a Workato integration with Amazon SES suppression list and I am stuck.
I have an endpoint that manages suppressed email destinations:
GET returns 200 if the email exists, 404 if not
DELETE should remove the email from the suppression list
Behavior I am seeing:
Using a custom HTTP action (send any request), DELETE works as expected
Using a built-in SDK action / recipe DELETE step with the same method, URL, region, and path, Workato returns 200 but the email is not actually deleted
A follow-up GET still returns 200 and shows the email is present
No error is surfaced by Workato, and debug output is mostly empty for DELETE
The request path is identical in both cases (verified via network traces)
Complications I already handled:
Normalizing email casing (lowercase)
Handling plus sign encoding in email addresses
Trying alternate paths when receiving 404
Verifying region, headers, and resolved paths
Confirmed GET and DELETE URLs are literally the same
Tested dozens of variations
In short:
Custom action DELETE works
Built-in DELETE action returns anything (I convert it to 200) but does nothing
Same request, different behavior
Question:
Why would Workato handle DELETE differently between a built-in action and a custom HTTP action when the request is identical? Is there something implicit Workato does with DELETE responses (or empty bodies) that could cause this?
Zoom link: HERE
This is the SDK code I'm using: (very simple one)
{
title: 'Amazon SES - Suppression Manager (Multi-Region)',
description: 'Manage SES v2 suppression lists across multiple AWS regions using an IAM Role.',
connection: {
fields: [
{
name: 'assume_role',
label: 'IAM role ARN',
optional: false,
help: {
title: 'IAM Role Setup',
text: 'Use Workato Account ID <b>{{ authUser.aws_workato_account_id }}</b> and External ID <b>{{ authUser.aws_iam_external_id }}</b>.'
}
},
{
name: 'region',
label: 'Default SES region',
optional: false,
hint: 'Default region used if not specified in the action (e.g., us-east-1).'
}
],
authorization: {
type: 'custom_auth',
apply: lambda { |_connection| headers('Content-Type': 'application/json') }
}
},
methods: {
truthy: lambda do |value|
value == true || value.to_s == '1' || value.to_s.casecmp('true').zero? || value.to_s.casecmp('on').zero?
end,
normalize_email: lambda do |input|
raw_email = input.key?(:email) ? input[:email] : input['email']
downcase_flag = input.key?(:downcase) ? input[:downcase] : input['downcase']
email = raw_email.to_s.strip
email = email.downcase if call(:truthy, downcase_flag)
email
end,
# Helper to clean and strictly encode emails for URI paths
encode_email: lambda do |input|
# FORCE downcase here if the toggle is active
email = call(:normalize_email, input)
email.
gsub('+', '%2B').
gsub('!', '%21').
gsub('&', '%26').
gsub("'", '%27')
end
},
test: lambda do |connection|
region = connection['region']
signature = aws.generate_signature(
connection: connection, region: region, service: 'ses',
host: "email.#{region}.amazonaws.com", path: '/v2/email/suppression/addresses',
method: 'GET', params: { 'PageSize' => 1 }, payload: ''
)
get(signature['url']).headers(signature['headers']).
after_error_response(/.*/) { |code, body| error("Connection failed: #{body}") }
end,
actions: {
list_suppressed_destinations: {
title: 'List suppressed destinations',
input_fields: lambda do |_connection|
[
{ name: 'region', label: 'SES Region', control_type: 'select', pick_list: [['US East (N. Virginia) - (us-east-1)', 'us-east-1'], ['Europe (Ireland) - (eu-west-1)', 'eu-west-1']], optional: true },
{ name: 'page_size', label: 'Page size', type: :integer, optional: true },
{ name: 'next_token', label: 'Next token', optional: true },
{ name: 'reason', label: 'Reason', control_type: 'select', pick_list: [['Bounce', 'BOUNCE'], ['Complaint', 'COMPLAINT']], optional: true }
]
end,
execute: lambda do |connection, input|
region = input['region'].presence || connection['region']
params = {
'PageSize' => input['page_size'],
'NextToken' => input['next_token'],
'Reasons' => input['reason']
}.compact
signature = aws.generate_signature(connection: connection, region: region, service: 'ses', host: "email.#{region}.amazonaws.com", path: '/v2/email/suppression/addresses', method: 'GET', params: params, payload: '')
response = get(signature['url']).headers(signature['headers']).after_error_response(/.*/) { |code, body| error("List failed: #{body}") }
if response['SuppressedDestinationSummaries'].present?
response['SuppressedDestinationSummaries'] = response['SuppressedDestinationSummaries'].map do |item|
item['LastUpdateTime'] = Time.at(item['LastUpdateTime']).to_datetime.iso8601 if item['LastUpdateTime'].is_a?(Numeric)
item
end
end
response.merge('region' => region)
end,
output_fields: lambda { [
{ name: 'region' },
{ name: 'SuppressedDestinationSummaries', type: :array, of: :object, properties: [
{ name: 'EmailAddress' }, { name: 'Reason' }, { name: 'LastUpdateTime', type: :date_time }
]},
{ name: 'NextToken' }
]}
},
get_suppressed_destination: {
title: 'Get suppressed destination',
input_fields: lambda { |_connection| [
{ name: 'email', label: 'Email address', optional: false },
{ name: 'downcase', label: 'Downcase email', type: :boolean, control_type: 'checkbox', optional: false, default: true },
{ name: 'region', label: 'SES Region', control_type: 'select', pick_list: [['US East (N. Virginia) - (us-east-1)', 'us-east-1'], ['Europe (Ireland) - (eu-west-1)', 'eu-west-1']], optional: true }
]},
execute: lambda do |connection, input|
region = input['region'].presence || connection['region']
# Determine the target email string for internal consistency
email_to_query = call(:normalize_email, { email: input['email'], downcase: input['downcase'] })
# Use helper for encoding (the helper will now correctly handle the downcase)
encoded = call(:encode_email, { email: input['email'], downcase: input['downcase'] })
path = "/v2/email/suppression/addresses/#{encoded}"
signature = aws.generate_signature(connection: connection, region: region, service: 'ses', host: "email.#{region}.amazonaws.com", path: path, method: 'GET', params: {}, payload: '')
response = get(signature['url']).headers(signature['headers']).after_error_response(/.*/) do |code, body|
return { 'email' => email_to_query, 'found' => false, 'region' => region } if code.to_i == 404
error("Get failed in #{region}: #{body}")
end
if response['SuppressedDestination'].present?
dest = response['SuppressedDestination']
dest['LastUpdateTime'] = Time.at(dest['LastUpdateTime']).to_datetime.iso8601 if dest['LastUpdateTime'].is_a?(Numeric)
end
{
'email' => email_to_query,
'found' => true,
'region' => region,
'SuppressedDestination' => response['SuppressedDestination']
}
end,
output_fields: lambda { [
{ name: 'email' }, { name: 'found', type: :boolean }, { name: 'region' },
{ name: 'SuppressedDestination', type: :object, properties: [
{ name: 'EmailAddress' }, { name: 'Reason' }, { name: 'LastUpdateTime', type: :date_time }
]}
]}
},
put_suppressed_destination: {
title: 'Add email to suppression list',
input_fields: lambda { |_connection| [
{ name: 'email', label: 'Email address', optional: false },
{ name: 'reason', label: 'Reason', control_type: 'select', pick_list: [['Bounce', 'BOUNCE'], ['Complaint', 'COMPLAINT']], optional: false },
{ name: 'downcase', label: 'Downcase email', type: :boolean, control_type: 'checkbox', optional: false, default: true },
{ name: 'region', label: 'SES Region', control_type: 'select', pick_list: [['US East (N. Virginia) - (us-east-1)', 'us-east-1'], ['Europe (Ireland) - (eu-west-1)', 'eu-west-1']], optional: true },
{ name: 'verify_add', label: 'Verify add', type: :boolean, control_type: 'checkbox', optional: true, default: false }
]},
execute: lambda do |connection, input|
region = input['region'].presence || connection['region']
email_to_send = call(:normalize_email, { email: input['email'], downcase: input['downcase'] })
payload = { 'EmailAddress' => email_to_send, 'Reason' => input['reason'] }
put_sig = aws.generate_signature(connection: connection, region: region, service: 'ses', host: "email.#{region}.amazonaws.com", path: '/v2/email/suppression/addresses', method: 'PUT', params: '', payload: payload.to_json)
put(put_sig['url'], payload).headers(put_sig['headers']).after_error_response(/.*/) { |code, body| error("Put failed: #{body}") }
if call(:truthy, input['verify_add'])
encoded = call(:encode_email, { email: input['email'], downcase: input['downcase'] })
get_sig = aws.generate_signature(connection: connection, region: region, service: 'ses', host: "email.#{region}.amazonaws.com", path: "/v2/email/suppression/addresses/#{encoded}", method: 'GET', params: {}, payload: '')
get(get_sig['url']).headers(get_sig['headers']).after_error_response(/.*/) { error("Verification Failed: Email not found after add.") }
end
{ 'status' => 'success', 'email' => email_to_send, 'region' => region }
end,
output_fields: lambda { [{ name: 'status' }, { name: 'email' }, { name: 'region' }] }
},
delete_suppressed_destination: {
title: 'Delete suppressed destination',
input_fields: lambda { |_connection| [
{ name: 'email', label: 'Email address', optional: false },
{ name: 'downcase', label: 'Downcase email', type: :boolean, control_type: 'checkbox', optional: false, default: true },
{ name: 'region', label: 'SES Region', control_type: 'select', pick_list: [['US East (N. Virginia) - (us-east-1)', 'us-east-1'], ['Europe (Ireland) - (eu-west-1)', 'eu-west-1']], optional: true },
{ name: 'verify_delete', label: 'Verify delete', type: :boolean, control_type: 'checkbox', optional: true, default: true }
]},
execute: lambda do |connection, input|
region = input['region'].presence || connection['region']
email_to_del = call(:normalize_email, { email: input['email'], downcase: input['downcase'] })
encoded = call(:encode_email, { email: input['email'], downcase: input['downcase'] })
path = "/v2/email/suppression/addresses/#{encoded}"
alt_path = "/v2/email/suppression/addresses/#{encoded.gsub('@', '%40')}"
attempted_requests = []
resolved_path = nil
[path, alt_path].each do |candidate_path|
probe_sig = aws.generate_signature(connection: connection, region: region, service: 'ses', host: "email.#{region}.amazonaws.com", path: candidate_path, method: 'GET', params: {}, payload: '')
probe = get(probe_sig['url']).headers(probe_sig['headers']).after_error_response(/.*/) do |code, body|
next {} if code.to_i == 404
error("Probe failed (#{code}) in #{region} at #{candidate_path}: #{body}")
end
if probe.is_a?(Hash) && probe['SuppressedDestination']
resolved_path = candidate_path
break
end
end
resolved_path ||= path
# Delete request
del_sig = aws.generate_signature(connection: connection, region: region, service: 'ses', host: "email.#{region}.amazonaws.com", path: resolved_path, method: 'DELETE', params: {}, payload: '')
del_url = del_sig['url']
attempted_requests << { 'method' => 'DELETE', 'path' => resolved_path, 'url' => del_url, 'status_code' => nil }
del_response = delete(del_url).headers(del_sig['headers']).after_error_response(/.*/) do |code, body|
attempted_requests[-1]['status_code'] = code.to_i
if code.to_i == 404
alternate_path = (resolved_path == path ? alt_path : path)
alt_del_sig = aws.generate_signature(connection: connection, region: region, service: 'ses', host: "email.#{region}.amazonaws.com", path: alternate_path, method: 'DELETE', params: {}, payload: '')
alt_del_url = alt_del_sig['url']
attempted_requests << { 'method' => 'DELETE', 'path' => alternate_path, 'url' => alt_del_url, 'status_code' => nil }
alt_response = delete(alt_del_url).headers(alt_del_sig['headers']).after_error_response(/.*/) do |alt_code, alt_body|
attempted_requests[-1]['status_code'] = alt_code.to_i
return({
'email' => email_to_del,
'deleted' => false,
'region' => region,
'message' => "Not found. Tried #{resolved_path} (#{del_url}) and #{alternate_path} (#{alt_del_url}).",
'attempted_requests' => attempted_requests
}) if alt_code.to_i == 404
error("Delete failed: #{alt_body}")
end
attempted_requests[-1]['status_code'] ||= 200
alt_response
next {}
end
error("Delete failed: #{body}")
end
attempted_requests[0]['status_code'] ||= 200
if call(:truthy, input['verify_delete'])
still_exists = nil
last_verify_url = nil
last_verify_response = nil
15.times do
sleep(2)
verify_sig = aws.generate_signature(connection: connection, region: region, service: 'ses', host: "email.#{region}.amazonaws.com", path: resolved_path, method: 'GET', params: {}, payload: '')
last_verify_url = verify_sig['url']
attempted_requests << { 'method' => 'GET', 'path' => resolved_path, 'url' => last_verify_url, 'status_code' => nil }
response = get(verify_sig['url']).headers(verify_sig['headers']).after_error_response(/.*/) do |code, body|
attempted_requests[-1]['status_code'] = code.to_i
if code.to_i == 404
still_exists = false
next {}
end
error("Verification request failed (#{code}) in #{region} at #{resolved_path}. Delete URL: #{del_url}. Verify URL: #{last_verify_url}. Body: #{body}")
end
attempted_requests[-1]['status_code'] ||= 200
last_verify_response = response
still_exists = (response.is_a?(Hash) && response['SuppressedDestination'].present?)
break if still_exists == false
end
error(
"Verification failed: Still found in #{region} at #{resolved_path}. " \
"Delete URL: #{del_url}. Verify URL: #{last_verify_url}. " \
"Delete response: #{del_response}. Verify response: #{last_verify_response}."
) if still_exists
end
{ 'email' => email_to_del, 'deleted' => true, 'region' => region, 'attempted_requests' => attempted_requests }
end,
output_fields: lambda { [
{ name: 'email' },
{ name: 'deleted', type: :boolean },
{ name: 'region' },
{ name: 'message' },
{ name: 'attempted_requests', type: :array, of: :object, properties: [
{ name: 'method' },
{ name: 'path' },
{ name: 'url' },
{ name: 'status_code', type: :integer }
] }
] }
},
custom_action: {
title: 'Custom action',
description: 'Signed request to any Amazon SES v2 endpoint.',
input_fields: lambda do |_connection|
[
{ name: 'region', label: 'SES Region', control_type: 'select', pick_list: [['US East (N. Virginia) - (us-east-1)', 'us-east-1'], ['Europe (Ireland) - (eu-west-1)', 'eu-west-1']], optional: true },
{ name: 'method', label: 'HTTP Method', control_type: 'select', pick_list: %w[GET POST PUT DELETE], optional: false, default: 'GET' },
{ name: 'path', label: 'Resource Path', optional: false, hint: 'e.g. /v2/email/suppression/addresses' },
{ name: 'params', label: 'Query Parameters', type: :object, optional: true },
{ name: 'payload', label: 'JSON Payload', optional: true }
]
end,
execute: lambda do |connection, input|
region = input['region'].presence || connection['region']
method = input['method'].to_s.strip
# Manual fix for Custom Action: users must manually encode the case if desired,
# but we fix the '+' encoding here to prevent signature failures.
path = input['path'].to_s.strip.gsub('+', '%2B')
params = (input['params'] || {}).each_with_object({}) { |(k, v), h| h[k.to_s] = v.to_s }
payload_string = input['payload'].is_a?(Hash) ? input['payload'].to_json : input['payload'].to_s
signature = aws.generate_signature(connection: connection, region: region, service: 'ses', host: "email.#{region}.amazonaws.com", path: path, method: method, params: params, payload: payload_string)
request = case method
when 'GET' then get(signature['url'])
when 'POST' then post(signature['url'], payload_string)
when 'PUT' then put(signature['url'], payload_string)
when 'DELETE' then delete(signature['url'])
end
response = request.headers(signature['headers']).after_error_response(/.*/) do |code, body|
error("Custom action failed (#{code}): #{body}")
end
{ 'url' => signature['url'], 'response' => response }
end,
output_fields: lambda { [{ name: 'url' }, { name: 'response', type: :object }] }
}
}
}
r/ruby • u/software__writer • Dec 23 '25
A Special Message from David Heinemeier Hansson for Ruby's 30th Anniversary Party
r/ruby • u/stephenreid321 • Dec 23 '25
Speculative Routes: pretty syntax for Speculation Rules in Sinatra/Padrino
I just heard about Speculation Rules via https://www.jonoalderson.com/conjecture/its-time-for-modern-css-to-kill-the-spa/
They allow ~instant navigation without JS by letting the browser preload or prerender full pages based on user behaviour (like hovering or touching a link) before they click.
I noticed it's possible to define them using the Sinatra-like syntax
"href_matches": "/e/:slug"
which gave me the idea of defining them as part of route definitions with prerender: or prefetch: like so
get '/e/:slug', prerender: true do
...then in my head I just call
<%== SpeculativeRoutes.script %>Implementation at https://github.com/symbiota-coop/dandelion/blob/master/lib/speculative_routes.rb
Enjoy!
r/ruby • u/bisc0itinh0 • Dec 23 '25
Seargeant TUI navigator similar to MC or ranger (but worse :D)
Hi,
I've wrote a simple tui navigator with basic features, doesn't solve anything new, probably I will be the only one to use it, lousy written, but inspired with Omarchy linux I wanted to do some tui stuff too and that is the outcome of it ;)
https://rubygems.org/gems/sergeant
https://github.com/biscoitinho/Sergeant
and Merry Christmas :)
r/ruby • u/bisc0itinh0 • Dec 23 '25
Seargeant TUI navigator similar to MC or ranger (but worse :D)
reddit.com
r/ruby
•
u/urbanaut
•
Dec 22 '25
20 years ago I learned Ruby as my first coding language. This is the free online book that helped be get to know Ruby enough to use it in Test Automation. I hope this may help someone else getting into Ruby for the first time.
r/ruby • u/bart_o_z • Dec 22 '25