Wire Solid Queue and add jwt/webmock gems
Solid Queue was declared (Gemfile, production queue: database) but never installed: no bin/jobs, no queue.yml, no queue schema, adapter still commented out. Run the installer and point production at the queue database, ahead of the first real ActiveJob (GitHub PR reviews). jwt mints the RS256 App JWTs for GitHub App installation tokens; webmock stubs GitHub REST calls in specs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WhPzinYJUYgBed63XMvJX1
Claude committed
Jul 2, 2026 at 12:21 UTC
78c7a21dfc3c5a333234533bcf317e5757cdb004
7 files changed
+198
-1
Gemfile
+6
@@ -31,6 +31,9 @@ gem "redcarpet", "~> 3.6"
31
# Syntax highlighting for code files
32
gem "rouge", "~> 4.5"
33
34
+# RS256 JWT minting for GitHub App authentication (installation access tokens)
35
+gem "jwt", "~> 3.1"
36
+
37
# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword]
38
# gem "bcrypt", "~> 3.1.7"
39
@@ -77,6 +80,9 @@ group :development, :test do
80
81
# RSpec for request/model specs [https://github.com/rspec/rspec-rails]
82
gem "rspec-rails", "~> 8.0"
83
+
84
+ # Stub outbound HTTP (GitHub REST) in specs
85
+ gem "webmock", require: false
86
end
87
88
group :development do
Gemfile.lock
+20
@@ -94,6 +94,9 @@ GEM
94
logger (~> 1.5)
95
concurrent-ruby (1.3.7)
96
connection_pool (3.0.2)
97
+ crack (1.0.1)
98
+ bigdecimal
99
+ rexml
100
crass (1.0.6)
101
date (3.5.1)
102
debug (1.11.1)
@@ -111,11 +114,13 @@ GEM
114
et-orbi (1.4.0)
115
tzinfo
116
ffi (1.17.4-arm64-darwin)
117
+ ffi (1.17.4-x86_64-linux-gnu)
118
fugit (1.12.2)
119
et-orbi (~> 1.4)
120
raabro (~> 1.4)
121
globalid (1.3.0)
122
activesupport (>= 6.1)
123
+ hashdiff (1.2.1)
124
i18n (1.15.1)
125
concurrent-ruby (~> 1.0)
126
image_processing (1.14.0)
@@ -135,6 +140,8 @@ GEM
140
actionview (>= 7.0.0)
141
activesupport (>= 7.0.0)
142
json (2.19.9)
143
+ jwt (3.2.0)
144
+ base64
145
kamal (2.12.0)
146
activesupport (>= 7.0)
147
base64 (~> 0.2)
@@ -189,12 +196,15 @@ GEM
196
nio4r (2.7.5)
197
nokogiri (1.19.4-arm64-darwin)
198
racc (~> 1.4)
199
+ nokogiri (1.19.4-x86_64-linux-gnu)
200
+ racc (~> 1.4)
201
ostruct (0.6.3)
202
parallel (2.1.0)
203
parser (3.3.11.1)
204
ast (~> 2.4.1)
205
racc
206
pg (1.6.3-arm64-darwin)
207
+ pg (1.6.3-x86_64-linux)
208
pp (0.6.3)
209
prettyprint
210
prettyprint (0.2.0)
@@ -262,6 +272,7 @@ GEM
272
regexp_parser (2.12.0)
273
reline (0.6.3)
274
io-console (~> 0.5)
275
+ rexml (3.4.4)
276
rouge (4.7.0)
277
rspec-core (3.13.6)
278
rspec-support (~> 3.13.0)
@@ -347,8 +358,10 @@ GEM
358
railties (>= 7.0.0)
359
tailwindcss-ruby (~> 3.0)
360
tailwindcss-ruby (3.4.19-arm64-darwin)
361
+ tailwindcss-ruby (3.4.19-x86_64-linux)
362
thor (1.5.0)
363
thruster (0.1.21-arm64-darwin)
364
+ thruster (0.1.21-x86_64-linux)
365
timeout (0.6.1)
366
tsort (0.2.0)
367
turbo-rails (2.0.23)
@@ -365,6 +378,10 @@ GEM
378
actionview (>= 8.0.0)
379
bindex (>= 0.4.0)
380
railties (>= 8.0.0)
381
+ webmock (3.26.2)
382
+ addressable (>= 2.8.0)
383
+ crack (>= 0.3.2)
384
+ hashdiff (>= 0.4.0, < 2.0.0)
385
websocket-driver (0.8.1)
386
base64
387
websocket-extensions (>= 0.1.0)
@@ -373,6 +390,7 @@ GEM
390
391
PLATFORMS
392
arm64-darwin
393
+ x86_64-linux
394
395
DEPENDENCIES
396
bootsnap
@@ -383,6 +401,7 @@ DEPENDENCIES
401
image_processing (~> 1.2)
402
importmap-rails
403
jbuilder
404
+ jwt (~> 3.1)
405
kamal
406
letter_opener
407
pg (~> 1.1)
@@ -404,6 +423,7 @@ DEPENDENCIES
423
turbo-rails
424
tzinfo-data
425
web-console
426
+ webmock
427
428
BUNDLED WITH
429
2.7.2
bin/jobs
new
+6
@@ -0,0 +1,6 @@
1
+#!/usr/bin/env ruby
2
+
3
+require_relative "../config/environment"
4
+require "solid_queue/cli"
5
+
6
+SolidQueue::Cli.start(ARGV)
config/environments/production.rb
+2
-1
@@ -50,7 +50,8 @@ Rails.application.configure do
50
# config.cache_store = :mem_cache_store
51
52
# Replace the default in-process and non-durable queuing backend for Active Job.
53
- # config.active_job.queue_adapter = :resque
53
+ config.active_job.queue_adapter = :solid_queue
54
+ config.solid_queue.connects_to = { database: { writing: :queue } }
55
56
# Ignore bad email addresses and do not raise email delivery errors.
57
# Set this to true and configure the email server for immediate delivery to raise delivery errors.
config/queue.yml
new
+20
@@ -0,0 +1,20 @@
1
+default: &default
2
+ dispatchers:
3
+ - polling_interval: 1
4
+ batch_size: 500
5
+ workers:
6
+ - queues: "*"
7
+ # Each review job can pin a thread on an Onde Cloud request for up to
8
+ # ~90s (OndeCloudService::READ_TIMEOUT) — keep concurrency low.
9
+ threads: 2
10
+ processes: <%= ENV.fetch("JOB_CONCURRENCY", 1) %>
11
+ polling_interval: 0.1
12
+
13
+development:
14
+ <<: *default
15
+
16
+test:
17
+ <<: *default
18
+
19
+production:
20
+ <<: *default
config/recurring.yml
new
+15
@@ -0,0 +1,15 @@
1
+# examples:
2
+# periodic_cleanup:
3
+# class: CleanSoftDeletedRecordsJob
4
+# queue: background
5
+# args: [ 1000, { batch_size: 500 } ]
6
+# schedule: every hour
7
+# periodic_cleanup_with_command:
8
+# command: "SoftDeletedRecord.due.delete_all"
9
+# priority: 2
10
+# schedule: at 5am every day
11
+
12
+production:
13
+ clear_solid_queue_finished_jobs:
14
+ command: "SolidQueue::Job.clear_finished_in_batches(sleep_between_batches: 0.3)"
15
+ schedule: every hour at minute 12
db/queue_schema.rb
new
+129
@@ -0,0 +1,129 @@
1
+ActiveRecord::Schema[7.1].define(version: 1) do
2
+ create_table "solid_queue_blocked_executions", force: :cascade do |t|
3
+ t.bigint "job_id", null: false
4
+ t.string "queue_name", null: false
5
+ t.integer "priority", default: 0, null: false
6
+ t.string "concurrency_key", null: false
7
+ t.datetime "expires_at", null: false
8
+ t.datetime "created_at", null: false
9
+ t.index [ "concurrency_key", "priority", "job_id" ], name: "index_solid_queue_blocked_executions_for_release"
10
+ t.index [ "expires_at", "concurrency_key" ], name: "index_solid_queue_blocked_executions_for_maintenance"
11
+ t.index [ "job_id" ], name: "index_solid_queue_blocked_executions_on_job_id", unique: true
12
+ end
13
+
14
+ create_table "solid_queue_claimed_executions", force: :cascade do |t|
15
+ t.bigint "job_id", null: false
16
+ t.bigint "process_id"
17
+ t.datetime "created_at", null: false
18
+ t.index [ "job_id" ], name: "index_solid_queue_claimed_executions_on_job_id", unique: true
19
+ t.index [ "process_id", "job_id" ], name: "index_solid_queue_claimed_executions_on_process_id_and_job_id"
20
+ end
21
+
22
+ create_table "solid_queue_failed_executions", force: :cascade do |t|
23
+ t.bigint "job_id", null: false
24
+ t.text "error"
25
+ t.datetime "created_at", null: false
26
+ t.index [ "job_id" ], name: "index_solid_queue_failed_executions_on_job_id", unique: true
27
+ end
28
+
29
+ create_table "solid_queue_jobs", force: :cascade do |t|
30
+ t.string "queue_name", null: false
31
+ t.string "class_name", null: false
32
+ t.text "arguments"
33
+ t.integer "priority", default: 0, null: false
34
+ t.string "active_job_id"
35
+ t.datetime "scheduled_at"
36
+ t.datetime "finished_at"
37
+ t.string "concurrency_key"
38
+ t.datetime "created_at", null: false
39
+ t.datetime "updated_at", null: false
40
+ t.index [ "active_job_id" ], name: "index_solid_queue_jobs_on_active_job_id"
41
+ t.index [ "class_name" ], name: "index_solid_queue_jobs_on_class_name"
42
+ t.index [ "finished_at" ], name: "index_solid_queue_jobs_on_finished_at"
43
+ t.index [ "queue_name", "finished_at" ], name: "index_solid_queue_jobs_for_filtering"
44
+ t.index [ "scheduled_at", "finished_at" ], name: "index_solid_queue_jobs_for_alerting"
45
+ end
46
+
47
+ create_table "solid_queue_pauses", force: :cascade do |t|
48
+ t.string "queue_name", null: false
49
+ t.datetime "created_at", null: false
50
+ t.index [ "queue_name" ], name: "index_solid_queue_pauses_on_queue_name", unique: true
51
+ end
52
+
53
+ create_table "solid_queue_processes", force: :cascade do |t|
54
+ t.string "kind", null: false
55
+ t.datetime "last_heartbeat_at", null: false
56
+ t.bigint "supervisor_id"
57
+ t.integer "pid", null: false
58
+ t.string "hostname"
59
+ t.text "metadata"
60
+ t.datetime "created_at", null: false
61
+ t.string "name", null: false
62
+ t.index [ "last_heartbeat_at" ], name: "index_solid_queue_processes_on_last_heartbeat_at"
63
+ t.index [ "name", "supervisor_id" ], name: "index_solid_queue_processes_on_name_and_supervisor_id", unique: true
64
+ t.index [ "supervisor_id" ], name: "index_solid_queue_processes_on_supervisor_id"
65
+ end
66
+
67
+ create_table "solid_queue_ready_executions", force: :cascade do |t|
68
+ t.bigint "job_id", null: false
69
+ t.string "queue_name", null: false
70
+ t.integer "priority", default: 0, null: false
71
+ t.datetime "created_at", null: false
72
+ t.index [ "job_id" ], name: "index_solid_queue_ready_executions_on_job_id", unique: true
73
+ t.index [ "priority", "job_id" ], name: "index_solid_queue_poll_all"
74
+ t.index [ "queue_name", "priority", "job_id" ], name: "index_solid_queue_poll_by_queue"
75
+ end
76
+
77
+ create_table "solid_queue_recurring_executions", force: :cascade do |t|
78
+ t.bigint "job_id", null: false
79
+ t.string "task_key", null: false
80
+ t.datetime "run_at", null: false
81
+ t.datetime "created_at", null: false
82
+ t.index [ "job_id" ], name: "index_solid_queue_recurring_executions_on_job_id", unique: true
83
+ t.index [ "task_key", "run_at" ], name: "index_solid_queue_recurring_executions_on_task_key_and_run_at", unique: true
84
+ end
85
+
86
+ create_table "solid_queue_recurring_tasks", force: :cascade do |t|
87
+ t.string "key", null: false
88
+ t.string "schedule", null: false
89
+ t.string "command", limit: 2048
90
+ t.string "class_name"
91
+ t.text "arguments"
92
+ t.string "queue_name"
93
+ t.integer "priority", default: 0
94
+ t.boolean "static", default: true, null: false
95
+ t.text "description"
96
+ t.datetime "created_at", null: false
97
+ t.datetime "updated_at", null: false
98
+ t.index [ "key" ], name: "index_solid_queue_recurring_tasks_on_key", unique: true
99
+ t.index [ "static" ], name: "index_solid_queue_recurring_tasks_on_static"
100
+ end
101
+
102
+ create_table "solid_queue_scheduled_executions", force: :cascade do |t|
103
+ t.bigint "job_id", null: false
104
+ t.string "queue_name", null: false
105
+ t.integer "priority", default: 0, null: false
106
+ t.datetime "scheduled_at", null: false
107
+ t.datetime "created_at", null: false
108
+ t.index [ "job_id" ], name: "index_solid_queue_scheduled_executions_on_job_id", unique: true
109
+ t.index [ "scheduled_at", "priority", "job_id" ], name: "index_solid_queue_dispatch_all"
110
+ end
111
+
112
+ create_table "solid_queue_semaphores", force: :cascade do |t|
113
+ t.string "key", null: false
114
+ t.integer "value", default: 1, null: false
115
+ t.datetime "expires_at", null: false
116
+ t.datetime "created_at", null: false
117
+ t.datetime "updated_at", null: false
118
+ t.index [ "expires_at" ], name: "index_solid_queue_semaphores_on_expires_at"
119
+ t.index [ "key", "value" ], name: "index_solid_queue_semaphores_on_key_and_value"
120
+ t.index [ "key" ], name: "index_solid_queue_semaphores_on_key", unique: true
121
+ end
122
+
123
+ add_foreign_key "solid_queue_blocked_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
124
+ add_foreign_key "solid_queue_claimed_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
125
+ add_foreign_key "solid_queue_failed_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
126
+ add_foreign_key "solid_queue_ready_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
127
+ add_foreign_key "solid_queue_recurring_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
128
+ add_foreign_key "solid_queue_scheduled_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
129
+end