Workshop · back to the section

Four WP-CLI deploy traps that fail silently

A failure with an error message is the good case: at least you know something is wrong. These four say nothing — the command succeeds, and afterwards something still isn't right.

All four come from an afternoon lost to them. None is exotic: anyone moving WordPress sites runs into them sooner or later. What they share is that the output is green, the exit code is zero, the log is empty — and the result is wrong anyway.

The shape of a silent failure The command runs successfully and returns zero, but the result is wrong — the failure only surfaces later, with the user. command runs exit code: 0 the result is still wrong the failure only surfaces with the visitor
  1. 01 command runs
  2. 02 exit code: 0
  3. 03 the result is still wrong

the failure only surfaces with the visitor

One — the escaped URL that search doesn't find

A compulsory step of any move is swapping the domain in the database. That's what wp search-replace is for, and it handles serialised data correctly — which is exactly why people rely on it.

Sites built with a page builder, however, store their layout as JSON in a meta field. In JSON a slash may be escaped, and the builder does escape it: URLs don't appear in their familiar form but with escaped slashes. The search therefore misses them — while replacing flawlessly everywhere else on the site.

The result is a site that mostly works. The text is fine, the menu is fine; only the images and videos placed with the builder still point at the old address. This is the failure that slips through a review most easily, because people check the top of the page, not the sixth section.

The fix is simple once you know: run the same replacement against the escaped form too. It's worth adding the urlencoded and double-encoded variants as well — those turn up in redirects and embedded links.

Two — the permission that travels through the archive

This is the most insidious of the four, because every check passes it.

tar carries not only the files but their permissions. If an image entered the project on the development machine with narrow permissions — typically because it was copied in from a download — then it stays exactly as narrow on the server. The web server's user cannot read it.

What happens then is genuinely misleading. The file is in place. Its contents are byte-identical. The checksum matches. A “what's missing” comparison returns an empty list. The page responds with HTTP 200. Only the visitor sees a broken image.

scp doesn't save you either: overwriting an existing file keeps the previous permissions. Readability therefore has to be set explicitly on the target after unpacking — and it's worth fixing at the source too, or every subsequent transfer recreates it.

Checking it takes one line: count the files under the web root that others have no read permission on. It must return zero. This is the kind of check that is born from a caught failure — the subject of the piece on the checking script.

Three — the database export that yields an empty file

wp db export doesn't dump the database from PHP; it invokes the mysqldump program. That difference is the trap.

If the database is reached not over the usual network port but over a socket — common in local development environments — then the PHP-side connection setting has no effect on the external program. It fails to find the server and finishes with an empty file.

The dangerous part is that this raises no error. The command runs, the file is created, its size is zero or a few hundred bytes. Anyone running this as an automated step then uploads an empty database to the target.

Hence a rule I've kept without exception since: always verify the integrity of a dump before overwriting anything with it. Two things: whether the completion line is present at the end of the file, and whether it contains as many table-creation statements as there are tables in the live database. File size on its own proves nothing.

Four — the collation the older server doesn't know

The last is the most harmless, because at least it fails loudly. The default collation of newer MySQL versions doesn't exist on older servers or in several MariaDB releases. If the development machine is newer than the target, the import dies complaining about an unknown collation.

The fix is to substitute a longer-established, widely supported collation in the dump. It's worth building this into the process rather than treating it as an ad-hoc repair: the development environment will eventually outpace the server anyway.

What follows from all four

The lesson isn't that these four should be memorised. It's that a deploy script has to be written complete before its first run — patching it afterwards is exactly what doesn't work with silent failures. If a failure doesn't announce itself, there is no moment at which you realise a step is still missing.

In practice: end the script with a file-level comparison of both sides — contents and permissions — and with a smoke test that requests the actual links extracted from the page, not filenames you invented. An HTTP 200 is not evidence; a broken image returns one too.

Questions on this topic

Why doesn't wp search-replace find the page builder's URLs?

Because the builder stores its layout as JSON, and in JSON the slashes are escaped. The pattern being searched for isn't present in its usual form but with escaped slashes, so a plain search misses it. The fix is to run the same replacement against the escaped form as well.

How can a file be uploaded and still not appear?

If its permissions don't let the web server read it. tar preserves local permissions, so a narrowly permissioned local file stays unreadable on the server. The file is there, the contents are correct, the checksum matches — and the visitor still sees a broken image.

Why does wp db export produce a zero-byte file?

Because the command calls the mysqldump binary, whose connection settings aren't the same as PHP's. If the database is reached over a socket, the PHP-side setting doesn't help the external program: the dump finishes with an empty file and no error.

What's the most important check after a database transfer?

The integrity of the dump, before you overwrite anything with it: whether the completion line is present at the end, and whether it contains as many table-creation statements as there are tables in the live database. File size on its own proves nothing.

← Back to the Workshop