Skip to content

Push model: upload local site to remote WordPress#97

Open
adamziel wants to merge 7 commits into
trunkfrom
adamziel/reverse-push
Open

Push model: upload local site to remote WordPress#97
adamziel wants to merge 7 commits into
trunkfrom
adamziel/reverse-push

Conversation

@adamziel

@adamziel adamziel commented Apr 7, 2026

Copy link
Copy Markdown
Collaborator

Summary

This adds the reverse of the current pull model. Instead of downloading from a remote site, the local machine uploads its database and files to a remote WordPress site with atomic cutover semantics.

The push uses the same multipart protocol, producers, cursors, and HMAC auth as the pull model, just with roles swapped. The receiver is the authority on progress — the client tracks "how much the server consumed," not "how much I sent."

Database tables land with a staging prefix (_push_wp_*), then a single RENAME TABLE statement atomically swaps all tables. Files land in a staging directory, then swap via symlink or directory rename. A unified commit endpoint orchestrates both swaps — files first (rollbackable), then DB (atomic) — with rollback on partial failure.

Nothing goes live until the explicit commit step. Push-sql and push-files can run in any order, be interrupted, and resume from the receiver's last cursor.

What's new

MultipartBodyStream — streaming multipart/mixed resource backed by php://temp for bounded-memory curl uploads with HMAC signing.

ChunkWriter — shared filesystem write operations (file chunks, directories, symlinks with security validation) used by the file_receive endpoint.

Receiver endpointssql_receive (rewrites table names to staging prefix, executes SQL), file_receive (writes to staging directory), commit (validates staging state, swaps files then DB, rolls back on failure).

CLI commandspush-sql, push-files, push-commit, all resumable with state management matching the pull model's reentrancy design.

Test plan

  • PHP syntax checks pass on all 7 files
  • Unit test: MultipartBodyStream round-trip with MultipartStreamParser
  • Unit test: table name rewriting across all SQL statement types
  • Unit test: RENAME TABLE builder handles new, dropped, and existing tables
  • Integration test: push SQL to local receiver, commit, verify tables swapped
  • Integration test: push files to staging dir, commit, verify directory swapped
  • Integration test: commit failure leaves live site untouched
  • E2E: Docker scenario pushing full WordPress site between containers

adamziel added 7 commits April 7, 2026 19:31
…over

The pull model downloads from remote to local. This adds the reverse: push local
database and files to a remote WordPress site with blue-green deployment semantics.

Database tables are pushed with a staging prefix (_push_wp_*), then atomically
swapped via a single RENAME TABLE statement. Files are pushed to a staging directory,
then swapped via symlink or directory rename. A unified commit endpoint orchestrates
both swaps with rollback on partial failure.

New classes: MultipartBodyStream (streaming multipart resource backed by php://temp
for bounded-memory uploads) and ChunkWriter (shared filesystem write operations for
the receiver). New CLI commands: push-sql, push-files, push-commit — all resumable
via receiver-authoritative cursors, matching the pull model's reentrancy design.
Initialize $existing_tables before the conditional block so PHPStan can
see it's always defined at usage. Fix FileTreeProducer namespace (it's in
global namespace, not WordPress\DataLiberation). Remove redundant null
coalescing on $staging_id. Baseline the is_finished() false-positive
warnings that stem from the same MySQLDumpProducer state machine analysis
already baselined for "unreachable statement."
… ChunkWriter security

Five test files covering the critical production-safety gaps in the push
implementation:

- TableNameRewritingTest: verifies SQL rewriting handles substring table names,
  FK references, multi-statement blocks, and doesn't corrupt string VALUES data

- CommitEndpointTest: tests RENAME TABLE generation, documents the silent table
  deletion bug (pushing a subset of tables permanently deletes remote-only tables
  like WooCommerce orders), validates missing staging table detection, tests file
  swap and rollback mechanics, and wp-config.php credential preservation

- MultipartBodyStreamTest: round-trip tests for all chunk types (SQL, file,
  directory, symlink, completion) through write → parse_multipart_body, binary
  data integrity, finalization idempotency, HMAC consistency

- ChunkWriterTest: filesystem write correctness, multi-chunk streaming, path
  traversal rejection, NUL byte injection, symlink escape prevention, symlink
  in directory path replacement, binary data integrity, interrupted write handling

- SqlReceiveIntegrationTest: full pipeline from MySQLDumpProducer export through
  multipart streaming, table name rewriting, staging table creation, RENAME TABLE
  commit, and data integrity verification including special characters and the
  empty-database-wipes-target scenario

Tests use a standalone push-test-bootstrap.php to avoid the utils.php double-load
fatal caused by Composer vendor copy vs. local package path collision.
assert_valid_path() throws InvalidArgumentException, not RuntimeException.
PDO::FETCH_COLUMN returns int on MySQL 8+, use COUNT(*) for simpler assertions.
… FK breakage

Five new integration tests that simulate what happens when the production site
has data the source doesn't know about:

- testPushDestroysProductionRowsNotInSource: production grew from 3 to 6 posts
  while dev was working locally. Push replaces the table entirely, posts 4-6 are
  gone. Documents that this is wholesale replacement, not a merge.

- testConflictingAutoIncrementIdsAreOverwrittenSilently: both dev and production
  independently created a post with ID=4 but different content. Push silently
  replaces the CEO's announcement with a dev draft. No conflict detection.

- testAutoIncrementCounterResetAfterPush: production's counter was at 11, source's
  at 3. After push, new inserts get ID=3, reusing an ID that existed in the old
  production data. Any cached references to old ID=3 now point to wrong content.

- testForeignKeyIntegrityAcrossPushedTables: when both wp_posts and wp_postmeta
  are pushed together, FK references remain internally consistent (positive test).

- testPartialTablePushBreaksForeignKeyIntegrity: pushing only wp_posts without
  wp_postmeta causes wp_postmeta to be silently deleted by the "dropped table"
  logic, since it wasn't in the pushed set. Documents the cascade from the silent
  table deletion bug.
…HEMA

INFORMATION_SCHEMA.TABLES.AUTO_INCREMENT can return stale values after
RENAME TABLE due to MySQL's internal caching. The observable behavior
(what ID does the next INSERT get) is what matters, so test that directly.
…rge file truncation

push-files crashed on every invocation because FileTreeProducer requires a
`paths` option that was never provided. push-commit failed on repeat runs
when a prior commit's best-effort cleanup was interrupted, leaving _old_*
tables that block MySQL's RENAME TABLE. And files larger than the per-request
body budget were silently truncated because ChunkWriter had no resume recovery
for continuation chunks arriving in a fresh instance.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant