wordmove-ng 6.0.3 → 6.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3b2ca340ce83622a7faa03d89c41573efb50933bd3c85f5427eac9f9ae07ee91
4
- data.tar.gz: 9183010aadcc92084a70839d140bd62604c8e4058aac8411b809f70558e14d7d
3
+ metadata.gz: dd4eb4e95fb86d7ce9727f7483018c1a8b11dfb05ea5bdaace45bbde8a3b7122
4
+ data.tar.gz: 88ff0153cf9147f4876dade9e035a8207a3396e06350dea1e51ad4e0c946fcbf
5
5
  SHA512:
6
- metadata.gz: 7411e375d8221a234dcb394ada25d0c3caa7e62225a328d5cd78d25f9d4a5be8887227844126d6045abd339b1c7c3b74966894d9fef5159c0e1fc1eab909a79e
7
- data.tar.gz: a1914ff1d4a9ed0b533588bd1aca0b13157576e0e4d731c326edaf54db72916524d2402460350b3d0315d3ca623acf870d57c800b04a6c21955cd301cc333952
6
+ metadata.gz: 21e75d8a0f362a0ad35610b544756d0324d7ac519a1a9bea065cb4549c971087ebd67928d78615522ecf9950f067ebea7f7aeddde085f692cacd4effe8f66a30
7
+ data.tar.gz: f99871ec8da25f7201894496dbe1d164b820e351f437f86f21c1623d68a95d9f61fcf3f0a7f498e22e15f03b69cdc6b9626f49624dee0b9ad8ec5c34552e5f98
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "6.0.3"
2
+ ".": "6.1.0"
3
3
  }
data/CHANGELOG.md CHANGED
@@ -5,6 +5,13 @@ All notable changes to wordmove-ng are documented here. The project follows
5
5
  are listed at https://github.com/welaika/wordmove/releases; the kokiddp fork's changes
6
6
  between 5.2.2 and this project are summarised under 6.0.0.
7
7
 
8
+ ## [6.1.0](https://github.com/tekgnosis-net/wordmove-ng/compare/v6.0.3...v6.1.0) (2026-10-06)
9
+
10
+
11
+ ### Features
12
+
13
+ * add the auto command that picks push or pull per component ([#9](https://github.com/tekgnosis-net/wordmove-ng/issues/9)) ([6733e70](https://github.com/tekgnosis-net/wordmove-ng/commit/6733e70c5e67b2628c6e8863fb1c6f85be714f13))
14
+
8
15
  ## [6.0.3](https://github.com/tekgnosis-net/wordmove-ng/compare/v6.0.2...v6.0.3) (2026-09-30)
9
16
 
10
17
 
data/CLAUDE.md CHANGED
@@ -86,7 +86,13 @@ Key pieces:
86
86
  - **`Prerequisites`** (`lib/wordmove/prerequisites.rb`): one `sh` probe listing missing
87
87
  programs; `DB_SOURCE`/`DB_TARGET`/`REMOTE_ALL` requirement sets shared by deployer and
88
88
  doctor.
89
+ - **`auto` command** (`lib/wordmove/auto_planner.rb` + `auto_command.rb`): per-component
90
+ direction from newest-mtime (directories) or latest post/comment (db) on both sides; core
91
+ never decided; plan-only unless `--apply`; exit 3 = pending moves. Specs stub `SshRunner`
92
+ and the planner's `local_shell`.
89
93
  - **Knobs**: `global.maintenance_mode` (env override `WORDMOVE_MAINTENANCE_MODE`),
94
+ `global.auto_window` / `global.auto_clock_skew_max` (`WORDMOVE_AUTO_WINDOW`,
95
+ `WORDMOVE_AUTO_CLOCK_SKEW_MAX`),
90
96
  `global.collation_fallbacks`, `global.charset_fallbacks`. Add new toggles as movefile keys
91
97
  with an env override and document them in README + CHANGELOG in the same commit.
92
98
 
data/README.md CHANGED
@@ -73,6 +73,10 @@ Component flags: `-w` core, `-u` uploads, `-t` themes, `-p` plugins, `-m` mu-plu
73
73
  `-l` languages, `-d` database, `--all` everything (`--all --no-uploads` to exclude one).
74
74
  `-s` simulates, `-e` picks the environment, `-c` points at another movefile.
75
75
 
76
+ `wordmove-ng auto -e production --all` compares each component on both sides by timestamp and
77
+ prints which direction it would move; add `--apply` to execute. See the
78
+ [usage page](https://tekgnosis-net.github.io/wordmove-ng/usage/#auto) for the signals and caveats.
79
+
76
80
  ## Peer dependencies
77
81
 
78
82
  wordmove-ng is orchestration glue around standard tools.
@@ -20,7 +20,7 @@ set -euo pipefail
20
20
  # ---- settings (override with environment variables) --------------------------
21
21
  SITE_DIR="${SITE_DIR:-/path/to/wordpress}" # directory holding movefile.yml
22
22
  ENVIRONMENT="${ENVIRONMENT:-production}" # remote environment (-e)
23
- ACTION="${ACTION:-pull}" # pull or push
23
+ ACTION="${ACTION:-pull}" # pull, push, or auto (plan only unless COMPONENTS adds --apply)
24
24
  COMPONENTS="${COMPONENTS:---all --no-db}" # component flags
25
25
  LOG_DIR="${LOG_DIR:-$HOME/log/wordmove}"
26
26
  KEEP_LOGS="${KEEP_LOGS:-30}" # how many run logs to keep
@@ -28,8 +28,8 @@ RUBY_VERSION="${RUBY_VERSION:-}" # e.g. ruby-3.4.9 for rvm; emp
28
28
  # -----------------------------------------------------------------------------
29
29
 
30
30
  case "$ACTION" in
31
- pull|push) ;;
32
- *) echo "ACTION must be pull or push, got '$ACTION'" >&2; exit 2 ;;
31
+ pull|push|auto) ;;
32
+ *) echo "ACTION must be pull, push or auto, got '$ACTION'" >&2; exit 2 ;;
33
33
  esac
34
34
 
35
35
  # Ruby version managers are not loaded by cron.
data/docs/automation.md CHANGED
@@ -140,6 +140,10 @@ tail -n 40 ~/log/wordmove/production-*.log
140
140
 
141
141
  Running `wordmove-ng doctor` under the same `env -i` prefix is the quickest check that the key and every tool resolve without an agent.
142
142
 
143
+ ## Drift reports with `auto`
144
+
145
+ `wordmove-ng auto -e production --all` without `--apply` compares every component on both sides and exits `3` when something has drifted, `0` when everything is in sync. Scheduled from cron with `MAILTO` set, that is a nightly "which side changed" report with no risk of moving anything: `ACTION=auto COMPONENTS="--all"` works with the shipped script since the exit code propagates. Add `--apply` only once you trust the signals for your site; see the warning on the [usage page]({{ site.baseurl }}/usage/#auto).
146
+
143
147
  ## Database jobs
144
148
 
145
149
  For a scheduled database push enable `global.maintenance_mode` so the seconds between import and `wp search-replace` show visitors the maintenance page instead of the wrong URLs. Scheduled database pulls are the common case (a nightly copy of production for development) and need nothing special; each run leaves a `local-backup-<timestamp>.sql.gz` in `wp-content/`, so prune those occasionally.
@@ -100,6 +100,8 @@ production:
100
100
  | `maintenance_mode` | `false` | Wrap the target's database import and adaptation in `wp maintenance-mode activate`/`deactivate`. `WORDMOVE_MAINTENANCE_MODE=1` in the environment forces it on for one run. |
101
101
  | `collation_fallbacks` | built-in table | Collations rewritten before import, e.g. `utf8mb3_uca1400_ai_ci: utf8mb4_unicode_ci`. Setting the key replaces the default table. |
102
102
  | `charset_fallbacks` | `utf8mb3: utf8mb4` | Charsets rewritten before import. |
103
+ | `auto_window` | `300` | `wordmove-ng auto`: timestamp differences up to this many seconds count as in sync. Env override `WORDMOVE_AUTO_WINDOW`. |
104
+ | `auto_clock_skew_max` | `60` | `wordmove-ng auto`: abort when local and remote clocks differ by more than this many seconds. Env override `WORDMOVE_AUTO_CLOCK_SKEW_MAX`. |
103
105
  | `sql_adapter` | ignored | Accepted with a warning for 5.x movefiles. |
104
106
 
105
107
  ## `local` and each remote
data/docs/quick-start.md CHANGED
@@ -72,6 +72,7 @@ A push of the database backs up the remote database to `wp-content/production-ba
72
72
  | `wordmove-ng push -e production --all --no-db` | mirror every folder, keep the production database |
73
73
  | `wordmove-ng push -e production --all --no-uploads` | everything but uploads |
74
74
  | `wordmove-ng pull -e production -d --no-adapt` | import the production database without rewriting URLs |
75
+ | `wordmove-ng auto -e production --all` | show which components are newer on which side, change nothing |
75
76
  | `wordmove-ng list` | show every environment and its vhost |
76
77
  | `wordmove-ng help push` | all flags |
77
78
 
data/docs/usage.md CHANGED
@@ -59,6 +59,47 @@ Options:
59
59
 
60
60
  `pull` copies from the remote given with `-e` to `local`; `push` copies from `local` to that remote. At least one component flag (or `--all`) is required.
61
61
 
62
+ ### `auto`
63
+ {: .no_toc }
64
+
65
+ Decides **per component** whether to push or pull, from timestamps on both sides, and prints a plan. Nothing changes unless you pass `--apply`.
66
+
67
+ ```bash
68
+ wordmove-ng auto -e production -t -p -u -d # print the plan
69
+ wordmove-ng auto -e production --all --apply -s # execute it as a dry run
70
+ wordmove-ng auto -e production --all --apply # execute it
71
+ ```
72
+
73
+ ```
74
+ ▬▬ Auto plan for "production" ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬
75
+ themes PUSH local 2026-10-06 09:12:40 remote 2026-10-01 18:03:11 local newer by 400769s
76
+ plugins SKIP local 2026-10-01 18:03:11 remote 2026-10-01 18:03:11 in sync (within 300s)
77
+ uploads PULL local 2026-09-28 11:20:02 remote 2026-10-06 08:55:30 remote newer by 682528s
78
+ db PULL local 2026-09-28 11:19:57 remote 2026-10-06 08:57:01 remote newer by 682624s
79
+ ℹ️ info | Plan only. Re-run with --apply to execute it (add -s for a dry run).
80
+ ```
81
+
82
+ How each side is measured:
83
+
84
+ | Component | Signal |
85
+ | --- | --- |
86
+ | uploads, themes, plugins, mu-plugins, languages | newest file modification time beneath the directory (`find … -printf '%T@'`) |
87
+ | database | latest of `MAX(post_modified_gmt)` and `MAX(comment_date_gmt)`, via `wp db query` on each side |
88
+ | core (`-w`) | never decided automatically; always `SKIP` |
89
+
90
+ Guard rails:
91
+
92
+ - Differences up to `global.auto_window` seconds (default 300) count as **in sync** and are skipped.
93
+ - Local and remote clocks are compared first; a skew above `global.auto_clock_skew_max` seconds (default 60) aborts, a smaller one is reported as a warning.
94
+ - A direction forbidden by the movefile shows as `BLOCKED` and is never executed.
95
+ - If a probe fails on either side the row is `ERROR` and nothing is applied, even with `--apply`.
96
+ - With `--apply`, pushes run first and pulls second, each wrapped in the usual `before`/`after` hooks, through exactly the same steps as `push` and `pull` (backups, prerequisites, `--simulate`).
97
+
98
+ Exit codes: `0` when nothing needs to move or after a successful `--apply`, `3` when the plan has pending moves and `--apply` was not given, `1` on any error. Cron can therefore run `auto` without `--apply` as a nightly "drift report" and treat `3` as "something to look at".
99
+
100
+ {: .warning }
101
+ Timestamps are evidence, not truth. A `git checkout` or an rsync without `--times` touches every file and makes that side look newer; a plugin auto-update on production happening after your local theme work makes production "newer" and would delete your work on `--apply`. Read the plan before applying it. The database signal only covers posts and comments; option and settings changes are invisible to it.
102
+
62
103
  ## Flags
63
104
 
64
105
  | Flag | Meaning |
@@ -78,6 +119,7 @@ Options:
78
119
  | `--no-adapt` | import the database without running `wp search-replace` |
79
120
  | `-v`, `--verbose` | more output |
80
121
  | `--debug` | accepted for compatibility; had an effect only with the removed FTP deployer |
122
+ | `--apply` | `auto` only: execute the plan instead of printing it |
81
123
 
82
124
  Component paths respect the `paths` section of each environment.
83
125
 
@@ -4,6 +4,10 @@ mapping:
4
4
  type: str
5
5
  maintenance_mode:
6
6
  type: bool
7
+ auto_window:
8
+ type: int
9
+ auto_clock_skew_max:
10
+ type: int
7
11
  collation_fallbacks:
8
12
  type: map
9
13
  mapping:
@@ -0,0 +1,43 @@
1
+ module Wordmove
2
+ # Helpers behind `wordmove-ng auto`, kept out of the Thor class so the
3
+ # command definitions stay readable.
4
+ module AutoCommand
5
+ private
6
+
7
+ def selected_tasks(options)
8
+ tasks = []
9
+ handle_options(options) { |task| tasks << task }
10
+ tasks
11
+ end
12
+
13
+ def print_auto_plan(rows, warnings, environment)
14
+ logger.task "Auto plan for \"#{environment}\""
15
+ warnings.each { |w| logger.warn(w) }
16
+ width = rows.map { |r| r.task.length }.max
17
+ rows.each do |row|
18
+ logger.plain format(
19
+ " %-#{width}s %-7s local %-20s remote %-20s %s",
20
+ row.task, row.direction.to_s.upcase, stamp(row.local_at), stamp(row.remote_at),
21
+ row.reason
22
+ )
23
+ end
24
+ end
25
+
26
+ def stamp(epoch)
27
+ epoch.nil? ? '-' : Time.at(epoch).strftime('%Y-%m-%d %H:%M:%S')
28
+ end
29
+
30
+ def apply_auto_plan(rows, options)
31
+ deployer = Wordmove::Deployer::Base.deployer_for(options.deep_symbolize_keys)
32
+ %i[push pull].each do |action|
33
+ tasks = rows.select { |r| r.direction == action }.map(&:task)
34
+ next if tasks.empty?
35
+
36
+ Wordmove::Hook.run(action, :before, options)
37
+ guardian = Wordmove::Guardian.new(options: options, action: action)
38
+ tasks.each { |task| deployer.send("#{action}_#{task}") if guardian.allows(task.to_sym) }
39
+ Wordmove::Hook.run(action, :after, options)
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,194 @@
1
+ require 'open3'
2
+ require 'shellwords'
3
+ require 'time'
4
+
5
+ module Wordmove
6
+ # Decides, per component, whether local or the remote holds the newer
7
+ # content, so `wordmove-ng auto` can push some components and pull others.
8
+ #
9
+ # Directories are compared by the newest file modification time beneath
10
+ # them; the database by the latest post or comment change on each side.
11
+ # Differences inside `global.auto_window` seconds are treated as in sync.
12
+ # The WordPress core is never decided automatically: its files change on
13
+ # every checkout and on every update, and mirroring it the wrong way is the
14
+ # most destructive mistake possible.
15
+ class AutoPlanner
16
+ class ClockSkewError < StandardError; end
17
+
18
+ Row = Struct.new(:task, :local_at, :remote_at, :direction, :reason) do
19
+ def pending?
20
+ %i[push pull].include?(direction)
21
+ end
22
+ end
23
+
24
+ DEFAULT_WINDOW = 300
25
+ DEFAULT_CLOCK_SKEW_MAX = 60
26
+ DIRECTORY_TASKS = %w[uploads themes plugins mu_plugins languages].freeze
27
+ EPOCH = /\A\d+(\.\d+)?\z/
28
+ TIMESTAMP = /\A\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\z/
29
+
30
+ attr_reader :warnings, :environment
31
+
32
+ def initialize(cli_options)
33
+ @cli_options = cli_options
34
+ @movefile = Wordmove::Movefile.new(cli_options[:config])
35
+ @movefile.load_dotenv(cli_options)
36
+ @options = @movefile.fetch(false)
37
+ @environment = @movefile.environment(cli_options)
38
+ @runner = SshRunner.new(@options[environment][:ssh])
39
+ @warnings = []
40
+ end
41
+
42
+ def self.pending?(rows)
43
+ rows.any?(&:pending?)
44
+ end
45
+
46
+ # Returns one Row per task, in the given order.
47
+ def plan(tasks)
48
+ check_clock_skew!
49
+ tasks.map { |task| plan_task(task) }
50
+ end
51
+
52
+ def window
53
+ knob('WORDMOVE_AUTO_WINDOW', :auto_window, DEFAULT_WINDOW)
54
+ end
55
+
56
+ def clock_skew_max
57
+ knob('WORDMOVE_AUTO_CLOCK_SKEW_MAX', :auto_clock_skew_max, DEFAULT_CLOCK_SKEW_MAX)
58
+ end
59
+
60
+ private
61
+
62
+ def plan_task(task)
63
+ return Row.new(task, nil, nil, :skip, 'core is never decided automatically') if core?(task)
64
+
65
+ local_at, local_err = probe(:local, task)
66
+ remote_at, remote_err = probe(:remote, task)
67
+ error = probe_error(local_err, remote_err)
68
+ return Row.new(task, local_at, remote_at, :error, error) if error
69
+
70
+ direction, reason = decide(local_at, remote_at)
71
+ direction, reason = apply_forbid(task, direction, reason)
72
+ Row.new(task, local_at, remote_at, direction, reason)
73
+ end
74
+
75
+ def core?(task)
76
+ task == 'wordpress'
77
+ end
78
+
79
+ def probe_error(local_err, remote_err)
80
+ return "local probe failed: #{local_err}" if local_err
81
+ return "remote probe failed: #{remote_err}" if remote_err
82
+
83
+ nil
84
+ end
85
+
86
+ def decide(local_at, remote_at)
87
+ return [:skip, 'both sides empty'] if local_at.nil? && remote_at.nil?
88
+ return [:push, 'remote empty'] if remote_at.nil?
89
+ return [:pull, 'local empty'] if local_at.nil?
90
+
91
+ delta = (remote_at - local_at).to_i
92
+ return [:skip, "in sync (within #{window}s)"] if delta.abs <= window
93
+
94
+ delta.positive? ? [:pull, "remote newer by #{delta}s"] : [:push, "local newer by #{-delta}s"]
95
+ end
96
+
97
+ def apply_forbid(task, direction, reason)
98
+ return [direction, reason] unless %i[push pull].include?(direction)
99
+
100
+ forbidden = @options.dig(environment, :forbid, direction, task.to_sym) == true
101
+ return [direction, reason] unless forbidden
102
+
103
+ [:blocked, "#{reason}; #{direction} forbidden by movefile"]
104
+ end
105
+
106
+ # Returns [epoch_or_nil, error_or_nil]. Probe output comes from a remote
107
+ # host and is treated as untrusted: anything that is not a plain number or
108
+ # timestamp is reported as an error rather than interpreted.
109
+ def probe(side, task)
110
+ command = task == 'db' ? db_command(side) : dir_command(side, task)
111
+ stdout, stderr, code = execute(side, command)
112
+ return [nil, stderr.to_s.strip.empty? ? "exit #{code}" : stderr.strip] unless code.zero?
113
+
114
+ [task == 'db' ? parse_db(stdout) : parse_epoch(stdout), nil]
115
+ rescue ArgumentError => e
116
+ [nil, "unparseable output: #{e.message}"]
117
+ end
118
+
119
+ def dir_command(side, task)
120
+ path = WordpressDirectory.new(task.to_sym, side_options(side)).path
121
+ # Newest regular file mtime under the directory; empty output if none.
122
+ "find #{Shellwords.escape(path)} -type f -printf '%T@\\n' 2>/dev/null | sort -n | tail -1"
123
+ end
124
+
125
+ # Latest post and comment change, one per line. The table prefix is read
126
+ # with `wp db prefix` so custom prefixes work; wp handles the credentials.
127
+ def db_command(side)
128
+ path = Shellwords.escape(side_options(side)[:wordpress_path])
129
+ prefix = "$(wp db prefix --path=#{path} --allow-root)"
130
+ sql = "SELECT MAX(post_modified_gmt) FROM #{prefix}posts " \
131
+ "UNION ALL SELECT MAX(comment_date_gmt) FROM #{prefix}comments"
132
+ "wp db query \"#{sql}\" --skip-column-names --path=#{path} --allow-root"
133
+ end
134
+
135
+ def parse_epoch(stdout)
136
+ value = stdout.to_s.strip
137
+ return nil if value.empty?
138
+
139
+ expect_format!(value, EPOCH, 'an epoch')
140
+ value.to_f
141
+ end
142
+
143
+ def expect_format!(value, pattern, what)
144
+ return if value.match?(pattern)
145
+
146
+ raise ArgumentError, "expected #{what}, got #{value.inspect[0, 40]}"
147
+ end
148
+
149
+ def parse_db(stdout)
150
+ stamps = stdout.to_s.lines.map(&:strip).reject { |l| l.empty? || l == 'NULL' }
151
+ return nil if stamps.empty?
152
+
153
+ stamps.map do |stamp|
154
+ expect_format!(stamp, TIMESTAMP, 'a timestamp')
155
+ Time.parse("#{stamp} UTC").to_i
156
+ end.max
157
+ end
158
+
159
+ def check_clock_skew!
160
+ local_out, local_err, local_code = local_shell('date +%s')
161
+ rout, rerr, rcode = @runner.run('date +%s')
162
+ raise ClockSkewError, "could not read the local clock: #{local_err}" unless local_code.zero?
163
+ raise ClockSkewError, "could not read the remote clock: #{rerr}" unless rcode.zero?
164
+
165
+ skew = (rout.to_i - local_out.to_i).abs
166
+ if skew > clock_skew_max
167
+ raise ClockSkewError,
168
+ "clock skew of #{skew}s between local and \"#{environment}\" exceeds the " \
169
+ "maximum of #{clock_skew_max}s; fix the clocks (ntp) before trusting timestamps"
170
+ end
171
+ @warnings << "clock skew of #{skew}s between local and \"#{environment}\"" if skew.positive?
172
+ end
173
+
174
+ def execute(side, command)
175
+ side == :local ? local_shell(command) : @runner.run(command)
176
+ end
177
+
178
+ def local_shell(command)
179
+ stdout, stderr, status = Open3.capture3('sh', '-c', command)
180
+ [stdout, stderr, status.exitstatus]
181
+ end
182
+
183
+ def side_options(side)
184
+ side == :local ? @options[:local] : @options[environment]
185
+ end
186
+
187
+ def knob(env_name, key, default)
188
+ raw = ENV.fetch(env_name, nil)
189
+ raw = @options.dig(:global, key) if raw.nil? || raw.to_s.strip.empty?
190
+ value = raw.nil? ? default : raw.to_i
191
+ value.clamp(0, 86_400 * 30)
192
+ end
193
+ end
194
+ end
data/lib/wordmove/cli.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  module Wordmove
2
2
  class CLI < Thor
3
+ include AutoCommand
4
+
3
5
  map %w[--version -v] => :__print_version
4
6
 
5
7
  desc "--version, -v", "Print the version"
@@ -104,6 +106,58 @@ module Wordmove
104
106
  Wordmove::Hook.run(:pull, :after, options)
105
107
  end
106
108
 
109
+ desc "auto", "Decides per component whether to push or pull, from timestamps on both sides"
110
+ long_desc <<-LONGDESC
111
+ Compares each selected component on local and on the remote (newest file
112
+ modification time for directories, latest post or comment change for the
113
+ database) and proposes a direction per component. The WordPress core is
114
+ never decided automatically.
115
+
116
+ Without --apply only the plan is printed and nothing changes; the exit
117
+ code is 3 when at least one component would move, 0 when everything is in
118
+ sync. With --apply the plan is executed through the regular push and pull
119
+ steps, honouring --simulate, forbid rules and hooks.
120
+ LONGDESC
121
+ shared_options.each do |option, args|
122
+ method_option option, args
123
+ end
124
+ method_option :apply, type: :boolean, default: false,
125
+ desc: "Execute the plan instead of only printing it"
126
+ # rubocop:disable-next Metrics/MethodLength
127
+ def auto
128
+ ensure_wordpress_options_presence!(options)
129
+ begin
130
+ planner = Wordmove::AutoPlanner.new(options.deep_symbolize_keys)
131
+ rows = planner.plan(selected_tasks(options))
132
+ rescue MovefileNotFound, Wordmove::AutoPlanner::ClockSkewError,
133
+ UndefinedEnvironment, UnmetPeerDependencyError => e
134
+ logger.error(e.message)
135
+ exit 1
136
+ rescue Psych::SyntaxError => e
137
+ logger.error("Your movefile is not parsable due to a syntax error: #{e.message}")
138
+ exit 1
139
+ end
140
+
141
+ print_auto_plan(rows, planner.warnings, planner.environment)
142
+
143
+ if rows.any? { |r| r.direction == :error }
144
+ logger.error "Some components could not be compared; nothing was changed."
145
+ exit 1
146
+ end
147
+
148
+ unless Wordmove::AutoPlanner.pending?(rows)
149
+ logger.success "Nothing to do."
150
+ return
151
+ end
152
+
153
+ unless options[:apply]
154
+ logger.info "Plan only. Re-run with --apply to execute it (add -s for a dry run)."
155
+ exit 3
156
+ end
157
+
158
+ apply_auto_plan(rows, options)
159
+ end
160
+
107
161
  desc "push", "Pushes WP data from local machine to remote host"
108
162
  shared_options.each do |option, args|
109
163
  method_option option, args
@@ -2,6 +2,11 @@ global:
2
2
  # Put the target site in `wp maintenance-mode` while its database is replaced.
3
3
  # Can also be forced with the WORDMOVE_MAINTENANCE_MODE=1 environment variable.
4
4
  maintenance_mode: false
5
+ # `wordmove-ng auto`: timestamp differences up to this many seconds count as "in sync",
6
+ # and a larger clock skew between local and the remote aborts the comparison.
7
+ # Env overrides: WORDMOVE_AUTO_WINDOW, WORDMOVE_AUTO_CLOCK_SKEW_MAX.
8
+ # auto_window: 300
9
+ # auto_clock_skew_max: 60
5
10
 
6
11
  local:
7
12
  vhost: <%= vhost %>
@@ -1,3 +1,3 @@
1
1
  module Wordmove
2
- VERSION = "6.0.3".freeze
2
+ VERSION = "6.1.0".freeze
3
3
  end
data/lib/wordmove.rb CHANGED
@@ -22,6 +22,8 @@ require 'yaml'
22
22
 
23
23
  require 'wordmove/ssh_runner'
24
24
  require 'wordmove/prerequisites'
25
+ require 'wordmove/auto_planner'
26
+ require 'wordmove/auto_command'
25
27
  require 'photocopier'
26
28
 
27
29
  require 'wordmove/cli'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: wordmove-ng
3
3
  version: !ruby/object:Gem::Version
4
- version: 6.0.3
4
+ version: 6.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kumar
@@ -275,6 +275,8 @@ files:
275
275
  - lib/wordmove/assets/wordmove_schema_global.yml
276
276
  - lib/wordmove/assets/wordmove_schema_local.yml
277
277
  - lib/wordmove/assets/wordmove_schema_remote.yml
278
+ - lib/wordmove/auto_command.rb
279
+ - lib/wordmove/auto_planner.rb
278
280
  - lib/wordmove/cli.rb
279
281
  - lib/wordmove/deployer/base.rb
280
282
  - lib/wordmove/deployer/ssh.rb