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
clicontainer, 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
buildjob 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
databasejob and restores the dump that job cached - Validates the Composer configuration
- Builds the full stack with Docker Compose: the
cliimage 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
buildandlintjobs 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
deployjob, CircleCI in a separatedeploy-tagsjob - For artifact deployments, ships the codebase that the
buildjob exported, without development dependencies - Adds the required secrets and environment variables
- Triggers the deployment through the
vortex-deployrouter 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:
| Provider | Location |
|---|---|
| GitHub Actions | The Security audit workflow in .github/workflows/audit.yml |
| CircleCI | The 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 --lockedchecks the packages pinned incomposer.lockagainst published security advisories.npm audit --package-lock-only, run through audit-ci, checks the packages pinned inpackage-lock.jsonand fails on advisories ratedhighorcritical. Advisories allowlisted inpackage-audit.jsoncdon't count while their entry is active and unexpired.- The same
npm auditruns against the custom theme's ownpackage-lock.json, with the theme's ownpackage-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.
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:
| Category | Findings |
|---|---|
composer-audit | composer audit advisories, abandoned packages and dependency policy matches |
npm-audit | npm audit advisories for the root package-lock.json |
npm-audit-theme | npm audit advisories for the custom theme's package-lock.json |
gitleaks | Committed 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.ymlin the tree, which theProcess the codebase to run in CIstep rewrites in place - The
db_cache_*files that hold the database cache keys - The
codecovuploader 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.
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/**andproject/**. A pull request from any branch builds when it targets 1 of those branches, or afeature/**orbugfix/**branch, which is how short-lived branches get built. Pull requests opened from aproject/**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
deployjob 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:
| Task | First container | Other containers |
|---|---|---|
| Jest tests | Yes | No |
| PHPUnit tests | Yes | No |
| Code coverage check and pull request comment | Yes | No |
| Single Directory Component validation | Yes | No |
| Behat tests | Yes, profile p0 | Yes, 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 Actions
- CircleCI
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' }}
- run:
name: Set test runner roles
command: |
{
echo "export VORTEX_CI_RUNNER_INDEX=${CIRCLE_NODE_INDEX:-0}"
echo "export VORTEX_CI_RUNNER_TOTAL=${CIRCLE_NODE_TOTAL:-1}"
echo "export VORTEX_CI_IS_JEST_RUNNER=$([ "${CIRCLE_NODE_INDEX:-0}" -eq 0 ] && echo 1 || echo 0)"
echo "export VORTEX_CI_IS_PHPUNIT_RUNNER=$([ "${CIRCLE_NODE_INDEX:-0}" -eq 0 ] && echo 1 || echo 0)"
echo "export VORTEX_CI_IS_SDC_DEVEL_RUNNER=$([ "${CIRCLE_NODE_INDEX:-0}" -eq 0 ] && echo 1 || echo 0)"
echo "export VORTEX_CI_IS_BEHAT_RUNNER=1"
echo "export VORTEX_CI_BEHAT_PROFILE_OFFSET=0"
} >> "${BASH_ENV}"
Each tool's step then reads its own flag as its first line:
- run:
name: Test with PHPUnit
command: |
[ "${VORTEX_CI_IS_PHPUNIT_RUNNER:-1}" = "1" ] || exit 0
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 Actions
- CircleCI
VORTEX_CI_IS_CYPRESS_RUNNER: ${{ matrix.instance == 1 }}
echo "export VORTEX_CI_IS_CYPRESS_RUNNER=$([ "${CIRCLE_NODE_INDEX:-0}" -eq 1 ] && echo 1 || echo 0)"
Give a tool its own container
To move a tool onto a container of its own:
- Add a container for the tool, because the default configuration has
only containers
0and1. See Adding more containers. - Point the tool's flag at the new container.
- 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:
| Container | Role | Behat profile |
|---|---|---|
| 0 | Jest, PHPUnit, coverage, SDC validation | None |
| 1 | Behat | p0 (catch-all) |
| 2 | Behat | p1 |
behat.php stays unchanged. Its profiles stay numbered from p0, and the
feature files keep their tags.
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:
-
Increase the container count. See the provider-specific pages:
-
Add a profile to
behat.phpfor each new container that runs Behat, and register it in the returnedConfig. Vortex ships withp0andp1only. A container's derived number is its index minusVORTEX_CI_BEHAT_PROFILE_OFFSET. When a container that runs Behat has no profile with that number, Behat stops withCan 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); -
Exclude the new tag from the
p0catch-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'))); -
Tag scenarios with
@p2to move them onto the new container. -
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:
-
Get the keys with
ssh-keyscan -p PORT HOST. ReplaceHOSTandPORTwith your hosting provider's SSH host and port. -
Check the keys against the fingerprints your provider publishes.
-
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 thecontainer.envblock of thedatabase,buildanddeployjobs in.github/workflows/build-test-deploy.yml, next toVORTEX_SSH_DISABLE_STRICT_HOST_KEY_CHECKING. - On CircleCI, add it as an environment variable, with multiple entries
joined with
\n.
- On GitHub Actions, add a repository variable with 1 entry per line.
The shipped workflow doesn't pass it to the jobs. Add
-
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
buildanddatabasejobs 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_configblock 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:
| Tool | Purpose | Variable |
|---|---|---|
| Behat | Run BDD acceptance tests | VORTEX_CI_BEHAT_IGNORE_FAILURE |
| Composer normalize | Check that composer.json is normalized | VORTEX_CI_COMPOSER_NORMALIZE_IGNORE_FAILURE |
| Composer security audit | Check PHP 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 and Stylelint | Lint JavaScript and CSS | VORTEX_CI_NODEJS_LINT_IGNORE_FAILURE |
| Gherkin Lint | Lint Behat feature files | VORTEX_CI_GHERKIN_LINT_IGNORE_FAILURE |
| Gitleaks | Scan the codebase for committed secrets | VORTEX_CI_GITLEAKS_IGNORE_FAILURE |
| Hadolint | Lint Dockerfiles for best practices | VORTEX_CI_HADOLINT_IGNORE_FAILURE |
| Jest | Run JavaScript unit tests | VORTEX_CI_JEST_IGNORE_FAILURE |
| npm security audit | Check JavaScript dependencies for vulnerabilities | VORTEX_CI_NPM_AUDIT_IGNORE_FAILURE |
| PHPCS | Check PHP coding standards | VORTEX_CI_PHPCS_IGNORE_FAILURE |
| PHPStan | Analyze PHP code statically | 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 |
| SDC Devel | Validate Single Directory Components | VORTEX_CI_SDC_DEVEL_IGNORE_FAILURE |
| Twig CS Fixer | Lint Twig templates | VORTEX_CI_TWIG_CS_FIXER_IGNORE_FAILURE |
| Working tree check | Check for files left behind by the build | VORTEX_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.