discharger 0.2.30 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3e4862f36fb92472471e953ab1c29809d2e9e777a83ae9742d348ba9855d363c
4
- data.tar.gz: 41b57081caa7c89bdd27ec7958cf1ac2e119ceac61420a7b9e3a138931f1cf5b
3
+ metadata.gz: 86a58f41a34e61bfc155a9563e55bbc1eb42d4bd6904c09fe4aafb9bc61f3411
4
+ data.tar.gz: 62d3b3ad5ffec0dcdee073f5ca0109416b383dd2fe180f8501af47f2028d9122
5
5
  SHA512:
6
- metadata.gz: cb018aef528e7a3a9caf6c8e45ce61541bc5e0acb47558b0247da4df513a3389732d7509569b0b12daae8cda9eaa620f2f90bc70531820de2eccd81426b17d99
7
- data.tar.gz: 8bb76e9bff688426a94d19d9d4887f986cf34afb9a023fc6e542fa8917ede77688b5948a48d410106b500ab6fc16d94d37a118eb0853b2696fffb79c1ee6d681
6
+ metadata.gz: d0f6c58d5fde62fb7dd63b5dbe56c867600eea46e5c8e74b9eee22025da5dd299bf9d0b1093a40a67411c5cc279b9ca826e0c6d0af8659635fb6692279fa8fab
7
+ data.tar.gz: 4ef09b9ca157755c99aa693d3c768e11df6f4fb470ed2d3c4736cf7c2fb894fb46352e941e550c2c200ec95bfd98f40a5daf22ca0928431231fdad04e07fe1ca
data/CHANGELOG.md CHANGED
@@ -5,10 +5,14 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](http://keepachangelog.com/)
6
6
  and this project adheres to [Semantic Versioning](http://semver.org/).
7
7
 
8
- ## [0.2.30] - 2026-03-11
8
+ ## [0.3.1] - 2026-05-13
9
9
 
10
- ### Changed
10
+ ### Added
11
11
 
12
- - Use git trailer fragments for changelog generation (70396ca)
12
+ - database.prefer_docker setup.yml option for explicit Docker vs native PostgreSQL selection (2fca81e)
13
13
 
14
- ## [0.2.29] - 2026-03-10
14
+ ## [0.2.31] - 2026-04-09
15
+
16
+ ### Added
17
+
18
+ - Unified setup entry point with pre-steps support (1459633)
data/README.md CHANGED
@@ -163,6 +163,12 @@ database:
163
163
  name: "db-your-app"
164
164
  version: "14"
165
165
  password: "postgres"
166
+ # Optional. Controls how the docker step handles a native PostgreSQL
167
+ # already listening on the configured port:
168
+ # omitted/nil/false - silently skip Docker and use the native instance (legacy default)
169
+ # true - always create the Docker container; fail if a native instance holds the port
170
+ # "prompt" - ask the developer (interactive shells only; non-interactive runs fall back to native)
171
+ prefer_docker: "prompt"
166
172
 
167
173
  redis:
168
174
  port: 6379
@@ -187,6 +193,30 @@ custom_steps:
187
193
  command: "bin/rails db:seed"
188
194
  ```
189
195
 
196
+ ### Pre-Rails Steps (Prerequisites)
197
+
198
+ The `pre_steps` array defines commands that run **before Rails loads**. These are for system dependencies and environment setup that must be in place before bundler or Rails can initialize.
199
+
200
+ Built-in pre_steps:
201
+ - `homebrew` - Installs Homebrew if not present (macOS)
202
+ - `postgresql_tools` - Installs PostgreSQL client tools (`pg_dump`, `psql`)
203
+
204
+ ```yaml
205
+ pre_steps:
206
+ - homebrew
207
+ - postgresql_tools
208
+ ```
209
+
210
+ You can also define custom pre_steps with shell commands:
211
+
212
+ ```yaml
213
+ pre_steps:
214
+ - homebrew
215
+ - description: "Set up environment variables"
216
+ command: "cp .env.example .env"
217
+ condition: "!File.exist?('.env')"
218
+ ```
219
+
190
220
  ### Using Default Steps
191
221
 
192
222
  The `steps` array specifies which built-in setup commands to run. Available commands include:
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Standalone loader for Discharger::SetupRunner::PrerequisitesLoader
4
+ # This can be required before Rails loads to set up environment variables
5
+ # and system dependencies.
6
+ #
7
+ # Usage in bin/setup:
8
+ # require "discharger/prerequisites"
9
+ # Discharger::SetupRunner::PrerequisitesLoader.run("config/setup.yml")
10
+ #
11
+ require_relative "setup_runner/pre_commands/base_pre_command"
12
+ require_relative "setup_runner/pre_commands/homebrew_pre_command"
13
+ require_relative "setup_runner/pre_commands/postgresql_tools_pre_command"
14
+ require_relative "setup_runner/pre_commands/pre_command_registry"
15
+ require_relative "setup_runner/prerequisites_loader"
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "pathname"
5
+
6
+ # Unified entry point for Discharger setup.
7
+ # Handles both pre-Rails prerequisites and post-Rails setup commands.
8
+ #
9
+ # Usage in bin/setup:
10
+ # require "discharger/setup"
11
+ # Discharger::Setup.run("config/setup.yml")
12
+ #
13
+ # This will automatically:
14
+ # 1. Load bundler/setup (activates correct gem versions)
15
+ # 2. Run pre_steps (before Rails loads) - env vars, homebrew, etc.
16
+ # 3. Initialize Rails
17
+ # 4. Run regular setup steps and custom_steps
18
+ #
19
+ module Discharger
20
+ class Setup
21
+ attr_reader :config_path, :app_root
22
+
23
+ def initialize(config_path, app_root: nil)
24
+ @config_path = config_path
25
+ @app_root = app_root || Dir.pwd
26
+ end
27
+
28
+ def self.run(config_path = "config/setup.yml", app_root: nil)
29
+ new(config_path, app_root: app_root).run
30
+ end
31
+
32
+ def run
33
+ FileUtils.chdir(app_root) do
34
+ validate_environment
35
+ print_header
36
+
37
+ # Phase 1: Load bundler first (activates correct gem versions)
38
+ # This must happen before we parse YAML to avoid psych version conflicts
39
+ load_bundler
40
+
41
+ # Phase 2: Pre-Rails setup (env vars, system dependencies)
42
+ # These run AFTER bundler but BEFORE Rails loads
43
+ run_prerequisites
44
+
45
+ # Phase 3: Load Rails (uses env vars from phase 2)
46
+ load_rails
47
+
48
+ # Phase 4: Run Discharger commands (after Rails loads)
49
+ run_setup_commands
50
+
51
+ print_footer
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ def validate_environment
58
+ unless File.exist?("Gemfile")
59
+ puts "No Gemfile found. Please run this script from the root of your Rails application."
60
+ exit 1
61
+ end
62
+
63
+ unless File.exist?(config_path)
64
+ puts "No #{config_path} found. Please run 'rails generate discharger:install' first."
65
+ exit 1
66
+ end
67
+ end
68
+
69
+ def print_header
70
+ puts "== Running Discharger setup =="
71
+ puts "Configuration loaded from: #{config_path}"
72
+ end
73
+
74
+ def print_footer
75
+ puts "\n== Setup completed successfully! =="
76
+ end
77
+
78
+ def load_bundler
79
+ require "bundler/setup"
80
+ end
81
+
82
+ def run_prerequisites
83
+ puts "\n== Setting up prerequisites =="
84
+ require_relative "prerequisites"
85
+ SetupRunner::PrerequisitesLoader.run(config_path)
86
+ end
87
+
88
+ def load_rails
89
+ # Load Rails from the standard location
90
+ rails_config = File.join(app_root, "config", "application.rb")
91
+ if File.exist?(rails_config)
92
+ require rails_config
93
+ Rails.application.initialize!
94
+ else
95
+ puts "Warning: config/application.rb not found. Skipping Rails initialization."
96
+ end
97
+ end
98
+
99
+ def run_setup_commands
100
+ require_relative "../discharger"
101
+ SetupRunner.run(config_path)
102
+ end
103
+ end
104
+ end
@@ -156,7 +156,12 @@ module Discharger
156
156
 
157
157
  def system_quiet(*args)
158
158
  require "open3"
159
- stdout, _stderr, status = Open3.capture3(*args)
159
+ # If it's a single string with shell metacharacters, run it through bash
160
+ if args.size == 1 && args.first.is_a?(String) && args.first.match?(/[|><&;]/)
161
+ stdout, _stderr, status = Open3.capture3("bash", "-c", args.first)
162
+ else
163
+ stdout, _stderr, status = Open3.capture3(*args)
164
+ end
160
165
  logger&.debug("Quietly executed #{args.join(" ")} - success: #{status.success?}")
161
166
  logger&.debug("Output: #{stdout}") if stdout && !stdout.empty? && logger
162
167
  status.success?
@@ -172,10 +177,20 @@ module Discharger
172
177
  end
173
178
 
174
179
  def proceed_with(task)
175
- return yield unless $stdin.tty?
180
+ # Auto-proceed in non-interactive environments (unless forced interactive for testing)
181
+ unless ENV["DISCHARGER_FORCE_INTERACTIVE"]
182
+ if ENV["CI"] || !$stdin.tty? || ENV["QUIET_SETUP"]
183
+ yield
184
+ return
185
+ end
186
+ end
176
187
 
177
- puts "Proceed with #{task}?\n ===> Type Y to proceed\nOtherwise hit any key to ignore."
178
- if gets&.chomp == "Y"
188
+ unless ENV["DISABLE_OUTPUT"]
189
+ puts "Proceed with #{task}?\n ===> Type Y to proceed\nOtherwise hit any key to ignore."
190
+ end
191
+
192
+ input = gets
193
+ if input&.chomp == "Y"
179
194
  yield
180
195
  end
181
196
  end
@@ -11,17 +11,18 @@ module Discharger
11
11
  # Drop and recreate development database
12
12
  terminate_database_connections
13
13
  with_spinner("Dropping and recreating development database") do
14
- _stdout, stderr, status = Open3.capture3("bash", "-c", "bin/rails db:drop db:create > /dev/null 2>&1")
14
+ stdout, stderr, status = Open3.capture3(db_env, "bin/rails", "db:drop", "db:create")
15
15
  if status.success?
16
16
  {success: true}
17
17
  else
18
- {success: false, error: "Failed to drop/create database: #{stderr}"}
18
+ error_msg = stderr.empty? ? stdout : stderr
19
+ {success: false, error: "Failed to drop/create database: #{error_msg}"}
19
20
  end
20
21
  end
21
22
 
22
23
  # Load schema and run migrations
23
24
  with_spinner("Loading database schema and running migrations") do
24
- _stdout, stderr, status = Open3.capture3("bin/rails db:schema:load db:migrate")
25
+ _stdout, stderr, status = Open3.capture3(db_env, "bin/rails", "db:schema:load", "db:migrate")
25
26
  if status.success?
26
27
  {success: true}
27
28
  else
@@ -30,9 +31,9 @@ module Discharger
30
31
  end
31
32
 
32
33
  # Seed the database
33
- env = (config.respond_to?(:seed_env) && config.seed_env) ? {"SEED_DEV_ENV" => "true"} : {}
34
+ seed_env = db_env.merge((config.respond_to?(:seed_env) && config.seed_env) ? {"SEED_DEV_ENV" => "true"} : {})
34
35
  with_spinner("Seeding the database") do
35
- _stdout, stderr, status = Open3.capture3(env, "bin/rails db:seed")
36
+ _stdout, stderr, status = Open3.capture3(seed_env, "bin/rails", "db:seed")
36
37
  if status.success?
37
38
  {success: true}
38
39
  else
@@ -43,11 +44,13 @@ module Discharger
43
44
  # Setup test database
44
45
  terminate_database_connections("test")
45
46
  with_spinner("Setting up test database") do
46
- _stdout, stderr, status = Open3.capture3({"RAILS_ENV" => "test"}, "bash", "-c", "bin/rails db:drop db:create db:schema:load > /dev/null 2>&1")
47
+ test_env = db_env.merge({"RAILS_ENV" => "test"})
48
+ stdout, stderr, status = Open3.capture3(test_env, "bin/rails", "db:drop", "db:create", "db:schema:load")
47
49
  if status.success?
48
50
  {success: true}
49
51
  else
50
- {success: false, error: "Failed to setup test database: #{stderr}"}
52
+ error_msg = stderr.empty? ? stdout : stderr
53
+ {success: false, error: "Failed to setup test database: #{error_msg}"}
51
54
  end
52
55
  end
53
56
 
@@ -72,6 +75,17 @@ module Discharger
72
75
 
73
76
  private
74
77
 
78
+ def db_env
79
+ # Use Docker PostgreSQL tools if bin/docker-pg directory exists
80
+ docker_pg_path = File.join(app_root, "bin", "docker-pg")
81
+ if File.directory?(docker_pg_path)
82
+ # Prepend docker-pg directory to PATH to use Docker's pg_dump/psql
83
+ {"PATH" => "#{docker_pg_path}:#{ENV["PATH"]}"}
84
+ else
85
+ {}
86
+ end
87
+ end
88
+
75
89
  def terminate_database_connections(rails_env = nil)
76
90
  # Use a Rails runner to terminate connections within the Rails context
77
91
  env_vars = rails_env ? {"RAILS_ENV" => rails_env} : {}
@@ -7,48 +7,98 @@ module Discharger
7
7
  module Commands
8
8
  class DockerCommand < BaseCommand
9
9
  def execute
10
- # Setup database container if configured
11
- if config.respond_to?(:database) && config.database
10
+ setup_database if database_configured?
11
+ setup_redis if redis_configured?
12
+ end
13
+
14
+ def can_execute?
15
+ # Only execute if Docker is available and containers are configured
16
+ docker_available? && (database_configured? || redis_configured?)
17
+ end
18
+
19
+ def description
20
+ "Setup Docker containers"
21
+ end
22
+
23
+ private
24
+
25
+ def setup_database
26
+ puts " → Checking database configuration..." unless ENV["QUIET_SETUP"]
27
+
28
+ case database_config.prefer_docker
29
+ when true
30
+ create_database_container_or_fail
31
+ when "prompt"
32
+ if native_postgresql_available? && wants_native_postgresql?
33
+ use_native_postgresql
34
+ else
35
+ create_database_container_or_fail
36
+ end
37
+ else
38
+ # Legacy default: prefer the native instance when one is available.
12
39
  if native_postgresql_available?
13
- log "Native PostgreSQL detected on port #{native_postgresql_port}, skipping Docker container setup"
14
- ENV["DB_PORT"] ||= native_postgresql_port.to_s
40
+ use_native_postgresql
15
41
  else
16
- ensure_docker_running
17
- setup_container(
18
- name: config.database.name || "db-app",
19
- port: config.database.port || 5432,
20
- image: "postgres:#{config.database.version || "14"}",
21
- env: {"POSTGRES_PASSWORD" => config.database.password || "postgres"},
22
- volume: "#{config.database.name || "db-app"}:/var/lib/postgresql/data",
23
- internal_port: 5432
24
- )
42
+ create_database_container
25
43
  end
26
44
  end
45
+ end
46
+
47
+ def setup_redis
48
+ setup_container(
49
+ name: redis_config.name || "redis-app",
50
+ port: redis_config.port || 6379,
51
+ image: "redis:#{redis_config.version || "latest"}",
52
+ internal_port: 6379
53
+ )
54
+ end
55
+
56
+ def use_native_postgresql
57
+ puts " → Native PostgreSQL detected on port #{native_postgresql_port}, skipping Docker setup" unless ENV["QUIET_SETUP"]
58
+ ENV["DB_PORT"] ||= native_postgresql_port.to_s
59
+ end
27
60
 
28
- # Setup Redis container if configured
29
- if config.respond_to?(:redis) && config.redis
30
- setup_container(
31
- name: config.redis.name || "redis-app",
32
- port: config.redis.port || 6379,
33
- image: "redis:#{config.redis.version || "latest"}",
34
- internal_port: 6379
35
- )
61
+ def create_database_container_or_fail
62
+ if native_postgresql_available?
63
+ raise "prefer_docker is set in the database config but native PostgreSQL is " \
64
+ "listening on port #{native_postgresql_port}. Stop the native instance " \
65
+ "(e.g., `brew services stop postgresql@15`) or remove prefer_docker."
36
66
  end
67
+ create_database_container
37
68
  end
38
69
 
39
- def can_execute?
40
- # Only execute if Docker is available and containers are configured
41
- docker_available? && (
42
- (config.respond_to?(:database) && config.database) ||
43
- (config.respond_to?(:redis) && config.redis)
70
+ def create_database_container
71
+ puts " → No native PostgreSQL found, setting up Docker container..." unless ENV["QUIET_SETUP"]
72
+ ensure_docker_running
73
+ setup_container(
74
+ name: database_config.name || "db-app",
75
+ port: database_config.port || 5432,
76
+ image: "postgres:#{database_config.version || "14"}",
77
+ env: {"POSTGRES_PASSWORD" => database_config.password || "postgres"},
78
+ volume: "#{database_config.name || "db-app"}:/var/lib/postgresql/data",
79
+ internal_port: 5432
44
80
  )
45
81
  end
46
82
 
47
- def description
48
- "Setup Docker containers"
83
+ # Ask the developer whether to use the detected native PostgreSQL or set up
84
+ # the configured Docker container instead. Falls back to native (the legacy
85
+ # behavior) when stdin is not a TTY, in CI, or when QUIET_SETUP is set,
86
+ # so non-interactive runs do not hang waiting for input.
87
+ def wants_native_postgresql?
88
+ unless interactive_prompt_available?
89
+ puts " → Non-interactive shell; defaulting to native PostgreSQL on port #{native_postgresql_port}" unless ENV["QUIET_SETUP"]
90
+ return true
91
+ end
92
+
93
+ puts "Native PostgreSQL detected on port #{native_postgresql_port}. Use the Docker container anyway?\n ===> Type Y to set up the Docker container\nOtherwise hit any key to use the native PostgreSQL."
94
+ $stdin.gets&.chomp != "Y"
49
95
  end
50
96
 
51
- private
97
+ def interactive_prompt_available?
98
+ return true if ENV["DISCHARGER_FORCE_INTERACTIVE"]
99
+ return false if ENV["CI"] || ENV["QUIET_SETUP"]
100
+ $stdin.tty?
101
+ end
52
102
 
53
103
  def setup_container(name:, port:, image:, internal_port:, env: {}, volume: nil)
54
104
  log "Checking #{name} container"
@@ -151,12 +201,41 @@ module Discharger
151
201
  true
152
202
  end
153
203
 
204
+ def database_configured?
205
+ config.respond_to?(:database) && config.database&.name
206
+ end
207
+
208
+ def redis_configured?
209
+ config.respond_to?(:redis) && config.redis&.name
210
+ end
211
+
212
+ def database_config
213
+ config.database
214
+ end
215
+
216
+ def redis_config
217
+ config.redis
218
+ end
219
+
154
220
  def native_postgresql_available?
155
- configured_port = (config.database&.port || 5432).to_i
221
+ # If a specific port is configured, ONLY check that port
222
+ # We should not use a native PostgreSQL on a different port
223
+ configured_port = config.database&.port
224
+ if configured_port
225
+ if postgresql_running_on_port?(configured_port)
226
+ @native_pg_port = configured_port
227
+ return true
228
+ end
229
+ # Configured port specified but PostgreSQL not running on it
230
+ return false
231
+ end
156
232
 
157
- if postgresql_running_on_port?(configured_port)
158
- @native_pg_port = configured_port
159
- return true
233
+ # No specific port configured, check common PostgreSQL ports
234
+ [5432, 5433].each do |port|
235
+ if postgresql_running_on_port?(port)
236
+ @native_pg_port = port
237
+ return true
238
+ end
160
239
  end
161
240
  false
162
241
  end
@@ -5,15 +5,16 @@ require "yaml"
5
5
  module Discharger
6
6
  module SetupRunner
7
7
  class Configuration
8
- attr_accessor :app_name, :database, :redis, :services, :steps, :custom_steps
8
+ attr_accessor :app_name, :database, :redis, :services, :steps, :custom_steps, :pre_steps
9
9
 
10
10
  def initialize
11
11
  @app_name = "Application"
12
12
  @database = DatabaseConfig.new
13
- @redis = RedisConfig.new
13
+ @redis = nil
14
14
  @services = []
15
15
  @steps = []
16
16
  @custom_steps = []
17
+ @pre_steps = []
17
18
  end
18
19
 
19
20
  def self.from_file(path)
@@ -25,23 +26,25 @@ module Discharger
25
26
 
26
27
  config.app_name = yaml["app_name"] if yaml["app_name"]
27
28
  config.database.from_hash(yaml["database"]) if yaml["database"]
28
- config.redis.from_hash(yaml["redis"]) if yaml["redis"]
29
+ config.redis = RedisConfig.new.tap { |r| r.from_hash(yaml["redis"]) } if yaml["redis"]
29
30
  config.services = yaml["services"] || []
30
31
  config.steps = yaml["steps"] || []
31
32
  config.custom_steps = yaml["custom_steps"] || []
33
+ config.pre_steps = yaml["pre_steps"] || []
32
34
 
33
35
  config
34
36
  end
35
37
  end
36
38
 
37
39
  class DatabaseConfig
38
- attr_accessor :port, :name, :version, :password
40
+ attr_accessor :port, :name, :version, :password, :prefer_docker
39
41
 
40
42
  def initialize
41
43
  @port = 5432
42
44
  @name = "db-app"
43
45
  @version = "14"
44
46
  @password = "postgres"
47
+ @prefer_docker = nil
45
48
  end
46
49
 
47
50
  def from_hash(hash)
@@ -49,6 +52,7 @@ module Discharger
49
52
  @name = hash["name"] if hash["name"]
50
53
  @version = hash["version"] if hash["version"]
51
54
  @password = hash["password"] if hash["password"]
55
+ @prefer_docker = hash["prefer_docker"] if hash.key?("prefer_docker")
52
56
  end
53
57
  end
54
58
 
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rbconfig"
4
+
5
+ module Discharger
6
+ module SetupRunner
7
+ module PreCommands
8
+ # Base class for pre-Rails commands.
9
+ # These commands run BEFORE bundler/Rails loads, so they must be
10
+ # pure Ruby with no gem dependencies.
11
+ class BasePreCommand
12
+ attr_reader :config
13
+
14
+ def initialize(config = {})
15
+ @config = config
16
+ end
17
+
18
+ def execute
19
+ raise NotImplementedError, "#{self.class} must implement #execute"
20
+ end
21
+
22
+ def description
23
+ self.class.name.split("::").last.gsub(/PreCommand$/, "").gsub(/([a-z])([A-Z])/, '\1 \2')
24
+ end
25
+
26
+ protected
27
+
28
+ def log(message)
29
+ puts " #{message}"
30
+ end
31
+
32
+ def platform_darwin?
33
+ RbConfig::CONFIG["host_os"] =~ /darwin/
34
+ end
35
+
36
+ def platform_linux?
37
+ RbConfig::CONFIG["host_os"] =~ /linux/
38
+ end
39
+
40
+ def command_exists?(command)
41
+ system("which #{command} > /dev/null 2>&1")
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_pre_command"
4
+
5
+ module Discharger
6
+ module SetupRunner
7
+ module PreCommands
8
+ # Ensures Homebrew/Linuxbrew is installed.
9
+ # This must run before `brew bundle` can install Brewfile dependencies.
10
+ class HomebrewPreCommand < BasePreCommand
11
+ HOMEBREW_INSTALL_URL = "https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh"
12
+ LINUXBREW_PATH = "/home/linuxbrew/.linuxbrew/bin"
13
+
14
+ def execute
15
+ if command_exists?("brew")
16
+ log "Homebrew found at: #{`which brew`.strip}"
17
+ return true
18
+ end
19
+
20
+ log "Homebrew not found. Installing..."
21
+
22
+ if platform_darwin?
23
+ install_homebrew_macos
24
+ elsif platform_linux?
25
+ install_homebrew_linux
26
+ else
27
+ log "WARNING: Unsupported platform for automatic Homebrew installation"
28
+ log "Please install Homebrew manually: https://brew.sh"
29
+ return false
30
+ end
31
+
32
+ verify_installation
33
+ end
34
+
35
+ def description
36
+ "Ensure Homebrew is installed"
37
+ end
38
+
39
+ private
40
+
41
+ def install_homebrew_macos
42
+ log "Installing Homebrew for macOS..."
43
+ system(%(/bin/bash -c "$(curl -fsSL #{HOMEBREW_INSTALL_URL})"))
44
+ end
45
+
46
+ def install_homebrew_linux
47
+ log "Installing Linuxbrew for Linux..."
48
+ system(%(/bin/bash -c "$(curl -fsSL #{HOMEBREW_INSTALL_URL})"))
49
+
50
+ # Add Linuxbrew to PATH for current session
51
+ if File.exist?(LINUXBREW_PATH)
52
+ ENV["PATH"] = "#{LINUXBREW_PATH}:#{ENV["PATH"]}"
53
+ log "Added Linuxbrew to PATH"
54
+ end
55
+ end
56
+
57
+ def verify_installation
58
+ unless command_exists?("brew")
59
+ log "ERROR: Homebrew installation failed or not in PATH"
60
+ log "Please install Homebrew manually and ensure it's in your PATH"
61
+ return false
62
+ end
63
+ log "Homebrew installed successfully"
64
+ true
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_pre_command"
4
+
5
+ module Discharger
6
+ module SetupRunner
7
+ module PreCommands
8
+ # Ensures PostgreSQL client tools (pg_dump, psql) are installed.
9
+ # These tools are needed for database operations like db:structure:dump.
10
+ class PostgresqlToolsPreCommand < BasePreCommand
11
+ def execute
12
+ if command_exists?("pg_dump")
13
+ log "PostgreSQL client tools found"
14
+ return true
15
+ end
16
+
17
+ log "PostgreSQL client tools (pg_dump) not found."
18
+ pg_version = config.dig("database", "version") || "14"
19
+
20
+ if platform_darwin?
21
+ install_via_homebrew(pg_version)
22
+ elsif platform_linux?
23
+ install_via_apt(pg_version)
24
+ else
25
+ log "WARNING: Unsupported platform for PostgreSQL client tools installation"
26
+ false
27
+ end
28
+ end
29
+
30
+ def description
31
+ "Ensure PostgreSQL client tools are installed"
32
+ end
33
+
34
+ private
35
+
36
+ def install_via_homebrew(version)
37
+ log "Installing PostgreSQL #{version} client tools via Homebrew..."
38
+ if system("brew install postgresql@#{version}")
39
+ log "PostgreSQL #{version} client tools installed successfully"
40
+ true
41
+ else
42
+ log "WARNING: Failed to install PostgreSQL client tools"
43
+ log "Please install manually: brew install postgresql@#{version}"
44
+ false
45
+ end
46
+ end
47
+
48
+ def install_via_apt(version)
49
+ unless command_exists?("apt-get")
50
+ log "WARNING: apt-get not found"
51
+ log "Please install postgresql-client-#{version} using your system's package manager"
52
+ return false
53
+ end
54
+
55
+ log "Installing PostgreSQL #{version} client tools via apt..."
56
+ if system("sudo apt-get install -y postgresql-client-#{version}")
57
+ # Set up alternatives for the installed version
58
+ priority = version.to_i * 10
59
+ system("sudo update-alternatives --install /usr/bin/pg_dump pg_dump /usr/lib/postgresql/#{version}/bin/pg_dump #{priority}")
60
+ system("sudo update-alternatives --install /usr/bin/psql psql /usr/lib/postgresql/#{version}/bin/psql #{priority}")
61
+ log "PostgreSQL #{version} client tools installed successfully"
62
+ true
63
+ else
64
+ log "WARNING: Failed to install PostgreSQL client tools"
65
+ log "Please install manually: sudo apt-get install postgresql-client-#{version}"
66
+ false
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "homebrew_pre_command"
4
+ require_relative "postgresql_tools_pre_command"
5
+
6
+ module Discharger
7
+ module SetupRunner
8
+ module PreCommands
9
+ # Registry for pre-Rails commands.
10
+ # Maps command names (from setup.yml) to command classes.
11
+ class PreCommandRegistry
12
+ BUILT_IN_COMMANDS = {
13
+ "homebrew" => HomebrewPreCommand,
14
+ "postgresql_tools" => PostgresqlToolsPreCommand
15
+ }.freeze
16
+
17
+ class << self
18
+ def get(name)
19
+ custom_commands[name.to_s] || BUILT_IN_COMMANDS[name.to_s]
20
+ end
21
+
22
+ def register(name, command_class)
23
+ custom_commands[name.to_s] = command_class
24
+ end
25
+
26
+ def names
27
+ (BUILT_IN_COMMANDS.keys + custom_commands.keys).uniq
28
+ end
29
+
30
+ private
31
+
32
+ def custom_commands
33
+ @custom_commands ||= {}
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require_relative "pre_commands/pre_command_registry"
5
+
6
+ module Discharger
7
+ module SetupRunner
8
+ # PrerequisitesLoader handles setup tasks that must run BEFORE Rails loads.
9
+ # This includes setting environment variables, checking for system dependencies,
10
+ # and installing tools like Homebrew that are needed before bundler/Rails can run.
11
+ #
12
+ # Usage in bin/setup:
13
+ # require "discharger/prerequisites"
14
+ # Discharger::SetupRunner::PrerequisitesLoader.run("config/setup.yml")
15
+ #
16
+ class PrerequisitesLoader
17
+ attr_reader :config_path, :config
18
+
19
+ def initialize(config_path)
20
+ @config_path = config_path
21
+ @config = load_config
22
+ end
23
+
24
+ def self.run(config_path)
25
+ new(config_path).run
26
+ end
27
+
28
+ def run
29
+ return false unless config
30
+
31
+ set_database_environment
32
+ run_pre_steps
33
+ true
34
+ end
35
+
36
+ private
37
+
38
+ def load_config
39
+ return nil unless File.exist?(config_path)
40
+
41
+ YAML.load_file(config_path)
42
+ rescue => e
43
+ puts "Warning: Could not load #{config_path}: #{e.message}"
44
+ nil
45
+ end
46
+
47
+ def set_database_environment
48
+ return unless config["database"]
49
+
50
+ db_config = config["database"]
51
+ set_db_port(db_config)
52
+ set_db_name(db_config)
53
+ end
54
+
55
+ def set_db_port(db_config)
56
+ if db_config["port"] && !ENV["DB_PORT"]
57
+ ENV["DB_PORT"] = db_config["port"].to_s
58
+ ENV["PGPORT"] = db_config["port"].to_s
59
+ puts " Setting DB_PORT=#{db_config["port"]} from config/setup.yml"
60
+ elsif ENV["DB_PORT"]
61
+ warn_if_mismatch("DB_PORT", ENV["DB_PORT"], db_config["port"].to_s)
62
+ ENV["PGPORT"] = ENV["DB_PORT"]
63
+ end
64
+ end
65
+
66
+ def set_db_name(db_config)
67
+ if db_config["name"] && !ENV["DB_NAME"]
68
+ container_name = db_config["name"].to_s
69
+ db_name = container_name.sub(/^db-/, "")
70
+ ENV["DB_NAME"] = db_name
71
+ puts " Setting DB_NAME=#{db_name} from config/setup.yml (container: #{container_name})"
72
+ elsif ENV["DB_NAME"]
73
+ container_name = db_config["name"].to_s
74
+ expected_db_name = container_name.sub(/^db-/, "")
75
+ warn_if_mismatch("DB_NAME", ENV["DB_NAME"], expected_db_name)
76
+ end
77
+ end
78
+
79
+ def warn_if_mismatch(var_name, env_value, config_value)
80
+ return unless config_value && env_value != config_value
81
+ puts "\n⚠️ WARNING: #{var_name} environment variable (#{env_value}) differs from config/setup.yml (#{config_value})"
82
+ puts " Using environment variable value. To use config/setup.yml value, unset #{var_name}."
83
+ end
84
+
85
+ def run_pre_steps
86
+ pre_steps = config["pre_steps"] || []
87
+ pre_steps.each do |step|
88
+ run_pre_step(step)
89
+ end
90
+ end
91
+
92
+ def run_pre_step(step)
93
+ case step
94
+ when String
95
+ run_built_in_pre_step(step)
96
+ when Hash
97
+ run_custom_pre_step(step)
98
+ end
99
+ end
100
+
101
+ def run_built_in_pre_step(name)
102
+ command_class = PreCommands::PreCommandRegistry.get(name)
103
+ unless command_class
104
+ puts " WARNING: Unknown pre-step '#{name}'"
105
+ return
106
+ end
107
+
108
+ command = command_class.new(config)
109
+ puts " #{command.description}..."
110
+ command.execute
111
+ end
112
+
113
+ def run_custom_pre_step(step)
114
+ description = step["description"] || step["command"]
115
+ command = step["command"]
116
+ condition = step["condition"]
117
+
118
+ if condition && !evaluate_condition(condition)
119
+ puts " Skipping: #{description} (condition not met)"
120
+ return
121
+ end
122
+
123
+ puts " Running: #{description}"
124
+ system(command)
125
+ end
126
+
127
+ def evaluate_condition(condition)
128
+ case condition
129
+ when /^ENV\['(\w+)'\]$/
130
+ !ENV[$1].nil? && !ENV[$1].empty?
131
+ when /^!ENV\['(\w+)'\]$/
132
+ ENV[$1].nil? || ENV[$1].empty?
133
+ when /^File\.exist\?\(['"](.+)['"]\)$/
134
+ File.exist?($1)
135
+ when /^!File\.exist\?\(['"](.+)['"]\)$/
136
+ !File.exist?($1)
137
+ else
138
+ false
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
@@ -5,6 +5,7 @@ require_relative "setup_runner/configuration"
5
5
  require_relative "setup_runner/command_registry"
6
6
  require_relative "setup_runner/command_factory"
7
7
  require_relative "setup_runner/runner"
8
+ require_relative "setup_runner/prerequisites_loader"
8
9
 
9
10
  module Discharger
10
11
  module SetupRunner
@@ -193,6 +193,16 @@ module Discharger
193
193
  pr.empty? ? nil : pr
194
194
  end
195
195
 
196
+ def pr_already_merged?(pr_ref)
197
+ stdout, _, status = Open3.capture3(
198
+ "gh", "pr", "view", pr_ref.to_s,
199
+ "--json", "state",
200
+ "--jq", ".state"
201
+ )
202
+ return false unless status.success?
203
+ stdout.strip == "MERGED"
204
+ end
205
+
196
206
  def define
197
207
  require "slack-ruby-client"
198
208
  Slack.configure do |config|
@@ -262,9 +272,13 @@ module Discharger
262
272
  end
263
273
  end
264
274
 
265
- syscall(
266
- ["gh pr merge #{pr_ref} --merge"]
267
- )
275
+ if pr_already_merged?(pr_ref)
276
+ sysecho "PR #{pr_ref} is already merged. Continuing..."
277
+ else
278
+ syscall(
279
+ ["gh pr merge #{pr_ref} --merge"]
280
+ )
281
+ end
268
282
 
269
283
  continue = syscall(
270
284
  ["git fetch origin #{production_branch}:#{production_branch}"],
@@ -1,3 +1,3 @@
1
1
  module Discharger
2
- VERSION = "0.2.30"
2
+ VERSION = "0.3.1"
3
3
  end
@@ -1,36 +1,30 @@
1
1
  #!/usr/bin/env ruby
2
- require "fileutils"
3
- require "pathname"
4
- require "bundler/setup"
5
2
 
6
3
  # Path to the application root.
7
4
  APP_ROOT = File.expand_path("..", __dir__)
8
5
 
9
- FileUtils.chdir APP_ROOT do
6
+ Dir.chdir(APP_ROOT) do
10
7
  # This script uses Discharger to set up your development environment automatically.
11
8
  # All setup steps are configured in config/setup.yml
12
9
  # This script is idempotent, so you can run it at any time and get an expectable outcome.
13
10
 
14
- unless File.exist?("Gemfile")
15
- puts "No Gemfile found. Please run this script from the root of your Rails application."
16
- exit 1
11
+ # Find discharger gem path from Gemfile without loading bundler
12
+ # (loading bundler early can cause gem version conflicts)
13
+ gemfile = File.read("Gemfile")
14
+ if (match = gemfile.match(/gem\s+["']discharger["'].*path:\s*["']([^"']+)["']/))
15
+ discharger_path = File.expand_path(match[1], APP_ROOT)
16
+ $LOAD_PATH.unshift(File.join(discharger_path, "lib"))
17
+ else
18
+ # Fall back to installed gem
19
+ begin
20
+ gem "discharger"
21
+ rescue Gem::MissingSpecError
22
+ puts "Could not find discharger gem."
23
+ puts "If using a custom bundle path, try: bundle exec bin/setup"
24
+ exit 1
25
+ end
17
26
  end
18
27
 
19
- unless File.exist?("config/setup.yml")
20
- puts "No config/setup.yml found. Please run 'rails generate discharger:install' first."
21
- exit 1
22
- end
23
-
24
- puts "== Running Discharger setup =="
25
- puts "Configuration loaded from: config/setup.yml"
26
-
27
- # Load Rails environment first, then discharger
28
- require_relative "../config/application"
29
- Rails.application.initialize!
30
-
31
- # Load the discharger gem and run the setup
32
- require "discharger"
33
- Discharger::SetupRunner.run("config/setup.yml")
34
-
35
- puts "\n== Setup completed successfully! =="
28
+ require "discharger/setup"
29
+ Discharger::Setup.run("config/setup.yml")
36
30
  end
@@ -8,7 +8,7 @@ app_name: "YourAppName"
8
8
  # Database configuration
9
9
  database:
10
10
  port: 5432
11
- name: "db-your-app"
11
+ name: "db-your-app" # Docker container name. Rails will use "your-app" for database names
12
12
  version: "14"
13
13
  password: "postgres"
14
14
 
@@ -18,6 +18,23 @@ redis:
18
18
  name: "redis-your-app"
19
19
  version: "latest"
20
20
 
21
+ # Pre-Rails steps (run BEFORE Rails loads)
22
+ # These are for system dependencies and environment setup that must happen
23
+ # before bundler/Rails can initialize. Use sparingly.
24
+ #
25
+ # Built-in pre_steps:
26
+ # - homebrew # Installs Homebrew if not present
27
+ # - postgresql_tools # Installs PostgreSQL client tools (pg_dump, psql)
28
+ #
29
+ # Custom pre_steps (run shell commands):
30
+ # - description: "Description of step"
31
+ # command: "shell command to run"
32
+ # condition: "ENV['VAR']" # Optional: only run if condition is true
33
+ #
34
+ pre_steps: []
35
+ # - homebrew
36
+ # - postgresql_tools
37
+
21
38
  # Built-in commands to run (empty array runs all available commands)
22
39
  # Available commands: brew, asdf, git, bundler, yarn, config, docker, pg_tools, env, database
23
40
  steps:
@@ -32,30 +49,22 @@ steps:
32
49
  - env
33
50
  - database
34
51
 
35
- # Custom commands for application-specific setup
52
+ # Custom commands for application-specific setup (run AFTER Rails loads)
36
53
  custom_steps:
37
54
  - description: "Seed application data"
38
55
  command: "bin/rails db:seed"
39
-
40
- - description: "Setup application-specific configurations"
41
- command: "bin/rails runner 'YourAppSetup.new.configure'"
42
- condition: "defined?(YourAppSetup)"
43
-
44
- - description: "Import application data"
45
- command: "bin/rails db:seed:import"
46
- condition: "File.exist?('db/seeds/import.rb')"
47
-
56
+
48
57
  # Example: Conditional setup based on environment variable
49
58
  # - description: "Seed legacy data"
50
59
  # command: "LEGACY_DATA=true bin/rails db:seed"
51
60
  # condition: "ENV['LEGACY_DATA'] == 'true'"
52
-
61
+
53
62
  # Example: Setup external services
54
63
  # - description: "Setup Elasticsearch"
55
64
  # command: "bin/rails search:setup"
56
65
  # condition: "defined?(Elasticsearch)"
57
-
66
+
58
67
  # Example: Setup background job processing
59
68
  # - description: "Setup Sidekiq"
60
69
  # command: "bundle exec sidekiq -d"
61
- # condition: "defined?(Sidekiq)"
70
+ # condition: "defined?(Sidekiq)"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: discharger
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.30
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jim Gay
@@ -106,7 +106,9 @@ files:
106
106
  - README.md
107
107
  - Rakefile
108
108
  - lib/discharger.rb
109
+ - lib/discharger/prerequisites.rb
109
110
  - lib/discharger/railtie.rb
111
+ - lib/discharger/setup.rb
110
112
  - lib/discharger/setup_runner.rb
111
113
  - lib/discharger/setup_runner/command_factory.rb
112
114
  - lib/discharger/setup_runner/command_registry.rb
@@ -124,6 +126,11 @@ files:
124
126
  - lib/discharger/setup_runner/commands/yarn_command.rb
125
127
  - lib/discharger/setup_runner/condition_evaluator.rb
126
128
  - lib/discharger/setup_runner/configuration.rb
129
+ - lib/discharger/setup_runner/pre_commands/base_pre_command.rb
130
+ - lib/discharger/setup_runner/pre_commands/homebrew_pre_command.rb
131
+ - lib/discharger/setup_runner/pre_commands/postgresql_tools_pre_command.rb
132
+ - lib/discharger/setup_runner/pre_commands/pre_command_registry.rb
133
+ - lib/discharger/setup_runner/prerequisites_loader.rb
127
134
  - lib/discharger/setup_runner/runner.rb
128
135
  - lib/discharger/setup_runner/version.rb
129
136
  - lib/discharger/task.rb