chagall 0.0.1.beta1

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.
@@ -0,0 +1,321 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "erb"
5
+ require "yaml"
6
+ require "fileutils"
7
+ require "optparse"
8
+ require "logger"
9
+ require "tmpdir"
10
+ require "securerandom"
11
+ require "net/http"
12
+ require "json"
13
+ require "uri"
14
+
15
+ # The Installer class is responsible for setting up the environment for a Ruby on Rails application.
16
+ # It detects the required services and versions, generates necessary Docker and Compose files, and logs the process.
17
+ #
18
+ # Constants:
19
+ # - TEMPLATES_DIR: Directory where template files are stored.
20
+ # - DEFAULT_NODE_VERSION: Default Node.js version to use if not specified.
21
+ # - DEFAULT_RUBY_VERSION: Default Ruby version to use if not specified.
22
+ #
23
+ # Attributes:
24
+ # - app_name: Name of the application.
25
+ # - services: List of services to be included in the setup.
26
+ # - versions: Hash containing versions of Ruby, Node.js, PostgreSQL, and Redis.
27
+ # - logger: Logger instance for logging messages.
28
+ # - database_type: Type of database to use (e.g., 'postgres', 'mysql', 'sqlite').
29
+ #
30
+ # Methods:
31
+ # - initialize(options = {}): Initializes the installer with given options.
32
+ # - install: Main method to perform the installation process.
33
+ #
34
+ # Private Methods:
35
+ # - backup_file(file): Creates a backup of the specified file if it exists.
36
+ # - detect_ruby_version: Detects the Ruby version from various files or defaults to a predefined version.
37
+ # - detect_node_version: Detects the Node.js version from various files or defaults to a predefined version.
38
+ # - node_version_from_package_json: Extracts the Node.js version from package.json if available.
39
+ # - node_version_from_file: Extracts the Node.js version from .node-version, .tool-versions, or .nvmrc files if available.
40
+ # - detect_services: Detects required services based on the gems listed in the Gemfile.
41
+ # - generate_compose: Generates the Docker Compose file from a template.
42
+ # - generate_dockerfile: Generates the Dockerfile from a template.
43
+ class Installer # rubocop:disable Metrics/ClassLength
44
+ TEMPLATES_DIR = File.expand_path("./templates", __dir__)
45
+ TEMP_DIR = File.join(Dir.tmpdir, "chagall-#{SecureRandom.hex(4)}").freeze
46
+ DEFAULT_RUBY_VERSION = "3.3.0"
47
+ DEFAULT_NODE_VERSION = "20.11.0"
48
+
49
+ GITHUB_REPO = "frontandstart/chagall"
50
+ TEMPLATE_FILES = %w[template.compose.yaml
51
+ template.Dockerfile].freeze
52
+
53
+ DEPENDENCIES = [
54
+ {
55
+ adapter: :postgresql,
56
+ gem_name: "pg",
57
+ service: :postgres,
58
+ image: "postgres:16.4-bullseye",
59
+ docker_env: "DATABASE_URL: postgres://postgres:postgres@postgres:5432"
60
+ },
61
+ {
62
+ adapter: :mysql2,
63
+ gem_name: "mysql2",
64
+ service: :mariadb,
65
+ image: "mariadb:8.0-bullseye",
66
+ docker_env: "DATABASE_URL: mysql://mysql:mysql@mariadb:3306"
67
+ },
68
+ {
69
+ adapter: :mongoid,
70
+ gem_name: "mongoid",
71
+ service: :mongodb,
72
+ image: "mongo:8.0-noble",
73
+ docker_env: "DATABASE_URL: mongodb://mongodb:27017"
74
+ },
75
+ {
76
+ gem_name: "redis",
77
+ service: :redis,
78
+ image: "redis:7.4-bookworm",
79
+ docker_env: "REDIS_URL: redis://redis:6379"
80
+ },
81
+ {
82
+ gem_name: "sidekiq",
83
+ service: :sidekiq,
84
+ image: -> { app_name }
85
+ },
86
+ {
87
+ gem_name: "elasticsearch",
88
+ service: :elasticsearch,
89
+ image: "elasticsearch:8.15.3",
90
+ docker_env: "ELASTICSEARCH_URL: elasticsearch://elasticsearch:9200"
91
+ },
92
+ {
93
+ gem_name: "solid_queue",
94
+ service: :solid_queue,
95
+ image: -> { app_name }
96
+ }
97
+ ].freeze
98
+
99
+ attr_reader :app_name,
100
+ :versions,
101
+ :logger,
102
+ :database_type,
103
+ :database_config,
104
+ :gemfile,
105
+ :gemfile_lock
106
+
107
+ attr_accessor :project_services,
108
+ :environments
109
+
110
+ def initialize(options = {})
111
+ raise "Gemfile not found" unless File.exist?("Gemfile")
112
+
113
+ Chagall::Settings.configure(argv)
114
+
115
+ @app_name = options[:app_name] || File.basename(Dir.pwd)
116
+ @services = []
117
+ @logger = Logger.new($stdout)
118
+ @logger.formatter = proc { |_, _, _, msg| "#{msg}\n" }
119
+ @environments = {}
120
+ @gemfile = File.read("Gemfile")
121
+ @gemfile_lock = File.read("Gemfile.lock")
122
+ @database_adapters = YAML.load_file("config/database.yml")
123
+ .map { |_, config| config["adapter"] }.uniq
124
+ @database_type = @database_adapters
125
+ end
126
+
127
+ def install
128
+ setup_temp_directory
129
+ detect_services
130
+ generate_environment_variables
131
+ generate_compose_file
132
+ generate_dockerfile
133
+ logger.info "Installation completed successfully!"
134
+ ensure
135
+ cleanup_temp_directory
136
+ end
137
+
138
+ private
139
+
140
+ def setup_temp_directory
141
+ FileUtils.mkdir_p(TEMP_DIR)
142
+ download_template_files
143
+ rescue StandardError => e
144
+ logger.error "Failed to set up temporary directory: #{e.message}"
145
+ cleanup_temp_directory
146
+ raise
147
+ end
148
+
149
+ def detect_services
150
+ DEPENDENCIES.each do |dependency|
151
+ @services << dependency if gemfile_has_dependency?(dependency[:gem_name])
152
+ end
153
+
154
+ logger.info "Detected services: #{services.map { |s| s[:service] }.join(', ')}"
155
+ end
156
+
157
+ def gemfile_has_dependency?(gem_name)
158
+ gemfile_match = gemfile.match?(/^\s*[^#].*gem ['"]#{gem_name}['"]/)
159
+ gemfile_lock_match = gemfile_lock.match?(/^\s+#{gem_name}\s+\(/) || false
160
+
161
+ gemfile_match && gemfile_lock_match
162
+ end
163
+
164
+ def generate_environment_variables
165
+ services.each do |service|
166
+ url = generate_service_url_for(service[:adapter])
167
+ environments[service[:url_name]] = url if url
168
+ end
169
+ end
170
+
171
+ def cleanup_temp_directory
172
+ FileUtils.remove_entry_secure(TEMP_DIR) if Dir.exist?(TEMP_DIR)
173
+ end
174
+
175
+ def download_template_files
176
+ release_info = fetch_latest_release
177
+ TEMPLATE_FILES.each do |filename|
178
+ download_template(filename, release_info)
179
+ end
180
+ rescue StandardError => e
181
+ logger.error "Failed to download template files: #{e.message}"
182
+ raise
183
+ end
184
+
185
+ def fetch_latest_release
186
+ uri = URI("https://api.github.com/repos/#{GITHUB_REPO}/releases/latest")
187
+ response = Net::HTTP.get_response(uri)
188
+
189
+ raise "Failed to fetch latest release info: #{response.message}" unless response.is_a?(Net::HTTPSuccess)
190
+
191
+ JSON.parse(response.body)
192
+ end
193
+
194
+ def download_template(filename, release_info)
195
+ asset = release_info["assets"].find { |a| a["name"] == filename }
196
+ raise "Template file #{filename} not found in release" unless asset
197
+
198
+ download_url = asset["browser_download_url"]
199
+ target_path = File.join(TEMP_DIR, filename)
200
+
201
+ uri = URI(download_url)
202
+ response = Net::HTTP.get_response(uri)
203
+
204
+ raise "Failed to download #{filename}: #{response.message}" unless response.is_a?(Net::HTTPSuccess)
205
+
206
+ File.write(target_path, response.body)
207
+ logger.info "Downloaded #{filename}"
208
+ end
209
+
210
+ def backup_file(file)
211
+ return unless File.exist?(file)
212
+
213
+ backup = "#{file}.chagall.bak"
214
+ FileUtils.cp(file, backup)
215
+ logger.info "Backed up existing #{file} to #{backup}"
216
+ end
217
+
218
+ def generate_database_url(adapter)
219
+ case adapter
220
+ when "postgresql"
221
+ "postgres://postgres:postgres@postgres:5432/db"
222
+ when "mysql2"
223
+ "mysql2://mysql:mysql@mysql:3306/db"
224
+ when "sqlite3"
225
+ "sqlite3:///data/db.sqlite3"
226
+ when "redis"
227
+ "redis://redis:5432/0"
228
+ else
229
+ raise "Unsupported adapter: #{adapter}"
230
+ end
231
+ end
232
+
233
+ def generate_compose_file
234
+ backup_file("compose.yaml")
235
+
236
+ template_path = File.join(TEMP_DIR, "template.compose.yaml")
237
+ raise "Compose template not found at #{template_path}" unless File.exist?(template_path)
238
+
239
+ template = File.read(template_path)
240
+ result = ERB.new(
241
+ template,
242
+ trim_mode: "-",
243
+ services: services,
244
+ environments: environments
245
+ ).result(binding)
246
+
247
+ File.write("compose.yaml", result)
248
+ logger.info "Generated compose.yaml"
249
+ end
250
+
251
+ def generate_dockerfile
252
+ backup_file("Dockerfile")
253
+
254
+ template_path = File.join(TEMP_DIR, "template.Dockerfile")
255
+ raise "Dockerfile template not found at #{template_path}" unless File.exist?(template_path)
256
+
257
+ template = File.read(template_path)
258
+ result = ERB.new(template, trim_mode: "-").result(binding)
259
+
260
+ File.write("Dockerfile", result)
261
+ logger.info "Generated Dockerfile"
262
+ end
263
+
264
+ def detect_ruby_version
265
+ from_gemfile = gemfile.match(/ruby ['"](.+?)['"]/)[1]
266
+ return from_gemfile if from_gemfile
267
+
268
+ if File.exist?(".ruby-version")
269
+ File.read(".ruby-version").strip
270
+ elsif File.exist?(".tool-versions")
271
+ File.read(".tool-versions").match(/ruby (.+?)\n/)[1]
272
+ else
273
+ DEFAULT_RUBY_VERSION
274
+ end
275
+ end
276
+
277
+ def detect_node_version
278
+ node_version_from_package_json || node_version_from_file || DEFAULT_NODE_VERSION
279
+ end
280
+
281
+ def node_version_from_package_json
282
+ return unless File.exist?("package.json")
283
+
284
+ begin
285
+ JSON.parse(File.read("package.json")).dig("engines", "node")&.delete("^")
286
+ rescue JSON::ParserError
287
+ nil
288
+ end
289
+ end
290
+
291
+ def node_version_from_file
292
+ if File.exist?(".node-version")
293
+ File.read(".node-version").strip
294
+ elsif File.exist?(".tool-versions")
295
+ File.read(".tool-versions").match(/node (.+?)\n/)[1]
296
+ elsif File.exist?(".nvmrc")
297
+ File.read(".nvmrc").strip
298
+ end
299
+ end
300
+
301
+ def find_dependecy_by(name, value)
302
+ DEPENDENCIES.find { |d| d[name.to_sym] == value.to_sym }
303
+ end
304
+ end
305
+
306
+ if __FILE__ == $PROGRAM_NAME
307
+ options = {}
308
+ OptionParser.new do |opts|
309
+ opts.banner = "Usage: install.rb [options]"
310
+
311
+ opts.on("-n", "--non-interactive", "Run in non-interactive mode") do
312
+ options[:non_interactive] = true
313
+ end
314
+
315
+ opts.on("-a", "--app-name NAME", "Set application name") do |name|
316
+ options[:app_name] = name
317
+ end
318
+ end.parse!
319
+
320
+ Installer.new(options).install
321
+ end
@@ -0,0 +1,37 @@
1
+ ARG RUBY_VERSION=<%= @versions['ruby'] %>
2
+ ARG NODE_VERSION=<%= @versions['node'] %>
3
+
4
+ FROM ruby:${RUBY_VERSION}-slim AS development
5
+
6
+ LABEL app.name="<%= @app_name %>"
7
+
8
+ ARG NODE_VERSION
9
+ ENV NODE_VERSION=${NODE_VERSION}
10
+ WORKDIR /app
11
+
12
+ RUN apt-get update -qq && apt-get install -y \
13
+ build-essential \
14
+ libvips \
15
+ libffi-dev \
16
+ libssl-dev \
17
+ gnupg2 \
18
+ curl \
19
+ git
20
+
21
+ # Node section
22
+ RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash && \
23
+ export NVM_DIR="/root/.nvm" && \
24
+ . "$NVM_DIR/nvm.sh" && \
25
+ nvm install "$NODE_VERSION" && \
26
+ nvm use "$NODE_VERSION" && \
27
+ nvm alias default "$NODE_VERSION"
28
+
29
+ RUN gem install bundler
30
+
31
+ FROM development AS production
32
+
33
+ COPY . .
34
+
35
+ RUN bundle config set production true
36
+ RUN BUNDLE_JOBS=$(nproc) bundle install
37
+ RUN bundle exec rails assets:precompile
@@ -0,0 +1,121 @@
1
+ x-docker-environment: &docker-environment
2
+ <% if services.each do |service| %>
3
+ <%= service[:docker_env] if service[:docker_env] %>
4
+ <% end %>
5
+ HISTFILE: tmp/.docker_shell_history
6
+ PRY_HISTFILE: tmp/.docker_pry_history
7
+ RAILS_LOG_TO_STDOUT: true
8
+
9
+ x-app: &app
10
+ image: <%= @app_name %>:development
11
+ environment:
12
+ <<: *docker-environment
13
+ build:
14
+ context: .
15
+ target: development
16
+ working_dir: /app
17
+ env_file:
18
+ - .env
19
+ stdin_open: true
20
+ ports:
21
+ - ${PORT:-3000}:${PORT:-3000}
22
+ depends_on:
23
+ postgres:
24
+ condition: service_healthy
25
+ redis:
26
+ condition: service_healthy
27
+ tmpfs:
28
+ - /app/tmp/pids
29
+ volumes:
30
+ - .:/app:c
31
+ - cache:/app/tmp/cache:d
32
+ - bundle:/usr/local/bundle:d
33
+ - node_modules:/app/node_modules:d
34
+
35
+ services:
36
+ app: &app
37
+ command: bin/dev
38
+
39
+ <%- if @services.include?('postgres') -%>
40
+ postgres:
41
+ image: postgres:<%= @versions['postgres'] %>
42
+ environment:
43
+ POSTGRES_USER: postgres
44
+ POSTGRES_PASSWORD: postgres
45
+ POSTGRES_HOST: 0.0.0.0
46
+ volumes:
47
+ - postgres:/var/lib/postgresql/data:c
48
+ healthcheck:
49
+ test: ["CMD-SHELL", "pg_isready -h postgres -U postgres"]
50
+ interval: 5s
51
+ timeout: 5s
52
+ retries: 10
53
+ <%- end -%>
54
+
55
+ <%- if services.include?('redis') -%>
56
+ redis:
57
+ image: redis:<%= @versions['redis'] %>
58
+ volumes:
59
+ - redis:/data:delegated
60
+ healthcheck:
61
+ test: ["CMD", "redis-cli", "ping"]
62
+ interval: 5s
63
+ timeout: 3s
64
+ retries: 10
65
+ entrypoint: redis-server --appendonly yes
66
+ <%- end -%>
67
+
68
+ prod:
69
+ <<: *app
70
+ image: <%= @app_name %>:production
71
+ environment:
72
+ <<: *docker-environment
73
+ RAILS_ENV: production
74
+ RACK_ENV: production
75
+ profiles:
76
+ - prod
77
+ healthcheck:
78
+ test: ["CMD", "curl", "http://prod:3000/health"]
79
+ interval: 20s
80
+ timeout: 5s
81
+ retries: 3
82
+ start_period: 20s
83
+ deploy:
84
+ mode: replicated
85
+ replicas: 2
86
+ endpoint_mode: vip
87
+ update_config:
88
+ parallelism: 1
89
+ order: start-first
90
+ delay: 5s
91
+ failure_action: rollback
92
+ restart_policy:
93
+ condition: on-failure
94
+ max_attempts: 3
95
+ window: 120s
96
+ volumes:
97
+ - cache:/app/tmp/cache:d
98
+
99
+ reproxy:
100
+ image: umputun/reproxy
101
+ profiles:
102
+ - prod
103
+ ports:
104
+ - 80:8080
105
+ - 443:8443
106
+ environment:
107
+ SSL_TYPE: auto
108
+ SSL_ACME_FQDN: domain.com
109
+ SSL_ACME_LOCATION: /srv/var/acme
110
+ SSL_ACME_EMAIL: mail@example.com
111
+ FILE_ENABLED: true
112
+ volumes:
113
+ - ./config/reproxy.conf:/srv/reproxy.yml
114
+ - certs:/srv/var/acme
115
+
116
+ volumes:
117
+ postgres:
118
+ redis:
119
+ bundle:
120
+ node_modules:
121
+ cache:
@@ -0,0 +1,232 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require "singleton"
5
+
6
+ module Chagall
7
+ class Settings
8
+ include Singleton
9
+
10
+ attr_accessor :options, :missing_options, :missing_compose_files
11
+ CHAGALL_PROJECTS_FOLDER = "~/projects"
12
+ TMP_CACHE_FOLDER = "tmp"
13
+
14
+ OPTIONS = [
15
+ {
16
+ key: :debug,
17
+ flags: [ "--debug" ],
18
+ description: "Debug mode with pry attaching",
19
+ type: :boolean,
20
+ default: false,
21
+ environment_variable: "CHAGALL_DEBUG"
22
+ },
23
+ {
24
+ key: :skip_uncommit,
25
+ flags: [ "--skip-uncommit" ],
26
+ description: "Skip uncommitted changes check",
27
+ type: :boolean,
28
+ default: false,
29
+ environment_variable: "CHAGALL_SKIP_UNCOMMIT"
30
+ },
31
+ {
32
+ key: :server,
33
+ flags: [ "-s", "--server" ],
34
+ description: "Server to deploy to",
35
+ type: :string,
36
+ required: true,
37
+ environment_variable: "CHAGALL_SERVER"
38
+ },
39
+ {
40
+ key: :name,
41
+ flags: [ "-n", "--name" ],
42
+ description: "Project name",
43
+ type: :string,
44
+ default: Pathname.new(Dir.pwd).basename.to_s,
45
+ environment_variable: "CHAGALL_NAME"
46
+ },
47
+ {
48
+ key: :release,
49
+ flags: [ "--release" ],
50
+ description: "Release tag",
51
+ required: true,
52
+ default: `git rev-parse --short HEAD`.strip,
53
+ type: :string,
54
+ environment_variable: "CHAGALL_RELEASE"
55
+ },
56
+ {
57
+ key: :dry_run,
58
+ type: :boolean,
59
+ default: false,
60
+ flags: [ "-d", "--dry-run" ],
61
+ environment_variable: "CHAGALL_DRY_RUN",
62
+ description: "Dry run"
63
+ },
64
+ {
65
+ key: :remote,
66
+ flags: [ "-r", "--remote" ],
67
+ description: "Deploy remotely (build on remote server)",
68
+ type: :boolean,
69
+ default: false,
70
+ environment_variable: "CHAGALL_REMOTE"
71
+ },
72
+ {
73
+ key: :compose_files,
74
+ flags: [ "-c", "--compose-files" ],
75
+ description: "Comma separated list of compose files",
76
+ type: :array,
77
+ required: true,
78
+ environment_variable: "CHAGALL_COMPOSE_FILES",
79
+ proc: ->(value) { value.split(",") }
80
+ },
81
+ {
82
+ key: :target,
83
+ type: :string,
84
+ default: "production",
85
+ flags: [ "--target" ],
86
+ environment_variable: "CHAGALL_TARGET",
87
+ description: "Target"
88
+ },
89
+ {
90
+ key: :dockerfile,
91
+ type: :string,
92
+ flags: [ "-f", "--file" ],
93
+ default: "Dockerfile",
94
+ environment_variable: "CHAGALL_DOCKERFILE",
95
+ description: "Dockerfile"
96
+ },
97
+ {
98
+ key: :projects_folder,
99
+ type: :string,
100
+ default: CHAGALL_PROJECTS_FOLDER,
101
+ flags: [ "-p", "--projects-folder" ],
102
+ environment_variable: "CHAGALL_PROJECTS_FOLDER",
103
+ description: "Projects folder"
104
+ },
105
+ {
106
+ key: :cache_from,
107
+ type: :string,
108
+ default: "#{TMP_CACHE_FOLDER}/.buildx-cache",
109
+ flags: [ "--cache-from" ],
110
+ environment_variable: "CHAGALL_CACHE_FROM",
111
+ description: "Cache from"
112
+ },
113
+ {
114
+ key: :cache_to,
115
+ type: :string,
116
+ default: "#{TMP_CACHE_FOLDER}/.buildx-cache-new",
117
+ flags: [ "--cache-to" ],
118
+ environment_variable: "CHAGALL_CACHE_TO",
119
+ description: "Cache to"
120
+ },
121
+ {
122
+ key: :keep_releases,
123
+ type: :integer,
124
+ default: 3,
125
+ flags: [ "-k", "--keep-releases" ],
126
+ environment_variable: "CHAGALL_KEEP_RELEASES",
127
+ description: "Keep releases",
128
+ proc: ->(value) { Integer(value) }
129
+ },
130
+ {
131
+ key: :ssh_args,
132
+ type: :string,
133
+ default: "-o StrictHostKeyChecking=no",
134
+ environment_variable: "CHAGALL_SSH_ARGS",
135
+ flags: [ "--ssh-args" ],
136
+ description: "SSH arguments"
137
+ },
138
+ {
139
+ key: :docker_context,
140
+ type: :string,
141
+ flags: [ "--docker-context" ],
142
+ environment_variable: "CHAGALL_DOCKER_CONTEXT",
143
+ default: ".",
144
+ description: "Docker context"
145
+ },
146
+ {
147
+ key: :platform,
148
+ type: :string,
149
+ flags: [ "--platform" ],
150
+ environment_variable: "CHAGALL_PLATFORM",
151
+ default: "linux/x86_64",
152
+ description: "Platform"
153
+ }
154
+ ].freeze
155
+ class << self
156
+ def configure(argv)
157
+ instance.configure(argv)
158
+ end
159
+
160
+ def [](key)
161
+ instance.options[key]
162
+ end
163
+ end
164
+
165
+ def configure(parsed_options)
166
+ @options = parsed_options
167
+ @missing_options = []
168
+ @missing_compose_files = []
169
+
170
+ # @options.merge!(options_from_config_file)
171
+ # @options.merge!(parsed_options)
172
+
173
+ validate_options
174
+ end
175
+
176
+ def validate_options
177
+ error_message_string = "\n"
178
+
179
+ OPTIONS.each do |option|
180
+ @missing_options << option if option[:required] && @options[option[:key]].to_s.empty?
181
+ end
182
+
183
+ if @missing_options.any?
184
+ error_message_string += " Missing required options: #{@missing_options.map { |o| o[:key] }.join(', ')}\n"
185
+ error_message_string += " These can be set via:\n"
186
+ error_message_string += " - CLI arguments (#{@missing_options.map { |o| o[:flags] }.join(', ')})\n"
187
+ error_message_string += " - Environment variables (#{@missing_options.map do |o|
188
+ o[:environment_variable] || o[:env_name]
189
+ end.join(', ')})\n"
190
+ error_message_string += " - chagall.yml file\n"
191
+ end
192
+
193
+ if @options[:compose_files]
194
+ @options[:compose_files].each do |file|
195
+ unless File.exist?(file)
196
+ @missing_compose_files << file
197
+ error_message_string += " Missing compose file: #{file}\n"
198
+ end
199
+ end
200
+ end
201
+
202
+ return unless @missing_options.any? || @missing_compose_files.any?
203
+
204
+ raise Chagall::SettingsError, error_message_string unless @options[:dry_run]
205
+ end
206
+
207
+ def options_from_config_file
208
+ @options_from_config_file ||= begin
209
+ config_path = File.join(Dir.pwd, "chagall.yml") || File.join(Dir.pwd, "chagall.yaml")
210
+ return {} unless File.exist?(config_path)
211
+
212
+ config = YAML.load_file(config_path)
213
+ config.transform_keys(&:to_sym)
214
+ rescue StandardError => e
215
+ puts "Warning: Error loading chagall.yml: #{e.message}"
216
+ {}
217
+ end
218
+ end
219
+
220
+ def true?(value)
221
+ value.to_s.strip.downcase == "true"
222
+ end
223
+
224
+ def image_tag
225
+ @image_tag ||= "#{options[:name]}:#{options[:release]}"
226
+ end
227
+
228
+ def project_folder_path
229
+ @project_folder_path ||= "#{options[:projects_folder]}/#{options[:name]}"
230
+ end
231
+ end
232
+ end