bounteous-inc / suds
An opinionated orchestrator for Drupal site development workflows.
Package info
Type:drupal-drush
pkg:composer/bounteous-inc/suds
Requires
- php: >=8.3
- drush/drush: ^13.0
Requires (Dev)
- composer/installers: ^2.0
- dealerdirect/phpcodesniffer-composer-installer: ^1.0
- drupal/coder: ^8.3
- drupal/core-recommended: ^10.4 || ^11.0
- ergebnis/composer-normalize: ^2.0
- mglaman/phpstan-drupal: ^2.1
- php-parallel-lint/php-parallel-lint: ^1.4
- phpro/grumphp: ^2.5
- phpstan/extension-installer: ^1.4
- phpstan/phpstan: ^2.0
- phpstan/phpstan-deprecation-rules: ^2.0
- phpstan/phpstan-phpunit: ^2.0
- phpunit/phpunit: ^11.0
- squizlabs/php_codesniffer: ^3.9
README
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.
How it works
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.
Requirements
- PHP 8.3 or later
- Drush 13
- Drupal 10.4 or 11
Installation
Install SUDS via Composer:
composer require bounteous-inc/suds
SUDS 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.
Getting started
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 configured
Edit 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 @dev
After the initial composer install, suds:sync runs it automatically on every subsequent sync so dependencies stay current as the project evolves.
Configuration
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.
The merge chain
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 |
Merge semantics
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 untouched
Lists (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.txt
CI configuration
suds.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'
Hooks
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" --yes
Example: 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-notification
Environment variables in deploy hooks
The 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"
Commands
All commands are prefixed with suds: and have su- short aliases.
suds:config:dump
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 |
suds:init
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 |
suds:scaffold:quality
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 --force
After 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-fixer
GrumPHP'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.
suds:scaffold:ci
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 --force
The 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.
suds:doctor
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 |
suds:setup
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 |
suds:sync
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 --latest
The 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 |
suds:update
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:update
Configuration 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 |
suds:db:export
Export the local database to a timestamped gzipped file in the configured export directory.
drush suds:db:export
Exports 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) |
suds:db:sync
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.
suds:db:sanitize
Sanitize the local database: truncate cache/flood tables, then scrub PII via drush sql:sanitize.
drush suds:db:sanitize
Sanitization 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 |
suds:files:sync
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 |
suds:deploy
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 build
Build 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
Multi-site
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 @prod
Hook-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:update
Note 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.
Contributing
See CONTRIBUTING.md for setup instructions, coding standards, commit message conventions, and how to submit a pull request.
Security
To report a vulnerability, see SECURITY.md.
License
GPL-2.0-or-later. See LICENSE for details.