druid / druid
druid - a command line shell and Unix scripting interface for upgrading Drupal core and contrib modules.
Requires
- php: >=8.2
- ext-json: *
- ext-simplexml: *
- composer/semver: ^3.3
- symfony/console: ^6.4 || ^7.0
- symfony/http-client: ^6.4 || ^7.0
- symfony/process: ^6.4 || ^7.0
- symfony/yaml: ^6.4 || ^7.0
Requires (Dev)
- phpunit/phpunit: ^10.5 || ^11.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-30 01:30:17 UTC
README
druid is a command line shell and Unix scripting interface for upgrading Drupal core and contrib.
It works like Drush (@alias, --root, --uri, -y, --simulate), uses Drush when the project has it,
and keeps track of every local Drupal site that needs security or compatibility updates.
- Scan: security advisories (composer audit +
drush pm:security), unsupported projects, minor and major updates, and drupal.org release data, for one site or all of them. - Report: coloured tables, or JSON / CSV / Markdown / HTML for every tracked site.
- Plan: numbered steps with the exact Composer commands, always targeting the safest release (the lowest version that clears every advisory, in the same major).
- Upgrade: a whole site, core only, or individual modules. Each step is snapshotted and verified. If it fails, druid tries another strategy, then defers the step and carries on. It does not abort the run.
- Major upgrades (e.g. 10 → 11): upgrade_status readiness check, then core first (with the contrib it needs and
the composer-drupal-lenient plugin for modules that haven't declared support yet), then the remaining contrib
one by one, then drupal-rector and
.info.ymlfixes for custom code.
Requires PHP 8.2+ and Composer 2 (2.7+ recommended for --minimal-changes).
Install
druid installs the same way as Drush.
Per project (recommended)
composer require --dev druid/druid vendor/bin/druid status
Global launcher
composer global require druid/druid druid status
Make sure Composer's global bin directory is on your PATH (composer global config bin-dir --absolute).
Like the Drush launcher, a global druid hands off to the project's own vendor/bin/druid when you run it inside a
project that has one. Set DRUID_NO_HANDOFF=1 to always use the global copy.
Until a stable release is tagged, require the development version: composer require --dev druid/druid:dev-main.
From a checkout
git clone git@github.com:ecpoelman/druid.git ~/druid && cd ~/druid && composer install ln -s ~/druid/bin/druid ~/.local/bin/druid # or add ~/druid/bin to PATH
Or add the checkout as a path repository in a project:
"repositories": [{"type": "path", "url": "../druid"}]
then composer require --dev druid/druid:@dev.
PHAR
box compile # https://github.com/box-project/box, uses box.json
./druid.phar status
Quick start
druid site:discover # find sites in ~/Sites, Herd, DDEV and Drush alias files druid sites # tracked sites and their last known status druid report --all # security / update overview of every site cd ~/Sites/mysite druid scan # what needs updating, colour-coded by severity druid plan # the steps and exact commands (security scope) druid upgrade # apply them
Target any tracked site from anywhere with @name, as in Drush:
druid @mysite plan --minor druid @mysite upgrade -y
Commands
| Command | Aliases | What it does |
|---|---|---|
status |
st |
Versions, tooling, environment (Herd/DDEV/Lando), Git state, last scan |
scan |
sec |
Advisories, unsupported projects, minor/major updates (--all, --format=json) |
report |
rp |
One or all sites as table, json, csv, md or html (--output=file) |
plan |
pl |
Outline steps and commands without changing anything |
upgrade |
up |
Upgrade the whole site: security by default, --minor, --major |
upgrade:core [version] |
up:core |
Core only: security/latest patch, --minor, an exact version, or 11 |
upgrade:module <names>... |
up:m |
Individual modules, themes or libraries (--to, --major, --latest) |
upgrade:major [target] |
up:major |
Move to the next Drupal major (--rector applies drupal-rector) |
upgrade:check |
readiness |
Drupal-major readiness: every module's support, PHP, Drush, upgrade_status |
rollback |
rb |
Restore Composer files (and with --with-db the database) from a snapshot |
site:add / site:remove / site:list / site:discover |
sa, sites, discover |
Manage tracked sites |
init |
Write a commented druid.yml into the project and track it |
|
doctor |
Check PHP, Composer, Drush, DDEV/Lando/Herd, network and storage |
Global options: @name / --site, -r|--root, -l|--uri, -y|--yes, --simulate, -v (full Composer output).
Scopes
| Flag | Includes |
|---|---|
(default) --security |
Releases that fix advisories, choosing the lowest safe one |
--minor |
Security, plus the latest minor/patch of every direct dependency within its current major |
--major |
Minor, plus the next Drupal core major and the contrib moves it needs |
--latest |
Prefer the newest release instead of the minimum safe one |
Exit codes
0 everything done · 2 finished, but some steps were deferred · 1 failed (nothing could run). Exit codes make druid easy to script, e.g. in cron or CI:
druid upgrade --all -y || echo "some sites need attention"
How an upgrade runs
-
Preflight: checks that Composer works, warns about uncommitted composer files, checks that Drush can bootstrap Drupal, saves a snapshot of
composer.json/composer.lock(and patches files) under~/.druid/snapshots, and takes a database dump withdrush sql:dumpwhen possible. -
Steps, core first: Drupal core packages always move together. Contrib stays locked until core succeeds. A step whose package is already at the target (often moved by an earlier step) is skipped.
-
The strategy ladder, tried for every step until one works:
Strategy Composer approach isolatedupdate pkg:VER --with-dependencies --minimal-changes; composer.json is only touched when its constraint must changerequire_bumpraise the constraint explicitly, keeping its style ( ^,~or exact pin)blockersupdate together with the packages Composer and why-notname as blockersall_dependencies--with-all-dependenciesno_audit_blockingretry with COMPOSER_NO_SECURITY_BLOCKING=1when audit blocking is in the waycore_firstupdate core within its major first when a module needs a newer core step_downother safe releases near the target (closest first) lenientmajor upgrades: allow modules through mglaman/composer-drupal-lenientEvery failed attempt is rolled back (and vendor re-synced) before the next one. Some failures are fixed automatically: network retries,
COMPOSER_MEMORY_LIMIT=-1, read-onlysites/defaultduring scaffolding, and trusted Composer plugins that a new release pulls in (such assymfony/runtimein Drupal 11). When a failure reveals something new, such as audit blocking, the ladder runs a second time with that knowledge. -
Verification after each step:
composer validateanddrush cache:rebuild(withupdatedbwhen the new code needs it). A step that breaks the site is rolled back. -
Defer, don't fail: a step that still fails is recorded with a diagnosis, hints and the manual command, and the run continues with the next step.
-
After the run:
drush updatedbanddrush cache:rebuild(configurable, plus optionalconfig:export), a history entry in~/.druid/state, and a rescan to confirm what's left.
Major upgrades
druid upgrade:check # compatibility of every module, PHP, Drush druid upgrade:check --install-upgrade-status # also scan code for deprecations (upgrade_status) druid plan --major # review the full path druid upgrade:major --rector # do it, applying drupal-rector to custom modules
Core goes first, then contrib, then custom code:
- core moves to the latest minor of the current major;
- all core packages move to the new major together with what they can't move without: contrib whose installed release doesn't support the new major (raised to the first line that does), Drush 13 for Drupal 11, and the lenient allow-list for modules with no declared support;
- the remaining contrib updates run one at a time on the new core (tried together first; set
upgrade.batch: falseindruid.ymlto always go one by one); - custom modules and themes are patched: drupal-rector, then
core_version_requirement.
Drupal can't boot halfway through, so drush updatedb and cache:rebuild run once, at the end.
Along the way it:
- reads each release's real
drupal/corerequirement from packages.drupal.org (the release feed is sometimes looser), and uses a pre-release only when it is the sole release that supports the new major; - adds the contrib replacement of enabled core modules the new major removes (
tour,forum,book, … in Drupal 11;color,rdf,seven, … in Drupal 10) to the core step; - stops before the core step when PHP is pinned too low (
config.platform.php,pantheon.yml,.ddev/config.yaml) and prints the exact fix; - releases transitive packages that hold a dependency back (e.g.
chi-teck/drupal-code-generatorpinningsymfony/console6) when Composer reports "found … but these were not loaded"; - defers steps that need the new core and retries them once core has moved;
- adds the new major to
core_version_requirementin custom modules and themes, so Drupal doesn't mark them incompatible; - checks custom code with phpstan-drupal's deprecation rules when upgrade_status isn't installed (both ship
with
drupal/core-dev).
After a major upgrade, a normal druid upgrade repairs leftovers: it reinstalls removed core modules that are
still enabled from contrib, and fixes custom .info.yml files. druid upgrade --rector applies drupal-rector
at any time.
Configuration
Settings are merged in this order (later wins): built-in defaults → ~/.druid/druid.yml → <project>/drush/druid.yml
→ <project>/druid.yml. druid init writes a commented example.
environment: auto # auto | native | ddev | lando discovery: paths: [~/Sites] upgrade: backup: true verify: true strategies: [isolated, require_bump, blockers, all_dependencies, no_audit_blocking, core_first, step_down, lenient] post_update: [updatedb, cache:rebuild] export_config: false trusted_plugins: [symfony/runtime, php-http/discovery, cweagans/composer-patches] packages: exclude: [drupal/patched_module] pin: drupal/core: "~10.6.0"
Storage lives in ~/.druid (override with DRUID_HOME): sites.yml, state/, snapshots/, cache/.
DDEV and Lando projects are detected automatically: Composer and Drush then run inside the container
(ddev exec, lando composer), so PHP versions and databases match the site.
Development
composer install composer test # PHPUnit