discharger 0.2.9 → 0.2.11
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 +4 -4
- data/README.md +10 -0
- data/Rakefile +3 -1
- data/lib/discharger/setup_runner/command_factory.rb +56 -0
- data/lib/discharger/setup_runner/command_registry.rb +62 -0
- data/lib/discharger/setup_runner/commands/asdf_command.rb +61 -0
- data/lib/discharger/setup_runner/commands/base_command.rb +185 -0
- data/lib/discharger/setup_runner/commands/brew_command.rb +26 -0
- data/lib/discharger/setup_runner/commands/bundler_command.rb +25 -0
- data/lib/discharger/setup_runner/commands/config_command.rb +51 -0
- data/lib/discharger/setup_runner/commands/custom_command.rb +41 -0
- data/lib/discharger/setup_runner/commands/database_command.rb +111 -0
- data/lib/discharger/setup_runner/commands/docker_command.rb +100 -0
- data/lib/discharger/setup_runner/commands/env_command.rb +42 -0
- data/lib/discharger/setup_runner/commands/git_command.rb +45 -0
- data/lib/discharger/setup_runner/commands/yarn_command.rb +45 -0
- data/lib/discharger/setup_runner/condition_evaluator.rb +130 -0
- data/lib/discharger/setup_runner/configuration.rb +71 -0
- data/lib/discharger/setup_runner/runner.rb +111 -0
- data/lib/discharger/setup_runner/version.rb +7 -0
- data/lib/discharger/setup_runner.rb +55 -0
- data/lib/discharger/task.rb +1 -5
- data/lib/discharger/version.rb +1 -1
- data/lib/discharger.rb +1 -0
- data/lib/generators/discharger/install/install_generator.rb +14 -0
- data/lib/generators/discharger/install/templates/setup +36 -0
- data/lib/generators/discharger/install/templates/setup.yml +60 -0
- metadata +49 -5
- data/lib/discharger/procedure.rb +0 -117
- data/lib/discharger/railway.rb +0 -111
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 23bf801a204e17e5f37876fba770bc9610814b629ee570924c04414547bc801b
|
4
|
+
data.tar.gz: 594b4fceeb396a77ada63a549eee831d72ea6b1c942ff1a1ac813cea877e6c56
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ec6dadb8198acd18144256694bcfdd0cd7c58275e6ad8f2ed6d9bc374633db99e6bf50278fb137a7675af74759e9ecc885dc8450852e2561655b0d5ba6e2cf75
|
7
|
+
data.tar.gz: fcf463c7d4e9721b4a94a470a6ee0b75673888cf0f0c9e87fa998f208e0a3a1a88d8338de4add5ae1205f1a643e0ae49b59f814bf28854c5cd78bb5a09b96b0e
|
data/README.md
CHANGED
@@ -58,6 +58,16 @@ $ gem install discharger
|
|
58
58
|
|
59
59
|
This gem is managed with [Reissue](https://github.com/SOFware/reissue).
|
60
60
|
|
61
|
+
### Releasing
|
62
|
+
|
63
|
+
Releases are automated via GitHub Actions:
|
64
|
+
|
65
|
+
1. Go to Actions → "Prepare Release" → Run workflow
|
66
|
+
2. Select version type (major, minor, patch, or custom)
|
67
|
+
3. Review the created PR with version bumps and changelog updates
|
68
|
+
4. Add the `approved-release` label and merge
|
69
|
+
5. The gem will be automatically published to RubyGems.org
|
70
|
+
|
61
71
|
Bug reports and pull requests are welcome on GitHub.
|
62
72
|
|
63
73
|
## License
|
data/Rakefile
CHANGED
@@ -7,7 +7,9 @@ require "reissue/gem"
|
|
7
7
|
|
8
8
|
Reissue::Task.create :reissue do |task|
|
9
9
|
task.version_file = "lib/discharger/version.rb"
|
10
|
-
task.commit =
|
10
|
+
task.commit = !ENV["GITHUB_ACTIONS"]
|
11
|
+
task.commit_finalize = !ENV["GITHUB_ACTIONS"]
|
12
|
+
task.push_finalize = :branch
|
11
13
|
end
|
12
14
|
|
13
15
|
Rake::TestTask.new(:test) do |t|
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "command_registry"
|
4
|
+
|
5
|
+
module Discharger
|
6
|
+
module SetupRunner
|
7
|
+
class CommandFactory
|
8
|
+
attr_reader :config, :app_root, :logger
|
9
|
+
|
10
|
+
def initialize(config, app_root, logger)
|
11
|
+
@config = config
|
12
|
+
@app_root = app_root
|
13
|
+
@logger = logger
|
14
|
+
end
|
15
|
+
|
16
|
+
def create_command(name)
|
17
|
+
command_class = CommandRegistry.get(name)
|
18
|
+
return nil unless command_class
|
19
|
+
|
20
|
+
command_class.new(config, app_root, logger)
|
21
|
+
rescue => e
|
22
|
+
logger&.warn "Failed to create command #{name}: #{e.message}"
|
23
|
+
nil
|
24
|
+
end
|
25
|
+
|
26
|
+
def create_all_commands
|
27
|
+
commands = []
|
28
|
+
|
29
|
+
# Create built-in commands from steps
|
30
|
+
if config.steps.any?
|
31
|
+
config.steps.each do |step|
|
32
|
+
command = create_command(step)
|
33
|
+
commands << command if command
|
34
|
+
end
|
35
|
+
else
|
36
|
+
# If no steps specified, create all registered commands
|
37
|
+
CommandRegistry.names.each do |name|
|
38
|
+
command = create_command(name)
|
39
|
+
commands << command if command
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Create custom commands
|
44
|
+
if config.respond_to?(:custom_steps) && config.custom_steps.any?
|
45
|
+
require_relative "commands/custom_command"
|
46
|
+
config.custom_steps.each do |step_config|
|
47
|
+
command = Commands::CustomCommand.new(config, app_root, logger, step_config)
|
48
|
+
commands << command
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
commands
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Discharger
|
4
|
+
module SetupRunner
|
5
|
+
class CommandRegistry
|
6
|
+
class << self
|
7
|
+
def register(name, command_class)
|
8
|
+
commands[name.to_s] = command_class
|
9
|
+
end
|
10
|
+
|
11
|
+
def get(name)
|
12
|
+
commands[name.to_s]
|
13
|
+
end
|
14
|
+
|
15
|
+
def all
|
16
|
+
commands.values
|
17
|
+
end
|
18
|
+
|
19
|
+
def names
|
20
|
+
commands.keys
|
21
|
+
end
|
22
|
+
|
23
|
+
def clear
|
24
|
+
commands.clear
|
25
|
+
end
|
26
|
+
|
27
|
+
def load_commands
|
28
|
+
# Load base command first
|
29
|
+
require_relative "commands/base_command"
|
30
|
+
|
31
|
+
# Load all command files from the commands directory
|
32
|
+
commands_dir = File.expand_path("commands", __dir__)
|
33
|
+
Dir.glob(File.join(commands_dir, "*_command.rb")).each do |file|
|
34
|
+
require file
|
35
|
+
end
|
36
|
+
|
37
|
+
# Auto-register commands based on naming convention
|
38
|
+
Commands.constants.each do |const_name|
|
39
|
+
next unless const_name.to_s.end_with?("Command")
|
40
|
+
|
41
|
+
command_class = Commands.const_get(const_name)
|
42
|
+
next unless command_class < Commands::BaseCommand
|
43
|
+
next if command_class == Commands::BaseCommand
|
44
|
+
|
45
|
+
# Convert class name to command name (e.g., AsdfCommand -> asdf)
|
46
|
+
command_name = const_name.to_s.sub(/Command$/, "").gsub(/([A-Z])/, '_\1').downcase.sub(/^_/, "")
|
47
|
+
register(command_name, command_class)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def commands
|
54
|
+
@commands ||= {}
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Load and register all built-in commands
|
59
|
+
load_commands
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base_command"
|
4
|
+
|
5
|
+
module Discharger
|
6
|
+
module SetupRunner
|
7
|
+
module Commands
|
8
|
+
class AsdfCommand < BaseCommand
|
9
|
+
def execute
|
10
|
+
log "Install tool-versions dependencies via ASDF"
|
11
|
+
|
12
|
+
unless system_quiet("which asdf")
|
13
|
+
log "asdf not installed. Run `brew install asdf` if you want bin/setup to ensure versions are up-to-date"
|
14
|
+
return
|
15
|
+
end
|
16
|
+
|
17
|
+
tools_versions_file = File.join(app_root, ".tool-versions")
|
18
|
+
return log("No .tool-versions file found") unless File.exist?(tools_versions_file)
|
19
|
+
|
20
|
+
dependencies = File.read(tools_versions_file).split("\n")
|
21
|
+
installables = []
|
22
|
+
|
23
|
+
# Check for nodejs plugin
|
24
|
+
unless system_quiet("asdf plugin list | grep nodejs")
|
25
|
+
node_deps = dependencies.select { |item| item.match?(/node/) }
|
26
|
+
if node_deps.any?
|
27
|
+
ask_to_install "asdf to manage Node JS" do
|
28
|
+
installables.concat(node_deps)
|
29
|
+
system! "asdf plugin add nodejs https://github.com/asdf-vm/asdf-nodejs.git"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Check for ruby plugin
|
35
|
+
unless system_quiet("asdf plugin list | grep ruby")
|
36
|
+
ruby_deps = dependencies.select { |item| item.match?(/ruby/) }
|
37
|
+
if ruby_deps.any?
|
38
|
+
ask_to_install "asdf to manage Ruby" do
|
39
|
+
installables.concat(ruby_deps)
|
40
|
+
system! "asdf plugin add ruby https://github.com/asdf-vm/asdf-ruby.git"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Install all versions
|
46
|
+
installables.each do |name_version|
|
47
|
+
system! "asdf install #{name_version}"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def can_execute?
|
52
|
+
File.exist?(File.join(app_root, ".tool-versions"))
|
53
|
+
end
|
54
|
+
|
55
|
+
def description
|
56
|
+
"Install tool versions with asdf"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,185 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Discharger
|
4
|
+
module SetupRunner
|
5
|
+
module Commands
|
6
|
+
class BaseCommand
|
7
|
+
attr_reader :config, :app_root, :logger
|
8
|
+
|
9
|
+
def initialize(config, app_root, logger)
|
10
|
+
@config = config
|
11
|
+
@app_root = app_root
|
12
|
+
@logger = logger
|
13
|
+
end
|
14
|
+
|
15
|
+
def execute
|
16
|
+
raise NotImplementedError, "#{self.class} must implement #execute"
|
17
|
+
end
|
18
|
+
|
19
|
+
def can_execute?
|
20
|
+
true
|
21
|
+
end
|
22
|
+
|
23
|
+
def description
|
24
|
+
class_name = self.class.name || "AnonymousCommand"
|
25
|
+
class_name.demodulize.underscore.humanize
|
26
|
+
end
|
27
|
+
|
28
|
+
protected
|
29
|
+
|
30
|
+
def log(message, emoji: nil)
|
31
|
+
return unless logger
|
32
|
+
class_name = self.class.name || "AnonymousCommand"
|
33
|
+
prefix = emoji ? "#{emoji} " : ""
|
34
|
+
logger.info "#{prefix}[#{class_name.demodulize}] #{message}"
|
35
|
+
end
|
36
|
+
|
37
|
+
def with_spinner(message)
|
38
|
+
if ENV["CI"] || ENV["NO_SPINNER"] || !$stdout.tty? || ENV["QUIET_SETUP"]
|
39
|
+
result = yield
|
40
|
+
# Handle error case when spinner is disabled
|
41
|
+
if result.is_a?(Hash) && !result[:success] && result[:raise_error] != false
|
42
|
+
raise result[:error]
|
43
|
+
end
|
44
|
+
return result
|
45
|
+
end
|
46
|
+
|
47
|
+
require "rainbow"
|
48
|
+
spinner_chars = %w[⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏]
|
49
|
+
spinner_thread = nil
|
50
|
+
stop_spinner = false
|
51
|
+
|
52
|
+
begin
|
53
|
+
# Print initial message
|
54
|
+
print Rainbow("◯ #{message}").cyan
|
55
|
+
$stdout.flush
|
56
|
+
|
57
|
+
# Start spinner in background thread
|
58
|
+
spinner_thread = Thread.new do
|
59
|
+
i = 0
|
60
|
+
until stop_spinner
|
61
|
+
print "\r#{Rainbow(spinner_chars[i % spinner_chars.length]).cyan} #{Rainbow(message).cyan}"
|
62
|
+
$stdout.flush
|
63
|
+
sleep 0.1
|
64
|
+
i += 1
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Execute the block
|
69
|
+
result = yield
|
70
|
+
|
71
|
+
# Stop spinner
|
72
|
+
stop_spinner = true
|
73
|
+
spinner_thread&.join(0.1)
|
74
|
+
|
75
|
+
# Clear line and print result
|
76
|
+
if result.is_a?(Hash)
|
77
|
+
if result[:success]
|
78
|
+
puts "\r#{Rainbow(result[:message] || "✓").green} #{message}"
|
79
|
+
else
|
80
|
+
puts "\r#{Rainbow(result[:message] || "✗").red} #{message}"
|
81
|
+
raise result[:error] if result[:error] && result[:raise_error] != false
|
82
|
+
end
|
83
|
+
else
|
84
|
+
puts "\r#{Rainbow("✓").green} #{message}"
|
85
|
+
end
|
86
|
+
|
87
|
+
result
|
88
|
+
rescue
|
89
|
+
stop_spinner = true
|
90
|
+
spinner_thread&.join(0.1)
|
91
|
+
puts "\r#{Rainbow("✗").red} #{message}"
|
92
|
+
raise
|
93
|
+
ensure
|
94
|
+
stop_spinner = true
|
95
|
+
spinner_thread&.kill if spinner_thread&.alive?
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def simple_action(message)
|
100
|
+
return yield if ENV["CI"] || ENV["NO_SPINNER"] || !$stdout.tty? || ENV["QUIET_SETUP"]
|
101
|
+
|
102
|
+
require "rainbow"
|
103
|
+
print Rainbow(" → #{message}...").cyan
|
104
|
+
$stdout.flush
|
105
|
+
|
106
|
+
begin
|
107
|
+
result = yield
|
108
|
+
puts Rainbow(" ✓").green
|
109
|
+
result
|
110
|
+
rescue
|
111
|
+
puts Rainbow(" ✗").red
|
112
|
+
raise
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def system!(*args)
|
117
|
+
require "open3"
|
118
|
+
command_str = args.join(" ")
|
119
|
+
|
120
|
+
# Create a more readable message for the spinner
|
121
|
+
spinner_message = if command_str.length > 80
|
122
|
+
if args.first.is_a?(Hash)
|
123
|
+
# Skip env hash in display
|
124
|
+
cmd_args = args[1..]
|
125
|
+
base_cmd = cmd_args.take(3).join(" ")
|
126
|
+
else
|
127
|
+
base_cmd = args.take(3).join(" ")
|
128
|
+
end
|
129
|
+
"Executing #{base_cmd}..."
|
130
|
+
else
|
131
|
+
"Executing #{command_str}"
|
132
|
+
end
|
133
|
+
|
134
|
+
result = with_spinner(spinner_message) do
|
135
|
+
stdout, stderr, status = Open3.capture3(*args)
|
136
|
+
|
137
|
+
if status.success?
|
138
|
+
# Log output if there is any (for debugging)
|
139
|
+
logger&.debug("Output: #{stdout}") if stdout && !stdout.empty?
|
140
|
+
{success: true, message: "✓"}
|
141
|
+
elsif args.first.to_s.include?("docker")
|
142
|
+
logger&.debug("Error: #{stderr}") if stderr && !stderr.empty?
|
143
|
+
{success: false, message: "✗ (Docker command failed)", raise_error: false}
|
144
|
+
else
|
145
|
+
{success: false, message: "✗", error: "#{command_str} failed: #{stderr}"}
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# Handle the case when spinner is disabled
|
150
|
+
if result.is_a?(Hash) && !result[:success] && result[:raise_error] != false
|
151
|
+
raise result[:error]
|
152
|
+
end
|
153
|
+
|
154
|
+
result
|
155
|
+
end
|
156
|
+
|
157
|
+
def system_quiet(*args)
|
158
|
+
require "open3"
|
159
|
+
stdout, _stderr, status = Open3.capture3(*args)
|
160
|
+
logger&.debug("Quietly executed #{args.join(" ")} - success: #{status.success?}")
|
161
|
+
logger&.debug("Output: #{stdout}") if stdout && !stdout.empty? && logger
|
162
|
+
status.success?
|
163
|
+
end
|
164
|
+
|
165
|
+
def ask_to_install(description)
|
166
|
+
unless ENV["QUIET_SETUP"] || ENV["DISABLE_OUTPUT"]
|
167
|
+
puts "You do not currently use #{description}.\n ===> If you want to, type Y\nOtherwise hit any key to ignore."
|
168
|
+
end
|
169
|
+
if gets.chomp == "Y"
|
170
|
+
yield
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def proceed_with(task)
|
175
|
+
unless ENV["QUIET_SETUP"] || ENV["DISABLE_OUTPUT"]
|
176
|
+
puts "Proceed with #{task}?\n ===> Type Y to proceed\nOtherwise hit any key to ignore."
|
177
|
+
end
|
178
|
+
if gets.chomp == "Y"
|
179
|
+
yield
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base_command"
|
4
|
+
|
5
|
+
module Discharger
|
6
|
+
module SetupRunner
|
7
|
+
module Commands
|
8
|
+
class BrewCommand < BaseCommand
|
9
|
+
def execute
|
10
|
+
proceed_with "brew bundle" do
|
11
|
+
log "Ensuring brew dependencies"
|
12
|
+
system! "brew bundle"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def can_execute?
|
17
|
+
File.exist?("Brewfile")
|
18
|
+
end
|
19
|
+
|
20
|
+
def description
|
21
|
+
"Install Homebrew dependencies"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base_command"
|
4
|
+
|
5
|
+
module Discharger
|
6
|
+
module SetupRunner
|
7
|
+
module Commands
|
8
|
+
class BundlerCommand < BaseCommand
|
9
|
+
def execute
|
10
|
+
log "Installing dependencies"
|
11
|
+
system! "gem install bundler --conservative"
|
12
|
+
system_quiet("bundle check") || system!("bundle install")
|
13
|
+
end
|
14
|
+
|
15
|
+
def can_execute?
|
16
|
+
File.exist?("Gemfile")
|
17
|
+
end
|
18
|
+
|
19
|
+
def description
|
20
|
+
"Install Ruby dependencies"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base_command"
|
4
|
+
require "fileutils"
|
5
|
+
|
6
|
+
module Discharger
|
7
|
+
module SetupRunner
|
8
|
+
module Commands
|
9
|
+
class ConfigCommand < BaseCommand
|
10
|
+
def execute
|
11
|
+
log "Ensuring configuration files are present"
|
12
|
+
|
13
|
+
# Copy database.yml if needed
|
14
|
+
database_yml = File.join(app_root, "config/database.yml")
|
15
|
+
database_yml_example = File.join(app_root, "config/database.yml.example")
|
16
|
+
|
17
|
+
if !File.exist?(database_yml) && File.exist?(database_yml_example)
|
18
|
+
FileUtils.cp(database_yml_example, database_yml)
|
19
|
+
log "Copied config/database.yml.example to config/database.yml"
|
20
|
+
end
|
21
|
+
|
22
|
+
# Copy Procfile.dev to Procfile if needed
|
23
|
+
procfile = File.join(app_root, "Procfile")
|
24
|
+
procfile_dev = File.join(app_root, "Procfile.dev")
|
25
|
+
|
26
|
+
if !File.exist?(procfile) && File.exist?(procfile_dev)
|
27
|
+
FileUtils.cp(procfile_dev, procfile)
|
28
|
+
log "Copied Procfile.dev to Procfile"
|
29
|
+
end
|
30
|
+
|
31
|
+
# Copy any other example config files
|
32
|
+
Dir.glob(File.join(app_root, "config/**/*.example")).each do |example_file|
|
33
|
+
config_file = example_file.sub(/\.example$/, "")
|
34
|
+
unless File.exist?(config_file)
|
35
|
+
FileUtils.cp(example_file, config_file)
|
36
|
+
log "Copied #{example_file.sub(app_root + "/", "")} to #{config_file.sub(app_root + "/", "")}"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def can_execute?
|
42
|
+
true
|
43
|
+
end
|
44
|
+
|
45
|
+
def description
|
46
|
+
"Setup configuration files"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../condition_evaluator"
|
4
|
+
|
5
|
+
module Discharger
|
6
|
+
module SetupRunner
|
7
|
+
module Commands
|
8
|
+
class CustomCommand < BaseCommand
|
9
|
+
attr_reader :step_config
|
10
|
+
|
11
|
+
def initialize(config, app_root, logger, step_config)
|
12
|
+
super(config, app_root, logger)
|
13
|
+
@step_config = step_config
|
14
|
+
end
|
15
|
+
|
16
|
+
def execute
|
17
|
+
command = step_config["command"]
|
18
|
+
description = step_config["description"] || command
|
19
|
+
condition = step_config["condition"]
|
20
|
+
|
21
|
+
# Check condition if provided using safe evaluator
|
22
|
+
if condition && !ConditionEvaluator.evaluate(condition)
|
23
|
+
log "Skipping #{description} (condition not met)"
|
24
|
+
return
|
25
|
+
end
|
26
|
+
|
27
|
+
log "Running: #{description}"
|
28
|
+
system!(command)
|
29
|
+
end
|
30
|
+
|
31
|
+
def can_execute?
|
32
|
+
step_config["command"].present?
|
33
|
+
end
|
34
|
+
|
35
|
+
def description
|
36
|
+
step_config["description"] || "Custom command: #{step_config["command"]}"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base_command"
|
4
|
+
require "open3"
|
5
|
+
|
6
|
+
module Discharger
|
7
|
+
module SetupRunner
|
8
|
+
module Commands
|
9
|
+
class DatabaseCommand < BaseCommand
|
10
|
+
def execute
|
11
|
+
# Drop and recreate development database
|
12
|
+
terminate_database_connections
|
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")
|
15
|
+
if status.success?
|
16
|
+
{success: true}
|
17
|
+
else
|
18
|
+
{success: false, error: "Failed to drop/create database: #{stderr}"}
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Load schema and run migrations
|
23
|
+
with_spinner("Loading database schema and running migrations") do
|
24
|
+
_stdout, stderr, status = Open3.capture3("bin/rails db:schema:load db:migrate")
|
25
|
+
if status.success?
|
26
|
+
{success: true}
|
27
|
+
else
|
28
|
+
{success: false, error: "Failed to load schema: #{stderr}"}
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Seed the database
|
33
|
+
env = (config.respond_to?(:seed_env) && config.seed_env) ? {"SEED_DEV_ENV" => "true"} : {}
|
34
|
+
with_spinner("Seeding the database") do
|
35
|
+
_stdout, stderr, status = Open3.capture3(env, "bin/rails db:seed")
|
36
|
+
if status.success?
|
37
|
+
{success: true}
|
38
|
+
else
|
39
|
+
{success: false, error: "Failed to seed database: #{stderr}"}
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Setup test database
|
44
|
+
terminate_database_connections("test")
|
45
|
+
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
|
+
if status.success?
|
48
|
+
{success: true}
|
49
|
+
else
|
50
|
+
{success: false, error: "Failed to setup test database: #{stderr}"}
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Clear logs and temp files
|
55
|
+
with_spinner("Clearing logs and temp files") do
|
56
|
+
_stdout, _stderr, status = Open3.capture3("bash", "-c", "bin/rails log:clear tmp:clear > /dev/null 2>&1")
|
57
|
+
if status.success?
|
58
|
+
else
|
59
|
+
# Don't fail for log clearing
|
60
|
+
end
|
61
|
+
{success: true}
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def can_execute?
|
66
|
+
File.exist?(File.join(app_root, "bin/rails"))
|
67
|
+
end
|
68
|
+
|
69
|
+
def description
|
70
|
+
"Setup database"
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def terminate_database_connections(rails_env = nil)
|
76
|
+
# Use a Rails runner to terminate connections within the Rails context
|
77
|
+
env_vars = rails_env ? {"RAILS_ENV" => rails_env} : {}
|
78
|
+
|
79
|
+
runner_script = <<~RUBY
|
80
|
+
begin
|
81
|
+
# Only proceed if using PostgreSQL
|
82
|
+
if defined?(ActiveRecord::Base) && ActiveRecord::Base.connection.adapter_name =~ /postgresql/i
|
83
|
+
ActiveRecord::Base.connection.execute <<-SQL
|
84
|
+
SELECT pg_terminate_backend(pid)
|
85
|
+
FROM pg_stat_activity
|
86
|
+
WHERE datname = current_database() AND pid <> pg_backend_pid();
|
87
|
+
SQL
|
88
|
+
end
|
89
|
+
rescue => e
|
90
|
+
# If we can't connect or terminate, that's okay - the database might not exist yet
|
91
|
+
# Log error silently in test environment
|
92
|
+
puts "Note: Could not terminate existing connections: \#{e.message}" unless ENV['QUIET_SETUP']
|
93
|
+
end
|
94
|
+
RUBY
|
95
|
+
|
96
|
+
with_spinner("Terminating existing database connections#{rails_env ? " (#{rails_env})" : ""}") do
|
97
|
+
stdout, stderr, status = Open3.capture3(env_vars, "bin/rails", "runner", runner_script)
|
98
|
+
|
99
|
+
if status.success?
|
100
|
+
logger&.debug("Output: #{stdout}") if stdout && !stdout.empty?
|
101
|
+
elsif stderr && !stderr.empty?
|
102
|
+
logger&.debug("Error: #{stderr}")
|
103
|
+
# Don't fail if we can't terminate connections - the database might not exist
|
104
|
+
end
|
105
|
+
{success: true}
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|