Continuous integration
This page is for the lead or DevOps engineer who sets up and 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 runs the tests. On the branches you deploy, it then deploys
the result after the lint, test, and build 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 and runner sizes, live on that provider's page. Deployment covers what happens during a deployment.
Every job runs on the provider's standard environment: a GitHub-hosted
runner on GitHub Actions, or the PHP convenience image on CircleCI.
Each job that calls a vendor/bin/vortex-* command first installs the
drevops/vortex-tooling
package with scripts/vortex-tooling.sh. That script installs the tooling
package on its own, 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 ─────────────────────── ───────────────────────────────────────────────────────┐
│ │
│ The Lint, Test and Build jobs run in parallel. Deployment runs after all three pass. │
│ │
│ Lint Job │
│ Composer validate ──► Hadolint ──► DCLint ──► Build CLI container ──► │
│ Composer normalize ──► 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 ──► deploy steps │
│ ▼ │
│ Jest ──► PHPUnit ──► SDC validation ──► Behat │
│ │
│ Build Job │
│ Assemble and validate the deployable artifact │
│ │
│ Deployment Job (after Lint, Test and Build pass) │
│ Webhook: call URL Artifact: push code package Lagoon: trigger deployment │
│ │
│ Security Audit (separate workflow, does not gate deployment) │
│ Composer audit ──► npm audit ──► Gitleaks │
│ │
└────────────────────────────────────────────────────────────────────────────────────────────┘
│
▼
Hosting Platform
═════════════════════════════════════════════════════════════════════════════════════════
◆ Environment ──No──► Sync DB from production ───┐
exists? │
│ Yes ▼
└──────────────────────────────────► drush deploy ──► Deploy steps ──► 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:
- Runs in parallel with the other jobs, since it doesn't wait for any 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 CLI removes the steps of the tools you don't select, so your pipeline runs only the linters your project uses.
2. Test
The test job provisions the site and tests it:
- Runs in parallel with the
lintandbuildjobs - Validates the Composer configuration
- Restores the cached database dump, or downloads a fresh one on the first run of the day (see Caching strategy)
- Builds the full stack with Docker Compose
- After a fresh download, provisions a site from the dump to check that it imports, then exports and caches it. Only the first container stores the cache when the job runs on several containers.
- 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
in the CLI 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)
3. Build
The build job builds what the deployment ships:
- Runs in parallel with the
lintandtestjobs - Builds the production stack with Docker Compose: the
cliimage build installs the Composer packages without development dependencies and builds the theme assets - For artifact deployments, such as Acquia, exports the built codebase for
the
deployjob - For image-based hosting, such as Lagoon, confirms that the production images build
4. Deployment
The deploy job sends the code to your hosting:
- Runs after the
lint,test, andbuildjobs succeed (a manual deployment on GitHub Actions runs it on its own) - Skips the branches that automated dependency updates create (
deps/*) - Deploys tags too: GitHub Actions handles them in the same
deployjob, and 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 workflow of their own, separate from the pipeline described so far. A failing audit then can't be mistaken for a failing linter, and you can re-run it on its own. 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. None of them needs the application containers or installed dependencies, 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, and the theme check ships with a custom theme. The Gitleaks check ships when you turn on secret scanning in the CLI.
Every check runs even if an earlier one failed, so a single run reports all
the findings at once. The workflow fails if any check failed, unless that
check's _IGNORE_FAILURE variable (see
Ignore tool failures) is set to 1.
The workflow runs on the same pushes, pull requests, and tags as the main
pipeline. 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 audit is a separate workflow and isn't a dependency of the deploy job,
so a failing audit doesn't by itself stop a deployment. To block merges on
it, add its check to the repository's branch protection rules as a required
status check.
Code scanning
On GitHub Actions, every check also writes its findings as SARIF, the interchange format that code scanning reads. The workflow 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. Dismissing a false positive records the reason in the alert, instead of in a committed ignore entry.
Gitleaks writes SARIF itself. Neither composer audit nor npm audit has a
SARIF formatter, so each of those checks runs once in JSON format and pipes
its output through tee. That single run feeds both the job log and a
report file in .logs/audit/. The npm checks pass
--output-format json --report-type full to audit-ci, which then 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, it 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
it.
Code scanning has to be available on the repository. It's available on public repositories, and private repositories need GitHub Advanced Security.
Where code scanning isn't available, and on pull requests from forks where
the workflow token is read-only, the upload step fails without failing the
workflow. 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, so the providers deliberately
differ here. On CircleCI the same checks run and fail the audit workflow,
but their findings live 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, and scaffolded files. Left unnoticed, the repository and the build output drift apart, and whoever builds next inherits a dirty working tree they didn't cause.
The test job runs vendor/bin/vortex-check-worktree-changes as its last
check, after the tests and after their results are stored, so a dirty tree
never suppresses 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. Everything else goes into
VORTEX_CHECK_WORKTREE_CHANGES_IGNORE, a comma-separated list of
Git pathspec patterns.
Set it in .env. The following example keeps the 6 default patterns and
adds an example 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 continuous integration run, so they're
excluded here rather than in .gitignore, which every checkout carries.
Setting the variable replaces the default instead of extending it, so copy
those entries over 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
Downloading the database on every run would slow builds down, so the first run of the day fetches a fresh dump and caches it. The runs that follow restore that cached dump instead of downloading their own.
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), and the timestamp from
VORTEX_CI_DB_CACHE_TIMESTAMP, a date format that defaults to
+%Y%m%d. The branch being built isn't part of the key, 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. 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. VORTEX_CI_DB_CACHE_FALLBACK controls this and defaults to yes.
Neither provider overwrites a saved cache key. GitHub Actions saves the
cache only when the dump changed during the job, as it does after a fresh
download. On both providers, only the first container saves the cache when
the test job runs on several containers.
The 3 variables live in the CI configuration: in the env block of the
test job 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 point it's worth packaging the dump into a container image, or using 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. Use MariaDB data container for Drupal with database captured as Docker layers to create the first image.
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 force a fresh cache, for example to pull a new database dump before the daily cache refreshes:
- Increment the last segment of the version prefix in the cache keys.
Change every occurrence in
.github/workflows/build-test-deploy.ymlor.circleci/config.yml:
# Before
v26.8.0
# After
v26.8.1
The next run finds no cache under the new prefix, so the test job
downloads a fresh dump and caches it. Confirm that its Fetch DB step
ends with Finished database download. rather than
Download 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. Bumping only
its last segment keeps a project's own cache resets from colliding with the
prefix shipped by a future Vortex update.
Trigger conditions
Both providers build on branch pushes, pull requests, and tag pushes. They
deploy tags differently: GitHub Actions deploys every tag it builds, and
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 one 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 test job runs on 2 containers in parallel by default, to get through
the tests faster. Each container repeats the full build and provisioning,
so the pipeline splits the test work between them instead of running every
test on every container.
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 |
Everything except Behat runs on the first container only, since running it on every container would repeat the same work. Behat runs on all containers, and the profile each container selects determines which scenarios it runs.
Choosing 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 test job, so you can see and
change the whole split in one 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
Giving a tool its own container).
To run a tool of your own on a specific container, add 1 more flag alongside 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)"
Giving a tool its own container
To move a tool onto a container of its own:
- Add a container for the tool to move onto, 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.
With a third container added, steps 2 and 3 look like this on GitHub
Actions; mirror the conditions in CircleCI's Set test runner roles step:
VORTEX_CI_IS_PHPUNIT_RUNNER: ${{ matrix.instance == 2 }}
VORTEX_CI_IS_BEHAT_RUNNER: ${{ matrix.instance != 2 }}
Point the flag at a container that exists. A flag whose condition matches no container disables the tool everywhere, and nothing reports it: the steps are skipped 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, and p0 is the catch-all that runs every
scenario without a @pX tag. With the dedicated container at the end, the
offset stays 0, and 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 once-only tool 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 untouched: the profiles stay numbered from p0, and no
feature file needs new 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.
Balancing 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 do. 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:
@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 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, and 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.
Adding more containers
Raising the container count takes 2 changes that must match: the container 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. If a container running Behat has no profile named after its derived number (its index minusVORTEX_CI_BEHAT_PROFILE_OFFSET), Behat stops with an error about the missing 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.
Scenarios tagged @smoke run on every container by design, so they stay out
of the balancing arithmetic. 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 them with ssh-keyscan -p PORT HOST, where HOST
and PORT are your hosting provider's SSH host and port. Check them against
the fingerprints your provider publishes, then add them as
VORTEX_SSH_KNOWN_HOSTS:
- On GitHub Actions, add a repository variable with 1 entry per line. The
shipped workflow doesn't reference it, so also add
VORTEX_SSH_KNOWN_HOSTS: ${{ vars.VORTEX_SSH_KNOWN_HOSTS }}to theenvblock of thetest,build, anddeployjobs 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, 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
they're empty. Those 2 variables don't keep strict checking on by
themselves, because the SSH setup still turns it off without
VORTEX_SSH_KNOWN_HOSTS. You don't need them after VORTEX_SSH_KNOWN_HOSTS
is mapped.
Confirm that the job log shows Pinned SSH host keys to known_hosts. where
the SSH setup runs.
Enable debug mode
For more output while you troubleshoot a failing build:
- Set the
VORTEX_DEBUGvariable to1in your CI provider's settings: a repository variable on GitHub Actions, or a project environment variable on CircleCI.
scripts/vortex-tooling.sh then prints every shell command it runs, each
one prefixed with +. The Lagoon deployment also prints the Lagoon CLI
output of a deployment that succeeds.
Runner disk space
Hosted runners come with a fixed amount of disk space, and running out of it produces a misleading failure. 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 stops during provisioning without reporting an error, suspect the disk first.
The fix that removes the cause is to reduce what has to fit: a sanitized
dump or a database container image instead of a full
dump. Vortex also keeps the build within its budget on both providers.
It prunes the Docker build cache and dangling images after the stack
starts. The test job removes the database dump from the runner after it's
copied into the container, so the job holds only 1 copy. What else helps
depends on the provider:
- On GitHub Actions, Vortex prints the disk state in the
testandbuildjobs of every run, and can reclaim space held by preinstalled toolchains. See GitHub Actions > Runner disk space. - On CircleCI, disk space isn't tied to the resource class, so upgrading it adds CPU and memory but no extra room for the build.
Ignore tool failures
Sometimes you want a build to 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 | 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 |
| 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 |
The code coverage threshold check has no variable of its own. To let it
pass, lower VORTEX_CI_CODE_COVERAGE_THRESHOLD instead. 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.