Vortex 1.42.0 is out, and its most important changes are about security. On 21 October 2026, 2 critical PHP vulnerabilities were published, and this release ships the PHP that fixes them. It also adds a static maintenance page that lets nginx answer every request on its own, so a site can keep requests away from PHP while a fix rolls out, for these 2 vulnerabilities and the next. Stage environments stop emailing real users, and CI now catches vulnerable npm packages.
Custom themes can opt into a Storybook component library, builds that rewrite files now fail CI, and the testing and front-end tooling move to Behat 4, ESLint 10 and Stylelint 17, so the upgrade work every project faces sooner or later arrives as a single tested step.
Highlights
Moved to the patched PHP in the Lagoon 26.10.0 images
On 21 October 2026, CERT-EU published advisory 2026-018(Opens in a new tab/window) on 2 critical PHP vulnerabilities, CVE-2026-109618 (GHSA-w8hw-w9fx-j79w(Opens in a new tab/window)) and CVE-2026-109619 (GHSA-6wmf-4rv5-p3rp(Opens in a new tab/window)). In the first, a request that runs out of memory leaves PHP's request memory corrupted. In the second, a request acts on state an earlier request left behind. Both score 9.2 and let an attacker run code on the server without logging in, and proofs of concept were already circulating. Only a patched PHP fixes them: 8.4.27 on the 8.4 line Vortex uses, which Lagoon shipped the same day in its 26.10.0 images.
This release moves every uselagoon/* image from 26.9.0 to 26.10.0: the 6 images built from .docker/*.dockerfile, plus uselagoon/mysql-8.4 and uselagoon/valkey-8 in docker-compose.yml. The update brings PHP 8.4.27 with it, and each environment stays exposed until the update is deployed there.
If you can't take the whole update straight away, move the uselagoon/* tags in .docker/*.dockerfile and docker-compose.yml to 26.10.0 on their own. That alone brings the patched PHP.
Added a static maintenance page that keeps requests away from PHP
Every patch takes time to land. The new image has to reach your project, pass CI and deploy to every environment, and until then the vulnerable PHP keeps answering requests.
The nginx image now ships a static maintenance page that takes PHP out of the request path. Set VORTEX_NGINX_MAINTENANCE_MODE=1 on an environment and redeploy it, and nginx answers every request, POST included, with HTTP status 503 and the page in .docker/config/nginx/maintenance.html, without passing anything to PHP. The site is offline for visitors while the page is on, but no web request can reach the vulnerable code. The page ships in the same release as the patched images, so a site could turn it on while these 2 vulnerabilities were announced and keep PHP out of reach until the patch was deployed. It stays in place for the next PHP vulnerability of this kind, and it covers a long deployment or a database restore just as well.
nginx reads the variable when its container starts, so turning the page on or off takes a redeploy. It's off by default. The page is a plain HTML file in your project, so edit .docker/config/nginx/maintenance.html to give it your site's branding. To preview it locally, run VORTEX_NGINX_MAINTENANCE_MODE=1 ahoy up nginx.
Serve a maintenance page(Opens in a new tab/window) on the Lagoon page has the steps for a hosted environment.
Rerouted email on stage environments
Stage usually runs on a recent copy of production, so its user table is full of real customer addresses. Anything a tester does there that sends mail, such as a password reset or an order confirmation, can land in a real person's inbox.
reroute_email(Opens in a new tab/window), which sends every outgoing message to a single address instead of its real recipient, now runs in every environment except local, ci and prod, so stage reroutes the way dev does. On Acquia it covers the stage and test environments, and on Lagoon it covers main, master, release/* and hotfix/* environments that aren't the production branch. The rule lives in web/sites/default/includes/modules/settings.reroute_email.php:
if (in_array($settings['environment'], [ENVIRONMENT_LOCAL, ENVIRONMENT_CI, ENVIRONMENT_PROD], TRUE)) {
// Deliver every message to its intended recipient. Local and CI capture
// outgoing mail by other means, and production sends to real recipients.
$config['reroute_email.settings']['enable'] = FALSE;
}
else {
// Send every outgoing message to the address above instead of to its
// intended recipient, unless that recipient matches the allowed list.
$config['reroute_email.settings']['enable'] = TRUE;
}
Rerouted messages go to DRUPAL_REROUTE_EMAIL_ADDRESS, unless the recipient matches DRUPAL_REROUTE_EMAIL_ALLOWED. The module itself now installs from the development modules script, scripts/provision-10-enable-dev-modules.sh, rather than the demo modules script, so it stays installed when a project replaces the demo provisioning with its own. It's also excluded from configuration export, so its enabled state stays out of config/default.
To deliver real mail from a stage environment that needs to, set DRUPAL_REROUTE_EMAIL_DISABLED=1 in that environment's hosting variables. The value has to be exactly 1, as the section on boolean settings flags below explains. After updating, your next ahoy drush cex drops reroute_email from core.extension.yml and config/default; commit that change.
The Settings page(Opens in a new tab/window) lays out what each environment turns on and off.
Audited npm packages in the security audit
Vortex already audited PHP dependencies with composer audit. The JavaScript side, the lint and test toolchain at the root and the theme's build tooling, wasn't checked, so an npm advisory could sit in a lock file until someone ran npm audit by hand.
The Security audit workflow now audits the root and theme package-lock.json files on both providers, failing on high and critical advisories. It runs them through audit-ci(Opens in a new tab/window), which wraps npm audit with a severity threshold and an allowlist. On CircleCI, every audit step now runs even when an earlier one fails.
Some advisories have no fix yet. For those, add an entry to package-audit.jsonc with a note on why it's acceptable and an expiry date; once the date passes, the entry stops applying and the audit fails again. The template ships one, for a braces advisory with no patched release:
{
"$schema": "https://github.com/IBM/audit-ci/raw/main/docs/schema.json",
"allowlist": [
{
"GHSA-vfj7-8cjw-p6xm": {
"active": true,
"notes": "No patched braces release exists yet. It is reached only through stylelint, which expands the glob patterns in this project's lint scripts, so no outside input reaches it and nothing from it ships with the site.",
"expiry": "2026-12-31"
}
}
]
}
The shipped lock files start clean as well: they were refreshed to clear the high-severity advisories that were open, so a project on the shipped lock files passes the audit with that 1 allowlisted exception. On GitHub Actions, an allowlisted advisory also stays out of code scanning, so accepting it clears it in both places. That part ships in drevops/vortex-tooling 1.5.1.
To accept an advisory, add it to the root or theme package-audit.jsonc with notes and an expiry. To report advisories without failing the build, set VORTEX_CI_NPM_AUDIT_IGNORE_FAILURE=1.
The JavaScript dependencies section(Opens in a new tab/window) of the Dependency audit page covers the threshold, the allowlist format and where findings appear.
Added an opt-in Storybook component library
A component-based theme is easiest to build one component at a time, but in a plain Drupal theme the only way to see a component is to find or build a page that renders it. Storybook(Opens in a new tab/window) is a workshop for building and reviewing UI components in isolation, so each component gets a page of its own.
Projects with a custom theme can now opt into it. Stories are written in Twig, as *.stories.twig files next to each component, and Drupal renders them through the storybook(Opens in a new tab/window) module, so what you review is the component's real markup. 3 ahoy commands cover the workflow:
ahoy storybook # Develop components: live Storybook that regenerates stories as they change
ahoy storybook-build # Rebuild the component library served at /storybook
ahoy storybook-stories # Generate stories only ("--watch" to keep regenerating)
Provisioning builds the library in local, ci and dev, so /storybook is ready on those environments without anyone running a command. To skip that build, set DRUPAL_STORYBOOK_SKIP=1.
To add it to a new project, answer yes to Storybook component library? during installation; the installer asks only when the project has a custom theme. To add it to an existing project, run ahoy update-vortex --interactive, refresh composer.lock and the theme package-lock.json, then run ahoy build.
The Storybook section(Opens in a new tab/window) of the Themes page walks through writing a story and running Storybook on the host.
Failed the build when it leaves a dirty working tree
A build step can change files without anyone noticing. One that rewrites exported configuration, recompiles assets or regenerates scaffold files still passes CI, and the difference only surfaces when the next developer builds and finds changes they didn't make.
The build job on both providers now ends with a check that fails when the build modified a tracked file or left an untracked one behind, and lists each path. It runs after the test steps, so a dirty tree never hides their results. On GitHub Actions it's the last step of the job:
- name: Check that the build left a clean working tree
run: ./vendor/bin/vortex-check-worktree-changes
continue-on-error: ${{ vars.VORTEX_CI_WORKTREE_CHANGES_IGNORE_FAILURE == '1' }}
To exclude a path your build generates and doesn't commit, set VORTEX_CHECK_WORKTREE_CHANGES_IGNORE. It takes comma-separated Git pathspecs and replaces the default list rather than adding to it, so carry the defaults over:
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
To see what the check reports without failing builds while you sort it out, set VORTEX_CI_WORKTREE_CHANGES_IGNORE_FAILURE=1.
The Working tree check section(Opens in a new tab/window) of the CI page explains what the defaults cover and why.
Moved Behat to 4 with drevops/behat-steps 4.x
Behat, the test runner Vortex uses for end-to-end scenarios, reached 4.0. It reads its configuration only from behat.php and no longer reads docblock annotations, so every project's suite has to move. behat-steps(Opens in a new tab/window), our library of ready-made Behat step definitions, moves to 4.x alongside it, and it takes over the job the Drupal Extension used to do.
FeatureContext now extends a single DrevOps\BehatSteps\DrupalContext that carries every library step, instead of extending the Drupal Extension's context and pulling in the library one trait at a time. Scenarios no longer need the @api tag, because the BehatStepsExtension picks a backend for each step: drupal, drush or blackbox. The shipped features use the 4.x step wording.
BEFORE
behat.yml YAML configuration
FeatureContext extends the Drupal Extension's DrupalContext,
plus 45 "use ...Trait;" lines
scenarios tagged @api to reach Drupal
custom steps docblock annotations
progress output drevops/behat-format-progress-fail
AFTER
behat.php PHP configuration
FeatureContext extends behat-steps' DrupalContext, which
carries every library step
scenarios no @api: the extension picks drupal, drush
or blackbox for each step
custom steps #[Given('...')] attributes
progress output Behat core, with inline_failures on
Behat 4 also needs the 3.x line of friends-of-behat/mink-extension, which only has an alpha release so far, so the template requires it directly at ^3.0@alpha, and drevops/behat-screenshot moves to 3.0. drevops/behat-format-progress-fail conflicts with Behat 3.33 and newer, so it's gone. Behat core's progress formatter runs with inline_failures in its place, printing failed, pending and undefined steps as they happen, and its failure banner reads --- FAILED --- where the old one read --- FAIL ---.
Every scenario now also runs the watchdog error check, which needs the dblog module.
The migration takes 7 steps, listed in order under Upgrading below, and the step renames are in the behat-steps MIGRATION.md(Opens in a new tab/window).
The Behat page(Opens in a new tab/window) documents the configuration and the step backends.
Moved ESLint to 10 and its import rules to eslint-plugin-import-x
npm now marks [email protected], the version both lock files held, as no longer supported, so every npm ci printed a deprecation warning, including the theme build inside the CLI image. Moving to ESLint 10 clears it, but a single plugin stood in the way: eslint-plugin-import, which checks that imports resolve and match what each module exports, doesn't support ESLint 10.
The import rules now come from eslint-plugin-import-x(Opens in a new tab/window), a fork of that plugin that does. Both eslint.config.mjs files, at the root and in the custom theme, load it in place of the original:
-import importPlugin from 'eslint-plugin-import';
+import { importX } from 'eslint-plugin-import-x';
- importPlugin.flatConfigs.recommended,
+ importX.flatConfigs.recommended,
- 'import/no-mutable-exports': ['warn'],
+ 'import-x/no-mutable-exports': ['warn'],
Rule names carry the fork's prefix, so import/no-mutable-exports becomes import-x/no-mutable-exports.
The rest of the front-end lint toolchain moved to its current majors too: Stylelint 17, eslint-plugin-yml 3 and eslint-plugin-no-jquery 6. Version 6 of the jQuery plugin runs on ESLint 10 as it is and breaks inside the fixupPluginRules() wrapper the configs used for it, so both configs now register it directly, and @eslint/compat is gone from both package.json files:
-import { fixupPluginRules } from '@eslint/compat';
- 'no-jquery': fixupPluginRules(noJQuery),
+ 'no-jquery': noJQuery,
Stylelint 17 and eslint-plugin-yml 3 need Node 22, so the root package.json now asks for Node 22.13.0 or later. That only matters if you run npm on the host, since the CLI container ships Node 24.
If you've customized either eslint.config.mjs, make the same changes in yours and rename every other import/* rule you configure. Check that any plugins or configs you've added support ESLint 10 and Stylelint 17. If you've also changed either package.json, run npm install in the root and the theme to update the lock files; otherwise ahoy fei installs from the shipped ones.
The ESLint page(Opens in a new tab/window) lists the presets and every rule the configuration sets on top of them.
Also in this release
Made Renovate auto-merge safer
Renovate(Opens in a new tab/window) opens pull requests for dependency updates, and it can merge the routine ones for you. A compromised release is usually caught and pulled within days, so a pipeline that merges a release the day it's published can let one into a project before anyone notices.
Each of the 6 update groups in renovate.json now carries its own automerge flag, and new installs choose the auto-merged groups in the installer, all of them by default. Those groups also wait until a release is 14 days old before opening a pull request:
"minimumReleaseAge": "14 days",
"automerge": true
Drupal core has no minimum age, so its security releases are proposed on the next daily run. The same change fixed a rule that silently disabled updates for the root package.json, so root npm dependencies now get updates in the weekly JavaScript group.
To change a group's behaviour, set its automerge to true or false, or edit its minimumReleaseAge, in renovate.json; an update keeps your current flags. Auto-merge needs "Allow auto-merge" enabled in the repository settings and at least 1 required status check that reports on deps/* branches. If a CircleCI project pins its own renovate/renovate image older than 41.150.0, raise it, because older images reject the new configuration.
The Dependency updates page(Opens in a new tab/window) describes each group and its flags.
Shipped Fast 404 for missing static files
A request for a missing image, stylesheet or script still has to be answered, and by default Drupal answers it the expensive way, with a full bootstrap to render a 404 page. On a busy site, or one that bots keep probing for files that were never there, that adds up.
Fast 404(Opens in a new tab/window), which answers those requests with a minimal 404 before Drupal boots, now ships with the template at ^3.7. It runs from settings.php through settings.fast_404.php, so the module is never enabled in Drupal. Vortex carried a settings override for it before, but its guard checked a path Composer never creates, so the fast path never ran.
The settings follow what the module documents. URL whitelisting is on, install.php and update.php stay out of the whitelist, and path and redirect checking are off, because the path check queries the database before Drupal has connected to it. Missing image style derivatives still pass through to Drupal, which generates each one on its first request, and only for a URL carrying a valid itok token.
Leave fast404_allow_anon_imagecache at TRUE. With FALSE, the module blocks derivative generation for logged-in users too, because it looks for SESS in cookie values instead of cookie names.
New installs get it by default, through the Fast 404 option in the installer's Modules list. To add it to an existing project, run ahoy composer require drupal/fast_404 before ahoy update-vortex, or select Fast 404 with ahoy update-vortex --interactive.
The Fast 404 entry(Opens in a new tab/window) on the Contributed modules page lists every setting it ships with.
Sent security audit findings to GitHub code scanning
A failed audit step tells you something is wrong, and finding out what meant reading the CI log.
On GitHub Actions, the Security audit workflow now converts composer audit and npm audit results to SARIF, the format GitHub's code scanning reads, and uploads them together with Gitleaks' own report. Findings appear under Security > Code scanning in the repository, where you can track and dismiss them, and every report is also attached to the run as the security-findings artifact. The audit steps alone still decide whether the build passes.
It applies automatically to GitHub Actions projects. Private repositories need a code scanning entitlement for the upload.
Where findings appear(Opens in a new tab/window) shows how to read a finding and dismiss one.
Turned off session upload progress in the php container
PHP can track the progress of a file upload by writing request data into the session before any application code runs. Drupal never reads it, so all it adds is attack surface on every multipart request.
The php container now turns it off, in a new .docker/config/php/custom.ini:
[Session]
; Drupal does not use session upload progress, so it is turned off to reduce
; the attack surface of multipart requests.
session.upload_progress.enabled = Off
File uploads, anonymous ones included, work as before. The setting applies wherever the Vortex images run: locally, in CI and on Lagoon. On Acquia, the platform's own PHP settings apply.
The same change gives each container a home for your own PHP settings. To change a PHP setting for web requests, edit .docker/config/php/custom.ini; for Drush and Composer, edit .docker/config/cli/custom.ini. Then rebuild with ahoy up --build. Both files override Lagoon's defaults, and in the cli container drush/php-ini/drush.ini still wins for any setting it also sets.
The Docker page(Opens in a new tab/window) covers both files, and the Security page(Opens in a new tab/window) the reasoning behind the upload setting.
Made boolean settings flags switch on only with 1
The settings includes read their on/off switches from environment variables, and they used to treat any non-empty value as on. So DRUPAL_SHIELD_DISABLED=false disabled Shield(Opens in a new tab/window), the module that puts a password prompt in front of non-production sites, which is the opposite of what anyone writing that line meant.
Every boolean flag now switches on only with the exact value 1:
if (getenv('DRUPAL_SHIELD_DISABLED') === '1') {
That covers DRUPAL_CLAMAV_ENABLED, DRUPAL_REDIS_ENABLED, DRUPAL_REROUTE_EMAIL_DISABLED, DRUPAL_SHIELD_DISABLED, DRUPAL_SHIELD_ALLOW_ACME_CHALLENGE and DRUPAL_TMP_PATH_IS_SHARED.
Before you deploy, change any true, yes or other non-empty value to 1 in .env, your CI variables and your hosting environment variables. A value that isn't 1 now reads as off, so Redis or ClamAV stop being configured, Shield or email rerouting come back on, and Acquia falls back to /tmp instead of the shared temporary path.
Every flag and its default is listed in the variables reference(Opens in a new tab/window).
Made updates work out the previous install state at run time
To update a project safely, the installer has to know which files are still exactly as the template shipped them, since those are the ones it may remove when the template retires them. Vortex 1.41.0 kept that reference in a .vortex-manifest.json committed to each project.
The installer now rebuilds the reference at run time from the version named in your README's Vortex badge, so nothing extra lives in your repository. The update deletes the existing .vortex-manifest.json; commit that deletion with it. Keep the README badge in its shipped form, since that's where the installer reads the version.
Every edit of yours that an update overwrites is now written to .logs/vortex-update.md, with the diff you need to re-apply it.
The Updating Vortex page(Opens in a new tab/window) walks through an update, including working through .logs/vortex-update.md.
Refreshed the database image nightly on both CI providers
Projects that ship their database inside a container image, by setting VORTEX_DB_IMAGE, rely on a nightly CI run to rebuild that image from a fresh dump. Until now only CircleCI did it.
On GitHub Actions, the scheduled database run now does the same job: it imports the fetched dump into a clean drevops/mariadb-drupal-data(Opens in a new tab/window) base image and pushes the result with vortex-push-db-image. For these projects the job also caches the image archive instead of the dump.
On CircleCI, the nightly job rebuilt the database from the previous VORTEX_DB_IMAGE instead of importing the fetched dump, or failed when that image didn't exist yet. Its Export DB step now starts the stack from the clean base, so the pushed image holds the fresh data.
To run it on GitHub Actions, give the VORTEX_CONTAINER_REGISTRY_USER and VORTEX_CONTAINER_REGISTRY_PASS secrets push access. If you set VORTEX_FETCH_DB_CONTAINER_REGISTRY_IMAGE_BASE in .env as a fallback, remove it, since it now replaces the database image on every container_registry fetch. Projects without VORTEX_DB_IMAGE keep caching the dump and push nothing.
The Database stage(Opens in a new tab/window) of the CI page covers how the database is cached and refreshed.
Installed PCOV with PIE instead of PECL
The CLI image installs PCOV(Opens in a new tab/window), a coverage-only PHP extension that PHPUnit collects code coverage through, and it used to fetch it from PECL, PHP's long-standing extension repository. That tied every CLI image build to pecl.php.net: when the site was down, the build failed at that step, locally and in CI alike.
The image now installs it with PIE(Opens in a new tab/window), the PHP Installer for Extensions:
pie install --no-build-tools-check pecl/pcov
PIE resolves pecl/pcov through Packagist and downloads it from the extension's GitHub repository, so a PECL outage no longer stops a build. It applies automatically on the next image build.
Coverage collection is explained on the Debugging page(Opens in a new tab/window).
Updated the DrevOps packages
Vortex runs on a set of packages we maintain ourselves, and 8 of them moved in this release. Each one links to its release page.
vortex-tooling(Opens in a new tab/window), the scripts that provisioning, deployments and CI call, goes from ~1.4.0 to ~1.5.0. 1.5.0 adds the scripts behind this release's working tree check, code scanning upload and Storybook commands. 1.5.1 moves artifact deployments to git-artifact 1.9.0 and teaches the SARIF conversion to skip allowlisted npm advisories. Composer picks up 1.5.1 within the ~1.5.0 constraint.
behat-steps(Opens in a new tab/window), our library of ready-made Behat step definitions, goes from ^3.14.2 to ^4.0@rc, which installs the newest 4.0 release candidate until 4.0.0 ships. The 4.0 line folds the Drupal Extension and Drupal Driver into this single package, runs on Behat 4 and Drupal 12, and drops PHP 8.2 and Drupal 10. Its MIGRATION.md(Opens in a new tab/window) maps every renamed step, method, class, setting and tag from 3.14, and a migrate-behat-steps skill lets an AI coding agent apply it to your project. The Behat section above covers what changes in a Vortex project.
behat-screenshot(Opens in a new tab/window), which captures screenshots when a Behat step fails, goes from ^2.6.0 to ^3.0.0. It adds Behat 4 support and raises its PHP floor to 8.3. Parts of its public PHP API were renamed without aliases, among them the fullscreen capture option, which is now is_fullscreen, so code that extends or calls its context needs checking.
phpcs-standard(Opens in a new tab/window), our coding standard for PHP_CodeSniffer, goes from ^1.0.0 to ^1.1.0, and it can report errors on code that passed before. Its ParameterNaming sniff now checks Drupal subclasses, so a parameter a subclass renamed from its parent gets flagged; rename it to match, or suppress it. Arrow function parameters moved from LocalVariableNaming to ParameterNaming, so a suppression that names the old sniff no longer covers them. To switch the Drupal subclass check off, set the sniff's drupalRoot property to false. It needs PHP 8.3.
git-artifact(Opens in a new tab/window), which assembles the deployment artifact and pushes it to your hosting repository, goes from 1.7.0 to 1.9.0. 1.8.0 adds the --source option, which Vortex now passes, deprecates --src, and fixes a crash on a bare [timestamp] token in --branch or --message. 1.9.0 updates its dependencies to clear 6 security advisories. If you pin your own version with VORTEX_DEPLOY_ARTIFACT_GIT_ARTIFACT_VERSION, it has to be 1.8.0 or later, with the matching VORTEX_DEPLOY_ARTIFACT_GIT_ARTIFACT_SHA256.
ci-runner(Opens in a new tab/window), the image CI jobs run in, goes from 26.8.0 to 26.10.0 with routine tool updates, among them PHP 8.4.26, Node.js 24.21.0, Composer 2.10.3 and Docker Compose 5.5.1.
mariadb-drupal-data(Opens in a new tab/window), the base image for projects that ship their database in a container, goes from 26.8.1 to 26.10.0. Its seed.sh can now sanitize the database before capturing it: set SANITIZE_PROCEED=1 and it runs the queries from SANITIZE_FILE, by default ./scripts/sanitize.sql, so the data they remove never reaches the image. FORCE_MYSQL_UPGRADE and DST_IMAGE are now MARIADB_FORCE_UPGRADE and DESTINATION_IMAGE; the old names still work, with a deprecation notice.
docker-wait-for-dependencies(Opens in a new tab/window), the container that holds the stack back until its services answer, goes from 26.6.1 to 26.10.0. A check still running at TIMEOUT_LENGTH is now killed and reported as a timeout, where it used to pass once it finished. If a service of yours needs longer to come up, set TIMEOUT_LENGTH on the wait-for-dependencies service in docker-compose.yml.
drevops/behat-format-progress-fail is no longer shipped. Behat core's inline_failures output replaces it, as the Behat section above explains.
Rewrote the documentation in one voice
81 documentation pages were rewritten in a consistent technical-writing voice, and the pass corrected statements that had drifted from the template: the minimum-stability example, the tools ahoy lint covers, a broken trigger-branch link, and the description of how .env and .env.local reach the containers.
Fixes
Made Gitleaks and DCLint scan the repository
Gitleaks(Opens in a new tab/window), which scans a repository for committed credentials, and DCLint(Opens in a new tab/window), which lints Docker Compose files, run in CI from their own containers. Both mounted the checkout with docker run -v "${PWD}", a path the Docker engine couldn't see: CircleCI's remote engine has no access to the checkout, and on GitHub Actions the path belongs to the job container. So both scanned an empty directory and passed.
CircleCI now copies the files into the container with docker create and docker cp, and GitHub Actions mounts them with --volumes-from. Both linters check the real files on both providers.
That means a build can start failing on findings that were always there. To clear a Gitleaks false positive, allowlist it in .gitleaks.toml, and fix what DCLint reports in your Compose files. To report without failing while you work through them, set VORTEX_CI_GITLEAKS_IGNORE_FAILURE=1 or VORTEX_CI_DCLINT_IGNORE_FAILURE=1.
The Secret scanning page(Opens in a new tab/window) shows the allowlist format, and the DCLint page(Opens in a new tab/window) the Compose rules it applies.
Made nginx start after php
Locally, nginx and php used to start in parallel. When php hadn't joined the network yet, nginx exited with host not found in upstream "php" and stayed down, so the site was unreachable until the next ahoy up.
docker-compose.yml now lists php under nginx's depends_on, so Docker Compose starts php first.
It applies on your next ahoy up. Lagoon ignores depends_on, so deployments are unaffected.
Made an unknown database source fail the fetch
VORTEX_FETCH_DB_SOURCE tells Vortex where to fetch the database from. A typo in it used to run no fetch script at all and still report a finished fetch, so a misspelled source looked exactly like a successful one.
vortex-fetch-db now checks the value first and stops with the list of sources it supports:
Unsupported database source "urll" in VORTEX_FETCH_DB_SOURCE. Use one of: url, ftp, acquia, lagoon, container_registry, s3, none.
The same check covers indexed variants such as VORTEX_FETCH_DB2_SOURCE. The list also shows the only new value: to skip fetching on purpose, for a project with no database to fetch, set VORTEX_FETCH_DB_SOURCE=none.
Fetching a database(Opens in a new tab/window) on the Provision page covers each source and its variables.
Improved the Acquia integration
The Acquia integration got a round of fixes. Its hook wrappers now stop with an error when a required value such as VORTEX_ACQUIA_KEY is empty, instead of letting a task run without credentials, and the Acquia task scripts name a rejected credential or a missing application or environment when they fail.
VORTEX_PROVISION_ACQUIA_SKIP now takes effect where it used to be ignored, so remove VORTEX_PROVISION_ACQUIA_SKIP=1 from any environment that should provision.
The Acquia page(Opens in a new tab/window) covers the hooks and their variables.
Upgrading
Run the update. The installer applies the template changes, removes template files you never touched, and logs every edit of yours it overwrote in .logs/vortex-update.md so you can re-apply it. The template changes include the uselagoon/* images at 26.10.0, so deploying the update is what brings the patched PHP.
What the installer can't reach is your lock files, your test suite, your CI and hosting settings, and code you wrote yourself. That leaves 28 things to check, and the first has to come before the rest, because CI calls scripts that only exist in the new tooling.
- Update
drevops/vortex-tooling. Runahoy composer update drevops/vortex-toolingand commitcomposer.lock. Thebuildjob, the GitHub Actions security audit and the Storybook commands call scripts that only exist from1.5.0. - Remove the packages Behat 4 replaces. Drop
drupal/drupal-extension,drupal/drupal-driver(if listed) anddrevops/behat-format-progress-failfromcomposer.json, and requirelullabot/mink-selenium2-driver, which@javascriptscenarios need. - Port
behat.ymltobehat.php. Behat 4 reads onlybehat.phpand ignores abehat.ymlwithout an error, so delete it once it's ported. - Point
FeatureContextatDrevOps\BehatSteps\DrupalContext. Extend it and remove theuselines for library traits. Custom contexts built on Drupal Extension classes need porting. - Convert step and hook annotations to attributes.
@Given,@When,@Then,@BeforeScenario,@AfterScenarioand@Transformdocblocks become attributes such as#[Given('...')]. - Update your feature files. Remove the
@apiand@drushtags, and rename steps using the behat-steps MIGRATION.md(Opens in a new tab/window). - Enable
dblog, or switch off the watchdog check. A site withoutdblogsets'watchdog' => ['enabled' => FALSE]understepsin theBehatStepsExtensionconfiguration. - Update anything that parses Behat output. The failure banner reads
--- FAILED ---instead of--- FAIL ---. - Update a customized
eslint.config.mjs. ImportimportXfromeslint-plugin-import-x, useimportX.flatConfigs.recommended, and rename everyimport/*rule toimport-x/*. - Register
eslint-plugin-no-jquerydirectly in a customizedeslint.config.mjs. Version 6 breaks insidefixupPluginRules(), so use'no-jquery': noJQuery, and keep@eslint/compatin yourpackage.jsononly if another plugin still needs it. - Check that your own ESLint plugins and configs support ESLint
10. If you've changed eitherpackage.json, runnpm installin the root and the theme; otherwiseahoy feiinstalls from the shipped lock files. - Check that your own Stylelint plugins and configs support Stylelint
17. The root and theme toolchains moved to Stylelint 17 withstylelint-config-standard40. - Run npm on the host with Node
22.13.0or later. The rootpackage.jsonnow requires it; the CLI container already ships Node 24. - Set every boolean settings flag to exactly
1. Do it in.env, CI variables and hosting variables, forDRUPAL_CLAMAV_ENABLED,DRUPAL_REDIS_ENABLED,DRUPAL_REROUTE_EMAIL_DISABLED,DRUPAL_SHIELD_DISABLED,DRUPAL_SHIELD_ALLOW_ACME_CHALLENGEandDRUPAL_TMP_PATH_IS_SHARED. - Set
DRUPAL_REROUTE_EMAIL_DISABLED=1on any stage environment that must send real mail. Every other stage environment now reroutes. - Commit the
reroute_emailexport change. Your nextahoy drush cexdrops it fromcore.extension.ymlandconfig/default. - Commit the deletion of
.vortex-manifest.json. The update removes it. - Tell the working tree check about generated files. Add the paths your build generates to
VORTEX_CHECK_WORKTREE_CHANGES_IGNORE, keeping the defaults, or setVORTEX_CI_WORKTREE_CHANGES_IGNORE_FAILURE=1to report without failing. - Allowlist or fix npm advisories. Add assessed ones to
package-audit.jsonc, or setVORTEX_CI_NPM_AUDIT_IGNORE_FAILURE=1while you work through them. - Clear Gitleaks and DCLint findings. Allowlist Gitleaks false positives in
.gitleaks.tomland fix what DCLint reports, or setVORTEX_CI_GITLEAKS_IGNORE_FAILURE=1orVORTEX_CI_DCLINT_IGNORE_FAILURE=1. - Remove
VORTEX_FETCH_DB_CONTAINER_REGISTRY_IMAGE_BASEfrom.envif you set it as a fallback. Only database-in-image projects are affected. - Remove
VORTEX_PROVISION_ACQUIA_SKIP=1from any Acquia environment that should provision. The variable takes effect now. - Raise a pinned CircleCI
renovate/renovateimage older than41.150.0. Older images reject the new Renovate configuration. - Fix the subclass parameters
drevops/phpcs-standard1.1.0flags. Rename a parameter a subclass renamed from its parent to match the parent, or suppress the error. - Point arrow function suppressions at
ParameterNaming. A suppression that namesLocalVariableNamingno longer covers arrow function parameters. - Check custom code that extends
drevops/behat-screenshot. Its3.0API renames have no aliases, and thefullscreencapture option isis_fullscreennow. - Pin
git-artifactto1.8.0or later if you override it. SetVORTEX_DEPLOY_ARTIFACT_GIT_ARTIFACT_VERSIONwith its matchingVORTEX_DEPLOY_ARTIFACT_GIT_ARTIFACT_SHA256, since older releases reject--source. - Raise
TIMEOUT_LENGTHif a dependency check now times out. Set it on thewait-for-dependenciesservice indocker-compose.yml.
Runtime for this release: PHP 8.4.27, Drupal core ~11.4.11, Lagoon containers 26.10.0, and drevops/ci-runner 26.10.0.
Full release notes, including every fix and dependency bump, are on the 1.42.0 release page(Opens in a new tab/window). If you're new to Vortex, vortextemplate.com(Opens in a new tab/window) is the place to start.