wordmove-ng 6.0.0

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.
Files changed (74) hide show
  1. checksums.yaml +7 -0
  2. data/.github/ISSUE_TEMPLATE/bug_report.md +40 -0
  3. data/.github/ISSUE_TEMPLATE/config.yml +11 -0
  4. data/.github/stale.yml +18 -0
  5. data/.github/workflows/release.yml +100 -0
  6. data/.github/workflows/ruby.yml +41 -0
  7. data/.gitignore +21 -0
  8. data/.release-please-manifest.json +3 -0
  9. data/.rspec +2 -0
  10. data/.rubocop.yml +50 -0
  11. data/.rubocop_todo.yml +89 -0
  12. data/.ruby-gemset +1 -0
  13. data/.ruby-version +1 -0
  14. data/.vscode/launch.json +73 -0
  15. data/CHANGELOG.md +78 -0
  16. data/CLAUDE.md +114 -0
  17. data/CONTRIBUTING.md +114 -0
  18. data/Gemfile +14 -0
  19. data/LICENSE +22 -0
  20. data/README.md +340 -0
  21. data/Rakefile +7 -0
  22. data/assets/images/wordmove-ng.png +0 -0
  23. data/bin/bundle +105 -0
  24. data/bin/bundler +17 -0
  25. data/bin/byebug +29 -0
  26. data/bin/coderay +29 -0
  27. data/bin/console +16 -0
  28. data/bin/htmldiff +29 -0
  29. data/bin/kwalify +29 -0
  30. data/bin/ldiff +29 -0
  31. data/bin/pry +29 -0
  32. data/bin/rake +29 -0
  33. data/bin/rspec +29 -0
  34. data/bin/rubocop +29 -0
  35. data/bin/ruby-parse +29 -0
  36. data/bin/ruby-rewrite +29 -0
  37. data/bin/rumoji +29 -0
  38. data/bin/setup +7 -0
  39. data/bin/thor +29 -0
  40. data/bin/wordmove-ng +10 -0
  41. data/deploy/deploy.sh +3 -0
  42. data/exe/wordmove-ng +6 -0
  43. data/lib/wordmove/assets/wordmove_schema_global.yml +16 -0
  44. data/lib/wordmove/assets/wordmove_schema_local.yml +31 -0
  45. data/lib/wordmove/assets/wordmove_schema_remote.yml +167 -0
  46. data/lib/wordmove/cli.rb +134 -0
  47. data/lib/wordmove/deployer/base.rb +356 -0
  48. data/lib/wordmove/deployer/ssh.rb +339 -0
  49. data/lib/wordmove/doctor/movefile.rb +112 -0
  50. data/lib/wordmove/doctor/mysql.rb +136 -0
  51. data/lib/wordmove/doctor/rsync.rb +27 -0
  52. data/lib/wordmove/doctor/ssh.rb +90 -0
  53. data/lib/wordmove/doctor/wpcli.rb +43 -0
  54. data/lib/wordmove/doctor.rb +67 -0
  55. data/lib/wordmove/environments_list.rb +68 -0
  56. data/lib/wordmove/exceptions.rb +10 -0
  57. data/lib/wordmove/generators/movefile.rb +50 -0
  58. data/lib/wordmove/generators/movefile.yml +104 -0
  59. data/lib/wordmove/generators/movefile_adapter.rb +188 -0
  60. data/lib/wordmove/guardian.rb +37 -0
  61. data/lib/wordmove/hook.rb +128 -0
  62. data/lib/wordmove/logger.rb +165 -0
  63. data/lib/wordmove/movefile.rb +129 -0
  64. data/lib/wordmove/prerequisites.rb +57 -0
  65. data/lib/wordmove/sql_adapter/wpcli.rb +72 -0
  66. data/lib/wordmove/ssh_runner.rb +110 -0
  67. data/lib/wordmove/version.rb +3 -0
  68. data/lib/wordmove/wordpress_directory/path.rb +11 -0
  69. data/lib/wordmove/wordpress_directory.rb +41 -0
  70. data/lib/wordmove-ng.rb +4 -0
  71. data/lib/wordmove.rb +51 -0
  72. data/release-please-config.json +25 -0
  73. data/wordmove-ng.gemspec +62 -0
  74. metadata +318 -0
@@ -0,0 +1,90 @@
1
+ module Wordmove
2
+ class Doctor
3
+ class Ssh
4
+ attr_reader :logger, :movefile
5
+
6
+ def initialize(movefile_name = nil, movefile_dir = '.')
7
+ @logger = Logger.new(STDOUT).tap { |l| l.level = Logger::INFO }
8
+ @movefile = Wordmove::Movefile.new(movefile_name, movefile_dir)
9
+ end
10
+
11
+ def check!
12
+ logger.task "Checking SSH client"
13
+
14
+ if system('which ssh', out: File::NULL, err: File::NULL)
15
+ logger.success "SSH command found"
16
+ else
17
+ logger.error "SSH command not found. And belive me: it's really strange it's not there."
18
+ return
19
+ end
20
+
21
+ ssh_environments.each { |name, ssh_options| check_environment!(name, ssh_options) }
22
+ end
23
+
24
+ private
25
+
26
+ # Returns { environment_name => ssh_options } for every remote using SSH.
27
+ # Any movefile problem is reported by the Movefile doctor, so it is
28
+ # silently skipped here.
29
+ def ssh_environments
30
+ options = movefile.fetch(false)
31
+ options.each_with_object({}) do |(name, env), memo|
32
+ next if %i[local global].include?(name)
33
+ next unless env.is_a?(Hash) && env[:ssh].is_a?(Hash)
34
+
35
+ memo[name] = env[:ssh]
36
+ end
37
+ rescue MovefileNotFound, Psych::SyntaxError
38
+ {}
39
+ end
40
+
41
+ def check_environment!(name, ssh_options)
42
+ logger.task "Checking SSH connection to \"#{name}\""
43
+ warn_about_gateway_password(name, ssh_options)
44
+ runner = Wordmove::SshRunner.new(ssh_options)
45
+ _stdout, stderr, exit_code = runner.run('true')
46
+
47
+ if exit_code.zero?
48
+ logger.success "Non interactive SSH authentication to \"#{name}\" works"
49
+ check_remote_prerequisites!(name, runner)
50
+ else
51
+ logger.error <<-LONG
52
+ Non interactive SSH authentication to "#{name}" failed (exit code #{exit_code}).
53
+ Database sync and remote hooks use `ssh` exactly like rsync does, so
54
+ check your keys, ssh-agent, ~/.ssh/config or the `ssh.password` option.
55
+ The command used to test was:
56
+ #{runner.ssh_argv.join(' ')} true
57
+ #{stderr}
58
+ LONG
59
+ end
60
+ rescue UnmetPeerDependencyError => e
61
+ logger.error e.message
62
+ end
63
+
64
+ def check_remote_prerequisites!(name, runner)
65
+ missing = Wordmove::Prerequisites.missing_remotely(runner, Wordmove::Prerequisites::REMOTE_ALL)
66
+ if missing.empty?
67
+ logger.success "All required programs are available on \"#{name}\""
68
+ else
69
+ logger.error <<-LONG
70
+ Missing on "#{name}": #{Wordmove::Prerequisites.describe(missing)}.
71
+ rsync is needed for file sync; gzip, mysql/mariadb, mysqldump/mariadb-dump
72
+ and wp for database sync (wp runs search-replace on the remote after a push).
73
+ Programs must be in the PATH of a non interactive login shell.
74
+ LONG
75
+ end
76
+ rescue ShellCommandError => e
77
+ logger.error e.message
78
+ end
79
+
80
+ def warn_about_gateway_password(name, ssh_options)
81
+ gateway = ssh_options[:gateway]
82
+ return unless gateway.is_a?(Hash) && gateway[:password].present?
83
+
84
+ logger.warn "\"#{name}\" sets ssh.gateway.password, which cannot be used: the gateway " \
85
+ "is reached with `ssh -J`, so it must accept your key or ssh-agent. " \
86
+ "The password is ignored."
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,43 @@
1
+ module Wordmove
2
+ class Doctor
3
+ class Wpcli
4
+ attr_reader :logger
5
+
6
+ def initialize
7
+ @logger = Logger.new(STDOUT).tap { |l| l.level = Logger::INFO }
8
+ end
9
+
10
+ def check!
11
+ logger.task "Checking local wp-cli installation"
12
+
13
+ if in_path?
14
+ logger.success "wp-cli is correctly installed"
15
+
16
+ if up_to_date?
17
+ logger.success "wp-cli is up to date"
18
+ else
19
+ logger.error <<-LONG
20
+ wp-cli is not up to date.
21
+ Use `wp cli update` to update to the latest version.
22
+ LONG
23
+ end
24
+ else
25
+ logger.error <<-LONG
26
+ wp-cli is not installed (or not in your $PATH).
27
+ Read http://wp-cli.org/#installing for installation info.
28
+ LONG
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def in_path?
35
+ system('which wp', out: File::NULL)
36
+ end
37
+
38
+ def up_to_date?
39
+ `wp cli check-update --format=json --allow-root`.empty?
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,67 @@
1
+ module Wordmove
2
+ class Doctor
3
+ def self.start
4
+ banner
5
+ movefile
6
+ mysql
7
+ wpcli
8
+ rsync
9
+ ssh
10
+ end
11
+
12
+ def self.movefile
13
+ movefile_doctor = Wordmove::Doctor::Movefile.new
14
+ movefile_doctor.validate!
15
+ end
16
+
17
+ def self.mysql
18
+ mysql_doctor = Wordmove::Doctor::Mysql.new
19
+ mysql_doctor.check!
20
+ end
21
+
22
+ def self.wpcli
23
+ wpcli_doctor = Wordmove::Doctor::Wpcli.new
24
+ wpcli_doctor.check!
25
+ end
26
+
27
+ def self.rsync
28
+ rsync_doctor = Wordmove::Doctor::Rsync.new
29
+ rsync_doctor.check!
30
+ end
31
+
32
+ def self.ssh
33
+ ssh_doctor = Wordmove::Doctor::Ssh.new
34
+ ssh_doctor.check!
35
+ end
36
+
37
+ # rubocop:disable-next Metrics/MethodLength
38
+ def self.banner
39
+ paint = <<-'ASCII'
40
+ .------------------------.
41
+ | PSYCHIATRIC |
42
+ | HELP 5¢ |
43
+ |________________________|
44
+ || .-"""--. ||
45
+ || / \.-. ||
46
+ || | ._, \ ||
47
+ || \_/`-' '-.,_/ ||
48
+ || (_ (' _)') \ ||
49
+ || /| |\ ||
50
+ || | \ __ / | ||
51
+ || \_).,_____,/}/ ||
52
+ __||____;_--'___'/ ( ||
53
+ |\ || (__,\\ \_/------||
54
+ ||\||______________________||
55
+ |||| |
56
+ |||| THE DOCTOR |
57
+ \||| IS [IN] _____|
58
+ \|| (______)
59
+ `|___________________//||\\
60
+ //=||=\\
61
+ ` `` `
62
+ ASCII
63
+
64
+ puts paint
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,68 @@
1
+ module Wordmove
2
+ class EnvironmentsList
3
+ attr_reader :movefile, :logger, :remote_vhosts, :local_vhost
4
+
5
+ class << self
6
+ def print(cli_options)
7
+ new(cli_options).print
8
+ end
9
+ end
10
+
11
+ def initialize(options)
12
+ @logger = Logger.new(STDOUT).tap { |l| l.level = Logger::INFO }
13
+ @movefile = Wordmove::Movefile.new(options[:config])
14
+ @remote_vhosts = []
15
+ @local_vhost = []
16
+ end
17
+
18
+ def print
19
+ contents = parse_movefile(movefile: movefile)
20
+ generate_vhost_list(contents: contents)
21
+ output
22
+ end
23
+
24
+ private
25
+
26
+ def select_vhost(contents:)
27
+ target = contents.select { |_key, env| env[:vhost].present? }
28
+ target.map { |key, env| { env: key, vhost: env[:vhost] } }
29
+ end
30
+
31
+ def parse_movefile(movefile:)
32
+ movefile.fetch
33
+ end
34
+
35
+ def output
36
+ logger.task('Listing Local')
37
+ logger.plain(output_string(vhost_list: local_vhost))
38
+
39
+ logger.task('Listing Remotes')
40
+ logger.plain(output_string(vhost_list: remote_vhosts))
41
+ end
42
+
43
+ def output_string(vhost_list:)
44
+ return 'vhost list is empty' if vhost_list.empty?
45
+
46
+ vhost_list.each_with_object("") do |entry, retval|
47
+ retval << "#{entry[:env]}: #{entry[:vhost]}\n"
48
+ end
49
+ end
50
+
51
+ #
52
+ # return env, vhost map
53
+ # Exp. {:env=>:local, :vhost=>"http://vhost.local"},
54
+ # {:env=>:production, :vhost=>"http://example.com"}
55
+ #
56
+ def generate_vhost_list(contents:)
57
+ # select object which has 'vhost' only
58
+ vhosts = select_vhost(contents: contents)
59
+ vhosts.each do |list|
60
+ if list[:env] == :local
61
+ @local_vhost << list
62
+ else
63
+ @remote_vhosts << list
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,10 @@
1
+ module Wordmove
2
+ class UndefinedEnvironment < StandardError; end
3
+ class NoAdapterFound < StandardError; end
4
+ class MovefileNotFound < StandardError; end
5
+ class ShellCommandError < StandardError; end
6
+ class ImplementInSubclassError < StandardError; end
7
+ class UnmetPeerDependencyError < StandardError; end
8
+ class RemoteHookException < StandardError; end
9
+ class LocalHookException < StandardError; end
10
+ end
@@ -0,0 +1,50 @@
1
+ module Wordmove
2
+ module Generators
3
+ class Movefile < Thor::Group
4
+ include Thor::Actions
5
+ include MovefileAdapter
6
+
7
+ def self.source_root
8
+ File.dirname(__FILE__)
9
+ end
10
+
11
+ def copy_movefile
12
+ template "movefile.yml"
13
+ uncomment_local_database_connection_overrides
14
+ end
15
+
16
+ no_commands do
17
+ def uncomment_local_database_connection_overrides
18
+ content = File.read(movefile_path)
19
+ updated = uncomment_local_port(content)
20
+ updated = uncomment_local_socket(updated)
21
+ File.write(movefile_path, updated)
22
+ end
23
+
24
+ def movefile_path
25
+ File.join(destination_root, "movefile.yml")
26
+ end
27
+
28
+ def uncomment_local_port(content)
29
+ port = database.respond_to?(:port) ? database.port : nil
30
+ return content if port.to_s.empty?
31
+
32
+ content.sub(
33
+ /^(\s*)# port: 3306$/,
34
+ "\\1port: #{port}"
35
+ )
36
+ end
37
+
38
+ def uncomment_local_socket(content)
39
+ socket = database.respond_to?(:socket) ? database.socket : nil
40
+ return content if socket.to_s.empty?
41
+
42
+ content.sub(
43
+ %r{^(\s*)# socket: /path/to/mysql\.sock # optional unix socket path$},
44
+ "\\1socket: #{socket.inspect} # optional unix socket path"
45
+ )
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,104 @@
1
+ global:
2
+ # Put the target site in `wp maintenance-mode` while its database is replaced.
3
+ # Can also be forced with the WORDMOVE_MAINTENANCE_MODE=1 environment variable.
4
+ maintenance_mode: false
5
+
6
+ local:
7
+ vhost: <%= vhost %>
8
+ wordpress_path: "<%= wordpress_path %>" # use an absolute path here
9
+
10
+ database:
11
+ name: <%= database.name %>
12
+ user: <%= database.user %>
13
+ password: "<%= database.password %>" # could be blank, so always use quotes around
14
+ host: <%= database.host %>
15
+ # port: 3306
16
+ # socket: /path/to/mysql.sock # optional unix socket path
17
+
18
+ production:
19
+ vhost: http://example.com
20
+ wordpress_path: "/var/www/your_site" # use an absolute path here
21
+
22
+ database:
23
+ name: database_name
24
+ user: user
25
+ password: password
26
+ host: host
27
+ # port: 3308 # Use just in case you have exotic server config
28
+ # socket: /path/to/mysql.sock # Useful for local tunnels or unix socket setups
29
+ # mysqldump_options: '--max_allowed_packet=1G' # Only available if using SSH
30
+ # mysql_options: '--protocol=TCP' # mysql command is used to import db
31
+
32
+ exclude:
33
+ - '.git/'
34
+ - '.gitignore'
35
+ - '.gitmodules'
36
+ - '.env'
37
+ - 'node_modules/'
38
+ - 'bin/'
39
+ - 'tmp/*'
40
+ - 'Gemfile*'
41
+ - 'Movefile'
42
+ - 'movefile'
43
+ - 'movefile.yml'
44
+ - 'movefile.yaml'
45
+ - 'wp-config.php'
46
+ - 'wp-content/*.sql.gz'
47
+ - '*.orig'
48
+
49
+ # paths: # you can customize wordpress internal paths
50
+ # wp_content: wp-content
51
+ # uploads: wp-content/uploads
52
+ # plugins: wp-content/plugins
53
+ # mu_plugins: wp-content/mu-plugins
54
+ # themes: wp-content/themes
55
+ # languages: wp-content/languages
56
+
57
+ # ssh:
58
+ # host: host
59
+ # user: user
60
+ # password: password # optional; requires `sshpass`. Without it ssh runs in BatchMode with your keys/agent.
61
+ # port: 22 # Port is optional
62
+ # rsync_options: '--verbose --itemize-changes' # Additional rsync options, optional
63
+ # gateway: # Gateway is optional
64
+ # host: host
65
+ # user: user
66
+ # password: password # password is optional, will use public keys if available.
67
+
68
+
69
+ # hooks:
70
+ # push:
71
+ # before:
72
+ # - command: 'echo "do something"'
73
+ # where: local
74
+ # raise: false # raise is true by default
75
+ # after:
76
+ # - command: 'echo "do something"'
77
+ # where: remote
78
+ # pull:
79
+ # before:
80
+ # - command: 'echo "do something"'
81
+ # where: local
82
+ # raise: false
83
+ # after:
84
+ # - command: 'echo "do something"'
85
+ # where: remote
86
+ #
87
+ # forbid:
88
+ # push:
89
+ # db: false
90
+ # plugins: false
91
+ # themes: false
92
+ # languages: false
93
+ # uploads: false
94
+ # mu-plugins: false
95
+ # pull:
96
+ # db: false
97
+ # plugins: false
98
+ # themes: false
99
+ # languages: false
100
+ # uploads: false
101
+ # mu-plugins: false
102
+
103
+ # staging: # multiple environments can be specified
104
+ # [...]
@@ -0,0 +1,188 @@
1
+ require 'shellwords'
2
+
3
+ module Wordmove
4
+ module Generators
5
+ module MovefileAdapter
6
+ def vhost
7
+ VhostReader.config
8
+ end
9
+
10
+ def wordpress_path
11
+ File.expand_path(Dir.pwd)
12
+ end
13
+
14
+ def database
15
+ DBConfigReader.config
16
+ end
17
+ end
18
+
19
+ class DBConfigReader
20
+ def self.config
21
+ new.config
22
+ end
23
+
24
+ def config
25
+ OpenStruct.new(database_config)
26
+ end
27
+
28
+ def database_config
29
+ if wp_config_exists?
30
+ WordpressDBConfig.config
31
+ else
32
+ DefaultDBConfig.config
33
+ end
34
+ end
35
+
36
+ def wp_config_exists?
37
+ File.exist?(WordpressDirectory.default_path_for(:wp_config))
38
+ end
39
+ end
40
+
41
+ class DefaultDBConfig
42
+ def self.config
43
+ {
44
+ name: "database_name",
45
+ user: "user",
46
+ password: "password",
47
+ host: "127.0.0.1"
48
+ }
49
+ end
50
+ end
51
+
52
+ class VhostReader
53
+ def self.config
54
+ new.config
55
+ end
56
+
57
+ def config
58
+ wp_config_vhost || wp_cli_vhost || default_vhost
59
+ end
60
+
61
+ def default_vhost
62
+ "http://vhost.local"
63
+ end
64
+
65
+ def wordpress_path
66
+ File.expand_path(Dir.pwd)
67
+ end
68
+
69
+ def wp_config_exists?
70
+ File.exist?(WordpressDirectory.default_path_for(:wp_config))
71
+ end
72
+
73
+ def wp_config
74
+ @wp_config ||= File.read(
75
+ WordpressDirectory.default_path_for(:wp_config)
76
+ ).encode('utf-8', invalid: :replace)
77
+ end
78
+
79
+ def wp_config_vhost
80
+ return unless wp_config_exists?
81
+
82
+ %w[WP_HOME WP_SITEURL].each do |definition|
83
+ wp_config.match(wp_definition_regex(definition)) do |match|
84
+ value = match[:value].to_s.strip
85
+ return value unless value.empty?
86
+ end
87
+ end
88
+
89
+ nil
90
+ end
91
+
92
+ def wp_cli_vhost
93
+ return unless wp_config_exists?
94
+ return unless wp_in_path?
95
+
96
+ read_wp_option('home').presence || read_wp_option('siteurl').presence
97
+ end
98
+
99
+ def wp_in_path?
100
+ system('which wp > /dev/null 2>&1')
101
+ end
102
+
103
+ def read_wp_option(option)
104
+ command = [
105
+ "wp option get #{option}",
106
+ "--path=#{Shellwords.escape(wordpress_path)}",
107
+ "--allow-root",
108
+ "2>/dev/null"
109
+ ].join(' ')
110
+
111
+ `#{command}`.strip
112
+ end
113
+
114
+ def wp_definition_regex(definition)
115
+ /
116
+ ^\s*define\(
117
+ \s*['"]#{Regexp.escape(definition)}['"]
118
+ \s*,\s*
119
+ (?<quote>['"])
120
+ (?<value>(?:\\.|(?!\k<quote>).)*)
121
+ \k<quote>
122
+ \s*\)\s*;?
123
+ /x
124
+ end
125
+ end
126
+
127
+ class WordpressDBConfig
128
+ def self.config
129
+ new.config
130
+ end
131
+
132
+ def wp_config
133
+ @wp_config ||= File.read(
134
+ WordpressDirectory.default_path_for(:wp_config)
135
+ ).encode('utf-8', invalid: :replace)
136
+ end
137
+
138
+ def wp_definitions
139
+ {
140
+ name: 'DB_NAME',
141
+ user: 'DB_USER',
142
+ password: 'DB_PASSWORD',
143
+ host: 'DB_HOST'
144
+ }
145
+ end
146
+
147
+ def wp_definition_regex(definition)
148
+ /
149
+ ^\s*define\(
150
+ \s*['"]#{Regexp.escape(definition)}['"]
151
+ \s*,\s*
152
+ (?<quote>['"])
153
+ (?<value>(?:\\.|(?!\k<quote>).)*)
154
+ \k<quote>
155
+ \s*\)\s*;?
156
+ /x
157
+ end
158
+
159
+ def defaults
160
+ DefaultDBConfig.config.clone
161
+ end
162
+
163
+ def config
164
+ config = wp_definitions.each_with_object(defaults) do |(key, definition), result|
165
+ wp_config.match(wp_definition_regex(definition)) do |match|
166
+ result[key] = match[:value]
167
+ end
168
+ end
169
+
170
+ normalize_host_config(config)
171
+ end
172
+
173
+ def normalize_host_config(config)
174
+ host = config[:host].to_s
175
+
176
+ if host.match(%r{\A(?<hostname>[^:]+):(?<socket>/.+)\z})
177
+ config[:host] = Regexp.last_match[:hostname]
178
+ config[:socket] = Regexp.last_match[:socket]
179
+ elsif host.match(/\A(?<hostname>[^:]+):(?<port>\d+)\z/)
180
+ config[:host] = Regexp.last_match[:hostname]
181
+ config[:port] = Regexp.last_match[:port]
182
+ end
183
+
184
+ config
185
+ end
186
+ end
187
+ end
188
+ end
@@ -0,0 +1,37 @@
1
+ module Wordmove
2
+ class Guardian
3
+ attr_reader :movefile, :environment, :action, :logger
4
+
5
+ def initialize(options: nil, action: nil)
6
+ @movefile = Wordmove::Movefile.new(options[:config])
7
+ @environment = @movefile.environment(options).to_sym
8
+ @action = action
9
+ @logger = Logger.new(STDOUT, @movefile.secrets).tap { |l| l.level = Logger::DEBUG }
10
+ end
11
+
12
+ def allows(task)
13
+ if forbidden?(task)
14
+ logger.task("#{action.capitalize}ing #{task.capitalize}")
15
+ logger.warn("You tried to #{action} #{task}, but is forbidden by configuration. Skipping")
16
+ end
17
+
18
+ !forbidden?(task)
19
+ end
20
+
21
+ private
22
+
23
+ def forbidden?(task)
24
+ return false unless forbidden_tasks[task].present?
25
+
26
+ forbidden_tasks[task] == true
27
+ end
28
+
29
+ def forbidden_tasks
30
+ environment_options = movefile.fetch(false)[environment]
31
+ return {} unless environment_options.key?(:forbid)
32
+ return {} unless environment_options[:forbid].key?(action)
33
+
34
+ environment_options[:forbid][action]
35
+ end
36
+ end
37
+ end