Installer
The Vortex installer is a self-contained Symfony Console application that turns the template into a project shaped by your answers. This page is for contributors who change the installer. It explains how the installer works, how its tests and snapshots run, how it's released, and how its demo video gets recorded. To install Vortex with it, see Installation instead.
The installer lives in the same repository as the template, at
.vortex/installer/. That way, a single pull request can change a template
file together with the installer logic that handles it, and the 2 stay in
step.
Architecture
The installer is written in PHP rather than shell because it touches many
parts of the template and has to manage every permutation of the template's
variants. PHP also gives it a terminal UI through
Laravel Prompts, which takes you
through the questions one at a time. Pass --no-interaction and it asks
nothing at all: it installs new and existing projects, and updates them,
unattended, which suits automated pipelines and CI environments.
Each prompt pre-fills its answer, and you can change it when the installer
asks. With --no-interaction, the pre-filled value is the answer. The
installer takes it from the first of these sources that has one:
- Prompt answers - a JSON object passed with
--prompts, either inline or as a path to a JSON file. Its keys are the prompt IDs that--schemaprints, and--validatechecks the object against that schema without installing anything. An invalid value stops the installer before the first prompt. - Discovery - auto-detection from existing project files (for example,
composer.json,.env, hosting-specific files like.lagoon.yml). - Handler defaults - fallback values defined by each handler.
The profile, theme, and web root prompts go a step further. When discovery
finds a value, or an earlier answer settles it, the installer uses that
value without asking, and that value wins over --prompts. Choosing Acquia
hosting, for example, settles the web root.
Together with --no-interaction, --prompts runs a whole installation
without a single question, which is how the installer's own tests drive
every scenario. To check a prompts file before such a run, validate it:
php installer.php --prompts=prompts.json --validate
The command prints a JSON object with valid, errors, warnings, and
resolved keys. It exits with 0 when valid is true and with 1
otherwise; warnings, such as an answer whose condition isn't met, don't
make the object invalid. Without --prompts, or when the input isn't a
JSON object, it prints an error message and exits with 1.
--config takes a different JSON object: the installer's own settings,
keyed by their VORTEX_INSTALLER_* names, such as
VORTEX_INSTALLER_TEMPLATE_REPO. An environment variable with the same name
wins over the value in the JSON.
The final Run the site build now? question is the only one with an
environment variable of its own. VORTEX_INSTALLER_PROMPT_BUILD_NOW sets
its default: 0 or an empty value defaults to no, and any other value, or
leaving the variable unset, defaults to yes. --build skips the question
and runs the build. A run with --no-interaction never asks the question,
so the variable has no effect there: the build runs only with --build.
An installation runs in this order:
- You download the
installer PHAR and run it with
php installer.php.InstallCommandorchestrates the rest. PromptManagercollects your choices through the prompts. The installer shows a summary of the answers, and in an interactive run, it asks Proceed with installing Vortex?- The installer downloads the template at the requested reference into a temporary directory.
PromptManagerruns the handlers inprocessWeight()order, lowest first. Each handler covers 1 aspect of the template, such as the CI provider, hosting, services, the theme, or the deployment types, and queues file operations based on the answers.PromptManagerapplies every queued file operation at once. Conditional tokens decide which content stays.- The installer copies the customized files to the destination directory and prepares the demo content.
- The installer runs the site build when you pass
--buildor answer yes to Run the site build now?, which only an interactive run asks.
The result is a fully customized Drupal project.
Conditional token system
The installer handles replacements and additions with string substitution. It also uses a token system to exclude whole blocks of content from template files. For an optional feature, you wrap the relevant content in token markers instead of writing logic that removes lines from configuration files, scripts, or documentation. When the feature isn't selected, the installer removes everything between the markers.
In a Markdown file, the markers are link reference comments:
[//]: # (#;< TOKEN_NAME)
Content removed if feature not selected
[//]: # (#;> TOKEN_NAME)
In a shell script, a YAML file, or another file that takes # comments,
the markers are comment lines:
#;< TOKEN_NAME
Content removed if feature not selected
#;> TOKEN_NAME
The installer matches any line that contains a marker and removes the whole
line, so a marker can also sit inside another comment syntax, such as
<!-- #;< TOOL_BEHAT --> in phpcs.xml. The match is a substring match: a
marker for a token that starts with another token's name, such as
TOOL_PHPUNIT_BEHAT for TOOL_PHPUNIT, also counts as a marker for the
shorter token.
A token with a ! prefix, such as #;< !PROVISION_TYPE_PROFILE, works the
other way round: the installer removes its content when the feature is
selected. Content between VORTEX_DEV markers always goes, because it
exists only for maintaining Vortex itself.
After the other handlers have run, the installer deletes every remaining
line that contains #;. That removes the markers of the selected features,
which keep their content. It also removes the internal comments that start
with #;, so a note that explains the template to its maintainers never
reaches a consumer site.
The installer also deletes every ##### prefix followed by a space. A line
that the template keeps commented out with that prefix becomes active in
the consumer site, as the VORTEX_DB_IMAGE example in .env does.
Testing
The installer has unit tests for individual components and functional tests for full installation scenarios.
Unit testing
Unit tests in tests/Unit/ check each component in isolation, with
PHPUnit's own mock objects standing in for collaborators such as the HTTP
client. They cover the following:
- The utilities in
src/Utils/, such asYaml,File,Git,Validator, andStrings - The downloaders, the runners, and the logger
- The task output and the prompt schema
- The prompt manager
- The discovery logic of the handlers
Functional testing with snapshots
Each scenario runs the installer to create a fresh project from the Vortex template with 1 combination of answers, then checks the resulting files. A single template change can affect more than 100 of these scenarios. Snapshot testing shows that impact as a set of diffs, so you can review a change across every scenario before it reaches a consumer site.
The scenarios live in functional test classes that extend
AbstractHandlerProcessTestCase, most of them dedicated to 1 handler or a
small group of related handlers. BaselineHandlerProcessTest covers the
default installation. A scenario that changes an answer passes it to the
installer through --prompts.
These tests use the
alexskrypnyk/snapshot library
for snapshot-based testing. The snapshot system works on a baseline + diff
pattern:
_baseline/contains the complete reference installation: a full set of template files as they would appear after a default installation.- Each scenario directory (for example,
services_no_clamav/,hosting_acquia/,deploy_types_all_circleci/) contains only the delta changes from the baseline. Its name matches the dataset name in the test class'sdataProviderHandlerProcess().
When a test runs, the snapshot library applies the scenario's diffs on top
of the baseline and compares the result against the actual installer output.
Before the comparison, the test replaces version numbers in the output with
__VERSION__, so a version bump alone doesn't change any fixture. Storing
only the differences, instead of a copy of the entire template for each
scenario, keeps the fixture files maintainable.
The fixture directories in
.vortex/installer/tests/Fixtures/handler_process/ include scenarios for
these areas, among many others:
- CI providers: GitHub Actions, CircleCI
- Hosting: Acquia, Lagoon
- Services: Solr, Redis, ClamAV (enabled/disabled combinations)
- Deployment: artifact, webhook, Lagoon, all combined
- Database sources: Acquia, Lagoon, FTP, S3, URL, container registry
Running tests
Run the installer tests from the .vortex/installer/ directory:
cd .vortex/installer
# Install Composer dependencies.
composer install
# Run all tests.
composer test
# Run only the handler tests: the functional process tests and the unit discovery tests.
./vendor/bin/phpunit --filter "Handlers\\\\"
# Run a specific handler test.
./vendor/bin/phpunit --filter "ServicesHandlerProcessTest"
composer test runs without code coverage. composer test-coverage runs
the same suite with coverage, the way CI does. CI runs it on PHP 8.3 and
8.4, after it checks that composer.json is normalized and runs
composer lint.
Updating snapshots
Never edit fixture files by hand. When the template or the installer logic
changes, regenerate the snapshots from the .vortex/ directory:
cd .vortex
ahoy update-snapshots
The regeneration runs the installer for every permutation, starting a fresh project for each scenario, and updates the fixture files to match the current output. It runs the baseline first and then the scenarios, 8 at a time, and it commits the regenerated installer fixtures as it goes. The changes land as diffs in version control, where you can review exactly how a template or installer change affects each scenario.
The same command refreshes the Docker Compose configuration fixtures in
.vortex/tests/phpunit/Fixtures/ first. That step doesn't commit, and when
one of those fixtures changes, the run stops with a failure before it
reaches the installer. Commit the updated fixtures and run the command
again.
Commit your template and installer changes before running
ahoy update-snapshots. The snapshots regenerate from the committed
baseline, so the run misses any uncommitted change. ahoy update-snapshots
is the only sanctioned way to regenerate fixtures: invoking the test suite
with UPDATE_SNAPSHOTS by hand bypasses part of the workflow and produces
partial, inconsistent fixtures.
Releasing
Box packages the installer as a PHAR
archive: composer build in .vortex/installer, or ahoy build-installer
from .vortex/, writes it to build/installer.phar. The
Vortex - Release docs workflow builds it on every tag and publishes it
with the documentation at the
installer download URL, so the
release process needs no separate installer step.
The workflow sets the version that the PHAR reports: the tag name for a tag
build, or the full commit SHA for a branch build. When you run the workflow
by hand, you can enter a version that the build reports instead. A local
build with composer build or ahoy build-installer always reports
development, whichever branch is checked out.
Each publish also refreshes the per-major pins at /v1/install and
/v2/install: the current major's installer comes from the release ref, and
the other major's binary comes from its {N}.x branch. The bare /install
serves a copy of the pin that VORTEX_CURRENT_MAJOR selects.
The workflow also publishes from every branch whose name contains
release-docs or release-installer, which lets you test installer
changes before a formal release. Those builds go to the live site, though,
so until the next publish, everyone who downloads /install gets that
branch's build.
Installer video
The documentation includes an interactive recording of the installer,
captured with asciinema. It's one of the 9
terminal demo videos that the PHP orchestrator at
.vortex/docs/.utils/update-videos.php produces with the shared
VideoRecorder class. For the installer video, the pipeline runs these
steps:
- Builds the installer PHAR from source.
- Generates an Expect script that answers the prompts. It types the site and organization names with human-like delays and accepts the default answer to every other prompt.
- Records the session with
asciinema recinto a JSON cast file. - Post-processes the recording: it drops the spawn line, replaces local paths with neutral ones, and masks tokens and other credentials.
- Decodes the cast to text at
.artifacts/videos/installer.txtand stops if the output reports an error, a warning, or a failure. - Renders the cast to an animated SVG with the custom
svg-term-render.jsscript, and 1 frame of it to the PNG poster withsharp-cli.
The pipeline writes 3 files to .vortex/docs/static/img/:
installer.json- the cast that the customAsciinemaPlayerReact component plays on the documentation pagesinstaller.png- the poster of the player on the site's home pageinstaller.svg- the animation that the repository'sREADME.mdembeds
To update the video:
cd .vortex
ahoy update-videos installer
Re-record the installer video whenever the prompt flow changes, because the
recording captures the live prompts. The command doesn't commit its output,
so review the diff under .vortex/docs/static/img/ and commit manually.
sharp-cli, which renders the PNG posters, is a declared dependency of
.vortex/docs, so yarn.lock pins it and the existing
yarn install --frozen-lockfile installs it.
asciinema is downloaded rather than installed from a lockfile, so the
workflows that regenerate the video in CI pin it explicitly.
.github/workflows/vortex-release.yml and
.github/workflows/vortex-test-installer.yml each set ASCIINEMA_VERSION
and ASCIINEMA_SHA256, and they check the download against that checksum
before running it. Bump both workflows together, so the release and the
installer test always generate assets with identical tooling. Neither
workflow publishes the video it records: the step only proves that
recording still works.