The Bridge Nobody Should Need Just Became the Only Way In

July 15, 202611 min read

Eight months after I over-engineered a GitHub Actions publishing pipeline for a friend's WordPress site, the site went down hard — and that pipeline held the only deploy key within reach. A post-mortem on WPIDE, PHP 8.2, --skip-plugins, and the day the automation math finally worked out.

Or: How a Code-Editor Plugin Murdered the Site, and the 10-Hour Automation of a 5-Minute Task Paid For Itself in One Evening

Previously, on Ali G's Website

Last November I wrote about building a WordPress publishing bridge nobody should need — a GitHub Actions pipeline that SSHes into SiteGround and drives WP-CLI directly, because the REST API is security-theater'd into uselessness. The site belonged to my friend Ali G1, the task was one page and one menu item, and I spent roughly ten hours automating what would have been five minutes of clicking in wp-admin.

I defended that decision at the time with what I called the programmer's disease: the conviction that automation is always worth it, even when the math clearly says it's not2.

This is the post where the math changes.

Act I: "There has been a critical error on this website"

Tuesday evening, a notification: aligstudios.com is down. Not degraded, not slow — down. Every URL on the site returns WordPress's beige little apology:

There has been a critical error on this website.

Homepage: critical error. Blog: critical error. And — this is the part that matters — wp-admin: critical error. The dashboard, the button-clicking interface, the thing WordPress optimizes its entire existence around? Also dead. When a plugin fatals during bootstrap, it takes everything down with it, including the admin screens you'd use to disable the plugin3.

So: no wp-admin. Fine, I thought, I'm not a wp-admin person anyway. I'll SSH in and fix it from the command line like a serious individual.

Except.

I'm not on the machine that did the November work. All of that — the initial WP-CLI setup, the SSH keys — happened on the laptop I'd brought to Queen Street Market4, and that laptop, along with the only local copy of the deploy key, was not in the room with me. The other copy was in GitHub Secrets, which is encrypted, access-controlled, audit-logged — and write-only. You can use a secret in a workflow, but you can never read it back out5.

Which means the only working credential within reach belonged to a GitHub Actions runner: an ephemeral Ubuntu VM in an Azure datacenter that exists only for the length of one job, then evaporates.

The bridge nobody should need was now the only way in.

Act II: Ops by Git Push

Here's the thing though: a CI pipeline that can SSH in and run wp post create can SSH in and run anything. The publishing workflow was, without my ever intending it, a remote execution framework with an audit log. I just had to write workflows instead of typing commands.

Step one: do no harm. Before touching anything I added a read-only diagnostics workflow — Diagnose Site — that SSHes in and looks: PHP version, WordPress version, plugin list, the last few hundred lines of the error log. Commit, push, wait for the runner, read the output like a telegram from the server6.

The error log gave it up immediately:

PHP Fatal error: ... E_COMPILE_ERROR ... php-di/php-di/src/Container.php

The chain: WPIDE 3.5.7 — a plugin — bundles its own copy of the php-di dependency-injection library, and that vintage of php-di does not compile under PHP 8.2. SiteGround had rolled the site onto PHP 8.27. PHP hit WPIDE's bundled library during plugin load and threw a compile error. And since plugins load during bootstrap, every request died before WordPress finished waking up.

And here is the irony so dense it has its own gravitational field: WPIDE is a code editor that runs inside wp-admin. It exists so that you can edit your site's PHP by clicking buttons in a browser — the purest possible expression of the humans-clicking-buttons philosophy I spent four thousand words complaining about last time. The button-clicking tool took down the site, and took the buttons down with it8.

Step two: the fix. One more workflow — Fix WPIDE — whose entire job is:

wp plugin deactivate wpide --skip-plugins

That --skip-plugins flag is the whole ballgame, and it's worth dwelling on, because the obvious approach does not work. WP-CLI loads WordPress, and loading WordPress loads plugins, which means the fatal error that killed the website also kills the command-line tool you'd use to fix the website9. Plain wp plugin deactivate wpide dies with the same compile error as the homepage. --skip-plugins tells WP-CLI to bootstrap WordPress without loading any plugin code — sneak past the body in the doorway, flip the database flag that marks WPIDE inactive, get out.

Push. Watch the runner. Deactivated. Flush the object cache and SiteGround's Dynamic Cache (also from the workflow — the cache was happily serving the error page even after the fix10), curl the homepage from the server side:

HTTP 200.

Site's back. Total time from "let me look at this" to recovery: about ninety minutes, most of it spent watching Actions runners spin up11.

Act III: The Part Where I Actually Earn the Friend Rate

A site that went down silently once will go down silently again. Recovery is table stakes; the actual deliverable is that I never again learn about an outage from Ali. So the rest of the evening went to prevention, all of it living in the same repo, all of it running on the same bridge:

A daily audit script (wp-audit.sh, scheduled workflow, 21:00 UTC) that checks the things that actually predict outages:

  • Bootstrap health — can WordPress even load with plugins enabled? This single check catches the entire class of failure that caused this outage, before a human notices.
  • Homepage HTTP status, from the server side.
  • Pending core, plugin, and theme updates.
  • A dormant-plugin inventory — the roommates nobody remembers inviting.

A report that survives the apocalypse it's reporting on. The audit emails its findings (to Ali and to me) through the site's own wp_mail — but the mailer runs with --skip-plugins, so the alert goes out even when the site is fatally broken, which is exactly the moment the site's own recovery machinery is at its least reliable12. If the site is unhealthy the subject line gets a [CRITICAL] prefix, and the workflow itself exits nonzero, so GitHub's failed-run notification acts as a second, independent alarm channel.

Attack-surface cleanup. Thirteen dormant plugins deleted — WPIDE, obviously, plus a museum of the site's past lives: ACF and its Extended sibling, All-in-One SEO, OptinMonster, TrustPulse, Social Warfare, a YARPP, a Duplicate Post, and four more of similar vintage13. Each one was bootstrap-path code and a future CVE waiting for its moment. A couple of premium ones stayed on disk, deactivated, because re-obtaining paid plugins is its own circle of hell.

And a core update, 6.9.1 → 6.9.4, security releases the site had been quietly skipping — with a database backup first, checksum verification of every core file after, cache flush, HTTP 200 confirmed. Boring. Boring is the goal.

Then I wrote the runbook down as a project skill in the repo, so the next incident starts from "run the diagnostics workflow" instead of from archaeology14.

Act IV: What This Was Actually About

The November post ended with a complaint: WordPress claims to be automatable and isn't, and if GitHub Actions can't reliably talk to it, AI agents certainly can't. I stand by every word. But this incident surfaced the stronger version of the argument, which is not about publishing at all.

The interface you build for content is the interface you'll have in a crisis. When the outage hit, wp-admin was dead, recovery-mode email was dead, WP-CLI-over-SSH-by-hand was a cross-town errand away for the key-location reason in footnote 4. What remained was the repo: versioned, credentialed, executable. The publishing bridge worked as an incident-response system because it never assumed the site was healthy — it just runs commands on a box. Every abstraction above raw SSH would have been leaning on some part of WordPress that was, at that moment, on fire.

Monitoring belongs outside the thing it monitors. WordPress's self-healing story (recovery mode, health checks, the Site Health screen) all lives inside the patient. The daily audit lives in GitHub's infrastructure, holds its own credentials, and is engineered — via --skip-plugins — to deliver bad news precisely when the patient can't speak for itself.

And the AI-agent point sharpens too. Most of this evening's work — the diagnostics workflow, the audit script, the runbook — was done with an AI agent driving inside the repo, because a repo full of workflows and markdown is an environment agents are native to. The agent couldn't have clicked through wp-admin's floating toolbars on its best day, and on this particular day there was no wp-admin to click. The bridge nobody should need turns out to be the exact shape of infrastructure that both CI systems and AI agents can operate: files, commits, commands, logs15.

Total damage: about four and a half hours across the recovery and all the hardening. Billed to Ali at the friend rate of exactly zero dollars16, which — given that the alternative universe version of this incident involves a panicked agency engagement, an emergency incident fee, and several days of downtime — might be the best ROI either of us has ever gotten from ten badly-justified hours in November.


The sanitized pipeline from the first post is still open source at wp-ali-public. The diagnostics-and-recovery patterns described here are the same trick pointed at a different problem: SSH in, --skip-plugins, do the smallest possible thing, verify from the server side, write it all down. May you never need any of it.

Footnotes

  1. Still not an alias. Still his actual name. The marketing is still working, which I know because his site now gets enough traffic that downtime is a genuine problem, which is what the rest of this post is about.

  2. Direct quote from footnote 4 of that post, which I am citing like scripture because — spoiler — the math is about to say something different.

  3. WordPress does have a "recovery mode" for this: a shutdown handler catches the fatal and emails the admin a magic login link. Credit where due — the alarm half worked, and that dying-gasp notification email is how anyone found out at all. But the magic link is supposed to open wp-admin with the broken plugin paused, and here every request was dying on the same compile error before WordPress could do anything clever. The fire extinguisher is stored inside the burning building. The smoke detector, to be fair, still went off.

  4. Queen Street Market is where Ali and I usually meet, and it's where the original ten-hours-for-one-page setup session happened last year. There's something right about a WordPress site whose entire deployment architecture was designed over cooked food in a Hong Kong market. The laptop went home afterward and so did the key, and neither had any reason to follow me to this machine — until now.

  5. This is the correct design for a secrets store and also extremely funny when you're locked out. The vault has a slot on top and no door. (The eventual fix was the oldest key-distribution protocol in computing: go get the other laptop and copy the file over by hand. But that's a next-day errand, and the site was down now.)

  6. There is something genuinely strange about debugging a live outage at one-commit-per-question latency. It's like playing chess by mail against a database. It also imposes a discipline that interactive SSH never does: every diagnostic step is written down, reviewable, and re-runnable, because every diagnostic step is code in the repo. I have had production incidents at real companies with worse paper trails than this one-page brochure site now has.

  7. Best reconstruction of how the site got there: PHP 8.1 reached end-of-life on December 31, 2025, and SiteGround's "Managed PHP" service — on by default — periodically migrates every site running the server default onto the new default version. Their mass migration to 8.2 even ran a compatibility pre-check first, which about 93% of sites passed and were upgraded directly. Ali's site evidently either passed the check (the pre-check can't execute every code path a plugin will hit at bootstrap) or rode a later default bump. And to be clear, hosts should do this — the alternative is a fleet of sites rotting on EOL PHP forever. PHP 8.2 isn't even new; it shipped in December 2022 and hits its own end-of-life this December. The plugin didn't change in years. The ground under it finally did.

  8. Ali, to be clear, wasn't even using it. It was installed at some point in the site's Drupal-refugee past, sat there dormant, and waited. This is the real lesson about the WordPress plugin model: every installed plugin is live code in your bootstrap path whether you use it or not. It's not an app on a shelf; it's a roommate.

  9. This is the part I'd want a past version of me to be able to Google, so, for the search engines: if your WordPress site shows "There has been a critical error on this website" and even WP-CLI crashes with E_COMPILE_ERROR, run wp plugin deactivate <the-broken-plugin> --skip-plugins. The flag exists precisely for this. It is mentioned approximately nowhere in the places you'll look first.

  10. A caching layer that cheerfully caches your fatal error page is technically doing its job. HTTP 200 or HTTP 500, everything deserves to be fast.

  11. For comparison: the November project — publish one page — took ten hours. This one — recover a fully down production site while the deploy key sat on a laptop across town — took an hour and a half, because of those ten hours. The programmer's disease footnote from last post can now be formally amended: the math on automating a 5-minute task is bad right up until the automation is the only door that still opens, at which point it is retroactively the best decision you made all year. This accounting method is unfalsifiable and I will not be taking questions.

  12. The audit mailer is deliberately the dumbest possible client of the smartest possible lesson from this incident: anything that needs to work during a fatal error must not depend on plugins loading. It's the fire extinguisher bolted to the outside of the building.

  13. Every aging WordPress site has this stratum — the plugin sediment of every marketing idea anyone ever had for it. Deleting them felt like clearing out a garage. Two of them I had to look up to find out what they even did. One of them, I remind you, had just taken the entire site down from inside the inactive list's waiting room.

  14. The repo now contains the publishing pipeline, the diagnostics, the fix workflows, the daily monitor, and the documentation for all of it. Ali's "website" is quietly becoming a git repository that happens to render through WordPress — which regular readers will recognize as the thesis of the last post arriving at its logical conclusion wearing a hard hat.

  15. Which suggests a general rule for 2026: build the automation-shaped interface before you need it, because the button-shaped interface has a habit of being part of the outage. The bridge gets built either way — you just get to choose whether you build it at 3 PM on a calm Tuesday or read about someone else building it in a post-mortem with footnotes.

  16. He's getting an invoice anyway. It says HKD 0.00 in large friendly numbers, itemized against the market rate, because the only thing better than fixing a friend's site for free is making sure he knows exactly what he's not paying for.

This post was written with the assistance of Claude (Anthropic). The author provided editorial direction, project context, and fact-checked all claims. The AI assisted with drafting and research.
Augustin Chan is CTO & Founder of Digital Rain Technologies, building production AI systems including 8-Bit Oracle. Previously Development Architect at Informatica and Senior Consultant at Dun & Bradstreet. BS Cognitive Science (Computation), UC San Diego.