Sync. Update. Deploy. Suite.
An opinionated orchestrator for Drupal development workflows, implemented as Drush commands. Inspired by the Acquia BLT project.
SUDS gives every developer on a project — and every CI pipeline — the same commands for the same tasks. Syncing a database, deploying a build artifact, applying updates after a pull: the answers are always drush suds:sync, drush suds:deploy, drush suds:update, regardless of which project you are working on.
SUDS is built around two workflows.
Local development sync — suds:sync pulls a database from a configured source environment, installs Composer dependencies, sanitizes PII from the local database, optionally syncs managed files, and applies pending updates (cache rebuild, database updates, configuration import) in a single command. New developers get a working local environment in one step; existing developers stay current with the same command.
Artifact deployment — suds:deploy assembles a production-ready artifact from the project source: it rsyncs the project into a clean directory, runs configured build steps (asset compilation, etc.), commits the result, and force-pushes to a deployment repository. Hosting platforms that deploy from a git repository receive a clean artifact that contains only what production needs — no build tooling, test files, or local configuration.
Both workflows are driven by suds.yml in the project root. One file, committed to the repository, defines the workflow for the entire team.
- PHP 8.3 or later
- Drush 13
- Drupal 10.4 or 11
Install SUDS via Composer:
composer require bounteous-inc/sudsSUDS belongs in require, not require-dev. Like Drush itself, it is an operational tool: suds:update runs on production servers, suds:deploy runs in CI pipelines, and both need to be present in the deployed vendor directory. Adding it to require-dev would exclude it from artifact builds and break server-side execution.
Drush discovers the commands automatically via the extra.drush.services entry in composer.json. No additional registration is needed.
Adding SUDS to a new project:
composer require bounteous-inc/suds
drush suds:init # creates suds.yml and prompts for a project name
drush suds:doctor # verifies the environment is correctly configuredEdit the generated suds.yml to set your deployment repository URL and default sync source, then commit it. See Configuration for available keys.
Onboarding a new developer to an existing project:
git clone <project-repo>
cd <project>
composer install # the only manual bootstrap step
drush suds:doctor # verify the environment
drush suds:sync @devAfter the initial composer install, suds:sync runs it automatically on every subsequent sync so dependencies stay current as the project evolves.
SUDS configuration lives in suds.yml in the project root. You only need to define values that differ from the defaults — drush suds:config:dump --defaults shows every available key with its built-in default value, and drush suds:config:dump shows the resolved configuration for the current project. Individual keys can be inspected directly: drush suds:config:dump sync.db.export_dir.
Configuration is assembled from four layers in order, each overriding the previous:
| Layer | File | Committed | Purpose |
|---|---|---|---|
| Built-in defaults | (shipped with SUDS) | — | All keys and their default values |
| Project config | suds.yml |
Yes | Shared team configuration |
| CI overrides | suds.ci.yml |
Yes | Loaded only when the $CI environment variable is set |
| Local overrides | suds.local.yml |
No | Per-developer overrides — add to .gitignore |
Associative (keyed) values merge recursively. A partial override in suds.yml preserves all other keys in that section — you do not need to repeat values you are not changing:
# suds.yml — only override what differs from the defaults
drupal:
root: docroot # overrides the default 'web'; drupal.profile is untouchedLists (indexed arrays) replace entirely. To add paths to the artifact exclusion list without discarding the built-in defaults, use deploy.exclude_extra rather than deploy.exclude:
deploy:
exclude_extra:
- private/
- secrets.txtsuds.ci.yml is intended for values that should only apply in CI — typically deploy identity and any environment-specific overrides. It is committed to the repository and excluded from the artifact by default:
# suds.ci.yml
deploy:
git:
name: 'My CI Pipeline'
email: 'ci@example.com'Every orchestration command exposes pre and post hooks — lists of shell commands that run at defined points in the workflow. Hooks are configured in suds.yml and run in the project root (build steps in suds:deploy run in the artifact directory instead).
Execution context matters for suds:update hooks. Update hooks run wherever suds:update executes — on the server when invoked via a remote alias (see suds:update), or locally when invoked directly. Use update.hooks for operations that belong on the target environment: toggling maintenance mode, triggering a search index rebuild, sending a deployment notification from the server. Use deploy.hooks.post_deploy for operations that must run on the CI machine after the push: triggering downstream pipelines, posting to a chat webhook from a CI secret, or invalidating a CDN cache via an API key that only CI holds.
| Command | Hooks |
|---|---|
suds:setup |
setup.hooks.pre_setup, setup.hooks.post_setup |
suds:sync |
sync.hooks.pre_sync, sync.hooks.post_sync |
suds:update |
update.hooks.pre_update, update.hooks.post_update |
suds:deploy |
deploy.hooks.pre_deploy, deploy.hooks.post_deploy |
Example: configure local settings and seed content after a sync
sync:
hooks:
post_sync:
- cp .env.example .env.local
- drush config:set system.site name "Local Dev" --yesExample: validate, build assets, and notify after a deploy
deploy:
build_steps:
- npm ci
- npm run build
hooks:
pre_deploy:
- composer validate:all
post_deploy:
- curl -X POST https://hooks.example.com/deploy-notificationThe following variables are set automatically before any suds:deploy hooks or build steps run. They are also available in deploy.repo.branch and deploy.commit_message:
| Variable | Value |
|---|---|
$SUDS_BRANCH |
Current git branch name |
$SUDS_HASH |
Full HEAD commit SHA |
$SUDS_SHORT_HASH |
First 8 characters of HEAD SHA |
deploy:
commit_message: "Deploy $SUDS_BRANCH [$SUDS_SHORT_HASH]"
repo:
branch: "$SUDS_BRANCH-build"All commands are prefixed with suds: and have su- short aliases.
Display the resolved project configuration (defaults merged with suds.yml overrides).
# Show resolved config for the current project
drush suds:config:dump
# Show the value of a single key (dot-notation)
drush suds:config:dump sync.db.export_dir
# Show built-in defaults only, ignoring suds.yml
drush suds:config:dump --defaults| Argument | Description |
|---|---|
key |
Dot-notation key to inspect (e.g. sync.db.export_dir). When omitted, the full config tree is shown. |
| Option | Default | Description |
|---|---|---|
--defaults |
disabled | Show built-in default values only, ignoring suds.yml |
Initialize a new SUDS-managed Drupal project by creating suds.yml. Also runs suds:scaffold:quality unless --skip-quality-scaffold is passed.
# Interactive — prompts for the project name and uses auto-detected webroot
drush suds:init
# Non-interactive — suitable for CI
drush suds:init --name="My Project" --drupal-root=web
# Skip quality tooling scaffolding
drush suds:init --skip-quality-scaffold| Option | Default | Description |
|---|---|---|
--name |
(prompt) | Project name; skips the interactive prompt when provided |
--drupal-root |
(auto-detect or prompt) | Drupal webroot directory relative to the project root; auto-detected from web/, docroot/, or html/ when omitted |
--skip-quality-scaffold |
disabled | Skip scaffolding quality tool config files |
Scaffold code quality configuration files into the project root: grumphp.yml, phpcs.xml.dist, and phpstan.neon. Files are pre-configured for a Drupal site with custom modules and themes under the configured webroot. Existing files are left untouched unless --force is passed.
suds:init runs this command automatically. Use it directly to add quality tooling to an existing project, or to regenerate files after deleting them.
# Scaffold using drupal.root from suds.yml
drush suds:scaffold:quality
# Scaffold with an explicit webroot
drush suds:scaffold:quality --drupal-root=docroot
# Overwrite any existing quality tool config files
drush suds:scaffold:quality --forceAfter scaffolding, require the quality tooling dependencies:
composer require --dev phpro/grumphp squizlabs/php_codesniffer drupal/coder dealerdirect/phpcodesniffer-composer-installer phpstan/phpstan mglaman/phpstan-drupal phpstan/phpstan-deprecation-rules ergebnis/composer-normalize vincentlanglet/twig-cs-fixerGrumPHP's composer plugin installs pre-commit and commit-msg git hooks automatically on composer install.
| Option | Default | Description |
|---|---|---|
--drupal-root |
(read from suds.yml) |
Drupal webroot directory; overrides drupal.root from suds.yml |
--force |
disabled | Overwrite files that already exist |
The scaffolded configuration includes:
- GrumPHP — runs composer validation, PHP_CodeSniffer, PHPStan, YAML linting, Twig CS, a debug-artifact blacklist, and Conventional Commits enforcement on every commit.
- PHP_CodeSniffer —
Drupal+DrupalPracticestandards scoped tomodules/customandthemes/custom. - PHPStan — level 6 with
mglaman/phpstan-drupalandphpstan/phpstan-deprecation-rules.
Scaffold a CI pipeline and suds.ci.yml into the project root. The pipeline installs Composer dependencies and runs GrumPHP quality checks. A commented deploy block shows how to wire up suds:deploy for automated artifact pushes. Existing files are left untouched unless --force is passed.
Supported providers:
| Provider | Files written |
|---|---|
github |
.github/workflows/ci.yml, suds.ci.yml |
gitlab |
.gitlab-ci.yml, suds.ci.yml |
bitbucket |
bitbucket-pipelines.yml, suds.ci.yml |
# Scaffold a GitHub Actions workflow
drush suds:scaffold:ci github
# Scaffold a GitLab CI configuration
drush suds:scaffold:ci gitlab
# Scaffold a Bitbucket Pipelines configuration
drush suds:scaffold:ci bitbucket
# Overwrite any existing CI files
drush suds:scaffold:ci github --forceThe PHP version is detected automatically from composer.json (config.platform.php takes priority over require.php). Falls back to 8.3 when neither is set.
suds.ci.yml is loaded automatically by SUDS when the CI environment variable is set (standard in all three providers). Use it to override deploy git identity or other settings that differ between local and CI environments.
| Option | Default | Description |
|---|---|---|
--force |
disabled | Overwrite files that already exist |
Run suds:scaffold:quality first — the generated pipeline assumes grumphp.yml is present.
Validate the local environment for use with SUDS. Checks required tools, configuration presence, and project structure. Exits non-zero if any required check fails.
drush suds:doctor| Status | Meaning |
|---|---|
[OK] |
Check passed |
[WARN] |
Non-critical issue — tool still functions |
[FAIL] |
Required check failed — exits non-zero |
Checks performed:
| Check | Failure level | Notes |
|---|---|---|
composer available |
FAIL | Required for suds:sync |
rsync available |
WARN | Required for suds:files:sync |
git available |
WARN | Required for suds:deploy |
| PHP >= 8.3 | FAIL | |
suds.yml found |
WARN | Run drush suds:init to create one |
project.name set |
WARN | |
drupal.root directory exists |
FAIL | |
drupal.root/core exists |
FAIL | Root dir found but not a Drupal installation |
deploy.repo.url set |
WARN | Only checked when git is available |
sync.default_source set |
WARN | suds:sync will require an explicit alias on every call |
| Config keys valid | WARN | Unknown key found in suds.yml — likely a typo; run suds:config:dump --defaults to see all valid keys |
| Config types valid | WARN | Config value has wrong type (e.g. string where bool expected); run suds:config:dump --defaults to see expected types |
| Sync alias defined | WARN | Configured sync source alias is not defined in Drush alias files |
| Project root is a git repo | WARN | Only checked when deploy.repo.url is set; suds:deploy requires a git repository |
grumphp.yml found |
WARN | Run drush suds:scaffold:quality to create it |
phpcs.xml.dist found |
WARN | Run drush suds:scaffold:quality to create it |
phpstan.neon found |
WARN | Run drush suds:scaffold:quality to create it |
| GrumPHP pre-commit hook installed | WARN | Only checked when grumphp.yml exists; run composer install to let GrumPHP auto-install hooks |
Set up a Drupal site — install dependencies, configure settings, and run the installer.
# Use the default installation profile (minimal)
drush suds:setup
# Specify a profile
drush suds:setup --profile=standard| Option | Default | Description |
|---|---|---|
--profile |
minimal |
Drupal installation profile to use |
--existing-config |
disabled | Install from existing configuration |
| Config key | Default | Description |
|---|---|---|
drupal.profile |
minimal |
Installation profile (overridden by --profile) |
setup.recipes |
[] |
Drupal recipes to apply after install, in order |
setup.hooks.pre_setup |
[] |
Commands run before drush site:install |
setup.hooks.post_setup |
[] |
Commands run after recipes are applied |
Orchestrate a full environment sync in sequence:
- Run
sync.hooks.pre_synccommands composer install— install/update PHP dependenciessuds:db:sync— pull the database from sourcesuds:db:sanitize— scrub PII from the local database (skippable)suds:files:sync— pull managed files from source (opt-in)suds:update— rebuild caches, run DB updates, import config- Run
sync.hooks.post_synccommands
# Sync database from production (sanitize per config, no files by default)
drush suds:sync @prod
# Sync database and files from production
drush suds:sync @prod --force-files
# Sync without sanitizing the database
drush suds:sync @prod --skip-sanitize
# Import a local backup instead of pulling from a remote alias
drush suds:sync --file=/path/to/backup.sql.gz
# Import the most recent export from sync.db.export_dir
drush suds:sync --latestThe source argument is optional when a default source is configured in suds.yml, or when --file or --latest is used.
Source resolution priority: CLI argument > sync.db.default_source / sync.files.default_source > sync.default_source.
| Argument | Description |
|---|---|
source |
Site alias of the source environment (e.g. @prod). Not required when --file or --latest is used. |
| Option | Default | Description |
|---|---|---|
--skip-sanitize |
disabled | Skip database sanitization regardless of config |
--force-files |
disabled | Force files sync even when sync.files.enabled: false |
--skip-files |
disabled | Skip files sync even when sync.files.enabled: true |
--file |
(empty) | Path to a local .sql or .sql.gz backup to import instead of pulling from a remote |
--latest |
disabled | Import the most recent file from sync.db.export_dir instead of pulling from a remote |
| Config key | Default | Description |
|---|---|---|
sync.default_source |
~ |
Fallback source alias for all sync steps |
sync.db.default_source |
~ |
Source alias for the db step; overrides sync.default_source |
sync.files.default_source |
~ |
Source alias for the files step; overrides sync.default_source |
sync.hooks.pre_sync |
[] |
Commands to run before any sync steps |
sync.hooks.post_sync |
[] |
Commands to run after all sync steps complete |
Apply code updates to a Drupal environment: rebuild caches, run database updates, and import configuration.
# Run locally
drush suds:update
# Run on a remote environment via a Drush site alias (CI/CD post-deploy step)
drush @prod suds:updateConfiguration import runs twice to handle modules that alter configuration during import (e.g. config_split). The second pass is a near-instant no-op when nothing remains.
Post-deploy usage. The standard CI/CD pattern is to run suds:deploy to push the artifact, then drush @prod suds:update to apply it. When Drush resolves @prod it SSHes to the production server and executes suds:update there — caches are rebuilt, database updates run, and configuration is imported on the actual server. The update hooks (pre_update, post_update) run on the server as well, making them the right place for operations that require server-side context (toggling maintenance mode, triggering a search index rebuild after deployment). See the Hooks section for guidance on what belongs in update hooks versus deploy.hooks.post_deploy.
# suds.yml — example update hooks for a CI/CD post-deploy workflow
update:
hooks:
pre_update:
- drush state:set system.maintenance_mode 1 --input-format=integer --yes
post_update:
- drush state:set system.maintenance_mode 0 --input-format=integer --yes
- drush search-api:index| Config key | Default | Description |
|---|---|---|
update.hooks.pre_update |
[] |
Commands to run before caches, DB updates, and config import |
update.hooks.post_update |
[] |
Commands to run after caches, DB updates, and config import complete |
Export the local database to a timestamped gzipped file in the configured export directory.
drush suds:db:exportExports are written to sync.db.export_dir (default: db-exports/) as YYYY-MM-DD-HH-MM.sql.gz. Add this directory to .gitignore.
Project-specific dump flags that your environment requires — such as --single-transaction for InnoDB tables — are set once in suds.yml via sync.db.dump_extra_flags so every developer gets the same result without needing to remember the incantation.
| Config key | Default | Description |
|---|---|---|
sync.db.export_dir |
db-exports |
Directory (relative to project root) for exported dumps |
sync.db.dump_extra_flags |
(empty) | Extra flags appended verbatim to drush sql:dump (e.g. --extra-dump=--single-transaction) |
Sync the database from a source environment to @self, import from a local file, or import the most recent export.
# Pull from a remote alias
drush suds:db:sync @prod
# Import a specific local backup (drops and reimports)
drush suds:db:sync --file=/path/to/backup.sql.gz
# Import the most recent file from sync.db.export_dir
drush suds:db:sync --latest| Option | Description |
|---|---|
--file |
Path to a local .sql or .sql.gz file to import. Source alias not required. |
--latest |
Import the most recent file from sync.db.export_dir. Source alias not required. |
When no source alias is given and neither --file nor --latest is set, the command falls back to sync.db.default_source, then sync.default_source. If neither is configured, the command exits non-zero with an explanatory message.
Sanitize the local database: truncate cache/flood tables, then scrub PII via drush sql:sanitize.
drush suds:db:sanitizeSanitization behaviour is controlled by sync.db.* keys in suds.yml:
| Config key | Default | Description |
|---|---|---|
sync.db.truncate_tables |
Drupal cache/flood/session tables | Tables to truncate before sanitization. Set to [] to skip truncation entirely |
sync.db.sanitize_email |
user+%uid@localhost |
Email pattern used by drush sql:sanitize. %uid is replaced with the user ID |
sync.db.sanitize_password |
password |
Password value applied to all user accounts by drush sql:sanitize |
Sync files from a source environment to @self for each path listed in sync.files.paths.
drush suds:files:sync @prod| Config key | Default | Description |
|---|---|---|
sync.files.paths |
['sites/default/files'] |
Directories to sync, relative to drupal.root |
sync.files.default_source |
~ |
Source alias for the files step; falls back to sync.default_source when not set |
Build a production artifact and push it to a deployment (build) repository.
Assembles a clean artifact directory via rsync, runs build steps, commits the result, and force-pushes to the configured deployment repository. Intended for CI/CD pipelines.
drush suds:deploy
# Print all commands that would run without executing them
drush suds:deploy --dry-run
# Deploy and create a tag on the artifact repository
drush suds:deploy --tag=v1.2.3| Option | Default | Description |
|---|---|---|
--dry-run |
disabled | Print commands that would run without executing them |
--tag |
(none) | Create and push a git tag on the artifact repository after the branch push |
Execution order:
- Run
deploy.hooks.pre_deploycommands in the project root - Rsync project into a fresh artifact directory (honouring
deploy.exclude) - Run
deploy.build_stepscommands in the artifact directory - Write build manifest to artifact root (when
deploy.manifest: true) - Commit and force-push to
deploy.repo.branch - Create and push
--tagon the artifact repository (when--tagis provided) - Run
deploy.hooks.post_deploycommands in the project root
| Config key | Default | Description |
|---|---|---|
deploy.repo.url |
~ |
URL of the deployment repository. Required. |
deploy.repo.branch |
$SUDS_BRANCH-build |
Branch to push the artifact to |
deploy.commit_message |
Deploy $SUDS_BRANCH [$SUDS_SHORT_HASH] |
Artifact commit message |
deploy.git.name |
SUDS Deploy |
Git user.name for the artifact commit |
deploy.git.email |
suds@localhost |
Git user.email for the artifact commit |
deploy.build_steps |
['composer install --no-dev --optimize-autoloader'] |
Shell commands run in the artifact directory, in order |
deploy.manifest |
true |
Write a SUDS_BUILD.txt manifest file to the artifact root before committing |
deploy.manifest_file |
SUDS_BUILD.txt |
Filename for the build manifest (relative to artifact root) |
deploy.exclude |
(see defaults) | Paths excluded from the artifact. Overrides the entire default list. |
deploy.exclude_extra |
[] |
Additional paths to exclude, appended to deploy.exclude. |
deploy.hooks.pre_deploy |
[] |
Commands run on the CI machine before artifact assembly |
deploy.hooks.post_deploy |
[] |
Commands run on the CI machine after the push completes |
Build steps. The default build_steps runs composer install --no-dev --optimize-autoloader in the artifact directory, producing a production vendor tree without development dependencies. To add frontend asset compilation, append to build_steps — the default entry is just the first step in the list:
deploy:
build_steps:
- composer install --no-dev --optimize-autoloader
- npm ci
- npm run buildBuild manifest. When deploy.manifest is enabled (the default), SUDS writes a small text file to the artifact root before committing. The manifest records the branch, full commit hash, short hash, and build timestamp — useful for auditing which source commit is running on any server:
branch: main
hash: a1b2c3d4e5f6...
short_hash: a1b2c3d4
built_at: 2026-03-19T12:00:00+00:00
SUDS is designed for single-site projects. There is no built-in concept of multiple sites within one suds.yml.
For multi-site Drupal installations, SUDS commands can be targeted at a specific site using Drush's standard mechanisms:
Site alias prefix — prefix any command with a site alias and SUDS will run all child commands in that site's context:
drush @myproject.site1 suds:sync @prod
drush @myproject.site2 suds:update--uri flag — pass --uri directly; SUDS forwards it to every child Drush process:
drush --uri=http://site1.local suds:sync @prodHook-based orchestration — for workflows that need to touch multiple sites in sequence, post_sync and post_update hooks can issue site-specific Drush calls directly:
# suds.yml
sync:
hooks:
post_sync:
- drush @myproject.site2 sql:sync @prod @self
- drush @myproject.site2 suds:updateNote that suds.yml configuration keys such as sync.default_source and sync.files.paths are single-site values. When targeting multiple sites you will typically pass the source alias explicitly on each invocation rather than relying on configured defaults.
See CONTRIBUTING.md for setup instructions, coding standards, commit message conventions, and how to submit a pull request.
To report a vulnerability, see SECURITY.md.
GPL-2.0-or-later. See LICENSE for details.