Skip to content

acli dev:init: one command from nothing to a working local Acquia dev environment - #2

Open
lauriii wants to merge 11 commits into
mainfrom
acli-create
Open

acli dev:init: one command from nothing to a working local Acquia dev environment#2
lauriii wants to merge 11 commits into
mainfrom
acli-create

Conversation

@lauriii

@lauriii lauriii commented Aug 7, 2026

Copy link
Copy Markdown
Owner

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:

acli dev:init

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.
  • Automated SSH key onboarding — if no local or agent key is registered with the Cloud account: one confirm, then generate RSA-4096, upload, and poll until git access actually works.
  • install.sh — the curl | sh bootstrap, plus a README quick-start section.
  • Two bug fixes found by running the flow for real (see verification), and a lockfile-only bump of squizlabs/php_codesniffer to 3.13.6 for CVE-2026-67434.

Why this entry point

npm create vite@latest works 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 with acli … 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).
  • npm shim — requires Node, needs a published npm package, and is dishonest tooling for a PHP ecosystem.
  • acli dev:init alone + separate installer line — two lines, and the first one is the one that loses people.
  • curl | sh bootstrap — 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 to acli.phar with a precise PHP remedy.

curl | sh deserves scrutiny, so the script is built to be scrutinized:

  • ~100 lines of commented POSIX sh, no obfuscation, designed to be read before running;
  • verifies the published .sha256 for every download and aborts on mismatch (checksum publishing itself still needs a CI change — see "What still has to be stood up");
  • installs to ~/.local/binno sudo, ever;
  • re-attaches /dev/tty so the interactive setup works even when piped to sh; in CI it prints the next step instead of hanging;
  • ACLI_INSTALL_DIR, ACLI_INSTALL_NO_SETUP, and ACLI_INSTALL_BASE_URL overrides for packaging/testing.

Naming: the dev namespace

The local-dev story lives in its own command namespace — dev:init (this flow), plus thin dev:start / dev:stop for the daily loop — so acli list shows one self-documenting block and newcomers stay in one vocabulary for the lifecycle moments. dev:init is 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:stop are 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:init runs 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:

Step Default / behavior
Prerequisites Checks git, docker, ddev + a running Docker daemon. One copy-pasteable per-OS remedy for each missing tool. PHP, Composer, Drush, and MySQL are not required on the host — they all run inside ddev.
Authenticate Reuses stored credentials; else runs auth:login (opens the token page in your browser). Non-interactive: ACLI_KEY/ACLI_SECRET.
App + environment determineEnvironment() prompts (production excluded); or pass acli dev:init myapp.dev.
SSH key Matches your local ~/.ssh/*.pub and 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-upload remains the passphrase-protected route and is pointed to), uploads it, and polls git ls-remote until the key is actually active. Non-interactive mode deliberately does not mint keys — it fails with the remedy instead.
Get code Confirms the clone directory (prefilled ./<sitegroup>; --dir or non-interactive mode skips the prompt), checks out the environment's branch. Detects an existing checkout and skips.
Link Writes .acquia-cli.yml so acli pull etc. work forever after.
Local stack ddev config --auto + ddev start (skipped if already configured/running).
Dependencies ddev composer install (skipped if vendor/ exists).
Database + files Downloads the latest Cloud backup, imports it into ddev, rsyncs files. Skipped entirely if the site already bootstraps (acli pull refreshes later).
Post-install 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).
Done Opens the site in your browser; prints what you have and the next steps: ddev drush uli to log in, git push to deploy (the environment runs the branch you're on; omitted for tag-tracking environments), acli pull to re-sync, acli dev:stop to 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

$ composer test    # lint + phpcs + phpstan + phpunit (serial, then paratest)
OK (63 tests, 97 assertions)
OK (544 tests, 2423 assertions)
$ composer audit --locked
No security vulnerability advisories found.

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.yml link file), the resume path (every step skipped), and dev:start / dev:stop (happy paths and the missing-project error).

The terminal transcripts below were captured while the command was still named setup. It was renamed dev:init before this PR with no behavioral change, and the closing summary has since gained the git push deploy line and acli dev:stop.

2. Prerequisite detection and the non-interactive contract (real terminal)

$ acli setup -n        # machine with git/docker/ddev but no credentials
Let's get you a local development environment.

✓ Found git, docker, and ddev

  This machine is not authenticated with the Cloud Platform. Run `acli auth:login`
  first or set the ACLI_KEY and ACLI_SECRET environment variables.

3. The bootstrap installer (real run against the live GitHub release)

$ ACLI_INSTALL_NO_SETUP=1 sh install.sh
Downloading Acquia CLI (native build, no PHP required) ...
Warning: this release publishes no checksum for native-acli-macos-aarch64.tar.gz; skipping verification.
Installed acli to .../acli-install-test/acli
Acquia CLI 3.0.1

Checksum verification was exercised against a local fixture release: a correct .sha256 verifies silently; a tampered one aborts with Error: 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:

$ acli setup <app-uuid>… -n --dir=$HOME/acli-e2e-site
Let's get you a local development environment.

✓ Found git, docker, and ddev
✓ Authenticated as <your Cloud account>
✓ An SSH key in your SSH agent is registered with the Cloud Platform
 ✔ Cloning the dev environment's code into /Users/…/acli-e2e-site
 ✔ Configuring ddev
 ✔ Starting ddev (the first run may download Docker images)
✓ Composer dependencies already installed
 ✔ Downloading <application> database copy from the Cloud Platform
 ✔ Importing database into ddev
 ✔ Copying Drupal's public files from the Cloud Platform
 ✔ Rebuilding Drupal caches and sanitizing the database
[OK] Your local development environment is ready: https://acli-e2e-site.ddev.site
$ echo $?
0
$ curl -sk -o /dev/null -w "%{http_code}" https://acli-e2e-site.ddev.site/   # → 200
$ curl -sk https://acli-e2e-site.ddev.site/ | grep -o "<title>.*</title>"
<title>Welcome! | <site name></title>       # identical to the real Dev environment
$ ddev drush uli   # → working one-time login link

Also verified live: resumability (a run interrupted mid-ddev start was re-run and fast-forwarded: ✓ Code already cloned, ✓ ddev is already configured, straight to the remaining steps), a real SSH-key upload + propagation cycle (via ssh-key:upload + polling, the same mechanics dev:init now 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:

  • Pre-existing upstream bug: PullCommandBase::cloneFromCloud() ran the branch checkout before checking whether git clone succeeded, so a failed clone (e.g. SSH key not yet propagated) crashed with The provided cwd … does not exist instead of the intended "Failed to clone repository from the Cloud Platform" message. Fixed, with a regression test pinning the order.
  • ddev import bug: ddev import-db --file stages the dump through the .ddev bind 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

  • The interactive prompt flow (choose app/env, browser-based token entry, and especially the automated SSH key generation + upload + propagation wait) was exercised only through unit tests' scripted inputs, not a live TTY session — the live runs were all non-interactive, and the automated key path landed after the live E2E. A human smoke test of acli dev:init with no arguments, on an account with no SSH keys, is the single most valuable manual check.
  • Windows (acli supports it; the prereq remedies and ddev orchestration are only written for macOS/Linux — realistically this flow implies WSL2 there).
  • The phar fallback path of install.sh (the test machine took the native-binary path; the phar branch is code-reviewed and syntax-checked only).
  • Multisite/ACSF applications and multi-database applications (single-db, single-site path verified live).
  • dev:start/dev:stop were unit-tested but not run against a live ddev project.

Notes for the reviewer

  • Fork CI: the build-release → Publish docs step is expected to be red here. It runs acli self:make-docs and then aws s3 sync on every push event, and the AWS secrets only exist in acquia/cli — no fork can pass it. The docs-generation half was verified locally (exits 0, includes the new dev:* commands). build-native shows 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.
  • The composer.lock change is a security fix, not feature fallout: squizlabs/php_codesniffer bumped to 3.13.6 for CVE-2026-67434 (published 2026-08-05), which fails composer audit --locked in CI for every branch, including upstream's next run. It may be worth sending that one-line bump to acquia/cli as its own PR so their CI doesn't go red independently of this work.
  • PullCommandBase::pullDatabase() changed from void to returning the downloaded dump paths so dev:init can 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

  1. A stable install URL. The honest line works today via raw.githubusercontent.com, but the home-page-worthy form is curl -fsSL https://cli.acquia.com/install | sh (or similar). Someone with acquia.com DNS/web access needs to serve or redirect to install.sh. Until then the raw GitHub URL is fully functional.

  2. Checksum publishing needs a workflow-scope push. The credential used for this branch cannot modify .github/workflows/, so the small CI change that publishes .sha256 files 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):

    # build-release job, after `composer box-compile`:
          - name: Generate checksum
            run: shasum -a 256 acli.phar > acli.phar.sha256
            working-directory: var
    # …and change the Release step files to:
    #   var/acli.phar
    #   var/acli.phar.sha256
    
    # build-native job, after the tar -czvf line:
              shasum -a 256 native-acli-${{ matrix.platform }}.tar.gz > native-acli-${{ matrix.platform }}.tar.gz.sha256
    # …and add native-acli-${{ matrix.platform }}.tar.gz.sha256 to its Release files.

    Signing (minisign/cosign) would be a further step — deliberately not invented here.

  3. 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.

  4. 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:init targets existing applications, and acli new already covers the local-scaffold case), and ACSF-specific flows.

lauriii and others added 10 commits August 5, 2026 10:17
- 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>

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

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:init plus dev: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 bumps squizlabs/php_codesniffer in composer.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.

Comment thread install.sh Outdated
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>
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