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 +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +7 -0
- data/CLAUDE.md +6 -0
- data/README.md +4 -0
- data/contrib/wordmove-sync.sh +3 -3
- data/docs/automation.md +4 -0
- data/docs/configuration.md +2 -0
- data/docs/quick-start.md +1 -0
- data/docs/usage.md +42 -0
- data/lib/wordmove/assets/wordmove_schema_global.yml +4 -0
- data/lib/wordmove/auto_command.rb +43 -0
- data/lib/wordmove/auto_planner.rb +194 -0
- data/lib/wordmove/cli.rb +54 -0
- data/lib/wordmove/generators/movefile.yml +5 -0
- data/lib/wordmove/version.rb +1 -1
- data/lib/wordmove.rb +2 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dd4eb4e95fb86d7ce9727f7483018c1a8b11dfb05ea5bdaace45bbde8a3b7122
|
|
4
|
+
data.tar.gz: 88ff0153cf9147f4876dade9e035a8207a3396e06350dea1e51ad4e0c946fcbf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 21e75d8a0f362a0ad35610b544756d0324d7ac519a1a9bea065cb4549c971087ebd67928d78615522ecf9950f067ebea7f7aeddde085f692cacd4effe8f66a30
|
|
7
|
+
data.tar.gz: f99871ec8da25f7201894496dbe1d164b820e351f437f86f21c1623d68a95d9f61fcf3f0a7f498e22e15f03b69cdc6b9626f49624dee0b9ad8ec5c34552e5f98
|
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.
|
data/contrib/wordmove-sync.sh
CHANGED
|
@@ -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
|
|
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
|
|
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.
|
data/docs/configuration.md
CHANGED
|
@@ -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
|
|
|
@@ -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 %>
|
data/lib/wordmove/version.rb
CHANGED
data/lib/wordmove.rb
CHANGED
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
|
|
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
|