Skip to content

fix(clone): allow ee site clone onto a host with no existing sites - #501

Merged
mrrobot47 merged 9 commits into
EasyEngine:developfrom
mrrobot47:fix/clone-empty-host-site-list
Oct 14, 2026
Merged

mrrobot47 merged 9 commits into
EasyEngine:developfrom
mrrobot47:fix/clone-empty-host-site-list

Conversation

@mrrobot47

@mrrobot47 mrrobot47 commented Jul 16, 2026 •

Copy link
Copy Markdown
Member

Problem

ee site clone aborts with:

Warning: Encountered error while cloning site. Rolling back.
Error: Unable to get site list on @localhost

whenever the destination host has zero existing EasyEngine sites — making it impossible to clone the very first site onto a fresh/empty host. ee site sync hits the same misleading error.

Root cause

Cloner::site_exists() inspected stdout for the Error: No sites found! sentinel, but on an empty host ee site list calls \EE::error( 'No sites found!' ), which writes that message to stderr and exits 1. The special-case never matched, so execution fell through to the generic Unable to get site list throw.

It fails whenever the streams stay separate: a local source or destination, and ssh when ee isn't run from a terminal (ssh -t only allocates a pty when stdin is a tty, so automation such as EasyDash hits it on remote hosts too). From an interactive terminal, ssh -t's pty merges the remote stderr into stdout, which the old stdout match happened to catch.

Fix

  • Match the sentinel against stderr . stdout (covers both the local separate-streams case and the ssh -t merged case), using strpos to tolerate a trailing newline / surrounding whitespace.
  • Extract the check into is_no_sites_error(), gated on exit code 1 (the code \EE::error always exits with) so an unrelated non-zero failure can't be mistaken for "no sites" via a substring match.
  • validate_parent_site_present_on_host() (reached on same-name --ssl=inherit clones) no longer runs foreach over null on an empty host. Only the "No sites found!" error counts as an empty host; any other unparseable ee site list result, including exit 0 with non-JSON output, now fails with Unable to get site list instead of being reported as "parent site not found".
  • site_exists() and the parent-site check now read the list through one get_site_list(). The "No sites found!" error gives an empty list. A non-zero exit, or output that isn't a JSON list of site rows (each with a site and an enabled/disabled status), fails with Unable to get site list. Before, output that wasn't a JSON list (e.g. shell-rc output, a PHP notice, or the "Executing migrations" line that the first command after an EE update prints on stdout) reported the site as missing, so a clone onto an existing destination site tried to create it, failed, and the rollback then deleted the existing destination site. A JSON object such as {} no longer reads as an empty host either.
  • The clone's rollback removes the destination site only when this run's ee site create succeeded, and admin-tools and mailhog are set up only on a site the clone created. Before, a failed create (for example when the name was taken between the existence check and the create) could still run ee site delete on the destination site or enable admin-tools on it.
  • The parent-site check for same-name --ssl=inherit clones accepts the wildcard flag as an int (site_ssl_wildcard is 1, not '1', on PHP 8.1+). develop already has the same change from fix(clone): quote clone and sync commands and validate the other host's site details #510.

Verification

  • Empty host: ee site clone a.test b.test and ee site sync a.test b.test now fail with Unable to find 'a.test' on 'localhost' instead of Unable to get site list on @localhost. The same happens with a remote source when ee runs without a terminal. The pty-merged ssh -t output is still recognized.
  • Clone onto an empty destination host gets past the existence check and runs ee site create there (before: Unable to get site list on user@host).
  • ee site sync onto an empty destination still fails, because the destination site must exist; the error is now Unable to find '<site>' on '<host>'.
  • --ssl=inherit same-name clone onto an empty host: "Parent site … not found" with no PHP warning; exit 0 with unparseable output gives Unable to get site list.
  • With existing sites: a local clone, cloning onto an existing site (refused, destination untouched), sync --files, sync to a missing site, a clone from a remote source, and a disabled source all behave as before.
  • Clone onto an existing destination site whose ssh session prints extra output before ee site list (exit 0): Unable to get site list on <host>, no ee site delete is run, and the destination site keeps working.
  • Clone onto an existing destination site right after its ee was updated, so its first ee site list printed "Executing migrations. This might take some time." before the JSON: Unable to get site list on <host>; ee site create isn't run on the existing site.
  • A failed clone into a new destination site (files copy failing after the create) still removes that new site, locally and over ssh.
  • --ssl=inherit same-name clone to another host whose parent has wildcard SSL: the parent is found and the clone goes on to ee site create … --ssl=inherit (before: "Parent site … not found").

Tested on Ubuntu 26.04 with EasyEngine 4.12.0 with local clones and syncs, and over real ssh to root@localhost: the empty-host case with and without a terminal, a clone from root@localhost onto the local host, and the destination with extra output in its ssh session. Because the test host is both source and destination, the clone onto an empty remote destination and the cross-host --ssl=inherit clone were exercised with ssh replaced by a local stand-in that runs the commands locally and replays the destination's recorded ee site list output.

Re-tested on 2026-10-14 after merging develop, between this host and a second EasyEngine host reached over real ssh: a clone onto a host with no sites, clones onto an existing destination site with a one-off migration notice and with a shell-rc line on every ssh session (the existing site's files, database, containers and site row are unchanged), clone and sync (--files, --db) in both directions, a local clone, and a failed clone into a new site, which is rolled back.

`ee site clone`/`ee site sync` aborted with "Unable to get site list"
whenever the destination host had zero existing EE sites. On an empty
host `ee site list` calls \EE::error('No sites found!'), which writes to
stderr and exits 1, but site_exists() inspected stdout for the sentinel,
so the special-case never matched and the generic error path threw.

Match the sentinel against stderr . stdout (covering both the local
separate-streams case and the ssh -t merged-output case) and use strpos
to tolerate trailing whitespace/debug noise.

Also guard validate_parent_site_present_on_host() against a null
json_decode on an empty host, which previously triggered a PHP warning
from foreach(null).

Refs rtCamp/EasyDash#4135
- Extract is_no_sites_error() so site_exists() and
  validate_parent_site_present_on_host() share one sentinel check.
- Narrow the gate back to exit code 1 (the code \EE::error always exits
  with) so an unrelated non-zero failure can no longer be mistaken for
  'no sites' via a substring match.
- validate_parent_site_present_on_host() now distinguishes an empty host
  from a genuine 'ee site list' failure, surfacing the latter as
  'Unable to get site list' instead of masking it as 'parent not found'.
validate_parent_site_present_on_host() treated any non-JSON 'ee site list' output with exit code 0 as an empty host, so garbled output (e.g. stderr noise merged into stdout under ssh -t) was still reported as 'Parent site not found'. Only the 'No sites found!' error now counts as an empty host.
site_exists() read `ee site list` output that exited 0 but wasn't JSON (e.g. shell-rc noise under ssh -t) as "site doesn't exist". For an existing destination site, the clone then ran `ee site create`, which failed, and the create step's rollback ran `ee site delete --yes` on that existing site. Throw "Unable to get site list" instead, like the parent-site check does.
On PHP 8.1+ PDO SQLite returns native ints, so `ee site info --format=json` gives `site_ssl_wildcard: 1` and the strict `=== '1'` check never matched. Same-name `--ssl=inherit` clones to another host then always failed with "Parent site not found", even when the parent had wildcard SSL. Check both fields with `empty()` instead.
@mrrobot47 mrrobot47 changed the title fix(clone): allow ee site clone/sync onto a host with no existing sites fix(clone): allow ee site clone onto a host with no existing sites Sep 24, 2026
The create step's rollback ran ee site delete even when this run's ee site create had failed, and the admin-tools and mailhog steps still ran afterwards. A site list that parses but doesn't show the site, or a site created in between, then let a failed clone delete or reconfigure a site it never made. Remove the site only after a successful create, and stop before admin-tools and mailhog when the create failed.
@rtBot
rtBot requested a balanced review from Copilot October 14, 2026 15:18

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

JSON objects and malformed list entries can still bypass the new site-list validation.

1 open finding
What changed in this PR

Fixes cloning and syncing behavior when a destination host has no sites.

Changes:

  • Recognizes empty-host errors across stderr and stdout.
  • Validates site-list output and improves rollback safety.
  • Supports integer wildcard SSL flags.
File Description
src/​clone/​Cloner.php Updates site discovery, SSL inheritance, and clone rollback behavior.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/clone/Cloner.php Outdated
A JSON object also decodes to a PHP array, so {} read as an empty list, and a row that isn't an array with a site name hit a string offset (a TypeError on PHP 8). Both site_exists() and the parent-site check now treat such output as an unreadable site list.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

Two validation paths can still trust incomplete or failed site-list output.

0 open findings

1 resolved since last review
Previously missed (2)

In code that hasn't changed since last review

Medium severity Reject valid JSON output when site list command fails

src/​clone/​Cloner.php:100

A nonzero ee site list run is only rejected when parsing returns null. If a failed run still emits a syntactically valid JSON array, this path trusts the output and can approve an SSL parent despite the command failure. Handle the no-sites sentinel first, then require exit code 0 for all other output, as site_exists() does.

Medium severity Validate site status values before checking disabled sites

src/​clone/​Cloner.php:356

The parser accepts rows without a valid status, but site_exists() immediately reads that key to enforce the disabled-site check. For example, [{"site":"a.test"}] now passes parsing, emits an undefined-key warning, and treats the site as enabled instead of surfacing Unable to get site list. Validate the field and its two values along with site.

🧠 Review effort: Balanced

The parent-site check trusted a failed ee site list run when its output still parsed, and a row without a valid status passed the parse and left the disabled-site check reading a missing key. site_exists() and the parent-site check now share get_site_list(): no sites gives an empty list, and a non-zero exit or anything but a list of site rows with an enabled or disabled status is an unreadable site list.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Approval recommended

The changes address the empty-host failure and unsafe rollback behavior without introducing unresolved issues.

0 open findings

🧠 Review effort: Balanced

mrrobot47 added a commit to mrrobot47/site-command that referenced this pull request Oct 14, 2026
mrrobot47 added a commit to mrrobot47/site-command that referenced this pull request Oct 14, 2026
@mrrobot47
mrrobot47 merged commit e19a49e into EasyEngine:develop Oct 14, 2026
3 checks passed
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.

2 participants