mirror of https://github.com/rails/rails
Generate a .devcontainer folder and its contents when creating a new app.
The .devcontainer folder includes everything needed to boot the app and do development in a remote container. The container setup includes: - A redis container for Kredis, ActionCable etc. - A database (SQLite, Postgres, MySQL or MariaDB) - A Headless chrome container for system tests - Active Storage configured to use the local disk and with preview features working If any of these options are skipped in the app setup they will not be included in the container configuration. These files can be skipped using the `--no-devcontainer` option. Co-authored-by: Rafael Mendonça França <rafael@franca.dev>
This commit is contained in:
parent
30506d2ef3
commit
c90a8701e5
|
@ -161,7 +161,7 @@ module ActionDispatch
|
|||
self.driver = SystemTesting::Driver.new(driver, **driver_options, &capabilities)
|
||||
end
|
||||
|
||||
# Configuration for the System Test application server
|
||||
# Configuration for the System Test application server.
|
||||
#
|
||||
# By default this is localhost. This method allows the host and port to be specified manually.
|
||||
def self.served_by(host:, port:)
|
||||
|
|
|
@ -1,3 +1,19 @@
|
|||
* Generate a .devcontainer folder and its contents when creating a new app.
|
||||
|
||||
The .devcontainer folder includes everything needed to boot the app and do development in a remote container.
|
||||
|
||||
The container setup includes:
|
||||
- A redis container for Kredis, ActionCable etc.
|
||||
- A database (SQLite, Postgres, MySQL or MariaDB)
|
||||
- A Headless chrome container for system tests
|
||||
- Active Storage configured to use the local disk and with preview features working
|
||||
|
||||
If any of these options are skipped in the app setup they will not be included in the container configuration.
|
||||
|
||||
These files can be skipped using the `--skip-devcontainer` option.
|
||||
|
||||
*Andrew Novoselac & Rafael Mendonça França*
|
||||
|
||||
* Introduce `SystemTestCase#served_by` for configuring the System Test application server
|
||||
|
||||
By default this is localhost. This method allows the host and port to be specified manually.
|
||||
|
|
|
@ -23,6 +23,7 @@ module Rails
|
|||
autoload :NamedBase, "rails/generators/named_base"
|
||||
autoload :ResourceHelpers, "rails/generators/resource_helpers"
|
||||
autoload :TestCase, "rails/generators/test_case"
|
||||
autoload :Devcontainer, "rails/generators/devcontainer"
|
||||
|
||||
mattr_accessor :namespace
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ module Rails
|
|||
module Generators
|
||||
class AppBase < Base # :nodoc:
|
||||
include Database
|
||||
include Devcontainer
|
||||
include AppName
|
||||
|
||||
NODE_LTS_VERSION = "18.15.0"
|
||||
|
@ -109,6 +110,9 @@ module Rails
|
|||
class_option :skip_ci, type: :boolean, default: nil,
|
||||
desc: "Skip GitHub CI files"
|
||||
|
||||
class_option :skip_devcontainer, type: :boolean, default: false,
|
||||
desc: "Skip devcontainer files"
|
||||
|
||||
class_option :dev, type: :boolean, default: nil,
|
||||
desc: "Set up the #{name} with Gemfile pointing to your Rails checkout"
|
||||
|
||||
|
@ -400,6 +404,10 @@ module Rails
|
|||
options[:skip_ci]
|
||||
end
|
||||
|
||||
def skip_devcontainer?
|
||||
options[:skip_devcontainer]
|
||||
end
|
||||
|
||||
class GemfileEntry < Struct.new(:name, :version, :comment, :options, :commented_out)
|
||||
def initialize(name, version, comment, options = {}, commented_out = false)
|
||||
super
|
||||
|
|
|
@ -90,6 +90,14 @@ module Rails
|
|||
"/opt/lampp/var/mysql/mysql.sock" # xampp for linux
|
||||
].find { |f| File.exist?(f) } unless Gem.win_platform?
|
||||
end
|
||||
|
||||
def mysql_database_host
|
||||
if options[:skip_devcontainer]
|
||||
"localhost"
|
||||
else
|
||||
"<%= ENV.fetch(\"DB_HOST\") { \"localhost\" } %>"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,129 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Rails
|
||||
module Generators
|
||||
module Devcontainer
|
||||
private
|
||||
def devcontainer_ruby_version
|
||||
gem_ruby_version.to_s.match(/^\d+\.\d+/).to_s
|
||||
end
|
||||
|
||||
def devcontainer_dependencies
|
||||
return @devcontainer_dependencies if @devcontainer_dependencies
|
||||
|
||||
@devcontainer_dependencies = []
|
||||
|
||||
@devcontainer_dependencies << "selenium" if depends_on_system_test?
|
||||
@devcontainer_dependencies << "redis" if devcontainer_needs_redis?
|
||||
@devcontainer_dependencies << db_name_for_devcontainer if db_name_for_devcontainer
|
||||
@devcontainer_dependencies
|
||||
end
|
||||
|
||||
def devcontainer_variables
|
||||
return @devcontainer_variables if @devcontainer_variables
|
||||
|
||||
@devcontainer_variables = {}
|
||||
|
||||
@devcontainer_variables["CAPYBARA_SERVER_PORT"] = "45678" if depends_on_system_test?
|
||||
@devcontainer_variables["SELENIUM_HOST"] = "selenium" if depends_on_system_test?
|
||||
@devcontainer_variables["REDIS_URL"] = "redis://redis:6379/1" if devcontainer_needs_redis?
|
||||
@devcontainer_variables["DB_HOST"] = db_name_for_devcontainer if db_name_for_devcontainer
|
||||
|
||||
@devcontainer_variables
|
||||
end
|
||||
|
||||
def devcontainer_volumes
|
||||
return @devcontainer_volumes if @devcontainer_volumes
|
||||
|
||||
@devcontainer_volumes = []
|
||||
|
||||
@devcontainer_volumes << "redis-data" if devcontainer_needs_redis?
|
||||
@devcontainer_volumes << db_volume_name_for_devcontainer if db_volume_name_for_devcontainer
|
||||
|
||||
@devcontainer_volumes
|
||||
end
|
||||
|
||||
def devcontainer_needs_redis?
|
||||
!(options.skip_action_cable? && options.skip_active_job?)
|
||||
end
|
||||
|
||||
def db_name_for_devcontainer(database = options[:database])
|
||||
case database
|
||||
when "mysql" then "mysql"
|
||||
when "trilogy" then "mariadb"
|
||||
when "postgresql" then "postgres"
|
||||
end
|
||||
end
|
||||
|
||||
def db_volume_name_for_devcontainer(database = options[:database])
|
||||
case database
|
||||
when "mysql" then "mysql-data"
|
||||
when "trilogy" then "mariadb-data"
|
||||
when "postgresql" then "postgres-data"
|
||||
end
|
||||
end
|
||||
|
||||
def devcontainer_db_service_yaml(**options)
|
||||
return unless service = db_service_for_devcontainer
|
||||
|
||||
service.to_yaml(**options)[4..-1]
|
||||
end
|
||||
|
||||
def db_service_for_devcontainer(database = options[:database])
|
||||
case database
|
||||
when "mysql" then mysql_service
|
||||
when "trilogy" then mariadb_service
|
||||
when "postgresql" then postgres_service
|
||||
end
|
||||
end
|
||||
|
||||
def postgres_service
|
||||
{
|
||||
"postgres" => {
|
||||
"image" => "postgres:16.1",
|
||||
"restart" => "unless-stopped",
|
||||
"networks" => ["default"],
|
||||
"volumes" => ["postgres-data:/var/lib/postgresql/data"],
|
||||
"environment" => {
|
||||
"POSTGRES_USER" => "postgres",
|
||||
"POSTGRES_PASSWORD" => "postgres"
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def mysql_service
|
||||
{
|
||||
"mysql" => {
|
||||
"image" => "mysql/mysql-server:8.0",
|
||||
"restart" => "unless-stopped",
|
||||
"environment" => {
|
||||
"MYSQL_ALLOW_EMPTY_PASSWORD" => true,
|
||||
"MYSQL_ROOT_HOST" => "%"
|
||||
},
|
||||
"volumes" => ["mysql-data:/var/lib/mysql"],
|
||||
"networks" => ["default"],
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def mariadb_service
|
||||
{
|
||||
"mariadb" => {
|
||||
"image" => "mariadb:10.5",
|
||||
"restart" => "unless-stopped",
|
||||
"networks" => ["dqefault"],
|
||||
"volumes" => ["mariadb-data:/var/lib/mysql"],
|
||||
"environment" => {
|
||||
"MARIADB_ALLOW_EMPTY_ROOT_PASSWORD" => true,
|
||||
},
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def db_service_names
|
||||
["mysql", "mariadb", "postgres"]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -266,6 +266,14 @@ module Rails
|
|||
def config_target_version
|
||||
@config_target_version || Rails::VERSION::STRING.to_f
|
||||
end
|
||||
|
||||
def devcontainer
|
||||
empty_directory ".devcontainer"
|
||||
|
||||
template ".devcontainer/devcontainer.json"
|
||||
template ".devcontainer/Dockerfile"
|
||||
template ".devcontainer/compose.yaml"
|
||||
end
|
||||
end
|
||||
|
||||
module Generators
|
||||
|
@ -455,6 +463,11 @@ module Rails
|
|||
build(:storage)
|
||||
end
|
||||
|
||||
def create_devcontainer_files
|
||||
return if skip_devcontainer? || options[:dummy_app]
|
||||
build(:devcontainer)
|
||||
end
|
||||
|
||||
def delete_app_assets_if_api_option
|
||||
if options[:api]
|
||||
remove_dir "app/assets"
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
# Make sure RUBY_VERSION matches the Ruby version in .ruby-version
|
||||
ARG RUBY_VERSION=<%= devcontainer_ruby_version %>
|
||||
FROM mcr.microsoft.com/devcontainers/ruby:1-$RUBY_VERSION-bookworm
|
||||
|
||||
<%- unless options.skip_active_storage -%>
|
||||
# Install packages needed to build gems
|
||||
RUN apt-get update -qq && \
|
||||
apt-get install --no-install-recommends -y \
|
||||
libvips \
|
||||
# For video thumbnails
|
||||
ffmpeg \
|
||||
# For pdf thumbnails. If you want to use mupdf instead of poppler,
|
||||
# you can install the following packages instead:
|
||||
# mupdf mupdf-tools
|
||||
poppler-utils
|
||||
<%- end -%>
|
|
@ -0,0 +1,55 @@
|
|||
services:
|
||||
rails-app:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: .devcontainer/Dockerfile
|
||||
|
||||
volumes:
|
||||
- ../..:/workspaces:cached
|
||||
|
||||
# Overrides default command so things don't shut down after the process ends.
|
||||
command: sleep infinity
|
||||
|
||||
networks:
|
||||
- default
|
||||
|
||||
# Uncomment the next line to use a non-root user for all processes.
|
||||
# user: vscode
|
||||
|
||||
# Use "forwardPorts" in **devcontainer.json** to forward an app port locally.
|
||||
# (Adding the "ports" property to this file will not forward from a Codespace.)
|
||||
ports:
|
||||
- 45678:45678
|
||||
<%- if !devcontainer_dependencies.empty? -%>
|
||||
depends_on:
|
||||
<%- devcontainer_dependencies.each do |dependency| -%>
|
||||
- <%= dependency %>
|
||||
<%- end -%>
|
||||
<%- end -%>
|
||||
|
||||
<%- if depends_on_system_test? -%>
|
||||
selenium:
|
||||
image: seleniarm/standalone-chromium
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- default
|
||||
<%- end -%>
|
||||
|
||||
<%- if devcontainer_needs_redis? -%>
|
||||
redis:
|
||||
image: redis:7.2
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- default
|
||||
volumes:
|
||||
- redis-data:/data
|
||||
<%- end -%>
|
||||
|
||||
<%= devcontainer_db_service_yaml(indentation: 4) %>
|
||||
|
||||
<%- if !devcontainer_volumes.empty? -%>
|
||||
volumes:
|
||||
<%- devcontainer_volumes.each do |volume| -%>
|
||||
<%= volume %>:
|
||||
<%- end -%>
|
||||
<%- end -%>
|
|
@ -0,0 +1,34 @@
|
|||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||
// README at: https://github.com/devcontainers/templates/tree/main/src/ruby
|
||||
{
|
||||
"name": "<%= app_name %>",
|
||||
"dockerComposeFile": "compose.yaml",
|
||||
"service": "rails-app",
|
||||
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
|
||||
|
||||
// Features to add to the dev container. More info: https://containers.dev/features.
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/github-cli:1": {}
|
||||
},
|
||||
|
||||
<%- if !devcontainer_variables.empty? -%>
|
||||
"containerEnv": {
|
||||
<%= devcontainer_variables.map { |key, value| "\"#{key}\": \"#{value}\"" }.join(",\n ") %>
|
||||
},
|
||||
<%- end -%>
|
||||
|
||||
// Features to add to the dev container. More info: https://containers.dev/features.
|
||||
// "features": {},
|
||||
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
// "forwardPorts": [],
|
||||
|
||||
// Configure tool-specific properties.
|
||||
// "customizations": {},
|
||||
|
||||
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
||||
// "remoteUser": "root",
|
||||
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
"postCreateCommand": "bin/setup"
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
# syntax = docker/dockerfile:1
|
||||
|
||||
# Make sure RUBY_VERSION matches the Ruby version in .ruby-version and Gemfile
|
||||
# Make sure RUBY_VERSION matches the Ruby version in .ruby-version
|
||||
ARG RUBY_VERSION=<%= gem_ruby_version %>
|
||||
FROM registry.docker.com/library/ruby:$RUBY_VERSION-slim as base
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ default: &default
|
|||
<% if mysql_socket -%>
|
||||
socket: <%= mysql_socket %>
|
||||
<% else -%>
|
||||
host: localhost
|
||||
host: <%= mysql_database_host %>
|
||||
<% end -%>
|
||||
|
||||
development:
|
||||
|
|
|
@ -18,6 +18,13 @@ default: &default
|
|||
# For details on connection pooling, see Rails configuration guide
|
||||
# https://guides.rubyonrails.org/configuring.html#database-pooling
|
||||
pool: <%%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
|
||||
<% unless options[:skip_devcontainer] -%>
|
||||
<%% if ENV["DB_HOST"] %>
|
||||
host: <%%= ENV["DB_HOST"] %>
|
||||
username: postgres
|
||||
password: postgres
|
||||
<%% end %>
|
||||
<% end %>
|
||||
|
||||
development:
|
||||
<<: *default
|
||||
|
|
|
@ -18,7 +18,7 @@ default: &default
|
|||
<% if mysql_socket -%>
|
||||
socket: <%= mysql_socket %>
|
||||
<% else -%>
|
||||
host: localhost
|
||||
host: <%= mysql_database_host %>
|
||||
<% end -%>
|
||||
|
||||
development:
|
||||
|
|
|
@ -1,5 +1,18 @@
|
|||
require "test_helper"
|
||||
|
||||
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
|
||||
<% if skip_devcontainer? -%>
|
||||
driven_by :selenium, using: :headless_chrome, screen_size: [ 1400, 1400 ]
|
||||
<% else -%>
|
||||
if ENV["CAPYBARA_SERVER_PORT"]
|
||||
served_by host: "rails-app", port: ENV["CAPYBARA_SERVER_PORT"]
|
||||
|
||||
driven_by :selenium, using: :headless_chrome, screen_size: [ 1400, 1400 ], options: {
|
||||
browser: :remote,
|
||||
url: "http://#{ENV["SELENIUM_HOST"]}:4444",
|
||||
}
|
||||
else
|
||||
driven_by :selenium, using: :headless_chrome, screen_size: [ 1400, 1400 ]
|
||||
end
|
||||
<% end -%>
|
||||
end
|
||||
|
|
|
@ -8,6 +8,7 @@ module Rails
|
|||
module System
|
||||
class ChangeGenerator < Base # :nodoc:
|
||||
include Database
|
||||
include Devcontainer
|
||||
include AppName
|
||||
|
||||
class_option :to, required: true,
|
||||
|
@ -54,6 +55,14 @@ module Rails
|
|||
end
|
||||
end
|
||||
|
||||
def edit_devcontainer_files
|
||||
devcontainer_path = File.expand_path(".devcontainer", destination_root)
|
||||
return unless File.exist?(devcontainer_path)
|
||||
|
||||
edit_devcontainer_json
|
||||
edit_compose_yaml
|
||||
end
|
||||
|
||||
private
|
||||
def all_database_gems
|
||||
DATABASES.map { |database| gem_for_database(database) }
|
||||
|
@ -88,6 +97,59 @@ module Rails
|
|||
gem_name_and_version.map! { |segment| "\"#{segment}\"" }
|
||||
"gem #{gem_name_and_version.join(", ")}"
|
||||
end
|
||||
|
||||
def edit_devcontainer_json
|
||||
devcontainer_json_path = File.expand_path(".devcontainer/devcontainer.json", destination_root)
|
||||
return unless File.exist?(devcontainer_json_path)
|
||||
|
||||
container_env = JSON.parse(File.read(devcontainer_json_path))["containerEnv"]
|
||||
db_name = db_name_for_devcontainer
|
||||
|
||||
if container_env["DB_HOST"]
|
||||
if db_name
|
||||
container_env["DB_HOST"] = db_name
|
||||
else
|
||||
container_env.delete("DB_HOST")
|
||||
end
|
||||
else
|
||||
if db_name
|
||||
container_env["DB_HOST"] = db_name
|
||||
end
|
||||
end
|
||||
|
||||
new_json = JSON.pretty_generate(container_env, indent: " ", object_nl: "\n ")
|
||||
|
||||
gsub_file(".devcontainer/devcontainer.json", /("containerEnv"\s*:\s*){[^}]*}/, "\\1#{new_json}")
|
||||
end
|
||||
|
||||
def edit_compose_yaml
|
||||
compose_yaml_path = File.expand_path(".devcontainer/compose.yaml", destination_root)
|
||||
return unless File.exist?(compose_yaml_path)
|
||||
|
||||
compose_config = YAML.load_file(compose_yaml_path)
|
||||
|
||||
db_service_names.each do |db_service_name|
|
||||
compose_config["services"].delete(db_service_name)
|
||||
compose_config["volumes"]&.delete("#{db_service_name}-data")
|
||||
compose_config["services"]["rails-app"]["depends_on"]&.delete(db_service_name)
|
||||
end
|
||||
|
||||
db_service = db_service_for_devcontainer
|
||||
|
||||
if db_service
|
||||
compose_config["services"].merge!(db_service)
|
||||
compose_config["volumes"] = { db_volume_name_for_devcontainer => nil }.merge(compose_config["volumes"] || {})
|
||||
compose_config["services"]["rails-app"]["depends_on"] = [
|
||||
db_name_for_devcontainer,
|
||||
compose_config["services"]["rails-app"]["depends_on"]
|
||||
].flatten.compact
|
||||
end
|
||||
|
||||
compose_config.delete("volumes") unless compose_config["volumes"]&.any?
|
||||
compose_config["services"]["rails-app"].delete("depends_on") unless compose_config["services"]["rails-app"]["depends_on"]&.any?
|
||||
|
||||
File.write(compose_yaml_path, compose_config.to_yaml)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
services:
|
||||
rails-app:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: .devcontainer/Dockerfile
|
||||
|
||||
volumes:
|
||||
- ../..:/workspaces:cached
|
||||
|
||||
# Overrides default command so things don't shut down after the process ends.
|
||||
command: sleep infinity
|
||||
|
||||
networks:
|
||||
- default
|
|
@ -0,0 +1,45 @@
|
|||
services:
|
||||
rails-app:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: .devcontainer/Dockerfile
|
||||
|
||||
volumes:
|
||||
- ../..:/workspaces:cached
|
||||
|
||||
# Overrides default command so things don't shut down after the process ends.
|
||||
command: sleep infinity
|
||||
|
||||
networks:
|
||||
- default
|
||||
|
||||
# Uncomment the next line to use a non-root user for all processes.
|
||||
# user: vscode
|
||||
|
||||
# Use "forwardPorts" in **devcontainer.json** to forward an app port locally.
|
||||
# (Adding the "ports" property to this file will not forward from a Codespace.)
|
||||
ports:
|
||||
- 45678:45678
|
||||
depends_on:
|
||||
- selenium
|
||||
- redis
|
||||
|
||||
selenium:
|
||||
image: seleniarm/standalone-chromium
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- default
|
||||
|
||||
redis:
|
||||
image: redis:7.2
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- default
|
||||
volumes:
|
||||
- redis-data:/data
|
||||
|
||||
|
||||
|
||||
|
||||
volumes:
|
||||
redis-data:
|
|
@ -0,0 +1,34 @@
|
|||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||
// README at: https://github.com/devcontainers/templates/tree/main/src/ruby
|
||||
{
|
||||
"name": "tmp",
|
||||
"dockerComposeFile": "compose.yaml",
|
||||
"service": "rails-app",
|
||||
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
|
||||
|
||||
// Features to add to the dev container. More info: https://containers.dev/features.
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/github-cli:1": {}
|
||||
},
|
||||
|
||||
"containerEnv": {
|
||||
"CAPYBARA_SERVER_PORT": "45678",
|
||||
"SELENIUM_HOST": "selenium",
|
||||
"REDIS_URL": "redis://redis:6379/1"
|
||||
},
|
||||
|
||||
// Features to add to the dev container. More info: https://containers.dev/features.
|
||||
// "features": {},
|
||||
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
// "forwardPorts": [],
|
||||
|
||||
// Configure tool-specific properties.
|
||||
// "customizations": {},
|
||||
|
||||
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
||||
// "remoteUser": "root",
|
||||
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
"postCreateCommand": "bin/setup"
|
||||
}
|
|
@ -5,6 +5,9 @@ require "rails/generators/rails/app/app_generator"
|
|||
require "generators/shared_generator_tests"
|
||||
|
||||
DEFAULT_APP_FILES = %w(
|
||||
.devcontainer/Dockerfile
|
||||
.devcontainer/compose.yaml
|
||||
.devcontainer/devcontainer.json
|
||||
.dockerignore
|
||||
.git
|
||||
.gitattributes
|
||||
|
@ -1005,11 +1008,17 @@ class AppGeneratorTest < Rails::Generators::TestCase
|
|||
def test_inclusion_of_ruby_version
|
||||
run_generator
|
||||
|
||||
ruby_version = "#{Gem::Version.new(Gem::VERSION) >= Gem::Version.new("3.3.13") ? Gem.ruby_version : RUBY_VERSION}"
|
||||
|
||||
assert_file "Gemfile" do |content|
|
||||
assert_match(/ruby "#{Gem::Version.new(Gem::VERSION) >= Gem::Version.new("3.3.13") ? Gem.ruby_version : RUBY_VERSION}"/, content)
|
||||
assert_match("ruby \"#{ruby_version}\"", content)
|
||||
end
|
||||
assert_file ".devcontainer/Dockerfile" do |content|
|
||||
minor_ruby_version = ruby_version.match(/^\d+\.\d+/).to_s
|
||||
assert_match(/ARG RUBY_VERSION=#{minor_ruby_version}$/, content)
|
||||
end
|
||||
assert_file "Dockerfile" do |content|
|
||||
assert_match(/ARG RUBY_VERSION=#{Gem::Version.new(Gem::VERSION) >= Gem::Version.new("3.3.13") ? Gem.ruby_version : RUBY_VERSION}/, content)
|
||||
assert_match(/ARG RUBY_VERSION=#{ruby_version}/, content)
|
||||
end
|
||||
assert_file ".ruby-version" do |content|
|
||||
if ENV["RBENV_VERSION"]
|
||||
|
@ -1229,6 +1238,185 @@ class AppGeneratorTest < Rails::Generators::TestCase
|
|||
assert_file "config/application.rb", /^module MyApp$/
|
||||
end
|
||||
|
||||
def test_devcontainer
|
||||
run_generator [destination_root, "--name=my-app"]
|
||||
|
||||
assert_file(".devcontainer/devcontainer.json") do |content|
|
||||
assert_match(/"name": "my_app"/, content)
|
||||
assert_match(/"REDIS_URL": "redis:\/\/redis:6379\/1"/, content)
|
||||
assert_match(/"CAPYBARA_SERVER_PORT": "45678"/, content)
|
||||
assert_match(/"SELENIUM_HOST": "selenium"/, content)
|
||||
end
|
||||
assert_file(".devcontainer/Dockerfile") do |content|
|
||||
assert_match(/libvips/, content)
|
||||
assert_match(/ffmpeg/, content)
|
||||
assert_match(/poppler-utils/, content)
|
||||
end
|
||||
assert_compose_file do |compose_config|
|
||||
expected_rails_app_config = {
|
||||
"build" => {
|
||||
"context" => "..",
|
||||
"dockerfile" => ".devcontainer/Dockerfile"
|
||||
},
|
||||
"volumes" => ["../..:/workspaces:cached"],
|
||||
"command" => "sleep infinity",
|
||||
"networks" => ["default"],
|
||||
"ports" => ["45678:45678"],
|
||||
"depends_on" => ["selenium", "redis"]
|
||||
}
|
||||
|
||||
assert_equal expected_rails_app_config, compose_config["services"]["rails-app"]
|
||||
|
||||
expected_selenium_conifg = {
|
||||
"image" => "seleniarm/standalone-chromium",
|
||||
"restart" => "unless-stopped",
|
||||
"networks" => ["default"]
|
||||
}
|
||||
|
||||
assert_equal expected_selenium_conifg, compose_config["services"]["selenium"]
|
||||
|
||||
expected_redis_config = {
|
||||
"image" => "redis:7.2",
|
||||
"restart" => "unless-stopped",
|
||||
"networks" => ["default"],
|
||||
"volumes" => ["redis-data:/data"]
|
||||
}
|
||||
|
||||
assert_equal expected_redis_config, compose_config["services"]["redis"]
|
||||
assert_equal ["redis-data"], compose_config["volumes"].keys
|
||||
end
|
||||
end
|
||||
|
||||
def test_devcontainer_no_redis_skipping_action_cable_and_active_job
|
||||
run_generator [ destination_root, "--skip-action-cable", "--skip-active-job" ]
|
||||
|
||||
assert_compose_file do |compose_config|
|
||||
assert_not_includes compose_config["services"]["rails-app"]["depends_on"], "redis"
|
||||
assert_nil compose_config["services"]["redis"]
|
||||
assert_nil compose_config["volumes"]
|
||||
end
|
||||
end
|
||||
|
||||
def test_devonctainer_postgresql
|
||||
run_generator [ destination_root, "-d", "postgresql" ]
|
||||
|
||||
assert_compose_file do |compose_config|
|
||||
assert_includes compose_config["services"]["rails-app"]["depends_on"], "postgres"
|
||||
|
||||
expected_postgres_config = {
|
||||
"image" => "postgres:16.1",
|
||||
"restart" => "unless-stopped",
|
||||
"networks" => ["default"],
|
||||
"volumes" => ["postgres-data:/var/lib/postgresql/data"],
|
||||
"environment" => {
|
||||
"POSTGRES_USER" => "postgres",
|
||||
"POSTGRES_PASSWORD" => "postgres"
|
||||
}
|
||||
}
|
||||
|
||||
assert_equal expected_postgres_config, compose_config["services"]["postgres"]
|
||||
assert_includes compose_config["volumes"].keys, "postgres-data"
|
||||
end
|
||||
assert_file(".devcontainer/devcontainer.json") do |content|
|
||||
assert_match(/"DB_HOST": "postgres"/, content)
|
||||
end
|
||||
assert_file("config/database.yml") do |content|
|
||||
assert_match(/host: <%= ENV\["DB_HOST"\] %>/, content)
|
||||
end
|
||||
end
|
||||
|
||||
def test_devonctainer_mysql
|
||||
run_generator [ destination_root, "-d", "mysql" ]
|
||||
|
||||
assert_compose_file do |compose_config|
|
||||
assert_includes compose_config["services"]["rails-app"]["depends_on"], "mysql"
|
||||
|
||||
expected_mysql_config = {
|
||||
"image" => "mysql/mysql-server:8.0",
|
||||
"restart" => "unless-stopped",
|
||||
"environment" => {
|
||||
"MYSQL_ALLOW_EMPTY_PASSWORD" => true,
|
||||
"MYSQL_ROOT_HOST" => "%"
|
||||
},
|
||||
"volumes" => ["mysql-data:/var/lib/mysql"],
|
||||
"networks" => ["default"],
|
||||
}
|
||||
|
||||
assert_equal expected_mysql_config, compose_config["services"]["mysql"]
|
||||
assert_includes compose_config["volumes"].keys, "mysql-data"
|
||||
end
|
||||
assert_file(".devcontainer/devcontainer.json") do |content|
|
||||
assert_match(/"DB_HOST": "mysql"/, content)
|
||||
end
|
||||
assert_file("config/database.yml") do |content|
|
||||
assert_match(/host: <%= ENV.fetch\("DB_HOST"\) \{ "localhost" } %>/, content)
|
||||
end
|
||||
end
|
||||
|
||||
def test_devonctainer_mariadb
|
||||
run_generator [ destination_root, "-d", "trilogy" ]
|
||||
|
||||
assert_compose_file do |compose_config|
|
||||
assert_includes compose_config["services"]["rails-app"]["depends_on"], "mariadb"
|
||||
expected_mariadb_config = {
|
||||
"image" => "mariadb:10.5",
|
||||
"restart" => "unless-stopped",
|
||||
"networks" => ["dqefault"],
|
||||
"volumes" => ["mariadb-data:/var/lib/mysql"],
|
||||
"environment" => {
|
||||
"MARIADB_ALLOW_EMPTY_ROOT_PASSWORD" => true,
|
||||
},
|
||||
}
|
||||
|
||||
assert_equal expected_mariadb_config, compose_config["services"]["mariadb"]
|
||||
assert_includes compose_config["volumes"].keys, "mariadb-data"
|
||||
end
|
||||
assert_file(".devcontainer/devcontainer.json") do |content|
|
||||
assert_match(/"DB_HOST": "mariadb"/, content)
|
||||
end
|
||||
assert_file("config/database.yml") do |content|
|
||||
assert_match(/host: <%= ENV.fetch\("DB_HOST"\) \{ "localhost" } %>/, content)
|
||||
end
|
||||
end
|
||||
|
||||
def test_devcontainer_no_selenium_when_skipping_system_test
|
||||
run_generator [ destination_root, "--skip-system-test" ]
|
||||
|
||||
assert_compose_file do |compose_config|
|
||||
assert_not_includes compose_config["services"]["rails-app"]["depends_on"], "selenium"
|
||||
assert_not_includes compose_config["services"].keys, "selenium"
|
||||
end
|
||||
assert_file(".devcontainer/devcontainer.json") do |content|
|
||||
assert_no_match(/CAPYBARA_SERVER_PORT/, content)
|
||||
end
|
||||
end
|
||||
|
||||
def test_devcontainer_no_Dockerfile_packages_when_skipping_active_storage
|
||||
run_generator [ destination_root, "--skip-active-storage" ]
|
||||
|
||||
assert_file(".devcontainer/Dockerfile") do |content|
|
||||
assert_no_match(/libvips/, content)
|
||||
assert_no_match(/ffmpeg/, content)
|
||||
assert_no_match(/poppler-utils/, content)
|
||||
end
|
||||
end
|
||||
|
||||
def test_devcontainer_no_depends_on_when_no_dependencies
|
||||
run_generator [ destination_root, "--minimal" ]
|
||||
|
||||
assert_compose_file do |compose_config|
|
||||
assert_not_includes compose_config["services"]["rails-app"].keys, "depends_on"
|
||||
end
|
||||
end
|
||||
|
||||
def test_skip_devcontainer
|
||||
run_generator [ destination_root, "--skip-devcontainer" ]
|
||||
|
||||
assert_no_file(".devcontainer/devcontainer.json")
|
||||
assert_no_file(".devcontainer/Dockerfile")
|
||||
assert_no_file(".devcontainer/compose.yaml")
|
||||
end
|
||||
|
||||
private
|
||||
def assert_node_files
|
||||
assert_file ".node-version" do |content|
|
||||
|
|
|
@ -17,6 +17,7 @@ module Rails
|
|||
ENTRY
|
||||
|
||||
copy_dockerfile
|
||||
copy_devcontainer_files
|
||||
end
|
||||
|
||||
test "change to invalid database" do
|
||||
|
@ -50,6 +51,28 @@ module Rails
|
|||
assert_match "build-essential git libpq-dev", content
|
||||
assert_match "curl libvips postgresql-client", content
|
||||
end
|
||||
|
||||
assert_file(".devcontainer/devcontainer.json") do |content|
|
||||
assert_match(/"DB_HOST": "postgres"/, content)
|
||||
end
|
||||
|
||||
assert_compose_file do |compose_config|
|
||||
assert_includes compose_config["services"]["rails-app"]["depends_on"], "postgres"
|
||||
|
||||
expected_postgres_config = {
|
||||
"image" => "postgres:16.1",
|
||||
"restart" => "unless-stopped",
|
||||
"networks" => ["default"],
|
||||
"volumes" => ["postgres-data:/var/lib/postgresql/data"],
|
||||
"environment" => {
|
||||
"POSTGRES_USER" => "postgres",
|
||||
"POSTGRES_PASSWORD" => "postgres"
|
||||
}
|
||||
}
|
||||
|
||||
assert_equal expected_postgres_config, compose_config["services"]["postgres"]
|
||||
assert_includes compose_config["volumes"].keys, "postgres-data"
|
||||
end
|
||||
end
|
||||
|
||||
test "change to mysql" do
|
||||
|
@ -69,6 +92,28 @@ module Rails
|
|||
assert_match "build-essential default-libmysqlclient-dev git", content
|
||||
assert_match "curl default-mysql-client libvips", content
|
||||
end
|
||||
|
||||
assert_file(".devcontainer/devcontainer.json") do |content|
|
||||
assert_match(/"DB_HOST": "mysql"/, content)
|
||||
end
|
||||
|
||||
assert_compose_file do |compose_config|
|
||||
assert_includes compose_config["services"]["rails-app"]["depends_on"], "mysql"
|
||||
|
||||
expected_postgres_config = {
|
||||
"image" => "mysql/mysql-server:8.0",
|
||||
"restart" => "unless-stopped",
|
||||
"environment" => {
|
||||
"MYSQL_ALLOW_EMPTY_PASSWORD" => true,
|
||||
"MYSQL_ROOT_HOST" => "%"
|
||||
},
|
||||
"volumes" => ["mysql-data:/var/lib/mysql"],
|
||||
"networks" => ["default"],
|
||||
}
|
||||
|
||||
assert_equal expected_postgres_config, compose_config["services"]["mysql"]
|
||||
assert_includes compose_config["volumes"].keys, "mysql-data"
|
||||
end
|
||||
end
|
||||
|
||||
test "change to sqlite3" do
|
||||
|
@ -88,6 +133,10 @@ module Rails
|
|||
assert_match "build-essential git", content
|
||||
assert_match "curl libsqlite3-0 libvips", content
|
||||
end
|
||||
|
||||
assert_file(".devcontainer/devcontainer.json") do |content|
|
||||
assert_no_match(/"DB_HOST"/, content)
|
||||
end
|
||||
end
|
||||
|
||||
test "change to trilogy" do
|
||||
|
@ -108,6 +157,27 @@ module Rails
|
|||
assert_match "curl libvips", content
|
||||
assert_no_match "default-libmysqlclient-dev", content
|
||||
end
|
||||
|
||||
assert_file(".devcontainer/devcontainer.json") do |content|
|
||||
assert_match(/"DB_HOST": "mariadb"/, content)
|
||||
end
|
||||
|
||||
assert_compose_file do |compose_config|
|
||||
assert_includes compose_config["services"]["rails-app"]["depends_on"], "mariadb"
|
||||
|
||||
expected_postgres_config = {
|
||||
"image" => "mariadb:10.5",
|
||||
"restart" => "unless-stopped",
|
||||
"networks" => ["dqefault"],
|
||||
"volumes" => ["mariadb-data:/var/lib/mysql"],
|
||||
"environment" => {
|
||||
"MARIADB_ALLOW_EMPTY_ROOT_PASSWORD" => true,
|
||||
},
|
||||
}
|
||||
|
||||
assert_equal expected_postgres_config, compose_config["services"]["mariadb"]
|
||||
assert_includes compose_config["volumes"].keys, "mariadb-data"
|
||||
end
|
||||
end
|
||||
|
||||
test "change from versioned gem to other versioned gem" do
|
||||
|
@ -124,6 +194,23 @@ module Rails
|
|||
assert_match 'gem "mysql2", "~> 0.5"', content
|
||||
end
|
||||
end
|
||||
|
||||
test "change from db with devcontainer service to one without" do
|
||||
copy_minimal_devcontainer_compose_file
|
||||
|
||||
run_generator ["--to", "mysql"]
|
||||
run_generator ["--to", "sqlite3", "--force"]
|
||||
|
||||
assert_file(".devcontainer/devcontainer.json") do |content|
|
||||
assert_no_match(/"DB_HOST"/, content)
|
||||
end
|
||||
|
||||
assert_compose_file do |compose_config|
|
||||
assert_not_includes compose_config["services"]["rails-app"].keys, "depends_on"
|
||||
assert_not_includes compose_config["services"].keys, "mysql"
|
||||
assert_not_includes compose_config.keys, "volumes"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -82,6 +82,25 @@ module GeneratorsTestHelper
|
|||
File.write File.join(destination, "Dockerfile"), dockerfile
|
||||
end
|
||||
|
||||
def copy_devcontainer_files
|
||||
destination = File.join(destination_root, ".devcontainer")
|
||||
mkdir_p(destination)
|
||||
|
||||
devcontainer_json = File.read(File.expand_path("../fixtures/.devcontainer/devcontainer.json", __dir__))
|
||||
File.write File.join(destination, "devcontainer.json"), devcontainer_json
|
||||
|
||||
compose_yaml = File.read(File.expand_path("../fixtures/.devcontainer/compose.yaml", __dir__))
|
||||
File.write File.join(destination, "compose.yaml"), compose_yaml
|
||||
end
|
||||
|
||||
def copy_minimal_devcontainer_compose_file
|
||||
destination = File.join(destination_root, ".devcontainer")
|
||||
mkdir_p(destination)
|
||||
|
||||
compose_yaml = File.read(File.expand_path("../fixtures/.devcontainer/compose-minimal.yaml", __dir__))
|
||||
File.write File.join(destination, "compose.yaml"), compose_yaml
|
||||
end
|
||||
|
||||
def evaluate_template(file, locals = {})
|
||||
erb = ERB.new(File.read(file), trim_mode: "-", eoutvar: "@output_buffer")
|
||||
context = Class.new do
|
||||
|
@ -97,6 +116,12 @@ module GeneratorsTestHelper
|
|||
erb.result()
|
||||
end
|
||||
|
||||
def assert_compose_file
|
||||
assert_file ".devcontainer/compose.yaml" do |content|
|
||||
yield YAML.load(content)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def gemfile_locals
|
||||
{
|
||||
|
|
Loading…
Reference in New Issue