Skip to content

pull: fix the flattened runtime root and delta re-pull after deferred files - #268

Merged
adamziel merged 2 commits into
trunkfrom
pull/studio-single-invocation
Jun 19, 2026
Merged

pull: fix the flattened runtime root and delta re-pull after deferred files#268
adamziel merged 2 commits into
trunkfrom
pull/studio-single-invocation

Conversation

@epeicher

@epeicher epeicher commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

What it does

Two fixes to the composite pull command 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:

  1. 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-to builds the flattened docroot and --runtime targets it.
  2. A completed pull can be re-pulled after its deferred-files tail ran. Once skipped-earlier files have been fetched, the files sub-command records a truthful status=complete, so the next pull --filter=essential-files proceeds as a delta re-pull without weakening the mid-flight --filter guard.

Rationale

--flatten-to did not reach apply-runtime. pull forwards --flatten-to to the flat-docroot stage as flatten_to, but run_apply_runtime() reads a different key, flat_document_root, and otherwise falls back to fs_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. pull supports delta re-pull: re-running a completed pull resets its own state via prepare_repull() and re-syncs. But after an --filter=essential-files pull fetched its deferred skipped-earlier files, the state still read filter=skipped-earlier, status=in_progress. The next pull --filter=essential-files loaded that stale state in ImportClient::run() and the mid-flight guard threw:

Cannot change --filter from 'skipped-earlier' to 'essential-files' while a sync is in progress.

That guard is correct for real in-progress syncs. The bug was the stale lifecycle state: a successful skipped-earlier tail had finished, but did not restore the files sub-command to status=complete before returning.

Implementation

validate_and_default_options() derives the missing runtime key:

if (!empty($options['flatten_to']) && empty($options['flat_document_root'])) {
    $options['flat_document_root'] = $options['flatten_to'];
}

run_files_sync() now restores status=complete after a successful deferred fetch-skipped tail. If the pipeline saves status=partial, it still returns early so the next invocation can resume.

$this->run_files_sync_pipeline();
if (($this->state["status"] ?? null) === "partial") {
    return;
}
$this->state["status"] = "complete";
$this->save_state($this->state);

The filter guard remains generic: it blocks real mid-flight filter changes, and completed skipped-file tails no longer look mid-flight.

Testing instructions

cd tests && ../vendor/bin/phpunit Import/PullFilterOptionTest.php Import/FilesSyncStateTest.php

Tests cover:

  • testPullDerivesFlatDocumentRootFromFlattenTo: a pull with --flatten-to plus --runtime hands apply-runtime a matching flat_document_root.
  • testSkippedEarlierTailRestoresCompletedStatus: a successful files-pull --filter=skipped-earlier tail restores status=complete and 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

  • Use a remote site that has uploads.
  • Using the trunk branch of reprint, build reprint and move it to Studio repo:
composer build:phar && cp reprint.phar <studio-repo>/apps/cli/dist/cli/wp-files/reprint/reprint.phar
  • Open a terminal on the root of the Studio repo and run:
node apps/cli/dist/cli/main.mjs pull-reprint --url <remote-site-url> --name g1 --yes
  • Run the command again so a delta-pull is done:
node apps/cli/dist/cli/main.mjs pull-reprint --url <remote-site-url> --name g1 --yes
  • Check there is an error Failed to pull site: {"error":"Cannot change --filter from 'skipped-earlier' to 'essential-files' while a sync is in progress.
  • Repeat the steps using this branch and verify the site can be re-pulled without errors.
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.
epeicher added a commit to Automattic/studio that referenced this pull request Jun 18, 2026
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).
@github-actions

github-actions Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Pull pipeline performance — large-directory

Site: large-directory · 2,000+ plus targeted file-transfer scenarios files · 10,000 posts · 25,000 postmeta · PHP 8.5.7

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.

@epeicher epeicher changed the title pull: support Studio's single-invocation flow Jun 18, 2026
@epeicher epeicher self-assigned this Jun 18, 2026
@epeicher
epeicher requested a review from Copilot June 18, 2026 17:11
@epeicher
epeicher requested a review from adamziel June 18, 2026 17:13

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_root from flatten_to in validate_and_default_options() so that pull --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 in ImportClient::run(), allowing prepare_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_toflat_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
```
@adamziel
adamziel merged commit cb8b39e into trunk Jun 19, 2026
18 checks passed
@adamziel
adamziel deleted the pull/studio-single-invocation branch June 19, 2026 15:37
epeicher added a commit to Automattic/studio that referenced this pull request Jun 19, 2026
## 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?
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

3 participants