Last week I wrote about the self-deleting PHP pattern I use to deploy changes to a 1,000-page site where I have no SSH, no CI/CD, and no shell - just a file manager. One comment stopped me cold:
"The self deleting PHP approach is a clever workaround, but how do you handle failures if the script gets interrupted halfway through?"
It's the right question. My script touches dozens of files in one run. If it dies at file 30, what state is the site in? Here's the answer: the design assumes the run WILL die at any point, and makes every partial state recoverable. Three mechanisms do the work.
1. Markers make re-runs safe
Every patch the script applies carries a marker - a unique comment token written next to the change. Before touching any file, the script greps for that marker:
- Marker present? Skip. Already patched, possibly by the interrupted run.
- Marker absent? Check the needle, then patch.
So re-running the script after an interruption isn't "doing it twice." It's finishing the job. Every file lands in exactly one of three states: not patched, patched, or skipped-because-patched. There is no "patched twice."
The same idea gates the whole run: the script refuses to execute unless it can find the exact code it expects. If a previous deploy moved something, you get a clean "needle not found" report instead of a corrupted file.
2. Backups happen before writes, per file
Before any file is modified, the script copies it to a backup with a run-specific suffix. One file, one backup. If a patch turns out wrong or a write is interrupted mid-file, the recovery is surgical: restore one file from one backup, and everything around it is untouched.
And the script verifies its own writes - after each modification it re-reads the file and confirms the marker is actually there. A failed verification stops the report loudly instead of printing a success message over a broken file. Every changed file shows up in an output report with sizes, so a run produces receipts you can eyeball, not a silent exit code.
3. The self-delete is the LAST step - and it's optional
This is the part that makes the name scary but the pattern safe: the script never deletes itself mid-flight. Deletion is a separate, explicit final action - I run it only after I've checked the live site and confirmed the change is real. If the script dies before that point, it's still sitting there, fully intact, waiting to be re-run.
That also means the delete isn't a fire-and-forget gamble. It's a deliberate "I verified the work, now clean up the tool" step. Interrupted runs leave the tool in place by definition.
The lifecycle, end to end
- Dry run (default): the script prints exactly what it WOULD change - files matched, bytes to append, needles found. Nothing is written.
- Apply: backups, writes, verify-after-write, output report.
- Live verification: I fetch the changed files with a cache-busting query string and check the served output, not the disk.
- Cleanup: only now does the script delete itself and remove the backups.
Every step is a separate request. Any step can fail, and the failure just means "do that step again" - not "restore from your nightmares."
Why not just... not do this?
Fair question. On a normal stack you'd git pull and the transaction problem mostly disappears. But when your deployment mechanism is a one-shot script running through a shared host's file manager, atomicity isn't something you're given - it's something you build. Markers, needles, per-file backups and delete-last are how you get close enough: not atomic, but recoverable from every direction.
Because "the deploy tool killed my site halfway through" isn't a deployment story. It's a resignation letter.
If you've built something similar on constrained hosting, I'd love to hear how you handled partial failures - especially if you've solved the problem I haven't: true atomicity without shell access.