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.
- checksums.yaml +7 -0
- data/.github/ISSUE_TEMPLATE/bug_report.md +40 -0
- data/.github/ISSUE_TEMPLATE/config.yml +11 -0
- data/.github/stale.yml +18 -0
- data/.github/workflows/release.yml +100 -0
- data/.github/workflows/ruby.yml +41 -0
- data/.gitignore +21 -0
- data/.release-please-manifest.json +3 -0
- data/.rspec +2 -0
- data/.rubocop.yml +50 -0
- data/.rubocop_todo.yml +89 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.vscode/launch.json +73 -0
- data/CHANGELOG.md +78 -0
- data/CLAUDE.md +114 -0
- data/CONTRIBUTING.md +114 -0
- data/Gemfile +14 -0
- data/LICENSE +22 -0
- data/README.md +340 -0
- data/Rakefile +7 -0
- data/assets/images/wordmove-ng.png +0 -0
- data/bin/bundle +105 -0
- data/bin/bundler +17 -0
- data/bin/byebug +29 -0
- data/bin/coderay +29 -0
- data/bin/console +16 -0
- data/bin/htmldiff +29 -0
- data/bin/kwalify +29 -0
- data/bin/ldiff +29 -0
- data/bin/pry +29 -0
- data/bin/rake +29 -0
- data/bin/rspec +29 -0
- data/bin/rubocop +29 -0
- data/bin/ruby-parse +29 -0
- data/bin/ruby-rewrite +29 -0
- data/bin/rumoji +29 -0
- data/bin/setup +7 -0
- data/bin/thor +29 -0
- data/bin/wordmove-ng +10 -0
- data/deploy/deploy.sh +3 -0
- data/exe/wordmove-ng +6 -0
- data/lib/wordmove/assets/wordmove_schema_global.yml +16 -0
- data/lib/wordmove/assets/wordmove_schema_local.yml +31 -0
- data/lib/wordmove/assets/wordmove_schema_remote.yml +167 -0
- data/lib/wordmove/cli.rb +134 -0
- data/lib/wordmove/deployer/base.rb +356 -0
- data/lib/wordmove/deployer/ssh.rb +339 -0
- data/lib/wordmove/doctor/movefile.rb +112 -0
- data/lib/wordmove/doctor/mysql.rb +136 -0
- data/lib/wordmove/doctor/rsync.rb +27 -0
- data/lib/wordmove/doctor/ssh.rb +90 -0
- data/lib/wordmove/doctor/wpcli.rb +43 -0
- data/lib/wordmove/doctor.rb +67 -0
- data/lib/wordmove/environments_list.rb +68 -0
- data/lib/wordmove/exceptions.rb +10 -0
- data/lib/wordmove/generators/movefile.rb +50 -0
- data/lib/wordmove/generators/movefile.yml +104 -0
- data/lib/wordmove/generators/movefile_adapter.rb +188 -0
- data/lib/wordmove/guardian.rb +37 -0
- data/lib/wordmove/hook.rb +128 -0
- data/lib/wordmove/logger.rb +165 -0
- data/lib/wordmove/movefile.rb +129 -0
- data/lib/wordmove/prerequisites.rb +57 -0
- data/lib/wordmove/sql_adapter/wpcli.rb +72 -0
- data/lib/wordmove/ssh_runner.rb +110 -0
- data/lib/wordmove/version.rb +3 -0
- data/lib/wordmove/wordpress_directory/path.rb +11 -0
- data/lib/wordmove/wordpress_directory.rb +41 -0
- data/lib/wordmove-ng.rb +4 -0
- data/lib/wordmove.rb +51 -0
- data/release-please-config.json +25 -0
- data/wordmove-ng.gemspec +62 -0
- 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
|