Skip to content

Commit acfaac6

Browse files
authored
Merge pull request rails#50914 from andrewn617/devcontainer
Generate devcontainer files by default
2 parents a9a359a + 75d5308 commit acfaac6

25 files changed

+779
-9
lines changed
File renamed without changes.

.devcontainer/devcontainer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// For format details, see https://aka.ms/devcontainer.json.
22
{
33
"name": "Rails project development",
4-
"dockerComposeFile": "docker-compose.yml",
4+
"dockerComposeFile": "compose.yaml",
55
"service": "rails",
66
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
77

actionpack/lib/action_dispatch/system_test_case.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,14 @@ def self.driven_by(driver, using: :chrome, screen_size: [1400, 1400], options: {
161161
self.driver = SystemTesting::Driver.new(driver, **driver_options, &capabilities)
162162
end
163163

164+
# Configuration for the System Test application server.
165+
#
166+
# By default this is localhost. This method allows the host and port to be specified manually.
167+
def self.served_by(host:, port:)
168+
Capybara.server_host = host
169+
Capybara.server_port = port
170+
end
171+
164172
private
165173
def url_helpers
166174
@url_helpers ||=

railties/CHANGELOG.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,31 @@
1+
* Generate a .devcontainer folder and its contents when creating a new app.
2+
3+
The .devcontainer folder includes everything needed to boot the app and do development in a remote container.
4+
5+
The container setup includes:
6+
- A redis container for Kredis, ActionCable etc.
7+
- A database (SQLite, Postgres, MySQL or MariaDB)
8+
- A Headless chrome container for system tests
9+
- Active Storage configured to use the local disk and with preview features working
10+
11+
If any of these options are skipped in the app setup they will not be included in the container configuration.
12+
13+
These files can be skipped using the `--skip-devcontainer` option.
14+
15+
*Andrew Novoselac & Rafael Mendonça França*
16+
17+
* Introduce `SystemTestCase#served_by` for configuring the System Test application server
18+
19+
By default this is localhost. This method allows the host and port to be specified manually.
20+
21+
```ruby
22+
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
23+
served_by host: "testserver", port: 45678
24+
end
25+
```
26+
27+
*Andrew Novoselac & Rafael Mendonça França*
28+
129
* `bin/rails test` will no longer load files named `*_test.rb` if they are located in the `fixtures` folder.
230

331
*Edouard Chin*

railties/lib/rails/generators.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ module Generators
2323
autoload :NamedBase, "rails/generators/named_base"
2424
autoload :ResourceHelpers, "rails/generators/resource_helpers"
2525
autoload :TestCase, "rails/generators/test_case"
26+
autoload :Devcontainer, "rails/generators/devcontainer"
2627

2728
mattr_accessor :namespace
2829

railties/lib/rails/generators/app_base.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ module Rails
1313
module Generators
1414
class AppBase < Base # :nodoc:
1515
include Database
16+
include Devcontainer
1617
include AppName
1718

1819
NODE_LTS_VERSION = "18.15.0"
@@ -109,6 +110,9 @@ def self.add_shared_options_for(name)
109110
class_option :skip_ci, type: :boolean, default: nil,
110111
desc: "Skip GitHub CI files"
111112

113+
class_option :skip_devcontainer, type: :boolean, default: false,
114+
desc: "Skip devcontainer files"
115+
112116
class_option :dev, type: :boolean, default: nil,
113117
desc: "Set up the #{name} with Gemfile pointing to your Rails checkout"
114118

@@ -400,6 +404,10 @@ def skip_ci?
400404
options[:skip_ci]
401405
end
402406

407+
def skip_devcontainer?
408+
options[:skip_devcontainer]
409+
end
410+
403411
class GemfileEntry < Struct.new(:name, :version, :comment, :options, :commented_out)
404412
def initialize(name, version, comment, options = {}, commented_out = false)
405413
super

railties/lib/rails/generators/database.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,14 @@ def mysql_socket
9090
"/opt/lampp/var/mysql/mysql.sock" # xampp for linux
9191
].find { |f| File.exist?(f) } unless Gem.win_platform?
9292
end
93+
94+
def mysql_database_host
95+
if options[:skip_devcontainer]
96+
"localhost"
97+
else
98+
"<%= ENV.fetch(\"DB_HOST\") { \"localhost\" } %>"
99+
end
100+
end
93101
end
94102
end
95103
end
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
# frozen_string_literal: true
2+
3+
module Rails
4+
module Generators
5+
module Devcontainer
6+
private
7+
def devcontainer_ruby_version
8+
gem_ruby_version.to_s.match(/^\d+\.\d+/).to_s
9+
end
10+
11+
def devcontainer_dependencies
12+
return @devcontainer_dependencies if @devcontainer_dependencies
13+
14+
@devcontainer_dependencies = []
15+
16+
@devcontainer_dependencies << "selenium" if depends_on_system_test?
17+
@devcontainer_dependencies << "redis" if devcontainer_needs_redis?
18+
@devcontainer_dependencies << db_name_for_devcontainer if db_name_for_devcontainer
19+
@devcontainer_dependencies
20+
end
21+
22+
def devcontainer_variables
23+
return @devcontainer_variables if @devcontainer_variables
24+
25+
@devcontainer_variables = {}
26+
27+
@devcontainer_variables["CAPYBARA_SERVER_PORT"] = "45678" if depends_on_system_test?
28+
@devcontainer_variables["SELENIUM_HOST"] = "selenium" if depends_on_system_test?
29+
@devcontainer_variables["REDIS_URL"] = "redis://redis:6379/1" if devcontainer_needs_redis?
30+
@devcontainer_variables["DB_HOST"] = db_name_for_devcontainer if db_name_for_devcontainer
31+
32+
@devcontainer_variables
33+
end
34+
35+
def devcontainer_volumes
36+
return @devcontainer_volumes if @devcontainer_volumes
37+
38+
@devcontainer_volumes = []
39+
40+
@devcontainer_volumes << "redis-data" if devcontainer_needs_redis?
41+
@devcontainer_volumes << db_volume_name_for_devcontainer if db_volume_name_for_devcontainer
42+
43+
@devcontainer_volumes
44+
end
45+
46+
def devcontainer_needs_redis?
47+
!(options.skip_action_cable? && options.skip_active_job?)
48+
end
49+
50+
def db_name_for_devcontainer(database = options[:database])
51+
case database
52+
when "mysql" then "mysql"
53+
when "trilogy" then "mariadb"
54+
when "postgresql" then "postgres"
55+
end
56+
end
57+
58+
def db_volume_name_for_devcontainer(database = options[:database])
59+
case database
60+
when "mysql" then "mysql-data"
61+
when "trilogy" then "mariadb-data"
62+
when "postgresql" then "postgres-data"
63+
end
64+
end
65+
66+
def devcontainer_db_service_yaml(**options)
67+
return unless service = db_service_for_devcontainer
68+
69+
service.to_yaml(**options)[4..-1]
70+
end
71+
72+
def db_service_for_devcontainer(database = options[:database])
73+
case database
74+
when "mysql" then mysql_service
75+
when "trilogy" then mariadb_service
76+
when "postgresql" then postgres_service
77+
end
78+
end
79+
80+
def postgres_service
81+
{
82+
"postgres" => {
83+
"image" => "postgres:16.1",
84+
"restart" => "unless-stopped",
85+
"networks" => ["default"],
86+
"volumes" => ["postgres-data:/var/lib/postgresql/data"],
87+
"environment" => {
88+
"POSTGRES_USER" => "postgres",
89+
"POSTGRES_PASSWORD" => "postgres"
90+
}
91+
}
92+
}
93+
end
94+
95+
def mysql_service
96+
{
97+
"mysql" => {
98+
"image" => "mysql/mysql-server:8.0",
99+
"restart" => "unless-stopped",
100+
"environment" => {
101+
"MYSQL_ALLOW_EMPTY_PASSWORD" => true,
102+
"MYSQL_ROOT_HOST" => "%"
103+
},
104+
"volumes" => ["mysql-data:/var/lib/mysql"],
105+
"networks" => ["default"],
106+
}
107+
}
108+
end
109+
110+
def mariadb_service
111+
{
112+
"mariadb" => {
113+
"image" => "mariadb:10.5",
114+
"restart" => "unless-stopped",
115+
"networks" => ["dqefault"],
116+
"volumes" => ["mariadb-data:/var/lib/mysql"],
117+
"environment" => {
118+
"MARIADB_ALLOW_EMPTY_ROOT_PASSWORD" => true,
119+
},
120+
}
121+
}
122+
end
123+
124+
def db_service_names
125+
["mysql", "mariadb", "postgres"]
126+
end
127+
end
128+
end
129+
end

railties/lib/rails/generators/rails/app/app_generator.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,14 @@ def vendor
266266
def config_target_version
267267
@config_target_version || Rails::VERSION::STRING.to_f
268268
end
269+
270+
def devcontainer
271+
empty_directory ".devcontainer"
272+
273+
template ".devcontainer/devcontainer.json"
274+
template ".devcontainer/Dockerfile"
275+
template ".devcontainer/compose.yaml"
276+
end
269277
end
270278

271279
module Generators
@@ -455,6 +463,11 @@ def create_storage_files
455463
build(:storage)
456464
end
457465

466+
def create_devcontainer_files
467+
return if skip_devcontainer? || options[:dummy_app]
468+
build(:devcontainer)
469+
end
470+
458471
def delete_app_assets_if_api_option
459472
if options[:api]
460473
remove_dir "app/assets"
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Make sure RUBY_VERSION matches the Ruby version in .ruby-version
2+
ARG RUBY_VERSION=<%= devcontainer_ruby_version %>
3+
FROM mcr.microsoft.com/devcontainers/ruby:1-$RUBY_VERSION-bookworm
4+
5+
<%- unless options.skip_active_storage -%>
6+
# Install packages needed to build gems
7+
RUN apt-get update -qq && \
8+
apt-get install --no-install-recommends -y \
9+
libvips \
10+
# For video thumbnails
11+
ffmpeg \
12+
# For pdf thumbnails. If you want to use mupdf instead of poppler,
13+
# you can install the following packages instead:
14+
# mupdf mupdf-tools
15+
poppler-utils
16+
<%- end -%>

0 commit comments

Comments
 (0)