• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

anycable / anycable-rails / 20319162023

17 Dec 2025 10:23PM UTC coverage: 76.69% (-2.8%) from 79.492%
20319162023

push

github

palkan
- ci: add rubocop.gemfile to all gemfiles

Since we have rubocop specs

450 of 747 branches covered (60.24%)

Branch coverage included in aggregate %.

991 of 1132 relevant lines covered (87.54%)

82.34 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

69.25
/lib/generators/anycable/setup/setup_generator.rb
1
# frozen_string_literal: true
2

3
require "generators/anycable/with_os_helpers"
10✔
4

5
module AnyCableRailsGenerators
10✔
6
  # Entry point for interactive installation
7
  class SetupGenerator < ::Rails::Generators::Base
10✔
8
    namespace "anycable:setup"
10✔
9
    source_root File.expand_path("templates", __dir__)
10✔
10

11
    DOCS_ROOT = "https://docs.anycable.io"
10✔
12
    DEVELOPMENT_METHODS = %w[skip local docker].freeze
10✔
13
    DEPLOYMENT_METHODS = %w[skip thruster fly heroku anycable_plus].freeze
10✔
14
    RPC_IMPL = %w[none grpc http].freeze
10✔
15

16
    class_option :development,
10✔
17
      type: :string,
18
      desc: "Select your development environment (options: #{DEVELOPMENT_METHODS.reverse.join(", ")})"
19
    class_option :rpc,
10✔
20
      type: :string,
21
      desc: "Select RPC implementation (options: #{RPC_IMPL.reverse.join(", ")})"
22
    class_option :skip_procfile,
10✔
23
      type: :boolean,
24
      desc: "Do not create/update Procfile.dev"
25
    class_option :version,
10✔
26
      type: :string,
27
      desc: "Specify AnyCable server version (defaults to latest release)",
28
      default: "latest"
29

30
    def welcome
10✔
31
      say ""
190✔
32
      say "👋 Welcome to AnyCable interactive installer. We'll guide you through the process of installing AnyCable for your Rails application. Buckle up!"
190✔
33
      say ""
190✔
34
      @todos = []
190✔
35
    end
36

37
    def rpc_implementation
10✔
38
      if RPC_IMPL.include?(options[:rpc])
190!
39
        @rpc_impl = options[:rpc]
190✔
40
        return
190✔
41
      end
42

43
      if hotwire? && !custom_channels?
×
44
        say <<~MSG
×
45
          ⚡️ Hotwire application has been detected, installing AnyCable in a standalone mode.
46
        MSG
47
        @rpc_impl = "none"
×
48
        return
×
49
      end
50

51
      if custom_channels?
×
52
        answer = RPC_IMPL.index(options[:rpc]) || 99
×
53

54
        unless RPC_IMPL[answer.to_i]
×
55
          say <<~MSG
×
56
            AnyCable connects to your Rails server to communicate with Action Cable channels either via HTTP or gRPC.
57

58
            gRPC provides better performance and scalability but requires running
59
            a separate component (a gRPC server).
60

61
            HTTP is a good option for a quick start or in case your deployment platform doesn't
62
            support running multiple web services (e.g., Heroku).
63

64
            If you only use Action Cable for Turbo Streams, you don't need RPC at all.
65

66
            Learn more from the docs 👉 #{DOCS_ROOT}/anycable-go/rpc
67
          MSG
68
          say ""
×
69
        end
70

71
        until RPC_IMPL[answer.to_i]
×
72
          answer = ask "Which RPC implementation would you like to use? (1) gRPC, (2) HTTP, (0) None"
×
73
        end
74

75
        @rpc_impl = RPC_IMPL[answer.to_i]
×
76
      end
77

78
      # no Hotwire, no custom channels
79
      say "Looks like you don't have any real-time functionality yet. Let's start with a miminal AnyCable setup!"
×
80
      @rpc_impl = "none"
×
81
    end
82

83
    def development_method
10✔
84
      if DEVELOPMENT_METHODS.include?(options[:development])
190!
85
        @development = options[:development]
190✔
86
      end
87

88
      # Fast-track for local development
89
      if file_exists?("bin/dev") && file_exists?("Procfile.dev")
190!
90
        @development = "local"
×
91
      end
92

93
      unless @development
190!
94
        say <<~MSG
×
95
          You can run AnyCable server locally (recommended for most cases) or as a Docker container (in case you develop in a containerized environment).
96

97
          For a local installation, we provide a convenient binstub (`bin/anycable-go`) which automatically
98
          installs AnyCable server for the current platform.
99
        MSG
100
        say ""
×
101

102
        answer = DEVELOPMENT_METHODS.index(options[:development]) || 99
×
103

104
        until DEVELOPMENT_METHODS[answer.to_i]
×
105
          answer = ask <<~MSG
×
106
            Which way to run AnyCable server locally would you prefer? (1) Binstub, (2) Docker, (0) Skip
107
          MSG
108
        end
109

110
        @development = DEVELOPMENT_METHODS[answer.to_i]
×
111
      end
112

113
      case @development
190✔
114
      when "skip"
65✔
115
        @todos << "Install AnyCable server for local development: #{DOCS_ROOT}/anycable-go/getting_started"
130✔
116
      else
30✔
117
        send "install_for_#{@development}"
60✔
118
      end
119
    end
120

121
    def configs
10✔
122
      inside("config") do
190✔
123
        template "anycable.yml"
190✔
124
      end
125

126
      template "anycable.toml"
190✔
127

128
      update_cable_yml
190✔
129
    end
130

131
    def rubocop_compatibility
10✔
132
      return unless rubocop?
190✔
133

134
      say_status :info, "🤖 Running static compatibility checks with RuboCop"
10✔
135
      res = run "bundle exec rubocop -r 'anycable/rails/compatibility/rubocop' --only AnyCable/InstanceVars,AnyCable/PeriodicalTimers,AnyCable/InstanceVars"
10✔
136

137
      unless res
10!
138
        say_status :help, "⚠️  Please, take a look at the icompatibilities above and fix them"
10✔
139

140
        @todos << "Fix Action Cable compatibility issues (listed above): #{DOCS_ROOT}/rails/compatibility"
10✔
141
      end
142
    end
143

144
    def cable_url_info
10✔
145
      meta_tag = norpc? ? "action_cable_with_jwt_meta_tag" : "action_cable_meta_tag"
190✔
146

147
      begin
148
        app_layout = nil
190✔
149
        inside("app/views/layouts") do
190✔
150
          next unless File.file?("application.html.erb")
190✔
151
          app_layout = File.read("application.html.erb")
180✔
152
        end
153
        return if app_layout&.include?(meta_tag)
190!
154

155
        if norpc? && app_layout&.include?("action_cable_meta_tag")
190!
156
          gsub_file "app/views/layouts/application.html.erb", %r{^\s+<%= action_cable_meta_tag %>.*$} do |match|
×
157
            match.sub("action_cable_meta_tag", "action_cable_with_jwt_meta_tag")
×
158
          end
159
          inform_jwt_identifiers("app/views/layouts/application.html.erb")
×
160
          return
×
161
        end
162

163
        found = false
190✔
164
        gsub_file "app/views/layouts/application.html.erb", %r{^\s+<%= csp_meta_tag %>.*$} do |match|
190✔
165
          found = true
170✔
166
          match << "\n    <%= #{meta_tag} %>"
170✔
167
        end
168
        if found
180✔
169
          inform_jwt_identifiers("app/views/layouts/application.html.erb") if norpc?
170✔
170
          return
170✔
171
        end
172
      rescue Errno::ENOENT
173
      end
174

175
      @todos << "⚠️  Ensure you have `action_cable_meta_tag`\n" \
20✔
176
        "      or `action_cable_with_jwt_meta_tag` included in your HTML layout:\n" \
177
        "      👉 https://docs.anycable.io/rails/getting_started"
178
    end
179

180
    def action_cable_engine
10✔
181
      return unless application_rb
190!
182
      return if application_rb.match?(/^require\s+['"](action_cable\/engine|rails\/all)['"]/)
190!
183

184
      found = false
190✔
185
      gsub_file "config/application.rb", %r{^require ['"]rails['"].*$} do |match|
190✔
186
        found = true
180✔
187
        match << %(\nrequire "action_cable/engine")
180✔
188
      end
189

190
      return if found
190✔
191

192
      @todos << "⚠️  Ensure Action Cable is loaded. Add `require \"action_cable/engine\"` to your `config/application.rb` file"
10✔
193
    end
194

195
    def anycable_client
10✔
196
      if hotwire? && install_js_packages
190✔
197
        gsub_file "app/javascript/application.js", /^import "@hotwired\/turbo-rails".*$/, <<~JS
20✔
198
          import "@hotwired/turbo"
199
          import { createCable } from "@anycable/web"
200
          import { start } from "@anycable/turbo-stream"
201

202
          // Use extended Action Cable protocol to support reliable streams and presence
203
          // See https://github.com/anycable/anycable-client
204
          const cable = createCable({ protocol: 'actioncable-v1-ext-json' })
205
          // Prevent frequent resubscriptions during morphing or navigation
206
          start(cable, { delayedUnsubscribe: true })
207
        JS
208
        return
20✔
209
      end
210

211
      @todos << "⚠️  Install AnyCable JS client to use advanced features (presence, reliable streams): 👉 https://github.com/anycable/anycable-client\n"
170✔
212
    end
213

214
    def turbo_verifier_key
10✔
215
      return unless hotwire?
190✔
216
      return if application_rb.include?("config.turbo.signed_stream_verifier_key = AnyCable.config.secret")
20!
217

218
      gsub_file "config/application.rb", %r{\s+end\nend} do |match|
20✔
219
        "\n\n" \
220
        "    # Use AnyCable secret to sign Turbo Streams\n" \
221
        "    # #{DOCS_ROOT}/guides/hotwire?id=rails-applications\n" \
20✔
222
        "    config.turbo.signed_stream_verifier_key = AnyCable.config.secret#{match}"
223
      end
224
    end
225

226
    def deployment_method
10✔
227
      @todos << "🚢 Learn how to run AnyCable in production: 👉 #{DOCS_ROOT}/deployment\n" \
190✔
228
        "      For the quick start, consider using AnyCable+ (https://plus.anycable.io)\n" \
229
        "      or AnyCable Thruster (https://github.com/anycable/thruster)"
230
    end
231

232
    def finish
10✔
233
      say_status :info, "✅ AnyCable has been configured"
190✔
234

235
      if @todos.any?
190!
236
        say ""
190✔
237
        say "📋 Please, check the following actions required to complete the setup:\n"
190✔
238
        @todos.each do |todo|
190✔
239
          say "- [ ] #{todo}"
560✔
240
        end
241
      end
242
    end
243

244
    private
10✔
245

246
    def redis?
10✔
247
      !!gemfile_lock&.match?(/^\s+redis\b/)
380✔
248
    end
249

250
    def nats?
10✔
251
      !!gemfile_lock&.match?(/^\s+nats-pure\b/)
380✔
252
    end
253

254
    def webpacker?
10✔
255
      !!gemfile_lock&.match?(/^\s+webpacker\b/)
×
256
    end
257

258
    def rubocop?
10✔
259
      !!gemfile_lock&.match?(/^\s+rubocop\b/)
190✔
260
    end
261

262
    def local?
10✔
263
      @development == "local"
×
264
    end
265

266
    def grpc?
10✔
267
      @rpc_impl == "grpc"
190✔
268
    end
269

270
    def http_rpc?
10✔
271
      @rpc_impl == "http"
410✔
272
    end
273

274
    def norpc?
10✔
275
      @rpc_impl == "none"
550✔
276
    end
277

278
    def hotwire?
10✔
279
      !!gemfile_lock&.match?(/^\s+turbo-rails\b/) &&
380✔
280
        application_js&.match?(/^import\s+"@hotwired\/turbo/)
48!
281
    end
282

283
    def custom_channels?
10✔
284
      @has_custom_channels ||= begin
×
285
        res = nil
×
286
        in_root do
×
287
          next unless File.directory?("app/channels")
×
288
          res = Dir["app/channels/*_channel.rb"].any?
×
289
        end
290
        res
×
291
      end
292
    end
293

294
    def gemfile_lock
10✔
295
      @gemfile_lock ||= begin
1,330✔
296
        res = nil
1,150✔
297
        in_root do
1,150✔
298
          next unless File.file?("Gemfile.lock")
1,150✔
299
          res = File.read("Gemfile.lock")
30✔
300
        end
301
        res
1,150✔
302
      end
303
    end
304

305
    def application_rb
10✔
306
      @application_rb ||= begin
400✔
307
        res = nil
190✔
308
        in_root do
190✔
309
          next unless File.file?("config/application.rb")
190!
310
          res = File.read("config/application.rb")
190✔
311
        end
312
        res
190✔
313
      end
314
    end
315

316
    def application_js
10✔
317
      @application_js ||= begin
40✔
318
        res = nil
20✔
319
        in_root do
20✔
320
          next unless File.file?("app/javascript/application.js")
20!
321
          res = File.read("app/javascript/application.js")
20✔
322
        end
323
        res
20✔
324
      end
325
    end
326

327
    def install_for_docker
10✔
328
      say_status :help, "️️⚠️  Docker development configuration could vary", :yellow
×
329

330
      say "Here is an example snippet for Docker Compose:"
×
331

332
      if @rpc_impl == "grpc"
×
333
        say <<~YML
×
334
          ─────────────────────────────────────────
335
          # your Rails application service
336
          rails: &rails
337
            # ...
338
            ports:
339
              - '3000:3000'
340
            environment: &rails_environment
341
              # ...
342
              ANYCABLE_HTTP_BROADCAST_URL: http://ws:8090/_broadcast
343
            depends_on: &rails_depends_on
344
              #...
345
              anycable:
346
                condition: service_started
347

348
          ws:
349
            image: anycable/anycable-go:1.6
350
            ports:
351
              - '8080:8080'
352
              - '8090'
353
            environment:
354
              ANYCABLE_HOST: "0.0.0.0"
355
              ANYCABLE_BROADCAST_ADAPTER: http
356
              ANYCABLE_RPC_HOST: anycable:50051
357
              ANYCABLE_DEBUG: ${ANYCABLE_DEBUG:-true}
358
              ANYCABLE_SECRET: "anycable-local-secret"
359

360
          anycable:
361
            <<: *rails
362
            command: bundle exec anycable
363
            environment:
364
              <<: *rails_environment
365
              ANYCABLE_RPC_HOST: 0.0.0.0:50051
366
            ports:
367
              - '50051'
368
            depends_on:
369
              <<: *rails_depends_on
370
              ws:
371
                condition: service_started
372
          ─────────────────────────────────────────
373
        YML
374
      else
×
375
        say <<~YML
×
376
          ─────────────────────────────────────────
377
          # Your Rails application service
378
          rails: &rails
379
            # ...
380
            ports:
381
              - '3000:3000'
382
            environment: &rails_environment
383
              # ...
384
              ANYCABLE_HTTP_BROADCAST_URL: http://ws:8090/_broadcast
385
            depends_on: &rails_depends_on
386
              #...
387
              anycable:
388
                condition: service_started
389

390
          ws:
391
            image: anycable/anycable-go:1.5
392
            ports:
393
              - '8080:8080'
394
            environment:
395
              ANYCABLE_HOST: "0.0.0.0"
396
              ANYCABLE_BROADCAST_ADAPTER: http
397
              ANYCABLE_RPC_HOST: http://rails:3000/_anycable
398
              ANYCABLE_DEBUG: ${ANYCABLE_DEBUG:-true}
399
              ANYCABLE_SECRET: "anycable-local-secret"
400
          ─────────────────────────────────────────
401
        YML
402
      end
403
    end
404

405
    def install_for_local
10✔
406
      unless file_exists?("bin/anycable-go")
50!
407
        generate "anycable:bin", "--version #{options[:version]}"
50✔
408
      end
409
      template_proc_files
50✔
410
      update_bin_dev
50✔
411
      true
50✔
412
    end
413

414
    def update_cable_yml
10✔
415
      if file_exists?("config/cable.yml")
190✔
416
        in_root do
10✔
417
          contents = File.read("config/cable.yml")
10✔
418
          # Replace any adapter: x with any_cable unless x == "test"
419
          new_contents = contents.gsub(/\sadapter:\s*([^$\n]+)/) do |match|
10✔
420
            adapter = Regexp.last_match[1]
20✔
421
            next match if adapter == "test" || adapter.include?("any_cable")
20✔
422

423
            match.sub(adapter, "any_cable")
10✔
424
          end
425

426
          # Try removing all lines contaning options for previous adapters,
427
          # only keep aliases (<<:*), adapter and channel_prefix options.
428
          new_clean_contents = new_contents.lines.select do |line|
10✔
429
            line.match?(/^(\S|\s+adapter:|\s+channel_prefix:|\s+<<:)/) || line.match?(/^\s*$/)
180✔
430
          end.join
431

432
          # Verify new config
433
          begin
434
            clean_config = YAML.safe_load(new_clean_contents, aliases: true).deep_symbolize_keys
10✔
435
            orig_config = YAML.safe_load(contents, aliases: true).deep_symbolize_keys
10✔
436

437
            new_contents = new_clean_contents if clean_config.keys == orig_config.keys
10!
438
          rescue => _e
439
            # something went wrong, keep older options
440
          end
441

442
          File.write "config/cable.yml", new_contents
10✔
443
        end
444
      else
90✔
445
        inside("config") do
180✔
446
          template "cable.yml"
180✔
447
        end
448
      end
449
    end
450

451
    def template_proc_files
10✔
452
      file_name = "Procfile.dev"
50✔
453

454
      if file_exists?(file_name)
50✔
455
        update_procfile(file_name)
20✔
456
      else
15✔
457
        return if options[:skip_procfile_dev]
30!
458

459
        template file_name
30✔
460
      end
461
    end
462

463
    def update_procfile(file_name)
10✔
464
      in_root do
20✔
465
        contents = File.read(file_name)
20✔
466

467
        if grpc?
20✔
468
          unless contents.match?(/^anycable:\s/)
10!
469
            append_file file_name, "anycable: bundle exec anycable\n", force: true
10✔
470
          end
471
        end
472
        unless contents.match?(/^ws:\s/)
20!
473
          append_file file_name, "ws: bin/anycable-go --port 8080", force: true
20✔
474
        end
475
      end
476
    end
477

478
    def update_bin_dev
10✔
479
      unless file_exists?("bin/dev")
50✔
480
        template "bin/dev"
30✔
481
        chmod "bin/dev", 0755, verbose: false # rubocop:disable Style/NumericLiteralPrefix
30✔
482

483
        @todos << "Now you should use bin/dev to run your application with AnyCable services"
30✔
484
        return
30✔
485
      end
486

487
      in_root do
20✔
488
        contents = File.read("bin/dev")
20✔
489

490
        return if contents.include?("Procfile.dev")
20✔
491

492
        if contents.include?(%(exec "./bin/rails"))
10!
493
          template "bin/dev", force: true
10✔
494
          chmod "bin/dev", 0755, verbose: false # rubocop:disable Style/NumericLiteralPrefix
10✔
495
        else
×
496
          @todos << "Please, check your bin/dev file and ensure it runs Procfile.dev with AnyCable services"
×
497
        end
498
      end
499
    end
500

501
    def file_exists?(name)
10✔
502
      in_root do
720✔
503
        return File.file?(name)
720✔
504
      end
505
    end
506

507
    def inform_jwt_identifiers(path)
10✔
508
      return unless file_exists?("app/channels/application_cable/connection.rb")
130!
509

510
      in_root do
×
511
        contents = File.read("app/channels/application_cable/connection.rb")
×
512

513
        if contents.match?(%r{^\s+identified_by\s})
×
514
          @todos << "⚠️  Please, provide the correct connection identifiers to the #action_cable_with_jwt_meta_tag in #{path}. Read more: 👉 #{DOCS_ROOT}/rails/authentication?id=jwt-authentication"
×
515
        end
516
      end
517
    end
518

519
    def install_js_packages
10✔
520
      if file_exists?("config/importmap.rb") && file_exists?("bin/importmap")
20!
521
        run "bin/importmap pin @hotwired/turbo @anycable/web @anycable/turbo-stream"
20✔
522
        true
20!
523
      elsif file_exists?("yarn.lock")
×
524
        run "yarn add @anycable/web @anycable/turbo-stream"
×
525
        true
×
526
      elsif file_exists?("package-json.lock")
×
527
        run "npm install @anycable/web @anycable/turbo-stream"
×
528
        true
×
529
      else
×
530
        false
×
531
      end
532
    rescue => e
533
      say_status :warn, "Failed to install JS packages: #{e.message}. Skipping..."
×
534
      false
×
535
    end
536
  end
537
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc