Skip to main content
Version: v1

Continuous integration

This page is for the lead or DevOps engineer who maintains the continuous integration (CI) pipeline of a project built from Vortex. It also helps developers who need to understand a failing CI run. It explains what each job does and how the database cache, the security audit and the test split work, so you can tune the pipeline and read its results.

Vortex ships the pipeline for GitHub Actions and CircleCI. Each run lints your code, builds and provisions the site and then runs the tests. On the branches you deploy, it deploys the result after the build and lint jobs pass. The pipeline has the same structure on both providers, so pick the one your project uses and follow its setup instructions.

Settings that exist on only 1 provider, such as trigger branches, schedules and runner sizes, live on that provider's page. Deployment covers what happens during a deployment.

Every job runs in a container made from the drevops/ci-runner image, so every run gets the same environment on either provider. Each job that calls a vendor/bin/vortex-* command first installs the drevops/vortex-tooling package with scripts/vortex-tooling.sh. That script installs only the tooling package, so those commands work before the codebase is assembled.

Workflow structure​

The following diagram shows how a change moves from a developer's machine through the pipeline to the hosting platform:

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 ──► Import and export ──► Store database cache │ │
│ │ trigger database to validate the dump │ │
│ │ │ │
│ └─────────────────────────────────────┬──────────────────────────────────────────────┘ │
│ │ Provides cached database │
│ ▼ │
│ ┌─ Lint Job ────────────┐ ┌─ Database Job ────────────────┐ ┌─ Audit Job ──────────┐ │
│ │ │ │ ◆ Nightly cache exists? │ │ Composer audit │ │
│ │ Composer validate │ │ Yes ──► job succeeds │ │ (advisories) │ │
│ │ ▼ │ │ No ──► download, validate, │ │ ▼ │ │
│ │ Hadolint │ │ store cache │ │ npm audit │ │
│ │ ▼ │ └───────────────┬───────────────┘ │ (advisories) │ │
│ │ DCLint │ ▼ │ ▼ │ │
│ │ ▼ │ ┌─ Build Job ───────────────────┐ │ Gitleaks │ │
│ │ Build CLI container │ │ Code assembly │ │ (committed secrets) │ │
│ │ ▼ │ │ Docker, Composer deps, │ │ │ │
│ │ Composer normalize │ │ NPM deps, assets │ │ │ │
│ │ ▼ │ │ ▼ │ │ │ │
│ │ PHPCS │ │ Website setup │ │ │ │
│ │ ▼ │ │ Import cached DB, drush │ │ │ │
│ │ PHPStan │ │ deploy, custom scripts │ │ │ │
│ │ ▼ │ │ ▼ │ │ │ │
│ │ Rector │ │ Testing │ │ │ │
│ │ ▼ │ │ Jest ──► PHPUnit ──► Behat │ │ │ │
│ │ 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​

The lint job checks the code without provisioning a site:

  • Starts at the same time as the other jobs and waits for none of them
  • Validates the Composer configuration with composer validate --strict
  • Lints Dockerfiles with Hadolint and Docker Compose files with DCLint
  • Starts only the cli container, because linting needs no database or other services
  • Installs the development dependencies
  • Checks that the Composer configuration is normalized
  • Runs the code linters, in this order:
    • PHPCS
    • PHPStan
    • Rector
    • Twig CS Fixer
    • Gherkin Lint
    • ESLint and Stylelint, on custom module code and on the custom theme

The installer removes the steps of the tools you don't select, so your pipeline runs only the linters your project uses.

2. Database​

The database job prepares the dump that the build job provisions the site from:

  • Restores the dump that an earlier run cached, or downloads a fresh dump when no cache matches (see Caching strategy)
  • After a fresh download, provisions a site from the dump to check that it imports
  • Exports the database as fetched, before any database updates, configuration import or sanitization
  • Caches the dump, so the build job and later runs skip the download
  • On the nightly run of a project that stores its database in a container image, pushes the refreshed image to the registry

The job exists only when the site provisions from a database dump. With the profile provision type, the pipeline has no database job and no nightly schedule.

3. Build​

The build job builds the site and tests it:

  • Runs after the database job and restores the dump that job cached
  • Validates the Composer configuration
  • Builds the full stack with Docker Compose: the cli image build installs the Composer packages without development dependencies and builds the theme assets
  • Exports that codebase for artifact deployments, before any development dependency is added
  • Installs the development dependencies
  • Provisions the site
  • Runs the following on the first container only:
    • The Jest and PHPUnit tests
    • The code coverage check and the coverage summary comment on the pull request
    • The upload of the coverage reports to Codecov, when you chose Codecov at install time and set CODECOV_TOKEN
    • The validation of Single Directory Components (SDC)
  • Runs the Behat (BDD) tests across all containers (see Test parallelism)
  • Collects and stores the test results and artifacts
  • Checks that the build left a clean working tree (see Working tree check)

4. Deployment​

The deploy job sends the code to your hosting:

  • Runs after both the build and lint jobs succeed (a manual deployment on GitHub Actions runs it on its own)
  • Skips deployment on the branches that automated dependency updates create (deps/*)
  • Deploys tags too: GitHub Actions in the same deploy job, CircleCI in a separate deploy-tags job
  • For artifact deployments, ships the codebase that the build job exported, without development dependencies
  • Adds the required secrets and environment variables
  • Triggers the deployment through the vortex-deploy router script (see Deployment)

Security audit​

Security checks run in a separate workflow. That keeps a failing audit distinct from a failing linter and lets you re-run the audit by itself. Each provider defines the workflow in its own file:

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

The workflow runs the same 4 checks on both providers. The checks read only the codebase and its lock files, so the audit never waits for a build:

  • composer audit --locked checks the packages pinned in composer.lock against published security advisories.
  • npm audit --package-lock-only, run through audit-ci, checks the packages pinned in package-lock.json and fails on advisories rated high or critical. Advisories allowlisted in package-audit.jsonc don't count while their entry is active and unexpired.
  • The same npm audit runs against the custom theme's own package-lock.json, with the theme's own package-audit.jsonc.
  • Gitleaks scans the codebase for committed secrets.

Only the Composer check is always present. The root npm check ships when your project keeps ESLint, Stylelint or Jest. The theme check ships with a custom theme. The Gitleaks check ships when you turn on secret scanning at install time.

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

The workflow runs on the same pushes, pull requests and tags as the main pipeline, but has no nightly schedule. You can also start it on demand. In GitHub Actions, use Actions > Security audit > Run workflow. In CircleCI, re-run the audit workflow from the pipeline view.

note

The deploy job doesn't depend on the audit workflow, so a failing audit doesn't stop a deployment. To block merges on the audit, add its check to the repository's branch protection rules as a required status check.

Code scanning​

On GitHub Actions, the workflow also records every check's findings as SARIF, the interchange format that code scanning reads. It uploads them to the repository's Security > Code scanning tab, each under its own category:

CategoryFindings
composer-auditcomposer audit advisories, abandoned packages and dependency policy matches
npm-auditnpm audit advisories for the root package-lock.json
npm-audit-themenpm audit advisories for the custom theme's package-lock.json
gitleaksCommitted secrets

Code scanning keeps a history of every finding and tracks whether it's open or fixed. When you dismiss a false positive, the alert stores the reason, so the repository needs no ignore entry for it.

Gitleaks writes SARIF itself. composer audit and npm audit have no SARIF formatter, so each of those checks runs once in JSON format. It pipes its output through tee, which feeds the job log and a report file in .logs/audit/ from the same run. The npm checks pass --output-format json --report-type full to audit-ci, so audit-ci prints npm's own report.

The vortex-convert-audit-sarif script from the Vortex tooling package converts each report file. The workflow passes the report format, composer or npm, as the script's second argument, which takes precedence over VORTEX_CONVERT_AUDIT_SARIF_FORMAT. For the theme report, the workflow also passes the theme's package-lock.json as the third argument, so each finding points at the right lock file. For each npm report, the script reads the package-audit.jsonc next to the report's lock file. It leaves out the advisories that an active, unexpired entry in that file allowlists.

Only the check steps fail the workflow on findings. The conversion and upload steps run after them with continue-on-error, so they can't fail the workflow.

Code scanning must be available on the repository. Public repositories have it, and private repositories need a GitHub Code Security license.

Without code scanning, and on pull requests from forks where the workflow token is read-only, the upload step fails and the workflow carries on. The checks still report their findings in the job log and still fail the workflow. Every report is also attached to the run as the security-findings artifact.

CircleCI has no code scanning destination. On CircleCI, the same checks run and fail the audit workflow, and their findings appear only in the job log.

➡️ See Dependency audit > Dismissing a finding and Secret scanning > Dismissing a finding for what dismissing a finding does and doesn't change.

Working tree check​

A build can produce files that belong in the repository, such as exported configuration, compiled front-end assets, generated documentation and scaffolded files. Left unnoticed, the repository and the build output drift apart, so the next developer to run the build gets changes they didn't make.

The build job runs vendor/bin/vortex-check-worktree-changes after the tests and after their results are stored, so a dirty tree never hides the results that would explain it. The step fails when the working tree holds modified or untracked files, and it prints each one with its Git status:

[INFO] Started working tree check.
Ignoring "docker-compose.yml".
Ignoring "*/docker-compose.yml".
Ignoring "db_cache_*".
Ignoring "codecov".
Ignoring "codecov.SHA256SUM".
Ignoring "codecov.SHA256SUM.sig".
The build modified or created these files:
M config/default/core.extension.yml
?? config/default/views.view.recent_content.yml
Run "ahoy build" locally and commit the resulting changes.
Add paths that are generated during the build and are not committed to VORTEX_CHECK_WORKTREE_CHANGES_IGNORE.
[FAIL] The build left changes in the working tree.

Git never reports the files it ignores, so .gitignore already covers most generated output. List everything else in VORTEX_CHECK_WORKTREE_CHANGES_IGNORE, a comma-separated list of Git pathspec patterns. Set it in .env, which the script loads. The following example keeps the 6 default patterns and adds a path of your own, web/sites/default/settings.generated.php:

VORTEX_CHECK_WORKTREE_CHANGES_IGNORE=docker-compose.yml,*/docker-compose.yml,db_cache_*,codecov,codecov.SHA256SUM,codecov.SHA256SUM.sig,web/sites/default/settings.generated.php

The default value covers what the pipeline itself drops into the working tree:

  • Every docker-compose.yml in the tree, which the Process the codebase to run in CI step rewrites in place
  • The db_cache_* files that hold the database cache keys
  • The codecov uploader and its 2 signature files

Those files exist only during a CI run, so they belong in the variable and not in .gitignore, which every checkout shares. Your value replaces the default, so copy the default entries into it when you add your own. Confirm that the next run's check prints an Ignoring line for each of your patterns.

The check reads the working tree on the runner, not the codebase inside the cli container.

To report the findings without failing the build, set VORTEX_CI_WORKTREE_CHANGES_IGNORE_FAILURE to 1 (see Ignore tool failures).

Caching strategy​

A scheduled run fetches a fresh database dump overnight and caches it. The runs that follow restore that cached dump, so they skip the download.

The cache key combines a version prefix, the cache source branch, the fallback setting and a daily timestamp. The branch comes from VORTEX_CI_DB_CACHE_BRANCH (develop by default). The timestamp comes from VORTEX_CI_DB_CACHE_TIMESTAMP, a date format that defaults to +%Y%m%d. The key leaves out the branch being built, so runs on every branch look up the same cache.

GitHub Actions also scopes caches by branch: a run restores only caches saved on its own branch or on the default branch. A pull request run can also restore caches from its base branch. The scheduled run saves its cache on the default branch, so every run can restore it. CircleCI shares caches across all branches of a project.

When no cache exists for the current day, a run falls back to the most recent earlier cache for the same source branch, normally the previous day's cache. VORTEX_CI_DB_CACHE_FALLBACK controls this and defaults to yes. The scheduled run sets it to no, so it always downloads a fresh dump.

Neither provider overwrites a saved cache key, so the first job to save the day's key wins. GitHub Actions saves the cache only when a job changed the dump, as a fresh download does. Jobs that restored a cache leave the key free for the scheduled run. On CircleCI, every database job saves the day's key, so the scheduled run's dump is cached only when no earlier job saved it (see CircleCI > Update nightly database schedule).

The 3 variables live in the CI configuration: in the container env block of the database and build jobs on GitHub Actions and in the shared runner_config block on CircleCI.

note

Caching makes the biggest difference on projects with a lot of data.

With a large database (over 1 GB), the import itself can take a long time. At that size, package the dump into a container image overnight, or use a sanitized dump that holds only the data the tests need.

Vortex can both create and use a database container image with the data built in. To create the first image, use MariaDB data container for Drupal with database captured as Docker layers.

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

Reset the cache​

To pull a new database dump outside the regular schedule, force a fresh cache. Increment the last segment of the version prefix in the cache keys. The database and build jobs both use the prefix, so change every occurrence in .github/workflows/build-test-deploy.yml or .circleci/config.yml:

# Before
v26.8.1
# After
v26.8.2

The next run finds no cache under the new prefix, so the database job downloads a fresh dump and caches it. Confirm that its Fetch DB step ends with Finished database fetch. and doesn't print Fetch will not proceed.

The prefix is a calendar-style stamp (vYY.M.Z) that Vortex moves forward when an update needs to reset every project's cache. Bump only its last segment, so your own cache resets never collide with the prefix that a future Vortex update ships.

Trigger conditions​

Both providers build on branch pushes, pull requests and tag pushes, plus a nightly schedule that refreshes the database cache when the site provisions from a database dump. They deploy tags differently. GitHub Actions deploys every tag it builds. CircleCI deploys only tags named after a semantic version (1.2.3, 1.2.3-rc.1) or a date (2023-04-17, 2023-04-17.1).

The exact branch filters differ per provider:

  • GitHub Actions builds pushes to long-lived branches: production, main, master, develop, release/**, hotfix/** and project/**. A pull request from any branch builds when it targets 1 of those branches, or a feature/** or bugfix/** branch, which is how short-lived branches get built. Pull requests opened from a project/** branch skip every job, because pushes to that branch already build it. See GitHub Actions > Update trigger branches.
  • CircleCI builds every pushed branch and restricts only the deploy job to a configured branch list. See CircleCI > Update deployment branches.

Test parallelism​

The build job runs on 2 containers in parallel by default, to get through the tests faster. Each container repeats the full build and provisioning, then runs its share of the tests.

Code linting runs in the separate lint job, so the parallelism settings don't affect it.

What runs where​

The following table shows where each test task runs with the default 2 containers:

TaskFirst containerOther containers
Jest testsYesNo
PHPUnit testsYesNo
Code coverage check and pull request commentYesNo
Single Directory Component validationYesNo
Behat testsYes, profile p0Yes, the profile numbered after the container, such as p1 on container 1

Every task except Behat runs on the first container only, because repeating a task on every container would duplicate the work. Behat runs on all containers. The profile each container selects determines which scenarios it runs.

Choose which container runs what​

Each tool's step runs only on the containers where its VORTEX_CI_IS_<TOOL>_RUNNER variable is true (1 on CircleCI). The variables sit together at the top of the build job, so you can see and change the whole split in 1 place:

.github/workflows/build-test-deploy.yml
env:
VORTEX_CI_RUNNER_INDEX: ${{ strategy.job-index }}
VORTEX_CI_RUNNER_TOTAL: ${{ strategy.job-total }}
VORTEX_CI_IS_JEST_RUNNER: ${{ matrix.instance == 0 || strategy.job-total == 1 }}
VORTEX_CI_IS_PHPUNIT_RUNNER: ${{ matrix.instance == 0 || strategy.job-total == 1 }}
VORTEX_CI_IS_SDC_DEVEL_RUNNER: ${{ matrix.instance == 0 || strategy.job-total == 1 }}
VORTEX_CI_IS_BEHAT_RUNNER: true
VORTEX_CI_BEHAT_PROFILE_OFFSET: 0

Each tool's step then reads its own flag:

- name: Test with PHPUnit
if: ${{ env.VORTEX_CI_IS_PHPUNIT_RUNNER == 'true' }}

VORTEX_CI_RUNNER_INDEX and VORTEX_CI_RUNNER_TOTAL hold the current container's index and the container count, under the same names on both providers. The Behat step subtracts VORTEX_CI_BEHAT_PROFILE_OFFSET from the container index to get its profile number (see Give a tool its own container).

To run your own tool on a specific container, add a flag next to the others and reference it from your step:

.github/workflows/build-test-deploy.yml
VORTEX_CI_IS_CYPRESS_RUNNER: ${{ matrix.instance == 1 }}

Give a tool its own container​

To move a tool onto a container of its own:

  1. Add a container for the tool, because the default configuration has only containers 0 and 1. See Adding more containers.
  2. Point the tool's flag at the new container.
  3. Exclude the new container from Behat.

On GitHub Actions, with a third container added, steps 2 and 3 look like this:

VORTEX_CI_IS_PHPUNIT_RUNNER: ${{ matrix.instance == 2 }}
VORTEX_CI_IS_BEHAT_RUNNER: ${{ matrix.instance != 2 }}

On CircleCI, mirror the conditions in the Set test runner roles step.

Point the flag at a container that exists. A flag whose condition matches no container disables the tool everywhere. Its steps are skipped silently, and the job still passes. Confirm that the tool's step runs only on the dedicated container. On the other containers, GitHub Actions marks the step as skipped, and CircleCI ends it before the tool starts.

Behat derives its profile number from the container index minus VORTEX_CI_BEHAT_PROFILE_OFFSET. p0 is the catch-all that runs every scenario without a @pX tag. With the dedicated container at the end, the offset stays 0. The containers before it keep their profiles.

To dedicate a leading container instead, raise the offset by 1 for each leading container you exclude from Behat, so the first Behat container still selects the p0 catch-all. The following example, on 3 containers, pins every tool that runs once to container 0 and keeps Behat off it:

VORTEX_CI_IS_JEST_RUNNER: ${{ matrix.instance == 0 }}
VORTEX_CI_IS_PHPUNIT_RUNNER: ${{ matrix.instance == 0 }}
VORTEX_CI_IS_SDC_DEVEL_RUNNER: ${{ matrix.instance == 0 }}
VORTEX_CI_IS_BEHAT_RUNNER: ${{ matrix.instance != 0 }}
VORTEX_CI_BEHAT_PROFILE_OFFSET: 1

The 3 containers then split the work as follows:

ContainerRoleBehat profile
0Jest, PHPUnit, coverage, SDC validationNone
1Behatp0 (catch-all)
2Behatp1

behat.php stays unchanged. Its profiles stay numbered from p0, and the feature files keep their tags.

caution

When VORTEX_CI_BEHAT_PROFILE is unset, the offset describes 1 unbroken block of Behat containers that starts at the offset. A Behat container below the offset derives a negative profile name such as p-1, and the run fails. A gap inside the block leaves the profile at the gap's position unselected, so its scenarios silently stop running. Setting VORTEX_CI_BEHAT_PROFILE explicitly bypasses the derivation, and every container then runs that profile.

Balance Behat tests​

The first container runs Jest, PHPUnit and the coverage checks on top of its share of Behat, so it has the most work. To keep the whole build short, give the other containers more of the Behat scenarios.

Profile tags determine where each scenario runs. A scenario without a profile tag runs on the first container. Tag it @p1 to move it to the second container, or @smoke to run it on every container:

Scenario: Quick smoke test (untagged, stays on the first container)
Given I go to the homepage
Then I should see "Welcome"

@p1
Scenario: Full content workflow (runs on the second container)
Given I am logged in as a user with the role "content_editor"
...

With only 1 container, every scenario runs there whatever its tags, except those tagged @skipped.

Keep the lightweight scenarios on the first container. Move the heavier ones, or more of them, to the other containers with their profile tags. The build then takes about as long as its slowest container, instead of the sum of all the tests.

@smoke scenarios check that Behat itself is configured and working. They run on every container by design, so keep them few and fast.

See Behat > Profiles for how the shipped profiles map tags to runners.

Add more containers​

Raising the container count takes 2 matching changes: the count itself and a Behat profile for every new container that runs Behat. To add containers:

  1. Increase the container count. See the provider-specific pages:

  2. Add a profile to behat.php for each new container that runs Behat, and register it in the returned Config. Vortex ships with p0 and p1 only. A container's derived number is its index minus VORTEX_CI_BEHAT_PROFILE_OFFSET. When a container that runs Behat has no profile with that number, Behat stops with Can not find configuration for `p2` profile. A container excluded from Behat needs no profile:

    behat.php
    $p2 = (new Profile('p2'))
    ->withGherkinOptions((new GherkinOptions())->withCacheDir('/tmp/behat_gherkin_cache')->withFilter(new TagFilter('@smoke,@p2&&~@skipped')));

    return (new Config())->withProfile($default)->withProfile($p0)->withProfile($p1)->withProfile($p2);
  3. Exclude the new tag from the p0 catch-all, so its scenarios don't also run on the first container:

    behat.php
    $p0 = (new Profile('p0'))
    ->withGherkinOptions((new GherkinOptions())->withCacheDir('/tmp/behat_gherkin_cache')->withFilter(new TagFilter('@smoke,~@p1&&~@p2&&~@skipped')));
  4. Tag scenarios with @p2 to move them onto the new container.

  5. Confirm that the Test with Behat step on the new container prints Running with p2 profile.

Maintenance​

The following sections cover the settings you change after the pipeline is up and running, on either provider.

Pin SSH host keys​

The shipped CI configuration turns off strict host-key checking for the SSH connections the pipeline makes to the hosting provider. Pinning the host keys is the secure alternative. With VORTEX_SSH_KNOWN_HOSTS set, the SSH setup writes the keys to known_hosts and keeps strict checking on, even where VORTEX_SSH_DISABLE_STRICT_HOST_KEY_CHECKING is set.

To pin the host keys:

  1. Get the keys with ssh-keyscan -p PORT HOST. Replace HOST and PORT with your hosting provider's SSH host and port.

  2. Check the keys against the fingerprints your provider publishes.

  3. Add the keys as VORTEX_SSH_KNOWN_HOSTS:

    • On GitHub Actions, add a repository variable with 1 entry per line. The shipped workflow doesn't pass it to the jobs. Add VORTEX_SSH_KNOWN_HOSTS: ${{ vars.VORTEX_SSH_KNOWN_HOSTS }} to the container.env block of the database, build and deploy jobs in .github/workflows/build-test-deploy.yml, next to VORTEX_SSH_DISABLE_STRICT_HOST_KEY_CHECKING.
    • On CircleCI, add it as an environment variable, with multiple entries joined with \n.
  4. Confirm that the job log shows Pinned SSH host keys to known_hosts. where the SSH setup runs.

On GitHub Actions, the steps that load the SSH keys also take a known_hosts input from the VORTEX_FETCH_DB_SSH_KNOWN_HOSTS and VORTEX_DEPLOY_SSH_KNOWN_HOSTS repository variables, and skip it when those variables are empty. Those 2 variables leave strict checking off, because the SSH setup still turns it off without VORTEX_SSH_KNOWN_HOSTS. After you map VORTEX_SSH_KNOWN_HOSTS, you don't need them.

Enable debug mode​

To get verbose output while you troubleshoot a failing build, set the VORTEX_DEBUG variable to 1 in your CI provider's settings. On GitHub Actions, add it as a repository variable. On CircleCI, add it as a project environment variable.

The Vortex scripts then print every command they run. Confirm that the job log shows those commands, each one prefixed with +.

Runner disk space​

Hosted runners come with a fixed amount of disk space. When a job runs out of it, the failure is misleading. The runner is stopped from the outside, and the step that was running never reports an error. The failure looks like a hang. If a build stops during provisioning without reporting an error, suspect the disk first.

To remove the cause, reduce what has to fit: use a sanitized dump or a database container image in place of a full dump. What else helps depends on the provider:

  • On GitHub Actions, Vortex prints the disk state in the build and database jobs of every run. It can also reclaim space held by preinstalled toolchains. See GitHub Actions > Runner disk space.
  • On CircleCI, disk space doesn't grow with the resource class, so a larger class 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 built for running CI jobs. It provides a consistent and 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 - command-line tools such as Git, curl, rsync and jq

Both providers run every job on this image, so a build is independent of the tools a provider happens to install. The image also speeds builds up, because the jobs skip reinstalling the same common tools on every run.

The image follows CalVer versioning (for example, 26.10.0), with a new release most months. The CI configuration pins it by tag and by digest (drevops/ci-runner:26.10.0@sha256:...). The digest fixes the exact image.

To update the image, change the tag and its @sha256: digest together, everywhere the image appears:

  • Each job in .github/workflows/build-test-deploy.yml
  • The job in .github/workflows/audit.yml
  • The shared runner_config block in .circleci/config.yml

Confirm that a search for drevops/ci-runner: in those files finds only the new tag and digest.

When Renovate manages your dependencies, as it does by default, it proposes new image releases for the GitHub Actions workflows. Nothing in its configuration matches the image: line in .circleci/config.yml, so on CircleCI you update the image yourself.

Ignore tool failures​

You can let a build pass despite linter or test failures, for example while you clear the findings of a rule you turned on.

To let the build pass, set the tool's VORTEX_CI_*_IGNORE_FAILURE variable to 1:

  • On GitHub Actions, add the variable under Settings > Secrets and variables > Actions > Variables.
  • On CircleCI, add it as a project environment variable.

The tool still runs and prints its results in the job log, so you can see what you're letting through. Confirm that the tool's step reports its findings and the job still passes.

The following table lists the variable for each tool:

ToolPurposeVariable
BehatRun BDD acceptance testsVORTEX_CI_BEHAT_IGNORE_FAILURE
Composer normalizeCheck that composer.json is normalizedVORTEX_CI_COMPOSER_NORMALIZE_IGNORE_FAILURE
Composer security auditCheck PHP 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
npm security auditCheck JavaScript dependencies for vulnerabilitiesVORTEX_CI_NPM_AUDIT_IGNORE_FAILURE
PHPCSCheck PHP coding standardsVORTEX_CI_PHPCS_IGNORE_FAILURE
PHPStanAnalyze PHP code staticallyVORTEX_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
Working tree checkCheck for files left behind by the buildVORTEX_CI_WORKTREE_CHANGES_IGNORE_FAILURE

To let the code coverage threshold check pass, lower VORTEX_CI_CODE_COVERAGE_THRESHOLD. It defaults to 90 and goes in the same place as the variables in the preceding table.

Configure deployment skip conditions​

You can skip deployments for specific branches or pull requests while their CI checks keep running. See Deployment > Skipping deployments.