Continuous Integration
Vortex offers continuous integration configurations for GitHub Actions and CircleCI providers that allow you to automate the process of building, testing, and deploying your site.
The workflow configuration is identical for both continuous integration providers. You would need to choose one of them and follow the setup instructions.
The continuous integration pipeline consists of multiple jobs executed in a
drevops/ci-runner container to ensure
consistency across runs.
Workflow structure
Local Development
═════════════════════════════════════════════════════════════════════════════════════════
Developer writes code ──► Build and test locally ──► Commit changes
│
▼
Git Repository
═════════════════════════════════════════════════════════════════════════════════════════
Push to remote branch ──► Open/Update Pull Request
│
▼
┌─ CI Pipeline ───────────────────── ─────────────────────────────────────────────────────────┐
│ │
│ The Lint, Test and Build jobs run in parallel. Deployment runs after all three pass. │
│ │
│ Lint Job │
│ Build CLI container ──► Composer validate / audit / normalize ──► Hadolint ──► │
│ DCLint ──► PHPCS ──► PHPStan ──► Rector ──► Twig CS Fixer ──► │
│ Gherkin Lint ──► ESLint / Stylelint │
│ │
│ Test Job │
│ Restore the database cache. On the first build of the day (cache miss), each runner │
│ downloads the production database and exports a clean pre-provision dump; only the │
│ primary runner stores it in the cache for the rest of the day's builds. │
│ ▼ │
│ Build Docker ──► Composer deps ──► NPM deps ──► Assets │
│ ▼ │
│ Import cached database ──► drush deploy ──► custom deploy steps │
│ ▼ │
│ PHPUnit tests ──► Behat tests │
│ │
│ Build Job │
│ Assemble and validate the deployable artifact │
│ │
│ Deployment Job (after Lint, Test and Build pass) │
│ Webhook URL ──► artifact package ──► Lagoon webhook ──► Docker container image │
│ │
└────────────────────────────────────────────────────────────────────────────────────────────┘
│
▼
Hosting Platform
═════════════════════════════════════════════════════════════════════════════════════════
◆ Environment ──No──► Sync DB from production ───┐
exists? │
│ Yes ▼
└──────────────────────────────────► drush deploy ──► Custom scripts ──► Notifications
Available Environments
═════════════════════════════════════════════════════════════════════════════════════════
┊ PR Environment ┊ Dev Staging Production
┊ (auto-removed) ┊ develop branch main branch production branch or tag
1. Lint
- Runs in parallel with other jobs (no dependencies)
- Builds only the CLI container (no database or other services needed)
- Validates Composer configuration
- Lints Dockerfiles and Docker Compose files
- Installs development dependencies
- Runs all code linters: PHPCS, PHPStan, Rector, Twig CS Fixer, Gherkin Lint, ESLint, Stylelint
- Audits and normalizes Composer packages
2. Test
- Runs in parallel with the
lintandbuildjobs - Downloads the database on the first build of the day and caches it for the rest of the day's builds (only one runner stores the cache when running in parallel)
- Uses Docker Compose to set up the full environment
- Validates Composer configuration
- Assembles the codebase by installing dependencies
- Provisions a website
- Runs PHPUnit tests (first instance only)
- Checks code coverage and posts PR comment (first instance only)
- Runs BDD tests (distributed across all instances)
- Collects and stores test results and artifacts
3. Build
- Runs in parallel with the
lintandtestjobs (no dependencies) - Uses Docker Compose to build the production stack
- For artifact-based hosting (e.g. Acquia), exports the built codebase for the deployment job
- For image-based hosting (e.g. Lagoon), a successful build confirms that the production images are deployable
4. Deployment
- Runs after successful completion of the
lint,test, andbuildjobs - Uses the built codebase without development dependencies from the
buildjob - Adds required secrets and environment variables
- Triggers a deployment using a router script
Caching strategy
The database is downloaded on the first continuous integration run of the day and cached so that the remaining runs on the same day reuse the cached database dump.
By default, the database is cached per-branch for 24 hours. If cache is not available, the fallback default branch is used.
Database caching is a very powerful feature that allows to speed up the continuous integration runs on large projects with a lot of data.
In case of a project with a large database >1GB, the database import itself may take a long time, so it may be worth looking into either packaging the database dump into a container image or using a sanitized database dump with only the required data for the tests.
Vortex supports both creating and using a database container image with embedded data. You may use MariaDB data container for Drupal with database captured as Docker layers to create an initial database image.
There are other tools also available for this purpose, such as Drush GDPR Dumper that allows to remove data from the database dump during Drush database export command without the need for an intermediate database import step.
Reset the cache
If you need to force a fresh cache (e.g., to pull a new database dump before the daily cache refreshes), increment the version tag in the cache keys:
# Before
v25.11.0-db11
# After
v25.11.1-db11
Trigger conditions
The continuous integration pipeline is triggered by:
- Push events to the following branches:
production,main,master,developfeature/*,bugfix/*release/*,hotfix/*(semantic version or date-based, e.g.,release/1.2.3,hotfix/2023-04-17)project/*ci*
- Pull requests to these branches
- Tags matching semantic version (
1.2.3,1.2.3-rc.1) or date-based (2023-04-17) patterns
Test parallelism
The test job runs across multiple parallel containers (2 by default) to speed up test execution. Since each container runs the full provision and test steps, the test workload is distributed to make the best use of each container.
Code linting runs in a separate lint job and is not affected by test
parallelism settings.
What runs where
| Task | First container | Other containers |
|---|---|---|
| PHPUnit tests | ✓ | — |
| Code coverage check and PR comment | ✓ | — |
| Behat tests | ✓ (profile p0) | ✓ (profile p1, p2, ...) |
PHPUnit and coverage reporting run exclusively on the first container to avoid duplicate work. Behat tests run on all containers using profile-based distribution.
Balancing Behat tests
Because the first container handles PHPUnit and coverage in addition to Behat tests, it has more work to do than the other containers. To keep overall build time low, assign more Behat scenarios to the non-first containers.
Behat scenarios are assigned to containers using profile tags. Tag a scenario
with @p0 to run it on the first container or @p1 to run it on the second:
@p0
Scenario: Quick smoke test
Given I go to the homepage
Then I should see "Welcome"
@p1
Scenario: Full content workflow
Given I am logged in as a content editor
...
Scenarios without a profile tag default to the first container. When only one container is available, all scenarios run regardless of tags.
As a rule of thumb, keep lightweight or smoke-test scenarios on the first
container (@p0) and move heavier or more numerous scenarios to additional
containers (@p1, @p2, etc.). This keeps the total build time closer to the
duration of the longest single container rather than the sum of all tests.
See the provider-specific pages for how to change the number of parallel containers:
Maintenance
Enable debug mode
To get verbose output when troubleshooting build failures, enable debug mode
by setting the VORTEX_DEBUG variable to 1 in your CI provider's settings.
Update CI runner image
The CI jobs run inside the drevops/ci-runner
container - a Docker image specifically designed for CI job execution. It
provides a consistent, reproducible environment with 25+ pre-installed tools:
- PHP & Node.js - PHP 8.4, Node.js, Composer, npm, Yarn
- Docker tools - Docker, Docker Compose, Docker Buildx
- Code quality - ShellCheck, shfmt, Bats testing framework
- Utilities - Git, curl, rsync, jq, and more
Using this image ensures all CI runs have identical tooling, eliminating environment inconsistencies between local development and CI. It also massively speeds up builds by avoiding repetitive installation of common tools.
To update to a newer version, change the image tag in your CI configuration
file. The image follows CalVer versioning (e.g., 26.2.0) with
monthly releases.
Ignore tool failures
Sometimes you may want to allow builds to pass despite linter and test failures.
Set the corresponding VORTEX_CI_*_IGNORE_FAILURE variable to 1 to ignore
failures (but still run the tool and see the results in the logs):
| Tool | Purpose | Variable |
|---|---|---|
| Behat | Run BDD acceptance tests | VORTEX_CI_BEHAT_IGNORE_FAILURE |
| Composer normalize | Ensure composer.json is sorted | VORTEX_CI_COMPOSER_NORMALIZE_IGNORE_FAILURE |
| Composer security audit | Check dependencies for vulnerabilities | VORTEX_CI_COMPOSER_AUDIT_IGNORE_FAILURE |
| Composer validate | Validate composer.json and lock file | VORTEX_CI_COMPOSER_VALIDATE_IGNORE_FAILURE |
| DCLint | Lint Docker Compose files | VORTEX_CI_DCLINT_IGNORE_FAILURE |
| ESLint | Run ESLint and Stylelint | VORTEX_CI_NODEJS_LINT_IGNORE_FAILURE |
| Gherkin Lint | Lint Behat feature files | VORTEX_CI_GHERKIN_LINT_IGNORE_FAILURE |
| Hadolint | Lint Dockerfiles for best practices | VORTEX_CI_HADOLINT_IGNORE_FAILURE |
| PHPCS | Check PHP coding standards | VORTEX_CI_PHPCS_IGNORE_FAILURE |
| PHPStan | Static analysis for PHP | VORTEX_CI_PHPSTAN_IGNORE_FAILURE |
| PHPUnit | Run unit, kernel, and functional tests | VORTEX_CI_PHPUNIT_IGNORE_FAILURE |
| Rector | Check for automated refactoring rules | VORTEX_CI_RECTOR_IGNORE_FAILURE |
| Twig CS Fixer | Lint Twig templates | VORTEX_CI_TWIG_CS_FIXER_IGNORE_FAILURE |
Configure deployment skip conditions
Sometimes it may be necessary to skip deployments for specific branches or pull requests. For example, you may want to temporarily avoid deploying more changes into already deployed environments which still run CI checks on new commits.
To skip deployments for specific branches, set the VORTEX_DEPLOY_SKIP_BRANCHES
variable to a comma-separated list of exact branch names:
VORTEX_DEPLOY_ALLOW_SKIP=1 # Enable deployment skipping
VORTEX_DEPLOY_SKIP_BRANCHES="feature/test,hotfix/urgent,project/experimental"
To skip deployments for specific pull requests, set the VORTEX_DEPLOY_SKIP_PRS
variable to a comma-separated list of PR numbers:
VORTEX_DEPLOY_ALLOW_SKIP=1 # Enable deployment skipping
VORTEX_DEPLOY_SKIP_PRS="123,456,789"
To skip all deployments entirely, set VORTEX_DEPLOY_SKIP=1.