Skip to content

Releases: 7Duckie/pontifex

v1.1.0 — Verify stops promising what a restore will not honour

Choose a tag to compare

@7Duckie 7Duckie released this 06 Aug 00:54
v1.1.0
a572f8e

A minor release. No breaking changes — no public API moved, but verify gains an outcome, so a script that checks its exit code can now see a failure it could not see before.

One theme in three parts: Pontifex saying clearly what it has decided.


Verifying a backup now answers for whether a restore would accept it

Verifying recalculated a fingerprint for every file and reported the backup sound. Restoring settled four further questions that verifying never asked — so a backup engineered to place a symbolic link outside your site, which by its nature has perfectly valid fingerprints, was reported sound and then refused by the very restore the check was meant to vouch for.

The person misled by this was the one checking their backups — exactly the habit a backup tool should reward.

There are now three answers, not two:

Verdict What it means What to do
Sound Intact, and a restore would accept it Nothing
Broken Damaged or unreadable Find another copy
Refused Not damaged — every fingerprint matched — but a restore will not accept it Keep it, do not restore it, find out where it came from

That third answer is deliberately its own verdict rather than a kind of "broken", because the two call for opposite actions. Broken sends you to delete the file. A refused backup is undamaged and unsafe: Pontifex never produces one, so its existence is information.

If your server has no room to restore, verifying says so — beside the verdict, never as the verdict. A full disk is not a damaged backup.

Also: wp pontifex import --dry-run is now a real rehearsal (it previously ran fewer checks than the import it claimed to rehearse), and the admin Restore screen refuses a backup it will not accept before making the safety copy rather than after — saving minutes of work and a second full copy of your site on disk.

A failed command explains itself instead of printing a crash

import, export and rollback each ended a failure by re-raising it into WordPress's fatal handler, so what you actually saw was a page of PHP internals followed by "There has been a critical error on this website."

All three now tell you what went wrong, which of three kinds of problem it is — the backup cannot be trusted, this server cannot comply, or the request was wrong — and what to do about it.

Messages no longer name Pontifex's internal parts

523 of them began with the name of the code that raised them — FileWriter: could not create directory — which means nothing to anybody who does not work on Pontifex. That detail now goes to the log instead, which records the exact file and line, so support diagnosis got better while the text you read got clearer.


Upgrading

Nothing to do. If you script wp pontifex verify and treat a non-zero exit as "the file is damaged", note that it can now also mean "intact but a restore will refuse it" — read the verdict rather than only the exit code.

Verification

Nine tag-run jobs green: quality gates and the real-MySQL integration suite across PHP 8.2, 8.3, 8.4 and 8.5, plus Plugin Check on the built package. 1796 unit tests / 70 integration tests. The ADR 0003 tag guard confirms the tag, the plugin header and PONTIFEX_VERSION all read 1.1.0.

Full detail in CHANGELOG.md and ADR 0023.

v1.0.3 — Exclusion patterns are kept as you typed them

Choose a tag to compare

@7Duckie 7Duckie released this 05 Aug 21:06
v1.0.3
c607993

Closes the last of the findings from the full-build audit that produced v1.0.1 and v1.0.2. No breaking changes.

If you use exclusion patterns, read this

The Backup screen was silently rewriting the patterns you typed into "Also leave out". They were passed through a WordPress text sanitiser that strips every percent sign followed by two hex digits — a sensible thing to do to a web address, and entirely the wrong thing to do to a file path, where those are just characters in a name.

wp-content/uploads/2024%2F*   →   wp-content/uploads/2024*

The second pattern excludes vastly more than the first. The files it swallowed were missing from every backup taken afterwards, and nothing anywhere said so. A scheduled backup stores the pattern, so every unattended run reused the rewritten version.

What to do: if any of your exclusion patterns contain a percent sign, open the Backup screen and check they still read the way you wrote them, then take a fresh backup. If none of your patterns contain a percent sign, you were never affected.

Also fixed

Pruning an offsite destination now says when a deletion failed. It reported how many old backups it removed and nothing at all about the ones it could not — so a destination quietly filling up, because the server was refusing deletions, looked exactly like a destination being kept tidy. Failures are now recorded in the log.

Internal

  • The WordPress installation root that a restore confines every write against — the value every path check downstream is measured from — existed as six identical copies. None had drifted, but nothing would have noticed if one had. It has one home now.
  • The release check additionally verifies the two readme headers that are hard errors in the plugin directory's own validator and were checked nowhere.
  • The coding-standards configuration now enables the input-sanitisation sniff that the directory's tooling enforces and the previous configuration omitted. A finding of that kind could previously only surface at a release gate, on work every local gate had already called clean — one did, on this release's own assembly.

Verification

Nine tag-run jobs green: quality gates and the real-MySQL integration suite across PHP 8.2, 8.3, 8.4 and 8.5, plus Plugin Check on the built package. 1759 unit tests / 66 integration tests. The ADR 0003 tag guard confirms the tag, the plugin header and PONTIFEX_VERSION all read 1.0.3.

Full detail in CHANGELOG.md.

v1.0.2 — the rest of what the audit found

Choose a tag to compare

@7Duckie 7Duckie released this 05 Aug 19:14
v1.0.2
511b0ee

Closes the remaining findings from the full-build audit that produced v1.0.1. Two of them could lose or damage something you care about, and one made the admin screens unusable on an entire platform. No breaking changes; every v1.0.x archive restores unchanged.


What changed

Bug fixes

  • Automatic pruning could delete the newest backup on an offsite destination. Pontifex decides which offsite copies to keep by sorting their names, which is oldest-first only for the names Pontifex generates itself. A backup you named yourself — wp pontifex export --output=/backups/before-upgrade.wpmig --destination=nas, say — joined that rotation, sorted ahead of every dated name, and was deleted as "the oldest": the backup taken minutes earlier, precisely so it would be there if the upgrade went wrong. If you prune an offsite destination and have ever uploaded a backup under your own filename, check what is still there. Pruning now only touches backups Pontifex named, and leaves everything else alone.
  • A restore stopped part-way through when the backup contained a read-only directory. Locking down a directory such as wp-content/uploads/private is a documented WordPress hardening step. Pontifex recreated that directory with its recorded permissions before writing the files inside it, so the next file could not be written and the restore stopped — at the one point where there is nothing to undo, leaving a site that was neither the old one nor the backup. Directory permissions are now applied once everything is in place.
  • The admin backup screen did not work on Windows. Downloading, deleting, verifying or restoring all reported that the backup could not be found — for backups listed on the screen in front of you — because the path check assumed a forward slash. Creating a backup still worked, so a Windows site produced backups it then refused to touch.
  • Several screens overstated what a backup contains. The Backup screen said a backup packs "every file and the whole database", then contradicted itself three lines later. A backup covers your content in wp-content, minus the exclusions shown, and the database tables belonging to this WordPress site — tables another application keeps in the same database are not included, which matters if your site shares a database with something else.

Changed

  • Pontifex can now say which kind of problem it hit. Until now every failure arrived as the same shape, so the admin screens could only offer one generic sentence whether your archive was corrupt or your disk was simply full. Refusals now carry that distinction — the archive cannot be trusted, this host cannot do it, or the request was wrong — which is what lets the screens tell you something useful. See ADR 0022.

Internal

  • The release check enforcing the WordPress.org submission limits now runs in CI rather than depending on somebody remembering it. The tag guard additionally requires the tagged commit to be on main. The package CI verifies is published as an artifact rather than discarded, so what users install can be what CI checked. Every workflow action is pinned to an exact commit, and every job has a timeout.
  • Guards that could previously be deleted with the whole suite still passing are now pinned by tests — signature enforcement, both entry-hash comparisons, and a completeness check comparing a real directory tree against the archive built from it.
  • Two facts that several files each kept their own copy of — the backup progress contract and the export counter schema — now have one home each.

Internal

  • Full changelog: see ## [1.0.2] in CHANGELOG.md.
  • Verified across PHP 8.2, 8.3, 8.4 and 8.5: quality gates, the real-MySQL integration suite, and Plugin Check on the built package. Tag guards: "Tag 1.0.2 matches both header (1.0.2) and constant (1.0.2). OK." and "Tagged commit is on main. OK."
  • Every fix was break-verified by reinstating the defect and confirming the matching tests fail for the right reason.

v1.0.1 — three defects a full-build audit found

Choose a tag to compare

@7Duckie 7Duckie released this 05 Aug 13:09
v1.0.1
95d9f91

A security and correctness release. An audit of the released v1.0.0 build — which had passed every quality gate this project has — found three defects, each proven by building a hostile archive or driving a real restore rather than by reading the code. If you restore archives you did not create yourself, or you migrate sites, upgrade before you next do either. No breaking changes; every v1.0.0 archive restores unchanged.


What changed

Security

  • An archive could disguise a symbolic link as an ordinary file and slip past the check that keeps links inside your site. An entry's kind is recorded in two places, and nothing compared them: the safety check deciding whether a link is allowed read one copy, the code that creates the link read the other. Changing two bytes of the archive's index — leaving the entry and its fingerprint untouched, so every integrity check still passed and verification still reported the archive sound — was enough to have a link created pointing anywhere the server could read, including wp-config.php. Restoring now refuses any archive whose index contradicts its own contents.

Bug fixes

  • A restore altered content that mentioned its own database table. While putting your database back, Pontifex renames the table it is working on, and that rename was applied to a whole chunk rather than only to the table name — so a post, a stored query, or any value containing a table name in backticks came back with pontifexstg_ written into it. Permanently: it survived the restore finishing. Where that text sat inside a stored settings array, the array stopped being readable and the plugin owning it silently reverted to its defaults. Nothing reported it — hashes matched, verification said sound — so it surfaced days later as settings that had vanished after a migration. Check any site you have migrated whose content or settings name a database table.
  • Verification skipped one of its checks unless it was given a memory budget, so it could report an archive sound that restoring would then refuse. That check now always runs: a verdict you use to decide whether to trust a backup should not depend on how the check was invoked.

Changed

  • Pontifex now refuses to load on WordPress multisite. It has no multisite handling: it reads a single table prefix, and a network has more than one, so it would have backed up part of a network while appearing to back up all of it — a backup that looks correct until the day you need it. It now stops with a notice explaining why. Multisite support remains a planned feature.

Internal

  • Full changelog: see ## [1.0.1] in CHANGELOG.md.
  • Verified across PHP 8.2, 8.3, 8.4 and 8.5: quality gates, the real-MySQL integration suite, and Plugin Check on the built package. Tag guard: "Tag 1.0.1 matches both header (1.0.1) and constant (1.0.1). OK."
  • Each fix is pinned by a test that fails when the fix is removed. The content-corruption fix was additionally proven against real MariaDB: a post naming its own table in backticks was exported, overwritten, restored, and returned byte-identical with a matching SHA-256.

v1.0.0 — Stable surface

Choose a tag to compare

@7Duckie 7Duckie released this 04 Aug 23:41
v1.0.0
1d6969e

The commitment release, and the first Pontifex version published as a full release rather than a pre-release. From here the public API is frozen and the .wpmig archive format specification is locked at specification version 1.1. There are no breaking changes — every v0.9.x archive restores unchanged.


What this release commits to

A v1.1 archive will remain readable by every future version of Pontifex. A change the specification cannot accommodate needs a new major specification version, never a silent revision of v1. That is the point of publishing the format at all: it is what makes "a backup is never hostage to the plugin" enforceable rather than aspirational. The specification is at docs/archive-format.md, published under CC BY 4.0 so anyone can implement a reader from it.

This is also the version submitted to the WordPress.org plugin directory.


What changed

Bug fixes

  • A restore is refused on a host that could not finish it. A restore that reached a symbolic link on a host where PHP's symlink() is unavailable — common on shared hosting, where it is often listed in disable_functions — overwrote every file ahead of that point and then stopped where the link should have gone, leaving a site that was neither the old one nor the archive's. Every link the archive declares is now resolved to the directory it would be created in and tested for real before the first byte is written, so the restore is refused with nothing changed. A backup containing no symbolic links is never tested and never affected.
  • export --passphrase never existed. The readme told you to encrypt a backup with that flag. There is no such flag, so following our own documentation produced an unencrypted backup with no error. The real flags are --encrypt and --passphrase-stdin. If you ever used --passphrase believing it encrypted a backup, re-take that backup with --encrypt.
  • Two further untrue statements in the readme. It said Pontifex never contacts any remote service, which stopped being true when offsite SFTP destinations shipped in v0.8.0 — Pontifex still runs no service and contacts nothing on its own initiative, but a destination you configure yourself is a real exception and is now described as one. And it pointed you at the Overview screen to check your environment, which no admin screen does; that check is wp pontifex doctor.

Features

  • wp pontifex doctor reports symbolic-link support. Whether this host can create symbolic links (needed to restore an archive containing them) and read them (needed to back up a site that has them) is now in the readout, so you learn it before you need it rather than during a restore. Reported as a warning, never a failure — a site with no symbolic links backs up and restores perfectly well without either.

Internal

  • The release check no longer advises publishing every release as a pre-release "since it is below v1.0.0" — a hardcoded reminder written when that was true of every version there had ever been, which on this assembly would have mislabelled the first stable release.
  • Development and CI dependencies updated: actions/setup-node 6 to 7, @wordpress/env 11.11.0 to 11.12.0, wp-phpunit/wp-phpunit 7.0.1 to 7.0.2. None ships in the plugin.

Internal

  • Full changelog: see ## [1.0.0] in CHANGELOG.md.
  • Verified across PHP 8.2, 8.3, 8.4 and 8.5: quality gates, the real-MySQL integration suite, and Plugin Check on the built package. Tag guard: "Tag 1.0.0 matches both header (1.0.0) and constant (1.0.0). OK."

v0.9.5 — An archive can no longer reach outside the site it restores into

Choose a tag to compare

@7Duckie 7Duckie released this 29 Jul 22:52
v0.9.5
c5401ca

A security release. Where v0.9.5's predecessor stopped an archive's SQL escaping its staging table, this one stops its files escaping the site.

If you ever restore or import an archive you did not create yourself — a backup from another site, a file someone sent you, a migration between servers — upgrade before you do it again.

Security

A restored symlink is confined to the site it restores into

A content-only restore — the default, and the only mode the admin screens offer — would create wp-content/uploads/leak.txt pointing at wp-config.php. Uploads are served as static files and web servers follow symlinks, so an unauthenticated request returned the database password and the authentication salts.

Measured against the real writer beforehand: eight of ten hostile shapes were written, and five returned the planted secret when read through. All ten are refused now.

Resolving a target as a string does not work, because the archive supplies the intermediate link — hop -> ".." followed by leak.txt -> "hop/../wp-config.php" collapses on paper to somewhere harmless while the kernel lands on the real file. Every link the archive declares is now resolved the way the kernel resolves it, across the whole archive, before the first byte is written.

The containment boundary is the site root rather than wp-content, deliberately: the tighter rule would refuse Composer-managed layouts that legitimately point at a vendor tree beside wp-content, making those sites' own backups unrestorable.

A forged archive cannot write into Pontifex's own directory

That namespace holds the safety archives a restore creates as its undo, the .htaccess keeping whole-database backups out of web reach, and the files the admin backup list is built from.

⚠️ Breaking

A site that pins PONTIFEX_PUBLIC_KEY must now upload backups signed with that key. Unsigned or differently-signed uploads are refused before the file is stored. This is the browser counterpart of the command-line enforcement shipped in v0.5.0, and breaking in the same way: a stripped signature is indistinguishable from one that was never there, so a warning would be no defence.

A site with no key configured is unaffected, byte for byte.

Enforcement sits at upload because nowhere later can know where an archive came from — once stored, an uploaded and a locally-produced archive are indistinguishable, and the provenance inside an archive is written by whoever made it. Recorded in ADR 0020.

Fixed

  • A backup too large for this installation to read back is refused before it is written, rather than completing and then being rejected. The entry ceiling rises from 50,000 to 100,000, and the manifest size projection is measured rather than assumed.
  • Opening a large backup no longer dies without explanation. Exhausting memory is an uncatchable fatal, so a restore that died this way never restored its safety archive, never released its lock and never cleaned up its job. Memory is now raised through WordPress where the host allows it, and refused with the megabytes named where it does not.
  • A restore that would run out of disk is stopped before it changes anything, sized by how much the incoming files are larger than what they replace — so a rollback on a nearly-full disk is not refused.
  • A force-killed backup can be resumed again. The lock guarding the dead job had been refusing the only command able to finish it, while the error advised "resume it, then retry".
  • A restore is stopped when no readable safety archive can be written. The undo was previously exempted from the size refusal; that produced an archive which opens at no memory limit at all, so the restore proceeded believing an undo existed.
  • The scheduler no longer records failures that never happened for signed backups it cannot continue.
  • verify --list carries a memory budget, so listing a large backup's contents cannot die silently.

Documentation

PRIVACY.md claimed no outbound network requests — untrue since offsite destinations shipped in v0.8.0 — and said all work happens "on the machine running WP-CLI", written before the admin screens existed. A test now fails the build on any network call outside the destination component, so the promise is enforced rather than asserted.

The format specification stated a reader obligation in three places the browser met in none; it now records that a reader may scope a trusted key's reach to archives admitted from outside its trust boundary.

Verification

1,596 tests before this work, 1,708 now. Five adversarial audit rounds, each of which overturned something that had already passed every automated gate — including a guard that was bypassable, a memory check that did not fire in the band where the fatal actually happens, and a safety archive written in a form no memory limit could open.

The full release gate — the real-database suite across PHP 8.2–8.5 plus Plugin Check on the built package — is green, and the upload behaviour was verified by hand in a browser across all four cases: no key, unreadable key, unsigned archive, and correctly signed archive.

Requirements

WordPress 6.5+ · PHP 8.2–8.5

Pre-release, as every version below v1.0.0 is.

v0.9.4 — restore refuses an archive's stray SQL

Choose a tag to compare

@7Duckie 7Duckie released this 28 Jul 21:58
v0.9.4
df8b8e0

A security release. If you ever restore or import an archive you did not create yourself — a backup from another site, a file someone sent you, a migration between servers — upgrade before you do it again. This is a pre-release, as every release below v1.0.0 is.


What changed

Security

  • A database chunk's SQL is confined to its own staged table. Restoring an archive replayed the SQL inside it against the live database almost verbatim: only the single table identifier a chunk declared was rewritten to its staging name, and every other statement ran as written — before the atomic cut-over, and outside anything the abort path could undo, because that drops staging tables and the damage was not in one. An archive carrying one extra statement could set a user's password and take over the destination site's administrator account. It needed no unusual database privilege, so it applied to any installation.

    Statements must now begin, byte for byte, with one of a small set of shapes composed from the staging identifier the restore built itself — a DROP of that table, a CREATE TABLE of that table, or an INSERT into it. Nothing may precede those bytes, so there is no comment to strip and no case to fold, and therefore none of that to get wrong: reading a statement's leading keyword cannot be made safe, because ordinary comments, comments that execute, unusual whitespace and a keyword split across a comment all defeat it.

    Once a table has been created, and before anything else in that chunk runs, the database is asked what it actually built — the object's type, its storage engine, its create options, the storage directories of any partitions, and its row count. A statement can look entirely ordinary and still ask for a table that is an alias onto a live one, or whose files are written to a path of the archive's choosing, or that is populated from a table it should never read. None of that is visible in a statement's opening bytes, and the server knows.

    A semicolon outside quoted text is refused, so an acceptable-looking statement cannot carry a second one behind it. This no longer depends on the database driver rejecting multiple statements, which was a behaviour the code neither stated nor controlled.

  • What this does not do. Two ways for an archive to read data it should not remain, both inside an INSERT's values: a scalar subquery, and a file read on the database server. Neither can be refused by a statement's shape without a list of forbidden functions, and those lists get worked around. What they reach lands in the restored site rather than travelling back to whoever supplied the archive. An archive from a source you do not trust still should not be restored — this reduces the consequences, it does not remove them. Both are recorded in ADR 0019 and the threat model.

Added

  • Files whose media type could not be determined are counted and reported. Determining a file's type can fail several ways — a missing extension on the host, an unreadable source, a broken or absent magic database — and every one produced exactly what a successful check produces for a file that genuinely has no identifiable type. The two were indistinguishable, so a host-wide failure would have recorded every file in every archive as raw bytes with nothing anywhere saying so. An export now reports the count, on every path that can finish one including the scheduled and admin backups that run unattended. A genuinely unidentifiable file is not counted as a failure, so an ordinary run reports nothing.

Fixed

  • Backups started from the admin screen no longer erase the changed-file tally that command-line exports had accumulated in the stored statistics.

Internal

  • The development coding-standards package was moved to the release patching an arbitrary code execution advisory published during this cycle. It ships in no plugin, but it runs in continuous integration, in the pre-commit hook and on the machine that signs releases.

Internal

  • ADR 0019 added: a db_chunk's statements are confined by anchored shapes plus server-reported facts about what a CREATE built, never by parsing SQL text. Allow-list throughout; the whole payload is validated before any statement executes.
  • Full changelog: see [0.9.4] in CHANGELOG.md.

v0.9.3 — operational hardening

Pre-release

Choose a tag to compare

@7Duckie 7Duckie released this 28 Jul 09:58
v0.9.3
4797a6c

Four patches of operational hardening on top of v0.9.0, released together under one tag, with no breaking changes. This is a pre-release, as every release below v1.0.0 is.

If you run a site deployed from git, or exclude any directory that happens to be empty, re-take your backups after upgrading. One of the fixes below is a silent data-loss defect: some backups taken before this release may be missing files, and would still have passed verification.


What changed

Fixed

  • The manifest scanner silently dropped the entry after an excluded directory. The walk was advanced twice for every excluded directory, so the next entry was consumed without ever being tested against the exclusion rules. Where that directory was empty — as several inside every git checkout are — the lost entry was a real site file, missing from an archive that still verified as sound, because the archive was internally consistent and merely incomplete. Excluded directories are now pruned inside the recursive walk, so one is never opened at all.
  • Backup, restore and rollback now share one lock, across both the admin and the command line. They previously took different locks and so did not exclude one another, which allowed a restore to replace the database while a backup was reading it; the command line took no lock at all. A CLI export, import or rollback now refuses and exits non-zero, naming the operation already running. Verify keeps its own lock, since it only reads an archive.
  • The backup lists tell the truth about where an archive came from. An uploaded backup showed its upload time and no origin, even when taken on another site entirely. Each backup now leads with its true source and real creation date, read from the archive's own recorded provenance. Downloads offer a friendly source-and-date filename; the stored file keeps its exact name.
  • A backup no longer appears to hang as it finishes. It ended by sitting at the complete byte count with zero seconds remaining while the database snapshot and the archive's manifest and footer were still being written. The finishing phase is now reported, and a phase can no longer travel backwards, which is what stopped the bar collapsing to zero at the database boundary.

Added

  • .git is left out of backups by default, at any depth. Version-control history is not site content: it is regenerable from the same remote the working copy came from, it can silently rewind a live git-deployed directory when restored over it, and it makes an archive carry every secret ever committed wherever that archive travels. .github, .gitignore, .gitattributes and .gitkeep are ordinary content and stay in. vendor and node_modules are deliberately not excluded, because a restored site needs both. Use --no-defaults to opt out of the curated defaults entirely.

Performance

  • Backups spend far less time scanning. Media types are now determined when an entry is written rather than when it is scanned, so a resumable export reads each file once across the whole backup instead of once per request, and its final pass — which writes nothing — reads nothing. On a tree of roughly 18,600 entries a scan drops from 7.41 seconds to 0.795.

Changed

  • An exclusion pattern naming a directory without also matching its children now leaves out that whole subtree, rather than omitting the directory while keeping its contents. The built-in defaults are unaffected.
  • An unreadable file or directory inside an excluded subtree no longer aborts an export, because nothing inside an excluded subtree is opened.
  • A resumable export that was in progress before this upgrade will refuse to resume and must be started again, because the repaired scan correctly finds entries the old one skipped.

Internal

  • ADR 0008 amended: .git added to the curated default exclusions, superseding the original rejection on new information, with vendor and node_modules explicitly kept.
  • Full changelog: see [0.9.3] in CHANGELOG.md.

v0.9.0 — admin legibility

Pre-release

Choose a tag to compare

@7Duckie 7Duckie released this 14 Jul 19:46
v0.9.0
9ad3a17

A pre-release (below v1.0.0). v0.9.0 — "admin legibility" — makes the engine's trustworthiness and each archive's contents visible from the admin, without removing a single safety confirmation. No engine or format change, and no breaking changes.


What changed

Features

  • A "verified — here's the proof" panel. A sound verify now shows a persistent, plain-language panel — the verdict, entry count and size, what the archive contains, when it was created, and the format version — with the assurance that every file and database chunk was re-read and its SHA-256 re-checked, and a link to the published .wpmig format specification. Typographic throughout; the words carry the verdict, never a status colour.
  • What each archive contains, in every list. The Backup, Restore, and Verify lists gain a "Contains" column — content and database, database only, files only, or whole site — read fail-soft from each archive's provenance, so a content backup is told apart from a database-only one without opening it.
  • The Verify screen re-attaches after a reload. A verification already running is picked back up on reload, showing live progress and an elapsed timer from the run's own start; a dead verifier's lock is reclaimed so it never blocks the next check.
  • A migration hint on the Restore screen. Selecting a backup recorded as taken on another site shows a quiet advisory line that the "rewrite its links" checkbox applies to it, from a read-only preview of the archive's provenance. It never ticks the box and stays silent for encrypted archives.

Bug fixes

  • The Backup screen no longer re-attaches to a finished backup. A backup killed by a fatal (the classic web execution-time limit) left the screen showing "a backup is running" until a transient's TTL expired. The shutdown handler now clears that transient, the progress endpoint reports idle for a stale transient with no active job, and the single-runner lock is reclaimable — so a dead run never blocks the next backup.
  • A restore that signs you out reports honestly instead of freezing. A restore replaces the users table and signs the operator out mid-run; the progress bar used to freeze while WordPress core's own session modal appeared over the top. The screen now recognises the reset, shows an honest in-page message (the restore reached its final stage and reset the session; the outcome is on the Overview and in the log), and reloads to that outcome once the operator logs back in through core's native session modal.
  • The backup list agrees with what can be restored. A stray or malformed file in the backups directory no longer appears as a phantom row that cannot be actioned.

Internal

  • No new ADRs.
  • Full changelog: see ## [0.9.0] in CHANGELOG.md.

v0.8.0 — offsite SFTP destinations

Pre-release

Choose a tag to compare

@7Duckie 7Duckie released this 14 Jul 09:51
v0.8.0
dc3ccaa

A feature release with no breaking changes. Below v1.0.0, so it is published as a pre-release. Offsite SFTP destinations: a finished backup can be uploaded to an SFTP server the user already owns — never a Pontifex service, never a phone-home.


What changed

Features

  • Offsite SFTP destinationswp pontifex export --destination=<name> uploads the finished .wpmig to a configured SFTP server after writing it locally, in the CLI process (no web-request timeout). The host key is pinned before any credential is sent — an unknown or mismatched key is refused unless --insecure-host-key is explicitly set — and credentials are referenced by environment-variable name, never a flag or plaintext.
  • wp pontifex destinationadd, remove, list, test (a live reachability, authentication, and writability check), archives, pull (fetch an archive back for recovery after a local loss, so a destination is never write-only), and prune.
  • Per-destination retention--retention=<count> keeps the newest N archives and prunes the rest, after each upload and on demand via destination prune, ordered by archive name, with a floor that can never prune a destination down to nothing.
  • A network-free destination health check in wp pontifex doctor — reports each destination's configuration and credential presence without connecting; live reachability stays the on-demand destination test.

Security

  • Plugin Check runs clean, and stays that way — four ValidatedSanitizedInput warnings on the admin schedule-save handler were fixed at source, and CI now fails the build on warnings, not only errors.

Privacy

  • An offsite upload is an outbound SFTP connection to the user's own server, on the user's own command, with the user's own credentials — Pontifex runs no server in the path and holds none of the data (ADR 0017).

Internal

  • Adds phpseclib/phpseclib (pure PHP, no native extension) for the SFTP transport. A planned S3 adapter was deferred (its pure-PHP library needs native extensions, a GPL-3.0 licence, and a cloud-SDK surface — ADR 0017 revision). Scheduled/cron and admin offsite are deferred; offsite is CLI-only this release.

Internal

  • ADRs: 0017 — offsite destination adapters (revised to SFTP-only for v0.8.0).
  • Full changelog: see = 0.8.0 = in readme.txt and the [0.8.0] section of CHANGELOG.md.
  • Suite: 1351 unit / 42 integration.