Template
This page is for maintainers changing the Vortex template itself. It sets the rules for the shell scripts the template ships and explains how the template gets tested at 3 levels. It also shows how to run those tests and refresh the test assets they rely on.
Authoring scripts
The shipped tooling scripts are PHP and follow their own conventions; see Tooling. These requirements apply to the shell scripts in the template:
- The
scripts/vortex-tooling.shbootstrap - The Acquia hooks in
hooks/library/ - The scripts in
.circleci/ - The maintenance scripts in
.vortex/docs/
From .vortex/, ahoy lint-scripts runs .vortex/tests/lint.scripts.sh
to enforce the ShellCheck and shfmt requirements. The script lints the
.sh, .bash, and .bats files under scripts/, .circleci/,
hooks/library/, and .vortex/docs/, skipping any vendor,
node_modules, or fixtures path. For each file, it runs shellcheck and
then shfmt -i 2 -ci -s -d, and it stops at the first file that fails.
Both commands must be on your PATH.
Requirements
The requirements use MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY
with the meanings that RFC 2119
defines:
-
MUST adhere to the POSIX standard.
-
MUST pass the ShellCheck analysis and the
shfmtformatting check, whichahoy lint-scriptsruns from.vortex/. -
MUST start with:
#!/usr/bin/env bash### Action description that the script performs.## More description and usage information with a last empty# comment line.#set -eu[ "${VORTEX_DEBUG-}" = "1" ] && set -x -
MUST list all variables with their default values and descriptions. For example:
# Notification git commit SHA.VORTEX_NOTIFY_SHA="${VORTEX_NOTIFY_SHA:-}" -
MUST include a delimiter between variables and the script body preceded and followed by an empty line (3 lines in total):
# ------------------------------------------------------------------------------ -
SHOULD include formatting helper functions.
infoopens and closes an operation,taskannounces work that's starting, andnoteis a standalone remark that starts no task.failprints the message and exits the script with status1, or with the status passed as its second argument. It's always the last statement of a failure branch, and anything that explains the failure runs before it:# @formatter:offinfo() { [ "${TERM:-}" != "dumb" ] && tput colors >/dev/null 2>&1 && printf "\033[36m[INFO] %s\033[0m\n" "${1}" || printf "[INFO] %s\n" "${1}"; }note() { printf " %s\n" "${1}"; }task() { [ "${TERM:-}" != "dumb" ] && tput colors >/dev/null 2>&1 && printf "\033[34m[TASK] %s\033[0m\n" "${1}" || printf "[TASK] %s\n" "${1}"; }pass() { [ "${TERM:-}" != "dumb" ] && tput colors >/dev/null 2>&1 && printf "\033[32m[ OK ] %s\033[0m\n" "${1}" || printf "[ OK ] %s\n" "${1}"; }fail() { [ "${TERM:-}" != "dumb" ] && tput colors >/dev/null 2>&1 && printf "\033[31m[FAIL] %s\033[0m\n" "${1}" || printf "[FAIL] %s\n" "${1}"; exit "${2:-1}"; }# @formatter:onKeep the
# @formatter:offand# @formatter:onmarkers:ahoy lint-scriptshides the lines between them fromshfmt, so each helper stays on 1 line. -
SHOULD include variable values checks with errors and early exit, for example:
[ -z "${VORTEX_NOTIFY_SHA}" ] && fail "Missing required value for VORTEX_NOTIFY_SHA." -
SHOULD include binaries checks if the script relies on them, for example:
command -v curl > /dev/null || fail "curl command is not available." -
MUST contain an
infomessage about the start of the script body, for example:info "Started GitHub notification for ${VORTEX_NOTIFY_GITHUB_EVENT} event." -
MUST contain a
passmessage about the finish of the script body, for example:pass "Finished GitHub notification for ${VORTEX_NOTIFY_GITHUB_EVENT} event." -
MUST close every
taskmessage with exactly 1passorfailmessage, on every path through the script, even when the work itself can't fail. For example:task "Disabling Search API Solr server."drush search-api:server-disable solr || truepass "Disabled Search API Solr server." -
MUST NOT open a
taskmessage while another one is open. A step that runs inside an open task, including each iteration of a loop, uses anotemessage. -
MAY use a
passor afailmessage on its own. A check reportspasswhen it holds andfailwhen it doesn't, and an operation that's skipped or finished reportspasswithout announcing ataskfirst. -
MUST use uppercase global variables.
-
MUST use lowercase local variables.
-
MUST use long options instead of short options for readability. For example,
drush cache:rebuildinstead ofdrush cr. -
MUST use
VORTEX_prefix for variables, unless it's a known third-party variable such asPACKAGE_TOKENorCOMPOSER. -
MUST use a script-specific prefix. For example, the variable that skips notifications in
vortex-notifyisVORTEX_NOTIFY_SKIP. -
MAY rely on variables from other scripts (not prefixed with a script-specific prefix), but MUST declare such variables in the header of the file.
-
MAY call other Vortex scripts (discouraged), but MUST source them rather than creating a sub-process. Sourcing runs the called script in the current shell, so both scripts share the same variables.
-
SHOULD use
notemessages to report the script's progress. -
MUST use variables in the form of
${VAR}.
Variables
When you create or update Vortex variables, follow these guidelines:
-
Local variables MUST be in lowercase, and global variables MUST be in uppercase.
-
All Vortex variables MUST start with
VORTEX_to separate Vortex from third-party variables. -
Global variables MAY be re-used as-is across scripts. For example, several scripts use the
WEBROOTvariable. -
Vortex action-specific script variables MUST be scoped within their own script. For example, the
VORTEX_PROVISION_OVERRIDE_DBvariable belongs to thevortex-provisionscript. -
Drupal-related variables SHOULD start with
DRUPAL_and SHOULD have a module name added as a second prefix. The prefixes keep Vortex variables, third-party service variables, and Drupal variables apart. For example, the user for the Drupal Shield module's configuration isDRUPAL_SHIELD_USER. -
Variables SHOULD NOT be exported into the global scope unless absolutely necessary. So values in
.envSHOULD have default values set, but SHOULD be commented out, which keeps them visible without exposing them to the global scope.
Boilerplate script
The boilerplate puts these requirements together in 1 working script. Copy it as the starting point for a new script:
Expand to see the boilerplate script
#!/usr/bin/env bash
##
# Action description that the script performs.
#
# More description and usage information with a last empty
# comment line.
#
set -eu
[ "${VORTEX_DEBUG-}" = "1" ] && set -x
# Example Vortex variable with a default value.
VORTEX_EXAMPLE_URL="${VORTEX_EXAMPLE_URL:-http://example.com}"
# ------------------------------------------------------------------------------
# @formatter:off
info() { [ "${TERM:-}" != "dumb" ] && tput colors >/dev/null 2>&1 && printf "\033[36m[INFO] %s\033[0m\n" "${1}" || printf "[INFO] %s\n" "${1}"; }
note() { printf " %s\n" "${1}"; }
task() { [ "${TERM:-}" != "dumb" ] && tput colors >/dev/null 2>&1 && printf "\033[34m[TASK] %s\033[0m\n" "${1}" || printf "[TASK] %s\n" "${1}"; }
pass() { [ "${TERM:-}" != "dumb" ] && tput colors >/dev/null 2>&1 && printf "\033[32m[ OK ] %s\033[0m\n" "${1}" || printf "[ OK ] %s\n" "${1}"; }
fail() { [ "${TERM:-}" != "dumb" ] && tput colors >/dev/null 2>&1 && printf "\033[31m[FAIL] %s\033[0m\n" "${1}" || printf "[FAIL] %s\n" "${1}"; exit "${2:-1}"; }
# @formatter:on
info "Started Vortex operations."
[ -z "${VORTEX_EXAMPLE_URL}" ] && fail "Missing required value for VORTEX_EXAMPLE_URL."
command -v curl >/dev/null || fail "curl command is not available."
# Example of the script body. Every task is closed by a pass or a fail. The
# assignment is guarded so a curl transport error reports through fail instead
# of aborting the script via 'set -e' before fail can run.
task "Requesting example page."
if ! status="$(curl -L -s -o /dev/null -w "%{http_code}" "${VORTEX_EXAMPLE_URL}")"; then
fail "Unable to reach ${VORTEX_EXAMPLE_URL}."
fi
echo "${status}" | grep -q '200\|403' || fail "Example page returned status ${status}."
pass "Requested example page."
pass "Finished Vortex operations."
Testing
Vortex is a template, so a change to 1 script, configuration file, or workflow reaches every project built from it. To catch problems before they do, Vortex tests at 3 levels of integration. Unit tests check the tooling scripts, end-to-end tests run whole workflows, and a real site takes every update.
Unit testing
PHPUnit unit tests cover the scripts shipped in the
drevops/vortex-tooling
Composer package, which consumer sites install at
vendor/drevops/vortex-tooling/src/.
Each test runs 1 script in isolation, with mocks standing in for external
commands such as drush, docker, and composer. That way, the tests can
check how a script handles environment variables, flags, and edge cases
without a running Drupal site or Docker containers.
Because no test starts a container, the unit tests run in seconds. The test
files in .vortex/tooling/tests/Unit/ cover deployment, database
operations, notifications, provisioning, and the other automation scripts.
See Tooling > Testing for how to run them.
End-to-end testing
PHPUnit also runs the functional end-to-end tests, which drive the full site build pipeline in real Docker containers. They do what a developer on a consumer site does: build containers, import databases, run Drupal operations, lint code, and deploy artifacts.
The template end-to-end tests cover these workflows:
- Docker Compose - building the container stack, verifying environment variables, running linters, executing PHPUnit and Behat tests inside containers, and checking Solr integration.
- Ahoy workflows - testing the full developer experience through Ahoy commands, including build, login, Drush operations, Composer, database import/export, and code quality checks.
- Deployment - testing deployment pipelines for artifact and webhook-based deployment strategies.
Smaller suites cover the following:
- The CLI: updating an existing project from the latest release or from a git reference
- The optional local Ahoy file,
.ahoy.local.yml - The Docker Compose configuration
- Where Composer installs Drupal libraries
- The artifacts and test results that a CircleCI build stores, checked only on CircleCI
The phpunit-helpers
library provides test helpers, traits, and assertions purpose-built for
running shell commands and checking their output in PHPUnit tests.
End-to-end tests take minutes to run because they operate on real containers and a real Drupal site.
Example site
The DrevOps website is a real-world production site built using Vortex, and it's the final validation ground. If Vortex works correctly, the website keeps building, testing, and deploying without issues after each upstream update.
The website repository receives regular upstream updates from Vortex, including CI configuration, testing workflows, and infrastructure changes. That way, each update meets a real, long-lived codebase as well as the test environments.
Running tests
The template has 2 suites to run locally: the functional tests in
.vortex/tests/ and the tooling unit tests in .vortex/tooling/tests/.
Functional tests with PHPUnit
Run the functional tests from the .vortex/tests/ directory:
cd .vortex/tests
# Install Composer dependencies.
composer install
# Run all tests.
composer test
# Some tests require Composer and container registry tokens.
TEST_PACKAGE_TOKEN=<yourtoken> TEST_VORTEX_CONTAINER_REGISTRY_USER=<youruser> TEST_VORTEX_CONTAINER_REGISTRY_PASS=<yourpass> composer test
The Vortex - Test workflow splits the suite into 6 parallel batches by
PHPUnit group, p0 to p5, so give every new test method a
#[Group('pN')] attribute. Outside those groups, a test runs in CI only in
the postbuild group, which PostBuildTest declares and a CircleCI job
runs after the test job. To run 1 batch locally, pass its group:
composer test -- --group=p1.
The suite reads these environment variables:
TEST_PACKAGE_TOKEN- a GitHub token with access to the privatedrevops/test-private-packagerepository. The private package build test fails without it.TEST_VORTEX_CONTAINER_REGISTRY_USERandTEST_VORTEX_CONTAINER_REGISTRY_PASS- container registry credentials. The tests pass them to every command they run asVORTEX_CONTAINER_REGISTRY_USERandVORTEX_CONTAINER_REGISTRY_PASS. Without them, both are empty, so Vortex skips the registry login.TEST_GITHUB_TOKEN- a GitHub token that the tests pass to every command asGITHUB_TOKEN, so the builds stay clear of API rate limits.TEST_VORTEX_DEBUG- set to1for debug output.TEST_CIRCLECI_TOKEN- a CircleCI API token for the post-build checks inPostBuildTest. The test skips itself outside CircleCI, so you need the token only on CircleCI, where the test fails without it.
Functional tests rely on database fixtures; see Updating test assets.
Unit tests for the tooling scripts
PHPUnit unit tests for the shipped scripts live in
.vortex/tooling/tests/Unit/. Run them from .vortex/:
cd .vortex
# Run all tooling tests.
ahoy test-tooling
# Run a single test class.
ahoy test-tooling -- --filter=DeployLagoonTest
➡️ See Tooling > Testing for the package's full test layers.
Updating test assets
The test assets are demo and test database dumps, published both as
files and as container images. The .vortex/tests/update-test-assets
script automates every flow and prints each shell command it runs. Run it
from the repository root:
php .vortex/tests/update-test-assets [mode] [--tag <tag>]
Without arguments, it runs all, a full refresh. The default tag is
latest. A successful run ends with ==> Done. When a command fails, the
script prints Error: command failed with exit code <n>. and stops the
run.
Modes
The script has these modes:
| Mode | What it does | Follow-up |
|---|---|---|
demo-dump | Builds the demo site in place from the standard profile; exports .data/db.demo.sql. | Upload as db.demo.sql to the latest release of drevops/vortex on GitHub. |
demo-image | Builds the demo site in place from the standard profile; pushes drevops/vortex-dev-mariadb-drupal-data-demo-11.x:<tag>. | None. |
test-dump | Installs Vortex into /tmp/star-wars with the CLI; builds; exports /tmp/star-wars/.data/db.test.sql. | Upload as db_d11.test.sql to the latest release of drevops/vortex on GitHub. |
test-image | Installs Vortex into /tmp/star-wars with the CLI; builds; pushes drevops/vortex-dev-mariadb-drupal-data-test-11.x:<tag>. | None. |
destination-images | Pulls drevops/vortex-dev-mariadb-drupal-data-demo-11.x:<tag> and pushes it to drevops/vortex-dev-mariadb-drupal-data-demo-destination-11.x under the didi tags vortex-dev-database-ii and vortex-dev-didi-database-fi. | None. |
all | Optimized full refresh: builds the demo stack once for both demo modes and the test stack once for both test modes (2 stack builds instead of 4). Then pushes images and tags destination images. Default when no mode is given. | Upload both dump files, as in the demo-dump and test-dump rows. |
Options
The script accepts these options:
| Option | Description |
|---|---|
--tag <tag> | Image tag for image modes. Default: latest. Also accepts --tag=<tag>. |
-h, --help | Prints the usage and exits. |
Requirements
The script needs the following:
- The
docker,ahoy,curl,sed, andphpcommands on yourPATH. - A Docker Hub session with push permission to
drevops/for any image mode. - The
demo-image,test-image, andallmodes downloadseed.shfrom a pinneddrevops/mariadb-drupal-datarelease and verify it against a recorded SHA-256 checksum. Adopting a newer release is a manual step: updateSEED_URLandSEED_SHA256together. - Those modes build each image for
linux/amd64andlinux/arm64through a Docker Buildx builder namedvortex-seed, using thedocker-containerdriver. The script creates the builder on first use.
Caveats
The script has these side effects:
- Demo modes are destructive against the current repository: the script
resets the containers and removes
.data/db.sqlbefore the rebuild. - Test modes empty
/tmp/star-warsand reinstall Vortex into it on each run. A throwaway root container clears the old files, because files the containers wrote there can belong to theirwww-datauser. destination-imagespullsdrevops/vortex-dev-mariadb-drupal-data-demo-11.x:<tag>from Docker Hub, so publish that image first withdemo-image(orall).
Examples
The following commands show typical runs:
# Full refresh of every asset at :latest (default).
php .vortex/tests/update-test-assets
# Full refresh, tagging images as :rc1.
php .vortex/tests/update-test-assets --tag rc1
# Refresh only the test container image at :latest.
php .vortex/tests/update-test-assets test-image
# Refresh only the test container image at :rc1.
php .vortex/tests/update-test-assets test-image --tag rc1