acli dev:init: one command from nothing to a working local Acquia dev environment - #2
Open
lauriii wants to merge 11 commits into
Open
acli dev:init: one command from nothing to a working local Acquia dev environment#2lauriii wants to merge 11 commits into
lauriii wants to merge 11 commits into
Conversation
- New top-level setup command that authenticates, picks an application and environment, ensures an SSH key (files or agent), clones code, provisions ddev, imports database and files, and opens the site. Every step no-ops when already done, so re-running resumes after a failure. - Host prerequisites are only git, Docker, and ddev, with one copy-pasteable remedy per missing tool; PHP/Composer/Drush/MySQL all run inside ddev. - install.sh bootstrap installs the native acli release build (no PHP required on macOS arm64/Linux x86_64) with sha256 verification, then runs acli setup. CI now publishes .sha256 files with release assets. - PullCommandBase::pullDatabase() now returns downloaded dump paths so callers can import them through ddev. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Live end-to-end testing caught two bugs: - cloneFromCloud() ran the branch checkout before checking whether git clone succeeded, turning a failed clone into an unhelpful 'cwd does not exist' process error instead of the intended message. - ddev import-db --file stages the dump through the .ddev bind mount, which fails on some Docker providers (e.g. colima); stream the dump through stdin instead. Also recognize SSH keys that exist only in an SSH agent (e.g. the 1Password agent) when checking Cloud Platform key registration. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A stack can come up with the site still broken (e.g. Docker providers that fail to share the project path produce a fallback nginx config that 404s everything). Warn with next steps instead of claiming success. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The push credential for this fork lacks the workflow scope. The two-line checksum change is carried in the PR description for someone with workflow permissions to apply; install.sh already handles releases without checksums gracefully. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Colons are invalid in NTFS filenames, so the fixture broke the Windows CI job. The file is unnecessary: Filesystem::remove() is a no-op for missing paths. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Instead of silently deriving the target directory, setup now asks where to clone, prefilled with ./<sitegroup> so Enter accepts the default. --dir and non-interactive runs skip the prompt as before. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The next steps now explain that committing and pushing deploys, since the cloned branch is what the chosen environment runs. Omitted for tag-tracking environments, where a push does not deploy. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…v:stop setup becomes dev:init, and the daily start/stop loop gets thin acli wrappers so newcomers stay in one vocabulary for the lifecycle moments. dev:start re-prints the site URL and health-checks it; dev:stop shuts the stack down. Everything else (drush, logs, ssh) intentionally stays with ddev directly. The namespace is platform-neutral so future platforms become a routing decision inside dev:init rather than a new prefix. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Instead of handing off to ssh-key:create-upload (three prompts and a required passphrase), dev:init now asks one question, generates an RSA-4096 key without a passphrase (the Cloud Platform API rejects ed25519), uploads it, and polls git access until the key is active. Users who want a passphrase-protected key are pointed at ssh-key:create-upload, which still works as before. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The advisory published 2026-08-05 fails composer audit --locked in CI for every branch. Lockfile-only dev-dependency bump within the 3.x pin. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds a new dev:* command namespace and a bootstrap installer to make it possible to go from “no local setup” to a working ddev-based Acquia local environment with a single command, integrating Cloud authentication/selection, SSH key onboarding, cloning, and initial data sync into acli dev:init.
Changes:
- Introduces
acli dev:initplusdev:start/dev:stop, including shared ddev helpers (DevStackTrait) and PHPUnit coverage. - Improves pull/clone plumbing to support the new flow (clone error ordering +
pullDatabase()returns dump paths) and adds an exception help hint for clone failures. - Adds
install.sh+ README quick start and bumpssquizlabs/php_codesnifferincomposer.lock.
Reviewed changes
Copilot reviewed 11 out of 12 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/phpunit/src/Commands/Dev/DevStartStopCommandTest.php | Adds tests for the new dev:start / dev:stop commands. |
| tests/phpunit/src/Commands/Dev/DevInitCommandTest.php | Adds comprehensive tests for dev:init (prereqs, auth, SSH key handling, clone, resume). |
| tests/phpunit/src/Application/KernelTest.php | Updates expected command list output to include dev:*. |
| src/EventListener/ExceptionListener.php | Adds a targeted help message for clone failures due to SSH key propagation. |
| src/Command/Pull/PullCommandBase.php | Fixes clone error ordering, makes cloneFromCloud() reusable, and returns dump paths from pullDatabase(). |
| src/Command/Dev/DevStopCommand.php | Implements dev:stop wrapper for stopping the ddev stack. |
| src/Command/Dev/DevStartCommand.php | Implements dev:start wrapper for starting the ddev stack and printing/health-checking URL. |
| src/Command/Dev/DevStackTrait.php | Shared ddev start/URL/health-check helpers used by dev:*. |
| src/Command/Dev/DevInitCommand.php | Implements the end-to-end “nothing to working local site” flow. |
| README.md | Adds a one-command quick start using install.sh and dev:init. |
| install.sh | Adds a curl-install bootstrap script for installing acli and launching dev:init. |
| composer.lock | Lockfile-only bump of squizlabs/php_codesniffer to 3.13.6. |
Suppressed comments (1)
tests/phpunit/src/Commands/Dev/DevInitCommandTest.php:387
- This test docblock still says “Re-running setup…”, but the command is
dev:init. Keeping terminology consistent makes the intent clearer for future readers.
* Re-running setup on an existing checkout with an installed site skips
* every completed step instead of redoing it.
*/
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| Linux-x86_64) ASSET="native-acli-linux-x86_64.tar.gz" ;; | ||
| esac | ||
|
|
||
| TMP_DIR="$(mktemp -d)" |
Comment on lines
+191
to
+197
| $vcsUrl = $environment->vcs->url; | ||
| LoopHelper::getLoopy($this->output, $this->io, 'Waiting for the key to be installed on the Cloud Platform (usually a minute or two)...', function () use ($vcsUrl): bool { | ||
| $process = $this->localMachineHelper->execute(['git', 'ls-remote', $vcsUrl, 'HEAD'], null, null, false, 30, ['GIT_SSH_COMMAND' => 'ssh -o StrictHostKeyChecking=accept-new -o BatchMode=yes']); | ||
| return $process->isSuccessful(); | ||
| }, function (): void { | ||
| $this->io->writeln('✓ SSH key is active'); | ||
| }); |
Comment on lines
+222
to
+225
| /** | ||
| * Without --dir, setup confirms the clone directory interactively with a | ||
| * derived default, and clones into whatever the user answers. | ||
| */ |
Copilot review: - Use a template with mktemp for BSD/POSIX portability in install.sh. - Do not report the SSH key as active when the propagation wait times out: LoopHelper invokes its done callback on watchdog timeout, so track success explicitly and fail with a resumable message instead. - Fix stale 'setup' wording in docblocks and help text. Mutation testing (--min-covered-msi=100 on changed lines): - Read the docroot from ddev's config.yaml instead of guessing docroot/ vs web/ — more correct for any custom docroot. - Exempt console output and checklist calls in infection.json5, same rationale as the existing logger exemption; annotate declarative configure() methods and untestable spots with documented reasons. - New tests: reusing a previously generated key, non-matching agent keys in non-interactive mode, per-OS prerequisite remedies, the web/ docroot file sync path, URL fallback when ddev describe emits no usable JSON, unreachable-site warning, and drush-warning absence. - Make dev:* trait helpers private. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
One command from nothing to a working local Acquia dev environment
The command, as it would appear on the developer home page
curl -fsSL https://raw.githubusercontent.com/acquia/cli/main/install.sh | sh…and for anyone who already has acli installed, the same flow is simply:
The documented path to a local site today is ~19 shell commands across ~300 lines of instructions (install acli, create an API token,
auth:login, find the app, upload an SSH key, clone, configure ddev, start it, composer install, pull DB, pull files, …). Every step is a drop-off point. This PR collapses all of it into one line.What this branch contains
acli dev:init— the full nothing-to-working-site flow (details below).acli dev:start/acli dev:stop— thin wrappers for the daily loop: start re-prints the site URL and health-checks it; stop shuts the stack down.install.sh— thecurl | shbootstrap, plus a README quick-start section.squizlabs/php_codesnifferto 3.13.6 for CVE-2026-67434.Why this entry point
npm create vite@latestworks because npm is already on every JS developer's machine. acli is a PHP PHAR, and PHP is not on a modern macOS machine — so any instruction that starts withacli …silently assumes the hardest step (getting acli itself) is already done.Candidates considered:
composer create-project— requires Composer and PHP preinstalled; also wrong semantics (most users clone an existing Cloud app, not a new template).acli dev:initalone + separate installer line — two lines, and the first one is the one that loses people.curl | shbootstrap — chosen. It is the pattern developers already trust for ddev, Homebrew, rustup, and OrbStack, and this repo is unusually well positioned for it: CI already builds self-contained native binaries (native-acli-macos-aarch64,native-acli-linux-x86_64) with PHP bundled via static-php-cli. That means the bootstrap needs no PHP on the host at all on those platforms. On other platforms it falls back toacli.pharwith a precise PHP remedy.curl | shdeserves scrutiny, so the script is built to be scrutinized:.sha256for every download and aborts on mismatch (checksum publishing itself still needs a CI change — see "What still has to be stood up");~/.local/bin— no sudo, ever;/dev/ttyso the interactive setup works even when piped tosh; in CI it prints the next step instead of hanging;ACLI_INSTALL_DIR,ACLI_INSTALL_NO_SETUP, andACLI_INSTALL_BASE_URLoverrides for packaging/testing.Naming: the
devnamespaceThe local-dev story lives in its own command namespace —
dev:init(this flow), plus thindev:start/dev:stopfor the daily loop — soacli listshows one self-documenting block and newcomers stay in one vocabulary for the lifecycle moments.dev:initis deliberately platform-neutral: if a second platform's flow lands later, it becomes a routing decision inside the command rather than a new prefix users must know. Boundary, stated on purpose:dev:start/dev:stopare the only ddev wrappers; everything else (drush, logs, ssh, snapshots) intentionally points at ddev itself to avoid a permanently lagging façade.The flow and its defaults
acli dev:initruns eleven idempotent steps. Interactively, the happy path asks only what cannot be defaulted: API token (first run only), which application (only if you have more than one), which environment (only if the app has several non-prod ones), where to clone (prefilled with a derived default so Enter accepts it), and a single confirm if an SSH key must be created. Everything else is decided for you:auth:login(opens the token page in your browser). Non-interactive:ACLI_KEY/ACLI_SECRET.determineEnvironment()prompts (production excluded); or passacli dev:init myapp.dev.~/.ssh/*.puband any keys in your SSH agent (1Password agent, forwarded agents) against the Cloud account. If none: one confirm, then it generates an RSA-4096 key without a passphrase (the API rejects ed25519;ssh-key:create-uploadremains the passphrase-protected route and is pointed to), uploads it, and pollsgit ls-remoteuntil the key is actually active. Non-interactive mode deliberately does not mint keys — it fails with the remedy instead../<sitegroup>;--diror non-interactive mode skips the prompt), checks out the environment's branch. Detects an existing checkout and skips..acquia-cli.ymlsoacli pulletc. work forever after.ddev config --auto+ddev start(skipped if already configured/running).ddev composer install(skipped ifvendor/exists).acli pullrefreshes later).ddev drush cache:rebuild+sql:sanitize(warn-only on failure), then an HTTP health check of the site URL (warns with next steps rather than claiming success).ddev drush ulito log in,git pushto deploy (the environment runs the branch you're on; omitted for tag-tracking environments),acli pullto re-sync,acli dev:stopto shut down.Resumability is structural, not stateful: every step no-ops when its outcome already exists, so after any failure you fix the cause and re-run
acli dev:init— it fast-forwards to where it stopped. There is no state file to corrupt.Non-interactive contract (CI, scripted demos):
ACLI_KEY=… ACLI_SECRET=… acli dev:init myapp.dev --no-interaction [--dir=…]Zero prompts; every unmet precondition fails with the exact command to fix it.
What was verified (with real output)
1. Repo checks — all green
New tests:
tests/phpunit/src/Commands/Dev/— 12 tests covering missing prerequisites, Docker down, unauthenticated non-interactive, missing SSH key in non-interactive mode, the automated SSH key generation + upload + install-polling path, the clone-directory prompt (clone targets the answered path), clone failure (error-ordering regression), the full fresh non-interactive flow against the mocked Cloud API (clone → ddev config → start → import → rsync → drush → summary, including the.acquia-cli.ymllink file), the resume path (every step skipped), anddev:start/dev:stop(happy paths and the missing-project error).2. Prerequisite detection and the non-interactive contract (real terminal)
3. The bootstrap installer (real run against the live GitHub release)
Checksum verification was exercised against a local fixture release: a correct
.sha256verifies silently; a tampered one aborts withError: Checksum mismatch for native-acli-macos-aarch64.tar.gz. Aborting.before anything is installed.4. Live end-to-end run against a real Cloud subscription
A limited-access test account was provided mid-task, so the full flow was run for real against application
<application>(dev environment) on macOS arm64 with colima:Also verified live: resumability (a run interrupted mid-
ddev startwas re-run and fast-forwarded:✓ Code already cloned,✓ ddev is already configured, straight to the remaining steps), a real SSH-key upload + propagation cycle (viassh-key:upload+ polling, the same mechanicsdev:initnow automates), and SSH-agent key detection. The test SSH key was deleted from the account and the ddev project torn down afterwards.Running the flow for real caught two bugs the mocked tests could not:
PullCommandBase::cloneFromCloud()ran the branch checkout before checking whethergit clonesucceeded, so a failed clone (e.g. SSH key not yet propagated) crashed withThe provided cwd … does not existinstead of the intended "Failed to clone repository from the Cloud Platform" message. Fixed, with a regression test pinning the order.ddev import-db --filestages the dump through the.ddevbind mount, which fails on some Docker providers (observed on colima). The import now streams the dump through stdin (gunzip -c … | ddev import-db), which sidesteps the mount entirely.Discovered along the way and folded into the design: the Cloud Platform API rejects ed25519 keys (RSA only), and keys living only in an SSH agent were previously invisible to the key check.
What remains unverified — please don't take this PR as claiming otherwise
acli dev:initwith no arguments, on an account with no SSH keys, is the single most valuable manual check.install.sh(the test machine took the native-binary path; the phar branch is code-reviewed and syntax-checked only).dev:start/dev:stopwere unit-tested but not run against a live ddev project.Notes for the reviewer
build-release → Publish docsstep is expected to be red here. It runsacli self:make-docsand thenaws s3 syncon every push event, and the AWS secrets only exist inacquia/cli— no fork can pass it. The docs-generation half was verified locally (exits 0, includes the newdev:*commands).build-nativeshows as skipped only because it depends on that job. All six test jobs (Ubuntu PHP 8.2–8.5 + coverage, Windows) and Mutation Testing are green on this branch.composer.lockchange is a security fix, not feature fallout:squizlabs/php_codesnifferbumped to 3.13.6 for CVE-2026-67434 (published 2026-08-05), which failscomposer audit --lockedin CI for every branch, including upstream's next run. It may be worth sending that one-line bump toacquia/clias its own PR so their CI doesn't go red independently of this work.PullCommandBase::pullDatabase()changed fromvoidto returning the downloaded dump paths sodev:initcan import them through ddev; existing callers ignore the return value and all pull tests pass unchanged.What still has to be stood up, and by whom
A stable install URL. The honest line works today via
raw.githubusercontent.com, but the home-page-worthy form iscurl -fsSL https://cli.acquia.com/install | sh(or similar). Someone with acquia.com DNS/web access needs to serve or redirect toinstall.sh. Until then the raw GitHub URL is fully functional.Checksum publishing needs a workflow-scope push. The credential used for this branch cannot modify
.github/workflows/, so the small CI change that publishes.sha256files with release assets must be applied by someone with workflow permissions (the installer treats missing checksums honestly: it warns and continues; once a release ships them, verification becomes strict):Signing (minisign/cosign) would be a further step — deliberately not invented here.
Native build coverage. Releases currently build native binaries for macOS arm64 and Linux x86_64 only. Intel Macs and arm64 Linux fall back to the phar + host PHP. Adding those two matrix entries in
ci.yml(needs the corresponding static-php-cli artifacts uploaded to the acquia-cli S3 bucket) would close the gap.Docs. The dev-portal quickstart can shrink to the one-liner once 1–2 land.
Also deliberately out of scope: creating new subscriptions/applications from the CLI (no public API for provisioning them —
acli dev:inittargets existing applications, andacli newalready covers the local-scaffold case), and ACSF-specific flows.