Skip to main content
Version: v2

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.sh bootstrap
  • 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:

  1. MUST adhere to the POSIX standard.

  2. MUST pass the ShellCheck analysis and the shfmt formatting check, which ahoy lint-scripts runs from .vortex/.

  3. 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
  4. MUST list all variables with their default values and descriptions. For example:

    # Notification git commit SHA.
    VORTEX_NOTIFY_SHA="${VORTEX_NOTIFY_SHA:-}"
  5. MUST include a delimiter between variables and the script body preceded and followed by an empty line (3 lines in total):

    # ------------------------------------------------------------------------------
  6. SHOULD include formatting helper functions. info opens and closes an operation, task announces work that's starting, and note is a standalone remark that starts no task. fail prints the message and exits the script with status 1, 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: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

    Keep the # @formatter:off and # @formatter:on markers: ahoy lint-scripts hides the lines between them from shfmt, so each helper stays on 1 line.

  7. SHOULD include variable values checks with errors and early exit, for example:

    [ -z "${VORTEX_NOTIFY_SHA}" ] && fail "Missing required value for VORTEX_NOTIFY_SHA."
  8. SHOULD include binaries checks if the script relies on them, for example:

    command -v curl > /dev/null || fail "curl command is not available."
  9. MUST contain an info message about the start of the script body, for example:

    info "Started GitHub notification for ${VORTEX_NOTIFY_GITHUB_EVENT} event."
  10. MUST contain a pass message about the finish of the script body, for example:

    pass "Finished GitHub notification for ${VORTEX_NOTIFY_GITHUB_EVENT} event."
  11. MUST close every task message with exactly 1 pass or fail message, 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 || true
    pass "Disabled Search API Solr server."
  12. MUST NOT open a task message while another one is open. A step that runs inside an open task, including each iteration of a loop, uses a note message.

  13. MAY use a pass or a fail message on its own. A check reports pass when it holds and fail when it doesn't, and an operation that's skipped or finished reports pass without announcing a task first.

  14. MUST use uppercase global variables.

  15. MUST use lowercase local variables.

  16. MUST use long options instead of short options for readability. For example, drush cache:rebuild instead of drush cr.

  17. MUST use VORTEX_ prefix for variables, unless it's a known third-party variable such as PACKAGE_TOKEN or COMPOSER.

  18. MUST use a script-specific prefix. For example, the variable that skips notifications in vortex-notify is VORTEX_NOTIFY_SKIP.

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

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

  21. SHOULD use note messages to report the script's progress.

  22. MUST use variables in the form of ${VAR}.

Variables​

When you create or update Vortex variables, follow these guidelines:

  1. Local variables MUST be in lowercase, and global variables MUST be in uppercase.

  2. All Vortex variables MUST start with VORTEX_ to separate Vortex from third-party variables.

  3. Global variables MAY be re-used as-is across scripts. For example, several scripts use the WEBROOT variable.

  4. Vortex action-specific script variables MUST be scoped within their own script. For example, the VORTEX_PROVISION_OVERRIDE_DB variable belongs to the vortex-provision script.

  5. 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 is DRUPAL_SHIELD_USER.

  6. Variables SHOULD NOT be exported into the global scope unless absolutely necessary. So values in .env SHOULD 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 private drevops/test-private-package repository. The private package build test fails without it.
  • TEST_VORTEX_CONTAINER_REGISTRY_USER and TEST_VORTEX_CONTAINER_REGISTRY_PASS - container registry credentials. The tests pass them to every command they run as VORTEX_CONTAINER_REGISTRY_USER and VORTEX_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 as GITHUB_TOKEN, so the builds stay clear of API rate limits.
  • TEST_VORTEX_DEBUG - set to 1 for debug output.
  • TEST_CIRCLECI_TOKEN - a CircleCI API token for the post-build checks in PostBuildTest. 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:

ModeWhat it doesFollow-up
demo-dumpBuilds 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-imageBuilds the demo site in place from the standard profile; pushes drevops/vortex-dev-mariadb-drupal-data-demo-11.x:<tag>.None.
test-dumpInstalls 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-imageInstalls Vortex into /tmp/star-wars with the CLI; builds; pushes drevops/vortex-dev-mariadb-drupal-data-test-11.x:<tag>.None.
destination-imagesPulls 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.
allOptimized 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:

OptionDescription
--tag <tag>Image tag for image modes. Default: latest. Also accepts --tag=<tag>.
-h, --helpPrints the usage and exits.

Requirements​

The script needs the following:

  • The docker, ahoy, curl, sed, and php commands on your PATH.
  • A Docker Hub session with push permission to drevops/ for any image mode.
  • The demo-image, test-image, and all modes download seed.sh from a pinned drevops/mariadb-drupal-data release and verify it against a recorded SHA-256 checksum. Adopting a newer release is a manual step: update SEED_URL and SEED_SHA256 together.
  • Those modes build each image for linux/amd64 and linux/arm64 through a Docker Buildx builder named vortex-seed, using the docker-container driver. 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.sql before the rebuild.
  • Test modes empty /tmp/star-wars and reinstall Vortex into it on each run. A throwaway root container clears the old files, because files the containers wrote there can belong to their www-data user.
  • destination-images pulls drevops/vortex-dev-mariadb-drupal-data-demo-11.x:<tag> from Docker Hub, so publish that image first with demo-image (or all).

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