<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Susumu Takahashi</title>
    <description>The latest articles on DEV Community by Susumu Takahashi (@susumun).</description>
    <link>https://dev.to/susumun</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3961116%2F87a59747-8eb8-43eb-9db6-c160d3592934.JPG</url>
      <title>DEV Community: Susumu Takahashi</title>
      <link>https://dev.to/susumun</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/susumun"/>
    <language>en</language>
    <item>
      <title>Browser-based updates getting stuck on the "Confirm your admin email" screen — why Playwright stalls in wp-admin</title>
      <dc:creator>Susumu Takahashi</dc:creator>
      <pubDate>Fri, 31 Jul 2026 02:02:59 +0000</pubDate>
      <link>https://dev.to/susumun/browser-based-updates-getting-stuck-on-the-confirm-your-admin-email-screen-why-playwright-6io</link>
      <guid>https://dev.to/susumun/browser-based-updates-getting-stuck-on-the-confirm-your-admin-email-screen-why-playwright-6io</guid>
      <description>&lt;p&gt;On hosting environments without SSH access, the fallback is browser automation: Playwright logs into wp-admin and drives the plugin update screen directly. That works fine for months — until one day the script stops dead before it ever reaches the update screen.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: Playwright is a browser automation tool. It reproduces clicks a human would make, but as code.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What the "confirm your admin email" screen is
&lt;/h2&gt;

&lt;p&gt;Since WordPress 5.3, wp-admin shows a periodic prompt — roughly every six months — asking the site owner to confirm they still have access to the registered admin email address. It's a core WordPress feature, not a plugin.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Is this still your address?
[ Yes, this is my address ]  [ I'll wait ]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A human clicking through wp-admin would just click "I'll wait" and move on. But a Playwright script written for a fixed sequence — log in, then navigate straight to the plugin update screen — has no branch for this extra screen. When it appears, the script stalls right there.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually happens
&lt;/h2&gt;

&lt;p&gt;The login itself succeeds. The problem is the page it lands on: instead of &lt;code&gt;wp-admin/index.php&lt;/code&gt;, the URL is &lt;code&gt;wp-admin/options-general.php?adminhash=...&lt;/code&gt;. Whatever &lt;code&gt;goto()&lt;/code&gt; call was supposed to take the script to the update screen next either gets redirected by this confirmation page, or the selectors it's waiting for never appear, and the script times out.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Check for the confirmation screen right after login
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;adminhash=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text=I&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ll wait&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Checking whether the post-login URL contains &lt;code&gt;adminhash=&lt;/code&gt; is enough to detect that this screen has appeared.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the six-month cycle makes this hard to catch
&lt;/h2&gt;

&lt;p&gt;The screen doesn't show up every time. WordPress core decides to show it only after enough time has passed since the last confirmation. That means automation that has been running cleanly for months can suddenly stop — and because it happens so rarely, tracing the cause back to this specific screen takes longer than it should.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;Detect the "I'll wait" button and click it automatically as a standing step right after login, regardless of whether the screen actually appears. That way the same code path works whether the confirmation screen shows up or not.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;ensure_past_email_check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;adminhash=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text=I&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ll wait&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait_for_load_state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;networkidle&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Calling this function immediately after every login means the twice-yearly surprise stops being a surprise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;Detail&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;When it triggers&lt;/td&gt;
&lt;td&gt;WordPress core decides enough time has passed since the last confirmation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Symptom&lt;/td&gt;
&lt;td&gt;Post-login navigation lands on the confirmation screen, later steps time out&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Detection&lt;/td&gt;
&lt;td&gt;Check whether the post-login URL contains &lt;code&gt;adminhash=&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fix&lt;/td&gt;
&lt;td&gt;Auto-click as a standing step right after every login&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Like &lt;a href="https://en.wpmm.jp/blog/playwright-strict-mode-wp-admin/" rel="noopener noreferrer"&gt;Playwright strict-mode violations colliding with wp-admin's repeated selectors&lt;/a&gt;, this comes down to the same root issue: wp-admin doesn't always move in a straight line, and browser automation has to account for the screens it doesn't expect. As long as SSH isn't available and browser-driven maintenance is the fallback, detecting and stepping past these unplanned screens isn't optional. The same distrust of wp-admin also shows up in &lt;a href="https://en.wpmm.jp/blog/wp-core-version-scraping-false-positive/" rel="noopener noreferrer"&gt;validating a version number scraped from the page before trusting it&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>php</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Why scheduled posts don't publish on time — inspecting WP-Cron with WP-CLI</title>
      <dc:creator>Susumu Takahashi</dc:creator>
      <pubDate>Thu, 30 Jul 2026 00:28:15 +0000</pubDate>
      <link>https://dev.to/susumun/why-scheduled-posts-dont-publish-on-time-inspecting-wp-cron-with-wp-cli-42d0</link>
      <guid>https://dev.to/susumun/why-scheduled-posts-dont-publish-on-time-inspecting-wp-cron-with-wp-cli-42d0</guid>
      <description>&lt;p&gt;A post scheduled to publish at a specific time doesn't go live when expected. A plugin's recurring email notification never arrives. This tends to happen on low-traffic sites, and there's a specific reason for it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: WP-Cron is WordPress’s built-in scheduling system. It sounds like the OS-level &lt;code&gt;cron&lt;/code&gt; daemon, but the underlying mechanism is quite different.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;WordPress’s WP-Cron doesn’t work like a real OS cron daemon. On every page load, WordPress checks whether any scheduled task is past its due time and, if so, runs it. This is what's known as "pseudo-cron" — and its weakness is that &lt;strong&gt;nothing runs without a page visit&lt;/strong&gt;. Schedule a post to publish at 3am on a site with little overnight traffic, and the publish task can sit unexecuted until the next visitor happens to load a page.&lt;/p&gt;

&lt;p&gt;WP-CLI lets you look inside this otherwise invisible system and run exactly the task you need, right now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Listing what's scheduled
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wp cron event list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;hook&lt;/th&gt;
&lt;th&gt;next_run_gmt&lt;/th&gt;
&lt;th&gt;recurrence&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;publish_future_post&lt;/td&gt;
&lt;td&gt;2026-06-20 03:00:00&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;wp_version_check&lt;/td&gt;
&lt;td&gt;2026-06-20 06:12:00&lt;/td&gt;
&lt;td&gt;12 hours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;wp_scheduled_delete&lt;/td&gt;
&lt;td&gt;2026-06-21 00:00:00&lt;/td&gt;
&lt;td&gt;daily&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;hook&lt;/code&gt; is the task's identifier, &lt;code&gt;next_run_gmt&lt;/code&gt; is the next scheduled run time in UTC, and &lt;code&gt;recurrence&lt;/code&gt; is the repeat interval. If &lt;code&gt;publish_future_post&lt;/code&gt; is still listed despite its time having already passed, that confirms the task is overdue simply because no page load has triggered it yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running a task right now
&lt;/h2&gt;

&lt;p&gt;To trigger a specific task immediately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Run a specific hook right now&lt;/span&gt;
wp cron event run publish_future_post

&lt;span class="c"&gt;# Run every overdue task at once&lt;/span&gt;
wp cron event run &lt;span class="nt"&gt;--due-now&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--due-now&lt;/code&gt; finds every task whose scheduled time has passed but hasn't run yet, and executes all of them. Instead of waiting for a visitor to trigger the check, this one command runs the post publish, the email notification, or whatever else is pending.&lt;/p&gt;

&lt;h2&gt;
  
  
  Confirming WP-Cron itself is working
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wp cron &lt;span class="nb"&gt;test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This checks whether the WP-Cron scheduler is functioning at all. On sites where &lt;code&gt;wp-config.php&lt;/code&gt; has &lt;code&gt;define('DISABLE_WP_CRON', true);&lt;/code&gt; set, the pseudo-cron mechanism is disabled entirely, and something else — a manual call or an external scheduler — has to call &lt;code&gt;wp cron event run --due-now&lt;/code&gt; on a regular basis.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking available recurrence intervals
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wp cron schedule list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;name&lt;/th&gt;
&lt;th&gt;display&lt;/th&gt;
&lt;th&gt;interval (seconds)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;hourly&lt;/td&gt;
&lt;td&gt;Once Hourly&lt;/td&gt;
&lt;td&gt;3600&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;twicedaily&lt;/td&gt;
&lt;td&gt;Twice Daily&lt;/td&gt;
&lt;td&gt;43200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;daily&lt;/td&gt;
&lt;td&gt;Once Daily&lt;/td&gt;
&lt;td&gt;86400&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Plugins that register custom intervals (e.g. every 15 minutes) show up here too. This is also where an unexpectedly frequent task can be spotted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Switching to a real cron
&lt;/h2&gt;

&lt;p&gt;Setting &lt;code&gt;DISABLE_WP_CRON&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt; and calling WP-CLI from the server's actual crontab instead removes the dependency on page visits entirely — tasks then run at a guaranteed time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# crontab example — every 5 minutes&lt;/span&gt;
&lt;span class="k"&gt;*&lt;/span&gt;/5 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; /path/to/wordpress &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; wp cron event run &lt;span class="nt"&gt;--due-now&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null 2&amp;gt;&amp;amp;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The more common alternative — hitting &lt;code&gt;wp-cron.php&lt;/code&gt; over HTTP with &lt;code&gt;curl&lt;/code&gt; on a schedule — works too, but calling WP-CLI directly skips the HTTP overhead and avoids exposing &lt;code&gt;wp-cron.php&lt;/code&gt; as a publicly reachable endpoint that has to be hit from outside the server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verifying after running
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Confirm the scheduled post is now published&lt;/span&gt;
wp post list &lt;span class="nt"&gt;--post_status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;publish &lt;span class="nt"&gt;--orderby&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;--order&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;DESC &lt;span class="nt"&gt;--posts_per_page&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3

&lt;span class="c"&gt;# Confirm no overdue tasks remain&lt;/span&gt;
wp cron event list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;publish_future_post&lt;/code&gt; no longer appears in the list, the corresponding post has already gone through its publish step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you need&lt;/th&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;List scheduled tasks&lt;/td&gt;
&lt;td&gt;&lt;code&gt;wp cron event list&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Run a specific task now&lt;/td&gt;
&lt;td&gt;&lt;code&gt;wp cron event run &amp;lt;hook&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Run every overdue task&lt;/td&gt;
&lt;td&gt;&lt;code&gt;wp cron event run --due-now&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Confirm WP-Cron is working&lt;/td&gt;
&lt;td&gt;&lt;code&gt;wp cron test&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;List available recurrence intervals&lt;/td&gt;
&lt;td&gt;&lt;code&gt;wp cron schedule list&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Knowing that WP-Cron is a page-visit-dependent pseudo-cron is what makes &lt;code&gt;wp cron event list&lt;/code&gt; the first move when a scheduled publish doesn't go through. Like &lt;a href="https://en.wpmm.jp/blog/wp-cli-lockout-recovery/" rel="noopener noreferrer"&gt;recovering from a wp-admin lockout&lt;/a&gt; or &lt;a href="https://en.wpmm.jp/blog/wp-search-replace-serialized-safe/" rel="noopener noreferrer"&gt;serialization-safe replacement&lt;/a&gt;, this is WP-CLI reaching past the admin dashboard to the mechanism actually running underneath it.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>php</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Locked out of wp-admin? Why WP-CLI works when `wp-login.php` doesn&amp;#8217;t</title>
      <dc:creator>Susumu Takahashi</dc:creator>
      <pubDate>Wed, 29 Jul 2026 00:19:18 +0000</pubDate>
      <link>https://dev.to/susumun/locked-out-of-wp-admin-why-wp-cli-works-when-wp-loginphp-doesn8217t-187c</link>
      <guid>https://dev.to/susumun/locked-out-of-wp-admin-why-wp-cli-works-when-wp-loginphp-doesn8217t-187c</guid>
      <description>&lt;p&gt;A forgotten password, a security plugin that blocked your own IP by mistake, a plugin bug that turns the admin screen white — the causes vary, but the result is the same: you can’t log in to wp-admin.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: WP-CLI is a command-line tool for managing WordPress, invoked as &lt;code&gt;wp&lt;/code&gt;. It operates directly on the server, without going through a browser.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is exactly the situation where WP-CLI is useful. It works here because it never touches &lt;code&gt;wp-login.php&lt;/code&gt; — it reads and writes the WordPress database and filesystem directly, so a broken login screen doesn’t affect it at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why WP-CLI keeps working when wp-admin doesn't
&lt;/h2&gt;

&lt;p&gt;A normal login follows the path: browser → &lt;code&gt;wp-login.php&lt;/code&gt; → authentication → wp-admin. If anything along that path is broken — a plugin throwing a fatal error during authentication, a security plugin blocking your IP, a fatal error in the admin theme — the login itself can’t complete.&lt;/p&gt;

&lt;p&gt;WP-CLI connects over SSH and reads/writes the &lt;code&gt;wp_users&lt;/code&gt; and &lt;code&gt;wp_options&lt;/code&gt; tables (and the filesystem) directly. The code in &lt;code&gt;wp-login.php&lt;/code&gt; is never executed, so problems on that path don’t matter. This does require that SSH access itself still works — on most hosting providers, SSH is a separate access path from the admin dashboard, so it usually still works even when wp-admin doesn’t.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scenario 1: Forgotten password
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# List administrator accounts&lt;/span&gt;
wp user list &lt;span class="nt"&gt;--role&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;administrator &lt;span class="nt"&gt;--fields&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ID,user_login,user_email

&lt;span class="c"&gt;# Overwrite the password directly&lt;/span&gt;
wp user update 1 &lt;span class="nt"&gt;--user_pass&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'a-strong-new-password'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;wp user update&lt;/code&gt; writes the new password directly to the database row — no password-reset email, no token, no waiting on a delivery that might land in spam or not arrive at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scenario 2: A security plugin blocked your own IP
&lt;/h2&gt;

&lt;p&gt;Login-attempt-limiting plugins occasionally misclassify legitimate activity as an attack and add the working IP to a block list.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Deactivate the plugin responsible for the block&lt;/span&gt;
wp plugin deactivate &amp;lt;plugin-causing-the-lockout&amp;gt;

&lt;span class="c"&gt;# Re-enable it later, after reviewing its configuration&lt;/span&gt;
wp plugin activate &amp;lt;plugin-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deactivating the plugin also disables whatever block list or rate-limiting logic it was enforcing. If the cause isn’t clear, deactivate everything to isolate the problem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Stop all plugins to isolate the cause&lt;/span&gt;
wp plugin deactivate &lt;span class="nt"&gt;--all&lt;/span&gt;

&lt;span class="c"&gt;# After confirming you can log in, re-enable them one at a time&lt;/span&gt;
wp plugin activate &amp;lt;plugin-a&amp;gt;
wp plugin activate &amp;lt;plugin-b&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Re-enabling one plugin at a time and checking login access after each step identifies exactly which plugin caused the lockout.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scenario 3: No administrator account exists
&lt;/h2&gt;

&lt;p&gt;A mistake during an offboarding cleanup can leave a site with no administrator account at all.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create a new administrator account&lt;/span&gt;
wp user create rescue_admin admin@example.com &lt;span class="nt"&gt;--role&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;administrator &lt;span class="nt"&gt;--user_pass&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'a-strong-password'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;wp user create&lt;/code&gt; creates an administrator account immediately, with no email verification or plugin approval flow in the way. After recovery, either delete this emergency account or scope it down to the minimum permissions actually needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scenario 4: A blank white screen in wp-admin
&lt;/h2&gt;

&lt;p&gt;A PHP fatal error in a plugin frequently shows up as a blank white screen in wp-admin. Deactivating the offending plugin usually resolves it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# After identifying the plugin from the error log&lt;/span&gt;
wp plugin deactivate &amp;lt;plugin-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If WP-CLI itself crashes while loading plugins, &lt;a href="https://en.wpmm.jp/blog/wp-cli-skip-plugins-rescue/" rel="noopener noreferrer"&gt;starting WP-CLI with &lt;code&gt;--skip-plugins --skip-themes&lt;/code&gt;&lt;/a&gt; is the step that has to come first — none of the commands above will run until WP-CLI itself starts successfully.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to verify after recovery
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Confirm no leftover emergency accounts remain&lt;/span&gt;
wp user list &lt;span class="nt"&gt;--role&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;administrator &lt;span class="nt"&gt;--fields&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ID,user_login,user_email

&lt;span class="c"&gt;# Confirm no plugins were left deactivated by mistake&lt;/span&gt;
wp plugin list &lt;span class="nt"&gt;--status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;inactive
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any account or password created for emergency recovery should be removed or changed once the site is stable. Leaving it in place turns the fix itself into the next security gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Forgotten password&lt;/td&gt;
&lt;td&gt;&lt;code&gt;wp user update &amp;lt;ID&amp;gt; --user_pass=...&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security plugin self-block&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;wp plugin deactivate &amp;lt;name&amp;gt;&lt;/code&gt; / &lt;code&gt;--all&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No administrator account left&lt;/td&gt;
&lt;td&gt;&lt;code&gt;wp user create ... --role=administrator&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Blank screen from a plugin error&lt;/td&gt;
&lt;td&gt;&lt;code&gt;wp plugin deactivate &amp;lt;name&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WP-CLI itself won’t start&lt;/td&gt;
&lt;td&gt;&lt;a href="https://en.wpmm.jp/blog/wp-cli-skip-plugins-rescue/" rel="noopener noreferrer"&gt;&lt;code&gt;--skip-plugins --skip-themes&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;What every scenario above has in common is that WP-CLI never touches &lt;code&gt;wp-login.php&lt;/code&gt; — it reads and writes the WordPress database directly. Like &lt;a href="https://en.wpmm.jp/blog/wp-search-replace-serialized-safe/" rel="noopener noreferrer"&gt;serialized-safe string replacement&lt;/a&gt;, where the &lt;code&gt;s:N:&lt;/code&gt; byte-length prefix matters, or &lt;a href="https://en.wpmm.jp/blog/wp-cli-cron-inspect-and-run/" rel="noopener noreferrer"&gt;inspecting WP-Cron directly&lt;/a&gt; instead of waiting on a page visit, this is WP-CLI operating one layer below the admin dashboard — and that's exactly the layer that matters during a lockout.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>php</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Why phpMyAdmin migrations break plugin settings — and why `wp search-replace` doesn&amp;#8217;t</title>
      <dc:creator>Susumu Takahashi</dc:creator>
      <pubDate>Tue, 28 Jul 2026 00:13:40 +0000</pubDate>
      <link>https://dev.to/susumun/why-phpmyadmin-migrations-break-plugin-settings-and-why-wp-search-replace-doesn8217t-4kjl</link>
      <guid>https://dev.to/susumun/why-phpmyadmin-migrations-break-plugin-settings-and-why-wp-search-replace-doesn8217t-4kjl</guid>
      <description>&lt;p&gt;After a domain migration or HTTPS switch, "all plugin settings are gone" or "Elementor layouts are broken" is a common outcome. The cause, in most cases, is running a string replacement against the WordPress database without accounting for PHP serialized data.&lt;/p&gt;

&lt;p&gt;WordPress stores plugin configurations, custom field values, and widget settings in PHP’s serialized format. Standard SQL replacements — phpMyAdmin’s find-and-replace, raw UPDATE statements, sed on a .sql dump — rewrite the string value without updating the length metadata that serialization embeds alongside it. The result is a database that appears intact but returns &lt;code&gt;false&lt;/code&gt; on every read of the affected values.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;wp search-replace&lt;/code&gt; handles this correctly. Understanding why makes the pre- and post-execution steps more deliberate.&lt;/p&gt;

&lt;h2&gt;
  
  
  What PHP serialization stores alongside the value
&lt;/h2&gt;

&lt;p&gt;A serialized entry in WordPress looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"home"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"http://example.com/top"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"My Site"&lt;/span&gt;&lt;span class="p"&gt;;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The segment &lt;code&gt;s:22:"http://example.com/top"&lt;/code&gt; means "a string of 22 bytes." The &lt;code&gt;s:N:&lt;/code&gt; prefix records the byte length.&lt;/p&gt;

&lt;p&gt;When a simple string replacement changes &lt;code&gt;http://example.com&lt;/code&gt; to &lt;code&gt;https://example.com&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Before: &lt;code&gt;s:22:"http://example.com/top"&lt;/code&gt; (22 bytes)&lt;/li&gt;
&lt;li&gt;After: &lt;code&gt;s:22:"https://example.com/top"&lt;/code&gt; (23 bytes)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;s:22&lt;/code&gt; stays unchanged even though the actual string is now 23 bytes. PHP’s &lt;code&gt;unserialize()&lt;/code&gt; detects this mismatch and returns &lt;code&gt;false&lt;/code&gt;. The plugin reads &lt;code&gt;false&lt;/code&gt; instead of its configuration array and behaves as though the settings were never saved.&lt;/p&gt;

&lt;p&gt;phpMyAdmin’s find-and-replace executes a SQL UPDATE at the storage layer. No PHP context exists there — it can’t know the column contains serialized data, and it doesn’t adjust the length prefix.&lt;/p&gt;

&lt;h2&gt;
  
  
  How &lt;code&gt;wp search-replace&lt;/code&gt; handles it
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;wp search-replace&lt;/code&gt; operates at the PHP layer, not the SQL layer:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Reads each column value&lt;/li&gt;
&lt;li&gt;Checks whether it’s serialized using &lt;code&gt;is_serialized()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;If serialized: calls &lt;code&gt;unserialize()&lt;/code&gt; to expand it into a PHP array or object&lt;/li&gt;
&lt;li&gt;Applies the string replacement to each value within the expanded structure&lt;/li&gt;
&lt;li&gt;Calls &lt;code&gt;serialize()&lt;/code&gt; on the result, recalculating all length prefixes&lt;/li&gt;
&lt;li&gt;Writes the corrected value back&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The length prefix is recalculated at step 5, so &lt;code&gt;s:N:&lt;/code&gt; always reflects the actual byte count after replacement. The distinction is the same as the difference between overwriting a file directly versus going through an application that understands the file’s format.&lt;/p&gt;

&lt;h2&gt;
  
  
  Basic usage
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Backup before anything else — no exceptions&lt;/span&gt;
wp db &lt;span class="nb"&gt;export &lt;/span&gt;before-migration-&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%Y%m%d&lt;span class="si"&gt;)&lt;/span&gt;.sql

&lt;span class="c"&gt;# Dry run: count how many rows would change, without writing anything&lt;/span&gt;
wp search-replace &lt;span class="s1"&gt;'http://example.com'&lt;/span&gt; &lt;span class="s1"&gt;'https://example.com'&lt;/span&gt; &lt;span class="nt"&gt;--dry-run&lt;/span&gt;

&lt;span class="c"&gt;# Execute&lt;/span&gt;
wp search-replace &lt;span class="s1"&gt;'http://example.com'&lt;/span&gt; &lt;span class="s1"&gt;'https://example.com'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--dry-run&lt;/code&gt; flag runs the full replacement logic internally but rolls it back, printing only the count of affected rows. Always run this first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scoping to specific tables
&lt;/h2&gt;

&lt;p&gt;By default, every table in the database is scanned. For targeted replacements:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Posts and custom fields only&lt;/span&gt;
wp search-replace &lt;span class="s1"&gt;'http://example.com'&lt;/span&gt; &lt;span class="s1"&gt;'https://example.com'&lt;/span&gt; wp_posts wp_postmeta

&lt;span class="c"&gt;# Options table only (plugin settings, site URL, etc.)&lt;/span&gt;
wp search-replace &lt;span class="s1"&gt;'http://example.com'&lt;/span&gt; &lt;span class="s1"&gt;'https://example.com'&lt;/span&gt; wp_options
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running table by table is also easier to reason about when something goes wrong — you know exactly which scope was in progress.&lt;/p&gt;

&lt;h2&gt;
  
  
  The &lt;code&gt;--skip-columns=guid&lt;/code&gt; case
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;guid&lt;/code&gt; column in &lt;code&gt;wp_posts&lt;/code&gt; is WordPress’s internal unique identifier for each post. It’s used as the item ID in RSS feeds. Changing &lt;code&gt;guid&lt;/code&gt; causes RSS readers to re-import previously-seen posts as new entries.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wp search-replace &lt;span class="s1"&gt;'http://example.com'&lt;/span&gt; &lt;span class="s1"&gt;'https://example.com'&lt;/span&gt; &lt;span class="nt"&gt;--skip-columns&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;guid
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a new site or one that doesn’t publish RSS, this can be omitted. For an existing site switching to HTTPS with RSS subscribers, include it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verifying the result
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Confirm the site URL options are updated&lt;/span&gt;
wp option get home
wp option get siteurl

&lt;span class="c"&gt;# Confirm nothing was missed — should return 0 replacements&lt;/span&gt;
wp search-replace &lt;span class="s1"&gt;'http://example.com'&lt;/span&gt; &lt;span class="s1"&gt;'https://example.com'&lt;/span&gt; &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;--dry-run&lt;/code&gt; returning 0 confirms the replacement is complete.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Serialization-safe&lt;/th&gt;
&lt;th&gt;Audit trail&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;phpMyAdmin find-and-replace&lt;/td&gt;
&lt;td&gt;❌ Corrupts data&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Raw SQL UPDATE&lt;/td&gt;
&lt;td&gt;❌ Corrupts data&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;wp search-replace&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅ Safe&lt;/td&gt;
&lt;td&gt;SSH log&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If plugin settings are already corrupted from a phpMyAdmin replacement, restore the pre-migration backup with &lt;code&gt;wp db import&lt;/code&gt;, then re-run the replacement with &lt;code&gt;wp search-replace&lt;/code&gt;. There is no reliable way to repair corrupted serialized data in place — the backup taken before execution is the only recovery path.&lt;/p&gt;

&lt;p&gt;For WP-CLI startup issues that can block this command from running, &lt;a href="https://en.wpmm.jp/blog/wp-cli-skip-plugins-rescue/" rel="noopener noreferrer"&gt;the &lt;code&gt;--skip-plugins --skip-themes&lt;/code&gt; rescue method&lt;/a&gt; covers how to start WP-CLI when a broken plugin prevents normal startup. If the issue is being locked out of wp-admin entirely rather than WP-CLI itself, &lt;a href="https://en.wpmm.jp/blog/wp-cli-lockout-recovery/" rel="noopener noreferrer"&gt;the lockout recovery guide&lt;/a&gt; covers password resets, plugin self-blocks, and missing administrator accounts.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>php</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Getting ready for WordPress 7.0 — three things to check now: PHP requirements, FSE migration, and editor extensions</title>
      <dc:creator>Susumu Takahashi</dc:creator>
      <pubDate>Fri, 24 Jul 2026 00:23:51 +0000</pubDate>
      <link>https://dev.to/susumun/getting-ready-for-wordpress-70-three-things-to-check-now-php-requirements-fse-migration-and-45k0</link>
      <guid>https://dev.to/susumun/getting-ready-for-wordpress-70-three-things-to-check-now-php-requirements-fse-migration-and-45k0</guid>
      <description>&lt;p&gt;&lt;a href="https://en.wpmm.jp/blog/wordpress-major-upgrade-failure-patterns/" rel="noopener noreferrer"&gt;Five failure patterns from past WordPress major upgrades&lt;/a&gt; ended with the observation that "the same structures will recur in some form in the next major (7.0 or 8.0)." This post applies that pattern knowledge to WordPress 7.0 specifically — not as a list of things that will happen, but as a preparatory checklist for what you can verify now.&lt;/p&gt;

&lt;p&gt;This sits alongside &lt;a href="https://en.wpmm.jp/blog/wordpress-major-upgrade-pre-flight-checklist/" rel="noopener noreferrer"&gt;the seven-item pre-flight checklist from W1&lt;/a&gt; rather than replacing it. Think of it as the 7.0-specific supplement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Item 1 — PHP minimum requirement increase (W3 pattern 3 again)
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://en.wpmm.jp/blog/wordpress-major-upgrade-failure-patterns/" rel="noopener noreferrer"&gt;W3’s pattern 3&lt;/a&gt; was "older plugins break with parse errors when the PHP minimum requirement is raised." This pattern repeats on every major upgrade cycle. WordPress 7.0 is expected to follow the same trajectory.&lt;/p&gt;

&lt;p&gt;Background:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PHP 7.4 reached end-of-life from the PHP project in November 2022 (security support ended November 2023)&lt;/li&gt;
&lt;li&gt;WordPress 6.6 requires PHP 7.2 minimum&lt;/li&gt;
&lt;li&gt;WordPress 7.0 is expected to raise the minimum to PHP 8.0 or later&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When this happens, sites running PHP 7.x will encounter plugins and themes that "worked fine before" but break with syntax or fatal errors after the upgrade. The mechanism is the same as past cycles — the plugin hasn’t updated its code to handle PHP 8.x stricter type handling, and the errors surface only at runtime.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you can check now:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Confirm the server&amp;amp;#8217;s PHP version&lt;/span&gt;
php &lt;span class="nt"&gt;-v&lt;/span&gt;

&lt;span class="c"&gt;# List all plugins with their current version and update status&lt;/span&gt;
wp plugin list &lt;span class="nt"&gt;--fields&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;name,version,update,update_version &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;table

&lt;span class="c"&gt;# Run a JSON output check — noisy output here means PHP warnings already leaking&lt;/span&gt;
wp plugin list &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pattern to watch for: plugins with an old "Tested up to" value and no updates in the past year. These are the highest risk candidates for PHP compatibility issues when the minimum requirement moves.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://en.wpmm.jp/blog/wp-cli-php-deprecated-noise/" rel="noopener noreferrer"&gt;The post on PHP 8.2 Deprecated warnings leaking into WP-CLI JSON output&lt;/a&gt; covers a related symptom — even when PHP compatibility doesn’t cause a hard break, it can silently corrupt output in ways that are hard to trace. Worth having on the checklist alongside the version check.&lt;/p&gt;

&lt;h2&gt;
  
  
  Item 2 — FSE standardization and theme migration (W3 pattern 4 revisited)
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://en.wpmm.jp/blog/wordpress-major-upgrade-failure-patterns/" rel="noopener noreferrer"&gt;W3’s pattern 4&lt;/a&gt; was the FSE migration period in 6.0, where child theme customizations stopped working when switching to Block Themes. The 6.x cycle has steadily moved Block Themes toward the center — WordPress 7.0 is expected to continue that trajectory, with Classic Themes moving further toward "supported but not the primary path."&lt;/p&gt;

&lt;p&gt;What this means in practice: sites still on Classic Themes don’t necessarily break when 7.0 arrives, but the friction accumulates. Theme updates, new block features, and plugin compatibility improvements increasingly target Block Theme architecture first. Classic Theme sites may find themselves in a "technically working but falling behind" position.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you can check now:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Identify which sites use Classic Themes and which use Block Themes
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  wp theme list &lt;span class="nt"&gt;--status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;active &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;table
  &lt;span class="c"&gt;# Check for a block-templates/ directory — its presence indicates a Block Theme&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;For sites on Classic Themes with deep customization: estimate what a migration to Block Theme would require&lt;/li&gt;
&lt;li&gt;Identify sites where the migration cost is high enough that staying on Classic Theme is the right call for now (and document that decision)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key discipline here: treat &lt;strong&gt;upgrading the WordPress core&lt;/strong&gt; and &lt;strong&gt;migrating the theme architecture&lt;/strong&gt; as two separate decisions. Doing both at once when a major version drops makes it impossible to isolate which change caused a problem. The core upgrade can happen on 7.0’s schedule; the theme migration happens on its own schedule.&lt;/p&gt;

&lt;h2&gt;
  
  
  Item 3 — Gutenberg Phase 3 and editor extension impact (W3 pattern 1 continuing)
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://en.wpmm.jp/blog/wordpress-major-upgrade-failure-patterns/" rel="noopener noreferrer"&gt;W3’s pattern 1&lt;/a&gt; was the 5.0 Gutenberg launch where editor extensions built for the Classic Editor stopped working. Gutenberg’s roadmap follows a phase structure — Phase 3 (collaborative editing, enhanced revision management, data library) continues that trajectory.&lt;/p&gt;

&lt;p&gt;Collaborative editing changes the fundamental state management model of the editor. Third-party plugins that hook into editor internals — custom meta boxes, custom field UIs, sidebar extensions — face the same structural risk they faced in 5.0. The specific APIs differ, but the pattern is the same: a significant internal change in how the editor works causes adjacent extensions to lose their footing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you can check now:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;List your active editor extension plugins
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  wp plugin list &lt;span class="nt"&gt;--status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;active &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;table
  &lt;span class="c"&gt;# Manually identify editor-adjacent plugins: ACF, Pods, Meta Box, custom meta boxes, etc.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Check whether major vendors (ACF, Pods, Meta Box, WooCommerce block extensions) have announced Phase 3 compatibility&lt;/li&gt;
&lt;li&gt;For plugins where no Phase 3 announcement exists yet, identify potential alternatives and flag them for follow-up&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Note: "we don’t use collaborative editing" doesn’t mean you’re unaffected. Internal API changes in Phase 3 can affect plugins even on sites where the collaborative features themselves are never used. The dependency is on the editor internals, not the end-user feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary — what to do before 7.0 drops
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Check now&lt;/th&gt;
&lt;th&gt;Act when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PHP requirement&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;php -v&lt;/code&gt; + plugin Tested-up-to audit&lt;/td&gt;
&lt;td&gt;PHP 8.0+ minimum is confirmed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;FSE / theme migration&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Identify Classic vs. Block Theme per site, estimate migration cost&lt;/td&gt;
&lt;td&gt;7.0 release announcement confirms Classic Theme support status&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Editor extensions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;List editor-adjacent plugins, check vendor Phase 3 status&lt;/td&gt;
&lt;td&gt;Phase 3 lands in core&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The common thread: starting this work after 7.0 is announced puts you under time pressure. Replacing plugins that don’t support PHP 8.x requires research, testing, and a migration window. Theme migration is a substantial project. Editor extension alternatives may need to wait for vendors to ship updates.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://en.wpmm.jp/blog/wordpress-major-upgrade-wpcli-step-by-step/" rel="noopener noreferrer"&gt;The step-by-step WP-CLI upgrade procedure from W4&lt;/a&gt; pairs well with this list: once you’ve completed the pre-flight checks, the actual upgrade sequence follows the same pattern regardless of which major version you’re applying. And &lt;a href="https://en.wpmm.jp/blog/wait-before-wordpress-major-update/" rel="noopener noreferrer"&gt;W2’s staged rollout approach&lt;/a&gt; — applying to lower-risk sites first before production — is especially useful when you have a mix of sites at different levels of readiness.&lt;/p&gt;

&lt;h2&gt;
  
  
  The W series in full
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Post&lt;/th&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;W1 — What to check&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://en.wpmm.jp/blog/wordpress-major-upgrade-pre-flight-checklist/" rel="noopener noreferrer"&gt;Seven-item pre-flight checklist&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;W2 — When to apply&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://en.wpmm.jp/blog/wait-before-wordpress-major-update/" rel="noopener noreferrer"&gt;The x.0.1 rule and staged rollout&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;W3 — What can go wrong&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://en.wpmm.jp/blog/wordpress-major-upgrade-failure-patterns/" rel="noopener noreferrer"&gt;Five failure patterns from past majors&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;W4 — How to run it&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://en.wpmm.jp/blog/wordpress-major-upgrade-wpcli-step-by-step/" rel="noopener noreferrer"&gt;Step-by-step WP-CLI commands&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;W5 — Preparing for 7.0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Three things to check now ← this post&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;W6 — Verifying the result&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://en.wpmm.jp/blog/wordpress-major-upgrade-verification-checklist/" rel="noopener noreferrer"&gt;7-step post-upgrade checklist&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Pattern knowledge from W3 is most useful before the upgrade, not after. Knowing that PHP requirement increases follow a predictable pattern is only actionable if you run the plugin audit before 7.0 drops, not after the first site breaks. The preparation window is now.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>php</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Running a WordPress major upgrade step by step with WP-CLI — commands and checkpoints for each stage</title>
      <dc:creator>Susumu Takahashi</dc:creator>
      <pubDate>Thu, 23 Jul 2026 00:00:41 +0000</pubDate>
      <link>https://dev.to/susumun/running-a-wordpress-major-upgrade-step-by-step-with-wp-cli-commands-and-checkpoints-for-each-stage-b94</link>
      <guid>https://dev.to/susumun/running-a-wordpress-major-upgrade-step-by-step-with-wp-cli-commands-and-checkpoints-for-each-stage-b94</guid>
      <description>&lt;p&gt;The W series has covered &lt;a href="https://en.wpmm.jp/blog/wordpress-major-upgrade-pre-flight-checklist/" rel="noopener noreferrer"&gt;what to check before a major upgrade&lt;/a&gt;, &lt;a href="https://en.wpmm.jp/blog/wait-before-wordpress-major-update/" rel="noopener noreferrer"&gt;when to apply it&lt;/a&gt;, and &lt;a href="https://en.wpmm.jp/blog/wordpress-major-upgrade-failure-patterns/" rel="noopener noreferrer"&gt;what can go wrong&lt;/a&gt;. This fourth part covers the remaining question: how to actually run it. Once the preparation is in place and the timing is right, this is the command sequence to follow — one stage at a time, with a confirmation step at each boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not use the admin dashboard Update button
&lt;/h2&gt;

&lt;p&gt;The WordPress admin screen has an "Update All" button that runs core, plugin, and theme updates in a single operation. This is the approach to avoid for major upgrades.&lt;/p&gt;

&lt;p&gt;The reason is &lt;strong&gt;loss of isolation&lt;/strong&gt;. When core plus five plugins plus two themes update in one pass, any breakage afterward has multiple suspects and no clear way to narrow them down. The dashboard update also displays "Updated successfully" even when PHP warnings or non-fatal errors occurred during the process — making it easy to miss problems until they surface later.&lt;/p&gt;

&lt;p&gt;WP-CLI lets you apply core, the DB migration, plugins (one at a time), and themes in separate steps, with a check between each one.&lt;/p&gt;

&lt;h2&gt;
  
  
  STEP 0 — Final checks before starting
&lt;/h2&gt;

&lt;p&gt;Before touching anything, establish a baseline and take a backup.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Confirm the current core version and what updates are available&lt;/span&gt;
wp core version
wp core check-update

&lt;span class="c"&gt;# Export the database with compression&lt;/span&gt;
wp db &lt;span class="nb"&gt;export&lt;/span&gt; - | &lt;span class="nb"&gt;gzip&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; backup_&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%Y%m%d_%H%M%S&lt;span class="si"&gt;)&lt;/span&gt;.sql.gz

&lt;span class="c"&gt;# See which plugins have updates pending&lt;/span&gt;
wp plugin list &lt;span class="nt"&gt;--update&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;available &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;table

&lt;span class="c"&gt;# See which themes have updates pending&lt;/span&gt;
wp theme list &lt;span class="nt"&gt;--update&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;available &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;table
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;wp core check-update&lt;/code&gt; tells you what the upgrade will move to before you start. Running it first also gives you a record of the pre-upgrade state in your session log.&lt;/p&gt;

&lt;p&gt;The database backup taken here is the recovery point for the entire upgrade. If core update fails or something breaks in the DB migration step, this is what you restore from. Take it immediately before starting, not hours earlier.&lt;/p&gt;

&lt;h2&gt;
  
  
  STEP 1 — Update core in version steps
&lt;/h2&gt;

&lt;p&gt;Jumping directly from 6.4 to 6.7 means several DB migration passes run back-to-back, and if something breaks you’re left trying to figure out which intermediate version caused it. Applying one version at a time keeps the problem space smaller.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Specify the target version explicitly — don&amp;amp;#8217;t rely on "latest"&lt;/span&gt;
wp core update &lt;span class="nt"&gt;--version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;6.5

&lt;span class="c"&gt;# Run the DB migration immediately after core update&lt;/span&gt;
wp core update-db

&lt;span class="c"&gt;# Verify core file checksums against the WordPress.org reference&lt;/span&gt;
wp core verify-checksums
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Omitting &lt;code&gt;--version=&lt;/code&gt; updates to the latest available, which skips the intermediate-version approach. For major upgrades, name the version explicitly.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;wp core update-db&lt;/code&gt; aligns the database schema version with the code. Without it, WordPress may run the migration automatically when the admin screen loads — which works, but runs without any visible confirmation that it completed cleanly. Running it from WP-CLI gives you a clear success or failure signal.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;wp core verify-checksums&lt;/code&gt; compares core files against the WordPress.org checksums for that version. On servers where files were previously modified directly via FTP, this catches discrepancies before they become confusing later. Worth running both before and after core updates.&lt;/p&gt;

&lt;p&gt;After the core step: confirm that the admin dashboard loads and that the front page returns a 200 before moving on.&lt;/p&gt;

&lt;h2&gt;
  
  
  STEP 2 — Update plugins one at a time
&lt;/h2&gt;

&lt;p&gt;With core confirmed stable, move to plugins. One plugin per update, with a check after each one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# List plugins that have updates available&lt;/span&gt;
wp plugin list &lt;span class="nt"&gt;--update&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;available &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;table

&lt;span class="c"&gt;# Update one plugin at a time&lt;/span&gt;
wp plugin update contact-form-7
&lt;span class="c"&gt;# ... check the site ...&lt;/span&gt;
wp plugin update woocommerce
&lt;span class="c"&gt;# ... check checkout flow ...&lt;/span&gt;
wp plugin update yoast-seo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A useful order: start with plugins that have a narrow blast radius (SEO, debugging tools, admin utilities), and finish with the high-stakes ones (e-commerce, forms, page builders, authentication).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Update earlier&lt;/th&gt;
&lt;th&gt;Update later&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SEO plugins, security scanners&lt;/td&gt;
&lt;td&gt;WooCommerce, payment plugins&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Admin utilities, debug tools&lt;/td&gt;
&lt;td&gt;Page builders (Elementor, Beaver Builder)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cache plugins&lt;/td&gt;
&lt;td&gt;Authentication, multisite-related&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://en.wpmm.jp/blog/wordpress-major-upgrade-failure-patterns/" rel="noopener noreferrer"&gt;W3’s failure patterns 2 and 3&lt;/a&gt; — JS library version jumps and PHP minimum requirement changes — are most likely to appear during plugin updates. After updating any plugin with heavy JavaScript dependencies, open the browser console and check for JS errors before calling the step done.&lt;/p&gt;

&lt;p&gt;If WP-CLI itself starts throwing errors or crashing after a plugin update, &lt;a href="https://en.wpmm.jp/blog/wp-cli-skip-plugins-rescue/" rel="noopener noreferrer"&gt;the &lt;code&gt;--skip-plugins --skip-themes&lt;/code&gt; rescue flag&lt;/a&gt; lets you restart WP-CLI without loading plugins — useful for investigating the problem or rolling back without getting stuck in a broken plugin load loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  STEP 3 — Update themes
&lt;/h2&gt;

&lt;p&gt;After plugins are confirmed stable, update themes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check which themes have updates&lt;/span&gt;
wp theme list &lt;span class="nt"&gt;--update&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;available &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;table

&lt;span class="c"&gt;# Update the theme&lt;/span&gt;
wp theme update twentytwentyfour
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a child theme is in use, the parent theme update doesn’t touch the child theme files — but &lt;strong&gt;parent theme hook changes can break child theme behavior&lt;/strong&gt; if the child overrides hooks that were renamed or removed. After any parent theme update, walk through the pages that use customized template parts.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://en.wpmm.jp/blog/wordpress-major-upgrade-failure-patterns/" rel="noopener noreferrer"&gt;W3’s pattern 4&lt;/a&gt; — Classic Theme child theme customizations breaking when switching to Block Themes — is most likely to appear here if theme updates involve moving from one theme architecture to another. That’s a case where the theme update decision needs to be treated separately from the core upgrade decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rolling back when something goes wrong
&lt;/h2&gt;

&lt;p&gt;Have these commands ready before starting. Running the upgrade without knowing the rollback path in advance is the kind of setup where panic sets in under pressure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Roll back core
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Force-reinstall a specific prior version of core&lt;/span&gt;
wp core update &lt;span class="nt"&gt;--version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;6.4 &lt;span class="nt"&gt;--force&lt;/span&gt;

&lt;span class="c"&gt;# Restore the database from the backup taken in STEP 0&lt;/span&gt;
wp db import backup_20260615_090000.sql.gz
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--force&lt;/code&gt; installs the specified version even if it’s already installed — necessary when rolling back to the previous version that’s already recorded as the current one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Roll back a plugin
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Force-install a specific prior version of a plugin&lt;/span&gt;
wp plugin &lt;span class="nb"&gt;install &lt;/span&gt;contact-form-7 &lt;span class="nt"&gt;--version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;5.8.3 &lt;span class="nt"&gt;--force&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Plugin rollback is covered in detail in &lt;a href="https://en.wpmm.jp/blog/wp-cli-pinpoint-rollback/" rel="noopener noreferrer"&gt;the WP-CLI pinpoint rollback post&lt;/a&gt;. The version number to restore to is in the &lt;code&gt;version&lt;/code&gt; column of &lt;code&gt;wp plugin list&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;When core has been rolled back, the situation is the same as &lt;a href="https://en.wpmm.jp/blog/halt-after-core-rollback/" rel="noopener noreferrer"&gt;the halt-after-core-rollback design&lt;/a&gt;: continuing plugin updates on an unstable core adds another variable to an already unclear state. Stabilize core first, then resume.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;When to use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;wp core check-update&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Baseline check before starting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;wp db export&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Immediately before core update (required)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;wp core update --version=X.X&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Core update with explicit version&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;wp core update-db&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Immediately after core update (required)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;wp core verify-checksums&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Core file integrity check&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;wp plugin update &amp;lt;name&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;One plugin at a time, check between each&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;wp theme update &amp;lt;name&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Theme update, visual check after&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;wp core update --version=X.X --force&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Core rollback if needed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;wp plugin install &amp;lt;name&amp;gt; --version=X.X.X --force&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Plugin rollback if needed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Where W1–W4 fit together:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Post&lt;/th&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;W1 — What to check&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://en.wpmm.jp/blog/wordpress-major-upgrade-pre-flight-checklist/" rel="noopener noreferrer"&gt;Pre-upgrade checklist: 7 items&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;W2 — When to apply&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://en.wpmm.jp/blog/wait-before-wordpress-major-update/" rel="noopener noreferrer"&gt;The case for waiting for x.0.1&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;W3 — What can go wrong&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://en.wpmm.jp/blog/wordpress-major-upgrade-failure-patterns/" rel="noopener noreferrer"&gt;Five failure patterns from past majors&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;W4 — How to run it&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Commands and checkpoints for each stage ← this post&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;W5 — Getting ready for 7.0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://en.wpmm.jp/blog/preparing-for-wordpress-7-upgrade/" rel="noopener noreferrer"&gt;Three things to check now: PHP, FSE, editor extensions&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;W6 — Verifying the result&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://en.wpmm.jp/blog/wordpress-major-upgrade-verification-checklist/" rel="noopener noreferrer"&gt;7-step post-upgrade checklist&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Running updates in stages isn’t a slower path — it’s the path that keeps each step reversible and each problem identifiable. When something breaks after a one-at-a-time update sequence, you know exactly which step caused it. That clarity is what makes the difference between a short investigation and a long one.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>php</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Three site-list UX improvements: instant completion feedback, error highlighting, and auto-deselect</title>
      <dc:creator>Susumu Takahashi</dc:creator>
      <pubDate>Wed, 22 Jul 2026 01:06:06 +0000</pubDate>
      <link>https://dev.to/susumun/three-site-list-ux-improvements-instant-completion-feedback-error-highlighting-and-auto-deselect-35o</link>
      <guid>https://dev.to/susumun/three-site-list-ux-improvements-instant-completion-feedback-error-highlighting-and-auto-deselect-35o</guid>
      <description>&lt;p&gt;The site-list visualization work has run across several posts: &lt;a href="https://en.wpmm.jp/blog/maintenance-status-visualization/" rel="noopener noreferrer"&gt;the blue pulse and green completion border&lt;/a&gt;, &lt;a href="https://en.wpmm.jp/blog/detect-running-site-from-streaming-logs/" rel="noopener noreferrer"&gt;the marker-based running-site detection&lt;/a&gt;, and &lt;a href="https://en.wpmm.jp/blog/three-state-visual-hierarchy/" rel="noopener noreferrer"&gt;the three-state visual hierarchy&lt;/a&gt;. v1.6.8 added three more UX improvements to the same area. This post records the design decisions behind them.&lt;/p&gt;

&lt;p&gt;All three share a common starting point: managing 10–20 sites in a bulk maintenance run that takes several tens of minutes, and making that time easier to follow without introducing new failure modes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Improvement 1 — refresh the thumbnail and badge the moment a site finishes
&lt;/h2&gt;

&lt;p&gt;Previously, thumbnails and the pending-plugin badge were only refreshed after the entire bulk run completed. If the first of ten sites finished early, its thumbnail stayed stale until all ten were done.&lt;/p&gt;

&lt;p&gt;The fix: refresh only the completed site the instant its completion is detected. A new &lt;code&gt;_refreshCompletedSitesNow&lt;/code&gt; function wires into the streaming-log loop. Each time a site-completion marker arrives, it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Updates that site’s &lt;code&gt;?v=&amp;lt;mtime&amp;gt;&lt;/code&gt; to bust the thumbnail cache&lt;/li&gt;
&lt;li&gt;Calls &lt;code&gt;_invalidatePendingPluginCacheForSiteIds&lt;/code&gt; to partially invalidate and re-render the plugin badge&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The key constraint: each refresh touches exactly one site and doesn’t interrupt the sites still running behind it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Improvement 2 — highlight error sites in red for 24 hours
&lt;/h2&gt;

&lt;p&gt;To make "which site had an error" visible at a glance after a run, a &lt;code&gt;site-error&lt;/code&gt; CSS class was added — red border (#ef4444) plus a light red background (rgba(239,68,68,0.08)), applied to both the grid and list views. It follows the same pattern as the existing running / completed / pending classes. Status priority is &lt;code&gt;running &amp;gt; error &amp;gt; completed &amp;gt; pending&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The error state persists for 24 hours via a &lt;code&gt;_siteErrorStatus&lt;/code&gt; map with a completion timestamp. Twenty-four hours matches the green completion border from the earlier work — long enough to cover "what happened today." Starting a new maintenance run resets the previous error badge for those sites, so badges don’t carry over across separate sessions.&lt;/p&gt;

&lt;h3&gt;
  
  
  False-positive prevention — marker lines only
&lt;/h3&gt;

&lt;p&gt;The most important design constraint in the error detection logic was avoiding false positives. Streaming logs contain many internal lines from the maintenance agent. If the detection is too broad, an innocent log line could trigger a red badge for a site that succeeded.&lt;/p&gt;

&lt;p&gt;The fix: the agent emits exactly one conclusive marker line at the end of each site’s processing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;[site name] ✗ Maintenance result: ERROR
[site name] ✓ Maintenance result: OK
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The front-end &lt;code&gt;_detectSiteResultFromLog&lt;/code&gt; function only looks at lines containing &lt;code&gt;Maintenance result:&lt;/code&gt; (or the Japanese equivalent). Every other line is ignored — the site stays green. This is the same conservative approach established in &lt;a href="https://en.wpmm.jp/blog/detect-running-site-from-streaming-logs/" rel="noopener noreferrer"&gt;the streaming-log marker post&lt;/a&gt;: one unambiguous marker, no inference from surrounding context.&lt;/p&gt;

&lt;p&gt;The tradeoff: a site whose marker line is never emitted stays green instead of showing an error. Missing an error badge is less harmful than showing a false-positive red badge, so the logic deliberately favors the cautious side.&lt;/p&gt;

&lt;h2&gt;
  
  
  Improvement 3 — auto-deselect after the run completes
&lt;/h2&gt;

&lt;p&gt;After a multi-site maintenance run, the selected checkboxes remained checked. The next operation could unintentionally target those same sites again if the user forgot to clear them.&lt;/p&gt;

&lt;p&gt;The fix: &lt;code&gt;_finalizeMaintenanceRun&lt;/code&gt; — the shared teardown path for both standard maintenance and selective plugin updates — now automatically unchecks the sites that were part of the completed run (&lt;code&gt;_runningSiteIdOrder&lt;/code&gt;). No toggle was added; auto-deselect is unconditional.&lt;/p&gt;

&lt;p&gt;After a run, completed sites have a green or red border for 24 hours, so context about what ran is still visible through the color state. The checkbox doesn’t need to carry that history — the border does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;p&gt;These three improvements extend the site list from three states to four: &lt;code&gt;waiting / running / completed / error&lt;/code&gt;. &lt;a href="https://en.wpmm.jp/blog/maintenance-status-visualization/" rel="noopener noreferrer"&gt;V33’s blue pulse and green border&lt;/a&gt; introduced running and completed. &lt;a href="https://en.wpmm.jp/blog/three-state-visual-hierarchy/" rel="noopener noreferrer"&gt;V44’s visual hierarchy&lt;/a&gt; formalized the three-level visual weight. Error is now an explicit fourth state with its own color and persistence.&lt;/p&gt;

&lt;p&gt;The thread running through all three improvements is the same: when you can’t determine the right answer with certainty, default to the safer outcome. Partial refresh should not disturb other running sites. Error detection should not fire on ambiguous lines. Auto-deselect should not depend on the user remembering to clear. Each is a different problem, but the same instinct drove each solution.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Follow-up: &lt;a href="https://en.wpmm.jp/blog/site-list-scroll-reset-keepscroll-fix/" rel="noopener noreferrer"&gt;Site list scrolls to top on every delete — fixing the missing keepScroll argument across 6 call sites&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Why environment variables don&amp;#8217;t suppress WP-CLI PHP Deprecated warnings — the phar + shebang path and a three-part structural fix</title>
      <dc:creator>Susumu Takahashi</dc:creator>
      <pubDate>Tue, 21 Jul 2026 00:32:56 +0000</pubDate>
      <link>https://dev.to/susumun/why-environment-variables-don8217t-suppress-wp-cli-php-deprecated-warnings-the-phar-shebang-pgl</link>
      <guid>https://dev.to/susumun/why-environment-variables-don8217t-suppress-wp-cli-php-deprecated-warnings-the-phar-shebang-pgl</guid>
      <description>&lt;p&gt;A previous post covered &lt;a href="https://en.wpmm.jp/blog/wp-cli-php-deprecated-noise/" rel="noopener noreferrer"&gt;how to absorb PHP 8.2 Deprecated warnings from WP-CLI using a three-layer defense&lt;/a&gt;. The approach — prepending &lt;code&gt;WP_CLI_PHP_ARGS&lt;/code&gt; to set &lt;code&gt;error_reporting&lt;/code&gt; — works in many environments. But a case came up where Deprecated warnings wouldn’t disappear despite the same configuration. Tracing the cause revealed a structural reason why the environment variable never arrived. This post records that root cause and the three-part fix added in v1.6.8.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why environment variables don’t arrive — the phar + shebang execution path
&lt;/h2&gt;

&lt;p&gt;An agency reported that on Xserver, plugin list retrieval was failing across multiple sites (referred to here as "site A / site B") with a large volume of Deprecated messages. We reproduced the same behavior on our own Xserver setup (PHP 8.2.30, WP-CLI 2.7.1) and traced the execution path.&lt;/p&gt;

&lt;p&gt;Xserver’s &lt;code&gt;/usr/bin/wp&lt;/code&gt; is a phar binary. Inside, it starts with a &lt;code&gt;#!/usr/bin/env php&lt;/code&gt; shebang, so the actual startup sequence looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;shell → /usr/bin/wp (shebang: #!/usr/bin/env php)
                  ↓
        env locates php and starts it
                  ↓
        php loads the phar → WP-CLI runs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this path, &lt;code&gt;WP_CLI_PHP_ARGS&lt;/code&gt; is never read as a PHP startup option. &lt;code&gt;WP_CLI_PHP_ARGS&lt;/code&gt; is supposed to let WP-CLI pass a &lt;code&gt;-d&lt;/code&gt; flag to PHP, but when PHP itself is launched via shebang, control never reaches the point where WP-CLI can inject that flag into PHP’s invocation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# doesn&amp;amp;#8217;t work — /usr/bin/wp on Xserver is a shebang-launched phar&lt;/span&gt;
&lt;span class="nv"&gt;WP_CLI_PHP_ARGS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"-d error_reporting='E_ALL &amp;amp; ~E_DEPRECATED'"&lt;/span&gt; wp plugin list &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;json

&lt;span class="c"&gt;# works — -d goes directly to the php binary&lt;/span&gt;
php &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;error_reporting&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'E_ALL &amp;amp; ~E_DEPRECATED &amp;amp; ~E_USER_DEPRECATED'&lt;/span&gt; /tmp/wp-cli-2.7.1.phar plugin list &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We verified this against our production setup: with the first form, 407 Deprecated lines remained; with the second, 0.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pillar A — detecting the php-direct path and injecting -d
&lt;/h2&gt;

&lt;p&gt;The fix: inspect &lt;code&gt;wp_cli_path&lt;/code&gt; for whether it’s a php-direct invocation, and if so, inject &lt;code&gt;-d error_reporting&lt;/code&gt; immediately after the PHP binary.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;inject_php_d_flag(wp_cli_path)&lt;/code&gt; in &lt;code&gt;core/wpcli_json.py&lt;/code&gt; splits the path and checks the leading binary name.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If the binary is &lt;code&gt;php&lt;/code&gt;, &lt;code&gt;php8.2&lt;/code&gt;, &lt;code&gt;php82&lt;/code&gt;, or any &lt;code&gt;php*&lt;/code&gt; variant — inject &lt;code&gt;-d error_reporting='E_ALL &amp;amp; ~E_DEPRECATED &amp;amp; ~E_USER_DEPRECATED'&lt;/code&gt; right after it.&lt;/li&gt;
&lt;li&gt;If the path is a bare &lt;code&gt;wp&lt;/code&gt; or a direct &lt;code&gt;.phar&lt;/code&gt; invocation — leave it as-is.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;inject_php_d_flag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wp_cli_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;shlex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wp_cli_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;wp_cli_path&lt;/span&gt;
    &lt;span class="n"&gt;binary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;basename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;rstrip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.exe&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;binary&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;php&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;binary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;php&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;wp_cli_path&lt;/span&gt;  &lt;span class="c1"&gt;# bare wp / phar — leave unchanged
&lt;/span&gt;    &lt;span class="n"&gt;flag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-d error_reporting=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;E_ALL &amp;amp; ~E_DEPRECATED &amp;amp; ~E_USER_DEPRECATED&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;shlex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;flag&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using &lt;code&gt;os.path.basename&lt;/code&gt; ensures this works with both &lt;code&gt;/&lt;/code&gt; and &lt;code&gt;\&lt;/code&gt; path separators — Linux on the server, Windows on the client.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pillar B — locating the JSON range within noisy output
&lt;/h2&gt;

&lt;p&gt;Even after suppressing Deprecated warnings, other noise (notices, warnings) can still appear in stdout. To handle that, we added a helper that extracts only the JSON portion from stdout rather than attempting to parse the entire string.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;extract_json_from_wpcli_output(stdout)&lt;/code&gt; locates the first &lt;code&gt;[&lt;/code&gt; or &lt;code&gt;{&lt;/code&gt; and the last &lt;code&gt;]&lt;/code&gt; or &lt;code&gt;}&lt;/code&gt; in the output, and passes only that range to &lt;code&gt;json.loads&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;extract_json_from_wpcli_output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rfind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PHP fatal errors and parse errors typically contain no JSON brackets, or contain them in positions that yield invalid JSON — so &lt;code&gt;json.loads&lt;/code&gt; raises and the helper returns &lt;code&gt;None&lt;/code&gt;. False positives stay low.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pillar C — hardening nine json.loads call sites
&lt;/h2&gt;

&lt;p&gt;The existing code had nine places calling &lt;code&gt;json.loads(stdout)&lt;/code&gt; directly. These were unified to return a &lt;code&gt;(parsed, raw)&lt;/code&gt; tuple, so failed parses still have the original stdout available for error handling and logging.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;parse_wpcli_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;extract_json_from_wpcli_output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;stdout&lt;/span&gt;
    &lt;span class="nf"&gt;except &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JSONDecodeError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;TypeError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stdout&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keeping the raw output alongside the parse result means that when JSON decoding fails, there’s still something useful available — the actual stdout — without reconstructing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  46 AST tests to prevent regression
&lt;/h2&gt;

&lt;p&gt;Alongside the three pillars, we added &lt;code&gt;tests/test_wpcli_deprecation_noise.py&lt;/code&gt; with 46 tests covering:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;php detection boundary tests&lt;/strong&gt; — &lt;code&gt;php&lt;/code&gt;, &lt;code&gt;php8.2&lt;/code&gt;, &lt;code&gt;php82&lt;/code&gt; trigger injection; &lt;code&gt;wp&lt;/code&gt;, &lt;code&gt;.phar&lt;/code&gt; do not&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;-d injection position tests&lt;/strong&gt; — the modified command string has the flag in the correct position&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;7+ JSON locate cases&lt;/strong&gt; — Deprecated noise mixed in, Fatal error only, clean JSON, empty string, and others&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structural static verification via AST&lt;/strong&gt; — confirming that the existing &lt;code&gt;json.loads&lt;/code&gt; call sites have actually been migrated to the tuple-return pattern&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The V40 tests confirmed that the defense patterns ran correctly. These go one step further and use &lt;a href="https://en.wpmm.jp/blog/python-nameerror-refactor-trap-ast-test/" rel="noopener noreferrer"&gt;AST-based static analysis&lt;/a&gt; to verify that the code itself retains the intended structure — catching regressions that runtime tests alone would miss. Total test count went from 942 to 993.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;p&gt;Environment-variable-based Deprecated suppression works when PHP is invoked directly, but doesn’t reach a shebang-launched phar like &lt;code&gt;/usr/bin/wp&lt;/code&gt; on Xserver. The v1.6.8 fix addresses this in three layers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pillar A&lt;/strong&gt;: parse &lt;code&gt;wp_cli_path&lt;/code&gt; and inject &lt;code&gt;-d&lt;/code&gt; directly when it’s a php-direct invocation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pillar B&lt;/strong&gt;: locate the JSON range in stdout rather than parsing the whole string&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pillar C&lt;/strong&gt;: unify &lt;code&gt;json.loads&lt;/code&gt; call sites to return &lt;code&gt;(parsed, raw)&lt;/code&gt; tuples&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When an environment variable "doesn’t work," the most productive first question is whether the path it’s supposed to reach even sees it. That structural diagnosis — rather than trying variations of the same configuration — is what led to this fix. For the original Deprecated noise and the v1.6.6 three-layer defense, &lt;a href="https://en.wpmm.jp/blog/wp-cli-php-deprecated-noise/" rel="noopener noreferrer"&gt;the earlier post on WP-CLI PHP Deprecated noise&lt;/a&gt; covers the symptom side of the same problem.&lt;/p&gt;

</description>
      <category>python</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>When SSH to Xserver suddenly stops working, it's fail2ban — the official confirmation and a safer reconnection check</title>
      <dc:creator>Susumu Takahashi</dc:creator>
      <pubDate>Sat, 18 Jul 2026 01:10:54 +0000</pubDate>
      <link>https://dev.to/susumun/when-ssh-to-xserver-suddenly-stops-working-its-fail2ban-the-official-confirmation-and-a-safer-3oha</link>
      <guid>https://dev.to/susumun/when-ssh-to-xserver-suddenly-stops-working-its-fail2ban-the-official-confirmation-and-a-safer-3oha</guid>
      <description>&lt;p&gt;If you run WordPress maintenance for clients on Xserver, sooner or later you hit this: SSH was working fine this morning, and now your connection dies the instant it touches the host. No key change on your side, no obvious server-side incident. Just &lt;code&gt;Connection closed by ...&lt;/code&gt;, and the silence deepens every time you retry.&lt;/p&gt;

&lt;p&gt;A recent escalation to Xserver support gave us an official answer about what's actually going on. Worth writing down what an operator needs to know.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the symptom looks like
&lt;/h2&gt;

&lt;p&gt;In our observation, the symptom moves through two stages.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lighter&lt;/strong&gt;: &lt;code&gt;Connection closed by [Xserver IP]&lt;/code&gt; — connection killed at the sshd layer before it's established&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Heavier&lt;/strong&gt;: &lt;code&gt;ssh: connect to host ... port 10022: Connection refused&lt;/code&gt; — escalated to a firewall reject&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you notice the lighter stage early, recovery is still in sight. If you keep retrying, you move into the heavier stage, and &lt;strong&gt;waiting is the only viable option for a while&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Xserver support told us
&lt;/h2&gt;

&lt;p&gt;While escalating an unrelated issue, we asked Xserver about this "source-IP gets blocked" behavior. The official response:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When the attempt count exceeds the threshold, &lt;strong&gt;fail2ban temporarily restricts the source IP&lt;/strong&gt;. The specific threshold, bantime, findtime, and whether manual unbanning is supported are kept confidential for security reasons.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So &lt;code&gt;Connection closed&lt;/code&gt; / &lt;code&gt;Connection refused&lt;/code&gt; from Xserver is the protection mechanism doing its job — not a server-side fault. The confidentiality of the threshold is reasonable (publishing it would help attackers), but two things you can operate with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;There is no manual-unban support channel&lt;/strong&gt; (you have to wait it out)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-recovery happens within roughly half a day to a day&lt;/strong&gt; (verified against our own production setup)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What triggers it
&lt;/h2&gt;

&lt;p&gt;Even without intentional brute force, normal operational patterns can structurally trigger it. The most common one we see is &lt;strong&gt;having multiple SSH keys available to the agent while a maintenance tool repeatedly opens connections to Xserver&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;paramiko and the OpenSSH client try the host key, &lt;code&gt;~/.ssh/id_*&lt;/code&gt;, and any keys in the SSH agent in sequence. With three keys registered, a single connection costs three to four auth attempts. When a maintenance tool fires those connections in parallel across multiple sites, the cumulative &lt;code&gt;MaxAuthTries&lt;/code&gt; overruns add up fast enough to trip fail2ban's threshold.&lt;/p&gt;

&lt;p&gt;The application-side fix (forcing &lt;code&gt;look_for_keys=False&lt;/code&gt; / &lt;code&gt;allow_agent=False&lt;/code&gt; in paramiko to constrain which keys are tried) is covered in &lt;a href="https://en.wpmm.jp/blog/paramiko-look-for-keys-trap/" rel="noopener noreferrer"&gt;the paramiko default that triggers IP blocks&lt;/a&gt;. This post stays on &lt;strong&gt;what to do once you're already blocked&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do once you're already blocked
&lt;/h2&gt;

&lt;p&gt;Two real options.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Wait.&lt;/strong&gt; No support channel for manual unban exists, so waiting half a day to a day is, in practice, the shortest path. If you have a scheduled maintenance window, tell the client "the server's protection mechanism temporarily restricted access; we'll resume after release." Calling it an "outage" misses the truth.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Switch your source IP.&lt;/strong&gt; If you can reach the server from a different network (mobile WiFi, phone tethering, a fixed-IP service), recovery is immediate. We keep a domestic fixed-IP service on hand (Interlink My IP) and have confirmed that even while the primary line is blocked, &lt;code&gt;ssh layer2024@layer2024.xsrv.jp&lt;/code&gt; works fine from the fixed IP. fail2ban operates per source IP, so different IPs go through normally.&lt;/p&gt;

&lt;h2&gt;
  
  
  A reconnection-check command that minimizes re-trigger risk
&lt;/h2&gt;

&lt;p&gt;Right after recovery, what you actually want is a command that &lt;strong&gt;uses the fewest possible auth attempts to verify the line is back&lt;/strong&gt;. If you have multiple keys registered and you just type &lt;code&gt;ssh user@host&lt;/code&gt;, the cascading attempts can drop you right back into the same block.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh &lt;span class="nt"&gt;-i&lt;/span&gt; ~/.ssh/layer2024_xserver.key &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;IdentitiesOnly&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;yes&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;IdentityAgent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;none &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-p&lt;/span&gt; 10022 &lt;span class="se"&gt;\&lt;/span&gt;
    layer2024@layer2024.xsrv.jp &lt;span class="s1"&gt;'echo ok'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;IdentitiesOnly=yes&lt;/code&gt; — only the key specified with &lt;code&gt;-i&lt;/code&gt; is tried (no auto-tries on &lt;code&gt;~/.ssh/id_*&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;IdentityAgent=none&lt;/code&gt; — keys held in the SSH agent are not tried at all&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;'echo ok'&lt;/code&gt; — checks the connection without opening a shell, then exits immediately&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Make this your standard "first touch" command for Xserver. If your maintenance tooling carries the same posture (paramiko's &lt;code&gt;look_for_keys=False&lt;/code&gt; / &lt;code&gt;allow_agent=False&lt;/code&gt;), the structural cause stops recurring.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;p&gt;If SSH to Xserver suddenly stops working, suspect fail2ban first, not a server fault. No manual-unban path exists; the choice is wait or switch source IPs. After recovery, run your first connection with &lt;code&gt;IdentitiesOnly=yes&lt;/code&gt; / &lt;code&gt;IdentityAgent=none&lt;/code&gt; so the verification itself doesn't re-trigger the block. For the application-side fix, &lt;a href="https://en.wpmm.jp/blog/paramiko-look-for-keys-trap/" rel="noopener noreferrer"&gt;paramiko's default behavior&lt;/a&gt; covers the design angle.&lt;/p&gt;

&lt;p&gt;The half-second of "did the server break?" panic settles a lot faster once you can name the protection mechanism by sight. It's a small piece of operational vocabulary that meaningfully changes how an agency carries the surprise.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>php</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Five failure patterns from past WordPress major upgrades — what 5.0 Gutenberg through 6.0 FSE taught maintenance teams</title>
      <dc:creator>Susumu Takahashi</dc:creator>
      <pubDate>Thu, 16 Jul 2026 23:33:30 +0000</pubDate>
      <link>https://dev.to/susumun/five-failure-patterns-from-past-wordpress-major-upgrades-what-50-gutenberg-through-60-fse-ocb</link>
      <guid>https://dev.to/susumun/five-failure-patterns-from-past-wordpress-major-upgrades-what-50-gutenberg-through-60-fse-ocb</guid>
      <description>&lt;p&gt;With &lt;a href="https://en.wpmm.jp/blog/wordpress-major-upgrade-pre-flight-checklist/" rel="noopener noreferrer"&gt;the pre-upgrade checklist of seven items&lt;/a&gt; and &lt;a href="https://en.wpmm.jp/blog/wait-before-wordpress-major-update/" rel="noopener noreferrer"&gt;the calibration framework for when to apply&lt;/a&gt;, we've covered &lt;strong&gt;preparation and timing&lt;/strong&gt; for WordPress major upgrades. The third installment is "&lt;strong&gt;what can go wrong&lt;/strong&gt;" — the failures that actually happened in past majors, organized into five patterns.&lt;/p&gt;

&lt;p&gt;The examples are tied to specific releases (5.0 / 5.6 / 6.0), but the point is that &lt;strong&gt;the same structural patterns repeat in new majors&lt;/strong&gt;. Carrying these as types in your head pays off when 7.0 or 8.0 lands and you need to triage fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 1 — "Editor-adjacent UI extensions" disappear en masse (5.0 Gutenberg)
&lt;/h2&gt;

&lt;p&gt;The most operationally impactful incident when WordPress 5.0 standardized the block editor (Gutenberg).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Symptom&lt;/strong&gt;: Custom meta boxes assuming the classic editor (TinyMCE), custom field UIs, and third-party plugin editor extensions stopped working simultaneously. Content itself wasn't broken — but &lt;strong&gt;the editing UI vanished&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cause&lt;/strong&gt;: Code written against the classic editor's DOM and hooks loses its anchor points in Gutenberg's React-based UI. Simple &lt;code&gt;add_meta_box()&lt;/code&gt; extensions kept working, but &lt;strong&gt;anything hooked directly into a TinyMCE instance fell over completely&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix pattern&lt;/strong&gt;: Run the &lt;code&gt;Classic Editor&lt;/code&gt; plugin to preserve the old UI temporarily, while planning the migration to Gutenberg-aware forks or replacement plugins. &lt;strong&gt;"Inventory editor-adjacent plugin dependencies before applying the upgrade"&lt;/strong&gt; makes the migration scope visible up front.&lt;/p&gt;

&lt;p&gt;What this incident demonstrates is that &lt;strong&gt;the flagship feature of a major release maximizes impact on the surrounding ecosystem&lt;/strong&gt;. The same structure will repeat with any future "new editor feature" or "new admin UI."&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 2 — A wholesale JS library bump triggers cascading breakage (5.6 jQuery 3.x)
&lt;/h2&gt;

&lt;p&gt;When WordPress 5.6 &lt;strong&gt;jumped jQuery from the 1.x series to 3.x&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Symptom&lt;/strong&gt;: Front-end JS goes dark. &lt;strong&gt;Broadly-used jQuery plugins&lt;/strong&gt; for sliders, modals, form validation, etc. stop working, and the site's "dynamic parts" break across themes and plugins.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cause&lt;/strong&gt;: jQuery 3.x removes APIs that had been deprecated for years — &lt;code&gt;$.live()&lt;/code&gt;, parts of &lt;code&gt;$.bind()&lt;/code&gt; semantics, parts of the &lt;code&gt;$.ajax&lt;/code&gt; Promise interface. Code depending on them doesn't error syntactically — it &lt;strong&gt;fails silently at runtime&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix pattern&lt;/strong&gt;: Bring in &lt;code&gt;jquery-migrate&lt;/code&gt; to provide a compatibility layer for the deprecated APIs, then refactor JS based on the console warnings it logs. &lt;strong&gt;Opening the browser dev-tools Console while clicking through staging&lt;/strong&gt; becomes a required step.&lt;/p&gt;

&lt;p&gt;The lesson here is that &lt;strong&gt;"the front-end isn't visible from an admin check."&lt;/strong&gt; That's why &lt;a href="https://en.wpmm.jp/blog/wordpress-major-upgrade-pre-flight-checklist/" rel="noopener noreferrer"&gt;the staging verification in W1&lt;/a&gt; includes "do the critical front-end pages render correctly" — exactly this pattern is why.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 3 — Raised PHP minimum trips syntax errors in old plugins
&lt;/h2&gt;

&lt;p&gt;Unlike the previous two, this is a &lt;strong&gt;general pattern that recurs across many majors&lt;/strong&gt;, not tied to a specific release. The notable boundaries are PHP 7.4 → 8.0 and PHP 8.1 → 8.2 (the &lt;code&gt;dynamic property&lt;/code&gt; deprecation).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Symptom&lt;/strong&gt;: After the upgrade, &lt;strong&gt;specific plugins show as "deactivated" in the admin&lt;/strong&gt;, or the site is white-screened with a Fatal error. The error log shows &lt;code&gt;PHP Parse error&lt;/code&gt; or &lt;code&gt;PHP Fatal error: Cannot use ... in write context&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cause&lt;/strong&gt;: Each new PHP version tightens syntax and the type system. A plugin's "slightly old code" &lt;strong&gt;can no longer be parsed&lt;/strong&gt; under newer PHP. Or, a warning becomes a deprecation, and depending on display settings, &lt;strong&gt;looks fatal&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix pattern&lt;/strong&gt;: Before the upgrade, run &lt;code&gt;wp plugin list --format=json&lt;/code&gt; against the new PHP environment and verify the JSON comes back clean (alongside &lt;a href="https://en.wpmm.jp/blog/wp-cli-php-deprecated-noise/" rel="noopener noreferrer"&gt;the trap of operational tools breaking from deprecated noise&lt;/a&gt;). Plugins with old "Tested up to" deserve replacement evaluation prior to the major upgrade.&lt;/p&gt;

&lt;p&gt;What's particularly painful is the &lt;strong&gt;"PHP raised, but old plugins kept"&lt;/strong&gt; in-between state. The PHP version may satisfy WordPress's minimum while the plugin lags behind PHP's new version — and the site still breaks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 4 — Theme structure changes invalidate child-theme customizations (6.0 FSE)
&lt;/h2&gt;

&lt;p&gt;When Full Site Editing (FSE) matured in WordPress 6.0.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Symptom&lt;/strong&gt;: Existing Classic Theme customizations don't break on the 6.x upgrade itself — but the moment you switch to a Block Theme, &lt;strong&gt;the &lt;code&gt;template-parts/*.php&lt;/code&gt; overrides your child theme provided stop being effective&lt;/strong&gt;. The traditional template hierarchy (&lt;code&gt;header.php&lt;/code&gt;, &lt;code&gt;footer.php&lt;/code&gt;) is replaced by HTML templates (&lt;code&gt;block-templates/*.html&lt;/code&gt;) in Block Themes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cause&lt;/strong&gt;: Block Themes have a &lt;strong&gt;fundamentally different file structure&lt;/strong&gt; from Classic Themes. The child-theme override mechanism relies on the old filename conventions, so it can't reach into the Block Theme's HTML template structure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix pattern&lt;/strong&gt;: Two paths — keep the Classic Theme for the foreseeable future, or commit to a &lt;strong&gt;child-theme redesign&lt;/strong&gt; as part of migrating to a Block Theme. "Should we migrate the client's theme?" and "Should we upgrade WordPress?" become &lt;strong&gt;separate decisions&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The takeaway is that &lt;strong&gt;major releases create transition periods&lt;/strong&gt;. When two systems coexist — Classic Theme and Block Theme — having a documented operational rule for "which one do we stay on" pays off.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 5 — Tiny internal API tweaks quietly break external integrations
&lt;/h2&gt;

&lt;p&gt;The first four are relatively easy to notice. This last one is &lt;strong&gt;the slowest to discover&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Symptom&lt;/strong&gt;: Everything looks normal immediately after the upgrade. Days or weeks later, &lt;strong&gt;"the API integration where an external system POSTs into WordPress has stopped working"&lt;/strong&gt; comes to light. Auto-posting and auto-updates from CRMs, core business systems, or internal workflows fail silently with no error log entries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cause&lt;/strong&gt;: REST API scope changes, subtle authentication tweaks, CORS header expectations, &lt;code&gt;X-WP-Nonce&lt;/code&gt; lifetime changes — small items buried in the "&lt;strong&gt;Developer Notes&lt;/strong&gt;" section of the official release notes that break external auth flows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix pattern&lt;/strong&gt;: &lt;a href="https://en.wpmm.jp/blog/wait-before-wordpress-major-update/" rel="noopener noreferrer"&gt;The one-week soak in staging from W2&lt;/a&gt; exists exactly to catch this pattern. The failure mode doesn't show up in an immediate post-upgrade admin click-through — it &lt;strong&gt;only surfaces with time and real traffic&lt;/strong&gt;. Run Site Health's Loopback test, let cron jobs cycle, leave staging running for a stretch and watch.&lt;/p&gt;

&lt;p&gt;For sites with external integrations, &lt;strong&gt;"every integration's auth path, tested end-to-end"&lt;/strong&gt; must be on the major-upgrade checklist.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing — knowing the patterns lets you handle future failures
&lt;/h2&gt;

&lt;p&gt;Stacking the five patterns shows the common axes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Editor and UI surfaces&lt;/strong&gt; concentrate breakage when flagship features ship (Pattern 1)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JS / PHP major version dependencies&lt;/strong&gt; trigger cascading failures (Patterns 2 &amp;amp; 3)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structural pre-condition changes&lt;/strong&gt; create a "transition period" with two systems coexisting (Pattern 4)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Silent breakage&lt;/strong&gt; only surfaces with time (Pattern 5)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These structures &lt;strong&gt;will recur in some form in the next major (7.0 or 8.0)&lt;/strong&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;The full series relationship&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;W1 = what to verify&lt;/strong&gt; (seven-item pre-flight checklist)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;W2 = when to apply&lt;/strong&gt; (five calibration axes)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;W3 = what can go wrong&lt;/strong&gt; (five typical failure patterns) ← this post&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;W4 = how to run it&lt;/strong&gt; (&lt;a href="https://en.wpmm.jp/blog/wordpress-major-upgrade-wpcli-step-by-step/" rel="noopener noreferrer"&gt;WP-CLI commands and checkpoints for each stage&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Aligning the whole upgrade lifecycle as "preparation → timing → risk prediction" sharpens operational decisions. When &lt;strong&gt;WordPress 7.0&lt;/strong&gt; drops, starting with "which of the five past patterns does this match?" tends to point you at the right response faster.&lt;/p&gt;

&lt;p&gt;The "types" here aren't visible when you only operate one site. Maintaining a portfolio of sites, you tend to meet at least one of them in some form on every major. Moving from &lt;strong&gt;reactive triage per incident&lt;/strong&gt; to &lt;strong&gt;predictive pattern matching&lt;/strong&gt; — that shift is, in my view, the core of making maintenance work sustainable as a practice.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>php</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Don't apply WordPress major releases on day one — the "x.0.1 rule" and a calibration framework</title>
      <dc:creator>Susumu Takahashi</dc:creator>
      <pubDate>Thu, 16 Jul 2026 00:10:26 +0000</pubDate>
      <link>https://dev.to/susumun/dont-apply-wordpress-major-releases-on-day-one-the-x01-rule-and-a-calibration-framework-1edb</link>
      <guid>https://dev.to/susumun/dont-apply-wordpress-major-releases-on-day-one-the-x01-rule-and-a-calibration-framework-1edb</guid>
      <description>&lt;p&gt;The companion to &lt;a href="https://en.wpmm.jp/blog/wordpress-major-upgrade-pre-flight-checklist/" rel="noopener noreferrer"&gt;the seven things to check before a WordPress major upgrade&lt;/a&gt; is the question that comes right after: &lt;strong&gt;when do you actually apply it?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A new WordPress major drops today. Do you ship it to production tonight? Tomorrow? In a week? Hold for the next scheduled monthly maintenance? This call tends to live in tribal knowledge, but a few clear axes combined together give you a calibration framework you can apply every time without re-deciding from scratch. Here are five axes worth using.&lt;/p&gt;

&lt;h2&gt;
  
  
  Premise — majors are not security patches
&lt;/h2&gt;

&lt;p&gt;The first thing to anchor: &lt;strong&gt;a major upgrade is not a security patch&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;WordPress ships security fixes via minor releases (&lt;code&gt;6.4.1 → 6.4.2&lt;/code&gt;, &lt;code&gt;6.5.0 → 6.5.1&lt;/code&gt; — the &lt;strong&gt;second-digit bumps&lt;/strong&gt;). Those are &lt;strong&gt;same-day apply by default&lt;/strong&gt;. Major releases (&lt;code&gt;6.4 → 6.5&lt;/code&gt;, eventually some &lt;code&gt;6.x → 7.0&lt;/code&gt;) carry new features and API changes; they aren't released to be applied immediately for security reasons.&lt;/p&gt;

&lt;p&gt;Without this distinction, the felt urgency of "&lt;strong&gt;we have to apply this for security&lt;/strong&gt;" pushes you to rush majors that should wait. Minors immediately, majors by judgment — that separation is the first rule worth writing down.&lt;/p&gt;

&lt;h2&gt;
  
  
  Axis 1 — wait for &lt;code&gt;x.0.1&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;For essentially every major release, &lt;strong&gt;&lt;code&gt;x.0.1&lt;/code&gt; (the first patch release) lands within 1–3 weeks&lt;/strong&gt; and absorbs the critical bugs that surfaced after launch.&lt;/p&gt;

&lt;p&gt;Past examples have included things like "the admin goes white under specific settings," "a particular theme breaks the block editor," "DB migration stalls in specific environments." Nobody hit these on launch day; they emerged as the world started using it for days or weeks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Just waiting for &lt;code&gt;x.0.1&lt;/code&gt; instead of &lt;code&gt;x.0&lt;/code&gt;&lt;/strong&gt; sidesteps most of those launch-window bugs. For the first few weeks after a major lands, the world's WordPress installations are effectively running the &lt;strong&gt;beta test&lt;/strong&gt;. Being downstream of the people who hit the mines is the rational position for a maintenance practice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Axis 2 — wait until major plugin vendors update "Tested up to"
&lt;/h2&gt;

&lt;p&gt;The thing that breaks most after a major upgrade is, almost always, &lt;strong&gt;plugin compatibility&lt;/strong&gt;. Theme compatibility follows, but plugins dominate the impact column.&lt;/p&gt;

&lt;p&gt;What to watch is whether &lt;strong&gt;major plugin vendors have declared support&lt;/strong&gt;. Concretely:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ACF (Advanced Custom Fields)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WooCommerce&lt;/strong&gt; (mandatory if you're touching commerce)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Yoast SEO / Rank Math&lt;/strong&gt; (SEO)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Elementor / Beaver Builder&lt;/strong&gt; (page builders)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WPML / Polylang&lt;/strong&gt; (multilingual)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wordfence / iThemes Security&lt;/strong&gt; (security)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Until these heavyweight plugins bump &lt;code&gt;Tested up to&lt;/code&gt; in their &lt;code&gt;readme.txt&lt;/code&gt; or publish a "we support it" announcement on their blog, production-side adoption stays paused. &lt;strong&gt;If the major plugins haven't caught up, the WordPress release hasn't 'aged' yet&lt;/strong&gt; is the call we make.&lt;/p&gt;

&lt;h2&gt;
  
  
  Axis 3 — soak in staging for a week
&lt;/h2&gt;

&lt;p&gt;The staging step from W1 has an extra job for majors: catch the &lt;strong&gt;bugs that surface over elapsed time&lt;/strong&gt;, not just the ones visible right after upgrade. Specifically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;wp-cron&lt;/strong&gt; continues to run (daily / weekly jobs fire on schedule)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Email delivery&lt;/strong&gt; hasn't quietly broken (comment notifications, contact forms)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Caches&lt;/strong&gt; haven't corrupted into stale content&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image / OGP generation&lt;/strong&gt; doesn't fail under time-delayed conditions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These don't show up in "upgrade and click around the admin for ten minutes." They need &lt;strong&gt;at least a week of natural operation&lt;/strong&gt;. Soak the staging site for a week; if the error logs aren't accumulating anything strange, you've earned the right to consider production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Axis 4 — site risk profile
&lt;/h2&gt;

&lt;p&gt;"When to apply" also shifts with the site's risk profile. A rough three-tier breakdown:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Profile&lt;/th&gt;
&lt;th&gt;Examples&lt;/th&gt;
&lt;th&gt;Recommended wait&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;High risk&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Commerce, booking, membership, LMS&lt;/td&gt;
&lt;td&gt;After &lt;code&gt;x.0.2&lt;/code&gt; + all major plugins confirmed + 2 weeks of staging&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Medium risk&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Mid-size corporate, blog/media, contact forms&lt;/td&gt;
&lt;td&gt;After &lt;code&gt;x.0.1&lt;/code&gt; + major plugins confirmed + 1 week of staging&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Low risk&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Static corporate, internal portal, experimentation&lt;/td&gt;
&lt;td&gt;After &lt;code&gt;x.0.1&lt;/code&gt; + a few days of staging&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Commerce and booking directly translate "&lt;strong&gt;dropped orders = lost revenue&lt;/strong&gt;," so the longer wait pays for itself. A static corporate site might tolerate a minor layout glitch with no immediate business impact, so a more forward posture is fine.&lt;/p&gt;

&lt;p&gt;Avoiding "all sites upgrade on the same day" and applying &lt;strong&gt;in waves by risk profile&lt;/strong&gt; is one of the better distribution strategies for a maintenance practice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Axis 5 — alignment with the client contract / SLA
&lt;/h2&gt;

&lt;p&gt;Last axis is &lt;strong&gt;contractual&lt;/strong&gt;. If you have monthly recurring maintenance contracts, aligning major upgrades to the next monthly window makes contractual sense — it's the cleanest billing and scope story.&lt;/p&gt;

&lt;p&gt;Check each release for "is there an emergency-update need that exceeds monthly-maintenance scope?" If no, fold it into the next month. If yes (rare cases where a critical security fix rides on the major), carve it out as a separate engagement with its own scope and price.&lt;/p&gt;

&lt;p&gt;Writing this boundary into the contract upfront reduces the friction between "apply this now" and "let's wait" expectations across clients, and the operational rhythm stays steady.&lt;/p&gt;

&lt;h2&gt;
  
  
  Framing — the cost of waiting vs. the cost of charging in
&lt;/h2&gt;

&lt;p&gt;Stacking five axes can read as "we never do anything," but waiting has real costs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The window without new features is longer&lt;/li&gt;
&lt;li&gt;When you do upgrade, the delta accumulates with simultaneous minor updates&lt;/li&gt;
&lt;li&gt;You drift closer to old-version support cutoffs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Charging in has its own:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Outages → client trust erosion&lt;/li&gt;
&lt;li&gt;Recovery cost (including after-hours engineering)&lt;/li&gt;
&lt;li&gt;In some cases, direct business continuity impact&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Calibrating between those, against each site's risk profile, is the heart of "when to apply." There's no closed-form rule, but the five axes together give you &lt;strong&gt;a decision skeleton that doesn't drift&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When a major upgrade does turn into an incident, you may need to invoke a &lt;a href="https://en.wpmm.jp/blog/halt-after-core-rollback/" rel="noopener noreferrer"&gt;post-rollback halt path on the core&lt;/a&gt; — and remembering that a rushed upgrade can create extended recovery work makes the few-week wait for &lt;code&gt;x.0.1&lt;/code&gt; look, in long-term maintenance ROI terms, very nearly always in the black. Read together with &lt;a href="https://en.wpmm.jp/blog/wordpress-major-upgrade-failure-patterns/" rel="noopener noreferrer"&gt;five failure patterns&lt;/a&gt; — which turns past incidents into types — and the calibration sharpens further.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>php</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Seven things to check before a WordPress major upgrade — before "patch what breaks after" becomes a disaster</title>
      <dc:creator>Susumu Takahashi</dc:creator>
      <pubDate>Wed, 15 Jul 2026 00:37:22 +0000</pubDate>
      <link>https://dev.to/susumun/seven-things-to-check-before-a-wordpress-major-upgrade-before-patch-what-breaks-after-becomes-a-32f7</link>
      <guid>https://dev.to/susumun/seven-things-to-check-before-a-wordpress-major-upgrade-before-patch-what-breaks-after-becomes-a-32f7</guid>
      <description>&lt;p&gt;WordPress major version upgrades (5.x → 6.x, and eventually 6.x → 7.x) are a different animal from minor releases. Minor releases (like 6.4.1 → 6.4.2) are mostly bug fixes with low compatibility risk. Majors land &lt;strong&gt;API deprecations, raised PHP minimum requirements, and core block replacements&lt;/strong&gt; all at once — and those things hit operations hard.&lt;/p&gt;

&lt;p&gt;The "&lt;strong&gt;just hit Update in the admin and patch whatever breaks&lt;/strong&gt;" workflow can survive on a single personal site, but it tends to fall apart under multi-site maintenance — simultaneous failures across sites overwhelm root-cause triage. This post collects the things worth verifying before you run a major upgrade, &lt;strong&gt;as a seven-item checklist&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Has the minimum PHP version been raised?
&lt;/h2&gt;

&lt;p&gt;Major WordPress releases sometimes &lt;strong&gt;raise the minimum supported PHP version&lt;/strong&gt; (6.6 lifted it to PHP 7.2.24, and a future 7.0 will very likely require PHP 8.x). What matters operationally isn't just the &lt;strong&gt;server's PHP version&lt;/strong&gt; and &lt;strong&gt;WordPress's stated minimum&lt;/strong&gt; — it's the &lt;strong&gt;intersection with the PHP versions your plugins and themes actually run on&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You can usually upgrade your server's PHP, but &lt;strong&gt;older themes and plugins not running on new PHP&lt;/strong&gt; isn't rare. A subtle failure mode here: traps like &lt;a href="https://en.wpmm.jp/blog/wp-cli-php-deprecated-noise/" rel="noopener noreferrer"&gt;PHP 8.2+ deprecated warnings leaking into older WP-CLI JSON output&lt;/a&gt;, where nothing visibly errors but your operational tooling silently breaks. Before upgrading, run &lt;code&gt;wp plugin list --format=json&lt;/code&gt; on the production PHP environment and verify you're getting clean JSON. That one check catches a lot of post-upgrade pain.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Audit "Tested up to" for every plugin
&lt;/h2&gt;

&lt;p&gt;Each plugin's &lt;code&gt;readme.txt&lt;/code&gt; carries a &lt;code&gt;Tested up to: X.X&lt;/code&gt; line — the developer's declaration of &lt;strong&gt;the highest WordPress version they've actually tested against&lt;/strong&gt;. It's the first signal for major-upgrade compatibility audits.&lt;/p&gt;

&lt;p&gt;WP-CLI gives you the inventory in one shot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wp plugin list &lt;span class="nt"&gt;--fields&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;name,version,update_version,update &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;table
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Plugins where &lt;strong&gt;"Tested up to" is old&lt;/strong&gt; AND &lt;strong&gt;no updates in the last year&lt;/strong&gt; deserve scrutiny. Active development may have stopped, which means no fix is coming when the major upgrade breaks them. Better to identify replacement candidates pre-upgrade than to scramble post-upgrade.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Theme compatibility — child themes and deep customizations
&lt;/h2&gt;

&lt;p&gt;Themes feel the major upgrade too. Watch especially for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Block editor APIs&lt;/strong&gt; (5.x→6.x landed massive API changes here)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Customized child themes&lt;/strong&gt; (parent theme updates, but child theme hooks stay on the old API)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Page-builder plugins&lt;/strong&gt; and their version requirements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In client work, "&lt;strong&gt;the theme is too old to upgrade PHP&lt;/strong&gt;" is a regular blocker. Theme-side constraints that effectively pin WordPress to an older version need to surface at planning time, not after you've already started the upgrade.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Database backups — keep at least three generations
&lt;/h2&gt;

&lt;p&gt;Major upgrades often involve database migrations. WordPress migrations are idempotent in principle, but &lt;strong&gt;recovering from one that dies halfway is unpleasant&lt;/strong&gt;. Take a full backup before running the upgrade, and keep &lt;strong&gt;the latest, one before, and one further back&lt;/strong&gt; — three generations is the safe floor.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# DB export (compressed)&lt;/span&gt;
wp db &lt;span class="nb"&gt;export&lt;/span&gt; - | &lt;span class="nb"&gt;gzip&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; backup_&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%Y%m%d_%H%M%S&lt;span class="si"&gt;)&lt;/span&gt;.sql.gz

&lt;span class="c"&gt;# wp-content if you want the full bundle&lt;/span&gt;
&lt;span class="nb"&gt;tar &lt;/span&gt;czf wp-content_&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%Y%m%d_%H%M%S&lt;span class="si"&gt;)&lt;/span&gt;.tar.gz wp-content/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For large sites, &lt;strong&gt;an SSH disconnect mid-export can leave a partial .sql lying around&lt;/strong&gt;. Verify completion before moving on.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Run it in staging first
&lt;/h2&gt;

&lt;p&gt;Hitting production with a major upgrade — &lt;strong&gt;even on a single site&lt;/strong&gt; — is something to avoid. Mirror production into staging, upgrade there first, eyeball the front end, click through the admin, check WP-CLI, walk through the top plugins' admin screens. Then production.&lt;/p&gt;

&lt;p&gt;Some hosts ship a staging feature out of the box; that's usually the fastest path. Otherwise an &lt;code&gt;rsync&lt;/code&gt; to a separate directory on the same server (with a separate DB import) does the job.&lt;/p&gt;

&lt;p&gt;The minimum verification list for staging:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Admin opens; post editor renders correctly&lt;/li&gt;
&lt;li&gt;Critical front-end pages render normally&lt;/li&gt;
&lt;li&gt;Forms and other dynamic features actually work&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;wp plugin list&lt;/code&gt; returns clean output&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  6. Use WP-CLI to stage the updates (skip the admin's one-click)
&lt;/h2&gt;

&lt;p&gt;The admin's "&lt;strong&gt;Update&lt;/strong&gt;" button is designed to &lt;strong&gt;fire everything at once&lt;/strong&gt;. Core, plugins, themes — all in a batch. When something breaks, &lt;strong&gt;you can't tell which one broke it&lt;/strong&gt;. That's particularly damaging on major upgrades.&lt;/p&gt;

&lt;p&gt;WP-CLI lets you do core / plugins / themes separately, in order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Core first&lt;/span&gt;
wp core update &lt;span class="nt"&gt;--version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;6.7

&lt;span class="c"&gt;# 2. DB migration&lt;/span&gt;
wp core update-db

&lt;span class="c"&gt;# 3. Plugins one at a time&lt;/span&gt;
wp plugin update &amp;lt;plugin-name&amp;gt;

&lt;span class="c"&gt;# 4. Themes the same way&lt;/span&gt;
wp theme update &amp;lt;theme-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Passing &lt;code&gt;--version=&lt;/code&gt; keeps you from &lt;strong&gt;jumping multiple versions at once&lt;/strong&gt;. From 6.4 to 6.7, going 6.5 → 6.6 → 6.7 instead of a direct jump makes it much easier to localize whatever goes wrong to a specific step.&lt;/p&gt;

&lt;p&gt;A related gotcha: there are scenarios where &lt;a href="https://en.wpmm.jp/blog/wp-cli-skip-plugins-rescue/" rel="noopener noreferrer"&gt;WP-CLI itself fails to bootstrap because of a broken plugin&lt;/a&gt;. Knowing the &lt;code&gt;--skip-plugins --skip-themes&lt;/code&gt; rescue flags pays for itself the first time WP-CLI refuses to start after an upgrade.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Write the rollback procedure down — before you upgrade
&lt;/h2&gt;

&lt;p&gt;Last item: &lt;strong&gt;write down how you'll roll back&lt;/strong&gt; before you start. "I'll just google it if I need to" doesn't survive contact with reality — under outage stress you don't want to be researching, you want to be executing a known procedure.&lt;/p&gt;

&lt;p&gt;Minimum contents:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DB restore command (using the backup from step 5)&lt;/li&gt;
&lt;li&gt;Filesystem restore command (the exact &lt;code&gt;tar xzf&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;WP-CLI command to roll core back one version:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  wp core update &lt;span class="nt"&gt;--version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;6.6 &lt;span class="nt"&gt;--force&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Per-plugin rollback (the approach from &lt;a href="https://en.wpmm.jp/blog/wp-cli-pinpoint-rollback/" rel="noopener noreferrer"&gt;pinpoint rollback with WP-CLI&lt;/a&gt;):
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  wp plugin &lt;span class="nb"&gt;install&lt;/span&gt; &amp;lt;plugin-name&amp;gt; &lt;span class="nt"&gt;--version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;X.X.X &lt;span class="nt"&gt;--force&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The act of writing this down surfaces missing prep — you'll find a missing backup, a server where WP-CLI isn't installed, the wrong privileges. The document itself doubles as a pre-flight check.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing — ten minutes before saves hours after
&lt;/h2&gt;

&lt;p&gt;Listing seven items makes the upfront work look heavy, but with practice &lt;strong&gt;the pre-upgrade check takes 10–15 minutes total&lt;/strong&gt; (the next question — "&lt;strong&gt;when do you actually apply it?&lt;/strong&gt;" — is covered in &lt;a href="https://en.wpmm.jp/blog/wait-before-wordpress-major-update/" rel="noopener noreferrer"&gt;the x.0.1 rule and a calibration framework&lt;/a&gt;, and the failures that actually happened in past majors are in &lt;a href="https://en.wpmm.jp/blog/wordpress-major-upgrade-failure-patterns/" rel="noopener noreferrer"&gt;five failure patterns&lt;/a&gt;). Charging in without it and recovering from a broken site is a &lt;strong&gt;multi-hour to half-day&lt;/strong&gt; project. With more than a handful of sites under maintenance, that ratio dominates your operational economics.&lt;/p&gt;

&lt;p&gt;"&lt;strong&gt;Just patch whatever breaks&lt;/strong&gt;" works for a hobby site. It doesn't work as a maintenance offering. Keep one checklist by your side, walk down it every time a major upgrade comes around — that alone bends the incident rate down measurably.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>php</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
