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
These requirements apply to all shell scripts in the template:
- The shipped tooling scripts (see Tooling)
- The scripts in
scripts/, including the provision subscripts - The Acquia hooks in
hooks/library/ - The scripts in
.circleci/
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/, .vortex/docs/, .vortex/tooling/tests/, and
.vortex/tooling/playground/, skipping any vendor, node_modules, or
fixtures path. It also lints every file in .vortex/tooling/src/. 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 -xScripts in
.vortex/tooling/src/also carry a line between the comment block andset -euthat loads.envand.env.localwithout overriding real environment variables. Copy it verbatim from any script in that directory, along with the ShellCheck directive that disablesSC1090andSC1091for it. -
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:onEvery
taskMUST be closed by apassor afail. A task announces work that's starting, so it always reports its outcome, even when the work itself can't fail.Keep the
# @formatter:offand# @formatter:onmarkers:ahoy lint-scriptshides the lines between them fromshfmt, so each helper stays on 1 line. The scripts in.vortex/tooling/src/extendtaskandpasswith a timer that appends the seconds a task took. Thescripts/provision-*.shsubscripts print indented==>,>,+, and!prefixes instead of the[INFO],[TASK],[ OK ], and[FAIL]labels. Copy the helpers from a script of the same kind. -
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, 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 scripts, end-to-end tests run whole workflows, and a real site takes every update.
Unit testing
BATS unit tests cover these shell scripts:
- The scripts shipped in the
drevops/vortex-toolingComposer package, which consumer sites install atvendor/drevops/vortex-tooling/src/ - The
scripts/provision-*.shsubscripts - The Acquia hooks in
hooks/library/ .circleci/post-coverage-comment.sh
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.
The bats-helpers library
provides a step-based testing approach with built-in mocking and assertions,
so you define the expected inputs and outputs for each script.
Because no test starts a container, the unit tests run much faster than the
end-to-end tests. 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 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 installer: installing from the latest release or from a git reference, and updating an existing project
- The
scripts/vortex-tooling.shbootstrap that runs before everyahoycommand - 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 PHPUnit tests in
.vortex/tests/ and the BATS tests in .vortex/tooling/tests/.
Functional tests with PHPUnit
Run the PHPUnit 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
composer test runs 2 suites, and you can run either one on its own:
composer test-unitcovers the PHPUnit traits that the template ships to consumer sites inweb/modules/custom/ys_base/tests/src/Traits/, with code coverage.composer test-functionalruns the end-to-end tests, without coverage.
The Vortex - Test workflow splits the functional 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 build job. To run 1 batch locally, pass its group:
composer test-functional -- --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, which the builds use to pull public images past the Docker Hub rate limit and to push images. 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. Without it,GITHUB_TOKENis empty and GitHub requests go out without a token.TEST_VORTEX_DEBUG- set to1for debug output.TEST_CIRCLECI_TOKEN- a CircleCI API token for the post-build checks inPostBuildTest, the only test that reads it. 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 with BATS
The BATS unit tests live in .vortex/tooling/tests/unit/. Most test files
are named after the script they cover, without its vortex- prefix, such
as deploy.bats for vortex-deploy. Run them from .vortex/:
cd .vortex
# Run all BATS tests.
ahoy test-bats tooling/tests/unit
# Run a single test file.
ahoy test-bats tooling/tests/unit/deploy.bats
Each test runs the scripts from a copy of the repository at its last commit,
so commit your changes before you run the tests. The exception is
update-vortex.bats, which runs vortex-update from your working tree.
➡️ 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; 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; 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