pull: fix the flattened runtime root and delta re-pull after deferred files - #268
Conversation
Studio is moving from driving reprint as separate sub-commands (files-sync, db-sync, db-apply, flat-docroot, apply-runtime) to a single `reprint pull`. Two gaps surfaced: - apply-runtime targeted the raw download tree, not the flattened site, when a pull used --flatten-to. `pull` forwards `flatten_to` to flat-docroot, but `run_apply_runtime` reads `flat_document_root`. Derive the latter from the former in `validate_and_default_options` so a single `pull --flatten-to=X --runtime=...` roots the runtime at the flattened layout. - A delta re-pull tripped the mid-flight --filter guard. After a pull, its deferred "skipped-earlier" tail leaves filter=skipped-earlier / status=in_progress; the next `pull --filter=essential-files` hit the guard in ImportClient::run() before Pull::run() could prepare_repull() and clear that stale state. A completed pull (pull.stage===complete) about to re-pull now bypasses the guard. Adds unit tests for both in PullFilterOptionTest.
Replace the per-sub-command orchestration (downloadEssentialSiteFiles, refreshFlattenedSiteDirectory, downloadRemoteDatabase, applyDownloadedDatabase, generateRuntimeConfiguration) and the clearCompletedSubcommandState/--abort delta-reset with one `reprint pull` call. reprint owns the pipeline ordering (files-pull -> db-pull -> db-apply -> flat-docroot -> apply-runtime) and resets its own state for a delta re-pull via prepare_repull(), so the Studio-side --abort wiring and per-phase stage gating go away. - runFullPull() issues the single pull with the same sqlite geometry the old db-apply used (target sqlite under the raw content dir) plus --flatten-to, --runtime=playground-cli, --start-runtime=none and --output-dir, mounting the site + runtime dirs up front. - ensurePort moves before the pull so --new-site-url is available. - Collapse the stage machine from 9 stages to 5 (initialized -> pulled -> site-registered -> site-started -> completed). - Bump the PHP-WASM memory_limit to 1024M: the single long-lived fork holds the file-index high-water-mark across phases. Requires reprint's flatten_to->flat_document_root bridge and the re-pull filter-guard fix (WordPress/reprint#268).
Pull pipeline performance —
|
| Stage | PR | trunk | Δ | Status | Details |
|---|---|---|---|---|---|
playground-sqlite-db-pull |
8.74 s | 8.66 s | ⚪ +83 ms (+1.0%) | ✓ | condition=db-pull in PHP.wasm runtime=php.wasm 8.3 wp_mysql_parser=enabled mode=lexer native_lexer=verified native_token_stream=WP_MySQL_Native_Token_Stream native_token_count=18 native_parser=selected trunk: condition=db-pull in PHP.wasm runtime=php.wasm 8.3 wp_mysql_parser=enabled mode=lexer native_lexer=verified native_token_stream=WP_MySQL_Native_Token_Stream native_token_count=18 native_parser=selected |
playground-sqlite-db-apply |
3.48 s | 3.47 s | ⚪ +9 ms (+0.3%) | ✓ | condition=db-apply to SQLite in PHP.wasm runtime=php.wasm 8.3 wp_mysql_parser=enabled mode=parser native_lexer=verified native_token_stream=WP_MySQL_Native_Token_Stream native_token_count=18 native_parser=verified native_ast=WP_MySQL_Native_Parser_Node sqlite_driver_parser=verified trunk: condition=db-apply to SQLite in PHP.wasm runtime=php.wasm 8.3 wp_mysql_parser=enabled mode=parser native_lexer=verified native_token_stream=WP_MySQL_Native_Token_Stream native_token_count=18 native_parser=verified native_ast=WP_MySQL_Native_Parser_Node sqlite_driver_parser=verified |
| Total | 12.22 s | 12.13 s | ⚪ +92 ms (+0.8%) |
Numbers carry runner noise; treat single-run deltas as directional, not authoritative.
📈 Trunk performance history — commit-by-commit timeline.
There was a problem hiding this comment.
Pull request overview
This PR fixes two bugs in the composite pull command so that the full pipeline (preflight → files-pull → db-pull → db-apply → flat-docroot → apply-runtime) works correctly both on first run with flatten + runtime options and on delta re-pulls after deferred files have been fetched.
Changes:
- Derives
flat_document_rootfromflatten_toinvalidate_and_default_options()so thatpull --flatten-to=DIR --runtime=...generates a runtime rooted at the flattened layout rather than the raw download tree. - Exempts completed pull re-pulls (
pull.stage === "complete") from the mid-flight filter guard inImportClient::run(), allowingprepare_repull()to clear the stale sub-command state before the guard would otherwise block the filter change. - Adds two focused unit tests covering both fixes.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
packages/reprint-importer/src/lib/pull/class-pull.php |
Bridges flatten_to → flat_document_root in option validation so apply-runtime targets the flattened directory |
packages/reprint-importer/src/import.php |
Adds $is_repull check to the filter guard to exempt completed pulls about to delta re-pull |
tests/Import/PullFilterOptionTest.php |
Adds PullBridgeFakeClient test helper and two new tests for the flatten bridge and the re-pull guard bypass |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
## What it does Marks a successful `files-pull --filter=skipped-earlier` tail as `status=complete` before returning. This lets the existing mid-flight `--filter` guard keep its original shape: later `pull --filter=essential-files` runs see the previous files sync as complete instead of bypassing the guard for completed pull pipelines. ## Rationale PR #268 correctly identifies that a delta re-pull can be blocked after deferred files are fetched. The root cause is the deferred `skipped-earlier` tail reopening a completed `files-pull` with `status=in_progress` and then returning without restoring the completed status after a successful fetch. That leaves stale lifecycle state behind. Fixing the state transition keeps one source of truth: completed work is recorded as complete, and the generic filter guard does not need a pull-specific exception. ## Implementation After `run_files_sync_pipeline()` handles the `fetch-skipped` stage, `run_files_sync()` now: 1. returns unchanged when the pipeline saved `status=partial` for an interrupted fetch; 2. otherwise restores `status=complete` and saves state before returning. The pull-level test now models the truthful post-tail state, and a new files-sync test drives the `skipped-earlier` tail through a no-network `file_fetch` test double. ## Testing instructions ```bash php -l packages/reprint-importer/src/import.php php -l tests/Import/FilesSyncStateTest.php php -l tests/Import/PullFilterOptionTest.php git diff --check cd tests && ../vendor/bin/phpunit Import/PullFilterOptionTest.php Import/FilesSyncStateTest.php ```
## Related issues - Required: WordPress/reprint#268 (pull's `flatten_to` -> `flat_document_root` bridge plus the re-pull filter-guard fix that this flow depends on). - Recommended: WordPress/reprint#264 (skipped-list ENOENT fix) in the bundled reprint. ## How AI was used in this PR Claude Code measured the single-fork memory profile against a live WP.com Atomic site (26k files / ~570 MB), implemented the migration and tests, and verified the full flow end-to-end (fresh pull plus delta re-pull: remote edit -> re-pull -> local content / auto-login checks). Each change was reviewed and re-tested live. ## Proposed Changes Re-running `studio pull-reprint` on a completed pull previously printed "pulled successfully" and exited without contacting the remote, so remote edits, inserts, and deletions never reached the local site. With this PR a re-run performs a **delta re-pull**: files re-sync incrementally and the database is refreshed so existing-row edits and deletions propagate. Studio now drives reprint with a **single `reprint pull`** instead of orchestrating the pipeline itself. One call runs preflight -> files-pull -> db-pull -> db-apply -> flat-docroot -> apply-runtime in one PHP-WASM fork, and reprint resets its own state for a delta re-pull via `prepare_repull()`. This deletes the former Studio orchestration: the five per-sub-command functions, the nine-stage machine, and the `--abort` delta-reset wiring (about 340 lines net). The single fork's `memory_limit` is raised to 1024M because the file-index high-water-mark now persists across phases (measured peak ~510M on the test site). Fixes that remain part of making re-pulls real: - **Auto-login keeps working after a re-pull.** The database refresh rebuilds the DB from the remote dump, wiping the local `admin` user and `studio_admin_username` option that `/studio-auto-login` depends on. The pull re-applies the stored credentials over the running site's admin API, and a connection failure on that request doubles as a health check that catches a stale daemon "online" state and falls back to starting the server. - **Database URLs can't drift to a dead port.** If the Studio site record was re-created with a different id/port, the re-pull adopts the live site record before the pull so the URL rewrite (`--new-site-url`) targets the right port. - **Site starts no longer crash on stale runtime mounts.** Transient reprint state files mounted for the remote-uploads proxy are dropped when loading the persisted `start-options.json` (previously ENOENT crashed the start). Bundle the reprint build via `REPRINT_VERSION` in `scripts/download-wp-server-files.ts`; it must include WordPress/reprint#268. ## Testing Instructions 1. Build the reprint phar from a branch that includes WordPress/reprint#268 (and #264): run `composer build:phar` on the reprint repo. 2. On this repo, place `reprint.phar` at `wp-files/reprint/reprint.phar` and run `npm run cli:build`. 3. Pull a site: `node apps/cli/dist/cli/main.mjs pull-reprint --verbose --url <site>` and let it complete. It should invoke a single `reprint pull` (visible with `--verbose`) and start a working site. 4. Edit an existing post on the remote site, then re-run the same command. It should report "Updating ... (delta sync)" and the local post should show the edit. 5. Open the local site's WP Admin via the printed auto-login URL and confirm the edit from step 4. 6. Stop and start the site (`studio site stop` / `site start`). No ENOENT mount errors. ## Pre-merge Checklist - [x] Have you checked for TypeScript, React or other console errors?
What it does
Two fixes to the composite
pullcommand so it correctly runs, and re-runs, the full preflight -> files-pull -> db-pull -> db-apply -> flat-docroot -> apply-runtime pipeline from a single invocation:pull --flatten-to=DIR --runtime=...now generates a runtime rooted at the flattened layout (DIR) instead of the raw download tree. The two options now compose:--flatten-tobuilds the flattened docroot and--runtimetargets it.skipped-earlierfiles have been fetched, the files sub-command records a truthfulstatus=complete, so the nextpull --filter=essential-filesproceeds as a delta re-pull without weakening the mid-flight--filterguard.Rationale
--flatten-todid not reach apply-runtime.pullforwards--flatten-toto the flat-docroot stage asflatten_to, butrun_apply_runtime()reads a different key,flat_document_root, and otherwise falls back tofs_root + remote document_root(the raw tree). So a pull that both flattens and generates a runtime produced a runtime pointing at the wrong root: the flatten and runtime stages did not compose.Delta re-pull was blocked after a deferred-files tail.
pullsupports delta re-pull: re-running a completed pull resets its own state viaprepare_repull()and re-syncs. But after an--filter=essential-filespull fetched its deferredskipped-earlierfiles, the state still readfilter=skipped-earlier,status=in_progress. The nextpull --filter=essential-filesloaded that stale state inImportClient::run()and the mid-flight guard threw:That guard is correct for real in-progress syncs. The bug was the stale lifecycle state: a successful
skipped-earliertail had finished, but did not restore the files sub-command tostatus=completebefore returning.Implementation
validate_and_default_options()derives the missing runtime key:run_files_sync()now restoresstatus=completeafter a successful deferredfetch-skippedtail. If the pipeline savesstatus=partial, it still returns early so the next invocation can resume.The filter guard remains generic: it blocks real mid-flight filter changes, and completed skipped-file tails no longer look mid-flight.
Testing instructions
Tests cover:
testPullDerivesFlatDocumentRootFromFlattenTo: a pull with--flatten-toplus--runtimehands apply-runtime a matchingflat_document_root.testSkippedEarlierTailRestoresCompletedStatus: a successfulfiles-pull --filter=skipped-earliertail restoresstatus=completeand clears the skipped list.testRepullAfterSkippedEarlierTailUsesCompletedFilesPullState: a completed pull can delta re-pull after the skipped-files tail because the files sub-command state is complete.Testing using Studio
trunkbranch of reprint, build reprint and move it to Studio repo:Failed to pull site: {"error":"Cannot change --filter from 'skipped-earlier' to 'essential-files' while a sync is in progress.