Skip to main content

Continuous integration

Vortex offers continuous integration configurations for GitHub Actions and CircleCI that automate the process of building, testing, and deploying your site.

The workflow structure is identical for both continuous integration providers. Choose one of them and follow its setup instructions.

The continuous integration pipeline consists of multiple jobs executed in a drevops/ci-runner container to ensure consistency across runs. Each job installs the drevops/vortex-tooling scripts via scripts/vortex-tooling.sh, so the vendor/bin/vortex-* commands are available before the codebase is assembled.

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 ────────────────────────────────────────────────────────────────────────────┐
│ │
│ ┌─ Database Job (Nightly) ───────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Scheduled ──► Download production ──► Sanitize database ──► Store database cache │ │
│ │ trigger database Remove sensitive data │ │
│ │ │ │
│ └─────────────────────────────────────┬──────────────────────────────────────────────┘ │
│ │ Provides cached database │
│ ▼ │
│ ┌─ Lint Job ────────────┐ ┌─ Database Job ────────────────┐ ┌─ Audit Job ──────────┐ │
│ │ │ │ ◆ Nightly cache exists? │ │ Composer audit │ │
│ │ Build CLI container │ │ Yes ──► job succeeds │ │ (advisories) │ │
│ │ ▼ │ │ No ──► download, sanitize, │ │ ▼ │ │
│ │ Composer validate │ │ store cache │ │ Gitleaks │ │
│ │ ▼ │ └───────────────┬───────────────┘ │ (committed secrets) │ │
│ │ Composer normalize │ ▼ │ │ │
│ │ ▼ │ ┌─ Build Job ───────────────────┐ │ │ │
│ │ Hadolint │ │ Code assembly │ │ │ │
│ │ ▼ │ │ Docker, Composer deps, │ │ │ │
│ │ DCLint │ │ NPM deps, assets │ │ │ │
│ │ ▼ │ │ ▼ │ │ │ │
│ │ PHPCS │ │ Website setup │ │ │ │
│ │ ▼ │ │ Import cached DB, drush │ │ │ │
│ │ PHPStan │ │ deploy, custom scripts │ │ │ │
│ │ ▼ │ │ ▼ │ │ │ │
│ │ Rector │ │ Testing │ │ │ │
│ │ ▼ │ │ PHPUnit tests ──► Behat tests │ │ │ │
│ │ Twig CS Fixer │ └───────────────┬───────────────┘ │ │ │
│ │ ▼ │ │ │ │ │
│ │ Gherkin Lint │ │ │ │ │
│ │ ▼ │ │ │ │ │
│ │ ESLint / Stylelint │ │ │ │ │
│ │ │ │ │ │ │
│ └──────────┬────────────┘ │ │ │ │
│ │ │ │ │ │
│ ▼ ▼ │ │ │
│ ┌─ Deployment Job ─────────────────────────────────────────┐ │ │ │
│ │ │ │ │ │
│ │ Webhook Artifact Lagoon │ │ Does not gate │ │
│ │ Call URL Package artifact Run Lagoon CLI deploy │ │ deployment │ │
│ │ │ │ │ │
│ └──────────────────────────────────────────────────────────┘ └──────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────────────────────────┘


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
  • Checks that Composer configuration is normalized

2. Database

  • Fetches the latest database version based on a caching strategy
  • Caches database dumps to speed up the follow-up runs

3. Build

  • Runs after the database job
  • Uses Docker Compose to set up the full environment
  • Validates Composer configuration
  • Assembles the codebase by installing dependencies
  • Provisions a website
  • Runs PHPUnit and Jest tests (first container only)
  • Checks code coverage and posts a PR comment (first container only)
  • Runs BDD tests (distributed across all containers - see Test parallelism)
  • Collects and stores test results and artifacts

4. Deployment

  • Runs after successful completion of both build and lint jobs
  • Uses the built codebase without development dependencies from the build step
  • Adds required secrets and environment variables
  • Triggers a deployment using a router script - see Deployment

Security audit

Security checks run in their own workflow, separate from the pipeline above, so that a failing audit is never confused with a failing linter and can be re-run on its own:

ProviderLocation
GitHub ActionsThe Security audit workflow in .github/workflows/audit.yml
CircleCIThe audit workflow in .circleci/config.yml

The workflow runs the same 2 checks in both providers, and needs neither the application containers nor installed dependencies:

  • composer audit --locked checks the packages pinned in composer.lock against published security advisories
  • Gitleaks scans the codebase for committed secrets

Every check runs even if an earlier one failed, so a single run reports all the findings at once. The workflow fails if any of the checks failed, unless that check's _IGNORE_FAILURE variable (see Ignore tool failures) is set to 1.

It is triggered by the same pushes, pull requests and tags as the main pipeline, and can also be started on demand - in GitHub Actions from Actions → Security audit → Run workflow, and in CircleCI by re-running the audit workflow from the pipeline view.

note

Because the audit is a separate workflow, it is not a dependency of the deploy job - a failing audit does not by itself stop a deployment. To block merges and deployments on it, add its check to the repository's branch protection rules as a required status check.

Caching strategy

The database is fetched overnight by a scheduled run and cached, so the follow-up continuous integration runs on the same day reuse the cached dump instead of downloading a fresh one.

The cache key is built from a configured cache source branch (the VORTEX_CI_DB_CACHE_BRANCH variable, develop by default) and a daily timestamp - every run on any branch reads the same shared cache. If no cache exists for the current day, the previous day's cache for the same source branch is used as a fallback.

note

Database caching speeds up continuous integration runs considerably on projects with a lot of data.

For a project with a large database (over 1 GB), the database import itself may take a long time, so it may be worth packaging the database dump into a container image (overnight) or using a sanitized database dump with only the data the tests require.

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.

Other tools serve the same goal: Drush GDPR Dumper, for example, removes data during the Drush database export itself, without an intermediate database import step.

Reset the cache

If you need to force a fresh cache (e.g., to pull a new database dump outside of the regular schedule), increment the last segment of the version tag in the cache keys:

# Before
v26.8.0
# After
v26.8.1

The version tag is the Vortex release version (CalVer). Bumping only its last segment keeps a project's own cache resets from colliding with the version shipped by a future Vortex update.

Trigger conditions

Both providers build on branch pushes, pull requests, tags matching semantic version (1.2.3, 1.2.3-rc.1) or date-based (2023-04-17) patterns, and a nightly schedule that refreshes the database cache.

The exact branch filters differ per provider:

  • GitHub Actions builds pushes to long-lived branches (production, main, master, develop, release/**, hotfix/**, project/**) and pull requests from any branch, so short-lived branches like feature/** and bugfix/** trigger builds through their pull requests - see GitHub Actions.
  • CircleCI builds every pushed branch, and restricts only the deployment job to a configured branch list - see CircleCI.

Test parallelism

The build job runs across multiple parallel containers (2 by default): Behat scenarios are distributed across all containers, while everything else runs on the first one. Which container runs which tool is declared in one place with CI_IS_<TOOL>_RUNNER variables.

➡️ See Test parallelism for the distribution model, balancing Behat scenarios, and adding more 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.

Runner disk space

Hosted runners come with a fixed amount of disk space, and running out of it is easy to misread: the runner is terminated from the outside, the step that was running never reports an error, and the failure looks like a hang rather than a disk problem. If a build dies during provisioning without reporting an error, suspect the disk first.

The most reliable fix is to reduce what has to fit: a sanitized dump or a database container image instead of a full dump.

  • On GitHub Actions, Vortex prints the disk state in every run and can reclaim space held by preinstalled toolchains - see GitHub Actions > Runner disk space.
  • On CircleCI, disk space is not tied to the resource class, so upgrading it adds CPU and memory but no extra room for the build.

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 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.8.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):

ToolPurposeVariable
BehatRun BDD acceptance testsVORTEX_CI_BEHAT_IGNORE_FAILURE
Composer normalizeEnsure composer.json is sortedVORTEX_CI_COMPOSER_NORMALIZE_IGNORE_FAILURE
Composer security auditCheck dependencies for vulnerabilitiesVORTEX_CI_COMPOSER_AUDIT_IGNORE_FAILURE
Composer validateValidate composer.json and lock fileVORTEX_CI_COMPOSER_VALIDATE_IGNORE_FAILURE
DCLintLint Docker Compose filesVORTEX_CI_DCLINT_IGNORE_FAILURE
ESLint and StylelintLint JavaScript and CSSVORTEX_CI_NODEJS_LINT_IGNORE_FAILURE
Gherkin LintLint Behat feature filesVORTEX_CI_GHERKIN_LINT_IGNORE_FAILURE
GitleaksScan the codebase for committed secretsVORTEX_CI_GITLEAKS_IGNORE_FAILURE
HadolintLint Dockerfiles for best practicesVORTEX_CI_HADOLINT_IGNORE_FAILURE
JestRun JavaScript unit testsVORTEX_CI_JEST_IGNORE_FAILURE
PHPCSCheck PHP coding standardsVORTEX_CI_PHPCS_IGNORE_FAILURE
PHPStanStatic analysis for PHPVORTEX_CI_PHPSTAN_IGNORE_FAILURE
PHPUnitRun unit, kernel, and functional testsVORTEX_CI_PHPUNIT_IGNORE_FAILURE
RectorCheck for automated refactoring rulesVORTEX_CI_RECTOR_IGNORE_FAILURE
SDC DevelValidate Single Directory ComponentsVORTEX_CI_SDC_DEVEL_IGNORE_FAILURE
Twig CS FixerLint Twig templatesVORTEX_CI_TWIG_CS_FIXER_IGNORE_FAILURE

Configure deployment skip conditions

Deployments can be skipped for specific branches or pull requests while their CI checks keep running - see Deployment > Skipping deployments.