You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
fix(clone): allow ee site clone onto a host with no existing sites - #501
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/disabledstatus), 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.
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
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.
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.
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.
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.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
ee site cloneaborts with: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 synchits the same misleading error.Root cause
Cloner::site_exists()inspectedstdoutfor theError: No sites found!sentinel, but on an empty hostee site listcalls\EE::error( 'No sites found!' ), which writes that message to stderr and exits1. The special-case never matched, so execution fell through to the genericUnable to get site listthrow.It fails whenever the streams stay separate: a local source or destination, and ssh when ee isn't run from a terminal (
ssh -tonly 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
stderr . stdout(covers both the local separate-streams case and thessh -tmerged case), usingstrposto tolerate a trailing newline / surrounding whitespace.is_no_sites_error(), gated on exit code1(the code\EE::erroralways 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=inheritclones) no longer runsforeachovernullon an empty host. Only the "No sites found!" error counts as an empty host; any other unparseableee site listresult, including exit 0 with non-JSON output, now fails withUnable to get site listinstead of being reported as "parent site not found".site_exists()and the parent-site check now read the list through oneget_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 asiteand anenabled/disabledstatus), fails withUnable 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.ee site createsucceeded, 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 runee site deleteon the destination site or enable admin-tools on it.--ssl=inheritclones accepts the wildcard flag as an int (site_ssl_wildcardis1, 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
ee site clone a.test b.testandee site sync a.test b.testnow fail withUnable to find 'a.test' on 'localhost'instead ofUnable to get site list on @localhost. The same happens with a remote source when ee runs without a terminal. The pty-mergedssh -toutput is still recognized.ee site createthere (before:Unable to get site list on user@host).ee site synconto an empty destination still fails, because the destination site must exist; the error is nowUnable to find '<site>' on '<host>'.--ssl=inheritsame-name clone onto an empty host: "Parent site … not found" with no PHP warning; exit 0 with unparseable output givesUnable to get site list.sync --files, sync to a missing site, a clone from a remote source, and a disabled source all behave as before.ee site list(exit 0):Unable to get site list on <host>, noee site deleteis run, and the destination site keeps working.eewas updated, so its firstee site listprinted "Executing migrations. This might take some time." before the JSON:Unable to get site list on <host>;ee site createisn't run on the existing site.--ssl=inheritsame-name clone to another host whose parent has wildcard SSL: the parent is found and the clone goes on toee 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 fromroot@localhostonto 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=inheritclone were exercised with ssh replaced by a local stand-in that runs the commands locally and replays the destination's recordedee site listoutput.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.