wordmove-ng
wordmove-ng moves WordPress sites between environments. One command pushes or pulls the core, uploads, themes, plugins, mu-plugins, languages and the database between a local install and any number of remote hosts over SSH, rewriting URLs and paths on the target with wp-cli.
It is the maintained, independent successor of Wordmove by weLaika, continued through the kokiddp fork. The movefile format, commands and flags are unchanged. What changed is under the hood, and in a few places where the old behaviour was unsafe. See Upgrading from wordmove 5.x.
Full documentation: https://tekgnosis-net.github.io/wordmove-ng/
Highlights
-
No Ruby-side SSH. Every remote operation goes through the system
ssh,scpandrsync, so your agent,~/.ssh/config, jump hosts and modern key types all just work, and nothing breaks on a new OpenSSL or Ruby. -
The source database is never written. Push and pull both dump the source, import on
the target and run
wp search-replaceon the target. -
Fails before it breaks anything. Prerequisites on both sides are probed before any
backup, dump or import.
wordmove-ng doctorchecks SSH authentication and remote programs per environment. - Optional maintenance mode around the target's database replacement.
- MariaDB first (
mariadb,mariadb-dump) with MySQL fallback, sandbox-header handling, collation and charset normalisation, socket and port support. - Runs on Ruby 3.0 through 4.0.
Installation
Ruby 3.0 or newer is required. If your system Ruby is older or missing, the docs walk through installing Ruby with your package manager, rbenv, RVM, mise or Docker.
gem install wordmove-ngTo run the latest master instead of a release:
gem install specific_install
gem specific_install https://github.com/tekgnosis-net/wordmove-ng.gitFrom a checkout:
bundle install
bin/wordmove-ng --helpQuick start
cd /path/to/your/wordpress # where wp-config.php lives
wordmove-ng init # writes movefile.yml, pre-filled from wp-config.php
wordmove-ng doctor # validates movefile.yml, local tools, SSH auth, remote tools
wordmove-ng pull -e staging --all
wordmove-ng push -e production -t -p # themes and plugins only
wordmove-ng push -e production -d -s # simulate a database pushRun wordmove-ng help for all commands and wordmove-ng help push for the flags.
Component flags: -w core, -u uploads, -t themes, -p plugins, -m mu-plugins,
-l languages, -d database, --all everything (--all --no-uploads to exclude one).
-s simulates, -e picks the environment, -c points at another movefile.
wordmove-ng auto -e production --all compares each component on both sides by timestamp and
prints which direction it would move; add --apply to execute. See the
usage page for the signals and caveats.
Peer dependencies
wordmove-ng is orchestration glue around standard tools.
Locally
| Program | Needed for |
|---|---|
ssh, scp, rsync
|
every remote operation |
sshpass |
only when ssh.password is set in the movefile |
gzip |
database sync |
mysqldump or mariadb-dump
|
push -d (dumping the local database) |
mysql or mariadb
|
pull -d (importing into the local database) and doctor
|
wp (WP-CLI) |
pull -d (adapting the local database) |
On each remote, in the $PATH of a non interactive login
| Program | Needed for |
|---|---|
rsync, gzip
|
file and database sync |
mysqldump or mariadb-dump
|
pull -d |
mysql or mariadb, wp
|
push -d |
wordmove-ng doctor reports exactly what is missing where, and every database operation
re-checks before doing anything.
SSH authentication
Everything uses the system ssh client. When no ssh.password is configured, connections run
with BatchMode=yes: a failing key authentication is reported as an error with the exact
command that failed, never an interactive password prompt.
-
ssh.host,ssh.user,ssh.portmap to the obvious ssh options. A host configured in~/.ssh/configworks with onlyssh.hostset. -
ssh.passwordis passed throughsshpasstossh,scpandrsync. Keys are strongly preferred. -
ssh.gatewaybecomesssh -J. The jump host must accept your key or agent; agateway.passwordcannot be honoured and is ignored (doctor warns). -
ssh.rsync_optionsare appended to the rsync command.
Remote commands always run through sh -c, so the remote login shell may be bash, zsh,
fish or csh.
Database sync
Both directions have the same shape: dump the source, import the dump on the target, run
wp search-replace on the target for vhost and wordpress_path. The source database is
only ever read.
-
wordmove-ng pull -d: back up the local database, dump the remote, import locally, search-replace locally. -
wordmove-ng push -d: back up the remote database, dump locally, import on the remote, search-replace on the remote over SSH.
Backups are written to the local wp-content/ directory as timestamped .sql.gz files
before any import. If the remote adaptation fails after the import, the log names the
backup to restore from.
wp search-replace runs with --all-tables --skip-columns=guid, so every table in the
target database is adapted, including non WordPress tables sharing it, and post GUIDs are
left alone as WordPress recommends. --no-adapt skips the search-replace entirely.
If one of the four search terms is a prefix of another (for example a local vhost of
https://site.test and a remote of https://site.test.example.com) the shorter replacement
also rewrites the longer value. Doctor reports this as an error and the DB step warns.
Maintenance mode
global:
maintenance_mode: truewraps the target's import and search-replace in wp maintenance-mode activate and
deactivate, so visitors see the WordPress maintenance page rather than a site pointing at
the other environment's URLs for a few seconds. Deactivation runs even when the adaptation
fails. WORDMOVE_MAINTENANCE_MODE=1 forces it on for a single run. Default: off.
MariaDB and MySQL compatibility
-
mariadbandmariadb-dumpare preferred when present, falling back tomysqlandmysqldump. - Dumps starting with the MariaDB sandbox header (
/*!999999- enable the sandbox mode */) import cleanly on older servers. - Imports run with
--binary-mode,SET FOREIGN_KEY_CHECKS=0and a trailingCOMMIT;, unlessmysql_optionsalready sets binary mode. -
database.socketanddatabase.portare first-class;--socketinsidemysql_optionsormysqldump_optionsstill works. - Newer
utf8mb3collations are rewritten toutf8mb4_unicode_ciandutf8mb3toutf8mb4before import. Override or extend the mappings:
global:
collation_fallbacks:
utf8mb3_uca1400_ai_ci: utf8mb4_unicode_ci
charset_fallbacks:
utf8mb3: utf8mb4-
wpis always called with--allow-root, so root-owned Docker installs work.
movefile.yml
global:
maintenance_mode: false
local:
vhost: http://vhost.local
wordpress_path: /home/john/sites/your_site
database:
name: database_name
user: user
password: password
host: localhost
# port: 3306
# socket: /path/to/mysql.sock
production:
vhost: https://example.com
wordpress_path: /var/www/your_site
database:
name: database_name
user: user
password: password
host: host
# mysqldump_options: --max_allowed_packet=50MB
# mysql_options: --protocol=TCP
exclude:
- ".git/"
- ".env"
- "node_modules/"
- "wp-config.php"
- "wp-content/*.sql.gz"
ssh:
host: host
user: user
# port: 22
# password: only with sshpass installed; prefer keys
# rsync_options: --verbose
# gateway:
# host: bastion.example.com
# user: jump
# forbid:
# push:
# db: true # never push the database to production
# hooks:
# push:
# before:
# - command: echo "about to push"
# where: local
# after:
# - command: wp cache flush
# where: remote
# raise: falseEvery first-level key other than global and local is a remote environment; pick one with
-e. movefile.yml is evaluated as ERB, so secrets can come from the environment or from
a .env / .env.<environment> file next to it:
production:
database:
password: "<%= ENV['PROD_DB_PASS'] %>"File sync mirrors the source: files missing on the source are deleted on the destination.
Put anything you need to keep in exclude, which is always relative to wordpress_path.
Scheduled syncs from cron or CI work out of the box; contrib/wordmove-sync.sh handles the
environment cron lacks (Ruby version manager, PATH, no agent, no TTY, overlap lock, logs).
See Automation and cron.
The documentation site has the full reference: Configuration, Usage and flags, Database sync, Hooks, Multiple environments, Automation and cron, Troubleshooting.
Logging
Long generated shell scripts are summarised into intent lines such as
dump database my_db to ./wp-content/dump.sql, compress ./wp-content/dump.sql,
wp search-replace old.test -> new.test in /var/www/site and
import SQL dump ... (strip sandbox header, append COMMIT). Passwords, hosts, vhosts and
paths from the movefile are masked as [secret] in all output, including hook output.
Upgrading from wordmove 5.x
This applies to both the original wordmove gem and the kokiddp fork. Your movefile.yml
keeps working with the exceptions below.
-
Install the new gem and use the new command.
gem install wordmove-ng, then callwordmove-nginstead ofwordmove. Both gems can stay installed side by side. - Ruby 3.0 or newer. Ruby 2.6 and 2.7 are no longer supported.
-
Put
wpon your remotes.push -dnow adapts the database on the remote, so WP-CLI must be installed there and be in the$PATHof a non interactive login. It is a single PHAR file: installing WP-CLI. Runwordmove-ng doctorto confirm. -
FTP and SFTP are gone. A movefile with an
ftpblock fails with a clear error. FTP only ever existed for shell-less shared hosts, where the database had to be handled by uploading temporary PHP scripts and URLs were rewritten in the dump with a regex. Keep the legacywordmove5.x gem for such hosts, or move them to SSH. -
global.sql_adapteris ignored. URL and path adaptation always uses wp-cli on the target. The key is still accepted, with a warning, so old movefiles validate; remove it when convenient.database.charset(removed in 3.0) is no longer accepted by the schema. -
Push no longer touches your local database. Old versions ran
wp search-replaceon the live local database during a push and restored it afterwards, which left the local site pointing at production if anything failed in between. That is gone. -
SSH behaves like rsync did. If rsync worked but the database step asked for a
password, the cause was the old Net::SSH library, which could not sign RSA keys with
SHA-2. That path no longer exists. If key authentication fails now, you get an error with
the failing
sshcommand instead of a prompt.ssh.gateway.passwordis ignored. -
Remote programs are checked up front. Missing
gzip,mysql,mysqldumporwpon either side aborts before any backup or dump, listing what is missing. -
New, optional:
global.maintenance_mode(see above).
Contributing
bundle install
bundle exec rake # specs + rubocop, what CI runs
bundle exec rspec spec/deployer/ssh_db_spec.rb # one fileCI runs on Ruby 3.0, 3.1, 3.2, 3.3, 3.4 and 4.0. Please keep this README updated when
changing user-facing behaviour, and write Conventional Commits
(feat:, fix:, feat!: for breaking changes): the changelog and version number are
generated from them. See CONTRIBUTING.md.
Releasing
Releases are automated by release-please and published to rubygems.org with trusted publishing, so no API key is stored anywhere.
- Every push to
masterupdates a "release PR" that bumpslib/wordmove/version.rbandCHANGELOG.mdaccording to the commits since the last release. - Merging that PR creates the
vX.Y.Ztag and the GitHub release. - The tag push triggers the
publishjob in.github/workflows/release.yml, which runs the test suite on the tagged commit, builds the gem, pushes it to rubygems.org and attaches the.gemfile to the GitHub release.
Pushing a v* tag by hand at the head of master goes through the same publish job; that
is how 6.0.0 was cut. Merge the release PR only when there is something worth shipping:
release-please keeps proposing a patch release for any Conventional Commit, including
ci: and docs: ones.
One-time setup for a new maintainer or a fork:
- On rubygems.org, under your profile's Trusted publishers, add a pending publisher
for gem
wordmove-ng, repository ownertekgnosis-net, repositorywordmove-ng, workflowrelease.yml, environmentrelease. It becomes a regular publisher, and you the gem owner, after the first push. - In the GitHub repository settings create an environment named
release, and under Actions → General enable "Allow GitHub Actions to create and approve pull requests" so release-please can open its PR. - Create a fine-grained personal access token scoped to this repository with Contents and
Pull requests set to read and write, and store it as the repository secret
RELEASE_PLEASE_TOKEN. Pull requests opened with the built-in Actions token never trigger other workflows, so without it the release PR cannot satisfy the requiredtestchecks on the protectedmasterbranch.
Credits and licence
- Wordmove was created and maintained for a decade by weLaika: Stefano Verna, Ju Liu, Fabrizio Monti, Alessandro Fazzi, Filippo Gangi Dino and many contributors. The workflow, movefile format and most of the code are theirs.
- kokiddp kept it running on modern Ruby, OpenSSL 3 and MariaDB and added socket, collation and Docker support.
- wordmove-ng is maintained by tekgnosis.net.
MIT licence, see LICENSE.
